Skip to content

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