Toggle a Todo β
Updating the TodoPageModel
β
Adding the toggleTodo
action β
We can add a toggleTodo
action on our TodoPageModel
:
ts
// src/spa-client-side/9-toggle-todo/TodoPageModel-attempt-1.ts
import { TodoApi, Todo } from '@/spa-client-side/setup/TodoApi'
import {
createRemoteAction,
RemoteAction,
} from '@/spa-client-side/setup/RemoteAction'
export interface TodoPageModel {
getTodoList: RemoteAction<Todo[]>
toggleTodo: RemoteAction<Todo, [todo: Todo]>
}
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 })
})
return {
getTodoList,
toggleTodo,
}
}
Updating the list upon toggle-todo-success β
Great, now letβs update the list upon toggle-todo-success:
ts
// src/spa-client-side/9-toggle-todo/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'
export interface TodoPageModel {
getTodoList: RemoteAction<Todo[]>
toggleTodo: RemoteAction<Todo, [todo: Todo]>
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 })
})
return {
getTodoList,
toggleTodo,
dispose: registerEffects(),
}
function registerEffects() {
const dispose = effect(() => {
const data = toggleTodo.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 instead of an unordered list, we will render a checkbox list:
tsx
// src/spa-client-side/9-toggle-todo/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
disabled: boolean
}
export function TodoUnorderedList({ todos }: Props) {
export function TodoCheckboxList({ todos, onToggle, disabled }: Props) {
return (
<ul>
<div>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
<label key={todo.id} className="todo-item">
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo)}
disabled={disabled}
/>
<span>{todo.title}</span>
</label>
))}
</ul>
</div>
)
}
Letβs update our TodoPage
component to render a checkbox list:
tsx
// src/spa-client-side/9-toggle-todo/react/TodoPage.tsx
/** @jsx React.createElement */
import React, { useEffect } from 'react'
import { TodoPageModel } from '../5-app-model/TodoPageModel'
import { useSignal } from './useSignal'
import { RemoteData } from './RemoteData'
import { TodoUnorderedList } from './TodoUnorderedList'
import React from 'react'
import { RemoteData } from '@/spa-client-side/6-react-app/RemoteData'
import { useSignal } from '@/spa-client-side/6-react-app/useSignal'
import { useEffect } from 'react'
import { TodoPageModel } from '../TodoPageModel'
import { TodoCheckboxList } from './TodoCheckboxList'
interface Props {
model: TodoPageModel
}
export function TodoPage({ model }: Props) {
const todoList = useSignal(model.getTodoList.data)
const toggleData = useSignal(model.toggleTodo.data)
// fetch the todo list on mount.
useEffect(() => {
void model.getTodoList.trigger()
// explicitly mark the promise as non-awaited with `void`
}, [])
// dispose on unmount.
useEffect(() => model.dispose, [])
return (
<div>
<p>Todo Page in React</p>
<RemoteData
data={todoList}
success={(todos) => <TodoUnorderedList todos={todos} />}
success={(todos) => (
<TodoCheckboxList
todos={todos}
onToggle={model.toggleTodo.trigger}
disabled={toggleData.state === 'pending'}
/>
)}
/>
</div>
)
}
Updating the Vue components β
The checkbox list components:
vue
// src/spa-client-side/9-toggle-todo/vue/TodoCheckboxList.vue
<script setup lang="ts">
import { Todo } from '@/spa-client-side/setup/TodoApi'
const props = defineProps<{ todos: Todo[] }>()
const props = defineProps<{
todos: Todo[]
disabled: boolean
}>()
const emit = defineEmits<{
toggle: [todo: Todo]
}>()
</script>
<template>
<ul>
<li v-for="todo in props.todos">{{ todo.title }}</li>
</ul>
<div class="todo-item">
<label v-for="todo in props.todos" style="display: block">
<input // [!code ++]
type="checkbox" // [!code ++]
:checked="todo.completed" // [!code ++]
:disabled="props.disabled" // [!code ++]
@change="emit('toggle', todo)" // [!code ++]
/>
<span>{{ todo.title }}</span>
</label>
</div>
</template>
The TodoPage
component:
vue
// src/spa-client-side/9-toggle-todo/vue/TodoPage.vue
<script setup lang="ts">
import { onMounted } from 'vue'
import { TodoPageModel } from '../5-app-model/TodoPageModel'
import { signalRef } from './signalRef'
import RemoteData from './RemoteData.vue'
import TodoUnorderedList from './TodoUnorderedList.vue'
import { onMounted, onUnmounted } from 'vue'
import { signalRef } from '@/spa-client-side/7-vue-app/signalRef'
import TodoCheckboxList from './TodoCheckboxList.vue'
import { TodoPageModel } from '../TodoPageModel'
import RemoteData from '@/spa-client-side/7-vue-app/RemoteData.vue'
const props = defineProps<{ model: TodoPageModel }>()
const model = props.model
const todoList = signalRef(model.getTodoList.data)
const toggleData = signalRef(model.toggleTodo.data)
// fetch the todos on mount.
onMounted(() => void model.getTodoList.trigger())
onUnmounted(model.dispose)
</script>
<template>
<div>
<p>Todo Page in Vue</p>
<RemoteData :data="todoList">
<template #success="{ value }">
<TodoUnorderedList :todos="value" />
<TodoCheckboxList // [!code ++]
:todos="value" // [!code ++]
:disabled="toggleData.state === 'pending'" // [!code ++]
@toggle="model.toggleTodo.trigger" // [!code ++]
/>
</template>
</RemoteData>
</div>
</template>
Next step: update a todo title