als-component
Version:
lightweight JavaScript library for creating reactive, server-rendered, and browser-based components.
87 lines (77 loc) • 3.77 kB
JavaScript
class Component {
static isBrowser = true
static componentsToUpdate = new Set()
static components = new WeakMap()
static qs(selector, host = document) { return host.querySelector(selector) }
static qsa(selector, host = document) { return [...host.querySelectorAll(selector)] }
static get isAsync() { return this.prototype.render.constructor.name === 'AsyncFunction' };
actions = []; counter = 0;
get element() { return Component.isBrowser ? document.querySelector(this.selector) : null }
set elementOuter(v) {
const element = this.element;
if (element) element.outerHTML = v
}
constructor(props = {}, inner) {
this.isAsync = this.constructor.isAsync
this.props = props;
this.inner = inner;
this.name = this.constructor.name + (this.props.key ? this.props.key : '');
this.selector = `[component=${this.name}]`;
this.hooks = { mount: [], unmount: [] };
Component.components[this.name] = this
this.Component = Component
}
on(event, fn) { if (this.hooks[event]) this.hooks[event].push(fn) }
async callAsync() { return (await this.render(this.props, this.inner)).replace(/^\<\w\w*/, m => `${m} component="${this.name}"`) }
callSync() { return this.render(this.props, this.inner).replace(/^\<\w\w*/, m => `${m} component="${this.name}"`) }
call() { this.Component.componentsToUpdate.add(this); return this.isAsync ? this.callAsync() : this.callSync() }
action(event, fn) {
const id = this.name + this.counter++;
this.actions.push({ event, id, fn });
return id;
}
async buildAsync(updateElement = true) {
const result = await this.call()
if (updateElement) this.elementOuter = result
if (this.Component.isBrowser) this.runActions()
return result
}
buildSync(updateElement = true) {
const result = this.call()
if (updateElement) this.elementOuter = result
if (this.Component.isBrowser) this.runActions()
return result
}
build(updateElement) { return this.isAsync ? this.buildAsync(updateElement) : this.buildSync(updateElement) }
update(props = this.props, inner = this.inner) { this.props = props; this.inner = inner; return this.build(true) }
runActions() {
const { components, componentsToUpdate } = this.Component
componentsToUpdate.forEach(component => {
const { actions, hooks, selector } = component;
const element = document.querySelector(selector);
const parent = element && element.childNodes.length ? element : document;
hooks.mount.forEach(fn => fn(element));
actions.forEach(({ event, fn, id }) => {
let elementsForEvent = parent.querySelectorAll(`[${event}="${id}"]`);
if (elementsForEvent.length === 0) elementsForEvent = document.querySelectorAll(`[${event}="${id}"]`)
if (elementsForEvent.length === 0) return;
elementsForEvent.forEach(elementForEvent => {
if (event === 'load') fn(elementForEvent);
else if (typeof elementForEvent.addEventListener === 'function') {
elementForEvent.addEventListener(event, fn);
}
})
})
component.actions = [];
component.hooks.mount = [];
component.count = 0;
})
for (const name in components) { // check if there are element's components which removed
const { selector, hooks } = components[name]
if (document.querySelector(selector)) continue;
hooks.unmount.forEach(fn => fn(name));
delete components[name];
}
this.Component.componentsToUpdate.clear()
}
}