I am working with some legacy website which is using the Backbone.js framework for the frontend. I'm new to the frontend and Backbone.js seems very confusing when compared with simple JavaScript.
Simple JavaScript function call will be like
document.getElementById("myBtn").addEventListener("click", myFunction);
function myFunction(){
alert("Hello");
}
<!DOCTYPE html>
<html>
<body>
<button id="myBtn">Click me for alert</button>
</body>
</html>
How to implement the same in Backbone.js?
How to add an event listener and call a simple function in Backbone.js on the click of a button?
The functions and scripting are different and is very confusing. All the functions are packaged into another variable and have a prefix but no name. It's something like this.
define(['app',
'underscore',
'handlebars',
'backbone',
'marionette',
'i18next',
'backbone.syphon',
'jquery-validation'
], function(MyApplication, _, Handlebars, Backbone, Marionette, i18n, Syphon, Validation, compiledTemplate) {
MyApplication.module('MyModule.View', function(View, MyApplication, Backbone, Marionette, $, _) {
View.MyView = Marionette.View.extend({
myFunction: function(){
alert("Hello"); // This is not working
}
});
});
return MyApplication.MyModule.View;
});
<!DOCTYPE html>
<html>
<body>
<button id="myBtn" onclick="myFunction();">Click me for alert</button>
</body>
</html>
Let's take this one step at a time. Generally when creating a view, you create a subclass of Backbone.View
(or Marionette.View
, which itself is a subclass of Backbone.View
):
var MyView = Backbone.View.extend({
// you COULD put interesting stuff in here
// (we will get to that)
// but it is not strictly required
});
Now, this only creates a blueprint or class for the type of view that we call MyView
. To actually use MyView
, we have to create an instance of it:
var anInstanceOfMyView = new MyView({
// again, interesting stuff COULD go in here.
});
But at this point, we are still not done. The view is not visible to the user until we insert its element somewhere in the DOM. I tend to refer to this as placing the view. A view always has exactly one HTML element, even if you don't explicitly define it. In that case, it is a <div></div>
by default. The raw element is accessible as its .el
property and a convenient, jQuery-wrapped version of it is available as .$el
. There are many ways you can go about placing the view; discussing all the options is beyond the scope of this answer. As a simple example, the following line will make it the last child element of the <body></body>
element:
anInstanceOfMyView.$el.appendTo(document.body);
If your application is modular, defining the blueprint for a view generally happens in a different module than instantiating and placing it. A modern and relatively straightforward pattern for this is using ES modules:
MyView.js
import { View } from 'backbone';
export var MyView = View.extend({
// ...
});
someOtherModule.js
import { MyView } from './MyView.js';
var anInstanceOfMyView = new MyView({});
anInstanceOfMyView.$el.appendTo(document.body);
The example code in the question appears to use two module systems on top of each other, something I would generally recommend against. The outer system is AMD, which was commonly used in the browser before ESM became a thing, and is still commonly used in order to emulate ESM. By itself, it looks like this:
MyView.js
define(['backbone'], function(Backbone) {
return Backbone.View.extend({
// ...
});
});
someOtherModule.js
define(['./MyView.js'], function(MyView) {
var anInstanceOfMyView = new MyView({});
anInstanceOfMyView.$el.appendTo(document.body);
});
The inner system does not look familiar to me, so I cannot comment on how it works. If you can avoid using it, I recommend doing so.
Anyway, back on track. Besides modules, we covered three aspects of Backbone views so far:
While we covered enough to enable the user to see the view, there is nothing to see yet; the view's element is empty by default.
We render a view to give its element internal HTML content. Note how this is roughly the dual operation of placing a view, where we give it an external context. Since the content is an internal affair, I would generally recommend that the view is in charge of rendering itself.
By convention, views have a template
method and a render
method. template
takes a data payload (any JavaScript value, usually an object) and returns a string with HTML code. render
serves as a trigger to actually update the contents of the view's element; it prepares a data payload, passes it to the template
method and sets the return value as the inner HTML of this.el
.
Here's how we might define a blueprint for a view that invites a website visitor to enter their name:
// ask-name.view.js
export var AskNameView = View.extend({
// Finally, some interesting content!
// This view's element is not a <div> but a <fieldset>.
tagName: 'fieldset',
// This template is trivial because it always returns the same
// HTML string. We will see more interesting examples later.
template: function(payload) {
return `
<label>Hi, please enter your name:
<input name=name>
</label>
`;
},
// initialize runs once during instantiation.
initialize: function(options) {
this.render();
},
// render nearly always looks similar or identical to the below
render: function() {
this.$el.html(this.template());
return this;
},
});
When we instantiate the above view, its element will look like this:
<fieldset>
<label>Hi, please enter your name:
<input name=name>
</label>
</fieldset>
There are a couple of things to note about the above example code:
<fieldset>
) "for free", we do not include it in the template.render
method in initialize
so that the view sets its internal content immediately when it is created. I generally recommend this, unless you want to postpone rendering until some condition is met, or unless rendering is very expensive. However, you should strive to make rendering cheap and idempotent (i.e., safe to repeat).template
and initialize
above both have a parameter that is never used: payload
and options
, respectively. I included them anyway to show that they are there.render
uses this.template
to generate raw HTML code. It then calls this.$el.html
, which is a jQuery method, to set that HTML code as the inner HTML of the view's element.render
returns this
. This makes it possible to chain other view methods after calling render
. This is commonly done with methods in Backbone classes if they don't have some other value to return.We have reached the point that we can show a name entry field to a user. Now, it is time to actually do something with the input. Handling user events generally involves three parts in Backbone:
events
hash, which binds user events in the view's element to methods of the view.event
argument, which is a jQuery-wrapped representation of the DOM event. They have this
bound to the view instance. Like all event handlers, their return values are ignored, but they can have their effect by operating on the view instance.Starting with the last part, this is how we create a plain, empty model user
:
import { Model } from 'backbone';
var user = new Model();
and here is how we create a view askNameForm
that is aware of the user
model:
import { AskNameView } from './ask-name.view.js';
var askNameForm = new AskNameView({model: user});
Simply because we pass user
as the model
option to the view constructor, methods of our view will be able to access it as this.model
. Here is how we might use that in an event handler in our definition of AskNameView
:
export var AskNameView = View.extend({
// ... same content as before ...
// added event handler
handleName: function(event) {
// in this case, event.target is our <input> element
var name = event.target.value;
this.model.set('name', name);
},
});
The model will trigger an event of its own whenever we change its contents. This will enable us to respond elsewhere in the application ("spooky action at a distance"). We will see an example of this next. First, however, let us complete this setup by actually registering the event handler:
export var AskNameView = View.extend({
// ... same content as before ...
events: {
'change input': 'handleName',
},
});
This notation means "when an internal element with the selector input
triggers a 'change'
event, call the handleName
method".
It is time to close the loop. Suppose that after the user has entered their name, we want to show a nice personal welcome message. To do this, we can exploit the fact that multiple views may share the same model. Let us define a simple new view type that does exactly that:
// show-welcome.view.js
import Handlebars from 'handlebars';
export var ShowWelcomeView = View.extend({
// now using a template engine
template: Handlebars.compile('Welcome, <b>{{name}}</b>!'),
initialize: function() {
// don't render yet if the model lacks a name
if (this.model.has('name')) this.render();
// update every time the name changes (or becomes set)
this.listenTo(this.model, 'change:name', this.render);
},
render: function() {
// the following line is a classic.
this.$el.html(this.template(this.model.toJSON()));
return this;
},
});
Again, there are a couple of things to note about this view:
tagName
, so by default, this view will have a <div>
as its outer element.template
might be generated using a template engine, rather than by hand-writing a function. I have chosen Handlebars for this example, since this is what appeared in the question, but you could use any other templating solution.this.model.has('name')
in the initialize
method, to check whether the model has a name
attribute yet.this.listenTo(this.model, ...)
in the initialize
method in order to respond to model events. In this case, I'm updating the view contents whenever the name
attribute changes. I could simply listen for 'change'
instead of 'change:name'
in order to re-render on any model change.this.model.toJSON()
, which is a safe way to extract all data from the model, in the render
method in order to supply the payload for the template. Remember that I declared a payload
parameter for the template
method of the AskNameView
but didn't use it? Now I did.In conclusion, here is a snippet that lets you play with all of the above interactively. A few notes in advance:
'change'
user event only triggers when you remove focus from the input field, for example by clicking outside of the box. Use input
or keyup
instead if you want to see immediate effect after typing into the box.import
/export
syntax from above is not repeated here.For more information, please refer to the documentation. I wish you much success on your Backbone journey!
// defining the view types
var AskNameView = Backbone.View.extend({
tagName: 'fieldset',
template: function(payload) {
return `
<label>Hi, please enter your name:
<input name=name>
</label>
`;
},
events: {
'change input': 'handleName',
},
initialize: function(options) {
this.render();
},
render: function() {
this.$el.html(this.template());
return this;
},
handleName: function(event) {
var name = event.target.value;
this.model.set('name', name);
},
});
var ShowWelcomeView = Backbone.View.extend({
template: Handlebars.compile('Welcome, <b>{{name}}</b>!'),
initialize: function() {
if (this.model.has('name')) this.render();
this.listenTo(this.model, 'change:name', this.render);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
});
// creating instances, linking them together
var user = new Backbone.Model();
var askNameForm = new AskNameView({model: user});
var showWelcome = new ShowWelcomeView({model: user});
// placing the views so the user can interact with them
$(document.body).append(askNameForm.el, showWelcome.el);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/underscore-umd-min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/backbone-min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/handlebars@latest/dist/handlebars.js"></script>