altinn-studio icon indicating copy to clipboard operation
altinn-studio copied to clipboard

Layout expressions: Extend with more functionality

Open olemartinorg opened this issue 2 years ago • 0 comments

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.

See discussion on Slack

olemartinorg avatar Aug 12 '22 12:08 olemartinorg