Master Detail

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

The drilldown pattern allows users to select an item from a grid to see more details next to the grid. This page explains how you can use this pattern in your interface, and walks through the design structure in detail.

Use a master-detail pattern to show a list of items and allow users to select an item to see more of its details alongside the list.

Arrange the list and details as two columns.

Choose the "Row Highlight" selection style for the grid. This will allow users to click anywhere on a row to select it. This style also highlights the selected row.

Design Structure

The main components in this pattern are a Read-Only Grid and a set of text display fields that are alternately visible depending on whether local!selectedEmployee is null.

For this pattern, a small set of items is used. You should use a relatively smaller batch size, such as 10 items, so that users don't have to scroll down to make a selection in the grid and scroll back up to see the details.

Pattern Expression

This pattern introduces a 131-line expression to the interface.

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
{
  a!localVariables(
    local!employees: {
      {id: 1, name: "Elizabeth Ward",  dept: "Engineering",     role: "Senior Engineer",      team: "Front-End Components",     pto: 15, startDate: today()-500},
      {id: 2, name: "Michael Johnson", dept: "Finance",         role: "Payroll Manager",      team: "Accounts Payable",         pto: 2,  startDate: today()-100},
      {id: 3, name: "John Smith",      dept: "Engineering",     role: "Quality Engineer",     team: "User Acceptance Testing",  pto: 5,  startDate: today()-1000},
      {id: 4, name: "Diana Hellstrom", dept: "Engineering",     role: "UX Designer",          team: "User Experience",          pto: 49, startDate: today()-1200},
      {id: 5, name: "Francois Morin",  dept: "Sales",           role: "Account Executive",    team: "Commercial North America", pto: 15, startDate: today()-700},
      {id: 6, name: "Maya Kapoor",     dept: "Sales",           role: "Regional Director",    team: "Front-End Components",     pto: 15, startDate: today()-1400},
      {id: 7, name: "Anthony Wu",      dept: "Human Resources", role: "Benefits Coordinator", team: "Accounts Payable",         pto: 2,  startDate: today()-300}
    },
    /* This variable is used to pass the full row of data on the selected item to the part of the interface showing the details of the selected item. */
    /* Here we are pre-selecting a row by indexing into the sample data; however, the data for the pre-selected row would typically be passed in as a *
     * rule input or generated with a query.                                                                                                          */
    local!selectedEmployee: local!employees[4],
    {
      a!columnsLayout(
        columns: {
          a!columnLayout(
            contents: {
              a!sectionLayout(
                label: "Employees",
                contents: {
                  a!gridField(
                    /* Replace the dummy data with a query, rule, or function that returns a datasubset and uses fv!pagingInfo as the paging configuration. */
                    data: todatasubset(
                      local!employees,
                      fv!pagingInfo
                    ),
                    columns: {
                      a!gridColumn(
                        label: "Name",
                        value: fv!row.name
                      ),
                      a!gridColumn(
                        label: "Department",
                        value: fv!row.dept
                      )
                    },
                    pageSize: 7,
                    selectable: true,
                    selectionStyle: "ROW_HIGHLIGHT",
                    selectionValue: index(local!selectedEmployee, "id", {}),
                    selectionSaveInto: {
                      /* This save replaces the value of the previously selected item with that of the newly selected item, ensuring only one item can be selected at once.*/
                      a!save(
                        local!selectedEmployee,
                        if(
                          length(fv!selectedRows) > 0,
                          fv!selectedRows[length(fv!selectedRows)],
                          null
                        )
                      )
                    },
                    shadeAlternateRows: false,
                    rowHeader: 1
                  )
                }
              )
            }
          ),
          a!columnLayout(
            contents: {
              a!sectionLayout(
                label: "Employee Details",
                contents: {
                  a!richTextDisplayField(
                    value: a!richTextItem(
                      text: "No employee selected.",
                      color: "SECONDARY",
                      size: "MEDIUM",
                      style: "EMPHASIS"
                    ),
                    showWhen: isnull(local!selectedEmployee)
                  ),
                  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
                          )
                        }
                      ),
                      a!columnLayout(
                        contents: {
                          a!textField(
                            label: "Role",
                            value: local!selectedEmployee.role,
                            readOnly: true
                          ),
                          a!textField(
                            label: "Start Date",
                            value: text(local!selectedEmployee.startDate, "MMM dd, yyyy"),
                            readOnly: true
                          )
                        }
                      ),
                      a!columnLayout(
                        contents: {
                          a!textField(
                            label: "Team",
                            value: local!selectedEmployee.team,
                            readOnly: true
                          ),
                          a!textField(
                            label: "Available PTO",
                            value: local!selectedEmployee.pto & " days",
                            readOnly: true
                          )
                        }
                      )
                    },
                    showWhen: not(isnull(local!selectedEmployee))
                  )
                }
              )
            }
          )
        }
      )
    }
  )
}

[Line 1-16] Set Local Variables

Since this pattern displays the details in components outside the grid, we assign the data in a local variable (local!employees) so all components have access to it. However, in this patricular case, this pattern would work even if the data was defined directly in the data parameter of the grid because the detail view is only reading the value of local!selectedEmployee, but the initial selected value wouldn't be possible.

The local!selectedEmployee variable holds the currently-selected row. To avoid showing an empty details view, it is recommended that the grid have an initial selectioned value. In this case, we set that to the forth item (line 15), but if you weren't passing that initial value from an input, you would want to set it to the first ([1]).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  a!localVariables(
    local!employees: {
      {id: 1, name: "Elizabeth Ward",  dept: "Engineering",     role: "Senior Engineer",      team: "Front-End Components",     pto: 15, startDate: today()-500},
      {id: 2, name: "Michael Johnson", dept: "Finance",         role: "Payroll Manager",      team: "Accounts Payable",         pto: 2,  startDate: today()-100},
      {id: 3, name: "John Smith",      dept: "Engineering",     role: "Quality Engineer",     team: "User Acceptance Testing",  pto: 5,  startDate: today()-1000},
      {id: 4, name: "Diana Hellstrom", dept: "Engineering",     role: "UX Designer",          team: "User Experience",          pto: 49, startDate: today()-1200},
      {id: 5, name: "Francois Morin",  dept: "Sales",           role: "Account Executive",    team: "Commercial North America", pto: 15, startDate: today()-700},
      {id: 6, name: "Maya Kapoor",     dept: "Sales",           role: "Regional Director",    team: "Front-End Components",     pto: 15, startDate: today()-1400},
      {id: 7, name: "Anthony Wu",      dept: "Human Resources", role: "Benefits Coordinator", team: "Accounts Payable",         pto: 2,  startDate: today()-300}
    },
    /* This variable is used to pass the full row of data on the selected item to the part of the interface showing the details of the selected item. */
    /* Here we are pre-selecting a row by indexing into the sample data; however, the data for the pre-selected row would typically be passed in as a *
     * rule input or generated with a query.                                                                                                          */
    local!selectedEmployee: `local!employees[4]`,
    {

[Line 17-61] Grid with Limited Selection

The first column contains the grid, whose selectionValue is set to the ID of the selected employee (line 43). Selection can only be an index integer, and because the grid data is a datasubset, the employee ID is also the selection identifier. You could simply set selectionValue: local!selectedEmployee.id and this pattern would work, but we use index() function because it's a great way to handle situations where the initial selection value isn't set and isn't null, which may be a common scenario.

Limiting the selection to a single row is done by only returning the last-selected value from fv!selectedRows (lines 44-54). This is necessary because certain network conditions could allow users to click faster than the interface can reevaluate, resulting in fv!selectedRows containing more than one row. To ensure that only the last selected row is returned, we use the legth() function to return the size of the array, which will also correspond to the index of the last item in the array: fv!selectedRows[length(fv!selectedRows)] (line 50). You can also use the index() function here to the same effect: selectionSaveInto: a!save(local!selectedEmployee, index(fv!selectedRows, length(fv!selectedRows), null)).

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
      a!columnsLayout(
        columns: {
          a!columnLayout(
            contents: {
              a!sectionLayout(
                label: "Employees",
                contents: {
                  a!gridField(
                    /* Replace the dummy data with a query, rule, or function that returns a datasubset and uses fv!pagingInfo as the paging configuration. */
                    data: todatasubset(
                      local!employees,
                      fv!pagingInfo
                    ),
                    columns: {
                      a!gridColumn(
                        label: "Name",
                        value: fv!row.name
                      ),
                      a!gridColumn(
                        label: "Department",
                        value: fv!row.dept
                      )
                    },
                    pageSize: 7,
                    selectable: true,
                    selectionStyle: "ROW_HIGHLIGHT",
                    `selectionValue:` index(local!selectedEmployee, "id", {}),
                    `selectionSaveInto:` {
                      /* This save replaces the value of the previously selected item with that of the newly selected item, ensuring only one item can be selected at once.*/
                      a!save(
                        local!selectedEmployee,
                        if(
                          length(fv!selectedRows) > 0,
                          `fv!selectedRows[length(fv!selectedRows)]`,
                          null
                        )
                      )
                    },
                    shadeAlternateRows: false,
                    rowHeader: 1
                  )
                }
              )
            }
          ),

[Line 62-131] Detail View

For when no row is selected, we use a rich text field to note that no employee is selected (line 67). This is recommended to make the functionality of the grid apparent to users. The text fields that display the selected employee information all pull from local!selectedEmployee.

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
          a!columnLayout(
            contents: {
              a!sectionLayout(
                label: "Employee Details",
                contents: {
                  `a!richTextDisplayField`(
                    value: a!richTextItem(
                      text: "No employee selected.",
                      color: "SECONDARY",
                      size: "MEDIUM",
                      style: "EMPHASIS"
                    ),
                    showWhen: isnull(local!selectedEmployee)
                  ),
                  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
                          )
                        }
                      ),
                      a!columnLayout(
                        contents: {
                          a!textField(
                            label: "Role",
                            value: local!selectedEmployee.role,
                            readOnly: true
                          ),
                          a!textField(
                            label: "Start Date",
                            value: text(local!selectedEmployee.startDate, "MMM dd, yyyy"),
                            readOnly: true
                          )
                        }
                      ),
                      a!columnLayout(
                        contents: {
                          a!textField(
                            label: "Team",
                            value: local!selectedEmployee.team,
                            readOnly: true
                          ),
                          a!textField(
                            label: "Available PTO",
                            value: local!selectedEmployee.pto & " days",
                            readOnly: true
                          )
                        }
                      )
                    },
                    showWhen: not(isnull(local!selectedEmployee))
                  )
                }
              )
            }
          )
        }
      )
    }
  )
}
FEEDBACK