Skip to content

ToDo List App Tutorial Part 5: Editing a ToDo Item

Andrei Fangli edited this page Jun 18, 2022 · 2 revisions

In ToDo List App Tutorial Part 4: Adding ToDo Items we have implemented a lot of the application. We have the our list of todo items, and we can add items. All nicely styled so it looks nice.

Now, lets add an edit form for items we already have. This will work similarly to the add todo item form in that when we click on the edit button for an item we will hide everything else and display just the edit form. Once we save our changes or discard them we get back to our list.

First, we will define our view model for editing items. We are going to use the index of the item as an ID. This form is very similar to the add form.

import type { IEvent } from "react-model-view-viewmodel";
import { DispatchEvent, ViewModel } from "react-model-view-viewmodel";
import { ToDoItem } from "../models/todo-item";
import { ToDoItemFormViewModel } from "./todo-item-form-view-model";

export class EditToDoItemViewModel extends ViewModel {
    private _itemIndex: number | undefined = undefined;
    private readonly _saved: DispatchEvent = new DispatchEvent();

    public readonly form: ToDoItemFormViewModel = new ToDoItemFormViewModel();

    public get saved(): IEvent {
        return this._saved;
    }

    public load(itemIndex: number): void {
        const todoItems: ToDoItem[] = JSON.parse(localStorage.getItem("todo-items") || "[]");
        const todoItem = todoItems[itemIndex];
        if (todoItem) {
            this._itemIndex = itemIndex;
            this.form.description.initialValue = this.form.description.value = todoItem.description;
            this.form.state.initialValue = this.form.state.value = todoItem.state;
        }
    }

    public save(): void {
        if (this._itemIndex !== undefined) {
            const updatedToDoItem = new ToDoItem(this.form.description.value, this.form.state.value);
            const todoItems: ToDoItem[] = JSON.parse(localStorage.getItem("todo-items") || "[]");
            todoItems[this._itemIndex] = updatedToDoItem;
            localStorage.setItem("todo-items", JSON.stringify(todoItems));

            this._saved.dispatch(this);
        }
    }
}

Next we will add the edit todo item form component, reusing what we have already defined for the add todo item form.

import React, { useCallback, useEffect, useRef } from "react";
import { watchEvent } from "react-model-view-viewmodel";
import { EditToDoItemViewModel } from "../view-models/edit-todo-item-view-model";
import { ToDoItemForm } from "./todo-item-form";

export interface IEditToDoItemFormProps {
    readonly itemIndex: number;
    readonly onSave?: () => void;
    readonly onCancel?: () => void;
}

export function EditToDoItemForm({ itemIndex, onSave, onCancel }: IEditToDoItemFormProps): JSX.Element {
    const { current: viewModel } = useRef(new EditToDoItemViewModel());

    const editCallback = useCallback(() => { viewModel.save(); }, []);
    const cancelCallback = useCallback(() => { onCancel && onCancel(); }, []);

    watchEvent(viewModel.saved, () => { onSave && onSave(); })

    useEffect(() => { viewModel.load(itemIndex) }, [itemIndex]);

    return (
        <>
            <ToDoItemForm form={viewModel.form} />
            <div>
                <button onClick={editCallback}>Edit</button>
                <button onClick={cancelCallback}>Cancel</button>
            </div>
        </>
    );
}

And now to add the button, we are going to add the edit button on the right side of each item.

export function ToDoList(): JSX.Element {
    const [selectedIndex, setSelectedIndex] = useState<number | undefined>(undefined);
    const [showAddForm, setShowAddForm] = useState(false);

    const { current: viewModel } = useRef(new ToDoListViewModel());
    watchCollection(viewModel.items);

    const showAddFormCallback = useCallback(() => { setShowAddForm(true) }, []);

    const reloadItems = useCallback(() => { setShowAddForm(false); setSelectedIndex(undefined); viewModel.load(); }, [viewModel]);

    useEffect(() => { viewModel.load(); }, []);

    if (selectedIndex !== undefined)
        return (
            <EditToDoItemForm itemIndex={selectedIndex} onSave={reloadItems} onCancel={reloadItems} />
        );
    else
        return (
            <>
                {!showAddForm && <button onClick={showAddFormCallback}>Add</button>}
                {showAddForm && <AddToDoItemForm onSave={reloadItems} onCancel={reloadItems} />}
                <div className="todo-list">
                    {viewModel.items.map((item, index) => <ToDoListItem key={index} item={item} selectItem={() => setSelectedIndex(index)} />)}
                </div>
            </>
        )
}

interface IToDoListItemProps {
    readonly item: ToDoItem;

    selectItem(): void;
}

function ToDoListItem({ item, selectItem }: IToDoListItemProps): JSX.Element {
    return (
        <div className="todo-list-item">
            <div className="todo-list-item-content">
                {item.description}
                <ToDoListItemState state={item.state} />
            </div>
            <div className="todo-list-item-actions">
                <button onClick={selectItem}>Edit</button>
            </div>
        </div>
    );
}
body > div#app > div.todo-list-app-content div.todo-list-item {
    display: flex;
    flex-direction: row;
    padding: 3px;
    font-size: 12pt;
}

body > div#app > div.todo-list-app-content div.todo-list-item div.todo-list-item-content {
    flex: 1 1 auto;
}

body > div#app > div.todo-list-app-content div.todo-list-item div.todo-list-item-actions {
    align-self: center;
}

Pretty simple, right? Let's go ahead and add progression on our items from the list view. This will change things a bit because each item will now become a view model because we want to add features to each of them. This will help us out a bit because we will not have to re-render the entire list when we progress one item.

import { ViewModel } from "react-model-view-viewmodel";
import { ToDoItemState } from "../models/to-do-item-state";
import { ToDoItem } from "../models/todo-item";

export class ToDoItemViewModel extends ViewModel {
    private readonly _itemIndex: number;
    private _state: ToDoItemState;

    public constructor(itemIndex: number, toDoItem: ToDoItem) {
        super();
        this._itemIndex = itemIndex;
        this.description = toDoItem.description;
        this._state = toDoItem.state;
    }

    public readonly description: string;

    public get state(): ToDoItemState {
        return this._state;
    }

    public get canProgress(): boolean {
        return this._state !== ToDoItemState.Done;
    }

    public progress(): void {
        if (this.canProgress) {
            switch (this._state) {
                case ToDoItemState.ToDo:
                    this._state = ToDoItemState.InProgress;
                    this.notifyPropertiesChanged("state", "canProgress");
                    break;

                case ToDoItemState.InProgress:
                    this._state = ToDoItemState.Done;
                    this.notifyPropertiesChanged("state", "canProgress");
                    break;
            }

            const todoItems: ToDoItem[] = JSON.parse(localStorage.getItem("todo-items") || "[]");
            todoItems[this._itemIndex] = new ToDoItem(this.description, this._state);
            localStorage.setItem("todo-items", JSON.stringify(todoItems));
        }
    }
}

And now to update the todo list view model.

import type { IObservableCollection, IReadOnlyObservableCollection } from "react-model-view-viewmodel";
import { ObservableCollection, ViewModel } from "react-model-view-viewmodel";
import { ToDoItem } from "../models/todo-item";
import { ToDoItemViewModel } from "./todo-item-view-model";

export class ToDoListViewModel extends ViewModel {
    private readonly _items: IObservableCollection<ToDoItemViewModel> = new ObservableCollection<ToDoItemViewModel>();

    public get items(): IReadOnlyObservableCollection<ToDoItemViewModel> {
        return this._items;
    }

    public load(): void {
        const todoItems: ToDoItem[] = JSON.parse(localStorage.getItem("todo-items") || "[]");
        this._items.reset(...todoItems.map((todoItem, index) => new ToDoItemViewModel(index, todoItem)));
    }
}

Updating the component that displays items is rather easy, the properties we have on our todo item are available on our todo item view model as well, we only need to add the logic for the progress button.

interface IToDoListItemProps {
    readonly item: ToDoItemViewModel;

    selectItem(): void;
}

function ToDoListItem({ item, selectItem }: IToDoListItemProps): JSX.Element {
    const progressCallback = useCallback(() => item.progress(), [item]);

    watchViewModel(item);

    return (
        <div className="todo-list-item">
            <div className="todo-list-item-content">
                {item.description}
                <ToDoListItemState state={item.state} />
            </div>
            <div className="todo-list-item-actions">
                <button onClick={selectItem}>Edit</button>
                <button onClick={progressCallback} disabled={!item.canProgress}>{">"}</button>
            </div>
        </div>
    );
}

That is all for this part. We have added edit features for our todo items as well as progression from the list view. This is looking great. We will continue with ToDo List App Tutorial Part 6: Deleting ToDo Items.

Clone this wiki locally