Add and Populate Sections Dynamically

See SAIL Recipes for information about how to work through recipes and adapt them to your application.

Goal

Add and populate a dynamic number of sections, one for each item in a CDT array.

Each section contains an input for each field of the CDT. A new entry is added to the CDT array as the user is editing the last section to allow the user to quickly add new entries without extra clicks. Sections can be independently removed by clicking on a "Remove" button. In the example below, attempting to remove the last section simply blanks out the inputs. Your own use case may involve removing the last section altogether.

The main expression uses a few supporting rules, so let's create them first.

  • ucDynamicSectionAddOne: Takes an array of records, and adds a null record of the same type if the given index is for the last item in the array.
  • ucDynamicSectionRemoveFromArray: Removes the item at the given index from the records array. If removing the last item in the array, replaces the last item with a null record of the same type.
  • ucDynamicSectionEach: Returns a section with its components populated with the value of the record at the specified index.

Create expression rule ucDynamicSectionAddOne with the following rule inputs:

  • array (Any Type)
  • index (Number Integer)

Enter the following definition for the rule:

=if(
  ri!index <> count(ri!array),
  ri!array,
  append(ri!array, cast(typeof(ri!array), null))
)

Create expression rule ucDynamicSectionRemoveFromArray with the following rule inputs:

  • index (Number Integer)
  • array (Any Type)

Enter the following definition for the rule:

=if(
  count(ri!array)=1,
  {cast(typeof(ri!array), null)},
  remove(ri!array, ri!index)
)

Create interface ucDynamicSectionEach with the following interface inputs:

  • index (Number Integer)
  • records (Any Type)
  • recordTokens (Any Type)

Enter the following definition for the interface:

=a!sectionLayout(
  label: "Section " & ri!index,
  firstColumnContents:{
    a!textField(
      label: "Label",
      value: ri!records[ri!index].label,
      saveInto: {
        ri!records[ri!index].label,
        a!save(ri!records, rule!ucDynamicSectionAddOne(ri!records, ri!index)),
        a!save(ri!recordTokens, rule!ucDynamicSectionAddOne(ri!recordTokens, ri!index))
      },
      refreshAfter: "KEYPRESS"
    ),
    a!textField(
      label: "Value",
      value: ri!records[ri!index].value,
      saveInto: ri!records[ri!index].value,
      refreshAfter: "KEYPRESS"
    ),
    if(
      count(ri!records) > 1,
      a!buttonArrayLayout(
        a!buttonWidget(
          label: "Remove",
          value: ri!index,
          saveInto: {
            a!save(ri!records, rule!ucDynamicSectionRemoveFromArray(save!value, ri!records)),
            a!save(ri!recordTokens, rule!ucDynamicSectionRemoveFromArray(save!value,  ri!recordTokens))
          }
        )
      ),
      {}
    )
  }
)

Now that we've created the supporting rules, let's move on to the main expression.

Expression

=load(
  local!records: {type!LabelValue()},
  local!recordTokens,
  a!formLayout(
    label: "SAIL Example: Add Sections Dynamically",
    instructions: "local!records: " & local!records,
    firstColumnContents: {
      a!applyComponents(
        function: rule!ucDynamicSectionEach(index: _, records: local!records, recordTokens: local!recordTokens),
        array: 1+enumerate(count(local!records)),
        arrayVariable: local!recordTokens
      )
    },
    buttons: a!buttonLayout(
      primaryButtons: a!buttonWidgetSubmit(
        label: "Submit"
      )
    )
  )
)

Test it out

  1. Fill in the first field and notice that a new section is added as you're typing.
  2. Add a few sections and click on the Remove button to remove items from the array.

Offline

Since sections cannot be added dynamically when offline, you should include multiple sections initially in case they are needed. To support this use case for offline, we will create a different expression with a different supporting rule.

Create expression rule ucSectionEach with the following rule inputs:

  • index (Number Integer)
  • records (Any Type)

Enter the following definition for the rule:

=a!sectionLayout(
  label: "Section " & ri!index,
  firstColumnContents: {
    a!textField(
      label: "Label",
      value: ri!records[ri!index].label,
      saveInto: ri!records[ri!index].label
    ),
    a!textField(
      label: "Value",
      value: ri!records[ri!index].value,
      saveInto: ri!records[ri!index].value
    )
  }
)

Now create your main interface with the following definition:

=load(
  local!records: repeat(3, type!LabelValue()),
  /* Replace 3 with the maximum number of sections that you expect will be needed */
  a!formLayout(
    label: "SAIL Example: Multiple Sections (Offline)",
    firstColumnContents: {
      a!applyComponents(
        function: rule!ucSectionEach(index: _, records: local!records),
        array: 1+enumerate(count(local!records))
      )
    },
    buttons: a!buttonLayout(
      primaryButtons: a!buttonWidgetSubmit(
        label: "Submit",
        saveInto: a!save(local!records, reject(fn!isnull, local!records))
        /* This will remove any null values from the array upon submission */
      )
    )
  )
)

Test it out

  1. There are now 5 sections available to the user immediately.
  2. Fill out some of the sections but leave others blank and submit the form. Notice that null values are removed from the array and only non-null values are saved.

Notable implementation details

  • local!recordTokens must be declared as a load() local variable as shown above for adding and removing of sections to work correctly. The local variable is then passed to the looping function a!applyComponents as its third parameter. a!applyComponents will create an array in this variable that is the same length as the record array. Changes to the record array such as adding, removing, or swapping must also be made to the recordTokens array.
  • When dynamically adding and generating SAIL components in this way, always use the a!xxxComponents() looping functions. See also: New Looping Functions for Components

To write your data to process

  1. Save your interface as sailRecipe
  2. Create interface input: records (labelValue(Array))
  3. Delete local variable: local!records
  4. In your expression, replace:
    • local!records with ri!records
  5. 1. In your process model, on the process start form or forms tab of an activity, enter the name of your interface in the search box and select it
  6. Click Yes when the process modeler asks, "Do you want to import the interface inputs?"
    • On a task form, this will create create node inputs
    • On a start form, this will create parameterized process variables
  7. On either the records Node Input or Process Variable, set the default value to ={label:""}
    • This will provide the interface with the appropriate data structure for the first section
FEEDBACK