Need help figuring out how to make"zoomable" widgets

I’m trying (hard) to migrate to MainUI (currently using a combo of HABPanel and Sitemap) and figure out how I can make it serve my needs.

I’ve created some widgets using ECharts as a part of this. While I’m reasonably happy with the widgets themselves, they scale terribly. Basically, it comes down to the fact that ECharts mix relative and absolute units when you define stuff, and I can’t find a way around that - which means that they will only ever “work” when at or close to the “design size”. It’s really depressing, because they could otherwise have been great.

Anyway, they are too big to effectively give any overview, at the same time they need to be big to show all the details. So, the only “solution” I can find is to make a simplified, smaller version of them with the most basic information, and then make them expand when clicked to show the full detail. I would need them to expand to somewhere near “design size”, not full screen - which will again scale them wrong.

I’ve been trying to look at oh-cell to try to find a way, but so far with no luck. I’m also hesitant to use a cell since it has limits to where it can be used - it feels like using a card would be more flexible. But regardless of cell/card, the whole F7 expandable functionality seems to require learning a lot more about F7 and their use of CSS classes. I’m hesitant to spend that time, since I don’t really need that knowledge for anything else in my life, so I’m pretty sure I will forget it quickly - and I’m not even sure it will do what I want to.

What is the best approach - or maybe, what is a workable approach to do this? I want only the “simplified” widget to be displayed when it’s not expanded, and only the “full” widget to be displayed when expanded. At the same time, I want to somehow make it all in one “widget” so that they use the same configuration/props, and can be used as “one package”.

There’s no simple option here. This is a pretty complex UI operation, no matter what UI you are using.

I have a very similar situation with my Nest thermostat widget:
nest

In my opinion the best solution for this is to have the full widget in a popup. My thermostat widget uses an oh-cell as a base that shows the basic summary:
image

and clicking on that cell will open the popup for the thermostat.

Using the built-in oh popup action for this would require two widgets, but by using the f7-popup component, I have more control of the popup (e.g., size) and it is all contained in one custom widget. The basic structure is this:

oh-context
  f7-block
    oh-label-cell
      extra components for the cell
    f7-popup
      full nest widget

The cell is a perfectly good option here, as I show above. Again, the cell does not have restrictions on where it can be used. It’s just that the layout page has a specific grid component optimized for cell arrangement so the UI button and options sequence leads the user to preferentially use cells in that grid. But, that cell above for my thermostat widgets (and all the other cells that make up the bulk of my overview page), they are not in an oh-grid-cells component, they are in a regular block grid, I just had to manually put them there instead of using the editor.

Furthermore, if you use the cell as the root component of your custom widget. That will still just be a custom widget and will show up in the UI lists of custom widgets when adding it to a layout page it will not show up in the list of cells available in the cell grid.

I, personally, find the popups slightly easier to work with than the expandable system will the cards or cells, but that is very much a matter of personal opinion and there’s no reason not to use the expandable system here if you find it easier to understand (there are some cases, such as my weather widget where I do use the expandable system in a cell).

On the other hand, you are right that the card as the base of the expandable system does give you a little more flexibility than the cell (especially in terms of size of the initial element).

1 Like

Thanks a lot - is sounds like the situation is so close to your that I probably can learn a lot by studying yours. And, I guess I will “learn to work around” the “cell” limitations, it’s just so inconvenient that you can’t even add “cells” most places where I’ve tried. Alternatively, I’ll see if I’ll manage to “transfer” what I learn to a card, if I’m lucky. In any case, I think you have helped me 90% along the way :+1:

Your thermostat widget is very nice BTW :+1:

@JustinG I was hoping to study your Nest thermostat widget, but I can’t find it. Am I just bad at searching, or isn’t it shared?

I have not ever shared it. Like your widgets, it is many hundreds of lines long, and there are some parts I never finished polishing up enough for general release.

1 Like

That nest widget looks awesome!

2 Likes

This is just preliminary testing, as you can see, but it looks promising. However, I’m not sure how to “know” when the popup has been closed. I assume you, @JustinG, have some key trick to handle that, but I haven’t been able to figure anything out yet:

uid: test_expanding_widget_7c280570a1
tags: []
props:
  parameters:
    - description: A text prop
      label: Prop 1
      name: prop1
      required: false
      type: TEXT
    - context: item
      description: An item to control
      label: Item
      name: item
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Jan 4, 2025, 2:25:39 AM
component: oh-context
config:
  variables: 
    popped: false
slots:
  default:
    - component: f7-block
      config: {}
      slots:
        default:
          - component: oh-label-card
            config:
              icon: oh:temperature
              title: Title
              subtitle: Subtitle
              label: Label
              action: variable
              actionVariable: popped
              actionVariableValue: true
          - component: f7-popup
            config:
              opened: =vars.popped
              tabletFullscreen: false
              closeByBackdropClick: true
              closeOnEscape: true
              swipeToClose: true
              style:
                width: 300px
                height: 300px
            slots:
              default:
                - component: div
                  config:
                    style:
                      width: 300px
                      height: 300px
                  slots:
                    default:
                      - component: "widget:wind_gauge"

What happens is that since my variable popped is never reset, trying to open it a second time doesn’t work - presumably because the “action logic” figures that the variable already has this value and does nothing - or that the expression evaluation makes the same conclusion. Regardless, I need to set it back to false when the popup is closed in some way. At the same time, I would like to keep the built-in close options using the Escape key, clicking outside the widget etc., so putting “an invisible button” somewhere that could clear the variable doesn’t seem like a good solution.

Alternatively, using some other way than a variable to open would probably solve it, but I’m not sure how.

Yes, this is the one tricky part to using the f7 based popups. You can either do all the opening and closing control via the built-in methods or you can do it manually using oh variables. There really is no good way to do them both cleanly because the OH widget expression context (and therefore the oh variables) have no access to the f7 events so they cannot react when the popup-close event is triggered.

In cases like this, if I don’t feel like going through the hassle of getting a full variable-based open/close system working I’ll just use a quick cheat. Instead of hard-coding the variable value:

              actionVariable: popped
              actionVariableValue: true

I’ll just use

              actionVariable: popped
              actionVariableValue: =!vars.popped

This means that in case the the popup is closed but the variable is still true, I’ll just wind up clicking on the cell twice (first click resets the variable to false, second click properly opens the popup).

The close button that you see on the popup in the video however, does just set the variable to false because that one will never be out of sync with the popup status. And if you use that to close the popup then it will only require the expected 1 click to reopen it.

Since these widgets are none-interactive, I’d prefer that they were easy to close without clicking on something specific - which makes it harder to reset the variable.

Is there no way to do some trickery and latch on to the popup:closed event? That would really be the ideal solution, but I don’t know nearly enough to know how to “abuse” the JS system.

Another alternative is if there was a way to “trick” the action event to always set the variable first to false and then to true (instead of toggling). I don’t see how that would be possible though, since whatever acrobatics one does there, I assume that the expression will always be evaluated first and then assigned to the variable only once.

This is an interesting thought that I haven’t explored before. It’s possible that you could use something like what I describe here:

to set the variable to two different values in rapid succession.

I have not tried it, so I don’t know if there is some additional hurdle or not, however, theoretically, you could create an oh-button with the action to set the popup variable to false. Give that button an id and set it’s display to none. Then you could use some of the basic javascript access tricks here add popup event listeners that trigger the click method of that hidden button.

I think I’m stuck. I’ve thought through numerous different JavaScript approaches, but they all crash and burn at the boundary between “widget context” and “JavaScript context”. Basically, I want to find a “generic” way to do this that can be used on different widgets, and that can be used by multiple widgets on the same page. So, I can’t use hardcoded IDs or classes.

I’ve tried to find a “way through” by generating a random variable and using the script tag to get this “identifier value” over to the JavaScript context, but I can’t find a way that works. If I were to create a JS function in the script tag and call that from onclick, I’d still basically have the same problem - multiple widgets would make multiple JS functions with the same name. onclick isn’t processed by the expression evaluator, so I can’t work the “identifier value” in there.

Likewise, if I were to make an event-listener and could figure out what element to listen to, it still wouldn’t help, because I have no way to reset the widget variable from JS.

The last thing I’ve tried is to play with popupOpen since it can take expressions. It doesn’t help however, because it only works on buttons and links - neither of which have slots (as far as I can tell) that can contain child elements. The idea is that you can click anywhere on the “mini-widget” to open the full widget - so the whole (mini) widget would need to be clickable and support popupOpen - which seems like a dead end.

What about having an oh-link component of the same size as your mini widget and use the popup as action….

Buttons and links both have a default slot which can be used for all child components.

uid: def_slots
props:
  parameterGroups: []
  parameters: []
tags: []
component: f7-card
config: {}
slots:
  default:
    - component: oh-button
      slots:
        default:
          - component: Label
            config:
              text: Button Default Slot
    - component: oh-link
      slots:
        default:
          - component: Label
            config:
              text: Link Default Slot

image
The multi widget action post I linked to above relies on these default slots.

onclick is processed by the expression parser. This is actually one of the few places where I use one of these js cross-overs on a daily basis. I want haptic feedback on a important button so that when I use it on my phone I know it has been pressed. To do that I use:

onclick: =(!device.desktop)?'window.navigator.vibrate(50);':''

on that button. The trick is that the expression is parsed before anything is sent further up the chain, so you have to make sure that your expression result is a fully qualified js function (or some falsy value). In my case the result of the expression is either '' (I don’t want a click listener) or window.navigator.vibrate(50); (listener should call the vibrate function).

You can use this to inject widget values into js functions by building the full function string with the widget expression. I find that using a string template often makes it easiest because it looks the most like writing the actual js:

onclick: =`myCustomJsFunction(${vars.myWidgetVariable});`

You can find other examples of how to do this in the 3D model tutorial post.

Here’s another option that I haven’t tried before, but should work at least in simple scenarios:

component: f7-card
config: {}
slots:
  default:
    - component: oh-button
      config:
        text: =`Popup Iteration - ${vars.popIter || 0}`
        action: variable
        actionVariable: popIter
        actionVariableValue: =(vars.popIter || 0) + 1
    - component: f7-popup
      config:
        key: =Math.random() + vars.popIter.toString()
        opened: =!!vars.popIter

This will reset the popup to an opened state everytime the button is pushed. The popup will not be opened when the widget first laods because the variable will not exist yet.

Ok, this is what I tried to do, but I guess I was a bit too fed up and didn’t check thoroughly enough. All I know is that when I try to put some “more complex” components in those slots, they don’t show up like they should. Maybe that is just due to their default styling, so that I must clear all the CSS stuff to make them behave? A link that filled the whole parent, was invisible and picked up all clicks (that were’n caught by children at least) and didn’t interfere with the rendering of the children, would be a good solution. Maybe I must tinker some more with this.

I looked at that, but here my lack of familiarity with JS, DOM etc. bites me - because when my attempt failed, I concluded that device was some DOM or browser object, so that it was all JS (I see that it doesn’t make sense quote-wise, but I’ve “given up” YAML and quotes so I just “accept” that it doesn’t make sense). Whatever I did, the browser complained that vars was undefined, so it was clear that it wasn’t substituted - and I concluded that was because it wasn’t evaluated. But, it turns out it probably was my faulty syntax instead :confused:

It that works (haven’t tested yet), it might be the best way. Why do you say “at least in simple scenarios”? Are there scenarios where you think this will fail?

The thing is that I would obviously like to avoid JS if I can, especially since using random() to make “unique IDs” is fundamentally flawed. Sure, you will probably never get two identical IDs on the same page, but you have no guarantee, so it feels more like a hack than a solution.

Maybe you were thinking of the fact that the variable is global? Won’t this work equally well is used with oh-context and just initialized to 0? That way, the variable won’t be global…?

Yes, that was one potential issue and the oh-context is the way to fix it.

I also can’t guarantee the behavior in situations where the widget is in some context that is not fully unloaded between times when it is rendered. For example, if this were included on a tabbed page that was navigated away and then back. I suspect the variable value is kept and but the widget is refreshed so that the popup would appear whenever you navigated back to the tab.

1 Like