├── .gitignore
├── README.md
├── profiles
└── app.js
├── util
├── build.sh
└── setup.sh
└── www
├── css
└── app.css
├── data
└── people.json
├── img
├── adam.png
├── alex.png
├── loading.gif
├── paul.png
└── rebecca.png
├── index.html
└── js
├── app
├── base.js
├── controllers
│ └── People.js
├── models
│ ├── People.js
│ └── Person.js
├── services
│ ├── Favorites.js
│ ├── Twitter.js
│ └── YQL.js
└── views
│ ├── People.js
│ ├── People
│ ├── People.html
│ └── Person.html
│ ├── Person.js
│ └── Person
│ ├── Person.html
│ ├── Tweet.html
│ └── Weather.html
└── dbp
├── Router.js
└── tests
├── Router.html
└── Router.js
/.gitignore:
--------------------------------------------------------------------------------
1 | www/js/dojo-release-1.6.0-src
2 | dist
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Using the Dojo Toolkit for MVC JavaScript applications
2 |
3 | This is a repository based on the [Dojo
4 | Boilerplate](https://github.com/rmurphey/dojo-boilerplate/) for demonstrating
5 | simple MVC concepts using the [Dojo Toolkit](http://dojotoolkit.org).
6 |
7 | This repository is set up to work using a version of Dojo hosted on the Google
8 | CDN. If you'd prefer to use a local version of Dojo -- or if you're interested
9 | in trying out the build script at `util/build.sh` -- you'll need to run the
10 | setup script at `util/setup.sh` to download the full Dojo SDK. You'll also need
11 | to follow the instructions regarding commenting and uncommenting script files
12 | in `www/index.html`.
13 |
14 | # Useful resources
15 |
16 | * [Dojo Reference Guide](http://dojotoolkit.org/reference-guide/)
17 | * [Introduction to Custom Dojo
18 | Widgets](http://www.enterprisedojo.com/2010/09/21/introduction-to-custom-dojo-widgets/)
19 |
20 | # License
21 |
22 | The Dojo Boilerplate is licensed under the [same
23 | terms](http://bugs.dojotoolkit.org/browser/dojo/trunk/LICENSE) as the Dojo
24 | Toolkit.
25 |
--------------------------------------------------------------------------------
/profiles/app.js:
--------------------------------------------------------------------------------
1 | dependencies = {
2 | stripConsole : 'all',
3 | action : 'clean,release',
4 | optimize : 'shrinksafe',
5 | releaseName : 'js',
6 | localeList : 'en-us',
7 |
8 | layers: [
9 | {
10 | name: "../app/base.js",
11 | resourceName : "app.base",
12 | dependencies: [
13 | "app.base"
14 | ]
15 | }
16 | ],
17 |
18 | prefixes: [
19 | [ "dijit", "../dijit" ],
20 | [ "dojox", "../dojox" ],
21 | [ "app", "../../app" ],
22 | [ "dbp", "../../dbp" ]
23 | ]
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/util/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | DOJOVERSION="1.6.0"
6 |
7 | THISDIR=$(cd $(dirname $0) && pwd)
8 | SRCDIR="$THISDIR/../www"
9 | UTILDIR="$SRCDIR/js/dojo-release-${DOJOVERSION}-src/util/buildscripts"
10 | PROFILE="$THISDIR/../profiles/app.js"
11 | CSSDIR="$SRCDIR/css"
12 | DATADIR="$SRCDIR/data"
13 | IMGDIR="$SRCDIR/img"
14 | DISTDIR="$THISDIR/../dist"
15 |
16 | if [ ! -d "$UTILDIR" ]; then
17 | echo "Can't find Dojo build tools -- did you run ./util/setup.sh?"
18 | exit 1
19 | fi
20 |
21 | if [ ! -f "$PROFILE" ]; then
22 | echo "Invalid input profile"
23 | exit 1
24 | fi
25 |
26 | echo "Using $PROFILE. CSS, DATA and IMG will be copied and JS will be built."
27 |
28 | # clean the old distribution files
29 | rm -rf "$DISTDIR"
30 |
31 | # i know this sucks, but sane-er ways didn't seem to work ... :(
32 | cd "$UTILDIR"
33 | ./build.sh profileFile=../../../../../profiles/app.js releaseDir=../../../../../dist/
34 | cd "$THISDIR"
35 |
36 | # copy the css, data and img files
37 | # todo: how to do this better?
38 | cp -r "$CSSDIR" "$DISTDIR/css"
39 | cp -r "$DATADIR" "$DISTDIR/data"
40 | cp -r "$IMGDIR" "$DISTDIR/img"
41 |
42 | # copy the index.html and make it production-friendly
43 | cp "$SRCDIR/index.html" "$DISTDIR/index.html"
44 |
45 | sed -i -e "s/var _dbpDev = true;//" "$DISTDIR/index.html"
46 | sed -i -e "s/js\/dojo-release-1.6.0-src/js/" "$DISTDIR/index.html"
47 |
--------------------------------------------------------------------------------
/util/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | VERSION="1.6.1"
6 |
7 | THISDIR=$(cd $(dirname $0) && pwd)
8 | OUTDIR="$THISDIR/../www/js"
9 | DOJODIR="dojo-release-${VERSION}-src"
10 | OUTDIR=$(cd "$OUTDIR" &> /dev/null && pwd || echo "")
11 |
12 | if which wget >/dev/null; then
13 | GET="wget --no-check-certificate -O -"
14 | elif which curl >/dev/null; then
15 | GET="curl -L --insecure -o -"
16 | else
17 | echo "No cURL, no wget, no downloads :("
18 | exit 1
19 | fi
20 |
21 | if [ "$OUTDIR" = "" ]; then
22 | echo "Output directory not found"
23 | exit 1
24 | fi
25 |
26 | if [ ! -d "$OUTDIR/$DOJODIR" ]; then
27 | echo "Fetching Dojo $VERSION"
28 | $GET http://download.dojotoolkit.org/release-$VERSION/$DOJODIR.tar.gz | tar -C "$OUTDIR" -xzf -
29 | echo "Dojo extracted to $OUTDIR/$DOJODIR"
30 | fi
31 |
--------------------------------------------------------------------------------
/www/css/app.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color:#222;
3 | color:#f0f0f0
4 | }
5 |
6 | #container {
7 | width:800px;
8 | font-family: Helvetica, Arial, sans-serif;
9 | margin: 20px auto;
10 | overflow:hidden;
11 | }
12 |
13 | #people {
14 | background-color:#ff6600;
15 | -webkit-border-radius:10px;
16 | -moz-border-radius:10px;
17 | padding:15px;
18 | margin:0 0 40px 0;
19 | overflow:hidden;
20 | font-weight:bold;
21 | }
22 |
23 | #people li {
24 | float:left;
25 | margin:0 15px 0 0;
26 | cursor: pointer;
27 | }
28 |
29 | .person .info {
30 | display: -webkit-box;
31 | -webkit-box-orient: horizontal;
32 |
33 | display: -moz-box;
34 | -moz-box-orient: horizontal;
35 |
36 | display: box;
37 | box-orient: horizontal;
38 | }
39 |
40 | ul {
41 | margin: 0;
42 | padding:0;
43 | }
44 |
45 | ul li {
46 | list-style:none;
47 | margin:0 0 1.2em 0;
48 | }
49 |
50 | p {
51 | line-height: 1.2em;
52 | margin:0 0 0.5em 0;
53 | }
54 |
55 | .latest-tweet {
56 | color:#000;
57 | background-color: #f5c700;
58 | -webkit-border-radius: 10px;
59 | -moz-border-radius: 10px;
60 | padding:15px;
61 | margin:0 0 2em 0;
62 | }
63 |
64 | .latest-tweet li {
65 | margin:0;
66 | }
67 |
68 | .latest-tweet p.tweet {
69 | font-size: 150%;
70 | }
71 |
72 | .latest-tweet p.date {
73 | margin: 0;
74 | }
75 |
76 | p.date {
77 | font-size:80%;
78 | }
79 |
80 | a {
81 | color: #c12552;
82 | text-decoration:none;
83 | }
84 |
85 | a:hover {
86 | text-decoration:underline;
87 | }
88 |
89 | .twitter {
90 | -webkit-box-flex: 2;
91 | padding:0 50px 0 0;
92 | }
93 |
94 | .weather {
95 | -webkit-box-flex: 1;
96 | -webkit-border-radius: 10px;
97 | -moz-border-radius: 10px;
98 | padding:15px;
99 | background-color: #008885;
100 | }
101 |
102 | h3 {
103 | margin:0 0 0.5em 0;
104 | }
105 |
106 | .loading {
107 | background-image: url(../img/loading.gif);
108 | background-position: center;
109 | background-repeat: no-repeat;
110 | }
111 |
112 | h1 span {
113 | font-size: 50%;
114 | font-weight: normal;
115 | cursor: pointer;
116 | }
117 |
--------------------------------------------------------------------------------
/www/data/people.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id" : 1,
4 | "name" : "Alex Sexton",
5 | "twitter" : "SlexAxton",
6 | "location" : "Austin, TX",
7 | "img" : "img/alex.png"
8 | },
9 |
10 | {
11 | "id" : 2,
12 | "name" : "Paul Irish",
13 | "twitter" : "paul_irish",
14 | "location" : "San Francisco, CA",
15 | "img" : "img/paul.png"
16 | },
17 |
18 | {
19 | "id" : 3,
20 | "name" : "Adam Sontag",
21 | "twitter" : "ajpiano",
22 | "location" : "New York, NY",
23 | "img" : "img/adam.png"
24 | },
25 |
26 | {
27 | "id" : 4,
28 | "name" : "Rebecca Murphey",
29 | "twitter" : "rmurphey",
30 | "location" : "Durham, NC",
31 | "img" : "img/rebecca.png"
32 | }
33 | ]
34 |
--------------------------------------------------------------------------------
/www/img/adam.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rmurphey/dojo-demo/225eb4b5da7334f08f9d5add771c5dea6938e354/www/img/adam.png
--------------------------------------------------------------------------------
/www/img/alex.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rmurphey/dojo-demo/225eb4b5da7334f08f9d5add771c5dea6938e354/www/img/alex.png
--------------------------------------------------------------------------------
/www/img/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rmurphey/dojo-demo/225eb4b5da7334f08f9d5add771c5dea6938e354/www/img/loading.gif
--------------------------------------------------------------------------------
/www/img/paul.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rmurphey/dojo-demo/225eb4b5da7334f08f9d5add771c5dea6938e354/www/img/paul.png
--------------------------------------------------------------------------------
/www/img/rebecca.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rmurphey/dojo-demo/225eb4b5da7334f08f9d5add771c5dea6938e354/www/img/rebecca.png
--------------------------------------------------------------------------------
/www/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
30 |
31 |
32 |
55 |
56 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/www/js/app/base.js:
--------------------------------------------------------------------------------
1 | dojo.provide('app.base');
2 | /**
3 | * This file is your application's base JavaScript file;
4 | * it is loaded into the page by the dojo.require() call in
5 | * index.html. You can write code in this file, use it to
6 | * express dependencies on other files, or both. Generally,
7 | * this file should be used only for bootstrapping code;
8 | * actual functionality should be placed in other files inside
9 | * the www/js/app directory.
10 | */
11 |
12 | /**
13 | * You can specify dependencies on other files by adding
14 | * dojo.require() statements for them:
15 | *
16 | * dojo.require('dijit.Dialog');
17 | *
18 | * This works for your application's files, too:
19 | *
20 | * dojo.require('app.Foo');
21 | *
22 | * The above would look for a file located at
23 | * www/js/app/Foo.js; however, it's important to note
24 | * that this only works because we've specified a modulePath for
25 | * the 'app' namespace in index.html. If we do not specify a
26 | * modulePath for a namespace, dojo.require will assume that the
27 | * namespace corresponds to a directory that is a sibling of
28 | * the directory that contains dojo.js. The modulePath setting
29 | * in index.html overrides that default, providing a location
30 | * for the namespace relative to the location of dojo.js.
31 | *
32 | * Note also that any files you include via dojo.require()
33 | * MUST include a call to dojo.provide at the beginning;
34 | * the dojo.provide() function should be passed a string
35 | * that specifies how you expect the module to be referred
36 | * to in dojo.require() calls:
37 | *
38 | * dojo.provide('app.Foo');
39 | *
40 | * Finally, note that you do not need to express all of your
41 | * application's dependencies in this one file; individual files
42 | * can express their own dependencies as well.
43 | */
44 |
45 | dojo.require('dbp.Router');
46 | dojo.require('app.controllers.People');
47 | dojo.require('app.services.Favorites');
48 |
49 | /**
50 | * Any functionality that depends on the DOM being available
51 | * should be passed inside a function to dojo.ready. If you're
52 | * making a single-page app, this is your application controller.
53 | */
54 | dojo.ready(function() {
55 | var router = new dbp.Router([
56 | {
57 | path : "/user/:id",
58 | handler : function(params) {
59 | app.controllers.People.set('personId', params.id);
60 | }
61 | },
62 |
63 | {
64 | path : "/",
65 | handler : function() {},
66 | defaultRoute : true
67 | }
68 | ]);
69 |
70 | app.controllers.People.init().then(dojo.hitch(router, 'init'));
71 | });
72 |
--------------------------------------------------------------------------------
/www/js/app/controllers/People.js:
--------------------------------------------------------------------------------
1 | dojo.provide('app.controllers.People');
2 |
3 | dojo.require('app.views.Person');
4 | dojo.require('app.views.People');
5 | dojo.require('app.models.People');
6 | dojo.require('dojo.Stateful');
7 |
8 | (function() {
9 |
10 | var peopleData = app.models.People;
11 |
12 | app.controllers.People = new dojo.Stateful({
13 | init: function() {
14 | this.watch('personId', this._showPerson);
15 |
16 | // load the initial data for the page
17 | return dojo.when(peopleData.load(), function() {
18 | // create the view using the data and place it in
19 | // the element with id="people"
20 | this.peopleWidget = new app.views.People({
21 | // ask the people model for the list of people
22 | people: peopleData.getPeople()
23 | }, 'people');
24 | });
25 | },
26 |
27 | _showPerson : function(propName, oldVal, newVal) {
28 | if (oldVal === newVal) { return; }
29 |
30 | // destroy the old person widget if there is one
31 | if (this.personWidget) {
32 | this.personWidget.destroy();
33 | }
34 |
35 | var person = peopleData.getPerson(newVal);
36 |
37 | // set up the person widget, passing in the person model
38 | var personWidget = this.personWidget = new app.views.Person({
39 | // get an instance of the Person model for the requested person
40 | person: person
41 | }).placeAt('detail');
42 |
43 | // ask the model for the person's tweets, and pass them
44 | // to the widget once we have them
45 | person.getTweets().then(
46 | dojo.hitch(personWidget, 'set', 'tweets')
47 | );
48 |
49 | // ask the model for the person's weather, and pass it
50 | // to the widget once we have it
51 | person.getWeather().then(
52 | dojo.hitch(personWidget, 'set', 'weatherData')
53 | );
54 | }
55 | });
56 |
57 | }());
58 |
--------------------------------------------------------------------------------
/www/js/app/models/People.js:
--------------------------------------------------------------------------------
1 | dojo.provide('app.models.People');
2 |
3 | dojo.require('dojo.store.Memory');
4 | dojo.require('app.models.Person');
5 |
6 | (function() {
7 |
8 | var store;
9 |
10 | app.models.People = {
11 | load : function() {
12 | if (store) { return store.data; }
13 |
14 | return dojo.xhrGet({
15 | url : 'data/people.json',
16 | handleAs : 'json',
17 | load : function(data) {
18 | store = new dojo.store.Memory({ data : data });
19 | }
20 | });
21 | },
22 |
23 | getPerson : function(id) {
24 | return new app.models.Person(store.get(id));
25 | },
26 |
27 | getPeople : function() {
28 | return dojo.map(store.data, function(p) {
29 | return new app.models.Person(p);
30 | });
31 | }
32 | };
33 |
34 | }());
35 |
--------------------------------------------------------------------------------
/www/js/app/models/Person.js:
--------------------------------------------------------------------------------
1 | dojo.provide('app.models.Person');
2 |
3 | dojo.require('app.services.Twitter');
4 | dojo.require('app.services.YQL');
5 | dojo.require('app.services.Favorites');
6 |
7 | (function() {
8 |
9 | dojo.declare('app.models.Person', [], {
10 | constructor : function(data) {
11 | this.data = data;
12 | },
13 |
14 | getTweets : function() {
15 | return app.services.Twitter.tweets(this.data.twitter);
16 | },
17 |
18 | getWeather : function() {
19 | return app.services.YQL.weather(this.data.location);
20 | },
21 |
22 | isFavorite : function() {
23 | return !!app.services.Favorites.get(this.data.id);
24 | }
25 | });
26 |
27 | }());
28 |
--------------------------------------------------------------------------------
/www/js/app/services/Favorites.js:
--------------------------------------------------------------------------------
1 | dojo.provide('app.services.Favorites');
2 |
3 | dojo.require('dojo.store.Memory');
4 |
5 | (function() {
6 | var storage = (function() {
7 | try {
8 | return 'localStorage' in window && window['localStorage'] !== null;
9 | } catch (e) {
10 | return false;
11 | }
12 | }()),
13 |
14 | data = storage ? window.localStorage.getItem('favorites') : null;
15 |
16 | dojo.declare('app.services.Favorites', [ dojo.store.Memory ], {
17 |
18 | constructor : function() {
19 | this.inherited(arguments);
20 |
21 | dojo.subscribe('/favorites/add', dojo.hitch(this, 'put'));
22 | dojo.subscribe('/favorites/remove', this, function(person) {
23 | this.remove(person.id);
24 | });
25 | },
26 |
27 | put : function() {
28 | this.inherited(arguments);
29 | this._save();
30 | },
31 |
32 | remove : function() {
33 | this.inherited(arguments);
34 | this._save();
35 | },
36 |
37 | _save : function() {
38 | if (!storage) { return; }
39 | window.localStorage.setItem('favorites', JSON.stringify(this.data));
40 | }
41 |
42 | });
43 |
44 | // create an instance that overwrites the constructor
45 | app.services.Favorites = new app.services.Favorites({
46 | data : data ? JSON.parse(data) : []
47 | });
48 |
49 | }());
50 |
--------------------------------------------------------------------------------
/www/js/app/services/Twitter.js:
--------------------------------------------------------------------------------
1 | dojo.provide('app.services.Twitter');
2 |
3 | dojo.require('dojo.io.script');
4 | dojo.require('dojo.string');
5 |
6 | app.services.Twitter = {
7 | tweets : function(username) {
8 | var url = 'http://twitter.com/status/user_timeline/' + username + '.json',
9 | dfd = new dojo.Deferred();
10 |
11 | dojo.io.script.get({
12 | url : url,
13 | callbackParamName : 'callback',
14 | content : { count : 10, format : 'json' },
15 | load : dojo.hitch(this, function(data) {
16 | dfd.resolve(dojo.map(data, function(t) {
17 | return this._processTweet(t, username);
18 | }, this));
19 | })
20 | });
21 |
22 | return dfd.promise;
23 | },
24 |
25 | _processTweet : function(t, username) {
26 | return {
27 | text : t.text,
28 | date : t.created_at,
29 | url : [ 'http://twitter.com', username, 'status', t.id_str ].join('/')
30 | };
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/www/js/app/services/YQL.js:
--------------------------------------------------------------------------------
1 | dojo.provide('app.services.YQL');
2 |
3 | dojo.require('dojo.io.script');
4 |
5 | app.services.YQL = {
6 | _doQuery : function(config) {
7 | dojo.io.script.get({
8 | url : 'http://query.yahooapis.com/v1/public/yql',
9 | callbackParamName : 'callback',
10 | content : dojo.mixin({
11 | format : 'json'
12 | }, config.content),
13 | load : config.load
14 | });
15 | },
16 |
17 | weather : function(location) {
18 | var dfd = new dojo.Deferred();
19 |
20 | dojo.when(this.zip(location), dojo.hitch(this, function(zip) {
21 | this._doQuery({
22 | content : {
23 | q : 'select * from weather.forecast where location=' + zip
24 | },
25 | load : function(data) {
26 | dfd.resolve(data.query.results.channel.item);
27 | }
28 | });
29 | }));
30 |
31 | return dfd.promise;
32 | },
33 |
34 | zip : function(location) {
35 | var dfd = new dojo.Deferred();
36 |
37 | this._doQuery({
38 | content : {
39 | q : 'select * from geo.placefinder where text="' + location + '"'
40 | },
41 | load : function(data) {
42 | dfd.resolve(data.query.results.Result.uzip);
43 | }
44 | });
45 |
46 | return dfd.promise;
47 | }
48 | };
49 |
--------------------------------------------------------------------------------
/www/js/app/views/People.js:
--------------------------------------------------------------------------------
1 | dojo.provide('app.views.People');
2 |
3 | dojo.declare('app.views.People', [ dijit._Widget, dijit._Templated ], {
4 | templateString : dojo.cache('app.views', 'People/People.html'),
5 | personTemplate : dojo.cache('app.views', 'People/Person.html'),
6 |
7 | postCreate : function() {
8 | this.domNode.innerHTML = dojo.map(this.people, function(p) {
9 | return dojo.string.substitute(this.personTemplate, p.data);
10 | }, this).join('');
11 | }
12 | });
13 |
--------------------------------------------------------------------------------
/www/js/app/views/People/People.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/www/js/app/views/People/Person.html:
--------------------------------------------------------------------------------
1 | ${name}
2 |
--------------------------------------------------------------------------------
/www/js/app/views/Person.js:
--------------------------------------------------------------------------------
1 | dojo.provide('app.views.Person');
2 |
3 | dojo.require('dijit._Widget');
4 | dojo.require('dijit._Templated');
5 |
6 | dojo.declare('app.views.Person', [ dijit._Widget, dijit._Templated ], {
7 | templateString : dojo.cache('app.views', 'Person/Person.html'),
8 | tweetTemplate : dojo.cache('app.views', 'Person/Tweet.html'),
9 | weatherTemplate : dojo.cache('app.views', 'Person/Weather.html'),
10 |
11 | postMixInProperties : function() {
12 | this.isFavorite = this.person.isFavorite();
13 | this.favoriteText = this.isFavorite ? 'unfavorite' : 'favorite';
14 | },
15 |
16 | postCreate : function() {
17 | // templated widgets can refer to a specific element in their template by
18 | // adding a dojoAttachPoint attribute to the element. so, for example, if a
19 | // template has
20 | //
21 | //
22 | //
23 | // then inside the templated widget, we can refer to that element as
24 | //
25 | // this.textContainer
26 | //
27 | // this code takes advantage of attach points to add a "loading" class to
28 | // three elements in the template
29 |
30 | dojo.forEach([ 'latestTweet', 'olderTweets', 'weather' ], function(n) {
31 | dojo.addClass(this[n], 'loading'); }, this);
32 | this.connect(this.favorite, 'click', '_handleFavorite');
33 | },
34 |
35 | _handleFavorite : function() {
36 | dojo.publish('/favorites/' + (this.isFavorite ? 'remove' : 'add'), [ this.person.data ]);
37 | this.isFavorite = !this.isFavorite;
38 | this.favorite.innerHTML = this.isFavorite ? 'unfavorite' : 'favorite';
39 | },
40 |
41 | _setTweetsAttr : function(tweets) {
42 | // this method will be called when .set('tweets', someValue) is called on
43 | // an instance of app.views.Person. it receives someValue as its argument.
44 | // inside templated widgets, custom setter methods can be created for any
45 | // property by creating a method named _setAttr.
46 | //
47 | // similarly, custom getters can be created using methods named
48 | // _getAttr.
49 |
50 | dojo.removeClass(this.olderTweets, 'loading');
51 | dojo.removeClass(this.latestTweet, 'loading');
52 |
53 | var latest = tweets.shift();
54 |
55 | this.olderTweets.innerHTML = dojo.map(tweets, function(t) {
56 | return dojo.string.substitute(this.tweetTemplate, t);
57 | }, this).join('');
58 |
59 | this.latestTweet.innerHTML = dojo.string.substitute(
60 | this.tweetTemplate, latest
61 | );
62 | },
63 |
64 | _setWeatherDataAttr : function(weather) {
65 | // this method will be called when .set('weatherData', someValue) is called
66 | // on an instance of app.views.Person. it receives someValue as its argument.
67 |
68 | dojo.removeClass(this.weather, 'loading');
69 | this.weather.innerHTML = dojo.string.substitute(
70 | this.weatherTemplate, weather
71 | );
72 | }
73 | });
74 |
--------------------------------------------------------------------------------
/www/js/app/views/Person/Person.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ${person.data.name}
4 | ${favoriteText}
5 |
6 |
7 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/www/js/app/views/Person/Tweet.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ${date}
4 |
5 |
--------------------------------------------------------------------------------
/www/js/app/views/Person/Weather.html:
--------------------------------------------------------------------------------
1 |
2 |
${title}
3 |
${description}
4 |
5 |
--------------------------------------------------------------------------------
/www/js/dbp/Router.js:
--------------------------------------------------------------------------------
1 | /**
2 | * THIS FILE IS A WORK IN PROGRESS AND MOST LIKELY
3 | * DOES NOT WORK AT THE MOMENT. YOU HAVE BEEN WARNED.
4 | */
5 | dojo.provide("dbp.Router");
6 |
7 | dojo.require("dojo.hash");
8 |
9 | /**
10 | * dbp.Router provides an API for specifying hash-based URLs ("routes") and
11 | * the functionality associated with each. It allows the routes to include both
12 | * path and query string parameters which are then available inside the
13 | * handling function:
14 | *
15 | * /things/:id -> #/things/3
16 | * /things -> #/things?id=3&color=red
17 | *
18 | * Defining a route involves providing a path for the route, and a function to
19 | * run for the route. The function receives two arguments: an object containing
20 | * the parameters associated with the route, if any; and an object containing
21 | * information about the route that was run.
22 | *
23 | * Example usage
24 | *
25 | * var myRouter = new dbp.Router([
26 | * {
27 | * path : "/foo/:bar",
28 | * handler : function(params) { },
29 | * defaultRoute : true
30 | * }
31 | * ]);
32 | *
33 | * myRouter.init();
34 | *
35 | * This router was heavily inspired by Sammy.js, a full-featured router
36 | * for jQuery projects. http://sammyjs.org/
37 | */
38 |
39 | (function(d){
40 |
41 | var routes = [],
42 | routeCache = {},
43 | currentPath,
44 | connections = [],
45 | subscriptions = [],
46 |
47 | PATH_REPLACER = "([^\/]+)",
48 | PATH_NAME_MATCHER = /:([\w\d]+)/g,
49 |
50 | hasHistoryState = "onpopstate" in window;
51 |
52 | dojo.declare('dbp.Router', null, {
53 | constructor : function(userRoutes) {
54 | if (!userRoutes || !userRoutes.length) {
55 | throw "No routes provided to dbp.Router.";
56 | }
57 |
58 | if (routes.length) {
59 | console.warn("An instance of dbp.Router already exists. You may want to create another one, but it's unlikely.");
60 | }
61 |
62 |
63 | d.forEach(userRoutes, function(r) {
64 | this._registerRoute(r.path, r.handler, r.defaultRoute);
65 | }, this);
66 |
67 | // use the first route as the default if one
68 | // is not marked as the default
69 | if (!this.defaultRoute) {
70 | this.defaultRoute = userRoutes[0];
71 | }
72 |
73 | },
74 |
75 | /**
76 | * Initialization method; looks at current hash and handles,
77 | * else uses default route to get started
78 | */
79 | init : function() {
80 | this.go(window.location.hash || this.defaultRoute.path);
81 |
82 | if (hasHistoryState) {
83 | connections.push(d.connect(window, "onpopstate", this, function() {
84 | this._handle(window.location.hash);
85 | }));
86 | } else {
87 | subscriptions.push(d.subscribe("/dojo/hashchange", this, "_handle"));
88 | }
89 | },
90 |
91 | /**
92 | * Redirect to a path
93 | * @param {String} path
94 | */
95 | go : function(path) {
96 | path = dojo.trim(path);
97 | if (!path) { return; }
98 |
99 | this._handle(path);
100 |
101 | if (path.indexOf("#") !== 0) {
102 | path = "#" + path;
103 | }
104 |
105 | if (hasHistoryState) {
106 | history.pushState(null, null, path);
107 | } else {
108 | window.location.hash = path;
109 | }
110 | },
111 |
112 | /**
113 | * When the router observes navigation to a new hash, it passes
114 | * the hash to this function to be handled.
115 | *
116 | * @param {String} The hash to which the user navigated
117 | */
118 | _handle : function(hash) {
119 | if (hash === currentPath) { return; }
120 | currentPath = hash;
121 |
122 | var path = hash.replace("#",""),
123 |
124 | route = this._chooseRoute(this._getRouteablePath(path)) ||
125 | this.defaultRoute;
126 |
127 | params = this._parseParams(path, route);
128 |
129 | route = d.mixin(route, {
130 | hash : hash,
131 | params : params
132 | });
133 |
134 | route.handler(params, route);
135 | },
136 |
137 | /**
138 | * Find the route to use for a given path
139 | */
140 | _chooseRoute : function(path) {
141 | var routeablePath;
142 |
143 | if (!routeCache[path]) {
144 | routeablePath = this._getRouteablePath(path);
145 | d.forEach(routes, function(r) {
146 | if (routeablePath.match(r.matcher)) { routeCache[path] = r; }
147 | });
148 | }
149 |
150 | return routeCache[path];
151 | },
152 |
153 | /**
154 | * Register a route with the router. Generally only used internally,
155 | * but exposed for external use as well.
156 | *
157 | * @param {Regex|String} path The path pattern to which the route applies
158 | * @param {Function} fn The handler to use for the route
159 | * @param {Boolean} defaultRoute Whether the route should be used as
160 | * the default route.
161 | */
162 | _registerRoute : function(path, fn, defaultRoute) {
163 | var r = {
164 | path : path,
165 | handler : fn,
166 | matcher : this._convertPathToMatcher(path),
167 | paramNames : this._getParamNames(path)
168 | };
169 |
170 | routes.push(r);
171 |
172 | if (defaultRoute) { this.defaultRoute = r; }
173 | },
174 |
175 | /**
176 | * Given a path, which may be a regex or a string, return a regex
177 | * that can be used to determine whether to use the associated route
178 | * to process a given path.
179 | *
180 | * @private
181 | * @param {Regex|String} route
182 | * @returns Regex for determining whether a path matches the route
183 | * @type {Regex}
184 | */
185 | _convertPathToMatcher : function(route) {
186 | return d.isString(route) ?
187 | new RegExp("^" + route.replace(PATH_NAME_MATCHER, PATH_REPLACER) + "$") :
188 | route;
189 | },
190 |
191 | /**
192 | * Given a path to which a user navigated, and the route that we've
193 | * determined should handle the path, return an object containg the parameter
194 | * name(s) and value(s)
195 | *
196 | * @private
197 | * @param {String} hash The hash to which a user navigated
198 | * @param {Route} route The route
199 | * @returns A params object containing parameter keynames and values
200 | * @type {Object}
201 | */
202 | _parseParams : function(hash, route) {
203 | // TODO
204 | var parts = hash.split('?'),
205 | path = parts[0],
206 | query = parts[1],
207 | params,
208 | pathParams,
209 | _decode = decodeURIComponent;
210 |
211 | params = query ? d.mixin({}, d.queryToObject(query)) : {};
212 |
213 | if ((pathParams = route.matcher.exec(this._getRouteablePath(path))) !== null) {
214 | // first match is the full path
215 | pathParams.shift();
216 |
217 | // for each of the matches
218 | d.forEach(pathParams, function(param, i) {
219 | // if theres a matching param name
220 | if (route.paramNames[i]) {
221 | // set the name to the match
222 | params[route.paramNames[i]] = _decode(param);
223 | } else {
224 | // initialize 'splat'
225 | if (!params.splat) { params.splat = []; }
226 | params.splat.push(_decode(param));
227 | }
228 | });
229 | }
230 |
231 | return params;
232 | },
233 |
234 | /**
235 | * Given a path that may contain a query string:
236 | *
237 | * /foo/bar?baz=1
238 | *
239 | * Return a string that does not contain the query string, so we
240 | * can use the string for matching to a route.
241 | *
242 | * @private
243 | * @param {String} path
244 | */
245 | _getRouteablePath : function(path) {
246 | return path.split('?')[0];
247 | },
248 |
249 | /**
250 | * Given a route, which could be a string or a regex, return
251 | * the parameter names expected by the route as an array.
252 | *
253 | * @param {Regex|String} path The path specified for a route
254 | * @returns An array of parameter names
255 | * @type Array
256 | */
257 | _getParamNames : function(path) {
258 | var pathMatch,
259 | paramNames = [];
260 |
261 | PATH_NAME_MATCHER.lastIndex = 0;
262 |
263 | while ((pathMatch = PATH_NAME_MATCHER.exec(path)) !== null) {
264 | paramNames.push(pathMatch[1]);
265 | }
266 |
267 | return paramNames;
268 | },
269 |
270 | destroy : function() {
271 | dojo.forEach(connections, d.disconnect);
272 | dojo.forEach(subscriptions, d.unsubscribe);
273 | routes = [];
274 | }
275 | });
276 |
277 | }(dojo));
278 |
279 |
--------------------------------------------------------------------------------
/www/js/dbp/tests/Router.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dojo Unit Test Runner
5 |
6 |
7 |
8 | Redirecting to D.O.H runner.
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/www/js/dbp/tests/Router.js:
--------------------------------------------------------------------------------
1 | dojo.provide('dbp.tests.Router');
2 |
3 | dojo.require('dbp.Router');
4 |
5 | (function() {
6 |
7 | var testVal,
8 |
9 | routes = [
10 | {
11 | path : '/test',
12 | handler : function() {
13 | testVal = '/test';
14 | },
15 | defaultRoute : true
16 | },
17 | {
18 | path : '/test2',
19 | handler : function() {
20 | testVal = '/test2';
21 | }
22 | },
23 | {
24 | path : '/basic',
25 | handler : function() {
26 | testVal = 'basic';
27 | }
28 | },
29 | {
30 | path : '/bar/:baz',
31 | handler : function(params) {
32 | testVal = params.baz;
33 | }
34 | },
35 | {
36 | path : '/bar/:baz/:bim',
37 | handler : function(params) {
38 | testVal = params.baz + params.bim;
39 | }
40 | },
41 | {
42 | path : '/bar/:baz/bim/:bop',
43 | handler : function(params) {
44 | testVal = params.baz + params.bop;
45 | }
46 | },
47 | {
48 | path : /\/splat\/(.*)/,
49 | handler : function(params) {
50 | testVal = params.splat[0];
51 | }
52 | },
53 | {
54 | path : /\/splat\/(.*)\/foo\/(.*)/,
55 | handler : function(params) {
56 | testVal = params.splat[0] + ':' + params.splat[1];
57 | }
58 | }
59 | ];
60 |
61 | doh.register('dbp.Router', [
62 |
63 | {
64 | name : "It should handle a hash if one is set when initialized",
65 | setUp : function() {
66 | this.router = new dbp.Router(routes);
67 | },
68 | runTest : function() {
69 | window.location.hash = '#/test';
70 | this.router.init();
71 | doh.is(testVal, '/test');
72 | },
73 | tearDown : function() {
74 | this.router.destroy();
75 | }
76 | },
77 |
78 | {
79 | name : "It should route the request to the appropriate route with the appropriate parameters",
80 | setUp : function() {
81 | this.router = new dbp.Router(routes);
82 | },
83 | runTest : function() {
84 | this.router.go('/bar/test-123');
85 | doh.is(testVal, 'test-123');
86 |
87 | this.router.go('/basic');
88 | doh.is(testVal, 'basic');
89 |
90 | this.router.go('/bar/hello/world');
91 | doh.is(testVal, 'helloworld');
92 |
93 | this.router.go('/bar/testing/bim/123');
94 | doh.is(testVal, 'testing123');
95 |
96 | this.router.go('/splat/1/2/3');
97 | doh.is(testVal, '1/2/3');
98 |
99 | this.router.go('/splat/1/2/3/foo/4/5/6');
100 | doh.is(testVal, '1/2/3:4/5/6');
101 | },
102 | tearDown : function() {
103 | this.router.destroy();
104 | }
105 | }
106 |
107 | ]);
108 |
109 | }());
110 |
--------------------------------------------------------------------------------