Example Implementation
See a live, interactive example of a fully reactive TODO list powered by @nhtio/vue-re-active-record. This demo shows both how the library works in practice and how to implement it in your own project.
How This Example Works
- Live Demo: The TODO list above is a real Vue component using the library's Composition API.
- Source Code: Both the plugin configuration and the full component source are shown below, so you can copy, adapt, or learn from them.
- Features Demonstrated:
- Reactive querying and live updates
- Creating, updating, and deleting records
- Type-safe model usage
- Integration with Vue's reactivity system
- Things to try
- Adding new tasks w/ different priorities
- Marking tasks as complete
- Deleting tasks
- Refreshing the page to see that the lists's is the same
- Opening a second tab and seeing updates propagate in real time without any refresh mechansim
Code for Example TODO List
Plugin Configuration
The following snippet shows how the plugin is configured:
ts
app.use(
Plugin,
{
namespace: 'vue-re-active-record-docs',
version: 1750000000000,
psk: 'vue-re-active-record-docs',
models: {
task: {
schema: '++id, title, completed, priority',
properties: ['id', 'title', 'completed', 'priority'],
primaryKey: 'id',
relationships: {},
constraints: makeModelConstraints({
id: joi.number().integer().positive(),
title: joi.string().required(),
completed: joi.boolean().default(false),
priority: joi.number().integer().min(0).max(5).default(3),
}),
},
},
},
{ useOptionsAPI: true, useDevTools: true }
)Component Source
Here is the full source code for the TODO list component:
ts
import { defineComponent, h, ref, shallowRef, triggerRef, onBeforeUnmount } from 'vue'
import { useReActiveModel, onReActiveDatabaseReady } from '@nhtio/vue-re-active-record'
import type { VueReactiveQueryCollection } from '@nhtio/vue-re-active-record'
export const TodoListExample = defineComponent({
name: 'TodoListExample',
setup() {
const colors = ['#3b8eed', '#5487cc', '#7870ad', '#9c598d', '#bf426c', '#cd2d3f']
const model = useReActiveModel('task')
const collection = shallowRef<VueReactiveQueryCollection<'task'> | undefined>(undefined)
const processing = ref<Array<string>>([])
const setAsProcessing = (id: string) => {
if (!processing.value.includes(id)) {
processing.value.push(id)
}
}
const unsetAsProcessing = (id: string) => {
const index = processing.value.indexOf(id)
if (index !== -1) {
processing.value.splice(index, 1)
}
}
onReActiveDatabaseReady(async () => {
const col = await model
.query()
.orderBy('completed', 'asc')
.orderBy('priority', 'desc')
.orderBy('id', 'asc')
.reactive()
.fetch()
collection.value = col as VueReactiveQueryCollection<'task'>
triggerRef(collection)
})
onBeforeUnmount(() => {
if (collection.value) {
collection.value.unmount()
}
})
const clearing = ref(false)
const reset = async () => {
clearing.value = true
model.truncate().then(() => {
clearing.value = false
})
}
const clear = async () => {
clearing.value = true
const promises: Promise<unknown>[] = []
if (collection.value) {
collection.value.$value.forEach((task) => {
promises.push(task.delete())
})
}
if (promises.length > 0) {
Promise.all(promises).then(() => {
clearing.value = false
})
} else {
clearing.value = false
}
}
const creating = ref(false)
const erroredOnCreate = ref(false)
const taskToCreate = ref({
title: '',
completed: false,
priority: 3,
})
const newTaskTitleField = ref<HTMLInputElement | null>(null)
const create = async () => {
if (creating.value) return
creating.value = true
try {
await model.create(taskToCreate.value)
taskToCreate.value.title = ''
taskToCreate.value.completed = false
taskToCreate.value.priority = 3
if (newTaskTitleField.value) {
newTaskTitleField.value.focus()
}
} catch (e) {
console.error('Failed to create task:', e)
erroredOnCreate.value = true
setTimeout(() => {
erroredOnCreate.value = false
}, 3000)
} finally {
creating.value = false
}
}
const deleteTask = async (taskId: string) => {
if (processing.value.includes(taskId)) return
setAsProcessing(taskId)
try {
const task = collection.value?.$value.find((t) => t.id.toString() === taskId)
if (task) {
await task.delete()
}
} catch (e) {
console.error('Failed to delete task:', e)
} finally {
unsetAsProcessing(taskId)
}
}
const toggleTaskCompletion = async (taskId: string) => {
if (processing.value.includes(taskId)) return
setAsProcessing(taskId)
try {
const task = collection.value?.$value.find((t) => t.id.toString() === taskId)
if (task) {
task.completed = !task.completed
await task.save()
}
} catch (e) {
console.error('Failed to toggle task completion:', e)
} finally {
unsetAsProcessing(taskId)
}
}
return () => [
h(
'h3',
{ id: 'vue-re-active-record-todo-list-example', tabindex: '-1' },
'Example TODO List'
),
h(
'form',
{
action: '#',
method: 'post',
onSubmit: (e: Event) => {
e.preventDefault()
create()
},
style: {
overflowX: 'auto',
width: '100%',
minWidth: '100%',
},
},
h(
'table',
{
style: {
display: 'table',
minWidth: '100%',
whiteSpace: 'nowrap',
},
},
[
h('thead', [
h('tr', [
h('th', { style: { width: '40px' } }, ''),
h('th', { style: { width: '40px' } }, 'ID'),
h('th', { style: { minWidth: '180px' } }, 'Title'),
h('th', {}, 'Priority'),
h(
'th',
{ style: { width: '40px' } },
h(
'button',
{
type: 'button',
disabled: clearing.value,
onClick: clear,
class: 'DocSearch DocSearch-Button',
style: {
fontWeight: 'bold',
color: '#ed3c50',
textAlign: 'center',
},
},
'Clear'
)
),
]),
]),
h(
'tbody',
!collection.value || !Array.isArray(collection.value.$value)
? h('tr', h('td', { colspan: 5, style: { textAlign: 'center' } }, 'Loading...'))
: collection.value.$value.length === 0
? h(
'tr',
h(
'td',
{ colspan: 5, style: { textAlign: 'center' } },
'No tasks found. Add a new task below.'
)
)
: collection.value.$value.map((t) => {
return h('tr', [
h(
'td',
{
style: {
textAlign: 'center',
},
},
h(
'button',
{
type: 'button',
disabled: processing.value.includes(t.id.toString()),
onClick: () => toggleTaskCompletion(t.id.toString()),
style: {
fontWeight: 'bold',
textAlign: 'center',
fontSize: '1.5rem',
},
},
processing.value.includes(t.id.toString())
? '⏳'
: t.completed
? '☑'
: '☐'
)
),
h(
'td',
{},
h(
'code',
{
style: {
textDecoration: t.completed ? 'line-through' : 'none',
},
},
t.id
)
),
h(
'td',
{},
h(
'div',
{
style: {
maxWidth: '100%',
whiteSpace: 'wrap',
textAlign: 'justify',
textDecoration: t.completed ? 'line-through' : 'none',
},
},
t.title
)
),
h(
'td',
{
style: {
textAlign: 'center',
},
},
h(
'span',
{
style: {
fontWeight: 'bold',
color: colors[t.priority % colors.length],
textDecoration: t.completed ? 'line-through' : 'none',
},
},
String(t.priority)
)
),
h(
'td',
{
style: {
textAlign: 'center',
},
},
h(
'button',
{
type: 'button',
disabled: processing.value.includes(t.id.toString()),
onClick: () => deleteTask(t.id.toString()),
style: {
fontWeight: 'bold',
color: '#ed3c50',
textAlign: 'center',
},
},
'🗑️'
)
),
])
})
),
h('tfoot', [
h('tr', [
h(
'th',
{ colspan: 3 },
h('input', {
style: {
border: '1px solid var(--vp-c-divider)',
width: '100%',
padding: '6px 12px',
borderRadius: '4px',
},
placeholder: 'New Task Title',
value: taskToCreate.value.title,
onInput: (e: Event) => {
taskToCreate.value.title = (e.target as any).value
},
ref: newTaskTitleField,
})
),
h(
'th',
{},
h('input', {
type: 'number',
min: 0,
max: 5,
style: {
border: '1px solid var(--vp-c-divider)',
width: '100%',
padding: '6px 12px',
borderRadius: '4px',
},
placeholder: 'Priority (0-5)',
value: taskToCreate.value.priority,
onInput: (e: Event) => {
taskToCreate.value.priority = Number.parseInt((e.target as any).value, 10)
},
})
),
h(
'th',
{ style: { width: '40px' } },
h(
'button',
{
type: 'submit',
disabled: creating.value,
class: 'DocSearch DocSearch-Button',
style: {
fontWeight: 'bold',
color: '#42b883',
textAlign: 'center',
},
},
h(
'span',
{
style: {
display: 'block',
width: '100%',
},
},
'Add'
)
)
),
]),
h('tr', [
h('td', { colspan: 4, style: { textAlign: 'center' } }, ''),
h(
'td',
{ style: { textAlign: 'center' } },
h(
'button',
{
type: 'button',
disabled: clearing.value,
onClick: () => reset(),
class: 'DocSearch DocSearch-Button',
style: {
fontWeight: 'bold',
color: '#ed3c50',
textAlign: 'center',
},
},
'Reset'
)
),
]),
erroredOnCreate.value
? h(
'tr',
h(
'td',
{ colspan: 5, style: { color: 'red', textAlign: 'center' } },
'Failed to create task. Please check the console for details.'
)
)
: null,
]),
]
)
),
]
},
})