from http://oyanglul.us
Install Node
brew install node
node here is use for setup tools like grunt and mocha
npm install -g grunt
grunt is js build tool.
npm install -g bower
bower is package management for frontend js.
Install Dependencies
npm install
this will install all dependecies under package.json
"devDependencies": {
"grunt": "~0.4.2",
"nunjucks": "~1.0.1",
"grunt-nunjucks": "~0.1.0",
"grunt-contrib-copy": "~0.5.0",
"grunt-contrib-concat": "~0.3.0",
"mocha": "~1.17.1",
"grunt-contrib-uglify": "~0.3.2",
"grunt-contrib-watch": "~0.5.3",
"requirejs": "~2.1.11",
"grunt-mocha": "~0.4.10"
}
Install Frontend Dependencies
bower install
same thing, bower install
will install all dependencies of bower.json
"devDependencies": {
"bootswatch": "~3.1.1",
"nunjucks": "~1.0.1",
"chai": "~1.9.0",
"sinon": "~1.8.2"
}
Run the mocha test
grunt mocha
to run test with phantomjs
or just open tests/index.html
with your browser.
chai is for assertion
nunjucks.render.calledOnce.should.be.true;
sinon for mock
sinon.stub(nunjucks,"render")
Precompile Template
we use nunjucks for template engine
nunjucks is very effective and powerful since it support precompile of templates, and python-like syntax.
grunt-nunjucks
will compile the templates for us through grunt task
nunjucks: {
precompile: {
src: 'src/templates/*',
dest: 'javascripts/templates.js'
}
}
run grunt nunjucks
will precompile all templates under src/templates/
into javascripts/tempalates.js
Generate Github API Token
open https://github.com/settings/applications and click generate token
copy the token. KEEP IT AS SAFE AS YOUR GITHUB PASSWORD
Run the app
simply run grunt
will precomile nunjucks and copy javascripts from src
into javascripts
folder.
then python -m SimpleHTTPServer
to start a server.
open localhost:8000
open console
and put it
localStorage.setItem("access_token",<PASTE>); <RETURN>
refresh and you will see...
DONE
TASK 1
get all gist list and render them into home page.
Check Points
- ajax get data.
- use nunjucks to render your data.
- mocha test your async code.
TASK 1 Walkthrough
Get Gist Data via API
function getGists(){
return Q($.ajax({
url:"https://api.github.com/users/"+login_user+"/gists?access_token=" + localStorage.getItem("access_token"),
method:'get'
}));
};
now we have the data, what we need to do is render the data into template, then put it on the page.
the data is wrapped with Q which will return a Promise object. with Promise, you can do
Q(do something aync).then(console.log)
Template
function renderGist(){
getGists().then(function(body){
$('.container .article').html(nunjucks.render("src/templates/gistlist.html",{gists:body}));
}).then(bindEvent);
}
see, then nunjuncks.render
will take 2 arguments, first the template address, then the data.
lets see what is in the template
<ul class="list-group">
{% for gist in gists %}
{% for name,file in gist.files%}
<li class="list-group-item">
<a href="#" data-id="{{gist.id}}" data-url="{{gist.html_url}}">
{{name}}
</a>
</li>
{% endfor%}
{% endfor%}
</ul>
simple ehh, print the data value with {{}}
but how nunjucks find the template html.
a grunt task will precompile all html template file into javascript/templates.js
nunjucks: {
precompile: {
src: 'src/templates/*',
dest: 'javascripts/templates.js'
}
},
Test
how to test function with promise
describe('article',function(){
it('show all gist', function(done){
var gists = [{
html_url:"https://gist.github.com/2171fd506edc072ca80e"
}];
var gistsData = Q(gists);
getGists = stub().returns(gistsData);
renderGist();
gistsData.should.be.fulfilled.then(function(){
nunjucks.render.calledOnce.should.be.true;
nunjucks.render.getCall(0).args[0].should.be.equal("src/templates/gistlist.html");
nunjucks.render.getCall(0).args[1].should.be.deep.equal({gists:gists});
}).should.notify(done);
});
});
the .should.be.fulfilled.then
and notify
is provide by chai promise plugin.
this test will check
- if the
getGists
is been called - the Promise it returned should be fulfilled.
- nunjucks.render has been called
- nunjucks.render has been called with the right data.
- finally notify the
done
callback provide by mocha, which tell moche the test has been done.
BTW change the before
and after
to beforeEach
, why
TASK 2: Play with Event
Bind event to items in the gists list, when I click, the content of the gist will be rendered.
Gists List
then detail page:
tip: use this api to get gist content
https://gist.github.com/<YOURNAME>/<GIST ID>.json
example: https://gist.github.com/jcouyang/9362622.json
Check points
- use JSONP to cross origin ajax
- get data and render is async, make sure bind event after render.
TASK 2 Walkthrough
Cross Domain Ajax
$.getJSON($(this).data("url")+".json?callback=?"
use getJSON and add callback=?
to get JSONP across origin
bind event
your code will may like this:
getGists().then(function(body){
$('.container .article').html(nunjucks.render("src/templates/gistlist.html",{gists:body}));
}).then(bindEvent);
function bindEvent(){
$(".article li a").click(function(){
$.getJSON($(this).data("url")+".json?callback=?",function(data){
$(".article").html(data.div);
});
});
}
renderHeader().then(renderGist);
bind event after render
now you'll find 2 problems here
- repeat code.
- how to go back from the gist detail page.
TASK 3
now your code should have repeat code
each time you want to add something new to the page, you'll probalily copy this code and make some minor change:
function renderGist(){
getGists().then(function(body){
$('.container .article').html(nunjucks.render("src/templates/gistlist.html",{gists:body}));
}).then(bindEvent);
}
Once you have to copy and paste your code, that means you need refactory
look at the code and you'll see the common things: 1. get Data from backend 2. use the data to render template 3. bind event to the element after the template rendered.
So, TASK 3 would be:
extract the View out of the common code
Check Points
- remove repeat code and replace them with View
- View can be extend -- inheritance
- View should be easy to use like backbone
Hint:
this will explain how to implement simple javascript inheritance using underscore
function Mammal(name){
this.name=name;
this.offspring=[];
}
Mammal.prototype.toString=function(){
return '[Mammal "'+this.name+'"]';
}
Mammal.extend = function(props){
var parent = this,child;
child = function(){
parent.apply(this, arguments)
}
var FixConstructor = function(){
this.constructor = child;
};
FixConstructor.prototype = parent.prototype
child.prototype = new FixConstructor();
_.extend(child, parent);
_.extend(child.prototype,props);
return child;
}
var Cat = Mammal.extend({type:"Cat"})
var cat = new Cat("meow")
Walkthrough
prototype
+----------------+ prototype+-----------------+
| Function +----------+ function |
+-------+--------+ +-----------------+
|
|Mammal instanceof Function === true
|.constructor
+-------+--------+ prototype+----------------+
| Mammal +----------+ empty Object {}
+----------------+ +----------------+
.. .. . . . . .Mammal.prototype.toString=function() .............
+------------------+ +----------------------+
| Mammal |.prototype | {}.toString |
+------------------+ +-----------+----------+
| .extend | ^
+------------------+ |
.......^.......var mammal= new Mammal("dog") .|................... | |toString | Object.getPrototypeOf +-----------+-----------+ +---------------------------+ mammal | .constructor +-----------------------+
constructor
as you can see here, Mammal.constructor == Function
mammal.constructor == Mammal
so function Mammal()
just like create new instance of Function
Function
- Function
new Function (arg1, arg2, ... argN, functionBody)
- function statment
function name([param[, param[, ... param]]]) {
statements
}
- function operater/expression
function [name]([param] [, param] [..., param]) {
statements
}
- Function constructor vs. function declaration vs. function expression
Function constructor
var multiply = new Function("x", "y", "return x * y;");
function declaration
function multiply(x, y) {
return x * y;
}
function expression
var multiply = function(x, y) {
return x * y;
};
named function expression
var multiply = function func_name(x, y) {
return x * y;
};
implement the inheritance of View,just like the mammal:
View.extend = function(props){
var parent = this,child;
child = function(){
parent.apply(this, arguments);
};
child.parent=parent;
var FixConstructor = function(){
this.constructor = child;
};
FixConstructor.prototype = parent.prototype;
child.prototype = new FixConstructor();
_.extend(child, parent);
_.extend(child.prototype,props);
return child;
};
- the mighty basic View
_.extend(View.prototype, {
el:$("body"),
templateEngine: nunjucks,
template:"",
data:"",
events:"click body:",
initialize:function(){
},
getData:function(options){
if (!this.data) return Q();
var methodAndUrl = this.data.split('@');
return Q($.ajax(_.extend({
url:methodAndUrl[1],
method:methodAndUrl[0]
},options)));
},
render:function(){
var self = this;
return this.getData(this.dataOptions).then(function(data){
self.el.html(self.templateEngine.render(self.template, {data:data}));
}).then(self.bindEvent.bind(self));
},
bindEvent:function(){
var self = this;
var events = _.result(this, 'events');
if (!events) return this;
_.each(_(events).keys(),function(key){
var match = key.match(delegateEventSplitter);
var eventName = match[1], selector = match[2];
if (selector==''){
self.el.on(eventName, _.bind(self[events[key]],self));
} else{
self.el.find(selector).on(eventName, _.bind(self[events[key]],self));
}
});
}
});
Why Router
as you may notice that when go to the detail page, you can't navigate back to blog list.
so basically, router can let us navigate around in single page app.
TASK 4
here is how we gonno use the router.
we should able new a Router, and register location to an action.
var router = new Router();
router.get("/", function(){
header.render().then(bloglist.render.bind(bloglist));
});
router.get("/:gistid",function(params){
new BlogDetailView('https://gitst.github.com/'+ params.gistid +".json").render();
});
you'll gonna implement this Router
once we have Router
, we can remove the useless event
'click li a':"renderDetail"
Hint
-
there are a window event listener call hashchange
-
you'll gonna like get help from regex
-
popstate will help you manage history go back and forward.
Walkthrough
git checkout task.4
- regist what hash goes where
router.get("/", function(){
console.log("homepage");
header.render();
bloglist.render();
});
this is define of a rout, when receive /
call the function which render header and blogist.
- implement the register
Router.prototype.get = function(url,callback){
var paramRegx = /:([^/.\\\\]+)/g;
var param;
var route = {
paramNames:[],
regex:'^'+ url +'$',
params:{},
callback:callback
};
while((param = paramRegx.exec(url))!==null){
route.paramNames.push(param[1]);
route.regex = route.regex.replace(param[0],"([^/.\\\\]+)").replace(":","");
}
this.routes.push(route);
};
we create a Router
object which contain a list of routes
, whenever get
is called, regist the route as an route hash like
var route = {
paramNames:[],
regex:'^'+ url +'$',
params:{},
callback:callback
};
which contains a regex to match hash, paramname array, params dict, and a callback.
- then we need to listen to the hashchange, and find the route according to the hash.
$(window).on("hashchange",function(e){
findRoute(e);
}
- what the findRoute actually do
_.each(self.routes,function(route){
var regex = new RegExp(route.regex,"g");
if((values = regex.exec(hash))!==null){
console.log(values);
values.shift();
route.callback(_.object(route.paramNames,values));
return;
}
findRoute need to iterate routes
we registed before, find the one who match the hash loaction, then call it's callback.
Currently we put all our job into View
, like define where to fetch data, fetch and operate on data, which is not good and should be done by Model
.
TASK 5
extract Model out of View.
Hint
better use Q.defer to define your data which need to wait.