Create Todo list app with Elf & React
Johan Pujol
Posted on January 15, 2022
Introduction
There are many libs for state management. But Elf (https://ngneat.github.io/elf/) have a good point than other.
🤘 It's independent, no bind to FW, you can use everywhere. Share the same class to many projects 😍
Here we are going to see how to create todo app with setup all logic into 1 file (elf repository)
Demo
Source code : https://github.com/workfel/react-todo-elf
Demo : https://workfel.github.io/react-todo-elf/
Installation
Use
create-react-app
and tailwindcss. https://tailwindcss.com/docs/guides/create-react-appAdd
elf
lib
npm i —save @ngneat/elf @ngneat/elf-entities
Add
npm i --save @ngneat/use-observable
to use custom hook with Observable .
Structure
Create folder into src
- components ("dumb")
- containers ("smart" components)
- infrastructures (manage context app)
- repository (todo repo)
Repository
Start by creating skeleton of repository into repository/todo.repository.ts
. Will be responsible for the logic of app.
// datas models
// repository/todo.repository.ts
export interface Todo {
id: string;
name: string;
completed: boolean;
}
export interface VisibilityFilterProps {
filter: 'active' | 'completed' | 'all';
}
We describe all we can do with the app.
// repository/todo.repository.ts
export interface TodoRepository {
todos$: Observable<Todo[]>;
addTodo(text: Todo['name']): void;
markAsComplete(id: string): void;
removeTodo(id: string): void;
markAsActive(id: string): void;
updateFilter(type: VisibilityFilterProps['filter']): void;
}
Now create the state
of app. Contains list of Todo
with withEntities<Todo>()
and props filter
to manage the items to display withProps<VisibilityFilterProps>({ filter: 'all' })
// repository/todo.repository.ts
const { state, config } = createState(
withProps<VisibilityFilterProps>({ filter: 'all' }),
withEntities<Todo>(),
);
Create store into repo class
We will name todos
the name of store and pass state
& config
previously created.
// repository/todo.repository.ts
export class TodoRepositoryElf implements TodoRepository {
private todosStore = new Store({ name: 'todos', state, config });
todos$: Observable<Todo[]>;
addTodo(text: Todo['name']): void {
}
markAsActive(id: string): void {
}
markAsComplete(id: string): void {
}
removeTodo(id: string): void {
}
updateFilter(type: VisibilityFilterProps['filter']): void {
}
}
Add context
Create context TodoContext
will permit to access on repository everywhere on app by using hook useContext
// infrastructure/todo.context.provider.ts
export interface TodoContextInterface {
repository: TodoRepository;
}
export const TodoContext = createContext<TodoContextInterface>({
repository: {} as TodoRepository
});
export default TodoContext;
Add context around <App/>
. And set the repository to use on TodoContext.Provider
// index.tsx
ReactDOM.render(
<React.StrictMode>
<TodoContext.Provider value={{ repository: new TodoRepositoryElf() }}>
<App/>
</TodoContext.Provider>
</React.StrictMode>,
document.getElementById('root')
);
React side
Todo
Todo component will contain all components of app, and communicate with repository via the context TodoContext
created before.
// containers/Todo.tsx
const Todo = () => {
// get repository from context
const { repository } = useContext(TodoContext);
// Call repo when visibility filter has changed
const handleVisibility = ((filter: VisibilityFilterProps['filter']) => {
repository.updateFilter(filter);
});
// Call repo when new todo is added
const handleAddTodo = ((text: string) => {
repository.addTodo(text);
});
return <div className="container mx-auto p-8 flex flex-col grow h-full">
<div className="flex flex-col grow">
<h1 className="text-4xl font-semibold mt-8 mb-8">
All tasks
</h1>
<h2 className="font-semibold uppercase text-xl tracking-wide text-slate-400 mt-8 mb-4">
Filters
</h2>
<VisibilityFilter onChange={handleVisibility}/>
<TodoList/>
</div>
<div>
<AddTodo onAdd={handleAddTodo}/>
</div>
</div>;
};
export default Todo;
TodoList
This component list all todo items. And manage state of todo by complete/active/remove TodoItem
.
With useObservable
when todos$
is updated on repository the value todos
will be automatically updated.
// containers/TodoList.tsx
const TodoList = () => {
const { repository } = useContext(TodoContext);
const [todos] = useObservable(repository.todos$);
// Call repo to mark item completed
const handlerComplete = (id: string) => {
repository.markAsComplete(id);
};
// Call repo to mark item active
const handleActive = (id: string) => {
repository.markAsActive(id);
};
// Call repo to remove item
const handleRemove = (id: string) => {
repository.removeTodo(id);
};
return (
<div>
<h2 className="font-semibold uppercase text-xl tracking-wide text-slate-400 mt-8 mb-4">Todo List</h2>
{todos.map((todo) => (
<TodoItem todo={todo} onCompleted={handlerComplete} onRemove={handleRemove}
onActive={handleActive}
key={todo.id}/>
))}
</div>
);
};
export default TodoList;
VisibilityFilter
"Dumb" component just raised event when filter
value change.
//components/VisibilityFilter.tsx
const VisibilityFilter = ({ onChange }: { onChange: (filter: VisibilityFilterProps['filter']) => void }) => {
return (
<div className="flex gap-x-2 justify-center">
<ButtonFilter onClick={onChange} id="all">
All
</ButtonFilter>
<ButtonFilter onClick={onChange} id="active">
Active
</ButtonFilter>
<ButtonFilter onClick={onChange} id="completed">
Completed
</ButtonFilter>
</div>
);
};
export default VisibilityFilter;
AddTodo
Simple form with input and button who raised event onAdd
with input value when the "add" button is clicked
// components/AddTodo.tsx
const AddTodo = ({ onAdd }: { onAdd: (name: string) => void }) => {
const inputRef = useRef<HTMLInputElement>(null);
const submitHandler = (e: React.FormEvent) => {
e.preventDefault();
const todoValue = inputRef.current?.value as string;
inputRef.current!.value = '';
if (todoValue) {
onAdd(todoValue);
}
};
return (
<div className="">
<form className="flex mt-4" onSubmit={submitHandler}>
<input className="shadow appearance-none border rounded w-full py-2 px-3 mr-4 text-slate-900"
placeholder="Add Todo"
ref={inputRef}/>
<button
className="flex-no-shrink p-2 border-2 rounded-full border-green-500 bg-green-500 hover:text-white hover:bg-green-600 fill-white hover:fill-green-300"
type="submit">
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
width="24" height="24"
viewBox="0 0 24 24">
<path fill-rule="evenodd"
d="M 11 2 L 11 11 L 2 11 L 2 13 L 11 13 L 11 22 L 13 22 L 13 13 L 22 13 L 22 11 L 13 11 L 13 2 Z"></path>
</svg>
</button>
</form>
</div>
);
};
export default AddTodo;
The Single Source Of Truth
Now we have plugged all UI event with the repository, but the repository do nothing at this time, so we are going to do this.
List
First we are to setup the todos$
. The list of items will be all entities filtered by the props filter
. When all
is applied all
todos will be listed, completed
only the todos with completed:true
will be listed , and active
only the completed:false
.
First we get the value of filter, on the repo
// repository/todo.repository.ts
export class TodoRepositoryElf implements TodoRepository {
private todosStore = new Store({ name: 'todos', state, config });
filter$ = this.todosStore.pipe(select(({ filter }) => filter));
//....
}
Now we have the filter value , we are set the todos$
observable.
// repository/todo.repository.ts
//....
todos$: Observable<Todo[]> = this.filter$.pipe(switchMap((filter) => {
return this.todosStore.pipe(selectAllApply({
filterEntity({ completed }): boolean {
if (filter === 'all') return true;
return filter === 'completed' ? completed : !completed;
}
}));
}));
//....
See in action by implementing addTodo
. To add entities simply use addEntities
https://ngneat.github.io/elf/docs/features/entities/entities#addentities
// repository/todo.repository.ts
addTodo(text: Todo['name']) {
this.todosStore.update(addEntities({
name: text,
id: Date.now().toString(),
completed: false
}));
}
Good now implements all methods
markAsComplete(id: string) {
this.todosStore.update(updateEntities(id, {
completed: true
}));
}
markAsActive(id: string) {
this.todosStore.update(updateEntities(id, {
completed: false
}));
}
removeTodo(id: string): void {
this.todosStore.update(deleteEntities(id));
}
updateFilter(type: VisibilityFilterProps['filter']): void {
this.todosStore.update((state) => ({
...state,
filter: type,
}));
}
And
voilà
.
Conclusion
Using Elf it's really easy and simple. You even can use the todo.repository.ts
in your Angular
Vue
Svelte
app because it's independent to FW.
Source : https://github.com/workfel/react-todo-elf
Demo : https://workfel.github.io/react-todo-elf/
Posted on January 15, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.