UNPKG

@surface/custom-element

Version:

Provides support of directives and data binding on custom elements.

94 lines (93 loc) 4.41 kB
import { DisposableMetadata, HookableMetadata, camelToDashed } from "@surface/core"; import Metadata from "../metadata/metadata.js"; import PrototypeMetadata from "../metadata/prototype-metadata.js"; import StaticMetadata from "../metadata/static-metadata.js"; import AsyncObserver from "../reactivity/async-observer.js"; import { scheduler } from "../singletons.js"; const STANDARD_BOOLEANS = new Set(["checked", "disabled", "readonly"]); function getTypeSerializer(type) { if (typeof type == "function") { switch (type) { case Boolean: return { parse: x => x === "" || x == "true" }; case Number: return { parse: x => Number(x) || 0 }; case String: default: return { parse: x => x }; } } return type; } function patchPrototype(prototype) { const metadata = PrototypeMetadata.from(prototype); if (!metadata.attributeChangedCallback) { const callback = prototype.attributeChangedCallback; if (!callback || callback != metadata.attributeChangedCallback) { function attributeChangedCallback(attributeName, oldValue, newValue, namespace) { const metadata = Metadata.from(this); if (!metadata.isPropagatingCallback && !metadata.reflectingAttribute.has(attributeName)) { const action = () => { const value = attributeName == newValue && STANDARD_BOOLEANS.has(attributeName) ? "" : newValue; StaticMetadata.from(this.constructor).converters[attributeName]?.(this, value); }; void scheduler.enqueue(action, "high"); } metadata.isPropagatingCallback = true; callback?.call(this, attributeName, oldValue, newValue, namespace); metadata.isPropagatingCallback = false; } metadata.attributeChangedCallback = prototype.attributeChangedCallback = attributeChangedCallback; } } if (!prototype.constructor.hasOwnProperty("observedAttributes")) { function get() { return StaticMetadata.from(this).observedAttributes; } Object.defineProperty(prototype.constructor, "observedAttributes", { get }); } } function attribute(...args) { const decorator = (prototype, propertyKey) => { const options = args.length == 1 ? typeof args[0] == "function" ? { type: args[0] } : args[0] : { type: String }; const constructor = prototype.constructor; const attributeName = options.name ?? camelToDashed(propertyKey); const staticMetadata = StaticMetadata.from(constructor); staticMetadata.observedAttributes.push(attributeName); const serializer = getTypeSerializer(options.type); staticMetadata.converters[attributeName] = (target, value) => { const current = target[propertyKey]; const parsed = serializer.parse(value); if (!Object.is(current, parsed)) { target[propertyKey] = parsed; } }; const initializer = (instance) => { const metadata = Metadata.from(instance); const action = (value) => { metadata.reflectingAttribute.add(attributeName); if (typeof value == "boolean" && !serializer.stringfy) { value ? instance.setAttribute(attributeName, "") : instance.removeAttribute(attributeName); } else { instance.setAttribute(attributeName, (serializer.stringfy ?? String)(value)); } metadata.reflectingAttribute.delete(attributeName); }; const subscription = AsyncObserver.observe(instance, [propertyKey], scheduler).subscribe(action); DisposableMetadata.from(instance).add({ dispose: () => subscription.unsubscribe() }); }; HookableMetadata.from(constructor).finishers.push(initializer); patchPrototype(prototype); }; if (args.length == 1) { return decorator; } const [target, propertyKey] = args; decorator(target, propertyKey); } export default attribute;