Angular Example: InjectHotkeySequenceRecorder

import { Component, signal } from '@angular/core'
import {
  formatForDisplay,
  injectHeldKeys,
  injectHotkeyRegistrations,
  injectHotkeySequenceRecorder,
  injectHotkeySequences,
} from '@tanstack/angular-hotkeys'
import type {
  HotkeySequence,
  InjectHotkeySequenceDefinition,
} from '@tanstack/angular-hotkeys'

interface Shortcut {
  id: string
  name: string
  description: string
  sequence: HotkeySequence
}

let nextId = 0
function createId(): string {
  return `shortcut_${++nextId}`
}

const INITIAL_SHORTCUTS: Array<Shortcut> = [
  {
    id: createId(),
    name: 'Save',
    description: 'Save the current document',
    sequence: ['Mod+S'],
  },
  {
    id: createId(),
    name: 'Open (gg)',
    description: 'Open the file browser',
    sequence: ['G', 'G'],
  },
  {
    id: createId(),
    name: 'New (dd)',
    description: 'Create a new document',
    sequence: ['D', 'D'],
  },
  {
    id: createId(),
    name: 'Close',
    description: 'Close the current tab',
    sequence: ['Mod+Shift+K'],
  },
  {
    id: createId(),
    name: 'Undo (yy)',
    description: 'Undo the last action',
    sequence: ['Y', 'Y'],
  },
  {
    id: createId(),
    name: 'Redo',
    description: 'Redo the last undone action',
    sequence: ['Mod+Shift+G'],
  },
]

@Component({
  selector: 'app-root',
  standalone: true,
  templateUrl: './app.component.html',
  styleUrl: './app.component.css',
})
export class AppComponent {
  shortcuts = signal<Array<Shortcut>>(INITIAL_SHORTCUTS)
  editingId = signal<string | null>(null)
  draftName = signal('')
  draftDescription = signal('')

  formatForDisplay = formatForDisplay
  heldKeys = injectHeldKeys()

  private readonly recorder = injectHotkeySequenceRecorder({
    onRecord: (sequence: HotkeySequence) => {
      const id = this.editingId()
      if (id) {
        this.shortcuts.update((prev) =>
          prev.map((s) =>
            s.id === id
              ? {
                  ...s,
                  sequence,
                  name: this.draftName(),
                  description: this.draftDescription(),
                }
              : s,
          ),
        )
        this.editingId.set(null)
      }
    },
    onCancel: () => {
      const id = this.editingId()
      if (id) {
        this.shortcuts.update((prev) => {
          const shortcut = prev.find((s) => s.id === id)
          if (shortcut && shortcut.sequence.length === 0) {
            return prev.filter((s) => s.id !== id)
          }
          return prev
        })
      }
      this.editingId.set(null)
    },
    onClear: () => {
      const id = this.editingId()
      if (id) {
        this.shortcuts.update((prev) =>
          prev.map((s) =>
            s.id === id
              ? {
                  ...s,
                  sequence: [],
                  name: this.draftName(),
                  description: this.draftDescription(),
                }
              : s,
          ),
        )
        this.editingId.set(null)
      }
    },
  })

  readonly isRecording = this.recorder.isRecording
  readonly recorderSteps = this.recorder.steps

  readonly registrations = injectHotkeyRegistrations()

  constructor() {
    // Register all sequences dynamically with meta
    injectHotkeySequences(() =>
      this.shortcuts()
        .filter((s) => s.sequence.length > 0)
        .map(
          (s): InjectHotkeySequenceDefinition => ({
            sequence: s.sequence,
            callback: () => {
              console.log(`${s.name} triggered:`, s.sequence)
            },
            options: {
              enabled: !this.recorder.isRecording(),
              meta: {
                name: s.name,
                description: s.description,
              },
            },
          }),
        ),
    )
  }

  handleEdit(id: string): void {
    const shortcut = this.shortcuts().find((s) => s.id === id)
    if (!shortcut) return
    this.editingId.set(id)
    this.draftName.set(shortcut.name)
    this.draftDescription.set(shortcut.description)
    this.recorder.startRecording()
  }

  handleSaveEditing(): void {
    const id = this.editingId()
    if (id) {
      this.shortcuts.update((prev) =>
        prev.map((s) =>
          s.id === id
            ? {
                ...s,
                name: this.draftName(),
                description: this.draftDescription(),
              }
            : s,
        ),
      )
      this.recorder.stopRecording()
      this.editingId.set(null)
    }
  }

  handleCancel(): void {
    this.recorder.cancelRecording()
  }

  handleDelete(id: string): void {
    this.shortcuts.update((prev) => prev.filter((s) => s.id !== id))
  }

  handleCreateNew(): void {
    const newShortcut: Shortcut = {
      id: createId(),
      name: '',
      description: '',
      sequence: [],
    }
    this.shortcuts.update((prev) => [...prev, newShortcut])
    this.editingId.set(newShortcut.id)
    this.draftName.set('')
    this.draftDescription.set('')
    this.recorder.startRecording()
  }

  onDraftNameInput(event: Event): void {
    this.draftName.set((event.target as HTMLInputElement).value)
  }

  onDraftDescriptionInput(event: Event): void {
    this.draftDescription.set((event.target as HTMLInputElement).value)
  }

  formatSeq(seq: HotkeySequence): string {
    return seq.map((h) => formatForDisplay(h)).join(' ')
  }

  recordingStepsLabel(): string {
    return this.recorder
      .steps()
      .map((h) => formatForDisplay(h))
      .join(' ')
  }
}
import { Component, signal } from '@angular/core'
import {
  formatForDisplay,
  injectHeldKeys,
  injectHotkeyRegistrations,
  injectHotkeySequenceRecorder,
  injectHotkeySequences,
} from '@tanstack/angular-hotkeys'
import type {
  HotkeySequence,
  InjectHotkeySequenceDefinition,
} from '@tanstack/angular-hotkeys'

interface Shortcut {
  id: string
  name: string
  description: string
  sequence: HotkeySequence
}

let nextId = 0
function createId(): string {
  return `shortcut_${++nextId}`
}

const INITIAL_SHORTCUTS: Array<Shortcut> = [
  {
    id: createId(),
    name: 'Save',
    description: 'Save the current document',
    sequence: ['Mod+S'],
  },
  {
    id: createId(),
    name: 'Open (gg)',
    description: 'Open the file browser',
    sequence: ['G', 'G'],
  },
  {
    id: createId(),
    name: 'New (dd)',
    description: 'Create a new document',
    sequence: ['D', 'D'],
  },
  {
    id: createId(),
    name: 'Close',
    description: 'Close the current tab',
    sequence: ['Mod+Shift+K'],
  },
  {
    id: createId(),
    name: 'Undo (yy)',
    description: 'Undo the last action',
    sequence: ['Y', 'Y'],
  },
  {
    id: createId(),
    name: 'Redo',
    description: 'Redo the last undone action',
    sequence: ['Mod+Shift+G'],
  },
]

@Component({
  selector: 'app-root',
  standalone: true,
  templateUrl: './app.component.html',
  styleUrl: './app.component.css',
})
export class AppComponent {
  shortcuts = signal<Array<Shortcut>>(INITIAL_SHORTCUTS)
  editingId = signal<string | null>(null)
  draftName = signal('')
  draftDescription = signal('')

  formatForDisplay = formatForDisplay
  heldKeys = injectHeldKeys()

  private readonly recorder = injectHotkeySequenceRecorder({
    onRecord: (sequence: HotkeySequence) => {
      const id = this.editingId()
      if (id) {
        this.shortcuts.update((prev) =>
          prev.map((s) =>
            s.id === id
              ? {
                  ...s,
                  sequence,
                  name: this.draftName(),
                  description: this.draftDescription(),
                }
              : s,
          ),
        )
        this.editingId.set(null)
      }
    },
    onCancel: () => {
      const id = this.editingId()
      if (id) {
        this.shortcuts.update((prev) => {
          const shortcut = prev.find((s) => s.id === id)
          if (shortcut && shortcut.sequence.length === 0) {
            return prev.filter((s) => s.id !== id)
          }
          return prev
        })
      }
      this.editingId.set(null)
    },
    onClear: () => {
      const id = this.editingId()
      if (id) {
        this.shortcuts.update((prev) =>
          prev.map((s) =>
            s.id === id
              ? {
                  ...s,
                  sequence: [],
                  name: this.draftName(),
                  description: this.draftDescription(),
                }
              : s,
          ),
        )
        this.editingId.set(null)
      }
    },
  })

  readonly isRecording = this.recorder.isRecording
  readonly recorderSteps = this.recorder.steps

  readonly registrations = injectHotkeyRegistrations()

  constructor() {
    // Register all sequences dynamically with meta
    injectHotkeySequences(() =>
      this.shortcuts()
        .filter((s) => s.sequence.length > 0)
        .map(
          (s): InjectHotkeySequenceDefinition => ({
            sequence: s.sequence,
            callback: () => {
              console.log(`${s.name} triggered:`, s.sequence)
            },
            options: {
              enabled: !this.recorder.isRecording(),
              meta: {
                name: s.name,
                description: s.description,
              },
            },
          }),
        ),
    )
  }

  handleEdit(id: string): void {
    const shortcut = this.shortcuts().find((s) => s.id === id)
    if (!shortcut) return
    this.editingId.set(id)
    this.draftName.set(shortcut.name)
    this.draftDescription.set(shortcut.description)
    this.recorder.startRecording()
  }

  handleSaveEditing(): void {
    const id = this.editingId()
    if (id) {
      this.shortcuts.update((prev) =>
        prev.map((s) =>
          s.id === id
            ? {
                ...s,
                name: this.draftName(),
                description: this.draftDescription(),
              }
            : s,
        ),
      )
      this.recorder.stopRecording()
      this.editingId.set(null)
    }
  }

  handleCancel(): void {
    this.recorder.cancelRecording()
  }

  handleDelete(id: string): void {
    this.shortcuts.update((prev) => prev.filter((s) => s.id !== id))
  }

  handleCreateNew(): void {
    const newShortcut: Shortcut = {
      id: createId(),
      name: '',
      description: '',
      sequence: [],
    }
    this.shortcuts.update((prev) => [...prev, newShortcut])
    this.editingId.set(newShortcut.id)
    this.draftName.set('')
    this.draftDescription.set('')
    this.recorder.startRecording()
  }

  onDraftNameInput(event: Event): void {
    this.draftName.set((event.target as HTMLInputElement).value)
  }

  onDraftDescriptionInput(event: Event): void {
    this.draftDescription.set((event.target as HTMLInputElement).value)
  }

  formatSeq(seq: HotkeySequence): string {
    return seq.map((h) => formatForDisplay(h)).join(' ')
  }

  recordingStepsLabel(): string {
    return this.recorder
      .steps()
      .map((h) => formatForDisplay(h))
      .join(' ')
  }
}