Jump to content

4.0 - Javascript framework

Javascript is a key component of front-end web development - it's essential for a modern web app to provide a good user experience, and javascript is central to enabling that. Getting it right in 4.0 has been one of our goals from the start.

Problems

To begin, let's summarize some of the issues with javascript in the current 3.x series:

  • Lack of file organization (a single JS directory that contains dozens of unrelated script files)
  • Different script types are bundled together (interface widgets, routes and full applications all in one place)
  • Lack of modularity (each JS file is pretty much a floating island in how it might implement functionality, with no formalized structure)
  • Simple things requiring code (how many times have you had to write half a dozen lines of JS just to launch a popup?)
  • New dom nodes aren't initialized automatically (load some new HTML via ajax, and your popups won't work without manually hooking them up)


Resolving these problems informed the javascript framework that we've built for 4.0. It all ultimately comes down to organizing code better and making the lives of developers easier (ours and yours!).

The solution

Our solution has been to build a framework which is modularized and heavily event-driven. In most cases, modules never call each other directly, but instead communicate with events which may (or may not) be responded to by another module. More on this later.

The framework breaks down into four types of module:
  • Widgets - interface widgets like popups, tooltips and menus
  • Utility modules - things like cookie handling or URL manipulation
  • Controllers - modules which work on a particular dom node. More on these later.
  • Models - modules which handle data. In the vast majority of cases this is simply fetching data from the server.

There's also 'interfaces', which are third party scripts like CKEditor, jQuery plugins and so forth. They aren't part of the framework, so I won't discuss them here.


The groundwork

Before getting to specific types of modules, we needed to lay the groundwork. Javascript 4.0 is modularized, with only a single global variable (ips) being created in the page. All other scripts are defined as modules, whether they are interface widgets, utilities or anything else. A module is defined as a function which returns an object containing public methods (the revealing module pattern, if you're interested). Here's an example module:

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.myModule', function () {

		// Private methods
		var _privateMethod = function () {

		};

		// Functions that become public methods
		var init = function () {

		},

		publicMethod = function () {

		};

		// Expose public methods
		return {
			init: init,
			publicMethod: publicMethod
		}
	});

}(jQuery, _));


This pattern works well for our purpose, because it enables a module to contain private methods for doing internal work, while exposing only those methods which should be public to the outside world.

This example module could then be used like so:

ips.myModule.publicMethod();


So this keeps everything neatly organized, and ensures variables don't leak into the global scope (which we want to avoid at all costs). When the document is ready, the module is automatically initialized (though you can also have functions that execute before DOMReady if necessary).


Interface widgets

It's fair to say that interface widgets make up a large proportion of the JS used in our software - a web app such as ours has an intrinsic need for popups, menus, tooltips and so on. As mentioned above though, the big hinderance in 3.x is that these widgets have to be created manually (or, in a few simple cases, a special classname is added to an element to initialize it). This feels unnecessary when all the developer wants to do is show a simple widget that is otherwise standard.

To alleviate this hassle, interface widgets in 4.0 all support a data API. What this means is that any widget can be created simply by adding some parameters to an HTML element, and specifying some options. Need a dialog box that loads a remote page? Simply do:

<a href='...' data-ipsDialog data-ipsDialog-title='My dialog' data-ipsDialog-url='http://...'>Click me to open a dialog</a> 


Or if you need a hovercard, just do:

<a href='...' data-ipsHover>This will launch a hovercard</a> 


We already have around two dozen widgets built, covering everything from dialogs, menus and tooltips, to keyboard navigation, tab bars and autocomplete - all supporting initialization with data attributes.

Building your own widgets is easy - they are built as a module, and then simply define some settings to be accepted. The widget module does the rest. They can either be initialized when they're first seen in the dom (which you'd want for something like an image slider widget), or when an event occurs (such as hovering on a link, in the case of hovercards). Whenever new content is loaded into the page, widgets will be found and initialized automatically, too.

Most widgets emit events when certain things occur - when we get to Controllers, you'll see why that is useful.


Utilities

Utilities are simple modules that don't need much discussion. They simply provide methods which do something useful - for example, fetch/set a cookie, write to the user's local browser database, or handle timestamps.


Controllers

Controllers are the meat of the application. Whereas interface widgets are used (and reused) as dumb tools on a page, controllers provide the logic for particular elements, sections and pages. They would, for example, handle the interactions in the topic listing, or the interactions with a post. Notice the word interaction - controllers are specifically designed to deal with events on the page. In fact, that's almost all they do!

Controllers are initialized by specifying the controller name on an element, like so:

<div id='topic_list' data-controller='forums.topicList'> </div>


This div becomes the controller's scope. The controller can manipulate content inside the div, watch for events, and so on.

Controllers, in general, should be as specific and succinct - so simply specifying a page-wide controller then handling everything inside it is discouraged. If we take the topic list in forum view as an example:

<div id='topic_list' data-controller='forums.topicList'> 
	<ul>
		<li data-controller='forums.topicRow'>
			...
		</li>
		<li data-controller='forums.topicRow'>
			...
		</li>
		<li data-controller='forums.topicRow'>
			...
		</li>
	</ul>
</div>


Each topic row might specify the forums.topicRow controller which handles locking, pinning, or marking that topic. The topic list itself might specify the forums.topicList controller, which handles sorting and loading more topics. By doing it this way, controllers become responsible only for a specific portion of the functionality, which keeps them lean and simple.


Controllers are entirely decoupled and cannot reference each other - which is by design, given that a controller is only interested and responsible for its own scope. To communicate, events are used. A controller can trigger events on the page, which other controllers, widgets and models might respond to (and in turn emit their own events).

Continuing the example above, let's assume one of our topic rows is being deleted. The forums.topicRow controller handles removing the HTML from the DOM, but it doesn't care what happens after that - it's not its responsibility. However, it emits a deletedTopic event to let the page know. The forums.topicList controller sees this event, and because it does care, it loads a new topic entry into the list. By using events like this, we can build interfaces that respond fluidly to user interactions while still maintaining separation of concerns.

So, how does a controller deal with events? Because we're using jQuery, event handling in controllers piggy-backs with the on and trigger methods. In the controller's initialize method (which is specifically for setting up event handlers), you simply do:

this.on( 'menuItemSelected', '#menuid', this.handleMenuClick );


Usually when setting up events in an object using jQuery, you need to use $.proxy to properly control the scope of this, but in controllers, this is handled for you automatically - you just specify the method name.

Notice the event we're observing here - menuItemSelected. This is an event that the ui.menu widget emits, and it illustrates how widgets and controllers can interact. Controllers can watch for events from widgets, then do something with the information given, all without ever directly referring to each other.

Triggering an event is similar:

this.trigger( 'doSomething', {
    color: 'yellow',
    size: 'big'
});


This is the same syntax as jQuery's own trigger, except that the controller will ensure the parameters object is passed between different event handlers in the same chain. This will hopefully be clearer when you get your hands on it.


Models

Models are quite similar to controllers (they also use the special on and trigger methods), but their only purpose is to handle data. By decoupling data handling from controllers, we can centralize data getting/setting so that any controller can use it.

Let's say we have a user model, which handles data for the current user. It might have event handlers for adding a friend, for example, so when it sees the event addFriend, it handles it appropriately. Let's also assume we have a controller on each post in a topic, there's three posts, and that the controllers are observing the click event on the 'add friend' button. Here's the sequence of events:

(controller1) click 'add friend button'
(controller1) emit 'addFriend' event
(user model) adds a friend via ajax
(user model) emits 'addedFriend' event
(controller1) updates friend icon
(controller2) updates friend icon
(controller3) updates friend icon

Even though it was controller1 that requested that the model adds a friend, all controllers respond to the event the model emits and updates the friend icon in its own post. This again shows the power of using events as the primary communication system - anyone can respond, and the caller doesn't have to deal with maintaining associations.



Conclusion

So that's about it - the new JS framework in IPS4. Hopefully this in-depth post has covered everything you need to know at this stage. You'll be pleased to know that most of the framework and widgets are already documented, and that will be available when IPS4 hits beta.

Do note that everything covered here is subject to change or removal, as usual in our development blogs.

If you have any questions, feel free to ask!


×
×
  • Create New...