Dynamic Table vs Dynamic Button 🥊

Product: PowerShell Universal
Version: 2.5.5
Framework: 3.8.0

TL;DR: When I have a New-UDTable with an -id, wrapped in a New-UDDynamic, the table does not reload/refresh when Sync-UDElement is called on the Dynamic wrapper. When the -id is omitted, it behaves as expected and reloads. Why does specifying the table’s ID change this behaviour?


I’ve bumped up against an issue and could do with some advice. I’ve come up with a work-around but it doesn’t really feel like how the framework should work - it’s brittle and makes me feel icky! I want to make sure I’m not missing a trick.

I’ll outline the problem and them show how I’ve worked around it. I’ve put together a dummy dashboard to repro this. The dashboard it’s a part of is more complex - but isn’t relevant to this issue.

Problem

I have a table of values that contains some data that I want the user to be able to select and perform some action on the selected rows. That action can affect the underlying data in the table so once the user has performed the action - the table needs to be reloaded. As the user selects the row items - the intent of the action needs to be made clear to them.

I’m doing this with a table that has selectable rows. On selection the table tells the button to refresh - everytime the button refreshes, it gets the table element and checks how many selected rows it has. It displays the appropriate text / disables itself as required.

Should be a simple one - a New-UDButton inside a New-UDDynamic and a New-UDTable inside a New-UDDynamic.

New-UDDashboard -Title "🐔 or 🥚" -Content {

    # Button needs to be wrapped in a dynamic along with logic to work out 
    # a) is the button enabled
    # b) what text to show the user
    New-UDDynamic -Id 'DynButton' -Content {
        # From the table, get the number of selected rows.
        # Assume if we fail to get the table element it may not be loaded yet
        # and so nothing can be selected.
        $SelectedRows = try {
            (Get-UDElement -Id 'Table' -ErrorAction Stop).'selectedRows'
        } catch {
            @()
        }
        # Decide, based on the number of selected rows, what the text of the
        # button will read.
        $NumSel = ($SelectedRows | Measure-Object).Count
        if ($NumSel -eq 10) {
            $NumSel = 'All'
        }
        $ButtonText = switch ($NumSel) {
            0 { 'Nothing Selected' }
            1 { '1 Item Selected' }
            Default { "$NumSel Items Selected" }
        }
        New-UDButton -Text $ButtonText -OnClick {
            # This is a placeholder for manipulating the selected rows in some
            # way. Think - interacting with a Database.
            $SelRows = (Get-UDElement -Id 'Table' -ErrorAction Stop).'selectedRows'
            Show-UDToast "$(
                ($SelRows | Measure-Object).Count
            ) rows selected. Their sum is $(
                ($SelRows | Select-Object -Expand Randos | Measure-Object -Sum).Sum
            )"
            # The underlying data for the table may have changed so reload the
            # table.
            Sync-UDElement -Id 'DynTable'
        } -Disabled:($NumSel -eq 0)
    }

    # Table needs to be dynamic
    New-UDDynamic -Id 'DynTable' {
        New-UDTable -Id 'Table' -LoadData {
            # This part is unimportant. Just showing 10 random numbers here so
            # it's very obvious when the table has reloaded.
            $TableData = ConvertFrom-Json $Body
            Get-Random -Count 10 | ForEach-Object {
                @{
                    'Randos' = $_
                }
            } | Out-UDTableData -Total 10 -Page 0 -Properties $TableData.Properties
        } -COlumns @(
            New-UDTableColumn -Property 'Randos'
        ) -ShowSelection -OnRowSelection {
            # When a row is selected/unselected - refresh the button
            Sync-UDElement -Id 'DynButton'
        }
    }
}

NoRefreshingTable

The table is able to refresh the button when rows are selected/unselected and the button is able to get the selected rows of the table just fine.

The issue is that the table does not refresh. No call to Sync-UDElement aimed a table with an ID or it’s parent New-UDDynamic will cause said table to refresh. Any other content inside the Dynamic is re-executed as expected.

If I remove the -id from the table (and in the example code - change the button logic so the button is enabled) - the table refreshes when Sync-UDElement is called on the New-UDDynamic. This, however, means I cannot use Get-UDElement to get the selected rows of the table (as I don’t know it’s -Id).

I can either have:

  • The table with an ID and row selection enables the button - but the button can’t refresh the table.

or

  • The table with No ID - is hypothetically refreshable. Rows can be selected but the button can’t find the table so stays at 'Nothing Selected`

Workaround

The workaround came about when I saw that New-UDElements return their content when you Get-UDElement on them. I remove the ID from the table and
basically replace the New-UDDynamic wrapping the table with a New-UDElement. Then in the button I first call Get-UDElement on the table’s parent element. Then I get the id of the first child in the content property and call Get-UDElement on that. If the order of it’s children changes - this workaround would break.

New-UDDashboard -Title "🐔 or 🥚" -Content {

    # Button needs to be wrapped in a dynamic along with logic to work out 
    # a) is the button enabled
    # b) what text to show the user
    New-UDDynamic -Id 'DynButton' -Content {
        # From the table, get the number of selected rows.
        # Assume if we fail to get the table element it may not be loaded yet
        # and so nothing can be selected.
        $SelectedRows = try {
            Get-UDElement -Id $((Get-UDElement -Id 'DynTable').Content.Id) -ErrorAction Stop | Select-Object -ExpandProperty 'selectedRows' -ErrorAction Stop
        } catch {
            @()
        }
        # Decide, based on the number of selected rows, what the text of the
        # button will read.
        $NumSel = ($SelectedRows | Measure-Object).Count
        if ($NumSel -eq 10) {
            $NumSel = 'All'
        }
        $ButtonText = switch ($NumSel) {
            0 { 'Nothing Selected' }
            1 { '1 Item Selected' }
            Default { "$NumSel Items Selected" }
        }
        New-UDButton -Text $ButtonText -OnClick {
            # This is a placeholder for manipulating the selected rows in some
            # way. Think - interacting with a Database.
            $SelRows = try {
                Get-UDElement -Id $((Get-UDElement -Id 'DynTable').Content.Id) -ErrorAction Stop | Select-Object -ExpandProperty 'selectedRows' -ErrorAction Stop
            } catch {
                @()
            }
            Show-UDToast "$(
                ($SelRows | Measure-Object).Count
            ) rows selected. Their sum is $(
                ($SelRows | Select-Object -Expand Randos | Measure-Object -Sum).Sum
            )"
            # The underlying data for the table may have changed so reload the
            # table.
            Sync-UDElement -Id 'DynTable'
            Sync-UDElement -Id 'DynButton'
        } -Disabled:($NumSel -eq 0)
    }

    # Table needs to be dynamic
    New-UDElement -Tag 'div' -Id 'DynTable' -Endpoint {
        New-UDTable -LoadData {
            # This part is unimportant. Just showing 10 random numbers here so
            # it's very obvious when the table has reloaded.
            $TableData = ConvertFrom-Json $Body
            Get-Random -Count 10 | ForEach-Object {
                @{
                    'Randos' = $_
                }
            } | Out-UDTableData -Total 10 -Page 0 -Properties $TableData.Properties
        } -COlumns @(
            New-UDTableColumn -Property 'Randos'
        ) -ShowSelection -OnRowSelection {
            # When a row is selected/unselected - refresh the button
            Sync-UDElement -Id 'DynButton'
        }
    }
}

Note: There was a gif here similar to above but showing the above workaround code working - I’m a new user so can only have one embedded image per post :cry:

Any advice appreciated - thanks in advance

Odd, Are you sure you that both your dynamic and table id’s are unique and not used elsewhere in your dashboard? I’ve sometimes seen issues when there are conflicting ID’s, beyond that I’m not sure on this one.

:wave: Hey. In the non-functional example all of the Ids are unique and it’s the only dashboard on my test instance - so I don’t know that it can be an ID conflict :cry:

Didn’t even realize your username until now :rofl:, hope its going well!
The only other thing I’d suggest if you haven’t already is just stripping it back to it’s basic raw components.
Put the minimal code in, so start with a fresh page - maybe even a clean dashboard so you know the environment isnt being effected too, get the basic components; table, dynamic, button etc, but no extra stuff for your data - just take the basic examples off the docs page, and see if you can replicate it. If you can, it’s likely a bug and we may have to ask on @Adam for his input.

I did notice in your first example you didnt have a -content on your table’s dynamic region, and that you’re opening directly up into a script block, probably nothing and i’m guessing its working because of positional parameters, but just figured i’d point it out incase it was doing anything there too

First off:

  • Awesome reproduction steps!
  • Smart workaround!

This is a bug and likely an issue with React rendering. We use the ID as the React key and sometimes React won’t render unless the key changes. When you use a component without an ID, it generates a random one which forces React to completely re-render the component. It’s a performance issue since React should intelligently render new content rather than the entire component.

I can open an issue for this.

2 Likes