From ec8ff28ea4a87509e81248103b7d3ea040a243b7 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Mon, 30 Jan 2017 15:53:38 +0100 Subject: [PATCH 1/6] feat(h): make h set attributes on custom-element via attributes prop Closes #14 --- src/index.js | 17 +++++++++++++-- test/unit.js | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index 91e358e..0f6d563 100644 --- a/src/index.js +++ b/src/index.js @@ -5,19 +5,32 @@ function startsWith (key, val) { return key.indexOf(val) === 0; } +function isAttribute (key) { + return key === 'attributes'; +} + function shouldBeAttr (key, val) { - return startsWith(key, 'aria-') || startsWith(key, 'data-'); + return startsWith(key, 'aria-') || startsWith(key, 'data-') || isAttribute(key); } function handleFunction (Fn) { return Fn.prototype instanceof HTMLElement ? new Fn() : Fn(); } +function setAttr (node, attrName, attrValue) { + if (isAttribute(attrName)) { + Object.keys(attrValue) + .forEach((key) => { node.setAttribute(key, attrValue[key]); }); + return; + } + node.setAttribute(attrName, attrValue); +} + export function h (name, attrs, ...chren) { const node = typeof name === 'function' ? handleFunction(name) : document.createElement(name); Object.keys(attrs || []).forEach(attr => shouldBeAttr(attr, attrs[attr]) - ? node.setAttribute(attr, attrs[attr]) + ? setAttr(node, attr, attrs[attr]) : (node[attr] = attrs[attr])); chren.forEach(child => node.appendChild(child instanceof Node ? child : document.createTextNode(child))); return node; diff --git a/test/unit.js b/test/unit.js index dacc1a8..7546791 100644 --- a/test/unit.js +++ b/test/unit.js @@ -26,7 +26,65 @@ describe('bore', () => { expect(.localName).to.equal('div'); }); - it('setting attributes', () => { + it('setting attributes on CustomElement', () => { + + let whoAttrReactionCount = 0; + let deckAttrReactionCount = 0; + + class Test extends HTMLElement { + static get is() { return 'x-test-0' } + static get observedAttributes() { return ['who','deck'] } + + set who(val) { this._who = val } + get who() { return this._who } + + set deck(val) { this._deck = val } + get deck() { return this._deck } + + connectedCallback () { + this.attachShadow({ mode: 'open' }); + this.shadowRoot.innerHTML = ''; + } + attributeChangedCallback(attrName, oldVal, newVal) { + switch (attrName) { + case Test.observedAttributes[0]: + whoAttrReactionCount++; + this._setPropFromAttr(attrName,oldVal,newVal); + break; + case Test.observedAttributes[1]: + deckAttrReactionCount++; + this._setPropFromAttr(attrName,oldVal,newVal); + break; + default: + break; + } + } + _setPropFromAttr(attrName,oldVal,newVal){ + oldVal !== newVal && (this[attrName] = newVal); + } + } + customElements.define(Test.is,Test) + + + const myGreeter = ; + + expect(myGreeter.hasAttribute('who')).to.equal(true); + expect(myGreeter.getAttribute('who')).to.equal('Tony Hawk'); + + expect(myGreeter.hasAttribute('deck')).to.equal(false); + expect(myGreeter.getAttribute('deck')).to.equal(null); + + return mount(myGreeter).wait((element)=>{ + expect(element.node.who).to.equal('Tony Hawk'); + // by default h sets to props + expect(element.node.deck).to.equal('birdhouse'); + expect(whoAttrReactionCount > 0).to.equal(true); + expect(deckAttrReactionCount).to.equal(0); + }); + + }) + + it('setting attributes on Native HTMLElement', () => { const div =
Date: Mon, 30 Jan 2017 17:05:55 +0100 Subject: [PATCH 2/6] style: apply semistandard --fix --- test/unit.js | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/test/unit.js b/test/unit.js index 7546791..8cbfb09 100644 --- a/test/unit.js +++ b/test/unit.js @@ -27,46 +27,44 @@ describe('bore', () => { }); it('setting attributes on CustomElement', () => { - let whoAttrReactionCount = 0; let deckAttrReactionCount = 0; class Test extends HTMLElement { - static get is() { return 'x-test-0' } - static get observedAttributes() { return ['who','deck'] } + static get is () { return 'x-test-0'; } + static get observedAttributes () { return ['who', 'deck']; } - set who(val) { this._who = val } - get who() { return this._who } + set who (val) { this._who = val; } + get who () { return this._who; } - set deck(val) { this._deck = val } - get deck() { return this._deck } + set deck (val) { this._deck = val; } + get deck () { return this._deck; } connectedCallback () { this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = ''; } - attributeChangedCallback(attrName, oldVal, newVal) { + attributeChangedCallback (attrName, oldVal, newVal) { switch (attrName) { case Test.observedAttributes[0]: whoAttrReactionCount++; - this._setPropFromAttr(attrName,oldVal,newVal); + this._setPropFromAttr(attrName, oldVal, newVal); break; case Test.observedAttributes[1]: deckAttrReactionCount++; - this._setPropFromAttr(attrName,oldVal,newVal); + this._setPropFromAttr(attrName, oldVal, newVal); break; default: break; } } - _setPropFromAttr(attrName,oldVal,newVal){ + _setPropFromAttr (attrName, oldVal, newVal) { oldVal !== newVal && (this[attrName] = newVal); } } - customElements.define(Test.is,Test) - + customElements.define(Test.is, Test); - const myGreeter = ; + const myGreeter = ; expect(myGreeter.hasAttribute('who')).to.equal(true); expect(myGreeter.getAttribute('who')).to.equal('Tony Hawk'); @@ -74,15 +72,14 @@ describe('bore', () => { expect(myGreeter.hasAttribute('deck')).to.equal(false); expect(myGreeter.getAttribute('deck')).to.equal(null); - return mount(myGreeter).wait((element)=>{ + return mount(myGreeter).wait((element) => { expect(element.node.who).to.equal('Tony Hawk'); // by default h sets to props expect(element.node.deck).to.equal('birdhouse'); expect(whoAttrReactionCount > 0).to.equal(true); expect(deckAttrReactionCount).to.equal(0); }); - - }) + }); it('setting attributes on Native HTMLElement', () => { const div =
Date: Wed, 1 Feb 2017 11:44:29 +0100 Subject: [PATCH 3/6] feat(h): make h set attributes only via attrs property BREAKING CHANGE: Before you could set attributes via bore's h via data-* or aria-* otherwise the attribute would be set to element property. Now if you wanna set attributes you have to explicitly do it via `attrs` property which accepts a object map. Before: ```js ``` After: ```js ``` --- src/index.js | 30 +++++++------------ test/unit.js | 82 ++++++++++++++-------------------------------------- 2 files changed, 32 insertions(+), 80 deletions(-) diff --git a/src/index.js b/src/index.js index 0f6d563..7d831e0 100644 --- a/src/index.js +++ b/src/index.js @@ -1,37 +1,27 @@ const { DocumentFragment, Node, Promise } = window; const { slice } = []; -function startsWith (key, val) { - return key.indexOf(val) === 0; -} - function isAttribute (key) { - return key === 'attributes'; -} - -function shouldBeAttr (key, val) { - return startsWith(key, 'aria-') || startsWith(key, 'data-') || isAttribute(key); + return key === 'attrs'; } function handleFunction (Fn) { return Fn.prototype instanceof HTMLElement ? new Fn() : Fn(); } -function setAttr (node, attrName, attrValue) { - if (isAttribute(attrName)) { - Object.keys(attrValue) - .forEach((key) => { node.setAttribute(key, attrValue[key]); }); - return; - } - node.setAttribute(attrName, attrValue); +function setAttrs (node, attrName, attrValue) { + Object.keys(attrValue) + .forEach((key) => { node.setAttribute(key, attrValue[key]); }); } export function h (name, attrs, ...chren) { const node = typeof name === 'function' ? handleFunction(name) : document.createElement(name); - Object.keys(attrs || []).forEach(attr => - shouldBeAttr(attr, attrs[attr]) - ? setAttr(node, attr, attrs[attr]) - : (node[attr] = attrs[attr])); + Object.keys(attrs || {}) + .forEach(attrName => { + isAttribute(attrName) + ? setAttrs(node, attrName, attrs[attrName]) + : (node[attrName] = attrs[attrName]); + }); chren.forEach(child => node.appendChild(child instanceof Node ? child : document.createTextNode(child))); return node; } diff --git a/test/unit.js b/test/unit.js index 8cbfb09..a83119c 100644 --- a/test/unit.js +++ b/test/unit.js @@ -26,76 +26,38 @@ describe('bore', () => { expect(.localName).to.equal('div'); }); - it('setting attributes on CustomElement', () => { - let whoAttrReactionCount = 0; - let deckAttrReactionCount = 0; - - class Test extends HTMLElement { - static get is () { return 'x-test-0'; } - static get observedAttributes () { return ['who', 'deck']; } - - set who (val) { this._who = val; } - get who () { return this._who; } - - set deck (val) { this._deck = val; } - get deck () { return this._deck; } - - connectedCallback () { - this.attachShadow({ mode: 'open' }); - this.shadowRoot.innerHTML = ''; - } - attributeChangedCallback (attrName, oldVal, newVal) { - switch (attrName) { - case Test.observedAttributes[0]: - whoAttrReactionCount++; - this._setPropFromAttr(attrName, oldVal, newVal); - break; - case Test.observedAttributes[1]: - deckAttrReactionCount++; - this._setPropFromAttr(attrName, oldVal, newVal); - break; - default: - break; - } - } - _setPropFromAttr (attrName, oldVal, newVal) { - oldVal !== newVal && (this[attrName] = newVal); - } - } - customElements.define(Test.is, Test); - - const myGreeter = ; - - expect(myGreeter.hasAttribute('who')).to.equal(true); - expect(myGreeter.getAttribute('who')).to.equal('Tony Hawk'); - - expect(myGreeter.hasAttribute('deck')).to.equal(false); - expect(myGreeter.getAttribute('deck')).to.equal(null); - - return mount(myGreeter).wait((element) => { - expect(element.node.who).to.equal('Tony Hawk'); - // by default h sets to props - expect(element.node.deck).to.equal('birdhouse'); - expect(whoAttrReactionCount > 0).to.equal(true); - expect(deckAttrReactionCount).to.equal(0); - }); - }); - - it('setting attributes on Native HTMLElement', () => { + it('setting attributes', () => { const div =
; - expect(div.getAttribute('aria-test')).to.equal('aria something'); - expect(div.getAttribute('data-test')).to.equal('data something'); + expect(div.hasAttribute('aria-test')).to.equal(false); + expect(div.hasAttribute('data-test')).to.equal(false); expect(div.hasAttribute('test1')).to.equal(false); expect(div.hasAttribute('test2')).to.equal(false); - expect(div['aria-test']).to.equal(undefined); - expect(div['data-test']).to.equal(undefined); + + expect(div.hasAttribute('aria-who')).to.equal(true); + expect(div.hasAttribute('who')).to.equal(true); + expect(div.hasAttribute('deck')).to.equal(true); + expect(div.hasAttribute('rating')).to.equal(true); + + expect(div['aria-test']).to.equal('aria something'); + expect(div['data-test']).to.equal('data something'); expect(div.test1).to.equal('test something'); expect(div.test2).to.equal(1); + + expect(div['aria-who']).to.equal(undefined); + expect(div.who).to.equal(undefined); + expect(div.deck).to.equal(undefined); + expect(div.rating).to.equal(undefined); }); it('mount: all(string)', () => { From c10d28c398f6d03310e3e3c0e360f5375ffb4771 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Wed, 1 Feb 2017 13:04:42 +0100 Subject: [PATCH 4/6] feat(h): add events setup via events attribute --- src/index.js | 42 +++++++++++++++++++++++++++++++++++------- test/unit.js | 21 ++++++++++++++++++++- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/index.js b/src/index.js index 7d831e0..e143fff 100644 --- a/src/index.js +++ b/src/index.js @@ -4,25 +4,53 @@ const { slice } = []; function isAttribute (key) { return key === 'attrs'; } +function isEvent (key) { + return key === 'events'; +} function handleFunction (Fn) { return Fn.prototype instanceof HTMLElement ? new Fn() : Fn(); } -function setAttrs (node, attrName, attrValue) { +function setAttrs (node, attrValue) { Object.keys(attrValue) .forEach((key) => { node.setAttribute(key, attrValue[key]); }); } +function setEvents (node, attrValue) { + Object.keys(attrValue) + .forEach((key) => { node.addEventListener(key, attrValue[key]); }); +} +function setProps (node, attrName, attrValue) { + node[attrName] = attrValue; +} -export function h (name, attrs, ...chren) { - const node = typeof name === 'function' ? handleFunction(name) : document.createElement(name); +function setupNodeAttrs (node, attrs) { Object.keys(attrs || {}) .forEach(attrName => { - isAttribute(attrName) - ? setAttrs(node, attrName, attrs[attrName]) - : (node[attrName] = attrs[attrName]); + const attrValue = attrs[attrName]; + + if (isAttribute(attrName)) { + setAttrs(node, attrValue); + return; + } + + if (isEvent(attrName)) { + setEvents(node, attrValue); + return; + } + + setProps(node, attrName, attrValue); }); - chren.forEach(child => node.appendChild(child instanceof Node ? child : document.createTextNode(child))); +} + +function setupNodeChildren (node, children) { + children.forEach(child => node.appendChild(child instanceof Node ? child : document.createTextNode(child))); +} + +export function h (name, attrs, ...chren) { + const node = typeof name === 'function' ? handleFunction(name) : document.createElement(name); + setupNodeAttrs(node, attrs); + setupNodeChildren(node, chren); return node; } diff --git a/test/unit.js b/test/unit.js index a83119c..4d42e8b 100644 --- a/test/unit.js +++ b/test/unit.js @@ -12,7 +12,7 @@ import '@webcomponents/shadydom'; // eslint-disable-next-line no-unused-vars import { h, mount } from '../src'; -const { customElements, DocumentFragment, HTMLElement, Promise } = window; +const { customElements, DocumentFragment, HTMLElement, Promise, Event, CustomEvent } = window; describe('bore', () => { it('creating elements by local name', () => { @@ -60,6 +60,25 @@ describe('bore', () => { expect(div.rating).to.equal(undefined); }); + it('setting events', () => { + const click = (e) => { e.target.clickTriggered = true; }; + const custom = (e) => { e.target.customTriggered = true; }; + + const dom =
; + + dom.dispatchEvent(new Event('click')); + dom.dispatchEvent(new CustomEvent('custom')); + + expect(dom.onclick).to.equal(null); + expect(dom.click).to.not.equal(undefined); + expect(dom.getAttribute('click')).to.equal(null); + expect(dom.clickTriggered).to.equal(true); + + expect(dom.custom).to.equal(undefined); + expect(dom.getAttribute('custom')).to.equal(null); + expect(dom.customTriggered).to.equal(true); + }); + it('mount: all(string)', () => { const div = mount(
From 69aebeae043510db88dcb19b4db4e86bb448732b Mon Sep 17 00:00:00 2001 From: Trey Shugart Date: Thu, 2 Feb 2017 17:41:35 +1100 Subject: [PATCH 5/6] chore(codestyle): minor nit updates to codestyle and naming (#16) --- src/index.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/index.js b/src/index.js index e143fff..cfb9c20 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,10 @@ const { DocumentFragment, Node, Promise } = window; const { slice } = []; -function isAttribute (key) { +function isAttr (key) { return key === 'attrs'; } + function isEvent (key) { return key === 'events'; } @@ -12,15 +13,17 @@ function handleFunction (Fn) { return Fn.prototype instanceof HTMLElement ? new Fn() : Fn(); } -function setAttrs (node, attrValue) { - Object.keys(attrValue) - .forEach((key) => { node.setAttribute(key, attrValue[key]); }); +function setAttrs (node, attrs) { + Object.keys(attrs) + .forEach(key => node.setAttribute(key, attrs[key])); } -function setEvents (node, attrValue) { - Object.keys(attrValue) - .forEach((key) => { node.addEventListener(key, attrValue[key]); }); + +function setEvents (node, events) { + Object.keys(events) + .forEach(key => node.addEventListener(key, events[key])); } -function setProps (node, attrName, attrValue) { + +function setProp (node, attrName, attrValue) { node[attrName] = attrValue; } @@ -29,7 +32,7 @@ function setupNodeAttrs (node, attrs) { .forEach(attrName => { const attrValue = attrs[attrName]; - if (isAttribute(attrName)) { + if (isAttr(attrName)) { setAttrs(node, attrValue); return; } @@ -39,7 +42,7 @@ function setupNodeAttrs (node, attrs) { return; } - setProps(node, attrName, attrValue); + setProp(node, attrName, attrValue); }); } From f0db4766bd2bb293f52566d7f54efc2481783a8c Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Thu, 2 Feb 2017 08:00:31 +0100 Subject: [PATCH 6/6] docs(README): document new h capabilities --- README.md | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e10a568..cc75985 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Since web components are an extension of the HTML standard, Bore inherently work 1. The custom element polyfill is supported by calling `flush()` after mounting the nodes so things appear synchronous. 2. Nodes are mounted to a fixture that is always kept in the DOM (even if it's removed, it will put itself back). This is so that custom elements can go through their natural lifecycle. 3. The fixture is cleaned up on every mount, so there's no need to cleanup after your last mount. -4. The `attachShadow()` method is overridden to *always* provide an `open` shadow root so that there is always a `shadowRoot` property and it can be queried against. +4. The `attachShadow()` method is overridden to *always* provide an `open` shadow root so that there is always a `shadowRoot` property and it can be queried against. @@ -71,15 +71,37 @@ This can probably be confusing to some, so this is only recommended as a last re -#### Setting attributes vs properties +#### Setting attributes vs properties vs events -The `h` function prefers props unless it's something that *must* be set as an attribute, such as `aria-` or `data-`. As a best practice, your web component should be designed to prefer props and reflect to attributes only when it makes sense. +The `h` function sets always props. If you wanna set something as an attribute, such as `aria-` or `data-` or anything else `h` accepts special `attrs` prop. +For setting event handlers use `events` property. + +> As a best practice, your web component should be designed to prefer props and reflect to attributes only when it makes sense. + +*Example:* + +```js +/* @jsx h */ +import { h } from 'bore'; + +const dom = console.log('just regular click'), + kickflip: e => console.log('just did kickflip') + }} +> +``` ### `mount(htmlOrNode)` -The mount function takes a node, or a string - and converts it to a node - and returns a wrapper around it. +The mount function takes a node, or a string - and converts it to a node - and returns a wrapper around it. ```js import { mount, h } from 'bore';