I am creating a view in backbone that accepts a collection I want to then render that view then use the collection to append another view to the orginal but I don't know how to reference the original view in the success function of the collection. When I try the following code I get undefined.
new GenreView().render(new PopVideosCollection());
define (['jquery','underscore','backbone'],function($,_,Backbone) {
GenreView = Backbone.View.extend({
tagName:"div",
className:"sect",
template: _.template($("#genreView").html()),
render: function (collection)
{
this.$el.html(this.template);
collection.fetch ({success:function (video)
{
console.log(video.toJSON());
console.log(GenreView.el);
},
});
},
});
return GenreView;
});
You need to get a reference to the instance of GenreView from inside the callback. Something like this should get you there:
var context = this;
collection.fetch ({success:function (video){
console.log(video.toJSON());
console.log(context.el);
}
});
However, you should re-think your approach a little. It would be better to call fetch on your collection, and have the view subscribe the reset event of your collection. Starting with your example code, that would look something like:
var GenreView = Backbone.View.extend({
initialize: function() {
this.listenTo(this.model, "reset", this.appendSubView);
},
render: function() {
this.model.fetch();
},
appendSubView : function(video){
console.log(video.toJSON());
console.log(this.el);
}
});
Related
I am trying to access an object(this.historyComboBox) declared in a view(StatusView)'s render function and trying to access the same object from another view(HistoryView)'s extendedEvents function.
I have tried to use this.historyComboBox to access it but unable to hold any reference. Got really puzzled. If anyone has got some different idea I am ready to try it out!
Note: StatusView gets initialized prior to HistoryView.
Following is the code snippet.
StatusView = Backbone.View.extend({
init: function() {
//some code
},
render: function() {
this.historyComoBox = new sys.ComboBox();
}
}
HistoryView = Backbone.View.extend({
template: _.template(historyTemplate),
init: function() {
//some code
},
extendedEvents: {
'click #refreshButton': function() {
//want to access historyComoBox; not accessible with 'this.historyComoBox'
}
}
}
To get a property of a StatusView instance, you need a reference to that instance. So, if you have something like this:
var statusView = new StatusView();
Then from within the methods of HistoryView, you can do this:
statusView.historyComboBox;
However, while you can do it this way, I wouldn't access the StatusView instance directly like this. A better way would be to pass it to the HistoryView instance as a parameter, which you would receive in the initialize method. This keeps the views loosely coupled,
var HistoryView = Backbone.View.extend({
initialize: function (options) {
this.statusView = options.statusView;
},
events: {
'click #refreshButton': function () {
// use this.statusView;
}
}
});
(I notice you're using the names init and extendedEvents. You don't mention that you're using a third-party library with Backbone or something else that might change those, so I'll just mention that Backbone expects these to be initialize and events respectively.)
I am using a collection view to render my array of model views. I have added a method that removes a single model view from the existing collection view, and attempts to re-render it in a new el: element.
I use collection.get(this.model) to save the model to a variable, I add that variable to my new collection which is the model of a new collection view associated with a new DOM element, and I re-use the same collection view render method. When I console.log() the new collection, I see the model that I picked from the old collection, but it's not rendering on the page.
<script>
(function($){
//---------SINGLE ENTRY MODEL----------
var Entry = Backbone.Model.extend({
defaults: function(){
return{
word: '',
definition: ''
}
}
});
//------------ENTRY MODEL COLLECTION------------
var EntryList = Backbone.Collection.extend({
model: Entry
});
//-----INSTANCIATE COLLECTION----
var dictionary = new EntryList();
var saved = new EntryList();
//-----SINGLE ENTRY VIEW------
var EntryView = Backbone.View.extend({
model: new Entry(),
tagName:'div',
events:{
'click .edit': 'edit',
'click .delete': 'delete',
'keypress .definition': 'updateOnEnter',
'click .save': 'save'
},
delete: function(ev){
ev.preventDefault;
dictionary.remove(this.model);
},
edit: function(ev){
ev.preventDefault;
this.$('.definition').attr('contenteditable', true).focus();
},
//method that adds existing model to new collection
save: function(ev){
ev.preventDefault;
var savedEntry = dictionary.get(this.model);
dictionary.remove(this.model);
saved.add(savedEntry);
console.log(savedEntry.toJSON());
},
close: function(){
var definition = this.$('.definition').text();
this.model.set('definition', definition);
this.$('.definition').attr('contenteditable', false).blur();
},
updateOnEnter: function(ev){
if(ev.which == 13){
this.close();
}
},
initialize: function(){
this.template = _.template($("#dictionary_template").html());
},
render: function(){
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
//--------------DICTIONARY VIEW------------
var DictionaryView = Backbone.View.extend({
model: dictionary,
el: $('#entries'),
initialize: function(){
this.model.on('add', this.render, this);
this.model.on('remove', this.render, this);
},
render: function(){
var self = this;
self.$el.html('');
_.each(this.model.toArray(), function(entry, i){
self.$el.append((new EntryView({model: entry})).render().$el);
});
return this;
}
});
//---------SAVED ENTRY VIEW-----------
var SavedView = Backbone.View.extend({
model: saved,
el: $('#saved'),
initialize: function(){
this.model.on('save', this.savedRender, this);
},
//method that renders new collection view with different el:
savedRender: function(){
var self = this;
self.$el.html('');
_.each(this.model.toArray(), function(saved, i){
self.$el.append((new EntryView({model: savedEntry})).render().$el);
});
return this;
}
});
//-------BINDING DATA ENTRY TO NEW MODEL VIEW-------
$(document).ready(function(){
$('#new-entry').submit(function(ev){
var entry = new Entry({word: $('#word').val(), definition: $('#definition').val() });
dictionary.add(entry);
dictionary.comparator = 'word';
console.log(dictionary.toJSON());
$('.form-group').children('input').val('');
return false;
});
var appView = new DictionaryView();
});
//--------------ROUTER----------------
var Router = Backbone.Router.extend({
routes:{
'':'home'
}
});
var router = new Router();
router.on('route:home', function(){
console.log('router home');
});
Backbone.history.start();
})(jQuery);
</script>
There are a number of problems here.
First, you do not have an instance of SavedView. The var SavedView = Backbone.View.extend(...); statement is just defining the SavedView class. In order to have a living instance of this class, you must initialize one with the new operator. You will need a line like the following somewhere in your code (a good place would be at the end of the jQuery ready handler):
var saved_view = new SavedView();
Next, we will investigate the save method of the EntryView class. The var savedEntry = dictionary.get(this.model); statement is completely unnecessary because we know that dictionary.get(this.model) will return this.model - which we obviously already have an instance of. So we can remove the clutter from this method and be left with the following:
ev.preventDefault;
saved.add(this.model);
dictionary.remove(this.model);
However, we are still not at our destination. If we turn our attention to the SavedView class definition, we see that it is binding its render method to the 'save' event on its collection, the saved object. Its not the 'save' event we should be binding to, but rather the 'add' event - as that is what will be triggered when we add models to saved:
this.model.on('add', this.savedRender, this);
If we test our code now we should get scolded with a reference error on savedEntry within SavedView.savedRender. It looks like this is a typo and what was intended was `saved'. (You will notice below that in addition to correcting the typo, I have also removed a set of parentheses from this expression that served no function save for making the code less readable):
self.$el.append(new EntryView({ model: saved }).render().$el);
EDIT:
In response to your follow-up question about the saved variable inside the SavedView.savedRender method:
The saved object in this case is a single Entry model. The reason for your confusion is that we are re-using the variable name "saved". Within the _.each callback we have defined the first parameter to be called "saved"; this "saved" is local to the callback and is not related to the EntryList collection defined previously. Within our callback, saved is an element of the saved collection (yikes!) - which is a lesson in why variable naming is important.
As I proceeded to change the name of "saved" in the savedRender method, I noticed a few other refactorings that were screaming to be made. I have listed my refactorings below:
A purpose of using Backbone (http://backbonejs.org/) is to give us access to convenient helpers for objects (models) and arrays (collections). Backbone collections have an each method we can make use of instead of passing our collection to Underscore's (http://underscorejs.org/) each.
As stated above, saved is a terrible name for our each callback parameter because it conflicts conceptually with the name of the collection. Because saved is a collection of Entry models, "entry" is a much more suitable name.
Backbone allows us to pass the context to our each callback that will be our this within that callback. This allows us to skip the step of caching our this in the self variable.
My refactored savedRender becomes:
savedRender: function () {
this.$el.empty();
this.model.each(function (entry) {
this.$el.append(new EntryView({ model: entry }).render().$el);
}, this);
return this;
}
I'm trying to include an optional argument in my Backbone view's render function, like this:
var AppView = Backbone.View.extend({
el: '#app',
initialize: function(){
_.bindAll(this,'render');
this.listenTo(app.collection.sellables,'reset', this.render);
},
render: function(sellableId){
if(typeof sellableId == "undefined"){
var sellable = app.collection.sellables.first();
}
else{
var sellable = app.collection.sellables.get(sellableId);
}
var view = new SellableView({model: sellable});
this.$('#sellables').append(view.render().el);
},
});
The first time this view renders, it occurs because the app.collection.sellables collection gets reset after receiving some data from the server. When this reset occurs, the view's render function gets called, but instead of being called with sellableId as undefined, the sellableId is equal to the app.collection.sellables variable, which is causing errors. To clarify, the view's render function is being called because of this:
app.collection.sellables.reset(data.skus);
Why would my render function be receiving the collection as an argument rather than the argument being undefined?
Also, if I render the view manually, it works correctly and the sellableId argument is undefined as expected:
var app.view.app = new AppView;
app.view.app.render();
You're binding your render to "reset" right here:
this.listenTo(app.collection.sellables,'reset', this.render);
From the Catalog of Events:
"reset" (collection, options) — when the collection's entire contents have been replaced.
So a handler for a "reset" event always gets the collection as its first argument. Keep in mind that a JavaScript function's arguments are determined when the function is called, not when the function is defined.
You have a few options. You could bind something else to the "reset" event:
initialize: function() {
//...
this.listenTo(app.collection.sellables, 'reset', this.been_reset);
},
been_reset: function() {
this.render();
}
You could leave render without arguments (like a standard render) and have a separate function to handle the arguments:
render: function() {
return this.append_model(app.collection.sellables.first());
},
append_model: function(sellable) {
var view = new SellableView({model: sellable});
this.$('#sellables').append(view.render().el);
return this;
}
Then you'd call view.append_model(app.collection.sellables.get(sellableId)) to add a specific model.
OK, super basic Backbone question - I've been searching all round for this, but am just too slow to get it despite a multitude of similar questions. Rest assured I am suitably ashamed.
Anyway, enough self-flagellation - why doesn't this render?
var app = app || {};
app.Option = Backbone.Model.extend({
url: 'http://localhost:4711/api'
//This url contains the following JSON: {"title": "Blahblah", "author": "Luke Skywalker"};
});
app.View = Backbone.View.extend({
el: 'body',
initialize: function(){
this.model.fetch();
this.model.bind('change', this.render(), this);
},
render: function(){
this.$el.html(this.model.get('title'));
return this;
}
});
$(function() {
var option = new app.Option();
this.homeView = new app.View({ //Tried changing this to a standard var declaration but didn't work
model: option
});
this.homeView.render();
});
So I'm expecting to see the JSON "Blahblah" on the screen, but I see nothing. The JSON is being fetched correctly (I can see the successful GET request in the firebug console) and I think I've ensured the data is fetched before I attempt to render it...
So what's wrong? The console is giving me this error: "TypeError: (intermediate value).callback.call is not a function"
Thanks!
One thing is that you're calling this.render() immediately in your event binding rather than just binding the callback. Do this instead (using listenTo for best practices):
initialize: function(){
this.listenTo(this.model, 'change', this.render);
this.model.fetch();
}
Is it possible that the model is not actually changing? You might try to bind to sync instead of change to see if that works.
You also render twice. Once directly with this.homeView.render() and once via the event handler. If you really want to keep your model fetch in initialize and bind to the change event you don't need the direct render.
Play with those and see if that doesn't fix it.
Just remove the parentheses from the render method while binding:
this.model.bind('change', this.render, this);
Also using on or listenTo is a better approach then bind.
I would construct the backbone skeleton in the following way:
var app = app || {};
app.Option.Model = Backbone.Model.extend({});
app.Option.Collection = Backbone.Collection.extend({
model : app.Option.Model,
fetch : function(options) {
Backbone.Collection.prototype.fetch.call(this, options);
},
url : function() {
return 'http://localhost:4711/api';
},
parse: function(response) { // this is the ajax call
console.log(response);
}
});
Then in View just call the fetch method on initialize:
app.Option.View = Backbone.View.extend({
collection : app.Option.Collection,
initialize : {
this.collection.bind('reset', this.render, this);
this.collection.fetch();
},
render : {
var results = this.collection.toJSON();
console.log(results);
}
});
This is my minimal backbone skeleton when i need to call a webservice. I haven't tested locally, but this way the code should work.
I have no idea why this code is not working.
Reading the documentation,
the templateHelpers should be called.
My goal is to pass the this.collection.length to the template.
Any hints? thanks.
I am using Backbone.Marionette v0.9.5
return Marionette.CompositeView.extend({
className: 'user-board',
template: usersTemplate,
itemView: userItemView,
initialize: function () {
this.collection = new UseList();
this.collection.fetch();
},
appendHtml: function (collectionView, itemView) {
collectionView.$el.find('ul.users-list').append(itemView.el);
},
templateHelpers: function () {
console.log(this.collection.length);
},
serializeData: function () {
return {
weekly: this.options.weekly,
users_length: this.collection.length // here the length is zero
// after the fetch the length is > 0
// but in template remains 0
};
}
});
To fix my issue I have to make the following...
initialize: function () {
_.bindAll(this, 'render');
this.collection = new NewCollection();
this.collection.fetch({
success: this.render
});
}
Is there a better way to make it working?
Reading the Marionette Documentation serializeData method, which is the one using mixinTemplateHelpers, is only called on Item View.render method here and in your current code you do not render at all
UPDATE:
This way everytime the collection receives new data it will update your view the new length
initialize: function () {
_.bindAll(this, 'render');
this.collection = new NewCollection();
this.collection.fetch();
this.collection.bind('reset', this.render);
}
This code only declares the a view. Can you share the code the instantiates the view and displays it? templateHelpers will be called and the data passed to the template when the template is rendered. That is, you either need to show the view in a region which implicitly calls the render method on the view, or explicitly call the render method.
To be useful, templateHelpers should return an object. For instance:
templateHelpers: function() {
return {colLength: this.collection.length};
}
One important thing to keep in mind: fetch trigger an AJAX request that is done asynchronously. If you want to wait for the fetch to succeed before rendering the view, then you need to use Marionette.Async.
Update based on the update question
To avoid calling render from the view's initialize and only do it when render is called externally, change your code to:
return Marionette.CompositeView.extend({
className: 'user-board',
template: usersTemplate,
itemView: userItemView,
initialize: function () {
this.collection = new UseList();
var that = this;
this.defer = $.Deferred();
this.collection.fetch({
success: that.defer.resolve,
error: that.defer.resolve
});
},
appendHtml: function (collectionView, itemView) {
collectionView.$el.find('ul.users-list').append(itemView.el);
},
templateHelpers: function () {
console.log(this.collection.length);
// For greater flexibility and maintainability, don't override `serializeData`.
return {
weekly: this.options.weekly,
users_length: this.collection.length
};
},
render: function() {
var that = this,
args = arguments;
$.when(this.defer).done(function() {
Marionette.CompositeView.prototype.apply(that, args);
});
}
});
I'm resolving this.render both on success and error, otherwise if there is an error the view will never render (unless that's what you want).
Note that if you use Marionette.Async then you would return this.defer in the view's beforeRender and Marionette.Async would take care of delaying the rendering.
Also note that once this.defer is resolved, future renders will run when called as there is nothing to wait for, until this.defer has been reset programmatically.
At least in Marionette v1.0.3, I'm liking the pattern that rendering is handled automatically during a call to Region.show(), so I call that from a controller object which has the collection and passes it to the view on instantiation then shows the view. I don't even have to put this logic in a fetch success callback or explicitly bind to the 'reset' event, because the Marionette composite/collection view already knows to (re-)render itself on fetch success (which a debugger will show you).
After using a setup like has been detailed, you can also use template helpers a bit more usefully than has been described so far.
For example,
If you simply drop in <%= functionName %> into the template where you are trying to get the number to show up visually on the front end page (since you want .length I see), marionette will simply do the work for you.
So like this:
--Template File--
<div id="followerCount"> <%= showCount %> </div>
--Helper Function in View--
templateHelpers: {
showCount: function(){
return this.collection.length;
}
}
Hope that made sense or at least helps someone else perhaps looking for a simpler way to integrate database returned json to their templates.