Use Links in a Grid to Show More Details and Edit Data

Interface patterns give you an opportunity to explore different interface designs. Be sure to check out How to Adapt a Pattern for Your Application.

Goal

Allow end users to click a link in a read-only grid to view the details for the row, and make changes to the data. The data available for editing may include more fields than are displayed in the 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.

images/SAIL_Recipe_Link_In_Grid_to_Edit_Data1.png

This scenario demonstrates:

  • How to use links in a grid that conditionally display other interface components.
  • How to allow editable fields to update the individual fields of the a data set.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
a!localVariables(
  /* We load the employee data into this variable. If you are populating
     this variable with a query, you would put .data at the end before passing
     it to the grid. */
  local!employees: {
    {id: 11, name: "Elizabeth Ward",  dept: "Engineering",     role: "Senior Engineer",      team: "Front-End Components",     pto: 15, startDate: today()-500},
    {id: 22, name: "Michael Johnson", dept: "Finance",         role: "Payroll Manager",      team: "Accounts Payable",         pto: 2,  startDate: today()-100},
    {id: 33, name: "John Smith",      dept: "Engineering",     role: "Quality Engineer",     team: "User Acceptance Testing",  pto: 5,  startDate: today()-1000},
    {id: 44, name: "Diana Hellstrom", dept: "Engineering",     role: "UX Designer",          team: "User Experience",          pto: 49, startDate: today()-1200},
    {id: 55, name: "Francois Morin",  dept: "Sales",           role: "Account Executive",    team: "Commercial North America", pto: 15, startDate: today()-700},
    {id: 66, name: "Maya Kapoor",     dept: "Sales",           role: "Regional Director",    team: "Front-End Components",     pto: 15, startDate: today()-1400},
    {id: 77, name: "Anthony Wu",      dept: "Human Resources", role: "Benefits Coordinator", team: "Accounts Payable",         pto: 2,  startDate: today()-300}
  },
  /* local!teamList would normally come from a constant or data source. */
  local!teamList: {
    "Accounts Payable",
    "User Acceptance Testing",
    "User Experience",
    "Commercial North America",
    "Front-End Components"
  },
  /* This variable is for storing the grid's selection. */
  local!selection,
  /* This variable is used to for the full row of data on the selected item
     to be passed to the details section of the interface. */
  local!selectedEmployee,
  local!readOnly: true,
  {
    a!columnsLayout(
      columns: {
        a!columnLayout(
          contents: {
            a!sectionLayout(
              label: "Employees",
              contents: {
                a!gridField(
                  data: local!employees,
                  columns: {
                    a!gridColumn(
                      label: "Name",
                      value: fv!row.name
                    ),
                    a!gridColumn(
                      label: "Team",
                      value: fv!row.team
                    )
                  },
                  pageSize: 10,
                  selectable: true,
                  selectionStyle: "ROW_HIGHLIGHT",
                  selectionValue: local!selection,
                  selectionSaveInto: {
                    /* Using the index function to return the last-selected item ensures that
                       only one item will be selected at a time, regardless of how fast the user
                       clicks. */
                    a!save(local!selectedEmployee, index(fv!selectedRows, length(fv!selectedRows), null)),
                    /* We use the same method as above to limit the selection variable. */
                    a!save(local!selection, index(save!value, length(save!value), null))
                  },
                  shadeAlternateRows: false,
                  rowHeader: 1
                )
              }
            )
          }
        ),
        a!columnLayout(
          contents: {
            a!sectionLayout(
              label: "Details",
              contents: {
                a!richTextDisplayField(
                  value: a!richTextItem(
                    text: "No employee selected.",
                    color: "SECONDARY",
                    size: "MEDIUM",
                    style: "EMPHASIS"
                  ),
                  showWhen: isnull(local!selection)
                ),
                a!columnsLayout(
                  columns: {
                    a!columnLayout(
                      contents: {
                        a!textField(
                          label: "Name",
                          value: local!selectedEmployee.name,
                          readOnly: true
                        ),
                        a!textField(
                          label: "Department",
                          value: local!selectedEmployee.dept,
                          readOnly: true
                        )
                      },
                      width: "MEDIUM"
                    ),
                    a!columnLayout(
                      contents: {
                        /* In the following fields, we display from, and save to
                           local!selectedEmployee. */
                        a!textField(
                          label: "Role",
                          value: local!selectedEmployee.role,
                          saveInto: local!selectedEmployee.role,
                          readOnly: local!readOnly
                        ),
                        /* Because dropdown components can't be readOnly, we use a textField to
                           display the value and an if() statement to swap it out for the dropdown
                           when it's time to edit. */
                        if(
                          local!readOnly,
                          a!textField(
                            label: "Team",
                            value: local!selectedEmployee.team,
                            readOnly: true
                          ),
                          a!dropdownField(
                            label: "Team",
                            choiceLabels: local!teamList,
                            choiceValues: local!teamList,
                            value: local!selectedEmployee.team,
                            saveInto: local!selectedEmployee.team,
                            disabled: local!readOnly
                          )
                        ),
                        /* The link enables editing in the other components, and is hidden when
                           editing is enabled. */
                        a!linkField(
                          labelPosition: "COLLAPSED",
                          links: a!dynamicLink(
                            label: "Reassign",
                            value: false,
                            saveInto: local!readOnly
                          ),
                          showWhen: local!readOnly
                        )
                      },
                      width: "WIDE"
                    )
                  },
                  showWhen: not(isnull(local!selection))
                )
              }
            ),
            a!buttonLayout(
              primaryButtons: {
                a!buttonWidget(
                  label: "Update",
                  value: local!selectedEmployee,
                  saveInto: {
                    /* When the user clicks UPDATE, we use the updatearray() function to update
                       local!employees with the new values in local!selectedEmployee. */
                    a!save(
                      local!employees,
                      updatearray(
                        local!employees,
                        where(tointeger(local!employees.id)=local!selectedEmployee.id),
                        local!selectedEmployee
                      )
                    ),
                    a!save(local!readOnly, true)
                  },
                  style: "PRIMARY"
                )
              },
              secondaryButtons: {
                a!buttonWidget(
                  label: "Cancel",
                  value: true,
                  saveInto: {
                    local!readOnly,
                    /* If the user has made any changes to local!selectedEmployee, when they click
                       CANCEL, we need to reset local!selectedEmployee to previous values in local!employees
                       in order to keep the selectedEmployee details true. If you don't want to persist the
                       selection, you can simply reset the selections instead with:
                       a!save(local!selectedEmployee, null), a!save(local!selection, null) */
                    a!save(
                      local!selectedEmployee,
                      cast(253, /* Casting the value to "list of map" (253) simplifies subsequent interactions. */
                        index(
                          local!employees,
                          where(tointeger(local!employees.id)=local!selectedEmployee.id),
                          null
                        )
                      )
                    )
                  },
                  submit: true,
                  style: "NORMAL",
                  validate: false
                )
              },
              showWhen: not(local!readOnly)
            )
          }
        )
      }
    ),
    /* The following test components just display the values of local!selectedEmployee and local!employees
       so you can see you the changes being made in realtime. */
    a!textField(
      label: "local!selectedEmployee",
      labelPosition: "ABOVE",
      instructions: {typename(typeof(local!selectedEmployee)),typeof(local!selectedEmployee)},
      value: local!selectedEmployee,
      refreshAfter: "UNFOCUS",
      readonly: true
    ),
    a!textField(
      label: "local!employees",
      labelPosition: "ABOVE",
      instructions: {typename(typeof(local!employees)),typeof(local!employees)},
      value: local!employees,
      refreshAfter: "UNFOCUS",
      readonly: true
    )
  }
)

Test it out

  1. Click on an employee's name in the grid's left column. Notice that an editable section appears.
  2. Change the employee's Team and/or Role and click the Update button.
  3. Click on an employee's name, change the department and click Cancel. Notice that the employee's department has not changed.
Open in Github Built: Fri, Mar 11, 2022 (04:59:07 PM)
FEEDBACK