Skip to content

Commit

Permalink
fix(web-components): fix conditional renders
Browse files Browse the repository at this point in the history
  • Loading branch information
aralroca committed Oct 31, 2023
1 parent 308be98 commit 1c574cb
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 82 deletions.
101 changes: 75 additions & 26 deletions src/utils/brisa-element/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@ describe("utils", () => {
function Counter({ name, children }: Props, { state, h }: any) {
const count = state(0);

return [
h("p", { class: () => (count.value % 2 === 0 ? "even" : "") }, [
h("button", { onClick: () => count.value++ }, "+"),
h("span", {}, () => ` ${name.value} ${count.value} `),
h("button", { onClick: () => count.value-- }, "-"),
children,
]),
];
return h("p", { class: () => (count.value % 2 === 0 ? "even" : "") }, [
["button", { onClick: () => count.value++ }, "+"],
["span", {}, () => ` ${name.value} ${count.value} `],
["button", { onClick: () => count.value-- }, "-"],
children,
]);
}

customElements.define(
Expand Down Expand Up @@ -63,15 +61,21 @@ describe("utils", () => {
it("should work with conditional rendering inside span node", () => {
type Props = { name: { value: string }; children: Node };
function ConditionalRender({ name, children }: Props, { h }: any) {
return [
h("h2", {}, [
h("b", {}, () => "Hello " + name.value),
h("span", {}, () =>
name.value === "Barbara" ? [h("b", {}, "!! 🥳")] : "🥴",
),
]),
return h(null, {}, [
[
"h2",
{},
[
["b", {}, () => "Hello " + name.value],
[
"span",
{},
() => (name.value === "Barbara" ? ["b", {}, "!! 🥳"] : "🥴"),
],
],
],
children,
];
]);
}

customElements.define(
Expand Down Expand Up @@ -109,15 +113,61 @@ describe("utils", () => {
it("should work with conditional rendering inside text node", () => {
type Props = { name: { value: string }; children: Node };
function ConditionalRender({ name, children }: Props, { h }: any) {
return [
h("h2", {}, [
h("b", {}, () => "Hello " + name.value),
h(null, {}, () =>
name.value === "Barbara" ? [h("b", {}, "!! 🥳")] : "🥴",
),
]),
return h("h2", {}, [
["b", {}, () => "Hello " + name.value],
[
null,
{},
() => (name.value === "Barbara" ? ["b", {}, "!! 🥳"] : "🥴"),
],
children,
]);
}

customElements.define(
"conditional-render",
brisaElement(ConditionalRender as any, ["name"]),
);

document.body.innerHTML = `
<conditional-render name="Aral">
<span>test</span>
</conditional-render>
`;

const conditionalRender = document.querySelector(
"conditional-render",
) as HTMLElement;

expect(conditionalRender?.shadowRoot?.innerHTML).toBe(
"<h2><b>Hello Aral</b>🥴<slot></slot></h2>",
);

conditionalRender.setAttribute("name", "Barbara");

expect(conditionalRender?.shadowRoot?.innerHTML).toBe(
"<h2><b>Hello Barbara</b><b>!! 🥳</b><slot></slot></h2>",
);
});

it("should work with conditional rendering inside text node and fragment", () => {
type Props = { name: { value: string }; children: Node };
function ConditionalRender({ name, children }: Props, { h }: any) {
return h(null, {}, [
[
"h2",
{},
[
["b", {}, () => "Hello " + name.value],
[
null,
{},
() => (name.value === "Barbara" ? ["b", {}, "!! 🥳"] : "🥴"),
],
],
],
children,
];
]);
}

customElements.define(
Expand All @@ -142,8 +192,7 @@ describe("utils", () => {
conditionalRender.setAttribute("name", "Barbara");

expect(conditionalRender?.shadowRoot?.innerHTML).toBe(
"",
// '<h2><b>Hello Barbara</b><b>!! 🥳</b></h2><slot></slot>',
"<h2><b>Hello Barbara</b><b>!! 🥳</b></h2><slot></slot>",
);
});
});
Expand Down
139 changes: 83 additions & 56 deletions src/utils/brisa-element/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ type Render = (
props: Record<string, unknown>,
ctx: ReturnType<typeof signals> & {
css(strings: string[], ...values: string[]): void;
h(tagName: string, attributes: Attr, children: unknown): Node;
h(tagName: string, attributes: Attr, children: unknown): void;
},
) => Node[];
type Children = unknown[] | string | (() => Children);

const c = document.createElement.bind(document);
const f = document.createDocumentFragment.bind(document);
const isPrimary = (el: unknown) =>
typeof el === "string" || typeof el === "number" || typeof el === "boolean";

export default function brisaElement(
render: Render,
Expand All @@ -25,66 +27,94 @@ export default function brisaElement(

connectedCallback() {
const ctx = signals();
const shadowRoot = this.attachShadow({ mode: "open" });

this.p = {};

for (let attr of observedAttributes) {
this.p[attr] = ctx.state(this.getAttribute(attr));
}

const shadowRoot = this.attachShadow({ mode: "open" });
const els = render(
{ children: c("slot"), ...this.p },
{
...ctx,
h(tagName: string, attributes: Attr, children: unknown) {
const fragment = f();
let el: Node = tagName ? c(tagName) : f();

// Handle attributes
Object.entries(attributes).forEach(([key, value]) => {
const isEvent = key.startsWith("on");

if (isEvent) {
el.addEventListener(
key.slice(2).toLowerCase(),
value as EventListener,
);
} else if (!isEvent && typeof value === "function") {
ctx.effect(() =>
(el as HTMLElement).setAttribute(key, value()),
);
} else {
(el as HTMLElement).setAttribute(key, value as string);
}
});

if (!children) return el;

// Handle children
if (Array.isArray(children)) {
children.forEach((child) => fragment.appendChild(child));
el.appendChild(fragment);
} else if (typeof children === "string") {
el.textContent = children;
} else if (typeof children === "function") {
ctx.effect(() => {
const child = children();

if (Array.isArray(child)) {
child.forEach((c) => fragment.appendChild(c));

(el as HTMLElement).innerHTML = "";
el.appendChild(fragment);
} else {
el.textContent = child;
}
});
function hyperScript(
tagName: string | null,
attributes: Attr,
children: Children,
parent: HTMLElement | DocumentFragment = shadowRoot,
) {
const el = (tagName ? c(tagName) : parent) as HTMLElement;

if (tagName) parent.appendChild(el);

// Handle attributes
for (let [key, value] of Object.entries(attributes)) {
const isEvent = key.startsWith("on");

if (isEvent) {
el.addEventListener(
key.slice(2).toLowerCase(),
value as EventListener,
);
} else if (!isEvent && typeof value === "function") {
ctx.effect(() => el.setAttribute(key, (value as () => string)()));
} else {
(el as HTMLElement).setAttribute(key, value as string);
}
}

if (!children) return;

// Handle children
if (children === "slot") {
el.appendChild(c("slot"));
} else if (Array.isArray(children)) {
if (Array.isArray(children[0])) {
children.forEach((child) => hyperScript(null, {}, child, el));
} else {
hyperScript(...(children as [string, Attr, Children]), el);
}
} else if (typeof children === "function") {
let lastNodes: ChildNode[] | undefined;

const insertOrUpdate = (e: ChildNode | DocumentFragment) => {
if (lastNodes) {
el.insertBefore(e, lastNodes[0]);
lastNodes.forEach((node) => node?.remove());
} else el.appendChild(e);
};

ctx.effect(() => {
const child = children();

if (isPrimary(child)) {
const textNode = document.createTextNode(child as string);

insertOrUpdate(textNode);

lastNodes = [textNode];
} else {
el.appendChild(children as Node);
const [t, a, c] = child as [string, Attr, Children];
let currentElNodes = Array.from(el.childNodes);
const fragment = document.createDocumentFragment();

hyperScript(t, a, c, fragment);

insertOrUpdate(fragment);

lastNodes = Array.from(el.childNodes).filter(
(node) => !currentElNodes.includes(node),
);
}
});
} else {
el.appendChild(document.createTextNode(children));
}
}

return el;
},
render(
{ children: "slot", ...this.p },
{
...ctx,
h: hyperScript,
// Handle CSS
css(strings: string[], ...values: string[]) {
const style = c("style");
Expand All @@ -93,9 +123,6 @@ export default function brisaElement(
},
},
);
const fragment = f();
els.forEach((el) => fragment.appendChild(el));
shadowRoot.appendChild(fragment);
}

attributeChangedCallback(
Expand Down

0 comments on commit 1c574cb

Please sign in to comment.