Skip to content

Commit 25d3324

Browse files
authored
fix: content projection can inject contexts (#615)
1 parent 5018fca commit 25d3324

File tree

9 files changed

+95
-92
lines changed

9 files changed

+95
-92
lines changed

packages/qwik/src/core/api.md

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ export interface InvokeContext {
301301
$props$?: Props;
302302
// (undocumented)
303303
$qrl$?: QRL<any>;
304-
// Warning: (ae-incompatible-release-tags) The symbol "$renderCtx$" is marked as @public, but its signature references "RenderContext" which is marked as @alpha
304+
// Warning: (ae-forgotten-export) The symbol "RenderContext" needs to be exported by the entry point index.d.ts
305305
//
306306
// (undocumented)
307307
$renderCtx$?: RenderContext;
@@ -452,42 +452,6 @@ export type RenderableProps<P, RefType = any> = P & Readonly<{
452452
children?: ComponentChildren;
453453
}>;
454454

455-
// @alpha (undocumented)
456-
export interface RenderContext {
457-
// (undocumented)
458-
$components$: ComponentCtx[];
459-
// (undocumented)
460-
$containerEl$: Element;
461-
// Warning: (ae-forgotten-export) The symbol "ContainerState" needs to be exported by the entry point index.d.ts
462-
//
463-
// (undocumented)
464-
$containerState$: ContainerState;
465-
// (undocumented)
466-
$doc$: Document;
467-
// (undocumented)
468-
$hostElements$: Set<Element>;
469-
// (undocumented)
470-
$operations$: RenderOperation[];
471-
// Warning: (ae-forgotten-export) The symbol "RenderPerf" needs to be exported by the entry point index.d.ts
472-
//
473-
// (undocumented)
474-
$perf$: RenderPerf;
475-
// (undocumented)
476-
$roots$: Element[];
477-
}
478-
479-
// @alpha (undocumented)
480-
export interface RenderOperation {
481-
// (undocumented)
482-
$args$: any[];
483-
// (undocumented)
484-
$el$: Node;
485-
// (undocumented)
486-
$fn$: () => void;
487-
// (undocumented)
488-
$operation$: string;
489-
}
490-
491455
// @alpha (undocumented)
492456
export type ServerFn = () => ValueOrPromise<void | (() => void)>;
493457

packages/qwik/src/core/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ export {
9696
export type { Context } from './use/use-context';
9797
export type { Ref } from './use/use-store.public';
9898
export type { InvokeContext } from './use/use-core';
99-
export type { RenderContext, RenderOperation } from './render/cursor';
10099

101100
//////////////////////////////////////////////////////////////////////////////////////////
102101
// Developer Low-Level API

packages/qwik/src/core/render/cursor.ts

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ export interface RenderContext {
7575
$roots$: Element[];
7676
$hostElements$: Set<Element>;
7777
$operations$: RenderOperation[];
78-
$components$: ComponentCtx[];
78+
$contexts$: QContext[];
79+
$currentComponent$: ComponentCtx | undefined;
7980
$containerState$: ContainerState;
8081
$containerEl$: Element;
8182
$perf$: RenderPerf;
@@ -290,14 +291,14 @@ export const patchVnode = (
290291
if (isSvg && vnode.type === 'foreignObject') {
291292
isSvg = false;
292293
} else if (isSlot) {
293-
const currentComponent =
294-
rctx.$components$.length > 0 ? rctx.$components$[rctx.$components$.length - 1] : undefined;
294+
const currentComponent = rctx.$currentComponent$;
295295
if (currentComponent) {
296296
currentComponent.$slots$.push(vnode);
297297
}
298298
}
299299
const isComponent = isComponentNode(vnode);
300300
if (dirty) {
301+
assertEqual(isComponent, true);
301302
promise = renderComponent(rctx, ctx);
302303
}
303304
const ch = vnode.children;
@@ -306,32 +307,33 @@ export const patchVnode = (
306307
const slotMaps = getSlots(ctx.$component$, elm as Element);
307308
const splittedChidren = splitBy(ch, getSlotName);
308309
const promises: ValueOrPromise<void>[] = [];
310+
const slotRctx = copyRenderContext(rctx);
311+
slotRctx.$contexts$.push(ctx);
309312

310313
// Mark empty slots and remove content
311314
Object.entries(slotMaps.slots).forEach(([key, slotEl]) => {
312315
if (slotEl && !splittedChidren[key]) {
313316
const oldCh = getChildren(slotEl, 'slot');
314317
if (oldCh.length > 0) {
315-
removeVnodes(rctx, oldCh, 0, oldCh.length - 1);
318+
removeVnodes(slotRctx, oldCh, 0, oldCh.length - 1);
316319
}
317320
}
318321
});
319322

320323
// Mark empty slots and remove content
321324
Object.entries(slotMaps.templates).forEach(([key, templateEl]) => {
322325
if (templateEl && !splittedChidren[key]) {
323-
removeNode(rctx, templateEl);
326+
removeNode(slotRctx, templateEl);
324327
slotMaps.templates[key] = undefined;
325328
}
326329
});
327-
328330
// Render into slots
329331
Object.entries(splittedChidren).forEach(([key, ch]) => {
330-
const slotElm = getSlotElement(rctx, slotMaps, elm as Element, key);
331-
promises.push(smartUpdateChildren(rctx, slotElm, ch, 'slot', isSvg));
332+
const slotElm = getSlotElement(slotRctx, slotMaps, elm as Element, key);
333+
promises.push(smartUpdateChildren(slotRctx, slotElm, ch, 'slot', isSvg));
332334
});
333335
return then(promiseAll(promises), () => {
334-
removeTemplates(rctx, slotMaps);
336+
removeTemplates(slotRctx, slotMaps);
335337
});
336338
});
337339
}
@@ -504,8 +506,7 @@ const createElm = (rctx: RenderContext, vnode: JSXNode, isSvg: boolean): ValueOr
504506
if (isSvg && tag === 'foreignObject') {
505507
isSvg = false;
506508
}
507-
const currentComponent =
508-
rctx.$components$.length > 0 ? rctx.$components$[rctx.$components$.length - 1] : undefined;
509+
const currentComponent = rctx.$currentComponent$;
509510
if (currentComponent) {
510511
const styleTag = currentComponent.$styleClass$;
511512
if (styleTag) {
@@ -538,13 +539,15 @@ const createElm = (rctx: RenderContext, vnode: JSXNode, isSvg: boolean): ValueOr
538539
if (children.length === 1 && children[0].type === SkipRerender) {
539540
children = children[0].children;
540541
}
542+
const slotRctx = copyRenderContext(rctx);
543+
slotRctx.$contexts$.push(ctx);
541544
const slotMap = isComponent ? getSlots(ctx.$component$, elm) : undefined;
542-
const promises = children.map((ch) => createElm(rctx, ch, isSvg));
545+
const promises = children.map((ch) => createElm(slotRctx, ch, isSvg));
543546
return then(promiseAll(promises) as any, () => {
544547
let parent = elm;
545548
for (const node of children) {
546549
if (slotMap) {
547-
parent = getSlotElement(rctx, slotMap, elm, getSlotName(node));
550+
parent = getSlotElement(slotRctx, slotMap, elm, getSlotName(node));
548551
}
549552
parent.appendChild(node.elm!);
550553
}
@@ -710,6 +713,35 @@ export const updateProperties = (
710713
return ctx.$dirty$;
711714
};
712715

716+
export const createRenderContext = (
717+
doc: Document,
718+
containerState: ContainerState,
719+
containerEl: Element
720+
): RenderContext => {
721+
const ctx: RenderContext = {
722+
$doc$: doc,
723+
$containerState$: containerState,
724+
$containerEl$: containerEl,
725+
$hostElements$: new Set(),
726+
$operations$: [],
727+
$roots$: [],
728+
$contexts$: [],
729+
$currentComponent$: undefined,
730+
$perf$: {
731+
$visited$: 0,
732+
},
733+
};
734+
return ctx;
735+
};
736+
737+
export const copyRenderContext = (ctx: RenderContext): RenderContext => {
738+
const newCtx: RenderContext = {
739+
...ctx,
740+
$contexts$: [...ctx.$contexts$],
741+
};
742+
return newCtx;
743+
};
744+
713745
export const setAttribute = (
714746
ctx: RenderContext,
715747
el: Element,

packages/qwik/src/core/render/notify-render.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { assertDefined } from '../assert/assert';
22
import { QContainerAttr, QHostAttr } from '../util/markers';
3-
import { executeContextWithSlots, printRenderStats, RenderContext } from './cursor';
3+
import {
4+
createRenderContext,
5+
executeContextWithSlots,
6+
printRenderStats,
7+
RenderContext,
8+
} from './cursor';
49
import { getContext, resumeIfNeeded } from '../props/props';
510
import { qDev, qTest } from '../util/qdev';
611
import { getPlatform } from '../platform/platform';
@@ -142,18 +147,7 @@ export const renderMarked = async (
142147
const renderingQueue = Array.from(hostsRendering);
143148
sortNodes(renderingQueue);
144149

145-
const ctx: RenderContext = {
146-
$doc$: doc,
147-
$containerState$: containerState,
148-
$hostElements$: new Set(),
149-
$operations$: [],
150-
$roots$: [],
151-
$containerEl$: containerEl,
152-
$components$: [],
153-
$perf$: {
154-
$visited$: 0,
155-
},
156-
};
150+
const ctx = createRenderContext(doc, containerState, containerEl);
157151

158152
for (const el of renderingQueue) {
159153
if (!ctx.$hostElements$.has(el)) {

packages/qwik/src/core/render/render-component.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { assertDefined } from '../assert/assert';
2-
import type { RenderContext } from './cursor';
2+
import { copyRenderContext, RenderContext } from './cursor';
33
import { visitJsxNode } from './render';
44
import { ComponentScopedStyles, QHostAttr, RenderEvent } from '../util/markers';
55
import { promiseAll, then } from '../util/promises';
@@ -30,10 +30,7 @@ export const renderComponent = (rctx: RenderContext, ctx: QContext): ValueOrProm
3030
// Component is not dirty any more
3131
rctx.$containerState$.$hostsStaging$.delete(hostElement);
3232

33-
const newCtx: RenderContext = {
34-
...rctx,
35-
$components$: [...rctx.$components$],
36-
};
33+
const newCtx = copyRenderContext(rctx);
3734

3835
// Invoke render hook
3936
const invocatinContext = newInvokeContext(rctx.$doc$, hostElement, hostElement, RenderEvent);
@@ -85,7 +82,8 @@ export const renderComponent = (rctx: RenderContext, ctx: QContext): ValueOrProm
8582
}
8683
}
8784
componentCtx.$slots$ = [];
88-
newCtx.$components$.push(componentCtx);
85+
newCtx.$contexts$.push(ctx);
86+
newCtx.$currentComponent$ = componentCtx;
8987
return visitJsxNode(newCtx, hostElement, processNode(jsxNode), false);
9088
});
9189
},

packages/qwik/src/core/render/render.public.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { isDocument } from '../util/element';
2-
import { executeContext, printRenderStats, RenderContext } from './cursor';
2+
import { createRenderContext, executeContext, printRenderStats } from './cursor';
33
import { isJSXNode, jsx, processNode } from './jsx/jsx-runtime';
44
import type { JSXNode, FunctionComponent } from './jsx/types/jsx-node';
55
import { visitJsxNode } from './render';
@@ -19,7 +19,7 @@ import { directSetAttribute } from './fast-calls';
1919
*
2020
* Use this method to render JSX. This function does reconciling which means
2121
* it always tries to reuse what is already in the DOM (rather then destroy and
22-
* recrate content.)
22+
* recreate content.)
2323
*
2424
* @param parent - Element which will act as a parent to `jsxNode`. When
2525
* possible the rendering will try to reuse existing nodes.
@@ -43,18 +43,8 @@ export const render = async (
4343
injectQContainer(containerEl);
4444

4545
const containerState = getContainerState(containerEl);
46-
const ctx: RenderContext = {
47-
$doc$: doc,
48-
$containerState$: containerState,
49-
$hostElements$: new Set(),
50-
$operations$: [],
51-
$roots$: [parent as Element],
52-
$components$: [],
53-
$containerEl$: containerEl,
54-
$perf$: {
55-
$visited$: 0,
56-
},
57-
};
46+
const ctx = createRenderContext(doc, containerState, containerEl);
47+
ctx.$roots$.push(parent as Element);
5848

5949
await visitJsxNode(ctx, parent as Element, processNode(jsxNode), false);
6050

packages/qwik/src/core/use/use-context.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@ const _useContext = <STATE extends object>(context: Context<STATE>): STATE => {
6868
if (!value) {
6969
const invokeContext = getInvokeContext();
7070
let hostElement = invokeContext.$hostElement$!;
71-
const components = invokeContext.$renderCtx$!.$components$;
72-
for (let i = components.length - 1; i >= 0; i--) {
73-
hostElement = components[i].$hostElement$;
74-
const ctx = getContext(components[i].$hostElement$);
71+
const contexts = invokeContext.$renderCtx$!.$contexts$;
72+
for (let i = contexts.length - 1; i >= 0; i--) {
73+
const ctx = contexts[i];
74+
hostElement = ctx.$element$;
7575
if (ctx.$contexts$) {
7676
const found = ctx.$contexts$.get(context.id);
7777
if (found) {

starters/apps/e2e/src/components/context/context.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
createContext,
66
useContextProvider,
77
useContext,
8+
Slot,
89
} from '@builder.io/qwik';
910

1011
export interface ContextI {
@@ -15,6 +16,7 @@ export interface ContextI {
1516
export const Context1 = createContext<ContextI>('ctx');
1617
export const Context2 = createContext<ContextI>('ctx1');
1718
export const Context3 = createContext<ContextI>('ctx2');
19+
export const ContextSlot = createContext<ContextI>('slot');
1820

1921
export const ContextRoot = component$(async () => {
2022
const state1 = useStore({ displayName: 'ROOT / state1', count: 0 });
@@ -32,12 +34,23 @@ export const ContextRoot = component$(async () => {
3234
Increment State 2
3335
</button>
3436

35-
<Level2 />
36-
<Level2 />
37+
<ContextFromSlot>
38+
<Level2 />
39+
<Level2 />
40+
</ContextFromSlot>
3741
</Host>
3842
);
3943
});
4044

45+
export const ContextFromSlot = component$(() => {
46+
const store = useStore({
47+
displayName: 'bar',
48+
count: 0,
49+
});
50+
useContextProvider(ContextSlot, store);
51+
return <Slot />;
52+
});
53+
4154
// This code will not work because its async before reading subs
4255
export const Level2 = component$(() => {
4356
const level2State1 = useStore({ displayName: 'Level2 / state1', count: 0 });
@@ -48,6 +61,7 @@ export const Level2 = component$(() => {
4861

4962
const state1 = useContext(Context1);
5063
const state2 = useContext(Context2);
64+
const stateSlot = useContext(ContextSlot);
5165

5266
return (
5367
<Host>
@@ -58,6 +72,9 @@ export const Level2 = component$(() => {
5872
<div class="level2-state2">
5973
{state2.displayName} = {state2.count}
6074
</div>
75+
<div class="level2-slot">
76+
{stateSlot.displayName} = {stateSlot.count}
77+
</div>
6178

6279
<button class="level2-increment3" onClick$={() => state3.count++}>
6380
Increment
@@ -74,6 +91,7 @@ export const Level3 = component$(() => {
7491
const state1 = useContext(Context1);
7592
const state2 = useContext(Context2);
7693
const state3 = useContext(Context3);
94+
const stateSlot = useContext(ContextSlot);
7795

7896
return (
7997
<Host>
@@ -87,6 +105,9 @@ export const Level3 = component$(() => {
87105
<div class="level3-state3">
88106
{state3.displayName} = {state3.count}
89107
</div>
108+
<div class="level3-slot">
109+
{stateSlot.displayName} = {stateSlot.count}
110+
</div>
90111
</Host>
91112
);
92113
});

0 commit comments

Comments
 (0)