Skip to content

Commit 5801ce9

Browse files
authored
Refactor dashboard conditional listeners to use mixin (#27837)
* Update media query to new APIs * Refactor conditional rendering * Cleanup * Restore original functionality * Restore * Reduce * Reduce * Reduce * Restore legacy code while we still support them * Clear conditional listeners before setting up again
1 parent 79ad9db commit 5801ce9

File tree

7 files changed

+169
-174
lines changed

7 files changed

+169
-174
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import type { ReactiveElement } from "lit";
2+
import { listenMediaQuery } from "../common/dom/media_query";
3+
import type { HomeAssistant } from "../types";
4+
import type { Condition } from "../panels/lovelace/common/validate-condition";
5+
import { checkConditionsMet } from "../panels/lovelace/common/validate-condition";
6+
7+
type Constructor<T> = abstract new (...args: any[]) => T;
8+
9+
/**
10+
* Extract media queries from conditions recursively
11+
*/
12+
export function extractMediaQueries(conditions: Condition[]): string[] {
13+
return conditions.reduce<string[]>((array, c) => {
14+
if ("conditions" in c && c.conditions) {
15+
array.push(...extractMediaQueries(c.conditions));
16+
}
17+
if (c.condition === "screen" && c.media_query) {
18+
array.push(c.media_query);
19+
}
20+
return array;
21+
}, []);
22+
}
23+
24+
/**
25+
* Helper to setup media query listeners for conditional visibility
26+
*/
27+
export function setupMediaQueryListeners(
28+
conditions: Condition[],
29+
hass: HomeAssistant,
30+
addListener: (unsub: () => void) => void,
31+
onUpdate: (conditionsMet: boolean) => void
32+
): void {
33+
const mediaQueries = extractMediaQueries(conditions);
34+
35+
if (mediaQueries.length === 0) return;
36+
37+
// Optimization for single media query
38+
const hasOnlyMediaQuery =
39+
conditions.length === 1 &&
40+
conditions[0].condition === "screen" &&
41+
!!conditions[0].media_query;
42+
43+
mediaQueries.forEach((mediaQuery) => {
44+
const unsub = listenMediaQuery(mediaQuery, (matches) => {
45+
if (hasOnlyMediaQuery) {
46+
onUpdate(matches);
47+
} else {
48+
const conditionsMet = checkConditionsMet(conditions, hass);
49+
onUpdate(conditionsMet);
50+
}
51+
});
52+
addListener(unsub);
53+
});
54+
}
55+
56+
/**
57+
* Mixin to handle conditional listeners for visibility control
58+
*
59+
* Provides lifecycle management for listeners (media queries, time-based, state changes, etc.)
60+
* that control conditional visibility of components.
61+
*
62+
* Usage:
63+
* 1. Extend your component with ConditionalListenerMixin(ReactiveElement)
64+
* 2. Override setupConditionalListeners() to setup your listeners
65+
* 3. Use addConditionalListener() to register unsubscribe functions
66+
* 4. Call clearConditionalListeners() and setupConditionalListeners() when config changes
67+
*
68+
* The mixin automatically:
69+
* - Sets up listeners when component connects to DOM
70+
* - Cleans up listeners when component disconnects from DOM
71+
*/
72+
export const ConditionalListenerMixin = <
73+
T extends Constructor<ReactiveElement>,
74+
>(
75+
superClass: T
76+
) => {
77+
abstract class ConditionalListenerClass extends superClass {
78+
private __listeners: (() => void)[] = [];
79+
80+
public connectedCallback() {
81+
super.connectedCallback();
82+
this.setupConditionalListeners();
83+
}
84+
85+
public disconnectedCallback() {
86+
super.disconnectedCallback();
87+
this.clearConditionalListeners();
88+
}
89+
90+
protected clearConditionalListeners(): void {
91+
this.__listeners.forEach((unsub) => unsub());
92+
this.__listeners = [];
93+
}
94+
95+
protected addConditionalListener(unsubscribe: () => void): void {
96+
this.__listeners.push(unsubscribe);
97+
}
98+
99+
protected setupConditionalListeners(): void {
100+
// Override in subclass
101+
}
102+
}
103+
return ConditionalListenerClass;
104+
};

src/panels/lovelace/badges/hui-badge.ts

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import type { PropertyValues } from "lit";
22
import { ReactiveElement } from "lit";
33
import { customElement, property } from "lit/decorators";
44
import { fireEvent } from "../../../common/dom/fire_event";
5-
import type { MediaQueriesListener } from "../../../common/dom/media_query";
65
import "../../../components/ha-svg-icon";
76
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
87
import type { HomeAssistant } from "../../../types";
98
import {
10-
attachConditionMediaQueriesListeners,
11-
checkConditionsMet,
12-
} from "../common/validate-condition";
9+
ConditionalListenerMixin,
10+
setupMediaQueryListeners,
11+
} from "../../../mixins/conditional-listener-mixin";
12+
import { checkConditionsMet } from "../common/validate-condition";
1313
import { createBadgeElement } from "../create-element/create-badge-element";
1414
import { createErrorBadgeConfig } from "../create-element/create-element-base";
1515
import type { LovelaceBadge } from "../types";
@@ -22,7 +22,7 @@ declare global {
2222
}
2323

2424
@customElement("hui-badge")
25-
export class HuiBadge extends ReactiveElement {
25+
export class HuiBadge extends ConditionalListenerMixin(ReactiveElement) {
2626
@property({ type: Boolean }) public preview = false;
2727

2828
@property({ attribute: false }) public config?: LovelaceBadgeConfig;
@@ -40,20 +40,16 @@ export class HuiBadge extends ReactiveElement {
4040

4141
private _element?: LovelaceBadge;
4242

43-
private _listeners: MediaQueriesListener[] = [];
44-
4543
protected createRenderRoot() {
4644
return this;
4745
}
4846

4947
public disconnectedCallback() {
5048
super.disconnectedCallback();
51-
this._clearMediaQueries();
5249
}
5350

5451
public connectedCallback() {
5552
super.connectedCallback();
56-
this._listenMediaQueries();
5753
this._updateVisibility();
5854
}
5955

@@ -137,26 +133,17 @@ export class HuiBadge extends ReactiveElement {
137133
}
138134
}
139135

140-
private _clearMediaQueries() {
141-
this._listeners.forEach((unsub) => unsub());
142-
this._listeners = [];
143-
}
144-
145-
private _listenMediaQueries() {
146-
this._clearMediaQueries();
147-
if (!this.config?.visibility) {
136+
protected setupConditionalListeners() {
137+
if (!this.config?.visibility || !this.hass) {
148138
return;
149139
}
150-
const conditions = this.config.visibility;
151-
const hasOnlyMediaQuery =
152-
conditions.length === 1 &&
153-
conditions[0].condition === "screen" &&
154-
!!conditions[0].media_query;
155140

156-
this._listeners = attachConditionMediaQueriesListeners(
141+
setupMediaQueryListeners(
157142
this.config.visibility,
158-
(matches) => {
159-
this._updateVisibility(hasOnlyMediaQuery && matches);
143+
this.hass,
144+
(unsub) => this.addConditionalListener(unsub),
145+
(conditionsMet) => {
146+
this._updateVisibility(conditionsMet);
160147
}
161148
);
162149
}

src/panels/lovelace/cards/hui-card.ts

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import type { PropertyValues } from "lit";
22
import { ReactiveElement } from "lit";
33
import { customElement, property } from "lit/decorators";
44
import { fireEvent } from "../../../common/dom/fire_event";
5-
import type { MediaQueriesListener } from "../../../common/dom/media_query";
65
import "../../../components/ha-svg-icon";
76
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
87
import type { HomeAssistant } from "../../../types";
8+
import {
9+
ConditionalListenerMixin,
10+
setupMediaQueryListeners,
11+
} from "../../../mixins/conditional-listener-mixin";
912
import { migrateLayoutToGridOptions } from "../common/compute-card-grid-size";
1013
import { computeCardSize } from "../common/compute-card-size";
11-
import {
12-
attachConditionMediaQueriesListeners,
13-
checkConditionsMet,
14-
} from "../common/validate-condition";
14+
import { checkConditionsMet } from "../common/validate-condition";
1515
import { tryCreateCardElement } from "../create-element/create-card-element";
1616
import { createErrorCardElement } from "../create-element/create-element-base";
1717
import type { LovelaceCard, LovelaceGridOptions } from "../types";
@@ -24,7 +24,7 @@ declare global {
2424
}
2525

2626
@customElement("hui-card")
27-
export class HuiCard extends ReactiveElement {
27+
export class HuiCard extends ConditionalListenerMixin(ReactiveElement) {
2828
@property({ type: Boolean }) public preview = false;
2929

3030
@property({ attribute: false }) public config?: LovelaceCardConfig;
@@ -44,20 +44,16 @@ export class HuiCard extends ReactiveElement {
4444

4545
private _element?: LovelaceCard;
4646

47-
private _listeners: MediaQueriesListener[] = [];
48-
4947
protected createRenderRoot() {
5048
return this;
5149
}
5250

5351
public disconnectedCallback() {
5452
super.disconnectedCallback();
55-
this._clearMediaQueries();
5653
}
5754

5855
public connectedCallback() {
5956
super.connectedCallback();
60-
this._listenMediaQueries();
6157
this._updateVisibility();
6258
}
6359

@@ -251,26 +247,17 @@ export class HuiCard extends ReactiveElement {
251247
}
252248
}
253249

254-
private _clearMediaQueries() {
255-
this._listeners.forEach((unsub) => unsub());
256-
this._listeners = [];
257-
}
258-
259-
private _listenMediaQueries() {
260-
this._clearMediaQueries();
261-
if (!this.config?.visibility) {
250+
protected setupConditionalListeners() {
251+
if (!this.config?.visibility || !this.hass) {
262252
return;
263253
}
264-
const conditions = this.config.visibility;
265-
const hasOnlyMediaQuery =
266-
conditions.length === 1 &&
267-
conditions[0].condition === "screen" &&
268-
!!conditions[0].media_query;
269254

270-
this._listeners = attachConditionMediaQueriesListeners(
255+
setupMediaQueryListeners(
271256
this.config.visibility,
272-
(matches) => {
273-
this._updateVisibility(hasOnlyMediaQuery && matches);
257+
this.hass,
258+
(unsub) => this.addConditionalListener(unsub),
259+
(conditionsMet) => {
260+
this._updateVisibility(conditionsMet);
274261
}
275262
);
276263
}

src/panels/lovelace/common/validate-condition.ts

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import { ensureArray } from "../../../common/array/ensure-array";
2-
import type { MediaQueriesListener } from "../../../common/dom/media_query";
3-
import { listenMediaQuery } from "../../../common/dom/media_query";
42

53
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
64
import { UNKNOWN } from "../../../data/entity";
@@ -362,31 +360,3 @@ export function addEntityToCondition(
362360
}
363361
return condition;
364362
}
365-
366-
export function extractMediaQueries(conditions: Condition[]): string[] {
367-
return conditions.reduce<string[]>((array, c) => {
368-
if ("conditions" in c && c.conditions) {
369-
array.push(...extractMediaQueries(c.conditions));
370-
}
371-
if (c.condition === "screen" && c.media_query) {
372-
array.push(c.media_query);
373-
}
374-
return array;
375-
}, []);
376-
}
377-
378-
export function attachConditionMediaQueriesListeners(
379-
conditions: Condition[],
380-
onChange: (visibility: boolean) => void
381-
): MediaQueriesListener[] {
382-
const mediaQueries = extractMediaQueries(conditions);
383-
384-
const listeners = mediaQueries.map((query) => {
385-
const listener = listenMediaQuery(query, (matches) => {
386-
onChange(matches);
387-
});
388-
return listener;
389-
});
390-
391-
return listeners;
392-
}

0 commit comments

Comments
 (0)