Building ToDo in Real-Time
Ivan Zaldivar
Posted on September 27, 2021
I want to start with a question Have you ever wondered how applications such as Messenger, WhatsApp can update new messages without the need to refresh the page? Well, in this article we are developing a ToDo with real-time communication so that you can better understand how it works.
Preview
The end of this tutorial you will have the following result.
Prerequisites.
Creating project.
Create a project on my desktop with the name we want to assign it.
mkdir todo-realtime
cd todo-realtime
code .
Initialize project.
Execute the following commands.
npm init -y
tsc --init
npm init -y
: This command generate a file named package.json in the root of the project, this file contains all the necessary settings for your application to work properly. For more information https://docs.npmjs.com/cli/v7/configuring-npm/package-json
tsc --init
: This command generate a file name tsconfig.json This file is similar to a package.json with the difference that it sets up a project with TypeScript. For more information https://www.typescriptlang.org/docs/handbook/tsconfig-json.html
Perfect, once the above explained, we download some packages.
npm i @feathersjs/feathers @feathersjs/socketio @feathersjs/express
npm i nodemon -D
Setting server.
Now, we are going to configure our project.
Create a file nodemon.json. This file will be in charge of updating your application every time we make changes to our files that end in .ts
> nodemon.json
{
"watch": ["src"],
"ext": "ts,json",
"ignore": ["src/**/*.spec.ts", "node_modules"],
"exec": "ts-node ./src/index.ts"
}
We update the package.json file and add the following content.
> package.json
{
// ...
"scripts": {
"serve": "nodemon",
"start": "node ./src/index.ts"
},
// ...
}
Now we create the directory src/index.ts For verify all correct add the following content and execute npm run serve
> src > index.ts
console.log("Hello world developers ♥");
If everything is correct, we see this in the console.
Perfect, this is all to configure.
Developing the dev server.
What we are going to do is create a simple development server that can add notes, and later we will add Real-Time support to it. Copy the following content.
> src > index.ts
import feathers from "@feathersjs/feathers";
import express, { Application } from "@feathersjs/express";
const app: Application = express(feathers());
// Allows interpreting json requests.
app.use(express.json());
// Allows interpreting urlencoded requests.
app.use(express.urlencoded({ extended: true }));
// Add support REST-API.
app.configure(express.rest());
// Use error not found.
app.use(express.notFound());
// We configure the errors to send a json.
app.use(express.errorHandler({ html: false }));
app.listen(3030, () => {
console.log("App execute in http://localhost:3030");
});
Setting our service.
According to the official Feathers documentation. The Services are the heart of every Feathers application. Services are JavaScript objects (or instances of ES6 classes) that implement certain methods. Feathers itself will also add some additional methods and functionality to its services.
Imports modules an defined interfaces.
src > services > note.service.ts
import { Id, Params, ServiceMethods } from "@feathersjs/feathers";
import { NotFound } from "@feathersjs/errors";
export enum Status {
COMPLETED = "completed",
PENDING = "pending"
}
export interface Note {
id: Id;
name: string;
status: Status;
createdAt: string;
updatedAt: string;
}
Define class.
export class NoteService implements ServiceMethods<Note> {
private notes: Note[] = [];
/**
* Get list of note.
*/
find(params?: Params): Promise<Note[]> {
throw new Error("Method not implemented.");
}
/**
* Get on note.
*/
get(id: Id, params?: Params): Promise<Note> {
throw new Error("Method not implemented.");
}
/**
* Create a new note.
*/
create(
data: Partial<Note> | Partial<Note>[],
params?: Params
): Promise<Note> {
throw new Error("Method not implemented.");
}
/**
* Udate note.
*/
update(
id: NullableId,
data: Note,
params?: Params
): Promise<Note> {
throw new Error("Method not implemented.");
}
/**
* Partially update a note.
*/
patch(
id: NullableId,
data: Partial<Note>,
params?: Params
): Promise<Note> {
throw new Error("Method not implemented.");
}
/**
* Delete a note.
*/
remove(id: NullableId, params?: Params): Promise<Note> {
throw new Error("Method not implemented.");
}
}
We added functionality to the methods.
NoteService.create
async create(
data: Pick<Note, "name">,
_?: Params
): Promise<Note> {
const note: Note = {
id: this.notes.length + 1,
name: data.name,
status: Status.PENDING,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
this.notes.unshift(note);
return note;
}
NoteService.find
async find(_?: Params): Promise<Note[]> {
return this.notes;
}
NoteService.get
async get(id: Id, _?: Params) {
const note: Note | undefined = this.notes.find(
note => Number(note.id) === Number(id)
);
if (!note) throw new NotFound("The note does not exist.");
return note;
}
NoteService.update
async update(id: Id, data: Note, _?: Params): Promise<Note> {
const index: number = this.notes.findIndex(
note => Number(note.id) === Number(id)
);
if (index < 0) throw new NotFound("The note does not exist");
const { createdAt }: Note = this.notes[index];
const note: Note = {
id,
name: data.name,
status: data.status,
createdAt,
updatedAt: new Date().toISOString(),
};
this.notes.splice(index, 1, note);
return note;
}
NoteService.patch
async patch(id: Id, data: Partial<Note>, _?: Params): Promise<Note> {
const index: number = this.notes.findIndex(
note => Number(note.id) === Number(id)
);
if (index < 0) throw new NotFound("The note does not exist");
const note: Note = this.notes[index];
data = Object.assign({ updatedAt: new Date().toISOString() }, data);
const values = Object.keys(data).reduce((prev, curr) => {
return { ...prev, [curr]: { value: data[curr as keyof Note] } };
}, {});
const notePatched: Note = Object.defineProperties(note, values);
this.notes.splice(index, 1, notePatched);
return note;
}
NoteService.remove
async remove(id: Id, _?: Params): Promise<Note> {
const index: number = this.notes.findIndex(
note => Number(note.id) === Number(id)
);
if (index < 0) throw new NotFound("The note does not exist");
const note: Note = this.notes[index];
this.notes.splice(index, 1);
return note;
}
Final result.
src > note.service.ts
import { Id, Params, ServiceMethods } from "@feathersjs/feathers";
import { NotFound } from "@feathersjs/errors";
export enum Status {
COMPLETED = "completed",
PENDING = "pending"
}
export interface Note {
id: Id;
name: string;
status: Status;
createdAt: string;
updatedAt: string;
}
export class NoteService implements Partial<ServiceMethods<Note>> {
private notes: Note[] = [
{
id: 1,
name: "Guns N' Roses",
status: Status.COMPLETED,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: 2,
name: "Motionless In White",
status: Status.PENDING,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
async create(
data: Pick<Note, "name">,
_?: Params
): Promise<Note> {
const note: Note = {
id: this.notes.length + 1,
name: data.name,
status: Status.PENDING,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
this.notes.unshift(note);
return note;
}
async find(_?: Params): Promise<Note[]> {
return this.notes;
}
async get(id: Id, _?: Params) {
const note: Note | undefined = this.notes.find(
note => Number(note.id) === Number(id)
);
if (!note) throw new NotFound("The note does not exist.");
return note;
}
async update(id: Id, data: Note, _?: Params): Promise<Note> {
const index: number = this.notes.findIndex(
note => Number(note.id) === Number(id)
);
if (index < 0) throw new NotFound("The note does not exist");
const { createdAt }: Note = this.notes[index];
const note: Note = {
id,
name: data.name,
status: data.status,
createdAt,
updatedAt: new Date().toISOString(),
};
this.notes.splice(index, 1, note);
return note;
}
async patch(id: Id, data: Partial<Note>, _?: Params): Promise<Note> {
const index: number = this.notes.findIndex(
note => Number(note.id) === Number(id)
);
if (index < 0) throw new NotFound("The note does not exist");
const note: Note = this.notes[index];
data = Object.assign({ updatedAt: new Date().toISOString() }, data);
const values = Object.keys(data).reduce((prev, curr) => {
return { ...prev, [curr]: { value: data[curr as keyof Note] } };
}, {});
const notePatched: Note = Object.defineProperties(note, values);
this.notes.splice(index, 1, notePatched);
return note;
}
async remove(id: Id, _?: Params): Promise<Note> {
const index: number = this.notes.findIndex(
note => Number(note.id) === Number(id)
);
if (index < 0) throw new NotFound("The note does not exist");
const note: Note = this.notes[index];
this.notes.splice(index, 1);
return note;
}
}
Once our service is configured, it is time to use it.
src > index.ts
import { NoteService } from "./services/note.service";
// Define my service.
app.use("/notes", new NoteService());
Note: It is very important that services are defined before error handlers.
Now, we test app. Enter to http://localhost:3030/notes
We setting support Real-Time
At this moment we are going to give real time support to our server.
src > index.ts
import socketio from "@feathersjs/socketio";
import "@feathersjs/transport-commons";
// Add support Real-Time
app.configure(socketio());
// My services...
// We listen connection event and join the channel.
app.on("connection", connection =>
app.channel("everyone").join(connection)
);
// Publish all events to channel <everyone>
app.publish(() => app.channel("everyone"));
Client development.
Now is neccessary serve the static files. We do this with the following content.
src > index.ts
import { resolve } from "path";
// Server static files.
app.use(express.static(resolve("public")));
Note: For this to work, is neccessary added before sets errors handler, otherwise always NotFound
The directory have the following structure.
Setting Frontend.
In this step we add the styles and scripts.
We added the following to the styles files.
@import url("https://fonts.googleapis.com/css2?family=Poppins&display=swap");
@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css");
@import url("https://unpkg.com/boxicons@2.0.9/css/boxicons.min.css");
* {
font-family: 'Poppins', sans-serif;
}
i {
font-size: 30px;
}
.spacer {
flex: 1 1 auto;
}
.card-body {
max-height: 50vh;
overflow: auto;
}
We added the styles and scripts of project.
<head>
<!-- Other tags -->
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<!-- My scripts -->
<script src="//unpkg.com/@feathersjs/client@^4.3.0/dist/feathers.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="/js/app.js"></script>
</body>
We establish all the visual section of our app. Copy the following content.
<div class="container-fluid">
<div
class="row justify-content-center align-items-center"
style="min-height: 100vh;"
>
<div class="col-12 col-sm-8 col-md-6 col-xl-4 p-3">
<div class="card border-0 shadow" style="max-height: 80vh;">
<div class="card-header border-0 bg-white">
<div class="d-flex align-items-center text-muted">
<small class="mx-1" id="box-completed"></small>
<small class="mx-1" id="box-pending"></small>
<small class="mx-1" id="box-total"></small>
<span class="spacer"></span>
<button class="btn btn-remove rounded-pill border-0">
<i class='bx bx-trash'></i>
</button>
</div>
</div>
<div class="card-body">
<ul class="list-group" id="container"></ul>
</div>
<div class="card-footer border-0 bg-white">
<form id="form">
<div class="form-group py-2">
<input
placeholder="Example: Learning Docker"
class="form-control"
autocomplete="off"
id="input"
name="title"
autofocus
>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
Now, is moment of added all logic of your project.
We capture the elements of the DOM.
const form = document.getElementById("form");
const input = document.getElementById("input");
const container = document.getElementById("container");
const boxCompleted = document.getElementById("box-completed");
const boxPending = document.getElementById("box-pending");
const boxTotal = document.getElementById("box-total");
const btnRemove = document.querySelector(".btn-remove");
We configure Feathers.js on the client side.
// Instance my app.
const socket = io();
const app = feathers(socket);
// Configure transport with SocketIO.
app.configure(feathers.socketio(socket));
// Get note service.
const NoteService = app.service("notes");
Sets values of some variables.
// The id of the notes are stored.
let noteIds = [];
// All notes.
let notes = [];
We added some functions that modify the header of the card, notes and others.
/**
* Insert id of the notes selected.
*/
async function selectNotes(noteId) {
const index = noteIds.findIndex(id => id === noteId);
index < 0 ? noteIds.push(noteId) : noteIds.splice(index, 1);
btnRemove.disabled = !noteIds.length;
}
/**
* Update stadistic of the notes.
*/
function updateHeader(items) {
const completed = items.filter(note => note.status).length;
const pending = items.length - completed;
boxCompleted.textContent = `Completed: ${ completed }`;
boxPending.textContent = `Pending: ${ pending }`;
boxTotal.textContent = `Total: ${ items.length }`;
}
/**
* Update note by Id
*/
function updateElement(noteId) {
const note = notes.find(note => note.id === noteId);
NoteService.patch(note.id, { status: !note.status });
}
We create a class that will be responsible of the creation of the elements
/**
* This class is responsible for the creation,
* removal and rendering of the component interfaces.
*/
class NoteUI {
/**
* Create element of the note.
*/
createElement(note) {
const element = document.createElement("li");
element.className = "list-group-item border-0";
element.id = note.id;
element.innerHTML = `
<div class="d-flex align-items-center">
<div>
<h6>
<strong>${ note.name }</strong>
</h6>
<small class="m-0 text-muted">${ note.createdAt }</small>
</div>
<span class="spacer"></span>
<div onclick="updateElement(${note.id})" class="mx-2 text-center text-${ note.status ? 'success' : 'danger' }">
<i class='bx bx-${ note.status ? 'check-circle' : 'error' }'></i>
</div>
<div class="ms-2">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
value=""
id="flexCheckDefault"
onclick="selectNotes(${ note.id })"
>
</div>
</div>
</div>
`;
return element;
}
/**
* Insert the element at the beginning of the container.
* @param {HTMLElement} container
* @param {HTMLElement} element
*/
insertElement(container, element) {
container.insertAdjacentElement("afterbegin", element);
}
/**
* Remove element by tag id.
*/
removeElement(id) {
const element = document.getElementById(id);
element.remove();
}
}
// Instance UI
const ui = new NoteUI();
We listen the events CRUD operations.
// Listening events CRUD.
NoteService.on("created", note => {
const element = ui.createElement(note);
ui.insertElement(container, element);
notes.push(note);
updateHeader(notes);
});
NoteService.on("updated", note => {
// I leave this method for you as homework.
console.log("Updated: ", note);
updateHeader(notes);
});
NoteService.on("patched", note => {
// Remove old element.
ui.removeElement(note.id);
// Create element updated.
const element = ui.createElement(note);
ui.insertElement(container, element);
// Update header.
const index = notes.findIndex(item => item.id === note.id);
notes.splice(index, 1, note);
updateHeader(notes);
});
NoteService.on("removed", note => {
ui.removeElement(note.id);
const index = notes.findIndex(note => note.id === note.id);
notes.splice(index, 1);
updateHeader(notes);
});
Initialize some values and get list of notes.
// Initialize values.
(async () => {
// Get lits of note.
notes = await NoteService.find();
notes.forEach(note => {
const element = ui.createElement(note);
ui.insertElement(container, element);
});
// Update header.
updateHeader(notes);
// Button for remove is disable.
btnRemove.disabled = true;
})();
We listen the events of DOM elements.
// Listen event of the DOM elements.
btnRemove.addEventListener("click", () => {
if (confirm(`Se eliminaran ${ noteIds.length } notas ¿estas seguro?`)) {
noteIds.forEach(id => NoteService.remove(id));
btnRemove.disabled = true;
noteIds = [];
}
});
form.addEventListener("submit", e => {
e.preventDefault();
const formdata = new FormData(form);
const title = formdata.get("title");
if (!title) return false;
NoteService.create({ name: title });
form.reset();
});
The final result.
// Get elements DOM.
const form = document.getElementById("form");
const input = document.getElementById("input");
const container = document.getElementById("container");
const boxCompleted = document.getElementById("box-completed");
const boxPending = document.getElementById("box-pending");
const boxTotal = document.getElementById("box-total");
const btnRemove = document.querySelector(".btn-remove");
// Instance my app.
const socket = io();
const app = feathers(socket);
// Configure transport with SocketIO.
app.configure(feathers.socketio(socket));
// Get note service.
const NoteService = app.service("notes");
// Sets values.
let noteIds = [];
let notes = [];
/**
* Insert id of the notes selected.
*/
async function selectNotes(noteId) {
const index = noteIds.findIndex(id => id === noteId);
index < 0 ? noteIds.push(noteId) : noteIds.splice(index, 1);
btnRemove.disabled = !noteIds.length;
}
/**
* Update stadistic of the notes.
*/
function updateHeader(items) {
const completed = items.filter(note => note.status).length;
const pending = items.length - completed;
boxCompleted.textContent = `Completed: ${ completed }`;
boxPending.textContent = `Pending: ${ pending }`;
boxTotal.textContent = `Total: ${ items.length }`;
}
/**
* Update note by Id
*/
function updateElement(noteId) {
const note = notes.find(note => note.id === noteId);
NoteService.patch(note.id, { status: !note.status });
}
/**
* This class is responsible for the creation,
* removal and rendering of the component interfaces.
*/
class NoteUI {
/**
* Create element of the note.
*/
createElement(note) {
const element = document.createElement("li");
element.className = "list-group-item border-0";
element.id = note.id;
element.innerHTML = `
<div class="d-flex align-items-center">
<div>
<h6>
<strong>${ note.name }</strong>
</h6>
<small class="m-0 text-muted">${ note.createdAt }</small>
</div>
<span class="spacer"></span>
<div onclick="updateElement(${note.id})" class="mx-2 text-center text-${ note.status ? 'success' : 'danger' }">
<i class='bx bx-${ note.status ? 'check-circle' : 'error' }'></i>
</div>
<div class="ms-2">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
value=""
id="flexCheckDefault"
onclick="selectNotes(${ note.id })"
>
</div>
</div>
</div>
`;
return element;
}
/**
* Insert the element at the beginning of the container.
* @param {HTMLElement} container
* @param {HTMLElement} element
*/
insertElement(container, element) {
container.insertAdjacentElement("afterbegin", element);
}
/**
* Remove element by tag id.
*/
removeElement(id) {
const element = document.getElementById(id);
element.remove();
}
}
// Instance UI
const ui = new NoteUI();
// Listening events CRUD.
NoteService.on("created", note => {
const element = ui.createElement(note);
ui.insertElement(container, element);
notes.push(note);
updateHeader(notes);
});
NoteService.on("updated", note => {
// I leave this method for you as homework.
console.log("Updated: ", note);
updateHeader(notes);
});
NoteService.on("patched", note => {
// Remove old element.
ui.removeElement(note.id);
// Create element updated.
const element = ui.createElement(note);
ui.insertElement(container, element);
// Update header.
const index = notes.findIndex(item => item.id === note.id);
notes.splice(index, 1, note);
updateHeader(notes);
});
NoteService.on("removed", note => {
ui.removeElement(note.id);
const index = notes.findIndex(note => note.id === note.id);
notes.splice(index, 1);
updateHeader(notes);
});
// Initialize values.
(async () => {
// Get lits of note.
notes = await NoteService.find();
notes.forEach(note => {
const element = ui.createElement(note);
ui.insertElement(container, element);
});
// Update header.
updateHeader(notes);
// Button for remove is disable.
btnRemove.disabled = true;
})();
// Listen event of the DOM elements.
btnRemove.addEventListener("click", () => {
if (confirm(`Se eliminaran ${ noteIds.length } notas ¿estas seguro?`)) {
noteIds.forEach(id => NoteService.remove(id));
btnRemove.disabled = true;
noteIds = [];
}
});
form.addEventListener("submit", e => {
e.preventDefault();
const formdata = new FormData(form);
const title = formdata.get("title");
if (!title) return false;
NoteService.create({ name: title });
form.reset();
});
Preview
Perfect, with this we have finished with the construction of our ToDo Real-Time. Well, more or less because you have your homework to complete the update of the notes.
Remember that if you have a question you can read the official documentation: https://docs.feathersjs.com/guides
Good developers, any questions, simplification of the code or improvement, do not hesitate to comment. Until next time...
Repository: https://github.com/IvanZM123/todo-realtime
Follow me on social networks.
- 🎉 Twitter: https://twitter.com/ToSatn2
- 💡 Github: https://github.com/IvanZM123
Posted on September 27, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 13, 2024