UNPKG

@neumatter/webc

Version:

Module to extend and use web components.

340 lines (294 loc) 8.5 kB
export function shadow (element, mode = 'open') { return element.attachShadow({ mode }) } export function html (strings, ...keys) { if (!strings.length || !keys?.length) return strings return strings .map((s, i) => s + (keys[i] || '')) .join('') } export function define (tag, Class) { customElements.define(tag, Class) } function toBuiltInName (obj) { return Object.prototype.toString.call(obj).slice(8, -1) } const ON_REGEX = /^on[A-Z]/ export class WebComponent extends HTMLElement { state context #contextMap #dataMap constructor () { super() // set up state object this.state = {} this.context = {} this.data = {} this.#contextMap = {} this.#dataMap = {} } connectedCallback () { if (!this.isConnected) return const props = Object.getOwnPropertyNames(this) const listeners = props.filter(prop => ON_REGEX.test(prop)) const { length } = listeners if (length) { let index = -1 while (++index < length) { const key = listeners[index] const event = key.replace(/^on/, '').toLowerCase() const callback = this[key] this.addEventListener(event, callback) console.info(`New Event Listener Added: ${key}`) } } if (typeof this.render === 'function') { console.info('WebComponent.render method found and called') try { const html = this.render() if (typeof html.then === 'function') { html.then(str => { this.innerHTML = str }) } else { this.innerHTML = html } } catch (err) { console.error(err) } } if (typeof this.componentOnMount === 'function') { console.info('WebComponent.componentOnMount method found and called') try { this.componentOnMount() } catch (err) { console.error(err) } } } disconnectedCallback () { console.info('component will unmount') if (typeof this.componentOnUnmount === 'function') { console.info('WebComponent.componentOnUnmount method found and called') try { this.componentOnUnmount() } catch (err) { console.error(err) } } } attributeChangedCallback (name, oldValue, newValue) { console.info('WebComponent attributes changed') if (typeof this.attributeListener === 'function') { console.info('WebComponent.attributeListener method found and called') try { this.attributeListener(this) } catch (err) { console.error(err) } } } isCustomElement (element) { return ( Object.getPrototypeOf(customElements.get(element.tagName.toLowerCase())) .name === 'WebComponent' ) } #updateBindings (prop, value = '') { const bindings = [...this.queryAll(`[data-bind$="${prop}"]`)] const { length } = bindings let index = -1 while (++index < length) { const node = bindings[index] const dataProp = node.dataset.bind const bindProp = dataProp.includes(':') ? dataProp.split(':').shift() : dataProp const bindValue = dataProp.includes('.') ? dataProp .split('.').slice(1) .reduce((obj, p) => obj[p], value) : value const target = [...this.queryAll(node.tagName)].find(el => el === node) const isStateUpdate = dataProp.includes(':') && this.isCustomElement(target) if (isStateUpdate) { target.setData({ [`${bindProp}`]: bindValue }) } else if (this.isArray(bindValue)) { target[bindProp] = bindValue } else { node.innerText = bindValue.toString() } } } #mapBindKey (key, obj) { const keys = Object.keys(obj) const { length } = keys let index = -1 const output = new Array() while (++index < length) { const k = this.isObject(obj[keys[index]]) ? this.#mapBindKey(keys[index], obj[keys[index]]) : keys[index] // push binding output.push(`${key}.${k}`) } return output } setContext (newContext) { const keys = Object.keys(newContext) const { length } = keys let index = -1 while (++index < length) { const key = keys[index] const value = newContext[key] this.context[key] = this.isObject(this.context[key]) && this.isObject(value) ? { ...this.context[key], ...value } : value if (this.#contextMap[key]) { this.#contextMap[key](this.context[key]) } } } getContext (key) { return this.context[key] } useContext (key, callback) { this.#contextMap[key] = callback return this } setData (newState) { const keys = Object.keys(newState) const { length } = keys let index = -1 while (++index < length) { const key = keys[index] const value = newState[key] this.data[key] = this.isObject(this.data[key]) && this.isObject(value) ? { ...this.data[key], ...value } : value const bindKey = this.isObject(value) ? this.#mapBindKey(key, value) : key const bindKeys = this.isArray(bindKey) ? bindKey : [bindKey] let bindIndex = -1 const { length: bindLength } = bindKeys while (++bindIndex < bindLength) { this.#updateBindings(bindKeys[bindIndex], value) } if (this.#dataMap[key]) { this.#dataMap[key](newState[key], key) } } return this } getData (key) { return this.data[key] } useData (key, callback) { this.#dataMap[key] = callback return this } getState (key) { return this.state[key] } setState (newState) { const keys = Object.keys(newState) const { length } = keys let index = -1 while (++index < length) { const key = keys[index] const value = newState[key] this.state[key] = this.isObject(this.state[key]) && this.isObject(value) ? { ...this.state[key], ...value } : value } const html = this.render() if (typeof html.then === 'function') { html.then(str => { this.innerHTML = str }) } else { this.innerHTML = html } return this } stateIs (key, value) { return this.state[key] && this.state[key] === value } isArray (arr) { return Array.isArray(arr) } isObject (obj) { return toBuiltInName(obj) === 'Object' } isNodeList (obj) { return toBuiltInName(obj) === 'NodeList' } get (attribute, childSelector = null) { return childSelector ? this.query(childSelector).getAttribute(attribute) : this.getAttribute(attribute) } query (selector) { return this.shadowRoot ? this.shadowRoot.querySelector(selector) : this.querySelector(selector) } queryAll (selector) { return this.shadowRoot ? this.shadowRoot.querySelectorAll(selector) : this.querySelectorAll(selector) } show (els = null) { const elems = els || this const elements = Array.isArray(elems) || this.isNodeList(elems) ? elems : [elems] const { length } = elements let index = -1 while (++index < length) { elements[index].style.display = '' elements[index].removeAttribute('hidden') } } hide (els = null) { const elems = els || this const elements = Array.isArray(elems) || this.isNodeList(elems) ? elems : [elems] const { length } = elements let index = -1 while (++index < length) { elements[index].style.display = 'none' elements[index].setAttribute('hidden', '') } } setStyle (els, styles) { const elements = Array.isArray(els) ? els : [els] const { length } = elements let index = -1 while (++index < length) { Object.assign(elements[index].style, styles) } } setClassList (els, ...classes) { const elements = Array.isArray(els) ? els : [els] const { length } = elements let index = -1 while (++index < length) { elements[index].classList.add(...classes) } } removeClassList (els, ...classes) { const elements = Array.isArray(els) ? els : [els] const { length } = elements let index = -1 while (++index < length) { elements[index].classList.remove(...classes) } } addTemplate (element, selector, replaceContents = false) { const template = this.query(selector).content.cloneNode(true) if (replaceContents) element.innerHTML = '' element.appendChild(template) } }