Refactor ConditionalListenerMixin and extract shared utilities (#27858)

* Refactor ConditionalListenerMixin and extract shared utilities

* Remove

* Use proper type

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Import

* Fix typing

* Docstrings

* Use generic types and refactor visibility handling

* Fix function signature and handle other keys separately

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
This commit is contained in:
Aidan Timson
2025-11-12 09:50:38 +00:00
committed by GitHub
parent 0036679553
commit d38d770e1a
9 changed files with 188 additions and 184 deletions

View File

@@ -1,82 +1,53 @@
import type { ReactiveElement } from "lit";
import { listenMediaQuery } from "../common/dom/media_query";
import type { HomeAssistant } from "../types";
import { setupMediaQueryListeners } from "../common/condition/listeners";
import type { Condition } from "../panels/lovelace/common/validate-condition";
import { checkConditionsMet } from "../panels/lovelace/common/validate-condition";
type Constructor<T> = abstract new (...args: any[]) => T;
/**
* Extract media queries from conditions recursively
* Base config type that can be used with conditional listeners
*/
export function extractMediaQueries(conditions: Condition[]): string[] {
return conditions.reduce<string[]>((array, c) => {
if ("conditions" in c && c.conditions) {
array.push(...extractMediaQueries(c.conditions));
}
if (c.condition === "screen" && c.media_query) {
array.push(c.media_query);
}
return array;
}, []);
}
/**
* Helper to setup media query listeners for conditional visibility
*/
export function setupMediaQueryListeners(
conditions: Condition[],
hass: HomeAssistant,
addListener: (unsub: () => void) => void,
onUpdate: (conditionsMet: boolean) => void
): void {
const mediaQueries = extractMediaQueries(conditions);
if (mediaQueries.length === 0) return;
// Optimization for single media query
const hasOnlyMediaQuery =
conditions.length === 1 &&
conditions[0].condition === "screen" &&
!!conditions[0].media_query;
mediaQueries.forEach((mediaQuery) => {
const unsub = listenMediaQuery(mediaQuery, (matches) => {
if (hasOnlyMediaQuery) {
onUpdate(matches);
} else {
const conditionsMet = checkConditionsMet(conditions, hass);
onUpdate(conditionsMet);
}
});
addListener(unsub);
});
export interface ConditionalConfig {
visibility?: Condition[];
[key: string]: any;
}
/**
* Mixin to handle conditional listeners for visibility control
*
* Provides lifecycle management for listeners (media queries, time-based, state changes, etc.)
* that control conditional visibility of components.
* Provides lifecycle management for listeners that control conditional
* visibility of components.
*
* Usage:
* 1. Extend your component with ConditionalListenerMixin(ReactiveElement)
* 2. Override setupConditionalListeners() to setup your listeners
* 3. Use addConditionalListener() to register unsubscribe functions
* 4. Call clearConditionalListeners() and setupConditionalListeners() when config changes
* 1. Extend your component with ConditionalListenerMixin<YourConfigType>(ReactiveElement)
* 2. Ensure component has config.visibility or _config.visibility property with conditions
* 3. Ensure component has _updateVisibility() or _updateElement() method
* 4. Override setupConditionalListeners() if custom behavior needed (e.g., filter conditions)
*
* The mixin automatically:
* - Sets up listeners when component connects to DOM
* - Cleans up listeners when component disconnects from DOM
* - Handles conditional visibility based on defined conditions
*/
export const ConditionalListenerMixin = <
T extends Constructor<ReactiveElement>,
TConfig extends ConditionalConfig = ConditionalConfig,
>(
superClass: T
superClass: Constructor<ReactiveElement>
) => {
abstract class ConditionalListenerClass extends superClass {
private __listeners: (() => void)[] = [];
protected _config?: TConfig;
public config?: TConfig;
public hass?: HomeAssistant;
protected _updateElement?(config: TConfig): void;
protected _updateVisibility?(conditionsMet?: boolean): void;
public connectedCallback() {
super.connectedCallback();
this.setupConditionalListeners();
@@ -87,17 +58,65 @@ export const ConditionalListenerMixin = <
this.clearConditionalListeners();
}
/**
* Clear conditional listeners
*
* This method is called when the component is disconnected from the DOM.
* It clears all the listeners that were set up by the setupConditionalListeners() method.
*/
protected clearConditionalListeners(): void {
this.__listeners.forEach((unsub) => unsub());
this.__listeners = [];
}
/**
* Add a conditional listener to the list of listeners
*
* This method is called when a new listener is added.
* It adds the listener to the list of listeners.
*
* @param unsubscribe - The unsubscribe function to call when the listener is no longer needed
* @returns void
*/
protected addConditionalListener(unsubscribe: () => void): void {
this.__listeners.push(unsubscribe);
}
protected setupConditionalListeners(): void {
// Override in subclass
/**
* Setup conditional listeners for visibility control
*
* Default implementation:
* - Checks config.visibility or _config.visibility for conditions (if not provided)
* - Sets up appropriate listeners based on condition types
* - Calls _updateVisibility() or _updateElement() when conditions change
*
* Override this method to customize behavior (e.g., filter conditions first)
* and call super.setupConditionalListeners(customConditions) to reuse the base implementation
*
* @param conditions - Optional conditions array. If not provided, will check config.visibility or _config.visibility
*/
protected setupConditionalListeners(conditions?: Condition[]): void {
const config = this.config || this._config;
const finalConditions = conditions || config?.visibility;
if (!finalConditions || !this.hass) {
return;
}
const onUpdate = (conditionsMet: boolean) => {
if (this._updateVisibility) {
this._updateVisibility(conditionsMet);
} else if (this._updateElement && config) {
this._updateElement(config);
}
};
setupMediaQueryListeners(
finalConditions,
this.hass,
(unsub) => this.addConditionalListener(unsub),
onUpdate
);
}
}
return ConditionalListenerClass;