ENGINEERED CODE BLOG

Power Pages: Adding a Custom Form Modal to List Action Buttons

Recently there was a comment on my blog Power Apps Portals: Related Entity as Source of Next Web Form Step about adding a custom Edit button to a list or subgrid. In this case, rather than editing the main row in the table, they wanted to edit a row that was related to the main row. If you want the edit form to pop up in a modal, it requires a bit of JavaScript. In this post, I’ll go through what you need to do in order to achieve this functionality.

Bootstrap Version

Before we get into the code, I did want to mention that this post covers how to do it with Bootstrap v3. I’m guessing that Bootstrap v5 will be similar, but I haven’t done it for that yet. Perhaps a topic for a future post.

Add Custom Action Button

First, we need to add our custom button. I’m going to assume that you’ve got other action buttons already in the list. This is helpful because then you don’t have to create the column that contains the buttons. If you don’t, I’d recommend adding a dummy button, and then hiding it using JavaScript. I don’t think adding the column yourself is worth it, especially if additional buttons might be added in the future.

The List functionality in Power Pages displays data one page at a time. As a user navigates through the pages, the grid refreshes without an entire page refresh. So, if we want to use JavaScript to modify how the list behaves, we can’t just run it when the page loads. Instead, we need to run our code each time a single page of data is shown. Thankfully Power Pages gives us a “loaded” event exactly for this. We wait until the whole page is ready, then we can attach to the event:

$(document).ready(function () {    
      $(".entitylist.entity-grid").on("loaded", function () {
            //this fires for each new page of data in the list
      });
});  

Within this event, we can then get all the rows in the body of the table, and loop around them and add a custom button to the action button dropdown. To determine the markup for an action button, I used the browser developer tools to inspect an out-of-the-box button to see that it looks something like:

<li role="none">
      <a class="edit-link launch-modal" role="menuitem" tabindex="-1" href="#" title="Edit" data-entityformid="ddb0ab5b-71d7-ec11-a7b5-002248ad8957" aria-setsize="2" aria-posinset="1">
            <span class="fa fa-edit fa-fw" aria-hidden="true"></span> Edit
      </a>
</li>

To create our custom button, I’ll use that markup, but I’ll leave out the data-entityformid attribute, and the launch-modal class (so that our button doesn’t get confused with an out-of-the-box button).

Let’s also add a click event handler to the button, which is where our code that creates the modal will go.

We can use the following code to add the button to the existing dropdown (which has a class of “dropdown-menu”) and handles the click event (this goes into the loaded event):

$(this).children(".view-grid").find("tbody tr").each(function (i, e){
      $("<li role=\"none\"><a role=\"menuitem\" tabindex=\"-1\" href=\"#\" title=\"Edit Custom\" aria-setsize=\"2\" aria-posinset=\"1\"><span class=\"fa fa-edit fa-fw\" aria-hidden=\"true\"><\/span> Edit Custom<\/a><\/li>").appendTo($(this).find('.dropdown-menu')).find('a').on('click', function () {      
            // custom button click event handler
      });
});

Open the Modal

Our goal is to replicate the out-of-the-box experience as much as possible. So, to figure out how to do it, I investigated how a normal edit button manages to open a modal.

The short answer is it uses the Bootstrap v3 Modal component, and uses an iframe to display the form. We’ll use the same technique.

Using my browser’s developer tools, I got the HTML for the out-of-the-box modal. It looks something like:

<div class="modal fade modal-form modal-form-edit" role="dialog" tabindex="-1">
      <div class="modal-dialog modal-lg">
            <div class="modal-content">
                  <div class="modal-header">
                        <button class="close" data-dismiss="modal" type="button"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
                        <h4 class="modal-title"><span class="fa fa-edit" aria-hidden="true"></span> Edit</h4>
                  </div>
                  <div class="modal-body">
                        <div class="form-loading">                            
                              <span class="fa fa-spinner fa-spin fa-4x" aria-hidden="true"></span>                            
                        </div>
                        <iframe data-page="/_portal/modal-form-template-path/33fb1ebf-5d75-ec11-8942-0022483bc1a1" src="/_portal/modal-form-template-path/33fb1ebf-5d75-ec11-8942-0022483bc1a1?id=676fabd7-71d7-ec11-a7b5-002248ad8957&amp;entityformid=ddb0ab5b-71d7-ec11-a7b5-002248ad8957&amp;languagecode=1033" title=" Edit"></iframe>
                  </div>
            </div>
      </div>
</div>

A pretty straight-forward Bootstrap v3 modal. Within the body, you’ll see there is a loading spinner, and then the iframe. Let’s look at the iframe src:

/_portal/modal-form-template-path/33fb1ebf-5d75-ec11-8942-0022483bc1a1?id=676fabd7-71d7-ec11-a7b5-002248ad8957&entityformid=ddb0ab5b-71d7-ec11-a7b5-002248ad8957&languagecode=1033

It points to a special page that is meant for embedding forms within a modal. This special pages doesn’t have the header and footer of the site. We can use this page for our solution.

There are a few IDs within this URL. These are all IDs for rows in Dataverse, including the website, the row we want to edit, and which form we want to load:

/_portal/modal-form-template-path/[website id]?id=[row id]&entityformid=[form id]&languagecode=1033

So we need to be able to set these values appropriate for the iframe to load correctly.

We can use the website Liquid object to get the ID of the website. Generally you know which form you want to use, so we’ll hardcode that. And we can get the ID of the row in the list from the data-id attribute of the <tr> element in the table using code like:

var tr = $(this);
var rowId = tr.data('id');

But, if you’ll remember, we don’t want to show a form for the row in the list, but for a row related to that row. So let’s use the Web API to get the ID of a row related via a lookup, and then use this to craft the URL for the iframe.

So, in the click event handler for my custom button, we need to add the HTML for the modal to the DOM, use the Web API to get the ID of the related row to set the src for the iframe, and then open the modal. We’ll add the modal HTML within the wrapper div for the list, which has a class of entity-grid. I’ll explain why later. Our event handler looks something like:

var modal = $("<div class=\"modal fade modal-form modal-form-edit\" role=\"dialog\" tabindex=\"-1\">\r\n\t<div class=\"modal-dialog modal-lg\">\r\n\t\t<div class=\"modal-content\">\r\n\t\t\t<div class=\"modal-header\">\r\n\t\t\t\t<button class=\"close\" data-dismiss=\"modal\" type=\"button\"><span aria-hidden=\"true\">\u00D7<\/span><span class=\"sr-only\">Close<\/span><\/button>\r\n\t\t\t\t<h4 class=\"modal-title\"><span class=\"fa fa-edit\" aria-hidden=\"true\"><\/span> Edit<\/h4>\r\n\t\t\t<\/div>\r\n\t\t\t<div class=\"modal-body\">\r\n\t\t\t\t<div class=\"form-loading\">\t\t\t\t\t\r\n\t\t\t\t\t<span class=\"fa fa-spinner fa-spin fa-4x\" aria-hidden=\"true\"><\/span>\t\t\t\t\t\r\n\t\t\t\t<\/div>\r\n\t\t\t\t<iframe data-page=\"\/_portal\/modal-form-template-path\/{{ website.id }}\" src=\"\" title=\" Edit\"><\/iframe>\r\n\t\t\t<\/div>\r\n\t\t<\/div>\r\n\t<\/div>\r\n<\/div>").appendTo(tr.closest('.entity-grid'));
var iframe = modal.find('iframe');                    

webapi.safeAjax({
      type: "GET",
      url: `/_api/new_yourtable(${rowId})?$select=_new_yourlookup_value`,
      contentType: "application/json",
      success: function (res, status, xhr) {
            var relatedId = res['_new_yourlookup_value'];
            var basicFormId = '153a4312-71d0-ec11-a7b5-000d3a84ae64';
            var src = `\/_portal\/modal-form-template-path\/{{ website.id }}?id=${relatedId}&entityformid=${basicFormId}&languagecode=1033`;
            iframe.attr('src', src);
      }
});

modal.modal();

Don’t forget to enable the Web API for your table if it isn’t already! We also need to include the webapi.safeAjax function provided by Microsoft.

Next, we need to write the JavaScript to handle the hide/show of the loading spinner. The modal HTML has the markup for it, but we need to make that happen. So when the modal is first shown, we want to show the spinner (and hide the form). And when the form finally loads, hide the spinner and show the form.

Also, let’s cleanup the modal by removing it from the DOM when the modal is closed. Our event handler code now looks like:

var modal = $("<div class=\"modal fade modal-form modal-form-edit\" role=\"dialog\" tabindex=\"-1\">\r\n\t<div class=\"modal-dialog modal-lg\">\r\n\t\t<div class=\"modal-content\">\r\n\t\t\t<div class=\"modal-header\">\r\n\t\t\t\t<button class=\"close\" data-dismiss=\"modal\" type=\"button\"><span aria-hidden=\"true\">\u00D7<\/span><span class=\"sr-only\">Close<\/span><\/button>\r\n\t\t\t\t<h4 class=\"modal-title\"><span class=\"fa fa-edit\" aria-hidden=\"true\"><\/span> Edit<\/h4>\r\n\t\t\t<\/div>\r\n\t\t\t<div class=\"modal-body\">\r\n\t\t\t\t<div class=\"form-loading\">\t\t\t\t\t\r\n\t\t\t\t\t<span class=\"fa fa-spinner fa-spin fa-4x\" aria-hidden=\"true\"><\/span>\t\t\t\t\t\r\n\t\t\t\t<\/div>\r\n\t\t\t\t<iframe data-page=\"\/_portal\/modal-form-template-path\/{{ website.id }}\" src=\"\" title=\" Edit\"><\/iframe>\r\n\t\t\t<\/div>\r\n\t\t<\/div>\r\n\t<\/div>\r\n<\/div>").appendTo(tr.closest('.entity-grid'));
var iframe = modal.find('iframe');                    

webapi.safeAjax({
      type: "GET",
      url: `/_api/new_yourtable(${rowId})?$select=_new_yourlookup_value`,
      contentType: "application/json",
      success: function (res, status, xhr) {
            var relatedId = res['_new_yourlookup_value'];
            var basicFormId = '153a4312-71d0-ec11-a7b5-000d3a84ae64';
            var src = `\/_portal\/modal-form-template-path\/{{ website.id }}?id=${relatedId}&entityformid=${basicFormId}&languagecode=1033`;
            iframe.attr('src', src);
      }
});

iframe.on("load", function() {
      modal.find(".form-loading").hide();
      modal.find("iframe").contents().find("#EntityFormControl").show()
});
modal.find(".form-loading").show();
iframe.contents().find("#EntityFormControl").hide();
modal.on("hide.bs.modal", function(e) {
      modal.remove();
});
modal.modal();

Putting everything together gets us:

$(document).ready(function () {    
      $(".entitylist.entity-grid").on("loaded", function () {
            $(this).children(".view-grid").find("tbody tr").each(function (i, e){
                  var tr = $(this);
                  var rowId = tr.data('id');
                  $("<li role=\"none\"><a role=\"menuitem\" tabindex=\"-1\" href=\"#\" title=\"Edit Custom\" aria-setsize=\"2\" aria-posinset=\"1\"><span class=\"fa fa-edit fa-fw\" aria-hidden=\"true\"><\/span> Edit Custom<\/a><\/li>").appendTo($(this).find('.dropdown-menu')).find('a').on('click', function () {                        
                        var modal = $("<div class=\"modal fade modal-form modal-form-edit\" role=\"dialog\" tabindex=\"-1\">\r\n\t<div class=\"modal-dialog modal-lg\">\r\n\t\t<div class=\"modal-content\">\r\n\t\t\t<div class=\"modal-header\">\r\n\t\t\t\t<button class=\"close\" data-dismiss=\"modal\" type=\"button\"><span aria-hidden=\"true\">\u00D7<\/span><span class=\"sr-only\">Close<\/span><\/button>\r\n\t\t\t\t<h4 class=\"modal-title\"><span class=\"fa fa-edit\" aria-hidden=\"true\"><\/span> Edit<\/h4>\r\n\t\t\t<\/div>\r\n\t\t\t<div class=\"modal-body\">\r\n\t\t\t\t<div class=\"form-loading\">\t\t\t\t\t\r\n\t\t\t\t\t<span class=\"fa fa-spinner fa-spin fa-4x\" aria-hidden=\"true\"><\/span>\t\t\t\t\t\r\n\t\t\t\t<\/div>\r\n\t\t\t\t<iframe data-page=\"\/_portal\/modal-form-template-path\/{{ website.id }}\" src=\"\" title=\" Edit\"><\/iframe>\r\n\t\t\t<\/div>\r\n\t\t<\/div>\r\n\t<\/div>\r\n<\/div>").appendTo(tr.closest('.entity-grid'));
                        var iframe = modal.find('iframe');                    
                        
                        webapi.safeAjax({
                              type: "GET",
                              url: `/_api/new_yourtable(${rowId})?$select=_new_yourlookup_value`,
                              contentType: "application/json",
                              success: function (res, status, xhr) {
                                    var relatedId = res['_new_yourlookup_value'];
                                    var basicFormId = '153a4312-71d0-ec11-a7b5-000d3a84ae64';
                                    var src = `\/_portal\/modal-form-template-path\/{{ website.id }}?id=${relatedId}&entityformid=${basicFormId}&languagecode=1033`;
                                    iframe.attr('src', src);
                              }
                        });                     
                        
                        iframe.on("load", function() {
                              modal.find(".form-loading").hide();
                              modal.find("iframe").contents().find("#EntityFormControl").show()
                        });
                        modal.find(".form-loading").show();
                        iframe.contents().find("#EntityFormControl").hide();
                        modal.on("hide.bs.modal", function(e) {
                              modal.remove();
                        });
                        modal.modal();
                  });
            });
      });
});   

(function (webapi, $) {
      function safeAjax(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();
      }
      webapi.safeAjax = safeAjax;
})(window.webapi = window.webapi || {}, jQuery);

When the Modal Closes

The above code handles opening the modal when the user clicks on our custom button. But what happens when the form is submitted? How do we make the modal close?

This is why I mentioned earlier that we added the modal markup within the list wrapper. Power Pages includes code to close the modal automatically once the form has been submitted. But it only looks for the modal within the list, so we need to make sure we have it in the right place.

Liquid Code Not Working?

If you’re trying to follow along, but you notice that the {{ website.id }} Liquid code doesn’t get executed, and instead you just see {{ website.id}} literally in the URL for the iframe, check out this blog post. It talks about how Liquid code within the Custom JavaScript on a list doesn’t get executed if the list is placed on the page using Liquid. So you might need to include your JavaScript on the web page, instead of on the list.

 

Leave a Reply

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

Contact

Engineered Code is a web application development firm and Microsoft Partner specializing in web portals backed by Dynamics 365 & Power Platform. Led by a professional engineer, our team of technology experts are based in Regina, Saskatchewan, Canada.