altinn-studio
altinn-studio copied to clipboard
Layout expressions: Extend with more functionality
In the layout expressions project we have intentionally set the bar fairly low, in order to get to a basic implementation faster. As planned for the first release, layout expressions can only be used to evaluate to a boolean value. On the backend, only the hidden
and required
properties for layout components will be evaluated, while the frontend will evaluate other properties like readOnly
and various properties in the edit
properties for groups.
This issue tracks extensions to these expressions we've thought of, but won't necessarily arrive in the first version. These tasks should probably be extracted to separate issues when/if we start working on them, if the efforts to implement them are large enough.
- [x] Recursive layout expressions
Passing an expression as an argument to an expression can be useful for more advanced logic. If we're implementing this, adding functions for and
and or
(both with any amount of arguments) is a given. An example of a recursive expression:
{
"hidden": {
"function": "and",
"args": [
{"component": "some-other-component"},
{
"function": "or",
"args": [
{"dataModel": "MyModel.Path"},
{"dataModel": "MyModel.Alternative.Path"},
{"dataModel": "MyModel.Third.Alternative.Path"},
]
}
]
}
}
- [ ] Expressions returning strings/non-booleans
We could also use expressions for conditionally swap out labels/texts in textResourceBindings
, etc. If we do, there should probably also be function(s) to map known data model (etc) values to the appropriate text resource keys, handle the default case if no mapping is valid, etc. We should also implement functions like concat
, toUpperCase
, etc, for simple string manipulations. There would also be a use-case for the most basic function (possibly even implicit) where an argument is return verbatim (i.e. a lookup
function):
{
"textResourceBindings": {
"title": "myTitle",
"description": { "dataModel": "MyModel.Description" }
}
}
Or explicit:
{
"textResourceBindings": {
"title": "myTitle",
"description": {
"function": "lookup",
"args": [
{ "dataModel": "MyModel.Description" }
]
}
}
}
A proposed example for mapping values and using an expression to find a text resource:
{
"textResourceBindings": {
"title": {
"function": "map",
"args": [
{
"function": "greaterThanEq",
"args": [
{ "component": "age" },
18
]
},
{
"true": "adultTitle",
"false": "childTitle"
},
"pleaseEnterAgeFirstTitle"
]
}
}
}
The above example is a bit contrived, as it is not expected that greaterThanEq
could possibly return anything other than true | false
, so the default value pleaseEnterAgeFirstTitle
would never be used. Additional functionality should be implemented to possibly support if
, elseif
and else
. This map
function could just as well be called switch
, as it behaves like a switch/case statement.
- [ ] Using expressions for values/calculations
We could possibly replace existing implementations for calculations with layout expressions as well. Care needs to be taken to ensure we don't restrict functionality in a way that makes it impossible to implement existing calculations that are already implemented in some apps - we'd like for a frictionless upgrade path to make use of layout expressions instead of RuleHandler.js
.
Careful thought should go into designing this in a way that makes it easy (or at least possible) to implement calculations for rows in a repeating group (respecting potentially hidden/filtered rows). See discussion on Slack here. It should also be possible to implement a function for counting the number of rows in a repeating group.
One advantage to using expressions for calculation is automatic API support and possibly valdation to avoid client-side tampering (if implemented on the backend).
Example for a sum
implementation:
{
"id": "total",
"type": "Input",
"readOnly": true,
"value": {
"function": "sum",
"args": [
{ "component": "value1" },
{ "component": "value2" },
{ "component": "value3" }
]
}
}
Or concatenating strings to present a full name:
{
"id": "fullName",
"type": "Input",
"readOnly": true,
"value": {
"function": "concat",
"args": [
{ "component": "lastName" },
", ",
{ "component": "firstName" }
]
}
}
- [ ] Using expressions for custom validations
This requires both validation (a boolean, in principle) and a validation message (a string, which could perform a lookup in text resources). We would also have to support soft validations. Consider if we should use a prefix like *WARNING*
like we do today, or implement this using a custom not-just-string data type.
- [ ] More argument types: Shadow fields
When we have an implementation for shadow fields, we should support lookup up those values.
- [ ] Expression re-use
Large expressions should be possible to re-use. Either by separating them out to definitions
(similar to a structure like in JsonSchema) and referencing them from there, or by implementing functions to directly reference other expressions:
{
"id": "dependsOnSomethingElse",
"type": "Input",
"hidden": {
"function": "isHidden",
"args": [
{ "component": "someOtherComponent" },
]
}
}
Or, more generically:
{
"id": "dependsOnSomethingElse",
"type": "Input",
"hidden": {
"function": "equals",
"args": [
{
"function": "getProperty",
"args": [
{ "component": "someOtherComponent" },
"hidden"
]
},
true
]
}
}
- [ ] Hiding an entire row in a repeating group
A likely need/want is to use layout expressions to hide an entire repeating group row, possibly based on some data within it. This type of dynamic should be placed on the group component, and would in effect hide all components inside that row. It is an outstanding question if this should lead to the entire row being deleted when submitting the data, if this should not happen, or if it should be configurable.