DEV Community

Ateina
Ateina

Posted on • Originally published at m365scribbles.com

Power Pages Web Templates: Working with Liquid and FetchXML

As someone in love with SPFx web parts, Power Pages web templates felt like something I'd want to try.

What we're building:

Analytical dashboard tiles for a book tracking application. Each tile will:

  • Connect to Dataverse using FetchXML
  • Accept a status code as a parameter
  • Display a count of books matching that status for the current user

App overview (drawing, table schema)

My 5-year-old Procreate license is finally seeing some action:
App design sketch

Obviously, there's no way to improve this perfection, so let's move on to the database schema.

Table Structure

This solution uses the following table structure:

Table schema

We only need the User Book Status table for our dashboard:

  • Which books the user has
  • What status each book is in (Read, Reading, Want to Read, etc.)
  • The link to the user (Contact)

Creating a Web Template

Quick setup: Power Pages Management β†’ Web Templates β†’ New Web Template. Name it, connect to your site, set MIME type to application/json

Creating a web template

Done with the basics, we can move to the actual web template building 😈

FetchXML Block

First things first, create table permissions for each table you'll use. Go to Power Pages Management β†’ Table Permissions, add your table, set the scope and assign roles. Without this, FetchXML will return nothing, even if you're god and emperor in your environment.

Fetching Book Status

Here is the FetchXML logic used to retrieve the book count:

{% assign status_value = status | default: '455810000' %}
{% assign color = card_color | default: '#4ade80' %}

{% fetchxml books %}
    <fetch aggregate="true">
    <entity name="adrbp_userbookstatus">
        <attribute name="adrbp_userbookstatusid" alias="bookcount" aggregate="count" />
        <filter type="and">
            <condition attribute="adrbp_status" operator="eq" value="{{ status_value }}" />
            <condition attribute="adrbp_user" operator="eq" value="{{ user.id }}" />
        </filter>
    </entity>
</fetch>
{% endfetchxml %}
Enter fullscreen mode Exit fullscreen mode

We set table User Book Status, configured to return count and filtering by current user and dynamic status.

Now you can pass parameters:

{% include "Dashboard Stat" status: '455810000' title: "Read" secondary_title: "Finished books" card_color: "#b6d7a8" card_icon: "βœ…" %}
Enter fullscreen mode Exit fullscreen mode

We can play with fetchxml code a little, but with some limitations.

What you can pass as parameters:

  • condition values (current config)
<condition attribute="adrbp_status" operator="eq" value="{{ status_value }}" />  
Enter fullscreen mode Exit fullscreen mode
  • optional conditions (on/off) Note: if you've set a default value with | default:, the variable will never be empty, so the {% if %} check won't work as expected. Remove the default to make this approach work.
<filter type="and">
    <condition attribute="adrbp_user" operator="eq" value="{{ user.id }}" />
    {% if status %}
        <condition attribute="adrbp_status" operator="eq" value="{{ status }}" />
    {% endif %}
</filter>
Enter fullscreen mode Exit fullscreen mode
  • comparison logic via {% if %} blocks
{% if filter_type == 'active' %}
  <condition attribute="adrbp_status" operator="ne" value="455810004" />
{% elsif filter_type == 'inactive' %}
  <condition attribute="adrbp_status" operator="eq" value="455810004" />
{% endif %}
Enter fullscreen mode Exit fullscreen mode

Layout

HTML Structure

The HTML block is straightforward - we use Liquid variables (in double curly brackets) to insert dynamic values, and CSS custom properties to pass colors to our styles.

<div class="stat-card" style="--card-color: {{ color }};">
    <div class="stat-content">
        <div class="stat-title">{{ title }}</div>
        <div class="stat-value">{{ books.results.entities[0].bookcount }}</div>
        <div class="stat-subtitle">{{ secondary_title }}</div>
    </div>
    <div class="stat-icon">
        {{ card_icon }}
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

As I wanted a different color for each card, I passed inline style="--card-color: {{ color }};" to feed each card's unique color into CSS.

CSS Styling

The CSS classes need to be defined in your site's styles. You can add them with any of these options:

  • Custom CSS file (Styling workspace)
  • Page-specific styles
  • Global CSS file
.stat-card {
    padding: 20px;
    border-radius: 12px;
    /* …… */
}

.stat-icon {
    background-color: color-mix(in srgb, var(--card-color) 20%, white);
    padding: 12px;
    /* …… */
}

/* ... other styles ... */
Enter fullscreen mode Exit fullscreen mode

The --card-color custom property sets each card’s unique color, and lets us create color variations (like the lighter icon background) from that same base color.

Manifest

Manifest is a JSON object which represents component configuration.

{% manifest %}  
{  
 "type": "Functional",  
 "displayName": "Display Name",  
 "tables": ["table1_logical_name", "table2_logical_name"],  
 "params": [...]  
}  
{% endmanifest %}
Enter fullscreen mode Exit fullscreen mode

Notes:

  1. The type can be either Functional or Layout. Use Functional for custom components (like our book card). Use Layout only for page templates and structural elements.
  2. The tables property is developer-defined in the manifest, not user-configurable. Unlike params, users cannot change which tables the component connects to.
  3. Parameters are always strings. You can convert them to boolean, decimal, integer or string (maybe you just love converting strings to strings) using Liquid Type filters. {% assign items_count = count | integer %}

So this is what our component's manifest ends up looking like:

{% manifest %}
{
    "type": "Functional",
    "displayName": "Book Status Card",
    "description": "Shows books by status",
    "tables": ["adrbp_userbookstatus"],
    "params": [
        {
        "id": "title",
        "displayName": "Title"
        },
        {
        "id": "secondary_title",
        "displayName": "Secondary Title"
        },
        {
        "id": "status",
        "displayName": "Status",
        "description": "Status to filter"
        },
        {
        "id": "card_color",
        "displayName": "Card Color",
        "description": "Card color, use hex in a format #xxxxxx"
        },
        {
        "id": "card_icon",
        "displayName": "Card Icon",
        "description": "Card icon"
        }
    ]
}
{% endmanifest %}
Enter fullscreen mode Exit fullscreen mode

And now, how it looks when you try to update web template’s config in UI:

Web template UI config

Putting It All Together

Once our web template is ready, we can add it to a page like this:

{% include "Dashboard Stat" status: '455810000' title: "Read" secondary_title: "Finished books" card_color: "#b6d7a8" card_icon: "βœ…" %}

{% include "Dashboard Stat" status: '455810001' title: "Reading" secondary_title: "Currently reading" card_color: "#a8d7c0" card_icon: "πŸ“–" %}

{% include "Dashboard Stat" status: '455810002' title: "Want to Read" secondary_title: "On your wishlist" card_color: "#f5c6a8" card_icon: "⭐" %}

{% include "Dashboard Stat" status: '455810004' title: "Dropped" secondary_title: "Set aside" card_color: "#f48fb1" card_icon: "🚫" %}
Enter fullscreen mode Exit fullscreen mode

Each {% include %} creates a reusable card with different data, so it is clean and easy to maintain.

Final Result

And here's our book app home page - no difference from the initial design:

Final result

Top comments (0)