Full Stack Mini ToDo-App With Javascript, Ajax, API Controller and In-Memory Database (Asp.Net Core Razor Pages)
Tags: dotnet, javascript, tutorial
Published: 2020-02-18 10:11:00

During the last couple of days, I decided to revisit my basic DOM Javascript skills, and among other things, decided to write some mini-projects to exercise.

The topics touched in this tutorial are:
1. Front-end Javascript for DOM manipulation
2. Fetch API
3. Web Api controller in Asp.Net Core
4. In-Memory database for EF Core
5. Razor pages project

**Materials to follow:**
1. Main inspiration of the tutorial was Ajax tutorials from Dennis Ivy (front-end is 90% from him) https://www.youtube.com/watch?v=hISSGMafzvU&t=1157s
2. Repo for the app is:  https://github.com/zoltanhalasz/TodoApp
3. In-Memory database used here (check my materials with Razor pages or https://exceptionnotfound.net/ef-core-inmemory-asp-net-core-store-database/)
4. Web Api - generated from EF Core CRUD automatically in Visual Studio, from the model
5. Application is live under: https://todolist.zoltanhalasz.net/

**Main steps of the app:**
1. Create Razor Pages App, without authentication

2. Create Class for ToDO
```csharp
    public class ToDoModel
    {
        public int id { get; set; }
        public string title { get; set; }
        public bool completed { get; set; }
    }
```
3. Based on the class, the context is created with a table and included in startup.cs. EntityFrameworkCore has to be installed as nuget package.
```csharp
    public class ToDoContext : DbContext
    {
        public ToDoContext(DbContextOptions<ToDoContext> options)
            : base(options)
        {
        }

        public DbSet<ToDoModel> ToDoTable { get; set; }
    }
```

and in the ConfigureServices method/startup.cs
```
 services.AddDbContext<ToDoContext>(options => options.UseInMemoryDatabase(databaseName: "ToDoDB"));
```         
4. Add a Controller folder, then scaffold Web-api (CRUD with EF Core), can be done based on above class and Context.

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TodoApp.Models;

namespace TodoApp.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ToDoModelsController : ControllerBase
    {
        private readonly ToDoContext _context;

        public ToDoModelsController(ToDoContext context)
        {
            _context = context;
        }

        // GET: api/ToDoModels
        [HttpGet]
  
        public async Task<ActionResult<IEnumerable<ToDoModel>>> GetToDoTable()
        {
            return await _context.ToDoTable.ToListAsync();
        }

        // GET: api/ToDoModels/5
        [HttpGet("{id}")]
        public async Task<ActionResult<ToDoModel>> GetToDoModel(int id)
        {
            var toDoModel = await _context.ToDoTable.FindAsync(id);

            if (toDoModel == null)
            {
                return NotFound();
            }

            return toDoModel;
        }

        // PUT: api/ToDoModels/5
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for
        // more details see https://aka.ms/RazorPagesCRUD.
        [HttpPut("{id}")]
        public async Task<IActionResult> PutToDoModel(int id, ToDoModel toDoModel)
        {
            if (id != toDoModel.id)
            {
                return BadRequest();
            }

            _context.Entry(toDoModel).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!ToDoModelExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

        // POST: api/ToDoModels
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for
        // more details see https://aka.ms/RazorPagesCRUD.
        [HttpPost]
        public async Task<ActionResult<ToDoModel>> PostToDoModel(ToDoModel toDoModel)
        {
            _context.ToDoTable.Add(toDoModel);
            await _context.SaveChangesAsync();

            return CreatedAtAction("GetToDoModel", new { id = toDoModel.id }, toDoModel);
        }

        // DELETE: api/ToDoModels/5
        [HttpDelete("{id}")]
        public async Task<ActionResult<ToDoModel>> DeleteToDoModel(int id)
        {
            var toDoModel = await _context.ToDoTable.FindAsync(id);
            if (toDoModel == null)
            {
                return NotFound();
            }

            _context.ToDoTable.Remove(toDoModel);
            await _context.SaveChangesAsync();

            return toDoModel;
        }

        private bool ToDoModelExists(int id)
        {
            return _context.ToDoTable.Any(e => e.id == id);
        }
    }
}
```
5. Front-End, content of index.cshtml file:
```html
@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <title>TO DO</title>

    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">

    <link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet">

    <style type="text/css">
        body {
            background: rgb(54,217,182);
            background: linear-gradient(90deg, rgba(54,217,182,1) 0%, rgba(32,152,126,1) 43%, rgba(0,212,255,1) 100%);
        }


        h1, h2, h3, h4, h5, p, span, strike {
            font-family: 'Montserrat', sans-serif;
        }


        #task-container {
            max-width: 600px;
            margin: 0 auto;
            box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
            background-color: #fff;
            margin-top: 100px;
            margin-bottom: 100px;
            justify-content: space-around;
            align-items: flex-start;
        }

        #form-wrapper {
            position: -webkit-sticky;
            position: sticky;
            top: 0rem;
            border-bottom: 1px solid #e9e9e9;
            background-color: #fff;
            box-shadow: 0 3px 8px rgba(0,0,0,0.25);
            padding: 40px;
        }

        #submit {
            background-color: #36d9b6;
            border-radius: 0;
            border: 0;
            color: #fff;
        }

        .flex-wrapper {
            display: flex;
        }

        .task-wrapper {
            margin: 5px;
            padding: 5px;
            padding: 20px;
            cursor: pointer;
            border-bottom: 1px solid #e9e9e9;
            color: #686868;
        }
    </style>

</head>
<body>
    <div class="container">

        <div id="task-container">
            <div id="form-wrapper">
                <form id="form">
                    <div class="flex-wrapper">
                        <div style="flex: 6">
                            <input id="title" class="form-control" type="text" name="title" placeholder="Add task">
                        </div>
                        <div style="flex: 1">
                            <input id="submit" class="btn" type="submit">
                        </div>
                    </div>
                </form>
            </div>

            <div id="list-wrapper">

            </div>
        </div>

    </div>

    <script type="text/javascript">
                /*
                        KEY COMPONENTS:
                        "activeItem" = null until an edit button is clicked. Will contain object of item we are editing
                        "list_snapshot" = Will contain previous state of list. Used for removing extra rows on list update

                        PROCESS:
                        1 - Fetch Data and build rows "buildList()"
                        2 - Create Item on form submit
                        3 - Edit Item click - Prefill form and change submit URL
                        4 - Delete Item - Send item id to delete URL
                        5 - Cross out completed task - Event handle updated item

                        NOTES:
                        -- Add event handlers to "edit", "delete", "title"
                        -- Render with strike through items completed
                        -- Remove extra data on re-render
        
                */

                function getCookie(name) {
                    var cookieValue = null;
                    if (document.cookie && document.cookie !== '') {
                        var cookies = document.cookie.split(';');
                        for (var i = 0; i < cookies.length; i++) {
                            var cookie = cookies[i].trim();
                            // Does this cookie string begin with the name we want?
                            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                                break;
                            }
                        }
                    }
                    return cookieValue;
                }
                //var csrftoken = getCookie('csrftoken');

                var activeItem = null
                var list_snapshot = []

                buildList()

                function buildList(){
                        var wrapper = document.getElementById('list-wrapper')
                        //wrapper.innerHTML = ''
            var url = '/api/ToDoModels/';

                        fetch(url)
                        .then((resp) => resp.json())
                        .then(function(data){
                                console.log('Data:', data)

                                var list = data
                                for (var i in list){
                                        try {
                                                document.getElementById(`data-row-${i}`).remove()
                    }
                    catch (err) {

                                        }
                                        var title = `<span class="title">${list[i].title}</span>`
                                        if (list[i].completed == true){
                                                title = `<strike class="title">${list[i].title}</strike>`
                                        }

                                        var item = `
                                                <div id="data-row-${i}" class="task-wrapper flex-wrapper">
                                                        <div style="flex:7">
                                                                ${title}
                                                        </div>
                                                        <div style="flex:1">
                                                                <button class="btn btn-sm btn-outline-info edit">Edit </button>
                                                        </div>
                                                        <div style="flex:1">
                                                                <button class="btn btn-sm btn-outline-danger delete">Delete</button>
                                                        </div>
                                                </div>

                                        `
                                        wrapper.innerHTML += item

                                }

                                if (list_snapshot.length > list.length){
                                        for (var i = list.length; i < list_snapshot.length; i++){
                                                document.getElementById(`data-row-${i}`).remove()
                                        }
                                }

                                list_snapshot = list


                                for (var i in list){
                                        var editBtn = document.getElementsByClassName('edit')[i]
                                        var deleteBtn = document.getElementsByClassName('delete')[i]
                                        var title = document.getElementsByClassName('title')[i]

                                        editBtn.addEventListener('click', (function(item){
                                                return function(){
                                                        editItem(item)
                                                }
                                        })(list[i]))


                                        deleteBtn.addEventListener('click', (function(item){
                                                return function(){
                                                        deleteItem(item)
                                                }
                                        })(list[i]))


                                        title.addEventListener('click', (function(item){
                                                return function(){
                                                        strikeUnstrike(item)
                                                }
                                        })(list[i]))

                                }


                        })
                }


                var form = document.getElementById('form-wrapper')
                form.addEventListener('submit', function(e){
                        e.preventDefault()
                        console.log('Form submitted')
            var urlpost = '/api/ToDoModels';
            var title = document.getElementById('title').value
            const myBody = { 'id': 0, 'title': title, 'completed': false };
            if (activeItem != null) {
                var urlput = `/api/ToDoModels/${activeItem.id}`;
                myBody.id = activeItem.id;
                myBody.completed = activeItem.completed;
                putTodo(urlput, JSON.stringify(myBody));
                activeItem = null
            }
            else {
                postTodo(urlpost, JSON.stringify(myBody));
            }

                })

        function postTodo(url, myBody) {
            console.log('postTodo', url, myBody);
                        fetch(url, {
                                method:'POST',
                                headers:{
                                        'Content-type':'application/json',                      
                },
                body: myBody,                           
                        }
                        ).then(function(response){
                                buildList()
                                document.getElementById('form').reset()
            })
        }

        function putTodo(url, myBody) {
                        console.log('putTodo', url, myBody);
                        fetch(url, {
                                method:'PUT',
                                headers:{
                                        'Content-type':'application/json',                      
                                },
                body: myBody,                           
                        }
                        ).then(function(response){
                                buildList()
                                document.getElementById('form').reset()
            })
        }



                function editItem(item){
                        console.log('Item clicked:', item)
                        activeItem = item
                        document.getElementById('title').value = activeItem.title
                }


                function deleteItem(item){
                        console.log('Delete clicked')
                        fetch(`/api/ToDoModels/${item.id}/`, {
                                method:'DELETE',
                                headers:{
                                        'Content-type':'application/json',                                      
                },                              
                        }).then((response) => {
                                buildList()
                        })
                }

                function strikeUnstrike(item){
            console.log('Strike clicked');
            item.completed = !item.completed;
            const myBody = JSON.stringify({'id': item.id, 'title': item.title, 'completed': item.completed });
            const myUrl = `/api/ToDoModels/${item.id}`;
            console.log(myBody);
            fetch(myUrl, {
                                method:'PUT',
                                headers:{
                                        'Content-type':'application/json',
                },
                body: myBody,                           
                        }).then((response) => {
                                buildList()
                        })
                }
    </script>

</body>
</html>
```