Skip to main content
The client-side architecture of Oro applications is built on the Chaplin architecture for JavaScript web applications, which is based on the Backbone.js library. Chaplin provides a lightweight but flexible structure that leverages proven design patterns on top of Backbone’s routing, models, views, and bindings. Because page functionality is distributed across multiple bundles, Oro extends the standard Chaplin approach with a Page Component model that allows each bundle to contribute to a page independently.

Topics

Quick Start

Get started writing JavaScript for OroCommerce and OroPlatform applications.

JavaScript Modularity

ES6 modules, AMD, CommonJS, and loading scripts from remote resources.

Page Component

Micro-controllers that manage specific parts of a page. The primary extension point for adding front-end functionality.

Registry

Application-level registry for sharing state between components.

Component Shortcuts

Concise syntax for declaring Page Components in HTML.

JavaScript Unit Tests

Writing and running unit tests for JavaScript modules.

NPM Dependencies with Composer

Manage JavaScript dependencies declared in Composer packages.

Framework Integration

Integrate third-party JavaScript frameworks into Oro applications.

Technology Stack

Libraries used by OroPlatform on the client side:
  • jQuery + jQuery UI
  • Bootstrap
  • Backbone + Underscore
  • Chaplin
Most of these libraries are defined in OroUIBundle as Webpack modules with short module IDs. For example, the module ID is jquery rather than oroui/lib/jquery.

Application Entry Point

The application is initialized by the oroui/js/app module, which is the entry point of the Webpack build. It exports an instance of the application (an extension of Chaplin.Application) and depends on:
  • oroui/js/app/application — Application class
  • oroui/js/app/routes — Collection of routers
  • Configuration passed from oroui/js/app
  • Optional app modules

Routes

The routes module contains a single route that matches any URL and routes to PageController::index:
[
    ['*pathname', 'page#index']
]

Application configuration

Application options are passed via @OroAsset/Asset.html.twig macros in the @OroUI/js_modules_config.html.twig template:
{% import '@OroAsset/Asset.html.twig' as Asset %}
{{ Asset.js_modules_config({
    'oroui/js/app': {
        baseUrl: app.request.getSchemeAndHttpHost(),
        headerId: oro_hash_navigation_header(),
        userName: app.user ? app.user.username : null,
        root: app.request.getBaseURL() ~ '\/',
        publicPath: asset('build/')|split('?', 2)[0],
        startRouteName: app.request.attributes.get('_master_request_route'),
        debug: app.debug ? true : false,
        skipRouting: '[data-nohash=true], .no-hash',
        controllerPath: 'controllers/',
        controllerSuffix: '-controller',
        trailing: null
    }
}) }}

Naming Conventions

File structures and naming follow Backbone best practices adapted for Oro:
Acme/Bundle/DemoBundle/Resources/public
├── css
│   └── style.css
├── templates                    # frontend templates
│   ├── projects
│   │   ├── project-item.html
│   │   └── projects-list.html
│   └── users
│       ├── user-item.html
│       └── users-list.html
├── js
│   ├── app                      # code that fully supports Chaplin architecture
│   │   ├── components
│   │   │   ├── view-component.js
│   │   │   └── widget-component.js
│   │   ├── controllers
│   │   │   └── page-controller.js
│   │   ├── models
│   │   │   └── projects
│   │   │       ├── project-model.js
│   │   │       └── projects-collection.js
│   │   ├── modules
│   │   │   ├── layout-module.js
│   │   │   └── views-module.js
│   │   └── views
│   │       └── projects
│   │           ├── project-item-view.js
│   │           └── projects-view.js
│   ├── app.js
│   └── tools.js
└── lib                          # third-party libraries
    ├── backbone
    │   └── backbone.min.js
    └── underscore
        └── underscore.min.js
Conventions:
  • Modules supporting Chaplin architecture live in the app folder.
  • The app directory contains five sub-folders: components, controllers, models, modules, views.
  • Each filename ends with a type suffix: -view.js, -model.js, -component.js.
  • All filenames and folder names use lowercase with - as word separators.
  • Utility code or jQuery-UI widgets live outside the app folder.

Application Lifecycle

Chaplin extends Backbone by introducing controllers and a structured lifecycle. A controller and all of its models and views exist only between navigation actions. When the route changes:
  1. The active controller is disposed, along with all nested views and related models.
  2. A new controller is created for the current route with fresh views and models.
This limited lifecycle prevents memory leaks. Components like the application, router, dispatcher, layout, and composer persist throughout the entire navigation session.

Page Controller

All routes lead to a single PageController::index action:
module.exports = [
    ['*pathname', 'page#index']
];
PageController loads page content via PageModel and emits the following system events in order:
EventDescription
page:beforeChangeFired before the page starts changing
page:requestFired when the page request begins
page:updateFired when new page HTML is ready
page:afterChangeFired after all components have initialized

Page Layout View

PageLayoutView is the top-level application view, initialized on the body element and kept in memory across route changes. In addition to handling link clicks and form submissions, it implements the ComponentContainer interface and initializes top-level Page Components defined in the page HTML.

JS Templates (Underscore.js)

For client-rendered templates, Oro uses Underscore.js templates. JS templates belong to specific JS components defined as JS modules and can be overridden the same way as any other JS module.

JavaScript Modularity

Oro uses ES6 modules as the recommended approach. Webpack also supports AMD and CommonJS for legacy code.

Loading scripts from remote resources

Use the scriptjs library to download dependencies not managed by Webpack:
import scriptjs from 'scriptjs';
import BaseView from 'oroui/jsbase/view';

const MyView = BaseView.extend({
    initialize() {
        scriptjs('foo.js', () => {
            // foo.js is ready
            this.doSomething();
        });
    },

    doSomething() {
        /* do something */
    }
});

export default MyView;

Page Component

Page Components are the primary mechanism for adding functionality to specific parts of a page. They act as micro-controllers tied to a DOM element.

Define a Page Component in HTML

{% set options = {
    metadata: metaData,
    data: data
} %}
<div data-page-component-module="mybundle/js/app/components/grid-component"
     data-page-component-options="{{ options|json_encode }}"></div>
Available data- attributes:
AttributeDescription
data-page-component-moduleModule name of the component
data-page-component-optionsJSON-encoded options object
data-page-component-nameOptional name for accessing the component by name
data-page-component-init-onDefer initialization until a DOM event (click, focusin, mouseover)

Extend BaseComponent

MyComponent = BaseComponent.extend({
    initialize: function (options) {
        options = options || {};
        this.processOptions(options);
        if (!_.isEmpty(options.modules)) {
            this._deferredInit();
            loadModules(options.modules, function (modules) {
                _.extend(options.viewOptions, modules);
                this.initView(options.viewOptions);
                self._resolveDeferredInit();
            }, this);
        } else {
            this.initView(options);
        }
    },
    processOptions: function (options) {
        /* manipulate options */
    },
    initView: function (options) {
        this.view = new MyView(options);
    }
});

Deferred initialization on DOM events

Postpone initialization until a DOM event to improve performance for hidden UI elements:
<button data-page-component-init-on="click,focusin"
     data-page-component-module="mybundle/js/app/components/foo-component">Show more...</button>

App Modules

App Modules are atomic parts of the application that run before the application instance is created. They:
  • Register handlers in the Chaplin mediator
  • Subscribe to mediator events
  • Perform preliminary setup actions
Declare App Modules in jsmodules.yml:
app-modules:
    - oroui/js/app/modules/messenger-module

Example

oroui/js/app/modules/messenger-module registers messenger handlers in the mediator:
import mediator from 'oroui/js/mediator';
import messenger from 'oroui/js/messenger';

mediator.setHandler('showMessage',
    messenger.notificationMessage, messenger);
mediator.setHandler('showFlashMessage',
    messenger.notificationFlashMessage, messenger);
Any component or view can then execute these handlers:
mediator.execute('showMessage', 'success', 'Record is saved');

Build docs developers (and LLMs) love