diff options
| author | Misko Hevery | 2011-04-29 15:18:27 -0700 |
|---|---|---|
| committer | Igor Minar | 2011-06-06 22:28:38 -0700 |
| commit | 11e9572b952e49b01035e956c412d6095533031a (patch) | |
| tree | 04dbf96802f552693d44c541c0d825a2769e3d57 /docs/content/cookbook | |
| parent | b6bc6c2ddf1ae1523ec7e4cb92db209cd6501181 (diff) | |
| download | angular.js-11e9572b952e49b01035e956c412d6095533031a.tar.bz2 | |
Move documentation under individual headings
Diffstat (limited to 'docs/content/cookbook')
| -rw-r--r-- | docs/content/cookbook/buzz.ngdoc | 63 | ||||
| -rw-r--r-- | docs/content/cookbook/deeplinking.ngdoc | 114 | ||||
| -rw-r--r-- | docs/content/cookbook/form.ngdoc | 103 | ||||
| -rw-r--r-- | docs/content/cookbook/formadvanced.ngdoc | 105 | ||||
| -rw-r--r-- | docs/content/cookbook/helloworld.ngdoc | 31 | ||||
| -rw-r--r-- | docs/content/cookbook/index.ngdoc | 60 | ||||
| -rw-r--r-- | docs/content/cookbook/mvc.ngdoc | 125 |
7 files changed, 601 insertions, 0 deletions
diff --git a/docs/content/cookbook/buzz.ngdoc b/docs/content/cookbook/buzz.ngdoc new file mode 100644 index 00000000..2e82b2d1 --- /dev/null +++ b/docs/content/cookbook/buzz.ngdoc @@ -0,0 +1,63 @@ +@workInProgress +@ngdoc overview +@name Cookbook: Resources - Buzz +@description + +External resources are URLs that provide JSON data, which are then rendered with the help of +templates. angular has a resource factory that can be used to give names to the URLs and then +attach behavior to them. For example you can use the +{@link http://code.google.com/apis/buzz/v1/getting_started.html#background-operations| Google Buzz API} +to retrieve Buzz activity and comments. + +<doc:example> + <doc:source> + <script> + BuzzController.$inject = ['$resource']; + function BuzzController($resource){ + this.Activity = $resource( + 'https://www.googleapis.com/buzz/v1/activities/:userId/:visibility/:activityId/:comments', + {alt:'json', callback:'JSON_CALLBACK'}, + { get: {method:'JSON', params:{visibility:'@self'}}, + replies: {method:'JSON', params:{visibility:'@self', comments:'@comments'}} + }); + } + BuzzController.prototype = { + fetch: function(){ + this.activities = this.Activity.get({userId:this.userId}); + }, + expandReplies: function(activity) { + activity.replies = + this.Activity.replies({userId:this.userId, activityId:activity.id}); + } + }; + </script> + <div ng:controller="BuzzController"> + <input name="userId" value="googlebuzz"/> + <button ng:click="fetch()">fetch</button> + <hr/> + <div class="buzz" ng:repeat="item in activities.data.items"> + <h1 style="font-size: 15px;"> + <img src="{{item.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/> + <a href="{{item.actor.profileUrl}}">{{item.actor.name}}</a> + <a href="" ng:click="expandReplies(item)" style="float: right;"> + Expand replies: {{item.links.replies[0].count}} + </a> + </h1> + {{item.object.content | html}} + <div class="reply" ng:repeat="reply in item.replies.data.items" style="margin-left: 20px;"> + <img src="{{reply.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/> + <a href="{{reply.actor.profileUrl}}">{{reply.actor.name}}</a>: + {{reply.content | html}} + </div> + </div> + </div> + </doc:source> + <doc:scenario> + it('fetch buzz and expand', function(){ + element(':button:contains(fetch)').click(); + expect(repeater('div.buzz').count()).toBeGreaterThan(0); + element('.buzz a:contains(Expand replies):first').click(); + expect(repeater('div.reply').count()).toBeGreaterThan(0); + }); + </doc:scenario> +</doc:example> diff --git a/docs/content/cookbook/deeplinking.ngdoc b/docs/content/cookbook/deeplinking.ngdoc new file mode 100644 index 00000000..7d69ee84 --- /dev/null +++ b/docs/content/cookbook/deeplinking.ngdoc @@ -0,0 +1,114 @@ +@workInProgress +@ngdoc overview +@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. + +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. + +# 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. + +# 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. + + +The two partials are defined in the following URLs: + +* {@link ./examples/settings.html} +* {@link ./examples/welcome.html} + + +<doc:example> + <doc:source> + <script> + AppCntl.$inject = ['$route'] + function AppCntl($route) { + // define routes + $route.when("", {template:'./examples/welcome.html', controller:WelcomeCntl}); + $route.when("/settings", {template:'./examples/settings.html', controller:SettingsCntl}); + $route.parent(this); + + // initialize the model to something useful + this.person = { + name:'anonymous', + contacts:[{type:'email', url:'anonymous@example.com'}] + }; + } + + function WelcomeCntl($route){} + WelcomeCntl.prototype = { + greet: function(){ + alert("Hello " + this.person.name); + } + }; + + function SettingsCntl(){ + this.cancel(); + } + SettingsCntl.prototype = { + cancel: function(){ + this.form = angular.copy(this.person); + }, + + save: function(){ + angular.copy(this.form, this.person); + window.location.hash = "#"; + } + }; + </script> + <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> + </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 + +* Routes are defined in the `AppCntl` class. The initialization of the controller causes the + initialization of the {@link angular.service.$route $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. diff --git a/docs/content/cookbook/form.ngdoc b/docs/content/cookbook/form.ngdoc new file mode 100644 index 00000000..c9fd9e9a --- /dev/null +++ b/docs/content/cookbook/form.ngdoc @@ -0,0 +1,103 @@ +@workInProgress +@ngdoc overview +@name Cookbook: Form +@description + +A web application's main purpose is to present and gather data. For this reason angular strives +to make both of these operations trivial. This example shows off how you can build a simple form to +allow a user to enter data. + + +<doc:example> + <doc:source> + <script> + function FormController(){ + this.user = { + name: 'John Smith', + address:{line1: '123 Main St.', city:'Anytown', state:'AA', zip:'12345'}, + contacts:[{type:'phone', value:'1(234) 555-1212'}] + }; + this.state = /^\w\w$/; + this.zip = /^\d\d\d\d\d$/; + } + </script> + <div ng:controller="FormController" class="example"> + + <label>Name:</label><br/> + <input type="text" name="user.name" ng:required/> <br/><br/> + + <label>Address:</label><br/> + <input type="text" name="user.address.line1" size="33" ng:required/> <br/> + <input type="text" name="user.address.city" size="12" ng:required/>, + <input type="text" name="user.address.state" size="2" ng:required ng:validate="regexp:state"/> + <input type="text" name="user.address.zip" size="5" ng:required ng:validate="regexp:zip"/><br/><br/> + + <label>Phone:</label> + [ <a href="" ng:click="user.contacts.$add()">add</a> ] + <div ng:repeat="contact in user.contacts"> + <select name="contact.type"> + <option>email</option> + <option>phone</option> + <option>pager</option> + <option>IM</option> + </select> + <input type="text" name="contact.value" ng:required/> + [ <a href="" ng:click="user.contacts.$remove(contact)">X</a> ] + </div> + <hr/> + Debug View: + <pre>user={{user}}</pre> + </div> + + </doc:source> + <doc:scenario> + it('should show debug', function(){ + expect(binding('user')).toMatch(/John Smith/); + }); + it('should add contact', function(){ + using('.example').element('a:contains(add)').click(); + using('.example div:last').input('contact.value').enter('you@example.org'); + expect(binding('user')).toMatch(/\(234\) 555\-1212/); + expect(binding('user')).toMatch(/you@example.org/); + }); + + it('should remove contact', function(){ + using('.example').element('a:contains(X)').click(); + expect(binding('user')).not().toMatch(/\(234\) 555\-1212/); + }); + + it('should validate zip', function(){ + expect(using('.example').element(':input[name=user.address.zip]').attr('className')) + .not().toMatch(/ng-validation-error/); + + using('.example').input('user.address.zip').enter('abc'); + + expect(using('.example').element(':input[name=user.address.zip]').attr('className')) + .toMatch(/ng-validation-error/); + }); + + 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> + + +# Things to notice + +* The user data model is initialized {@link angular.ng:controller controller} and is available in + the {@link angular.scope scope} with the initial data. +* For debugging purposes we have included a debug view of the model to better understand what + is going on. +* The {@link angular.widget.HTML input widgets} simply refer to the model and are auto bound. +* The inputs {@link angular.validator validate}. (Try leaving them blank or entering non digits + in the zip field) +* In your application you can simply read from or write to the model and the form will be updated. +* By clicking the 'add' link you are adding new items into the `user.contacts` array which are then + reflected in the view. diff --git a/docs/content/cookbook/formadvanced.ngdoc b/docs/content/cookbook/formadvanced.ngdoc new file mode 100644 index 00000000..181dd5e9 --- /dev/null +++ b/docs/content/cookbook/formadvanced.ngdoc @@ -0,0 +1,105 @@ +@workInProgress +@ngdoc overview +@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. + +<doc:example> + <doc:source> + <script> + UserForm.$inject = ['$invalidWidgets']; + function UserForm($invalidWidgets){ + this.$invalidWidgets = $invalidWidgets; + this.state = /^\w\w$/; + this.zip = /^\d\d\d\d\d$/; + this.master = { + name: 'John Smith', + address:{ + line1: '123 Main St.', + city:'Anytown', + state:'AA', + zip:'12345' + }, + contacts:[ + {type:'phone', value:'1(234) 555-1212'} + ] + }; + this.cancel(); + } + + UserForm.prototype = { + cancel: function(){ + this.form = angular.copy(this.master); + }, + + save: function(){ + this.master = this.form; + this.cancel(); + } + }; + </script> + <div ng:controller="UserForm"> + + <label>Name:</label><br/> + <input type="text" name="form.name" ng:required/> <br/><br/> + + <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: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> ] + <div ng:repeat="contact in form.contacts"> + <select name="contact.type"> + <option>email</option> + <option>phone</option> + <option>pager</option> + <option>IM</option> + </select> + <input type="text" name="contact.value" ng:required/> + [ <a href="" ng:click="form.contacts.$remove(contact)">X</a> ] + </div> + <button ng:click="cancel()" disabled="{{master.$equals(form)}}">Cancel</button> + <button ng:click="save()" disabled="{{$invalidWidgets.visible() || master.$equals(form)}}">Save</button> + + <hr/> + Debug View: + <pre>form={{form}} + master={{master}}</pre> + </div> + </doc:source> + <doc:scenario> + it('should enable save button', function(){ + expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy(); + input('form.name').enter(''); + 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 + +* 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. diff --git a/docs/content/cookbook/helloworld.ngdoc b/docs/content/cookbook/helloworld.ngdoc new file mode 100644 index 00000000..ab4c337a --- /dev/null +++ b/docs/content/cookbook/helloworld.ngdoc @@ -0,0 +1,31 @@ +@workInProgress +@ngdoc overview +@name Cookbook: Hello World +@description + +<doc:example> + <doc:source> + Your name: <input type="text" name="name" value="World"/> + <hr/> + Hello {{name}}! + </doc:source> + <doc:scenario> + it('should change the binding when user enters text', function(){ + expect(binding('name')).toEqual('World'); + input('name').enter('angular'); + expect(binding('name')).toEqual('angular'); + }); + </doc:scenario> +</doc:example> + +# Things to notice + +Take a look through the source and note: + +* The script tag that {@link guide.bootstrap bootstraps} the angular environment. +* The text {@link angular.widget.HTML input widget} which is bound to the greeting name text. +* No need for listener registration and event firing on change events. +* The implicit presence of the `name` variable which is in the root {@link angular.scope scope}. +* The double curly brace `{{markup}}`, which binds the name variable to the greeting text. +* The concept of {@link guide.data-binding data binding}, which reflects any changes to the + input field in the greeting text. diff --git a/docs/content/cookbook/index.ngdoc b/docs/content/cookbook/index.ngdoc new file mode 100644 index 00000000..7dc937c5 --- /dev/null +++ b/docs/content/cookbook/index.ngdoc @@ -0,0 +1,60 @@ +@workInProgress +@ngdoc overview +@name Cookbook +@description + +Welcome to the angular cookbook. Here we will show you typical uses of angular by example. + + +# Hello World + +{@link cookbook.helloworld Hello World}: The simplest possible application that demonstrates the +classic Hello World! + + +# Basic Form + +{@link cookbook.form Basic Form}: Displaying forms to the user for editing is the bread and butter +of web applications. Angular makes forms easy through bidirectional data binding. + + +# Advanced Form + +{@link cookbook.formadvanced Advanced Form}: Taking the form example to the next level and +providing advanced features such as dirty detection, form reverting and submit disabling if +validation errors exist. + + +# Model View Controller + +{@link cookbook.mvc MVC}: Tic-Tac-Toe: Model View Controller (MVC) is a time-tested design pattern +to separate the behavior (JavaScript controller) from the presentation (HTML view). This +separation aids in maintainability and testability of your project. + + +# Multi-page App and Deep Linking + +{@link cookbook.deeplinking Deep Linking}: An AJAX application never navigates away from the +first page it loads. Instead, it changes the DOM of its single page. Eliminating full-page reloads +is what makes AJAX apps responsive, but it creates a problem in that apps with a single URL +prevent you from emailing links to a particular screen within your application. + +Deep linking tries to solve this by changing the URL anchor without reloading a page, thus +allowing you to send links to specific screens in your app. + + +# Services + +{@link angular.service Services}: Services are long lived objects in your applications that are +available across controllers. A collection of useful services are pre-bundled with angular but you +will likely add your own. Services are initialized using dependency injection, which resolves the +order of initialization. This safeguards you from the perils of global state (a common way to +implement long lived objects). + + +# External Resources + +{@link cookbook.buzz Resources}: Web applications must be able to communicate with the external +services to get and update data. Resources are the abstractions of external URLs which are +specially tailored to angular data binding. + diff --git a/docs/content/cookbook/mvc.ngdoc b/docs/content/cookbook/mvc.ngdoc new file mode 100644 index 00000000..94688547 --- /dev/null +++ b/docs/content/cookbook/mvc.ngdoc @@ -0,0 +1,125 @@ +@workInProgress +@ngdoc overview +@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. + +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> + <doc:source> + <script> + function TicTacToeCntl(){ + this.cellStyle= { + 'height': '20px', + 'width': '20px', + 'border': '1px solid black', + 'text-align': 'center', + 'vertical-align': 'middle', + 'cursor': 'pointer' + }; + this.reset(); + this.$watch('$location.hashSearch.board', this.readUrl); + } + TicTacToeCntl.prototype = { + dropPiece: function(row, col) { + if (!this.winner && !this.board[row][col]) { + this.board[row][col] = this.nextMove; + this.nextMove = this.nextMove == 'X' ? 'O' : 'X'; + this.setUrl(); + } + }, + reset: function(){ + this.board = [ + ['', '', ''], + ['', '', ''], + ['', '', ''] + ]; + this.nextMove = 'X'; + this.winner = ''; + this.setUrl(); + }, + grade: function(){ + var b = this.board; + this.winner = + row(0) || row(1) || row(2) || + col(0) || col(1) || col(2) || + diagonal(-1) || diagonal(1); + function row(r) { return same(b[r][0], b[r][1], b[r][2]);} + function col(c) { return same(b[0][c], b[1][c], b[2][c]);} + function diagonal(i) { return same(b[0][1-i], b[1][1], b[2][1+i]);} + function same(a, b, c) { return (a==b && b==c) ? a : '';}; + }, + setUrl: function(){ + var rows = []; + angular.forEach(this.board, function(row){ + rows.push(row.join(',')); + }); + 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){ + this.board[i] = row.split(','); + }, this); + this.grade(); + } else { + this.reset(); + } + } + }; + </script> + <h3>Tic-Tac-Toe</h3> + <div ng:controller="TicTacToeCntl"> + Next Player: {{nextMove}} + <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> + </tr> + </table> + <button ng:click="reset()">reset board</button> + </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 + +* 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. |
