jQuery widget coding standard

Overview

In the Magento system, all jQuery UI widgets and interactions are built on a simple, reusable base—the jQuery UI Widget Factory. The factory provides a flexible base for building complex, stateful plug-ins with a consistent API. It is designed not only for plug-ins that are part of jQuery UI, but for general usage by developers who want to create object-oriented components without reinventing common infrastructure.

For more information, see the jQuery Widget API documentation.

This standard is mandatory for Magento core developers and recommended for third-party extension developers. Some parts of Magento code might not comply with the standard, but we are working to gradually improve this.

Use RFC 2119 to interpret the "must," "must not," "required," "shall," "shall not," "should," "should not," "recommended," "may," and "optional" keywords.

Naming conventions

Widget names must consist of one or more non-abbreviated English words

Correct Incorrect
(function($) {
$.widget('mage.accordion', $.ui.accordion, {
  // ... My custom code ...
});
}) (jQuery);
(function($) {
$.widget('mage.ak123', $.ui.accordion, {
  // ... My custom code ...
});
}) (jQuery);

Widget names must be formatted in camelCase

Correct Incorrect
(function($) {
$.widget('mage.vdeHistoryToolbar', {
  // ... My custom code ...
});
}) (jQuery);
(function($) {
$.widget('mage.vde_historyToolbar', {
  // ... My custom code ...
});
}) (jQuery);

Verbosity is generally encouraged

Widget names should be as verbose as needed to fully describe their purpose and behavior.

Correct Incorrect
// Declaration of the frontend.advancedEventTrigger widget
(function($) {
"use strict";

$.widget('mage.advancedEventTrigger', $.ui.button, {
  // ... My custom code ...
});
}) (jQuery);
// Declaration of the ui.button widget
(function($) {
"use strict";

$.widget('ui.button', $.ui.button, {
  // ... My custom code ...
});
}) (jQuery);

Instantiation and resources

Additional JavaScript files used as resources by a widget

Additional JavaScript files used as resources must be dynamically loaded using the $.mage.components() method and must not be included in the <head> block.

Correct Incorrect
</pre> </pre>

You must use $.mage.extend() to extend an existing set of widget resources

Correct Incorrect
</pre>

You must instantiate widgets using the data-mage-init attribute

You can use the .mage() plug-in to instantiate widgets that use callback methods.

Benefits:

  • You leverage benefits of $.mage.extend() and $.mage.components()
  • Using data-mage-init minimizes inline JavaScript code footprint.
  • You can modify widget initialization parameters.
Correct Incorrect
// Widget initialization using the data-mage-init attribute
<form data-mage-init="{form:[], validation:{ignore:':hidden'}}"></form>
// Widget initialization using the mage plug-in
<script type="text/javascript">
(function($) {
$('selector').mage('dialog', {
    close: function(e) {
        $(this).dialog('destroy');
    }
});
})(jQuery);
</script>
// Widget initialization without using the mage plug-in
<script type="text/javascript">
(function($) {
$('[data-role="form"]')
    .form()
    .validation({
        ignore: ':hidden'
    });
})(jQuery);
</script>

Methods and widgets must not be declared using inline JavaScript

You can declare callback methods inline.

Correct Incorrect
// Widget initialization and configuration
$('selector').mage('dialog', {
close: function(e) {
    $(this).dialog('destroy');
}
});
// Widget initialization and binding event handlers
$('selector').mage('dialog').on('dialogclose', {
$(this).dialog('destroy');
});
// Extension for widget in a JavaScript file
$.widget('mage.dialog', $.ui.dialog, {
close: function() {
    this.destroy();
}
});
// Extension of widget resources
<script type="text/javascript">
(function($) {
$.mage
    .extend('dialog', 'dialog',
        '<?php echo $this->getViewFileUrl('Enterprise_\*Module\*::page/js/dialog.js') ?>')
})(jQuery);
</script>
// Initialization
$('selector').dialog();
$('selector')
.find('.ui-dialog-titlebar-close')
.on('click', function() {
    $('selector').dialog('destroy');
});
</li></ul>

Development standard

Widgets should comply with the single responsibility principle.

The responsibilities which are not related to the entity described by the widget should be moved to another widget.

Correct Incorrect
// Widget "dialog" that is responsible
// only for opening content in an interactive overlay.
$.widget('mage.dialog', {
/* ... */
});
// Widget "validation" that is responsible
// only for validating the form fields.
$.widget('mage.validation', $.ui.sortable, {
/* ... */
});
$('selector')
.mage('dialog')
.find('form')
.mage('validation');
// Widget named 'dialog' that is
// responsible for opening content in
// an interactive overlay and
// validating the form fields.
$.widget('mage.dialog', {
/* ... */
_validateForm: function() {
    /* code which validates the form */
}
});
$('selector').mage('dialog')

All widget properties that can be used to modify widget's behavior must be located in widget's options.

Benefit: Widgets become configurable and reusable.

Correct Incorrect
//Declaration of the
// backend.dialog widget
    $.widget('mage.dialog', {
options: {
    modal: false,
    autoOpen: true,
    /* ... */
},
/* ... */
});
// Initializing
$('selector').mage('dialog', {
modal: true,
autoOpen: false
});
// Declaration of the
// backend.modalDialog and backend.nonModalDialog
// widgets
$.widget('mage.modalDialog', {
/* ... */
});
$.widget('mage.nonModalDialog', {
/* ... */
});
// Initialization
$('selector').mage('modalDialog');
$('selector').mage('nonModalDialog');

Widget communications must be handled by jQuery events

Correct Incorrect
// HTML structure
    <body>
...
<button data-mage-init="{button: {event: 'save', target:'[data-role=edit-form]'}}" />
...
<form data-role="edit-form">
    ...
</form>
...
</body>
// Declaration of the mage.form widget
$.widget("mage.form," {
/* ... */
_create: function() {
    this._bind();
},
_bind: function() {
    this._on({
        save: this._submit
    })
}
_submit: function(e, data) {
    this._rollback();
    if (false !== this._beforeSubmit(e.type, data)) {
        this.element.trigger('submit', e);
    }
}
});
// HTML structure
<body>
...
<button data-mage-init="{formButton: {}}" />
...
<form data-role="edit-form">
    ...
</form>
...
</body>
// Declaration of the mage.button widget
$.widget('mage.formButton', $.ui.button, {
/* ... */
_create: function() {
    this._bind();
    this._super();
},
_bind: function() {
    this._on({
        click: function() {
            $('[data-role=edit-form]').form('submit');
        }
    });
}
});
// Declaration of the mage.form widget
$.widget("mage.form," {
/* ... */
_create: function() {
    this._bind();
}
submit: function(data) {
    this._rollback();
    if (false !== this._beforeSubmit(e.type, data)) {
        this.element.trigger('submit', e);
    }
}
});

You must use DOM event bubbling to perform one-way communication between a child widget and its parent widget

Correct Incorrect

Widgets must comply with the Law of Demeter principle

About the Law of Demeter principle. We recommended against instantiating a widget or calling a widget's methods inside another widget.

Correct Incorrect

We recommend you make widgets abstract enough so that they can be used anywhere in your Magento system

Example: Unlike the mage.topShoppingCart widget, the mage.dropdown widget can be used in many other scenarios.

Correct Incorrect

Abstract widgets which can be used shared with non-Magento applications

Place all such widgets under the <your Magento install dir>/pub/lib/<your company>/<author> directory.

Correct Incorrect
/pub
/lib
    /magento
        dropdown.js
        validation.js
        dialog.js
/pub
/lib
    /magento
        vde-block.js
        vde-container.js

Magento product specific widgets

You must locate all of these under the <your Magento install dir>/app/code/<namespace>/<ModuleName>/view/<areaname>/js directory.

Correct Incorrect
/app
/code
    /Mage
        /DesignEditor
            /view
                /frontend
                    /js
                        vde-block.js
                        vde-container.js
/pub
/lib
    /magento
        vde-block.js
        vde-container.js

Architecture

Use the underscore prefix only to declare private widget methods

Widget properties names should not start with underscore because those properties would not be accessible using the jQuery Widget Factory public API.

Correct Incorrect
// Declaration of the backend.accordion widget
$.widget('mage.accordion', {
/* ... */
_create: function() {
    this.header = this.element.find(this.options.header);
    this.icon = $(this.options.icon).prependTo(this.header);
}
});
// Declaration of the backend.accordion widget
$.widget('mage.accordion', {
/* ... */
_create: function() {
    this._header = this.element.find(this.options.header);
    this._icon = $(this.options.icon).prependTo(this._header);
}
});

A widget's element selection should start with this.element

Correct Incorrect

Widgets must not interact with certain DOM elements

Widgets must not interact with DOM elements that can be selected with this.element.parent(), this.element.parents('selector'), or this.element.closest('selector').

Benefit: Reduced number of widget conflicts because widgets interact only with their child elements.

Correct Incorrect

All widget options should have default values

If there is no default value for an option by design, a null value must be used.

Correct Incorrect

All DOM selectors used by a widget must be passed to the widget as options

Correct Incorrect

If an immediate state change is required, the change must be processed by the _setOption method

Correct Incorrect

To call widget methods, you must use the public widget API

Benefit: The public widget API enables using chaining for widget methods.

Correct Incorrect
// Call the 'open' method on the menu widget using the public widgets API
$('selector')
.menu('open')
.addClass('ui-state-active');
// Call the 'open' method on the
// menu widget without using the public
// widgets API
var menuInstance = $('selector').data('menu');
menuInstance.open();
menuInstance.element.addClass('ui-state-active');

Widget initialization

Initializing a widget must be handled only if there is a logical action to perform on successive calls to the widget with no arguments.

The widget factory automatically fires the _create() and _init() methods during initialization, in that order. The widget factory prevents multiple instantiations on the same element, which is why _create() is called only once for each widget instance, whereas _init() is called each time the widget is called without arguments.

Correct Incorrect

When a widget is destroyed, the element should be left exactly like it was before the widget was attached to it

Common tasks include:

  • Removing or adding of any CSS classes your widget added/removed from the element.
  • Detaching any elements your widget added to the DOM.
  • Destroying any widgets that your widget applied to other elements.

Example:

All event handlers must be bound by the _bind() method

Benefit: All widget event handlers are bound in one place (by the _bind method), which makes it easy to find what events the widget reacts on.

Correct Incorrect

You must use the _on() method to bind events

Benefits:

  • Delegation is supported using selectors in the event names; for example, click .foo
  • Maintains proper this context inside the handlers, so it is not necessary to use the $.proxy() method.
  • Event handlers are automatically namespaced and cleaned up on destruction.
Correct Incorrect

Related topics