Skip to content

Instantly share code, notes, and snippets.

@jmas
Created July 30, 2018 16:23
Show Gist options
  • Save jmas/07348b311988d5056fa8db779b0fee92 to your computer and use it in GitHub Desktop.
Save jmas/07348b311988d5056fa8db779b0fee92 to your computer and use it in GitHub Desktop.
Custom Elements
<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