Skip to content

Commit 53dede7

Browse files
committed
feat: support input signals in injectHotkeyRecorder
1 parent efe94b6 commit 53dede7

1 file changed

Lines changed: 54 additions & 18 deletions

File tree

packages/angular-hotkeys/src/injectHotkeyRecorder.ts

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
import { injectStore } from '@tanstack/angular-store'
2-
import { DestroyRef, effect, inject } from '@angular/core'
1+
import {
2+
DestroyRef,
3+
computed,
4+
effect,
5+
inject,
6+
linkedSignal,
7+
untracked,
8+
} from '@angular/core'
39
import { HotkeyRecorder } from '@tanstack/hotkeys'
410
import { injectDefaultHotkeysOptions } from './hotkeys-provider'
11+
import type { Atom, ReadonlyAtom } from '@tanstack/angular-store'
12+
import type { Signal } from '@angular/core'
513
import type { Hotkey, HotkeyRecorderOptions } from '@tanstack/hotkeys'
614

715
export interface AngularHotkeyRecorder {
@@ -53,40 +61,68 @@ export function injectHotkeyRecorder(
5361
const defaultOptions = injectDefaultHotkeysOptions()
5462
const destroyRef = inject(DestroyRef)
5563

56-
const resolvedOptions = typeof options === 'function' ? options() : options
57-
const mergedOptions = {
58-
...defaultOptions.hotkeyRecorder,
59-
...resolvedOptions,
60-
} as HotkeyRecorderOptions
64+
// Stable signal to lazy initialize the recorder
65+
const recorderSignal = computed(() =>
66+
untracked(() => {
67+
const resolvedOptions =
68+
typeof options === 'function' ? options() : options
6169

62-
// Create recorder once synchronously (matches React's useRef pattern)
63-
const recorder = new HotkeyRecorder(mergedOptions)
70+
const mergedOptions = {
71+
...defaultOptions.hotkeyRecorder,
72+
...resolvedOptions,
73+
} as HotkeyRecorderOptions
6474

65-
// Subscribe to recorder state using useStore (same pattern as useHotkeyRecorder)
66-
const isRecording = injectStore(recorder.store, (state) => state.isRecording)
67-
const recordedHotkey = injectStore(
68-
recorder.store,
75+
return new HotkeyRecorder(mergedOptions)
76+
}),
77+
)
78+
79+
// Subscribe to recorder state
80+
const recorderStore = computed(() => untracked(() => recorderSignal().store))
81+
const isRecording = injectLazyStore(
82+
recorderStore,
83+
(state) => state.isRecording,
84+
)
85+
const recordedHotkey = injectLazyStore(
86+
recorderStore,
6987
(state) => state.recordedHotkey,
7088
)
7189

7290
// Sync options on every effect run (matches React's sync on render)
7391
effect(() => {
7492
const resolved = typeof options === 'function' ? options() : options
75-
recorder.setOptions({
93+
recorderSignal().setOptions({
7694
...defaultOptions.hotkeyRecorder,
7795
...resolved,
7896
} as HotkeyRecorderOptions)
7997
})
8098

8199
destroyRef.onDestroy(() => {
82-
recorder.destroy()
100+
recorderSignal().destroy()
83101
})
84102

85103
return {
86104
isRecording,
87105
recordedHotkey,
88-
startRecording: () => recorder.start(),
89-
stopRecording: () => recorder.stop(),
90-
cancelRecording: () => recorder.cancel(),
106+
startRecording: () => recorderSignal().start(),
107+
stopRecording: () => recorderSignal().stop(),
108+
cancelRecording: () => recorderSignal().cancel(),
91109
}
92110
}
111+
112+
function injectLazyStore<TState, TSelected = NoInfer<TState>>(
113+
storeSignal: Signal<Atom<TState> | ReadonlyAtom<TState>>,
114+
selector: (state: NoInfer<TState>) => TSelected,
115+
): Signal<TSelected> {
116+
const slice = linkedSignal(() => selector(storeSignal().get()))
117+
118+
effect((onCleanup) => {
119+
const currentStore = storeSignal()
120+
slice.set(selector(currentStore.get()))
121+
const { unsubscribe } = currentStore.subscribe((s) => {
122+
slice.set(selector(s))
123+
})
124+
onCleanup(() => unsubscribe())
125+
})
126+
127+
return slice.asReadonly()
128+
}

0 commit comments

Comments
 (0)