(reading notes from JavaScript Web Applications)
- Chapter 1, MVC and Classes
- Chapter 2, Events and Observing
- Chapter 3, Models and Data
- Chapter 4, Controllers and State
- Chapter 5, Views and Templating
- Chapter 6, Dependency Management
- Chapter 7, Working with Files
- Chapter 8, The Real-Time Web
- Chapter 9, Testing and Debugging
- Chapter 10, Deploying
- Chapter 11, The Spine Library
- Chapter 12, The Backbone Library
- Chapter 13, The JavascriptMVC Library
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:
- When a constructor function is called with the
new
keyword, the context switches from global (window) to a new and empty context specific to that instance. So thethis
keyword inside refers to the current instance. - By default, if you don't return anything from a constructor function,
this
will be returned. Otherwise, you can return any nonprimitive type.
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)