Add context providers and transform decorator (#15902)
This commit is contained in:
parent
4ba7e5cf0f
commit
74cfccaac7
|
@ -45,6 +45,7 @@
|
|||
"@fullcalendar/list": "6.1.5",
|
||||
"@fullcalendar/timegrid": "6.1.5",
|
||||
"@lezer/highlight": "1.1.3",
|
||||
"@lit-labs/context": "0.2.0",
|
||||
"@lit-labs/motion": "1.0.3",
|
||||
"@lit-labs/virtualizer": "1.0.1",
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
import { PropertyDeclaration, PropertyValues, ReactiveElement } from "lit";
|
||||
import { ClassElement } from "../../types";
|
||||
import { shallowEqual } from "../util/shallow-equal";
|
||||
|
||||
/**
|
||||
* Transform function type.
|
||||
*/
|
||||
export interface Transformer<T = any, V = any> {
|
||||
(value: V): T;
|
||||
}
|
||||
|
||||
type ReactiveTransformElement = ReactiveElement & {
|
||||
_transformers: Map<PropertyKey, Transformer>;
|
||||
_watching: Map<PropertyKey, Set<PropertyKey>>;
|
||||
};
|
||||
|
||||
type ReactiveElementClassWithTransformers = typeof ReactiveElement & {
|
||||
prototype: ReactiveTransformElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* Specifies an tranformer callback that is run when the value of the decorated property, or any of the properties in the watching array, changes.
|
||||
* The result of the tranformer is assigned to the decorated property.
|
||||
* The tranformer receives the current as arguments.
|
||||
*/
|
||||
export const transform =
|
||||
<T, V>(config: {
|
||||
transformer: Transformer<T, V>;
|
||||
watch?: PropertyKey[];
|
||||
propertyOptions?: PropertyDeclaration;
|
||||
}): any =>
|
||||
(clsElement: ClassElement) => {
|
||||
const key = String(clsElement.key);
|
||||
return {
|
||||
...clsElement,
|
||||
kind: "method",
|
||||
descriptor: {
|
||||
set(this: ReactiveTransformElement, value: V) {
|
||||
const oldValue = this[`__transform_${key}`];
|
||||
const trnsformr: Transformer<T, V> | undefined =
|
||||
this._transformers.get(key);
|
||||
if (trnsformr) {
|
||||
this[`__transform_${key}`] = trnsformr.call(this, value);
|
||||
} else {
|
||||
this[`__transform_${key}`] = value;
|
||||
}
|
||||
this[`__original_${key}`] = value;
|
||||
this.requestUpdate(key, oldValue);
|
||||
},
|
||||
get(): T {
|
||||
return this[`__transform_${key}`];
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
},
|
||||
finisher(cls: ReactiveElementClassWithTransformers) {
|
||||
// if we haven't wrapped `willUpdate` in this class, do so
|
||||
if (!cls.prototype._transformers) {
|
||||
cls.prototype._transformers = new Map<PropertyKey, Transformer>();
|
||||
cls.prototype._watching = new Map<PropertyKey, Set<PropertyKey>>();
|
||||
// @ts-ignore
|
||||
const userWillUpdate = cls.prototype.willUpdate;
|
||||
// @ts-ignore
|
||||
cls.prototype.willUpdate = function (
|
||||
this: ReactiveTransformElement,
|
||||
changedProperties: PropertyValues
|
||||
) {
|
||||
userWillUpdate.call(this, changedProperties);
|
||||
const keys = new Set<PropertyKey>();
|
||||
changedProperties.forEach((_v, k) => {
|
||||
const watchers = this._watching;
|
||||
const ks: Set<PropertyKey> | undefined = watchers.get(k);
|
||||
if (ks !== undefined) {
|
||||
ks.forEach((wk) => keys.add(wk));
|
||||
}
|
||||
});
|
||||
keys.forEach((k) => {
|
||||
// trigger setter
|
||||
this[k] = this[`__original_${String(k)}`];
|
||||
});
|
||||
};
|
||||
// clone any existing observers (superclasses)
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
} else if (!cls.prototype.hasOwnProperty("_transformers")) {
|
||||
const tranformers = cls.prototype._transformers;
|
||||
cls.prototype._transformers = new Map();
|
||||
tranformers.forEach((v: any, k: PropertyKey) =>
|
||||
cls.prototype._transformers.set(k, v)
|
||||
);
|
||||
}
|
||||
// set this method
|
||||
cls.prototype._transformers.set(clsElement.key, config.transformer);
|
||||
if (config.watch) {
|
||||
// store watchers
|
||||
config.watch.forEach((k) => {
|
||||
let curWatch = cls.prototype._watching.get(k);
|
||||
if (!curWatch) {
|
||||
curWatch = new Set();
|
||||
cls.prototype._watching.set(k, curWatch);
|
||||
}
|
||||
curWatch.add(clsElement.key);
|
||||
});
|
||||
}
|
||||
cls.createProperty(clsElement.key, {
|
||||
noAccessor: true,
|
||||
hasChanged: (v: any, o: any) => !shallowEqual(v, o),
|
||||
...config.propertyOptions,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* Compares two values for shallow equality, only 1 level deep.
|
||||
*/
|
||||
export const shallowEqual = (a: any, b: any): boolean => {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a && b && typeof a === "object" && typeof b === "object") {
|
||||
if (a.constructor !== b.constructor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let i: number | [any, any];
|
||||
let length: number;
|
||||
if (Array.isArray(a)) {
|
||||
length = a.length;
|
||||
if (length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a instanceof Map && b instanceof Map) {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
for (i of a.entries()) {
|
||||
if (!b.has(i[0])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (i of a.entries()) {
|
||||
if (i[1] !== b.get(i[0])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a instanceof Set && b instanceof Set) {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
for (i of a.entries()) {
|
||||
if (!b.has(i[0])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
|
||||
// @ts-ignore
|
||||
length = a.length;
|
||||
// @ts-ignore
|
||||
if (length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a.constructor === RegExp) {
|
||||
return a.source === b.source && a.flags === b.flags;
|
||||
}
|
||||
if (a.valueOf !== Object.prototype.valueOf) {
|
||||
return a.valueOf() === b.valueOf();
|
||||
}
|
||||
if (a.toString !== Object.prototype.toString) {
|
||||
return a.toString() === b.toString();
|
||||
}
|
||||
|
||||
const keys = Object.keys(a);
|
||||
length = keys.length;
|
||||
if (length !== Object.keys(b).length) {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, keys[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = length; i-- !== 0; ) {
|
||||
const key = keys[i];
|
||||
|
||||
if (a[key] !== b[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// true if both NaN, false otherwise
|
||||
// eslint-disable-next-line no-self-compare
|
||||
return a !== a && b !== b;
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
import { createContext } from "@lit-labs/context";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { EntityRegistryEntry } from "./entity_registry";
|
||||
|
||||
export const statesContext = createContext<HomeAssistant["states"]>("states");
|
||||
export const entitiesContext =
|
||||
createContext<HomeAssistant["entities"]>("entities");
|
||||
export const devicesContext =
|
||||
createContext<HomeAssistant["devices"]>("devices");
|
||||
export const areasContext = createContext<HomeAssistant["areas"]>("areas");
|
||||
export const localizeContext =
|
||||
createContext<HomeAssistant["localize"]>("localize");
|
||||
export const localeContext = createContext<HomeAssistant["locale"]>("locale");
|
||||
export const configContext = createContext<HomeAssistant["config"]>("config");
|
||||
export const themesContext = createContext<HomeAssistant["themes"]>("themes");
|
||||
export const selectedThemeContext =
|
||||
createContext<HomeAssistant["selectedTheme"]>("selectedTheme");
|
||||
export const userContext = createContext<HomeAssistant["user"]>("user");
|
||||
export const userDataContext =
|
||||
createContext<HomeAssistant["userData"]>("userData");
|
||||
export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
|
||||
|
||||
export const extendedEntitiesContext =
|
||||
createContext<EntityRegistryEntry[]>("extendedEntities");
|
|
@ -181,6 +181,10 @@ export class HomeAssistantMain extends LitElement {
|
|||
this.drawer.close();
|
||||
}
|
||||
|
||||
if (!changedProps.has("hass")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
|
||||
// Make app-drawer adjust to a potential LTR/RTL change
|
||||
|
|
|
@ -1,25 +1,21 @@
|
|||
import { consume } from "@lit-labs/context";
|
||||
import "@material/mwc-ripple";
|
||||
import type { Ripple } from "@material/mwc-ripple";
|
||||
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
queryAsync,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { customElement, eventOptions, queryAsync, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { DOMAINS_TOGGLE } from "../../../common/const";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||
|
@ -30,6 +26,13 @@ import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
|||
import { iconColorCSS } from "../../../common/style/icon_color_css";
|
||||
import "../../../components/ha-card";
|
||||
import { HVAC_ACTION_TO_MODE } from "../../../data/climate";
|
||||
import {
|
||||
entitiesContext,
|
||||
localeContext,
|
||||
localizeContext,
|
||||
statesContext,
|
||||
themesContext,
|
||||
} from "../../../data/context";
|
||||
import { LightEntity } from "../../../data/light";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
@ -40,6 +43,9 @@ import { hasAction } from "../common/has-action";
|
|||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { ButtonCardConfig } from "./types";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { FrontendLocaleData } from "../../../data/translation";
|
||||
import { Themes } from "../../../data/ws-themes";
|
||||
|
||||
@customElement("hui-button-card")
|
||||
export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
|
@ -71,10 +77,39 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
|||
};
|
||||
}
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@state() private _config?: ButtonCardConfig;
|
||||
|
||||
@consume<any>({ context: statesContext, subscribe: true })
|
||||
@transform({
|
||||
transformer: function (this: HuiButtonCard, value: HassEntities) {
|
||||
return this._config?.entity ? value[this._config?.entity] : undefined;
|
||||
},
|
||||
watch: ["_config"],
|
||||
})
|
||||
_stateObj?: HassEntity;
|
||||
|
||||
@consume({ context: themesContext, subscribe: true })
|
||||
_themes!: Themes;
|
||||
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
_localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: localeContext, subscribe: true })
|
||||
_locale!: FrontendLocaleData;
|
||||
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
@transform<HomeAssistant["entities"], HomeAssistant["entities"]>({
|
||||
transformer: function (this: HuiButtonCard, value) {
|
||||
return this._config?.entity
|
||||
? { [this._config?.entity]: value[this._config?.entity] }
|
||||
: {};
|
||||
},
|
||||
watch: ["_config"],
|
||||
})
|
||||
_entities!: HomeAssistant["entities"];
|
||||
|
||||
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
|
||||
|
||||
@state() private _shouldRenderRipple = false;
|
||||
|
@ -114,35 +149,11 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
|||
};
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.has("_config")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
|
||||
if (
|
||||
!oldHass ||
|
||||
oldHass.themes !== this.hass!.themes ||
|
||||
oldHass.locale !== this.hass!.locale
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
Boolean(this._config!.entity) &&
|
||||
oldHass.states[this._config!.entity!] !==
|
||||
this.hass!.states[this._config!.entity!]
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config || !this.hass) {
|
||||
if (!this._config) {
|
||||
return nothing;
|
||||
}
|
||||
const stateObj = this._config.entity
|
||||
? this.hass.states[this._config.entity]
|
||||
: undefined;
|
||||
const stateObj = this._stateObj;
|
||||
|
||||
if (this._config.entity && !stateObj) {
|
||||
return html`
|
||||
|
@ -207,10 +218,10 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
|||
${this._config.show_state && stateObj
|
||||
? html`<span class="state">
|
||||
${computeStateDisplay(
|
||||
this.hass.localize,
|
||||
this._localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
this._locale,
|
||||
this._entities
|
||||
)}
|
||||
</span>`
|
||||
: ""}
|
||||
|
@ -221,21 +232,23 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
|||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (!this._config || !this.hass) {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const oldThemes = changedProps.get("_themes") as
|
||||
| HomeAssistant["themes"]
|
||||
| undefined;
|
||||
const oldConfig = changedProps.get("_config") as
|
||||
| ButtonCardConfig
|
||||
| undefined;
|
||||
|
||||
if (
|
||||
!oldHass ||
|
||||
!oldThemes ||
|
||||
!oldConfig ||
|
||||
oldHass.themes !== this.hass.themes ||
|
||||
oldThemes !== this._themes ||
|
||||
oldConfig.theme !== this._config.theme
|
||||
) {
|
||||
applyThemesOnElement(this, this.hass.themes, this._config.theme);
|
||||
applyThemesOnElement(this, this._themes, this._config.theme);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import { ContextProvider } from "@lit-labs/context";
|
||||
import {
|
||||
areasContext,
|
||||
configContext,
|
||||
devicesContext,
|
||||
entitiesContext,
|
||||
localeContext,
|
||||
localizeContext,
|
||||
panelsContext,
|
||||
selectedThemeContext,
|
||||
statesContext,
|
||||
themesContext,
|
||||
userContext,
|
||||
userDataContext,
|
||||
} from "../data/context";
|
||||
import { Constructor, HomeAssistant } from "../types";
|
||||
import { HassBaseEl } from "./hass-base-mixin";
|
||||
|
||||
export const contextMixin = <T extends Constructor<HassBaseEl>>(
|
||||
superClass: T
|
||||
) =>
|
||||
class extends superClass {
|
||||
private __contextProviders: Record<
|
||||
string,
|
||||
ContextProvider<any> | undefined
|
||||
> = {
|
||||
states: new ContextProvider(
|
||||
this,
|
||||
statesContext,
|
||||
this.hass ? this.hass.states : this._pendingHass.states
|
||||
),
|
||||
entities: new ContextProvider(
|
||||
this,
|
||||
entitiesContext,
|
||||
this.hass ? this.hass.entities : this._pendingHass.entities
|
||||
),
|
||||
devices: new ContextProvider(
|
||||
this,
|
||||
devicesContext,
|
||||
this.hass ? this.hass.devices : this._pendingHass.devices
|
||||
),
|
||||
areas: new ContextProvider(
|
||||
this,
|
||||
areasContext,
|
||||
this.hass ? this.hass.areas : this._pendingHass.areas
|
||||
),
|
||||
localize: new ContextProvider(
|
||||
this,
|
||||
localizeContext,
|
||||
this.hass ? this.hass.localize : this._pendingHass.localize
|
||||
),
|
||||
locale: new ContextProvider(
|
||||
this,
|
||||
localeContext,
|
||||
this.hass ? this.hass.locale : this._pendingHass.locale
|
||||
),
|
||||
config: new ContextProvider(
|
||||
this,
|
||||
configContext,
|
||||
this.hass ? this.hass.config : this._pendingHass.config
|
||||
),
|
||||
themes: new ContextProvider(
|
||||
this,
|
||||
themesContext,
|
||||
this.hass ? this.hass.themes : this._pendingHass.themes
|
||||
),
|
||||
selectedTheme: new ContextProvider(
|
||||
this,
|
||||
selectedThemeContext,
|
||||
this.hass ? this.hass.selectedTheme : this._pendingHass.selectedTheme
|
||||
),
|
||||
user: new ContextProvider(
|
||||
this,
|
||||
userContext,
|
||||
this.hass ? this.hass.user : this._pendingHass.user
|
||||
),
|
||||
userData: new ContextProvider(
|
||||
this,
|
||||
userDataContext,
|
||||
this.hass ? this.hass.userData : this._pendingHass.userData
|
||||
),
|
||||
panels: new ContextProvider(
|
||||
this,
|
||||
panelsContext,
|
||||
this.hass ? this.hass.panels : this._pendingHass.panels
|
||||
),
|
||||
};
|
||||
|
||||
protected _updateHass(obj: Partial<HomeAssistant>) {
|
||||
super._updateHass(obj);
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (key in this.__contextProviders) {
|
||||
this.__contextProviders[key]!.setValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
|
@ -6,6 +6,7 @@ import DisconnectToastMixin from "./disconnect-toast-mixin";
|
|||
import { hapticMixin } from "./haptic-mixin";
|
||||
import { HassBaseEl } from "./hass-base-mixin";
|
||||
import { loggingMixin } from "./logging-mixin";
|
||||
import { contextMixin } from "./context-mixin";
|
||||
import MoreInfoMixin from "./more-info-mixin";
|
||||
import NotificationMixin from "./notification-mixin";
|
||||
import { panelTitleMixin } from "./panel-title-mixin";
|
||||
|
@ -31,4 +32,5 @@ export class HassElement extends ext(HassBaseEl, [
|
|||
hapticMixin,
|
||||
panelTitleMixin,
|
||||
loggingMixin,
|
||||
contextMixin,
|
||||
]) {}
|
||||
|
|
13
yarn.lock
13
yarn.lock
|
@ -1955,6 +1955,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@lit-labs/context@npm:0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "@lit-labs/context@npm:0.2.0"
|
||||
dependencies:
|
||||
"@lit/reactive-element": ^1.5.0
|
||||
lit: ^2.5.0
|
||||
checksum: 0b3d803ba81683d9650ba384e5f138656ecd52d6a54448535e867e0a0ba0cb23e4526ec52e82ed657e9c3598a103c0e8b164bfe927222467e349fb070c770af3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@lit-labs/motion@npm:1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "@lit-labs/motion@npm:1.0.3"
|
||||
|
@ -1982,7 +1992,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@lit/reactive-element@npm:^1.3.0, @lit/reactive-element@npm:^1.6.0":
|
||||
"@lit/reactive-element@npm:^1.3.0, @lit/reactive-element@npm:^1.5.0, @lit/reactive-element@npm:^1.6.0":
|
||||
version: 1.6.1
|
||||
resolution: "@lit/reactive-element@npm:1.6.1"
|
||||
dependencies:
|
||||
|
@ -9416,6 +9426,7 @@ __metadata:
|
|||
"@fullcalendar/timegrid": 6.1.5
|
||||
"@koa/cors": 4.0.0
|
||||
"@lezer/highlight": 1.1.3
|
||||
"@lit-labs/context": 0.2.0
|
||||
"@lit-labs/motion": 1.0.3
|
||||
"@lit-labs/virtualizer": 1.0.1
|
||||
"@material/chips": =14.0.0-canary.53b3cad2f.0
|
||||
|
|
Loading…
Reference in New Issue