@neumatter/webc
Version:
Module to extend and use web components.
340 lines (294 loc) • 8.5 kB
JavaScript
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)
}
}