Loosing an array element on every toggle

Hi,
experimenting with my first custom widget set I struggle on a behaviour, I can’t understand right know. Through a combination of widgets I pass a JSON object as configuration for a template I would like to reuse on a few other widgets. Among other things this config contains a description to show different icons on states of a given items.
At first this seems to work like a charm, but every time I toggle the state of one of the three items in this configuration, the list seems to be reduce by one entry and thereby the icons are reduced too.

First “ON” command:
grafik

Second “ON” command:
grafik

Third “ON” command:
grafik

Maybe someone could see something I miss at the moment? Below is my used code and configuration.

uid: card_template
tags: []
props:
  parameters:
    - label: Header
      name: cardLabel
      required: false
      type: TEXT
      groupName: cardConfig
    - description: Top-left corner icon
      label: Icon
      name: cardIcon
      required: true
      type: TEXT
      groupName: cardConfig
    - context: page
      label: Page to show as a popup
      name: cardPopupPage
      required: false
      type: TEXT
      groupName: cardConfig
    - context: item
      description: Humidity item
      label: Humidity item
      name: actionItem
      required: false
      type: TEXT
    - label: States
      name: stateData
      required: false
      type: TEXT
      groupName: statusConfig
    - context: widget
      label: Test
      name: contentWidget
      required: false
      type: TEXT
      groupName: cwidget
    - label: Content widget data
      name: contentData
      required: false
      type: TEXT
      groupName: cwidget
    - label: Accent color
      name: acolor
      required: true
      type: TEXT
  parameterGroups:
    - name: cardConfig
      description: Card configuration
    - name: cardAction
      context: action
      label: Action
      description: Choose your recomended action
    - name: statusConfig
      description: Status section configuration
    - name: cwidget
      description: Content Widget
timestamp: Aug 18, 2024, 8:58:48 AM
component: oh-link
config:
  style:
    --hf-accent-color: =(props.acolor || "var(--f7-theme-color)")
    margin: 10px
    border-radius: 20px
    background-color: white
    border: 6px solid var(--hf-accent-color)
    box-shadow: rgba(50, 50, 93, 0.25) 0px 10px 16px -4px, rgba(0, 0, 0, 0.3) 0px 7px 11px -7px, inset rgba(50, 50, 93, 0.25) 0px 6px 12px -2px, inset rgba(0, 0, 0, 0.3) 0px 3px 7px -3px
    margin-top: 10px
    display: grid
    grid-template: >
      "icon header status" "values values status"
    grid-template-columns: 70px calc(100% - 144px) 34px
    grid-template-rows: 70px calc(100% - 70px)
    column-gap: 20px
    white-space: nowrap
    opacity: 1 !important
  action: popup
  actionModal: =props.cardPopupPage
slots:
  default:
    - component: div
      config:
        style:
          width: 70px
          height: 70px
          background-color: var(--hf-accent-color)
          border: 4px
          border-bottom-right-radius: 20px
          border-top-left-radius: 13px
          grid-area: icon
      slots:
        default:
          - component: div
            config:
              style:
                position: absolute
                background-color: transparent
                width: 20px
                height: 20px
                margin-top: 0px
                margin-left: 70px
                border-top-left-radius: 16px
                box-shadow: -5px -5px 0px 0px var(--hf-accent-color)
          - component: div
            config:
              style:
                position: absolute
                background-color: transparent
                width: 20px
                height: 20px
                margin-top: 70px
                margin-left: 0px
                border-top-left-radius: 16px
                box-shadow: -5px -5px 0px 0px var(--hf-accent-color)
          - component: oh-button
            config:
              style:
                width: 52px
                height: 52px
                margin: 6px
                background-color: white
                border-radius: 16px
                color: var(--hf-accent-color)
                display: grid
                align-items: center
                justify-content: center
                opacity: 1 !important
              disabled: =(undefined === props.card_actionItem)
              actionPropsParameterGroup: cardAction
            slots:
              default:
                - component: oh-icon
                  config:
                    icon: =props.cardIcon || ''
                    style:
                      height: 34px
                      width: 34px
    - component: div
      config:
        style:
          grid-area: status
          background-color: var(--hf-accent-color)
          position: relative
          color: white
          height: 100%
      slots:
        default:
          - component: div
            config:
              style:
                margin-top: 16px
                margin-bottom: 16px
                margin-left: 6px
                display: flex
                flex-direction: column
                justify-content: space-evenly
                gap: 16px
            slots:
              default:
                - component: oh-repeater
                  config:
                    for: listitem
                    fragment: true
                    sourceType: array
                    in: =JSON.parse(props.stateData)
                  slots:
                    default:
                      - component: oh-icon
                        config:
                          icon: =loop.listitem.states.find((e) => e.state === items[loop.listitem.item].state).icon
                          style:
                            height: 28px
                            width: 100%
                          visible: =((0 < loop.listitem.states.length) && (items[loop.listitem.item].state) && (undefined !== loop.listitem.states.find((e) => e.state === items[loop.listitem.item].state)))
                      - component: div
                        config:
                          style:
                            height: 28px
                            width: 100%
                          visible: =((0 == loop.listitem.states.length) || (!items[loop.listitem.item].state) || (undefined === loop.listitem.states.find((e) => e.state === items[loop.listitem.item].state)))
          - component: div
            config:
              style:
                position: absolute
                background-color: transparent
                width: 20px
                height: 20px
                top: 0px
                left: -20px
                border-top-right-radius: 16px
                box-shadow: 5px -5px 0px 0px var(--hf-accent-color)
          - component: div
            config:
              style:
                position: absolute
                background-color: transparent
                width: 20px
                height: 20px
                bottom: 0px
                left: -20px
                border-bottom-right-radius: 15px
                box-shadow: 5px 5px 0px 0px var(--hf-accent-color)
    - component: Label
      config:
        style:
          grid-area: header
          width: 100%
          color: var(--hf-accent-color)
          font-weight: bold
          font-size: 24px
          text-align: left
          vertical-align: middle
          line-height: 70px
        text: =props.cardLabel || ""
    - component: =props.contentWidget
      config:
        style:
          grid-area: values
          margin-bottom: 20px
        contentData: =props.contentData

uid: card_room
tags: []
props:
  parameters:
    - description: A text prop
      label: Prop 1
      name: cardLabel
      required: false
      type: TEXT
    - description: A text prop
      label: Prop 1
      name: cardIcon
      required: true
      type: TEXT
    - context: page
      label: Page to show as a popup
      name: cardPopupPage
      required: false
      type: TEXT
    - context: item
      description: Toggle item
      label: Item to toggle on icon click
      name: actionItem
      required: false
      type: TEXT
    - context: item
      description: Temprature item
      label: Temperature item
      name: tempItem
      required: false
      type: TEXT
    - context: item
      description: Brightness item
      label: Brightness item
      name: brightItem
      required: false
      type: TEXT
    - context: item
      description: Humidity item
      label: Humidity item
      name: humItem
      required: false
      type: TEXT
    - context: item
      description: Light status item
      label: Light status item
      name: lightItem
      required: false
      type: TEXT
    - context: item
      description: Security status item
      label: Security status item
      name: securityItem
      required: false
      type: TEXT
    - context: item
      description: Occupancy status item
      label: Occupancy status item
      name: occupancyItem
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Aug 18, 2024, 9:07:29 AM
component: widget:card_template
config:
  cardLabel: =props.cardLabel
  cardIcon: =props.cardIcon
  cardPopupPage: =props.cardPopupPage
  card_action: toggle
  card_actionCommand: ON
  card_actionCommandAlt: OFF
  card_actionItem: =props.actionItem
  contentWidget: widget:hf_value_row
  contentData: ='[{"label":"Temperatur","item":"'+props.tempItem+'"},{"label":"Helligkeit","item":"'+props.brightItem+'"},{"label":"Luftfeuchtigkeit","item":"'+props.humItem+'"}]'
  stateData: ='[{"item":"'+props.lightItem+'","states":[{"state":"ON", "icon":"iconify:mdi:lightbulb-on"}]},{"item":"'+props.securityItem+'", "states":[{"state":"ON", "icon":"iconify:mdi:shield-unlocked"}]},{"item":"'+props.occupancyItem+'", "states":[{"state":"ON", "icon":"iconify:mdi:person-circle"}]}]'
uid: hf_value_row
tags: []
props:
  parameters:
    - label: Values
      name: contentData
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Aug 15, 2024, 1:58:48 PM
component: div
config:
  style:
    grid-area: values
    display: flex
    flex-direction: row
    margin: 10px 0px 16px 16px
    gap: 10px
    justify-content: space-evenly
    flex-wrap: wrap
slots:
  default:
    - component: oh-repeater
      config:
        for: listitem
        sourceType: array
        fragment: true
        in: =JSON.parse(props.contentData)
      slots:
        default:
          - component: div
            config:
              style:
                color: props.textColor || var(--hf-accent-color)
                display: flex
                flex-direction: column
                height: 46px
            slots:
              default:
                - component: Label
                  config:
                    text: =loop.listitem.label
                    style:
                      font-weight: bold
                      text-align: center
                      font-size: 14px
                - component: Label
                  config:
                    style:
                      padding-top: 4px
                      text-align: center
                      font-size: 14px
                    text: "=('-' === @loop.listitem.item ? '' : @loop.listitem.item)"

Configuration used for testing:

          - component: widget:card_room
            config:
              cardIcon: iconify:mdi:face-male
              cardLabel: Zimmer Markus
              lightItem: DeckenlampeMarkus
              securityItem: DeckenlampeMarkus
              occupancyItem: DeckenlampeMarkus
              cardPopupPage: page:pageMarkus
              card_action: toggle
              card_actionItem: DeckenlampeMarkus
              card_actionCommand: ON
              card_actionCommandAlt: OFF
              actionItem: DeckenlampeMarkus

I think there might be constellations where both visible conditions are met and the div component either covers the icon above or covers the next icon in the repeater list.
Haven‘t tried it. Just what comes into my mind by looking at the code.

Thanks for your input. I agree the visible conditions need a little additional love :wink:

But I think I saw in the inspector that no child element was displayed at all. I have to look again.

This is almost certainly another variant of a bug that has been around for a long time. I reported it several years ago:

and since then, no one has been able to figure out what the actual bug even is (it’s quite possible that’s it’s actually upstream in the vue library). Fortunately, it’s rare enough that it’s been ignorable all along. You’re the first other person I’ve seen to come across it.

The good news is, however, that there are some options to mitigate it that I have found over the years.

The easiest one is just to remove the fragment property from the repeater, if you can get away with that. It means there’s an extra div element wrapping each of your icons, but I don’t think that will cause any significant issues in this case.

The other solution is just to make sure that the element that is being toggled is not the last element in the repeaters array of children. Just throw in a completely empty div element at the end of the repeaters default slot. Now you already have a div element after the icon, which I guess you’re using to space the icons out, but it’s not clear given that you are putting these icons in a flexbox div where you can already increase the spacing between them by just changing the gap. But that div does not circumvent the issue because you are also toggling the visibility of that element. You need to add something to that repeater that get drawn every time but takes up no actual space in the widget. Something like this:

- component: div
  config:
    comment: Dummy element to bypass repeater bug

The most complex solution would be to use the repeaters filter property to reduce the final loop array to only the relevant elements before anything is ever drawn. Then you don’t even need the visible properties.

2 Likes

As I’m not a HTML and CSS guy, the empty divs are my attempt to always keep the same icons in the same place in order to have the same look for all cards of the same type. Even if various icons are hidden or not used at all. I didn’t test it so far, but at least this is how it should work in the end.

If that is the use case then go for the following structure:

- component: oh-icon
  config:
    icon: =(condition)?name_of_icon:name_of_ blank_icon

In that case you have a solution that works around the error and simplifies your widget code a little. Instead of using the OH visible parameter, you want to use the direct css visibility parameter. When you set the OH visible parameter to false, the vue renderer ignores that component (and all it’s children) completely so it never even gets drawn and its position is not taken into account by the elements around it. However, when you use css to set an elements visibility property to hidden the object still exists on the page exactly as if it were there, it is just not drawn, and that is exactly what you want if you are looking to have an icon disappear without the other icons moving.

So, you don’t need the extra empty div in the repeater because your icons will always be drawn. You can keep the repeater fragment property which will then maintain all the styling you have currently set, and you just need to move your visible logic and change it from a basic boolean to a ternary statement so that it returns 'visible' or 'hidden':

                - component: oh-repeater
                  config:
                    for: listitem
                    fragment: true
                    sourceType: array
                    in: =JSON.parse(props.stateData)
                  slots:
                    default:
                      - component: oh-icon
                        config:
                          icon: =loop.listitem.states.find((e) => e.state === items[loop.listitem.item].state).icon
                          style:
                            height: 28px
                            width: 100%
                            visibility: =((0 < loop.listitem.states.length) && (items[loop.listitem.item].state) && (undefined !== loop.listitem.states.find((e) => e.state === items[loop.listitem.item].state)))?'visible':'hidden'

1 Like

Perfect, thanks a lot.

Unfortunately, switching from oh-visible to css-visibility doesn’t actually fix the problem.
I have now attached the empty extra div and that seems to work.

I have just tested the css version with a version of the widget you posted originally (modified slightly of course since I don’t have the underlying items or json), and in my testing it does bypass the repeater error.

All I changed:

component: oh-link
config:
  ...
  action: variable <-- changed the action of the root link component to modify a varaible
  actionVariable: ant
  actionVariableValue: =(vars.ant == 'hidden')?'visible':'hidden'
                - component: oh-repeater
                  config:
                    for: listitem
                    fragment: true
                    sourceType: array
                    in:                   <--- changed input array
                      - icon: ant
                      - icon: airplane
                      - icon: alarm
                  slots:
                    default:
                      - component: oh-icon
                        config:
                          icon: ='f7:'+loop.listitem.icon <--- changed icon definition to match input array
                          style:
                            height: 28px
                            width: 100%
                            visibility: =vars[loop.listitem.icon] || 'visible' <---- changed visibility to match input array

repeat-error2

It would be informitive to see what you tried that still resulted in the error, as that might help track it down.

I was a bit lazy and just copied your code snippet. I’m not at home at the moment, so I can’t have a closer look at it. But when I take a quick look at your answer, I notice three things:

  • For testing I connected all the icons to the same item. This is how I first became aware of the problem.
  • I have a JSON object containing some arrays passed through the hierarchy of widgets. This feels a bit “heavier” than your array.
  • I use iconify icons