@surface/custom-element
Version:
Provides support of directives and data binding on custom elements.
94 lines (93 loc) • 4.41 kB
JavaScript
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;