Jim Cheung

(reading notes from JavaScript Web Applications)

Chapter 1, MVC and Classes

Toward Modularity, Creating Classes

Any javascript function can be used as a constructor. Use the new operator with a constructor function to create a new instance.

The new operator changes a function's context, as well as the behavior of the return statement:

Adding Functions to Classes

adding class functions to a constructor function:

Person.find = function(id){ /* ... */ };
var person = Person.find(1);

adding instance functions to a constructor function:

Person.prototype.breath = function() { /*...*/ }
var person = new Person;
person.breath();

A common pattern is to alias a class' prototype to fn:

Person.fn = Person.prototype;
Person.fn.run = function(){ /*...*/ }

Adding Methods to Our Class Library

let's create a different way of adding properties to our classes using two functions, extend() and include():

var Class = function() {
    var klass = function() {
        this.init.apply(this, arguments);
    };
    klass.prototype.init = function(){};
    // shortcut to access prototype
    klass.fn = klass.prototype;
    // shortcut to access class
    klass.fn.parent = klass;
    // adding class properties
    klass.extend = function(obj) {
        var extended = obj.extended;
        for (var i in obj) {
            klass[i] = obj[i];
        }
        if (extended) extended(klass)
    };
    // adding instance properties
    klass.include = function(obj) {
        var included = obj.included;
        for (var i in obj) {
            klass.fn[i] = obj[i];
        }
        if (included) included(klass)
    };
    return klass;
};
// usage of extend()
var Person = new Class;
Person.extend({
    find: function(id){ /*...*/ },
    exists: function(id){ /*...*/ }
});
var person = Person.find(1);
// usage of include()
var Person = new Class;
Person.include({
    save: function(id){ /*...*/ },
    destroy: function(id){ /*...*/ }
});
var person = new Person;
person.save();
// usage of extended()
Person.extend({
    extended: function(klass) {
        console.log(klass, " was extended!");
    }
});

the beauty of this approach is that we've now support for modules:

var ORMModule = {
    save: function() { /* ... */ }
};
var Person = new Class;
var Asset = new Class;
Person.include(ORMModule);
Asset.include(ORMModule);

Class Inheritance Using Prototype

let's add inheritance to our custom class library:

var Class = function(parent) {
    var klass = function() {
        this.init.apply(this, arguments);
    }
    // change klass' prototype
    if (parent) {
        var subclass = function() {};
        subclass.prototype = parent.prototype;
        klass.prototype = new subclass;
    };
    klass.prototype.init = function(){};
    // shortcuts
    klass.fn = klass.prototype;
    klass.fn.parent = klass;
    klass._super = klass.__proto__;
    /* include/extend code ... */
    return klass;
};
// usage
var Animal = new Class;
Animal.include({
    breath: function(){
        console.log('breath');
    }
});
var Cat = new Class(Animal);
var tommy = new Cat;
tommy.breath();

Function Invocation

Apart from using brackets, there are two other ways to invoke a function: apply() and call(). The difference between them has to do with the arguments you want to pass to the function.

function.apply(this, [1, 2, 3])
function.call(this, 1, 2, 3)

javascript uses context changes to share state, especially during event callbacks.

Controlling Scope in Our Class Library

we'll add a proxy function on both class and instance:

var Class = function(parent) {
    /* ... */
    // adding a proxy function
    klass.proxy = function(func){
        var self = this;
        return (function(){
            return func.apply(self, arguments);
        });
    }
    // add it on instances too
    klass.fn.proxy = klass.proxy;
    return klass;
};
// usage
var Button = new Class;
Button.include({
    init: function(element){
        this.element = jQuery(element);
        // proxy the click function
        this.element.click(this.proxy(this.click));
    },
    click: function(){ /* ... */ }
});

if we didn't wrap the click() callback with a proxy, it would be called within the context of this.element, rather than Button. ECMAScript 5 added support for controlling invocation scope with the bind() function:

Button.include({
    init: function(element){
        this.element = jQuery(element);
        this.element.click(this.click.bind(this));
    },
    click: function() { /* ... */ }
});

use es5-shim to support es5 functions in old browsers.

Adding Private Functions

use anonymous functions to create a private scope:

var Person = function(){};

(function() {
    var findById = function() { /* ... */ };
    Person.find = function(id) {
        if (typeof id == "integer")
            return findById(id);
    };
})();

Class Libraries

jQuery doesn't include class support natively, but it can easily be added with a plug-in like HJS.

Spine also has a class implementation.

Prototype also has an excellent class API.

Chapter 2, Events and Observing

Event Libraries

jQuery has a bind() function for adding cross-browser event listeners.

the element must exist before you start adding events to it. jQuery abstracts DOMContentLoaded with a ready() function that has cross-browser support:

jQuery.ready(function($) {
    /* ... */
});
// in fact, you can skip the ready()
jQuery(function($) {
    /* ... */
});

Delegating Events

(note, jQuery >1.7 replaces delegate with on)

// don't do this, it's expensive
$("ul li").click(function(){ /* ... */ });
// this only adds one event listener
$("ul").delegate("li", "click", /* ... */);

another advantage to event delegation is that any children added dynamically to the element would still have the event listener.

Custom Events

jQuery lets you fire custom events using the trigger() function:

$('.class').bind('refresh.widget', function(){});
$('.class').trigger('refresh.widget');
// to pass data
$('.class').bind('frob.widget', function(event, dataNumber){
    console.log(dataNumber);
});
$('.class').trigger('frob.widget', 5);

like native events, custom events will propagate up the DOM tree.

Custom Events and jQuery Plug-Ins

(this section has a very good example of using custom events to clean up codes, check my github/jstest)

Non-DOM Events

Events aren't restricted to the DOM though, you can easily write your own event handler library. the pattern is called publish/subscribe.

publicher publish messages to a particular channel, and subscriber subscribe to channels, receiving notifications when new messages are published. The key here is that publishers and subscribers are completely decoupled – they have no idea of each other's existence. the only thing the two share is the channel name.

var PubSub = {
    subscribe: function(ev, callback){
        // create _callbacks object
        var calls = this._callbacks || (this._callbacks = {});
        // create an array for the given event key, then append the callback to the array
        (this._callbacks[ev] || (this._callbacks[ev] = [])).push(callback);
        return this;
    },
    publish: function(){
        // turn arguments object into a real array
        var args = Array.prototype.slice.call(arguments, 0);
        // extract the first argument, the event name
        var ev = args.shift();
        // return if there is no _callbacks object or it doesn't contain an array for the given event name
        var list, calls, i, l;
        if (!(calls = this._callbacks)) return this;
        if (!(list = this._callbacks[ev])) return this;
        // invoke the callbacks
        for (i = 0, l = list.length; i < l; i++) {
            list[i].apply(this, args);
        }
        return this;
    }
};
// usage
PubSub.subscribe("wem", function() {
    alert("Wem!");
});
PubSub.publish("wem");

scope PubSub to an object:

var Asset = {};
jQuery.extend(Asset, PubSub);
// we now have publish/subscribe Functions
Asset.subscribe("create", function() {
    /* ... */
});

for jQuery, there is an easier library by Ben Alman, super simple:

(function($) {
    var o = $({});
    $.subscribe = function() {
        o.bind.apply(o, arguments);
    };
    $.unsubscribe = function() {
        o.unbind.apply(o, arguments);
    };
    $.publish = function() {
        o.trigger.apply(o, arguments);
    };
})(jQuery);

Chapter 3, Models and Data

(the ORM is using object.create(), check book)

Adding ID Support

guid function (by Robert Kieffer)

Math.guid = function() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
        return v.toString(16);
    }).toUpperCase();
};

Storing Data Locally

HTML5 includes local storage, data is stored exclusively on the client side and is never sent to servers. amount of storage differs per browser, but they all offer at least 5MB per domain.

HTML5 storage consists of two types: local storage and session storage. Local storage persists after the browser is closed; session storage persists only for the lifetime of the window.

you can access and manipulate local storage and session storage using localStorage and sessionStorage objects.

// setting a value
localStorage["someData"] = "wem";
// set an itme (same as above)
localStorage.setItem("someData", "wem");
// getter, return null if unknown
localStorage.getItem("someData");
// delete, return null if unknown
localStorage.removeItem("someData");
// clear all
localStorage.clear();
// saving object
localStorage.setItem("seriData", JSON.stringify(object));
// load
var result = JSON.parse(localStorage.getItem("seriData"));

Chapter 4, Controllers and State

Module Pattern

(function(){
    /* ... */
})();

and inject global vars in

(function($){
    /* ... */
})(jQuery);

and export to global

(function($, exports){
    exports.Foo = "wem";
})(jQuery, window);

use global context rather than the window object

var exports = this;
(function($){
    exports.Bar = "blah";
})(jQuery);

State Machines

a state machine (or Finite State Machine FSM) consists of two things: states and transitions. It has only one active state, but it has a multitude of passive state. when the active state switches, transitions between the states are called.

var StateMachine = function(){};
StateMachine.fn = StateMachine.prototype;
$.extend(StateMachine.fn, Events);
StateMachine.fn.add = function(controller){
    this.bind("change", function(e, current){
        if (controller == current)
            controller.activate();
        else
            controller.deactivate();
    });
    controller.active = $.proxy(function(){
        this.trigger("change", controller);
    }, this);
};
// usage
var con1 = {
    activate: function() { /* ... */ },
    deactivate: function() { /* ... */ }
};
var con2 = {
    activate: function() { /* ... */ },
    deactivate: function() { /* ... */ }
};
var sm = new StateMachine;
sm.add(con1);
sm.add(con2);
con1.active();

Routing

using url hash to avoid page refreshing. the hash is never sent to the server. we can retrieve and alter page's hash using the location object

// set the hash
window.location.hash = "foo";
// strip '#'
var hashValue = window.location.hash.slice(1);

to detect hash changes, use hashchange Events

windows.addEventListener('hashchange', function(){/* ... */}, false);
// or with jQuery
$(window).bind('hashchange', function(event){ /* ... */ });

this event isn't fire when the page initially loads, you'll need to trigger it manually on page load.

Ajax Crawling

http://twitter.com/#!/maccman

// will be translate to by crawler, if it saw ! after #
http://twitter.com/?_escaped_fragment_=/maccman
// then you can do a crawler friendly version with that url

Using the HTML5 History API

var dataObject = { /* ... */ };
var url = '/post/new-url';
history.pushState(dataObject, document.title, url);

a popstate event is triggered when the page is loaded or when history.pushState() is called, in the case of the latter, the event object will contain a state property that holds the data object given to history.pushState()

window.addEventListener('popstate', function(event){
    if (event.state) {
        // history.pushState() was called
    }
});
    
// if use jquery, need to use the originalEvent
$(window).bind('popstate', function(event){
    event = event.originalEvent;
    if (event.state) {
        // history.pushState() was called
    }
});

Chapter 5, Views and Templating

use the jQuery.tmpl library

(nothing interesting here)

Chapter 6, Dependency Management

CommonJS

// declaring a module
// maths.js
exports.per = function(value, total){
    return ((value / total) * 100);
};
// app.js
var Maths = require("./maths");

Modules and the Browser

CommonJS uses module transport format to allow for asynchronous loading on clients.

// maths.js
require.define("maths", function(require, exports){
    export.per = /* ... */
});
// app.js
require.define('app', function(require, exports){
    var per = require("./maths").per;
    /* ... */
}, ["./maths"]); // list dependencies

Module Loaders

Yabble and RequireJS

Module Alternatives

Sprockets and LABjs

Chapter 7, Working with Files

Getting Information About Files

Files are represented in HTML5 by File objects, which have 3 attributes (all read-only): name, size and type (for security reasons, path information is never exposed)

Multiple files are exposed as FileList objects (an array of File objects)

File Inputs

By specifying multiple attribute on file input, you're indicating to the browser that users should be allowed to select multiple files.

<input type="file" multiple>
// to get information
var input = $("input[type=file]");
input.change(function(){
    var files = this.files;
    for (var i = 0; i < files.length; i++)
        assert(files[i].type.match(/image.*/))
});

Drag and Drop

There are no less than 7 events associated with drag and drop: dragstart, drag, dragover, dragenter, dragleave, drop and dragend

to make an element draggable, set its draggable attribute to true

<div id="dragme" draggable="true">Drag me!</div>
// js
var element = $("#dragme");
element.bind('dragstart', function(event){
    // we don't want to use jQuery's abstraction
    event = event.originalEvent;
    event.dataTransfer.effectAllowed = "move";
    event.dataTransfer.setData("text/plain", $(this).text());
    event.dataTransfer.setData("text/html", $(this).html());
    event.dataTransfer.setDragImage("/images/drag.png", -10, -10);
});
// Dragging links
event.dataTransfer.setData("text/uri-list", "http://example.com");
event.dataTransfer.setData("text/plain", "http://example.com");
// multiple links, just use new line
event.dataTransfer.setData("text/uri-list", "http://example.com\nhttp://google.com");
event.dataTransfer.setData("text/plain", "http://example.com\nhttp://google.com");

for drop event to fire, you have to cancel the defaults of both dragover and dragenter events:

var element = $("#dropzone");
element.bind("dragenter", function(e){
    e.stopPropagation();
    e.preventDefault();
});
element.bind("dragover", function(e){
    // set cursor
    e.originalEvent.dataTransfer.dropEffect = "copy";
    e.stopPropagation();
    e.preventDefault();
});

once we've canceled these two events, we can start listening to drop events

element.bind("drop", function(event){
    // cancel redirection
    event.stopPropagation();
    event.preventDefault();
    event = event.originalEvent;
    var files = event.dataTransfer.files;
    for (var i = 0; i < files.length; i++)
        alert("dropped " + files[i].name);
});
// to access other data
var text = event.dataTransfer.getData("Text");

by default, dragging a file onto a web page makes the browser navigate to that file, we can prevent this by cancelling body's dragover event:

$("body").bind("dragover", function(e){
    e.stopPropagation();
    e.preventDefault();
});

Copy and Paste

there are two events associated with copying and two with cutting: beforecopy, copy, beforecut, cut

when copy event fires, will give you a clipboardData object

$("textarea").bind("copy", function(event){
    event.stopPropagation();
    event.preventDefault();
    var cd = event.originalEvent.clipboardData;
    // for IE
    if (!cd) cd = window.clipboardData;
    // for firefox
    if (!cd) return;
    cd.setData("text/plain", $(this).text());
});

there are two events associated with pasting: beforepaste and paste

$("textarea").bind("paste", function(event){
    event.stopPropagation();
    event.preventDefault();
    var cd = event.originalEvent.clipboardData;
    // for IE
    if (!cd) cd = window.clipboardData;
    // for firefox
    if (!cd) return;
    $("#result").text(cd.getData("text/plain"));
    // safari event support file pasting
    var files = cd.files;
});

Reading Files

once you've obtained a File reference, you can instantiate a FileReader object to read its contents into memory. Files are read asynchronously.

var preview = $("img#preview");
if (file.type.match(/image.*/) && file.size < 50000000) {
    var reader = new FileReader();
    reader.onload = function(e){
        var data = e.target.result;
        preview.attr("src", data);
    };
    reader.readAsDataURL(file);
}

Custom Browse Buttons

to create a custom browse button, just call the browseElement() function on a jQuery instance:

var input = $("#attach").browseElement();
input.change(function(){
    var files = $(this).attr("files");
});

Uploading Files

File uploads can be done via the exiting XMLHttpRequest API, using the send() function.

var formData = new FormData($('form')[0]);
// you can add form data as strings
formData.append("stringKey", "stringData");
// and even add File obejcts
formData.append("fileKey", file);
jQuery.ajax({
    data: formData,
    processData: false, // stop jQuery serialize data
    url: "http:example.com",
    type: "POST"
});
// alternative to using formData, send the File object directly
$.ajax({
    url: "http://example.com",
    type: "POST",
    success: function(){/* .. */},
    processData: false,
    
    // this upload is a bit different from the tranditional multipart/form-data one, to add file information, send custom header:
    contentType: "multipart/form-data",
    beforeSend: function(xhr, settings){
        xhr.setRequestHeader('Cache-Control', 'no-cache');
        xhr.setRequestHeader('X-File-Name', file.fileName);
        xhr.setRequestHeader('X-File-Size', file.fileSize);
    },
    data: file
});

Ajax Progress

to listen to the progress event on the download request, add it directly on the XHR instance:

var req = new XMLHttpRequest();
req.addEventListener('progress', updateProgress, false);
req.addEventListener('load', transferComplete, false);
req.open();
// for upload progress event, add it to the upload attribute of the XHR instance
var req = new XMLHttpRequest();
req.upload.addEventListener('progress', updateProgress, false);
req.upload.addEventListener('load', transferComplete, false);
req.open();

the progress event contains the position, total and timeStamp

var startStamp = new Date();
var progress = function(event) {
    var percentage = Math.round((event.position / event.total) * 100);
    var lapsed = startStamp - event.timeStamp;
    var eta = lapsed * event.total / e.position - lapsed;
}

jQuery Drag and Drop Uploader

(check book for the snippet)

Chapter 8, The Real-Time Web

WebSockets

detecting support for WebSockets

var supported = ("WebSocket" in window);

the WebSockets API is clear and logical:

var socket = new WebSocket("ws://example.com");
// the connection has connected
socket.onopen = function(){/* .. */};
// the connection has some data
socket.onmessage = function(){/* .. */};
// the connection has closed
socket.onclose = function(){/* .. */};
// communications
socket.onmessage = function(msg){
    console.log("new data - ", msg);
};
socket.onopen = function(){
    socket.send("hell from client");
};

WebSocket scheme ws:// default use port 80 for nonencrypted connection and 443 for encrypted one (wss://)

Node.js and Socket.IO

for server side, Socket.IO is a node.js library for WebSockets

var socket = new io.Socket();
socket.on("connect", function() {
    socket.send("hi!");
});
socket.on("message", function(data){
    alert(data);
});
socket.on("disconnect", function(){});

Chapter 9, Testing and Debugging

Unit Testing

var asset = function(value, msg) {
    if (!value)
        throw(msg || (value + " does not equal true"));
};

javascript automatically type-converts undefined, 0 and null to false during a Boolean check, this function works for a null check too.

assert libraries usually include an assertEqual() function:

var assertEqual = function(val1, val2, msg) {
    if (val1 !== val2)
        throw(msg || (val1 + " does not equal " + val2));
}

Testing libraries

QUnit and Jasmine

Drivers

Watir and Selenium

Headless Testing

Zombie.js and Ichabod (and phantomJS?)

Distributed Testing

TestSwarm

(rest of the chapter is browser dev tools for testing and debuging)

Chapter 10, Deploying

(this chapter has some very short into to cache headers, minification, compression, cdn and auditors)

Chapter 11, The Spine Library

(no notes)

Chapter 12, The Backbone Library

(no notes)

Chapter 13, The JavascriptMVC Library

(no notes)