Ariel Gueta
Posted on June 3, 2019
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>
That is pretty cool. Here is a link to the complete code.
Posted on June 3, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.