Skip to content

Commit 1160e9e

Browse files
committed
Add dynamic dropdown and selector options from attributes and templates
Let a dropdown or selector generate its options from a list instead of requiring every option to be hand-written, creating one option per item. Implements Discussion Nerwyn#137 ("Automatically populate dropdown options from effects / entity attribute lists"). Builds on the per-option config.option template variable from Nerwyn#198. Sources (all backwards compatible with the existing explicit list): - options_attribute / options_entity: read the list straight from an entity attribute, e.g. a light's effect_list, with no template. The same can be written as an `options: { attribute, entity_id }` object. select/input_select default to their `options` attribute. - options as a template string: render to a list for computed sources. Parsed from comma/newline strings or JSON/YAML arrays (including nunjucks `| dump`, HTML-escaped); items may be {value,label,icon} objects. - option_template: per-item label/icon/action applied to every generated option; each item's value is exposed as the `option` variable (alias of config.option). select/input_select options get a default select_option action, so those are zero-config. - Resolved lists are cached on a source signature so long lists stay cheap on frequent state updates, and re-render when the source changes. - Selected option label/icon in the dropdown window now render in the option's own context (also fixes the blank selected label from Nerwyn#198). - Editor: an "Options source" picker (manual / entity attribute / template) with a reusable option-template editor. - Docs: Dropdowns/Selectors, a Dynamic Options section, and worked examples (Example 9 light effect_list, 10 media_player source_list, 11 climate hvac_modes selector). - Tests: parseOptionsList / buildTemplatedOption / resolveOptionsAttribute (node:test + tsx). Ref Nerwyn#137
1 parent 83cb14f commit 1160e9e

12 files changed

Lines changed: 1530 additions & 48 deletions

README.md

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ You can override the default behavior of each option by changing their action. T
5353

5454
You can also choose to not give any of the dropdown options `Option` values, so that none are ever marked as the selected option. This makes it so that the default dropdown icon and label are always displayed, and the dropdown feature becomes more of a menu for firing different actions rather than one for selecting an option.
5555

56+
Instead of writing out every option by hand, you can also generate them from an entity attribute that contains a list (such as a light's `effect_list`) or from a template. This is useful for entities with long, dynamic attribute lists. See [Dynamic Options](#dynamic-options) below and [Example 9](#example-9) for a worked example.
57+
5658
## Inputs
5759

5860
<img src="https://raw.githubusercontent.com/Nerwyn/custom-card-features/main/assets/inputs_tile.png" width="600"/>
@@ -73,6 +75,8 @@ Like dropdowns, this feature works best with Home Assistant `select/input_select
7375

7476
Since each selector option is a custom feature button, you can override its default behavior by changing its tap action. The `Option` field will be the value to compare against the feature's value, whether that is its entity's state or one of its attributes. If they match and are not undefined, then the the option will be highlighted. The option highlight color defaults to the parent card color (usually the tile card color), but can be changed by setting the CSS attribute `--color` to a different value, either for the entire feature or an individual option.
7577

78+
Like dropdowns, selectors can also generate their options from an entity attribute list or a template. See [Dynamic Options](#dynamic-options) below and [Example 11](#example-11) for a worked example.
79+
7680
## Sliders
7781

7882
<img src="https://raw.githubusercontent.com/Nerwyn/custom-card-features/main/assets/sliders_tile.png" width="600"/>
@@ -166,6 +170,80 @@ Like sliders and spinboxes, selectors have a one second delay before updating th
166170

167171
Dropdowns can also be assigned their own default icon and label. When no option is selected, the default icon and label are used.
168172

173+
#### Dynamic Options
174+
175+
Instead of writing out each option by hand, dropdowns and selectors can generate their options from a list, automatically creating one option per item. This is ideal for entities with long, dynamic attribute lists like a light's `effect_list`, a media player's `source_list`, or a `select`/`input_select` entity's `options`. In the configuration UI, set **Options source** to `Entity attribute` or `Template` in the dropdown or selector general options. There are three sources:
176+
177+
##### From an entity attribute
178+
179+
Point the options straight at an entity attribute that contains a list. This requires no template:
180+
181+
```yaml
182+
- type: dropdown
183+
entity_id: light.my_light
184+
value_attribute: effect
185+
options_attribute: effect_list
186+
# options_entity: light.other_light # optional, defaults to entity_id
187+
```
188+
189+
The same can also be written as an object on `options`:
190+
191+
```yaml
192+
options:
193+
attribute: effect_list
194+
# entity_id: light.other_light # optional override
195+
```
196+
197+
For `select` and `input_select` entities you can leave the attribute blank — it defaults to their `options` attribute, so this is enough:
198+
199+
```yaml
200+
- type: dropdown
201+
entity_id: input_select.scene
202+
options_attribute: # blank → uses the select's "options" attribute
203+
```
204+
205+
##### From a template
206+
207+
For computed lists (joins, cross-entity lookups, filtering, etc.) point `options` at a template that renders to a list:
208+
209+
```yaml
210+
options: "{{ state_attr('light.my_light', 'effect_list') }}"
211+
```
212+
213+
A template result can be expressed in any of these forms:
214+
215+
- A comma-separated string, which is what the default `{{ state_attr(...) }}` output produces (e.g. `Solid,Rainbow,Fireworks`).
216+
- A newline-separated string, useful when values themselves contain commas (e.g. `{{ my_list | join('\n') }}`).
217+
- A JSON or YAML array, which preserves values containing commas and supports objects. Use the `dump` filter to emit JSON, e.g. `{{ my_list | dump }}`. List items may be objects of the form `{ value, label, icon }` (the keys `option`, `id`, `name`, `friendly_name`, and `title` are also accepted as aliases), letting you show a friendly label while selecting a different underlying value:
218+
219+
```yaml
220+
options: >-
221+
{{ [{'value': 'src1', 'label': 'Spotify', 'icon': 'mdi:spotify'},
222+
{'value': 'src2', 'label': 'Radio'}] | dump }}
223+
```
224+
225+
A template expression must be wrapped in `{{ }}` like all other templates, e.g. `options: "{{ state_attr('light.x', 'effect_list') }}"`. A plain string with no template expression is instead used verbatim as a literal comma-separated list, so `options: "Cool, Warm, Hot"` also works. Attribute sources can use the same object item form as templates.
226+
227+
##### Option template
228+
229+
For both dynamic sources, an **Option template** is applied to every generated option (with its own label, icon, action, and styles). Each item's value is exposed to the option template's fields as the `option` template variable (also available as `config.option`), so you wire up the label and action once:
230+
231+
```yaml
232+
option_template:
233+
label: '{{ option }}'
234+
tap_action:
235+
action: perform-action
236+
perform_action: light.turn_on
237+
target:
238+
entity_id: light.my_light
239+
data:
240+
effect: '{{ option }}'
241+
```
242+
243+
Generated options re-render automatically when their source changes, and the resolved list is cached so that frequent state updates remain cheap even for very long lists.
244+
245+
Selected state detection works exactly as it does for manual options: the value of each generated option is compared against the feature's value (its entity's state or attribute). For `select` and `input_select` entities, generated options that have no action of their own are automatically given a `select_option` action, so generating options from these requires no option template at all.
246+
169247
### Toggle General Options
170248

171249
<img src="https://raw.githubusercontent.com/Nerwyn/custom-card-features/main/assets/toggle_general_options.png" width="600"/>
@@ -188,7 +266,7 @@ Buttons, dropdowns, selectors, sliders, and toggles have design variants that ca
188266

189267
Almost all fields support nunjucks templating. Nunjucks is a templating engine for JavaScript, which is heavily based on the jinja2 templating engine for Python which Home Assistant uses. While the syntax of nunjucks and jinja2 is almost identical, you may find the [nunjucks documentation](https://mozilla.github.io/nunjucks/templating.html) useful. Most extensions supported by Home Assistant templates are supported by this templating system, but not all and the syntax may vary. Please see the [ha-nunjucks](https://github.com/Nerwyn/ha-nunjucks) repository for a list of available extensions. If you want additional extensions to be added or have templating questions or bugs, please make an issue or discussion on that repository, not this one.
190268

191-
You can include the current value of a feature and its units by using the variables `value` and `unit` in a label template. You can also include `hold_secs` in a template if performing a momentary repeat or end action. For toggles you can use the boolean variable `checked` to check whether the toggle is on or off. Each custom feature can also reference its entry using `config` within templates. `config.entity` and `config.attribute` will return the features entity ID and attribute with their templates rendered (if they have them), and other templated config fields can be rendered within templates by wrapping them in the function `render` within a template. Information about the parent card such as its entity ID, state, and attributes can be accessed using `stateObj`. The structure of `stateObj` can be found [here](https://github.com/home-assistant/home-assistant-js-websocket/blob/1d51737f6092b95e2bc98e85aca752771b97b760/lib/types.ts#L72-L96) as a `HassEntity` type and is listed below.
269+
You can include the current value of a feature and its units by using the variables `value` and `unit` in a label template. You can also include `hold_secs` in a template if performing a momentary repeat or end action. For toggles you can use the boolean variable `checked` to check whether the toggle is on or off. For dropdown and selector options you can use the variable `option` (also available as `config.option`) to reference that option's own value, which is especially useful with [dynamic options](#dynamic-options). Each custom feature can also reference its entry using `config` within templates. `config.entity` and `config.attribute` will return the features entity ID and attribute with their templates rendered (if they have them), and other templated config fields can be rendered within templates by wrapping them in the function `render` within a template. Information about the parent card such as its entity ID, state, and attributes can be accessed using `stateObj`. The structure of `stateObj` matches the [Home Assistant websocket `HassEntity` type definition](https://github.com/home-assistant/home-assistant-js-websocket/blob/1d51737f6092b95e2bc98e85aca752771b97b760/lib/types.ts#L72-L96) and is listed below.
192270

193271
<details>
194272

@@ -2457,3 +2535,113 @@ transparent: true
24572535
```
24582536
24592537
</details>
2538+
2539+
## Example 9
2540+
2541+
Generating dropdown options dynamically from a light's `effect_list` attribute. The dropdown lists every effect the light supports, marks the active one as selected, and tapping an option sets that effect — all without hand writing a single option. If the light's firmware adds or removes effects, the dropdown updates automatically.
2542+
2543+
<details>
2544+
2545+
<summary>Config</summary>
2546+
2547+
```yaml
2548+
type: tile
2549+
entity: light.wled
2550+
features:
2551+
- type: custom:service-call
2552+
entries:
2553+
- type: dropdown
2554+
entity_id: light.wled
2555+
value_attribute: effect
2556+
icon: mdi:string-lights
2557+
label: '{{ state_attr(config.entity, "effect") }}'
2558+
options_attribute: effect_list
2559+
option_template:
2560+
label: '{{ option }}'
2561+
tap_action:
2562+
action: perform-action
2563+
perform_action: light.turn_on
2564+
target:
2565+
entity_id: light.wled
2566+
data:
2567+
effect: '{{ option }}'
2568+
```
2569+
2570+
</details>
2571+
2572+
The same pattern works for any entity with a list attribute, for example a media player's `source_list` (paired with `media_player.select_source`) or a climate entity's `preset_modes`. For `select` and `input_select` entities you can omit both `options_attribute` and `option_template`, since the `options` attribute is used and a `select_option` action is generated for you:
2573+
2574+
```yaml
2575+
- type: dropdown
2576+
entity_id: input_select.scene
2577+
options_attribute:
2578+
```
2579+
2580+
If you need a computed list instead of a single attribute, point `options` at a template that renders to a list, for example to merge two lists or filter them:
2581+
2582+
```yaml
2583+
options: "{{ (state_attr(config.entity, 'effect_list') or []) | reject('eq', 'Solid') | list }}"
2584+
```
2585+
2586+
## Example 10
2587+
2588+
Generating a dropdown from a media player's `source_list`, with the active source marked as selected and tapping a source switching to it. A dropdown is a good fit here because source lists can be long.
2589+
2590+
<details>
2591+
2592+
<summary>Config</summary>
2593+
2594+
```yaml
2595+
type: tile
2596+
entity: media_player.living_room
2597+
features:
2598+
- type: custom:service-call
2599+
entries:
2600+
- type: dropdown
2601+
entity_id: media_player.living_room
2602+
value_attribute: source
2603+
icon: mdi:import
2604+
label: '{{ state_attr(config.entity, "source") }}'
2605+
options_attribute: source_list
2606+
option_template:
2607+
label: '{{ option }}'
2608+
tap_action:
2609+
action: perform-action
2610+
perform_action: media_player.select_source
2611+
target:
2612+
entity_id: media_player.living_room
2613+
data:
2614+
source: '{{ option }}'
2615+
```
2616+
2617+
</details>
2618+
2619+
## Example 11
2620+
2621+
Generating a **selector** from a climate entity's `hvac_modes`. Because a selector lays its options out in a single row, attribute lists with only a handful of items (like HVAC modes, fan presets, or vacuum fan speeds) work best; use a dropdown for longer lists. The climate state is the active HVAC mode, so the matching option is highlighted automatically.
2622+
2623+
<details>
2624+
2625+
<summary>Config</summary>
2626+
2627+
```yaml
2628+
type: tile
2629+
entity: climate.living_room
2630+
features:
2631+
- type: custom:service-call
2632+
entries:
2633+
- type: selector
2634+
entity_id: climate.living_room
2635+
options_attribute: hvac_modes
2636+
option_template:
2637+
label: '{{ option | replace("_", " ") | capitalize }}'
2638+
tap_action:
2639+
action: perform-action
2640+
perform_action: climate.set_hvac_mode
2641+
target:
2642+
entity_id: climate.living_room
2643+
data:
2644+
hvac_mode: '{{ option }}'
2645+
```
2646+
2647+
</details>

dist/custom-card-features.min.js

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)