Small improvements and customizations to the Power Pages Entity List.

Small improvements and customizations to the Power Pages Entity List.

In this article, I will take apart the structure of the page, which is publicly available, and in the development of which I was involved. Please take a look at it (FYI: you do not have access to the values of drop-down lists - sorry, this privilege is only available to authorized users):

Also, I apologize in advance for the abundance of code in this article. If you're too lazy to read it, you can take a quick look at all the code, quickly pay attention to the images, put Like the article (this is a must 😊), and run to check how this Entity List works on a live portal.

We've all used Entity List at least once in our work with portals. Some of us love them and some of us hate them, but it's a component that can often be useful. Yes, we can replace it with liquid, fetch, or custom code, but is it always justified?

The challenge for me and my team was to create a list with lots of data. Display it paginated, be able to apply a filter and reset filters, and display the amount of data found.

By default, Entity List doesn't provide much functionality. For example, we can't clear filters, we can't display the amount of data found, there's no way to jump to a record, and there's no way to manage styles.

In general, functionality out of the box is often very limited and looks pretty cheesy, but the task is approved and we need to solve it.

First, let's create a simple Entity List with vertical filtering, which will use a pre-prepared view: 

No alt text provided for this image
OOB Entity List

Looks very sad, doesn't it? But this can be fixed with the magic of CSS and JS!

Let's start with CSS. Let's add some styles that will adjust the appearance of the table, fonts, buttons, define styles for pagination and also add some elements for us, which we'll reuse a little later (next comes quite a lot of CSS code, which you can safely skip if it bores you, but after that, all the fun begins!).

#mainContent .col-md-12 .entitylist .row .col-md-3 
    width: 15% !important;
}


#mainContent .col-md-12 .entitylist .row .col-md-9 {
    width: 85% !important;
}


.xrm-editable-html .xrm-attribute-value h2 {
    padding: 0px !important;
    margin: 0px 0px 20px 0px !important;
    font-size: 20px !important;
    font-weight: normal !important;
    color: #122a58 !important;
}


#mainContent .col-md-12 .entitylist .row div.entity-grid.entitylist .view-toolbar.grid-actions.clearfix div.pull-right.toolbar-actions button {
    font-size: 12px !important;
    height: 32px !important;
    min-height: 32px !important;
    font-size: 12px;
}


#mainContent .col-md-12 .entitylist .row div.entity-grid.entitylist .view-toolbar.grid-actions.clearfix div.pull-right.toolbar-actions input {
    font-size: 12px !important;
    height: 32px !important;
    font-size: 12px;
    background-color: #fff;
    border: 1px solid #e3e6ec !important;
    padding-left: 10px !important;
}

#mainContent .col-md-12 .entitylist .row #entitylist-filters .entitylist-filter-option-group input {
    font-size: 12px !important;
    height: auto !important;
    min-height: 28px;
    font-size: 12px;
    background-color: #fff;
    border: 1px solid #e3e6ec !important;
    padding-left: 10px !important;
}


#mainContent .col-md-12 .entitylist .row .btn-entitylist-filter-submit {
    margin-right: 6px;
}


#mainContent .col-md-12 .entitylist .row #entitylist-filters .entitylist-filter-option-group select {
    font-size: 12px !important;
    height: auto !important;
    min-height: 28px;
    font-size: 12px;
    background-color: #fff;
    border: 1px solid #e3e6ec !important;
    padding-left: 10px !important;
}


#mainContent .col-md-12 .entitylist .row #entitylist-filters .entitylist-filter-option-group span.input-group-addon {
    height: 28px;
    min-width: 28px;
    padding: 0px;
}


#mainContent .col-md-12 .entitylist .row div.entity-grid.entitylist div#medicinalProductsALLTable_Info {
    margin-top: 0px;
    padding-top: 8px;
    white-space: nowrap;
    font-style: italic;
    color: #506d94 !important;
}


#mainContent .col-md-12 .entitylist .row div.entity-grid.entitylist .view-toolbar span.mpaDeleteIcon, #mainContent .col-md-12 .entitylist .row #entitylist-filters .entitylist-filter-option-group span.mpaDeleteIcon {
    position: relative;
    display: inline-flex;
    align-items: center;
    width: 100%;
}


#mainContent .col-md-12 .entitylist .row div.entity-grid.entitylist .view-toolbar span.mpaDeleteIcon span, #mainContent .col-md-12 .entitylist .row #entitylist-filters .entitylist-filter-option-group span.mpaDeleteIcon span {
        position: absolute;
        display: none;
        right: 3px;
        width: 20px;
        height: 20px;
        border-radius: 50%;
        color: #555;
        background-color: #eee;
        font: 13px monospace;
        text-align: center;
        line-height: 1em;
        cursor: pointer;
        z-index: 99999999;
        font-size: 16px;
        font-weight: bold;
}


#mainContent .col-md-12 .entitylist .row div.entity-grid.entitylist .view-toolbar span.mpaDeleteIcon input, #mainContent .col-md-12 .entitylist .row #entitylist-filters .entitylist-filter-option-group span.mpaDeleteIcon input{
        padding-right: 18px;
        box-sizing: border-box;
}


#mainContent .col-md-12 .entitylist .row .col-md-3 {
        width: 100% !important;
}


#mainContent .col-md-12 .entitylist .row .col-md-9 {
        width: 100% !important;
}


#mainContent .col-md-12 .entitylist .row div.entity-grid.entitylist {
        margin-top: unset;
}


@media (min-width: 700px) and (max-width: 1499px) {
    .container {
        width: 98vw !important;
    }

    .ema-rside .container {
        width: 100% !important;
    }
}


@media (min-width: 1500px) and (max-width:8900px) {
    .container {
        width: 98vw !important;
    }

    .ema-rside .container {
        width: 100% !important;
    }
}

/*----------Buttons-------*/
#mainContent .col-md-12 .entitylist .row .btn.btn-default {
	background-color: #3a5ee6 !important;
	border-color: #3a5ee6 !important;
	font-size: 12px;
	color: #fff
}
/*-----------Tables-----------*/

.table>thead>tr>th {
	border-bottom: 0px;
	color: #3a5ee6;
}

.table>thead>tr>th,
.table>thead>tr>td,
.table>tbody>tr>th,
.table>tbody>tr>td,
.table>tfoot>tr>th,
.table>tfoot>tr>td {
	border-top: 0px;
	word-break: break-all;
}
/*---------Pagination---------*/
body .pagination li a {
    padding: 6px 10px;
    border: none !important;
    height: 28px;
    line-height: 15px;
    font-weight: bold;
    color: #2962ed;
    border-radius: 4px !important;
    font-family: monospace;
}


body .pagination > .active > a, body .pagination > .active > a:focus, body .pagination > .active > a:hover {
    background-color: #f4f6f9 !important;
    color: #2962ed !important;
}


body .pagination > li > a, body .pagination > li > span {
    background-color: transparent !important;
}


body .pagination > .disabled > span, body .pagination > .disabled > span:hover, body .pagination > .disabled > span:focus, body .pagination > .disabled > a, body .pagination > .disabled > a:hover, body .pagination > .disabled > a:focus {
    color: #c5c5c5 !important;
}


body .pagination > .paginate_button.previous a:before {
    content: "<"
}

And here's the result. Don't you agree, it's much nicer now?

No alt text provided for this image
Entity List with custom styles

You can put this code directly into the Entity List additional (code), or (I highly recommend) you can put it as a separate web file and attach it to your portal page.

Now let's improve the functionality a little bit. Let's add four changes:

  1. a clearing button for each filter individually;
  2. a button to clear all filters;
  3. a button for clearing the global search filter;
  4. counting the number of lines in the list.

Similarly, the code we write can be used directly in Entity List, or it can be put into a web file.

1) Let's create a clear button for each filter individually by implementing a little code:

function removeDataFromFilterField() {
	// via jQuery get entity list filters
	$('#mainContent .col-md-12 .entitylist .row #entitylist-filters .entitylist-filter-option-group input') 
		// use wrap function for wrap an HTML structure around each element 
		.wrap('<span class="mpaDeleteIcon"></span>')
		// add (x) in the field if we have some value	
		.after($('<span>x</span>').click(function() {	
			$(this).prev('input').val('').trigger('change').focus();
		}));
}
No alt text provided for this image
The button with the cross is displayed in the field with the filter only when there is any data in the field

2) Let's create a button to clear all filters. I would like to draw your attention to a rather interesting indication of filter element identifiers. To generate their identifiers PowerPages just use an integer number, from 0 to the last element, but if we have drop-down lists, like here:

No alt text provided for this image

then the filters will have identifiers #0, #dropdown_1, #2, dropdown_3, #4, #5, etc. Look:

No alt text provided for this image

Also, I want to remind you that, unfortunately, there is no option that exists in OOB to clear the filters applied to the entity list. But we can add the script below to the entity list so that we have that capability:

// .btn-entitylist-filter-submit identifies the "Filter Results" button and add the Reset button after it
// Note: If you want to insert the button before "Filter Results" button, then you can replace the insertAfter function with insertBefore function.

$('<button type="button" class="btn btn-link btn-clear" style="">Reset</button>')
    // set action
    .insertAfter('.btn-entitylist-filter-submit').on("click", function() {
        $('#entitylist-filters select').val('');
        textFilterPositions = Array.from(Array(10).keys()); 
        for (i = 0; i < textFilterPositions.length; i++) {
            ResetFilter(textFilterPositions[i], '');
        }
    });

// reset current field
function ResetFilter(field, resetVal) {
    $("#" + field).val(resetVal);
}
No alt text provided for this image
Reset button for all filters

3) Next, we'll create a clearing button for the global search filter. Specify its location and immediately add the function of clearing the field itself:

$('#mainContent .col-md-12 .entitylist .row div.entity-grid.entitylist .view-toolbar input')
        .after($('<div style="display:none" class="input-group-btn" id="clearSearch" role="presentation">
<button type="button" id="clearSearchBtn" class="btn btn-default">
<span class="fa fa-remove"></span></button></div>').click(function() {
            $(this).prev('input').val('').trigger('change').focus();
            $('.btn-entitylist-filter-submit').click();
        }));
No alt text provided for this image
The button with the cross is displayed in the global filter only when there is any data in the field

4) And finally, let's add the most interesting detail: counting the number of values in the list, but only so that this number varies depending on the filtering. Here we can use the magic of JS and, in particular, intercept intermediate values and embed code in the query:

function RowCount() {
    $("#ID_YOUR_TABLE").remove();
    var rows = $('#mainContent .col-md-12 .entitylist .row div.entity-grid.entitylist .view-grid table tr').length - 1;
    $("#mainContent .col-md-12 .entitylist .row div.entity-grid.entitylist").append("<div class=\"dataTables_info\" id=\"ID_YOUR_TABLE\" >Total <b>" + rows + "</b> entries</div>");
}


/**
 * 
 * Script for count no of row in table on all events like list,search and filter
 */
$("#mainContent .col-md-12 .entitylist .row div.entity-grid.entitylist").append("<div class=\"dataTables_info\" id=\"ID_YOUR_TABLE\" ></div>");


(function(XHR) {
    "use strict";


    var open = XHR.prototype.open;
    var send = XHR.prototype.send;
    XHR.prototype.open = function(method, url, async, user, pass) {
        this._url = url;
        open.call(this, method, url, async, user, pass);
    };
    XHR.prototype.send = function(data) {
        var self = this;
        var oldOnReadyStateChange;


        function onReadyStateChange() {
            if (self.readyState == 4 /* complete */ ) {
                /* This is where you can put code that you want to achive somthing after any api response*/
                /* URL is kept in this._url */
                if (this._url.includes('entity-grid-data.json')) {
					
                    var d = document.getElementById("ID_YOUR_TABLE");
                    if (d != null && d != undefined) {
                        d.innerHTML = "Total " + (JSON.parse(this.response).ItemCount) + " entries";
                    }
                }
            }


            if (oldOnReadyStateChange) {
                oldOnReadyStateChange();
            }
        }


        /* Set xhr.noIntercept to true to disable the interceptor for a particular call */
        if (!this.noIntercept) {
            if (this.addEventListener) {
                this.addEventListener("readystatechange", onReadyStateChange, false);
            } else {
                oldOnReadyStateChange = this.onreadystatechange;
                this.onreadystatechange = onReadyStateChange;
            }
        }


        send.call(this, data);
    }
})(XMLHttpRequest);
No alt text provided for this image
Each time the value of this field is recalculated depending on the query (with filters) that was generated

FULL JS CODE:

$(document).ready(function() 
    setTimeout(() => {
        SearchFieldBeauty();
        removeDataFromFilterField();
    }, 2000);
});


// functionality for main search field
function SearchFieldBeauty() {
    $('#mainContent .col-md-12 .entitylist .row div.entity-grid.entitylist .view-toolbar input')
        .after($('<div style="display:none"  class="input-group-btn" id="clearSearch" role="presentation"><button type="button" id="clearSearchBtn" class="btn btn-default"><span class="fa fa-remove"></span></button></div>').click(function() {
            $(this).prev('input').val('').trigger('change').focus();
            $('.btn-entitylist-filter-submit').click();
        }));


    $("#mainContent .col-md-12 .entitylist .row div.entity-grid.entitylist .view-toolbar input").on('change keyup paste mouseup', function() {
        if ($(this).val() && $(this).val().length > 0) {
            var p = $(this).parent()[0];
            $(p).children()[1].style.display = 'table-cell'
            $(p).children()[2].style.display = 'none'
        } else {
            var p = $(this).parent()[0]
            $(p).children()[1].style.display = 'none'
            $(p).children()[2].style.display = 'table-cell'
        }
    });
}


function removeDataFromFilterField() {
    // method for removing value from the filter field
    $('#mainContent .col-md-12 .entitylist .row #entitylist-filters .entitylist-filter-option-group input')
        .wrap('<span class="mpaDeleteIcon"></span>')
        .after($('<span>x</span>').click(function() {
            $(this).prev('input').val('').trigger('change').focus();
        }));
}


// behavior for data in search-field. If we have some data - then we show cross
$("#mainContent .col-md-12 .entitylist .row input")
    .on('change keyup paste mouseup', function() {
        if ($(this).val() && $(this).val().length > 0) {
            var p = $(this).parent()[0];
            $(p).children('span').show();
        } else {
            var p = $(this).parent()[0]
            $(p).children('span').hide()
        }
    });


// reset current field
function ResetFilter(field, resetVal) {
    $("#" + field).val(resetVal);
}


function RowCount() {
    $("#ID_YOUR_TABLE").remove();
    var rows = $('#mainContent .col-md-12 .entitylist .row div.entity-grid.entitylist .view-grid table tr').length - 1;
    $("#mainContent .col-md-12 .entitylist .row div.entity-grid.entitylist").append("<div class=\"dataTables_info\" id=\"ID_YOUR_TABLE\" >Total <b>" + rows + "</b> entries</div>");
}


// add new reset button on the UI and functionality for it
$('<button type="button" class="btn btn-default" style="">Reset</button>')
    .insertAfter('.btn-entitylist-filter-submit').on("click", function() {
        $('#entitylist-filters select').val('');
        textFilterPositions = Array.from(Array(10).keys()); // array from 0 to 10
        for (i = 0; i < textFilterPositions.length; i++) {
            ResetFilter(textFilterPositions[i], '');
        }
        // remove cross on the filters
        $('#mainContent .col-md-12 .entitylist .row #entitylist-filters .entitylist-filter-option-group input').next('span').remove();
        $('#mainContent .col-md-12 .entitylist .row #entitylist-filters .entitylist-filter-option-group input').unwrap();


        // call again
        removeDataFromFilterField();
        // clear Search field
        $('#clearSearchBtn').click();
        // get list again
        $('.btn-entitylist-filter-submit').click();
    });


/**
 * 
 * Script for count no of row in table on all events like list,search and filter
 */
$("#mainContent .col-md-12 .entitylist .row div.entity-grid.entitylist").append("<div class=\"dataTables_info\" id=\"ID_YOUR_TABLE\" ></div>");


(function(XHR) {
    "use strict";

    var open = XHR.prototype.open;
    var send = XHR.prototype.send;
    XHR.prototype.open = function(method, url, async, user, pass) {
        this._url = url;
        open.call(this, method, url, async, user, pass);
    };
    XHR.prototype.send = function(data) {
        var self = this;
        var oldOnReadyStateChange;


        function onReadyStateChange() {
            if (self.readyState == 4 /* complete */ ) {
                /* This is where you can put code that you want to achive somthing after any api response*/
                /* URL is kept in this._url */
                if (this._url.includes('entity-grid-data.json')) {
					
                    var d = document.getElementById("ID_YOUR_TABLE");
                    if (d != null && d != undefined) {
                        d.innerHTML = "Total " + (JSON.parse(this.response).ItemCount) + " entries";
                    }
                }
            }


            if (oldOnReadyStateChange) {
                oldOnReadyStateChange();
            }
        }


        /* Set xhr.noIntercept to true to disable the interceptor for a particular call */
        if (!this.noIntercept) {
            if (this.addEventListener) {
                this.addEventListener("readystatechange", onReadyStateChange, false);
            } else {
                oldOnReadyStateChange = this.onreadystatechange;
                this.onreadystatechange = onReadyStateChange;
            }
        }


        send.call(this, data);
    }
})(XMLHttpRequest);{

RESULT:

No alt text provided for this image

Thus, our Entity List was transformed from a boring box version into a beautiful list of items with the ability to filter and count rows. In the same way, you can add to it other functionality, and other styles - all it takes is imagination!

Thank you for your attention and good luck with your development!

Elkin Leguizamon Camargo

Estudiante en Fundación Universitaria Panamericana

10mo

como siempre excelente trabajo, estoy implementando entidades virtuales de FO sin configuración de LCS si no siguiendo este paso a paso, "https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/power-platform/admin-reference" no he logrado aplicar las relaciones ni que funcionen los filtros a nivel de lookup, cualquier guia seria lo maximo, como simpre gracias y exemente trabajo

Like
Reply
Francesco Musso

Freelance Power Platform Solution Architect & Functional Consultant specialising in Power Pages (formerly Power Apps Portals)

2y

Excellent article, thanks for sharing! I find myself implementing the clear search and clear filter functionality on every single project. Lots of other nice additions here too. Great work :)

To view or add a comment, sign in

Insights from the community

Others also viewed

Explore topics