Quantcast
Channel: BarDev – Archive
Viewing all articles
Browse latest Browse all 28

Re-Learning Backbone.js – Multiple Views

$
0
0

In this post we will learn about how a view can monitor changes or events in other views. A common scenario is to display a list of items. When the user clicks an item from the list, the details of the item is displayed in a different view. The user can then edit and save the details back to the list.

In this example we will have a list of movies. When the user selects a movie from the list, the data for the movie will be populated in text boxes, which is in a different view. Once the user changes the data and clicks “Change Record” button the changes will be updated back to the list.

The fundamental concept to understand is that the child movie view is not aware of the detail movie view. Neither one of the views are aware of the other. This is important because it decouples the views; it allows us to remove or exchange views without modifying other views.

In a previous post Re-Learning Backbone.js – Events (Pub-Sub) we learned about PubSub in isolation. In this post we will use PubSub to notify the triggering of events when a change occurs in a view.

This tutorial will start where the Re-Learning Backbone.js – Nested Views ended. For convenience here is the code from the previous post:

<html >
<head>
    <title></title>
    <script src="/scripts/jquery-1.8.3.js" type="text/javascript"></script>
   	<script src="/scripts/underscore.js" type="text/javascript"></script>
	<script src="/scripts/backbone.js" type="text/javascript"></script>
    <style >
        div .table {display: table; border:#505050 solid 1px; padding: 5px;}
        div .tableRow {display: table-row}
        div .tableCell {display: table-cell; border:#737373 solid 1px; padding: 5px;}
    </style>
</head>
<body>
    Re-Learning Backbone.js – Nested Views
    
    <div id="container-movieList" class="table"></div>
    <script type="text/template" id="template-movieItem">
        <div id="title" class="tableCell"><%=title%></div>
        <div id="releaseYear" class="tableCell"><%=releaseYear%></div>
    </script>


    <script type="text/javascript">
        //Create MovieListView and loop through movies
        //**************************************************************************
        var Movie = Backbone.Model.extend({});

        var Movies = Backbone.Collection.extend({
            model: Movie
        });

        //**************************************************************************
        var MovieListView = Backbone.View.extend({
            type: "MovieListView", //for debugging
            el: "#container-movieList",  //the view should be decoupled from the DOM, but for this example this will do.
            //collection:  This will be passed in during initialization
            initialize: function () {

            },
            render: function () {
                _.each(this.collection.models, this.processMovie, this);
                return this;
            },
            processMovie: function(movie){
                var childMovieItemView = new MovieItemView({ model: movie });
                childMovieItemView.render();
                this.$el.append(childMovieItemView.el);
            }
        })
        
        var MovieItemView = Backbone.View.extend({
            type: "MovieItemView", //for debugging
            template: _.template($("#template-movieItem").html()),
            tagName: "div",
            className: "tableRow",
            //el: since we're setting the tagName, we don't need set the el
            //model: pass model in
            initialize: function () {
                this.model.on("change", this.modelChanged, this);
            },
            events: {
                "click": "viewClicked"
            },
            render: function () {
                var outputHtml = this.template(this.model.toJSON());
                this.$el.html(outputHtml);
                return this;
            },
            modelChanged: function (model, changes) {
                console.log("modelChanged:" + model.get("title"));
                this.render();
            },
            viewClicked: function (event) {
                console.log("viewClicked: " + this.model.get("title"));
            }
        })

        //**************************************************************************
        var myMovies = [
            { "id": "1", "title": "Bag It", "releaseYear": 2010 },
            { "id": "2", "title": "Lost Boy: The Next Chapter", "releaseYear": 2009 },
            { "id": "3", "title": "To Live & Ride in L.A.", "releaseYear": 2010 },
            { "id": "4", "title": "K-ON!: Vol. 1", "releaseYear": 2009 },
            { "id": "5", "title": "Archer: Season 2: Disc 1", "releaseYear": 2011 },
        ];
        var movies = new Movies(myMovies);
        var movieListView = new MovieListView({ collection: movies });
        movieListView.render();
    </script>
</body>
</html>

*******************************************************************

Lets start making changes

The following HTML will be managed by a Backbone.js View. This is where the user will change the movie’s title and release year. In the past we have used templates. To keep this simple, we are just using static HTML.

    <div id="container-movieDetailEdit">
        Title <input type="text" id="txtMovieTitle"/>
        Release Year <input type="text" id="txtMovieReleaseYear"/>
        <input type="button" id="butChange" value="Change Record"/>
    </div>

Nothing much special here, just some static HTML that will display two text boxes and a button. We put the input control in a “div” with an id of “container-movieDetailEdit”. This will allow us to use a Backbone.js View to manage this element and its children.

**********************************************************************

We need a way to manage messaging from the different views. If you have questions about the following code, see this blog post: Re-Learning Backbone.js – Events (Pub-Sub).

        var PubSub = function () {
            this.events = _.extend({}, Backbone.Events);
        };
        var pubSub = new PubSub();

You will see the “pubSub” variable reference multiple times in different the views. This is how the views are able to communicate with each other.

**************************************************************

When an individual MovieItemView is clicked, we need to notify all the subscribers. Remember there will be a MovieItemView object for each movie.

        var MovieItemView = Backbone.View.extend({
            type: "MovieItemView", //for debugging
            template: _.template($("#template-movieItem").html()),
            tagName: "div",
            className: "tableRow",
            //el: since we're setting the tagName, we don't need set the el
            //model: pass model in
            initialize: function() {
                this.model.on("change", this.modelChanged, this);
            },
            events: {
                "click": "viewClicked"
            },
            render: function() {
                var outputHtml = this.template(this.model.toJSON());
                this.$el.html(outputHtml);
                return this;
            },
            modelChanged: function(model, changes) {
                console.log("modelChanged:" + model.get("title"));
                this.render();
            },
            viewClicked: function(event) {
                console.log("viewClicked: " + this.model.get("title"));
                pubSub.events.trigger("movie:selected", this.model);
            }
        });

Very little has changed from the original code for “MovieItemView”.

At the very end of the code, when the “viewClicked” is called, we use the “pubSub.events” object to trigger an event called “movie:selected”. Also, we pass in the model that the view is managing. At this point, the “pubSub” should notify others that have subscribed to the “movie:selected” event and provide them the movie.

You may have noticed that “pubSub” in the method “viewClicked’ references the global “pubSub”. There’re multiple things wrong here. But, for brevity and simplicity, I decided just to use the global “pubSub”. What should actually occur is when the view is created the “pubSub” reference should be passed in to the initialize method. Then the “pubSub” should be referenced by using “this”, such as “this.pubSub.events.trigger”.

**************************************************************

The MovieDetailView manages the Dom element (“#container-movieDetailEdit”) where the user will update the movie. This view will be notified when a movie has been selected.


        var MovieDetailView = Backbone.View.extend({
            el: "#container-movieDetailEdit",
            initialize: function () {
                pubSub.events.on("movie:selected", this.movieSelected, this);
            },
            events: {
                "click  #butChange": "viewChanged"
            },
            render: function() {
                var title = this.model.get("title");
                this.$el.find("#txtMovieTitle").val(title);
                
                var releaseYear = this.model.get("releaseYear");
                this.$el.find("#txtMovieReleaseYear").val(releaseYear);
            },
            movieSelected: function(movie) {
                console.log(movie);
                this.model = movie;
                this.render();
            },
            viewChanged: function () {
                var title = this.$el.find("#txtMovieTitle").val();
                this.model.set({ "title": title });
                
                var releaseYear = this.$el.find("#txtMovieReleaseYear").val();
                this.model.set({ "releaseYear": releaseYear });
            }
        });

Even though it’s marked up alot, this view is not much different than the views we have created in other blog posts.

In the first line of this view, we are telling the view to manage the DOM element “#container-movieDetailEdit” by assinging it to “el”.

In the initialize method, we are telling this view to subscribe to any “movie:selected” events. If pubSub hears that this event is triggered, then call “movieSelected” method in the view.

In regards to “events” object literal, when the button “#butChange” is clicked call the method “viewChanged”.

The method “movieSelected” will be called when a use selects a movie from the list. When this method is called, a movie reference will be passed in. The movie that is passed in will be assigned to the view’s model property. It’s important to realize that two views will reference the same movie object. So if this view changes the model, then all other views that reference this model should be updated. This is a good thing. If this view updates its model and the model is referenced by a second view, then the second view should be updated also.

When the button in the view is clicked, the “viewChange” method should be called. The “viewChange” method updates the movie model.

You may have noticed that “pubSub” in the “initialize” method references the global “pubSub”. There’re multiple things bad here. But, for brevity and simplicity, I decided just to use the global “pubSub”. What should actually occur is when the view is created; the “pubSub” reference should be passed in to the initialize. Then the “pubSub” should be referenced by using “this”, such as “this.pubSub.events.on”.

**************************************************************

Create the view “movieDetailView”

var movieDetailView = new MovieDetailView();

This is nothing special. Just create the object movieDetailView.

*******************************************************************

How it works

Here’s the complete source code:

<html >
<head>
    <title></title>
    <script src="/scripts/jquery-1.8.3.js" type="text/javascript"></script>
   	<script src="/scripts/underscore.js" type="text/javascript"></script>
	<script src="/scripts/backbone.js" type="text/javascript"></script>
    <style >
        div .table {display: table; border:#505050 solid 1px; padding: 5px;}
        div .tableRow {display: table-row}
        div .tableCell {display: table-cell; border:#737373 solid 1px; padding: 5px;}
    </style>
</head>
<body>
    <div id="container-movieList" class="table"></div>
    <script type="text/template" id="template-movieItem">
        <div id="title" class="tableCell"><%=title%></div>
        <div id="releaseYear" class="tableCell"><%=releaseYear%></div>
    </script>
    
    *****************************************
    <div id="container-movieDetailEdit">
        Title <input type="text" id="txtMovieTitle"/>
        Release Year <input type="text" id="txtMovieReleaseYear"/>
        <input type="button" id="butChange" value="Change Record"/>
    </div>

    <script type="text/javascript">
        var PubSub = function () {
            this.events = _.extend({}, Backbone.Events);
        };
        var pubSub = new PubSub();

        //Create MovieListView and loop through movies
        //**************************************************************************
        var Movie = Backbone.Model.extend({});

        var Movies = Backbone.Collection.extend({
            model: Movie
        });

        //**************************************************************************
        var MovieListView = Backbone.View.extend({
            type: "MovieListView", //for debugging
            el: "#container-movieList",  //the view should be decoupled from the DOM, but for this example this will do.
            //collection:  This will be passed in during initialization
            initialize: function() {
            },
            render: function() {
                _.each(this.collection.models, this.processMovie, this);
                return this;
            },
            processMovie: function(movie) {
                var childMovieItemView = new MovieItemView({ model: movie});
                childMovieItemView.render();
                this.$el.append(childMovieItemView.el);
            }
        });

        var MovieItemView = Backbone.View.extend({
            type: "MovieItemView", //for debugging
            template: _.template($("#template-movieItem").html()),
            tagName: "div",
            className: "tableRow",
            //el: since we're setting the tagName, we don't need set the el
            //model: pass model in
            initialize: function() {
                this.model.on("change", this.modelChanged, this);
            },
            events: {
                "click": "viewClicked"
            },
            render: function() {
                var outputHtml = this.template(this.model.toJSON());
                this.$el.html(outputHtml);
                return this;
            },
            modelChanged: function(model, changes) {
                console.log("modelChanged:" + model.get("title"));
                this.render();
            },
            viewClicked: function(event) {
                console.log("viewClicked: " + this.model.get("title"));
                pubSub.events.trigger("movie:selected", this.model);
            }
        });

        var MovieDetailView = Backbone.View.extend({
            el: "#container-movieDetailEdit",
            initialize: function () {
                pubSub.events.on("movie:selected", this.movieSelected, this);
            },
            events: {
                "click  #butChange": "viewChanged"
            },
            render: function() {
                var title = this.model.get("title");
                this.$el.find("#txtMovieTitle").val(title);
                
                var releaseYear = this.model.get("releaseYear");
                this.$el.find("#txtMovieReleaseYear").val(releaseYear);
            },
            movieSelected: function(movie) {
                console.log(movie);
                this.model = movie;
                this.render();
            },
            viewChanged: function () {
                var title = this.$el.find("#txtMovieTitle").val();
                this.model.set({ "title": title });
                
                var releaseYear = this.$el.find("#txtMovieReleaseYear").val();
                this.model.set({ "releaseYear": releaseYear });
            }
        });

        //**************************************************************************
        var myMovies = [
            { "id": "1", "title": "Bag It", "releaseYear": 2010 },
            { "id": "2", "title": "Lost Boy: The Next Chapter", "releaseYear": 2009 },
            { "id": "3", "title": "To Live & Ride in L.A.", "releaseYear": 2010 },
            { "id": "4", "title": "K-ON!: Vol. 1", "releaseYear": 2009 },
            { "id": "5", "title": "Archer: Season 2: Disc 1", "releaseYear": 2011 }
        ];

        var movies = new Movies(myMovies);
        var movieListView = new MovieListView({ collection: movies});
        movieListView.render();

        var movieDetailView = new MovieDetailView();
    </script>
</body>
</html>

 

Viewing all articles
Browse latest Browse all 28

Trending Articles