Skip to content

Commit

Permalink
support special head tag for rendering node in the document head (#805)
Browse files Browse the repository at this point in the history
  • Loading branch information
agubler authored Jun 15, 2020
1 parent c70b240 commit e5aa994
Show file tree
Hide file tree
Showing 2 changed files with 279 additions and 18 deletions.
45 changes: 27 additions & 18 deletions src/core/vdom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,14 @@ function isBodyWrapper(wrapper?: DNodeWrapper): boolean {
return isVNodeWrapper(wrapper) && wrapper.node.tag === 'body';
}

function isHeadWrapper(wrapper?: DNodeWrapper): boolean {
return isVNodeWrapper(wrapper) && wrapper.node.tag === 'head';
}

function isSpecialWrapper(wrapper?: DNodeWrapper): boolean {
return isHeadWrapper(wrapper) || isBodyWrapper(wrapper) || isVirtualWrapper(wrapper);
}

function isAttachApplication(value: any): value is AttachApplication | DetachApplication {
return !!value.type;
}
Expand Down Expand Up @@ -1392,7 +1400,7 @@ export function renderer(renderer: () => RenderResult): Renderer {
}
if (nextSibling.childDomWrapperId) {
const childWrapper = _idToWrapperMap.get(nextSibling.childDomWrapperId);
if (childWrapper && !isBodyWrapper(childWrapper)) {
if (childWrapper && !isBodyWrapper(childWrapper) && !isHeadWrapper(childWrapper)) {
domNode = childWrapper.domNode;
}
}
Expand Down Expand Up @@ -2285,6 +2293,7 @@ export function renderer(renderer: () => RenderResult): Renderer {
const parentDomNode = findParentDomNode(next)!;
const isVirtual = isVirtualWrapper(next);
const isBody = isBodyWrapper(next);
const isHead = isHeadWrapper(next);
let mergeNodes: Node[] = [];
next.id = `${wrapperId++}`;
_idToWrapperMap.set(next.id, next);
Expand All @@ -2297,6 +2306,8 @@ export function renderer(renderer: () => RenderResult): Renderer {
}
if (isBody) {
next.domNode = global.document.body;
} else if (isHead) {
next.domNode = global.document.head;
} else if (next.node.tag && !isVirtual) {
if (next.namespace) {
next.domNode = global.document.createElementNS(next.namespace, next.node.tag);
Expand Down Expand Up @@ -2332,14 +2343,13 @@ export function renderer(renderer: () => RenderResult): Renderer {
_idToChildrenWrappers.set(next.id, children);
}
}
const dom: ApplicationInstruction | undefined =
isVirtual || isBody
? undefined
: {
next: next!,
parentDomNode: parentDomNode,
type: 'create'
};
const dom: ApplicationInstruction | undefined = isSpecialWrapper(next)
? undefined
: {
next: next!,
parentDomNode: parentDomNode,
type: 'create'
};
if (children) {
return {
item: {
Expand Down Expand Up @@ -2380,8 +2390,7 @@ export function renderer(renderer: () => RenderResult): Renderer {
}

function _removeDom({ current }: RemoveDomInstruction): ProcessResult {
const isVirtual = isVirtualWrapper(current);
const isBody = isBodyWrapper(current);
const isSpecial = isSpecialWrapper(current);
const children = _idToChildrenWrappers.get(current.id);
_idToChildrenWrappers.delete(current.id);
_idToWrapperMap.delete(current.id);
Expand All @@ -2396,18 +2405,18 @@ export function renderer(renderer: () => RenderResult): Renderer {
instanceData && instanceData.nodeHandler.remove(current.node.properties.key);
}
}
if (current.hasAnimations || isVirtual || isBody) {
if (current.hasAnimations || isSpecial) {
return {
item: { current: children, meta: {} },
dom: isVirtual || isBody ? undefined : { type: 'delete', current }
dom: isSpecial ? undefined : { type: 'delete', current }
};
}

if (children) {
_deferredRenderCallbacks.push(() => {
let wrappers = children || [];
let wrapper: DNodeWrapper | undefined;
let bodyIds = [];
let specialIds = [];
while ((wrapper = wrappers.pop())) {
if (isWNodeWrapper(wrapper)) {
wrapper = getWNodeWrapper(wrapper.id) || wrapper;
Expand All @@ -2428,11 +2437,11 @@ export function renderer(renderer: () => RenderResult): Renderer {
if (wrapperChildren) {
wrappers.push(...wrapperChildren);
}
if (isBodyWrapper(wrapper)) {
bodyIds.push(wrapper.id);
} else if (bodyIds.indexOf(wrapper.parentId) !== -1) {
if (isBodyWrapper(wrapper) || isHeadWrapper(wrapper)) {
specialIds.push(wrapper.id);
} else if (specialIds.indexOf(wrapper.parentId) !== -1) {
if (isWNodeWrapper(wrapper) || isVirtualWrapper(wrapper)) {
bodyIds.push(wrapper.id);
specialIds.push(wrapper.id);
} else if (wrapper.domNode && wrapper.domNode.parentNode) {
wrapper.domNode.parentNode.removeChild(wrapper.domNode);
}
Expand Down
252 changes: 252 additions & 0 deletions tests/core/unit/vdom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4850,6 +4850,258 @@ jsdomDescribe('vdom', () => {
});
});

describe('head node', () => {
let root = document.createElement('div');
beforeEach(() => {
root = document.createElement('div');
document.body.appendChild(root);
});

afterEach(() => {
document.body.removeChild(root);
});

it('can attach a node to the head', () => {
let show = true;
const factory = create({ invalidator });
const App = factory(function App({ middleware: { invalidator } }) {
return v('div', [
v('button', {
onclick: () => {
show = !show;
invalidator();
}
}),
v('head', [show ? v('div', { id: 'my-head-node-1' }, ['My head Div 1']) : null]),
v('head', [show ? v('div', { id: 'my-head-node-2' }, ['My head Div 2']) : null])
]);
});
const r = renderer(() => w(App, {}));
r.mount({ domNode: root });
let headNodeOne = document.getElementById('my-head-node-1')!;
assert.isOk(headNodeOne);
assert.strictEqual(headNodeOne.outerHTML, '<div id="my-head-node-1">My head Div 1</div>');
assert.strictEqual(headNodeOne.parentNode, document.head);
assert.isNull(root.querySelector('#my-head-node-1'));
let headNodeTwo = document.getElementById('my-head-node-2')!;
assert.isOk(headNodeTwo);
assert.strictEqual(headNodeTwo.outerHTML, '<div id="my-head-node-2">My head Div 2</div>');
assert.strictEqual(headNodeTwo.parentNode, document.head);
assert.isNull(root.querySelector('#my-head-node-2'));
sendEvent(root.childNodes[0].childNodes[0] as Element, 'click');
resolvers.resolve();
headNodeOne = document.getElementById('my-head-node-1')!;
assert.isNull(headNodeOne);
assert.isNull(root.querySelector('#my-head-node-1'));
headNodeTwo = document.getElementById('my-head-node-2')!;
assert.isNull(headNodeTwo);
assert.isNull(root.querySelector('#my-head-node-2'));
});

it('can attach head and have widgets inserted nodes that are positioned after the head', () => {
const factory = create({ icache });
const Button = factory(function Button({ children }) {
return (
<div>
<button>{children()}</button>
</div>
);
});
const Head = factory(function Button({ children }) {
return (
<head>
<div id="head-node">{children()}</div>
</head>
);
});
const App = factory(function App({ middleware }) {
const open = middleware.icache.getOrSet('open', false);
return (
<div>
<div>first</div>
{open && <Button>Close</Button>}
{open && <Head>Head</Head>}
<div>
<button
onclick={() => {
middleware.icache.set('open', !middleware.icache.getOrSet('open', false));
}}
>
Click Me
</button>
</div>
</div>
);
});

const r = renderer(() => w(App, {}));
r.mount({ domNode: root });
(root as any).children[0].children[1].children[0].click();
resolvers.resolve();
assert.strictEqual(
root.innerHTML,
'<div><div>first</div><div><button>Close</button></div><div><button>Click Me</button></div></div>'
);
const headNode = document.getElementById('head-node');
assert.isNotNull(headNode);
assert.strictEqual(headNode!.outerHTML, '<div id="head-node">Head</div>');
});

it('should detach nested head nodes from dom', () => {
let doShow: any;

class A extends WidgetBase<any> {
render() {
return v('div', [v('head', [v('span', { classes: ['head-span'] }, ['and im in the head!'])])]);
}
}

class App extends WidgetBase {
private renderWidget = false;

constructor() {
super();
doShow = () => {
this.renderWidget = !this.renderWidget;
this.invalidate();
};
}

protected render() {
return v('div', [this.renderWidget && w(A, {})]);
}
}

const r = renderer(() => w(App, {}));
r.mount({ domNode: root });

let results = document.querySelectorAll('.head-span');
assert.lengthOf(results, 0);
doShow();
resolvers.resolveRAF();
resolvers.resolveRAF();
results = document.querySelectorAll('.head-span');
assert.lengthOf(results, 1);
doShow();
resolvers.resolveRAF();
resolvers.resolveRAF();
results = document.querySelectorAll('.head-span');
assert.lengthOf(results, 0);
doShow();
resolvers.resolveRAF();
resolvers.resolveRAF();
results = document.querySelectorAll('.head-span');
assert.lengthOf(results, 1);
doShow();
resolvers.resolveRAF();
resolvers.resolveRAF();
results = document.querySelectorAll('.head-span');
assert.lengthOf(results, 0);
});

it('should detach widgets nested in a head tag', () => {
let doShow: any;

class A extends WidgetBase<any> {
render() {
return v('div', [v('head', [w(B, {})])]);
}
}

class B extends WidgetBase<any> {
render() {
return v('span', { classes: ['head-span'] }, ['and im in the head!!']);
}
}

class App extends WidgetBase {
private show = true;

constructor() {
super();
doShow = () => {
this.show = !this.show;
this.invalidate();
};
}

protected render() {
return v('div', [this.show && w(A, {})]);
}
}

const r = renderer(() => w(App, {}));
r.mount({ domNode: root });

let results = document.querySelectorAll('.head-span');
assert.lengthOf(results, 1);
doShow();
resolvers.resolveRAF();
resolvers.resolveRAF();
results = document.querySelectorAll('.head-span');
assert.lengthOf(results, 0);
doShow();
resolvers.resolveRAF();
resolvers.resolveRAF();
results = document.querySelectorAll('.head-span');
assert.lengthOf(results, 1);
doShow();
resolvers.resolveRAF();
resolvers.resolveRAF();
results = document.querySelectorAll('.head-span');
assert.lengthOf(results, 0);
});

it('should detach virtual nodes nested in a head tag', () => {
let doShow: any;

class A extends WidgetBase<any> {
render() {
return v('div', [
v('head', [v('virtual', [v('span', { classes: ['head-span'] }, ['and im in the head!!'])])])
]);
}
}

class App extends WidgetBase {
private show = true;

constructor() {
super();
doShow = () => {
this.show = !this.show;
this.invalidate();
};
}

protected render() {
return v('div', [this.show && w(A, {})]);
}
}

const r = renderer(() => w(App, {}));
r.mount({ domNode: root });

let results = document.querySelectorAll('.head-span');
assert.lengthOf(results, 1);
doShow();
resolvers.resolveRAF();
resolvers.resolveRAF();
results = document.querySelectorAll('.head-span');
assert.lengthOf(results, 0);
doShow();
resolvers.resolveRAF();
resolvers.resolveRAF();
results = document.querySelectorAll('.head-span');
assert.lengthOf(results, 1);
doShow();
resolvers.resolveRAF();
resolvers.resolveRAF();
results = document.querySelectorAll('.head-span');
assert.lengthOf(results, 0);
});
});

describe('virtual node', () => {
it('can use a virtual node', () => {
const [Widget, meta] = getWidget(v('virtual', [v('div', ['one', 'two', v('div', ['three'])])]));
Expand Down

0 comments on commit e5aa994

Please sign in to comment.