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:
- Front-end Javascript for DOM manipulation
- Fetch API
- Web Api controller in Asp.Net Core
- In-Memory database for EF Core
- Razor pages project
Materials to follow:
- 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
- Repo for the app is: https://github.com/zoltanhalasz/TodoApp
- In-Memory database used here (check my materials with Razor pages or https://exceptionnotfound.net/ef-core-inmemory-asp-net-core-store-database/)
- Web Api - generated from EF Core CRUD automatically in Visual Studio, from the model
- Application is live under: https://todolist.zoltanhalasz.net/
Main steps of the app:
Create Razor Pages App, without authentication
Create Class for ToDO
public class ToDoModel
{
public int id { get; set; }
public string title { get; set; }
public bool completed { get; set; }
}
- Based on the class, the context is created with a table and included in startup.cs. EntityFrameworkCore has to be installed as nuget package.
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"));
- Add a Controller folder, then scaffold Web-api (CRUD with EF Core), can be done based on above class and Context.
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);
}
}
}
- Front-End, content of index.cshtml file:
@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>
Top comments (0)