Dynamic Favorites Bar

I’m putting this particular widget up not because I think that there will be too many people with a use case for it, but because it contains many smaller parts and workarounds that others might find a use for in their own work.

The widget itself allows for the dynamic addition and removal of other simple, single item widgets. This happens on a live page, not in the UI page editor. This gives other OH users who do not have an administrator account some control over the items that are directly accessible on a page.

For this example, the dynamic widgets are restricted to switches, dimmers, and rollershutters, but this could be expanded easily enough.

FAVsm

The components

The basic underlying concept here is all items that have the custom tag FAV will be rendered by a repeater with an appropriate basic oh-card. The full integration, therefore, requires the widget code itself, two proxy items, and two rules that add or remove the FAV tag. There is also an optional metadata namespace for custom labels.

Widget Code
uid: widget_favorite_grid
tags: []
component: f7-block
config:
  style:
    display: flex
    flex-wrap: wrap
slots:
  default:
    - component: oh-repeater
      config:
        key: =Math.random() + items['Rule_RemoveFav'].state + items['Rule_AddFav'].state
        for: favItem
        sourceType: itemsWithTags
        itemTags: FAV
        fragment: true
        fetchMetadata: FavLabel
      slots:
        default:
          - component: f7-block
            config:
              class:
                - no-padding
                - no-margin
              style:
                display: flex
            slots:
              default:
                - component: oh-toggle-card
                  config:
                    item: =loop.favItem.name
                    title: "=loop.favItem.metadata ? loop.favItem.metadata.FavLabel.value : loop.favItem.label"
                    visible: =loop.favItem.type=='Switch'
                - component: oh-slider-card
                  config:
                    item: =loop.favItem.name
                    title: "=loop.favItem.metadata ? loop.favItem.metadata.FavLabel.value : loop.favItem.label"
                    scale: true
                    scaleSteps: 2
                    visible: =loop.favItem.type=='Dimmer'
                - component: oh-rollershutter-card
                  config:
                    item: =loop.favItem.name
                    title: "=loop.favItem.metadata ? loop.favItem.metadata.FavLabel.value : loop.favItem.label"
                    visible: =loop.favItem.type=='Rollershutter'
                - component: oh-link
                  config:
                    iconF7: multiply_circle_fill
                    actionItem: Rule_RemoveFav
                    action: command
                    actionCommand: =loop.favItem.name
                    style:
                      position: absolute
                      right: 1px
    - component: f7-block
      config:
        style:
          min-width: 75px
          max-height: 200px
      slots:
        default:
          - component: f7-fab-backdrop
            config:
              id: fabBack
              class:
                - fab-backdrop
          - component: f7-fab
            config:
              position: center-center
              color: blue
              morphTo: .card.add-fav
            slots:
              link:
                - component: f7-icon
                  config:
                    f7: plus
                    size: 24
          - component: f7-card
            config:
              title: Add Fav Card
              class:
                - add-fav
                - fab-morph-target
              style:
                z-index: 1501
                max-height: 150px
                overflow-y: scroll
            slots:
              default:
                - component: oh-list
                  config:
                    virtualList: true
                  slots:
                    default:
                      - component: oh-repeater
                        config:
                          for: ohItem
                          sourceType: itemsWithTags
                          itemTags: ","
                          fragment: true
                          filter: '(loop.ohItem.type=="Switch" || loop.ohItem.type=="Dimmer" || loop.ohItem.type=="Rollershutter") ? true : false'
                        slots:
                          default:
                            - component: oh-list-item
                              config:
                                title: =loop.ohItem.label
                                after: =loop.ohItem.name
                                action: command
                                actionItem: Rule_AddFav
                                actionCommand: =loop.ohItem.name

The structure of the widget itself is reasonably short and sweet. It’s actually a little artificially long because for each FAV item it must have the lines for all of the card types even though only the appropriate one is ever visible.

The two proxy items are just basic, no-frills string items. One to temporarily hold the name of any item to be added to the list and one for removal.

Rule code
triggers:
  - id: "1"
    configuration:
      itemName: Rule_AddFav
      previousState: None
    type: core.ItemStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      script: >-
        var itemName = items['Rule_AddFav'];

        var favItem = ir.getItem(itemName);

        favItem.addTag("FAV");

        ir.update(favItem);

        events.postUpdate('Rule_AddFav','None');
    type: script.ScriptAction
The two rules are identical except that one triggers when the widget add item receives an item name string (and therefore adds the `FAV` tag via `addTag()`) while the other triggers the widget remove item (and makes a `removeTag()` call).

The full flow has the two string items in a default state of 'None'. If the item list in the widget is opened and any of the list items are clicked on then the Rule_AddFav item receives the name of the item that was selected from the list. The change from 'None' triggers the add rule and the FAV tag is added to the item that was selected and Rule_AddFav is reset to 'None'. The complimentary flow is true for removal when one of the red close buttons on a visible item is clicked.

These two rules could easily have been combined into a single one. For that matter, with only slightly more complexity the two proxy items could be pared down to a single item that contains the action (add or remove) and the item name which is then parsed in a single rule. For the simplicity of initial development, however, I have left these separate.

Several of my item have labels that are appropriate in other aspects of the UI but not appropriate here in these stand-alone small widgets. You may have caught in the animation above that the garage door item’s label in the item list was ‘open/close’ but it appeared on the added widget as Garage Door. The main widget looks in a custom namespace, FavLabel, as it collects each of the tagged items. Items that have a value for this namespace, e.g.:

value: Garage Door
config: {}

Will use that value instead of the item’s default label.

The Tricks

The FAB Button

The f7 Floating Action Button (FAB) presents some difficulties. As an element intended to be “floating” above all the others its given a default z-index of 1500. This becomes a problem when you want to use the morph-to property of the button instead of just the other popup links that FABs are often used for. If the component that the FAB morphs into is not given a higher z-index than 1500 then the FAB layer stays on top. The morphed element is then visible but cannot be interacted with because it is below the FAB layer and any click in that element registers as a closing click returning the FAB to its button state. The opposite is true if the morph-to target element is given a z-index higher than 1500. The element itself can be interacted with, but the FAB is completely covered and there is no way to click-to-close. To get both functions the morph-to target must have the higher z-index but you almost must include the FAB-background element:

- component: f7-fab-backdrop
  config:
    id: fabBack
    class:
      - fab-backdrop

which makes the margin around the target card a viable area for click-to-close on the FAB.

Auto Refresh

Edit: The following is an improved solution to the auto refresh issue (credit to Yannick)

Vue’s renderer minimizes changes to elements and reuses those it deems static. There are certain changes to the items involved that do not trigger a recognized change. Often then to have the widget visuals catch up with the current system state it is necessary to refresh the page (or redraw in the widget editor window). Changing the tag on an item falls into this category.

To get the dynamic changes in the oh-repeater widget, the vue property key can be added to the repeater’s config:

- component: oh-repeater
  config:
    key: =Math.random() + items['Rule_RemoveFav'].state + items['Rule_AddFav'].state

This key property is a special flag for the renderer to take a more careful look at component and render the changes. This will force changes in even in the repeater element when the tags of associated items change. The vue docs say

Children of the same common parent must have unique keys . Duplicate keys will cause render errors.

Because it is possible to have multiple instances of these widgets on a page it is best to be safe and use the additional Math.random() which will make each widget instance’s keys different and avoid the possibility of “render errors”.

Original solution I've seen several questions related to this, but I haven't seen anywhere this simple work-around. Particularly as widgets get more complex, there are certain changes to the items involved that do not trigger a refresh of the widget and the only way to have the widget visuals catch up with the current system state is to refresh the page (or redraw in the widget editor window). Changing the tag on an item falls into this category. As far as the repeater and its child widgets are concerned none of its relevant items change when a tag is added or removed. To get the dynamic changes, a component refresh has to be triggered further upstream. That's where ``` forcedRefreshRemove: =items['Rule_RemoveFav'].state forcedRefreshAdd: =items['Rule_AddFav'].state ``` on the base `f7-block` come in. These are not special, undocumented keys or f7 properties, they are completely made-up. In fact, what they are called is totally irrelevant. The point is that the YAML parser, instead of throwing an error when it encounters a key it doesn't recognize, just ignores it and keeps on going. However, by setting these imaginary keys to expressions that include the widget's proxy items each time those items change state the base f7-block is refreshed along with all of its child widgets including the repeater.

I suspect this might be closer to taking advantage of a bug instead of a legitimate feature. I hope it’s not a bug that’s patched up anytime soon.

The Item List

Similar to the auto refresh this is related to a question that has come up several times, “how do I access the item picker dialog in a widget?” Of course, the answer is you can’t, but it is possible to get an oh-repeater to return a list of every item in the registry. Both the oh-repeaters in this widget use the itemsWithTags source. The first, obviously, just looks for the FAV tag:

sourceType: itemsWithTags
itemTags: FAV

However, the itemTags key also allows you to specify a comma separated list of tags. The repeater that populates the item list takes advantage of this and requests all items that have some empty tag (which is all items) with just a comma:

sourceType: itemsWithTags
itemTags: ,

This list of all the items then takes advantage of the of the repeater’s filter config to return only those items that match one of the supported types so that we don’t have to scroll through extraneous group items etc.

In most OH setups this tends to be a very long list. I have, therefore, used the virtual list in that list element, but I can’t actually say if that’s necessary.

The same comment as with the auto refresh applies. I’m sure this is not a use Yannick intended but it is handy…

8 Likes

I truly like your workaround! That’s an amazing idea! :+1: :+1:

Clever combinations of techniques, thanks for sharing!

You might want to check the key property in Vue (Special Attributes | Vue.js) which is the standard attribute to set when you want to make sure things are properly updated and nodes in the VDOM aren’t reused - just make sure it’s unique, like key: =Math.random() + items['Rule_RemoveFav'].state + items['Rule_AddFav'].state

Also you shouldn’t use the REST API in a rule, especially if it involves credentials, you have access to the ItemRegistry (ir) and can update it so your code should simply be:

var item = ir.getItem(state);
// see https://www.openhab.org/javadoc/latest/org/openhab/core/items/genericitem
item.addTag("FAV"); // resp.: item.removeTag("FAV");
ir.update(item);
events.sendCommand('Rule_AddFav','None');
1 Like

Oh cool, I’ll look into that.

Edit: Thanks, that’s a much more elegant solution. Widget updated accordingly.

D’oh! Of course. The rules weren’t the primary concern so I just whipped up the first things that came to mind and totally forgot the obvious… :laughing: all fixed now. (Gonna be a while before I let myself live that one down).

Very impressive work! Thanks for posting!

A couple questions/comments about the rule, since that’s my bailiwick.

  • Is there a reason you didn’t use the sendHttpXRequest openHAB Actions?

  • The Item object has an addTag and removeTag method. You probably don’t need to mess with the REST API at all for this which would drop the rule to a single line of code.

ir.getItem('Rule_AddFav').addTag('FAV');
  • You change the code as I was typing. :smiley:

  • It’s usually more straight forward to use the dict of Item states than to use the Item registry to pull the Item just to get it’s state. Instead of ir.getItem('Rule_AddFav').getState() you can use items['Rule_AddFav'].

  • You can add a condition to the rule to not run when the Item changed to ‘None’ so the rule doesn’t run unnecessarily. That can be an alternative to your trigger. Lots of different ways to do things. The condition is more useful when the test is too complicated to include in the trigger like you have.

  • It’s a pretty minor thing and doesn’t really mean much, but typically you would want to update the Item back to the default rather than command it to ‘None’. In fact, this rule doesn’t even work with commands at all so that’s why it doesn’t really matter. But in some cases the distinction can matter a great deal.

Don’t get me wrong, there is nothing wrong with the rule as written. These are just some things to consider to take them to the next level.

Great post! I suspect it will be referenced for a long time to come.

I usually prefer ir methods to the items dict because, for some reason I cannot explain, in my scritps I find the method stack more readable. That probably makes me the exception and not the rule, so you’re right I should update this shared code accordingly.

The changed from trigger (previousState: None) already takes this into account, but I can certainly see more advanced implementations that would require more complex logic in a rule condition.

That’s a great point. I’ll fix that now.