Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
mhevery committed Nov 13, 2023
1 parent 1d71aec commit 60a031a
Show file tree
Hide file tree
Showing 10 changed files with 209 additions and 73 deletions.
6 changes: 3 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@
},
{
"type": "node",
"name": "vscode-jest-tests",
"name": "vitest",
"request": "launch",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"program": "${workspaceFolder}/node_modules/jest/bin/jest.js",
"program": "${workspaceFolder}/./node_modules/vitest/vitest.mjs",
"cwd": "${workspaceFolder}",
"args": ["--runInBand", "--watchAll=false"]
"args": ["--threads=false", "packages/qwik/src/core/container/render.unit.tsx"]
}
]
}
73 changes: 73 additions & 0 deletions packages/qwik/src/core/container/render.unit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { assert, suite, test } from 'vitest';
import type { JSXNode } from '../../jsx-runtime';
import { renderToString } from '../../server/render';
import { createDocument } from '../../testing/document';
import { createDOM } from '../../testing/library';
import { component$ } from '../component/component.public';
import { _fnSignal } from '../internal';
import { useSignal } from '../use/use-signal';

suite('jsx signals', () => {
const RenderJSX = component$(() => {
const jsx = useSignal<string | JSXNode>(<span>SSR</span>);
return (
<>
<button class="text" onClick$={() => (jsx.value = 'text')} />
<button class="i" onClick$={() => (jsx.value = <i>i</i>)} />
<button class="b" onClick$={() => (jsx.value = <b>b</b>)} />
<button class="virtual" onClick$={() => (jsx.value = <>v</>)} />
<div class="jsx">{jsx.value}</div>
<div class="jsx-signal">{_fnSignal((p0) => p0.value, [jsx], 'p0.value')}</div>
</>
);
});

test.skip('SSR jsx', async () => {
const output = await renderToString(<RenderJSX />, { containerTagName: 'div' });
const document = createDocument();
document.body.innerHTML = output.html;
const div = document.querySelector('.jsx')!;
assert.equal(div.innerHTML, '<span>SSR</span>');
const divSignal = document.querySelector('.jsx-signal')!;
assert.equal(divSignal.innerHTML, '<!--t=1--><span>SSR</span><!---->');
});

test('CSR jsx', async () => {
const { screen, render, userEvent } = await createDOM();

await render(<RenderJSX />);
const div = screen.querySelector('.jsx')!;
const divSignal = screen.querySelector('.jsx-signal')!;
assert.equal(div.innerHTML, '<span>SSR</span>');
assert.equal(divSignal.innerHTML, '<span>SSR</span>');

await userEvent('button.text', 'click');
assert.equal(div.innerHTML, 'text');
assert.equal(divSignal.innerHTML, 'text');

await userEvent('button.i', 'click');
assert.equal(div.innerHTML, '<i>i</i>');
assert.equal(divSignal.innerHTML, '<i>i</i>');

await userEvent('button.b', 'click');
assert.equal(div.innerHTML, '<b>b</b>');
assert.equal(divSignal.innerHTML, '<b>b</b>');

await userEvent('button.virtual', 'click');
assert.equal(div.innerHTML, 'v');
assert.equal(divSignal.innerHTML, 'v');

await userEvent('button.b', 'click');
assert.equal(div.innerHTML, '<b>b</b>');
assert.equal(divSignal.innerHTML, '<b>b</b>');

await userEvent('button.text', 'click');
assert.equal(div.innerHTML, 'text');
assert.equal(divSignal.innerHTML, 'text');
});

//TODO(misko): More tests
// - Render component
// - Render promise
// - Render array of JSX
});
2 changes: 1 addition & 1 deletion packages/qwik/src/core/render/dom/notify-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ const renderMarked = async (containerState: ContainerState): Promise<void> => {
}

signalOperations.forEach((op) => {
executeSignalOperation(staticCtx, op);
executeSignalOperation(rCtx, op);
});

// Add post operations
Expand Down
1 change: 1 addition & 0 deletions packages/qwik/src/core/render/dom/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export const createTemplate = (doc: Document, slotName: string) => {

export const executeDOMRender = (staticCtx: RenderStaticContext) => {
for (const op of staticCtx.$operations$) {
// PERF(misko): polymorphic execution
op.$operation$.apply(undefined, op.$args$);
}
resolveSlotProjection(staticCtx);
Expand Down
3 changes: 2 additions & 1 deletion packages/qwik/src/core/render/dom/render-dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,11 @@ export const processData = (
} else if (isJSXNode(node)) {
return processNode(node, invocationContext);
} else if (isSignal(node)) {
const newNode = new ProcessedJSXNodeImpl('#text', EMPTY_OBJ, null, EMPTY_ARRAY, 0, null);
const newNode = new ProcessedJSXNodeImpl('#signal', EMPTY_OBJ, null, EMPTY_ARRAY, 0, null);
newNode.$signal$ = node;
return newNode;
} else if (isArray(node)) {
// PERF(misko): possible place to make it faster by not creating promises unnecessarily
const output = promiseAll(node.flatMap((n) => processData(n, invocationContext)));
return maybeThen(output, (array) => array.flat(100).filter(isNotNullable));
} else if (isPromise(node)) {
Expand Down
53 changes: 39 additions & 14 deletions packages/qwik/src/core/render/dom/signals.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { assertEqual } from '../../error/assert';
import { qError } from '../../error/error';
import type { SubscriberSignal } from '../../state/common';
import { tryGetContext } from '../../state/context';
import { getContext, tryGetContext } from '../../state/context';
import { trackSignal } from '../../use/use-core';
import { jsxToString, serializeClassWithHost, stringifyStyle } from '../execute-component';
import type { RenderStaticContext } from '../types';
import { setProperty } from './operations';
import { getVdom } from './render-dom';
import { smartSetProperty, SVG_NS } from './visitor';
import { logError } from '../../util/log';
import { serializeClassWithHost, stringifyStyle } from '../execute-component';
import type { RenderContext } from '../types';
import { insertBefore, removeNode } from './operations';
import { getVdom, processData, type ProcessedJSXNode } from './render-dom';
import type { QwikElement } from './virtual-element';
import { SVG_NS, createElm, diffVnode, getVnodeFromEl, smartSetProperty } from './visitor';

export const executeSignalOperation = (
staticCtx: RenderStaticContext,
operation: SubscriberSignal
) => {
export const executeSignalOperation = (rCtx: RenderContext, operation: SubscriberSignal) => {
try {
const type = operation[0];
const staticCtx = rCtx.$static$;
switch (type) {
case 1:
case 2: {
Expand Down Expand Up @@ -49,13 +51,36 @@ export const executeSignalOperation = (
}
case 3:
case 4: {
const elm: Text = operation[3] as Text;

const elm = operation[3];
if (!staticCtx.$visited$.includes(elm)) {
// assertTrue(elm.isConnected, 'text node must be connected to the dom');
staticCtx.$containerState$.$subsManager$.$clearSignal$(operation);
const value = trackSignal(operation[2], operation.slice(0, -1) as any);
return setProperty(staticCtx, elm, 'data', jsxToString(value));
const signal = operation[2];
// MISKO: I believe no `invocationContext` is OK because the JSX in signal
// has already been converted to JSX and there is nothing to execute there.
const invocationContext = undefined;
const newVnode = processData(signal.value, invocationContext) as ProcessedJSXNode;
const oldVnode = getVnodeFromEl(elm);
rCtx.$cmpCtx$ = getContext(operation[1] as QwikElement, rCtx.$static$.$containerState$);
if (
oldVnode.$type$ == newVnode.$type$ &&
oldVnode.$key$ == newVnode.$key$ &&
oldVnode.$id$ == newVnode.$id$
) {
diffVnode(rCtx, oldVnode, newVnode, 0);
} else {
const promises: Promise<any>[] = []; // TODO(misko): hook this up
const oldNode = oldVnode.$elm$;
const newElm = createElm(rCtx, newVnode, 0, promises);
if (promises.length) {
logError('Rendering promises in JSX signals is not supported');
}
rCtx.$static$;
operation[3] = newElm;
insertBefore(rCtx.$static$, elm.parentElement!, newElm, oldNode);
oldNode && removeNode(staticCtx, oldNode);
}
trackSignal(operation[2], operation.slice(0, -1) as any);
}
}
}
Expand Down
130 changes: 81 additions & 49 deletions packages/qwik/src/core/render/dom/visitor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { ELEMENT_ID, OnRenderProp, QSlot, QSlotRef, QSlotS, QStyle } from '../../util/markers';
import { isOnProp, PREVENT_DEFAULT, setEvent } from '../../state/listeners';
import type { ValueOrPromise } from '../../util/types';
import { isPromise, promiseAll, promiseAllLazy, maybeThen } from '../../util/promises';
import type { OnRenderFn } from '../../component/component.public';
import { getEventName, setRef, type ContainerState } from '../../container/container';
import {
assertDefined,
assertElement,
Expand All @@ -10,20 +8,14 @@ import {
assertQwikElement,
assertTrue,
} from '../../error/assert';
import { logWarn } from '../../util/log';
import { qDev, qInspector, qTest } from '../../util/qdev';
import type { OnRenderFn } from '../../component/component.public';
import { directGetAttribute, directSetAttribute } from '../fast-calls';
import { SKIP_RENDER_TYPE } from '../jsx/jsx-runtime';
import { assertQrl, isQrl } from '../../qrl/qrl-class';
import { PREVENT_DEFAULT, isOnProp, setEvent } from '../../state/listeners';
import { isElement, isQwikElement, isText, isVirtualElement } from '../../util/element';
import {
getVdom,
type ProcessedJSXNode,
ProcessedJSXNodeImpl,
renderComponent,
} from './render-dom';
import type { RenderContext, RenderStaticContext } from '../types';
import { logWarn } from '../../util/log';
import { ELEMENT_ID, OnRenderProp, QSlot, QSlotRef, QSlotS, QStyle } from '../../util/markers';
import { isPromise, maybeThen, promiseAll, promiseAllLazy } from '../../util/promises';
import { qDev, qInspector, qTest } from '../../util/qdev';
import type { ValueOrPromise } from '../../util/types';
import {
dangerouslySetInnerHTML,
isAriaAttribute,
Expand All @@ -35,17 +27,47 @@ import {
static_subtree,
stringifyStyle,
} from '../execute-component';
import { type ContainerState, setRef, getEventName } from '../../container/container';
import { directGetAttribute, directSetAttribute } from '../fast-calls';
import { SKIP_RENDER_TYPE, isJSXNode } from '../jsx/jsx-runtime';
import type { RenderContext, RenderStaticContext } from '../types';
import {
ProcessedJSXNodeImpl,
getVdom,
processData,
renderComponent,
type ProcessedJSXNode,
} from './render-dom';
import {
VIRTUAL,
getRootNode,
newVirtualElement,
processVirtualNodes,
queryAllVirtualByAttribute,
type QwikElement,
VIRTUAL,
type VirtualElement,
} from './virtual-element';

import { isBrowser } from '@builder.io/qwik/build';
import {
getProxyTarget,
getSubscriptionManager,
type SubscriberC,
type SubscriptionManager,
} from '../../state/common';
import { _IMMUTABLE, _IMMUTABLE_PREFIX } from '../../state/constants';
import {
HOST_FLAG_DIRTY,
HOST_FLAG_NEED_ATTACH_LISTENER,
cleanupContext,
createContext,
getContext,
tryGetContext,
type QContext,
} from '../../state/context';
import { isSignal } from '../../state/signal';
import { ReadWriteProxyHandler, createPropsState, createProxy } from '../../state/store';
import { trackSignal } from '../../use/use-core';
import { EMPTY_OBJ } from '../../util/flyweight';
import {
appendChild,
createElement,
Expand All @@ -61,26 +83,6 @@ import {
setProperty,
setPropertyPost,
} from './operations';
import { EMPTY_OBJ } from '../../util/flyweight';
import { isSignal } from '../../state/signal';
import {
cleanupContext,
createContext,
getContext,
HOST_FLAG_DIRTY,
HOST_FLAG_NEED_ATTACH_LISTENER,
type QContext,
tryGetContext,
} from '../../state/context';
import {
getSubscriptionManager,
getProxyTarget,
type SubscriptionManager,
} from '../../state/common';
import { createPropsState, createProxy, ReadWriteProxyHandler } from '../../state/store';
import { _IMMUTABLE, _IMMUTABLE_PREFIX } from '../../state/constants';
import { trackSignal } from '../../use/use-core';
import { isBrowser } from '@builder.io/qwik/build';

export const SVG_NS = 'http://www.w3.org/2000/svg';

Expand Down Expand Up @@ -637,7 +639,7 @@ const getSlotName = (node: ProcessedJSXNode): string => {
return node.$props$[QSlot] ?? '';
};

const createElm = (
export const createElm = (
rCtx: RenderContext,
vnode: ProcessedJSXNode,
flags: number,
Expand All @@ -647,18 +649,48 @@ const createElm = (
const doc = rCtx.$static$.$doc$;
const currentComponent = rCtx.$cmpCtx$;
if (tag === '#text') {
const signal = vnode.$signal$;
const elm = doc.createTextNode(vnode.$text$);
if (signal) {
assertDefined(currentComponent, 'signals can not be used outside components');
const subs =
flags & IS_IMMUTABLE
? ([3, elm, signal, elm] as const)
: ([4, currentComponent.$element$, signal, elm] as const);
return (vnode.$elm$ = doc.createTextNode(vnode.$text$));
}

elm.data = vnode.$text$ = jsxToString(trackSignal(signal, subs));
if (tag === '#signal') {
const signal = vnode.$signal$!;
assertDefined(signal, 'expecting signal here');
assertDefined(currentComponent, 'signals can not be used outside components');
const signalValue = signal.value;
if (isJSXNode(signalValue)) {
// convert signal value to ProcessedJSXNode
const processedSignal = processData(signalValue);
if (isSignal(processedSignal)) {
throw new Error('NOT IMPLEMENTED: Promise');
} else if (Array.isArray(processedSignal)) {
throw new Error('NOT IMPLEMENTED: Array');
} else {
// crate elements
const elm = createElm(rCtx, processedSignal as ProcessedJSXNode, flags, promises);
// create subscription
trackSignal(
signal,
flags & IS_IMMUTABLE
? ([3, elm, signal, elm] as SubscriberC)
: ([4, currentComponent.$element$, signal, elm] as SubscriberC)
);
// update the vNode for future diff.
return (vnode.$elm$ = elm);
}
} else {
// create element
const elm = doc.createTextNode(vnode.$text$);
elm.data = vnode.$text$ = jsxToString(signalValue);
// create subscription
trackSignal(
signal,
flags & IS_IMMUTABLE
? ([3, elm, signal, elm] as SubscriberC)
: ([4, currentComponent.$element$, signal, elm] as SubscriberC)
);
// update the vNode for future diff.
return (vnode.$elm$ = elm);
}
return (vnode.$elm$ = elm);
}

let elm: QwikElement;
Expand Down
10 changes: 7 additions & 3 deletions packages/qwik/src/core/render/ssr/render-ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -875,9 +875,13 @@ const processData = (
: ([4, hostEl, node, ('#' + id) as any] as const);

value = trackSignal(node, subs);
const str = jsxToString(value);
ssrCtx.$static$.$textNodes$.set(str, id);
stream.write(`<!--t=${id}-->${escapeHtml(str)}<!---->`);
if (isString(value)) {
const str = jsxToString(value);
ssrCtx.$static$.$textNodes$.set(str, id);
}
stream.write(`<!--t=${id}-->`);
processData(value, rCtx, ssrCtx, stream, flags, beforeClose);
stream.write(`<!---->`);
return;
} else {
value = invoke(ssrCtx.$invocationContext$, () => node.value);
Expand Down
Loading

0 comments on commit 60a031a

Please sign in to comment.