Introduce

Make a simple todolist with below.

  • Express
  • Typescript
  • Prisma
  • gulp
  • ejs

app(router)
- app.ts
- config.ts
- todo.ts
prisma(orm)
- schema.prisma
views
- index.ejs
- todo.ejs
gulpfile.js


Database structure


Code

app.ts

import express from 'express';
import path from 'path';
//router from todo, separate them for scalability in future.
import { todo } from './todo';

const app = express();

//view engine
app.set('view engine', 'ejs');

//bodyEncoder, using for POST from form
app.use(express.json());
app.use(express.urlencoded({
    extended: true
}));

app.use(express.static(path.join(__dirname, 'public')));
//include todo's router
app.use('/', todo);

//index
app.get('/', async (req, res) => {
    res.redirect('/todo');
});

//listening
app.listen(3000, () => {
    console.log("listening port on 3000.");
})

config.ts

import { PrismaClient } from '@prisma/client'
const database = new PrismaClient();

export { database };

todo.ts

import express from 'express';
import { database } from './config';

const router = express.Router();

router.get('/todo', async (req, res) => {
    res.render('index');
});

//Separate front-end and backend
router.get('/loadTodolist', async (req, res) => {
    const todolist = await database.todolist.findMany();
    res.json(todolist);
})

router.get('/todo/:id', async (req, res) => {
    res.render('todo');
});

//API for ajax
router.get('/loadTasks/:id', async (req, res) => {
    //load all the tasks which are belong to this id's todolist
    const tasks = await database.todolist.findUnique(
        {
            where: { id: parseInt(req.params.id) },
            include: {
                taskrelations:
                {
                    include: { taskdetails: true }
                }
            }
        }
    );
    res.json(tasks);
});

router.post('/addTodo', async (req, res) => {
    const name = req.body.name;
    console.log(name);
    const createTodolist = await database.todolist.create({
        data: {
            name: name,
        }
    });
    res.json(createTodolist);
});

//create a new task, will also create a task's relation, Prisma is useful btw.
router.post('/todo/:id', async (req, res) => {
    const id = req.params.id;
    const task = await database.taskdetails.create({
        data: {
            name: req.body.name,
            finished: false,
            taskrelations: {
                create: [
                    { todolist: { connect: { id: parseInt(id) } }, },
                ]
            }
        }
    });
    res.json(task);
});

//delete the task and the task relations at the same time.
router.delete('/todo', async (req, res) => {
    const taskid = parseInt(req.body.id);

    await database.taskdetails.delete({
        where: {
            id: taskid
        },
        include: {
            taskrelations: true
        }
    });
    res.sendStatus(200);
});

//update the task's "finished" attribute
router.patch('/todo', async (req, res) => {
    const taskid = parseInt(req.body.id);
    const checked = req.body.checked === "true" ? true : false;
    await database.taskdetails.update({
        where: {
            id: taskid
        },
        data: {
            finished: checked
        }
    });
    res.sendStatus(200);
});

export { router as todo }

schema.prisma

/*
 * Prisma generate a model relations by using command.
 * I'm using mysql in this project, if you use sqlite or others, remember to change the latest word.
 * Remember to set the settings in .env which is created by prisma itself.
 * npx prisma init --datasource-provider mysql
 * npx prisma db pull
 * npx prisma generate
 * 
 */
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model taskdetails {
  id            Int             @id @default(autoincrement())
  name          String          @db.VarChar(100)
  finished      Boolean
  createdAt     DateTime        @default(now()) @db.DateTime(0)
  updatedAt     DateTime        @default(now()) @db.DateTime(0)
  taskrelations taskrelations[]
}

//I add foreign key in data table, it will automatic create by itself when I use command.
model taskrelations {
  id          Int         @id @default(autoincrement())
  task_id     Int
  list_id     Int
  todolist    todolist    @relation(fields: [list_id], references: [id], onDelete: Cascade, map: "foreign_list")
  taskdetails taskdetails @relation(fields: [task_id], references: [id], onDelete: Cascade, map: "foreign_task")

  @@index([list_id], map: "foreign_list")
  @@index([task_id], map: "foreign_task")
}

model todolist {
  id            Int             @id @default(autoincrement())
  name          String          @db.VarChar(50)
  taskrelations taskrelations[]
}

index.ejs

<script src="https://code.jquery.com/jquery-3.6.1.min.js"
    integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous"></script>

<body>
    <input type="text" id="todolistName" placeholder="todolist-name">
    <button id="addTodolistBtn">new Todolist</button>
    <ul id="todolist">
    </ul>
</body>
<script>
    let addTodolist = () => {
        let name = $("#todolistName").val();
        if ($.trim(name) == '') {
            alert("todolist's name cannot be empty.");
            return false;
        }
        $.ajax({
            url: "./addTodo",
            data: { name },
            method: "POST",
            dataType: "json"
        }).done(rs => {
            let e = $("<a>", { href: `./todo/${rs.id}`, text: rs.name });
            e = $("<h1>").append(e);
            e = $("<li>").append(e);
            $("#todolist").append(e);
        }).fail(rs => console.error(rs));
    }

    let loadTodolist = () => {
        $.ajax({
            url: "./loadTodolist",
            method: "GET",
            dataType: "json"
        }).done(rs => {
            rs.forEach(element => {
                let e = $("<a>", { href: `./todo/${element.id}`, text: element.name });
                e = $("<h1>").append(e);
                e = $("<li>").append(e);
                $("#todolist").append(e);
            });
        }).fail(rs => console.error(rs));
    }

    $("body").ready(() => {
        loadTodolist();
        $("body").on("click", "#addTodolistBtn", addTodolist);
    })
</script>

todo.ejs

<script src="https://code.jquery.com/jquery-3.6.1.min.js"
    integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous"></script>

<body>
    <a href="/todo">Index</a>
    <h1 id="todolist-name"></h1>
    <ol id="task-container">
    </ol>
    <form method="post">
        name: <input name="name" id="taskName">
        <button type="button" id="submitBtn">submit</button>
    </form>
</body>
<script>
    let todoID = new URL(location.href).pathname.split('/')[2];
    let loadTasks = () => {
        if (!todoID.match(/\d/)) return;

        $.ajax({
            url: `/loadTasks/${todoID}`,
            method: "GET",
            dataType: "json",
        }).done(rs => {
            $("#todolist-name").text(rs.name);
            rs['taskrelations'].forEach(task => {
                let e = $("<span>", { text: task.taskdetails.name });
                let btn = $("<button>", { class: 'deleteTaskBtn', text: 'x', "d-id": task.taskdetails.id });
                let checkbox = $("<input>", { type: 'checkbox', class: 'checkTaskBtn', "d-id": task.taskdetails.id, "checked": task.taskdetails.finished });
                
                e = $("<li>", { "d-id": task.taskdetails.id }).append(btn, e, checkbox);
                $("#task-container").append(e);
            });
        }).fail(rs => console.error(rs));
    }

    let addTask = (e) => {
        if (!todoID.match(/\d/)) return;

        let name = $("#taskName").val();
        if ($.trim(name) == '') {
            alert("task's name cannot be empty.");
            return false;
        }
        $.ajax({
            url: `/todo/${todoID}`,
            data: { name },
            method: "POST",
            dataType: "json"
        }).done(task => {
            let e = $("<span>", { text: task.name });
            let btn = $("<button>", { class: 'deleteTaskBtn', text: 'x', "d-id": task.id });
            let checkbox = $("<input>", { type: 'checkbox', class: 'checkTaskBtn', "d-id": task.id, "checked": task.finished });

            e = $("<li>", { "d-id": task.id }).append(btn, e, checkbox);
            $("#task-container").append(e);
        }).fail(rs => console.error(rs));
    }

    let deleteTask = (e) => {
        let id = $(e.target).attr('d-id');
        $.ajax({
            url: `/todo`,
            data: { id },
            method: "DELETE",
            dataType: "text"
        }).done(rs => {
            $("#task-container").find(`li[d-id=${id}]`).remove();
        }).fail(rs => console.error(rs));
    }

    let checkTask = (e) => {
        let id = $(e.target).attr('d-id');
        let checked = $(e.target).is(":checked");
        console.log(checked);
        $.ajax({
            url: `/todo`,
            data: { id, checked },
            method: "PATCH",
            dataType: "text"
        }).done(rs => {

        }).fail(rs => console.error(rs));
    }

    $("body").ready(() => {
        loadTasks();
        $("body").on("click", "#submitBtn", addTask);
        $("body").on("click", ".deleteTaskBtn", deleteTask);
        $("body").on("change", ".checkTaskBtn", checkTask);
    })

</script>

gulpfile.js

/*
 * use gulp to automatic refresh when the code changed.
 */
var gulp = require('gulp');
const nodemon = require('gulp-nodemon');
const express = require('gulp-express');

gulp.task('webserver', function () {
    nodemon({
        script: './app/app.ts',
    });

    //when below's code changed, refresh the task.
    gulp.watch('./app/*.ts', express.notify);
    gulp.watch('./app/view/*.html', express.notify);
});

//use command -> "gulp serve" to start the task
gulp.task('serve', gulp.series('webserver'));