Expand/Collapse Rows in a Tree Grid

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

Create a grid that shows hierarchical data and allows users to dynamically expand and collapse rows within 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.

This scenario demonstrates:

  • How to use the rich text display component inside an editable grid to create a tree grid that users can dynamically expand/collapse to show/hide rows of data.

Setup

For this recipe, you'll need two Data Store Entities that are populated with data:

  1. Create a custom data type called PurchaseRequest with the following fields:
    • id (Number (Integer))
    • summary (Text)
  2. Designate the id field as the primary key and set to generate value.
  3. Save and publish the CDT.
  4. Create a custom data type called PurchaseRequestItem with the following fields:
    • id (Number (Integer))
    • summary (Text)
    • qty (Number (Integer))
    • unitPrice (Number (Decimal))
    • purchaseRequest (PurchaseRequest)
  5. Designate the id field as the primary key and set to generate value.
  6. Save and publish the CDT.
  7. Create a Data Store called "Purchase Request" with two entities, one of each data type that was just created:
    • PurchaseRequests (PurchaseRequest)
    • PurchaseRequestItems (PurchaseRequestItem)
  8. Insert the following values into PurchaseRequest:

    id summary
    1 PR 1
    2 PR 2
  9. Insert the following values into PurchaseRequestItem:

    id summary qty unitPrice purchaseRequest.id
    1 Item 1 2 10 1
    2 Item 2 3 50 1
    3 Item 3 1 100 1
    4 Item 4 3 75 2
    5 Item 5 10 25 2

Now that we have the data, let's create a couple of supporting constants:

  • PR_ENTITY: A constant of type Data Store Entity with value PurchaseRequests.
  • PR_ITEM_ENTITY: A constant of type Data Store Entity with value PurchaseRequestItems.

Now that we have created all of the supporting objects, let's move on to the main expression.

Expression

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
=load(
  local!prs: a!queryEntity(
    entity: cons!PR_ENTITY,
    query: a!query(
      /* To return all fields, leave the selection parameter blank. `*/
      /*`If you are not displaying all fields, use the selection    `*/
      /*` parameter to only return the necessary fields              */
      pagingInfo: a!pagingInfo(startIndex: 1, batchSize: -1)
    )
  ).data,
  local!items: a!queryEntity(
    entity: cons!PR_ITEM_ENTITY,
    query: a!query(
      pagingInfo: a!pagingInfo(startIndex: 1, batchSize: -1)
    )
  ).data,
  a!gridLayout(
    headerCells: {
      a!gridLayoutHeaderCell(label: "Summary"),
      a!gridLayoutHeaderCell(label: "Qty", align: "RIGHT"),
      a!gridLayoutHeaderCell(label: "Unit Price", align: "RIGHT"),
      a!gridLayoutHeaderCell(label: "Total Price", align: "RIGHT")
    },
    columnConfigs: {
      a!gridLayoutColumnConfig(width: "DISTRIBUTE", weight: 4),
      a!gridLayoutColumnConfig(width: "DISTRIBUTE"),
      a!gridLayoutColumnConfig(width: "DISTRIBUTE", weight: 2),
      a!gridLayoutColumnConfig(width: "DISTRIBUTE", weight: 2)
    },
    rowHeader: 1,
    rows: a!forEach(
      items: local!prs,
      expression: load(
        local!expanded: false,
        with(
          local!itemsForPr: index(
            local!items,
            /* Must cast to integer because a!queryEntity() returns a dictionary */
            wherecontains(tointeger(fv!item.id), local!items.purchaseRequest.id), 
            {}
          ),
          local!totalPrice: sum(
            a!forEach(
              items: local!itemsForPr, 
              expression: 
                tointeger(fv!item.qty) * todecimal(fv!item.unitPrice)
            )
          ),
          {
            a!gridRowLayout(
              contents: {
                a!richTextDisplayField(
                  label: "Summary " & fv!index,
                  value: {
                    if(
                      length(local!itemsForPr)=0,
                      fv!item.summary,
                      a!richTextItem(
                        text: if(local!expanded, "-", "+") &" "& fv!item.summary,
                        link: a!dynamicLink(
                          value: not(local!expanded),
                          saveInto: local!expanded
                        )
                      )
                    )
                  }
                ),
                a!textField(
                  label: "Qty " & fv!index,
                  readOnly: true
                ),
                a!textField(
                  label: "Unit Price " & fv!index,
                  readOnly: true
                ),
                a!textField(
                  label: "Total Price " & fv!index,
                  value: dollar(local!totalPrice),
                  readOnly: true,
                  align: "RIGHT"
                )
              }
            ),
            if(
              local!expanded,
              a!forEach(
                items: local!itemsForPr,
                expression: a!gridRowLayout(contents: {
                  a!richTextDisplayField(
                    label: "Item Summary " & fv!index,
                    value: a!richTextBulletedList(
                      items: fv!item.summary
                    )
                  ),
                  a!integerField(
                    label: "Item Qty " & fv!index,
                    value: fv!item.qty,
                    readOnly: true,
                    align: "RIGHT"
                  ),
                  a!textField(
                    label: "Item Unit Price " & fv!index,
                    value: dollar(fv!item.unitPrice),
                    readOnly: true,
                    align: "RIGHT"
                  ),
                  a!textField(
                    label: "Item Total Price " & fv!index,
                    value: dollar(tointeger(fv!item.qty) * todecimal(fv!item.unitPrice)),
                    readOnly: true,
                    align: "RIGHT"
                  )
                })
              ),
              {}
            )
          }
        )
      )
    )
  )
)

Test it out

  1. Click on "+ PR 1" in the "Summary" column to expand to show the item rows corresponding to PR 1.
  2. Click on "- PR 1" to hide the item rows for PR 1 again.
  3. The same can be done for PR 2.

Notable implementation details

  • Notice that we used a rich text display component to create a dynamic link used to expand and collapse the item rows for each purchase request. Alternatively, we could have used a link component containing the same dynamic link. The rich text display component would be useful here if a rich text style (e.g. underline) needed to be applied to the purchase request summary or if the summary needed to be a combination of links and normal text.
  • The bullet appearing in front of each item summary is made possible by using a rich text bulleted list within a rich text display component. See also: Rich Text
  • We left the selection parameter blank in our a!query()function because we wanted to return all fields of the entities that we were querying.
FEEDBACK