Building Customisable Components

Building truly reusable components means finding the right compromise between hard-coded logic, parametric adjustment and customisation. In this post, I want to share my way of building customisable components.

Background

To understand how this works, we have to start with a specific feature of the Appian expression language. It is a functional language with expression rules as first class citizens. This means, that you can pass references to expression rules just like any other value to a rule input of the type Any or a local variable and then execute them. Find the documentation here.

Let’s do a quick example. My first rule is this:

ReferencedRule(name(text))

"Hello " & ri!name & "!"

Then I can write code like below:

a!localVariables(
  local!rule: rule!SSH_ReferencedRule,
  
  local!rule(name: "Stefan")
)

Take a close look at line 2. By omitting the brackets, I assign a reference to the local variable instead of calling it. I can then execute the reference stored in the local by treating it like a normal rule.

Customisable Components

With this technique at hand, you can build a framework component like a grid, and then let the designer pass an expression to display each item. Let’s have a look at an example. This needs three separate expressions.

EXP_SegmentList

As I want to put a grid onto the screen, I somehow need to turn a list of items into a two-dimensional matrix. I use the following expression out of my toolbox to turn a list of items into a list of lists of items with a defined length. It transforms a one-dimensional list (1, 2, 3, 4, 5) into a two-dimensional ((1, 2, 3), (4, 5)). I added some additional features to allow a 90° rotation ((1, 4), (2, 5), (3)) and padding to have a uniform number of items in each column or row.

It uses the following rule inputs:

  • items: Any Type
  • segmentSize: Integer
  • rotate: Boolean
  • enablePadding: Boolean
if(
  or(
    a!isNullOrEmpty(ri!items),
    a!isNullOrEmpty(ri!segmentSize)
  ),
  ri!items,
  a!localVariables(
    local!numSegments: ceiling(count(ri!items) / ri!segmentSize),
    if(
      ri!rotate,
      /* WITH ROTATION: segmentSize means the number of segments */
      if(
        ri!enablePadding,
        /* WITH PADDING: Adds type casted NULL values to the last segment to fill up to the size of the segment */
        a!forEach(
          items: enumerate(ri!segmentSize),
          expression: index(
            ri!items,
            1 /* First item in list is at index 1 and enumerate creates numbers starting at 0 */
            + fv!item /* Start number for current segment */
            /* Fix segment size */
            + enumerate(local!numSegments)
            * ri!segmentSize,
            cast(runtimetypeof(ri!items[1]), null)
          )
        ),
        /* WITHOUT PADDING: Only add left over items into the last segment. The last segment might contain less items */
        a!forEach(
          items: enumerate(ri!segmentSize),
          expression: reject(
            a!isNullOrEmpty(_),
            index(
              ri!items,
              1 /* First item in list is at index 1 and enumerate creates numbers starting at 0 */
              + fv!item /* Start number for current segment */
              /* Adjust the size of the segment to either the required size or for the last segment the left over items */
              + enumerate(local!numSegments)
              * ri!segmentSize,
              null
            )
          )
        )
      ),
      /* WITHOUT ROTATION: segmentSize means the number of items per segment */
      if(
        ri!enablePadding,
        /* WITH PADDING: Adds type casted NULL values to the last segment to fill up to the size of the segment */
        a!forEach(
          items: enumerate(local!numSegments),
          expression: index(
            ri!items,
            1 /* First item in list is at index 1 and enumerate creates numbers starting at 0 */
            + (fv!item * ri!segmentSize) /* Start number for current segment */
            /* Fixe segment size */
            + enumerate(ri!segmentSize),
            cast(runtimetypeof(ri!items[1]), null)
          )
        ),
        /* WITHOUT PADDING: Only add left over items into the last segment. The last segment might contain less items */
        a!forEach(
          items: enumerate(local!numSegments),
          expression: index(
            ri!items,
            1 /* First item in list is at index 1 and enumerate creates numbers starting at 0 */
            + (fv!item * ri!segmentSize) /* Start number for current segment */
            /* Adjust the size of the segment to either the required size or for the last segment the left over items */
            + enumerate(min(ri!segmentSize, count(ri!items) - (fv!item * ri!segmentSize))),
            null
          )
        )
      )
    )
  )
)

EXP_RenderGridItem

This expression is responsible for rendering an individual item from the list. In my example, these items only have a single field “name”. There is no restriction to the complexity or source of the items you want to use. Just make sure that your expression works with your data structure.

The rule inputs are:

  • item: Any Type (This can also be your CDT or Record type, or a map)
a!cardLayout(
  showWhen: a!isNotNullOrEmpty(ri!item),
  contents: {
    a!richTextDisplayField(
      labelPosition: "COLLAPSED",
      value: ri!item.name
    )
  },
  height: "AUTO",
  style: "NONE",
  marginBelow: "STANDARD",
  showBorder: false,
  showShadow: true
)

EXP_Grid

This is my framework component, which uses a foreach() to create a columns layout for the horizontal dimension. In each column, the inner foreach() calls the contentExpression to display the actual contents in a vertical orientation.

It needs the following rule inputs:

  • items: Any Type
  • contentExpression: Any Type
  • numColumns: Integer
a!columnsLayout(
  columns: a!forEach(
    items: rule!PCO_SegmentList(
      items: ri!items,
      segmentSize: ri!numColumns,
      rotate: true,
      enablePadding: true
    ),
    expression: a!columnLayout(
      contents: a!forEach(
        items: fv!item,
        expression: ri!contentExpression(
          item: fv!item
        )
      )
    )
  )
)

If you like, you can add a default rendering style and make the contentExpression optional. Below, a simple example:

        expression: if(
          a!isNullOrEmpty(ri!contentExpression),
          a!richTextDisplayField(
            label: "Default",
            value: tostring(fv!item)
          ),
          ri!contentExpression(
            item: fv!item
          )
        )

Putting the Pieces Together

Let’s do a quick recap. You now have an expression that can render a two-dimensional matrix of items onto the screen. And you have another expression which knows how to display an individual item. Putting it together should look like this:

rule!EXP_Grid(
  items: {
    {name: "One"},
    {name: "Two"},
    {name: "Three"},
    {name: "Four"},
    {name: "Five"},
  },
  contentExpression: rule!EXP_ContentExpression,
  numColumns: 2
)

Summary

I hope I could give you some insights into advanced coding techniques using the Appian expression language and inspiration for building your own customisable components.

I am very curious about the ideas you will implement with this method. Let me know in a comment.

2 thoughts on “Building Customisable Components

  1. Hi, please let me know your thoughts on below use case on exp rules as first class citizens.
    #rule 1 – validate_user_email
    Rule input- user – type -> usercdt
    Exp rule: logic to validate user email address and return true or false

    #rule 2 – validate_user_pincode
    Rule input- user – type -> usercdt
    Exp rule: logic to validate user pincode and return true or false

    #rule3 – filter_cdt
    Rule inputs:
    1. function – type -> any type
    2. userarray – type -> usercdt multiple
    Exp rule:
    a!foreach(
    items: ri!userarray,
    expression: function(fv!item)
    # reject nulls if required.

    #main rule
    local!filter_cdt:rule!filter_cdt,
    local!filter_cdt(rule!validate_user_email, userarray),
    local!filter_cdt(rule!validate_user_pincode, userarray),
    local!filter_cdt(rule!check_if_active, userarray), # example

    Please let me know if i implemented it correctly.
    – VAMSI KRISHNA

    1. Hi Vamsi,

      I suggest to use the filter() function to filter any kind of lists. This function takes a predicate function which is just the reference to another function or expression. So in your use case it would look something like this:

      filter(
      rule!validate_user_email,
      userarray
      )

      One more thing. In Appian we typically use camelCasing and not underscore_casing. Then, I try to name validation expression more like “isUserEmailValid”. This also indicates that the return value is a boolean.

      A foreach() creates a list with the same number of items as the input list. Trying to use foreach() for filtering only works because Appian merges empty lists in most cases. In some cases this breaks which might result in some hard to debug errors.

      Stefan.

Leave a Reply