jQuery in Liferay 7.0

Might not be the best way, but it works for me...

Introduction

So those who know me or have worked with me know that I hate theming.

I do. I find it to be one of the hardest things in Liferay to get right.

I can build modules all day long, but ask me how to make the default buttons increase the height by a couple of pixels and change the color to orange, and I honestly have to start digging through doco and code to figure out how to do it.

Friends that I work with that focus on front end stuff? They run circles around me. Travis and Alex, you guys know I'm referring to you. AMD loader issues? I feel helpless and have to reach out to another friend, Chema, to set me straight.

So recently I'm trying to work with a client who is trying to get a silly jQuery mask plugin to work and they were struggling. They asked for my help.

Well, I got it working through a bunch of trial and error. What I was missing was kind of a straight-forward guide telling me how to build a theme that had jQuery in it (a full jQuery, not the minimal version Liferay needs for Bootstrap) and would allow me to pull in perhaps some jQuery UI, but at the very least I needed to get the Mask plugin to work.

Since I couldn't find that guide, well I just had to write a blog that could be that guide.

Note that I haven't shown this to Travis, Alex or even Chema. I honestly expect when they see what I've done here, they will just shake their heads, but hopefully they might point out my mistakes so I get all of the details right.

Creating The Theme

So I have to rely on the doco and tooling to help me with theme creation. Fortunately we all have access to https://dev.liferay.com/en/develop/tutorials/-/knowledge_base/7-0/themes-generator because that's my go-to starting point.

I used the theme generator, version 7.2.0 (version 8.0.0 is currently beta as I write this, but I'm guessing it is really focused on Liferay 7.1). I pretty much used all of the defaults for the project, targeting Liferay 7.0 and using Styled as my starting point. This gave me a basic Gulp-based theme project which is as good a starting point as any.

I used the gulp build command to get the build directory that has all of the base files. I created the src/templates directory and copied build/templates/portal_normal.ftl over to the src/templates directory.

Adding jQuery

So there's two rules that I know about including jQuery in the theme thanks to my friend Chema:

  1. Load jQuery after Liferay/AUI has loaded the minimal jQuery.
  2. Use no conflict mode.

Additionally there is a best practice recommendation to use a namespace for your jQuery. This will help to ensure that you isolate your jQuery from anything else going on in the portal.

So I know the rules, but knowing the rules and implementing a solution can sometimes seem worlds apart.

In my src/templates/portal_normal.ftl file, I changed the <head /> section to be:

<head>
  <title>${the_title} - ${company_name}</title>

  <meta content="initial-scale=1.0, width=device-width" name="viewport" />

  <@liferay_util["include"] page=top_head_include />

  <script src="https://code.jquery.com/jquery-latest.js"></script>

  <script type="text/javascript">
    // handle the noconflict designation, use namespace dnjq for DN's jQ.
    dnjq = jQuery.noConflict(true);
  </script>
</head>

Okay, so since I'm doing this just before the end of the closing tag for <head />, I should be loading jQuery after Liferay/AUI has loaded its version. I'm also using no conflict mode, and finally I'm following the best practice and using my own namespace, dnjq.

I can use the gulp deploy command to now build my theme and deploy it to my locally running Liferay 7 instance (because I did the full configuration during project setup). In the console tailing Tomcat's catalina.out file, I can see that my theme is successfully deployed, processed and made available.

I can now create a new page and assign my theme to it. Now anyone who has done this much before, you already know that the page rendered using this simple theme is, well, pretty ugly. I mean, it's missing a lot of the normal sort of chrome I'd expect to see in a base theme including some initial positioning, margins, etc. I know, I know, Styled is meant for the experts like my friends Travis and Alex and any kind of initial defaults for those would just get in their way. For me, though, I'd be much better served if there were some kind of "Styled++" base theme that was somewhere between Styled and Classic (aka Clay Atlas), and honestly closer to the Classic side of the table. But we're not here to talk about that, so let's keep going.

So the page renders its ugly self but it looks like the same ugly self I've seen before, so nothing is broken. I can view source on the page and see that my changes to portal_normal.ftl were included, so that's good. I can even see that my namespace variable is there, so that's good too. So far this seems like a success.

Adding jQuery Mask

So my next step is to include the jQuery Mask Plugin.  This is actually pretty easy to do, I just add the following line after my <script /> tag that pulls in jquery-latest.js:

  <script src="http://igorescobar.github.io/jQuery-Mask-Plugin/js/jquery.mask.min.js"></script>

I pulled the URL straight from Igor's site because his demo is working, so I should have no problems.

I use gulp deploy to rebuild the theme and send it to the bundle, the console shows it successfully deploys and my page with my custom theme still renders fine when I refresh the page.

I did see an error in the console:

Mismatched anonymous define() module: function(a){var l=function(b,e,f){...

But it is reportedly coming from everything.jsp (which I didn't touch). So I'm worried about the warning, yes, but still am feeling pretty good about my progress, so on we go.

Testing the Mask

To test, I just created a simple web content. I had to use the "code" mode to get to the HTML fragment view, then I used the following:

<div class="input-group"><label for="date">Date</label>&nbsp;<input class="dn-date" type="text" /></div>
<script type="text/javascript">
  dnjq(document).ready(function() {
    dnjq('.dn-date').mask('00/00/0000');
  });
</script>

Nothing fancy here, just a test to verify that my theme was going to deliver the goods.

I save the web content then add it to my page and, well, fail.

In the console I can see:

Uncaught TypeError: dnjq(...).mask is not a function
  at HTMLDocument. (alt:453)
  at fire (jquery-latest.js:3119)
  at Object.fireWith [as resolveWith] (jquery-latest.js:3231)
  at Function.ready (jquery-latest.js:3443)
  at HTMLDocument.completed (jquery-latest.js:3474)

Nuts. I know this should work, it is working on Igor's page and I haven't really changed anything. So of course mask() is a function.

Diving Into The Source

So I have to solve this problem, so I approach it the same way any developer would, I go and check out Igor's code, fortunately he has shared the project on Github.

I don't have to go very far into the code before I realize what my problem is. Here's the relative fragment, I'll give you a second to look at it and guess where the problem is:

// UMD (Universal Module Definition) patterns for JavaScript modules that work everywhere.
// https://github.com/umdjs/umd/blob/master/templates/jqueryPlugin.js
(function (factory, jQuery, Zepto) {

  if (typeof define === 'function' && define.amd) {
    define(['jquery'], factory);
  } else if (typeof exports === 'object') {
    module.exports = factory(require('jquery'));
  } else {
    factory(jQuery || Zepto);
  }

}(function ($) {...

I see this and I'm thinking that my problem lies with the AMD loader, or at least my lack of understanding how to get it to correctly deal with my script import. It has stepped in and smacked me around, leaving me standing there holding nothing but a bunch of broken javascript.

AMD Bypass

So "ha ha", I think, because I know how to bypass the AMD loader...

I download the jquery.mask.js file and save it in my theme as src/js/jquery.mask.js. I then change the stanza above to simplify it as:

// UMD (Universal Module Definition) patterns for JavaScript modules that work everywhere.
// https://github.com/umdjs/umd/blob/master/templates/jqueryPlugin.js
(function (factory, jQuery, Zepto) {
  factory(jQuery || Zepto);
}(function ($) {...

Basically I just strip out everything that might be going to the AMD loader and just get the browser and jQuery to load the plugin.

Retesting the Mask

I change the portal_normal.ftl line for the mask plugin to be:

<script src="${javascript_folder}/jquery.mask.js"></script>

It will now pull from my theme rather than the web and will use my "fixed" version.

So I gulp deploy my theme, it builds, deploys and starts without any problems in the console, so far so good.

I refresh my browser page (I'm using incognito mode, so no cache to worry about). No errors in the console, still looking good.

I test enter some numbers in the displayed input field, and it all seems to work.

Wahoo! Success!

Conclusion

Well, kind of.

I mean, like I said, this is probably not the right way to do all of this. I'm sure my friends Travis, Alex and Chema will point out my mistakes, well after they're done laughing at me.

Until then, I can at least consider this issue kind of closed...

Blogs

"If you’re running Liferay Portal CE 7.0 GA4, Liferay Digital Enterprise 7.0 SP2 (Fix Pack 8), or a higher patch level, you can hide the Liferay AMD Loader through the control panel"  

 

https://dev.liferay.com/develop/tutorials/-/knowledge_base/7-0/using-external-libraries#using-libraries-that-you-host