Rename a todo title ​
Updating the TodoPageModel
​
We can leverage a generic patchTodo
action and use for both toggling a todo and changing a todo’s title:
ts
// src/spa-client-side/10-update-a-todo-title/TodoPageModel.ts
import { TodoApi, Todo } from '@/spa-client-side/setup/TodoApi'
import {
createRemoteAction,
RemoteAction,
} from '@/spa-client-side/setup/RemoteAction'
import { effect } from '@/spa-client-side/setup/Signal'
import {
computed,
effect,
ReadonlySignal,
} from '@/spa-client-side/setup/Signal'
export interface TodoPageModel {
getTodoList: RemoteAction<Todo[]>
toggleTodo: RemoteAction<Todo, [todo: Todo]>
canPatchAnyTodo: ReadonlySignal<boolean>
toggleTodo: (todo: Todo) => Promise<void>
changeTodoTitle: (todo: Todo, title: string) => Promise<void>
dispose: () => void
}
export function makeTodoPageModel(api: TodoApi): TodoPageModel {
const getTodoList = createRemoteAction(api.getTodos.bind(api))
const toggleTodo = createRemoteAction((todo: Todo) => {
return api.patchTodo(todo.id, { completed: !todo.completed })
})
const patchTodo = createRemoteAction(api.patchTodo.bind(api))
return {
getTodoList,
toggleTodo,
canPatchAnyTodo: computed(() => patchTodo.data.get().state !== 'pending'),
toggleTodo: (todo) => {
return patchTodo.trigger(todo.id, { completed: !todo.completed })
},
changeTodoTitle: (todo, title) => {
return patchTodo.trigger(todo.id, { title })
},
dispose: registerEffects(),
}
function registerEffects() {
const dispose = effect(() => {
const data = toggleTodo.data.get()
const data = patchTodo.data.get()
if (data.state !== 'success') return
// update the current todo list:
getTodoList.data.update((list) => {
if (list.state !== 'success') return list
// replace the todo in the list by the patched todo
const nextList = list.value.map((todo) => {
return todo.id === data.value.id ? data.value : todo
})
return { state: 'success', value: nextList }
})
})
return dispose
}
}
Updating the React components ​
Now let’s update our TodoCheckboxList
component to add an input:
tsx
// src/spa-client-side/10-update-a-todo-title/react/TodoCheckboxList.tsx
/** @jsx React.createElement */
import React from 'react'
import { Todo } from '@/spa-client-side/setup/TodoApi'
interface Props {
todos: Todo[]
onToggle: (todo: Todo) => unknown
onTitleChanged: (todo: Todo, title: string) => unknown
disabled: boolean
}
export function TodoCheckboxList({ todos, onToggle, disabled }: Props) {
export function TodoCheckboxList({
todos,
onToggle,
onTitleChanged,
disabled,
}: Props) {
return (
<div>
{todos.map((todo) => (
<label key={todo.id} className="todo-item">
<div key={todo.id} className="todo-item">
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo)}
disabled={disabled}
/>
<span>{todo.title}</span>
</label>
<input
type="text"
disabled={disabled}
defaultValue={todo.title}
onBlur={(event) => {
const nextTitle = event.target.value
if (nextTitle === todo.title) return
return onTitleChanged(todo, nextTitle)
}}
/>
</div>
))}
</div>
)
}
Updating the Vue components ​
Same, let’s update our TodoCheckboxList
component to add an input:
vue
// src/spa-client-side/10-update-a-todo-title/vue/TodoCheckboxList.vue
<script setup lang="ts">
import { Todo } from '@/spa-client-side/setup/TodoApi'
const props = defineProps<{
todos: Todo[]
disabled: boolean
}>()
const emit = defineEmits<{
toggle: [todo: Todo]
titleChanged: [todo: Todo, title: string]
}>()
function maybeEmitTitleChanged(todo: Todo, event: FocusEvent) {
const nextTitle = (event.target as HTMLInputElement).value
if (nextTitle === todo.title) return
emit('titleChanged', todo, nextTitle)
}
</script>
<template>
<div class="todo-item">
<label v-for="todo in props.todos" style="display: block">
<div>
<div v-for="todo in props.todos" class="todo-item">
<input
type="checkbox"
:checked="todo.completed"
:disabled="props.disabled"
@change="emit('toggle', todo)"
/>
<span>{{ todo.title }}</span>
</label>
<input // [!code ++]
type="text" // [!code ++]
:value="todo.title" // [!code ++]
:disabled="props.disabled" // [!code ++]
@blur="(event) => maybeEmitTitleChanged(todo, event)" // [!code ++]
/>
</div>
</div>
</template>
Next step: delete a todo