element-vir
Version:
Heroic. Reactive. Declarative. Type safe. Web components without compromise.
130 lines (129 loc) • 5.35 kB
JavaScript
import { isObservableBase } from 'observavir';
import { property } from '../../lit-exports/base-lit-exports.js';
import { ElementPropertyType, createPropertyCollisionError } from './assign-inputs.js';
/**
* Binds the given property key as a reactive property on the given element.
*
* @category Internal
*/
export function bindReactiveProperty(element, propertyKey) {
if (!(propertyKey in element)) {
property()(element, propertyKey);
}
}
function assertValidPropertyName(propKey, element, elementTagName) {
if (!(propKey in element)) {
throw new Error(`Property '${String(propKey)}' does not exist on '${elementTagName.toLowerCase()}'.`);
}
}
/**
* Creates an internal proxy for setting inputs and state properties and then updating them on the
* element itself so they will trigger lit's change detection.
*
* @category Internal
*/
export function createElementPropertyProxy(element, shouldAlreadyExist) {
/**
* Lit element updates state and inputs by setting them directly on the element, so we must do
* that here. DeclarativeElement's types, however, do not expose this behavior, so we add that
* back in here.
*/
const elementAsProps = element;
function assertPropertyIsReadable(propertyKey) {
if (shouldAlreadyExist) {
assertValidPropertyName(propertyKey, element, element.tagName);
}
}
function valueGetter(target, propertyKey) {
assertPropertyIsReadable(propertyKey);
return elementAsProps[propertyKey];
}
function setValue(target, propertyKey, newValue) {
/**
* The inputs and state proxies write to the same element properties, so a key used by both
* would silently overwrite the other.
*/
if (element.instanceInputs !== propsProxy &&
Object.hasOwn(element.instanceInputs, propertyKey)) {
throw createPropertyCollisionError({
propertyKey,
propertyType: ElementPropertyType.State,
tagName: element.tagName,
});
}
return writeValue(target, propertyKey, newValue);
}
/** Skips the collision check: clearing a colliding key must not be blocked by the collision. */
function writeValue(target, propertyKey, newValue) {
assertPropertyIsReadable(propertyKey);
if (!shouldAlreadyExist) {
bindReactiveProperty(element, propertyKey);
}
const oldValue = elementAsProps[propertyKey];
const existingPropertyListener = element.observablePropertyListenerMap[propertyKey];
if (oldValue !== newValue && isObservableBase(oldValue) && existingPropertyListener) {
/** Stop listening to the old value now that we have a new value */
oldValue.removeListener(existingPropertyListener);
}
if (isObservableBase(newValue)) {
/** If we're using an existing observable property */
if (existingPropertyListener) {
newValue.listen(false, existingPropertyListener);
}
else {
function newListener() {
element.requestUpdate();
}
element.observablePropertyListenerMap[propertyKey] = newListener;
newValue.listen(false, newListener);
}
}
else if (isObservableBase(oldValue)) {
/** Clear out old listener that is no longer used. */
delete element.observablePropertyListenerMap[propertyKey];
}
/**
* We need to at least set the property on target so we can detect it in "ownKeys" and
* "getOwnPropertyDescriptor". We don't need duplicates of the values stored in target but
* doing so makes console logging more effective it actually works).
*/
target[propertyKey] = newValue;
elementAsProps[propertyKey] = newValue;
return true;
}
const propsProxy = new Proxy({}, {
get: valueGetter,
set: setValue,
deleteProperty(target, propertyKey) {
/**
* Lit's reactive property accessors live on the element's prototype so the value must
* be cleared rather than deleted.
*/
writeValue(target, propertyKey, undefined);
return Reflect.deleteProperty(target, propertyKey);
},
ownKeys(target) {
return Reflect.ownKeys(target);
},
getOwnPropertyDescriptor(target, propertyName) {
if (propertyName in target) {
/**
* A data descriptor rather than an accessor: every render spreads these proxies for
* `_lastRenderedProps`, which reads each value immediately anyway.
*/
return {
value: valueGetter(target, propertyName),
configurable: true,
enumerable: true,
writable: true,
};
}
return undefined;
},
has(target, propertyName) {
/** Match "get", which always reads from the element itself. */
return Reflect.has(target, propertyName) || Reflect.has(element, propertyName);
},
});
return propsProxy;
}