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>
Navigate to the route
/* ... */
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),
},
];
}
}