Questions on Backbone.js with Handlebars.js - javascript

My backbone.js app with Handelbars does the following.
setup a model, its collection, view and router.
at the start, get a list of articles from the server and render it using the view via Handlebars.js template.
The code is below.
(function ($)
{
// model for each article
var Article = Backbone.Model.extend({});
// collection for articles
var ArticleCollection = Backbone.Collection.extend({
model: Article
});
// view for listing articles
var ArticleListView = Backbone.View.extend({
el: $('#main'),
render: function(){
var js = JSON.parse(JSON.stringify(this.model.toJSON()));
var template = Handlebars.compile($("#articles_hb").html());
$(this.el).html(template(js[0]));
return this;
}
});
// main app
var ArticleApp = Backbone.Router.extend({
_index: null,
_articles: null,
// setup routes
routes: {
"" : "index"
},
index: function() {
this._index.render();
},
initialize: function() {
var ws = this;
if( this._index == null ) {
$.get('blogs/articles', function(data) {
var rep_data = JSON.parse(data);
ws._articles = new ArticleCollection(rep_data);
ws._index = new ArticleListView({model: ws._articles});
Backbone.history.loadUrl();
});
return this;
}
return this;
}
});
articleApp = new ArticleApp();
})(jQuery);
Handlebars.js template is
<script id="articles_hb" type="text/x-handlebars-template">
{{#articles}}
{{title}}
{{/articles}}
</script>
The above code works fine and it prints article titles. However, my question is
When passing context to Handlebars.js template, I am currently doing $(this.el).html(template(js[0])). Is this the right way? When I do just "js" instead of js[0], the JSON object has leading and ending square brackets. Hence it recognizes as a array object of JSON object. So I had to js[0]. But I feel like it isn't a proper solution.
When I first create the "View", I am creating it like below.
ws._index = new ArticleListView({model: ws._articles});
But in my case, I should do
ws._index = new ArticleListView({collection: ws._articles});
Shouldn't I? (I was following a tutorial btw). Or does this matter? I tried both, and it didn't seem to make much difference.
Thanks in advance.

It seems like you are creating a view for a collection so you should initialize your view using collection instead of model.
As far as handlebars, I haven't used it a lot but I think you want to do something like this:
var ArticleListView = Backbone.View.extend({
el: $('#main'),
render: function(){
var js = this.collection.toJSON();
var template = Handlebars.compile($("#articles_hb").html());
$(this.el).html(template({articles: js}));
return this;
}
});
and then use something like this for the template
{{#each articles}}
{{this.title}}
{{/each}}
p.s. the line
JSON.parse(JSON.stringify(this.model.toJSON())) is equivalent to this.model.toJSON()
Hope this helps

var ArticleListView = Backbone.View.extend({
initialize: function(){
this.template = Handlebars.compile($('#articles_hb').html());
},
render: function(){
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
/////////////////////////////////////
ws._index = new ArticleListView({model: ws._articles});

Related

Unable to set the Model Attributes after a Backbone model.fetch() call

I have come across a lot of examples where the backbone-view would be like var view1 = Backbone.View.extend( { } ) but unable to get one where the backbone view is returned directly. In the below code I am able to render the default values of the model attribute and display the same in the dust template but when I do model.fetch(), in the success function I am able to see the json response in the console but unable to set the fetched values to the model attributes and render the new values. Do, let me know what I am missing here. Any help is appreciated.
define(function (require) {
'use strict';
var $ = require('jquery');
var Backbone = require('backbone');
var g = require('global/dust-globals');
var template = require('text!/dust/table1.dust');
var SampleModel = Backbone.Model.extend({
initialize: function () {
},
defaults:{
SampleUpdate:'Test date',
SampleCount: 0
},
urlRoot: "/Sample"
});
var obj1 = new SampleModel();
return Backbone.View.extend({
events: {
// 'click .search-btn': 'searchBtnClick',
},
initialize: function(){
this.testfunc();
this.render();
this.model.on("change", this.render, this);
},
render: function () {
this.$el.html(g.renderTemplate('TabView', template, {}));
//template is compiled and rendered successfully
console.log('CHECK:'+obj1.get("lastUpdate"));
return this;
},
testfunc : function () {
obj1.fetch({
success: function (response) {
console.log(JSON.stringify(response));
obj1.set("SampleUpdate", response.get("sampleUpdate"));
obj1.set("SampleCount", response.get("sampleCount"));
console.log('CHECK1:'+obj1.get("SampleUpdate"));
}
});
}
});
});
My JS code calling the above code would be as below.
var TabView = require('/SampleTab');
return Backbone.View.extend({
initialize: function () {
this.tabView = new TabView({el: '#sample-div', model:this.model, appView: this});
this.render();
},
render: function() {
this.tabView.$el.show();
this.tabView.render();
}
});
I'm having trouble understanding what exactly it is you are trying to do with your code, but it doesn't look like you're using Backbone.View.extend({ ... }) correctly. From the documentation for Backbone.View.extend:
Get started with views by creating a custom view class. You'll want to override the render function, specify your declarative events, and perhaps the tagName, className, or id of the View's root element.
[Emphasis mine.]
The Backbone.View.extend is for creating your own Backbone View classes, not instantiating objects.
If you're looking for more information, I highly recommend that you read through Addy Osmani's free e-book, Developing Backbone.js Applications. You might know some of what it teaches already, but it has some good examples of extending Backbone Views and does a much better job of explaining other fundamentals of using Backbone.js than I could here.

How to filter/search a nested backbone collection?

I have a tree view in my Backbone app, I use nested collections and models:
Collection:
define(function(require) {
var Backbone = require('backbone')
, UserListModel = require('app/models/userList');
return Backbone.Collection.extend({
model: UserListModel,
url: '/api/lists',
});
});
Model:
define(function(require) {
var Backbone = require('backbone');
return Backbone.Model.extend({
constructor: function(data, opts) {
opts = _.extend({}, opts, {parse: true});
var UserLists = require('app/collections/userLists');
this.children = new UserLists();
Backbone.Model.call(this, data, opts);
},
parse: function(data) {
if (_.isArray(data.children))
this.children.set(data.children);
return _.omit(data, 'chilren');
}
});
});
Part of The View: (full views here: http://laravel.io/bin/O9oYX)
var UserListTreeItemView = Backbone.View.extend({
render: function() {
var data = this.model.toJSON();
data.hasChildren = !!this.model.get('isFolder');
this.$el.html(this.template(data));
if( this.model.get('isFolder') ) {
var list = new UserListTreeView({
collection: this.model.children
});
this.$el.append(list.render().el);
}
return this;
}
});
And I use two Views to render my collection as a tree view. I want to add a search feature to my tree view, I can’t figure out how. It should be able to search name attributes on all models and their nested ones.
Any ideas?
If you have already the models you want on your collection, just use the inherited Underscore method filter() on the collection itself. It will return an Array of models, not a Backbone Collection, though.
http://underscorejs.org/#filter
Supposing filtering by attribute name:
var nameToSearch = "whatever";
var itemsByName = this.model.children.filter(function(item){
return item.get("name").indexOf(nameToSearch) >=0;
}
What I would do is isolate your getData method to cover both cases: filtering on/off.
You didn't specify how do you search, but I'll suppose you have a text input around and you want to use that value. Will that search in the top items only? A search-in-depth would be a little more complicated, involving each parent item to look for the name on its children. For the simple case that you'll be searching for files in every folder, keep the search filter in you parent View state. For that, I normally use a plain vanilla Backbone Model, just to leverage events.
var MySearchView = Backbone.View.extend({
initialize: function(options){
//I like the idea of having a ViewModel to keep state
this.viewState = new Backbone.Model({
searchQuery: ""
});
//whenever the search query is changed, re-render
this.listenTo(this.viewState, "change:searchQuery", this.render);
},
events: {
"click .js-search-button": "doSearch"
},
doSearch: function(e){
e.preventDefault();
var query = this.$(".js-search-input").val();
this.viewState.set("seachQuery", query);
},
render: function(){
var data = this.model.toJSON();
data.hasChildren = !!this.model.get('isFolder');
this.$el.html(this.template(data));
if( this.model.get('isFolder') ) {
//be careful with this, you're not removing your child views ever
if(this._listView) {
this._listView.remove();
}
this._listView = new UserListTreeView({
collection: this.model.children,
**searchQuery: this.viewState.get("searchQuery")**
});
this.$el.append(this._listView.render().el);
}
return this;
}
});
Now in your UserListTreeView, abstract the data-feeding for the template into a method that takes into account the search query:
var UserListTreeView = Backbone.View.extend({
initialize: function(options){
this.searchQuery = options.searchQuery || "";
},
...
getData: function(){
//filter your collection if needed
var query = this.searchQuery;
if(query !== ""){
return this.collection.filter(function(file){
return file.get("name").indexOf(query) >= 0;
}
else {
return this.collection.toJSON();
}
},
render: function() {
var items = this.getData(),
template = this.template(items);
this.$el.empty().append(template);
return this;
}
});
Voilá, the same view will render either the full collection or a filtered version whose items contain the searchQuery in their name. You can adjust the search method just by changing the comparison inside the filter call: you could do RegExp, search only for files starting with (indexOf(searchQuery) == 0), and so on.
Took it longer than expected, hope it helps. Another option would be to implement this in the collection itself, you can override its toJSON() method to return either all, or some items on it. If you find yourself writing another view that needs filterint, then probably it's a better idea to create a SearchableCollection and inherit both from there. Keep it DRY. :)
As a side note: you should have a look at MarionetteJS or build your own specialized views (Collection, and so on) just to save from typing the same over and over again.
I’m not sure I’ve totally understood your app, but here’s how I’ve done something similar before:
In your model add this:
matches: function(search) {
// a very simple and basic implementation
return this.get('name').indexOf(search) != -1;
}
And use it in UserListTreeView’s render:
render: function() {
var search = $someElement.val();
var _this = this;
_.each(this.collection.models, function(model) {
if (model.matches(search)) {
_this.addItem(model);
}
});
return this;
}
Very simple, yet effective. This is actually the most basic version to transfer the idea. You can improve this approach by extending it to other models and collections, checking for some edge cases, and improving its performance by simple optimizations.

Backbone getting attributes from a model in a view

I need help in trying to get attributes out of my model with backbone.js
Below is what I have tried so far. I connect to a REST URL and pull back data in json. I now want to display some of that data within the view. However, when I try print/console out club_url I get an undefined error. If I print out the test object itself I can see the value in the attributes section of the object.
Can someone tell me where I'm going wrong?
(function ($) {
var Model = Backbone.Model.extend({
urlRoot: '/api/test/',
initialize: function () {
this.club_url = this.club_url
}
});
var thisCollection = Backbone.Collection.extend({
urlRoot: '/api/test/',
model: Model
});
var PanelView = Backbone.View.extend({
el: '#reward_view',
initialize: function () {
_.bindAll(this, 'render');
this.collection = new thisCollection();
this.collection.bind('add', this.appendItem);
this.render();
},
render: function () {
var test = new thisCollection;
test.fetch();
console.log(test.get('club_url'))
return this;
}
});
var listView = new PanelView();
})(jQuery);
As another test I tried was to init something like this in the view
this.model = new Model()
this.model.fetch()
but then in the render function I did this:
this.model.get('club_url')
however this did not work either!
The fetching of data is async operation. So, I guess that you should wait for an event before to get club_url. I.e. something like that:
render: function () {
var test = new thisCollection;
test.fetch({
success: function(collection, response) {
console.log(test.get('club_url'))
}
});
return this;
}

Backbone.js: nested Models and Collections of the same type

Im new to Backbone and am struggling wrapping my head around something.
Let me try explain what Im trying to do:
Im making an app that has an SVG element with SVG groups inside it. The groups will be nested within each other something like this:
http://i.stack.imgur.com/yAkTE.png
I was trying to use Backbone to create a model and a view for each group. And nesting children groups inside these using collections.
Ive written the following so far just using divs instead of any SVG so as to get the logic working before I implement that side of things. But I guess my thinking is probably just completely wonky somewhere and any help would be much appreciated.
I know there are similar threads around about nesting etc. in Backbone but I havent been able to find any help in them.
You can see a JSFiddle of what Ive written so far here: http://jsfiddle.net/ZqMeV/
Here's the code so far:
(function ($) {
var Cell = Backbone.Model.extend({
initialize: function(){
this.children = new CellCollection();
}
});
var CellCollection = Backbone.Collection.extend({
model: Cell
});
var CellView = Backbone.View.extend({
tagName: "div",
initialize: function(){
if(this.model.children.models.length > 0){
_.each(this.model.children.models, function(child){
console.log(child);
var cell = new CellView({
model: child
});
$(this.el).append(cell.el);
});
} else {
$(this.el).html('cell');
}
}
});
var Canvas = Backbone.Model.extend({
initialize: function(){
//below is just for testing purposes
this.content = new Cell();
var c = new Cell();
var d = new Cell();
var e = new Cell();
this.content.children.add(c);
this.content.children.add(d);
d.children.add(e);
}
});
var CanvasView = Backbone.View.extend({
el: $("#canvas"),
tagName: "div",
initialize: function(){
this.cellView = new CellView({
model: this.model.content
});
$(this.el).append(this.cellView.el);
}
});
var canvas = new Canvas();
var canvasView = new CanvasView({
model: canvas
});
} (jQuery));
Thanks
You have quite a big context problem:
_.each(this.model.children.models, function(child){
console.log(child);
var cell = new CellView({
model: child
});
$(this.el).append(cell.el);
});
Here your this inside the each has changed. You could replace it by:
var self = this;
this.model.children.each(function(child) {
var cell = new CellView({
model: child
});
$(self.el).append(cell.el);
});
Oh and by the way, Backbone's Collections proxy a lot of underscore methods, so I took the liberty to change your each. You can also replace this.model.children.models.length by this.model.children.length. Learn to use the collections, not only the array inside :)

Backbone.js render collection in View

I have a Backbone Collection that I'm trying to render in the View. The JSON data seems correct, however I can't access the values from within the view.
Here's the basic collection:
define(['backbone', 'BaseModel'], function(Backbone, BaseModel) {
var BaseCollection = Backbone.Collection.extend({
model: BaseModel,
url: "/collection/get?json=true",
initialize: function() {}
});
return BaseCollection;
});
Here's the View:
define(['backbone', 'BaseCollection'], function(Backbone, BaseCollection) {
var BaseView = Backbone.View.extend({
el: $('#baseContainer'),
template: _.template($('#baseTemplate').html()),
initialize: function() {
_.bindAll(this);
this.collection = new BaseCollection();
this.collection.bind('all', this.render, this);
this.collection.fetch();
},
render: function() {
//This returns 3 objects which is correct based on the JSON data being returned from the server
console.log(this.collection.toJSON());
var html = this.template(this.collection.toJSON());
this.$el.html(html);
return this;
},
});
return BaseView;
});
I think I need to iterate through this.render for each model within the collection. But, I'm not sure, because it shouldn't 'render' until it completes all iterations.
Any suggestions would be great! Thank you!
You need to give your template access to the models via name. When you do this:
var html = this.template(this.collection.toJSON());
You end up passing an array to the template function, which normally expects a context object (name/value pairs). Try this:
var html = this.template({collection: this.collection});
Then in your template you can iterate through them using the collection.each iterator function or any of the underscore utility methods for iteration/filtering/map/etc. I also recommend NOT using toJSON when giving your template access to the collection as it makes your data dumber and harder to work with. toJSON is best left for when you are making HTTP requests.

Categories