Microsoft Power Pages - Roll Your Own UI

INTRODUCTION

As part of getting my feet wet with Power Pages, I'm creating a simple site that does most of things I'd expect a site might need to do.  The more I play with the platform, the more potential I see for creating sites that range in complexity from simple to sophisticated.  This is a GREAT feature - make simple things simple to do and make difficult things possible.  

In this post, I'm going a little deeper with a discussion of bypassing the platform's built-in integration with Dataverse to performing customizations that let you roll your own UI and talk directly with SQL Server...  no Dataverse required!

ASSUMPTIONS

This article will be most helpful if you have some familiarity with the following concepts

  • HTML
    • used to create our custom UI
  • CSS (just a little in-line styling)
    • used  to style the UI
  • JavaScript
    • used to manipulate the HTML DOM
    • and call our Power Automate Flow Endpoints
  • Power Automate Flows
    • to get data from the UI and send it to SQL Server
      • Email validation
      • New user registration creation
    • but you could use any API you like
  • Power Pages
    • we won't be explaining the basics of creating a site and adding Pages/Components
  • Bootstrap (ugh... 3.x)
    • used to style the pages
    • used to implement a Modal message box
SCENARIO

As shown in a previous post, we have a simple site that promotes a software product.  On the Home page, there is an immediate call to action: Register.  The Registration page 

  • Prompts for basic information
  • Validates a non-duplicated email address
    • using a Power Automate Api call
  • Creates the new User in the database
    • using a Power Automate Api call
      • Adds the user
      • Assigns appropriate roles
      • Sends a confirmation email with Quick Start attachment
  • Uses a Bootstrap Modal 
    • for Validation/Api messages
LET'S DO IT - QUICK POWER AUTOMATE FLOW OVERVIEW

The Email Validation Flow

This flow is simple enough, it receives a JSON payload with an "email" property for the value to be checked for duplicates.  It returns an empty array (if no duplicates are found) or an array of the duplicates.


The Create Registration Flow

This flow receives a JSON payload with the registration data and creates the new user record as well as appropriate roles for the user.  It then sends a confirmation email with a QuickStart.pdf attachment.


In the remainder of this article, we'll refer to these two flows as the API endpoints being called when we need to do some database interactions.

A PEEK AT A POWER PAGE

Every page that is added to a Power Pages site has 3 important files that define how the page looks and works:
  • HTML file
    • defines the layout and elements on the page
    • populated with HTML markup as you add elements (like text, images, etc) to the page
  • CSS file
    • defines the styling of the page and elements
    • empty by default
  • JavaScript file
    • defines behaviors
    • empty by default
You access these 3 files by clicking the Edit code button on the page Designer to open Visual Studio Code for Web in a new browser tab


It will look something like this.  When you select a file, as shown below, that file will be loaded into the VS Code editor for you to make any changes you like


If you've done any web development, your eyes should be getting wide at this point.  With access to these files, you can customize a site page to do lots of things that you couldn't otherwise accomplish.... like creating a custom Registration form that interacts with SQL Server through an API....  And that's exactly what we'll be showing next!

ADD A SECTION FOR THE FORM

The Registration page we started with a simple page that we laid out using the out-of-box tools - it's just a couple of text controls and an image.  We then used the Add Section feature to create a new section under the page header.

When you add a new section, you are prompted to select an element to put in the section - we didn't do this!  The result was a placeholder in the HTML markup that we used to contain our custom form.  In the HTML file, the section started with these tags:


This gives us a perfect place to put our HTML for the Registration fields and submit button.  After the custom HTML changes (shown below), it will look like this



THE CSS FILE

For this exercise we did mostly in-line styling; however, we did add the following to the empty CSS file:

 label {  
   text-transform: uppercase;  
   font-weight: 600;  
   font-size: 80%;  
 }  
   
 .fldRequired {  
   border-left: 5px solid red;  
 }  
   
 .fldRecommended {  
   border-left: 5px solid yellow;  
 }  
   
 .fldValidated {  
   border-left: 5px solid green;  
 }  
The label simply standardizes how the field labels look.  The 3 classes help with making required field validations visually effective.

THE HTML FILE

In the placeholder section mentioned above, we added the markup for the fields used to gather Registration information from the visitor and a DIV at the bottom for the Bootstrap modal.  The full HTML markup is shown here and the customized sections are commented:

 <div data-component-theme="portalThemeColor3" class="row sectionBlockLayout text-left" style="display: flex; flex-wrap: wrap; margin: 0px; min-height: auto; padding: 8px;">  
  <div class="container" style="display: flex; flex-wrap: wrap;">  
   <div class="col-md-6 columnBlockLayout" style="flex-grow: 1; min-width: 310px; word-break: break-word; display: flex; flex-direction: column; margin: 25px 0px; padding: 16px;">  
    <div class="row sectionBlockLayout" style="display: flex; flex-wrap: wrap; padding: 8px; margin: 0px; min-height: 15px;"></div>  
    <div class="row sectionBlockLayout" style="display: flex; flex-wrap: wrap; padding: 8px; margin: 0px; min-height: 15px;"></div>  
    <h3 style="text-align: left;">Registration</h3>  
    <p style="text-align: left;">Want to see what iQueue can do for you?&nbsp; Take a few seconds to register and you'll get an email with login information and a Quick Start guide so that you can kick the tires.&nbsp; The "demo" site is fully functional and waiting for you - elevate your Process Management today!</p>  
   </div>  
   <div class="col-md-6 columnBlockLayout" style="flex-grow: 1; min-width: 310px; word-break: break-word; display: flex; flex-direction: column; margin: 25px 0px; padding: 16px;"><img src="/Registration/register.jpg" alt="" name="register.jpg" style="width: 100%; height: 295px; max-width: 100%; box-shadow: 0px 0px 6px rgb(0, 0, 0); border-radius: 5px;" /></div>  
  </div>  
 </div>  
 <div class="row sectionBlockLayout text-left" style="display: flex; flex-wrap: wrap; margin: 0px; min-height: auto; padding: 8px; background-image: linear-gradient(0deg, rgba(161, 159, 157, 0), rgba(161, 159, 157, 0)); border: 0px solid rgb(161, 159, 157); border-radius: 0px;">  
  <div class="container" style="padding: 0px; display: flex; flex-wrap: wrap;">  
   <!--the customized form section-->  
   <div class="col-md-12 columnBlockLayout" style="flex-grow: 1; display: flex; flex-direction: column; min-width: 310px; word-break: break-word; margin: 20px 0px; padding: 16px;">  
    <div class="mb-3" style="margin-bottom: 15px;"><label for="firstName">First Name</label><input type="text" id="firstName" class="form-control fldRequired" /></div>  
    <div class="mb-3" style="margin-bottom: 15px;"><label for="lastName">Last Name</label><input type="text" id="lastName" class="form-control fldRequired" /></div>  
    <div class="mb-3" style="margin-bottom: 15px;"><label for="email">Email</label><input type="text" id="email" class="form-control fldRequired" /></div>  
    <div class="mb-3" style="margin-bottom: 15px;"><label for="company">Company</label><input type="text" id="company" class="form-control" /></div>  
    <div class="mb-3"><button onclick="submitRegistration()" class="btn btn-primary">Submit</button></div>  
   </div>  
   <!--end customized form section-->  
  </div>  
 </div>  
 <!--error modal-->  
 <div class="modal fade" id="errorModal" tabindex="-1" role="dialog">  
  <div class="modal-dialog" role="document">  
   <div class="modal-content">  
    <div class="modal-header">  
     <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>  
     <h4 class="modal-title" id="errorModalLabel">...</h4>  
    </div>  
    <div class="modal-body" id="errorModalBody">  
     ...  
    </div>  
    <div class="modal-footer">  
     <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>  
    </div>  
   </div><!-- /.modal-content -->  
  </div><!-- /.modal-dialog -->  
 </div><!-- /.modal -->  

When you break this down, you see that there are 2 divs
  • a Data Entry div
    • we use this to gather the data from the visitor and then pass it along to the API endpoints
  • a Bootstrap Modal div
    • for messaging
It looks like this when the custom HTML is added




THE JAVASCRIPT FILE

The following JavaScript code provides the functionality we're looking for.  There is nothing too fancy here but it does include some required field validation and the accompanying visual styling manipulations.  
   console.log("initRegistrations started");  
   document.getElementById('email').addEventListener(  
     'change',   
     email_Change  
     );   
    
   document.querySelectorAll('.fldRequired').forEach(c => {  
     c.addEventListener('change', event => {  
       validateRequired(c);  
     });  
   });  
   
   setTimeout(() => {  
     document.querySelectorAll('.fldRequired').forEach(c => {  
       validateRequired(c);  
     });  
   }, 1000);  
     
     
     
   function email_Change(){  
     //some code here to see if that email is in play already  
     console.log("email_Change executed");  
     let payload = {};  
     payload.email = document.getElementById('email').value;  
   
     if (payload.email.length === 0){return;}  
   
     let url = "https://prod-84.westus.logic.azure.com:443/workflows/abc/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=123";  
     executeApiRequest(payload, url,   
       function(retVal){  
         if (retVal.length > 0){  
           document.getElementById('email').value = "";  
           validateRequired(document.getElementById("email"));  
           popModal("Duplicate Email", "The email you entered is already in use - please enter a different email address.");  
         }  
       },   
       function(error){},   
       true);  
   
   }  
   
   function executeApiRequest (parameters, url, successCallback, errorCallback, async) {  
     let uri = url;  
     let req = new XMLHttpRequest();  
     req.open("POST", uri, async);  
     req.setRequestHeader("Accept", "application/json");  
     req.setRequestHeader("Content-Type", "application/json; charset=utf-8");  
     req.setRequestHeader("Cache-Control", "no-cache'");  
   
     if (async) {  
       req.onreadystatechange = function () {  
         if (this.readyState === 4 /* complete */) {  
           req.onreadystatechange = null;  
           if (this.status === 200) {  
             let data = JSON.parse(this.response, dateReviver);  
             successCallback(data);  
           }  
           else {  
             errorCallback(this.responseText);  
           }  
         }  
       };  
       parameters ? req.send(JSON.stringify(parameters)) : req.send();  
     }  
     else {  
       parameters ? req.send(JSON.stringify(parameters)) : req.send();  
       if (req.status === 200 || req.status === 204) {  
         if (req.status === 200) {  
           let data = JSON.parse(req.response, dateReviver);  
           successCallback(data);  
         }  
       }  
       else {  
         errorCallback(this.responseText);  
       }  
     }  
   }  
   
   function dateReviver (key, value) {  
     if (typeof value === 'string') {  
       let a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);  
       if (a) {  
         return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]));  
       }  
     }  
     return value;  
   }  
   
   function validateRequired(c) {  
   
     if (c.value.length > 0) {  
       c.style.borderLeft = "5px solid green";  
     }  
     else {  
       c.style.borderLeft = "5px solid red";  
     }      
   }  
   
   function submitRegistration() {  
     console.log("submit button clicked");  
       
     let reg = {};  
     reg.FirstName = document.getElementById("firstName").value;  
     reg.LastName = document.getElementById("lastName").value;  
     reg.Email = document.getElementById("email").value;  
     reg.Company = document.getElementById("company").value;  
   
     let updateOk = true;  
   
     document.querySelectorAll(".fldRequired").forEach(c => {  
       if (window.getComputedStyle(c).borderLeft.indexOf("red") >= 0 ||  
         window.getComputedStyle(c).borderLeft.indexOf("255") >= 0) {  
         updateOk = false;  
       }  
     });  
   
     if (!updateOk){  
         
       popModal("Validation", "Fields marked in RED are required")  
       return;  
     }  
    
     let url = "https://prod-176.westus.logic.azure.com:443/workflows/abc/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=123";  
     //validations passed... create the registration record  
     executeApiRequest(reg, url,   
       function(retVal){  
         popModal("Success", "Registration Succeeded!<br/><br/>Be sure to check for the Confimration email.<br/>Stand by for automatic navigation to Home page.");  
         setTimeout(function(){  
           window.location.href = "https://iqueue.powerappsportals.com/";  
         }, 10000);  
       },   
       function(error){},   
       true);  
   
   }  
   
   function popModal(title, body){  
   
     document.getElementById("errorModalLabel").innerHTML = title;  
     document.getElementById("errorModalBody").innerHTML = body;  
     $('#errorModal').modal('show');  
   
   }  

  • When the Registration page loads
    • add an Event Listener to the email field so that we can fire the function to check for duplicates
    • add an Event Listener to all required fields so that we can provide the visual cues
    • do a validation to set the required field visual (the red left border)
  • email_Change
    • sends the email value entered by the user to the Email Validation Flow and processes the result
  • executeApiRequest
    • a reusable XmlHttpRequest function to call the API Flows
  • dateReviver
    • a help for executeApiRequest to generate friendly date formats
  • validateRequired
    • sets the left border for required fields
  • submitRegistration
    • gathers the Registration information and sends it to the Create Registraion Flow
  • popModal
    • populates and displays the notifications div

OF INTEREST - BOOTSTAP INTEGRATION

Microsoft includes BOTH Bootstrap CSS and Bootstrap Bundle JS.... so you can do a lot of cool things in the UI natively.  However, this is a 3.x version of the Bootstrap stack and it definitely works a bit differently than 4.x and 5.x versions.

An example of a Modal Bootstrap message



WRAP-UP

Power Pages gives you the flexibility to keep things simple when they are simple.... or to go hog wild when you need elevated levels of complexity.  

No comments:

Post a Comment

Microsoft Power Pages - Importing JS Libraries

  INTRODUCTION Microsoft Power Pages is a platform for developing web sites - from very simple... to surprisingly complex.  However, with ad...