Search code examples
javascripthtmlbackbone.js

How to write simple functions in Backbone.js?


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>


Solution

  • Views 101

    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);
    

    Intermezzo: modules

    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.

    Rendering views

    Anyway, back on track. Besides modules, we covered three aspects of Backbone views so far:

    1. defining a view class (blueprint);
    2. creating an instance of the class, an actual view;
    3. placing the view's element in the DOM so the user can see it.

    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:

    • Since we get the outer element (<fieldset>) "for free", we do not include it in the template.
    • In this case, the template is a hand-written function, but usually we will be using a templating engine to create this function for us based on a template string. More on this below.
    • We call the 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).
    • The example definitions of 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.
    • As I wrote before, 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.
    • By convention, 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.

    Handling events

    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:

    1. The view class (blueprint) has an events hash, which binds user events in the view's element to methods of the view.
    2. The view methods that handle these events receive a single 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.
    3. Generally, the view is associated with a model and the effect of an event handler is achieved by changing this model. This is how application state is managed in Backbone.

    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".

    Responding to a model change

    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:

    • We didn't set the tagName, so by default, this view will have a <div> as its outer element.
    • I have now demonstrated how the 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.
    • I used this.model.has('name') in the initialize method, to check whether the model has a name attribute yet.
    • I used 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.
    • I used 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.

    Putting it all together

    In conclusion, here is a snippet that lets you play with all of the above interactively. A few notes in advance:

    • The '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.
    • Stack Overflow snippets don't support modules, so the import/export syntax from above is not repeated here.
    • Models are as deep a subject as views. You can make your own subclasses of them, with their own logic, and they can listen for each other's events as well. I recommend building models before views, because this gives you a solid foundation for your business logic.

    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>