Skip to content

Commit

Permalink
Merge pull request #7148 from QwikDev/v2-replacing-projection-content…
Browse files Browse the repository at this point in the history
…-with-null

fix: replacing projection content with null or undefined
  • Loading branch information
shairez authored Dec 13, 2024
2 parents fe61946 + 9e4bf8f commit f75361c
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 44 deletions.
5 changes: 5 additions & 0 deletions .changeset/twenty-goats-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik.dev/core': patch
---

fix: replacing projection content with null or undefined
79 changes: 40 additions & 39 deletions packages/qwik/src/core/client/vnode-diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,51 +402,52 @@ export const vnode_diff = (
/////////////////////////////////////////////////////////////////////////////

function descendContentToProject(children: JSXChildren, host: VirtualVNode | null) {
if (!Array.isArray(children)) {
children = [children];
}
if (children.length) {
const createProjectionJSXNode = (slotName: string) => {
return new JSXNodeImpl(Projection, EMPTY_OBJ, null, [], 0, slotName);
};
const projectionChildren = Array.isArray(children) ? children : [children];
const createProjectionJSXNode = (slotName: string) => {
return new JSXNodeImpl(Projection, EMPTY_OBJ, null, [], 0, slotName);
};

const projections: Array<string | JSXNodeInternal> = [];
if (host) {
// we need to create empty projections for all the slots to remove unused slots content
for (let i = vnode_getPropStartIndex(host); i < host.length; i = i + 2) {
const prop = host[i] as string;
if (isSlotProp(prop)) {
const slotName = prop;
projections.push(slotName);
projections.push(createProjectionJSXNode(slotName));
}
const projections: Array<string | JSXNodeInternal> = [];
if (host) {
// we need to create empty projections for all the slots to remove unused slots content
for (let i = vnode_getPropStartIndex(host); i < host.length; i = i + 2) {
const prop = host[i] as string;
if (isSlotProp(prop)) {
const slotName = prop;
projections.push(slotName);
projections.push(createProjectionJSXNode(slotName));
}
}
}

/// STEP 1: Bucketize the children based on the projection name.
for (let i = 0; i < children.length; i++) {
const child = children[i];
const slotName = String(
(isJSXNode(child) && directGetPropsProxyProp(child, QSlot)) || QDefaultSlot
);
const idx = mapApp_findIndx(projections, slotName, 0);
let jsxBucket: JSXNodeImpl<typeof Projection>;
if (idx >= 0) {
jsxBucket = projections[idx + 1] as any;
} else {
projections.splice(~idx, 0, slotName, (jsxBucket = createProjectionJSXNode(slotName)));
}
const removeProjection = child === false;
if (!removeProjection) {
(jsxBucket.children as JSXChildren[]).push(child);
}
if (projections.length === 0 && children == null) {
// We did not find any existing slots and we don't have any children to project.
return;
}

/// STEP 1: Bucketize the children based on the projection name.
for (let i = 0; i < projectionChildren.length; i++) {
const child = projectionChildren[i];
const slotName = String(
(isJSXNode(child) && directGetPropsProxyProp(child, QSlot)) || QDefaultSlot
);
const idx = mapApp_findIndx(projections, slotName, 0);
let jsxBucket: JSXNodeImpl<typeof Projection>;
if (idx >= 0) {
jsxBucket = projections[idx + 1] as any;
} else {
projections.splice(~idx, 0, slotName, (jsxBucket = createProjectionJSXNode(slotName)));
}
/// STEP 2: remove the names
for (let i = projections.length - 2; i >= 0; i = i - 2) {
projections.splice(i, 1);
const removeProjection = child === false;
if (!removeProjection) {
(jsxBucket.children as JSXChildren[]).push(child);
}
descend(projections, true);
}
/// STEP 2: remove the names
for (let i = projections.length - 2; i >= 0; i = i - 2) {
projections.splice(i, 1);
}
descend(projections, true);
}

function expectProjection() {
Expand Down Expand Up @@ -1036,7 +1037,7 @@ export const vnode_diff = (
container.$scheduler$(ChoreType.COMPONENT, host, componentQRL, jsxProps);
}
}
jsxNode.children != null && descendContentToProject(jsxNode.children, host);
descendContentToProject(jsxNode.children, host);
} else {
const lookupKey = jsxNode.key;
const vNodeLookupKey = getKey(host);
Expand Down
211 changes: 206 additions & 5 deletions packages/qwik/src/core/tests/projection.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,11 @@ describe.each([
});
const { vNode } = await render(<Parent />, { debug: DEBUG });
expect(vNode).toMatchVDOM(
<Fragment>
<Fragment>
<Fragment>default-value</Fragment>
</Fragment>
</Fragment>
<Component>
<Component>
<Projection>default-value</Projection>
</Component>
</Component>
);
});
it('should save default value in q:template if not used', async () => {
Expand Down Expand Up @@ -258,6 +258,207 @@ describe.each([
</Fragment>
);
});

it('should replace projection content with undefined', async () => {
const Test = component$(() => {
return (
<div>
<Slot />
</div>
);
});

const Cmp = component$(() => {
const test = useSignal<number | undefined>(1);

return (
<div>
<Test>
{test.value ? (
<div>
<h1>Hello from Qwik</h1>
</div>
) : undefined}
</Test>

<button
onClick$={() => {
test.value = test.value ? undefined : 1;
}}
></button>
</div>
);
});

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

expect(vNode).toMatchVDOM(
<Component>
<div>
<Component>
<div>
<Projection>
<div>
<h1>Hello from Qwik</h1>
</div>
</Projection>
</div>
</Component>
<button></button>
</div>
</Component>
);

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

expect(vNode).toMatchVDOM(
<Component>
<div>
<Component>
<div>
<Projection>{''}</Projection>
</div>
</Component>
<button></button>
</div>
</Component>
);

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

expect(vNode).toMatchVDOM(
<Component>
<div>
<Component>
<div>
<Projection>
<div>
<h1>Hello from Qwik</h1>
</div>
</Projection>
</div>
</Component>
<button></button>
</div>
</Component>
);

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

expect(vNode).toMatchVDOM(
<Component>
<div>
<Component>
<div>
<Projection>{''}</Projection>
</div>
</Component>
<button></button>
</div>
</Component>
);
});

it('should replace projection content with null', async () => {
const Test = component$(() => {
return (
<div>
<Slot />
</div>
);
});

const Cmp = component$(() => {
const test = useSignal<number | null>(1);

return (
<div>
<Test>
{test.value ? (
<div>
<h1>Hello from Qwik</h1>
</div>
) : null}
</Test>

<button
onClick$={() => {
test.value = test.value ? null : 1;
}}
></button>
</div>
);
});

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

expect(vNode).toMatchVDOM(
<Component>
<div>
<Component>
<div>
<Projection>
<div>
<h1>Hello from Qwik</h1>
</div>
</Projection>
</div>
</Component>
<button></button>
</div>
</Component>
);

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

expect(vNode).toMatchVDOM(
<Component>
<div>
<Component>
<div>
<Projection>{''}</Projection>
</div>
</Component>
<button></button>
</div>
</Component>
);

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

expect(vNode).toMatchVDOM(
<Component>
<div>
<Component>
<div>
<Projection>
<div>
<h1>Hello from Qwik</h1>
</div>
</Projection>
</div>
</Component>
<button></button>
</div>
</Component>
);

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

expect(vNode).toMatchVDOM(
<Component>
<div>
<Component>
<div>
<Projection>{''}</Projection>
</div>
</Component>
<button></button>
</div>
</Component>
);
});

it('should ignore Slot inside inline-component', async () => {
const Child = (props: { children: any }) => {
return (
Expand Down

0 comments on commit f75361c

Please sign in to comment.