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>