Skip to content

Commit

Permalink
Merge pull request #17 from allevo/feat/allow-replace-html-element
Browse files Browse the repository at this point in the history
Fix #14 and fix #8
  • Loading branch information
allevo authored May 24, 2024
2 parents f551486 + 6a4fd01 commit 7e58019
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 108 deletions.
72 changes: 49 additions & 23 deletions packages/seqflow-js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export interface SeqflowFunctionContext {
replaceChild: (
key: string,
newChild: () => JSX.Element | Promise<JSX.Element>,
) => void;
) => void | Promise<void>;
/**
* The DOM element where this component is mounted
*/
Expand Down Expand Up @@ -154,7 +154,7 @@ function startComponent<T extends { children?: ChildenType[]; key?: string }>(
componentOption: T | undefined,
) {
const componentName = component.name;
parentContext.app.log.debug?.({
parentContext.app.log.debug({
message: "startComponent",
data: { componentOption, componentName },
});
Expand Down Expand Up @@ -214,10 +214,10 @@ function startComponent<T extends { children?: ChildenType[]; key?: string }>(
this: SeqflowFunctionContext,
key: string,
newChild: () => JSX.Element | Promise<JSX.Element>,
) {
): void | Promise<void> {
const oldChildIndex = componentChildren.findIndex((c) => c.key === key);
if (oldChildIndex < 0) {
this.app.log.error?.({
this.app.log.error({
message: "replaceChild: wrapper not found",
data: { key, newChild },
});
Expand All @@ -235,12 +235,25 @@ function startComponent<T extends { children?: ChildenType[]; key?: string }>(

const { el: wrapper } = oldChild;

for (const otherChild of componentChildren) {
// If we replace a child which contains another child,
// we have to abort the other child also
if (oldChild.el.contains(otherChild.el)) {
this.app.log.debug({
message: "replaceChild: wrapper contains other child",
data: { parent: key, child: otherChild.key },
});
this.abortController.signal.dispatchEvent(
new Event(`abort-component-${otherChild.key}`),
);
}
}

const a = newChild();
if (a instanceof Promise) {
a.then((child) => {
return a.then((child) => {
wrapper.replaceWith(child as Node);
});
return;
}

wrapper.replaceWith(a as Node);
Expand All @@ -267,7 +280,7 @@ function startComponent<T extends { children?: ChildenType[]; key?: string }>(
return domainEvent(b.t, eventTarget);
},
navigationEvent(): EventAsyncGenerator<NavigationEvent> {
this.app.log.debug?.({
this.app.log.debug({
message: "navigationEvent",
data: {
componentName,
Expand All @@ -285,7 +298,7 @@ function startComponent<T extends { children?: ChildenType[]; key?: string }>(
this: SeqflowFunctionContext,
{ children }: { children?: ChildenType[] },
): DocumentFragment {
this.app.log.debug?.({
this.app.log.debug({
message: "createDOMFragment",
data: { children },
});
Expand Down Expand Up @@ -314,7 +327,7 @@ function startComponent<T extends { children?: ChildenType[]; key?: string }>(
continue;
}

this.app.log.error?.({
this.app.log.error({
message: "Unsupported child type. Implement me",
data: { child, children },
});
Expand All @@ -329,7 +342,7 @@ function startComponent<T extends { children?: ChildenType[]; key?: string }>(
options?: { [key: string]: string },
...children: ChildenType[]
): Node {
this.app.log.debug?.({
this.app.log.debug({
message: "createDOMElement",
data: { tagName, options, children },
});
Expand Down Expand Up @@ -359,6 +372,15 @@ function startComponent<T extends { children?: ChildenType[]; key?: string }>(
}
continue;
}

// keep the key for the componentChildren array
if (key === "key") {
componentChildren.push({
key: options[key],
el,
});
}

el.setAttribute(key, options[key]);
}

Expand All @@ -380,7 +402,7 @@ function startComponent<T extends { children?: ChildenType[]; key?: string }>(
continue;
}

this.app.log.error?.({
this.app.log.error({
message: "Unsupported child type. Implement me",
data: {
child,
Expand All @@ -399,19 +421,22 @@ function startComponent<T extends { children?: ChildenType[]; key?: string }>(
return this.createDOMFragment({ children });
}

const wrapper = document.createElement("div");
if (options?.key) {
componentChildren.push({
key: options.key,
el: wrapper,
});
const opt = options || {};
if (!opt?.key) {
opt.key = Math.random().toString();
}
if (options?.wrapperClass) {
wrapper.classList.add(options.wrapperClass);

const wrapper = document.createElement("div");
componentChildren.push({
key: opt.key,
el: wrapper,
});
if (opt?.wrapperClass) {
wrapper.classList.add(opt.wrapperClass);
}

startComponent(childContext, wrapper, tagName, {
...options,
...opt,
children,
});
return wrapper;
Expand All @@ -430,7 +455,7 @@ function startComponent<T extends { children?: ChildenType[]; key?: string }>(
if (v.then !== undefined) {
v.then(
() => {
parentContext.app.log.debug?.({
parentContext.app.log.debug({
message: "Component rendering ended",
data: {
componentOption,
Expand All @@ -439,7 +464,7 @@ function startComponent<T extends { children?: ChildenType[]; key?: string }>(
});
},
(e) => {
parentContext.app.log.error?.({
parentContext.app.log.error({
message: "Component throws an error",
data: {
componentOption,
Expand Down Expand Up @@ -484,7 +509,7 @@ export function start<
router: seqflowConfig.router,
};
seqflowConfig.router.getEventTarget().addEventListener("navigation", (ev) => {
appContext.log.info?.({
appContext.log.info({
message: "navigate",
data: {
path: (ev as NavigationEvent).path,
Expand Down Expand Up @@ -653,6 +678,7 @@ declare global {
> &
ARG<{
style?: Partial<CSSStyleDeclaration> | string;
key?: string;
}>;
};
interface IntrinsicElements extends Foo {}
Expand Down
84 changes: 0 additions & 84 deletions packages/seqflow-js/tests/render.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,90 +154,6 @@ test("re-render plain nested jsx", async () => {
await screen.findByText(/third/i);
});

test("replace a child", async () => {
const texts = ["first", "second", "third"];
async function Button(this: SeqflowFunctionContext, data: { text: string }) {
this.renderSync(<button type="button">{data.text}</button>);
}
async function App(this: SeqflowFunctionContext) {
const text = texts.shift();
if (!text) {
throw new Error("no text");
}
this.renderSync(
<div id="1">
<Button key="aa" text={text} />
</div>,
);

const events = this.waitEvents(this.domEvent("click", { el: this._el }));
for await (const _ of events) {
const nextText = texts.shift();
if (!nextText) {
break;
}
this.replaceChild("aa", async () => <Button key="aa" text={nextText} />);
}
}

start(document.body, App, undefined, {});
const firstButton = await screen.findByText(/first/i);
firstButton.click();
const secondButton = await screen.findByText(/second/i);
secondButton.click();
const thirdButton = await screen.findByText(/third/i);
thirdButton.click();

await waitFor(() =>
expect(document.body.innerHTML).toBe(
'<div id="1"><div><button type="button">third</button></div></div>',
),
);
});

test("replace a child - stop listen", async () => {
let counter = 0;
async function Button1(this: SeqflowFunctionContext) {
this.renderSync(<button type="button">button1</button>);
const events = this.waitEvents(this.domEvent("click", { el: this._el }));
for await (const _ of events) {
counter++;
}
}
async function Button2(this: SeqflowFunctionContext) {
this.renderSync(<button type="button">button2</button>);
}
async function App(this: SeqflowFunctionContext) {
this.renderSync(
<div>
<Button1 key="aa" />
</div>,
);

const events = this.waitEvents(this.domEvent("click", { el: this._el }));
for await (const _ of events) {
this.replaceChild("aa", async () => <Button2 key="aa" />);
break;
}
}

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

// Click button1, just to see if it's working
const firstButton = await screen.findByText(/button1/i);
firstButton.click();
await waitFor(() => expect(counter).toBe(1));

// Click button2, is displayed
await screen.findByText(/button2/i);

// If I click button1 again, it should not increment the counter
firstButton.click();
firstButton.click();
await new Promise((resolve) => setTimeout(resolve, 100));
await waitFor(() => expect(counter).toBe(1));
});

test("class & className", async () => {
async function App(this: SeqflowFunctionContext) {
this.renderSync(
Expand Down
Loading

0 comments on commit 7e58019

Please sign in to comment.