Make Backbone.js items within localStorage unique? - javascript

I have a dupe check within my collection, where I'm overriding the add function and it seems to work until a page refresh.
Duplicates are blocked with an alert saying "You've already added this item to the todo list!" but it seems like when the page refreshes, the duplicate is added to localStorage either way. Would love a solution to this issue -- been scratching my head for the past few days on this issue.
My collection below:
app.TodoList = Backbone.Collection.extend({
model: app.Todo,
localStorage: new Store("backbone-todo"),
completed: function() {
return this.filter(function(todo){
return todo.get('completed');
});
},
remaining: function(){
return this.without.apply(this, this.completed());
}
});
app.TodoList.prototype.add = function(todo) {
var isDupe = this.any(function(_todo){ return _todo.get('title').toLowerCase() === todo.get('title').toLowerCase();
});
return isDupe ? alert("You've already added this item to the todo list!") : Backbone.Collection.prototype.add.call(this, todo);}
// instance of the Collection
app.todoList = new app.TodoList();
Here is the model:
app.Todo = Backbone.Model.extend({
defaults: {
title: '',
completed: false
},
toggle: function(){
this.save({ completed: !this.get('completed')});
}
});
The View:
app.TodoView = Backbone.View.extend({
tagName: 'li',
template: _.template($('#item-template').html()),
render: function(){
this.$el.html(this.template(this.model.toJSON()));
this.input = this.$('.edit');
return this; // enable chained calls
},
initialize: function(){
this.model.on('change', this.render, this);
this.model.on('destroy', this.remove, this); // remove: 'Convenience Backbone'
},
events: {
'dblclick label' : 'edit',
'keypress .edit' : 'updateOnEnter',
'blur .edit' : 'close',
'click .toggle' : 'toggleCompleted',
'click .destroy' : 'destroy'
},
edit: function(){
this.$el.addClass('editing');
this.input.focus();
},
close: function(){
var value = this.input.val().trim();
if(value) {
this.model.save({ title: value });
}
this.$el.removeClass('editing');
},
updateOnEnter: function(e){
if(e.which == 13){
this.close();
}
},
toggleCompleted: function(){
this.model.toggle();
},
destroy: function(){
this.model.destroy();
}
});
// renders the full list of todo items calling TodoView for each one.
app.AppView = Backbone.View.extend({
el: '#todoapp',
initialize: function () {
this.input = this.$('#new-todo');
app.todoList.on('add', this.addAll, this);
app.todoList.on('reset', this.addAll, this);
app.todoList.fetch(); // Loads list from local storage
},
events: {
'keypress #new-todo': 'createTodoOnEnter'
},
createTodoOnEnter: function(e){
if ( e.which !== 13 || !this.input.val().trim() ) { // ENTER_KEY = 13
return;
}
app.todoList.create(this.newAttributes());
this.input.val(''); // clean input box
},
addOne: function(todo){
var view = new app.TodoView({model: todo});
$('#todo-list').append(view.
render().el);
},
addAll: function(){
this.$('#todo-list').html(''); // clean the todo list
// filter todo item list
switch(window, filter){
case 'pending':
_.each(app.todoList.remaining(), this.addOne);
break;
case 'completed':
_.each(app.todoList.completed(), this.addOne);
break;
default:
app.todoList.each(this.addOne, this);
break;
}
},
newAttributes: function(){
return {
title: this.input.val().trim(),
completed: false
}
}
});
The Router:
app.Router = Backbone.Router.extend({
routes: {
'*filter' : 'setFilter'
},
setFilter: function(params){
console.log('app.router.params = ' + params);
window.filter = params.trim() || '';
app.todoList.trigger('reset');
}
})
And the initializer:
app.router = new app.Router();
Backbone.history.start();
app.appView = new app.AppView();
If any more information is needed, would gladly provide it. Thanks!

In Backbone, when you call create, both add and save are called. Read the source here: http://backbonejs.org/docs/backbone.html#section-113
So you blocked the add from happening, but the save still happened when adding a duplicate.
You can use Backbone's built in validation to accomplish what you were trying to do:
app.Todo = Backbone.Model.extend({
defaults: {
title: '',
completed: false
},
initialize: function() {
this.on('error', function(model, error) {
alert(error);
});
},
toggle: function(){
this.save({ completed: !this.get('completed')});
},
validate: function(attrs, options) {
if ( this.collection.isExistingTodoTitleOnOtherTodo(attrs) ) {
return "You've already added this item to the todo list!";
}
}
});
app.TodoList = Backbone.Collection.extend({
model: app.Todo,
localStorage: new Store("backbone-todo"),
completed: function() {
return this.filter(function(todo){
return todo.get('completed');
});
},
remaining: function(){
return this.without.apply(this, this.completed());
},
isExistingTodoTitleOnOtherTodo: function(attrs) {
return this.any(function(todo) {
var titleMatch = todo.get('title').toLowerCase() === attrs.title.toLowerCase();
var idMatch = attrs.id === todo.id;
return titleMatch && !idMatch;
});
}
});
BTW, your Backbone is outdated so the docs on the site don't reflect what you can do in your code.

Related

How to keep track of actual rendered Model in a Backbone Collection

I'm new to Backbone and I'm trying to create a simple Slideshow that show all the models in a Collection.
Models are created through a fetch from the server and here is the code:
var Post = Backbone.Model.extend({
defaults:{
text: "",
source: "",
image: "",
posted_at: "",
rendered : false,
},
});
In PostCollection there're modelBefore and modelAfter that return next and previous model respectively.
var PostCollection = Backbone.Collection.extend({
model: Post,
url: "https://milkytags.com/api/v1/boards/edcb2c43-1448-4c81-97d5-1c315c8f9589/posts",
initialize: function() {
this.fetch({ data: $.param({ page: pageCounter, per_page:3}) });
},
parse: function(response) {
return response.posts;
},
modelBefore: function(model) {
index = this.indexOf(model) - 1;
if (index < 0) {
index = this.length - 1;
}
return this.at(index);
},
modelAfter: function(model) {
index = this.indexOf(model) + 1;
if (index === this.length) {
index = 0;
}
return this.at(index);
},
});
I've created a view called SlideShowView that creates the view from a template relying on Post View: next and prev method dealing with rendering the next or previous template.
var SlideShowView = Backbone.View.extend({
tagName: 'div',
className: 'slideshow',
events: {
'click #close': 'close',
'click #next': 'next',
'click #prev': 'prev',
},
template: _.template($('#slideShowTemplate').html()),
initialize: function() {
this.render();
},
render: function() {
this.$el.html(this.template());
post = new PostView({ model: this.model });
this.$el.append(post.el);
return this.$el;
},
close: function(){
this.remove();
},
next: function(){
var next = this.model.collection.modelAfter( this.model );
post = new PostView({ model: next });
this.$el.html(this.template());
this.$el.append(post.el);
return this.$el;
},
prev: function(){
var prev= this.model.collection.modelBefore( this.model );
post = new PostView({ model: prev });
this.$el.html(this.template());
this.$el.append(post.el);
return this.$el;
},
});
Finally, Post View:
// The View for single Post
var PostView = Backbone.View.extend({
tagName: 'div',
className: 'post',
events: {
'click' : 'slideShow',
},
template: _.template($('#postTemplate').html()),
initialize: function() {
this.render();
},
render: function() {
this.$el.html(this.template(this.model.toJSON_milky()));
return this;
},
slideShow: function(){
test=new SlideShowView({model: this.model});
$('#milkyContainer').append(test.$el);
}
});
Problems arises when I press next or prev, in practice it is as if the collection was not updated with the latest rendered element, I have to find a way to tell to the collection what is the current collection element shown.
Tips?
Thanks
Right now your code is using this.model in SlideShowView as "the current model". However, you do not update it. Something like this would do it:
next: function(){
var next = this.model.collection.modelAfter( this.model );
post = new PostView({ model: next });
this.$el.html(this.template());
this.$el.append(post.el);
this.model = next; // <<---- Added this line.
return this.$el;
},
Similarly for prev.

Backbone nested view

I'm trying to make a nested view. Backendview call ListPostView and ListPostView call SinglePostView. ListPostview and SinglePostView recursively creates a list by a collection.
BackendView is used only to wrap the list in a page html.
The collection passed to BackendView is retrieved by fetch and by method reset.
The problem is I can't render my collection and error is "undefined" inside SinglePostView.
If I call directly ListPostView it works perfectly.
I think maybe depends by event "bind" inside initialize function.
This is collection:
var Attori = Backbone.Collection.extend({
model:Attore,
idAttribute: "id",
fetch: function(options) {
var collection = this;
var cb = new Codebird;
cb.setConsumerKey("1Cx*mfA", "YedD*4s");
cb.__call(
"oauth2_token",
{},
function (reply) {
var bearer_token = reply.access_token;
console.log(bearer_token);
cb.setBearerToken(bearer_token);
}
);
console.log(options);
cb.setToken("259**g4ONJYi2","z8LLm52M**PS");
var params = {
q: "jim carrey"
//screen_name:"brad"
};
cb.__call(
"users/search",
params,
function (reply) {
console.log(reply);
collection.reset(reply);
}
);
}
});
return Attori;
});
this is Backendview:
var BackendView = Backbone.View.extend({
tagName: "ul",
id: "list",
events: {
"touchend": "goToDetails"
},
template: Handlebars.compile(template),
initialize: function () {
this.collection.bind("reset", this.render, this);
},
render: function (eventName) {
console.log(this.collection.length);
/* _.each(this.collection.models, function (ad) {
$(this.el).append(new ListPostView({
collection: ad
}).render().el);
}, this);*/
/* $(this.el).append(new ListPostView({
collection: this.collection
}).render().el);*/
if (typeof this.collection !== 'undefined' && this.collection.length > 0) {
// the array is defined and has at least one element
var List=new ListPostView({collection:this.collection});
//List.render();
}
//console.log(List);
return this;
},
goToDetails: function () {
Parse.history.navigate("ads/" + this.model.cid, {trigger: true});
}
});
return BackendView;
});
this is ListpostView:
var ListPostView = Backbone.View.extend({
tagName: "ul",
id: "list",
template: Handlebars.compile(template),
initialize: function () {
console.log(this.collection);
this.collection.bind("reset", this.render, this);
},
render: function (eventName) {
console.log(this.collection.models);
$(this.el).empty();
_.each(this.collection.models, function (a) {
$(this.el).append(new SinglePostView({
model: a
}).render().el);
}, this);
return this;
}
});
return ListPostView;
});
and this is SinglePostView:
var SinglePostView = Backbone.View.extend({
tagName: "li",
events: {
"touchend": "goToDetails"
},
template: Handlebars.compile(template),
initialize: function () {
console.log(this.model);
this.model.bind("change", this.render, this);
this.model.bind("destroy", this.close, this);
},
render: function (eventName) {
var ad = this.model.toJSON();
ad.cid = this.model.cid;
$(this.el).html(this.template(ad));
return this;
},
goToDetails: function () {
Parse.history.navigate("ads/" + this.model.cid, {trigger: true});
}
});
return SinglePostView;
});

How to pass filtered data into view from collection in backbone

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
}

After Adding a record it is not displaying the data immediately to the view

When I loads the page , it is getting all the datas and I am displaying the datas. But When I add a record, that is I am submitting the form "addcontact", the datas are creating in the database. But It is not adding into the collection and that this.collection.on('add') is not getting triggered. So, I think the problem was because of this. Can any one tell me that where I am doing wrong? Is there any other way to solve this.
This code works functionally, but the only problem with this is , on creating new record using this.collection.create({new post},{wait: true}); the values are getting updated in the database. But it is not adding into the collection.
(function(){
Backbone.emulateHTTP = true;
//Backbone.emulateJSON = true;
window.App = {
Models : {},
Collections: {},
Views : {},
Router : {}
};
window.vent = _.extend({},Backbone.Events);
window.template = function(id){
return _.template( $('#'+id).html() );
};
// Contact Model
App.Models.Contact = Backbone.Model.extend({
validate: function(attrs) {
if( !attrs.first_name ||
!attrs.last_name ||
!attrs.email_address) {
alert('Fill the missing fields');
}
}
});
// Collection
App.Collections.Contacts = Backbone.Collection.extend({
model: App.Models.Contact,
url : 'index.php/ContactsController/contacts'
});
// Global View
App.Views.App = Backbone.View.extend({
initialize: function(){
vent.on('contact:edit',this.editContact,this);
//console.log(this.collection.toJSON());
App.addContactView = new App.Views.AddContact({collection: App.Contacts});
App.allContactsView = new App.Views.Contacts({collection: App.Contacts});
$('#allcontacts').append(App.allContactsView.el);
}
});
// Add Contact View
App.Views.AddContact = Backbone.View.extend({
el: '#addcontact',
initialize: function(){
this.first_name = $('#first_name');
this.last_name = $('#last_name');
this.email_address = $('#email_address');
this.description = $('#description');
//this will fix it
this.collection.on("change", this.render , this);
},
events: {
'submit' : 'addContact'
},
addContact: function(e){
e.preventDefault();
this.collection.create({
first_name: this.first_name.val(), // <===== same as $this.el.find('#first_name')
last_name: this.last_name.val(),
email_address: this.email_address.val(),
description: this.description.val()
},{wait: true});
this.clearForm();
},
clearForm: function(){
this.first_name.val('');
this.last_name.val('');
this.email_address.val('');
this.description.val('');
}
});
// All Contacts Views
App.Views.Contacts = Backbone.View.extend({
tagName: 'tbody',
initialize: function(){
this.collection.on('add',this.addOne,this);
this.render();
},
render: function(){
this.collection.each(this.addOne,this);
//console.log(this.el);
return this;
},
addOne: function(contact){
var ContactView = new App.Views.Contact({model: contact});
//console.log(ContactView.render().el);
this.$el.append(ContactView.render().el);
}
});
// A view for a single View
App.Views.Contact = Backbone.View.extend({
tagName: 'tr',
template: template('allContactsTemplate'),
initialize: function(){
this.model.on('change',this.render,this);
},
render: function(){
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
})();

Backbone.js - Collection within Model - Object function (a){return new n(a)} has no method 'has'

I am getting an
Object function (a){return new n(a)} has no method 'has'
error on calling the fetch() method on my model. Heres the code:
var Exercise = Backbone.Model.extend({
defaults: {
idAttribute: 'e_id',
e_id: "-1",
exerciseName: "Exercise",
exerciseDescription: "Address",
exerciseURL: "vimeo.com",
reps: "0",
sequence: "0"
},
initialize: function() {
alert("Exercise!");
}
});
var ExerciseList = Backbone.Collection.extend({
url: "/getWorkoutList.php",
model: Exercise,
initialize: function() { }
});
var Workout = Backbone.Model.extend({
urlRoot: "/getWorkoutList.php",
url: function() {
return this.urlRoot + "?workoutID=" + this.get('workoutId');
},
defaults: {
idAttribute: 'workoutId',
workoutId: "-1",
workoutName: "WorkoutName",
workoutDescription: "WorkoutDescription",
exercises: new ExerciseList()
},
initialize: function() {
_.bindAll(this);
directory.renderWorkout(this);
},
parse: function(response) {
return response;
}
});
var WorkoutList = Backbone.Collection.extend({
url: "/getWorkoutList.php",
model: Workout,
initialize: function() {
_.bindAll(this);
},
parse: function(response) {
return response;
}
});
var WorkoutView = Backbone.View.extend({
tagName: "div",
className: "workout-container",
template: $("#tmp-workout").html(),
initialize: function() {
_.bindAll(this);
this.model.bind('change', this.render, this);
},
render: function(){
console.log("WorkoutView");
var tmpl = _.template(this.template);
this.$el.html(tmpl(this.model.toJSON()));
return this;
},
//add ui events
events: {
"click #workout-details": "getWorkoutDetails"
},
getWorkoutDetails: function (e) {
e.preventDefault();
this.model.fetch();
}
});
var ExerciseView = Backbone.View.extend({
tagName: "exercise",
className: "exercise-container",
template: $("#tmp-exercise").html(),
initialize: function() {
_.bindAll(this);
alert("ExerciseView");
},
render: function(){
console.log("render exercise view");
var tmpl = _.template(this.template);
this.$el.html(tmpl(this.model.toJSON()));
return this;
}
});
var WorkoutListingView = Backbone.View.extend({
el: $("#workouts"),
initialize: function() {
var collection = new WorkoutList();
collection.fetch();
},
render: function() {
var that = this;
_.each(this.collection.models, function(item){
that.renderWorkout(item);
});
},
renderWorkout: function(item) {
var workoutView = new WorkoutView({
model:item
});
this.$el.append(workoutView.render().el);
var that = this;
_.each(workoutView.model.get('exercises').models, function(exercise) {
that.renderExercise(exercise);
});
},
renderExercise: function(item) {
var exerciseView = new ExerciseView({
model:item
});
this.$el.append(exerciseView.render().el);
}
});
Everything works fine when I am retrieving the Workout Collection the fist time. However, when I call getWorkoutDetails, I get the error. By inserting alerts and console.logs in parse() of Workout Model, I've found out that it does get the correct response from server, but for some reason, its giving this error.
Any ideas? Thanks.
OK, after spending a lot of time in the beautiful world of minified javascript spaghetti, I found out that the underscore.js version I was using didnt had the function 'has' in it. Updating underscore.js from 1.2.2 to 1.4.4 solved the problem. Also, my backbone.js version is 0.9.1

Categories