Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update render contexts while moving nodes #139

Merged
merged 3 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/selfish-maps-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chialab/dna": patch
---

Reuse render context for referenced nodes.
5 changes: 5 additions & 0 deletions .changeset/shy-yaks-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chialab/dna": patch
---

Fix runtime error while moving and replacing slot nodes in the same render cycle.
57 changes: 37 additions & 20 deletions src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,15 @@
type: FunctionComponent | string | null;
root?: Context;
owner?: Context;
parent?: Context;
children: Context[];
contexts: WeakMap<Node, Context>;
properties?: KeyedProperties & TreeProperties & Record<string, unknown>;
state?: HooksState;
end?: Context;
key?: unknown;
keys?: Map<unknown, Context>;
refs?: Map<Node, Context>;
_pos: number;
};

Expand All @@ -70,6 +73,7 @@
root,
owner,
children: [],
contexts: new WeakMap(),

Check warning on line 76 in src/render.ts

View check run for this annotation

Codecov / codecov/patch

src/render.ts#L76

Added line #L76 was not covered by tests
_pos: 0,
});

Expand Down Expand Up @@ -283,8 +287,9 @@
* Insert a node into the render tree.
* @param parentContext The parent context.
* @param childContext The child context.
* @param rootContext The root context.
*/
const insertNode = (parentContext: Context, childContext: Context) => {
const insertNode = (parentContext: Context, childContext: Context, rootContext: Context) => {
const { node: parentNode, _pos: pos } = parentContext;
const currentChildren = parentContext.children;
if (currentChildren.includes(childContext)) {
Expand All @@ -293,12 +298,20 @@
let currentContext: Context | null;
while ((currentContext = currentChildren[pos]) && childContext !== currentContext) {
parentNode.removeChild(currentContext.node);
rootContext.contexts.delete(currentContext.node);
currentChildren.splice(pos, 1);
}
} else {
// we need to insert the new node into the tree
const currentChildContext = rootContext.contexts.get(childContext.node);
if (currentChildContext?.parent && currentChildContext.parent !== parentContext) {
const { node: currentParentNode, children: currentParentChildren } = currentChildContext.parent;
currentParentNode.removeChild(childContext.node);
currentParentChildren.splice(currentParentChildren.indexOf(currentChildContext), 1);
}

Check warning on line 310 in src/render.ts

View check run for this annotation

Codecov / codecov/patch

src/render.ts#L310

Added line #L310 was not covered by tests
parentNode.insertBefore(childContext.node, currentChildren[pos]?.node);
currentChildren.splice(pos, 0, childContext);
childContext.parent = parentContext;
rootContext.contexts.set(childContext.node, childContext);
}
parentContext._pos++;
};
Expand All @@ -310,6 +323,7 @@
* @param template The template to render in Virtual DOM format.
* @param namespace The current namespace uri of the render.
* @param keys The current keys map of the render.
* @param refs The current refs map of the render.
* @param realm The realm to use for the render.
* @param fragment The fragment context to update.
*/
Expand All @@ -319,6 +333,7 @@
template: Template,
namespace: string,
keys: Map<unknown, Context> | undefined,
refs: Map<Node, Context> | undefined,

Check warning on line 336 in src/render.ts

View check run for this annotation

Codecov / codecov/patch

src/render.ts#L336

Added line #L336 was not covered by tests
realm?: Realm,
fragment: Context = context
) => {
Expand All @@ -332,13 +347,13 @@
return;
}
if (len === 1) {
renderTemplate(context, rootContext, template[0], namespace, keys, realm, fragment);
renderTemplate(context, rootContext, template[0], namespace, keys, refs, realm, fragment);
return;
}

// call the render function for each child
for (let i = 0; i < len; i++) {
renderTemplate(context, rootContext, template[i], namespace, keys, realm, fragment);
renderTemplate(context, rootContext, template[i], namespace, keys, refs, realm, fragment);
}
return;
}
Expand All @@ -348,7 +363,7 @@

if (isVObject(template)) {
if (isVFragment(template)) {
renderTemplate(context, rootContext, template.children, namespace, keys, realm, fragment);
renderTemplate(context, rootContext, template.children, namespace, keys, refs, realm, fragment);
return;
}

Expand All @@ -373,6 +388,7 @@
rootNode || document.createComment(Function.name),
namespace,
keys,
refs,

Check warning on line 391 in src/render.ts

View check run for this annotation

Codecov / codecov/patch

src/render.ts#L391

Added line #L391 was not covered by tests
realm,
fragment
);
Expand Down Expand Up @@ -438,6 +454,7 @@
),
namespace,
keys,
refs,

Check warning on line 457 in src/render.ts

View check run for this annotation

Codecov / codecov/patch

src/render.ts#L457

Added line #L457 was not covered by tests
realm
);

Expand All @@ -454,9 +471,9 @@
const name = properties?.name;
const slotted = realm.childNodesBySlot(name);
if (slotted.length) {
renderTemplate(context, rootContext, slotted, namespace, keys, realm, fragment);
renderTemplate(context, rootContext, slotted, namespace, keys, refs, realm, fragment);
} else if (children) {
renderTemplate(context, rootContext, children, namespace, keys, realm, fragment);
renderTemplate(context, rootContext, children, namespace, keys, refs, realm, fragment);
}
return;
}
Expand Down Expand Up @@ -489,9 +506,8 @@
if (!templateContext) {
if (isVNode(template)) {
const node = template.type;
templateContext =
currentChildren.find((child) => child.node === node) ||
createContext(ContextKind.REF, null, template.type, rootContext);
templateContext = refs?.get(node) || createContext(ContextKind.REF, null, template.type, rootContext);
fragment.refs = (fragment.refs || new Map()).set(node, templateContext);
} else {
const constructor = customElements?.get(properties?.is ?? template.type);
templateContext = createContext(
Expand Down Expand Up @@ -552,7 +568,7 @@
(node as ComponentInstance).collectUpdatesEnd();
}

insertNode(context, templateContext);
insertNode(context, templateContext, rootContext);

if ((templateContext && children && children.length) || templateContext.root === rootContext) {
internalRender(templateContext, children, realm, rootContext, namespaceURI, undefined);
Expand All @@ -563,7 +579,8 @@
insertNode(
context,
currentChildren.find((child) => child.node === template) ||
createContext(ContextKind.REF, null, template, rootContext)
createContext(ContextKind.REF, null, template, rootContext),
rootContext

Check warning on line 583 in src/render.ts

View check run for this annotation

Codecov / codecov/patch

src/render.ts#L582-L583

Added lines #L582 - L583 were not covered by tests
);
return;
}
Expand All @@ -583,7 +600,7 @@
currentContext.type = template;
currentContext.node.nodeValue = template as string;
}
insertNode(context, currentContext);
insertNode(context, currentContext, rootContext);
return;
}

Expand All @@ -596,7 +613,8 @@
document.createTextNode(template as string),
rootContext,
rootContext
)
),
rootContext

Check warning on line 617 in src/render.ts

View check run for this annotation

Codecov / codecov/patch

src/render.ts#L616-L617

Added lines #L616 - L617 were not covered by tests
);
};

Expand Down Expand Up @@ -624,18 +642,21 @@

let endContext: Context | undefined;
let currentKeys: Map<unknown, Context> | undefined;
let currentRefs: Map<Node, Context> | undefined;

Check warning on line 645 in src/render.ts

View check run for this annotation

Codecov / codecov/patch

src/render.ts#L645

Added line #L645 was not covered by tests
if (fragment) {
context._pos = context.children.indexOf(fragment);
endContext = fragment.end as Context;
currentKeys = fragment.keys;
currentRefs = fragment.refs;
delete fragment.keys;
} else {
context._pos = 0;
currentKeys = context.keys;
currentRefs = context.refs;
delete context.keys;
}

renderTemplate(context, rootContext, template, namespace, currentKeys, realm, fragment);
renderTemplate(context, rootContext, template, namespace, currentKeys, currentRefs, realm, fragment);

// all children of the root have been handled,
// we can start to cleanup the tree
Expand All @@ -649,11 +670,7 @@
}

while (currentIndex <= --end) {
const [child] = contextChildren.splice(end, 1);
const parentNode = child.node.parentNode;
if (parentNode === context.node || (realm?.open && parentNode === realm.node && realm.root === context.node)) {
context.node.removeChild(child.node);
}
context.node.removeChild(contextChildren.splice(end, 1)[0].node);
}

return contextChildren;
Expand Down
50 changes: 50 additions & 0 deletions test/render.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,56 @@ describe.runIf(typeof window !== 'undefined')(

expect(child.textContent).toBe('Hello');
});

it('should resuse refs context', () => {
const name = getComponentName();
DNA.define(
name,
class TestElement extends DNA.Component {
static get properties() {
return {
showList: {
type: Boolean,
defaultValue: true,
},
items: {
type: Array,
},
};
}

list = this.ownerDocument.createElement('ul');

render() {
if (this.showList) {
return DNA.h('ul', { ref: this.list }, [
...this.items.map((item) => DNA.h('li', null, item)),
]);
}
}
}
);

const element = document.createElement(name);
element.items = ['one', 'two', 'three'];
wrapper.appendChild(element);

expect(element.list.childNodes).toHaveLength(3);
expect(element.list.childNodes[0].textContent).toBe('one');
expect(element.list.childNodes[1].textContent).toBe('two');
expect(element.list.childNodes[2].textContent).toBe('three');

element.showList = false;
element.items = ['four', 'five'];

expect(element.list.isConnected).toBe(false);

element.showList = true;

expect(element.list.childNodes).toHaveLength(2);
expect(element.list.childNodes[0].textContent).toBe('four');
expect(element.list.childNodes[1].textContent).toBe('five');
});
});

describe('hooks', () => {
Expand Down
50 changes: 50 additions & 0 deletions test/template.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,56 @@ describe.runIf(typeof window !== 'undefined')(
}
});

describe('slot moved and replaced', () => {
const TEMPLATES = {
JSX(switchProp) {
return [
DNA.h('div', { class: 'parent-1' }, switchProp ? DNA.h('slot') : 'Empty'),
DNA.h('div', { class: 'parent-2' }, !switchProp ? DNA.h('slot') : 'Empty'),
];
},
HTML(switchProp) {
return DNA.html`
<div class="parent-1">${switchProp ? DNA.html`<slot />` : 'Empty'}</div>
<div class="parent-2">${!switchProp ? DNA.html`<slot />` : 'Empty'}</div>
`;
},
};

for (const type in TEMPLATES) {
it(type, () => {
const name = getComponentName();
DNA.define(
name,
class TestElement extends DNA.Component {
static get properties() {
return {
switch: {
type: Boolean,
},
};
}

render() {
return TEMPLATES[type](this.switch);
}
}
);

const element = document.createElement(name);
element.appendChild(document.createTextNode('Hello'));

expect(element.querySelector('.parent-1').textContent).toBe('Empty');
expect(element.querySelector('.parent-2').textContent).toBe('Hello');

element.switch = true;

expect(element.querySelector('.parent-1').textContent).toBe('Hello');
expect(element.querySelector('.parent-2').textContent).toBe('Empty');
});
}
});

describe('events', () => {
describe('should add an event listener', () => {
const TEMPLATES = {
Expand Down
Loading