Liquid Templates in Dynamics Portals – Part 3
It’s been a while since the last post in this series Liquid Templates in Dynamics Portals – Part 2. Since then, I’ve had the opportunity to deliver a presentation on Liquid Templates a few times and receive some great feedback. You can check out a recording of my Power Platform 24 virtual session titled Working with Liquid Templates in Power Portals hosted by xrmVirtual. This was an excellent event all around so check out the other videos available!
Let’s get back to it with a look at data collections in Liquid templates.
Working with lists of data
Displaying CDS data from a single record is a powerful feature, but working with data collections is a common requirements for Power Portal solutions. For example, we may need to display a list of Open Cases as demonstrated on the Customer Self Service Portal or display a list of open Application records on a custom Portal handling some variety of applications submissions.
We can usually handle these situations via configuration but sometimes requirements go beyond configuration. Fortunately, Liquid templates offers tools for working with data collections – Objects of type Array and Dictionary that contain a list of items, Tags that allow iterating over the list, and Filters that offer additional list refinement.
Arrays and Dictionary Types
Liquid Objects of type Array and Dictionary are similar in that we can iterate over the items list using Liquid Tags. The primary difference between the two is that with Arrays, we can access a data element using the item position while the Dictionary allows accessing item via key. Some examples of Arrays and Dictionaries:
page.children
– collection of child pages for the current Portal Pageentityview.records
– the current page of record results returned when requesting anentitylist
entity attributes
– a list of attribute values accessible by attribute name
These objects provide access to the Portal configuration details and CDS data within our system.
Iteration tags
Liquid Iteration Tags allows us to loop over the items in our Array or Dictionary. I like the definition provided in the online documentation with the for
Tag:
Executes a block of code repeatedly
Thefor
Iteration tag
It seemed an odd phrase at first but it makes sense – for each item in the list, execute the block of code within the Tag definition.
When using the for
Tag, you also gain access to the forloop
object. While iterating over a list of elements, this object provides information about the loop itself through several forloop
attributes. For example, you can check the list length
, the current index
, whether the current index is the first
or last
item, or the number of remaining items via rindex
. This can be helpful depending on the complexity of the work being done in the loop.
The for
Tag seems to be the most common, but we also have a few other iteration options:
cycle
– loop on a group of strings. This is used within afor
tag (and I can’t actually think of a real world scenario where it might be used).tablerow
– generate an HTML table row for each item in an array. This is a nice option to minimize your coding if you know that you will be using HTML tables
Collection Filters
As discussed in part 2, Filters allow manipulation of our Liquid Objects when rendering the data. Liquid Templates also offer Array filters available that allow for processing Arrays and Dictionaries. Here are some of the filters that when applied to an existing array, they return a new array or object:
batch
– divides an array into smaller arrays of a given size. An example where this is useful is rendering a list of items on more than one row in a table layout, breaking the batches into the number of columns you would like to display.concat
– combine two arrays into a new array. This might be handy of you need to run multiple queries, or combine arrays from two template callswhere
/except
– query for items where an attribute value matches a given criteria, or all items that do NOT match the criteriaselect
– select a given attribute value from each item in a list. This can be handy if you want to pull out a single attribute value forfirst
/last
– return the first or last element of the list. This can be helpful either when filtering items or while iterating on items in afor
loop, such as closing out an element on thelast
item.group_by
/order_by
– reorder your list, grouping or sorting by a given element attribute. You might be able to group and order in your initial list retrieval, such as with an Entity View or FetchXml query. But this filter might be required after you apply aconcat
filter to reorder your combined results.join
– combine the elements in the array into a delimited string
This is not the complete list, so be sure to check out the links for additional filters that might suit a particular need. For example, I had not seen batch
recently and it allowed me to greatly simplify an exiting Web Template.
Now that we have reviewed Arrays and Dictionaries tools in Liquid, let’s take a look at an example to illustrate these capabilities working together.
An example
The Power Portals platform offers a variety of methods for displaying CDS data – Entity Lists, Entity Forms, and Web Forms just to name a few. But if you’ve worked on any extended Portals projects, you have likely hit a limitation to what is available. This is where Liquid can help.
Here’s a fairly simple scenario as an example – let’s display a list of Contacts on a Web Page and include a clickable link for a website URL field on your Portal that is not just the URL text. A standard grid view in the Portal will render text fields tagged as URL or Email as clickable links. For example, here is a grid showing sample data with the emailaddress1
and websiteurl
attributes from the Contact, the blue text indicating a clickable link:
Here the Portal uses the Entity Attribute metadata to render links which is really nice for maintainability. What if your requirements are to display alternate text for the website to the end user? Currently there is method to swap out the text value of the websiteurl
attribute that is displayed as the text of the anchor tag.
This example was inspired by a discussion around an article by Nick Doelman on his readyxrm.blog titled Dynamics 365 Portals – Overcoming Entity List Roadblocks with HTML and Liquid. Nick’s scenario outlines how to apply custom formatting to table cells and adding links to related records his Event and Event Session entities. As usual, it’s an very detailed article and I highly recommend reading it to see the steps to set up a Web Page with the custom Web Template.
Some Setup…
Our example will be simpler than Nick’s – we are going to display a list of Contacts and display a link to their Website (websiteurl
) using alternate text instead of the raw URL. If the Contact record has a parent Account, then we will use the name of the Account, otherwise we will provide some alternate placeholder text.
We will assume a few things so that we can focus on the Liquid Objects, Tags, and Filters. Let’s assume that we have:
- We have configured an Entity List named Active Contacts that displays the system view Active Contacts, with Website (
websiteurl
) and Email address (emailaddress1
) attributes as additional columns. - We created a new Web Page named Entity List Sample configured to display an Entity List named Active Contacts.
- Our Page uses a custom Page template of type Web Template bound to a custom Web Template, each named Entity List Sample and Liquid – Entity List Sample respectively.
- We have the correct Entity Permissions to see the list of Contacts in the system.
Here is a quick look at our Web Page settings:
Our Web Template
Now that we have all that Portal configuration set up, we start building our custom Web Template. To keep things simple, we are extending the out of the box Web Template named ‘Layout 1 Column’ and overriding the block named ‘main’. (More on extends and blocks in the next post!). Our starting Web Template looks like this:
{% extends 'Layout 1 Column' %}
{% block main %}
{% include 'Page Copy' %}
{% endblock %}
Our custom Liquid for the Entity List just below the statement {% include 'Page Copy' %}
. So when the Page Copy on the parent Web Page is updated, it will be rendered as well.
Using the Entity List
We configured our Web Page to include an Entity List, so now we can use Liquid to access and render the results of the View behind the Entity List. To access this data, we will include the entitylist
and entityview
Tags in our Web template. Our updated Web Template looks like:
{% extends 'Layout 1 Column' %}
{% block main %}
{% include 'Page Copy' %}
{% entitylist id:page.adx_entitylist.id %}
{% entityview id:params.view %}
{% for contact in entityview.records %}
... our code here ...
{% endfor %}
{% endentityview %}
{% endentitylist %}
{% endblock %}
The new entitylist
Tag has a single parameter named id
that passes the id of the Entity List we have included in our page. This can be important for maintainability if you want to change the Entity List in the Web Page record without updating the Web Template.
The entityview
Tag is a child of entitylist
and includes its own id parameter. The entityview
tag include several additional parameters such as sorting, searching, setting page size. These options can make our display even more dynamic but let’s save all that for a follow up post on the entityview
and its capabilities! For now, we will just render our list of Contacts.
Within the entityview
, we have included the for
Tag and finally get to the iterating on the collection of CDS records. Within this for Tag, we will access the individual Contact records and display our data.
Display the Contact info
We added the Entity List which includes a View definition to our Web Template and when the Portal engine renders the page, it retrieves the records as defined by underlying query. Behind the scenes, Liquid is essentially executing a FetchXml query for us. The list of returned records are accessible via the entityview.records
object and attribute, as we see in the for tag:
{% for contact in entityview.records %}
... our code here ...
{% endfor %}
When Liquid processes the for Tag, it executes the code within once for each record in the entityview.records
list, placing a reference to the record in the variable named contact
. It’s essentially performing an {% assign %}
operation behind the scenes for us. Now that the record is available via the contact variable, we can access the Contact attributes. For example, we can retrieve the Contact Website using the following syntax:
{{contact['websiteurl']}
or {{contact.websiteurl}
Now we have full control over how we want to render each of the Contact fields.
Bootstrap tables!
Within the for
loop, we want to render a new table row for each contact. Instead of an HTML table element, we will use div
tags and leverage the Bootstrap styles made available with the Portal. This means that for each contact, we will have a new div
tag flagged as a row, and for each Contact attribute, we will add a div
tag flagged as a column. A general bootstrap row and cell look like the following:
<div class="row text-wrap">
<div class="col-md-2">
<div>
</div>
The row class declaration identifies the row while the col-md-2
class indicates a table column. We will include a div
tag for each of our Contact fields, filling in the data using the data from the contact
Object. You can read more about Bootstrap tables and the grid system at this link: Bootstrap Grid System.
Most fields are simple and we can simply display the contact
attribute value as is. For example,
<div class="row text-wrap">
<div class="col-md-2">
{{contact.fullname}}
<div>
</div>
With the website field, we get to the fun stuff. We will need to add the Liquid code to render the anchor tag for the URL with alternate text. The logic is straightforward: if we have a website value for the contact, check for the parent Account, and if present, use the parent Account name as the anchor tag text. Otherwise, use the placeholder text.
So our Liquid code for rendering the websiteurl looks like this:
{% capture contact_website %}
{% if contact.websiteurl != nil %}
{% assign link_label="Check out my website!" %}
{% if contact.parentcustomerid != nil %}
{% assign link_label={{contact['parentcustomerid'].name}} %}
{% endif %}
<a href="{{contact.websiteurl}}">{{link_label}}</a>
{% else %}
(No Website)
{% endif %}
{% endcapture %}
<div class="row text-wrap">
<div class="col-md-2">
{{contact_website}}
<div>
</div>
Note that we pre-populated the value of link_label
in our first assign tag. This is to ensure that the variable has a value when evaluated because of some odd behavior rendering uninitialized variables. We can also added a simpler version of the anchor tag logic to the emailaddress1
column to include a clickable link.
An updated version of the table row logic for all fields will now look like this:
{% capture myWebsite %}
{% if contact.websiteurl != nil %}
{% assign link_label="Check out my website!" %}
{% if contact.parentcustomerid != nil %}
{% assign link_label={{contact['parentcustomerid'].name}} %}
{% endif %}
<a href="{{contact.websiteurl}}" target="_li" >{{link_label}}</a>
{% else %}
(No Website)
{% endif %}
{% endcapture %}
<div class="col-md-2">
{{contact.fullname}}
</div>
<div class="col-md-2">
{{contact["parentcustomerid"].name}}
</div>
<div class="col-md-2">
{{contact.jobtitle}}
</div>
<div class="col-md-2">
{% if contact.emailaddress1 != nil %}
<a href="mailto:{{contact['emailaddress1']}}">{{contact['emailaddress1']}}</a>
{% endif %}
</div>
<div class="col-md-2">
{{myWebsite}}
</div>
<div class="col-md-2">
{{contact.adx_publicprofilecopy}}
</div>
This block of Liquid now renders our contact attributes, formatting both the website and email anchor tags. We have the option of additional logic for each. For example, if the contact.parentcustomerid
is null, we can add some alternative text for that column too.
Note that we are using the capture
Tag to build the website anchor element. This Tag allows saving complex HTML in a variable for reference later within the div. Using capture is not absolutely required because as we can see when rendering the email link, we can place the formatting logic inline in the attribute column div
. I feel it makes things more readable when the logic is more complex.
Wrapping up the Web Template
Now that we have the logic ready, make sure our table fits nicely into the parent page, so we will wrap it up in div
decorated with another Bootstrap CSS class available with with default Portal styles. We also want to make sure that our table has a header and is formatted correctly, so we include a few additional Bootstrap CSS classes in the header columns. We can also add a quick check for no records returned with the entityview
. It would look odd to have a table header with no rows, so we can just display a friendly message.
We will place the container div and the table header within the entityview
Tag and outside the for
Tag. This will ensure that no stray HTML is left around if the entitylist
is removed from the Web Page record. Our updated template now looks like this:
{% extends 'Layout 1 Column' %}
{% block main %}
{% include 'Page Copy' %}
{% entitylist id:page.adx_entitylist.id %}
{% entityview id:params.view %}
<div class="content">
{% if entityview.records.first == nil %}
<h4 class="text-center">No contacts found!</h4>
{% else %}
<div class="row alert">
<div class="col-md-2 active alert-link">Full Name</div>
<div class="col-md-2 alert-link">Company</div>
<div class="col-md-2 alert-link">Job Title</div>
<div class="col-md-2 alert-link">Email</div>
<div class="col-md-2 alert-link">Website</div>
<div class="col-md-2 alert-link">Public Profile</div>
</div>
{% for contact in entityview.records %}
... our code here ...
{% endfor %}
{% endif %}
</div>
{% endentityview %}
{% endentitylist %}
{% endblock %}
We have the Liquid ready for each Contact in the Entity List, we’ve updated the parent container, and we’ve included a header for our table. So with everything combined, our Web Template looks like this:
{% extends 'Layout 1 Column' %}
{% block main %}
{% include 'Page Copy' %}
{% entitylist id:page.adx_entitylist.id %}
{% entityview id:params.view %}
<div class="content">
{% if entityview.records.first == nil %}
<h4 class="text-center">No contacts found!</h4>
{% else %}
<div class="row alert">
<div class="col-md-2 active alert-link">Full Name</div>
<div class="col-md-2 alert-link">Company</div>
<div class="col-md-2 alert-link">Job Title</div>
<div class="col-md-2 alert-link">Email</div>
<div class="col-md-2 alert-link">Website</div>
<div class="col-md-2 alert-link">Public Profile</div>
</div>
{% for contact in entityview.records %}
{% capture myWebsite %}
{% if contact.websiteurl != nil %}
{% assign link_label="Check out my website!" %}
{% if contact.parentcustomerid != nil %}
{% assign link_label={{contact['parentcustomerid'].name}} %}
{% endif %}
<a href="{{contact.websiteurl}}" target="_li" >{{link_label}}</a>
{% else %}
(No Website)
{% endif %}
{% endcapture %}
<div class="col-md-2">
{{contact.fullname}}
</div>
<div class="col-md-2">
{{contact["parentcustomerid"].name}}
</div>
<div class="col-md-2">
{{contact.jobtitle}}
</div>
<div class="col-md-2">
{% if contact.emailaddress1 != nil %}
<a href="mailto:{{contact['emailaddress1']}}">{{contact['emailaddress1']}}</a>
{% endif %}
</div>
<div class="col-md-2">
{{myWebsite}}
</div>
<div class="col-md-2">
{{contact.adx_publicprofilecopy}}
</div>
{% endfor %}
{% endif %}
</div>
{% endentityview %}
{% endentitylist %}
{% endblock %}
A quick summary of what we’ve just built. In our updated Web Template:
- inherit from the Web Template named Layout 1 Column and override only the block named
main
- included the Page Copy from the Web Page to ensure it is rendered
- set reference the Entity List configured with the Web Page record by it’s id available on the
page.adx_entitylist
object atribute value - render a new Bootstrap based table with a row for each Contact returned with the Entity List and Entity View
- for each Contact
websiteurl
attribute, if available, we display the name of the parent Account instead of the website URL value
How does our final product look?
We’ve kept things simple to focus on the logical flow control and filters available with Liquid Templates in Power Portals, but we can easily see lots of options for formatting – sortable columns, paging, filtering, highlighting special Contacts, etc. Or we can make some logical changes such as displaying the website URL of the parent customer and not the Contact.
Reusing Liquid templates
So what happens if we need to extend and change this example a bit? For example, instead of an entitylist, we want to return and format similar data using FetchXml? I really don’t want to copy this code in several locations, so in our next post, we can take a look at some of the Liquid components that enable reuse, such as include
, extends
, and block
Tags.
Until then, as always, all comments, questions, and suggestions are welcome!
Awesome. Best explanation for liquid template.I am still learning Portal, so how do we display web page in portal. Do we need to use webset link.
Thanks,
Abhishek
Hi Jim
Can we add custom code to this web template to display a different entity list based on web role check?
Thanks
There seems to be little to no info on having sortable columns on and entity list. All of the columns I have on the view I use for the entity list are sortable when that view is displayed in my Powerapps application. However, when that same view is displayed in my entity list on my portal, the lookup fields columns are not filterable/sortable.
The portal view is for the Incident table. it has these relationships:
Incident N:1 Account N:1 Territory
A lookup field from the Account (looks into the Territory) is an example of a column that is sortable in the application but not sortable in the portal.
Anyone know why this is and/or how to fix it?
This is a critical requirement for my portal.