Creating a Reactive Todo Application With the Firefly Semantics Slice State Manager

Introduction

This articles guides us through how to go about building a minimal reactive Angular Todo application with the Firefly Semantics Slice State Manager.

The application allows us to add Todo instances / entities and also Slice the rendering of the instances using the categories All, Completed, and Active .

If the All filter is selected then all the Todo instances will be rendered regardless of whether they have been marked as Completed or not.

If the Completed filter is selected then only Completed Todo instances are rendered, and if their checkbox is unchecked, then the Todo instance will be hidden reactively.

If the Active filter is selected then only Active todo instances are rendered, and if their checkbox is checked, then the Todo instance will be hidden reactively.

Also Finito! is rendered at the bottom of the application when all of the Todo instances have been marked as completed.

Here is the Stackblitz Demo of what we will be creating:

UX and Styling

We will be using MaterializeCSS for styling.

In our Stackblitz we have added the following to our index.html :

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/css/materialize.min.css">

Dependencies

First install Firefly Semantics Slice and the peer dependencies:

npm i @fireflysemantics/slice @types/nanoid nanoid

In order to unsubscribe from our component observable state we will use the package:

Since Angular already includes RxJS we don’t need to install it.

Model

Visibility Filter

The VISIBILITY_FILTER enum below is used to provide the filter.component.ts with values that are used to reactively filter the list of Todo instances.

The code is contained within model/todo-filter.enum.ts :

export enum VISIBILITY_FILTER {
SHOW_COMPLETED = 'Completed',
SHOW_ACTIVE = 'Active',
SHOW_ALL = 'All'
}

Visibility Filter Values

In order to get the selectable values used to populate the select dropdown control we will create model/todo-filter-values.function.ts:

import { VISIBILITY_FILTER } from './todo-filter.enum';
export function VISIBILITY_FILTER_VALUES(): string[] {
return Object.keys(VISIBILITY_FILTER).map((k) =>
VISIBILITY_FILTER[k]);
}

Slices

The Entity slices for separating complete and incomplete Todo instances are modeled in model/todo-slices.enum.ts :

/**
* The Slice Keys
*/
export const enum TodoSliceEnum {
COMPLETE = 'Complete',
INCOMPLETE = 'Incomplete'
}

We use this to initialize the Slice predicates of the entity store like this:

this.todoStore.addSlice((todo) => todo.completed,
TodoSliceEnum.COMPLETE);
this.todoStore.addSlice((todo) => !todo.completed,
TodoSliceEnum.INCOMPLETE);

State Service

We will create our application state service in services/state.service.ts :

Object Store

We create an Object Store (OStore) to hold and publish changes to the filter:

public OS: OStore<ISTART> = new OStore(this.START);

The interface ISTART is needed in order to give us autocomplete within the IDE for properties on the OStore instance .

The START object is used to initialize the store:

START: OStoreStart = {    
ACTIVE_FILTER_KEY: {
value: VISIBILITY_FILTER.SHOW_ALL,
reset: VISIBILITY_FILTER.SHOW_ALL
},
};

We take a snapshot of this value and use it to initialize the select control in the filter component like this:

this.active = this.s.OS.snapshot(this.s.OS.S.ACTIVE_FILTER_KEY);    this.control = new FormControl(this.active);

And whenever the user selects a different filter we subscribe to the change and update the object store like this:

this.control.valueChanges.pipe(untilDestroyed(this)).subscribe((c) => {      
this.s.OS.put(this.s.OS.S.ACTIVE_FILTER_KEY, c);
});

For more info on how this work see the guide:

Entity Store

The Todo entity store is initialized like this:

public todoStore: EStore<Todo> = new EStore<Todo>();

And the slices used to observe active and complete Todo entities are initialized like this:

this.todoStore.addSlice(
(todo) => todo.completed,
TodoSliceEnum.COMPLETE);
this.todoStore.addSlice(
(todo) => !todo.completed,
TodoSliceEnum.INCOMPLETE
);

For more details on the Entity Store (EStore) API see:

Observable Todo State

We create an observable for all the Todo instances in the store like this:

public todos$: Observable<Todo[]> = this.todoStore.observe()

The completed Todo instance are observed by getting the corresponding slice and observing it like this:

this.completeTodos$ = this.todoStore
.getSlice(TodoSliceEnum.COMPLETE).observe();

The active Todo instance are observed by getting the corresponding slice and observing it like this:

this.incompleteTodos$ = this.todoStore      .getSlice(TodoSliceEnum.INCOMPLETE).observe();

In order to be able to react to and render the Todo instances based on the filter selection we listen for filter selection events using combineLatest :

Finally in order to notify when all Todo instances have been marked as completed we observe the finito$ observable:

this.finito$ = combineLatest(
[this.completeTodos$, this.todos$]).
pipe(map( (arr) => {
return this.isComplete(arr[0], arr[1]);
})
);

The isComplete function checks whether the length of the todo array containing all the Todo instances equals the length of the Todo instances in the slice that contains Todo instances marked as completed.

The remaining methods ( add , delete , and complete are used by our components to update application state.

Components

Add Component

The components/add.component.ts is used to add Todo instances to the entity store:

The input control is bound to the keydown.enter event and when the user presses enter addTodo() on the state service is called with the value of the input control.

addTodo() {
this.s.add(this.titleControl.value);
this.titleControl.reset();
}

The filter component components/filter.component.ts is used to select the current Todo filter :

The control is initialized by the currently active filter value stored in the state service:

this.active = this.s.OS.snapshot(this.s.OS.S.ACTIVE_FILTER_KEY);    this.control = new FormControl(this.active);

The state service is updated with user selected values like this:

this.control.valueChanges.pipe(untilDestroyed(this)).subscribe((c) => {
this.s.OS.put(this.s.OS.S.ACTIVE_FILTER_KEY, c);
});

Todos Component

The components/todos.component.ts renders all the Todo instances in the entity store based on the current filter selection:

The template accesses the selected Todo instances directly from the state service:

<app-todo *ngFor="let todo of s.selectedTodos$ | async;"
class="collection-item"[todo]="todo">
</app-todo>

Todo Component

The components/todo.component.ts is used to render each Todo instance contained in the entity store. It also contains a delete button and allows the Todo instance to be marked as completed :

The component receives the Todo instance it renders via the todo:Todo input property.

The control checkbox is initialized with the completed property of the Todo instance bound to the control via the todo input property.

The control checkbox is subscribed to and the value of the completed property of the Todo instance is updated when the user clicks on the control:

this.control.valueChanges.pipe(untilDestroyed(this)).subscribe(
(completed: boolean) => {
this.todo.completed = completed;
this.complete();
});

App Component

In app.component.ts we inject the state service in order to be able to detect when all the Todo instances have been marked as completed:

export class AppComponent {
constructor(public s: StateService) {}
}

Finally we put it all together in app.component.html :

<div style="margin: 2rem;">
<app-add-todo></app-add-todo>
<app-todos-filter></app-todos-filter>
<app-todos></app-todos>
<div *ngIf="s.finito$ | async">Finito!</div>
</div>

Demo

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store