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
Enter fullscreen mode Exit fullscreen mode

Next, we create a new express application:

express --no-view shopping-list
Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Next, we need to add Akita to our project:

ng add @datorama/akita
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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

npm i socket.io-client
Enter fullscreen mode Exit fullscreen mode

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);
  }
}

Enter fullscreen mode Exit fullscreen mode

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();
  }

}

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

And here is the result:

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

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.