Tung
Tung

Reputation: 1671

Accommodate multiple Backbone views, models and collections in the same page

I am struggling with displaying two models/collections in the same page.

    <body>

<div id="mainContainer">
    <div id="contentContainer"></div>
</div>
<div id="mainContainer2">
    <div id="contentContainer2"></div>
</div>

<script id="list_container_tpl" type="text/template">
<div class="grid_5 listContainer">
    <div class="box">
        <h2 class="box_head grad_colour">Your tasks</h2>
        <div class="sorting">Show: <select id="taskSorting"><option value="0">All Current</option><option value="1">Completed</option></select>
            <input class="search round_all" id="searchTask" type="text" value="">
        </div>
        <div class="block">
            <ul id="taskList" class="list"></ul>
        </div>
    </div>
</div>
</script>

<script id="list2_container_tpl" type="text/template">
<div class="grid_5 mylistContainer">
    <div class="box">
        <h2 class="box_head grad_colour">Your facets</h2>
        <div class="sorting">
            %{--Show: <select id="taskSorting"><option value="0">All Current</option><option value="1">Completed</option></select>--}%
            <input class="search round_all" id="searchFacet" type="text" value="">
        </div>
        <div class="block">
            <ul id="facetList" class="list"></ul>
        </div>
    </div>
</div>
</script>


<script id="task_item_tpl" type="text/template">
<li class="task">
    <h4 class="name searchItem">{{ name }}</h4>
</li>
</script>
<script id="facet_item_tpl" type="text/template">
<li class="facet">
    <h5 class="label searchItem">{{ label }}</h5>
</li>
</script>

<script>
    var myapp = {
        model: {},
        view: {},
        collection: {},
        router: {}
    };
    var facetsSearch = {
        model: {},
        view: {},
        collection: {},
        router: {}
    };
</script>

<script src="underscore-min.js"></script>
<script src="handlebars.min.js"></script>
<script src="backbone-min.js"></script>
<script>
    /* avoid */
    _.templateSettings = {
        interpolate: /\{\{(.+?)\}\}/g
    };
</script>
<script>
    // model.tasks.js
    myapp.model.Tasks = Backbone.Model.extend({
        default:{
            completed: 0,
            name: ""
        },
        //url:"/js/libs/fixtures/task.json"
    });
    var tasks1 = new myapp.model.Tasks({
            completed: 0,
            name: "Clear dishes"
        }
    );
    var tasks2 = new myapp.model.Tasks({
            completed: 1,
            name: "Get out the trash"
        }
    );
    var tasks3 = new myapp.model.Tasks({
            completed: 0,
            name: "Do the laundry"
        }
    );
    var tasks4 = new myapp.model.Tasks({
            completed: 1,
            name: "Vacuuming the carpet"
        }
    );

    // collection.tasks.js
    myapp.collection.Tasks = Backbone.Collection.extend({
        currentStatus : function(status){
            return _(this.filter(function(data) {
                return data.get("completed") == status;
            }));
        },
        search : function(letters){
            if (letters == "") return this;

            var pattern = new RegExp(letters,"gi");
            return _(this.filter(function(data) {
                return pattern.test(data.get("name"));
            }));
        }
    });
    myapp.collection.tasks = new myapp.collection.Tasks([tasks1, tasks2, tasks3, tasks4]);

    // route.tasks.js
    myapp.router.Tasks = Backbone.Router.extend({
        routes: {
            "": "list",
        },
        list: function(){
            this.listContainerView = new myapp.view.TasksContainer({
                collection: myapp.collection.tasks
            });
            $("#contentContainer").append(this.listContainerView.render().el);
            this.listContainerView.sorts()
        }
    });
    myapp.router.tasks = new myapp.router.Tasks;

    <!-- render views -->
    myapp.view.TasksContainer = Backbone.View.extend({
        events: {
            "keyup #searchTask"     : "search",
            "change #taskSorting"   : "sorts"
        },
        render: function(data) {
            $(this.el).html(this.template);
            return this;
        },
        renderList : function(tasks){
            $("#taskList").html("");

            tasks.each(function(task){
                var view = new myapp.view.TasksItem({
                    model: task,
                    collection: this.collection
                });
                $("#taskList").append(view.render().el);
            });
            return this;
        },
        initialize : function(){
            this.template = _.template($("#list_container_tpl").html());
            this.collection.bind("reset", this.render, this);
        },
        search: function(e){
            var letters = $("#searchTask").val();
            this.renderList(this.collection.search(letters));
        },
        sorts: function(e){
            var status = $("#taskSorting").find("option:selected").val();
            if (status == "") status = 0;
            this.renderList(this.collection.currentStatus(status));
        }
    });
    myapp.view.TasksItem = Backbone.View.extend({
        events: {},
        render: function(data) {
            $(this.el).html(this.template(this.model.toJSON()));
            console.log(this.model.toJSON(), "became", this.template(this.model.toJSON()));
            return this;
        },
        initialize : function(){
            this.template = _.template($("#task_item_tpl").html());
        }
    });
</script>

<script>
    // model.facets.js
    facetsSearch.model.Facets = Backbone.Model.extend({
        default: {
            id: 0,
            label: "",
            facetValues: []
        }
    });

    var facet1 = new facetsSearch.model.Facets({
        id: 1,
        label: "Organism",
        facetValues: ["Orga1", "Orga2"]
    });
    var facet2 = new facetsSearch.model.Facets({
        id: 2,
        label: "Omics",
        facetValues: ["Omics1", "Omics2"]
    });
    var facet3 = new facetsSearch.model.Facets({
        id: 3,
        label: "Publication Date",
        facetValues: ["2016-11-01", "2016-11-02"]
    });

    // collection.facets.js
    facetsSearch.collection.Facets = Backbone.Collection.extend({
        search : function(letters){
            if (letters == "") return this;

            /**
             * the g modifier is used to perform a global match (find all matches rather than stopping after the first match).
             * Tip: To perform a global, case-insensitive search, use this modifier together with the "i" modifier.
             */
            var pattern = new RegExp(letters, "gi");
            return _(this.filter(function(data) {
                return pattern.test(data.get("label"));
            }));
        }
    });
    facetsSearch.collection.facets = new facetsSearch.collection.Facets([facet1, facet2, facet3]);

    // route.facets.js
    facetsSearch.router.Facets = Backbone.Router.extend({
        routes: {
            "": "list",
        },
        list: function(){
            this.mylistContainerView = new facetsSearch.view.FacetsContainer({
                collection: facetsSearch.collection.facets
            });
            console.log("Facet collection: ", facetsSearch.collection.facets);
            $("#contentContainer2").append(this.mylistContainerView.render().el);
            this.mylistContainerView.sorts()
        }
    });

    facetsSearch.router.Facets = new facetsSearch.router.Facets;

    facetsSearch.view.FacetsContainer = Backbone.View.extend({
        events: {
            "keyup #searchFacet" : "search",
            "change #facetSorting": "sorts"
        },
        render: function(data) {
            $(this.el).html(this.template);
            return this;
        },
        renderList : function(facets){
            $("#facetList").html("");

            facets.each(function(facet){
                var view2 = new facetsSearch.view.FacetsItem({
                    model: facet,
                    collection: this.collection
                });
                $("#facetList").append(view2.render().el);
            });
            return this;
        },
        initialize : function(){
            this.template = _.template($("#list2_container_tpl").html());
            this.collection.bind("reset", this.render, this);
        },
        search: function(e){
            var letters = $("#searchFacet").val();
            this.renderList(this.collection.search(letters));
        },
        sorts: function(e){
            /*var status = $("#taskSorting").find("option:selected").val();
             if (status == "") status = 0;
             this.renderList(this.collection.currentStatus(status));*/
        }
    });
    facetsSearch.view.FacetsItem = Backbone.View.extend({
        events: {},
        render: function(data) {
            $(this.el).html(this.template(this.model.toJSON()));
            console.log(this.model.toJSON(), "became", this.template(this.model.toJSON()));
            return this;
        },
        initialize : function(){
            this.template = _.template($("#facet_item_tpl").html());
        }
    });

</script>
<script>
    Backbone.history.start();
</script>
</body>

Display Tasks and Facets

The problem

To display Tasks above Your facets. I created two bunches of codes to render Tasks and Facets but modified the variable names respectively. Unfortunately, the former cannot be displayed.

Upvotes: -1

Views: 254

Answers (2)

T J
T J

Reputation: 43166

As Emile mentioned in his detailed answer, your problem is that you're initializing multiple routers with the same route.

Since you seem to be beginning with Backbone, I'll give you a simpler answer than creating a complicated Layout (parent) view:

Just have a single handler for the specific route, and initialize both your views inside it.

It'll look something like:

myapp.router = Backbone.Router.extend({
  routes: {
    "": "list",
  },
  list: function() {
    this.listContainerView = new myapp.view.TasksContainer({
      collection: myapp.collection.tasks
    });
    $("#contentContainer").append(this.listContainerView.render().el);

    this.listContainerView.sorts(); //this can be done inside the view
    this.mylistContainerView = new facetsSearch.view.FacetsContainer({
      collection: facetsSearch.collection.facets
    });
    $("#contentContainer2").append(this.mylistContainerView.render().el);
    this.mylistContainerView.sorts(); //this can be done inside the view
  }
});

You simply initialize 2 views in the same route.

Upvotes: 1

Emile Bergeron
Emile Bergeron

Reputation: 17430

You made 2 routers, both with an empty route. Each route is registered in Backbone.history, so when the facets router is initialized, its route overrides the tasks router route.

How to have multiple routers?

For the scope of your application, you should just start by making a single router, and handling the page with 2 lists within a parent view. Make a sort of Layout view for that page, which will handle the 2 lists:

var Layout = Backbone.View.extend({
    template: _.template($('#layout-template').html()),
    // keep the selector strings in a simple object
    selectors: {
        tasks: '.task-container',
        facets: '.facet-container',
    },
    initialize: function() {

        this.view = {
            tasks: new TaskList(),
            facets: new FacetList()
        };
    },
    render: function() {
        this.$el.html(this.template());
        var views = this.views,
            selectors = this.selectors;
        this.$(selectors.tasks).append(views.tasks.render().el);
        this.$(selectors.facets).append(views.facets.render().el);
        return this;
    }
});

Then, only one router:

var Router = Backbone.Router.extend({
    routes: {
        "": "list",
    },
    list: function() {
        this.listContainerView = new Layout();
        $("body").html(this.listContainerView.render().el);
    }
});

This won't work with your code as-is, you'll have to incorporate the concepts yourself into your app.

Otherwise, if you really want multiple routers, you must understand that they can't share a route and that at any moment, only one route can be triggered.

When you have multiple routers, each manages the routes of a single module.

var TaskRouter = Backbone.Router.extend({
    routes: {
        'tasks': 'taskList',
        'tasks/:id': 'taskDetails'
    }
    // ...snip...
});

var FacetsRouter = Backbone.Router.extend({
    routes: {
        'facets': 'facetList',
        'facets/:id': 'facetDetails'
    }
    // ...snip...
});

Other improvements

Compile the template once

It's more efficient to compile the template once, when the view is being extended, than each time a new view is initialized.

myapp.view.TasksContainer = Backbone.View.extend({
    // gets compiled once
    template: _.template($("#list_container_tpl").html()),

    initialize: function() {
        // not here, as it gets compiled for each view
        // this.template = _.template($("#list_container_tpl").html())
    },
});

Avoid the global jQuery function

See What is the difference between $el and el for additional informations.

Cache the jQuery objects

Whenever you want to select a child of the view's element, do it once and put the result in a variable.

render: function(data) {
    this.$el.html(this.template);
    // I like to namespace them inside an object.
    this.elements = {
        $list: this.$('.task-list'),
        $search: this.$('.task-sorting')
    };

    // then werever you want to use them
    this.elements.$list.toggleClass('active');

    return this;
},

Use listenTo

Avoid bind/unbind and on/off/once (aliases) in favor of listenTo/stopListening/listenToOnce.

listenTo is an improved version of bind which solves problem with memory leaks.

this.collection.bind("reset", this.render, this);
// becomes
this.listenTo(this.collection, "reset", this.render);

Pass an array of objects to collection

myapp.collection.Tasks = Backbone.Collection.extend({
    model: myapp.model.Tasks
    // ...snip...
});

myapp.collection.tasks = new myapp.collection.Tasks([{
    completed: 0,
    name: "Clear dishes"
}, {
    completed: 1,
    name: "Get out the trash"
}, {
    completed: 0,
    name: "Do the laundry"
}, {
    completed: 1,
    name: "Vacuuming the carpet"
}]);

This would be enough, Backbone collection takes care of the rest.

Upvotes: 1

Related Questions