Create Todo list app with Elf & React

workfel

Johan Pujol

Posted on January 15, 2022

Create Todo list app with Elf & React

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

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';
}

Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>(),
);
Enter fullscreen mode Exit fullscreen mode

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 {
  }

}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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')
);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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));
 //....
}
Enter fullscreen mode Exit fullscreen mode

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;
      }
    }));
  }));
//....
Enter fullscreen mode Exit fullscreen mode

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
    }));
  }
Enter fullscreen mode Exit fullscreen mode

Try it !
Add first todo

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,
    }));
  }
Enter fullscreen mode Exit fullscreen mode

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/

💖 💪 🙅 🚩
workfel
Johan Pujol

Posted on January 15, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related