7. Make tasks editable

Let the service update tasks

export class TaskService extends Service<TaskService> {
    /* ... */
    public updateTask(task: ITask): void {
        this._tasks = this._tasks.map((t) => {
            return t.id === task.id ? task : t
        });
        AppEventBus.emit('task-event', { taskId: task.id })
    }
}

Generate the component

npm run generate component components/task-inputs
# Creates src/app/components/task-inputs/task-inputs.component.ts|html|scss

Give the component a task

import { Component, html } from '../../../core/index.js';
import { taskService } from '../../services/task/task.service.js';

const templateFn = (_task: ITask) => html`__TEMPLATE_PLACEHOLDER__`;

export class TaskInputsComponent extends Component {
  private _task: ITask;

  constructor(mountTarget: HTMLElement, config: ITask) {
    super({templateFn, mountTarget, config});
    this._task = config;
  }

  private _editTask(field: keyof ITask, value: string): void { 
    this._task = { ...this._task, [field]: value };
    taskService.updateTask(this._task);
  }
}

Handle changes

<div class="task-inputs">
    <form>
        <input
            type="text"
            id="name"
            placeholder="Name"
            value="${_task.name}"
            data-event="change:_editTask:name,@value"
        />
        <textarea
            id="content"
            placeholder="Task details..."
            data-event="change:_editTask:content,@value"
        >${_task.content ?? ''}</textarea>
    </form>
</div>

Add style

.task-inputs {
    padding: 2rem;
    input {
        font-size: 1.5rem;
        font-weight: bold;
        padding: .2rem .5rem;
        margin-bottom: .5rem;
        width: 100%;
        background: #F1F1EF;
        border-radius: 8px;
    }
    textarea {
        width: 100%;
        min-height: 20vh;
        font-size: 1.2rem;
        padding: .5rem 1rem;
        background: #E5E5E2;
        border-radius: 8px;
    }
}

Use dynamic routing for your tasks

Register a dynamic route

// src/app/routes.ts
export const appRoutes: Nutin.Routes = {
    /* ... */
    '/tasks/:id?': () => new TaskCatalogView(),
    /* ... */
}

Guard the route

A route guard runs before the view is created, gets the route's params, and returns true to let the navigation through, or a path to redirect to.

Add one next to the generated requireAuth example:

// src/app/guards.ts
import { taskService } from './services/task/task.service.js';

export const Guards = {
    /* requireAuth ... */

    /**
     * Redirects when the route's task doesn't exist, e.g. after a reload: tasks only live in memory.
     */
    requireTask: (redirectTo: string = '/'): Nutin.RouteGuard => {
        return ({ id }) => id === undefined || taskService.getTask(+id) !== undefined || redirectTo;
    },
};

Then use the route's object form to attach it:

// src/app/routes.ts
import { Guards } from './guards.js';

export const appRoutes: Nutin.Routes = {
    /* ... */
    '/tasks/:id?': { view: () => new TaskCatalogView(), guards: [Guards.requireTask()] },
    /* ... */
}

See Use route guards.

Handle routeParams in the view

/* ... */
import { TaskInputsComponent } from '../../components/task-inputs/task-inputs.component.js';

export class TaskCatalogView extends View {
    /* ... */

    registerChildren(): Nutin.ComponentConfig[] {
        const taskCatalogChildren: Nutin.ComponentConfig[] = [ /* new-task, task-cards */ ];

        if (this.hasRouteParam('id')) {
            taskCatalogChildren.push(...this.getTaskInputsChild());
        }

        return taskCatalogChildren;
    }
  
    private getTaskInputsChild(): Nutin.ComponentConfig[] {
        const taskId = this.getRouteParam('id') || 0;
        const task = taskService.getTask(+taskId);
        if (!task) return [];

        return [
            { 
                selector: `task-inputs`,
                factory: (el) => new TaskInputsComponent(el, task)
            }
        ]  
    }
}
<div class="task-catalog__body">
    <div data-catalog="task-cards" class="task-catalog__task-cards"></div>
    <div data-component="task-inputs"></div>
</div>
/* ... */
import { Navigation } from '../../../core/index.js';

export class TaskCardComponent extends Component {
    /* ... */

    registerChildren(): Nutin.ComponentConfig[] {
        return [
            /* ... */
            {
                selector: 'edit',
                factory: (el) => new TaskActionComponent(el, {
                    callback: () => this._goToEdit(),
                    textContent: 'Edit',
                })
            },
        ]
    }

    private _goToEdit(): void {
        Navigation.navigateTo(`/tasks/${this.config.id}`)
    }
}
<div class="task-card">
    <!-- ... -->
    <div class="task-card__actions">
        <div data-component="edit"></div>
        <div data-component="remove"></div>
    </div>
</div>
.task-card__actions {
    padding-top: .5rem;
    display: flex;
    justify-content: flex-end;
}

Keep elements across re-renders

The focus is lost when the view re-renders: every child is destroyed and created again.

Tell the view which children are the same as before:

export class TaskCatalogView extends View {
    /* ... */

    registerChildren(): Nutin.ComponentConfig[] {
        const taskCatalogChildren: Nutin.ComponentConfig[] = [
            /* new-task */
            ...this.createCatalogComponents({
                /* ... */
                // A card whose task didn't change is kept on re-render.
                keepBy: (task) => task.id,
            }),
        ];
        /* ... */
    }

    private getTaskInputsChild(): Nutin.ComponentConfig[] {
        /* ... */
        return [
            {
                selector: 'task-inputs',
                // Same task, same form: it's kept while you edit.
                keep: task.id,
                factory: (el) => new TaskInputsComponent(el, task),
            },
        ];
    }
}

See Keeping children across re-renders.

Next step →