Move card loading logic into hui-card (#21018)

* Move card rebuild to hui-card

* Use hui card in stack card

* add once to event

* Do not use state

* Use hui card in conditional card

* Use editMode instead of lovelace in hui card

* Fix edit mode

* Use hui-card in card dialog and panel todo

* Fix edit mode

* Fix types

* Migrate entity filter card

* Update demo card

* Fix UI view

* Allow edit mode attribute

* Remove unused condition

* Remove unused section preview code

* Remove useless check for config
This commit is contained in:
Paul Bottein 2024-06-12 13:38:21 +02:00 committed by GitHub
parent a497f42f73
commit 433c00b73a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 267 additions and 569 deletions

View File

@ -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 {
)}
</div>
<mwc-button @click=${this._nextConfig} .disabled=${this._switching}>
<ha-button @click=${this._nextConfig} .disabled=${this._switching}>
${this.hass.localize("ui.panel.page-demo.cards.demo.next_demo")}
</mwc-button>
</ha-button>
</div>
<div class="content">
<p class="small-hidden">
@ -87,9 +87,9 @@ export class HADemoCard extends LitElement implements LovelaceCard {
</div>
<div class="actions small-hidden">
<a href="https://www.home-assistant.io" target="_blank">
<mwc-button>
<ha-button>
${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")}
</mwc-button>
</ha-button>
</a>
</div>
</ha-card>
@ -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;
}

View File

@ -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 :-(");
}
});
};
});

View File

@ -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 {
: ""}
</h2>
<div class="root">
<div id="card"></div>
${this.showConfig ? html`<pre>${this.config.config.trim()}</pre>` : ""}
<hui-card
.config=${this._config(this.config.config)}
.hass=${this.hass}
@card-updated=${this._cardUpdated}
></hui-card>
${this.showConfig
? html`<pre>${this.config.config.trim()}</pre>`
: nothing}
</div>
`;
}
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<any> | Map<PropertyKey, unknown>
): 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;
}

View File

@ -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<number> {
@ -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<typeof this>) {
super.update(changedProperties);
protected update(changedProps: PropertyValues<typeof this>) {
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<any> | Map<PropertyKey, unknown>
): 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) {

View File

@ -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;

View File

@ -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 {

View File

@ -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 {

View File

@ -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<T extends StackCardConfig = StackCardConfig>
extends LitElement
@ -32,7 +25,7 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
@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<T extends StackCardConfig = StackCardConfig>
}
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<T extends StackCardConfig = StackCardConfig>
}
`;
}
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
);
}
}

View File

@ -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[] = [];

View File

@ -152,6 +152,7 @@ const _lazyCreate = <T extends keyof CreateElementConfigTypes>(
customElements.whenDefined(tag).then(() => {
try {
customElements.upgrade(element);
fireEvent(element, "ll-upgrade");
// @ts-ignore
element.setConfig(config);
} catch (err: any) {

View File

@ -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);

View File

@ -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`
<div class="element-preview">
<hui-card-preview
<hui-card
.hass=${this.hass}
.config=${this._cardConfig}
></hui-card-preview>
editMode
></hui-card>
</div>
`
: ""}
@ -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;

View File

@ -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
></hui-card-element-editor>
</div>
<div class="element-preview">
<hui-card-preview
<hui-card
.hass=${this.hass}
.config=${this._cardConfig}
editMode
class=${this._error ? "blur" : ""}
></hui-card-preview>
></hui-card>
${this._error
? html`
<ha-circular-progress
@ -452,7 +453,7 @@ export class HuiDialogEditCard
flex-direction: column;
}
.content hui-card-preview {
.content hui-card {
margin: 4px auto;
max-width: 390px;
}
@ -470,7 +471,7 @@ export class HuiDialogEditCard
flex-shrink: 1;
min-width: 0;
}
.content hui-card-preview {
.content hui-card {
padding: 8px 10px;
margin: auto 0px;
max-width: 500px;
@ -498,7 +499,7 @@ export class HuiDialogEditCard
position: absolute;
z-index: 10;
}
hui-card-preview {
hui-card {
padding-top: 8px;
margin-bottom: 4px;
display: block;

View File

@ -18,7 +18,7 @@ import {
LovelaceContainerPath,
parseLovelaceContainerPath,
} from "../lovelace-path";
import "./hui-card-preview";
import "../../cards/hui-card";
import { showCreateCardDialog } from "./show-create-card-dialog";
import { SuggestCardDialogParams } from "./show-suggest-card-dialog";
@ -84,10 +84,7 @@ export class HuiDialogSuggestCard extends LitElement {
<div class="element-preview">
${this._cardConfig.map(
(cardConfig) => html`
<hui-card-preview
.hass=${this.hass}
.config=${cardConfig}
></hui-card-preview>
<hui-card .hass=${this.hass} .config=${cardConfig}></hui-card>
`
)}
</div>
@ -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;

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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)
);
}
}

View File

@ -17,6 +17,7 @@ declare global {
// eslint-disable-next-line
interface HASSDomEvents {
"ll-rebuild": Record<string, unknown>;
"ll-upgrade": Record<string, unknown>;
"ll-badge-rebuild": Record<string, unknown>;
}
}

View File

@ -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);

View File

@ -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) {

View File

@ -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;

View File

@ -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

View File

@ -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}
</ha-button-menu>
<div id="columns">
<div class="column">${this._card}</div>
<div class="column">
${this._entityId
? html`
<hui-card
.hass=${this.hass}
.config=${this._cardConfig(this._entityId)}
></hui-card>
`
: nothing}
</div>
</div>
${entityState &&
supportsFeature(entityState, TodoListEntityFeature.CREATE_TODO_ITEM)