element-vir
Version:
Heroic. Reactive. Declarative. Type safe. Web components without compromise.
106 lines (105 loc) • 4.32 kB
JavaScript
import { isObservableBase } from 'observavir';
import { property } from '../../lit-exports/base-lit-exports.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 (typeof propKey !== 'string' && typeof propKey !== 'number' && typeof propKey !== 'symbol') {
throw new TypeError(`Property name must be a string, got type '${typeof propKey}' from: '${String(propKey)}' for '${elementTagName.toLowerCase()}'`);
}
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 verifyProperty(propertyKey) {
if (shouldAlreadyExist) {
assertValidPropertyName(propertyKey, element, element.tagName);
}
else {
bindReactiveProperty(element, propertyKey);
}
}
function valueGetter(target, propertyKey) {
verifyProperty(propertyKey);
return elementAsProps[propertyKey];
}
const propsProxy = new Proxy({}, {
get: valueGetter,
set(target, propertyKey, newValue) {
verifyProperty(propertyKey);
const oldValue = elementAsProps[propertyKey];
function setValueOnElement(value) {
/**
* 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] = value;
elementAsProps[propertyKey] = value;
}
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. */
element.observablePropertyListenerMap[propertyKey] = undefined;
}
setValueOnElement(newValue);
return true;
},
ownKeys(target) {
return Reflect.ownKeys(target);
},
getOwnPropertyDescriptor(target, propertyName) {
if (propertyName in target) {
return {
get value() {
return valueGetter(target, propertyName);
},
configurable: true,
enumerable: true,
};
}
return undefined;
},
has(target, propertyName) {
return Reflect.has(target, propertyName);
},
});
return propsProxy;
}