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

refactor: add missing SlotController logic and tests #3179

Merged
merged 3 commits into from
Dec 13, 2021
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
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', () => {
vursen marked this conversation as resolved.
Show resolved Hide resolved
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);
});
});
});
});