UNPKG

rimmel

Version:

A Stream-Oriented UI library for the Rx.Observable Universe

154 lines (151 loc) 6.35 kB
import { RESOLVE_SELECTOR } from './constants.js'; import { isSourceBindingConfiguration, isSinkBindingConfiguration } from './types/internal.js'; import { Rimmel_Mount, Rimmel_Bind_Subtree } from './lifecycle/data-binding.js'; import { subscribe } from './lib/drain.js'; import { waitingElementHandlers } from './internal-state.js'; import { BehaviorSubject, Subject } from 'rxjs'; import { camelCase } from './utils/camelCase.js'; const SubjectProxy = (defaults = {}) => { const subjects = {}; return new Proxy({}, { get(_target, prop) { return subjects[prop] ?? (subjects[prop] = prop in defaults ? new BehaviorSubject(defaults[prop]) : new Subject()); } }); }; const SubjectProxy2 = (initials = {}, sources = {}) => { //const subjects = <Record<string | symbol, BehaviorSubject<unknown> | Subject<unknown>>>; const subjects = new Map(); return new Proxy(sources, { get(_target, prop) { return _target[prop] ?? subjects.get(prop) ?? subjects.set(prop, prop in initials ? new BehaviorSubject(initials[prop]) : new Subject()).get(prop); } }); }; class RimmelElement extends HTMLElement { component; attrs; #externalMutationObserver; #internalMutationObserver; extSinks; /** * Attributes on the external HTML element */ externalSourceAttributes; bindings; constructor(component, initFn) { super(); if (component) { this.component = component; // this.#events = {}; this.attachShadow({ mode: 'open' }); // shadow.adoptedStyleSheets = [...]; } const [attrs, events] = [...this.attributes].reduce((acc, b) => { // FIXME: REF0000266279391633 this is an awful way to look up leftover event handlers from the parser. const isEvent = +/^_?on/.test(b.nodeName); const t = acc[isEvent]; t[isEvent ? b.nodeName : camelCase(b.nodeName)] = b.nodeValue; return acc; }, [{}, {}]); const refs = waitingElementHandlers.get(this.attributes.resolve?.nodeValue ?? '') ?? []; this.attrs = SubjectProxy(attrs); // This condition holds for non-virtual custom elements. Won't be needed anymore if we split web components from virtual web components if (refs.length) { // Connect/Bind Outbound Events Object.keys(events) .map(name => refs.find(x => isSourceBindingConfiguration(x))) .filter(f => !!f) .forEach(f => { // TODO: store subscription for later removal subscribe(this, this.attrs[`on${f.eventName}`], f.listener); }); const sinkBindingConfigurations = refs.filter(r => isSinkBindingConfiguration(r)); this.extSinks = Object.fromEntries(sinkBindingConfigurations // .map(s => {[s.t]: s.sink = hijack?... .map((s) => [camelCase(s.t), s.source])); // Inbound Attributes this.externalSourceAttributes = Object.fromEntries(sinkBindingConfigurations // .map(s => {[s.t]: s.sink = hijack?... .map((s) => [s.t, s.sink])); // Outbound Events this.bindings = Object.fromEntries(refs.map(s => isSinkBindingConfiguration(s) ? [s.t, s.source] : [s.eventName, s.listener])); if (initFn) { //initFn?.(this, this.attrs, extSinks); // FIXME: maybe too much stuff merged in? const attributeProxy = SubjectProxy2(attrs, this.extSinks); initFn?.(this, attributeProxy); // initFn?.(this, { ...attrs, ...this.attrs, ...this.extSinks }); } } } render() { this.shadowRoot.innerHTML = this.component(this.attrs); } connectedCallback() { // Monitor for attribute changes on the custom element this.#externalMutationObserver = new MutationObserver((mutations) => { mutations.forEach(mutation => { const k = mutation.attributeName; const v = this.getAttribute(k); this.attrs[k].next(v); //this.bindings[k]?.next?.(v); // debugger; // this.externalSourceAttributes?.[k]?.next?.(v); // --- // Set the attribute on the custom element // Actually, don't, as it would cause an infinite loop // with this same mutationObserver... // Shall we just make it ignore self-originated changes // or should we just not set the attribute? // const sink = this.externalSourceAttributes?.[k]; // sink?.(this)(v); }); }); this.#externalMutationObserver.observe(this, { attributes: true, childList: false, subtree: false }); // Monitor for all other (RML) changes within the custom element, for data binding this.#internalMutationObserver = new MutationObserver(Rimmel_Mount); this.#internalMutationObserver.observe(this, { attributes: false, childList: true, subtree: true }); if (this.component) { this.render(); [...this.shadowRoot?.children ?? [], ...this.shadowRoot.querySelectorAll(RESOLVE_SELECTOR)] .forEach(s => { Rimmel_Bind_Subtree(s); }); } } disconnectedCallback() { // AKA: unmount this.#externalMutationObserver?.disconnect(); this.#internalMutationObserver?.disconnect(); } } /** * Register a Rimmel Component as a Custom Element in the DOM * * ## Examples * * ### Create a simple "Hello, World" web component * ```ts * import { rml, RegisterElement } from 'rimmel'; * * RegisterElement('custom-element', () => { * return rml` * <h1>Hello, world</h1> * `; * } * ``` **/ const RegisterElement = (tagName, component, initFn) => { // FIXME: prevent redefinition... // TODO: UnregisterElement? customElements.define(tagName, class extends RimmelElement { constructor() { super(component, initFn); } }); }; export { RegisterElement }; //# sourceMappingURL=custom-element.js.map