DEV Community

Cover image for Build a Real-Time Shopping List App with Angular, Socket.IO & Akita
Ariel Gueta
Ariel Gueta

Posted on

Build a Real-Time Shopping List App with Angular, Socket.IO & Akita

In this article, we are going to build a real-time shopping list using Angular, Akita, and Socket.io. Our example application will feature three things: Adding a new item, deleting an item, and change the item's complete status.

What is Akita?

Akita is a state management pattern, built on top of RxJS, which takes the idea of multiple data stores from Flux and the immutable updates from Redux, along with the concept of streaming data, to create the Observable Data Store model.

Akita encourages simplicity. It saves you the hassle of creating boilerplate code and offers powerful tools with a moderate learning curve, suitable for both experienced and inexperienced developers alike.

Create the Server

We will start by creating the server. First, we install the express application generator:

npm install express-generator -g

Next, we create a new express application:

express --no-view shopping-list

Now, delete everything in your app.js file and replace it with the following code:

const app = require('express')();
const server = require('http').Server(app);
const io = require('socket.io')(server);

let list = [];

io.on('connection', function(socket) {

  // Send the entire list
  socket.emit('list', {
    type: 'SET',
    data: list
  });

  // Add the item and send it to everyone
  socket.on('list:add', item => {
    list.push(item);
    io.sockets.emit('list', {
      type: 'ADD',
      data: item
    });
  });

  // Remove the item and send the id to everyone
  socket.on('list:remove', id => {
    list = list.filter(item => item.id !== id);

    io.sockets.emit('list', {
      type: 'REMOVE',
      ids : id
    });
  });

  // Toggle the item and send it to everyone
  socket.on('list:toggle', id => {
    list = list.map(item => {
      if( item.id === id ) {
        return {
          ...item,
          completed: !item.completed
        }
      }
      return item;
    });

    io.sockets.emit('list', {
      type: 'UPDATE',
      ids : id,
      data: list.find(current => current.id === id)
    });
  })
});

server.listen(8000);

module.exports = app;


We define a new socket-io server and save the user's list in memory (in real-life it will be saved in a database). We create several listeners based on the actions we need in the client: SET (GET), ADD, REMOVE, UPDATE.

Note that we use a specific pattern. We send the action type and the action payload. We will see in a second how we use this with Akita.

Create the Angular Application

First, we need to install the angular-cli package and create a new Angular project:

npm i -g @angular/cli
ng new akita-shopping-list

Next, we need to add Akita to our project:

ng add @datorama/akita

The above command automatically adds Akita, Akita's dev-tools, and Akita's schematics into our project. We need to maintain a collection of items, so we scaffold a new entity feature:

ng g af shopping-list

This command generates a store, a query, a service, and a model for us:

// store
import { Injectable } from '@angular/core';
import { EntityState, EntityStore, StoreConfig } from '@datorama/akita';
import { ShoppingListItem } from './shopping-list.model';

export interface ShoppingListState extends EntityState<ShoppingListItem> {}

@Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'shopping-list' })
export class ShoppingListStore extends EntityStore<ShoppingListState, ShoppingListItem> {

  constructor() {
    super();
  }

}


// query
import { Injectable } from '@angular/core';
import { QueryEntity } from '@datorama/akita';
import { ShoppingListStore, ShoppingListState } from './shopping-list.store';
import { ShoppingListItem } from './shopping-list.model';

@Injectable({ providedIn: 'root' })
export class ShoppingListQuery extends QueryEntity<ShoppingListState, ShoppingListItem> {

  constructor(protected store: ShoppingListStore) {
    super(store);
  }

}

// model
import { guid, ID } from '@datorama/akita';

export interface ShoppingListItem {
  id: ID;
  title: string;
  completed: boolean;
}

export function createShoppingListItem({ title }: Partial<ShoppingListItem>) {
  return {
    id: guid(),
    title,
    completed: false,
  } as ShoppingListItem;
}

Now, let's install the socket-io-client library:

npm i socket.io-client

and use it in our service:

import { Injectable } from '@angular/core';
import io from 'socket.io-client';
import { ShoppingListStore } from './state/shopping-list.store';
import { ID, runStoreAction, StoreActions } from '@datorama/akita';
import { createShoppingListItem } from './state/shopping-list.model';

const resolveAction = {
  ADD: StoreActions.AddEntities,
  REMOVE: StoreActions.RemoveEntities,
  SET: StoreActions.SetEntities,
  UPDATE: StoreActions.UpdateEntities
};

@Injectable({ providedIn: 'root' })
export class ShoppingListService {
  private socket;

  constructor(private store: ShoppingListStore) {
  }

  connect() {
    this.socket = io.connect('http://localhost:8000');

    this.socket.on('list', event => {
      runStoreAction(this.store.storeName, resolveAction[event.type], {
        payload: {
          entityIds: event.ids,
          data: event.data
        }
      });
    });

    return () => this.socket.disconnect();
  }

  add(title: string) {
    this.socket.emit('list:add', createShoppingListItem({ title }));
  }

  remove(id: ID) {
    this.socket.emit('list:remove', id);
  }

  toggleCompleted(id: ID) {
    this.socket.emit('list:toggle', id);
  }
}

First, we create a connect method where we connect to our socket server and listening for the list event. When this event fires, we call the runStoreAction method, passing the store name, the action, the entities id, and the data we get from the server. We also return a dispose function so we won't have a memory leak.

Next, We create three methods, add, remove and toggleCompleted that emit the corresponding events with the required data. Now, we can use it in our component:

import { Component, OnDestroy, OnInit } from '@angular/core';
import { ShoppingListService } from './shopping-list.service';
import { Observable } from 'rxjs';
import { ShoppingListQuery } from './state/shopping-list.query';
import { ID } from '@datorama/akita';
import { ShoppingListItem } from './state/shopping-list.model';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnDestroy {
  items$: Observable<ShoppingListItem[]>;
  private disposeConnection: VoidFunction;

  constructor(private shoppingListService: ShoppingListService, 
              private shoppingListQuery: ShoppingListQuery) {
  }

  ngOnInit() {
    this.items$ = this.shoppingListQuery.selectAll();
    this.disposeConnection = this.shoppingList.connect();
  }

  add(input: HTMLInputElement) {
    this.shoppingListService.add(input.value);
    input.value = '';
  }

  remove(id: ID) {
    this.shoppingListService.remove(id);
  }

  toggle(id: ID) {
    this.shoppingListService.toggleCompleted(id);
  }

  track(_, item) {
    return item.title;
  }

  ngOnDestroy() {
    this.disposeConnection();
  }

}

And the component's HTML:


<div>

  <div>
    <input(keyup.enter)="add(input)" #input placeholder="Add Item..">
  </div>

  <ul>
    <li *ngFor="let item of items$ | async; trackBy: track">
      <div [class.done]="item.completed">{{item.title}}
        <i (click)="remove(item.id)">X</i>
        <i (click)="toggle(item.id)">done</i>
      </div>
    </li>
  </ul>

</div>

And here is the result:

That is pretty cool. Here is a link to the complete code.

Top comments (0)