How to Replace Inline-Javascript with a Component¶
Embedding Functionality in a Page¶
Commonly, inline JavaScript is used to bind some interactive functionality into the particular markup fragment:
1 2 3 4 5 6 7 8 9 10 11 12 | <select id="my-select">
<option value="foo">Foo</option>
<option value="bar">Bar</option>
</select>
<script type="text/javascript">
require(['jquery', 'jquery.select2'], function ($) {
$('#my-select').select2({
placeholder: 'Select one ...',
allowClear: true
});
});
</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. Additionally, it’s hard to maintain it.
Furthermore, the lifecycle of its instances is not defined and it is not specified how the function will be disabled 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’s no guarantee that jQuery would always handle this task successfully without developer’s help.
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 chapter 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | // src/Acme/DemoBundle/Resources/public/js/app/components/select2-component.js
define(function (require) {
'use strict';
var Select2Component,
BaseComponent = require('oroui/js/app/components/base/component');
require('jquery.select2');
Select2Component = BaseComponent.extend({
/**
* Initializes Select2 component
*
* @param {Object} options
*/
initialize: function (options) {
// _sourceElement refers to the HTMLElement
// that contains the component declaration
this.$elem = options._sourceElement;
delete options._sourceElement;
this.$elem.select2(options);
Select2Component.__super__.initialize.call(this, options);
},
/**
* Disposes the component
*/
dispose: function () {
if (this.disposed) {
// component is already removed
return;
}
this.$elem.select2('destroy');
delete this.$elem;
Select2Component.__super__.dispose.call(this);
}
});
return Select2Component;
});
|
This code can be tested, extended and reused. What’s 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 lifesycle 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 Layout
which
HTML elements are related to the Select2Component
component:
1 2 3 4 5 6 7 8 9 10 11 12 | {% set options = {
placeholder: 'Select one ...',
allowClear: true
} %}
{# assign the component module name and initialization options to HTML #}
<select
data-page-component-module="acmedemo/js/app/components/select2-component"
data-page-component-options="{{ options|json_encode }}">
<option value="foo">Foo</option>
<option value="bar">Bar</option>
</select>
|
The Layout
uses two attributes to resolve the Component module associated with an HTML element
when layout:init
is executed by the PageController:
data-page-component-module
- The name of the module
data-page-component-options
- A JSON encoded string containing module configuration options
Once this HTML code is injected into the document, the PageController
will execute the
layout:init
handler and the component will be initialized.
Using the View Component¶
The code is now reusable. Though it can be improved by separating 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // src/Acme/DemoBundle/Resources/public/js/app/views/select2-view.js
define(function (require) {
'use strict';
var Select2View,
BaseView = require('oroui/js/app/views/base/view');
require('jquery.select2');
Select2View = BaseView.extend({
autoRender: true,
/**
* Renders a select2 view
*/
render: function () {
this.$el.select2(this.options);
return Select2View.__super__.render.call(this);
},
/**
* Disposes the view
*/
dispose: function () {
if (this.disposed) {
// the view is already removed
return;
}
this.$el.select2('destroy');
Select2View.__super__.dispose.call(this);
}
});
return Select2View;
});
|
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 2 3 4 5 6 7 8 9 10 11 12 13 | {% set options = {
view: 'acmedemo/js/app/views/select2-view',
placeholder: 'Select one ...',
allowClear: true
} %}
{# assign the component module name and initialization options to the HTML #}
<select
data-page-component-module="oroui/js/app/components/view-component"
data-page-component-options="{{ options|json_encode }}">
<option value="foo">Foo</option>
<option value="bar">Bar</option>
</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 2 3 4 5 6 7 | # src/Acme/DemoBundle/Resources/config/requirejs.yml
config:
paths:
# for the Select2View class
'acmedemo/js/app/views/select2-view': 'bundles/acmeui/js/app/views/select2-view.js'
# for the Select2Component class
'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:
$ php app/console cache:clear
Reinstall your assets if you don’t deploy them via symlinks:
$ php app/console oro:assets:install
In production mode, you also have to rebuild the JavaScript code:
$ php app/console oro:requirejs:build