Tuesday, February 13, 2024

Searchable, Sortable, Filterable Items property for Datatables and Galleries

People don't generally ask me this question, but I do wind up rolling my eyes a bit when I see how others have solved for the problem of having a multi-item control (Data Table, Gallery, etc.) and they want to include the ability to search, filter, and sort within it.  

This isn't so much a problem that others won't figure out on their own, just that I think it isn't obvious how to do it in a reasonably consistent manner that you can use almost everywhere.  So I've included a walkthrough below to make it easier to get started.

Example using a Data Table

To jump right to it, this is my generic code I put in the Items property for approaching these problems:

Sort(
    Filter( //fuzzy filter for search
        Filter( //hard filter by drop down or similar
            cachedItems, //name of collection or source
            If(
                filter1DDown.Selected.Value= "All",
                true,
                fieldName1 = filter1DDown.Selected.Value
            ),
            If(
                filter2DDown.Selected.Value= "All",
                true,
                fieldName2 = filter2DDown.Selected.Value
            )
        ),
        (searchInput.Text in fieldName1) Or (searchInput.Text in fieldName2) || (searchInput.Text in fieldName3)
    ),
    Switch(  //figure out the sort order and change to reflect your column names
sortColumnName,
"fieldName1",
fieldName1,  
"fieldName2",
fieldName2,
"fieldName3",
fieldName3
),
If(
sortAscending,
SortOrder.Ascending,
SortOrder.Descending
)
)

I've broken it down into where the orange section relates to the sorting, the blue section is the fuzzy text search, and the green section is the drop-down filter.  For newbies, understand that the "inner most" code executes first (green), then the next layer (blue), then the last layer (orange).  Formatted code generally will include indentation that makes this slightly easier to read (as I've done above).

The above code gives you a framework for doing both manual hard filtering through a drop-down (or similar) as well as also allowing simultaneously for open text searching.  It also includes the logic for sorting based upon some variables that you'll set when column headings are clicked.

There are still a few scenarios where I alter my code outside of this, but most of the time, this is what I use by default and it works for most scenarios.

How it Works

Let's imagine we have a page with:
  • An open text searchable field
  • A dropdown where we are going to use as a hard filter (filter1DDown)
  • A second dropdown where we are going to use as another hard filter (filter2DDown)
  • A data table listing all of our items
  • Clickable header controls above each column to allow for (de-)selecting/changing sort
  • An icon on top of each header control that displays the sort direction

Something like this:


The end-user can type in whatever text they want and just do a search.  The moment they start typing, the information they type in that field begins filtering down results.  Also, they can choose to select from one or more filters via the dropdowns.  These are hard exact-match filters that you might use for things like a specific Location, or Status, etc.  All of these methods can be combined and all work simultaneously (i.e. select a location, a status, and start typing the name of the person).

For the sorting, the user can choose which column they want to sort by and quickly change direction or remove the sort intuitively by clicking the appropriate header.  The behavior of clicking a column header changes based upon prior values of the column name and direction.

NOTE: While this doesn't handle multi-sort (and I generally find users confused by that), you can adapt this by using a Collection of columns you want sorted and some additional logic steps within the Sort.

The code snippet at the beginning reflects how this would be handled by your Data Table (or Gallery) control.

Hard Filtering

Let's return to the hard filtering section of the code above:

        Filter( //hard filter by drop down or similar
            cachedItems, //name of collection or source
            If(
                filter1DDown.Selected.Value= "All",
                true,
                fieldName1 filter1DDown.Selected.Value
            ),
            If(
                filter2DDown.Selected.Value= "All",
                true,
                fieldName2 filter2DDown.Selected.Value
            )
        ),

NOTE: You would change the bold text to reflect your own control names, column names, and Collection or data source name.

The name of our actual collection (or data source) that we're working with in this example is cachedItems.  The Filter() works by deciding if the end-user has changed the default value for the drop-down from the first value ("All" in my own lists).  If they haven't, then the value simply is "true".

How Filter() segments work is they are evaluated to see if they are true/false.  If a specific row in your Collection evaluates to true then it is included, if false then it is not.  So by simply saying if the drop-down = "All" we return a true value, that means we really are just skipping this logic.  All rows are returned because all of them are evaluated as being true.

If we change the value to anything other than "All", then we use the logic included to evaluate that specific column inside our Collection.

NOTE: You could copy/paste as many drop-downs here as you have (e.g. filter3DDown).

Soft Filtering

Again, from our original code above, here is the soft filtering section:

    Filter( //fuzzy filter for search
...
        (searchInput.Text in fieldName1) || (searchInput.Text in fieldName2) || (searchInput.Text in fieldName3)
    ),

When cut out of the code above, this is a little simpler to understand.  We take the results of the Hard Filter above, then we do a search to see if the text typed into our search field (named searchInput in my code) exists within those columns.  You could add as many columns as you'd like and just include the || (or Or) characters/command between them.  

NOTE: Notice the parenthesis that wrap each distinct item.  Logical precedence is a thing that can trip you up, so wrapping discrete items you want evaluated inside parenthesis makes certain things will work as expected.

Since we take the first Hard Filter as our input, this allows us to combine search methods on the fly.  If all the drop-downs have "All" selected, then we're searching based upon all items in the Collection.  If they choose an option in any/all drop-downs, then we are only searching within items that match those choices.



Sorting

This is a section of code that might change depending upon the columns you're sorting by (combining numbers and text sorting can get PowerApps confused).  However, again, from the above code:

Sort(
...
...
...
    Switch(  //figure out the sort order and change to reflect your column names
sortColumnName,
"fieldName1",
fieldName1,  
"fieldName2",
fieldName2,
"fieldName3",
fieldName3
),
If(
sortAscending,
SortOrder.Ascending,
SortOrder.Descending
)
)

This allows our sorting to be controlled by two variables:

  • sortColumnName (string)
  • sortAscending (Boolean)

By default, all Booleans equal false and all strings are null. 

When we click our "header control" we will place above each column, we will run specific code when it is clicked.

For a basic version, the following is workable:

UpdateContext(
    {
        sortColumnName: Self.Text,
        sortAscending: !sortAscending
    }
)

This will simply change the sortColumnName to the same text as what the control displays and flip the sort from up/down.

A slightly more user-friendly version is:

If(
    (Self.Text = sortColumnName) And (!sortAscending),
    UpdateContext(
        {
            sortColumnName: "",
            sortAscending: false
        }
    ),
    If(
        (Self.Text <> sortColumnName),
        UpdateContext(
            {
                sortColumnName: Self.Text,
                sortAscending: true
            }
        ),
        UpdateContext({sortAscending: false})
    )
)

This one is context-sensitive based upon prior values.  It switches to an Ascending sort by the column clicked if it isn't currently selected, then descending if clicked again, then de-selects the sort if clicked again.  Changing between columns switches to the default of an Ascending sort for the specific column.


For the icons, they have their OnSelect set to select the header they are grouped with.  Additionally, their Icon value is set to Up/Down based upon the value of the sortAscending value.

If(
    sortAscending,
    Icon.ArrowUp,
    Icon.ArrowDown
)

Finally, the Fill is set similarly so that the column header has a slightly different color if we have selected it:

If(
    sortColumnName = Self.Text,
    ColorFade(
        RGBA(164,38,44,1),
        -20%
    ),
    RGBA(164,38,44,1)
)


Performance

The first thing for me is that if you're going to have a very responsive control in this scenario where end-users are going to be flipping a lot of levers and switches to alter appearance, it is important to cache your data or work with a very small dataset.

If you are constantly querying your datasource and needing to account for delegation, then you (and your users) are going to get frustrated.  Bringing that data into the application so you can easily do this in memory is key.  Certainly you can do this w/ live links to lists, but you should do everything in your power to get things to a point where you can work w/ them in your app.

ClearCollect(
    cachedItems,
    allCandidatesSP //from SharePoint
);

Final Thoughts

This is a little more complex than you'd want, but it is a pattern that plays out quite often.  Giving users some complex ways to filter/search/sort that ultimately are simple for them to understand is key.

Do keep in mind that if your Sort() code isn't working as expected, check the column types.  If you're mixing Numbers/Text, then it is possible that the Sort code as written will get confused.  You might need to break out a numerical sort vs. a text sort into a little more complex code vs. the simpler version above.

No comments:

Post a Comment

Because some d-bag is throwing 'bot posts at my blog I've turned on full Moderation. If your comment doesn't show up immediately then that's why.