Build a Wizard in SAIL

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

Goal

Divide a big form into sections presented one step at a time with validation.

All the fields must be valid before the user is allowed to move to the next steps. However, the user is allowed to move to a previous step even if the fields in the current step aren't valid. The last step in the wizard is a confirmation screen.

Note:

  • SAIL should be used to break up a large form into smaller steps rather than activity-chaining. Users can step back and forth within a SAIL wizard without losing data in between steps.
  • This design pattern is not recommended for offline interfaces because the dynamic behavior of a wizard requires a connection to the server.

In the first example, we save the entire expression as one rule so that you can more easily read it and follow the logic of how the buttons are configured to go forward and back. In the second example, we show you how to break up the expression into multiple rules so that we can test each step independently, and keep the main rule easier to read and maintain. This will make it easier for you to add more fields to each step of the wizard.

Expression 1:

=load(
  local!cdt: type!DataSubset(),
  local!currentStep: 1,
  choose(
    local!currentStep,
    a!formLayout(
      label: "SAIL Example: Wizard Step 1",
      firstColumnContents: {
        a!integerField(
          label: "Start Index",
          value: local!cdt.startIndex,
          saveInto: local!cdt.startIndex,
          required: true
        )
      },
      buttons: a!buttonLayout(
        primaryButtons: {
          a!buttonWidget(
            label: "Next",
            style: "PRIMARY",
            value: 2,
            saveInto: local!currentStep,
            validate: true
          )
        }
      )
    ),
    a!formLayout(
      label: "SAIL Example: Wizard Step 2",
      firstColumnContents: {
        a!integerField(
          label: "Batch Size",
          value: local!cdt.batchSize,
          saveInto: local!cdt.batchSize,
          required: true
        )
      },
      buttons: a!buttonLayout(
        primaryButtons: {
          a!buttonWidget(
            label: "Next",
            style: "PRIMARY",
            value: 3,
            saveInto: local!currentStep,
            validate: true
          )
        },
        secondaryButtons: {
          a!buttonWidget(
            label: "Previous",
            value: 1,
            saveInto: local!currentStep
          )
        }
      )
    ),
    a!formLayout(
      label: "SAIL Example: Wizard Confirmation",
      firstColumnContents: {
        a!integerField(
          label: "Start Index",
          value: local!cdt.startIndex,
          saveInto: local!cdt.startIndex,
          readOnly: true
        ),
        a!integerField(
          label: "Batch Size",
          value: local!cdt.batchSize,
          saveInto: local!cdt.batchSize,
          readOnly: true
        )
      },
      buttons: a!buttonLayout(
        primaryButtons: {
          a!buttonWidgetSubmit(
            label: "Submit",
            style: "PRIMARY"
          )
        },
        secondaryButtons: {
          a!buttonWidget(
            label: "Previous",
            value: 2,
            saveInto: local!currentStep
          )
        }
      )
    )
  )
)

Test it out

  1. Click "Next" without entering a value for Start Index. The user stays on step 1 until a valid integer is entered.
  2. Enter a valid number in step 1, then click "Next" to go to step 2. Enter a valid number, then click "Previous". Click "Next" again, and notice that the number on step 2 is preserved.
    • If you use activity-chaining, the user's input on step 2 would be lost at this point, which is why Appian recommends using SAIL when designing interface wizards.
  3. Navigate back and forth through the wizard to understand its mechanics.

Notable implementation details

  • We use the choose() function to show/hide the wizard steps instead of nested if() functions. The choose() function only evaluates the expression at the index given as its first parameter.
  • Each "Next" button is configured with validation: true. This ensures that the fields on the current step must be valid before local!currentStep is updated to the next step index.
  • Each "Previous" button is not configured to enforce validation. This allows the user to go to a previous step even if the current step has null required fields and other invalid fields.

Now that you understand how the simple case works, let's break up the large expression into more manageable chunks to make it easier to add more fields to each step. We'll create a rule that is responsible for the layout and buttons for every step. We then create one rule for the fields of each step.

Let's start by creating the supporting rules.

  • ucWizardStep: Configures a form layout and the buttons to be used for displaying every step of the wizard.
  • ucWizardFieldsFirst: Returns the contents of the first step in the wizard.
  • ucWizardFieldsSecond: Returns the contents of the first step in the wizard.

Create interface ucWizardStep with the following rule inputs:

  • stepLabel (Text)
  • currentStepVariable (Number Integer)
  • numSteps (Number Integer)
  • stepContents (Any Type)

Enter the following definition for the interface:

=a!formLayout(
  label: ri!stepLabel,
  firstColumnContents: ri!stepContents,
  buttons: a!buttonLayout(
    primaryButtons: a!buttonWidget(
      /* On the last step, show "Submit", and configure the button to submit */
      label: if(ri!currentStepVariable=ri!numSteps, "Submit", "Next"),
      style: "PRIMARY",
      value: ri!currentStepVariable+1,
      saveInto: ri!currentStepVariable,
      submit: ri!currentStepVariable=ri!numSteps,
      validate: true
    ),
    secondaryButtons: {
      if(
        ri!currentStepVariable=1,
        {},
        a!buttonWidget(
          label: "Previous",
          value: ri!currentStepVariable-1,
          saveInto: ri!currentStepVariable
        )
      )
    }
  )
)

Now create interface ucWizardFieldsFirst with the following rule inputs:

  • cdt (Any Type)
  • readOnly (Boolean)

Enter the following definition for the interface:

={
  a!integerField(
    label: "Start Index",
    value: ri!cdt.startIndex,
    saveInto: ri!cdt.startIndex,
    required: if(ri!readOnly, false, true),
    readOnly: ri!readOnly
  )
}

Now create interface ucWizardFieldsSecond with the following rule inputs:

  • cdt (Any Type)
  • readOnly (Boolean)

Enter the following definition for the interface:

={
  a!integerField(
    label: "Batch Size",
    value: ri!cdt.batchSize,
    saveInto: ri!cdt.batchSize,
    required: if(ri!readOnly, false, true),
    readOnly: ri!readOnly
  )
}

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

Expression 2:

=load(
  local!cdt: type!DataSubset(),
  local!currentStep: 1,
  choose(
    local!currentStep,
    /* step 1 `*/
    rule!ucWizardStep(
      stepLabel: "SAIL Example: Wizard Step 1",
      currentStepVariable: local!currentStep,
      numSteps: 3,
      stepContents: rule!ucWizardFieldsFirst(cdt: local!cdt)
    ),
    /*` step 2 `*/
    rule!ucWizardStep(
      stepLabel: "SAIL Example: Wizard Step 2",
      currentStepVariable: local!currentStep,
      numSteps: 3,
      stepContents: rule!ucWizardFieldsSecond(cdt: local!cdt)
    ),
    /*` confirmation step */
    rule!ucWizardStep(
      stepLabel: "SAIL Example: Wizard Confirmation",
      currentStepVariable: local!currentStep,
      numSteps: 3,
      stepContents: {
        rule!ucWizardFieldsFirst(cdt: local!cdt, readOnly: true),
        rule!ucWizardFieldsSecond(cdt: local!cdt, readOnly: true)
      }
    )
  )
)

Test it out

  1. The behavior should be functionally the same as for expression 1.

The way that the expression is set up makes it much easier to add more fields to each step and to read the overall flow of the steps in the main expression. The fields in the wizard steps are reused in the confirmation step, with a flag to show each field in read-only mode.

To write your data to process

  1. Save your interface as sailRecipe
  2. Create interface input: cdt (datasubset)
  3. Remove the load() function
  4. Delete local variables: local!cdt
  5. In your expression, replace:
    • local!cdt with ri!cdt
  6. 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
  7. 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
FEEDBACK