The DetailsList code component allows using of the Fluent UI DetailsList component from inside canvas apps and custom pages:
- Can be bound to a Dataverse dataset or local collection.
- Supports configurable columns separate to the column metadata provided by the source dataset for flexibility.
- Cell types for links, icons, expand/collapse, and sub text cells.
- Support for paging.
- Support for sorting either using Dataverse sorting or configurable
SortBy
properties.
When configured against a Dataverse connection it can look like the following:
The DetailsList component has the following properties:
Records
- The dataset that contains the rows to render:RecordKey
(optional) - The unique key column name. Provide this if you want the selection to be preserved when the Records are updated, and when you want theEventRowKey
to contain the id instead of the row index after theOnChange
event is fired.RecordCanSelect
(optional) - The column name that contains aboolean
value defining if a row can be selected.RecordSelected
(optional) - The column name that contains aboolean
value defining if a row is selected by default and when setting theInputEvent
to containSetSelection
. See the section onSet Selection
below.
Columns
(Optional) - The dataset that contains option metadata for the columns. If this dataset is provided, it will completely replace the columns provided in the Records dataset.ColDisplayName
(Required) - Provides the name of the column to show in the header.ColName
(Required) - Provides the actual field name of the column in the Items collection.ColWidth
(Required) - Provides the absolute fixed width of the column in pixels.ColCellType
- The type of cell to render. Possible values:expand
,tag
,indicatortag
,image
,clickableimage
,link
. See below for more information.ColHorizontalAlign
- The alignment of the cell content if theColCellType
is of typeimage
, orclickableimage
.ColVerticalAlign
- The alignment of the cell content if theColCellType
is of typeimage
, orclickableimage
.ColMultiLine
- True when the text in the cells text should wrap if too long to fit the available width.ColResizable
- True when the column header width should be resizable.ColSortable
- True when the column should show be sortable. If the dataset supports automatic sorting via a direct Dataverse connection, the data will automatically be sorted. Otherwise, theSortEventColumn
andSortEventDirection
outputs will be set and must be used in the records Power FX binding expression.ColSortBy
- The name of the column to provide to theOnChange
event when the column is sorted. For example, if you are sorting date columns, you want to sort on the actual date value rather than the formatted text shown in the column.ColIsBold
- True when the data cell data should be boldColTagColorColumn
- If the cell type is tag, set to the hex background color of the text tag. Can be set totransparent
. If the cell type is a not a tag, set to a hex color to use as an indicator circle tag cell. If the text value is empty, the tag is not shown.ColTagBorderColorColumn
- Set to a hex color to use as the border color of a text tag. Can be set totransparent
.ColHeaderPaddingLeft
- Adds padding to the column header text (pixels)ColShowAsSubTextOf
- Setting this to the name of another column will move the column to be a child of that column. See below under Sub Text columns.ColPaddingLeft
- Adds padding to the left of the child cell (pixels)ColPaddingTop
- Adds padding to the top of the child cell (pixels)ColLabelAbove
- Moves the label above the child cell value if it is shown as a Sub Text column.ColMultiValueDelimiter
- Joins multi value array values together with this delimiter. See below under multi-valued columns.ColFirstMultiValueBold
- When showing a multi-valued array value, the first item is shown as bold.ColInlineLabel
- If set to a string value, then this is used to show a label inside the cell value that could be different to the column name. E.g.
ColHideWhenBlank
- When true, any cell inline label & padding will be hidden if the cell value is blank.ColSubTextRow
- When showing multiple cells on a sub text cell, set to the row index. Zero indicates the main cell content row.ColAriaTextColumn
- The column that contains the aria description for cells (e.g. icon cells).ColCellActionDisabledColumn
- The column that contains a boolean flag to control if a cell action (e.g. icon cells) is disabled.ColImageWidth
- The icon/image size in pixels.ColImagePadding
- The padding around an icon/image cell.ColRowHeader
- Defines a column to render larger than the other cells (14px rather than 12px). There normally would only be a single Row Header per column set.
SelectionType
- Selection Type (None, Single, Multiple)PageSize
- Defines how many records to load per page.PageNumber
- Outputs the current page shown.HasNextPage
- Outputs true if there is a next page.HasPreviousPage
- Outputs true if there is a previous page.TotalRecords
- Outputs the total number of records available.CurrentSortColumn
- The name of the column to show as currently used for sortingCurrentSortDirection
- The direction of the current sort column being usedAccessibilityLabel
- The label to add to the table aria descriptionRaiseOnRowSelectionChangeEvent
- TheOnChange
event is raised when a row is selected/unselected. (see below)InputEvent
- One or more input events (that can be combined together using string concatenation). Possible valuesSetFocus
,SetFocusOnRow
,SetFocusOnHeader
,ClearSelection
,SetSelection
. Must be followed by random string element to ensure the event is triggered. Events can be combined e.g.SetFocusClearSelection
will clear and set the focus at the same time.SetFocusOnRowSetSelection
will set focus on a row and set the selection at the same time.EventName
- Output Event whenOnChange
is triggered. Possible values -Sort
,CellAction
,OnRowSelectionChange
EventColumn
- Output Event column field name used whenCellAction
is invokedEventRowKey
- Output Event column that holds either the index of the row that the event was invoked on, or the Row Key if theRecordKey
property is set.SortEventColumn
- The name of the column that triggered the SortOnChange
eventSortEventDirection
- The direction of the sort that triggered the SortOnChange
eventTheme
- The Fluent UI Theme JSON to use that is generated and exported from Fluent UI Theme Designer.Compact
- True when the compact style should be usedAlternateRowColor
- The hex value of the row color to use on alternate rows.SelectionAlwaysVisible
- Should the selection radio buttons always be visible rather than only on row hover.
The ColShowAsSubTextOf
column property defines a column as being shown below the value in another column. This can be used to show secondary information and expandable content (see below).
If you had a collection defined as:
ClearCollect(colAccounts,
{id:"1",name:"Contoso",city:"Redmond",country:"U.S.",description:"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",expand:false},
{id:"2",name:"Litware, Inc",city:"Dallas",country:"U.S.",description:"Donec vel pellentesque turpis.",expand:false});
You could define the columns to be:
Table(
{
ColName: "name",
ColDisplayName: "Account Name",
ColWidth: Self.Width-100,
ColIsBold:true
},
{
ColName: "city",
ColDisplayName: "City:",
ColShowAsSubTextOf:"name",
ColLabelAbove:false,
ColIsBold:true,
ColSubTextRow: 1
},
{
ColName: "country",
ColDisplayName: "Country:",
ColShowAsSubTextOf:"name",
ColLabelAbove:false,
ColIsBold:true,
ColSubTextRow: 1
},
{
ColName: "description",
ColDisplayName: "",
ColShowAsSubTextOf:"name",
ColLabelAbove:false,
ColIsBold:false,
ColSubTextRow: 2
}
)
The result will be a table that looks like the following:
The ColCellType
column property accepts the following values: expand
, tag
, image
, indicatortag
, clickableimage
, link
If the 'Sub Text' rows requires to have an expand/collapse icon, an additional column can be added to the column dataset and the column definition ColCellType
set expand
also:
{ColName:"expand",ColDisplayName:"",ColWidth:32,ColResponsive:false, ColRightAligned:true, ColCellType:"expand"}
Assuming that a RecordKey
property is set to the index
column, the OnChange
event could then contain the following to expand/collapse the rows:
If(Self.EventName="CellAction" && Self.EventColumn="expand",
With(LookUp(colExpand,index=Self.EventRowKey) As Row,
Patch(colExpand,Row,{expand:!Row.expand})
)
);
This searches for the row that has had the Cell Action invoked on using the index (if no RecordKey
is set, then the EventRowKey
will contain the row number), and then toggles the expand value.
This will give the following result:
Using a cell type of tag or indicatortag you can create inline colored tags to display the cell content.
tag
- This will show a simple tag box with a colored background and bordertagindicator
- Shows a tag box with a colored circle indicator
The colors can be varied by row, and so the column metadata dataset simply provides the name of the columns that holds the colors for the tags.
Consider the dataset:
ClearCollect(
colAccounts,
{
name: "Contoso",
city: "Redmond",
country: "U.S.",
TagColor: "rgb(0, 183, 195)",
TagBorderColor: "rgb(0,137,147)"
},
{
name: "Litware, Inc",
city: "Dallas",
country: "U.S.",
TagColor: "rgb(255, 140, 0)",
TagBorderColor: "rgb(194,107,0)"
}
);
You could then add the column metadata to add two columns, one displayed as a tag and the other as a tagindicator - each using the TagColor and TagBorderColor columns to determine the colors:
{
ColName: "country",
ColDisplayName: "Country",
ColCellType:"tag",
ColWidth: 60,
ColTagColorColumn: "TagColor",
ColTagBorderColorColumn: "TagBorderColor"
},
{
ColName: "country",
ColDisplayName: "Country",
ColCellType:"indicatortag",
ColWidth: 60,
ColTagColorColumn: "TagColor",
ColTagBorderColorColumn: "TagBorderColor"
}
This will give the following result:
Using a cell type of image
or clickableimage
, you can configure inline images that can optionally be selected to raise the OnChange
action.
The image content can be defined by prefixing with:
https:
A link to an external image. e.g. https://via.placeholder.com/100x70icon:
Using one of the Fluent UI icons e.g.icon:SkypeCircleCheck
data:
Using inline svg image data: e.g.data:image/svg+xml;utf8, %3Csvg%20%20viewBox%3D%270%200%2020...
If the image is of type clickableimage
the OnChange
event will fire when the icon is selected, with an EvenName
of CellAction
, EventColumn
providing the name of the image column and EventRowKey
being the RecordKey
of the row (if no RecordKey
is set, then the EventRowKey
will contain the row number).
E.g. Consider the row data:
{
id: "1",
name: "Contoso",
city: "Redmond",
country: "U.S.",
ImageColor: "rgb(0, 183, 195)",
externalimage: "https://via.placeholder.com/100x70",
iconimage: "icon:SkypeCircleCheck"
},
{
id: "2",
name: "Litware, Inc",
city: "Dallas",
country: "U.S.",
ImageColor: "rgb(255, 140, 0)",
externalimage: "https://via.placeholder.com/100x70",
iconimage: "icon:SkypeCircleCheck"
}
and the column metadata:
{
ColName: "externalimage",
ColDisplayName: "Image",
ColCellType:"image",
ColWidth: 60,
ColImageWidth: 60,
ColImagePadding: 8,
ColVerticalAlign: "Top"
},
{
ColName: "iconimage",
ColDisplayName: "Clickable Image",
ColCellType:"clickableimage",
ColWidth: 60,
}
For the clickableimage
column, OnChange
event can then handle when a user selects (mouse or keyboard) and icon (assuming that it is not disabled) using:
If(Self.EventName="CellAction",
Notify("CellAction " & Self.EventColumn & " " & Self.EventRowKey)
)
The EventRowKey
will be populated with the column value defined as the RecordKey
property.
Columns can be rendered as links, that will raise the OnChange event when the link is selected in a similar way to how clickable images work described above.
The column metadata for a link is configured as follows:
{
ColName: "name",
ColDisplayName: "Account Name",
ColWidth: 150,
ColIsBold:true,
ColCellType: "link"
}
This will result in the cell content being rendered as:
The OnChange
event is again fired when the link is clicked, with the EventColumn
being the name of the column that contains the link, and the EventRowKey
populated with the column value defined as the RecordKey
property.
A column is defined as being sortable by setting the ColSortable
property to true. If the column shows a text value that is different to the sort order required (e.g. a formatted date or status column), then a different sort column can be specified using the ColSortBy
property.
Sorting is then handled in two ways:
- Automatically when connected to a Dataverse data source.
- Manually when using collections.
When the Items dataset is a native Dataverse dataset, it will automatically sorted if a column is marked as sortable. If there any changes made to the shape of a Dataverse collection by using AddColumn
, or by storing the data in a collection, automatic sorting will no longer work, and manual sorting must be implemented.
Manual Sorting is supported outside of the component to allow for custom connector support and local collection sorting when not connected to a Dataverse connection. Columns can be defined as being sortable or not. When the column sort is selected, an OnChange
event is raised providing the column and direction. The app should then use these values to change the bound collection to the table to update with the sorted records.
-
In the Columns collection, add a sortable Boolean column
-
Add the name of the sortable column to the
Columns.ColSortable
property -
Inside the
OnChange
event of the Table, add the code:If(Self.EventName="Sort", UpdateContext({ ctxSortCol:Self.SortEventColumn, ctxSortAsc:If(Self.SortEventDirection='PowerCAT.FluentDetailsList.SortEventDirection'.Ascending,true,false) }) );
-
Set the property
Sort Column
to bectxSortCol
-
Set the property
Sort Direction
to be:If(ctxSortAsc, 'PowerCAT.FluentDetailsList.CurrentSortDirection'.Ascending, 'PowerCAT.FluentDetailsList.CurrentSortDirection'.Descending)
-
Set the input items collection to sort using the context variables set above:
SortByColumns(colData,ctxSortCol,If(ctxSortAsc,SortOrder.Ascending,SortOrder.Descending))
When the OnChange event is fired after the user clicks on the column header to change the sort, the sort context variables are updated, using the new sort information provided, which causes the input dataset to be re-sorted and the table is updated accordingly.
Paging is handled internally by the component, however the buttons to move back/forwards must be created by the hosting app, and events sent to the component.
The following properties are used to control paging:
PageSize
- Defines how many records to load per page.PageNumber
- Outputs the current page shown.HasNextPage
- Outputs true if there is a next page.HasPreviousPage
- Outputs true if there is a previous page.TotalRecords
- Outputs the total number of records available.
The paging buttons can then be defined as follows:
- Load First Page
OnSelect
:UpdateContext({ctxGridEvent:"LoadFirstPage" & Text(Rand())})
DisplayMode
:If(grid.HasPreviousPage,DisplayMode.Edit,DisplayMode.Disabled)
- Load Previous Page
OnSelect
:UpdateContext({ctxGridEvent:"LoadPreviousPage" & Text(Rand())})
DisplayMode
:If(grid.HasPreviousPage,DisplayMode.Edit,DisplayMode.Disabled)
- Load Next Page
OnSelect
:UpdateContext({ctxGridEvent:"LoadNextPage" & Text(Rand())})
DisplayMode
:If(grid.HasNextPage,DisplayMode.Edit,DisplayMode.Disabled)
The number of records label can be set to an expression similar to:
grid.TotalRecords & " record(s) " & Text(CountRows(grid.SelectedItems)+0) & " selected"
The InputEvent
property can be set to one or more of the following:
SetFocus
- Sets focus on the first row of the gridClearSelection
- Clears any selection, and sets back to the default selection.SetSelection
- Sets the selection as defined by theRowSelected
column.LoadNextPage
- Loads the next page if there is oneLoadPreviousPage
- Loads the previous page if there oneLoadFirstPage
- Loads the first page
To ensure that the input event is picked up, it must be sufficed with a random value. e.g. SetSelection" & Text(Rand())
See below for more details.
The component supports Single, Multiple or None selection modes.
When selecting items, the SelectedItems
and Selected
properties are updated.
SelectedItems
- If the table is in Multiple selection mode, this will contain one or more records from the Items collection.Selected
- If the table is in Single selection mode, this will contain the selected records.
When a user invokes the row action, either by double clicking or pressing enter or a selected row, the OnSelect
event is fired. The Selected
property will contain a reference to the record that has been invoked. This event can be used to show a detailed record or navigate to another screen.
If the RaiseOnRowSelectionChangeEvent
property is enabled, when the selected rows is changed, the OnChange
event is raised with the EventName
set to OnRowSelectionChange
. If the app needs to respond to a single row select rather than a row double click, the OnChange
can detect this using code similar to:
If(
Self.EventName = "OnRowSelectionChange",
If(!IsBlank(Self.EventRowKey),
// Row Selected
)
);
To clear the selected records, you must set the InputEvent
property to a string that starts with
E.g.
UpdateContext({ctxTableEvent:"ClearSelection"&Text(Rand())})
The context variable ctxTableEvent
can then be bound to the InputEvent
property.
If there is a scenario where a specific set of records should be programmatically selected, the InputEvent
property can be set to SetSelection
or SetFocusOnRowSetSelection
in combination with setting the RecordSelected
property on the record.
E.g. If you had a dataset as follows:
{RecordKey:1, RecordSelected:true, name:"Row1"}
To select and select the first row you can set the InputEvent
to be "SetFocusOnRowSetSelection"&Text(Rand())
or "SetSelection"&Text(Rand())
If a column value can has multiple values by setting it to a Table/Collection. This will then render the values as multiple cell values. E.g.:
{
id: "1",
name: "Contoso",
tags:["#PowerApps","#PowerPlatform"]
},
The column metadata then could be:
{
ColName: "tags",
ColDisplayName: "Tags",
ColWidth: 250,
ColFirstMultiValueBold :true,
ColMultiValueDelimiter:" "
}
This would result in the table showing:
The table can be programmatically set focus on (e.g. after a search or using a keyboard shortcut). To set focus on the first row, set the Input Event to a variable that contains "SetFocus" & Text(Rand())
.
A theme can be passed to the Theme
property. The following is an example of setting the theme based on the output from the Fluent UI Theme Designer (windows.net).
Set(varThemeBlue, {
palette: {
themePrimary: ColorValue("#0078d4"),
themeLighterAlt: ColorValue("#eff6fc"),
themeLighter: ColorValue("#deecf9"),
themeLight: ColorValue("#c7e0f4"),
themeTertiary: ColorValue("#71afe5"),
themeSecondary: ColorValue("#2b88d8"),
themeDarkAlt: ColorValue("#106ebe"),
themeDark: ColorValue("#005a9e"),
themeDarker: ColorValue("#004578"),
neutralLighterAlt: ColorValue("#faf9f8"),
neutralLighter: ColorValue("#f3f2f1"),
neutralLight: ColorValue("#edebe9"),
neutralQuaternaryAlt: ColorValue("#e1dfdd"),
neutralQuaternary: ColorValue("#d0d0d0"),
neutralTertiaryAlt: ColorValue("#c8c6c4"),
neutralTertiary: ColorValue("#a19f9d"),
neutralSecondary: ColorValue("#605e5c"),
neutralPrimaryAlt: ColorValue("#3b3a39"),
neutralPrimary:ColorValue( "#323130"),
neutralDark: ColorValue("#201f1e"),
black: ColorValue("#000000"),
white: ColorValue("#ffffff")
}});
Set(varThemeBlueJSON,"{""palette"":{
""themePrimary"": ""#0078d4"",
""themeLighterAlt"": ""#eff6fc"",
""themeLighter"": ""#deecf9"",
""themeLight"": ""#c7e0f4"",
""themeTertiary"": ""#71afe5"",
""themeSecondary"": ""#2b88d8"",
""themeDarkAlt"": ""#106ebe"",
""themeDark"": ""#005a9e"",
""themeDarker"": ""#004578"",
""neutralLighterAlt"": ""#faf9f8"",
""neutralLighter"": ""#f3f2f1"",
""neutralLight"": ""#edebe9"",
""neutralQuaternaryAlt"": ""#e1dfdd"",
""neutralQuaternary"": ""#d0d0d0"",
""neutralTertiaryAlt"": ""#c8c6c4"",
""neutralTertiary"": ""#a19f9d"",
""neutralSecondary"": ""#605e5c"",
""neutralPrimaryAlt"": ""#3b3a39"",
""neutralPrimary"": ""#323130"",
""neutralDark"": ""#201f1e"",
""black"": ""#000000"",
""white"": ""#ffffff""
}
}");
The Theme JSON string is passed to the component property, whilst the varTheme
can be used to style other standard components such as buttons using the individual colors.
Canvas apps (not custom pages) allow the maker to assign a tab index for components to control the tab order. Even if the tab index is set to zero, the accessibility manager assigns a positive tab index to standard controls. The DetailsList component does not allow setting positive tab indexes on the FocusZone
for headers and rows. A modified version of the DetailsList
and FocusZone
components is required to address this issue. This is not implemented at this time.
The result of this is if a user uses tab to move out of a details list, or tab into it from a control above, the tab order will be incorrect.
Note: this issue does not apply to custom pages since they force the tab index to zero.
When using the DetailsList for datasets that require a scrollbar, the headers are added to the Stick component to ensure they remain at the top of the grid. The Stick Header logic has a bug when it is interacting with the FocusZone
.
-
Select a row and use cursor keys so that the top most row is partially scrolled out of view so that the sticky header is showing.
-
Use cursor up keys to move up to the first row - notice that the row is not scrolled into view and partially obscured by the sticky header. This is because the scroll zone thinks that it is showing since it doesn't take into consideration the sticky header
-
Move up using the cursor keys again, notice how the focus is lost from the grid and the cursor key start scrolling the scrollable area.
This is because when the DetailsHeader
is inside a sticky header, the componentref
is set to null on unmount when it is swapped out to the floating version. When using keyup
to move focus to the header after scrolling down (inside DetailsList.onContentKeyDown
), the call to focus()
does not work because componentref.current
is null. To work around this, a modified version of initializeComponentRef
is used that does not null the ref on unmount.
When using Shift-Tab to move focus back to previous elements from the DetailsList
grid rows, the sticky header is reset and the focus moves to the top most document.
-
Move down using cursor keys when focused on the grid rows so that the sticky header shows
-
Press Shift-Tab - notice how the focus moves to the window
-
If you move up so that the sticky header is reset, Shift-Tab now correctly moves to the header focus
When moving down from header using down arrow, the first item will be automatically selected even if isSelectedOnFocus
is false.
-
Select the first column header
-
Press key down
-
Notice how the first item is selected, but
isSelectedOnFocus
is set to false on theselectionZoneProps
props.
There is no way using IDetailsList
to set focus on the grid when there are no rows. focusIndex
can be used where there are rows - but there is no focus
method. This means that the SetFocus
Input event cannot be used for tables of zero rows.
When cell events are invoked (clicking on a link/image or expanding/collapsing rows), the OnChange
event is fired. Currently there is no supported way to output a similar property to the Selected property that contains a reference to the row who's action is being invoked. If openDatasetItem
is called, it is not guaranteed to set the Selected property before the OnChange
event is fired with the EventColumn
output property set.