AlloyUI - Working with Plugins

 I mentioned last time that I would talk about the IO Plugin that we have that makes one of the most common tasks of updating content.

So first, I'll show you how to use it, then we'll talk more about how to make your own plugin.
 
So let's set up our sandbox:
AUI().use('io-plugin', function(A){
 // Our code will go here
});
 
So the IO plugin is essentially everything that A.io.request is. All the same arguments, same behavior, but what it does for you is kinda cool.
 
There is a common pattern we kept seeing in our ajax calls which was:
 
  1. Insert loading icon into region
  2. Do ajax call
  3. On success, update region with new content
Here is what the code would look might like before:
 
var contentNode = A.one('#contentNode');

if(contentNode) {
  contentNode.html('<div class="loading-animation"></div>');

  var myRequest = A.io.request('test.html', {
    on: {
      success: function(event, id, xhr) {
        contentNode.html(this.get('responseData'));
      }
    }
  });
}

It's a trivial process, but when you see it so often, you start to think that it's one of those patterns that could imagine yourself sitting in a padded room chanting over and over.
 
So we created a handy little plugin that handles this for us, and in a much nicer way. What this plugin does is does an ajax request the same as A.io.request, but also adds a loading mask to the area that we want to load content into. As soon as the content is ready, it parses through it and looks for any javascript that may be inside of it and executes it, and sets the content into our area.
 
var contentNode = A.one('#myNode');

if(contentNode) {
 contentNode.plug(A.Plugin.IO, { uri: 'test.html' });
}
And that's it.
 
Here's what it looks like:
 
It basically will mask any existing content, and add a loading indicator that is centered above it.
 
Here is some of the cool stuff about the plugin (and all plugins in Alloy):
 

1. It has it's own namespace

This means that this plugin lives inside of it's own area on the node so that it won't clobber with other plugins. Here's a good reason. A.Plugin.IO inherits all of the methods and properties that are on A.io.request, so you can call things like .start(), .stop() (to start and stop the request of course), or .set('uri', 'new_test.html'), or .set('dataType', 'json') and everything else we covered in the <a href="http://www.liferay.com/web/nathan.cavanaugh/blog/-/blogs/alloyui-working-with-ajax?_33_redirect=%2Fweb%2Fnathan.cavanaugh%2Fblog">last post</a>.
If all of that was placed on the main object, then it would conflict with any methods that might exist already on that node, or maybe another plugin.
So instead, it's placed in a namespace, and you can access that like so:
 
contentNode.io
 
So if you want to set the dataType on the plugin to json, you can do:
 
contentNode.io.set('dataType', 'json');
or if you want to stop the connection:
contentNode.io.stop();

2. Plugins can be "unplugged"

This is incredibly useful if you're writing a plugin that should do some clean up work when a user is finished with it (for instance, if you have a plugin that adds in some children elements or adds on some class names to a container).
 
You would just call:
contentNode.unplug(A.Plugin.IO);
 

3. Plugins can be plugged to NodeLists as well as Nodes

So this would work as well:
 
var contentNodes = A.all('.content-nodes');

contentNodes.plug(A.Plugin.IO, { uri: 'test.html' });
 
Then we could grab the first item in the NodeList and access the plugin namespace
contentNodes.item(0).io.set('cache', false);
 

4. Plugins can also be on Widgets

I'll cover widgets more next time, but the same exact process applies, and in fact, the IO plugin is written in such a way that it knows whether it's in a Node or a Widget and will behave accordingly.
 

5. Plugging doesn't have to be a per instance affair.

You can do this:
A.Node.plug(A.Plugin.IO, {autoLoad: false, uri: 'test.html'});
Now you could do:
var contentNode = A.one('#contentNode');
if(contentNode) {
 contentNode.io.start();
}
 
The difference is that since we called A.Node.plug() (which is a static method on the Node class), it plugs all newly created instances with that plugin.
 
I recommend doing it on a per instance basis, however, simply because 1, you'll consume less resources, and two, you don't have to worry about if your existing objects have been plugged.
 
6. You can plug with multiple plugins at once.
So for instance, you can do this:
 
contentNode.plug([
 { fn: A.Plugin.IO, cfg: {uri: 'test.html'} },
 { fn: A.Plugin.MyOtherPlugin }
]);
If that looks confusing, feel free to ignore it, but it simply is a way to pass in mutliple plugins and their configurations (if they need one) all at once.
 

Creating a plugin

 
What's the simplest way to get started creating a plugin? Well here's what's to remember: A plugin, at the very least, is a function, with a property on it called NS which will be it's namespace.
 
So for this example, I'm going to create a plugin that takes an input field, and inserts a " defaultValue". When you focus the field, if the value matches the " defaultValue", it will empty the field, and allow the user to enter their value. When the user moves away from the field, if they haven't entered anything new, it will add in the default text.
 
If you wish to jump to the demo, go ahead and take a look here: Plugin Demo.

 

 
I'm going to start with this markup:
 
<input data-defaultValue="Enter Text" id="myInput" type="text" value="" />
 
HTML5 allows for custom attributes if you prefix the attribute with "data-", so you'll notice I added a new attribute called " data-defaultValue", which our plugin will read.
 
So I'll create the javascript:
 
var defaultValuePlugin = function(config) {
    var node = config.host;

    var defaultValue = node.getAttribute('data-defaultValue');
    var startingValue = node.val();

    if (!startingValue) {
      node.val(defaultValue);
    }
    node.on('focus', function(event) {
      var value = node.val();

      if (value == defaultValue) {
        node.val('');

      }

    });

    node.on('blur', function(event) {
      var value = node.val();

      if (value == '') {
        node.val(defaultValue);

      }
 });
};

defaultValuePlugin.NS = 'defaultValue';

Now all we have to do to get it working is simply plug it onto a node:
 
A.one('#myInput').plug(defaultValuePlugin);
 
I'll go over some points of the code above.
 
One is that, notice that the first line points to config.host. The argument config is the configuration object that is passed into the plugin, but by default the host is always passed into the plugin, so you always have access from the plugin to whatever is being plugged.
It's like a magic link to whatever item you're plugging.
 
The next lines I'm doing the basic work getting an attribute, setting a value if one hasn't been set, and in the bulk of it, attaching focus and blur listeners to do the checking for the value.
 
On the last line, I'm attaching a property called NS to the function that we created. This is the namespace that this plugin will live under, and even if we don't need to access anything specifically, it's there so we can plug something without worrying about it colliding with any other plugins.
 
This is really just scratching the surface of the power that the plugin system offers, but I wanted to show a simple case, rather than bog down in the mire of complexity. If there is any interest in seeing advanced plugins, I can always write an 11th blog post, but the YUI3 page also offers a lot more info if you would like to investigate further as well.
 
Until next time, see you guys later!

 

Blogs
It looks very nice!

My main questions at the moment are:

1 - is it a fact that this will become liferay's default UI framework or still in the air?
2 - Will I be able to write UI for LR 5.2.x CE/Enterprise ?
3 - How is the UI going to interact with the backend? Is it using struts? something else?

I'm excited about this framework but so far have not seen how it will be used inside of LR... I'm checking out the trunk right now...

Thanks!
I've checked out the trunk (the right one now) and I can answer myself #1, It is all over the place... #2, don't think so, and #3 looks like struts mostly although it could be used with regular jsp portlets... YUI3 can be used with JSF if one does all the piping manually to connect controls to a bean via ajax calls or request parameters.
Hi Alex,
Sorry for the delay (we're finishing up a release now, so it's taking me longer to reply and/or blog).
But to answer your questions:

1. Yeah, Alloy is the default UI framework for Liferay (though of course, we support all of the same UI frameworks we always have).
2. You actually can, at least for the HTML/CSS/JS piece. The only piece that might not work would be the taglibs, but the other portions would work, you would just need to link to the file.
3. We're using Struts, but of course, as you said, there are other ways to go about connecting it all. We actually are looking for ways to connect it more naturally for JSF developers and possibly even the GWT style of development. Those are a little longer term, but if you have any ideas, I'd love to hear them.

Thanks for posting Alex,
Very nice but when i try to use the io.plugin to read a URI, i receive a "failed to retrieve content" message. I have made sure that the path is valid. I even tried to use test the code that comes withe the download to no avail. Any clues from anyone?

Thanks I'm kinda desperate.
Hi Eric,
I usually see that if there is a problem with permissions at the URL, or if there are problems going across the domain.
Are you able to do a basic A.io.request(url, {after: {success: function(){
alert(this.get('responseData'));
}}}) on the URL?

If you're able to read the URL(ie, you have permissions, and it's on the same domain) everything should be going through fine.
Let me know emoticon
Thanks Nate,

Sorry for the delay but I am still having the still having the same problem. Interestingly i am calling a simple html file on my local file system, so I dont know why permissions would be an issue(the file is not read only). When substitute this for a live rss url, the content does not load, but i dont get an error either. So...back to a simple call to an html file, does the response data have to be in a certain format?
Hi Eric,
The permissions error I was talking about was that 1. the browser does not allow ajax requests to files on your local system (otherwise some evil page could access personal information). You'll have to test it out using a server, even if it's a local one.
If the RSS url you are passing in is on a different domain than the one you're currently on (and it sounds like you're running this page as part of your file system, so any url would be a different domain), then it won't work. For that, you would need to use the io-xdr module. You can read more about that here: http://developer.yahoo.com/yui/3/examples/io/io-xdr.html

I hope that helps emoticon