Creating an image gallery with tag filtering and pagination in Power Pages

In my previous blogpost I showed how to create a tag filtering and pagination features in Power Pages using FetchXML, Liquid, JavaScript, CSS and HTML.

This is not officially a continuation of that blogpost but I wanted to leverage those features to create this time an image gallery in Power Pages and also adding a download feature every time the user clicks on an image.

The business case this time will focus on the product catalog for the sales module. So let’s imagine that we want to make available to our customers the product catalog but showing an image of each product, and every time the user clicks on an image it will be downloaded.

Without further introduction let’s go to it.

Product Catalog

The first step would be to configure the product catalog, in my scenario I’ve created a few products:

And on each product I’ve created a note with an image attached:

I’m pretty sure at this point you would be wondering why store the image as a note attachment and not in SharePoint or any other file storage.

I want to retrieve the images using FetchXML and liquid, that’s why I store the images in Dataverse as note attachments, and it should be noted that the storage occupied by these images is not the same as the storage occupied by the records in Dataverse.

But let’s imagine you decide to store the images in SharePoint or Blob storage, the only thing that will change is that instead of using FetchXML to retrieve the images you will have to make an http request to a cloud flow, Azure function or an API to retrieve the images.

For this time let’s continue with the FetchXML method.

Web Page

Using the Portal Management Model Driven App, go to Web Pages and click on the +New button.

This is how I filled out the form:

That would be it for now in the web page, we will be back in a moment to continue with it.

Web Template

The next step is to create a web template, it will contain the FetchXML to retrieve all the notes that are documents and it will also contain the HTML needed to create the image gallery with the pagination.

Here is the code of the web template:

{% assign org_url = ‘https://’ | append: website.adx_primarydomainname %}
 
{% assign pageurl = page.url %}
 
{% if request.params[‘page’] == null %}
  {% assign page = 1 %}
{% else %}
  {% assign page = request.params[‘page’] | integer %}
{% endif %}
 
{% assign nextPage = page | plus: 1 | string %}
{% assign prevPage = page | minus: 1 | string %}
 
{% fetchxml getimages %}
<fetch version=‘1.0’ output-format=‘xml-platform’ mapping=“logical” page=“{{ request.params[‘page’] | default:1 }}” count=“3” distinct=“true”>
  <entity name=‘annotation’>
    <attribute name=‘subject’ />
    <attribute name=‘notetext’ />
    <attribute name=‘filename’ />
    <attribute name=‘annotationid’ />
    <attribute name=‘filesize’ />
    <attribute name=‘documentbody’ />
    <order attribute=‘subject’ descending=‘false’ />
    <filter type=‘and’>
      <condition attribute=‘isdocument’ operator=‘eq’ value=‘1’ />
    </filter>
    <link-entity name=‘product’ from=‘productid’ to=‘objectid’ link-type=‘inner’ alias=‘ac’ />
  </entity>
</fetch>
{% endfetchxml %}
 
<div class=“wrapper”>
  <!– filter Items –>
    <nav>
      <div class=“items”>
        <span class=“item active” data-name=“all”>All</span>
        <span class=“item” data-name=“bag”>Bag</span>
        <span class=“item” data-name=“shoe”>Shoe</span>
        <span class=“item” data-name=“watch”>Watch</span>
        <span class=“item” data-name=“camera”>Camera</span>
        <span class=“item” data-name=“headphone”>Headphone</span>
      </div>
    </nav>
    <!– filter Images –>
  <div class=“gallery” style=“width: 100%;”>
    {% for photo in getimages.results.entities %}    
      <div class=“image” data-name=“{{ photo.subject }}” onclick=preview(this)”>
        <span>
          <a id=“imgdownload {{ photo.subject }}” href=“data:image/jpg;base64,{{ photo.documentbody }}” download=“{{ photo.subject }}.jpg”>
            <img src=“data:image/jpg;base64,{{ photo.documentbody }}” alt=>
          </a>
        </span>
        <div>
          <p style=‘color:white;’>{{ photo.subject }}</p>
        </div>
      </div>
    {% endfor %}
  </div>
</div>
<!– Image preview box –>
<div class=“preview-box”>
  <div class=“details”><span class=“title”>Image Category: <p></p></span><span class=“icon fas fa-times”></span></div>
  <div class=“image-box”><img src=“” alt=“”></div>
</div>
<div class=“shadow”></div>
<!– Pagination –>
<br>
<div>
  <nav role=“navigation”>
    <ol class=“pagination”>
      {% if page > 1 %}
        <li>
          <a href=“{{ org_url | append: pageurl | append: ‘?page=’ | append: prevPage }}”>
            <span>&laquo;</span><span class=“visuallyhidden”>Prev</span>
          </a>
        </li>
      {% else %}
        <li class=“disabled”>
          <span>&raquo;</span><span class=“visuallyhidden”>Prev</span>
        </li>
      {% endif %}
 
      {% if getimages.results.more_records %}
        <li>
          <a href=“{{ org_url | append: pageurl | append: ‘?page=’ | append: nextPage }}”>
            <span class=“visuallyhidden”>Next</span><span>&raquo;</span>
          </a>
        </li>
      {% else %}
        <li class=“disabled”>
          <span class=“visuallyhidden”>Next</span><span>&raquo;</span>
        </li>
      {% endif %}
    </ol>
  </nav>
</div>

I will not explain the pagination part because that is part of the previous blogpost, but let me explain the HTML.

First we have a div element that will be the main container and just below another div with the “items” class that will be the container of the tag filters. Just below another div that will contain the foreach to iterate the result of FetchXML.

In each iteration a link <a> element is created that also contains an image element.

This trick of putting an image inside of a link element is to allow the user to download the image each time they click on it.

Then we have a div with the class class=”preview-box”, and the purpose of this is when the user clicks on an image it will be displayed in a larger size but as a modal

And the rest of the HTML is for the pagination feature.

Applying JavaScript and CSS to the Web Page Content

Now we have to go to the web page content and in the “Content HTML” column we have to add the call to the web template we’ve just created:

Then we go to the advance tab in which we will see two sections, one for JavaScript and the second one for the CSS:

Here is the code for the JavaScript:

//selecting all required elements
const filterItem = document.querySelector(“.items”);
const filterImg = document.querySelectorAll(“.gallery .image”);
 
window.onload = ()=>{ //after window loaded
  filterItem.onclick = (selectedItem)=>{ //if user click on filterItem div
    if(selectedItem.target.classList.contains(“item”)){ //if user selected item has .item class
      filterItem.querySelector(“.active”).classList.remove(“active”); //remove the active class which is in first item
      selectedItem.target.classList.add(“active”); //add that active class on user selected item
      let filterName = selectedItem.target.getAttribute(“data-name”); //getting data-name value of user selected item and store in a filtername variable
      filterImg.forEach((image) => {
        let filterImges = image.getAttribute(“data-name”); //getting image data-name value
        //if user selected item data-name value is equal to images data-name value
        //or user selected item data-name value is equal to “all”
        if((filterImges == filterName) || (filterName == “all”)){
          image.classList.remove(“hide”); //first remove the hide class from the image
          image.classList.add(“show”); //add show class in image
        }else{
          image.classList.add(“hide”); //add hide class in image
          image.classList.remove(“show”); //remove show class from the image
        }
      });
    }
  }
  for (let i = 0; i < filterImg.length; i++) {
    filterImg[i].setAttribute(“onclick”, “preview(this)”); //adding onclick attribute in all available images
  }
}
 
//image preview function
//selecting all required elements
const previewBox = document.querySelector(“.preview-box”),
categoryName = previewBox.querySelector(“.title p”),
previewImg = previewBox.querySelector(“img”),
closeIcon = previewBox.querySelector(“.icon”),
shadow = document.querySelector(“.shadow”);
 
function preview(element){
  //once user click on any image then remove the scroll bar of the body, so user can’t scroll up or down
  document.querySelector(“body”).style.overflow = “hidden”;
  let selectedPrevImg = element.querySelector(“img”).src; //getting user clicked image source link and stored in a variable
  let selectedImgCategory = element.getAttribute(“data-name”); //getting user clicked image data-name value
  previewImg.src = selectedPrevImg; //passing the user clicked image source in preview image source
  categoryName.textContent = selectedImgCategory; //passing user clicked data-name value in category name
  previewBox.classList.add(“show”); //show the preview image box
  shadow.classList.add(“show”); //show the light grey background
  closeIcon.onclick = ()=>{ //if user click on close icon of preview box
    previewBox.classList.remove(“show”); //hide the preview box
    shadow.classList.remove(“show”); //hide the light grey background
    document.querySelector(“body”).style.overflow = “auto”; //show the scroll bar on body
  }
}

The above code will work for tag filtering and for previewing each image.

In a nutshell, what the JavaScript is doing for tag filtering is playing with CSS classes to hide or show articles based on the tag the customer clicked on.

And here is the code for the CSS part:

*{
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: ‘Poppins’, sans-serif;
}
::selection{
  color: #fff;
  background: #007bff;
}
body{
  padding: 10px;
}
.wrapper{
  margin: 100px auto;
  max-width: 1100px;
}
.wrapper nav{
  display: flex;
  justify-content: center;
}
.wrapper .items{
  display: flex;
  max-width: 720px;
  width: 100%;
  justify-content: space-between;
}
.items span{
  padding: 7px 25px;
  font-size: 18px;
  font-weight: 500;
  cursor: pointer;
  color: #007bff;
  border-radius: 50px;
  border: 2px solid #007bff;
  transition: all 0.3s ease;
}
.items span.active,
.items span:hover{
  color: #fff;
  background: #007bff;
}

 

.gallery{
  display: flex;
  flex-wrap: wrap;
  margin-top: 30px;
}
.gallery .image{
  width: calc(100% / 4);
  padding: 7px;
}
.gallery .image span{
  display: flex;
  width: 100%;
  overflow: hidden;
}
.gallery .image img{
  width: 100%;
  vertical-align: middle;
  transition: all 0.3s ease;
}
.gallery .image:hover img{
  transform: scale(1.1);
}
.gallery .image.hide{
  display: none;
}
.gallery .image.show{
  animation: animate 0.4s ease;
}
@keyframes animate {
  0%{
    transform: scale(0.5);
  }
  100%{
    transform: scale(1);
  }
}

 

.preview-box{
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(0.9);
  background: #fff;
  max-width: 700px;
  width: 100%;
  z-index: 5;
  opacity: 0;
  pointer-events: none;
  border-radius: 3px;
  padding: 0 5px 5px 5px;
  box-shadow: 0px 0px 15px rgba(0,0,0,0.2);
}
.preview-box.show{
  opacity: 1;
  pointer-events: auto;
  transform: translate(-50%, -50%) scale(1);
  transition: all 0.3s ease;
}
.preview-box .details{
  padding: 13px 15px 13px 10px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.details .title{
  display: flex;
  font-size: 18px;
  font-weight: 400;
}
.details .title p{
  font-weight: 500;
  margin-left: 5px;
}
.details .icon{
  color: #007bff;
  font-style: 22px;
  cursor: pointer;
}
.preview-box .image-box{
  width: 100%;
  display: flex;
}
.image-box img{
  width: 100%;
  border-radius: 0 0 3px 3px;
}
.shadow{
  position: fixed;
  left: 0;
  top: 0;
  height: 100%;
  width: 100%;
  z-index: 2;
  display: none;
  background: rgba(0,0,0,0.4);
}
.shadow.show{
  display: block;
}

 

@media (max-width: 1000px) {
  .gallery .image{
    width: calc(100% / 3);
  }
}
@media (max-width: 800px) {
  .gallery .image{
    width: calc(100% / 2);
  }
}
@media (max-width: 700px) {
  .wrapper nav .items{
    max-width: 600px;
  }
  nav .items span{
    padding: 7px 15px;
  }
}
@media (max-width: 600px) {
  .wrapper{
    margin: 30px auto;
  }
  .wrapper nav .items{
    flex-wrap: wrap;
    justify-content: center;
  }
  nav .items span{
    margin: 5px;
  }
  .gallery .image{
    width: 100%;
  }
}

 

/*Style pagination*/
.visuallyhidden {
  border: 0;
  clip: rect(0 0 0 0);
  height: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;

 

  width: 1px;
  white-space: nowrap;
}
.pagination{ display:flex; justify-content: center; }
.pagination li {
  display:inline-block;
 
  margin: 0 5px;
}

Table Permissions

Finally, don’t forget to grant read access to Note and Product tables to the specific web roles, otherwise the customer will not see anything on the website:

Testing

Here is the result:

This is a bit better CSS than the previous blogpost, but still requires a lot more care, so I’m sure you’ll do much better work than me to make a nicer UI.

Conclusion

As I said in the previous blogpost, the combination of Liquid, JavaScript, CSS and HTML is a great combo to create a tag filtering and pagination experience, but I also wanted to show that the same code (with some modifications) can be applied to solve different business cases while providing a better user experience at the same time.

As you can see I’m not the best at CSS, so I’m sure you will do it better than me.

It is also worth mentioning that if you store the images in SharePoint or in another file storage, you will need to call a cloud flow, Azure function or an API to retrieve the images instead of using FetchXML. Which brings us to the performance topic, we must take into account the number and size of images in order to choose the best way to retrieve the images.

I hope you find it useful for a Power Pages project you are working on, and I hope you enjoy reading this blog as much as I did creating it.

2 Responses

  1. Howdy! This article could not be written much better!

    Going through this article reminds me of my previous roommate!
    He constantly kept talking about this. I will send this article
    to him. Fairly certain he’ll have a very good read.
    Thanks for sharing!

Leave a Reply

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