diff --git a/packages/design-system/src/components/N8nFormBox/FormBox.vue b/packages/design-system/src/components/N8nFormBox/FormBox.vue index ed01219eb914e..7881c4b5c9ba4 100644 --- a/packages/design-system/src/components/N8nFormBox/FormBox.vue +++ b/packages/design-system/src/components/N8nFormBox/FormBox.vue @@ -44,7 +44,7 @@ import N8nHeading from '../N8nHeading'; import N8nLink from '../N8nLink'; import N8nButton from '../N8nButton'; import type { IFormInput } from 'n8n-design-system/types'; -import { createEventBus } from '../../utils'; +import { createFormEventBus } from '../../utils'; interface FormBoxProps { title?: string; @@ -67,7 +67,7 @@ withDefaults(defineProps(), { redirectLink: '', }); -const formBus = createEventBus(); +const formBus = createFormEventBus(); const emit = defineEmits<{ submit: [value: { [key: string]: Value }]; update: [value: { name: string; value: Value }]; diff --git a/packages/design-system/src/components/N8nFormInputs/FormInputs.vue b/packages/design-system/src/components/N8nFormInputs/FormInputs.vue index 620778cd2cb9d..977a10a88fe16 100644 --- a/packages/design-system/src/components/N8nFormInputs/FormInputs.vue +++ b/packages/design-system/src/components/N8nFormInputs/FormInputs.vue @@ -3,12 +3,12 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'; import N8nFormInput from '../N8nFormInput'; import type { IFormInput } from '../../types'; import ResizeObserver from '../ResizeObserver'; -import type { EventBus } from '../../utils'; -import { createEventBus } from '../../utils'; +import type { FormEventBus } from '../../utils'; +import { createFormEventBus } from '../../utils'; export type FormInputsProps = { inputs?: IFormInput[]; - eventBus?: EventBus; + eventBus?: FormEventBus; columnView?: boolean; verticalSpacing?: '' | 'xs' | 's' | 'm' | 'l' | 'xl'; teleported?: boolean; @@ -19,7 +19,7 @@ type Value = string | number | boolean | null | undefined; const props = withDefaults(defineProps(), { inputs: () => [], - eventBus: createEventBus, + eventBus: createFormEventBus, columnView: false, verticalSpacing: '', teleported: true, diff --git a/packages/design-system/src/utils/__tests__/event-bus.spec.ts b/packages/design-system/src/utils/__tests__/event-bus.spec.ts index e403b61008f4e..2fb0735404ee0 100644 --- a/packages/design-system/src/utils/__tests__/event-bus.spec.ts +++ b/packages/design-system/src/utils/__tests__/event-bus.spec.ts @@ -14,18 +14,30 @@ describe('createEventBus()', () => { expect(handler).toHaveBeenCalled(); }); + }); - it('should return unregister fn', () => { + describe('once()', () => { + it('should register event handler', () => { const handler = vi.fn(); const eventName = 'test'; - const unregister = eventBus.on(eventName, handler); + eventBus.once(eventName, handler); + + eventBus.emit(eventName, {}); + + expect(handler).toHaveBeenCalled(); + }); + + it('should unregister event handler after first call', () => { + const handler = vi.fn(); + const eventName = 'test'; - unregister(); + eventBus.once(eventName, handler); + eventBus.emit(eventName, {}); eventBus.emit(eventName, {}); - expect(handler).not.toHaveBeenCalled(); + expect(handler).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/design-system/src/utils/event-bus.ts b/packages/design-system/src/utils/event-bus.ts index f6ffc597f5fd6..eb08228b47afe 100644 --- a/packages/design-system/src/utils/event-bus.ts +++ b/packages/design-system/src/utils/event-bus.ts @@ -1,51 +1,84 @@ // eslint-disable-next-line @typescript-eslint/ban-types export type CallbackFn = Function; -export type UnregisterFn = () => void; -export interface EventBus { - on: (eventName: string, fn: CallbackFn) => UnregisterFn; - off: (eventName: string, fn: CallbackFn) => void; - emit: (eventName: string, event?: T) => void; -} +type Payloads = { + [E in keyof ListenerMap]: unknown; +}; -export function createEventBus(): EventBus { - const handlers = new Map(); +type Listener = (payload: Payload) => void; - function off(eventName: string, fn: CallbackFn) { - const eventFns = handlers.get(eventName); +// TODO: Fix all usages of `createEventBus` and convert `any` to `unknown` +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface EventBus = Record> { + on( + eventName: EventName, + fn: Listener, + ): void; - if (eventFns) { - eventFns.splice(eventFns.indexOf(fn) >>> 0, 1); - } - } + once( + eventName: EventName, + fn: Listener, + ): void; - function on(eventName: string, fn: CallbackFn): UnregisterFn { - let eventFns = handlers.get(eventName); + off( + eventName: EventName, + fn: Listener, + ): void; - if (!eventFns) { - eventFns = [fn]; - } else { - eventFns.push(fn); - } + emit( + eventName: EventName, + event?: ListenerMap[EventName], + ): void; +} - handlers.set(eventName, eventFns); +/** + * Creates an event bus with the given listener map. + * + * @example + * ```ts + * const eventBus = createEventBus<{ + * 'user-logged-in': { username: string }; + * 'user-logged-out': never; + * }>(); + */ +export function createEventBus< + // TODO: Fix all usages of `createEventBus` and convert `any` to `unknown` + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ListenerMap extends Payloads = Record, +>(): EventBus { + const handlers = new Map(); - return () => off(eventName, fn); - } + return { + on(eventName, fn) { + let eventFns = handlers.get(eventName); + if (!eventFns) { + eventFns = [fn]; + } else { + eventFns.push(fn); + } + handlers.set(eventName, eventFns); + }, - function emit(eventName: string, event?: T) { - const eventFns = handlers.get(eventName); + once(eventName, fn) { + const handler: typeof fn = (payload) => { + this.off(eventName, handler); + fn(payload); + }; + this.on(eventName, handler); + }, - if (eventFns) { - eventFns.slice().forEach(async (handler) => { - await handler(event); - }); - } - } + off(eventName, fn) { + const eventFns = handlers.get(eventName); + if (eventFns) { + eventFns.splice(eventFns.indexOf(fn) >>> 0, 1); + } + }, - return { - on, - off, - emit, + emit(eventName, event) { + const eventFns = handlers.get(eventName); + if (eventFns) { + eventFns.slice().forEach((handler) => handler(event)); + } + }, }; } diff --git a/packages/design-system/src/utils/form-event-bus.ts b/packages/design-system/src/utils/form-event-bus.ts new file mode 100644 index 0000000000000..5b518c8a4233d --- /dev/null +++ b/packages/design-system/src/utils/form-event-bus.ts @@ -0,0 +1,12 @@ +import { createEventBus } from './event-bus'; + +export interface FormEventBusEvents { + submit: never; +} + +export type FormEventBus = ReturnType; + +/** + * Creates a new event bus to be used with the `FormInputs` component. + */ +export const createFormEventBus = createEventBus; diff --git a/packages/design-system/src/utils/index.ts b/packages/design-system/src/utils/index.ts index cdc1b91597fa1..3f4ed339f0f73 100644 --- a/packages/design-system/src/utils/index.ts +++ b/packages/design-system/src/utils/index.ts @@ -1,4 +1,5 @@ export * from './event-bus'; +export * from './form-event-bus'; export * from './markdown'; export * from './typeguards'; export * from './uid';