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

Support special head tag for rendering node in the document head #805

Merged
merged 1 commit into from
Jun 15, 2020
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
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