Todos - Basic

A step by step guide to create a Todos application with ConanJs

In ConanJs we wanted to be original, and we have decided to start showing how to build a list of Todos in our first tutorial. We will then go a bit further than other frameworks do, and will show how we can add Asynchronous calls and optimistic updates.

The code for this example is available at GitHub:

and you can also check this code sandbox at the bottom of this page.

If you are coming from Redux, you might want to see how this compares to the TODO from Redux, if that is the case, have a look at this article:

Initial steps

Adding ConanJs to the project

In the project root, just run:

npm install conan-js-core

Implementing the Todos app

Domain model

Our examples are in typescript, if you use javascript, you can just ignore the type definitions and it will all work just fine on your end.

The following interfaces are to define with Typescript the domain for the application.

Our data structure for this example will be very straightforward, simply having a Todo, TodoList and status entities. We will also model the visibility filters as:

export enum ToDoStatus {
PENDING = 'PENDING',
COMPLETED = 'COMPLETED'
}
​
export interface ToDo {
id: string;
description: string;
status: ToDoStatus;
}
​
export enum VisibilityFilters {
SHOW_ALL = 'SHOW_ALL',
SHOW_COMPLETED = 'SHOW_COMPLETED',
SHOW_ACTIVE = 'SHOW_ACTIVE'
}
​
export interface TodoListData {
todos: ToDo[];
appliedFilter: VisibilityFilters;
}

Creating the Todos state

To create the state we will use Conan.state as we will need to pass our custom reducers later on. The data part of our state will be represented by the TodoListData, which holds an array of Todos and a current applied filter. We are initially just passing one reducer to add a Todo to the current array of todos.

Js
Ts
Js
export const todoListSyncState$ = Conan.state({
name: 'todos-sync',
initialData: {
appliedFilter: VisibilityFilters.SHOW_ALL,
todos: []
},
reducers: ({
$addTodo: (todo) => ({
todos: [...getState().todos, todo],
appliedFilter: getState().appliedFilter
})
}),
});
Ts
export type TodoListState = ConanState<TodoListData>;
​
​
export const todoListSyncState$: TodoListState = Conan.state<TodoListData>({
name: 'todos-sync',
initialData: {
appliedFilter: VisibilityFilters.SHOW_ALL,
todos: []
},
reducers: getState => ({
$addTodo: (todo: ToDo): TodoListData => ({
todos: [...getState().todos, todo],
appliedFilter: getState().appliedFilter
})
}),
});

​

Let's add our newly defined state to Conan Dependency Injection, so that is accesible from all the app.

Js
Ts
Js
export let diContext = DiContextFactory.createContext({
todoListState: todoListSyncState$,
});
Ts
export let diContext: App = DiContextFactory.createContext<App>({
todoListState: todoListSyncState$,
}
);

​

Connecting the state

To use our new reducer and add a Todo, we just need to connect our presentation components with Conan state. For a guide on the different ways of connecting the state please visit Using the State​

For this example we will use Conan HOC StateConnect:

Js
Ts
Js
<StateConnect from={diContext.todoListState} into={TodoListRenderer}
fallbackValue={{
todos: [],
appliedFilter: VisibilityFilters.SHOW_ALL
}}/>
Ts
<StateConnect<TodoListData, TodoListActions>
from={diContext.todoListState}
into={TodoListRenderer}
fallbackValue={{
todos: [],
appliedFilter: VisibilityFilters.SHOW_ALL
}}
/>

As we have added our state to Conan DI, we can just use it with diContext.todoListState

Adding a Todo

To add a Todo, TodoListRenderer can now make use of our state data and actions. It can now include the AddTodo component and pass the action addTodo created by ConanJs, from the reducer we created before:

<AddTodo onClick={actions.addTodo}/>

Toggling a Todo

To toggle a Todo we will add a new reducer to the Conan state, and pass it in the reducers input parameter:

Js
Ts
Js
$toggleTodo: (toggledTodo) => ({
todos: getState().todos.map(todo => todo.id !== toggledTodo.id ? todo : {
...todo,
status: (todo.status === ToDoStatus.PENDING ? ToDoStatus.COMPLETED : ToDoStatus.PENDING)
}),
appliedFilter: getState().appliedFilter
}),
Ts
$toggleTodo: (toggledTodo: ToDo): TodoListData => ({
todos: getState().todos.map(todo =>
todo.id !== toggledTodo.id ? todo : {
...todo,
status: (todo.status === ToDoStatus.PENDING ? ToDoStatus.COMPLETED : ToDoStatus.PENDING)
},
),
appliedFilter: getState().appliedFilter
})

TodoListRenderer can now render the list of Todos and pass the action to toggle them:

Js
Ts
Js
<ul>
(data.todos).map(todo =>
<Todo key={todo.id} onClick={() => actions.toggleTodo(todo)}
text={todo.description} completed={todo.status === ToDoStatus.COMPLETED}/>)}
</ul>
Ts
{filterToDos(data.todos, data.appliedFilter).map(todo =>
<Todo
key={todo.id}
onClick={() => actions.toggleTodo(todo)}
text={todo.description}
completed={todo.status === ToDoStatus.COMPLETED}
/>
)}

Filtering Todos

Let's add the last reducer to the Conan state in order to filter Todos:

Js
Ts
Js
$filter: (filter) => ({
todos: getState().todos,
appliedFilter: filter
})
Ts
$filter: (filter: VisibilityFilters): TodoListData => ({
todos: getState().todos,
appliedFilter: filter
})

TodoListRenderer can now pass the filter action to the Footer component:

<FooterRenderer appliedFilter={data.appliedFilter} filterUpdater={actions.filter}/>

We can use the current filter now to only show the correct Todos:

Js
Ts
Js
<ul>
{filterToDos(data.todos, data.appliedFilter).map(todo => <Todo key={todo.id} onClick={() => actions.toggleTodo(todo)} text={todo.description} completed={todo.status === ToDoStatus.COMPLETED}/>)}
</ul>
​
export function filterToDos(todos, filter) {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos;
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(t => t.status === ToDoStatus.COMPLETED);
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(t => t.status === ToDoStatus.PENDING);
default:
throw new Error('Unknown filter: ' + filter);
}
}
Ts
<ul>
{filterToDos(data.todos, data.appliedFilter).map(todo =>
<Todo
key={todo.id}
onClick={() => actions.toggleTodo(todo)}
text={todo.description}
completed={todo.status === ToDoStatus.COMPLETED}
/>
)}
</ul>
​
export function filterToDos(todos: ToDo[], filter: VisibilityFilters): ToDo[] {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos;
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(t => t.status === ToDoStatus.COMPLETED);
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(t => t.status === ToDoStatus.PENDING);
default:
throw new Error('Unknown filter: ' + filter);
}
}

Code sandbox

All the files needed for this example are available in this codesandbox. Please be aware that although we explain the stops in both Javascript and Typescript, our examples are written in Typescript.

​

​