Skip to content

Commit

Permalink
refactor: add missing SlotController logic and tests (#3179)
Browse files Browse the repository at this point in the history
  • Loading branch information
web-padawan authored Dec 13, 2021
1 parent 47b04f6 commit 0d16dae
Show file tree
Hide file tree
Showing 3 changed files with 267 additions and 2 deletions.
5 changes: 5 additions & 0 deletions packages/component-base/src/slot-controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,10 @@ export class SlotController implements ReactiveController {
*/
node: HTMLElement;

/**
* Return a reference to the node managed by the controller.
*/
getSlotChild(): Node;

protected initialized: boolean;
}
21 changes: 19 additions & 2 deletions packages/component-base/src/slot-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ export class SlotController {
if (!this.initialized) {
const { host, slotName, slotFactory, slotInitializer } = this;

const slotted = host.querySelector(`[slot=${slotName}]`);
const slotted = this.getSlotChild();

if (!slotted) {
// Slot factory is optional, some slots don't have default content.
if (slotFactory) {
const slotContent = slotFactory(host);
if (slotContent instanceof Element) {
slotContent.setAttribute('slot', slotName);
if (slotName !== '') {
slotContent.setAttribute('slot', slotName);
}
host.appendChild(slotContent);
this.node = slotContent;
}
Expand All @@ -40,4 +42,19 @@ export class SlotController {
this.initialized = true;
}
}

/**
* Return a reference to the node managed by the controller.
* @return {Node}
*/
getSlotChild() {
const { slotName } = this;
return Array.from(this.host.childNodes).find((node) => {
// Either an element (any slot) or a text node (only un-named slot).
return (
(node.nodeType === Node.ELEMENT_NODE && node.slot === slotName) ||
(node.nodeType === Node.TEXT_NODE && node.textContent.trim() && slotName === '')
);
});
}
}
243 changes: 243 additions & 0 deletions packages/component-base/test/slot-controller.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { expect } from '@esm-bundle/chai';
import { fixtureSync } from '@vaadin/testing-helpers';
import sinon from 'sinon';
import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
import { ControllerMixin } from '../src/controller-mixin.js';
import { SlotController } from '../src/slot-controller.js';

customElements.define(
'slot-controller-element',
class extends ControllerMixin(PolymerElement) {
static get template() {
return html`
<slot name="foo"></slot>
<slot></slot>
`;
}
}
);

describe('slot-controller', () => {
let element, child, controller, initializeSpy;

describe('named slot', () => {
describe('default content', () => {
beforeEach(() => {
element = fixtureSync('<slot-controller-element></slot-controller-element>');
initializeSpy = sinon.spy();
controller = new SlotController(
element,
'foo',
() => {
const div = document.createElement('div');
div.textContent = 'foo';
return div;
},
initializeSpy
);
element.addController(controller);
child = element.querySelector('[slot="foo"]');
});

it('should append an element to named slot', () => {
expect(child).to.be.ok;
expect(child.textContent).to.equal('foo');
});

it('should store a reference to named slot child', () => {
expect(controller.node).to.equal(child);
});

it('should return a reference to named slot child', () => {
expect(controller.getSlotChild()).to.equal(child);
});

it('should run initializer for named slot child', () => {
expect(initializeSpy.calledOnce).to.be.true;
expect(initializeSpy.firstCall.args[0]).to.equal(element);
expect(initializeSpy.firstCall.args[1]).to.equal(child);
});
});

describe('custom content', () => {
beforeEach(() => {
element = fixtureSync(`
<slot-controller-element>
<div slot="foo">bar</div>
</slot-controller-element>
`);
// Get element reference before adding the controller
child = element.querySelector('[slot="foo"]');
initializeSpy = sinon.spy();
controller = new SlotController(
element,
'foo',
() => {
const div = document.createElement('div');
div.textContent = 'foo';
return div;
},
initializeSpy
);
element.addController(controller);
});

it('should not override an element passed to named slot', () => {
expect(child).to.be.ok;
expect(child.textContent).to.equal('bar');
});

it('should store a reference to the custom slot child', () => {
expect(controller.node).to.equal(child);
});

it('should return a reference to custom named slot child', () => {
expect(controller.getSlotChild()).to.equal(child);
});

it('should run initializer for custom named slot child', () => {
expect(initializeSpy.calledOnce).to.be.true;
expect(initializeSpy.firstCall.args[0]).to.equal(element);
expect(initializeSpy.firstCall.args[1]).to.equal(child);
});
});
});

describe('un-named slot', () => {
describe('default content', () => {
beforeEach(() => {
element = fixtureSync('<slot-controller-element></slot-controller-element>');
initializeSpy = sinon.spy();
controller = new SlotController(
element,
'',
() => {
const div = document.createElement('div');
div.textContent = 'bar';
return div;
},
initializeSpy
);
element.addController(controller);
child = element.querySelector(':not([slot])');
});

it('should append an element to un-named slot', () => {
expect(child).to.be.ok;
expect(child.textContent).to.equal('bar');
});

it('should store a reference to un-named slot child', () => {
expect(controller.node).to.equal(child);
});

it('should return a reference to un-named slot child', () => {
expect(controller.getSlotChild()).to.equal(child);
});

it('should run initializer for un-named slot child', () => {
expect(initializeSpy.calledOnce).to.be.true;
expect(initializeSpy.firstCall.args[0]).to.equal(element);
expect(initializeSpy.firstCall.args[1]).to.equal(child);
});
});

describe('custom element', () => {
beforeEach(() => {
element = fixtureSync(`
<slot-controller-element>
<div>baz</div>
</slot-controller-element>
`);
// Get element reference before adding the controller
child = element.querySelector(':not([slot])');
initializeSpy = sinon.spy();
controller = new SlotController(
element,
'',
() => {
const div = document.createElement('div');
div.textContent = 'bar';
return div;
},
initializeSpy
);
element.addController(controller);
});

it('should not override an element passed to un-named slot', () => {
expect(child).to.be.ok;
expect(child.textContent).to.equal('baz');
});

it('should store a reference to element passed to un-named slot', () => {
expect(controller.node).to.equal(child);
});

it('should return a reference to element passed to un-named slot', () => {
expect(controller.getSlotChild()).to.equal(child);
});

it('should run initializer for un-named slot element', () => {
expect(initializeSpy.calledOnce).to.be.true;
expect(initializeSpy.firstCall.args[0]).to.equal(element);
expect(initializeSpy.firstCall.args[1]).to.equal(child);
});
});

describe('custom text node', () => {
beforeEach(() => {
element = fixtureSync('<slot-controller-element>baz</slot-controller-element>');
initializeSpy = sinon.spy();
controller = new SlotController(
element,
'',
() => {
const div = document.createElement('div');
div.textContent = 'bar';
return div;
},
initializeSpy
);
element.addController(controller);
// Check last child to ensure no custom node is added.
child = element.lastChild;
});

it('should not override a text node passed to un-named slot', () => {
expect(child).to.be.ok;
expect(child.textContent).to.equal('baz');
});

it('should store a reference to the slotted text node', () => {
expect(controller.node).to.equal(child);
});

it('should return a reference to the slotted text node', () => {
expect(controller.getSlotChild()).to.equal(child);
});

it('should run initializer for the slotted text node', () => {
expect(initializeSpy.calledOnce).to.be.true;
expect(initializeSpy.firstCall.args[0]).to.equal(element);
expect(initializeSpy.firstCall.args[1]).to.equal(child);
});
});

describe('empty text node', () => {
beforeEach(() => {
element = fixtureSync('<slot-controller-element> </slot-controller-element>');
child = element.firstChild;
});

it('should override an empty text node passed to un-named slot', () => {
controller = new SlotController(element, '', () => {
const div = document.createElement('div');
div.textContent = 'bar';
return div;
});
element.addController(controller);
});
});
});
});

0 comments on commit 0d16dae

Please sign in to comment.