Skip to content

Alpine Example: CreateHotkeys

import {
  createHotkeysScope,
  formatForDisplay,
  normalizeHotkey,
  validateHotkey,
} from '@tanstack/alpine-hotkeys'
import Alpine from 'alpinejs'
import type {
  AlpineHotkeyDefinition,
  Hotkey,
  HotkeyRegistrationsResult,
} from '@tanstack/alpine-hotkeys'
import './index.css'

interface DynamicShortcut {
  id: number
  hotkey: Hotkey
  label: string
  description: string
  count: number
}

let nextId = 0

const DEFAULT_SHORTCUTS: Array<DynamicShortcut> = [
  {
    id: nextId++,
    hotkey: 'Shift+A',
    label: 'Action A',
    description: 'First dynamic action',
    count: 0,
  },
  {
    id: nextId++,
    hotkey: 'Shift+B',
    label: 'Action B',
    description: 'Second dynamic action',
    count: 0,
  },
  {
    id: nextId++,
    hotkey: 'Shift+C',
    label: 'Action C',
    description: 'Third dynamic action',
    count: 0,
  },
]

class App {}

class BasicMultiHotkeys {
  private hotkeysScope = createHotkeysScope()

  formatForDisplay = formatForDisplay
  usage0 =
    "scope.createHotkeys([\n  { hotkey: 'Shift+S', callback: save,\n    options: { meta: { name: 'Save', description: 'Save the document' } } },\n  { hotkey: 'Shift+U', callback: undo,\n    options: { meta: { name: 'Undo' } } },\n])"
  log: Array<string> = []
  saveCount = 0
  undoCount = 0
  redoCount = 0

  init() {
    this.hotkeysScope.createHotkeys([
      {
        hotkey: 'Shift+S',
        callback: (_e, { hotkey }) => {
          this.saveCount = this.saveCount + 1
          this.log = [`${hotkey} pressed`, ...this.log].slice(0, 20)
        },
        options: {
          meta: { name: 'Save', description: 'Save the current document' },
        },
      },
      {
        hotkey: 'Shift+U',
        callback: (_e, { hotkey }) => {
          this.undoCount = this.undoCount + 1
          this.log = [`${hotkey} pressed`, ...this.log].slice(0, 20)
        },
        options: {
          meta: { name: 'Undo', description: 'Undo the last action' },
        },
      },
      {
        hotkey: 'Shift+R',
        callback: (_e, { hotkey }) => {
          this.redoCount = this.redoCount + 1
          this.log = [`${hotkey} pressed`, ...this.log].slice(0, 20)
        },
        options: {
          meta: { name: 'Redo', description: 'Redo the last undone action' },
        },
      },
    ])
  }
  destroy() {
    this.hotkeysScope.destroy()
  }
}

class CommonOptionsDemo {
  private hotkeysScope = createHotkeysScope()

  formatForDisplay = formatForDisplay
  usage0 =
    "scope.createHotkeys(\n  [\n    { hotkey: 'Alt+J', callback: actionA },\n    { hotkey: 'Alt+L', callback: actionC, options: { enabled: true } },\n  ],\n  () => ({ enabled: this.enabled }),\n)"
  enabled = true
  counts: { a: number; b: number; c: number } = { a: 0, b: 0, c: 0 }

  init() {
    this.hotkeysScope.createHotkeys(
      [
        {
          hotkey: 'Alt+J',
          callback: () =>
            (this.counts = { ...this.counts, a: this.counts.a + 1 }),
          options: {
            meta: {
              name: 'Action A',
              description: 'First action (respects toggle)',
            },
          },
        },
        {
          hotkey: 'Alt+K',
          callback: () =>
            (this.counts = { ...this.counts, b: this.counts.b + 1 }),
          options: {
            meta: {
              name: 'Action B',
              description: 'Second action (respects toggle)',
            },
          },
        },
        {
          hotkey: 'Alt+L',
          callback: () =>
            (this.counts = { ...this.counts, c: this.counts.c + 1 }),
          options: {
            enabled: true,
            meta: {
              name: 'Action C',
              description: 'Always-on action (overrides toggle)',
            },
          },
        },
      ],
      () => ({ enabled: this.enabled }),
    )
  }
  destroy() {
    this.hotkeysScope.destroy()
  }
}

class DynamicHotkeysDemo {
  private hotkeysScope = createHotkeysScope()

  formatForDisplay = formatForDisplay
  usage0 =
    'scope.createHotkeys(() => this.shortcuts.map((shortcut) => ({\n  hotkey: shortcut.hotkey,\n  callback: () => this.runShortcut(shortcut.id),\n  options: { meta: { name: shortcut.label, description: shortcut.description } },\n})))'
  shortcuts: Array<DynamicShortcut> = DEFAULT_SHORTCUTS
  newHotkey = ''
  newLabel = ''
  newDescription = ''
  error = ''
  get definitions(): Array<AlpineHotkeyDefinition> {
    return this.shortcuts.map((s) => ({
      hotkey: s.hotkey,
      callback: () => {
        this.shortcuts = this.shortcuts.map((item) =>
          item.id === s.id ? { ...item, count: item.count + 1 } : item,
        )
      },
      options: {
        meta: { name: s.label, description: s.description },
      },
    }))
  }
  addShortcut() {
    const trimmed = this.newHotkey.trim()
    if (!this.newLabel.trim()) return
    const validation = validateHotkey(trimmed)
    if (!validation.valid || validation.warnings.length > 0) {
      this.error = [...validation.errors, ...validation.warnings].join(' ')
      return
    }
    this.error = ''
    this.shortcuts = [
      ...this.shortcuts,
      {
        id: nextId++,
        hotkey: normalizeHotkey(trimmed),
        label: this.newLabel.trim(),
        description: this.newDescription.trim(),
        count: 0,
      },
    ]
    this.newHotkey = ''
    this.newLabel = ''
    this.newDescription = ''
  }
  removeShortcut(id: number) {
    this.shortcuts = this.shortcuts.filter((s) => s.id !== id)
  }

  init() {
    this.addShortcut = this.addShortcut.bind(this)
    this.removeShortcut = this.removeShortcut.bind(this)
    this.hotkeysScope.createHotkeys(() => this.definitions)
  }
  destroy() {
    this.hotkeysScope.destroy()
  }
}

class RegistrationsViewer {
  private hotkeysScope = createHotkeysScope()

  formatForDisplay = formatForDisplay
  usage0 =
    'const registrations = scope.createHotkeyRegistrations()\n\n// Alpine reads registrations.hotkeys, chords and sequences, reactively.\n// <template x-for="reg in registrations.hotkeys" :key="reg.id">\n//   <p x-text="reg.options.meta?.name + \': \' + reg.triggerCount"></p>\n// </template>'
  registrationState0!: HotkeyRegistrationsResult
  get hotkeys() {
    return this.registrationState0.hotkeys
  }
  init() {
    this.registrationState0 = this.hotkeysScope.createHotkeyRegistrations()
  }
  destroy() {
    this.hotkeysScope.destroy()
  }
}
Alpine.data('app', () => new App())
Alpine.data('basicMultiHotkeys', () => new BasicMultiHotkeys())
Alpine.data('commonOptionsDemo', () => new CommonOptionsDemo())
Alpine.data('dynamicHotkeysDemo', () => new DynamicHotkeysDemo())
Alpine.data('registrationsViewer', () => new RegistrationsViewer())
Alpine.start()