Looked around SO but couldn't find anything useful, so..
I have a Backbone.js contacts model with a contact card view. This view has many inputs where you can edit the contacts information.
I have many other forms on the page that are NOT backbone models, so they use a 'save button' to save. I basically want this save button to also trigger Contacts.CardView.saveCard(); (which could possibly be FileApp.cardView.saveCard as well? -- some of my code is below.
Is there any way to do this? I thought I could just use the following, but it seems it won't bind an event to anything outside the view?:
events: {
"change input": "change",
"click #save": "saveCard"
},
$('#save').click(function() {
FileApp.cardView.saveCard;
_SAVE.save();
})
CardView
window.Contacts.CardView = Backbone.View.extend({
events: {
"click #save": "saveCard" // doesnt work because #save is outside the view?
},
saveCard: function(e) {
this.model.set({
name:$('#name').val()
});
if (this.model.isNew()) {
var self = this;
FileApp.contactList.create(this.model, {
success:function () {
FileApp.navigate('contacts/' + self.model.id, false);
}
});
} else {
this.model.save();
}
return false;
}
}
Router:
var FileRouter = Backbone.Router.extend({
contactCard:function (id) {
if (this.contactList) {
this.cardList = new Contacts.CardCollection();
var self = this;
this.cardList.fetch({
data: {
"id":id
},
success: function(collection, response) {
if (self.cardView) self.cardView.close();
self.cardView = new Contacts.CardView({
model: collection.models[0]
});
self.cardView.render();
}
});
} else {
CONTACT_ID = id;
this.list();
}
}
});
var FileApp = new FileRouter();
One option is to create your own Events object for this case:
// Before initializing views, etc.
var formProxy = {};
_.extend(formProxy, Backbone.Events);
// Add the listener in the initialize for the CardView
window.Contacts.CardView = Backbone.View.extend({
initialize : function() {
formProxy.on('save', this.saveCard, this);
},
saveCard: function() {
this.model.set({
name:$('#name').val()
});
if (this.model.isNew()) {
var self = this;
FileApp.contactList.create(this.model, {
success:function () {
FileApp.navigate('contacts/' + self.model.id, false);
}
});
} else {
this.model.save();
}
return false;
}
}
// Save
$('#save').click(function() {
formProxy.trigger('save');
});
See: http://documentcloud.github.com/backbone/#Events
Related
I want to build a simple backbone app for depositing and withdrawing funds via Stripe. Since a lot of the functionality is common, I placed that in a Stripe view, and extend the Deposit and Withdraw views from it, like so:
var App = {
Models: {},
Collections: {},
Views: {},
Router: {}
}
App.Views.Home = Backbone.View.extend({
el: $('#main-content'),
template: Handlebars.compile($('#home-template').html()),
render: function() {
this.$el.html(this.template())
return this
},
events: {
'click #deposit-button': 'navigateToDeposit',
'click #withdraw-button': 'navigateToWithdraw'
},
navigateToDeposit: function(e) {
Backbone.history.navigate('/deposit', true)
},
navigateToWithdraw: function(e) {
Backbone.history.navigate('/withdraw', true)
}
})
App.Views.Stripe = Backbone.View.extend({
el: $('#main-content'),
initialize: function() {
Stripe.setPublishableKey('pk_test_0QvQdPBkXAjB4EBsT4mf226x')
},
render: function() {
this.$el.html(this.template())
return this
},
events: {
'click #submit': 'submitForm'
},
submitForm: function(e) {
e.preventDefault()
$('#submit').prop('disabled', true)
var that = this
Stripe.card.createToken($('#form'), that.stripeResponseHandler)
},
stripeResponseHandler: function(status, response) {
var $form = $('#form')
if(response.error) {
$form.find('.payment-errors').text(response.error.message)
$('submit').prop('disabled', false)
} else {
console.log(this)
var form_data = this.getFormData(response.id),
that = this
$.post(that.transaction_endpoint, form_data, function(data, textStatus, jqXHR) {
Backbone.history.navigate('/home', true)
})
}
}
})
App.Views.Deposit = App.Views.Stripe.extend({
template: Handlebars.compile($('#deposit-template').html()),
getFormData: function(token) {
return {
amount: $('#form input[name=amount]').val(),
token: token
}
},
transaction_endpoint: 'handledeposit'
})
App.Views.Withdraw = App.Views.Stripe.extend({
template: Handlebars.compile($('#withdraw-template').html()),
getFormData: function(token) {
return {
name: $('#form input[name=name]').val(),
email: $('#form input[name=email]').val(),
token: token
}
},
transaction_endpoint: 'handlewithdraw'
})
App.Router = Backbone.Router.extend({
routes: {
'deposit' : 'showDepositView',
'withdraw' : 'showWithdrawView',
'*path' : 'showHomeView'
},
showDepositView: function() {
var depositView = new App.Views.Deposit()
depositView.render()
},
showWithdrawView: function() {
var withdrawView = new App.Views.Withdraw()
withdrawView.render()
},
showHomeView: function() {
var homeView = new App.Views.Home()
homeView.render()
}
})
var router = new App.Router()
Backbone.history.start()
The call to the getFormData method gives me an error saying the function is undefined, even though I have defined it in both Deposit and Withdraw views. Also, I added a console.log(this) right above it, and it logs the Window object to the console instead of the View. What am I doing wrong here?
I have a feeling it's to do with this call:
Stripe.card.createToken($('#form'), that.stripeResponseHandler)
Try binding this to the calling scope using .bind():
Stripe.card.createToken($('#form'), that.stripeResponseHandler.bind(this))
You don't really need to do var that = this but I'll leave it in for simplicity's sake.
Heres my code:
var RowsSubView = Backbone.View.extend({
initialize: function() {
log.debug(this.collection);
},
render: function() {
var html = RowView();
this.setElement(html);
return this;
}
});
var View = BaseView.extend({
id: 'wrapper',
className: 'container-fluid',
events: {
},
initialize: function() {
_.bindAll(this, 'render');
log.debug('Initialized Queue View');
this.opportunities = new Opportunities();
this.opportunities.on('add', function(model){
});
this.opportunities.fetch({
success: function(response, options) {
},
error: function(response) {
}
});
},
render: function() {
var template = QueueView();
this.$el.html(template);
this.renderRowsSubView();
return this;
},
renderRowsSubView: function() {
// render rows
this.row = new RowsSubView({collection: this.opportunities});
this.row.render();
this.$el.find('tbody').append(this.row.el);
}
});
Heres my question:
Sorry for the noob question! I am learning Backbone and having a bit of an issue. I've looked at a bunch of tutorials/guides, but I think I've confused myself.
I am trying to create a list of items and render them in a table. I want to pass each item into my template and spit it out in the view.
I am stuck after passing my collection to my RowsSubView. I'm not sure how to render each object in the template. Then insert those.
PS: I am able to log this.collection in my RowsSubView and see an object with the array of items.
Thanks.
Ok well start with this. Looks like there's quite a bit of cleanup that needs to be done =)
var RowsSubView = Backbone.View.extend({
initialize: function() {
log.debug(this.collection);
},
render: function() {
//var html = RowView(); // Looks like you're already placing a tbody as the container
//this.setElement(html);
this.collection.forEach(function( model ){
this.$el.append( RowView( model.toJSON() ) ); // Assuming RowView knows what to do with the model data
});
return this;
}
});
Then change the renderRowsSubView to
renderRowsSubView: function() {
// render rows
this.row = new RowsSubView({collection: this.opportunities});
this.row.render();
this.$el.find('tbody').append(this.row.$el.html());
}
For those that this might help, heres what I ended up with:
var RowsSubView = Backbone.View.extend({
initialize: function() {
},
render: function() {
var html = RowView({
opp: this.model.toJSON()
});
this.setElement(html);
return this;
}
});
var View = BaseView.extend({
id: 'wrapper',
className: 'container-fluid',
events: {
},
initialize: function() {
_.bindAll(this, 'render', 'add');
log.debug('Initialized Queue View');
this.opportunities = new Opportunities();
this.opportunities.on('add', this.add);
this.fetch();
},
add: function(row) {
this.row = new RowsSubView({model: row});
this.row.render();
$('tbody').append(this.row.el);
},
fetch: function() {
this.opportunities.fetch({
data: $.param({
$expand: "Company"
}),
success: function(response, options) {
// hide spinner
},
error: function(response) {
// hide spinner
// show error
}
});
},
render: function() {
var template = QueueView();
this.$el.html(template);
return this;
}
});
return View;
});
Hi I have a basic question related to my Backbone code.
I first initialize 4 SpinnerView in a file called js.js. In my main code file called app.js I declare the model views and inside each view there is a model called Spinner. Inside each Spinner there is a collection called WordCollection and inside the collection there are models called Word.
The question is, how do I access to "test" variable inside SpinnerView, only in one of the 4 renders (i.e. the 3rd SpinnerView render) from the file js.js.
All help will be appreciated. Thanks!
Here is a sample of my code in the file where I render the Spinners:
//file js.js
(new SpinnerView()).render();
(new SpinnerView()).render();
(new SpinnerView()).render();
(new SpinnerView()).render();
And here is a sample of my code from my main code file:
//file app.js
(function($) {
// model word
window.Word = Backbone.Model.extend({
url: 'save.php',
defaults: {
word: '',
}
});
//collection word
window.WordCollection = Backbone.Collection.extend({
model: Word
});
// spinner model
window.Spinner = Backbone.Model.extend({
url: '/beta/save.php',
wordCollection: null,
defaults: {
title: 'title',
},
initialize: function() {
this.wordCollection = new WordCollection();
},
addWord: function(bs) {
this.wordCollection.add(bs);
}
});
// spinner view
window.SpinnerView = Backbone.View.extend({
template: null,
spinner: null,
el: '',
test: false, //<---- THIS IS THE VARIABLE I WANT TO ACCESS
initialize: function() {
_.bindAll(this, 'focusAddWord', 'addWord', 'onEnterAddWord', 'focusSetTitle', 'setTitle', 'onEnterSetTitle');
this.template = _.template($('#spinner-template').text());
this.spinner = new Spinner();
},
render: function() {
var el = $(this.template()).appendTo('.spinners');
this.setElement(el);
},
focusAddWord: function() {
this.$el.find('.add-word-input input').val('');
this.$el.find('.add-word-input input').focus();
},
addWord: function() {
var word = new Word();
var val = this.$el.find('.add-word-input input').val();
// validate minimum characters
if(this.$el.find('.add-word-input input').val().length > 0){
// go on
this.spinner.addWord({
word: val,
});
word.set({
word: val,
});
word.toJSON();
word.save();
this.$el.find('.add-word-input').hide();
this.renderWordCollection();
}
this.$el.find('.add-word-input').hide();
},
onEnterAddWord: function(ev) {
if (ev.keyCode === 13) {
this.$el.find('.add-word-input input').trigger('blur');
this.$el.find('.viewbox').trigger('click');
}
},
focusSetTitle: function() {
this.$el.find('.set-title-input input').val('');
this.$el.find('.set-title-input input').focus();
this.$el.find('.set-title-input input').addClass('input-active');
},
setTitle: function() {
var val = this.$el.find('.set-title-input input').val();
if(this.$el.find('.set-title-input input').val().length > 0){
// go on
this.spinner.set('title', val);
this.spinner.toJSON();
this.spinner.save();
}
},
onEnterSetTitle: function(ev) {
if (ev.keyCode === 13) {
this.$el.find('.set-title-input input').trigger('blur');
}
},
// call after adding a word to spinner.
renderWordCollection: function() {
var wc = this.spinner.wordCollection;
var ListTemplate = _.template($('#word-collection-template').html(),{wc: wc});
this.$el.find('ul').html(ListTemplate);
}
});
})(jQuery);
It's not entirely clear what you want to do with test, but to use it as an instance variable simply initialize it:
window.SpinnerView = Backbone.View.extend({
// code removed for brevity
test: false, //<---- THIS IS THE VARIABLE I WANT TO ACCESS
initialize: function() {
_.bindAll(this, 'focusAddWord', 'addWord', 'onEnterAddWord', 'focusSetTitle', 'setTitle', 'onEnterSetTitle');
this.template = _.template($('#spinner-template').text());
this.spinner = new Spinner();
this.test = false, //<---- PUT IT HERE
},
Then, you can access it from within function inside the view:
focusAddWord: function() {
console.log(this.test);
this.$el.find('.add-word-input input').val('');
this.$el.find('.add-word-input input').focus();
},
And you can also access it from outside:
var view = new SpinnerView();
view.render();
console.log(view.test);
And modify it:
view.test = true;
In addition, don't forget you can pass options when instanciating a view:
initialize: function(options) {
_.bindAll(this, 'focusAddWord', 'addWord', 'onEnterAddWord', 'focusSetTitle', 'setTitle', 'onEnterSetTitle');
this.template = _.template($('#spinner-template').text());
this.spinner = new Spinner();
// use an empty `options` object if none is provided, fallback to `false` default
this.test = (options || {}).mustBeTested || false,
},
// ...
focusAddWord: function() {
if(this.test){
// do something when the view needs to be tested
}
this.$el.find('.add-word-input input').val('');
this.$el.find('.add-word-input input').focus();
},
You then simply pass options as appropriate:
(new SpinnerView()).render();
(new SpinnerView()).render();
(new SpinnerView({ mustBeTested: true })).render();
(new SpinnerView()).render();
I've created 2 separate views, 1 to render the template and the other one is where I bind the events, then I tried merging them into one in which case it causes an Uncaught TypeError: Object [object Object] has no method 'template'. It renders the template and the events are working as well, but I get the error.
edit.js, this is the combined view, which I think it has something to do with their el where the error is coming from
window.EditView = Backbone.View.extend ({
events: {
"click #btn-save" : "submit"
},
initialize: function() {
this.render();
},
render: function() {
$(this.el).html(this.template());
return this;
},
submit: function () {
console.log('editing');
$.ajax({ ... });
return false;
}
});
var editView = new EditView();
signin.js, this is the view that I can't merge because of the el being used by the ajax call and in SigninView's $(this.el) which causes the rendering of the templates faulty
window.toSigninView = Backbone.View.extend ({
el: '#signin-container',
events: {
"click #btn-signin" : "submit"
},
initialize: function() {
console.log('Signin View');
},
submit: function() {
$.ajax({ ... });
return false;
}
});
var toSignin = new toSigninView();
window.SigninView = Backbone.View.extend({
initialize: function() {
this.render();
},
render: function() {
$(this.el).html(this.template());
return this;
}
});
and I use utils.js to call my templates
window.utils = {
loadTpl: function(views, callback) {
var deferreds = [];
$.each(views, function(index, view) {
if (window[view]) {
deferreds.push($.get('templates/' + view + '.html', function(data) {
window[view].prototype.template = _.template(data);
}));
} else {
alert(view + " not found");
}
});
$.when.apply(null, deferreds).done(callback);
}
};
In my Router.js, this is how I call the rendering of templates
editProfile: function() {
if (!this.editView) {
this.editView = new EditView();
}
$('#global-container').html(this.editView.el);
},
utils.loadTpl (['SigninView', 'EditView'],
function() {
appRouter = new AppRouter();
Backbone.history.start();
});
I think that I figured out your problem.
First merge your views and delete the line var toSignin = new toSigninView();
Second modify your utils.js code like this :
window[view].prototype.template = _.template(data);
new window[view]();
I'm relatively new to Backbone and though I know the general idea of how to use it, my learning has been rapid and I'm probably missing some key elements.
So I have a collection that contains an attribute called "type" which can be article, book, video, class. I have the view rendering and everything but I need to be able to filter the collection when links are clicked.
My question is - how can I get it to filter down the collection and still be able to refilter the original collection when I click on another type?
Here's the gist of my code, I simplified it for easy reading:
var TagsView = Backbone.View.extend({
initialize: function(query) {
this.collection = new TagsCollection([], {query: self.apiQuery} );
this.collection.on('sync', function() {
self.render();
});
this.collection.on('reset', this.render, this);
},
render: function() {
//renders the template just fine
},
filter: function() {
//filtered does work correctly the first time I click on it but not the second.
var filtered = this.collection.where({'type':filter});
this.collection.reset(filtered);
}
});
update: I managed to get this working. I ended up triggering a filter event.
var TagsCollection = Backbone.Collection.extend({
initialize: function(model, options) {
this.query = options.query;
this.fetch();
},
url: function() {
return '/api/assets?tag=' + this.query;
},
filterBy: function(filter) {
filtered = this.filter(function(asset) {
return asset.get('type') == filter;
});
this.trigger('filter');
return new TagsCollection(filtered, {query: this.query});
},
model: AssetModel
});
And then in my view, I added some stuff to render my new collection.
var TagsView = Backbone.View.extend({
initialize: function(query) {
this.collection = new TagsCollection([], {query: self.apiQuery} );
this.collection.on('sync', function() {
self.render();
});
this.collection.on('filter sync', this.filterTemplate, this);
this.collection.on('reset', this.render, this);
},
render: function() {
//renders the template just fine
},
filterCollection: function(target) {
var filter = $(target).text().toLowerCase().slice(0,-1);
if (filter != 'al') {
var filtered = this.collection.filterBy(filter);
} else {
this.render();
}
},
filterTemplate: function() {
filterResults = new TagsCollection(filtered, {query: self.apiQuery});
console.log(filterResults);
$('.asset').remove();
filterResults.each(function(asset,index) {
dust.render('dust/academy-card', asset.toJSON(), function(error,output) {
self.$el.append(output);
});
});
},
});
The reason it's not working a second time is because you're deleting the models that don't match your filter when you call reset. That's normal behaviour for the reset function.
Instead of rendering with the view's main collection, try using a second collection just for rendering which represents the filtered data of the original base collection. So your view MIGHT look something like:
var TagsView = Backbone.View.extend({
filter: null,
events: {
'click .filter-button': 'filter'
},
initialize: function (query) {
this.baseCollection = new TagsCollection([], {query: self.apiQuery} );
this.baseCollection.on('reset sync', this.filterCollection, this);
this.collection = new Backbone.Collection;
this.collection.on('reset', this.render, this);
},
render: function () {
var self = this,
data = this.collection.toJSON();
// This renders all models in the one template
dust.render('some-template', data, function (error, output) {
self.$el.append(output);
});
},
filter: function (e) {
// Grab filter from data attribute or however else you prefer
this.filter = $(e.currentTarget).attr('data-filter');
this.filterCollection();
},
filterCollection: function () {
var filtered;
if (this.filter) {
filtered = this.baseCollection.where({'type': this.filter});
} else {
filtered = this.baseCollection.models;
}
this.collection.reset(filtered);
}
});
To remove any filters, set a button with class filter-button to have an empty data-filter attribute. collection will then be reset with all of baseCollection's models
Here's a better answer to this. Instead of making it so complicated, you can just use the where method. Here's my replacement solution for the question above.
filterby: function(type) {
return type === 'all' ? this : new BaseCollection(this.where({type: type});
});
You can try using comparator function of your Collection.
http://backbonejs.org/#Collection-comparator
Basically its is like sorting your collection.