Added remainder of the cookbook

This commit is contained in:
Misko Hevery 2011-02-03 15:21:34 -08:00
parent 0d4def68ae
commit 882f412d57
13 changed files with 180 additions and 74 deletions

View file

@ -3,43 +3,59 @@
@name Cookbook: Deep Linking
@description
Deep linking allows you to encode the state of the application in the URL so that it can be bookmarked and the application can be restored from the URL to the same state.
Deep linking allows you to encode the state of the application in the URL so that it can be
bookmarked and the application can be restored from the URL to the same state.
While <angular/> does not force you to deal with bookmarks in any particular way, it has services which make the common case described here very easy to implement.
While <angular/> does not force you to deal with bookmarks in any particular way, it has services
which make the common case described here very easy to implement.
[edit] Assumptions
# Assumptions
Your application consists of a single HTML page which bootstraps the application. We will refer to this page as the chrome.
Your application is divided into several screens (or views) which the user can visit. For example, the home screen, settings screen, details screen, etc. For each of these screens, we would like to assign a URL so that it can be bookmarked and later restored. Each of these screens will be associated with a controller which define the screen's behavior. The most common case is that the screen will be constructed from an HTML snippet, which we will refer to as the partial. Screens can have multiple partials, but a single partial is the most common construct. This example makes the partial boundary visible using a blue line.
You can make a routing table which shows which URL maps to which partial view template and which controller.
[edit] Example
Your application consists of a single HTML page which bootstraps the application. We will refer
to this page as the chrome.
Your application is divided into several screens (or views) which the user can visit. For example,
the home screen, settings screen, details screen, etc. For each of these screens, we would like to
assign a URL so that it can be bookmarked and later restored. Each of these screens will be
associated with a controller which define the screen's behavior. The most common case is that the
screen will be constructed from an HTML snippet, which we will refer to as the partial. Screens can
have multiple partials, but a single partial is the most common construct. This example makes the
partial boundary visible using a blue line.
You can make a routing table which shows which URL maps to which partial view template and which
controller.
# Example
In this example we have a simple app which consist of two screens:
Welcome: url # Show the user contact information.
Settings: url #/settings Show an edit screen for user contact information.
* Welcome: url `#` Show the user contact information.
* Settings: url `#/settings` Show an edit screen for user contact information.
The two partials are defined in the following URLs:
http://angularjs.org/cb/settings.html
http://angularjs.org/cb/welcome.html
* {@link ./static/settings.html}
* {@link ./static/welcome.html}
<doc:example>
<doc:source>
<script>
angular.service('myApplication', function($route){
AppCntl.$inject = ['$route']
function AppCntl($route) {
// define routes
$route.when("", {template:'/cb/welcome.html', controller:WelcomeCntl});
$route.when("/settings", {template:'/cb/settings.html', controller:SettingsCntl});
$route.when("", {template:'./static/welcome.html', controller:WelcomeCntl});
$route.when("/settings", {template:'./static/settings.html', controller:SettingsCntl});
$route.parent(this);
// initialize the model to something useful
this.person = {
name:'anonymous',
contacts:[{type:'email', url:'anonymous@example.com'}]
};
}, {inject:['$route']});
}
function WelcomeCntl(){}
function WelcomeCntl($route){}
WelcomeCntl.prototype = {
greet: function(){
alert("Hello " + this.person.name);
@ -60,25 +76,39 @@ http://angularjs.org/cb/welcome.html
}
};
</script>
<h1>Your App Chrome</h1>
[ <a href="#">Welcome</a> | <a href="#/settings">Settings</a> ]
<hr/>
<span style="background-color: blue; color: white; padding: 3px;">
Partial: {{$route.current.template}}
</span>
<div style="border: 1px solid blue; margin: 0;">
<ng:include src="$route.current.template" scope="$route.current.scope"></ng:include>
<div ng:controller="AppCntl">
<h1>Your App Chrome</h1>
[ <a href="#">Welcome</a> | <a href="#/settings">Settings</a> ]
<hr/>
<span style="background-color: blue; color: white; padding: 3px;">
Partial: {{$route.current.template}}
</span>
<ng:view style="border: 1px solid blue; margin: 0; display:block; padding:1em;"></ng:view>
<small>Your app footer </small>
</div>
<small>Your app footer </small>
</doc:source>
<doc:scenario>
it('should navigate to URL', function(){
element('a:contains(Welcome)').click();
expect(element('ng\\:view').text()).toMatch(/Hello anonymous/);
element('a:contains(Settings)').click();
input('form.name').enter('yourname');
element(':button:contains(Save)').click();
element('a:contains(Welcome)').click();
expect(element('ng\\:view').text()).toMatch(/Hello yourname/);
});
</doc:scenario>
</doc:example>
Things to notice
# Things to notice
Routes are defined in the myApplication service. The service is initialized on application startup. Initialization of the services causes the initialization of the $route service with the proper URL routes. The $route service then watches the URL and instantiates the appropriate controller when the URL changes.
The ng:include widget loads the partial when the URL changes. It also sets the partial scope to the newly instantiated controller.
Changing the URL is sufficient to change the controller and screen/view/partial. It makes no difference whether the URL is changed programatically or by the user.
* Routes are defined in the `AppCntl` class. The initialization of the controller causes the
initialization of the {@link angular.service.$rouet $route} service with the proper URL routes.
* The {@link angular.service.$route $route} service then watches the URL and instantiates the
appropriate controller when the URL changes.
* The {@link angular.widget.ng:view ng:view} widget loads the view when the URL changes. It also
sets the view scope to the newly instantiated controller.
* Changing the URL is sufficient to change the controller and view. It makes no difference whether
the URL is changed programatically or by the user.

View file

@ -66,18 +66,24 @@ allow a user to enter data.
expect(binding('user')).not().toMatch(/\(234\) 555\-1212/);
});
iit('should validate zip', function(){
var form = using('.example');
expect(form.element(':input[name=user.address.zip]').attr('className'))
it('should validate zip', function(){
expect(using('.example').element(':input[name=user.address.zip]').attr('className'))
.not().toMatch(/ng-validation-error/)
form.input('user.address.zip').enter('abc');
using('.example').input('user.address.zip').enter('abc');
expect(form.element(':input[name=user.address.zip]').attr('className'))
expect(using('.example').element(':input[name=user.address.zip]').attr('className'))
.toMatch(/ng-validation-error/)
});
iit('should validate state', function(){
it('should validate state', function(){
expect(using('.example').element(':input[name=user.address.state]').attr('className'))
.not().toMatch(/ng-validation-error/)
using('.example').input('user.address.state').enter('XXX');
expect(using('.example').element(':input[name=user.address.state]').attr('className'))
.toMatch(/ng-validation-error/)
});
</doc:scenario>
</doc:example>

View file

@ -3,12 +3,15 @@
@name Cookbook: Advanced Form
@description
Here we extend the basic form example to include common features such as reverting, dirty state detection, and preventing invalid form submission.
Here we extend the basic form example to include common features such as reverting, dirty state
detection, and preventing invalid form submission.
<doc:example>
<doc:source>
<script>
function UserForm(){
this.state = /^\w\w$/;
this.zip = /^\d\d\d\d\d$/;
this.master = {
name: 'John Smith',
address:{
@ -32,7 +35,6 @@ Here we extend the basic form example to include common features such as reverti
save: function(){
this.master = this.form;
this.cancel();
alert('SAVED: ' + angular.toJson(this.master));
}
};
</script>
@ -44,8 +46,8 @@ Here we extend the basic form example to include common features such as reverti
<label>Address:</label><br/>
<input type="text" name="form.address.line1" size="33" ng:required/> <br/>
<input type="text" name="form.address.city" size="12" ng:required/>,
<input type="text" name="form.address.state" size="2" ng:required ng:validate="regexp:/^\w\w$/"/>
<input type="text" name="form.address.zip" size="5" ng:required ng:validate="regexp:/^\d\d\d\d\d$/"/><br/><br/>
<input type="text" name="form.address.state" size="2" ng:required ng:validate="regexp:state"/>
<input type="text" name="form.address.zip" size="5" ng:required ng:validate="regexp:zip"/><br/><br/>
<label>Phone:</label>
[ <a href="" ng:click="form.contacts.$add()">add</a> ]
@ -69,14 +71,31 @@ Here we extend the basic form example to include common features such as reverti
</div>
</doc:source>
<doc:scenario>
it('should enable save button', function(){
expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy();
input('form.name').enter('change');
expect(element(':button:contains(Save)').attr('disabled')).toBeFalsy();
element(':button:contains(Save)').click();
expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy();
});
it('should enable cancel button', function(){
expect(element(':button:contains(Cancel)').attr('disabled')).toBeTruthy();
input('form.name').enter('change');
expect(element(':button:contains(Cancel)').attr('disabled')).toBeFalsy();
element(':button:contains(Cancel)').click();
expect(element(':button:contains(Cancel)').attr('disabled')).toBeTruthy();
expect(element(':input[name=form.name]').val()).toEqual('John Smith');
});
</doc:scenario>
</doc:example>
Things to notice
#Things to notice
Cancel & save buttons are only enabled if the form is dirty -- there is something to cancel or save.
Save button is only enabled if there are no validation errors on the form.
Cancel reverts the form changes back to original state.
Save updates the internal model of the form.
Debug view shows the two models. One presented to the user form and the other being the pristine copy master.
* Cancel & save buttons are only enabled if the form is dirty -- there is something to cancel or
save.
* Save button is only enabled if there are no validation errors on the form.
* Cancel reverts the form changes back to original state.
* Save updates the internal model of the form.
* Debug view shows the two models. One presented to the user form and the other being the pristine
copy master.

View file

@ -10,7 +10,7 @@
Hello {{name}}!
</doc:source>
<doc:scenario>
iit('should change the binding when user enters text', function(){
it('should change the binding when user enters text', function(){
expect(binding('name')).toEqual('World');
input('name').enter('angular');
expect(binding('name')).toEqual('angular');

View file

@ -3,9 +3,13 @@
@name Cookbook: MVC
@description
MVC allows for a clean an testable separation between the behavior (controller) and the view (HTML template). A Controller is just a JavaScript class which is grafted onto the scope of the view. This makes it very easy for the controller and the view to share the model.
MVC allows for a clean an testable separation between the behavior (controller) and the view
(HTML template). A Controller is just a JavaScript class which is grafted onto the scope of the
view. This makes it very easy for the controller and the view to share the model.
The model is simply the controller's this. This makes it very easy to test the controller in isolation since one can simply instantiate the controller and test without a view, because there is no connection between the controller and the view.
The model is simply the controller's this. This makes it very easy to test the controller in
isolation since one can simply instantiate the controller and test without a view, because there is
no connection between the controller and the view.
<doc:example>
@ -21,7 +25,7 @@ The model is simply the controller's this. This makes it very easy to test the c
'cursor': 'pointer'
};
this.reset();
this.$watch('$location.hashPath', this.readUrl);
this.$watch('$location.hashSearch.board', this.readUrl);
}
TicTacToeCntl.prototype = {
dropPiece: function(row, col) {
@ -54,16 +58,16 @@ The model is simply the controller's this. This makes it very easy to test the c
},
setUrl: function(){
var rows = [];
angular.foreach(this.board, function(row){
angular.forEach(this.board, function(row){
rows.push(row.join(','));
});
this.$location.hashPath = rows.join(';') + '/' + this.nextMove;
this.$location.hashSearch.board = rows.join(';') + '/' + this.nextMove;
},
readUrl: function(value) {
if (value) {
value = value.split('/');
this.nextMove = value[1];
angular.foreach(value[0].split(';'), function(row, i){
angular.forEach(value[0].split(';'), function(row, i){
this.board[i] = row.split(',');
}, this);
this.grade();
@ -76,8 +80,8 @@ The model is simply the controller's this. This makes it very easy to test the c
<h3>Tic-Tac-Toe</h3>
<div ng:controller="TicTacToeCntl">
Next Player: {{nextMove}}
<div ng:show="winner">Player {{winner}} has won!</div>
<table>
<div class="winner" ng:show="winner">Player {{winner}} has won!</div>
<table class="board">
<tr ng:repeat="row in board" style="height:15px;">
<td ng:repeat="cell in row" ng:style="cellStyle"
ng:click="dropPiece($parent.$index, $index)">{{cell}}</td>
@ -87,17 +91,35 @@ The model is simply the controller's this. This makes it very easy to test the c
</div>
</doc:source>
<doc:scenario>
it('should play a game', function(){
piece(1, 1);
expect(binding('nextMove')).toEqual('O');
piece(3, 1);
expect(binding('nextMove')).toEqual('X');
piece(1, 2);
piece(3, 2);
piece(1, 3);
expect(element('.winner').text()).toEqual('Player X has won!');
});
function piece(row, col) {
element('.board tr:nth-child('+row+') td:nth-child('+col+')').click();
}
</doc:scenario>
</doc:example>
Things to notice
# Things to notice
The controller is defined in JavaScript and has no reference to the rendering logic.
The controller is instantiated by <angular/> and injected into the view.
The controller can be instantiated in isolation (without a view) and the code will still execute. This makes it very testable.
The HTML view is a projection of the model. In the above example, the model is stored in the board variable.
All of the controller's properties (such as board and nextMove) are available to the view.
Changing the model changes the view.
The view can call any controller function.
In this example, the setUrl() and readUrl() functions copy the game state to/from the URL's hash so the browser's back button will undo game steps. See deep-linking. This example calls $watch() to set up a listener that invokes readUrl() when needed.
* The controller is defined in JavaScript and has no reference to the rendering logic.
* The controller is instantiated by <angular/> and injected into the view.
* The controller can be instantiated in isolation (without a view) and the code will still execute.
This makes it very testable.
* The HTML view is a projection of the model. In the above example, the model is stored in the
board variable.
* All of the controller's properties (such as board and nextMove) are available to the view.
* Changing the model changes the view.
* The view can call any controller function.
* In this example, the `setUrl()` and `readUrl()` functions copy the game state to/from the URL's
hash so the browser's back button will undo game steps. See deep-linking. This example calls
{@link angular.Scope.$watch $watch()} to set up a listener that invokes `readUrl()` when needed.

View file

@ -296,6 +296,7 @@ describe('ngdoc', function(){
'foo {@link angular.foo}\n\n da {@link angular.foo bar foo bar } \n\n' +
'dad{@link angular.foo}\n\n' +
'external{@link http://angularjs.org}\n\n' +
'external{@link ./static.html}\n\n' +
'{@link angular.directive.ng:foo ng:foo}');
doc.parse();
expect(doc.description).
@ -308,6 +309,8 @@ describe('ngdoc', function(){
toContain('<a href="#!angular.directive.ng:foo"><code>ng:foo</code></a>');
expect(doc.description).
toContain('<a href="http://angularjs.org">http://angularjs.org</a>');
expect(doc.description).
toContain('<a href="./static.html">./static.html</a>');
});
});

View file

@ -24,7 +24,8 @@ var writes = callback.chain(function(){
});
var metadata = ngdoc.metadata(docs);
writer.output('docs-keywords.js', ['NG_PAGES=', JSON.stringify(metadata).replace(/{/g, '\n{'), ';'], writes.waitFor());
writer.copyImages(writes.waitFor());
writer.copyDir('img', writes.waitFor());
writer.copyDir('static', writes.waitFor());
writer.copy('index.html', writes.waitFor());
writer.copy('docs.js', writes.waitFor());
writer.copy('docs.css', writes.waitFor());

View file

@ -59,7 +59,7 @@ Doc.prototype = {
markdown: function (text) {
var self = this;
var IS_URL = /^(https?:\/\/|ftps?:\/\/|mailto:)/;
var IS_URL = /^(https?:\/\/|ftps?:\/\/|mailto:|\.|\/)/;
var IS_ANGULAR = /^angular\./;
if (!text) return text;
var parts = text.split(/(<pre>[\s\S]*?<\/pre>|<doc:example>[\s\S]*?<\/doc:example>)/),

View file

@ -1,11 +1,15 @@
var HAS_HASH = /#/;
DocsController.$inject = ['$location', '$browser', '$window'];
function DocsController($location, $browser, $window) {
this.pages = NG_PAGES;
window.$root = this.$root;
this.$location = $location;
this.$watch('$location.hashPath', function(hashPath){
hashPath = hashPath || '!angular';
if (!HAS_HASH.test($location.href)) {
$location.hashPath = '!angular';
}
this.$watch('$location.hashPath', function(hashPath) {
if (hashPath.match(/^!/)) {
this.partialId = hashPath.substring(1);
this.partialTitle = (angular.Array.filter(NG_PAGES, {id:this.partialId})[0]||{}).name;

View file

@ -61,14 +61,12 @@ function copy(from, to, callback) {
});
}
exports.copyImages = function(callback) {
exports.makeDir(OUTPUT_DIR + '/img', callback.waitFor(function(){
fs.readdir('docs/img', callback.waitFor(function(err, files){
exports.copyDir = function(dir, callback) {
exports.makeDir(OUTPUT_DIR + '/' + dir, callback.waitFor(function(){
fs.readdir('docs/' + dir, callback.waitFor(function(err, files){
if (err) return this.error(err);
files.forEach(function(file){
if (file.match(/\.(png|gif|jpg|jpeg)$/)) {
copy('docs/img/' + file, OUTPUT_DIR + '/img/' + file, callback.waitFor());
}
copy('docs/' + dir + '/' + file, OUTPUT_DIR + '/' + dir + '/' + file, callback.waitFor());
});
callback();
}));

18
docs/static/settings.html vendored Normal file
View file

@ -0,0 +1,18 @@
<label>Name:</label>
<input type="text" name="form.name" ng:required>
<div ng:repeat="contact in form.contacts">
<select name="contact.type">
<option>url</option>
<option>email</option>
<option>phone</option>
</select>
<input type="text" name="contact.url">
[ <a href="" ng:click="form.contacts.$remove(contact)">X</a> ]
</div>
<div>
[ <a href="" ng:click="form.contacts.$add()">add</a> ]
</div>
<button ng:click="cancel()">Cancel</button>
<button ng:click="save()">Save</button>

5
docs/static/welcome.html vendored Normal file
View file

@ -0,0 +1,5 @@
Hello {{person.name}},
<div>
Your contact information:
<div ng:repeat="contact in person.contacts">{{contact.type}}: {{contact.url|linky}}</div>
</div>

View file

@ -235,7 +235,7 @@ function errorHandlerFor(element, error) {
expect(using('.doc-example-live').repeater('li').row(1)).
toEqual(['1', 'Hello', 'Earth']);
expect(using('.doc-example-live').element('pre').text()).
toBe('$index=\nsalutation=Hello\nname=Misko');
toBe(' $index=\n salutation=Hello\n name=Misko');
});
</doc:scenario>
</doc:example>