So I'm still a bit of a beginner when it comes to Backbone as I was trying to play around with it today to produce some results. The issue I am coming across is being able to see the results from the json collection object. Was hoping someone could help me out a bit and point me in the right direction.
So far my set up is like so:
var Game = Backbone.Model.extend({});
var GameList = Backbone.Collection.extend({
model: Game,
url: 'link to server json object',
parse: function(response) {
return response;
}
});
var GameListView = Backbone.View.extend({
el: $('#games-list'),
initialize: function() {
var self = this;
this.collection = new GameList();
this.collection.fetch().done(function() {
self.render();
});
},
render: function() {
this.collection.each(function(game) {
console.log('Game.', game);
});
}
});
var testApp = new GameListView();
This produces in the console:
Game.
r {cid: "c2", attributes: Object, collection: r, _changing: false, _previousAttributes: Object…}
I'm not sure where I am going wrong, would like to first see the json object, then be able to cycle through each item.
You are looking for toJSON() method, this method converts the model from Backbone collection or model to JSON, for example:
var json = game.toJSON();
This method is used very often in Backbone development,usually for passing the model to the html template
Related
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.
I am not sure if I am using Models and Collections correctly. If I'm not I would really appreciate any guidance or advice into what I am doing wrong.
I have set up a Model and a Collection. The Collection has a url which is executed using the .fetch() method. I pass the Collection to the View where I log the results to the console. When I console.log(this.model) in the View I see the attributes nested a few levels deep. I would like to see the attributes in the console.log. The .toJSON() method doe not seem to work.
Here's a Fiddle to my current code: http://jsfiddle.net/Gacgc/
Here is the JS:
(function () {
var DimensionsModel = Backbone.Model.extend();
var setHeader = function (xhr) {
xhr.setRequestHeader('JsonStub-User-Key', '0bb5822a-58f7-41cc-b8a7-17b4a30cd9d7');
xhr.setRequestHeader('JsonStub-Project-Key', '9e508c89-b7ac-400d-b414-b7d0dd35a42a');
};
var DimensionsCollection = Backbone.Collection.extend({
model: DimensionsModel,
url: 'http://jsonstub.com/calltestdata'
});
var dimensionsCollection = new DimensionsCollection();
var DimensionsView = Backbone.View.extend({
el: '.js-container',
initialize: function (options) {
this.model.fetch({beforeSend: setHeader});
_.bindAll(this, 'render');
this.model.bind('reset', this.render());
return this;
},
template: _.template( $('#dimensions-template').html() ),
render: function () {
console.log( this.model.toJSON() ); //Why does this return an empty array???
return this;
}
});
var myView = new DimensionsView({model: dimensionsCollection});
}());
Is this what you're looking for?
If you're passing a collection to the view you should assign it to the collection property:
// It's a collection. Backbone views have a collection
// property. We should totally use that!
var myView = new DimensionsView({collection: dimensionsCollection});
When you attempt to bind the reset event to your view's render function, you're actually invoking the function immediately (by including the braces):
// Omit the braces to assign the function definition rather than invoke
// it directly (and immediately)
this.model.bind('reset', this.render);
But that's beside the point, because backbone's collection doesn't trigger a reset event (see documentation). One approach would be to assign the view's render function to the success parameter of the options object you pass to your collection:
var self = this;
this.collection.fetch({
beforeSend: setHeader,
success: function() {
self.render();
}
});
Finally, you need a parse function in your collection to pull the dimensions array out of the JSON you're loading:
var DimensionsCollection = Backbone.Collection.extend({
model: DimensionsModel,
url: 'http://jsonstub.com/calltestdata',
parse: function(response) {
return response.dimensions;
}
});
I'm having issues syncing JSON data received from the server with my views after a fetch.
I do not have a collection of "mainmodel", because I'm only working with one "mainmodel" at a time but numerous "mymodel", anyhow, the structure follows:
var mymodel = Backbone.Model.extend({
defaults: {k1:"",
k2:"",
k3:""}
});
var collection = Backbone.Collection.extend({model:mymodel,});
var mainmodel = Backbone.Model.extend({
defaults: {v1:"",
v2:"",
v3:"",
v4:new collection()
});
I create the nested views for "mymodel" from a render function in a parent view. This works..., only when I'm working with a new model.
// My ParentView render function
render: function() {
for (var i = 0; i < this.model.v4.length;i++) {
var view = new MyModelView({model:this.model.v4.at(i)});
this.$el.append($(view.render().el));
}
this.$el.append(this.template(this.model.toJSON()));
return this;
},
// The MyModelView render function below
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
Now, the above works if I open my application and create models from there. However, If I open my app and supply an id, I make a fetch to the server, retrieve the data, and create a new ParentView I end up getting an error that says "this.model.v4.at not a function". Ugh.
So now, if I change the FIRST render function to be, changing the at(i) to [i]
var view = new MyModelView({model:this.model.v4[i]});
And change the second render function, removing toJSON, to be:
this.$el.html(this.template(this.model));
It renders. But I still can't move around views without errors. No surprise.
I have used console.log(JSON.stringify(this.model)); as they arrive into the parentView and MyModelView. The JSON returned looks like this, whether fetched or created.
{"v1":"val1",
"v2":"val2,
"v3":"val3",
"v4":[{"k1":"key1","k2":"key2","k3","key"}, { ... }, { ... }]
}
The JSON data structures appear to be identical. I thought the JSON format was incorrect, so I tried using JSON.parse before handing the model to the view, but that didn't work. Maybe I'm way off, but I originally thought I had a JSON formatting issue, but now I don't know. The server is returning content as 'application/json'.
Edit: The JSON values for v1,v2,v3 render correctly.
Any ideas?
You have two problems: one you know about and one you don't.
The problem you know about is that your mainmodel won't automatically convert your v4 JSON to a collection so you end up with an array where you're expecting a collection. You can fix this by adding a parse to your mainmodel:
parse: function(response) {
if(response.v4)
response.v4 = new collection(response.v4);
return response;
}
The problem you don't know about is that your defaults in mainmodel has a hidden reference sharing problem:
var mainmodel = Backbone.Model.extend({
defaults: {
//...
v4: new collection()
}
});
Anything you define in the Backbone.Model.extend object ends up on your model's prototype so the entire defaults object is shared by all instances of your model. Also, Backbone will do a shallow copy of defaults into your new models. So if you m1 = new mainmodel() and m2 = new mainmodel(), then m1 and m2 will have exactly the same v4 attribute. You can solve this by using a function for defaults:
var mainmodel = Backbone.Model.extend({
defaults: function() {
return {
v1: '',
v2: '',
v3: '',
v4: new collection()
};
}
});
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.
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});