├── .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 |
11 |
12 |
13 |
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 |

    ${person.data.name} 3 | ${person.data.name} 4 | ${favoriteText} 5 |

    6 | 7 |
      8 |
      9 | 10 |
      11 |
      12 |
      13 | -------------------------------------------------------------------------------- /www/js/app/views/Person/Tweet.html: -------------------------------------------------------------------------------- 1 |
    • 2 |

      ${text}

      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 | --------------------------------------------------------------------------------