Skip to content

Comments

feat: Add Angular Adapter#31

Open
benjavicente wants to merge 11 commits intoTanStack:mainfrom
benjavicente:main
Open

feat: Add Angular Adapter#31
benjavicente wants to merge 11 commits intoTanStack:mainfrom
benjavicente:main

Conversation

@benjavicente
Copy link

🎯 Changes

This PR adds an Angular Adapter + examples for each main function exported.

There is not devtools package yet, since there is no createAngularPlugin in @tanstack/devtools-utils.

I had to dowgrade TS from 5.9 to 5.8 since it's the latest TS version Angular 19 supports. It supports angular 19 and up since 19 is the oldest with LTS.

A lot of the code was AI asisted, but I reviewed and refractored the adapter to match as closelly as the solid adapter.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

}

export const HOTKEYS_INJECTION_TOKEN = new InjectionToken<HotkeysContextValue>(
'HOTKEYS_INJECTION_TOKEN',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend to add a providedIn: 'root'and a factory for the default value in the second argument to revisit the injectHotkeysContext implementation and always return a value (so optional flag will not be needed anymore)

const defaultOptions = injectDefaultHotkeysOptions()
const manager = getHotkeyManager()

let registration: HotkeyRegistrationHandle | null = null

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This onCleanup function lets you register a callback that is invoked before the next run of the effect begins, or when the effect is destroyed.

As the angular docs says, onCleanup is also called before the next run of the effect begins. I think we can declare that variable inside the effect as a constant and just call the unregister method inside the onCleanup. It allows us to just move that checks like:

   
    if (registration?.isActive) { // <-- into onCleanup
      registration.unregister()
      registration = null // <-- probably not needed
    }

Comment on lines 7 to 18
export interface AngularHotkeyRecorder {
/** Whether recording is currently active */
isRecording: () => boolean
/** The currently recorded hotkey (for live preview) */
recordedHotkey: () => Hotkey | null
/** Start recording a new hotkey */
startRecording: () => void
/** Stop recording (same as cancel) */
stopRecording: () => void
/** Cancel recording without saving */
cancelRecording: () => void
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export interface AngularHotkeyRecorder {
/** Whether recording is currently active */
isRecording: () => boolean
/** The currently recorded hotkey (for live preview) */
recordedHotkey: () => Hotkey | null
/** Start recording a new hotkey */
startRecording: () => void
/** Stop recording (same as cancel) */
stopRecording: () => void
/** Cancel recording without saving */
cancelRecording: () => void
}
export interface AngularHotkeyRecorder {
/** Whether recording is currently active */
readonly isRecording: () => boolean
/** The currently recorded hotkey (for live preview) */
readonly recordedHotkey: () => Hotkey | null
/** Start recording a new hotkey */
readonly startRecording: () => void
/** Stop recording (same as cancel) */
readonly stopRecording: () => void
/** Cancel recording without saving */
readonly cancelRecording: () => void
}

)

export function provideHotkeys(defaultOptions?: HotkeysProviderOptions): {
provide: typeof HOTKEYS_INJECTION_TOKEN

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return type of provideHotkeys could be "StaticProvider"

Comment on lines 56 to 70
const resolvedOptions = typeof options === 'function' ? options() : options
const mergedOptions = {
...defaultOptions.hotkeyRecorder,
...resolvedOptions,
} as HotkeyRecorderOptions

// Create recorder once synchronously (matches React's useRef pattern)
const recorder = new HotkeyRecorder(mergedOptions)

// Subscribe to recorder state using useStore (same pattern as useHotkeyRecorder)
const isRecording = injectStore(recorder.store, (state) => state.isRecording)
const recordedHotkey = injectStore(
recorder.store,
(state) => state.recordedHotkey,
)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only blocker before making this PR ready is that the options are called on initialization, causing an error when the options have a value from a input signal.

We could subscribe to the store manually, but it would be better to have a clear solution for this problem in the Angular Store adapter.

I started a discussion here: TanStack/store#284

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@theVedanta Did you have to solve something like this recently in TanStack Angular Pacer?

Copy link
Author

@benjavicente benjavicente Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference is that pacer doesn't look like it supports reactive options in Angular or Solid. For example, React's useRateLimiter has options as an object, and in every render, the options are updated. But both Solid and Angular also only accept an object, so they don't allow reactive options. Ideally, both should be able to accept a function, to update the options like in React, and match how we are doing options it in hotkeys and in query. I will open an issue in pacer.

@KevinVandy
Copy link
Member

KevinVandy commented Feb 21, 2026

Hi, I'm also following along with this PR. Consider merging main back in to resolve some larger conflicts.

The sequence was rewritten to a much more in-depth sequence-manager today with full parity with the hot-key manager.

@KevinVandy KevinVandy changed the title Add Angular Adapter feat: Add Angular Adapter Feb 23, 2026
@benjavicente
Copy link
Author

Rebased the branch with the sequence-manager 🏃

For the input signal issue: I added a temporary approach in injectHotkeyRecorder to create the store (HotkeyRecorder) post initialization in a stable signal. We could swap that to a standardized solution when we land something in TanStack/store#284 (discussion) or TanStack/store#285 (alternative API PR).

@benjavicente benjavicente marked this pull request as ready for review February 24, 2026 02:11
@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 24, 2026

Open in StackBlitz

@tanstack/angular-hotkeys

npm i https://pkg.pr.new/TanStack/hotkeys/@tanstack/angular-hotkeys@31

@tanstack/hotkeys

npm i https://pkg.pr.new/TanStack/hotkeys/@tanstack/hotkeys@31

@tanstack/hotkeys-devtools

npm i https://pkg.pr.new/TanStack/hotkeys/@tanstack/hotkeys-devtools@31

@tanstack/preact-hotkeys

npm i https://pkg.pr.new/TanStack/hotkeys/@tanstack/preact-hotkeys@31

@tanstack/preact-hotkeys-devtools

npm i https://pkg.pr.new/TanStack/hotkeys/@tanstack/preact-hotkeys-devtools@31

@tanstack/react-hotkeys

npm i https://pkg.pr.new/TanStack/hotkeys/@tanstack/react-hotkeys@31

@tanstack/react-hotkeys-devtools

npm i https://pkg.pr.new/TanStack/hotkeys/@tanstack/react-hotkeys-devtools@31

@tanstack/solid-hotkeys

npm i https://pkg.pr.new/TanStack/hotkeys/@tanstack/solid-hotkeys@31

@tanstack/solid-hotkeys-devtools

npm i https://pkg.pr.new/TanStack/hotkeys/@tanstack/solid-hotkeys-devtools@31

commit: 4e0575d

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants