rimmel
Version:
A Stream-Oriented UI library for the Rx.Observable Universe
154 lines (151 loc) • 6.35 kB
JavaScript
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