Skip to content

Commit

Permalink
fix(patch): not working if oldVNode is not present
Browse files Browse the repository at this point in the history
  • Loading branch information
aidenybai committed Jul 15, 2021
1 parent 8ee46e7 commit 1b55e5e
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 12 deletions.
4 changes: 3 additions & 1 deletion scripts/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ source $(dirname "$0")/helpers.sh

rm -rf dist
rollup -c
mv dist/types/million.min.d.ts dist/million.d.ts
mv dist/types/million.esm.d.ts dist/million.d.ts
rm -rf dist/types

sed -i '' 's/export type/export/g' dist/million.d.ts

info "Dist: `ls -xm -d dist/*`"
13 changes: 11 additions & 2 deletions src/__test__/m.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { className, m, ns, svg, style } from '../m';
import { VNode, VProps } from '../structs';
import { className, m, ns, svg, style, INSERT, DELETE, UPDATE } from '../m';
import { VNode, VProps, VDeltaOperationTypes } from '../structs';

const h = (tag: string, props?: VProps, ...children: VNode[]) =>
m(
Expand Down Expand Up @@ -208,4 +208,13 @@ describe('.m', () => {
key: 'foo',
});
});

it('should return delta operation when operation helper is used', () => {
expect(INSERT()).toEqual([VDeltaOperationTypes.INSERT, 0]);
expect(INSERT(5)).toEqual([VDeltaOperationTypes.INSERT, 5]);
expect(UPDATE()).toEqual([VDeltaOperationTypes.UPDATE, 0]);
expect(UPDATE(5)).toEqual([VDeltaOperationTypes.UPDATE, 5]);
expect(DELETE()).toEqual([VDeltaOperationTypes.DELETE, 0]);
expect(DELETE(5)).toEqual([VDeltaOperationTypes.DELETE, 5]);
});
});
75 changes: 73 additions & 2 deletions src/__test__/patch.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createElement } from '../createElement';
import { m } from '../m';
import { m, INSERT, UPDATE, DELETE } from '../m';
import { patch, patchChildren, patchProps } from '../patch';
import { VNode, VProps } from '../structs';
import { VFlags, VNode, VProps } from '../structs';

const h = (tag: string, props?: VProps, ...children: VNode[]) =>
m(
Expand Down Expand Up @@ -107,4 +107,75 @@ describe('.patch', () => {

expect([...el.childNodes]).toEqual(virtualArrayToDOMNodes(['foo', m('div'), 'baz']));
});

// Deltas are behaving weird because they are "delayed" patching
it('should execute INSERT deltas', () => {
const el = document.createElement('div');
const children: string[] = [];
const createVNode = () => m('div', undefined, [...children], undefined, [INSERT(0)]);

children.unshift('foo');
patch(el, createVNode());
expect(el.childNodes.length).toEqual(1);

children.unshift('bar');
patch(el, createVNode());
expect(el.childNodes.length).toEqual(2);

children.unshift('baz');
patch(el, createVNode());
expect(el.childNodes.length).toEqual(3);
});

it('should execute UPDATE deltas', () => {
const el = document.createElement('div');
el.textContent = 'foo';
const children: string[] = ['foo'];
const createVNode = () => m('div', undefined, [...children], undefined, [UPDATE(0)]);

children[0] = 'bar';
patch(el, createVNode());
expect(el.textContent).toEqual('bar');

children[0] = 'baz';
patch(el, createVNode());
expect(el.textContent).toEqual('baz');
});

it('should execute INSERT deltas', () => {
const el = document.createElement('div');
const children: string[] = [];
const createVNode = () => m('div', undefined, [...children], undefined, [INSERT(0)]);

children.unshift('bar');
patch(el, createVNode());
expect(el.childNodes.length).toEqual(1);

children.unshift('baz');
patch(el, createVNode());
expect(el.childNodes.length).toEqual(2);
});

it('should execute DELETE deltas', () => {
const el = document.createElement('div');
el.appendChild(document.createTextNode('foo'));
el.appendChild(document.createTextNode('bar'));
el.appendChild(document.createTextNode('baz'));
const children: string[] = ['foo', 'bar', 'baz'];
const createVNode = () => m('div', undefined, [...children], undefined, [DELETE(0)]);

children.splice(0, 1);
patch(el, createVNode());
expect(el.firstChild!.nodeValue).toEqual('bar');

children.splice(0, 1);
patch(el, createVNode());
expect(el.firstChild!.nodeValue).toEqual('baz');
});

it('should shortcut if flags are present', () => {
const el = document.createElement('div');
patch(el, m('div', undefined, ['foo'], VFlags.ONLY_TEXT_CHILDREN));
expect(el.textContent).toEqual('foo');
});
});
2 changes: 1 addition & 1 deletion src/createElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { OLD_VNODE_FIELD, VNode } from './structs';
*/
export const createElement = (vnode: VNode, attachField = true): HTMLElement | Text => {
if (typeof vnode === 'string') return document.createTextNode(vnode);
const el = Object.assign(document.createElement(vnode.tag), vnode.props);
const el = <HTMLElement>Object.assign(document.createElement(vnode.tag), vnode.props);

if (vnode.children) {
vnode.children.forEach((child) => {
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { OLD_VNODE_FIELD, VElement, VFlags, VNode, VProps } from './structs';
export type { VElement, VFlags, VNode, VProps } from './structs';
export { OLD_VNODE_FIELD } from './structs';
export { createElement } from './createElement';
export { className, DELETE, INSERT, m, style, svg, UPDATE } from './m';
export { patch, patchChildren, patchProps } from './patch';
14 changes: 10 additions & 4 deletions src/patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const patchChildren = (
case VDeltaOperationTypes.INSERT: {
el.insertBefore(
createElement(newVNodeChildren[deltaPosition]),
el.childNodes[deltaPosition + 1],
el.childNodes[deltaPosition],
);
break;
}
Expand Down Expand Up @@ -131,23 +131,29 @@ export const patch = (
// remove props/children
return replaceElementWithVNode(el, newVNode);
}
if (oldVNode && !(el instanceof Text)) {
patchProps(el, (<VElement>oldVNode).props || {}, (<VElement>newVNode).props || {});
if (!(el instanceof Text)) {
patchProps(el, (<VElement>oldVNode)?.props || {}, (<VElement>newVNode).props || {});

// Flags allow for greater optimizability by reducing condition branches.
// Generally, you should use a compiler to generate these flags, but
// hand-writing them is also possible
switch (<VFlags>(<VElement>newVNode).flag) {
case VFlags.NO_CHILDREN: {
el.textContent = '';
break;
}
case VFlags.ONLY_TEXT_CHILDREN: {
// Joining is faster than setting textContent to an array
el.textContent = <string>(<VElement>newVNode).children!.join('');
break;
}
default: {
patchChildren(
el,
(<VElement>oldVNode).children || [],
(<VElement>oldVNode)?.children || [],
(<VElement>newVNode).children!,
// We need to pass delta here because this function does not have
// a reference to the actual vnode.
(<VElement>newVNode).delta,
);
break;
Expand Down
3 changes: 2 additions & 1 deletion src/structs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
*/
export const OLD_VNODE_FIELD = '__m_old_vnode';

export type VProps = Record<string, string | unknown | (() => void)>;
// Props can contain a standard value or a callback function (for events)
export type VProps = Record<string, string | boolean | (() => void)>;
export type VNode = VElement | string;
export type VDeltaOperation = [VDeltaOperationTypes, number];
export type VDelta = VDeltaOperation[];
Expand Down

0 comments on commit 1b55e5e

Please sign in to comment.