Skip to content

Commit

Permalink
Custom element slots (#714)
Browse files Browse the repository at this point in the history
  • Loading branch information
rorticus authored Apr 6, 2020
1 parent 46c893e commit 7393784
Show file tree
Hide file tree
Showing 3 changed files with 297 additions and 3 deletions.
64 changes: 61 additions & 3 deletions src/core/registerCustomElement.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Registry from './Registry';
import { renderer, w, dom, isTextNode, create as vdomCreate, diffProperty, invalidator } from './vdom';
import { create as vdomCreate, diffProperty, dom, invalidator, isTextNode, renderer, w } from './vdom';
import { from } from '../shim/array';
import global from '../shim/global';
import Injector from './Injector';
import { VNode, WNode } from './interfaces';

const RESERVED_PROPS = ['focus'];

Expand Down Expand Up @@ -246,13 +247,70 @@ export function create(descriptor: any, WidgetConstructor: any): any {
}

public __children__() {
const wrap = (domNode: HTMLElement, node: WNode | VNode) => {
const w = (...args: any[]) => {
if (args.length) {
setTimeout(() => {
domNode.dispatchEvent(
new CustomEvent('render', {
bubbles: false,
detail: args
})
);
});
}
return node;
};

Object.keys(node).forEach((key) => ((w as any)[key] = (node as any)[key]));

return w;
};

if (this._children.some((child) => child.domNode.getAttribute && child.domNode.getAttribute('slot'))) {
const slots = this._children.reduce((slots, child) => {
const { domNode } = child;

const slotName = domNode.getAttribute && domNode.getAttribute('slot');

if (!slotName) {
return slots;
}

let slotResult = wrap(
child.domNode,
child.domNode.isWidget
? w(child, { ...domNode.__properties__() }, [...domNode.__children__()])
: dom({ node: domNode, diffType: 'dom' })
);

const existingSlotValue = slots[slotName];

return {
...slots,
[slotName]: existingSlotValue ? [...existingSlotValue, slotResult] : [slotResult]
};
}, {});

return [
Object.keys(slots).reduce((result, key) => {
const value = slots[key];

return {
...result,
[key]: value.length === 1 ? value[0] : value
};
}, {})
];
}

if (this._childType === CustomElementChildType.DOJO) {
return this._children.filter((Child) => Child.domNode.isWidget).map((Child: any) => {
const { domNode } = Child;
return w(Child, { ...domNode.__properties__() }, [...domNode.__children__()]);
return wrap(domNode, w(Child, { ...domNode.__properties__() }, [...domNode.__children__()]));
});
} else {
return this._children;
return this._children.map((child) => wrap(child.domNode, child));
}
}

Expand Down
17 changes: 17 additions & 0 deletions tests/core/support/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { stub, SinonStub } from 'sinon';
export function createResolvers() {
let rAFStub: SinonStub;
let rICStub: SinonStub;
let timeoutStub: SinonStub;

function resolveRAFCallbacks() {
const calls = rAFStub.getCalls();
Expand All @@ -21,10 +22,21 @@ export function createResolvers() {
}
}

function resolveTimeoutCallbacks() {
if (timeoutStub) {
const calls = timeoutStub.getCalls();
timeoutStub.resetHistory();
for (let i = 0; i < calls.length; i++) {
calls[i].callArg(0);
}
}
}

return {
resolve() {
resolveRAFCallbacks();
resolveRICCallbacks();
resolveTimeoutCallbacks();
},
resolveRIC() {
resolveRICCallbacks();
Expand All @@ -36,13 +48,18 @@ export function createResolvers() {
rAFStub = stub(global, 'requestAnimationFrame').returns(1);
if (global.requestIdleCallback) {
rICStub = stub(global, 'requestIdleCallback').returns(1);
timeoutStub = stub(global, 'setTimeout').returns(1);
} else {
rICStub = stub(global, 'setTimeout').returns(1);
}
},
restore() {
rAFStub.restore();
rICStub.restore();

if (timeoutStub) {
timeoutStub.restore();
}
}
};
}
219 changes: 219 additions & 0 deletions tests/core/unit/registerCustomElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { waitFor } from './waitFor';

const { describe, it, beforeEach, afterEach, before } = intern.getInterface('bdd');
const { assert } = intern.getPlugin('chai');
import { stub } from 'sinon';

@customElement({
tag: 'foo-element'
Expand Down Expand Up @@ -567,4 +568,222 @@ describe('registerCustomElement', () => {
);
console.log(element.outerHTML);
});

it('transforms children with slots into a child object', () => {
@customElement({
tag: 'parent-element',
properties: [],
attributes: [],
events: []
})
class WidgetA extends WidgetBase<any> {
render() {
const child = this.children[0];

return v('div', {}, [
v('div', { classes: ['a-slot'] }, [child && (child as any).a]),
v('div', { classes: ['b-slot'] }, [child && (child as any).b && (child as any).b()])
]);
}
}

@customElement({
tag: 'slot-b-element',
properties: [],
attributes: [],
events: []
})
class WidgetB extends WidgetBase<any> {
render() {
return 'WidgetB';
}
}

const CustomElement = create((WidgetA as any).__customElementDescriptor, WidgetA);
const CustomElementB = create((WidgetB as any).__customElementDescriptor, WidgetB);

customElements.define('parent-element', CustomElement);
customElements.define('slot-b-element', CustomElementB);

const element = document.createElement('parent-element');

const slotChild = document.createElement('div');
slotChild.setAttribute('slot', 'a');
slotChild.innerHTML = 'test';

const slotBChild = document.createElement('slot-b-element');
slotBChild.setAttribute('slot', 'b');

element.appendChild(slotChild);
element.appendChild(slotBChild);
document.body.appendChild(element);

resolvers.resolve();

assert.strictEqual(
element.outerHTML,
'<parent-element style="display: block;"><div><div class="a-slot"><div slot="a">test</div></div><div class="b-slot"><slot-b-element slot="b">WidgetB</slot-b-element></div></div></parent-element>'
);
});

it('wraps child nodes in render functions', () => {
@customElement({
tag: 'render-func-element',
properties: [],
attributes: [],
events: []
})
class WidgetA extends WidgetBase<any> {
render() {
const child: any = this.children[0];

return v('div', {}, [child && child()]);
}
}

const CustomElement = create((WidgetA as any).__customElementDescriptor, WidgetA);

customElements.define('render-func-element', CustomElement);

const element = document.createElement('render-func-element');

const slotChild = document.createElement('label');
slotChild.innerHTML = 'test';

element.appendChild(slotChild);
document.body.appendChild(element);

resolvers.resolve();

assert.strictEqual(
element.outerHTML,
'<render-func-element style="display: block;"><div><label>test</label></div></render-func-element>'
);
});

it('combines children with the same slot name into an array', () => {
@customElement({
tag: 'slot-array-element',
properties: [],
attributes: [],
events: []
})
class WidgetA extends WidgetBase<any> {
render() {
const child: any = this.children[0];

return v('div', {}, child.foo);
}
}

const CustomElement = create((WidgetA as any).__customElementDescriptor, WidgetA);

customElements.define('slot-array-element', CustomElement);

const element = document.createElement('slot-array-element');

const slotChild1 = document.createElement('label');
slotChild1.setAttribute('slot', 'foo');
slotChild1.innerHTML = 'test1';
const slotChild2 = document.createElement('label');
slotChild2.setAttribute('slot', 'foo');
slotChild2.innerHTML = 'test2';

element.appendChild(slotChild1);
element.appendChild(slotChild2);
document.body.appendChild(element);

resolvers.resolve();

assert.strictEqual(
element.outerHTML,
'<slot-array-element style="display: block;"><div><label slot="foo">test1</label><label slot="foo">test2</label></div></slot-array-element>'
);
});

it('ignores elements with no slots when at least one element has a slot', () => {
@customElement({
tag: 'ignore-slot-element',
properties: [],
attributes: [],
events: []
})
class WidgetA extends WidgetBase<any> {
render() {
const child: any = this.children[0];

return v('div', {}, [child.foo]);
}
}

const CustomElement = create((WidgetA as any).__customElementDescriptor, WidgetA);

customElements.define('ignore-slot-element', CustomElement);

const element = document.createElement('ignore-slot-element');

const slotChild1 = document.createElement('label');
slotChild1.setAttribute('slot', 'foo');
slotChild1.innerHTML = 'test1';
const slotChild2 = document.createElement('label');
slotChild2.innerHTML = 'test2';

element.appendChild(slotChild1);
element.appendChild(slotChild2);
document.body.appendChild(element);

resolvers.resolve();

assert.strictEqual(
element.outerHTML,
'<ignore-slot-element style="display: inline;"><label>test2</label><div><label slot="foo">test1</label></div></ignore-slot-element>'
);
});

it('dispatches events when child render funcs have arguments', async () => {
const eventStub = stub();

@customElement({
tag: 'dispatch-element',
properties: [],
attributes: [],
events: []
})
class WidgetA extends WidgetBase<any> {
render() {
const child: any = this.children[0];

return v('div', {}, [child.foo(15)]);
}
}

const CustomElement = create((WidgetA as any).__customElementDescriptor, WidgetA);

customElements.define('dispatch-element', CustomElement);

const element = document.createElement('dispatch-element');

const slotChild1 = document.createElement('label');
slotChild1.setAttribute('slot', 'foo');
slotChild1.innerHTML = 'test1';

slotChild1.addEventListener('render', (event: any) => {
eventStub(event.detail[0]);
});

element.appendChild(slotChild1);
document.body.appendChild(element);

// this one to render
resolvers.resolve();
// this one to call the event dispatch
resolvers.resolve();

assert.strictEqual(
element.outerHTML,
'<dispatch-element style="display: block;"><div><label slot="foo">test1</label></div></dispatch-element>'
);

assert.isTrue(eventStub.calledWith(15));
});
});

0 comments on commit 7393784

Please sign in to comment.