Add, Edit, and Remove Data in an Inline Editable Grid

SAIL Recipes give you an opportunity to explore different interface design patterns. To learn how to directly use SAIL recipes within your interfaces, see Adapt a SAIL Recipe to Work with My Applications.

Goal

Allow the user to change data directly in an inline editable grid.

This design pattern is not recommended for offline interfaces because reflecting immediate changes in an interface based on user interaction requires a connection to the server.

image:SAIL_Recipe_Inline_Editable_Grid.png

This scenario demonstrates:

  • How to use the grid layout component to build an inline editable grid
  • How to allow users to add and remove rows in the grid
  • How to pass in a default value initially and while adding a new row

Expression

=load(
  /*  
  * local!employess is provided in this recipe as a way to start with hard-coded 
  * data. However, this data is identical to the data created from the entity-backed
  * tutorial. Replace the hard-coded data with a query to the employee data store
  * entity and all of the employee records from the tutorial will appear.
  *
  * To replace this data with your own, replace (ctrl+H or cmd+H) all references to
  * local!employees with your data source, either via rule input or local variable.
  */
  local!employees: {
      { id: 1, firstName: "John" , lastName: "Smith" , department: "Engineering" , title: "Director" , phoneNumber: "555-123-4567" , startDate: today()-360 },
      { id: 2, firstName: "Michael" , lastName: "Johnson" , department: "Finance" , title: "Analyst" , phoneNumber: "555-987-6543" , startDate: today()-360 },
      { id: 3, firstName: "Mary", lastName: "Reed" , department: "Engineering" , title: "Software Engineer" , phoneNumber: "555-456-0123" , startDate: today()-240 },
  },
  a!formLayout(
    label: "SAIL Example: Add,Update, or Remove Employee Data",
    contents: {
      a!gridLayout(
        totalCount: count(local!employees),
        headerCells: {
          a!gridLayoutHeaderCell(label: "First Name" ),
          a!gridLayoutHeaderCell(label: "Last Name" ),
          a!gridLayoutHeaderCell(label: "Department" ),
          a!gridLayoutHeaderCell(label: "Title" ),
          a!gridLayoutHeaderCell(label: "Phone Number" ),
          a!gridLayoutHeaderCell(label: "Start Date", align: "RIGHT" ),
          /* For the "Remove" column */
          a!gridLayoutHeaderCell(label: "" )
        },
        /* Only needed when some columns need to be narrow */
        columnConfigs: {
          a!gridLayoutColumnConfig(width: "DISTRIBUTE", weight:3 ),
          a!gridLayoutColumnConfig(width: "DISTRIBUTE", weight:3 ),
          a!gridLayoutColumnConfig(width: "DISTRIBUTE", weight:3 ),
          a!gridLayoutColumnConfig(width: "DISTRIBUTE", weight:3 ),
          a!gridLayoutColumnConfig(width: "DISTRIBUTE", weight:3 ),
          a!gridLayoutColumnConfig(width: "DISTRIBUTE", weight:2 ),
          a!gridLayoutColumnConfig(width: "ICON")
        },
        /*
        * a!forEach() will take local!employee data and used that data to loop through an
        * expression that creates each row. 
        *
        * When modifying the recipe to work with your data, you only need to change:
        * 1.) the number of fields in each row
        * 2.) the types of fields for each column (i.e. a!textField() for text data elements)
        * 3.) the fv!item elements. For example fv!item.firstName would change to fv!item.yourdata
        */
        rows: a!forEach(
          items: local!employees,
          expression: a!gridRowLayout(
            contents: {
              /* For the First Name Column*/
              a!textField(
                /* Labels are not visible in grid cells but are necessary to meet accessibility requirements */
                label: "first name " & fv!index,
                value: fv!item.firstName,
                saveInto: fv!item.firstName,
                required: true
              ),
              /* For the Last Name Column*/
              a!textField(
                label: "last name " & fv!index,
                value: fv!item.lastName,
                saveInto: fv!item.lastName,
                required:true
              ),
              /* For the Department Column*/
              a!dropdownField(
                label: "department " & fv!index,
                placeholderLabel: "-- Select -- ",
                choiceLabels: { "Corporate", "Engineering", "Finance", "Human Resources", "Professional Services", "Sales" },
                choiceValues: { "Corporate", "Engineering", "Finance", "Human Resources", "Professional Services", "Sales" },
                value: fv!item.department,
                saveInto: fv!item.department,
                required:true
              ),
              /* For the Title Column*/
              a!textField(
                label: "title " & fv!index,
                value: fv!item.title,
                saveInto: fv!item.title,
                required:true
              ),
              /* For the Phone Number Column*/
              a!textField(
                label: "phone number " & fv!index,
                placeholder:"555-456-7890",
                value: fv!item.phoneNumber,
                saveInto: fv!item.phoneNumber
              ),
              /* For the Start Date Column*/
              a!dateField(
                label: "start date " & fv!index,
                value: fv!item.startDate,
                saveInto: fv!item.startDate,
                required:true,
                align: "RIGHT"
              ),
              /* For the Removal Column*/
              a!imageField(
                label: "delete " & fv!index,
                images: a!documentImage(
                  document: a!iconIndicator("REMOVE"),
                  altText: "Remove Employee",
                  caption: "Remove " & fv!item.firstName & " " & fv!item.lastName,
                  link: a!dynamicLink(
                    value: fv!index,
                    saveInto: {
                      a!save(local!employees, remove(local!employees, save!value))
                    }
                  )
                ),
                size: "ICON"
              )
            },
            id: fv!index
          )
        ),
        addRowlink: a!dynamicLink(
          label: "Add Employee",
          /*
           * For your use case, set the value to a blank instance of your CDT using
           * the type constructor, e.g. type!Employee(). Only specify the field
           * if you want to give it a default value e.g. startDate: today()+1.
           */
          value: {
            startDate: today() + 1
          },
          saveInto: {
            a!save(local!employees, append(local!employees, save!value))
          }
        )
      )
    },
    buttons: a!buttonLayout(
      primaryButtons: a!buttonWidgetSubmit(
        label: "Submit"
      )
    )
  )
)

Test it out

  1. Edit values in an existing row.
  2. Click the Add Employee link to insert a new row into the editable grid. Add values to that row.
  3. Delete a row by clicking on the red X at the end of a row. Notice the remaining rows will adjust automatically.

Notable implementation details

  • The component in each cell has a label, but the label isn't displayed. Labels and instructions aren't rendered within a grid cell. It is useful to enter a label for expression readability, and to help identify which cell has an expression error since the label is displayed in the error message. Labels are also necessary to meet accessibility requirements, so that a screen reader can properly parse the grid.
  • To keep track of which items are removed so that you can execute the Delete from Data Store Entity smart service for the removed items, see the Track Adds and Deletes in an Inline Editable Grid recipe.
FEEDBACK