Labs » Personal Capital Pre-screening challenge

Summary

Write a single page app that allows us users to search for financial institutions of their interest.

Demo

Code

  $(document).ready(function() {

  Handlebars.registerHelper('highlight', function(query, str) {
    if (!(query && str)) {
      return;
    }
    var re = new RegExp("(" + query + ")(?!([^<]+)?>)", 'gi');
    return new Handlebars.SafeString(str.replace(re, '$1'));
  });

  var ResultModel = Backbone.Model.extend({});
    ResultsCollection = Backbone.Collection.extend({
      model: ResultModel,
      url: '/labs/codingchallenge-pcap/products.json'
    });

  var ResultView = Marionette.ItemView.extend({
    tagName: "li",
    template: Handlebars.compile($("#results-childview-template").html()),
    templateHelpers: function() {
      return {
        query: this.options.query
      }
    }
  });

  var SearchCompositeView = Marionette.CompositeView.extend({
    MAX_DISPLAYED_RESULTS: 15,

    template: "#results-compositeview-template",
    childView: ResultView,
    childViewContainer: "ul",
    childViewOptions: function() {
      return {
        query: this.model.get('query')
      }
    },

    ui: {
      query: 'input[name=q]',
      filter: 'input[name=filter]',
      results: '.results',
      list: 'ul'
    },

    events: {
      'keyup @ui.query': 'handleKeyup',
      'focus @ui.query': 'handleFocus',
      'blur @ui.query': 'handleBlur',
      'click @ui.filter': 'handleFilter'
    },

    initialize: function(options) {
      this.options = options;
      // state model
      this.model = new Backbone.Model({
        query: '',
        filter: 'ALL'
      });

      this.listenTo(this.model, 'change:query', this.handleFetch);
      this.listenTo(this.model, 'change:filter', this.handleFetch);
    },

    onRender: function() {
      this.ui.query.focus();
    },

    handleFocus: function() {
      this.ui.list.css({
        visibility: 'visible'
      });
    },

    handleBlur: function() {
      this.ui.list.css({
        visibility: 'hidden'
      });
      this.selectedResult = null;
    },

    handleKeyup: function(event) {
      switch (event.which) {
        // disabled for now
        case 27:
          this.ui.query.blur();
          return;

        case 38: // up
        case 40: // down
          this.handleNavigation(event);
          return;
      }

      var query = $.trim(this.ui.query.val()).toLowerCase();
      if (query === "") {
        this.model.set('query', '');
        this.collection.reset([]);
        return;
      }
      if (query === this.model.get('query')) {
        return;
      }
      this.model.set('query', query);
    },

    handleNavigation: function(event) {
      // nothing is selected.
      if (!this.selectedResult) {
        switch (event.which) {
          case 38: // up
            this.selectedResult = $(this.ui.list).children('li:last-child').addClass('selected');
            return;
          case 40: // down
            this.selectedResult = $(this.ui.list).children('li:first-child').addClass('selected')
            return;
        }
      } else {
        this.selectedResult.removeClass('selected');
        switch (event.which) {
          case 38: // up
            if (this.selectedResult.prev().length > 0) {
              this.selectedResult = this.selectedResult.prev().addClass('selected')
            } else {
              this.selectedResult = $(this.ui.list).children('li:last-child').addClass('selected');
            }
            break;
          case 40: // down
            if (this.selectedResult.next().length > 0) {
              this.selectedResult = this.selectedResult.next().addClass('selected')
            } else {
              this.selectedResult = $(this.ui.list).children('li:first-child').addClass('selected')
            }
            break;
        }
      }
    },

    handleFilter: function(event) {
      event.stopPropagation();
      // UI Hash does not seem to know how to find the selected value of a group of radio buttons,
      //   so gotta do it the jQuery way.
      var val = this.$el.find("input[name='filter']:checked").val();
      this.model.set('filter', val);
      this.ui.query.focus();
    },

    handleFetch: function() {
      var query = this.model.get('query');
      if ($.trim(query) === "") {
        return;
      }

      // if cached products
      if (this.products) {
        this.handleQueryProducts(query,this.products);
      } else {
        this.collection.fetch({
          success: _.bind(function(collection, response) {
            // cached the list of products, cause it doesn't change.
            this.products = response.products;
            this.handleQueryProducts(query, response.products);
          }, this)
        });
      }
    },

    handleQueryProducts: function(query) {
      var filter = this.model.get('filter');
      var parsed =  _.map(_.filter(this.products, function(item) {
        return (filter === 'ALL' || filter === item.type) && item.name.toLowerCase().indexOf(query) >= 0;
      }), function(item) {
        return new ResultModel(item);
      });
      this.collection.reset(parsed.slice(0, this.options.num_displayed_results || this.MAX_DISPLAYED_RESULTS));
    }
  });

  var searchView = new SearchCompositeView({
    el: "#component-search-institutions",
    collection: new ResultsCollection()
  });
  searchView.render();
});