Skip to content

Commit

Permalink
Add oh-context component (#2533)
Browse files Browse the repository at this point in the history
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 to inject these three things into the widget:
* 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.
  * 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.

---------

Also-by: Florian Hotze <florianh_dev@icloud.com>
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
  • Loading branch information
JustinGeorgi authored Jun 1, 2024
1 parent aa4e218 commit 6ceeee3
Show file tree
Hide file tree
Showing 20 changed files with 341 additions and 32 deletions.
1 change: 1 addition & 0 deletions bundles/org.openhab.ui/doc/components/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ source: https://github.com/openhab/openhab-webui/edit/main/bundles/org.openhab.u
| [`oh-chart`](./oh-chart.html) | [Chart](./oh-chart.html) | Visualize series of data |
| [`oh-clock`](./oh-clock.html) | [Digital Clock](./oh-clock.html) | Display a digital clock |
| [`oh-colorpicker`](./oh-colorpicker.html) | [Colorpicker](./oh-colorpicker.html) | Control to pick a color |
| [`oh-context`](./oh-context.html) | [Context](./oh-context.html) | Non-rendered component with functions, constants, and scoped variables for widgets |
| [`oh-gauge`](./oh-gauge.html) | [Gauge](./oh-gauge.html) | Circular or semi-circular read-only gauge |
| [`oh-icon`](./oh-icon.html) | [Icon](./oh-icon.html) | Display an openHAB icon |
| [`oh-image`](./oh-image.html) | [Image](./oh-image.html) | Displays an image from a URL or an item |
Expand Down
101 changes: 101 additions & 0 deletions bundles/org.openhab.ui/doc/components/oh-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
title: oh-context - Widget Context
component: oh-context
label: Widget Context
description: Non-rendered component with functions, constants, and local variables for widgets
source: https://github.com/openhab/openhab-webui/edit/main/bundles/org.openhab.ui/doc/components/oh-context.md
prev: /docs/ui/components/
---

# oh-context - Widget Context

<!-- Put a screenshot here if relevant:
![](./images/oh-context/header.jpg)
-->

[[toc]]

<!-- Note: you can overwrite the definition-provided description and add your own intro/additional sections instead -->
<!-- DO NOT REMOVE the following comments if you intend to keep the definition-provided description -->
<!-- GENERATED componentDescription -->
Non-rendered component with functions, constants, and scoped variables for widgets
<!-- GENERATED /componentDescription -->

## Configuration

<!-- DO NOT REMOVE the following comments -->
<!-- GENERATED props -->
### General
<div class="props">
<PropGroup label="General">
<PropBlock type="TEXT" name="functions" label="Widget Functions">
<PropDescription>
Object with key:arrow-function pairs. Functions are available to expressions in all child components via the <code>fn</code> object.
</PropDescription>
</PropBlock>
<PropBlock type="TEXT" name="constants" label="Widget Constants">
<PropDescription>
Object with key:constant pairs. Constants are available to expressions in all child components via the <code>const</code> object.
</PropDescription>
</PropBlock>
<PropBlock type="TEXT" name="variables" label="Widget Variables">
<PropDescription>
Object with key:variable default value pairs. Variables are available to expressions in all child components via the <code>vars</code> object and take precedence over variables with the same name from higher contexts.
</PropDescription>
</PropBlock>
</PropGroup>
</div>


<!-- GENERATED /props -->

<!-- If applicable describe how properties are forwarded to a underlying component from Framework7, ECharts, etc.:
### Inherited Properties
-->

<!-- If applicable describe the slots recognized by the component and what they represent:
### Slots
#### `default`
The contents of the oh-context.
-->

<!-- Add as many examples as desired - put the YAML in a details container when it becomes too long (~150/200+ lines):
## Examples
### Example 1
![](./images/oh-context/example1.jpg)
```yaml
component: oh-context
config:
prop1: value1
prop2: value2
```
### Example 2
![](./images/oh-context/example2.jpg)
::: details YAML
```yaml
component: oh-context
config:
prop1: value1
prop2: value2
slots
```
:::
-->

<!-- Try to clean up URLs to the forum (https://community.openhab.org/t/<threadID>[/<postID>] should suffice)
## Community Resources
- [Community Post 1](https://community.openhab.org/t/12345)
- [Community Post 2](https://community.openhab.org/t/23456)
-->
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { pt, pb, pi, pn } from '../helpers.js'

export default () => [
pt('functions', 'Widget Functions', 'Object with key:arrow-function pairs. Functions are available to expressions in all child components via the <code>fn</code> object.'),
pt('constants', 'Widget Constants', 'Object with key:constant pairs. Constants are available to expressions in all child components via the <code>const</code> object.'),
pt('variables', 'Widget Variables', 'Object with key:variable default value pairs. Variables are available to expressions in all child components via the <code>vars</code> object and take precedence over variables with the same name from higher contexts.')
]
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import ColorpickerParameters from './colorpicker.js'
export const OhColorpickerDefinition = () => new WidgetDefinition('oh-colorpicker', 'Colorpicker', 'Control to pick a color')
.params(ColorpickerParameters())

import ContextParameters from './context.js'
export const OhContextDefinition = () => new WidgetDefinition('oh-context', 'Context', 'Non-rendered component with functions, constants, and scoped variables for widgets')
.params(ContextParameters())

import GaugeParameters from './gauge.js'
export const OhGaugeDefinition = () => new WidgetDefinition('oh-gauge', 'Gauge', 'Circular or semi-circular read-only gauge')
.params(GaugeParameters())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ function hintExpression (cm, line) {
{ text: 'props.', displayText: 'props', description: 'Access to the props of the parent root component' },
{ text: 'config.', displayText: 'config', description: 'Access to the configuration of the current component' },
{ text: 'vars.', displayText: 'vars', description: 'Access to context vars' },
{ text: 'fn.', displayText: 'fn', description: 'Access to oh-context functions' },
{ text: 'const.', displayText: 'const', description: 'Access to oh-context constants' },
{ text: 'loop.', displayText: 'loop', description: 'Access to oh-repeater loop variables' },
{ text: 'JSON.', displayText: 'JSON', description: 'Access to the JSON object functions' },
{ text: 'Math.', displayText: 'Math', description: 'Access to the Math object functions' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default {
return {
currentTab: 0,
vars: {},
ctxVars: {},
tabVars: {}
}
},
Expand All @@ -24,6 +25,7 @@ export default {
store: this.$store.getters.trackedItems,
props: this.modalConfig,
vars: this.vars,
ctxVars: this.ctxVars,
modalConfig: this.modalConfig // For configuration of oh- components
}
},
Expand Down Expand Up @@ -67,6 +69,7 @@ export default {
onTabChange (idx) {
this.currentTab = idx
this.$set(this, 'vars', {})
this.$set(this, 'ctxVars', {})
},
tabContext (tab) {
const page = this.$store.getters.page(tab.config.page.replace('page:', ''))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ export { default as OhRepeater } from './oh-repeater.vue'
export { default as OhChart } from './oh-chart.vue'
export { default as OhClock } from './oh-clock.vue'
export { default as OhSipclient } from './oh-sipclient.vue'
export { default as OhContext } from './oh-context.vue'
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,29 @@ export default {
}
if (this.config.clearVariable && !this.config.clearVariableKey) {
if (Array.isArray(this.config.clearVariable)) {
this.config.clearVariable.forEach((v) => this.$set(this.context.vars, v, undefined))
this.config.clearVariable.forEach((v) => {
const clearVariableScope = this.getVariableScope(this.context.ctxVars, this.context.varScope, v)
const clearVariableLocation = (clearVariableScope) ? this.context.ctxVars[clearVariableScope] : this.context.vars
this.$set(clearVariableLocation, v, undefined)
})
} else if (typeof this.config.clearVariable === 'string') {
this.$set(this.context.vars, this.config.clearVariable, undefined)
const clearVariableScope = this.getVariableScope(this.context.ctxVars, this.context.varScope, this.config.clearVariable)
const clearVariableLocation = (clearVariableScope) ? this.context.ctxVars[clearVariableScope] : this.context.vars
this.$set(clearVariableLocation, this.config.clearVariable, undefined)
}
}
if (this.config.clearVariable && this.config.clearVariableKey) {
let value = this.context.vars[this.config.clearVariable]
if (Array.isArray(this.config.clearVariableKey)) {
this.config.clearVariableKey.forEach((key) => {
value = this.setVariableKeyValues(value, key, undefined)
const clearVariableScope = this.getVariableScope(this.context.ctxVars, this.context.varScope, this.config.clearVariable)
const clearVariableLocation = (clearVariableScope) ? this.context.ctxVars[clearVariableScope] : this.context.vars
value = this.setVariableKeyValues(clearVariableLocation, key, undefined)
})
} else if (typeof this.config.clearVariableKey === 'string') {
value = this.setVariableKeyValues(value, this.config.clearVariableKey, undefined)
const clearVariableScope = this.getVariableScope(this.context.ctxVars, this.context.varScope, this.config.clearVariable)
const clearVariableLocation = (clearVariableScope) ? this.context.ctxVars[clearVariableScope] : this.context.vars
value = this.setVariableKeyValues(clearVariableLocation, this.config.clearVariableKey, undefined)
}
this.$set(this.context.vars, this.config.clearVariable, value)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<template>
<fragment v-if="(context.component.slots && context.component.slots.default)">
<generic-widget-component v-for="(slotComponent, idx) in context.component.slots.default" :key="'default-' + idx" :context="childrenContext(slotComponent)" />
</fragment>
</template>

<script>
import mixin from '../widget-mixin'
import { OhContextDefinition } from '@/assets/definitions/widgets/system'
import { Fragment } from 'vue-fragment'
export default {
mixins: [mixin],
components: {
Fragment
},
widget: OhContextDefinition,
data () {
return {
varScope: (this.context.varScope || 'varScope') + '-' + this.$f7.utils.id()
}
},
computed: {
fn () {
if (!this.context || !this.context.component || !this.context.component.config) return {}
let evalFunc = {}
const sourceFunc = this.context.component.config.functions || {}
console.debug('oh-context: sourceFunc =', sourceFunc)
if (sourceFunc) {
if (typeof sourceFunc !== 'object') return {}
for (const key in sourceFunc) {
evalFunc[key] = this.evaluateExpression(key, sourceFunc[key])
}
}
console.debug('oh-context: evalFunc =', evalFunc)
return evalFunc
}
},
methods: {
childrenContext (childComp) {
const ctx = this.childContext(childComp)
const ctxFunctions = this.fn
if (this.context.fn) {
for (const funcKey in this.context.fn) {
if (!ctxFunctions[funcKey]) this.$set(ctxFunctions, funcKey, this.context.fn[funcKey])
}
}
this.$set(ctx, 'fn', ctxFunctions)
const ctxConstants = this.const
if (this.context.const) {
for (const constKey in this.context.const) {
if (!ctxConstants[constKey]) this.$set(ctxConstants, constKey, this.context.const[constKey])
}
}
this.$set(ctx, 'const', ctxConstants)
this.$set(ctx.ctxVars, this.varScope, this.ctxVars)
return ctx
}
},
beforeMount () {
const evaluateDefaults = () => {
if (!this.context || !this.context.component || !this.context.component.config) return
this.const = {}
const sourceConst = this.context.component.config.constants || {}
if (sourceConst) {
if (typeof sourceConst !== 'object') return
for (const key in sourceConst) {
this.$set(this.const, key, this.evaluateExpression(key, sourceConst[key]))
}
}
this.ctxVars = {}
const sourceCtxVars = this.context.component.config.variables || {}
if (sourceCtxVars) {
if (typeof sourceCtxVars !== 'object') return
for (const key in sourceCtxVars) {
this.$set(this.ctxVars, key, this.evaluateExpression(key, sourceCtxVars[key]))
}
}
}
evaluateDefaults()
}
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ export default {
widget: OhGaugeDefinition,
computed: {
value () {
if (this.config.variable) return this.context.vars[this.config.variable]
if (this.config.variable) {
const variableScope = this.getVariableScope(this.context.ctxVars, this.context.varScope, this.config.variable)
const variableLocation = (variableScope) ? this.context.ctxVars[variableScope] : this.context.vars
return variableLocation[this.config.variable]
}
let value = (this.config.item) ? this.context.store[this.config.item].state : this.config.value
// use as a brightness indicator for HSB values
if (value.split && value.split(',').length === 3) value = value.split(',')[2]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,18 @@ export default {
},
computed: {
value () {
let variableLocation = this.context.vars
if (this.config.variable) {
const variableScope = this.getVariableScope(this.context.ctxVars, this.context.varScope, this.config.variable)
if (variableScope) variableLocation = this.context.ctxVars[variableScope]
}
if (this.config.variable && this.config.variableKey) {
const keyValue = this.getLastVariableKeyValue(this.context.vars[this.config.variable], this.config.variableKey)
const keyValue = this.getLastVariableKeyValue(variableLocation[this.config.variable], this.config.variableKey)
if (keyValue) {
return keyValue
}
} else if (this.config.variable && this.context.vars[this.config.variable] !== undefined) {
return this.context.vars[this.config.variable]
} else if (this.config.variable && variableLocation[this.config.variable] !== undefined) {
return variableLocation[this.config.variable]
} else if (this.config.sendButton && this.pendingUpdate !== null) {
return this.pendingUpdate
} else if (this.config.item && this.context.store[this.config.item].state !== 'NULL' && this.context.store[this.config.item].state !== 'UNDEF' && this.context.store[this.config.item].state !== 'Invalid Date') {
Expand Down Expand Up @@ -107,10 +112,12 @@ export default {
this.$set(this, 'pendingUpdate', value)
}
if (this.config.variable) {
const variableScope = this.getVariableScope(this.context.ctxVars, this.context.varScope, this.config.variable)
const variableLocation = (variableScope) ? this.context.ctxVars[variableScope] : this.context.vars
if (this.config.variableKey) {
value = this.setVariableKeyValues(this.context.vars[this.config.variable], this.config.variableKey, value)
value = this.setVariableKeyValues(variableLocation[this.config.variable], this.config.variableKey, value)
}
this.$set(this.context.vars, this.config.variable, value)
this.$set(variableLocation, this.config.variable, value)
}
},
sendButtonClicked () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,29 @@ export default {
}
if (this.config.clearVariable && !this.config.clearVariableKey) {
if (Array.isArray(this.config.clearVariable)) {
this.config.clearVariable.forEach((v) => this.$set(this.context.vars, v, undefined))
this.config.clearVariable.forEach((v) => {
const clearVariableScope = this.getVariableScope(this.context.ctxVars, this.context.varScope, v)
const clearVariableLocation = (clearVariableScope) ? this.context.ctxVars[clearVariableScope] : this.context.vars
this.$set(clearVariableLocation, v, undefined)
})
} else if (typeof this.config.clearVariable === 'string') {
this.$set(this.context.vars, this.config.clearVariable, undefined)
const clearVariableScope = this.getVariableScope(this.context.ctxVars, this.context.varScope, this.config.clearVariable)
const clearVariableLocation = (clearVariableScope) ? this.context.ctxVars[clearVariableScope] : this.context.vars
this.$set(clearVariableLocation, this.config.clearVariable, undefined)
}
}
if (this.config.clearVariable && this.config.clearVariableKey) {
let value = this.context.vars[this.config.clearVariable]
if (Array.isArray(this.config.clearVariableKey)) {
this.config.clearVariableKey.forEach((key) => {
value = this.setVariableKeyValues(value, key, undefined)
const clearVariableScope = this.getVariableScope(this.context.ctxVars, this.context.varScope, this.config.clearVariable)
const clearVariableLocation = (clearVariableScope) ? this.context.ctxVars[clearVariableScope] : this.context.vars
value = this.setVariableKeyValues(clearVariableLocation, key, undefined)
})
} else if (typeof this.config.clearVariableKey === 'string') {
value = this.setVariableKeyValues(value, this.config.clearVariableKey, undefined)
const clearVariableScope = this.getVariableScope(this.context.ctxVars, this.context.varScope, this.config.clearVariable)
const clearVariableLocation = (clearVariableScope) ? this.context.ctxVars[clearVariableScope] : this.context.vars
value = this.setVariableKeyValues(clearVariableLocation, this.config.clearVariableKey, undefined)
}
this.$set(this.context.vars, this.config.clearVariable, value)
}
Expand Down
Loading

0 comments on commit 6ceeee3

Please sign in to comment.