Fun with custom:button-card

As per discussion in GitHub repo, the confirmation is carried out by Home Assistant. button-card just passes the confirmation config along with the action. If you want a custom solution take a look at PIN Code & Password Confirmation · custom-cards/button-card · Discussion #951 · GitHub. There are a bunch of diaog helpers on window.cardHelpers.

1 Like

Thanks a lot, it turned out to be super easy

/* standard dialog that I want to replace with button card's dialog */
//let reply = confirm(this.message);
//this.processUserReply(reply);
window.cardHelpers.showConfirmationDialog(this.button, {
  title: 'Are you sure?',
  text: this.message,
  confirmText: 'DEW IT!',
  dismissText: 'Cancel',
  destructive: true,
  confirm: () => this.processUserReply(true),
  dismiss: () => this.processUserReply(false),
});
1 Like

that might be a fine example for the show-and-tell section in button-card?

I’m having issues with my code, does anyone spot what I’m doing wrong here?

  - type: custom:button-card
    template: "list_2_items"
    show_name: false
    show_icon: false
    show_label: false
    entity: sensor.test_sensor
    custom_fields:
      item1:
        card:
          type: custom:button-card
          entity: sensor.test_sensor
          show_icon: false
          show_name: false
          show_label: true
          show_state: false
# Leads to 'TypeError: e.trim is not a function'
          label: '[[[ return helpers.relativeTime(entity.last_changed) ]]]'
      item2:
        card:
          type: custom:button-card
          entity: sensor.test_sensor
          show_icon: false
          show_name: false
          show_label: false
          show_state: true
# Leads to 'TypeError: this._config.state.find is not a function'
          state: '[[[ return helpers.relativeTime(entity.last_changed) ]]]'
  - type: custom:button-card
    template: "list_2_items"
    show_name: false
    show_icon: false
    show_label: false
    #entity: sensor.test_sensor
    custom_fields:
      item1:
        card:
          type: custom:button-card
          entity: sensor.test_sensor_1
          show_icon: false
          show_name: false
          show_label: true
          show_state: false
# Leads to 'ButtonCardJSTemplateError: Cannot read properties of undefined (reading 'last_changed')'
          label: '[[[ return helpers.relativeTime(entity.last_changed) ]]]'
      item2:
        card:
          type: custom:button-card
          entity: sensor.test_sensor_2
          show_icon: false
          show_name: false
          show_label: false
          show_state: true
          state: '[[[ return helpers.relativeTime(entity.last_changed) ]]]'

Apply four brackets when using JS code inside a custom field :arrow_down:

label: '[[[[ return helpers.relativeTime(entity.last_changed) ]]]]'

1 Like

I saw @LiQuid_cOOled’s message about using four brackets in custom_fields. I’ve never had to use four brackets before so I wanted to try this myself.

Interestingly, the four brackets was needed, but I don’t think it is a requirement of the custom_field specifically. I think it has something to do with the helper function within the custom_field. In my test,

name: "[[[ return entity.last_changed ]]]"
label: "[[[[ return helpers.relativeTime(entity.last_changed) ]]]]"

both worked in the custom_field. Note that the name uses three brackets while the label uses four. While the top-level card was able to use

name: "[[[ return entity.last_changed ]]]"
label: "[[[ return helpers.relativeTime(entity.last_changed) ]]]"


I’m still not completely sure why it works this way but it will definitely be something that I’ll have to keep in mind in the future.

label: "[[[[ return helpers.relativeTime(states[entity.entity_id].last_changed) ]]]]" also worked.

Card's Full Code
type: custom:button-card
show_icon: false
show_label: true
entity: "[[[ return variables.e ]]]"
name: "[[[ return entity.last_changed ]]]"
label: "[[[ return helpers.relativeTime(entity.last_changed) ]]]"
variables:
  e: sensor.living_room_motion_detector_temperature
styles:
  grid:
    - grid-template-areas: "'n' 'l' 'item1'"
    - grid-template-columns: min-content 
    - grid-template-rows: min-content min-content min-content
  card:
    - width: 250px
    - padding: 10px
custom_fields:
  item1:
    card:
      type: custom:button-card
      entity:  "[[[ return variables.e ]]]"
      show_icon: false
      show_label: true
      styles:
        card:
          - color: red
          - border: none
      name: "[[[ return entity.last_changed ]]]"
      label: "[[[[ return helpers.relativeTime(entity.last_changed) ]]]]"


It appears that I’ve grown pretty fond of button-card! It’s so great at making nice compact buttons for my primary tablet dashboard main display.

2 Likes

Hello! nice dashboard. Please can you show me the yaml code for the card where you display the three badges with the total number of phones in the house (away, on charge)?

Curious about your 7 day weather from a TV station. What are you using for that? Looks much better than my previous attempts and I’ve gone to a less appealing weather card.

there is a huge convo based on my misunderstanding of the use of those brackets in the button-card repo.

Jerome and Darryn explain it in detail there.

@d_sellers1, @Mariusthvdb is correct in saying that there is a background to nested templates with button-card. A few things to note in regards to the example in discussion:

  1. For clarity, it is best not to think of parent/child with button card as it is not that. The custom_field card be any card, but conveniently a custom_field can be a button-card. Nested templates can be included for any card should that card understand a [[[]]] javascript template.
  2. The nesting refers to the context of when the template is executed. So for [[[]]] (3 square brackets) this is the container button card, so entity here will be the container button-card’s entity. For [[[[]]]] (4 square brackets) the context is the custom_field card, in this case a button-card, and the entity here will be the entity of the custom_field button-card.
  3. If a containing button card gets [[[]]] (3 square brackets), a regular template, in config for custom_field, it will render the template and include in card config to create the card in the custom_field.
  4. If a containing button card gets [[[[]]]], it strips off one pair and passes remaining template to the custom_field card config. So in case of a custom_field being a button-card, it would then be a template for that button-card. It could also be an expander-card supported template, or Browser Mod popup config that contained a card that accepts templates.
  5. Now to understanding why [[[]]] (3 square brackets) in the containing card with helpers.relativeTime() provides an error. Well, it’s the output of helpers.relativeTime() that will get passed as config to the custom_field card. helpers.relativeTime() outputs a Lit HTML template (using <ha-relative-time> which does the update of the relative time string) which is an object with a strings array and Lit part etc. So it won’t be understood as card config for a string, even multiline, hence the error. Notwithstanding that, and following on from above, when used with [[[]]] (3 sqaure brackets) it would be using the entity of the containing card, not the contained custom_field button-card.

I hope that clears up a few things, including the error for helpers.relativeTime(). I am happy to answer any further questions that aids in understanding.

2 Likes

I’ve created a compact row using the Button Card. Unfortunately, I can’t figure out how to assign a tap function to the two rear buttons, “gen” and “grid.” I want the measured value to be displayed when tapped. Tap-Action = more.

No matter what I do, the tap function of the first button is always executed (“navigate”).

Can someone please help me?

name: PV
icon: kuf:measure_photovoltaic_inst
show_icon: true
show_name: true
tap_action:
  action: navigate
  navigation_path: /dashboard-neu/pv
styles:
  grid:
    - grid-template-areas: "\"i n gen grid\""
    - grid-template-columns: 36px auto max-content max-content
    - align-items: center
    - column-gap: 12px
  icon:
    - grid-area: i
    - width: 22px
    - height: 22px
    - color: var(--primary-text-color)
  card:
    - padding: 6px 14px
    - height: 32px
    - border-radius: 15px
  name:
    - grid-area: "n"
    - justify-self: start
    - text-align: left
    - padding-left: 0
    - font-weight: 500
    - font-size: 13px
  custom_fields:
    gen:
      - grid-area: gen
      - justify-self: end
    grid:
      - grid-area: grid
      - justify-self: end
custom_fields:
  gen: |
    [[[
      const v = Number(states['sensor.pv_erzeugung_w']?.state || 0);
      return `
        <span
          style="
            cursor:pointer;
            display:flex;
            align-items:center;
            gap:5px;
            font-size:12px;
            color:${v > 0 ? 'green' : '#999'};
            font-weight:500
          ">
          <ha-icon icon="mdi:solar-panel"
                   style="--mdc-icon-size:16px;"></ha-icon>
          ${v} W
        </span>
      `;
    ]]]
  grid: |
    [[[
      const v = Number(states['sensor.pv_bezug_lieferung_w']?.state || 0);
      const color = v < 0 ? 'green' : v > 0 ? 'red' : '#999';
      const icon  = v < 0
        ? 'mdi:transmission-tower-export'
        : 'mdi:transmission-tower';
      return `
        <span
          style="
            cursor:pointer;
            display:flex;
            align-items:center;
            gap:5px;
            font-size:12px;
            color:${color};
            font-weight:500
          ">
          <ha-icon icon="${icon}"
                   style="--mdc-icon-size:16px;"></ha-icon>
          ${Math.abs(v)} W
        </span>
      `;
    ]]]

The recommended solution would be to create each custom field as a nested button card, with its own tap_action. However, you can also add event listeners to your current custom fields, and run your actions via helpers.runAction. To try that, make the following changes:

  1. disable ripple and rename tap_action to icon_tap_action, so that it executes only when clicking the icon
show_ripple: false
icon_tap_action:
  action: navigate
  navigation_path: /dashboard-neu/pv
  1. add 2 items to card style
styles:
  card:
    - pointer-events: auto
    - pointer: default
  1. add variables:
variables:
  genAction:
    action: more-info
    entity: sensor.pv_erzeugung_w
  gridAction:
    action: more-info
    entity: sensor.pv_bezug_lieferung_w
  installListeners:
    force_eval: true
    value: |
      [[[
        this.updateComplete.then(() => {
          const eventName = 'pointerup';
          let gen = this.shadowRoot.querySelector('#gen');
          gen.addEventListener(eventName, (e) => {
            helpers.runAction(variables.genAction);
          }, {passive: true});
          let grid = this.shadowRoot.querySelector('#grid');
          grid.addEventListener(eventName, (e) => {
            helpers.runAction(variables.gridAction);
          }, {passive: true});
        });
      ]]]

The genAction and gridAction variables are action definitions (see documentation). The installListeners variable attaches listeners to custom fields in the DOM as soon as they are created. The eventName is quite important: I found that pointerup works well on both desktop and mobile, but you could try different or multiple event names.

Full card
type: custom:button-card
name: PV
icon: kuf:measure_photovoltaic_inst
show_icon: true
show_name: true
show_ripple: false
icon_tap_action:
  action: navigate
  navigation_path: /dashboard-neu/pv
styles:
  grid:
    - grid-template-areas: "\"i n gen grid\""
    - grid-template-columns: 36px auto max-content max-content
    - align-items: center
    - column-gap: 12px
  icon:
    - grid-area: i
    - width: 22px
    - height: 22px
    - color: var(--primary-text-color)
  card:
    - padding: 6px 14px
    - height: 32px
    - border-radius: 15px
    - pointer-events: auto
    - pointer: default
  name:
    - grid-area: "n"
    - justify-self: start
    - text-align: left
    - padding-left: 0
    - font-weight: 500
    - font-size: 13px
  custom_fields:
    gen:
      - grid-area: gen
      - justify-self: end
    grid:
      - grid-area: grid
      - justify-self: end
variables:
  genAction:
    action: more-info
    entity: sensor.pv_erzeugung_w
  gridAction:
    action: more-info
    entity: sensor.pv_bezug_lieferung_w
  installListeners:
    force_eval: true
    value: |
      [[[
        this.updateComplete.then(() => {
          const eventName = 'pointerup';
          let gen = this.shadowRoot.querySelector('#gen');
          gen.addEventListener(eventName, (e) => {
            helpers.runAction(variables.genAction);
          }, {passive: true});
          let grid = this.shadowRoot.querySelector('#grid');
          grid.addEventListener(eventName, (e) => {
            helpers.runAction(variables.gridAction);
          }, {passive: true});
        });
      ]]]
custom_fields:
  gen: |
    [[[
      const v = Number(states['sensor.pv_erzeugung_w']?.state || 0);
      return `
        <span
          style="
            cursor:pointer;
            display:flex;
            align-items:center;
            gap:5px;
            font-size:12px;
            color:${v > 0 ? 'green' : '#999'};
            font-weight:500
          ">
          <ha-icon icon="mdi:solar-panel"
                   style="--mdc-icon-size:16px;"></ha-icon>
          ${v} W
        </span>
      `;
    ]]]
  grid: |
    [[[
      const v = Number(states['sensor.pv_bezug_lieferung_w']?.state || 0);
      const color = v < 0 ? 'green' : v > 0 ? 'red' : '#999';
      const icon  = v < 0
        ? 'mdi:transmission-tower-export'
        : 'mdi:transmission-tower';
      return `
        <span
          style="
            cursor:pointer;
            display:flex;
            align-items:center;
            gap:5px;
            font-size:12px;
            color:${color};
            font-weight:500
          ">
          <ha-icon icon="${icon}"
                   style="--mdc-icon-size:16px;"></ha-icon>
          ${Math.abs(v)} W
        </span>
      `;
    ]]]
3 Likes

Thanks for the quick reply and the solution. Is it also possible to perform a tap action on the name “PV”?

Action similar to the “navigate” icon?

Yes, the same method, using ‘#name’ as the element ID.

installListeners:
    force_eval: true
    value: |
      [[[
        this.updateComplete.then(() => {
          const eventName = 'pointerup';
          let gen = this.shadowRoot.querySelector('#gen');
          gen.addEventListener(eventName, (e) => {
            helpers.runAction(variables.genAction);
          }, {passive: true});
          let grid = this.shadowRoot.querySelector('#grid');
          grid.addEventListener(eventName, (e) => {
            helpers.runAction(variables.gridAction);
          }, {passive: true});
          const nameAction = {action: 'navigate', navigation_path: '/dashboard-neu/pv' };
          let name = this.shadowRoot.querySelector('#name');
          name.addEventListener(eventName, (e) => {
            helpers.runAction(nameAction);
          }, {passive: true});
        });
      ]]]

And you probably also want to add - cursor: pointer to name’s style, to make it look “clickable”.

this is a very nice example for those actions and showcase the versatility of Button-card, almost make it into an integration :wink:

Would you be willing to add that, and the example below, to the Showcase section in the repo? I feel many others would benefit from that.

btw, did you consider adding action: javascript in there? Maybe that would allow for even more options in this config?

Sure, I’ll do that :slight_smile:

Your suggestion led me to an alternative (and easier) method: check if any of the custom fields are “under” the pointer.

tap_action:
  action: javascript
  javascript: |
    [[[
      /* each key/name in the object below is the ID of an HTML element in the DOM
         for which we want a separate action; the last key denotes the default action
      */
      const ACTIONS = {
        gen: {action: 'more-info', entity: 'sensor.pv_erzeugung_w'},
        grid: {action: 'more-info', entity: 'sensor.pv_bezug_lieferung_w'},
        'default action': {action: 'navigate', navigation_path: '/dashboard-neu/pv'},
      };
      const KEYS = Object.keys(ACTIONS);
      
      let elements = this.shadowRoot?.querySelectorAll(':hover') ?? [];
      let filtered = Array.from(elements)
        .filter(el => KEYS.includes(el.id))
        .map(el => el.id);
      let actionKey = filtered.length > 0 ? filtered[0] : 'default action';
      helpers.runAction(ACTIONS[actionKey]);
    ]]]

This is the only change to the original card code, so @hartmutw you may check this variant.

2 Likes

I inserted it and removed the other parts. It works perfectly and is much easier than using the handlers.

1 Like

I have rewritten my entire dashboard to use button-cards, I have a piece of code that looks like it ought to work but does not. This is the beginning of a template:

menu:
    variables:
      room_entity: null
    triggers_update: '[[[ return variables.room_entity ]]]'
    tap_action:
      action: multi-actions
      actions:
        - action: call-service
          service: script.togr
          description: change to togx for KFM, Tablet; change input_boolean.r to .x
          service_data:
            room: '[[[ return variables.room_entity ]]]'
        - action: navigate
          navigation_path: |
            [[[ 
              if (states[variables.room_entity].state == 'on') return "/dashboard-RPM/?anchor=menu";
              else return "/dashboard-RPM/?anchor=top"; 
            ]]]
          description: Change for KFM, Tablet
    styles:
      icon:
        - color: |
            [[[ 
              if (states[variables.room_entity].state == 'on') return "#FF00FF";
              else return "var(--primary-text-color)"; 
            ]]]

This should navigate to /dashboard-RPM/?anchor=menu when room_entity is on, and to /dashboard-RPM/?anchor=top when room_entity is off. I believe the code to be correct because the icon is controlled by the same javascript and its color varies depending on the state of room_entity correctly. The links are correct because if I replace the action with

        - action: navigate
          navigation_path: "/dashboard-RPM/?anchor=menu"

it does in fact navigate to the anchored position. I’m really scratching my head over this.

I have tested navigation per enity being on or not and it works fine.

Is it that the navigation is not correct, or the action on the dashboard being navigated to that is not correct. If the browser address bar showing correct URL? As your URL base is the same but search string is different, I am assuming you have something like bubble-card on dashboard-RPM, so maybe there lies the issue.