The Vue
App β
The reactivity system adapter β signalRef
β
Because our AppModel
is defined using our reactivity system, we need to adapt it to Vue. Vue relies on the ref
concept, so letβs adapt our signal:
ts
// src/spa-client-side/7-vue-app/signalRef.ts
import { onUnmounted, ref } from 'vue'
import { effect, ReadonlySignal } from '@/spa-client-side/setup/Signal'
export function signalRef<T>(signal: ReadonlySignal<T>) {
const value = ref(signal.get())
const dispose = effect(() => {
value.value = signal.get()
})
onUnmounted(dispose)
return value
}
As you can see, the adapter is quite simple.
The App
component β
We will adapt the route
signal to a Vue state, and handle both cases NotFound
and TodoListPage
.
vue
// src/spa-client-side/7-vue-app/App.vue
<script setup lang="ts">
import { AppModel } from '../5-app-model/AppModel'
import { signalRef } from './signalRef'
import TodoPage from './TodoPage.vue'
const props = defineProps<{ model: AppModel }>()
const model = props.model
const route = signalRef(model.route)
</script>
<template>
<h2>Vue App</h2>
<p v-if="route.name === 'NotFound'">
Page Not Found
<button type="button" @click="model.goToTodos">Go to todos</button>
</p>
<TodoPage v-if="route.name === 'TodoListPage'" :model="route.make()" />
</template>
The TodoPage
component β
We will adapt the todos
signal to a React state, and display the remote list of todos, leveraging a yet-to-create <RemoteData />
component.
vue
// src/spa-client-side/7-vue-app/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'
const props = defineProps<{ model: TodoPageModel }>()
const model = props.model
const todoList = signalRef(model.getTodoList.data)
// fetch the todos on mount.
onMounted(() => void model.getTodoList.trigger())
</script>
<template>
<div>
<p>Todo Page in Vue</p>
<RemoteData :data="todoList">
<template #success="{ value }">
<TodoUnorderedList :todos="value" />
</template>
</RemoteData>
</div>
</template>
The TodoUnorderedList
component β
This one is purely presentational, nothing much to say:
vue
// src/spa-client-side/7-vue-app/TodoUnorderedList.vue
<script setup lang="ts">
import { Todo } from '@/spa-client-side/setup/TodoApi'
const props = defineProps<{ todos: Todo[] }>()
</script>
<template>
<ul>
<li v-for="todo in props.todos">{{ todo.title }}</li>
</ul>
</template>
And finally, the RemoteData
component β
This one is key for readability, it also enables the possibility to handle all the errors at a dedicated place, while still allowing customization.
vue
// src/spa-client-side/7-vue-app/RemoteData.vue
<script setup lang="ts" generic="T">
import { RemoteData } from '@/spa-client-side/setup/RemoteData'
defineProps<{ data: RemoteData<T> }>()
</script>
<template>
<slot v-if="data.state === 'initial'" name="initial">
<p>Waiting for data to be loaded</p>
</slot>
<slot v-else-if="data.state === 'pending'" name="pending">
<p>Loadingβ¦</p>
</slot>
<slot v-else-if="data.state === 'failure'" name="failure" :error="data.error">
<p>Error: {{ data.error.message }}</p>
</slot>
<slot v-else name="success" :value="data.value"></slot>
</template>
Excellent, everything is in place. We can now render these apps π