diff --git a/demo/src/custom-cards/ha-demo-card.ts b/demo/src/custom-cards/ha-demo-card.ts index 3bc9837a1a..f3c37cb975 100644 --- a/demo/src/custom-cards/ha-demo-card.ts +++ b/demo/src/custom-cards/ha-demo-card.ts @@ -1,8 +1,9 @@ -import "@material/mwc-button"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { until } from "lit/directives/until"; +import { fireEvent } from "../../../src/common/dom/fire_event"; import "../../../src/components/ha-card"; +import "../../../src/components/ha-button"; import "../../../src/components/ha-circular-progress"; import { LovelaceCardConfig } from "../../../src/data/lovelace/config/card"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; @@ -11,7 +12,6 @@ import { demoConfigs, selectedDemoConfig, selectedDemoConfigIndex, - setDemoConfig, } from "../configs/demo-configs"; @customElement("ha-demo-card") @@ -64,9 +64,9 @@ export class HADemoCard extends LitElement implements LovelaceCard { )} - + ${this.hass.localize("ui.panel.page-demo.cards.demo.next_demo")} - +

@@ -87,9 +87,9 @@ export class HADemoCard extends LitElement implements LovelaceCard {

- + ${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")} - +
@@ -113,13 +113,7 @@ export class HADemoCard extends LitElement implements LovelaceCard { private async _updateConfig(index: number) { this._switching = true; - try { - await setDemoConfig(this.hass, this.lovelace!, index); - } catch (err: any) { - alert("Failed to switch config :-("); - } finally { - this._switching = false; - } + fireEvent(this, "set-demo-config" as any, { index }); } static get styles(): CSSResultGroup { @@ -149,7 +143,7 @@ export class HADemoCard extends LitElement implements LovelaceCard { height: 60px; } - .picker mwc-button { + .picker ha-button { margin-right: 8px; } diff --git a/demo/src/stubs/lovelace.ts b/demo/src/stubs/lovelace.ts index 53ada3e2e0..6896de37ef 100644 --- a/demo/src/stubs/lovelace.ts +++ b/demo/src/stubs/lovelace.ts @@ -1,9 +1,12 @@ import type { LocalizeFunc } from "../../../src/common/translations/localize"; import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; -import { selectedDemoConfig } from "../configs/demo-configs"; +import { + selectedDemoConfig, + selectedDemoConfigIndex, + setDemoConfig, +} from "../configs/demo-configs"; import "../custom-cards/cast-demo-row"; import "../custom-cards/ha-demo-card"; -import type { HADemoCard } from "../custom-cards/ha-demo-card"; export const mockLovelace = ( hass: MockHomeAssistant, @@ -19,17 +22,22 @@ export const mockLovelace = ( hass.mockWS("lovelace/resources", () => Promise.resolve([])); }; -customElements.whenDefined("hui-card").then(() => { +customElements.whenDefined("hui-root").then(() => { // eslint-disable-next-line - const HUIView = customElements.get("hui-card"); - // Patch HUI-VIEW to make the lovelace object available to the demo card - const oldCreateCard = HUIView!.prototype.createElement; + const HUIRoot = customElements.get("hui-root")!; - HUIView!.prototype.createElement = function (config) { - const el = oldCreateCard.call(this, config); - if (config.type === "custom:ha-demo-card") { - (el as HADemoCard).lovelace = this.lovelace; - } - return el; + const oldFirstUpdated = HUIRoot.prototype.firstUpdated; + + HUIRoot.prototype.firstUpdated = function (changedProperties) { + oldFirstUpdated.call(this, changedProperties); + this.addEventListener("set-demo-config", async (ev) => { + const index = (ev as CustomEvent).detail.index; + try { + await setDemoConfig(this.hass, this.lovelace!, index); + } catch (err: any) { + setDemoConfig(this.hass, this.lovelace!, selectedDemoConfigIndex); + alert("Failed to switch config :-("); + } + }); }; }); diff --git a/gallery/src/components/demo-card.ts b/gallery/src/components/demo-card.ts index 3e24d1897e..50048f081a 100644 --- a/gallery/src/components/demo-card.ts +++ b/gallery/src/components/demo-card.ts @@ -1,7 +1,9 @@ import { load } from "js-yaml"; -import { html, css, LitElement, PropertyValues } from "lit"; +import { LitElement, PropertyValueMap, css, html, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; -import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element"; +import memoizeOne from "memoize-one"; +import "../../../src/panels/lovelace/cards/hui-card"; +import type { HuiCard } from "../../../src/panels/lovelace/cards/hui-card"; import { HomeAssistant } from "../../../src/types"; export interface DemoCardConfig { @@ -19,7 +21,12 @@ class DemoCard extends LitElement { @state() private _size?: number; - @query("#card") private _card!: HTMLElement; + @query("hui-card", false) private _card?: HuiCard; + + private _config = memoizeOne((config: string) => { + const c = (load(config) as any)[0]; + return c; + }); render() { return html` @@ -30,63 +37,32 @@ class DemoCard extends LitElement { : ""}
-
- ${this.showConfig ? html`
${this.config.config.trim()}
` : ""} + + ${this.showConfig + ? html`
${this.config.config.trim()}
` + : nothing}
`; } - updated(changedProps: PropertyValues) { - super.updated(changedProps); - - if (changedProps.has("config")) { - const card = this._card; - while (card.lastChild) { - card.removeChild(card.lastChild); - } - - const el = this._createCardElement((load(this.config.config) as any)[0]); - card.appendChild(el); - this._getSize(el); - } - - if (changedProps.has("hass")) { - const card = this._card.lastChild; - if (card) { - (card as any).hass = this.hass; - } - } + private async _cardUpdated(ev) { + ev.stopPropagation(); + this._updateSize(); } - async _getSize(el) { - await customElements.whenDefined(el.localName); - - if (!("getCardSize" in el)) { - this._size = undefined; - return; - } - this._size = await el.getCardSize(); + private async _updateSize() { + this._size = await this._card?.getCardSize(); } - _createCardElement(cardConfig) { - const element = createCardElement(cardConfig); - if (this.hass) { - element.hass = this.hass; - } - element.addEventListener( - "ll-rebuild", - (ev) => { - ev.stopPropagation(); - this._rebuildCard(element, cardConfig); - }, - { once: true } - ); - return element; - } - - _rebuildCard(cardElToReplace, config) { - const newCardEl = this._createCardElement(config); - cardElToReplace.parentElement.replaceChild(newCardEl, cardElToReplace); + protected update( + _changedProperties: PropertyValueMap | Map + ): void { + super.update(_changedProperties); + this._updateSize(); } static styles = css` @@ -101,7 +77,7 @@ class DemoCard extends LitElement { font-size: 0.5em; color: var(--primary-text-color); } - #card { + hui-card { max-width: 400px; width: 100vw; } diff --git a/src/panels/lovelace/cards/hui-card.ts b/src/panels/lovelace/cards/hui-card.ts index 7d931632c2..4da45042df 100644 --- a/src/panels/lovelace/cards/hui-card.ts +++ b/src/panels/lovelace/cards/hui-card.ts @@ -1,5 +1,6 @@ -import { PropertyValues, ReactiveElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { PropertyValueMap, PropertyValues, ReactiveElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; import { MediaQueriesListener } from "../../../common/dom/media_query"; import "../../../components/ha-svg-icon"; import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; @@ -10,23 +11,41 @@ import { checkConditionsMet, } from "../common/validate-condition"; import { createCardElement } from "../create-element/create-card-element"; -import type { Lovelace, LovelaceCard, LovelaceLayoutOptions } from "../types"; +import { createErrorCardConfig } from "../create-element/create-element-base"; +import type { LovelaceCard, LovelaceLayoutOptions } from "../types"; declare global { interface HASSDomEvents { "card-visibility-changed": { value: boolean }; + "card-updated": undefined; } } @customElement("hui-card") export class HuiCard extends ReactiveElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public lovelace?: Lovelace; + @property({ type: Boolean }) public editMode = false; - @property({ attribute: false }) public isPanel = false; + @property({ type: Boolean }) public isPanel = false; - @state() public _config?: LovelaceCardConfig; + set config(config: LovelaceCardConfig | undefined) { + if (!config) return; + if (config.type !== this._config?.type) { + this._buildElement(config); + } else if (config !== this.config) { + this._element?.setConfig(config); + fireEvent(this, "card-updated"); + } + this._config = config; + } + + @property({ attribute: false }) + public get config() { + return this._config; + } + + private _config?: LovelaceCardConfig; private _element?: LovelaceCard; @@ -44,7 +63,7 @@ export class HuiCard extends ReactiveElement { public connectedCallback() { super.connectedCallback(); this._listenMediaQueries(); - this._updateElement(); + this._updateVisibility(); } public getCardSize(): number | Promise { @@ -56,7 +75,7 @@ export class HuiCard extends ReactiveElement { } public getLayoutOptions(): LovelaceLayoutOptions { - const configOptions = this._config?.layout_options ?? {}; + const configOptions = this.config?.layout_options ?? {}; if (this._element) { const cardOptions = this._element.getLayoutOptions?.() ?? {}; return { @@ -67,51 +86,76 @@ export class HuiCard extends ReactiveElement { return configOptions; } - // Public to make demo happy - public createElement(config: LovelaceCardConfig) { - const element = createCardElement(config) as LovelaceCard; + private _createElement(config: LovelaceCardConfig) { + const element = createCardElement(config); element.hass = this.hass; - element.editMode = this.lovelace?.editMode; + element.editMode = this.editMode; // Update element when the visibility of the card changes (e.g. conditional card or filter card) - element.addEventListener("card-visibility-changed", (ev) => { + element.addEventListener("card-visibility-changed", (ev: Event) => { ev.stopPropagation(); - this._updateElement(); + this._updateVisibility(); }); + element.addEventListener( + "ll-upgrade", + (ev: Event) => { + ev.stopPropagation(); + fireEvent(this, "card-updated"); + }, + { once: true } + ); + element.addEventListener( + "ll-rebuild", + (ev: Event) => { + ev.stopPropagation(); + this._buildElement(config); + fireEvent(this, "card-updated"); + }, + { once: true } + ); return element; } - public setConfig(config: LovelaceCardConfig): void { - if (this._config === config) { - return; - } - this._config = config; - this._element = this.createElement(config); + private _buildElement(config: LovelaceCardConfig) { + this._element = this._createElement(config); while (this.lastChild) { this.removeChild(this.lastChild); } - this.appendChild(this._element!); + this._updateVisibility(); } - protected update(changedProperties: PropertyValues) { - super.update(changedProperties); + protected update(changedProps: PropertyValues) { + super.update(changedProps); if (this._element) { - if (changedProperties.has("hass")) { - this._element.hass = this.hass; + if (changedProps.has("hass")) { + try { + this._element.hass = this.hass; + } catch (e: any) { + this._buildElement(createErrorCardConfig(e.message, null)); + } } - if (changedProperties.has("lovelace")) { - this._element.editMode = this.lovelace?.editMode; + if (changedProps.has("editMode")) { + try { + this._element.editMode = this.editMode; + } catch (e: any) { + this._buildElement(createErrorCardConfig(e.message, null)); + } } - if (changedProperties.has("hass") || changedProperties.has("lovelace")) { - this._updateElement(); - } - if (changedProperties.has("isPanel")) { + if (changedProps.has("isPanel")) { this._element.isPanel = this.isPanel; } } } + protected willUpdate( + changedProps: PropertyValueMap | Map + ): void { + if (changedProps.has("hass") || changedProps.has("lovelace")) { + this._updateVisibility(); + } + } + private _clearMediaQueries() { this._listeners.forEach((unsub) => unsub()); this._listeners = []; @@ -119,42 +163,50 @@ export class HuiCard extends ReactiveElement { private _listenMediaQueries() { this._clearMediaQueries(); - if (!this._config?.visibility) { + if (!this.config?.visibility) { return; } - const conditions = this._config.visibility; + const conditions = this.config.visibility; const hasOnlyMediaQuery = conditions.length === 1 && conditions[0].condition === "screen" && !!conditions[0].media_query; this._listeners = attachConditionMediaQueriesListeners( - this._config.visibility, + this.config.visibility, (matches) => { - this._updateElement(hasOnlyMediaQuery && matches); + this._updateVisibility(hasOnlyMediaQuery && matches); } ); } - private _updateElement(forceVisible?: boolean) { - if (!this._element) { + private _updateVisibility(forceVisible?: boolean) { + if (!this._element || !this.hass) { return; } if (this._element.hidden) { - this.style.setProperty("display", "none"); - this.toggleAttribute("hidden", true); + this._setElementVisibility(false); return; } const visible = forceVisible || - this.lovelace?.editMode || - !this._config?.visibility || - checkConditionsMet(this._config.visibility, this.hass); + this.editMode || + !this.config?.visibility || + checkConditionsMet(this.config.visibility, this.hass); + this._setElementVisibility(visible); + } + + private _setElementVisibility(visible: boolean) { + if (!this._element) return; + + if (this.hidden !== !visible) { + this.style.setProperty("display", visible ? "" : "none"); + this.toggleAttribute("hidden", !visible); + fireEvent(this, "card-visibility-changed", { value: visible }); + } - this.style.setProperty("display", visible ? "" : "none"); - this.toggleAttribute("hidden", !visible); if (!visible && this._element.parentElement) { this.removeChild(this._element); } else if (visible && !this._element.parentElement) { diff --git a/src/panels/lovelace/cards/hui-conditional-card.ts b/src/panels/lovelace/cards/hui-conditional-card.ts index b39d24e7bf..0152e73b60 100644 --- a/src/panels/lovelace/cards/hui-conditional-card.ts +++ b/src/panels/lovelace/cards/hui-conditional-card.ts @@ -3,7 +3,6 @@ import { fireEvent } from "../../../common/dom/fire_event"; import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import { computeCardSize } from "../common/compute-card-size"; import { HuiConditionalBase } from "../components/hui-conditional-base"; -import { createCardElement } from "../create-element/create-card-element"; import { LovelaceCard, LovelaceCardEditor } from "../types"; import { ConditionalCardConfig } from "./types"; @@ -38,28 +37,13 @@ class HuiConditionalCard extends HuiConditionalBase implements LovelaceCard { } private _createCardElement(cardConfig: LovelaceCardConfig) { - const element = createCardElement(cardConfig) as LovelaceCard; - if (this.hass) { - element.hass = this.hass; - } - element.addEventListener( - "ll-rebuild", - (ev) => { - ev.stopPropagation(); - this._rebuildCard(cardConfig); - }, - { once: true } - ); + const element = document.createElement("hui-card"); + element.hass = this.hass; + element.editMode = this.editMode; + element.config = cardConfig; return element; } - private _rebuildCard(config: LovelaceCardConfig): void { - this._element = this._createCardElement(config); - if (this.lastChild) { - this.replaceChild(this._element, this.lastChild); - } - } - protected setVisibility(conditionMet: boolean): void { const visible = this.editMode || conditionMet; const previouslyHidden = this.hidden; diff --git a/src/panels/lovelace/cards/hui-entity-filter-card.ts b/src/panels/lovelace/cards/hui-entity-filter-card.ts index 6dc0bb343f..25071a7b2a 100644 --- a/src/panels/lovelace/cards/hui-entity-filter-card.ts +++ b/src/panels/lovelace/cards/hui-entity-filter-card.ts @@ -1,5 +1,6 @@ import { PropertyValues, ReactiveElement } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import { HomeAssistant } from "../../../types"; import { computeCardSize } from "../common/compute-card-size"; @@ -11,11 +12,10 @@ import { checkConditionsMet, extractConditionEntityIds, } from "../common/validate-condition"; -import { createCardElement } from "../create-element/create-card-element"; import { EntityFilterEntityConfig } from "../entity-rows/types"; import { LovelaceCard } from "../types"; +import { HuiCard } from "./hui-card"; import { EntityFilterCardConfig } from "./types"; -import { fireEvent } from "../../../common/dom/fire_event"; @customElement("hui-entity-filter-card") export class HuiEntityFilterCard @@ -59,7 +59,7 @@ export class HuiEntityFilterCard @state() private _config?: EntityFilterCardConfig; - private _element?: LovelaceCard; + private _element?: HuiCard; private _configEntities?: EntityFilterEntityConfig[]; @@ -173,12 +173,12 @@ export class HuiEntityFilterCard } if (!this.lastChild) { - this._element.setConfig({ + this._element.config = { ...this._baseCardConfig!, entities: entitiesList, - }); + }; this._oldEntities = entitiesList; - } else if (this._element.tagName !== "HUI-ERROR-CARD") { + } else { const isSame = this._oldEntities && entitiesList.length === this._oldEntities.length && @@ -186,10 +186,10 @@ export class HuiEntityFilterCard if (!isSame) { this._oldEntities = entitiesList; - this._element.setConfig({ + this._element.config = { ...this._baseCardConfig!, entities: entitiesList, - }); + }; } } @@ -245,33 +245,12 @@ export class HuiEntityFilterCard } private _createCardElement(cardConfig: LovelaceCardConfig) { - const element = createCardElement(cardConfig) as LovelaceCard; - if (this.hass) { - element.hass = this.hass; - } - element.isPanel = this.isPanel; + const element = document.createElement("hui-card"); + element.hass = this.hass; element.editMode = this.editMode; - element.addEventListener( - "ll-rebuild", - (ev) => { - ev.stopPropagation(); - this._rebuildCard(element, cardConfig); - }, - { once: true } - ); + element.config = cardConfig; return element; } - - private _rebuildCard( - cardElToReplace: LovelaceCard, - config: LovelaceCardConfig - ): void { - const newCardEl = this._createCardElement(config); - if (cardElToReplace.parentElement) { - cardElToReplace.parentElement!.replaceChild(newCardEl, cardElToReplace); - } - this._element = newCardEl; - } } declare global { diff --git a/src/panels/lovelace/cards/hui-error-card.ts b/src/panels/lovelace/cards/hui-error-card.ts index 752ecbf9c3..a1047ec090 100644 --- a/src/panels/lovelace/cards/hui-error-card.ts +++ b/src/panels/lovelace/cards/hui-error-card.ts @@ -1,6 +1,6 @@ import { dump } from "js-yaml"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; -import { customElement, state } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import "../../../components/ha-alert"; import { HomeAssistant } from "../../../types"; import { LovelaceCard } from "../types"; @@ -10,6 +10,8 @@ import { ErrorCardConfig } from "./types"; export class HuiErrorCard extends LitElement implements LovelaceCard { public hass?: HomeAssistant; + @property({ attribute: false }) public editMode = false; + @state() private _config?: ErrorCardConfig; public getCardSize(): number { diff --git a/src/panels/lovelace/cards/hui-stack-card.ts b/src/panels/lovelace/cards/hui-stack-card.ts index 1f7ec20c53..aad2eacb40 100644 --- a/src/panels/lovelace/cards/hui-stack-card.ts +++ b/src/panels/lovelace/cards/hui-stack-card.ts @@ -1,19 +1,12 @@ -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - nothing, -} from "lit"; +import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import { property, state } from "lit/decorators"; -import { fireEvent } from "../../../common/dom/fire_event"; +import { computeRTLDirection } from "../../../common/util/compute_rtl"; import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import { HomeAssistant } from "../../../types"; -import { createCardElement } from "../create-element/create-card-element"; import { LovelaceCard, LovelaceCardEditor } from "../types"; +import "./hui-card"; +import type { HuiCard } from "./hui-card"; import { StackCardConfig } from "./types"; -import { computeRTLDirection } from "../../../common/util/compute_rtl"; export abstract class HuiStackCard extends LitElement @@ -32,7 +25,7 @@ export abstract class HuiStackCard @property({ type: Boolean }) public editMode = false; - @state() protected _cards?: LovelaceCard[]; + @state() protected _cards?: HuiCard[]; @state() protected _config?: T; @@ -49,30 +42,36 @@ export abstract class HuiStackCard } this._config = config; this._cards = config.cards.map((card) => { - const element = this._createCardElement(card) as LovelaceCard; + const element = this._createCardElement(card); return element; }); } - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - if ( - !this._cards || - (!changedProps.has("hass") && !changedProps.has("editMode")) - ) { - return; - } + protected update(changedProperties) { + super.update(changedProperties); - for (const element of this._cards) { - if (this.hass) { - element.hass = this.hass; + if (this._cards) { + if (changedProperties.has("hass")) { + this._cards.forEach((card) => { + card.hass = this.hass; + }); } - if (this.editMode !== undefined) { - element.editMode = this.editMode; + if (changedProperties.has("editMode")) { + this._cards.forEach((card) => { + card.editMode = this.editMode; + }); } } } + private _createCardElement(cardConfig: LovelaceCardConfig) { + const element = document.createElement("hui-card"); + element.hass = this.hass; + element.editMode = this.editMode; + element.config = cardConfig; + return element; + } + protected render() { if (!this._config || !this._cards) { return nothing; @@ -110,34 +109,4 @@ export abstract class HuiStackCard } `; } - - private _createCardElement(cardConfig: LovelaceCardConfig) { - const element = createCardElement(cardConfig) as LovelaceCard; - if (this.hass) { - element.hass = this.hass; - } - element.addEventListener( - "ll-rebuild", - (ev) => { - ev.stopPropagation(); - this._rebuildCard(element, cardConfig); - fireEvent(this, "ll-rebuild"); - }, - { once: true } - ); - return element; - } - - private _rebuildCard( - cardElToReplace: LovelaceCard, - config: LovelaceCardConfig - ): void { - const newCardEl = this._createCardElement(config); - if (cardElToReplace.parentElement) { - cardElToReplace.parentElement.replaceChild(newCardEl, cardElToReplace); - } - this._cards = this._cards!.map((curCardEl) => - curCardEl === cardElToReplace ? newCardEl : curCardEl - ); - } } diff --git a/src/panels/lovelace/components/hui-conditional-base.ts b/src/panels/lovelace/components/hui-conditional-base.ts index f392fc5c80..c921c2623d 100644 --- a/src/panels/lovelace/components/hui-conditional-base.ts +++ b/src/panels/lovelace/components/hui-conditional-base.ts @@ -3,6 +3,7 @@ import { customElement, property, state } from "lit/decorators"; import { MediaQueriesListener } from "../../../common/dom/media_query"; import { deepEqual } from "../../../common/util/deep-equal"; import { HomeAssistant } from "../../../types"; +import { HuiCard } from "../cards/hui-card"; import { ConditionalCardConfig } from "../cards/types"; import { Condition, @@ -12,7 +13,6 @@ import { validateConditionalConfig, } from "../common/validate-condition"; import { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types"; -import { LovelaceCard } from "../types"; declare global { interface HASSDomEvents { @@ -28,7 +28,7 @@ export class HuiConditionalBase extends ReactiveElement { @state() protected _config?: ConditionalCardConfig | ConditionalRowConfig; - protected _element?: LovelaceCard | LovelaceRow; + protected _element?: HuiCard | LovelaceRow; private _listeners: MediaQueriesListener[] = []; diff --git a/src/panels/lovelace/create-element/create-element-base.ts b/src/panels/lovelace/create-element/create-element-base.ts index 48bf11bef9..5ba763733c 100644 --- a/src/panels/lovelace/create-element/create-element-base.ts +++ b/src/panels/lovelace/create-element/create-element-base.ts @@ -152,6 +152,7 @@ const _lazyCreate = ( customElements.whenDefined(tag).then(() => { try { customElements.upgrade(element); + fireEvent(element, "ll-upgrade"); // @ts-ignore element.setConfig(config); } catch (err: any) { diff --git a/src/panels/lovelace/editor/card-editor/hui-card-preview.ts b/src/panels/lovelace/editor/card-editor/hui-card-preview.ts deleted file mode 100644 index 72a652834a..0000000000 --- a/src/panels/lovelace/editor/card-editor/hui-card-preview.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { PropertyValues, ReactiveElement } from "lit"; -import { property } from "lit/decorators"; -import { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; -import { HomeAssistant } from "../../../../types"; -import { createCardElement } from "../../create-element/create-card-element"; -import { createErrorCardConfig } from "../../create-element/create-element-base"; -import { LovelaceCard } from "../../types"; - -export class HuiCardPreview extends ReactiveElement { - @property({ attribute: false }) public hass?: HomeAssistant; - - @property({ attribute: false }) public config?: LovelaceCardConfig; - - private _element?: LovelaceCard; - - private get _error() { - return this._element?.tagName === "HUI-ERROR-CARD"; - } - - constructor() { - super(); - this.addEventListener("ll-rebuild", () => { - this._cleanup(); - if (this.config) { - this._createCard(this.config); - } - }); - } - - protected createRenderRoot() { - return this; - } - - protected update(changedProperties: PropertyValues) { - super.update(changedProperties); - - if (changedProperties.has("config")) { - const oldConfig = changedProperties.get("config") as - | undefined - | LovelaceCardConfig; - - if (!this.config) { - this._cleanup(); - return; - } - - if (!this.config.type) { - this._createCard( - createErrorCardConfig("No card type found", this.config) - ); - return; - } - - if (!this._element) { - this._createCard(this.config); - return; - } - - // in case the element was an error element we always want to recreate it - if (!this._error && oldConfig && this.config.type === oldConfig.type) { - try { - this._element.setConfig(this.config); - } catch (err: any) { - this._createCard(createErrorCardConfig(err.message, this.config)); - } - } else { - this._createCard(this.config); - } - } - - if (changedProperties.has("hass")) { - if (this._element) { - this._element.hass = this.hass; - } - } - } - - private _createCard(configValue: LovelaceCardConfig): void { - this._cleanup(); - this._element = createCardElement(configValue); - - this._element.editMode = true; - - if (this.hass) { - this._element!.hass = this.hass; - } - - this.appendChild(this._element!); - } - - private _cleanup() { - if (!this._element) { - return; - } - this.removeChild(this._element); - this._element = undefined; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hui-card-preview": HuiCardPreview; - } -} - -customElements.define("hui-card-preview", HuiCardPreview); diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-delete-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-delete-card.ts index 1483268554..59974f4f00 100644 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-delete-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-delete-card.ts @@ -5,7 +5,7 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; import { haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; -import "./hui-card-preview"; +import "../../cards/hui-card"; import type { DeleteCardDialogParams } from "./show-delete-card-dialog"; @customElement("hui-dialog-delete-card") @@ -45,10 +45,11 @@ export class HuiDialogDeleteCard extends LitElement { ${this._cardConfig ? html`
- + editMode + >
` : ""} @@ -74,7 +75,7 @@ export class HuiDialogDeleteCard extends LitElement { .element-preview { position: relative; } - hui-card-preview { + hui-card { margin: 4px auto; max-width: 500px; display: block; diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts index 7c5c20d94a..288e835290 100644 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts @@ -36,7 +36,7 @@ import { findLovelaceContainer } from "../lovelace-path"; import type { GUIModeChangedEvent } from "../types"; import "./hui-card-element-editor"; import type { HuiCardElementEditor } from "./hui-card-element-editor"; -import "./hui-card-preview"; +import "../../cards/hui-card"; import type { EditCardDialogParams } from "./show-edit-card-dialog"; declare global { @@ -245,11 +245,12 @@ export class HuiDialogEditCard >
- + > ${this._error ? html` ${this._cardConfig.map( (cardConfig) => html` - + ` )}
@@ -191,7 +188,7 @@ export class HuiDialogSuggestCard extends LitElement { .element-preview { position: relative; } - hui-card-preview, + hui-card, hui-section { padding-top: 8px; margin: 4px auto; diff --git a/src/panels/lovelace/editor/card-editor/hui-section-preview.ts b/src/panels/lovelace/editor/card-editor/hui-section-preview.ts deleted file mode 100644 index c569fcf4c1..0000000000 --- a/src/panels/lovelace/editor/card-editor/hui-section-preview.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { PropertyValues, ReactiveElement } from "lit"; -import { customElement, property } from "lit/decorators"; -import { LovelaceSectionElement } from "../../../../data/lovelace"; -import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section"; -import { HomeAssistant } from "../../../../types"; -import { createSectionElement } from "../../create-element/create-section-element"; -import { createErrorSectionConfig } from "../../sections/hui-error-section"; -import { LovelaceConfig } from "../../../../data/lovelace/config/types"; - -@customElement("hui-section-preview") -export class HuiSectionPreview extends ReactiveElement { - @property({ attribute: false }) public hass?: HomeAssistant; - - @property({ attribute: false }) public lovelace?: LovelaceConfig; - - @property({ attribute: false }) public config?: LovelaceSectionConfig; - - private _element?: LovelaceSectionElement; - - private get _error() { - return this._element?.tagName === "HUI-ERROR-SECTION"; - } - - constructor() { - super(); - this.addEventListener("ll-rebuild", () => { - this._cleanup(); - if (this.config) { - this._createSection(this.config); - } - }); - } - - protected createRenderRoot() { - return this; - } - - protected update(changedProperties: PropertyValues) { - super.update(changedProperties); - - if (changedProperties.has("config")) { - const oldConfig = changedProperties.get("config") as - | undefined - | LovelaceSectionConfig; - - if (!this.config) { - this._cleanup(); - return; - } - - if (!this.config.type) { - this._createSection(createErrorSectionConfig("No section type found")); - return; - } - - if (!this._element) { - this._createSection(this.config); - return; - } - - // in case the element was an error element we always want to recreate it - if (!this._error && oldConfig && this.config.type === oldConfig.type) { - try { - this._element.setConfig(this.config); - } catch (err: any) { - this._createSection(createErrorSectionConfig(err.message)); - } - } else { - this._createSection(this.config); - } - } - - if (changedProperties.has("hass")) { - if (this._element) { - this._element.hass = this.hass; - } - } - } - - private _createSection(configValue: LovelaceSectionConfig): void { - this._cleanup(); - this._element = createSectionElement(configValue) as LovelaceSectionElement; - - if (this.hass) { - this._element!.hass = this.hass; - } - - this.appendChild(this._element!); - } - - private _cleanup() { - if (!this._element) { - return; - } - this.removeChild(this._element); - this._element = undefined; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hui-section-preview": HuiSectionPreview; - } -} diff --git a/src/panels/lovelace/sections/hui-grid-section.ts b/src/panels/lovelace/sections/hui-grid-section.ts index 557559c394..a9698207b0 100644 --- a/src/panels/lovelace/sections/hui-grid-section.ts +++ b/src/panels/lovelace/sections/hui-grid-section.ts @@ -11,10 +11,10 @@ import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; +import { HuiCard } from "../cards/hui-card"; import "../components/hui-card-edit-mode"; import { moveCard } from "../editor/config-util"; import type { Lovelace } from "../types"; -import { HuiCard } from "../cards/hui-card"; const CARD_SORTABLE_OPTIONS: HaSortableOptions = { delay: 100, diff --git a/src/panels/lovelace/sections/hui-section.ts b/src/panels/lovelace/sections/hui-section.ts index 5a12701384..798025e0cc 100644 --- a/src/panels/lovelace/sections/hui-section.ts +++ b/src/panels/lovelace/sections/hui-section.ts @@ -1,5 +1,6 @@ import { PropertyValues, ReactiveElement } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; import { MediaQueriesListener } from "../../../common/dom/media_query"; import "../../../components/ha-svg-icon"; import type { LovelaceSectionElement } from "../../../data/lovelace"; @@ -16,7 +17,6 @@ import { attachConditionMediaQueriesListeners, checkConditionsMet, } from "../common/validate-condition"; -import { createErrorCardConfig } from "../create-element/create-element-base"; import { createSectionElement } from "../create-element/create-section-element"; import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog"; import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; @@ -26,7 +26,6 @@ import { parseLovelaceCardPath } from "../editor/lovelace-path"; import { generateLovelaceSectionStrategy } from "../strategies/get-strategy"; import type { Lovelace } from "../types"; import { DEFAULT_SECTION_LAYOUT } from "./const"; -import { fireEvent } from "../../../common/dom/fire_event"; declare global { interface HASSDomEvents { @@ -54,23 +53,15 @@ export class HuiSection extends ReactiveElement { private _listeners: MediaQueriesListener[] = []; - // Public to make demo happy - public createCardElement(cardConfig: LovelaceCardConfig) { + private _createCardElement(cardConfig: LovelaceCardConfig) { const element = document.createElement("hui-card"); element.hass = this.hass; - element.lovelace = this.lovelace; - element.setConfig(cardConfig); - element.addEventListener( - "ll-rebuild", - (ev: Event) => { - // In edit mode let it go to hui-root and rebuild whole section. - if (!this.lovelace!.editMode) { - ev.stopPropagation(); - this._rebuildCard(element, cardConfig); - } - }, - { once: true } - ); + element.editMode = this.lovelace?.editMode || false; + element.config = cardConfig; + element.addEventListener("card-updated", (ev: Event) => { + ev.stopPropagation(); + this._cards = [...this._cards]; + }); return element; } @@ -121,22 +112,14 @@ export class HuiSection extends ReactiveElement { // Config has not changed. Just props if (changedProperties.has("hass")) { this._cards.forEach((element) => { - try { - element.hass = this.hass; - } catch (e: any) { - this._rebuildCard(element, createErrorCardConfig(e.message, null)); - } + element.hass = this.hass; }); this._layoutElement.hass = this.hass; } if (changedProperties.has("lovelace")) { this._layoutElement.lovelace = this.lovelace; this._cards.forEach((element) => { - try { - element.lovelace = this.lovelace; - } catch (e: any) { - this._rebuildCard(element, createErrorCardConfig(e.message, null)); - } + element.editMode = this.lovelace?.editMode || false; }); } if (changedProperties.has("_cards")) { @@ -283,22 +266,8 @@ export class HuiSection extends ReactiveElement { return; } - this._cards = config.cards.map((cardConfig) => { - const element = this.createCardElement(cardConfig); - return element; - }); - } - - private _rebuildCard( - cardElToReplace: HuiCard, - config: LovelaceCardConfig - ): void { - const newCardEl = this.createCardElement(config); - if (cardElToReplace.parentElement) { - cardElToReplace.parentElement!.replaceChild(newCardEl, cardElToReplace); - } - this._cards = this._cards!.map((curCardEl) => - curCardEl === cardElToReplace ? newCardEl : curCardEl + this._cards = config.cards.map((cardConfig) => + this._createCardElement(cardConfig) ); } } diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts index bf7709f612..5aba06015d 100644 --- a/src/panels/lovelace/types.ts +++ b/src/panels/lovelace/types.ts @@ -17,6 +17,7 @@ declare global { // eslint-disable-next-line interface HASSDomEvents { "ll-rebuild": Record; + "ll-upgrade": Record; "ll-badge-rebuild": Record; } } diff --git a/src/panels/lovelace/views/hui-masonry-view.ts b/src/panels/lovelace/views/hui-masonry-view.ts index 0f15af53bb..b360b9601b 100644 --- a/src/panels/lovelace/views/hui-masonry-view.ts +++ b/src/panels/lovelace/views/hui-masonry-view.ts @@ -17,7 +17,7 @@ import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../types"; import { HuiCard } from "../cards/hui-card"; import { computeCardSize } from "../common/compute-card-size"; -import type { Lovelace, LovelaceBadge, LovelaceCard } from "../types"; +import type { Lovelace, LovelaceBadge } from "../types"; // Find column with < 5 size, else smallest column const getColumnIndex = (columnSizes: number[], size: number) => { @@ -249,7 +249,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement { } private _addCardToColumn(columnEl, index, editMode) { - const card: LovelaceCard = this.cards[index]; + const card: HuiCard = this.cards[index]; if (!editMode || this.isStrategy) { card.editMode = false; columnEl.appendChild(card); diff --git a/src/panels/lovelace/views/hui-panel-view.ts b/src/panels/lovelace/views/hui-panel-view.ts index ab31074891..9da53437c8 100644 --- a/src/panels/lovelace/views/hui-panel-view.ts +++ b/src/panels/lovelace/views/hui-panel-view.ts @@ -17,7 +17,7 @@ import type { HomeAssistant } from "../../../types"; import { HuiCard } from "../cards/hui-card"; import { HuiCardOptions } from "../components/hui-card-options"; import { HuiWarning } from "../components/hui-warning"; -import type { Lovelace, LovelaceCard } from "../types"; +import type { Lovelace } from "../types"; let editCodeLoaded = false; @@ -32,7 +32,7 @@ export class PanelView extends LitElement implements LovelaceViewElement { @property({ attribute: false }) public cards: HuiCard[] = []; - @state() private _card?: LovelaceCard | HuiWarning | HuiCardOptions; + @state() private _card?: HuiCard | HuiWarning | HuiCardOptions; public setConfig(_config: LovelaceViewConfig): void {} @@ -104,7 +104,7 @@ export class PanelView extends LitElement implements LovelaceViewElement { return; } - const card: LovelaceCard = this.cards[0]; + const card: HuiCard = this.cards[0]; card.isPanel = true; if (this.isStrategy || !this.lovelace?.editMode) { diff --git a/src/panels/lovelace/views/hui-sidebar-view.ts b/src/panels/lovelace/views/hui-sidebar-view.ts index 433bd87834..a33441232b 100644 --- a/src/panels/lovelace/views/hui-sidebar-view.ts +++ b/src/panels/lovelace/views/hui-sidebar-view.ts @@ -15,7 +15,7 @@ import type { HomeAssistant } from "../../../types"; import { HuiCard } from "../cards/hui-card"; import { HuiCardOptions } from "../components/hui-card-options"; import { replaceCard } from "../editor/config-util"; -import type { Lovelace, LovelaceCard } from "../types"; +import type { Lovelace } from "../types"; export class SideBarView extends LitElement implements LovelaceViewElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -140,9 +140,9 @@ export class SideBarView extends LitElement implements LovelaceViewElement { }); } - this.cards.forEach((card: LovelaceCard, idx) => { + this.cards.forEach((card, idx) => { const cardConfig = this._config?.cards?.[idx]; - let element: LovelaceCard | HuiCardOptions; + let element: HuiCard | HuiCardOptions; if (this.isStrategy || !this.lovelace?.editMode) { card.editMode = false; element = card; diff --git a/src/panels/lovelace/views/hui-view.ts b/src/panels/lovelace/views/hui-view.ts index f74984d32b..7f614b2c1f 100644 --- a/src/panels/lovelace/views/hui-view.ts +++ b/src/panels/lovelace/views/hui-view.ts @@ -21,7 +21,6 @@ import "../cards/hui-card"; import type { HuiCard } from "../cards/hui-card"; import { processConfigEntities } from "../common/process-config-entities"; import { createBadgeElement } from "../create-element/create-badge-element"; -import { createErrorCardConfig } from "../create-element/create-element-base"; import { createViewElement } from "../create-element/create-view-element"; import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog"; import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; @@ -77,19 +76,12 @@ export class HUIView extends ReactiveElement { private _createCardElement(cardConfig: LovelaceCardConfig) { const element = document.createElement("hui-card"); element.hass = this.hass; - element.lovelace = this.lovelace; - element.setConfig(cardConfig); - element.addEventListener( - "ll-rebuild", - (ev: Event) => { - // In edit mode let it go to hui-root and rebuild whole view. - if (!this.lovelace!.editMode) { - ev.stopPropagation(); - this._rebuildCard(element, cardConfig); - } - }, - { once: true } - ); + element.editMode = this.lovelace.editMode; + element.config = cardConfig; + element.addEventListener("card-updated", (ev: Event) => { + ev.stopPropagation(); + this._cards = [...this._cards]; + }); return element; } @@ -183,11 +175,7 @@ export class HUIView extends ReactiveElement { }); this._cards.forEach((element) => { - try { - element.hass = this.hass; - } catch (e: any) { - this._rebuildCard(element, createErrorCardConfig(e.message, null)); - } + element.hass = this.hass; }); this._sections.forEach((element) => { @@ -226,12 +214,7 @@ export class HUIView extends ReactiveElement { } }); this._cards.forEach((element) => { - try { - element.hass = this.hass; - element.lovelace = this.lovelace; - } catch (e: any) { - this._rebuildCard(element, createErrorCardConfig(e.message, null)); - } + element.editMode = this.lovelace.editMode; }); } if (changedProperties.has("_cards")) { @@ -388,19 +371,6 @@ export class HUIView extends ReactiveElement { }); } - private _rebuildCard( - cardElToReplace: HuiCard, - config: LovelaceCardConfig - ): void { - const newCardEl = this._createCardElement(config); - if (cardElToReplace.parentElement) { - cardElToReplace.parentElement!.replaceChild(newCardEl, cardElToReplace); - } - this._cards = this._cards!.map((curCardEl) => - curCardEl === cardElToReplace ? newCardEl : curCardEl - ); - } - private _rebuildBadge( badgeElToReplace: LovelaceBadge, config: LovelaceBadgeConfig diff --git a/src/panels/todo/ha-panel-todo.ts b/src/panels/todo/ha-panel-todo.ts index b1dcee7fb4..fd5436fe9b 100644 --- a/src/panels/todo/ha-panel-todo.ts +++ b/src/panels/todo/ha-panel-todo.ts @@ -17,12 +17,13 @@ import { html, nothing, } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { storage } from "../../common/decorators/storage"; import { fireEvent } from "../../common/dom/fire_event"; import { computeStateName } from "../../common/entity/compute_state_name"; +import { supportsFeature } from "../../common/entity/supports-feature"; import { navigate } from "../../common/navigate"; import { constructUrlCurrentPath } from "../../common/url/construct-url"; import { @@ -40,6 +41,7 @@ import "../../components/ha-two-pane-top-app-bar-fixed"; import { deleteConfigEntry } from "../../data/config_entries"; import { getExtendedEntityRegistryEntry } from "../../data/entity_registry"; import { fetchIntegrationManifest } from "../../data/integration"; +import { LovelaceCardConfig } from "../../data/lovelace/config/card"; import { TodoListEntityFeature, getTodoLists } from "../../data/todo"; import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow"; import { @@ -49,11 +51,8 @@ import { import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; import { haStyle } from "../../resources/styles"; import { HomeAssistant } from "../../types"; -import { HuiErrorCard } from "../lovelace/cards/hui-error-card"; -import { createCardElement } from "../lovelace/create-element/create-card-element"; -import { LovelaceCard } from "../lovelace/types"; +import "../lovelace/cards/hui-card"; import { showTodoItemEditDialog } from "./show-dialog-todo-item-editor"; -import { supportsFeature } from "../../common/entity/supports-feature"; @customElement("ha-panel-todo") class PanelTodo extends LitElement { @@ -63,8 +62,6 @@ class PanelTodo extends LitElement { @property({ type: Boolean, reflect: true }) public mobile = false; - @state() private _card?: LovelaceCard | HuiErrorCard; - @storage({ key: "selectedTodoEntity", state: true, @@ -128,15 +125,10 @@ class PanelTodo extends LitElement { if (changedProperties.has("_entityId") || !this.hasUpdated) { this._setupTodoElement(); } - - if (changedProperties.has("hass") && this._card) { - this._card.hass = this.hass; - } } private _setupTodoElement(): void { if (!this._entityId) { - this._card = undefined; navigate(constructUrlCurrentPath(""), { replace: true }); return; } @@ -144,13 +136,16 @@ class PanelTodo extends LitElement { constructUrlCurrentPath(createSearchParam({ entity_id: this._entityId })), { replace: true } ); - this._card = createCardElement({ - type: "todo-list", - entity: this._entityId, - }) as LovelaceCard; - this._card.hass = this.hass; } + private _cardConfig = memoizeOne( + (entityId: string) => + ({ + type: "todo-list", + entity: entityId, + }) as LovelaceCardConfig + ); + protected render(): TemplateResult { const entityRegistryEntry = this._entityId ? this.hass.entities[this._entityId] @@ -274,7 +269,16 @@ class PanelTodo extends LitElement { : nothing}
-
${this._card}
+
+ ${this._entityId + ? html` + + ` + : nothing} +
${entityState && supportsFeature(entityState, TodoListEntityFeature.CREATE_TODO_ITEM)