Search code examples
javascriptbackbone.jsunderscore.js

What is the 'without' function doing on a collection?


This is a backbone tutorial. Here is the code:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <title>To-do App in Backbone.js</title>

  <!-- ========= -->
  <!--    CSS    -->
  <!-- ========= -->
  <style type="text/css">
    #todoapp ul {
      list-style-type: none; /* Hides bullet points from todo list */
    }
    #todo-list input.edit {
      display: none; /* Hides input box*/
    }
    #todo-list .editing label {
      display: none; /* Hides label text when .editing*/
    }
    #todo-list .editing input.edit {
      display: inline; /* Shows input text box when .editing*/
    }
  </style>
</head>
<body>
  <!-- ========= -->
  <!-- Your HTML -->
  <!-- ========= -->

  <section id="todoapp">
    <header id="header">
      <h1>Todos</h1>
      <input id="new-todo" placeholder="What needs to be done?" autofocus>
      <div>
        <a href="#/">show all</a> |
        <a href="#/pending">show pending</a> |
        <a href="#/completed">show completed</a>
      </div>
    </header>
    <section id="main">
      <ul id="todo-list"></ul>
    </section>
  </section>
  <div>
    <p>Find the tutorial and code in <a href="http://adrianmejia.com/blog/2012/09/11/backbone-dot-js-for-absolute-beginners-getting-started/">here</a></p>
  </div>

  <!-- Templates -->
  <script type="text/template" id="item-template">
    <div class="view">
      <input class="toggle" type="checkbox" <%= completed ? 'checked' : '' %>>
      <label><%- title %></label>
      <input class="edit" value="<%- title %>">
      <button class="destroy">remove</button>
    </div>
  </script>

  <!-- ========= -->
  <!-- Libraries -->
  <!-- ========= -->
  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script>
  <script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.3.3/underscore-min.js" type="text/javascript"></script>
  <script src="http://cdnjs.cloudflare.com/ajax/libs/backbone.js/0.9.2/backbone-min.js" type="text/javascript"></script>
  <script src="http://cdnjs.cloudflare.com/ajax/libs/backbone-localstorage.js/1.0/backbone.localStorage-min.js" type="text/javascript"></script>

  <!-- =============== -->
  <!-- Javascript code -->
  <!-- =============== -->
  <script type="text/javascript">
    'use strict';

    var app = {}; // create namespace for our app

    //--------------
    // Models
    //--------------
    app.Todo = Backbone.Model.extend({
      defaults: {
        title: '',
        completed: false
      },
      toggle: function(){
        this.save({ completed: !this.get('completed')});
      }
    });

    //--------------
    // Collections
    //--------------
    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() {
        debugger;
        return this.without.apply( this, this.completed() );
      }
    });

    // instance of the Collection
    app.todoList = new app.TodoList();

    //--------------
    // Views
    //--------------

    // renders individual todo items list (li)
    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's function for removing the view from the DOM.
      },
      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
        }
      }
    });

    //--------------
    // Routers
    //--------------

    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');
      }
    });

    //--------------
    // Initializers
    //--------------

    app.router = new app.Router();
    Backbone.history.start();
    app.appView = new app.AppView();

  </script>

</body>
</html>

What is this line doing:

remaining: function() {
        return this.without.apply( this, this.completed() );
      }

What is that doing?

without is from underscore but what is apply from? Also I put a debugger above that line inside the anonymous function and this was an error:

this.apply(this, this.copmleted());

Why didn't that work? What is apply?


Solution

  • Without

    Backbone Collection's underscore methods

    Backbone proxies to Underscore.js to provide 46 iteration functions on Backbone.Collection.

    this.without is the same as _.without applied on the collection itself.

    More detailed answer on this.

    Apply

    apply is a function from Function's prototype (in the JavaScript spec). It takes the context as the first argument (this in this case) and an array-like object specifying the arguments with which the function should be called.

    The context is the value of this within the function; it's the object on which the function will do its logic.

    this.completed() returns an array, but this.without needs each argument passed separately. For example:

    this.without(1, 2, 3, 4, 6, 78);
    

    To unroll the array returned by this.completed() into an argument list, .apply can be used:

    this.without.apply(this, [completedModel1, completedModel2, /* etc. */]);