• Wall.js

    A multiple search built with Backbone and jQuery UI autocomplete
    LIVE DEMO Search for: java, php, coldfusion, coffeescript, javascript, asp, ruby
  • ¶

    Style: all private methods & objects start with underscore (_)

    (function () {
  • ¶

    Global namespace

    	var wall = window.wall = {
  • ¶

    This is the entry point to the application. It takes care of instantiating the views, models, controllers, etc.

    wall.start()
    		start: function(inputId) {
    			var searchCollection = new wall.Languages;
    			var view = new wall.View({inputId: inputId, collection: searchCollection});
    		},
  • ¶

    demo data

    		demoData: [ "java", "php", "coldfusion", "coffeescript", "javascript", "asp", "ruby" ]
    	};
  • ¶

    View that holds the text box and different brick views

    	wall.View = Backbone.View.extend({
    		initialize: function() {
    			this._bricks = new Backbone.Collection;
    			this._input  = $("#" + this.options.inputId);
    			this._initContainer();
    			this._initAutocomplete();
    			this._bind();
    			var brickView = new wall.BrickView({collection: this._bricks});
    			this._input.prop('placeholder', 'Enter a language').focus();
    		},
  • ¶

    The container of the wall is a UL element. The container of a brick is a LI element, and this will be a separate view The input will be stored within its own brick The final markup should be similar to:

        <div class="wall">
            <ul class="wall-ul">
                <li class="wall-brick">
                    <span class="wall-brick-name">Name</span>
                    <a href="#remove" class="wall-brick-remove">x</a>
                </li>
                <li class="wall-brick">
                    <input type="text" class="wall-input" />
                </li>
            </ul>
        </div>
    		_initContainer: function() {
    			this._input
    				.addClass('wall-input')
    				.wrap(
    					'<div class="wall">' +
    						'<ul class="wall-ul ui-helper-clearfix">' + 
    							'<li class="wall-brick wall-brick-input"></li>' +
    						'</ul>' + 
    					'</div>'
    				);
  • ¶

    Store these in the global object so they can be accessed by other objects

    			wall.container = this._input.parents('div.wall');
    			wall.inputListItem = this._input.parent().before('<li class="wall-brick-search"></li>');
    		},
  • ¶

    Typical jQuery UI autocomplete options, apart from source which is delegated to the search collection; select is triggered when an item in the list selected; the position is shifted down as the container is padded

    		_initAutocomplete: function() {
    			var self = this;
    			var options = {
    				position: { at: "left bottom+7" },
    				appendTo: wall.container,
    				source:   function(request, response) { self._getData(request, response); },
    				focus:    function() { return false; },
    				select:   function(e, ui) { self._select(e, ui); }
    			};
    			this._input.autocomplete(options);
    		},
  • ¶

    Fetch the models from the collection, based on the term entered by the user. This would normallly communicate with a remote server, simply hard coded here. The fetched models are translated to ui.item format expected by jQuery UI autocomplete.

    		_getData: function(request, response) {
    			var languages = this.collection.fetch(request.term);
    			var models = languages.map(function(language) {
    				var name = language.get('name');
    				return {
    					id:    language.get('id'),
    					label: name,
    					value: name
    				};
    			});
    			response(models);
    		},
  • ¶

    Create a model from the brick selected, and add it to the collection. The brick view, listening to this event, will be added automatically to the wall. We (almost) never call the brick view directly.

    		_select: function(e, ui) {
    			e.preventDefault();
    			this._bricks.add({
    				id: ui.item.id,
    				name: ui.item.label
    			});
    			this._input.val('').focus()
    			return false;
    		},
  • ¶

    When the user presses BACKSPACE for the first time, select the last brick, giving the user a cue that the brick is selected; if the user presses BACKSPACE again, find the last brick, and remove it from collection

    		_onkeydown: function(e) {
    			if (e.keyCode == $.ui.keyCode.BACKSPACE && this._input.val().length == 0) {
    				this._onLastBrickSelected(function(lastBrick) {
    					this._input.autocomplete('close');
    					var id = lastBrick.data('id');
    					var model = this._bricks.get(id);
    					this._bricks.remove(model);
    				});
    			} else {
    				this._unselectLastBrick();
    			}
    		},
  • ¶

    Remove the selected class from the last brick

    		_unselectLastBrick: function() {
    			this._onLastBrick(function(lastBrick) {				
    				lastBrick.removeClass('wall-brick-selected');
    			});
    		},
  • ¶

    If the last brick is selected then call the callback function provided, otherwise select it

    		_onLastBrickSelected: function(fn) {
    			var self = this;
    			this._onLastBrick(function(lastBrick) {				
    				if (lastBrick.hasClass('wall-brick-selected')) {
    					if (fn) fn.call(self, lastBrick);
    				} else {
    					lastBrick.addClass('wall-brick-selected');
    				}
    			});
    		},
  • ¶

    Find the last brick, by checking the sibling before the brick holding the input box.

    		_onLastBrick: function(fn) {
    			var lastBrick = wall.inputListItem.prev('.wall-brick');
    			if (lastBrick && lastBrick.length && fn) fn(lastBrick);
    		},
  • ¶

    Selects the text box anytime the user clicks on the entire box.

    		_bind: function() {
    			var self = this;
    			wall.container
    				.click(function() { self._input.focus(); })
    				.keydown(function(e) { self._onkeydown(e); });
    		}
    	});
    
    	wall.BrickView = Backbone.View.extend({
    		_markup: 
    			'<li class="wall-brick" id="wall-brick-<%= id %>" data-id="<%= id %>">' +
    				'<span class="wall-brick-name"><%= name %></span>' + 
    				'<a href="#remove" data-id="<%= id %>" class="wall-brick-remove"></a>' + 
    		  '</li>',
    
    		initialize: function() {
    			var self = this;
    			this.collection.on('add', this.render, this);
    			this.collection.on('remove', this._remove, this);
    			wall.container.click(function(e) { self._onremove(e); });
    		},
    
    		render: function(model) {
    			if (!wall.inputListItem) return;
    			this._tmpl = this._tmpl || _.template(this._markup);
    			html = this._tmpl(model.toJSON());
    			wall.inputListItem.before(html);
    			return this;
    		},
    
    		_onremove: function(e) {
    			if (e.target.className !== 'wall-brick-remove') return;
    			e.preventDefault();
    			var id = $(e.target).data('id');
    			var model = this.collection.get(id);
    			this.collection.remove(model);
    		},
    
    		_remove: function(model) {
    			var li = wall.container.find('#wall-brick-' + model.get('id'));
    			if (li && li.length) {
    				li.fadeOut(function() {
    					li.remove();
    				});
    			}
    		}
    	});
    
    	wall.Languages = Backbone.Collection.extend({
    		model: wall.Language,
  • ¶

    Populate the collection with the dummy data

    		initialize: function() {
    			models = $.map(wall.demoData, function(language, i) {
    				return new wall.Language({
    					id: i + 1,
    					name: language
    				});
    			})
    			this.reset(models);
    		},
    
    		fetch: function(term) {
    			var reg = new RegExp(term, 'i');
    			return this.filter(function(model) {
    				return reg.test(model.get('name'));
    			});
    		}
    	});
    
    	wall.Bricks = Backbone.Collection.extend({
    		model: wall.Language
    	});
    
    	wall.Language = Backbone.Model.extend({
    	});
  • ¶

    start the app on document ready

    	$(function() {
    		wall.start('wall-input');
    	});
    		
    
    }).call(this);