oxe
Version:
A mighty tiny web components framework/library
198 lines (160 loc) • 6.63 kB
text/typescript
import Observer from './observer';
import Binder from './binder';
import Css from './css';
export default class Component extends HTMLElement {
static attributes: string[];
static get observedAttributes () { return this.attributes; }
static set observedAttributes (attributes) { this.attributes = attributes; }
#root: any;
#binder: any;
// #template: any;
#flag: boolean = false;
#ready: boolean = false;
#name: string = this.nodeName.toLowerCase();
// this overwrites extends methods
// adopted: () => void;
// rendered: () => void;
// connected: () => void;
// disconnected: () => void;
// attributed: (name: string, from: string, to: string) => void;
#adopted: () => void;
#rendered: () => void;
#connected: () => void;
#disconnected: () => void;
#attributed: (name: string, from: string, to: string) => void;
#readyEvent = new Event('ready');
#afterRenderEvent = new Event('afterrender');
#beforeRenderEvent = new Event('beforerender');
#afterConnectedEvent = new Event('afterconnected');
#beforeConnectedEvent = new Event('beforeconnected');
// #css: string = typeof (this as any).css === 'string' ? (this as any).css : '';
// #html: string = typeof (this as any).html === 'string' ? (this as any).html : '';
// #data: object = typeof (this as any).data === 'object' ? (this as any).data : {};
// #adopt: boolean = typeof (this as any).adopt === 'boolean' ? (this as any).adopt : false;
// #shadow: boolean = typeof (this as any).shadow === 'boolean' ? (this as any).shadow : false;
css: string = '';
html: string = '';
data: object = {};
adopt: boolean = false;
shadow: boolean = false;
get root () { return this.#root; }
get ready () { return this.#ready; }
get binder () { return this.#binder; }
constructor () {
super();
this.#binder = new Binder();
this.#adopted = (this as any).adopted;
this.#rendered = (this as any).rendered;
this.#connected = (this as any).connected;
this.#attributed = (this as any).attributed;
this.#disconnected = (this as any).disconnected;
if (this.shadow && 'attachShadow' in document.body) {
this.#root = this.attachShadow({ mode: 'open' });
} else if (this.shadow && 'createShadowRoot' in document.body) {
this.#root = (this as any).createShadowRoot();
} else {
this.#root = this;
}
// this.#template = document.createElement('template');
// this.#template.innerHTML = this.html;
}
async #observe (path, type) {
const parents = this.#binder.pathBinders.get(path);
if (parents) {
// console.log('path:',path);
const parentTasks = [];
for (const binder of parents) {
if (!binder) continue;
parentTasks.push(binder[ type ]());
}
await Promise.all(parentTasks);
}
for (const [ key, children ] of this.#binder.pathBinders) {
if (!children) continue;
if (key.startsWith(`${path}.`)) {
// console.log('key:', key);
for (const binder of children) {
if (!binder) continue;
binder[ type ]();
}
}
}
};
async #render () {
const tasks = [];
this.data = Observer(
typeof this.data === 'function' ? await this.data() : this.data,
this.#observe.bind(this)
);
if (this.adopt) {
let child = this.firstChild;
while (child) {
tasks.push(this.#binder.add(child, this, this.data));
// this.#binder.add(child, this, this.data);
child = child.nextSibling;
}
}
const template = document.createElement('template');
template.innerHTML = this.html;
if (
!this.shadow ||
!('attachShadow' in document.body) &&
!('createShadowRoot' in document.body)
) {
const templateSlots = template.content.querySelectorAll('slot[name]');
const defaultSlot = template.content.querySelector('slot:not([name])');
for (let i = 0; i < templateSlots.length; i++) {
const templateSlot = templateSlots[ i ];
const name = templateSlot.getAttribute('name');
const instanceSlot = this.querySelector('[slot="' + name + '"]');
if (instanceSlot) templateSlot.parentNode.replaceChild(instanceSlot, templateSlot);
else templateSlot.parentNode.removeChild(templateSlot);
}
if (this.children.length) {
while (this.firstChild) {
if (defaultSlot) defaultSlot.parentNode.insertBefore(this.firstChild, defaultSlot);
else this.removeChild(this.firstChild);
}
}
if (defaultSlot) defaultSlot.parentNode.removeChild(defaultSlot);
}
let child = template.content.firstChild;
while (child) {
tasks.push(this.#binder.add(child, this, this.data));
// this.#binder.add(child, this, this.data);
child = child.nextSibling;
}
this.#root.appendChild(template.content);
await Promise.all(tasks);
}
// async whenReady () {
// if (!this.#ready) {
// return new Promise(resolve => this.addEventListener('afterrender', resolve));
// }
// }
async attributeChangedCallback (name: string, from: string, to: string) {
await this.#attributed(name, from, to);
}
async adoptedCallback () {
if (this.#adopted) await this.#adopted();
}
async disconnectedCallback () {
Css.detach(this.#name);
if (this.#disconnected) await this.#disconnected();
}
async connectedCallback () {
Css.attach(this.#name, this.css);
if (!this.#flag) {
this.#flag = true;
this.dispatchEvent(this.#beforeRenderEvent);
await this.#render();
if (this.#rendered) await this.#rendered();
this.dispatchEvent(this.#afterRenderEvent);
this.#ready = true;
this.dispatchEvent(this.#readyEvent);
}
this.dispatchEvent(this.#beforeConnectedEvent);
if (this.#connected) await this.#connected();
this.dispatchEvent(this.#afterConnectedEvent);
}
}