Creating a Dynamic Text Interpolator in PowerFx 📝

Creating a Dynamic Text Interpolator in PowerFx 📝

Edit 11/07/2023 - It was brought to my attention that an error existed in the example code for the interpolator which used an incomplete version of the Regular Expression statement. This has now been rectified 🦀


I'm in the process of doing a write up on Component Actions which follows my previous article on Functions, but I wanted to take a different steer today and discuss a solution I put together.

You may or may not already know about String Inteprolation in Canvas Apps. It's a way to mask variables and values within a text value itself. It's really handy to help make your code more readable; for example you could use:

$"{gvString}, {gvText}, {gvLike}, {gvThis}"

where you would otherwise use:

Concatenate("String","Text","Like","This")

or

"String" & "Text" & "Like" & "This"

Whilst this is great for text that's static in your app, you may come across requirements where you'd like an administrator to be able to edit this text without having to edit the app itself, which isn't a difficult problem in of itself until they also need to be able to refer to values in your app.

No alt text provided for this image

I had this very challenge recently, and this is how I solved it!


Planning

A problem like this requires some planning to ascertain how this will work exactly. We want to be able to do the following:

  • Present text with replacement tags

  • Present values

  • Replace tags with values when presenting to the user.

  • User can edit the text and add replacement tags, which can be interpreted by the app and rendered correctly.

The first thing we would need to ascertain is what is our replacement tag? This is going to be an identifiable piece of text we can look for in our text block to replace with a value. I settled for #Tag# in this instance.

So let's think about some of the approaches we could take.

We have a block of text that looks like this:

"Hello #FirstName# #Surname#"

And a Table of Tags like this:

Table(
    {
        Tag: "#FirstName#", 
        Value: "Mike"},
    ....
)

So we'll try and replace any #tags# in the text with the corresponding Values in the Table.

And with this, we could try the following:


Trying Find() and Replace()

We could try the Find() function

Find("#FirstName#",txtInputString)

But we'd need to test every tag:

Concat(
  ForAll(
      colTabs,
      Find(
        ThisRecord.Tag,
        txtInputString)),
      ThisRecord.Value)

So Find() will return the Starting Locations as Numbers of these tags, we'd need to somehow replace them with the tag value.

But how do we iterate over every Find() value, run a Replace() and combine everything back together? What if our replacement text exceeds the length of the replacement tag? Would it overwrite our text? Will the code get too long and unmaintainable?

Let's try another approach:


Enter Regular Expression and MatchAll()

We can use Regular Expression with the MatchAll() function to describe the text we're looking for. I personally use regex101.com to write and; ultimately, diganose my Regular Expressions as I find it quick and easy to see where it's going wrong, and see useful information on characters I can use. It's super powerful and definietly a weapon to have in your arsenal.

We're going to combine a few functions here to do what we want:

  • MatchAll() - Will use our Regular Expression to find our tags

  • ForAll() - Will iterate through all our results to find our tags and do something

  • Coalesce() - Which will help us do the text substitution

  • Concat() - Is going to put everything back together

The regular expression we're going to use is as follows:

(<[^>]+>|#\S+#|\S+(?:\s|$)|[.!,]\s*|\s)

And if this looks like a load of hieroglyphics to you, you're not alone! You can use Regex101.com to find out what this expession actually does (A full explaination is out of the scope of this article), and we can also test it's functionality out.

No alt text provided for this image

That. Looks. Perfect 👍

And this isn't just matching whole words, it's also handling punctuation as well:

No alt text provided for this image

It also plays nice with HTML tags, for those using HTMLText Controls 😊

Oh, and normal #HashTags are left as is too 😁

The basis of what it does is:

  • It finds all words with trailing spaces

  • It finds words encapsulated in #'s, and removes any trailing spaces

  • The trailing spaces from the words encapsulated in #'s are added as a match after the tags

  • Punctuation is added as it's own match.


The Tags Table

We mustn't forget that this solution will be useless unless we declare and use that Tags table. We can delcare a collection in our app like the following:

ClearCollect(
    colTabs,
    Table(
        {
            Tag: "#FirstName#", 
            Value: gvUser.givenName
        },
        {
            Tag: "#Surname#", 
            Value: gvUser.surName
        },
        {
            Tag: "#JobTitle#", 
            Value: gvUser.jobTitle
        },
        {
            Tag: "#Today#",
            Value: Today()
        }))

This Collection will need to be referenced when we use the formula below.


Let's get Matching!

So, combining this expression with MatchAll() will give us a table of words in our script. This is good as it breaks our problem down:

No alt text provided for this image

So now we have our text block broken down, and our replacement tags in a state that we can just do a direct replacement, we can reconstruct the above table, replacing tags as we go in a ForAll() statement by using LookUp() to match the FullMatch value in our table of tags, and Coalesce() to handle instances where our tags don't match.

Concat(
    ForAll(
        MatchAll(
            TextInput1.Text, //Get all matches on our RegEx
            "(<[^>]+>|#\S+#|\S+(?:\s|$)|[.!,]\s*|\s)") As Elements,
        Coalesce(               //Coalesce will either replace the tag if
                                //it's found, or just insert the text we're evaluating
        LookUp(
                tags,
                ThisRecord.Tag = Elements.FullMatch
            ).Value,
            Elements.FullMatch
        )
    ),
    ThisRecord.Value       //Finally let's concat the resulting table from the ForAll statement, 
                            //without any seperators
)  //Output as Text)

And when added into a Function, the end result should look something like this:

No alt text provided for this image


Implementation Tips

Looking to put this into action? Look at these tips below:

  • Similar to variable naming conventions, give your tags meaningful names to help your users understand what they will display.

  • Define in advance what fields you want to present to users to be able to tag. Go beyond the basics and include extras, for example; the current date.

  • Make sure your users know what tags they can use. Display a list of them whilst they're editing scripts.

  • You can store the scripts in practically any data source, it may be worth storing a list of tags in a table for reference.

  • Will this be administered from a Model Driven App? Consider using a Custom Page to modify the scripts so you can implement the code above.


Next Time

  • We'll implement this with full editing capability in a Dataverse table with custom page.

  • We'll make this a function of a component in a library, so we can easily reuse the functionality.


Thanks for reading this article, I hope this helps you implement this functionality in your future apps.