Building ToDo in Real-Time
Tags: node, typescript, webdev
Published: 2021-09-27 22:48:12

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.

![Preview Functionality](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cxtpi8ge4erh26xsq3s5.gif)

## Prerequisites.
1. Have [Node.js](https://nodejs.org) installed.
2. Have a code editor installed (in my case [VSCode](https://code.visualstudio.com/download))

## 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`

```json
> 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.

```json
> 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`

```ts
> src > index.ts

console.log("Hello world developers ♥");
```

If everything is correct, we see this in the console.

![Print message in the console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4xgp55ahb0arl2k51njl.png);

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.

```ts
> 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.

```ts
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.

```ts

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**
```ts

  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**

```ts

  async find(_?: Params): Promise<Note[]> {
    return this.notes;
  }
```

**NoteService.get**

```ts
  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**

```ts
  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**

```ts
  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**

```ts
  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.

```ts
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.

```ts
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

![List of resources.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yjpf3w5msiu2lq2mlncl.png)

## We setting support Real-Time
At this moment we are going to give real time support to our server.

```ts
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.

```ts
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.

![Structure Directory](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/huuw6agtcpifoeby62yb.png)

## Setting Frontend.
In this step we add the styles and scripts.

We added the following to the styles files.

```css
@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.

```html
<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.

```html
  <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.

```js
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.

```js
// 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.

```js
// 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.

```js
/**
 * 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

```js
/**
 * 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.

```js
// 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.

```js
// 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.

```js
// 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.

```js
// 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

![Preview Functionality](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cxtpi8ge4erh26xsq3s5.gif)

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