Skip to content

Commit

Permalink
Merge pull request #19 from allevo/feat/key-for-dom-event
Browse files Browse the repository at this point in the history
Feat/key for dom event
  • Loading branch information
allevo committed May 24, 2024
2 parents 7e58019 + 113dfc0 commit 3dfbb79
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 16 deletions.
70 changes: 60 additions & 10 deletions packages/seqflow-js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,12 @@ export interface SeqflowFunctionContext {
*/
domEvent: <K extends keyof HTMLElementEventMap>(
eventType: K,
options: {
el: HTMLElement;
preventDefault?: boolean;
},
options:
| {
el: HTMLElement;
preventDefault?: boolean;
}
| string,
) => EventAsyncGenerator<HTMLElementEventMap[K]>;
/**
* Wait for a domain event to happen
Expand All @@ -92,6 +94,20 @@ export interface SeqflowFunctionContext {
* Wait for a navigation event to happen
*/
navigationEvent(): EventAsyncGenerator<NavigationEvent>;
/**
* Get a child component by its key
*
* @param key the key of the child to get
* @returns the child component
*/
getChild(key: string): HTMLElement;
/**
* Get a child component by its key
*
* @param key the key of the child to get
* @returns the child component or null if not found
*/
findChild(key: string): HTMLElement | null;
/**
* Replace a child component with a new one
*
Expand Down Expand Up @@ -210,6 +226,24 @@ function startComponent<T extends { children?: ChildenType[]; key?: string }>(
yield ev;
}
},
getChild(this: SeqflowFunctionContext, key: string): HTMLElement {
const child = this.findChild(key);
if (!child) {
this.app.log.error({
message: "getChild: wrapper not found",
data: { key },
});
throw new Error("getChild: wrapper not found");
}
return child;
},
findChild(this: SeqflowFunctionContext, key: string): HTMLElement | null {
const child = componentChildren.find((c) => c.key === key);
if (child) {
return child.el;
}
return null;
},
replaceChild(
this: SeqflowFunctionContext,
key: string,
Expand Down Expand Up @@ -261,14 +295,24 @@ function startComponent<T extends { children?: ChildenType[]; key?: string }>(
domEvent<K extends keyof HTMLElementEventMap>(
this: SeqflowFunctionContext,
eventType: K,
options: {
el: HTMLElement;
preventDefault?: boolean;
},
options:
| {
el: HTMLElement;
preventDefault?: boolean;
}
| string,
): EventAsyncGenerator<HTMLElementEventMap[K]> {
let el: HTMLElement;
let preventDefault = false;
if (typeof options === "string") {
el = this.getChild(options);
} else {
el = options.el;
preventDefault = options.preventDefault ?? false;
}
return domEvent(eventType, {
el: options.el,
preventDefault: options.preventDefault ?? false,
el,
preventDefault,
});
},
domainEvent<BEE extends typeof DomainsPackage.DomainEvent<unknown>>(
Expand Down Expand Up @@ -527,6 +571,12 @@ export function start<
waitEvents: async function* <A>(): AsyncGenerator<A> {
throw new Error("waitEvents is not supported in the main context");
},
getChild: () => {
throw new Error("getChild is not supported in the main context");
},
findChild: () => {
throw new Error("findChild is not supported in the main context");
},
replaceChild: () => {
throw new Error("replaceChild is not supported in the main context");
},
Expand Down
46 changes: 46 additions & 0 deletions packages/seqflow-js/tests/domEvent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,49 @@ test("child component can be used to listen", async () => {

await waitFor(() => expect(counter).toBe(3));
});

test("uses `key`", async () => {
let counter = 0;

async function Button(this: SeqflowFunctionContext, data: { text: string }) {
this.renderSync(<button type="button">{data.text}</button>);
}

async function App(this: SeqflowFunctionContext) {
const counterDiv = <div>{counter}</div>;
this.renderSync(
<>
<div>
<Button key="decrement-button" text="Decrement" />
<Button key="increment-button" text="Increment" />
</div>
{counterDiv}
</>,
);

const events = this.waitEvents(
this.domEvent("click", "decrement-button"),
this.domEvent("click", "increment-button"),
);
for await (const ev of events) {
if (!(ev.target instanceof HTMLElement)) {
continue;
}
if (this.getChild("increment-button").contains(ev.target)) {
counter++;
} else if (this.getChild("decrement-button").contains(ev.target)) {
counter--;
}

counterDiv.textContent = `${counter}`;
}
}

start(document.body, App, undefined, {});

(await screen.findByText(/increment/i)).click();
(await screen.findByText(/increment/i)).click();
(await screen.findByText(/increment/i)).click();

await waitFor(() => expect(counter).toBe(3));
});
24 changes: 24 additions & 0 deletions packages/seqflow-js/tests/render.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,27 @@ test("style as CSSStyleDeclaration", async () => {

expect(document.body.innerHTML).toBe('<div style="display: none;"></div>');
});

test("getChild", async () => {
async function App(this: SeqflowFunctionContext) {
this.renderSync(
<>
<div key="key1" />
<div key="key2">
<div key="key3" />
</div>
</>,
);

this.getChild("key1").innerHTML = "key1";
this.getChild("key3").appendChild(document.createTextNode("key4"));
this.getChild("key3").innerHTML = "key3";
}
start(document.body, App, undefined, {});

await new Promise((resolve) => setTimeout(resolve, 100));

expect(document.body.innerHTML).toBe(
'<div key="key1">key1</div><div key="key2"><div key="key3">key3</div></div>',
);
});
12 changes: 7 additions & 5 deletions packages/seqflow-js/tests/replaceChild.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ test("replace a child", async () => {
if (!nextText) {
break;
}
await this.replaceChild("button", async () => <Button key="button" text={nextText} />);
await this.replaceChild("button", async () => (
<Button key="button" text={nextText} />
));
}
}

Expand Down Expand Up @@ -144,7 +146,7 @@ test("replace a child - html element", async () => {
// This test is to make sure the `replaceChild` function will stop all components
// if a parent component is replaced
test("replace a child - should unmount child components", async () => {
let counter = 0
let counter = 0;
async function Button(this: SeqflowFunctionContext, data: { text: string }) {
this.renderSync(<button type="button">{data.text}</button>);

Expand All @@ -157,7 +159,7 @@ test("replace a child - should unmount child components", async () => {
const button = <Button text="click me" />;
this.renderSync(
<div>
<div key="content" >{button}</div>
<div key="content">{button}</div>
</div>,
);

Expand All @@ -178,7 +180,7 @@ test("replace a child - should unmount child components", async () => {
expect(document.body.innerHTML).toBe(
'<div><div key="content"><div><button type="button">click me</button></div></div></div>',
),
)
);
expect(counter).toBe(0);

button.click();
Expand All @@ -187,7 +189,7 @@ test("replace a child - should unmount child components", async () => {
expect(document.body.innerHTML).toBe(
'<div><div key="content">No button here</div></div>',
),
)
);
expect(counter).toBe(1);

for (let i = 0; i < 10; i++) {
Expand Down
6 changes: 5 additions & 1 deletion packages/website/src/pages/ApiReference.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,15 @@ This interface is the parameter object that is passed to the component function.
- `abortController: AbortController` - The instance of the `AborteController` linked to the component.
- `renderSync: (html: string | JSX.Element) => void` - This method renders the HTML string or JSX element into the component mounting DOM element.
- `waitEvents: <Fns extends EventAsyncGenerator<GetYieldType<Fns[number]>>[]>(...fns: Fns) => AsyncGenerator<GetYieldType<Fns[number]>>` - The method to wait for multiple events to be triggered.
- `domEvent: <K extends keyof HTMLElementEventMap>(eventType: K, options) => EventAsyncGenerator<HTMLElementEventMap[K]>` - The method to create an async generator that waits for the DOM event to be triggered. The `options` object can be used to customize the event listener and to prevent the default behavior.
- `domEvent: <K extends keyof HTMLElementEventMap>(eventType: K, options) => EventAsyncGenerator<HTMLElementEventMap[K]>` - The method to create an async generator that waits for the DOM event to be triggered. The `options` can:
- a child key.
- be an object used to customize the event listener and to prevent the default behavior.
- `domainEvent<BEE extends typeof DomainsPackage.DomainEvent<unknown>>(domainEventClass: BEE): EventAsyncGenerator<InstanceType<BEE>>` - The method to create an async generator that waits for the domain event to be triggered.
- `navigationEvent(): EventAsyncGenerator<NavigationEvent>` - The method to create an async generator that waits for the navigation event to be triggered.
- `replaceChild: (key: string, newChild: () => JSX.Element | Promise<JSX.Element>) => void | Promise<void>` - The method to replace a child component with the same `key` with a new component.
- `_el: HTMLElement` - The DOM element where the component is mounted.
- `findChild: (key: string) => HTMLElement | null` - The method to find a child component by the key. Returns `null` if the child component is not found.
- `getChild: (key: string) => HTMLElement` - The method to get a child component by the key. Thows an error if the child component is not found.
- `createDOMElement` - The method to create a DOM element. Don't use this method directly.
- `createDOMFragment` - The method to create a Fragment DOM element. Don't use this method directly.

Expand Down

0 comments on commit 3dfbb79

Please sign in to comment.