Skip to content

Commit

Permalink
Add listeners block to x-element.
Browse files Browse the repository at this point in the history
A new `listeners-mixin` exists which is meant to be more low-level than
the `properties-mixin`. It allows us to pull out some code that’s not
strictly needed for the `element-mixin`, which mostly wants to concern
itself with necessities for creating custom elements.

Closes #29
  • Loading branch information
theengineear committed May 10, 2019
1 parent c769fcf commit 0ec3b76
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 6 deletions.
5 changes: 5 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ custom extension is supported.
Provides base functionality for creating custom elements with shadow roots and
hooks to re-render the element.

### `listeners-mixin`

Provides a declarative `listeners` block which adds bound listeners on connect
and removes them on disconnect.

### `properties-mixin`

Allows you to declare the `properties` block. This leverages the `element-mixin`
Expand Down
49 changes: 49 additions & 0 deletions mixins/listeners-mixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Provides declarative 'listeners' block.
*/

export default superclass =>
class extends superclass {
static get listeners() {
// Mapping of event type to method name. E.g., `{ click: 'onClick' }`.
return {};
}

static setupListeners(target) {
// Loops over mapping declared in this.listeners and calls listen.
for (const [type, methodName] of Object.entries(this.listeners)) {
const ok = target.listen(target.shadowRoot, type, target[methodName]);
if (ok === false) {
target.dispatchError(
new Error(
`"${type}" listener error: "${methodName}" does not exist`
)
);
}
}
}

static teardownListeners(target) {
// Loops over mapping declared in this.listeners and calls unlisten.
for (const [type, methodName] of Object.entries(this.listeners)) {
const ok = target.unlisten(target.shadowRoot, type, target[methodName]);
if (ok === false) {
target.dispatchError(
new Error(
`Failed to unbind "${type}" listener: "${methodName}" does not exist`
)
);
}
}
}

connectedCallback() {
super.connectedCallback();
this.constructor.setupListeners(this);
}

disconnectedCallback() {
super.disconnectedCallback();
this.constructor.teardownListeners(this);
}
};
51 changes: 51 additions & 0 deletions test/fixture-element-listeners.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import ElementMixin from '../mixins/element-mixin.js';
import ListenersMixin from '../mixins/listeners-mixin.js';

class TestElement extends ListenersMixin(ElementMixin(HTMLElement)) {
static get listeners() {
return { click: 'onClick' };
}

static get observedAttributes() {
return ['clicks', 'count'];
}

static template() {
return ({ clicks, count }) => `
<button id="increment" type="button">+</button>
<button id="decrement" type="button">-</button>
<span>clicks: ${clicks} count ${count}</span>
`;
}

attributeChangedCallback() {
this.invalidate();
}

get clicks() {
return Number(this.getAttribute('clicks'));
}

set clicks(value) {
this.setAttribute('clicks', value);
}

get count() {
return Number(this.getAttribute('count'));
}

set count(value) {
this.setAttribute('count', value);
}

onClick(evt) {
this.clicks++;
if (evt.target.id === 'increment') {
this.count++;
} else if (evt.target.id === 'decrement') {
this.count--;
}
}
}

customElements.define('test-element-listeners', TestElement);
1 change: 1 addition & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import run from './runner.js';

run('./test-upgrade.js');
run('./test-basic.js');
run('./test-listeners.js');
run('./test-attr-binding.js');
run('./test-attr-reflection.js');
run('./test-read-only-properties.js');
Expand Down
29 changes: 29 additions & 0 deletions test/test-listeners.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { suite, it } from './runner.js';
import './fixture-element-listeners.js';

suite('x-element listeners', async ctx => {
document.onerror = evt => {
console.error(evt.error);
};
const el = document.createElement('test-element-listeners');
ctx.body.appendChild(el);

it('initialized as expected', el.clicks === 0 && el.count === 0);

el.click();
it('listens on shadowRoot, not on host', el.clicks === 0 && el.count === 0);

el.shadowRoot.getElementById('increment').click();
it('listens to events', el.clicks === 1 && el.count === 1);

el.shadowRoot.getElementById('decrement').click();
it('works for delegated event handling', el.clicks === 2 && el.count === 0);

ctx.body.removeChild(el);
el.shadowRoot.getElementById('increment').click();
it('removes listeners on disconnection', el.clicks === 2 && el.count === 0);

ctx.body.appendChild(el);
el.shadowRoot.getElementById('increment').click();
it('adds back listeners on reconnection', el.clicks === 3 && el.count === 1);
});
10 changes: 4 additions & 6 deletions x-element.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
/**
* Base class for custom elements.
*
* Extends XElementBasic and XElementProperties
*
* Introduces template rendering using the `lit-html` library for improved
* performance and added functionality.
*/

import ElementMixin from './mixins/element-mixin.js';
import ListenersMixin from './mixins/listeners-mixin.js';
import LitHtmlMixin from './mixins/lit-html-mixin.js';
import PropertiesMixin from './mixins/properties-mixin.js';
import PropertyEffectsMixin from './mixins/property-effects-mixin.js';

export default LitHtmlMixin(
PropertyEffectsMixin(PropertiesMixin(ElementMixin(HTMLElement)))
ListenersMixin(
PropertyEffectsMixin(PropertiesMixin(ElementMixin(HTMLElement)))
)
);

0 comments on commit 0ec3b76

Please sign in to comment.