jQuery's Place in JavaScript Frameworks

About Me

In the Beginning

A New Wave

jQuery Spaghetti

Enter the Framework

Tonight Only!*

See how these frameworks interact with jQuery:

* Also all following nights at http://jugglinmike.github.io/presentations

Backbone.js

Capturing


// Initial Setup
// -------------

// Save a reference to the global object (`window` in the browser, `exports`
// on the server).
var root = this;
			

// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
// the `$` variable.
Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$;
			

Backbone.js

Internal Usage


// Set callbacks, where `this.events` is a hash of
//
// *{"event selector": "callback"}*
//
//     {
//       'mousedown .title':  'edit',
//       'click .button':     'save'
//       'click .open':       function(e) { ... }
//     }
//
// pairs. Callbacks will be bound to the view, with `this` set properly.
// Uses event delegation for efficiency.
// Omitting the selector binds the event to `this.el`.
// This only works for delegate-able events: not `focus`, `blur`, and
// not `change`, `submit`, and `reset` in Internet Explorer.
delegateEvents: function(events) {
  if (!(events || (events = _.result(this, 'events')))) return this;
  this.undelegateEvents();
  for (var key in events) {
    var method = events[key];
    if (!_.isFunction(method)) method = this[events[key]];
    if (!method) continue;

    var match = key.match(delegateEventSplitter);
    var eventName = match[1], selector = match[2];
    method = _.bind(method, this);
    eventName += '.delegateEvents' + this.cid;
    if (selector === '') {
      this.$el.on(eventName, method);
    } else {
      this.$el.on(eventName, selector, method);
    }
  }
  return this;
},
			

Backbone.js

Extensions


Backbone.sync = function(method, model, options) {
			

// If we're sending a `PATCH` request, and we're in an old Internet Explorer
// that still has ActiveX enabled by default, override jQuery to use that
// for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
if (params.type === 'PATCH' && noXhrPatch) {
  params.xhr = function() {
    return new ActiveXObject("Microsoft.XMLHTTP");
  };
}
			

Backbone.js

Application Usage


var MyView = Backbone.View.extend({
  events: {
    "click .hedgehog": "winPrize"
  },
  winPrize: function(event) {
    event.stopPropagation();
    this.$(".status").text("You won!");
  }
});
			

Ember.js

Capturing


var jQuery = Ember.imports.jQuery;
Ember.assert("Ember Views require jQuery 1.8, 1.9 or 2.0", jQuery && (jQuery().jquery.match(/^((1\.(8|9))|2.0)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY));
			

// Default imports, exports and lookup to the global object;
var imports = Ember.imports = Ember.imports || this;
var exports = Ember.exports = Ember.exports || this;
var lookup  = Ember.lookup  = Ember.lookup  || this;
			

Ember.js

Internal Usage


/**
  @private

  Sets up event listeners for standard browser events.

  This will be called after the browser sends a `DOMContentReady` event. By
  default, it will set up all of the listeners on the document body. If you
  would like to register the listeners on a different element, set the event
  dispatcher's `root` property.

  @method setup
  @param addedEvents {Hash}
*/
setup: function(addedEvents, rootElement) {
  var event, events = {
    touchstart  : 'touchStart',
    touchmove   : 'touchMove',
    touchend    : 'touchEnd',
    touchcancel : 'touchCancel',
    keydown     : 'keyDown',
    keyup       : 'keyUp',
    keypress    : 'keyPress',
    mousedown   : 'mouseDown',
    mouseup     : 'mouseUp',
    contextmenu : 'contextMenu',
    click       : 'click',
    dblclick    : 'doubleClick',
    mousemove   : 'mouseMove',
    focusin     : 'focusIn',
    focusout    : 'focusOut',
    mouseenter  : 'mouseEnter',
    mouseleave  : 'mouseLeave',
    submit      : 'submit',
    input       : 'input',
    change      : 'change',
    dragstart   : 'dragStart',
    drag        : 'drag',
    dragenter   : 'dragEnter',
    dragleave   : 'dragLeave',
    dragover    : 'dragOver',
    drop        : 'drop',
    dragend     : 'dragEnd'
  };

  Ember.$.extend(events, addedEvents || {});


  if (!Ember.isNone(rootElement)) {
    set(this, 'rootElement', rootElement);
  }

  rootElement = Ember.$(get(this, 'rootElement'));

  Ember.assert(fmt('You cannot use the same root element (%@) multiple times in an Ember.Application', [rootElement.selector || rootElement[0].tagName]), !rootElement.is('.ember-application'));
  Ember.assert('You cannot make a new Ember.Application using a root element that is a descendent of an existing Ember.Application', !rootElement.closest('.ember-application').length);
  Ember.assert('You cannot make a new Ember.Application using a root element that is an ancestor of an existing Ember.Application', !rootElement.find('.ember-application').length);

  rootElement.addClass('ember-application');

  Ember.assert('Unable to add "ember-application" class to rootElement. Make sure you set rootElement to the body or an element in the body.', rootElement.is('.ember-application'));

  for (event in events) {
    if (events.hasOwnProperty(event)) {
      this.setupHandler(rootElement, event, events[event]);
    }
  }
},
			

Ember.js

Extensions


/**
@module ember
@submodule ember-views
*/
if (Ember.$) {
  // http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#dndevents
  var dragEvents = Ember.String.w('dragstart drag dragenter dragleave dragover drop dragend');

  // Copies the `dataTransfer` property from a browser event object onto the
  // jQuery event object for the specified events
  Ember.EnumerableUtils.forEach(dragEvents, function(eventName) {
    Ember.$.event.fixHooks[eventName] = { props: ['dataTransfer'] };
  });
}
			

Ember.js

Application Usage


App.AudioView = Ember.View.extend({
  classNames: ['audio-control'],
  templateName: 'audioControl',
  play: function() {
    this.$('audio')[0].play();

    this.set('isPlaying', true);
  },
  pause: function() {
    this.$('audio')[0].pause();

    this.set('isPlaying', false);
  },
  didInsertElement: function() {
    var view = this;
    var $audio = this.$('audio');

    $audio.prop('autoplay', true)
      .attr('src', this.get('song.url'));

    $audio.on('canplaythrough', function() {
      view.set('isLoaded', true);
    }).on('loadedmetadata', function() {
      view.set('duration', Math.floor(this.duration));
    }).on('timeupdate', function() {
      view.set('currentTime', Math.floor(this.currentTime));
    }).on('playing', function() {
      view.set('isPlaying', true);
    }).on('pause', function() {
      view.set('isPlaying', false);
    });
  },
  src: function(attr, val) {
    if (arguments.length === 2) {
      this.$('audio').attr('src', val);
      return val;
    }
    return $('audio').attr('src');
  }.property()
});
			

Angular.js

Capturing


function bindJQuery() {
  // bind to jQuery if present;
  jQuery = window.jQuery;
  // reset to jQuery or default to us.
  if (jQuery) {
    jqLite = jQuery;
    extend(jQuery.fn, {
      scope: JQLitePrototype.scope,
      controller: JQLitePrototype.controller,
      injector: JQLitePrototype.injector,
      inheritedData: JQLitePrototype.inheritedData
    });
    // Method signature:
    // JQLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments)
    JQLitePatchJQueryRemove('remove', true, true, false);
    JQLitePatchJQueryRemove('empty', false, false, false);
    JQLitePatchJQueryRemove('html', false, false, true);
  } else {
    jqLite = JQLite;
  }
  angular.element = jqLite;
}
			

  //try to bind to jquery now so that one can write angular.element().read()
  //but we will rebind on bootstrap again.
  bindJQuery();

  publishExternalAPI(angular);

  jqLite(document).ready(function() {
    angularInit(document, bootstrap);
  });

})(window, document);
			

Angular.js

Internal Usage


/**
 * @ngdoc object
 * @name ng.$document
 * @requires $window
 *
 * @description
 * A {@link angular.element jQuery (lite)}-wrapped reference to the browser's
 * `window.document` element.
 */
function $DocumentProvider(){
  this.$get = ['$window', function(window){
    return jqLite(window.document);
  }];
}
			

function compileTemplateUrl(directives, beforeTemplateNodeLinkFn, $compileNode, tAttrs,
    $rootElement, replace, childTranscludeFn) {
  var linkQueue = [],
      afterTemplateNodeLinkFn,
      afterTemplateChildLinkFn,
      beforeTemplateCompileNode = $compileNode[0],
      origAsyncDirective = directives.shift(),
      // The fact that we have to copy and patch the directive seems wrong!
      derivedSyncDirective = extend({}, origAsyncDirective, {
        controller: null, templateUrl: null, transclude: null, scope: null
      }),
      templateUrl = (isFunction(origAsyncDirective.templateUrl))
          ? origAsyncDirective.templateUrl($compileNode, tAttrs)
          : origAsyncDirective.templateUrl;

  $compileNode.html('');

  $http.get(templateUrl, {cache: $templateCache}).
    success(function(content) {
      var compileNode, tempTemplateAttrs, $template;

      content = denormalizeTemplate(content);

      if (replace) {
        $template = jqLite('
' + trim(content) + '
').contents(); compileNode = $template[0]; if ($template.length != 1 || compileNode.nodeType !== 1) { throw new Error(MULTI_ROOT_TEMPLATE_ERROR + content); } tempTemplateAttrs = {$attr: {}}; replaceWith($rootElement, $compileNode, compileNode); collectDirectives(compileNode, directives, tempTemplateAttrs); mergeTemplateAttributes(tAttrs, tempTemplateAttrs); } else { compileNode = beforeTemplateCompileNode; $compileNode.html(content); }

Angular.js

Extensions


function bindJQuery() {
  // bind to jQuery if present;
  jQuery = window.jQuery;
  // reset to jQuery or default to us.
  if (jQuery) {
    jqLite = jQuery;
    extend(jQuery.fn, {
      scope: JQLitePrototype.scope,
      controller: JQLitePrototype.controller,
      injector: JQLitePrototype.injector,
      inheritedData: JQLitePrototype.inheritedData
    });
    // Method signature: JQLitePatchJQueryRemove(name, dispatchThis,
    // filterElems, getterIfNoArguments)
    JQLitePatchJQueryRemove('remove', true, true, false);
    JQLitePatchJQueryRemove('empty', false, false, false);
    JQLitePatchJQueryRemove('html', false, false, true);
  } else {
    jqLite = JQLite;
  }
  angular.element = jqLite;
}
			

function JQLiteInheritedData(element, name, value) {
  element = jqLite(element);

  // if element is the document object work with the html element instead
  // this makes $(document).scope() possible
  if(element[0].nodeType == 9) {
    element = element.find('html');
  }

  while (element.length) {
    if (value = element.data(name)) return value;
    element = element.parent();
  }
}
			

Angular.js

Application Usage


/*global todomvc */
'use strict';

/**
 * Directive that executes an expression when the element it is applied to
 * loses focus
 */
todomvc.directive('todoBlur', function () {
  return function (scope, elem, attrs) {
    elem.bind('blur', function () {
      scope.$apply(attrs.todoBlur);
    });
  };
});
			

Thanks, jQuery!

Resources

Further Reading

Image Credits