Motivation

At Meanbee, we have a lot of experience in both developing and maintaining Magento stores. One common problematic factor, which frequently arises during these activities, revolves around JavaScript. We have had a great deal of trouble maintaining JavaScript quite simply because it is implemented in a number of locations, including but not limited to:

  • Template files
  • The theme’s skin directory
  • The root JS directory

Frequently, these can be small functionalities. A click handler on a button, for instance, but there is no clear location to place this code. Additionally, there are a number of methods which this could be implemented; should we use the html onclick attribute? Should we use a library to attach a click handler? This document will attempt to provide a standard which considers both best development practice and ease of maintainability.

Introduction

We define Magento development into two broad categories. These are:

  • Module development
  • Theme development

Module development typically concerns some single, specific backend feature, which can be added as a component to a Magento store. Theme development is much more front end focused, and concerns the appearance of a Magento site. Both types of development can have JavaScript associated with them, so we break down this document here to discuss standard and best practice in both cases.

Module Development

File Location

There are two locations which your JavaScript should belong placed, when writing for a module. this is the js/ directory in the root of the Magento install and somewhere in the skin/frontend/base/default (frontend) folder or skin/adminhtml/default/default (admin) folder. For the latter two, you should place all module JavaScript inside an appropriately named sub-folder here. For example, the frontend JavaScript associated with Meanbee’s Postcode module should be located at skin/frontend/base/default/js/Meanbee/Postcode/. As an aside, we’d also advise placing CSS as another sub-folder at the same level as the js/.

  • js/
    • You should place any library code, or JavaScript which you don’t want to be customised or edited by the theme here. For example, if we wanted to include underscore.js on the page, we’d put it here.
  • skin/frontend/base/default and skin/adminhtml/default/default
    • You should place any other frontend or admin JavaScript in these two locations. Note that you may (and in many cases - should!) have several files here. Lengthy, unwieldy files are a nightmare to maintain.

As mentioned above, don’t be afraid to split your code into multiple files. Provided your files are named sensibly, it makes maintenance simpler as a developer can tell where the JavaScript for a given feature / page lives at a glance. From a performance perspective this won’t matter anyway, since JavaScript tends to be minified and merged in production anyway. In a similar vein, we advise a single file per class.

Back to top

Using OOP

Programmers have been controlling complexity in programming with OOP since the 1960s, JavaScript is not an exception! This is likely because objects and classes in JavaScript are non-standard compared to other languages. Luckily, for Magento developers, Prototype offers this functionality in a more familiar way. An example, taken from their site:

var Person = Class.create({
    initialize: function(name) {
        this.name = name;
    },
    say: function(message) {
        return this.name + ': ' + message;
    }
});

Further to this, we’ve found it best not to rely on this. There are instances where it’s value can change (for example inside a prototype click handler) and you are unable to access your classes methods / fields. You can fix this by using bind but it’s our opinion that this leaves messy and cluttered code. We’d recommend storing a reference to your instance at the beginning of each method, since then this provides a consistent way to reference your fields.

var Person = Class.create({
    initialize: function(name) {
        var self = this;
        self.name = name;
    },
    say: function(message) {
        var self = this;
        return self.name + ': ' + message;
    }
});

In this way, we can ensure that we always have access to your class’ methods and variables no matter where you are in your code.

For further information, including inheritance, we’d recommend looking at the prototype website.

If we were to implement all our JavaScript in the way described above we could quickly run into maintainability issues, mainly because each class we’ve defined will find it’s way into the global namespace, which any programmer worth his salt will tell you is a bad way to do things. Instead, we could take a leaf out of Magento’s book and employ a semantic folder hierarchy which allows us better control of where our classes exist. For instance:

// Ensure our hierarchy exists
if (!window.Meanbee) window.Meanbee = {};
if (!window.Meanbee.Core) window.Meanbee.Core = {};

(function() {
    var Person = Class.create({
        initialize: function(name) {
            var self = this;
            self.name = name;
        },
        say: function(message) {
            var self = this;
            return self.name + ': ' + message;
        }
    });

    window.Meanbee.Core.Person = Person;
})();

In this way our JavaScript has a place to belong and there is less pollution of the global namespace.

Back to top

Classes

Class arguments should be passed in as an object and extended inside the initialize method so that defaults can be set but also redefined on instantiation of the Class:

var StickyNav = Class.create({
    initialize: function( options ) {
        this.options = Object.extend({
            navSelector: '.nav-container',
            mobileToggleClassName: 'navigation-toggle-bar'
        }, options || {} );
    }
});

If a Class has a dependency it should be checked in the initialize method and an error thrown if it is not found. This applies for both Libraries, other Classes and DOM elements that cannot have a default set:

if ( !self.options.defaultColourSelectElement ) {
    throw 'defaultColourSelectElement is not provided to JournalColorHandler. Unable to initialise.';
}

Destroy methods should be added that reverse any manipulation of the DOM caused by the Class. This can be the addition of class names, inline styles, observers or injected markup. The Class once destroyed should leave no trace of it ever having been instantiated. Take the following example of a destroy method:

destroy: function() {
    var self = this;

    // Find the original parent node of the element
    self.checkedElements.each(function( item ) {
        // Return the checkedElements previous state before DOM lookup
        item.show();

        var className = item.type === 'radio' ? self.options.radioClassName : self.options.checkboxClassName,
            createdNode = item.up( '.' + className ),
            innerHTML = createdNode.innerHTML;

        // Destroy our observers on the checkedElements
        Event.stopObserving( item, 'change',  self.arrayOfItemChangeObservers.shift() );
        Event.stopObserving( createdNode, 'click', self.arrayOfWrapperClickObservers.shift() );

        // Insert the child back into the original parent
        createdNode.replace( innerHTML );
    });

    // Reset Class Vars
    self.checkedElements = null;
    self.radioElements = null;
    self.arrayOfWrapperClickObservers = [];
    self.arrayOfItemChangeObservers = [];
},

This destroy method removes observers for each item where a pointer to that observer has been stored in an array. The array is then reset alongside stored dom references in the Class.

Back to top

Events

Bear in mind, while developing, that other's will want to perform their own JavaScript logic which depends on your own. Of course, a developer could go into your own JavaScript file in the `skin/` directory and change your code, but a good developer would not want to do this since, among other reasons, it's not an upgrade-proof change. A developer could also overwrite this file in their theme, while this is slightly better, it still is not upgrade-proof. We recommend using events in your classes so that developers can hook into them to implement their logic. In action: {% highlight js %} // Your class code (function() { var MyFeature = Class.create({ initialize: function() { document.fire('myfeature:before_initialize'); // Do some stuff... document.fire('myfeature:after_initialize'); }, customMethod: function() { document.fire('myfeature:before_customMethod'); // Do some stuff for customMethod... document.fire('myfeature:important_stuff') document.fire('myfeature:after_customMethod'); } }); })(); // Observer code $(document).observe('myfeature:important_stuff_happened', function() { alert('Important stuff just happened!'); }); {% endhighlight %} The advantage here is that we don't need to overwrite any code to append our own logic. One important note is we don’t need to use the document element, we can target some specific element; for example, in Meanbee Postcode, we might want to send out custom events on our postcode dropdown element.

Back to top

Theme Development

File Location

Much of what was said in Module Development applies to theme development; Place any libraries in `js/` and place your theme specific JavaScript in the `skin/` subdirectory. The one addition, for `frontend` development, is that it shouldn't be inside `skin/frontend/base/default/js` but inside `skin/frontend/your_package/your_theme/js` instead. We suggest the following file structure when you begin theme development global.js product.js category.js home.js checkout.js ... (any other high level files) lib/ Where JavaScript for all pages (equivalent to the default handle in Magento) goes in `global.js` and specific JavaScript to each page goes into the obvious files. You may wish to add more js files here, but that depends on your project. These files should contain things minor JavaScript, behavioural tweaks. Things like click handlers, page onload animations; generally code that doesn't belong as part of a larger feature, or isn't reusable across other pages. One point I'd like to stress regarding things like click handlers is to provide an easy route to trace this behaviour from the markup. At Meanbee, we've run into several problems we return to a project to perform some maintenance and it's a mystery what JavaScript is manipulating a page. Therefore our simple solution is just to place a PHP comment in the template describing what JavaScript is affecting the markup and where it can be found. Features can be placed in a folder hierarchy in the lib folder, for instance let's say we have a class Meanbee.Core.Calendar. We would place the JavaScript file in `lib/Meanbee/Core/Calendar.js`. We can then instantiate the class in a template or in one of the top level JavaScript files.

Back to top

Access to PHP values

One frequent issue which we run into is needing to access values which either exist or need to be computed in php in our JavaScript. The usual culprit is the site's base URL, often we solve this by code similar to: {% highlight html %}

A title

Some markup

{% endhighlight %} If this value is required in a feature we could, of course, pass it as a constructor. There are a couple of reasons this doesn't feel like the correct way to do things: - The JavaScript can become very difficult to read - We could be computing the same value several times, e.g. we might have that same base URL retrieval in several locations on the site. - It contradicts what we've said previously in this document with regards to best practice placement of JavaScript. One solution could be to set commonly used values in a special template file: {% highlight html %} {% endhighlight %} In this way we are not polluting global namespace and we'll have access to these values in all our JavaScript files.

Back to top

Unit Testing

TODO - Investigate unit testing solutions - [Jasmine](http://jasmine.github.io/) - [QUnit](http://qunitjs.com/) - [UnitJS](http://unitjs.com/) - [Karma](http://karma-runner.github.io/)

Back to top