Skip to content

Commit

Permalink
Merge pull request #6978 from QwikDev/v2-fix-inline-component-rendering
Browse files Browse the repository at this point in the history
fix(render): fix inline component rendering
  • Loading branch information
shairez authored Oct 15, 2024
2 parents 48b5156 + 2497485 commit 103581c
Show file tree
Hide file tree
Showing 4 changed files with 254 additions and 32 deletions.
80 changes: 52 additions & 28 deletions packages/qwik/src/core/client/vnode-diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1032,35 +1032,45 @@ export const vnode_diff = (
}
jsxValue.children != null && descendContentToProject(jsxValue.children, host);
} else {
// Inline Component
vnode_insertBefore(
journal,
vParent as VirtualVNode,
(vNewNode = vnode_newVirtual()),
vCurrent && getInsertBefore()
);
isDev && vnode_setProp(vNewNode, DEBUG_TYPE, VirtualType.InlineComponent);
vnode_setProp(vNewNode, ELEMENT_PROPS, jsxValue.props);

host = vNewNode;
let component$Host: VNode | null = host;
// Find the closest component host which has `OnRender` prop.
while (
component$Host &&
(vnode_isVirtualVNode(component$Host)
? vnode_getProp(component$Host, OnRenderProp, null) === null
: true)
) {
component$Host = vnode_getParent(component$Host);
const lookupKey = jsxValue.key;
const vNodeLookupKey = getKey(host);
const lookupKeysAreEqual = lookupKey === vNodeLookupKey;

if (!lookupKeysAreEqual) {
// See if we already have this inline component later on.
vNewNode = retrieveChildWithKey(null, lookupKey);
if (vNewNode) {
// We found the inline component, move it up.
vnode_insertBefore(journal, vParent as VirtualVNode, vNewNode, vCurrent);
} else {
// We did not find the inline component, create it.
insertNewInlineComponent();
}
host = vNewNode as VirtualVNode;
}

if (host) {
let componentHost: VNode | null = host;
// Find the closest component host which has `OnRender` prop. This is need for subscriptions context.
while (
componentHost &&
(vnode_isVirtualVNode(componentHost)
? vnode_getProp(componentHost, OnRenderProp, null) === null
: true)
) {
componentHost = vnode_getParent(componentHost);
}

const jsxOutput = executeComponent(
container,
host,
(componentHost || container.rootVNode) as HostElement,
component as OnRenderFn<unknown>,
jsxValue.props
);

asyncQueue.push(jsxOutput, host);
}
const jsxOutput = executeComponent(
container,
host,
(component$Host || container.rootVNode) as HostElement,
component as OnRenderFn<unknown>,
jsxValue.props
);
asyncQueue.push(jsxOutput, host);
}
}

Expand All @@ -1084,6 +1094,20 @@ export const vnode_diff = (
container.setHostProp(vNewNode, ELEMENT_KEY, jsxValue.key);
}

function insertNewInlineComponent() {
vnode_insertBefore(
journal,
vParent as VirtualVNode,
(vNewNode = vnode_newVirtual()),
vCurrent && getInsertBefore()
);
isDev && vnode_setProp(vNewNode, DEBUG_TYPE, VirtualType.InlineComponent);
vnode_setProp(vNewNode, ELEMENT_PROPS, jsxValue.props);
if (jsxValue.key) {
vnode_setProp(vNewNode, ELEMENT_KEY, jsxValue.key);
}
}

function expectText(text: string) {
if (vCurrent !== null) {
const type = vnode_getType(vCurrent);
Expand Down
6 changes: 3 additions & 3 deletions packages/qwik/src/core/ssr/ssr-render-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import type { JSXOutput } from '../shared/jsx/types/jsx-node';

export const applyInlineComponent = (
ssr: SSRContainer,
component$Host: ISsrNode,
component: OnRenderFn<any>,
componentHost: ISsrNode,
inlineComponentFunction: OnRenderFn<any>,
jsx: JSXNode
) => {
const host = ssr.getLastNode();
return executeComponent(ssr, host, component$Host, component, jsx.props);
return executeComponent(ssr, host, componentHost, inlineComponentFunction, jsx.props);
};

export const applyQwikComponentBody = (
Expand Down
7 changes: 6 additions & 1 deletion packages/qwik/src/core/ssr/ssr-render-jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,12 @@ function processJSXNode(
isPromise(jsxOutput) && enqueue(Promise);
enqueue(new ParentComponentData(compStyleComponentId, componentFrame));
} else {
ssr.openFragment(isDev ? [DEBUG_TYPE, VirtualType.InlineComponent] : EMPTY_ARRAY);
const inlineComponentProps = [ELEMENT_KEY, jsx.key];
ssr.openFragment(
isDev
? [DEBUG_TYPE, VirtualType.InlineComponent, ...inlineComponentProps]
: inlineComponentProps
);
enqueue(ssr.closeFragment);
const component = ssr.getComponentFrame(0)!;
const jsxOutput = applyInlineComponent(
Expand Down
193 changes: 193 additions & 0 deletions packages/qwik/src/core/tests/inline-component.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,4 +226,197 @@ describe.each([
</Component>
);
});

it('should render array of inline components inside normal component', async () => {
const Cmp = component$(() => {
const items = useStore(['qwik', 'foo', 'bar']);

const Item = (props: { name: string }) => {
return <div>{props.name}</div>;
};

return (
<footer>
<button onClick$={() => items.sort()}></button>
{items.map((item, key) => (
<Item name={item} key={key} />
))}
</footer>
);
});

const { vNode, document } = await render(<Cmp />, { debug });

expect(vNode).toMatchVDOM(
<Component>
<footer>
<button></button>
<InlineComponent>
<div>qwik</div>
</InlineComponent>
<InlineComponent>
<div>foo</div>
</InlineComponent>
<InlineComponent>
<div>bar</div>
</InlineComponent>
</footer>
</Component>
);

await trigger(document.body, 'button', 'click');

expect(vNode).toMatchVDOM(
<Component>
<footer>
<button></button>
<InlineComponent>
<div>bar</div>
</InlineComponent>
<InlineComponent>
<div>foo</div>
</InlineComponent>
<InlineComponent>
<div>qwik</div>
</InlineComponent>
</footer>
</Component>
);
});

it('should conditionally render different inline component', async () => {
const Cmp = component$(() => {
const show = useSignal(true);

const Item = (props: { name: string }) => {
return <div>{props.name}</div>;
};

const Item2 = (props: { name: string }) => {
return <span>{props.name}</span>;
};

return (
<footer>
<button onClick$={() => (show.value = !show.value)}></button>
{show.value ? <Item name={'foo'} /> : <Item2 name={'bar'} />}
</footer>
);
});

const { vNode, document } = await render(<Cmp />, { debug });

expect(vNode).toMatchVDOM(
<Component>
<footer>
<button></button>
<InlineComponent>
<div>foo</div>
</InlineComponent>
</footer>
</Component>
);

await trigger(document.body, 'button', 'click');

expect(vNode).toMatchVDOM(
<Component>
<footer>
<button></button>
<InlineComponent>
<span>bar</span>
</InlineComponent>
</footer>
</Component>
);

await trigger(document.body, 'button', 'click');

expect(vNode).toMatchVDOM(
<Component>
<footer>
<button></button>
<InlineComponent>
<div>foo</div>
</InlineComponent>
</footer>
</Component>
);
});

it('should conditionally render different inline component inside inline component', async () => {
const Cmp = component$(() => {
const show = useSignal(true);

const Item = (props: { name: string }) => {
return <div>{props.name}</div>;
};

const Item2 = (props: { name: string }) => {
return <span>{props.name}</span>;
};

const Wrapper = () => {
return <>{show.value ? <Item name={'foo'} /> : <Item2 name={'bar'} />}</>;
};

return (
<footer>
<button onClick$={() => (show.value = !show.value)}></button>
<Wrapper />
</footer>
);
});

const { vNode, document } = await render(<Cmp />, { debug });

expect(vNode).toMatchVDOM(
<Component>
<footer>
<button></button>
<InlineComponent>
<Fragment>
<InlineComponent>
<div>foo</div>
</InlineComponent>
</Fragment>
</InlineComponent>
</footer>
</Component>
);

await trigger(document.body, 'button', 'click');

expect(vNode).toMatchVDOM(
<Component>
<footer>
<button></button>
<InlineComponent>
<Fragment>
<InlineComponent>
<span>bar</span>
</InlineComponent>
</Fragment>
</InlineComponent>
</footer>
</Component>
);

await trigger(document.body, 'button', 'click');

expect(vNode).toMatchVDOM(
<Component>
<footer>
<button></button>
<InlineComponent>
<Fragment>
<InlineComponent>
<div>foo</div>
</InlineComponent>
</Fragment>
</InlineComponent>
</footer>
</Component>
);
});
});

0 comments on commit 103581c

Please sign in to comment.