Skip to main content

Integrating with Other Frameworks

Use duck-vim's framework-agnostic core with Vue, Svelte, Angular, or any other framework.

The pattern

Create a Registry and KeyHandler during initialization

Call handler.attach() when the component/app mounts

Call handler.detach() when it unmounts

Register bindings via registry.register() and store the handle for cleanup

Loading diagram...


Vue 3 (Composition API)

composables/useKeyBind.ts
import { onMounted, onUnmounted } from 'vue'
import { Registry, KeyHandler } from '@gentleduck/vim/command'
import type { Command, KeyBindOptions } from '@gentleduck/vim/command'
 
const registry = new Registry()
const handler = new KeyHandler(registry, 600)
let attached = false
 
export function useKeyBind(binding: string, execute: () => void, options?: KeyBindOptions) {
  let handle: ReturnType<typeof registry.register> | null = null
 
  onMounted(() => {
    if (!attached) {
      handler.attach(document)
      attached = true
    }
 
    handle = registry.register(binding, { name: binding, execute }, options)
  })
 
  onUnmounted(() => {
    handle?.unregister()
  })
}
composables/useKeyBind.ts
import { onMounted, onUnmounted } from 'vue'
import { Registry, KeyHandler } from '@gentleduck/vim/command'
import type { Command, KeyBindOptions } from '@gentleduck/vim/command'
 
const registry = new Registry()
const handler = new KeyHandler(registry, 600)
let attached = false
 
export function useKeyBind(binding: string, execute: () => void, options?: KeyBindOptions) {
  let handle: ReturnType<typeof registry.register> | null = null
 
  onMounted(() => {
    if (!attached) {
      handler.attach(document)
      attached = true
    }
 
    handle = registry.register(binding, { name: binding, execute }, options)
  })
 
  onUnmounted(() => {
    handle?.unregister()
  })
}

Usage in a component:

<script setup>
import { useKeyBind } from './composables/useKeyBind'
 
useKeyBind('ctrl+k', () => {
  console.log('Palette opened!')
}, { preventDefault: true })
</script>
<script setup>
import { useKeyBind } from './composables/useKeyBind'
 
useKeyBind('ctrl+k', () => {
  console.log('Palette opened!')
}, { preventDefault: true })
</script>

Svelte

actions/keybind.ts
import { Registry, KeyHandler } from '@gentleduck/vim/command'
 
const registry = new Registry()
const handler = new KeyHandler(registry, 600)
handler.attach(document)
 
export function keybind(node: HTMLElement, params: { binding: string; handler: () => void }) {
  const handle = registry.register(params.binding, {
    name: params.binding,
    execute: params.handler,
  })
 
  return {
    destroy() {
      handle.unregister()
    },
  }
}
actions/keybind.ts
import { Registry, KeyHandler } from '@gentleduck/vim/command'
 
const registry = new Registry()
const handler = new KeyHandler(registry, 600)
handler.attach(document)
 
export function keybind(node: HTMLElement, params: { binding: string; handler: () => void }) {
  const handle = registry.register(params.binding, {
    name: params.binding,
    execute: params.handler,
  })
 
  return {
    destroy() {
      handle.unregister()
    },
  }
}

Usage:

<script>
  import { keybind } from './actions/keybind'
</script>
 
<div use:keybind={{ binding: 'ctrl+k', handler: () => console.log('palette') }}>
  App content
</div>
<script>
  import { keybind } from './actions/keybind'
</script>
 
<div use:keybind={{ binding: 'ctrl+k', handler: () => console.log('palette') }}>
  App content
</div>

Angular

keybind.service.ts
import { Injectable, OnDestroy } from '@angular/core'
import { Registry, KeyHandler } from '@gentleduck/vim/command'
import type { Command, KeyBindOptions, RegistrationHandle } from '@gentleduck/vim/command'
 
@Injectable({ providedIn: 'root' })
export class KeyBindService implements OnDestroy {
  private registry = new Registry()
  private handler = new KeyHandler(this.registry, 600)
 
  constructor() {
    this.handler.attach(document)
  }
 
  register(binding: string, command: Command, options?: KeyBindOptions): RegistrationHandle {
    return this.registry.register(binding, command, options)
  }
 
  ngOnDestroy() {
    this.handler.detach(document)
    this.registry.clear()
  }
}
keybind.service.ts
import { Injectable, OnDestroy } from '@angular/core'
import { Registry, KeyHandler } from '@gentleduck/vim/command'
import type { Command, KeyBindOptions, RegistrationHandle } from '@gentleduck/vim/command'
 
@Injectable({ providedIn: 'root' })
export class KeyBindService implements OnDestroy {
  private registry = new Registry()
  private handler = new KeyHandler(this.registry, 600)
 
  constructor() {
    this.handler.attach(document)
  }
 
  register(binding: string, command: Command, options?: KeyBindOptions): RegistrationHandle {
    return this.registry.register(binding, command, options)
  }
 
  ngOnDestroy() {
    this.handler.detach(document)
    this.registry.clear()
  }
}

Usage in a component:

@Component({ ... })
export class AppComponent implements OnInit, OnDestroy {
  private handle?: RegistrationHandle
 
  constructor(private keybind: KeyBindService) {}
 
  ngOnInit() {
    this.handle = this.keybind.register('ctrl+k', {
      name: 'Open Palette',
      execute: () => this.openPalette(),
    }, { preventDefault: true })
  }
 
  ngOnDestroy() {
    this.handle?.unregister()
  }
}
@Component({ ... })
export class AppComponent implements OnInit, OnDestroy {
  private handle?: RegistrationHandle
 
  constructor(private keybind: KeyBindService) {}
 
  ngOnInit() {
    this.handle = this.keybind.register('ctrl+k', {
      name: 'Open Palette',
      execute: () => this.openPalette(),
    }, { preventDefault: true })
  }
 
  ngOnDestroy() {
    this.handle?.unregister()
  }
}

Vanilla (module pattern)

import { Registry, KeyHandler } from '@gentleduck/vim/command'
 
const registry = new Registry()
const handler = new KeyHandler(registry, 600)
 
export function init() {
  handler.attach(document)
}
 
export function registerShortcut(binding: string, name: string, execute: () => void) {
  return registry.register(binding, { name, execute })
}
 
export function destroy() {
  handler.detach(document)
  registry.clear()
}
import { Registry, KeyHandler } from '@gentleduck/vim/command'
 
const registry = new Registry()
const handler = new KeyHandler(registry, 600)
 
export function init() {
  handler.attach(document)
}
 
export function registerShortcut(binding: string, name: string, execute: () => void) {
  return registry.register(binding, { name, execute })
}
 
export function destroy() {
  handler.detach(document)
  registry.clear()
}

Key principles for any integration