diff --git a/src/core/vdom.ts b/src/core/vdom.ts
index f96ba4662..b173eee41 100644
--- a/src/core/vdom.ts
+++ b/src/core/vdom.ts
@@ -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;
}
@@ -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;
}
}
@@ -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);
@@ -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);
@@ -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: {
@@ -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);
@@ -2396,10 +2405,10 @@ 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 }
};
}
@@ -2407,7 +2416,7 @@ export function renderer(renderer: () => RenderResult): Renderer {
_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;
@@ -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);
}
diff --git a/tests/core/unit/vdom.tsx b/tests/core/unit/vdom.tsx
index 68cd8c367..537f1a0cc 100644
--- a/tests/core/unit/vdom.tsx
+++ b/tests/core/unit/vdom.tsx
@@ -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, '
My head Div 1
');
+ 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, 'My head Div 2
');
+ 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 (
+
+
+
+ );
+ });
+ const Head = factory(function Button({ children }) {
+ return (
+
+ {children()}
+
+ );
+ });
+ const App = factory(function App({ middleware }) {
+ const open = middleware.icache.getOrSet('open', false);
+ return (
+
+
first
+ {open &&
}
+ {open && Head}
+
+
+
+
+ );
+ });
+
+ 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,
+ ''
+ );
+ const headNode = document.getElementById('head-node');
+ assert.isNotNull(headNode);
+ assert.strictEqual(headNode!.outerHTML, 'Head
');
+ });
+
+ it('should detach nested head nodes from dom', () => {
+ let doShow: any;
+
+ class A extends WidgetBase {
+ 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 {
+ render() {
+ return v('div', [v('head', [w(B, {})])]);
+ }
+ }
+
+ class B extends WidgetBase {
+ 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 {
+ 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'])])]));