Important

You are browsing the documentation for version 3.1 of OroCommerce, OroCRM and OroPlatform, which is no longer maintained. Read version 5.1 (the latest LTS version) of the Oro documentation to get up-to-date information.

See our Release Process documentation for more information on the currently supported and upcoming releases.

How to Replace Inline-Javascript with a Component

Embedding Functionality in a Page

The easiest way to bind an interactive functionality with the particular markup is to write an inline JavaScript fragment:

 1<select id="my-select">
 2    <option value="foo">Foo</option>
 3    <option value="bar">Bar</option>
 4</select>
 5<script type="text/javascript">
 6    require(['jquery', 'jquery.select2'], function ($) {
 7        $('#my-select').select2({
 8            placeholder: 'Select one ...',
 9            allowClear: true
10        });
11    });
12</script>

Inline scripts are often larger than in the example above and may also make use of inline Twig code. It is impossible to reuse this code, extend it or test it, and it is also hard to maintain.

Furthermore, the lifecycle of its instances is not defined and it is not specified how the function will be destructed properly when the control is not used anymore. Instead, one has to rely on jQuery to properly clean the memory. Most of the time, jQuery does this fine. However, there is no guarantee that jQuery would always handle this task successfully without the help from developers.

The solution for the problems explained above is a Page Component.

A Page Component

A Page Component is a controller that is used to implement certain parts of the page functionality. It is responsible for the following things:

  • creating related views, collections, models and even sub-components

  • handling environment events

  • disposing obsolete instances

See also

You can find more information about Page Components in the Frontend Architecture section and in the Page Component documentation.

Creating a Page Component

A Page Component is a JavaScript object based on the BaseComponent. The inline JavaScript from the introduction will be replaced by the new Select2Component. Start with creating the select2-component.js file that lives in the Resources/public/js/app/components directory of your bundle.

 1// src/Acme/DemoBundle/Resources/public/js/app/components/select2-component.js
 2define(function (require) {
 3    'use strict';
 4
 5    var Select2Component,
 6        BaseComponent = require('oroui/js/app/components/base/component');
 7    require('jquery.select2');
 8
 9    Select2Component = BaseComponent.extend({
10        /**
11         * Initializes Select2 component
12         *
13         * @param {Object} options
14         */
15        initialize: function (options) {
16            // _sourceElement refers to the HTMLElement
17            // that contains the component declaration
18            this.$elem = options._sourceElement;
19            delete options._sourceElement;
20            this.$elem.select2(options);
21            Select2Component.__super__.initialize.call(this, options);
22        },
23
24        /**
25         * Disposes the component
26         */
27        dispose: function () {
28            if (this.disposed) {
29                // component is already removed
30                return;
31            }
32            this.$elem.select2('destroy');
33            delete this.$elem;
34            Select2Component.__super__.dispose.call(this);
35        }
36    });
37
38    return Select2Component;
39});

This code can be tested, extended and reused. What is even more important is that the component provides two methods initialize() and dispose() which restrict the existence of the select2 instance. Thus, it defines its own lifecycle and therefore minimizes the risk of memory leaks.

Declaring a Page Component in HTML

Next, the HTML code of the related template has to be modified to tell the parent View (or other parent ComponentContainer) which HTML elements are related to the Select2Component component:

 1{% set options = {
 2    placeholder: 'Select one ...',
 3    allowClear: true
 4} %}
 5
 6{# assign the component module name and initialization options to HTML #}
 7<select
 8    data-page-component-module="acmedemo/js/app/components/select2-component"
 9    data-page-component-options="{{ options|json_encode }}">
10    <option value="foo">Foo</option>
11    <option value="bar">Bar</option>
12</select>

The parent ComponentContainer uses two attributes to resolve the Component module associated with an HTML element when the initPageComponents method is executed:

data-page-component-module

The name of the module

data-page-component-options

A JSON encoded string containing module configuration options

Using the View Component

The code is now reusable. Though it can be improved by separating the business logic from the view layer. Therefore, replace the Select2Component with the Select2View class in the file named select2-view.js that lives in the Resources/public/js/app/views directory of your bundle and that extends the BaseView class:

 1// src/Acme/DemoBundle/Resources/public/js/app/views/select2-view.js
 2define(function (require) {
 3    'use strict';
 4
 5    var Select2View,
 6        BaseView = require('oroui/js/app/views/base/view');
 7    require('jquery.select2');
 8
 9    Select2View = BaseView.extend({
10        autoRender: true,
11
12        /**
13         * Renders a select2 view
14         */
15        render: function () {
16            this.$el.select2(this.options);
17            return Select2View.__super__.render.call(this);
18        },
19
20        /**
21         * Disposes the view
22         */
23        dispose: function () {
24            if (this.disposed) {
25                // the view is already removed
26                return;
27            }
28            this.$el.select2('destroy');
29            Select2View.__super__.dispose.call(this);
30        }
31    });
32
33    return Select2View;
34});

This looks pretty much like the initially created Select2Component except that you don’t have to deal with retrieving the associated HTML element and that you don’t have to parse the options. This is done for you by the ViewComponent.

However, you still need to tell the component to instantiate your Select2View. For this purpose OroPlatform is shipped with the ViewComponent that instantiates views for HTML elements. To make use of the ViewComponent, replace the value of data-page-component-module attribute with the oroui/js/app/components/view-component and use the view option to point to your new Select2View:

 1{% set options = {
 2    view: 'acmedemo/js/app/views/select2-view',
 3    placeholder: 'Select one ...',
 4    allowClear: true
 5} %}
 6
 7{# assign the component module name and initialization options to the HTML #}
 8<select
 9    data-page-component-module="oroui/js/app/components/view-component"
10    data-page-component-options="{{ options|json_encode }}">
11    <option value="foo">Foo</option>
12    <option value="bar">Bar</option>
13</select>

The ViewComponent loads the required module, fetches the view and the _sourceElement from the options and instantiates the View instance. This View instance is attached to the component instance. Once the component gets disposed, it automatically invokes the dispose() methods of all attached instances (if the dispose() method was defined for the instance).

Please note that as we instantiate the view in the module load callback, we deal with asynchronous process. Therefore, the component is not ready for use right after the initialization method has finished its work. We need to inform the super controller that this is async initialization. To do so, we first call this._deferredInit() that creates a promise object, and once the initialization is over, we invoke this._resolveDeferredInit() that resolves this promise. This way the super controller gets informed that the component is initialized.

Configure RequireJS

Finally, you need to make your new classes known to RequireJS:

1# src/Acme/DemoBundle/Resources/config/requirejs.yml
2config:
3    paths:
4        # for the Select2View class
5        'acmedemo/js/app/views/select2-view': 'bundles/acmeui/js/app/views/select2-view.js'
6        # for the Select2Component class
7        'acmedemo/js/app/components/select2-component': 'bundles/acmeui/js/app/components/select2-component.js'

Whether you have created your own component or a view (that is instantiated by the ViewComponent), you’ll have to add the module name into RequireJS configuration, so that it can trace this module and include it into the build file.

Note

To see your component in action, you need to do several more things:

  • Clear the Symfony application cache to update the cache and the included RequireJS config:

    1$ php bin/console cache:clear
    
  • Reinstall your assets if you don’t deploy them via symlinks:

    1$ php bin/console assets:install
    
  • In production mode, you also have to rebuild the JavaScript code:

    1$ php bin/console oro:requirejs:build