Skip to content

Commit

Permalink
feat(vdom): check return type for render functions in dev mode
Browse files Browse the repository at this point in the history
  • Loading branch information
localvoid committed May 14, 2018
1 parent fb10817 commit c479ca4
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 10 deletions.
26 changes: 25 additions & 1 deletion packages/ivi/__tests__/render_components.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
Component, VNode, statelessComponent, statefulComponent, getComponentInstanceFromVNode, isComponentAttached,
Component, VNode, statelessComponent, statefulComponent, getComponentInstanceFromVNode, isComponentAttached, fragment,
} from "ivi";
import * as h from "ivi-html";
import { startRender } from "./utils";
Expand Down Expand Up @@ -112,3 +112,27 @@ test(`stateful component should be in detached state when it is removed from the
expect(isComponentAttached(c!)).toBeFalsy();
});
});

test(`stateless component should raise an exception when render function returns children collection`, () => {
startRender<HTMLElement>((r) => {
const v = Stateless(
fragment(
h.div(),
h.div(),
)!,
);
expect(() => { r(v); }).toThrowError("singular");
});
});

test(`stateful component should raise an exception when render function returns children collection`, () => {
startRender<HTMLElement>((r) => {
const v = Stateful(
fragment(
h.div(),
h.div(),
)!,
);
expect(() => { r(v); }).toThrowError("singular");
});
});
16 changes: 16 additions & 0 deletions packages/ivi/__tests__/render_connect.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { fragment, connect } from "ivi";
import * as h from "ivi-html";
import { startRender } from "./utils";

test(`connect should raise an exception when render function returns children collection`, () => {
startRender<HTMLElement>((r) => {
const Connector = connect(
() => 1,
(props) => fragment(
h.div(),
h.div(),
)!,
);
expect(() => { r(Connector()); }).toThrowError("singular");
});
});
44 changes: 43 additions & 1 deletion packages/ivi/__tests__/sync_components.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, VNode, statelessComponent, statefulComponent } from "ivi";
import { Component, VNode, statelessComponent, statefulComponent, fragment } from "ivi";
import * as h from "ivi-html";
import { startRender, checkDOMOps, domOps } from "./utils";

Expand Down Expand Up @@ -483,3 +483,45 @@ test(`#20`, () => {
});
});
});

test(`stateless component should raise an exception when render function returns children collection`, () => {
startRender<HTMLElement>((r) => {
const v1 = (
Stateless(
h.div(),
)
);
const v2 = (
Stateless(
fragment(
h.div(),
h.div(),
)!,
)
);

r(v1);
expect(() => { r(v2); }).toThrowError("singular");
});
});

test(`stateful component should raise an exception when render function returns children collection`, () => {
startRender<HTMLElement>((r) => {
const v1 = (
Stateful(
h.div(),
)
);
const v2 = (
Stateful(
fragment(
h.div(),
h.div(),
)!,
)
);

r(v1);
expect(() => { r(v2); }).toThrowError("singular");
});
});
21 changes: 21 additions & 0 deletions packages/ivi/__tests__/sync_connect.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { fragment, connect } from "ivi";
import * as h from "ivi-html";
import { startRender } from "./utils";

test(`connect should raise an exception when render function returns children collection`, () => {
startRender<HTMLElement>((r) => {
let i = 0;
const Connector = connect(
() => i++,
(props) => (i === 1) ?
h.div() :
fragment(
h.div(),
h.div(),
)!,
);

r(Connector());
expect(() => { r(Connector()); }).toThrowError("singular");
});
});
69 changes: 61 additions & 8 deletions packages/ivi/src/vdom/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,15 @@ export function dirtyCheck(parent: Node, vnode: VNode, context: {}, dirtyContext
} else if ((flags & VNodeFlags.StatefulComponent) !== 0) {
instance = vnode.instance as Component<any>;
if (((instance as Component<any>).flags & ComponentFlags.Dirty) !== 0) {
syncVNode(parent, children, vnode.children = (instance as Component<any>).render(), context, dirtyContext);
syncVNode(
parent,
children,
vnode.children = DEBUG ?
shouldBeSingleVNode((instance as Component<any>).render()) :
/* istanbul ignore next */(instance as Component<any>).render(),
context,
dirtyContext,
);
(instance as Component<any>).flags &= ~ComponentFlags.Dirty;
(instance as Component<any>).updated(true);
deepUpdate = 1;
Expand All @@ -155,7 +163,15 @@ export function dirtyCheck(parent: Node, vnode: VNode, context: {}, dirtyContext
} else {
deepUpdate = 1;
vnode.instance = selectData;
syncVNode(parent, children, vnode.children = connect.render(selectData), context, dirtyContext);
syncVNode(
parent,
children,
vnode.children = DEBUG ?
shouldBeSingleVNode(connect.render(selectData)) :
/* istanbul ignore next */connect.render(selectData),
context,
dirtyContext,
);
}
} else {
if ((flags & VNodeFlags.UpdateContext) !== 0) {
Expand Down Expand Up @@ -304,20 +320,26 @@ function _render(parent: Node, vnode: VNode, context: {}): Node {
instance = node;
} else { // ((flags & VNodeFlags.StatefulComponent) !== 0)
const component = instance = new (vnode.tag as StatefulComponent<any>)(vnode.props);
const root = vnode.children = component.render();
const root = vnode.children = DEBUG ?
shouldBeSingleVNode(component.render()) :
/* istanbul ignore next */component.render();
node = _render(parent, root, context);
}
} else { // ((flags & (VNodeFlags.StatelessComponent | VNodeFlags.UpdateContext | VNodeFlags.Connect)) !== 0)
if ((flags & (VNodeFlags.UpdateContext | VNodeFlags.Connect)) !== 0) {
if ((flags & VNodeFlags.Connect) !== 0) {
const connect = (vnode.tag as ConnectDescriptor<any, any, {}>);
const selectData = instance = connect.select(null, vnode.props, context);
vnode.children = connect.render(selectData);
vnode.children = DEBUG ?
shouldBeSingleVNode(connect.render(selectData)) :
/* istanbul ignore next */connect.render(selectData);
} else {
context = instance = Object.assign({}, context, vnode.props);
}
} else {
vnode.children = (vnode.tag as StatelessComponent<any>).render(vnode.props);
vnode.children = DEBUG ?
shouldBeSingleVNode((vnode.tag as StatelessComponent<any>).render(vnode.props)) :
/* istanbul ignore next */(vnode.tag as StatelessComponent<any>).render(vnode.props);
}
node = _render(parent, vnode.children as VNode, context);
}
Expand Down Expand Up @@ -534,7 +556,15 @@ export function syncVNode(
((component.flags & ComponentFlags.Dirty) !== 0) ||
(component.shouldUpdate(oldProps, newProps) === true)
) {
syncVNode(parent, a.children as VNode, b.children = component.render(), context, dirtyContext);
syncVNode(
parent,
a.children as VNode,
b.children = DEBUG ?
shouldBeSingleVNode(component.render()) :
/* istanbul ignore next */component.render(),
context,
dirtyContext,
);
component.flags &= ~ComponentFlags.Dirty;
component.updated(true);
} else {
Expand All @@ -555,7 +585,15 @@ export function syncVNode(
b.children = a.children;
dirtyCheck(parent, b.children as VNode, context, dirtyContext);
} else {
syncVNode(parent, a.children as VNode, b.children = connect.render(selectData), context, dirtyContext);
syncVNode(
parent,
a.children as VNode,
b.children = DEBUG ?
shouldBeSingleVNode(connect.render(selectData)) :
/* istanbul ignore next */connect.render(selectData),
context,
dirtyContext,
);
}
} else {
if (a.props !== b.props) {
Expand All @@ -571,7 +609,15 @@ export function syncVNode(
(a.props !== b.props) &&
((bFlags & VNodeFlags.ShouldUpdateHint) === 0 || sc.shouldUpdate!(a.props, b.props) === true)
) {
syncVNode(parent, a.children as VNode, b.children = sc.render(b.props), context, dirtyContext);
syncVNode(
parent,
a.children as VNode,
b.children = DEBUG ?
shouldBeSingleVNode(sc.render(b.props)) :
/* istanbul ignore next */sc.render(b.props),
context,
dirtyContext,
);
} else {
b.children = a.children;
dirtyCheck(parent, b.children as VNode, context, dirtyContext);
Expand Down Expand Up @@ -1081,3 +1127,10 @@ function lis(a: number[]): number[] {

return result;
}

function shouldBeSingleVNode<T extends VNode>(vnode: T): T {
if (vnode.prev !== vnode) {
throw new Error("Invalid render function. Render function should return singular VNode.");
}
return vnode;
}

0 comments on commit c479ca4

Please sign in to comment.