Search code examples
javascriptbackbone.jsbackbone-local-storage

Make Backbone.js items within localStorage unique?


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!


Solution

  • 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.