June 9, 2024

Making Custom HTML Tables with PowerFx

What follows is another journey into using PowerFx to do abnormal things. Tread carefully.

Lately I have been using the HTMLText control for a lot of things. Even when there is another control available that would do the same thing, I try to use the HTML one. Why? Because it is flexible and multi-purpose. For displaying information, it is great. It can:

  • Show plain and rich text
  • Show unicode with UniChar()
  • Show images
  • Include links
  • Include tables
  • Include complex SVGs like data visuals
  • And much more!

For web developers, the fact that a HTML control can do HTML things is not news. For Power Apps developers, trying to do all of these things with one control seems like adding an unnecessary level of complexity. I am going to try to argue against these and maybe convince a few people as to why this control can be something special and reduce the number of controls you actually need on a screen by using one specific example: Tables.

Working with Tables

If you are trying to display tabular information for someone and give some limited functionality, the common way to go about it is to use a gallery. Let’s say you have the following collection:

ClearCollect(
    colDataExample,
    {
        Measure: "Measure A",
        Target: 50,
        Actual: 25,
        ReportingPeriod: "Q1"
    },
    {
        Measure: "Measure B",
        Target: 50,
        Actual: 20,
        ReportingPeriod: "Q2"
    },
    {
        Measure: "Measure C",
        Target: 50,
        Actual: 55,
        ReportingPeriod: "Q3"
    },
    {
        Measure: "Measure D",
        Target: 50,
        Actual: 45,
        ReportingPeriod: "Q4"
    },
    {
        Measure: "Measure E",
        Target: 50,
        Actual: 100,
        ReportingPeriod: "Q1"
    },
    {
        Measure: "Measure F",
        Target: Blank(),
        Actual: 25,
        ReportingPeriod: "Q2"
    },
    {
        Measure: "Measure G",
        Target: 50,
        Actual: 2,
        ReportingPeriod: "Q3"
    },
    {
        Measure: "Measure H",
        Target: 50,
        Actual: 30,
        ReportingPeriod: "Q4"
    },
    {
        Measure: "Measure I",
        Target: 50,
        Actual: 45,
        ReportingPeriod: "Q1"
    },
    {
        Measure: "Measure A",
        Target: 50,
        Actual: 25,
        ReportingPeriod: "Q1"
    },
    {
        Measure: "Measure B",
        Target: 50,
        Actual: 20,
        ReportingPeriod: "Q2"
    },
    {
        Measure: "Measure C",
        Target: 50,
        Actual: 55,
        ReportingPeriod: "Q3"
    },
    {
        Measure: "Measure D",
        Target: 50,
        Actual: 45,
        ReportingPeriod: "Q4"
    },
    {
        Measure: "Measure E",
        Target: 50,
        Actual: 100,
        ReportingPeriod: "Q1"
    },
    {
        Measure: "Measure F",
        Target: Blank(),
        Actual: 25,
        ReportingPeriod: "Q2"
    },
    {
        Measure: "Measure G",
        Target: 50,
        Actual: 2,
        ReportingPeriod: "Q3"
    },
    {
        Measure: "Measure H",
        Target: 50,
        Actual: 30,
        ReportingPeriod: "Q4"
    },
    {
        Measure: "Measure I",
        Target: 50,
        Actual: 45,
        ReportingPeriod: "Q1"
    },
    {
        Measure: "Measure A",
        Target: 50,
        Actual: 25,
        ReportingPeriod: "Q1"
    },
    {
        Measure: "Measure B",
        Target: 50,
        Actual: 20,
        ReportingPeriod: "Q2"
    },
    {
        Measure: "Measure C",
        Target: 50,
        Actual: 55,
        ReportingPeriod: "Q3"
    },
    {
        Measure: "Measure D",
        Target: 50,
        Actual: 45,
        ReportingPeriod: "Q4"
    },
    {
        Measure: "Measure E",
        Target: 50,
        Actual: 100,
        ReportingPeriod: "Q1"
    },
    {
        Measure: "Measure F",
        Target: Blank(),
        Actual: 25,
        ReportingPeriod: "Q2"
    },
    {
        Measure: "Measure G",
        Target: 50,
        Actual: 2,
        ReportingPeriod: "Q3"
    },
    {
        Measure: "Measure H",
        Target: 50,
        Actual: 30,
        ReportingPeriod: "Q4"
    },
    {
        Measure: "Measure I",
        Target: 50,
        Actual: 45,
        ReportingPeriod: "Q1"
    },
    {
        Measure: "Measure A",
        Target: 50,
        Actual: 25,
        ReportingPeriod: "Q1"
    },
    {
        Measure: "Measure B",
        Target: 50,
        Actual: 20,
        ReportingPeriod: "Q2"
    },
    {
        Measure: "Measure C",
        Target: 50,
        Actual: 55,
        ReportingPeriod: "Q3"
    },
    {
        Measure: "Measure D",
        Target: 50,
        Actual: 45,
        ReportingPeriod: "Q4"
    },
    {
        Measure: "Measure E",
        Target: 50,
        Actual: 100,
        ReportingPeriod: "Q1"
    },
    {
        Measure: "Measure F",
        Target: Blank(),
        Actual: 25,
        ReportingPeriod: "Q2"
    },
    {
        Measure: "Measure G",
        Target: 50,
        Actual: 2,
        ReportingPeriod: "Q3"
    },
    {
        Measure: "Measure H",
        Target: 50,
        Actual: 30,
        ReportingPeriod: "Q4"
    },
    {
        Measure: "Measure I",
        Target: 50,
        Actual: 45,
        ReportingPeriod: "Q1"
    }
);

This is a simple 4 column collection that looks at targets over time and whether they were met or not. Imagine that this is just data coming from a SharePoint list. It’s pretty simple and let’s say that this is something you want to show in Power Apps and looking at it you think, “Well, they just want this in the app, and there is no point in making a Power BI for this at this time.”

Let’s count the controls that could possibly be involved. Many would use a gallery (1). Each field would require a control (probably text label). That brings our total up to 5. Then you need to provide context and provide headers for each column. Now up to 9. Then you review the requirements and see that they want indicators to show whether or not the measure was met. You go with an icon and conditionally change it with an IF() statement. Tally is up to 10. Later you get a requirement that they want a link to the item. You add a link to the gallery and then a label for the header (12). You also decide to make a separator between the header and the rows with a rectangle and now you are up to 13.

As you continue to improve the way it looks, you start to see the overhead involved and how complicated making a simple table can be. While it is not particularly difficult to do, it may involve a flurry of controls. This could really make the tree view rough to deal with.

The funny thing is that an HTMLText Control could actually handle all of it. One control. That’s it. Don’t believe me? Keep reading.

Using HTMLText and PowerFx

Making HTML tables with PowerFx does not come without some level of complexity. First thing: You can only use inline styling. For Web Developers, this is probably going to be a red flag. But, if you use PowerFx as you would use client scripting languages (JavaScript), you can get around duplication when it comes to making tables.

Taking the previously defined collection, I was able to make an HTML table with banded rows and a header (with a background color). To make the banded rows happen, I did have to add an index column to the collection. I found this answer that shows you how to use ForAll() and Collect() to add an index column. It’s a pretty cool and simple way of doing this, as the index is being incremented as the collection is being filled.

With that in hand, after creating the first collection, I added this code:

Clear(colDataIndexed);

ForAll(
    colDataExample As data,
    Collect(
        colDataIndexed,
        {
            I: CountRows(colDataIndexed) + 1,
            Measure: data.Measure,
            Target: data.Target,
            Actual: data.Actual,
            ReportingPeriod: data.ReportingPeriod
        }
    )
)

Please note that PowerFx uses 1 as the base for indices, meaning the first element in a table is 1. This varies from language to language (0 vs 1 as the first element).

After adding the index column, I could start building the code for the table.

The Table

The table came together with nested With() functions. The first one set up the data source, header labels, styling, and the caption for the table. The nested With() then used Concat() to generate the HTML by row and mush it all together. The return was the whole HTML for the table by using string interpolation to generate the table, caption, header, and rows.

Here is the full code:

With(
        {
            Source: colDataIndexed,

            // Set up table headers
            Headers: Table(
                {Name: "Measure"},
                {Name: "Reporting Period"},
                {Name: "Actual"},
                {Name: "Target"}
            ),

            // Set up general styling
            GenStyle: $"style={Char(34)}border: 1px solid black;border-collapse: collapse;padding:10px;{Char(34)}",

            // Set up header style
            HeaderStyle: $"style={Char(34)}border: 1px solid black;border-collapse: collapse;color: white; background-color:#8a9999;padding:5px;{Char(34)}",

            // Add a caption
            Caption: "Sample Dataset"
        },
        // Nested With() to concatenate the HTML together
        With(
            {
                // Caption
                cap: $"<caption><b>{Caption}</b></caption>",

                // Header
                header: $"<tr>{Concat(Headers, $"<th {HeaderStyle}>{Name}</th>")}</tr>",

                // Rows
                rows: Concat(
                    Source,
                    // Here is row-banding by using the index
                    $"<tr {If(Mod(Value(ThisRecord.I),2)=0, "style=""background-color: #D6EEEE;""", "")}>
                        
                        <td {GenStyle}>
                            {ThisRecord.Measure}
                        </td>
                        <td {GenStyle}>
                            {ThisRecord.ReportingPeriod}
                        </td>
                        <td {GenStyle}>
                            {ThisRecord.Actual}
                        </td>
                        <td {GenStyle}>
                            {ThisRecord.Target}
                        </td>
                    </tr>"
                )
            },

            // Put everything together
            $"<table {GenStyle}>{cap}{header}{rows}</table>"
        )
    )

Concat() and string interpolation were the biggest help for the HTML generation. The one that was the trickiest to figure out (after adding the index) was how I was going to band the rows. That was handled here:

// Here is row-banding by using the index
 $"<tr {If(Mod(Value(ThisRecord.I),2)=0, "style=""background-color: #D6EEEE;""", "")}>

Using Mod, which gives you the remainder after dividing by a number, I was able to figure out the odd and even rows. If I divided by two and there was no remainder, it was an even row and should have a background color. Otherwise, do not add a style.

Here is what the table looked like:

Why Do All of This?

It’s a great question. I have enjoyed templating things and having more control over things in terms of placement and style. While most of the time multiple controls are the way to go, sometimes you can feel inundated with them for the simplest things.

After using the Deneb Custom Visual in Power BI and being able to template out visuals, I think that simpler things can also be templated out in Power Apps using HTMLText controls. If you don’t need OnSelect properties to fire events from multiple controls (different events) and you are just trying to display information, then learning some light HTML, CSS and using PowerFx to piece it all together may make the Tree View slimmer and the app more appealing.

You may also like