Angular Example: InjectHotkeys

import { Component, signal } from '@angular/core'
import {
  formatForDisplay,
  injectHotkeyRegistrations,
  injectHotkeys,
} from '@tanstack/angular-hotkeys'
import type { Hotkey, InjectHotkeyDefinition } from '@tanstack/angular-hotkeys'

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

@Component({
  selector: 'app-root',
  standalone: true,
  templateUrl: './app.component.html',
})
export class AppComponent {
  fd = (h: string) => formatForDisplay(h as Hotkey)

  // Basic demo
  log = signal<string[]>([])
  saveCount = signal(0)
  undoCount = signal(0)
  redoCount = signal(0)

  // Common options demo
  commonEnabled = signal(true)
  counts = signal({ a: 0, b: 0, c: 0 })

  // Dynamic demo
  nextId = 0
  shortcuts = signal<DynamicShortcut[]>([
    {
      id: this.nextId++,
      hotkey: 'Shift+A',
      label: 'Action A',
      description: 'First dynamic action',
      count: 0,
    },
    {
      id: this.nextId++,
      hotkey: 'Shift+B',
      label: 'Action B',
      description: 'Second dynamic action',
      count: 0,
    },
    {
      id: this.nextId++,
      hotkey: 'Shift+C',
      label: 'Action C',
      description: 'Third dynamic action',
      count: 0,
    },
  ])
  newHotkey = signal('')
  newLabel = signal('')
  newDescription = signal('')

  // Registrations viewer
  readonly registrations = injectHotkeyRegistrations()

  constructor() {
    // Basic multi-hotkey with meta
    injectHotkeys([
      {
        hotkey: 'Shift+S',
        callback: (_e, { hotkey }) => {
          this.saveCount.update((c) => c + 1)
          this.log.update((l) => [`${hotkey} pressed`, ...l].slice(0, 20))
        },
        options: {
          meta: { name: 'Save', description: 'Save the current document' },
        },
      },
      {
        hotkey: 'Shift+U',
        callback: (_e, { hotkey }) => {
          this.undoCount.update((c) => c + 1)
          this.log.update((l) => [`${hotkey} pressed`, ...l].slice(0, 20))
        },
        options: {
          meta: { name: 'Undo', description: 'Undo the last action' },
        },
      },
      {
        hotkey: 'Shift+R',
        callback: (_e, { hotkey }) => {
          this.redoCount.update((c) => c + 1)
          this.log.update((l) => [`${hotkey} pressed`, ...l].slice(0, 20))
        },
        options: {
          meta: { name: 'Redo', description: 'Redo the last undone action' },
        },
      },
    ])

    // Common options with per-hotkey overrides and meta
    injectHotkeys(
      [
        {
          hotkey: 'Alt+J',
          callback: () => this.counts.update((c) => ({ ...c, a: c.a + 1 })),
          options: {
            meta: {
              name: 'Action A',
              description: 'First action (respects toggle)',
            },
          },
        },
        {
          hotkey: 'Alt+K',
          callback: () => this.counts.update((c) => ({ ...c, b: c.b + 1 })),
          options: {
            meta: {
              name: 'Action B',
              description: 'Second action (respects toggle)',
            },
          },
        },
        {
          hotkey: 'Alt+L',
          callback: () => this.counts.update((c) => ({ ...c, c: c.c + 1 })),
          options: {
            enabled: true,
            meta: {
              name: 'Action C',
              description: 'Always-on action (overrides toggle)',
            },
          },
        },
      ],
      () => ({ enabled: this.commonEnabled() }),
    )

    // Dynamic hotkeys with meta
    injectHotkeys(() =>
      this.shortcuts().map(
        (s): InjectHotkeyDefinition => ({
          hotkey: s.hotkey as Hotkey,
          callback: () => {
            this.shortcuts.update((prev) =>
              prev.map((item) =>
                item.id === s.id ? { ...item, count: item.count + 1 } : item,
              ),
            )
          },
          options: {
            meta: { name: s.label, description: s.description },
          },
        }),
      ),
    )
  }

  toggleCommon() {
    this.commonEnabled.update((e) => !e)
  }

  addShortcut() {
    const trimmed = this.newHotkey().trim()
    if (!trimmed || !this.newLabel().trim()) return
    this.shortcuts.update((prev) => [
      ...prev,
      {
        id: this.nextId++,
        hotkey: trimmed,
        label: this.newLabel().trim(),
        description: this.newDescription().trim(),
        count: 0,
      },
    ])
    this.newHotkey.set('')
    this.newLabel.set('')
    this.newDescription.set('')
  }

  removeShortcut(id: number) {
    this.shortcuts.update((prev) => prev.filter((s) => s.id !== id))
  }

  onNewHotkeyInput(event: Event) {
    this.newHotkey.set((event.target as HTMLInputElement).value)
  }

  onNewLabelInput(event: Event) {
    this.newLabel.set((event.target as HTMLInputElement).value)
  }

  onNewDescriptionInput(event: Event) {
    this.newDescription.set((event.target as HTMLInputElement).value)
  }

  onInputKeydown(event: KeyboardEvent) {
    if (event.key === 'Enter') this.addShortcut()
  }

  formatSeq(seq: Array<string>): string {
    return seq.map((h) => formatForDisplay(h as Hotkey)).join(' ')
  }
}
import { Component, signal } from '@angular/core'
import {
  formatForDisplay,
  injectHotkeyRegistrations,
  injectHotkeys,
} from '@tanstack/angular-hotkeys'
import type { Hotkey, InjectHotkeyDefinition } from '@tanstack/angular-hotkeys'

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

@Component({
  selector: 'app-root',
  standalone: true,
  templateUrl: './app.component.html',
})
export class AppComponent {
  fd = (h: string) => formatForDisplay(h as Hotkey)

  // Basic demo
  log = signal<string[]>([])
  saveCount = signal(0)
  undoCount = signal(0)
  redoCount = signal(0)

  // Common options demo
  commonEnabled = signal(true)
  counts = signal({ a: 0, b: 0, c: 0 })

  // Dynamic demo
  nextId = 0
  shortcuts = signal<DynamicShortcut[]>([
    {
      id: this.nextId++,
      hotkey: 'Shift+A',
      label: 'Action A',
      description: 'First dynamic action',
      count: 0,
    },
    {
      id: this.nextId++,
      hotkey: 'Shift+B',
      label: 'Action B',
      description: 'Second dynamic action',
      count: 0,
    },
    {
      id: this.nextId++,
      hotkey: 'Shift+C',
      label: 'Action C',
      description: 'Third dynamic action',
      count: 0,
    },
  ])
  newHotkey = signal('')
  newLabel = signal('')
  newDescription = signal('')

  // Registrations viewer
  readonly registrations = injectHotkeyRegistrations()

  constructor() {
    // Basic multi-hotkey with meta
    injectHotkeys([
      {
        hotkey: 'Shift+S',
        callback: (_e, { hotkey }) => {
          this.saveCount.update((c) => c + 1)
          this.log.update((l) => [`${hotkey} pressed`, ...l].slice(0, 20))
        },
        options: {
          meta: { name: 'Save', description: 'Save the current document' },
        },
      },
      {
        hotkey: 'Shift+U',
        callback: (_e, { hotkey }) => {
          this.undoCount.update((c) => c + 1)
          this.log.update((l) => [`${hotkey} pressed`, ...l].slice(0, 20))
        },
        options: {
          meta: { name: 'Undo', description: 'Undo the last action' },
        },
      },
      {
        hotkey: 'Shift+R',
        callback: (_e, { hotkey }) => {
          this.redoCount.update((c) => c + 1)
          this.log.update((l) => [`${hotkey} pressed`, ...l].slice(0, 20))
        },
        options: {
          meta: { name: 'Redo', description: 'Redo the last undone action' },
        },
      },
    ])

    // Common options with per-hotkey overrides and meta
    injectHotkeys(
      [
        {
          hotkey: 'Alt+J',
          callback: () => this.counts.update((c) => ({ ...c, a: c.a + 1 })),
          options: {
            meta: {
              name: 'Action A',
              description: 'First action (respects toggle)',
            },
          },
        },
        {
          hotkey: 'Alt+K',
          callback: () => this.counts.update((c) => ({ ...c, b: c.b + 1 })),
          options: {
            meta: {
              name: 'Action B',
              description: 'Second action (respects toggle)',
            },
          },
        },
        {
          hotkey: 'Alt+L',
          callback: () => this.counts.update((c) => ({ ...c, c: c.c + 1 })),
          options: {
            enabled: true,
            meta: {
              name: 'Action C',
              description: 'Always-on action (overrides toggle)',
            },
          },
        },
      ],
      () => ({ enabled: this.commonEnabled() }),
    )

    // Dynamic hotkeys with meta
    injectHotkeys(() =>
      this.shortcuts().map(
        (s): InjectHotkeyDefinition => ({
          hotkey: s.hotkey as Hotkey,
          callback: () => {
            this.shortcuts.update((prev) =>
              prev.map((item) =>
                item.id === s.id ? { ...item, count: item.count + 1 } : item,
              ),
            )
          },
          options: {
            meta: { name: s.label, description: s.description },
          },
        }),
      ),
    )
  }

  toggleCommon() {
    this.commonEnabled.update((e) => !e)
  }

  addShortcut() {
    const trimmed = this.newHotkey().trim()
    if (!trimmed || !this.newLabel().trim()) return
    this.shortcuts.update((prev) => [
      ...prev,
      {
        id: this.nextId++,
        hotkey: trimmed,
        label: this.newLabel().trim(),
        description: this.newDescription().trim(),
        count: 0,
      },
    ])
    this.newHotkey.set('')
    this.newLabel.set('')
    this.newDescription.set('')
  }

  removeShortcut(id: number) {
    this.shortcuts.update((prev) => prev.filter((s) => s.id !== id))
  }

  onNewHotkeyInput(event: Event) {
    this.newHotkey.set((event.target as HTMLInputElement).value)
  }

  onNewLabelInput(event: Event) {
    this.newLabel.set((event.target as HTMLInputElement).value)
  }

  onNewDescriptionInput(event: Event) {
    this.newDescription.set((event.target as HTMLInputElement).value)
  }

  onInputKeydown(event: KeyboardEvent) {
    if (event.key === 'Enter') this.addShortcut()
  }

  formatSeq(seq: Array<string>): string {
    return seq.map((h) => formatForDisplay(h as Hotkey)).join(' ')
  }
}