Skip to content

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,
            ]),
          ]
        )
      ),
    ]
  },
})