lion/packages/form-core/src/utils/SyncUpdatableMixin.js

93 lines
3.5 KiB
JavaScript

import { dedupeMixin } from '@lion/core';
// TODO: will be moved to @Lion/core later?
/**
* @desc Why this mixin?
* - it adheres to the "Member Order Independence" web components standard:
* https://github.com/webcomponents/gold-standard/wiki/Member-Order-Independence
* - sync observers can be dependent on the outcome of the render function (or, more generically
* speaking, the light and shadow dom). This aligns with the 'updated' callback that is supported
* out of the box by LitElement, which runs after connectedCallback as well.
* - makes the propertyAccessor.`hasChanged` compatible in synchronous updates:
* `updateSync` will only be called when new value differs from old value.
* See: https://lit-element.polymer-project.org/guide/lifecycle#haschanged
* - it is a stable abstaction on top of a protected/non offical lifecycle LitElement api.
* Whenever the implementation of `_requestUpdate` changes (this happened in the past for
* `requestUpdate`) we only have to change our abstraction instead of all our components
*/
export const SyncUpdatableMixin = dedupeMixin(
superclass =>
class SyncUpdatable extends superclass {
constructor() {
super();
// Namespace for this mixin that guarantees naming clashes will not occur...
this.__SyncUpdatableNamespace = {};
}
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.__SyncUpdatableNamespace.connected = true;
this.__syncUpdatableInitialize();
}
disconnectedCallback() {
super.disconnectedCallback();
this.__SyncUpdatableNamespace.connected = false;
}
/**
* Makes the propertyAccessor.`hasChanged` compatible in synchronous updates
* @param {string} name
* @param {*} oldValue
*/
static __syncUpdatableHasChanged(name, newValue, oldValue) {
const properties = this._classProperties;
if (properties.get(name) && properties.get(name).hasChanged) {
return properties.get(name).hasChanged(newValue, oldValue);
}
return newValue !== oldValue;
}
__syncUpdatableInitialize() {
const ns = this.__SyncUpdatableNamespace;
const ctor = this.constructor;
ns.initialized = true;
// Empty queue...
if (ns.queue) {
Array.from(ns.queue).forEach(name => {
if (ctor.__syncUpdatableHasChanged(name, this[name], undefined)) {
this.updateSync(name, undefined);
}
});
}
}
_requestUpdate(name, oldValue) {
super._requestUpdate(name, oldValue);
this.__SyncUpdatableNamespace = this.__SyncUpdatableNamespace || {};
const ns = this.__SyncUpdatableNamespace;
const ctor = this.constructor;
// Before connectedCallback: queue
if (!ns.connected) {
ns.queue = ns.queue || new Set();
// Makes sure that we only initialize one time, with most up to date value
ns.queue.add(name);
} // After connectedCallback: guarded proxy to updateSync
else if (ctor.__syncUpdatableHasChanged(name, this[name], oldValue)) {
this.updateSync(name, oldValue);
}
}
/**
* @desc A public abstraction that has the exact same api as `_requestUpdate`.
* All code previously present in _requestUpdate can be placed in this method.
* @param {string} name
* @param {*} oldValue
*/
updateSync(name, oldValue) {} // eslint-disable-line class-methods-use-this, no-unused-vars
},
);