Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add oh-context component #2533

Merged
merged 15 commits into from
Jun 1, 2024
Merged

Conversation

JustinGeorgi
Copy link
Contributor

@JustinGeorgi JustinGeorgi commented Apr 13, 2024

Closes #2437, Closes #2148

This PR adds a new component, the oh-context. Similar to the repeater, this component is not rendered, but injects information into the widget at it's tree location.

The component allows for that addition of three things:

  • functions - using the arrow function syntax, named functions can be declared and reused in all subsequent expressions
  • constants - constants can be defined as either single values, arrays, or objects
  • variables - variables can be defined with default values. These variables are local in scope to the oh-context and it's descendants and take precedence over other variables of the same name from higher contexts (including general component variable usage.
    • Variables are not divided into global vs local explicitly. But a oh-context used as the root component of a widget will have its variables in the context of all the other components on that widget and thus they essentially have a global context within that widget.
    • In contrast to the basic widget variables, oh-context variables do have bi-directional passage between a main widget and a sub widget.

Here is are widgets that I have been using to test all these different capabilities:

Main widget:

uid: oh_context_demo
tags: []
props: {}
timestamp: Apr 13, 2024, 10:44:43 AM
component: oh-context
config:
  constants:
    fruitColor:
      apple: red
      banana: =(dayjs().hour() < 6)?'green':((dayjs().hour() < 14)?'yellow':'black')
      orange: orange
      blueberry: blue
  functions:
    nextFruit: >
      =(fr) =>
        (fr)
        ? (['apple','banana','orange','blueberry'])[(['blueberry','apple','banana','orange']).indexOf(fr)]
        : ('apple')
    a2o: =(str) => (str || 'no fruit').replaceAll('a','o')
  variables:
    currentFruit: apple
slots:
  default:
    - component: f7-card
      config:
        content: =`Global fruit is ${fn.a2o(vars.currentFruit)}`
        textColor: =const.fruitColor[vars.currentFruit] || 'deeppurple'
        title: Fn, Const, and Scoped Variables
      slots:
        default:
          - component: oh-context
            config:
              functions:
                a2o: =(str) => (str || 'no fruit').replaceAll('a','0')
              variables:
                currentFruit: orange
            slots:
              default:
                - component: f7-row
                  config:
                    style:
                      justify-content: space-around
                  slots:
                    default:
                      - component: oh-button
                        config:
                          action: variable
                          actionVariable: currentFruit
                          actionVariableValue: =fn.nextFruit(vars.currentFruit)
                          text: =`Local fruit is ${fn.a2o(vars.currentFruit)}`
                      - component: oh-button
                        config:
                          text: =`Clear local fruit`
                          clearVariable: currentFruit
                - component: f7-row
                  config:
                    style:
                      justify-content: center
                  slots:
                    default:
                      - component: Label
                        config:
                          text: Input local fruit
                      - component: oh-input
                        config:
                          type: text
                          variable: currentFruit
                          placeholder: Please select a fruit
                          outline: true
                          style:
                            margin-left: 18px
                            color: var(--f7-text-color)
    - component: oh-input-card
      config:
        title: Input global fruit
        type: text
        variable: currentFruit
        placeholder: Please select a fruit
    - component: oh-context
      config:
        constants:
          fruitColor:
            apple: green
            banana: black
            orange: orange
            blueberry: pink
      slots:
        default:
          - component: f7-card
            config:
              content: =`Global fruit is ${fn.a2o(vars.currentFruit)}`
              textColor: =const.fruitColor[vars.currentFruit] || 'deeppurple'
              title: Fn, Const, and Scoped Variables
            slots:
              default:
                - component: f7-row
                  config:
                    style:
                      justify-content: space-around
                  slots:
                    default:
                      - component: f7-row
                        config:
                          style:
                            justify-content: space-around
                        slots:
                          default:
                            - component: oh-button
                              config:
                                action: variable
                                actionVariable: currentFruit
                                actionVariableValue: =fn.nextFruit(vars.currentFruit)
                                text: =`Global fruit is ${fn.a2o(vars.currentFruit)}`
                            - component: oh-button
                              config:
                                text: =`Clear global fruit`
                                clearVariable: currentFruit
                            - component: oh-button
                              config:
                                action: variable
                                actionVariable: widgetFruit
                                actionVariableValue: =fn.nextFruit(vars.widgetFruit || 'apple')
                                text: =`Global widget fruit is ${fn.a2o(vars.widgetFruit || 'apple')}`
    - component: widget:oh_context_widget
    - component: oh-list-card
      config:
        title: Variables
      slots:
        default:
          - component: oh-toggle-item
            config:
              title: Toggle
              variable: togVar
          - component: oh-slider-item
            config:
              title: Slider
              variable: slideVar
          - component: oh-stepper-item
            config:
              title: Stepper
              variable: stepVar
    - component: oh-context
      config:
        variables:
          togVar: true
          slideVar: 10
          stepVar: 10
      slots:
        default:
          - component: oh-list-card
            config:
              title: Local Variables
            slots:
              default:
                - component: oh-toggle-item
                  config:
                    title: Toggle
                    variable: togVar
                - component: oh-slider-item
                  config:
                    title: Slider
                    variable: slideVar
                - component: oh-stepper-item
                  config:
                    title: Stepper
                    variable: stepVar

image

Sub widget included in the main widget:

uid: oh_context_widget
tags: []
props:
  parameters: []
  parameterGroups: []
timestamp: Apr 13, 2024, 9:20:55 AM
component: f7-card
config:
  content: =`Global fruit is ${fn.a2o(vars.currentFruit)} but global widget fruit
    is ${fn.a2o(vars.widgetFruit || 'apple')}`
  textColor: =const.fruitColor[vars.widgetFruit || 'apple']
  title: Fn, Const, and Scoped Variables in Widgets
slots:
  default:
    - component: f7-row
      config:
        style:
          justify-content: space-around
      slots:
        default:
          - component: oh-button
            config:
              action: variable
              actionVariable: currentFruit
              actionVariableValue: =fn.nextFruit(vars.currentFruit)
              text: =`Global fruit is ${fn.a2o(vars.currentFruit)}`
          - component: oh-button
            config:
              action: variable
              actionVariable: widgetFruit
              actionVariableValue: =fn.nextFruit(vars.widgetFruit || 'apple')
              text: =`Global widget fruit is ${fn.a2o(vars.widgetFruit || 'apple')}`
          - component: oh-context
            config:
              variables:
                widgetFruit: blueberry
            slots:
              default:
                - component: oh-button
                  config:
                    action: variable
                    actionVariable: widgetFruit
                    actionVariableValue: =fn.nextFruit(vars.widgetFruit)
                    text: =`Local widget fruit is ${fn.a2o(vars.widgetFruit)}`

I've done some pretty extensive testing, but this has grown into a fairly large addition, so any and all additional testing is appreciated. There's no telling what I've missed.

@JustinGeorgi JustinGeorgi requested a review from a team as a code owner April 13, 2024 18:24
Copy link

relativeci bot commented Apr 13, 2024

#1995 Bundle Size — 10.63MiB (+0.05%).

8a91ec4(current) vs aa4e218 main#1991(baseline)

Warning

Bundle contains 2 duplicate packages – View duplicate packages

Bundle metrics  Change 3 changes Regression 1 regression
                 Current
#1995
     Baseline
#1991
Regression  Initial JS 1.87MiB(+0.29%) 1.86MiB
No change  Initial CSS 607.87KiB 607.87KiB
Change  Cache Invalidation 18.65% 19.42%
No change  Chunks 223 223
No change  Assets 246 246
Change  Modules 2888(+0.14%) 2884
No change  Duplicate Modules 149 149
No change  Duplicate Code 1.85% 1.85%
No change  Packages 97 97
No change  Duplicate Packages 2 2
Bundle size by type  Change 1 change Regression 1 regression
                 Current
#1995
     Baseline
#1991
Regression  JS 8.82MiB (+0.06%) 8.81MiB
No change  CSS 891.48KiB 891.48KiB
No change  Fonts 526.1KiB 526.1KiB
No change  Media 295.6KiB 295.6KiB
No change  IMG 140.74KiB 140.74KiB
No change  HTML 1.24KiB 1.24KiB
No change  Other 871B 871B

Bundle analysis reportBranch JustinGeorgi:jag-oh-contextProject dashboard

Copy link
Contributor

@florian-h05 florian-h05 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!
Very nice addition.

It took me a bit to figure out the scoping of the vars, so I added JSDoc to the getVariableScope method:

    /**
     * Get the oh-context variables scope for a given variable key.
     *
     * If no variable with the given key is found in the given scope, the parent context/scope is checked, and so on.
     * If the variable is not found in any scope, <code>null</code> is returned.
     *
     * oh-context variables are local in scope to the oh-context and it's children and take precedence over other variables
     * of the same name from higher contexts/scopes, including normal variables.
     * Changes to oh-context variables done by children are propagated to the parent, which is not the case with normal variables.
     *
     * @param {object} varObj the object containing the variables for each context/scope
     * @param {string} scopeObj the key of the given variable context/scope
     * @param {string} key the key of the variable
     * @returns {string|null} the key of the variable context/scope to be used
     */

Please let me know it that doc is correct @JustinGeorgi.

I also did several minor code improvements, I will soon rebase and resolve the conflicts.

JustinGeorgi and others added 12 commits May 31, 2024 22:47
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
@florian-h05
Copy link
Contributor

@JustinGeorgi As far as I have tested with your demo widgets, nothing got broken by the rebasing, even though the conflict was a bit tricky.

Please have a final look/do a final test and let me know when I should merge.

@florian-h05 florian-h05 added this to the 4.2 milestone May 31, 2024
Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
@JustinGeorgi
Copy link
Contributor Author

Thanks, the additions look pretty good to me.

I don't know if it's worth being more precise on the last line of the JSDoc because the normal variable system for propagating variable values to parents is actually different for components and widgets (components do, widgets do not). So technically,

* Changes to oh-context variables done by children are propagated to the parent, which is not the case with normal variables.

is a little mis-leading about the normal variables, but I personally don't think it's a big concern because it is true that both work with the oh-context.

Please have a final look/do a final test and let me know when I should merge.

Checks out with all the tests I've run. As far as I'm concerned it's ready to merge.

@florian-h05
Copy link
Contributor

florian-h05 commented Jun 1, 2024

Thanks for the feedback.
So the following should be better?

Changes to oh-context variables done by children are always propagated to the parent, which is not the case with normal variables used inside widgets.

@JustinGeorgi
Copy link
Contributor Author

So the following should be better?

Changes to oh-context variables done by children are always propagated to the parent, which is not the case with normal variables used inside widgets.

Yeah, that's great.

Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
@florian-h05 florian-h05 merged commit 6ceeee3 into openhab:main Jun 1, 2024
8 checks passed
@florian-h05
Copy link
Contributor

Can you please update the docs?

@JustinGeorgi
Copy link
Contributor Author

Can you please update the docs?

Of course. I'll probably have time in the next week or so.

@SimonB27P
Copy link

This is excellent news! Thank-you for all the hard work on this. This will greatly simplify the widgets I have been working on recently.

Can you tell me - how can I test this?

@florian-h05
Copy link
Contributor

Basically you use oh-context „above“ all components that should access its context, so I guess in most cases as the root component of your widget. Add the actual widget into the default slots of the oh-context component and you‘ll be able to access constants on the const. object, variables as usual on the vars. object and functions on the fn. object.
Take a look at the first code box in the PR description, its a good example.

For more detailed explanation please wait for the docs.

@SimonB27P
Copy link

Thanks Florian

Your example widget above looks pretty self-explanantory. I noticed the docs are not updated yet and I presume it's not in the current stable distribution but I was wondering whether it is in the 4.2 M3 milestone?

Thanks again for the work on this. It promises to be awesome. I've got some widgets to re-write having spent a lot of time scratching my head and working around the lack of declared variables with defaults or passing variables into custom sub-widgets. The functions willbe useful too.

@florian-h05
Copy link
Contributor

Your example widget above looks pretty self-explanantory.

FYI this PR is @JustinGeorgi ‘s work, not mine. I am the one who reviewed and merged, but not who wrote it.

I noticed the docs are not updated yet.

Once Justin created a PR to update the docs, they will be at https://next.openhab.org/docs. https://www.openhab.org/docs holds the stable docs, next always the latest docs from the current state of the repo.

I was wondering whether it is in the 4.2 M3 milestone?

No, if it was, the release notes would mention it, and as M3 was released two weeks ago and this PR was merged yesterday, it obviously cannot be in the milestone 😉
FYI you can click on the commit hash above where it says „florian-h05 merged commit … into main“, where you’ll get to the commit page. After the commit message you’ll see information about the branch (in that case „main“) and see if any tags are „active“ for that commit. If something is in a release or a milestone, it will have the according tag.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request main ui Main UI
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Widget variables not accessible to custom sub-widgets Widget functions and constants
3 participants