refactor(editor): Add typed event bus (no-changelog) (#10367)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Tomi Turtiainen 2024-08-13 15:11:28 +03:00 committed by GitHub
parent bfa7075950
commit b2e0f33959
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 110 additions and 52 deletions

View file

@ -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<FormBoxProps>(), {
redirectLink: '',
});
const formBus = createEventBus();
const formBus = createFormEventBus();
const emit = defineEmits<{
submit: [value: { [key: string]: Value }];
update: [value: { name: string; value: Value }];

View file

@ -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<FormInputsProps>(), {
inputs: () => [],
eventBus: createEventBus,
eventBus: createFormEventBus,
columnView: false,
verticalSpacing: '',
teleported: true,

View file

@ -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);
unregister();
eventBus.once(eventName, handler);
eventBus.emit(eventName, {});
expect(handler).not.toHaveBeenCalled();
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);
});
});

View file

@ -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: <T = Event>(eventName: string, event?: T) => void;
type Payloads<ListenerMap> = {
[E in keyof ListenerMap]: unknown;
};
type Listener<Payload> = (payload: Payload) => void;
// TODO: Fix all usages of `createEventBus` and convert `any` to `unknown`
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface EventBus<ListenerMap extends Payloads<ListenerMap> = Record<string, any>> {
on<EventName extends keyof ListenerMap & string>(
eventName: EventName,
fn: Listener<ListenerMap[EventName]>,
): void;
once<EventName extends keyof ListenerMap & string>(
eventName: EventName,
fn: Listener<ListenerMap[EventName]>,
): void;
off<EventName extends keyof ListenerMap & string>(
eventName: EventName,
fn: Listener<ListenerMap[EventName]>,
): void;
emit<EventName extends keyof ListenerMap & string>(
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<ListenerMap> = Record<string, any>,
>(): EventBus<ListenerMap> {
const handlers = new Map<string, CallbackFn[]>();
function off(eventName: string, fn: CallbackFn) {
const eventFns = handlers.get(eventName);
if (eventFns) {
eventFns.splice(eventFns.indexOf(fn) >>> 0, 1);
}
}
function on(eventName: string, fn: CallbackFn): UnregisterFn {
let eventFns = handlers.get(eventName);
if (!eventFns) {
eventFns = [fn];
} else {
eventFns.push(fn);
}
handlers.set(eventName, eventFns);
return () => off(eventName, fn);
}
function emit<T = Event>(eventName: string, event?: T) {
const eventFns = handlers.get(eventName);
if (eventFns) {
eventFns.slice().forEach(async (handler) => {
await handler(event);
});
}
}
return {
on,
off,
emit,
on(eventName, fn) {
let eventFns = handlers.get(eventName);
if (!eventFns) {
eventFns = [fn];
} else {
eventFns.push(fn);
}
handlers.set(eventName, eventFns);
},
once(eventName, fn) {
const handler: typeof fn = (payload) => {
this.off(eventName, handler);
fn(payload);
};
this.on(eventName, handler);
},
off(eventName, fn) {
const eventFns = handlers.get(eventName);
if (eventFns) {
eventFns.splice(eventFns.indexOf(fn) >>> 0, 1);
}
},
emit(eventName, event) {
const eventFns = handlers.get(eventName);
if (eventFns) {
eventFns.slice().forEach((handler) => handler(event));
}
},
};
}

View file

@ -0,0 +1,12 @@
import { createEventBus } from './event-bus';
export interface FormEventBusEvents {
submit: never;
}
export type FormEventBus = ReturnType<typeof createFormEventBus>;
/**
* Creates a new event bus to be used with the `FormInputs` component.
*/
export const createFormEventBus = createEventBus<FormEventBusEvents>;

View file

@ -1,4 +1,5 @@
export * from './event-bus';
export * from './form-event-bus';
export * from './markdown';
export * from './typeguards';
export * from './uid';