Created
July 30, 2018 16:23
-
-
Save jmas/07348b311988d5056fa8db779b0fee92 to your computer and use it in GitHub Desktop.
Custom Elements
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<my-list of="counters"> | |
<template> | |
<my-counter index></my-counter> | |
</template> | |
</my-list> | |
<p> | |
<my-action on="click"> | |
<button data-action="addCounter"> | |
Add Counter | |
</button> | |
<button data-action="removeCounter"> | |
Remove Counter | |
</button> | |
</my-action> | |
</p> | |
<script> | |
const EVENT_ACTION_NAME = 'action'; | |
class ContainerElement extends HTMLElement { | |
constructor() { | |
super(); | |
this.state = this.getInitialState(); | |
} | |
getInitialState() { | |
if (super.getInitialState) { | |
return super.getInitialState(); | |
} | |
return undefined; | |
} | |
dispatch(action) { | |
this.dispatchEvent(new CustomEvent(EVENT_ACTION_NAME, { | |
detail: action | |
})); | |
} | |
stateChangedCallback(prevState) {} | |
} | |
class ListElement extends ContainerElement { | |
constructor() { | |
super(); | |
this.itemTemplate = this.innerHTML; | |
} | |
stateChangedCallback() { | |
if (!this.itemTemplate) { | |
this.itemTemplate = this.querySelector('template'); | |
} | |
this.innerHTML = | |
this.state[this.attributes.of.value] | |
.forEach((item, index) => { | |
this.itemTemplate.querySelector('[index]').setState('index', index); | |
document.importNode(this.itemTemplate.content, true); | |
return `${this.itemTemplate.replace('{{index}}', index)}`; | |
}) | |
.join(''); | |
} | |
} | |
class ActionElement extends ContainerElement { | |
constructor() { | |
super(); | |
this.addEventListener(this.attributes.on.value, event => { | |
if (event.target.dataset.action) { | |
event.preventDefault(); | |
this.dispatch({ | |
type: event.target.dataset.action, | |
payload: this.dataset | |
}); | |
} | |
}); | |
} | |
} | |
class CounterElement extends ContainerElement { | |
connectedCallback() { | |
this.innerHTML = ` | |
<my-action on="click"> | |
<h1>Count <strong data-display="count"></strong></h1> | |
<button type="button" data-action="decrease">-</button> | |
<button type="button" data-action="increase">+</button> | |
</my-action> | |
<hr /> | |
`; | |
this.querySelector('[data-action="decrease"]').addEventListener('click', () => { | |
this.dispatch({ | |
type: 'decrease', | |
counter: this.getCounterState() | |
}); | |
}); | |
this.querySelector('[data-action="increase"]').addEventListener('click', () => { | |
this.dispatch({ | |
type: 'increase', | |
counter: this.getCounterState() | |
}); | |
}); | |
} | |
stateChangedCallback() { | |
this.querySelector('[data-display="count"]').innerText = this.getCounterState().count; | |
} | |
getCounterState() { | |
console.log('getCounterState', this.state); | |
return this.state.counters[this.getCounterIndex()]; | |
} | |
getCounterIndex() { | |
return parseInt(this.attributes.index.value, 10); | |
} | |
} | |
function createStore(reducer) { | |
const listeners = []; | |
let state = reducer(undefined, { type: undefined }); | |
return { | |
subscribe(listener) { | |
listeners.push(listener); | |
return () => { | |
listeners.splice(listeners.indexOf(listener), 1); | |
}; | |
}, | |
dispatch(action) { | |
const newState = reducer(state, action); | |
if (newState !== state) { | |
state = newState; | |
listeners.forEach(listener => listener()); | |
} | |
}, | |
getState() { | |
return state; | |
} | |
}; | |
} | |
function withStore(CustomElement, store, mapState = state => state) { | |
return class extends CustomElement { | |
constructor() { | |
super(); | |
this.addEventListener(EVENT_ACTION_NAME, event => { | |
console.log('action', event); | |
store.dispatch(event.detail); | |
}); | |
} | |
connectedCallback() { | |
console.log('connectedCallback'); | |
this.unsubscribe = store.subscribe(() => { | |
this.setState(store.getState()); | |
}); | |
if (super.connectedCallback) { | |
super.connectedCallback(); | |
} | |
this.setState(store.getState()); | |
} | |
disconnectedCallback() { | |
console.log('disconnectedCallback', this); | |
this.unsubscribe(); | |
if (super.disconnectedCallback) { | |
super.disconnectedCallback(); | |
} | |
} | |
stateChangedCallback(prevState) { | |
console.log('stateChangedCallback', this.state); | |
if (super.stateChangedCallback) { | |
super.stateChangedCallback(prevState); | |
} | |
} | |
setState(state) { | |
console.log('setState', state); | |
const prevState = this.state; | |
this.state = mapState(state); | |
this.stateChangedCallback(prevState); | |
} | |
} | |
} | |
const store = createStore((state = { counters: [{ count: 0 }, { count: 0 }, { count: 0 }] }, action) => { | |
switch(action.type) { | |
case 'increase': | |
return { | |
...state, | |
counters: updateCounters(state.counters, action.counter, count => count + 1) | |
}; | |
case 'decrease': | |
return { | |
...state, | |
counters: updateCounters(state.counters, action.counter, count => count - 1) | |
}; | |
case 'addCounter': | |
return { | |
...state, | |
counters: [ | |
...state.counters, | |
{ count: 0 } | |
] | |
}; | |
case 'removeCounter': | |
return { | |
...state, | |
counters: state.counters.filter((counter, index) => index !== state.counters.length - 1) | |
}; | |
default: | |
return state; | |
} | |
}); | |
function updateCounters(counters, counter, operation) { | |
return counters.reduce((counters, item) => { | |
return [ | |
...counters, | |
item === counter | |
? { | |
...item, | |
count: operation(counter.count) | |
} | |
: item | |
]; | |
}, []); | |
} | |
window.customElements.define('my-counter', withStore(CounterElement, store)); | |
window.customElements.define('my-list', withStore(ListElement, store)); | |
window.customElements.define('my-action', withStore(ActionElement, store)); | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment