# Custom elements

LiveStore can be used with custom elements/web components.

## Example

See [examples](/examples) for a complete example.


## `reference/custom-elements/main.ts`

```ts filename="reference/custom-elements/main.ts"
import { makePersistedAdapter } from '@livestore/adapter-web'
import LiveStoreSharedWorker from '@livestore/adapter-web/shared-worker?sharedworker'
import { createStorePromise, queryDb } from '@livestore/livestore'

import LiveStoreWorker from './livestore/livestore.worker.ts?worker'
import { events, schema, tables } from './livestore/schema.ts'

const adapter = makePersistedAdapter({
  storage: { type: 'opfs' },
  worker: LiveStoreWorker,
  sharedWorker: LiveStoreSharedWorker,
})

const store = await createStorePromise({ schema, adapter, storeId: 'custom-elements-demo' })

const visibleTodos$ = queryDb(tables.todos.where({ deletedAt: null }))

class TodoListElement extends HTMLElement {
  private list: HTMLUListElement
  private input: HTMLInputElement

  constructor() {
    super()
    const shadow = this.attachShadow({ mode: 'open' })

    this.input = document.createElement('input')
    this.input.placeholder = 'What needs to be done?'

    this.list = document.createElement('ul')
    this.list.style.listStyle = 'none'
    this.list.style.padding = '0'
    this.list.style.margin = '16px 0 0'

    this.input.addEventListener('keydown', (event) => {
      if (event.key === 'Enter' && this.input.value.trim() !== '') {
        store.commit(events.todoCreated({ id: crypto.randomUUID(), text: this.input.value.trim() }))
        this.input.value = ''
      }
    })

    shadow.append(this.input, this.list)
  }

  connectedCallback(): void {
    this.renderTodos(Array.from(store.query(tables.todos.where({ deletedAt: null }))))

    store.subscribe(visibleTodos$, (todos) => this.renderTodos(todos))
  }

  private renderTodos(todos: ReadonlyArray<typeof tables.todos.Type>): void {
    const nodes = Array.from(todos, (todo) => {
      const item = document.createElement('li')
      item.textContent = todo.text
      item.style.cursor = 'pointer'
      item.addEventListener('click', () => {
        store.commit(
          todo.completed === true ? events.todoUncompleted({ id: todo.id }) : events.todoCompleted({ id: todo.id }),
        )
      })

      const deleteButton = document.createElement('button')
      deleteButton.type = 'button'
      deleteButton.textContent = '✕'
      deleteButton.style.marginLeft = '8px'
      deleteButton.addEventListener('click', (event) => {
        event.stopPropagation()
        store.commit(events.todoDeleted({ id: todo.id, deletedAt: new Date() }))
      })

      const row = document.createElement('div')
      row.style.display = 'flex'
      row.style.alignItems = 'center'
      row.appendChild(item)
      row.appendChild(deleteButton)

      const wrapper = document.createElement('li')
      wrapper.appendChild(row)
      return wrapper
    })

    this.list.replaceChildren(...nodes)
  }
}

customElements.define('todo-list', TodoListElement)
```

### `reference/custom-elements/livestore/schema.ts`

```ts filename="reference/custom-elements/livestore/schema.ts"
import { Events, makeSchema, Schema, SessionIdSymbol, State } from '@livestore/livestore'

export const tables = {
  todos: State.SQLite.table({
    name: 'todos',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      text: State.SQLite.text({ default: '' }),
      completed: State.SQLite.boolean({ default: false }),
      deletedAt: State.SQLite.integer({ nullable: true, schema: Schema.DateFromNumber }),
    },
  }),
  uiState: State.SQLite.clientDocument({
    name: 'uiState',
    schema: Schema.Struct({ newTodoText: Schema.String }),
    default: { id: SessionIdSymbol, value: { newTodoText: '' } },
  }),
}

export const events = {
  todoCreated: Events.synced({
    name: 'v1.TodoCreated',
    schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
  }),
  todoCompleted: Events.synced({
    name: 'v1.TodoCompleted',
    schema: Schema.Struct({ id: Schema.String }),
  }),
  todoUncompleted: Events.synced({
    name: 'v1.TodoUncompleted',
    schema: Schema.Struct({ id: Schema.String }),
  }),
  todoDeleted: Events.synced({
    name: 'v1.TodoDeleted',
    schema: Schema.Struct({ id: Schema.String, deletedAt: Schema.Date }),
  }),
  uiStateSet: tables.uiState.set,
}

const materializers = State.SQLite.materializers(events, {
  'v1.TodoCreated': ({ id, text }) => tables.todos.insert({ id, text, completed: false }),
  'v1.TodoCompleted': ({ id }) => tables.todos.update({ completed: true }).where({ id }),
  'v1.TodoUncompleted': ({ id }) => tables.todos.update({ completed: false }).where({ id }),
  'v1.TodoDeleted': ({ id, deletedAt }) => tables.todos.update({ deletedAt }).where({ id }),
})

const state = State.SQLite.makeState({ tables, materializers })

export const schema = makeSchema({ events, state })
```

