I'm having a problem where my backbone model isn't parsing something correctly. Here is the listing.js:
SpendYourSavings.Models.Listing = Backbone.Model.extend({
urlRoot: "api/listings/",
images: function() {
this._images = this._images || new SpendYourSavings.Collections.Images([], { listing: this });
return this._images;
},
reviews: function() {
this._reviews = this._reviews || new SpendYourSavings.Collections.Reviews([], { listing: this });
return this._reviews;
},
shop: function() {
this._shop = this._shop || new SpendYourSavings.Models.Shop([], { listing: this });
return this._shop;
},
parse: function(data) {
if(data.images) {
this.images().set(data.images, { parse: true });
delete data.images;
}
if(data.reviews) {
this.reviews().set(data.reviews, { parse: true });
delete data.reviews;
}
if(data.shop) {
this.shop().set(data.shop, { parse: true });
delete data.shop;
}
return data;
}
});
Images and reviews work, but shop doesn't quite work. It sets the attributes of shop correctly, but it doesn't set the image properly.
Here is the shop.js:
SpendYourSavings.Models.Shop = Backbone.Model.extend({
urlRoot: "/api/shops",
reviews: function() {
this._reviews = this._reviews || new SpendYourSavings.Collections.Reviews([], {});
return this._reviews;
},
listings: function() {
this._listings = this._listings || new SpendYourSavings.Collections.Listings([], {});
return this._listings;
},
user: function() {
this._user = this._user || new SpendYourSavings.Models.User([], {});
return this._user;
},
image: function() {
this._image = this._image || new SpendYourSavings.Models.Image([], {});
return this._image
},
parse: function(data) {
console.log("shop parse data: " + data);
debugger
if(data.listings) {
this.listings().set(data.listings, { parse: true });
delete data.listings;
}
if(data.reviews) {
this.reviews().set(data.reviews, { parse: true });
delete data.reviews;
}
if(data.user) {
this.user().set(data.user, { parse: true });
delete data.user;
}
if(data.image) {
debugger
this.image().set(data.image, { parse: true });
delete data.image;
}
return data
}
});
The parse function in the shop.js never even when I receive a shop in the listing.js parse function! shop.image() doesn't get set to an image model properly, so I have to call something wonky like shop.get('image').url to get the url.
Presumably, the reason you're memoizing the image model in the shop is to maintain listeners and keep a single instance of that model around.
Collection#set takes a parse option that tells it to call parse on all the models that were set on the collection. Model#set is the method called immediately after calling parse using the attributes returned from parse.
In this case, we want to call #set on the associated shop model using the parsed attributes. So first lets call parse. It should look something like this:
SpendYourSavings.Models.Listing = Backbone.Model.extend({
urlRoot: "api/listings",
images: function() {
this._images = this._images || new SpendYourSavings.Collections.Images([], { listing: this });
return this._images;
},
reviews: function() {
this._reviews = this._reviews || new SpendYourSavings.Collections.Reviews([], { listing: this });
return this._reviews;
},
shop: function() {
// Notice the first argument is an object when initializing models.
this._shop = this._shop || new SpendYourSavings.Models.Shop({}, { listing: this });
return this._shop;
},
parse: function(data) {
if(data.images) {
this.images().set(data.images, { parse: true });
delete data.images;
}
if(data.reviews) {
this.reviews().set(data.reviews, { parse: true });
delete data.reviews;
}
if(data.shop) {
var shopParams = this.shop().parse(data.shop);
this.shop().set(shopParams);
delete data.shop;
}
return data;
}
}
});
Your issue is that parse: true on set only really applies to collections.
These lines
this.images().set(data.images, { parse: true });
this.reviews().set(data.reviews, { parse: true });
work, because you are saying "add whole new models from this JSON".
This line
this.image().set(data.image, { parse: true });
however, is trying to say, parse these params, and set values, but that is weird on a model. Should it literally only parse the attributes that were passed in? Should it merge the attributes that the model already has? What if there were dependencies between the things already in the model and the things being parsed?
Instead, you might try restructuring your top-level parsing, e.g
SpendYourSavings.Models.Listing = Backbone.Model.extend({
urlRoot: "api/listings/",
images: function() {
return this.get('images');
},
reviews: function() {
return this.get('reviews');
},
shop: function() {
return this.get('shop');
},
parse: function(data) {
if (data.images){
data.images = new SpendYourSavings.Collections.Images(data.images, { listing: this, parse: true});
}
if (data.reviews){
data.reviews = new SpendYourSavings.Collections.Reviews(data.reviews, { listing: this, parse: true});
}
if (data.shop){
data.shop = new SpendYourSavings.Models.Shop(data.shop, { listing: this, parse: true});
}
return data;
}
});
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.
I know Im pretty close to figuring this out. Im trying to filter out my collection based on if favorite eq true. If I console.log - I can see it's doing its job. But it's not updating my view.
Anyone have any idea what I'm missing or doing wrong?
Here is my code:
var Products = Backbone.Model.extend({
// Set default values.
defaults: {
favorite: false
}
});
var ProductListCollection = Backbone.Collection.extend({
model: Products,
url: '/js/data/wine_list.json',
parse: function(data) {
return data;
},
comparator: function(products) {
return products.get('Vintage');
},
favoritesFilter1: function(favorite) {
return this.filter(function(products) {
return products.get('favorite') == true;
});
},
favoritesFilter: function() {
return this.filter(function(products) {
return products.get('favorite') == true;
});
},
});
var products = new ProductListCollection();
var ProductListItemView = Backbone.View.extend({
el: '#wine-cellar-list',
initialize: function() {
products.bind('reset', this.render, this);
products.fetch();
this.render();
},
render: function() {
console.log(this.collection);
var source = $('#product-template').html();
var template = Handlebars.compile(source);
var html = template(this.collection.toJSON());
this.$el.html(html);
return this;
},
});
// Create instances of the views
var productView = new ProductListItemView({
collection: products
});
var CellarRouter = Backbone.Router.extend({
routes: {
'': 'default',
"favorites": "showFavorites",
"purchased": "showPurchased",
"top-rated": "showTopRated",
},
default: function() {
productView.render();
},
showFavorites: function() {
console.log('Favorites');
productView.initialize(products.favoritesFilter());
},
showPurchased: function() {
console.log('Purchased');
},
showTopRated: function() {
console.log('Top Rated');
}
});
$(function() {
var myCellarRouter = new CellarRouter();
Backbone.history.start();
});
There's many mistakes in your code, I'll try to clarify the most I can :
Your collection should be just like this :
var ProductListCollection = Backbone.Collection.extend({
model: Products,
url: '/js/data/wine_list.json',
comparator: 'Vintage' // I guess you want to sort by this field
});
Your view like this :
var ProductListItemView = Backbone.View.extend({
el: '#wine-cellar-list',
initialize: function() {
this.collection.bind('reset', this.full, this);
this.collection.fetch();
},
full: function() {
this.render(this.collection.models);
},
favorites: function(favorite) {
this.render(this.collection.where(favorite)); // here's the answer to your question
},
render: function(models) {
console.log(models);
var source = $('#product-template').html();
var template = Handlebars.compile(source);
var html = template(models.toJSON()); // You may have to change this line
this.$el.html(html);
return this;
},
});
And in your router :
showFavorites: function() {
console.log('Favorites');
productView.favorites(true); // or false, as you like
}
I have a backbone collection:
var user = new Backbone.Collection.extend({
url: '/user',
parse: function (response) {
return response.lunch;
return response.dinner;
}
});
which returns a json like this:
[
{
lunch: [{
appetizer : 'bagel',
maincourse: 'steak',
desert: 'sweets'
}]
},
{
dinner: [{
appetizer : 'chips',
main: 'rice',
desert: 'sweets'
}]
}
]
I want to combine both response.lunch and response.dinner and have a common collection: I tried:
parse: function (response) {
var collection1 = response.lunch;
var collection2 = response.dinner;
return collection1.add(collection2.toJSON(), {silent : true});
}
But it doesnot work. Also how do i do a each function to override all main with maincourse? I tried:
this.collection.each(function(model) {
var a = model.get("main");
model.set({a: "maincourse"});
}
Any help would be appreciated. Thanks!
I'm guessing that you want to merge lunch and dinner so that your collection ends up with { appetizer : 'bagel', ... } and { appetizer : 'chips', ... } inside it. If so, then simply concat the two arrays together:
parse: function(response) {
return response.lunch.concat(response.dinner);
}
If you want to rename all the main attributes to maincourse then you'd want to use get to pull out the mains, unset to remove them, and then set to put them back in with the new name:
var maincourse = model.get('main');
model.unset('main', { silent: true });
model.set('maincourse', maincourse, { silent: true });
or just edit attributes directly:
model.attributes.maincourse = model.attributes.main;
delete model.attributes.main;
or better, just rename the attribute in your parse method.
I am currently trying to render out this json object in a ul. I'd like to be able to cycle through the GamesList and get the games and their attributes in a list. I've kinda hit a wall where I am not entirely sure how to accomplish this. Still very new to backbone so any help would be greatly appreciated.
JSON Object:
{
"GamesList":[
{
"Date":"2013/07/02",
"Games":[
{
"Id":"3252",
"Time":"12:10 AM"
}
]
},
{
"Date":"2013/07/02",
"Games":[
{
"Id":"3252",
"Time":"12:10 AM"
}
]
},
{
"Date":"2013/07/02",
"Games":[
{
"Id":"3252",
"Time":"12:10 AM"
}
]
}
]
}
App Structure:
App.Models.Game = Backbone.Model.extend({
defaults: {
GamesList: ''
}
});
App.Collections.Game = Backbone.Collection.extend({
model: App.Models.Game,
url: 'path/to/json',
parse: function (response) {
return response;
}
});
App.Views.Games = Backbone.View.extend({
tagName: 'ul',
initialize: function () {
this.collection = new App.Collections.Game();
this.listenTo(this.collection, 'reset', this.render, this);
this.collection.fetch();
},
render: function () {
//filter through all items in a collection
this.collection.each(function (game) {
var gameView = new App.Views.Game({
model: game
});
this.$el.append(gameView.render().el);
}, this)
return this;
}
});
App.Views.Game = Backbone.View.extend({
tagName: 'li',
template: _.template($('#gameTemplate').html()),
render: function () {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
var gameCollection = new App.Collections.Game();
gameCollection.fetch({
data: {
collection_id: 25
},
success: function (data, textStatus, jqXHR) {
console.log(data);
console.log(textStatus);
console.log(jqXHR);
console.log('success');
},
error: function () {
alert('Oh noes! Something went wrong!')
}
});
var gamesView = new App.Views.Games({
collection: gameCollection
});
$(document.body).append(gamesView.render().el);
It looks like your JSON object is not inlined with Backbone.Collection...
as you declared App.Collections.Game has url /path/to/json which means the json that needs to return is a list... without the GamesList that is seen in your JSON
EDIT:
You can use the parse function in your Games Collection to fix the json retrieved from your server
parse:function(response){
return response.GamesList;
}
Important:
Please note that your json objects that are fetched from the server should have ID. Backbone will 'think' these models are new and will create them upon save...
I'm seeing a little confusion in it. Let's proceed step by step:
--------- AFTER COMMENT ---------
You can set your model as:
defaults: {
Date:'',
Games:''
}
then modifying your parse function as
parse: function (response)
{
var _this = this;
_.map(response, function(obj) {
_this.add(obj)
});
}
This way you add each single item in the collection as your model expect.
Another problem I'm seeing is that you're creating and fetching the collection twice:
...
this.collection = new App.Collections.Game();
this.listenTo(this.collection, 'reset', this.render, this);
this.collection.fetch();
...
and then
var gameCollection = new App.Collections.Game();
...
gameCollection.fetch({
data: {
....
...
var gamesView = new App.Views.Games({
collection: gameCollection
});
I've just started using Backbone.js and my test cases are churning up something pretty weird.
In short, what I am experiencing is -- after I call a Backbone Model's constructor, some of the fields in my object seem to come from a previously item. For instance, if I call:
var playlist = new Playlist({
title: playlistTitle,
position: playlists.length,
userId: user.id
});
playlist.get('items').length; //1
however if I do:
var playlist = new Playlist({
title: playlistTitle,
position: playlists.length,
userId: user.id,
items: []
});
playlist.get('items').length; //0
Here's the code:
define(['ytHelper', 'songManager', 'playlistItem'], function (ytHelper, songManager, PlaylistItem) {
'use strict';
var Playlist = Backbone.Model.extend({
defaults: {
id: null,
userId: null,
title: 'New Playlist',
selected: false,
position: 0,
shuffledItems: [],
history: [],
items: []
},
initialize: function () {
//Our playlistItem data was fetched from the server with the playlist. Need to convert the collection to Backbone Model entities.
if (this.get('items').length > 0) {
console.log("Initializing a Playlist object with an item count of:", this.get('items').length);
console.log("items[0]", this.get('items')[0]);
this.set('items', _.map(this.get('items'), function (playlistItemData) {
var returnValue;
//This is a bit more robust. If any items in our playlist weren't Backbone.Models (could be loaded from server data), auto-convert during init.
if (playlistItemData instanceof Backbone.Model) {
returnValue = playlistItemData;
} else {
returnValue = new PlaylistItem(playlistItemData);
}
return returnValue;
}));
//Playlists will remember their length via localStorage w/ their ID.
var savedItemPosition = JSON.parse(localStorage.getItem(this.get('id') + '_selectedItemPosition'));
this.selectItemByPosition(savedItemPosition != null ? parseInt(savedItemPosition) : 0);
var songIds = _.map(this.get('items'), function(item) {
return item.get('songId');
});
songManager.loadSongs(songIds);
this.set('shuffledItems', _.shuffle(this.get('items')));
}
},
//TODO: Reimplemnt using Backbone.sync w/ CRUD operations on backend.
save: function(callback) {
if (this.get('items').length > 0) {
var selectedItem = this.getSelectedItem();
localStorage.setItem(this.get('id') + '_selectedItemPosition', selectedItem.get('position'));
}
var self = this;
console.log("Calling save with:", self);
console.log("my position is:", self.get('position'));
$.ajax({
url: 'http://localhost:61975/Playlist/SavePlaylist',
type: 'POST',
dataType: 'json',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(self),
success: function (data) {
console.log('Saving playlist was successful.', data);
self.set('id', data.id);
if (callback) {
callback();
}
},
error: function (error) {
console.error("Saving playlist was unsuccessful", error);
}
});
},
selectItemByPosition: function(position) {
//Deselect the currently selected item, then select the new item to have selected.
var currentlySelected = this.getSelectedItem();
//currentlySelected is not defined for a brand new playlist since we have no items yet selected.
if (currentlySelected != null && currentlySelected.position != position) {
currentlySelected.set('selected', false);
}
var item = this.getItemByPosition(position);
if (item != null && item.position != position) {
item.set('selected', true);
localStorage.setItem(this.get('id') + '_selectedItemPosition', item.get('position'));
}
return item;
},
getItemByPosition: function (position) {
return _.find(this.get('items'), function(item) {
return item.get('position') == position;
});
},
addItem: function (song, selected) {
console.log("this:", this.get('title'));
var playlistId = this.get('id');
var itemCount = this.get('items').length;
var playlistItem = new PlaylistItem({
playlistId: playlistId,
position: itemCount,
videoId: song.videoId,
title: song.title,
relatedVideos: [],
selected: selected || false
});
this.get('items').push(playlistItem);
this.get('shuffledItems').push(playlistItem);
this.set('shuffledItems', _.shuffle(this.get('shuffledItems')));
console.log("this has finished calling");
//Call save to give it an ID from the server before adding to playlist.
songManager.saveSong(song, function (savedSong) {
song.id = savedSong.id;
playlistItem.set('songId', song.id);
console.log("calling save item");
$.ajax({
type: 'POST',
url: 'http://localhost:61975/Playlist/SaveItem',
dataType: 'json',
data: {
id: playlistItem.get('id'),
playlistId: playlistItem.get('playlistId'),
position: playlistItem.get('position'),
songId: playlistItem.get('songId'),
title: playlistItem.get('title'),
videoId: playlistItem.get('videoId')
},
success: function (data) {
playlistItem.set('id', data.id);
},
error: function (error) {
console.error(error);
}
});
});
return playlistItem;
},
addItemByVideoId: function (videoId, callback) {
var self = this;
ytHelper.getVideoInformation(videoId, function (videoInformation) {
var song = songManager.createSong(videoInformation, self.get('id'));
var addedItem = self.addItem(song);
if (callback) {
callback(addedItem);
}
});
},
//Returns the currently selected playlistItem or null if no item was found.
getSelectedItem: function() {
var selectedItem = _.find(this.get('items'), function (item) {
return item.get('selected');
});
return selectedItem;
}
});
return function (config) {
var playlist = new Playlist(config);
playlist.on('change:title', function () {
this.save();
});
return playlist;
};
});
basically I am seeing the property 'items' is populated inside of initialize when I've passed in a config object that does not specify items at all. If I specify a blank items array in my config object, then there are no items in initialize, but this seems counter-intuitive. Am I doing something wrong?
The problem is with using reference types (arrays) in the defaults object. When a new Playlist model is created without specifying an items value, the default is applied. In case of arrays and objects this is problematic, because essentially what happens is:
newModel.items = defaults.items
And so all models initialized this way refer to the same array. To verify this, you can test:
var a = new Playlist();
var b = new Playlist();
var c = new Playlist({items:[]});
//add an item to a
a.get('items').push('over the rainbow');
console.log(b.get('items')); // -> ['over the rainbow'];
console.log(c.get('items')); // -> []
To get around this problem, Backbone supports defining Model.defaults as a function:
var Playlist = Backbone.Model.extend({
defaults: function() {
return {
id: null,
userId: null,
title: 'New Playlist',
selected: false,
position: 0,
shuffledItems: [],
history: [],
items: []
};
}
});