Creating Custom Fragments

Creating custom fragments can be challenging, but the end result of being able to just drop a custom Widget on a page can be quite rewarding...

Introduction

So recently I was helping a client eliminate some FreeMarker from their site. As many who have read my blogs, I'm a quick one to call out "FreeMarker Abuse!" when I find that folks are doing things in FM that Liferay never intended (i.e. making any additional service calls). Liferay's intent with supporting FreeMarker is that you are just generating the DOM to represent the current entity being rendered, and that's pretty much it. As soon as you start doing service calls or json/xml parsing, you're already on the wrong path.

But enough of that rant again, yeah? ;-)

The Comments Custom Fragment

So, what was I trying to replace? Well it looks like this:


Blown up to be clear, but basically it's a comments bubble/link. The goal is to use this within a Collection Display fragment for a Web Contents collection, and each collection items would [optionally] include this bubble and the count of comments available for the article.

It's also a link, and clicking on the link would be a simple URL to the web content display page, and comments could be added at the bottom of the page.

Now I said "optional", because the web contents displayed in my collection actually use the same structure, and the structure includes a boolean field "Display Comments". The idea here is that the content creator would possibly want to not show this comments bubble for some articles, but would show it for others.

The FreeMarker implementation for this was done as part of the template tied to the structure. The template would render the headline and some other details, but the template would also see if the "Display Comments" checkbox was checked and, if so, it would make a synchronous headless call to fetch the number of comments, and then the FM would generate the DOM for what you see above (the "Comments" field, the parenthesis, and the count of the comments for the article).

Okay, I'm not done with the rant, so let's examine why this was "bad". First, the FM had to extract the value for the checkbox and introduced the conditional logic. Next it was doing a synchronous headless call (waiting for it to complete before it could finish rendering the DOM for the article), so this would add overhead into the rendering of just a single instance, and this all compounds if you imagine an asset publisher rendering a list of 20 of these guys... Performance nightmare, and also FreeMarker Abuse! Okay, okay, I'm stepping off of the soapbox again...

Okay, so for me, I started thinking about how I would turn this into a custom fragment.

I already knew that I was going to be using a Collection Display fragment for the list handling, and that I would have a web content collection with the same structure, so this meant that my fragment would be eligible for using structure field mapping directly. This would mean that I could map something from my fragment to the "Display Comments" field in the structure automagically, I wouldn't have to look anything up.

As a custom fragment, I would also be able to invoke JavaScript when the fragment was rendered, and of course I could use Liferay.fetch() to invoke my headless call, but I could do this in an asynchronous manner; my Collection Display fragment with 20 items would render the 20 items right away, then asynchronously the browser could fetch the 20 counts that I needed and update the fragments as the details became available. This would be a noticeable performance impact during rendering, although users could see a delay until the counts were updated. Personally I didn't see this being an issue, I think most users are kind of used to our browser pages dynamically updating with counts and bubbles and stuff as subsequent data comes in...

Preparatory Work

Okay, so first I know that I'm going to develop this fragment right in Liferay using the Fragments Editor. Most of the time, you'll probably want to use the Fragments Toolkit so you can deploy your fragment across multiple environments. However, for me I kind of like using the Editor first because it just seems easier to edit, save, refresh page w/ the fragment on it and see the changes. When I'm done in the editor, though, I can always use the toolkit to create the official artifact.

Now, since I'm going to be using mapping to a structure field in my fragment, I also know that I'm going to need to create an editable element.

Okay, so there's a number of links above pointing to Liferay Learn documentation, and that became my preparatory work. I read those and other sections related to creating custom fragments so I would have at least a basic background before diving in.

In addition, I need my content stuff and collection defined, so let's do that now. I've created my sample structure, and then exported as JSON and it's available via gist: https://gist.github.com/dnebing/9a9665e72e1f06cacb795db0c475c393

With this structure in place, I then created a number of web contents that used this structure, some I would set the Show Contents slider on, and some I'd leave as off.

Next, I went to Site Builder -> Collections and defined a new collection that would only return Web Content Articles type using the Comment Test Structure as the subtype.

Finally I needed a test page. I created a new content page, then I dropped a Collection Display fragment on it, then in the collection item box I dropped a Header fragment mapped to the title field of the structure, a Paragraph fragment mapped to the body field, and an image fragment mapped to the banner image.

With this work out of the way, as I developed my custom fragment I would have it also in the collection item and could see how it was working...

Knocking Out The Simple Things

I figured the easiest thing to do would be to create the portion of the fragment that would be mapped to the structure field, so I started with a simple fragment:

<div class="fragment_3408">
  <div data-lfr-editable-id="display-comments-flag" data-lfr-editable-type="text" 
    id="display-comments-flag" 
    class="comments-flag">true</div>
</div>

Okay, so this is super basic so far. I define my <div />, I define the data-lfr-editable-id field that must be unique, I also declare the data-lfr-editable-type as text (for lack of anything better for the flag, available types are listed here). I also gave my <div /> an id, because I do intend on finding this element using JS later on and pulling the value from it.

I published the new custom fragment and, since I didn't place it on the page before, I had to do that the first time. After publishing the page, I could see my true/false values:

Now, one problem that I have, each of the rows the items all have the same id, the "display-comments-flag" id. Clearly this will not be unique and will not give me something I can easily select.

So I add a tiny bit of FreeMarker to my fragment:

[#assign jaId=0 /]

[#assign infoItemReference = (request.getAttribute("INFO_ITEM_REFERENCE"))! /]
[#if infoItemReference?has_content]
  [#assign jaId = infoItemReference.classPK! /]
[/#if]

<div class="fragment_3408">
  <div data-lfr-editable-id="display-comments-flag" data-lfr-editable-type="text" 
    id="${fragmentEntryLinkNamespace}-display-comments-flag-${jaId}" 
    class="comments-flag">true</div>
  <div class="ja-id">${jaId}</div>
</div>

The FM script here is merely using values available to me within the collection display item. And it is important to point out that this FM code is merely using what is already available; it's not fetching or converting or marshaling or doing anything that would take no real processing time at all.

The key is the FM code request.getAttribute("INFO_ITEM_REFERENCE"). When you are within a collection display item, you have request attributes available to you. The available attributes might be one or more from this list: https://github.com/liferay/liferay-portal/blob/master/modules/apps/info/info-api/src/main/java/com/liferay/info/constants/InfoDisplayWebKeys.java, but note that you may not always get the one you want or need.

In the case of my Collection Display configured with my Collection of web contents and my structure subtype, I get an InfoItemReference instance and it contains the classPK which, for me, is the journal article ID (yay!).

So, assuming I get the guy and he has the right value, now when I check my rendered fragment page, I find rows like <div data-lfr-editable-id="display-comments-flag" data-lfr-editable-type="text" id="wnkz-display-comments-flag-221213" class="comments-flag">false</div>. The fragmentEntryLinkNamespace is a predefined FM value which actually comes from the collection display and should make for a unique prefix, and then I've tacked on the article ID at the end which will make my ID for this specific element unique.

I'm also adding the article ID into the DOM so I can later access it from javascript also.

Rendering Proper Placeholder Text

Okay, so our custom fragment isn't supposed to be showing true/false, it has that look above that we're aiming for. So we adjust our fragment with some more HTML...

[#assign jaId=0 /]

[#assign infoItemReference = (request.getAttribute("INFO_ITEM_REFERENCE"))! /]
[#if infoItemReference?has_content]
  [#assign jaId = infoItemReference.classPK! /]
[/#if]

<div class="fragment_3408">
  <div data-lfr-editable-id="display-comments-flag" data-lfr-editable-type="text" 
    class="comments-flag">true</div>
  <div class="ja-id">${jaId}</div>
  <div class="btn-comments"><i class="icon fa-regular fa-comment"></i> <span 
    class="comments"><span class="text-actions">Comments</span> ( <code 
    class="comments-${jaId}" 
    >...</code> )</span></div>
</div>

Okay, so this new line, it adds a div to contain everything, it uses an icon for the little quote bubble image, then there's a span with the Comments and we're going to put the count into a <code /> block, so it has an id so we can find it later to update and has a ... for the placeholder text.

I've also added a bit of CSS to the fragment now:

.fragment_3408 .comments-flag, .fragment_3408 .ja-id {
	display: none;
}

.fragment_3408 .comments {
	white-space: nowrap;
}

The first basically hides the true/false from rendering. We don't need it to render, but we do need it to be in the DOM. The second I added because the small box that I'm putting this into wanted to wrap the content.

Anyways, our new presentation looks like:


Looking pretty good, yeah? I see a few flaws, though. My oval bubble extends all the way to the right side of the box, so I've got something to fix there.

Otherwise, at this point I'm where JS is going to have to take over...

Knocking Out The 'Hard' Things

So we're now rendering something that looks close to what we want, but there are two bits missing that we need to solve via javascript:

  1. The true/false flags are not being used to hide the comments that shouldn't be there (my last row was false, not true).
  2. The comments count is not being displayed.

So since we're using the Fragments Editor, we get this cool pre-defined javascript function. The signature of the function is function(fragmentElement, configuration) { }. The first argument, well this is going to be the DIV element that contains your fragment. The second argument, that's your fragment configuration object. I didn't do any configuration for this fragment, so I'm pretty much going to ignore that in this blog post.

So let's take care of our hiding of the div if the flag value is false...

To do this, we need to find the div with the value, extract the text, convert into a boolean and, if it is false, we'll just hide the div altogether. This is the Javascript for this part:

// get value of the display comments flag out of our dom element
var flag = fragmentElement.getElementsByClassName('comments-flag')[0];
var displayComments = flag.innerHTML.trim().toLowerCase() === 'true';

if (displayComments === false) {
  // need to hide the element
  fragmentElement.getElementsByClassName("fragment_3408")[0].classList.add("hidden");
} else {
  // need to display the comment count
}

Once we publish the fragment, we should see when we refresh the page with the fragment on it, that the line item(s) that did not have the Show Comments slider on, the fragment would be invisible here, we only see the Comments (...) for those items that did have the slider on.

So that part is done, next we need to handle the display of the comments count. Before I share the code, suffice it to say that I'm going to be doing a Liferay Headless call here to retrieve info about the article, and once I get the response, I'm just going to be using JS to update the DOM to reflect the count.

Now I could do this in the FreeMarker code, but like I said before, that would make it a synchronous call that would need to complete while the DOM was being generated for the fragment, and I wanted to avoid that as a performance concern.

So I built this out in the Javascript function, it's built using Liferay.fetch() so it is an asynchronous call w/ a promise so it can complete whenever, and I think the code itself ends up looking pretty darn simple:

// grab the value of the journal article id out of our dom element
var idElem = fragmentElement.getElementsByClassName('ja-id')[0];
var jaId = parseInt(idElem.innerHTML.trim());

// do the headless call to retrieve the number of comments
Liferay.Util.fetch(
  '/o/headless-delivery/v1.0/structured-contents/' + jaId + 
    '?fields=numberOfComments'
).then(
  response => {
    const { status } = response;

    const responseContentType = response.headers.get('content-type');

    if (status === 204) {
      return { status };
    } else if (response.ok && 
        responseContentType === 'application/json') {
      return response.json();
    } else {
      return response.text();
    }
  }
).then(response => {
  // get the element that shows the count and update from the response
  var countElem = fragmentElement.getElementsByClassName(
    'comments-' + jaId)[0];
  countElem.innerHTML = response.numberOfComments;
});

The code itself is not so complex. This code actually goes inside of the else {} block from the code snippet above, so the whole thing is:

// get value of the display comments flag out of our dom element
var flag = fragmentElement.getElementsByClassName('comments-flag')[0];
var displayComments = flag.innerHTML.trim().toLowerCase() === 'true';

if (displayComments === false) {
  // need to hide the element
  fragmentElement.getElementsByClassName("fragment_3408")[0].classList.add("hidden");
} else {
  // comments are to be displayed, we need to fetch the count...
	
  // grab the value of the journal article id out of our dom element
  var idElem = fragmentElement.getElementsByClassName('ja-id')[0];
  var jaId = parseInt(idElem.innerHTML.trim());

  // do the headless call to retrieve the number of comments
  Liferay.Util.fetch(
    '/o/headless-delivery/v1.0/structured-contents/' + jaId + 
      '?fields=numberOfComments'
  ).then(
    response => {
      const { status } = response;

      const responseContentType = response.headers.get('content-type');

      if (status === 204) {
        return { status };
      } else if (response.ok && 
          responseContentType === 'application/json') {
        return response.json();
      } else {
        return response.text();
      }
    }
  ).then(response => {
    // get the element that shows the count and update from the response
    var countElem = fragmentElement.getElementsByClassName(
      'comments-' + jaId)[0];
    countElem.innerHTML = response.numberOfComments;
  });
}

Now, when we save the fragment and refresh the page we placed it on, this is what we'll see:

Conclusion

Well, as you can see, I'm almost done with my new custom fragment.

I still have some CSS styling to do, but at this point I'm like celebrating since I've gotten this bad boy working.

So, what have we done here?

Well, we've built a custom fragment using the Fragments Editor in Liferay. We've used very limited FreeMarker code so we can access information available to us (the article id), and we've also created a hidden field where we've mapped a value from our structure.

We then built out the Javascript function that can process the mapped value as a flag, and we've also executed a headless call to fetch the count of comments so we could show that on each line item.

For me, the really cool part about this is that I've basically started building my own set of custom fragments that I can reuse while building out my site.

As I examine the rest of the UI elements that are part of my requirements, I'm always going to be looking for ways to decompose it into fragments, hopefully able to use the OOTB fragments, but also custom ones when its necessary.

Blogs

Great Dave! Would be nice having a repository of reusable custom fragments like this, wouldn't ? Something inside of Liferay Marketplace, or stuff like that.

Well that is the intent of the new Marketplace...

For custom fragments like this that you build in the Fragments Editor, they won't be in a form to submit to the Marketplace.

But, if you build them out using the Fragments Toolkit, you are supposed to be able to submit those to the Marketplace, even if they're free.