Implementing GC Web (government of Canada) theme. Part 5: let’s add a custom form, GC Web – style

By | August 14, 2024

In the previous post, I wrote about how to create a GC Web-based web template to render tables in Power Pages. The idea was to discuss localizations next, but let’s take a de-tour and talk about custom forms first.

Why would we even do such a thing if there are out of the box basic forms in Power Pages?

Mostly, it’s because those out of the box ones use a slightly different approach to displaying validation summaries, required fields, “*” character next to the required field, etc. All of this may look like minor differences, but I would rather not get involved in the accessibility discussions later on, so, assuming GC Web has been “pre-approved”, one sure way to avoid such discussions would be to say that our Power Pages implementation follows GC Web theme “standards”.

Let’s see what’s involved into custom rendering, even though, ultimately, you may as well decide not to do it and rely on the out of the box basic forms.

Have a look at the link below to see how GC Web form validation is supposed to behave:

https://wet-boew.github.io/gcweb-compiled-demos/wetboew-demos/formvalid/formvalid-en.html

Also, have a look at the “scaffolding” link for various HTML component in GC Web to see how those are supposed to be rendered in HTML:

https://wet-boew.github.io/GCWeb/common/scaffolding/forms.html

One obvious disadvantage of using custom forms is that we have to do all the “plumbing” ourselves, since we have to take care of all the CRUD operations (creating data, reading data, updating data, deleting data).

Which is where we’ll be using Web API:

https://learn.microsoft.com/en-us/power-pages/configure/web-api-overview

With that, let’s start setting things up.

1. We’ll need progress bar indicator/spinner, so let’s add required HTML

Add the following code to the bottom of the GC Web Header web template. This will be used later once we start making Web API calls:

<div id="itaProgressIndicator" class="ita-progressbar-indicator">
   <img src="/spinner.gif"/>
   <div id="itaProgressIndicatorMessage"></div>
</div>

2. HTML code above is referencing spinner.gif, so let’s add that file, too

There are lots of spinners you can download for free. For example, have a look here: https://pixabay.com/gifs/search/loading/

Download a spinner in gif format, save it as “spinner.gif”, then create a web file in Power Pages right under your “Home” page:

3. Let’s add a stylesheet for the required styles

You’ll need this style to be added there:

.ita-progressbar-indicator {

    padding-top: 10px;
    z-index: 9999;
    -webkit-border-radius: 0 0 2px 2px;
    border-radius: 0 0 2px 2px;
    -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
    background-color: #ffffff;
    border: 1px solid #aaaaaa;
    
    position: fixed;
    display: none;
    
    width: 250px;
    height: 100px;
    left:0; right:0;
    top:0; bottom:0;
    margin:auto;
    max-width:100%;
    max-height:100%;
    overflow:auto;

    text-align:center;
}
.ita-progressbar-indicator img {
    width:52px;
    height:52px;
}

Create a file somewhere, add css “code” from the above, then create a Web File in Power Pages and call it ita_styles.css

You will also need to reference that stylesheet file from the Head/Bottom snippet. Add the following line to the bottom of that snippet:

<link rel="stylesheet" href="/ita_style.css" />

4. Now let’s add a wrapper script for Web API

It’s going to be a slightly extended version of the script you can also find here: https://learn.microsoft.com/en-us/power-pages/configure/webapi-how-to

(function( itaWebApi, $) {
    

    itaWebApi.languageCode = "",

    //itaWebApi.fillDropDown("datatype", "dataTypes", null);
    itaWebApi.fillDropDown = function(controlId, optionSetName, selectedId) {
      var optionSets = {
        dataTypes:[

          {Title_En: "Not Provided", Title_Fr: "Non-fourni", Id: null},
          {Title_En: "Car", Title_Fr: "Voiture", Id: '22a7b9c7-2153-ef11-bfe3-0022486d709c'},
          {Title_En: "Truck", Title_Fr: "Camion", Id: '02e0c8d8-2153-ef11-bfe3-0022486d709c'}
          
        ]

      };

      $.each(optionSets[optionSetName], function (i, item) {
        $('#'+controlId).append($('<option>', { 
            value: item.Id,
            text : itaWebApi.languageCode == "en" ? item.Title_En : item.Title_Fr,
            selected: selectedId == item.Id
        }));
      });

    }

    itaWebApi.safeAjax = function(ajaxOptions) {
        var deferredAjax = $.Deferred();
        shell.getTokenDeferred().done(function(token) {
          // Add headers for ajax
          if (!ajaxOptions.headers) {
            $.extend(ajaxOptions, {
              headers: {
                "__RequestVerificationToken": token
              }
            });
          } else {
            ajaxOptions.headers["__RequestVerificationToken"] = token;
          }
          $.ajax(ajaxOptions)
            .done(function(data, textStatus, jqXHR) {
              validateLoginSession(data, textStatus, jqXHR, deferredAjax.resolve);
            }).fail(deferredAjax.reject); //ajax
        }).fail(function() {
          deferredAjax.rejectWith(this, arguments); // On token failure pass the token ajax and args
        });
        return deferredAjax.promise();
    }  

    itaWebApi.appAjax = function (processingMsg, ajaxOptions) {
      
      if(processingMsg != null)
      {
        if(processingMsg == "default")
        {
          if(itaWebApi.languageCode == "en")
          {
            processingMsg = "Processing...";
          }
          else 
          {
            processingMsg = "Traitement...";
          }
        }
        itaWebApi.progressIndicator.show(processingMsg);
      }
      return this.safeAjax(ajaxOptions)
        .fail(function(response) {
          itaWebApi.progressIndicator.hide();
          if (response.responseJSON) {
            alert("Error: " + response.responseJSON.error.message)
          } else {
            alert("Error: Web API is not available... ")
          }
        }).always(
            
            );
    }

    

    itaWebApi.deleteRecord = function(collectionName, recordObj) {
      var response = confirm("Are you sure, you want to delete \"" + recordObj.name + "\" ?");
      if (response == true) {
        this.appAjax('Deleting...', {
          type: "DELETE",
          url: "/_api/"+collectionName+"(" + recordObj.id + ")",
          contentType: "application/json",
          success: function(res) {
            table.removeRecord(recordObj);
          }
        });
      }
      return false;
    }

    itaWebApi.updateRecordAttribute = function(collectionName, col, recordObj) {
      var attributeName = col.name,
        value = recordObj[attributeName],
        newValue = prompt("Please enter \"" + col.label + "\"", value);
      if (newValue != null && newValue !== value) {
        this.appAjax('Updating...', {
          type: "PUT",
          url: "/_api/"+collectionName+"(" + recordObj.id + ")/" + attributeName,
          contentType: "application/json",
          data: JSON.stringify({
            "value": newValue
          }),
          success: function(res) {
            table.updateRecord(attributeName, newValue, recordObj);
          }
        });
      }
      return false;
    }

    itaWebApi.updateRecord = function(collectionName, recordObj) {
        this.appAjax('Updating...', {
          type: "PATCH",
          url: "/_api/"+collectionName+ "(" + recordObj.id + ")/" + attributeName,
          contentType: "application/json",
          data: recordObj,
          success: function(res) {
            //
          }
        });
      return false;
    }

    itaWebApi.createNewDataRecord = function(contactId, name, typeId) {
      this.appAjax('default', {
        type: "POST",
        url: "/_api/ita_itadatatables",
        contentType: "application/json",
        data: JSON.stringify ({
            "[email protected]": "/contacts(" + contactId + ")",
            "ita_name": name,
            "[email protected]": "/ita_italookuptables(" + typeId + ")"
            
        }),
        success: function(res, status, xhr) {
          itaWebApi.progressIndicator.hide();
          window.location.assign(redirectUrl + id);
        }
      });
    }

    itaWebApi.applyLocalizedLabels = function(labels)
    {
      if(typeof labels == "undefined") labels = itaLocalizedLabels;
      $("[ita-label]").each(function (i, item) {
        if($(item).prop("tagName") == "INPUT") $(item).val(labels[$(item).attr('ita-label')][itaWebApi.languageCode]);
        else $(item).text(labels[$(item).attr('ita-label')][itaWebApi.languageCode]);
      });
      $("[data-label]").each(function (i, item) {
        if(labels[$(item).attr('data-label')] != null)
        {  
          $(item).attr("data-label", labels[$(item).attr('data-label')][itaWebApi.languageCode]);
        }
      });
    }

    itaWebApi.disableMain = function (isDisabled)
    {
        if (isDisabled) {
            $('main :input').attr('disabled', true);
        } else {
            $('main :input').removeAttr('disabled');
        }   
      
    }

    itaWebApi.progressIndicator = (function() {
      var $progressIndicatorMessage = $('#itaProgressIndicatorMessage'),
          $progressIndicator = $('#itaProgressIndicator'),
        _msg = 'Processing...',
        _stack = 0,
        _endTimeout;
      return {
        show: function(msg) {
          $progressIndicatorMessage.text(msg || _msg);
          if (_stack === 0) {
            clearTimeout(_endTimeout);
            itaWebApi.disableMain(true);
            $progressIndicator.show();
            
          }
          _stack++;
        },
        hide: function() {
          _stack--;
          if (_stack <= 0) {
            _stack = 0;
            clearTimeout(_endTimeout);
            _endTimeout = setTimeout(function() {
              itaWebApi.disableMain(false);
              $progressIndicator.hide();
            }, 500);
          }
        }
      }
    })();
   

}( window.itaWebApi = window.itaWebApi || {}, jQuery));

Same as before, create a web file for the script:

And you’ll need to reference that script, so add the code below to the bottom of GC Web Footer template:

<script src="/ita_webapi.js"></script>

<script>
  {% substitution %}
  itaWebApi.languageCode = '{{website.selected_language.code}}';
  {% endsubstitution %}
</script>

Notice how the code above will include that script, but, also, it will set languageCode property of the itaWebApi object.

5. Also, let’s remove bootstrap 5 and basic portal themes

There are some additional clarificationshere: https://www.itaintboring.com/power-platform/power-pages-what-if-you-did-not-want-to-use-bootstrap5/

GC Web is compatible with bootstrap 3, not with bootstrap 5; and, besides, that’ prtalbasictheme is playing a bit of a havoc with GC Web styling, so… The code below goes to the bottom of GC Web Footer template, too:

$(document).ready(function(){
  
  $("link[href*=BootstrapV5").remove();
  $("script[src*=BootstrapV5").remove();
  $("link[href='/portalbasictheme.css'").remove();
  $("link[href='/theme.css'").remove();
  
});

</script>

6. ANd, finally, let’s add a form

First of all, in order for GC Web validations to work for the form, we’ll need to add a div element somewhere and use wm-frmvld css class for that element. Let’s add it to the GC Web Template, here is how the template looks as a result:

<main property="mainContentOfPage" class="container" resource="#wb-main" typeof="WebPageElement">
<dib class="wb-frmvld">
    {% include 'Page Copy' %}

    {% if page.adx_entitylist %}
    {% include 'entity_list' key: page.adx_entitylist.id %}
    {% endif %}

    {% if page.adx_entityform %}
    {% entityform id: page.adx_entityform.id %}
    {% endif %}

    {% if page.adx_webform %}
    {% webform id: page.adx_webform.id %}
    {% endif %}
</div>
<!-- Main content footer starts -->
	<section class="pagedetails">
		<h2 class="wb-inv">Page details</h2>
		<dl id="wb-dtmd">
			<dt>Date modified:</dt>
			<dd><time property="dateModified">2024-07-24</time></dd>
		</dl>
	</section>
<!-- Main content footer ends -->
</main>

Next, we need the form itself. You can organize it the way you want – add a snippet/template and include it into your content pages, or put that form right in the content page (but you’ll have to copy almost everything for both English and French). Either way, there is an example below just to illustrate:

{% if  pre_page_block %}

{% else %}

{% fetchxml dataTypes %}
<fetch version="1.0" mapping="logical" distinct="true">
   <entity name="ita_italookuptable">
      <attribute name="ita_italookuptableid"></attribute>
      <attribute name="ita_labelen"></attribute>
      <attribute name="ita_labelfr"></attribute>
   </entity>
</fetch>
{% endfetchxml %}
<form action="#" method="post" id="ita_data_form" novalidate>

        <legend>Data record information</legend>
        <div class="form-group">
            <label for="dataname" class="required"><span class="field-name">Data name</span> <strong class="required">(required)</strong></label>
            <input class="form-control" id="dataname" name="dataname" type="text" autocomplete="dataname" required="required" data-rule-minlength="2" />
        </div>
        <div class="form-group">
            <label for="datatype" class="required"><span class="field-name">Data Type</span> <strong class="required">(required)</strong></label>
            <select class="form-control" id="datatype" name="datatype" autocomplete="datatype" required="required">
            <option value>Select Data Type</option>
            {% for record in dataTypes.results.entities %} 
            <option value="{{record.Id}}">{{record['ita_labelen']}}</option>
            {% endfor %}
            
            </select>
        </div>

    <input type="submit" value="Submit" class="btn btn-primary" /> <input type="reset" value="Reset page to defaults" class="btn btn-link btn-sm show p-0 mrgn-tp-md" />
</form>



<script>
   $(document).ready(function(){
       document.getElementById("ita_data_form").addEventListener("submit", submitForm);
   });

   function submitForm(e){
       
       var validator = $("#ita_data_form").validate();
       if(validator.form())
       {
        debugger;
          itaWebApi.createNewDataRecord('{{User.Id}}', $('#dataname').val(), $('#datatype').val());
       }
       e.preventDefault();
       return false;
   }

</script>


{% endif %}

There are a few things to consider:

  • When rendering the form, we can use FetchXml to read existing/default data and put it into the input elements
  • However, when creating/updating underlying data, we have to use Web API

So you will also need to create required Table Permissions and assign them to the correct web roles.

To create a record, you can use this kind of code, for instance – it’ll add submit event listener to the form, and, when the form is being submitted, it’ll invoke validations, and, if they pass, it’ll call itaWebApi to create a new data record (and, then, you can scroll up and see what will be happening in that script):

I may have skipped a few things, let me know if I missed to explain some steps, but, ultimately, here is the end result:

With that, it seems, we can get back to the localizations (English & French) and talk about that a bit more in the next part.

To be continued…

Leave a Reply

Your email address will not be published. Required fields are marked *