|
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' |
3 | 9 | import { HotkeyRecorder } from '@tanstack/hotkeys' |
4 | 10 | import { injectDefaultHotkeysOptions } from './hotkeys-provider' |
| 11 | +import type { Atom, ReadonlyAtom } from '@tanstack/angular-store' |
| 12 | +import type { Signal } from '@angular/core' |
5 | 13 | import type { Hotkey, HotkeyRecorderOptions } from '@tanstack/hotkeys' |
6 | 14 |
|
7 | 15 | export interface AngularHotkeyRecorder { |
@@ -53,40 +61,68 @@ export function injectHotkeyRecorder( |
53 | 61 | const defaultOptions = injectDefaultHotkeysOptions() |
54 | 62 | const destroyRef = inject(DestroyRef) |
55 | 63 |
|
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 |
61 | 69 |
|
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 |
64 | 74 |
|
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, |
69 | 87 | (state) => state.recordedHotkey, |
70 | 88 | ) |
71 | 89 |
|
72 | 90 | // Sync options on every effect run (matches React's sync on render) |
73 | 91 | effect(() => { |
74 | 92 | const resolved = typeof options === 'function' ? options() : options |
75 | | - recorder.setOptions({ |
| 93 | + recorderSignal().setOptions({ |
76 | 94 | ...defaultOptions.hotkeyRecorder, |
77 | 95 | ...resolved, |
78 | 96 | } as HotkeyRecorderOptions) |
79 | 97 | }) |
80 | 98 |
|
81 | 99 | destroyRef.onDestroy(() => { |
82 | | - recorder.destroy() |
| 100 | + recorderSignal().destroy() |
83 | 101 | }) |
84 | 102 |
|
85 | 103 | return { |
86 | 104 | isRecording, |
87 | 105 | 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(), |
91 | 109 | } |
92 | 110 | } |
| 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