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

Ole Ersoy
Mar - 07  -  9 min

Introduction

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

We have also created a Todo Application Youtube Tutorial.

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 @ngneat/until-destroy.

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

Todo Class

The Todo class is modeled in model/Todo.class:

/**
 * The Todo Model
 */
export class Todo {
  gid: string;
  constructor(public title: string, public completed: boolean) {}
}

State Service

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

import { Injectable } from '@angular/core';
import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import {
  KeyObsValueReset,
  ObsValueReset,
  OStore,
  EStore,
  OStoreStart,
} from '@fireflysemantics/slice';

import { VISIBILITY_FILTER } from '../model/todo-filter.enum';
import { Todo } from '../model/todo.class';
import { TodoSliceEnum } from '../model/todo-slices.enum';

interface ISTART extends KeyObsValueReset {
  ACTIVE_FILTER_KEY: ObsValueReset;
}

@Injectable({
  providedIn: 'root',
})
export class StateService {
  //============================================
  // Define the Object Store
  //============================================
  START: OStoreStart = {
    ACTIVE_FILTER_KEY: {
      value: VISIBILITY_FILTER.SHOW_ALL,
      reset: VISIBILITY_FILTER.SHOW_ALL,
    },
  };
  public OS: OStore<ISTART> = new OStore(this.START);
  //============================================
  // Observe the Active Filter
  //============================================
  public activeFilter$: Observable<VISIBILITY_FILTER> = this.OS.observe(
    this.START.ACTIVE_FILTER_KEY
  );

  //============================================
  // Define the Todo Entity Store
  //============================================
  public todoStore: EStore<Todo> = new EStore<Todo>();
  //============================================
  // Create an Observable Reference
  // to the current set of Todo instances
  //============================================
  public todos$: Observable<Todo[]> = this.todoStore.observe();

  //============================================
  // References for our remaining Observable state
  //============================================
  public completeTodos$: Observable<Todo[]>;
  public incompleteTodos$: Observable<Todo[]>;
  public selectedTodos$: Observable<Todo[]>;

  //============================================
  // Observable state for when all Todo instances
  // have been marked as completed
  //============================================
  public finito$: Observable<boolean>;

  constructor() {
    //============================================
    // Initialize Entity Store Slices
    //============================================
    this.todoStore.addSlice((todo) => todo.completed, TodoSliceEnum.COMPLETE);
    this.todoStore.addSlice(
      (todo) => !todo.completed,
      TodoSliceEnum.INCOMPLETE
    );

    //============================================
    // Observe Entity Store Slices
    //============================================
    this.completeTodos$ = this.todoStore
      .getSlice(TodoSliceEnum.COMPLETE)
      .observe();

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

    //============================================
    // Observe the Selected Todo category
    //============================================
    this.selectedTodos$ = combineLatest([
      this.activeFilter$,
      this.completeTodos$,
      this.incompleteTodos$,
      this.todos$,
    ]).pipe(
      map((arr) => {
        return this.applyFilter(arr[0], arr[1], arr[2], arr[3]);
      })
    );

    //============================================
    // Observe whether all Todo instances
    // have been completed
    //============================================
    this.finito$ = combineLatest([this.completeTodos$, this.todos$]).pipe(
      map((arr) => {
        return this.isComplete(arr[0], arr[1]);
      })
    );
  }

  public applyFilter(filter, completeTodos, incompleteTodos, todos): Todo[] {
    switch (filter) {
      case VISIBILITY_FILTER.SHOW_COMPLETED:
        return completeTodos;
      case VISIBILITY_FILTER.SHOW_ACTIVE:
        return incompleteTodos;
      default:
        return todos;
    }
  }

  isComplete(completeTodos: Todo[], todos: Todo[]): boolean {
    if (todos.length > 0) {
      return completeTodos.length == todos.length ? true : false;
    }
    return false;
  }

  //============================================
  // State API Methods
  //============================================
  complete(todo: Todo) {
    this.todoStore.put(todo);
  }
  add(title: string) {
    const todo = new Todo(title, false);
    this.todoStore.post(todo);
  }
  delete(todo: Todo) {
    this.todoStore.delete(todo);
  }
}

Object Store

The object store is used to model the state of the currently active filter.

We initialize the store like this:

START: OStoreStart = {
  ACTIVE_FILTER_KEY: {
    value: VISIBILITY_FILTER.SHOW_ALL,
    reset: VISIBILITY_FILTER.SHOW_ALL,
  },
};
public OS: OStore<ISTART> = new OStore(this.START);
//============================================
// Observe the Active Filter
//============================================
public activeFilter$: Observable<VISIBILITY_FILTER> = this.OS.observe(
  this.START.ACTIVE_FILTER_KEY
);

And we create an Observable of the currently active filter value like this:

public activeFilter$: Observable<VISIBILITY_FILTER> = this.OS.observe(
  this.START.ACTIVE_FILTER_KEY
);

For more info on how this works see Introduction to the Firefly Semantics Slice Reactive Object Store.

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 Introduction to the Firefly Semantics Slice Reactive Entity Store.

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:

this.selectedTodos$ = combineLatest([
  this.activeFilter$,
  this.completeTodos$,
  this.incompleteTodos$,
  this.todos$,
]).pipe(
  map((arr) => {
    return this.applyFilter(arr[0], arr[1], arr[2], arr[3]);
  })
);

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

Filter Component

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:

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { StateService } from '../services/state.service';

@Component({
  selector: 'app-todos',
  template: `
    <div class="collection with-header">
      <h4 class="collection-header">Todos:</h4>
      <app-todo *ngFor="let todo of s.selectedTodos$ | async;"
                class="collection-item"
                [todo]="todo"></app-todo>
    </div>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TodosComponent {
  constructor(public s: StateService) {}
}

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