From 1d00301b19ebf302bbd9ad8bedcc0e6b92563f89 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 13 Aug 2024 10:11:09 +0300 Subject: [PATCH] refactor(editor): Add typed event bus --- .../src/components/N8nFormBox/FormBox.vue | 4 +- .../components/N8nFormInputs/FormInputs.vue | 8 +-- .../src/utils/__tests__/event-bus.spec.ts | 25 ++++++++ packages/design-system/src/utils/event-bus.ts | 58 +++++++++++++++++-- .../design-system/src/utils/form-event-bus.ts | 12 ++++ packages/design-system/src/utils/index.ts | 1 + 6 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 packages/design-system/src/utils/form-event-bus.ts 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..016209a316767 100644 --- a/packages/design-system/src/utils/__tests__/event-bus.spec.ts +++ b/packages/design-system/src/utils/__tests__/event-bus.spec.ts @@ -29,6 +29,31 @@ describe('createEventBus()', () => { }); }); + describe('once()', () => { + it('should register event handler', () => { + const handler = vi.fn(); + const eventName = 'test'; + + 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'; + + eventBus.once(eventName, handler); + + eventBus.emit(eventName, {}); + eventBus.emit(eventName, {}); + + expect(handler).toHaveBeenCalledTimes(1); + }); + }); + describe('off()', () => { it('should register event handler', () => { const handler = vi.fn(); diff --git a/packages/design-system/src/utils/event-bus.ts b/packages/design-system/src/utils/event-bus.ts index f6ffc597f5fd6..1b7cb10182a48 100644 --- a/packages/design-system/src/utils/event-bus.ts +++ b/packages/design-system/src/utils/event-bus.ts @@ -2,13 +2,51 @@ 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; +export type Listener = (payload: Payload) => void; + +export type Payloads = { + [E in keyof ListenerMap]: unknown; +}; + +// 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, + ) => UnregisterFn; + + once: ( + eventName: EventName, + fn: Listener, + ) => UnregisterFn; + + off: ( + eventName: EventName, + fn: Listener, + ) => void; + + emit: ( + eventName: EventName, + event?: ListenerMap[EventName], + ) => void; } -export function createEventBus(): EventBus { +/** + * 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(); function off(eventName: string, fn: CallbackFn) { @@ -33,6 +71,15 @@ export function createEventBus(): EventBus { return () => off(eventName, fn); } + function once(eventName: string, fn: CallbackFn): UnregisterFn { + const unregister = on(eventName, (...args: unknown[]) => { + unregister(); + fn(...args); + }); + + return unregister; + } + function emit(eventName: string, event?: T) { const eventFns = handlers.get(eventName); @@ -45,6 +92,7 @@ export function createEventBus(): EventBus { return { on, + once, off, emit, }; 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';