Compare commits
281 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b00262fffe | |||
| 3f98d6ac99 | |||
| 22309c312f | |||
| fcf95a47d1 | |||
| e1e7aca9a6 | |||
| 039041e3ae | |||
| 3da441b580 | |||
| f9502d2ad3 | |||
| 0356c90af8 | |||
| 0d4def452e | |||
| 897d0f1424 | |||
| 92af30ce6e | |||
| 54581d36df | |||
| b587091b6e | |||
| c49b8a2db5 | |||
| 5cdfe45aa3 | |||
| 16a40c626f | |||
| b7f4d8c3c3 | |||
| 939c8e8fac | |||
| d2ba4c5170 | |||
| 46691c2721 | |||
| e7a23e4b65 | |||
| 15fd735793 | |||
| 985d3d7558 | |||
| 249c89c091 | |||
| 5164ae545b | |||
| e1e0ddb910 | |||
| d648d709f3 | |||
| 9a8dbfef51 | |||
| 28114de8dc | |||
| c6ea1be053 | |||
| 5143e7bf06 | |||
| afd25446d2 | |||
| 3c3e6980b3 | |||
| e0b4b107ee | |||
| 614fd3d55a | |||
| 7146f70636 | |||
| 11cb9423a7 | |||
| c76a120bfe | |||
| b8960c3710 | |||
| 67338ce061 | |||
| 23f8da7cbb | |||
| b911e303ec | |||
| a13b5ed3bc | |||
| 63cca9afbc | |||
| d47ec772c3 | |||
| 5c19766063 | |||
| f2119c7524 | |||
| 08029c7b72 | |||
| 0bf611087b | |||
| acb4338b70 | |||
| cd9a7b9608 | |||
| 1dccaaaaa2 | |||
| 9632f5c1c7 | |||
| bb3be87606 | |||
| 174952e443 | |||
| 6f91ffeb91 | |||
| c594f75b4c | |||
| 50eb7f15b8 | |||
| 212a6ff29a | |||
| 871252ab4c | |||
| 0c534644bc | |||
| c28662d28d | |||
| b97c6e5f74 | |||
| 4e3c05b99e | |||
| 5e4d59adf0 | |||
| fd38655e6c | |||
| b9001e9147 | |||
| d1e7a5394a | |||
| 2090136dd8 | |||
| c9f2b1eec5 | |||
| 163e05ed36 | |||
| 2986a09c0d | |||
| bb2e7488fa | |||
| 44b2f44f93 | |||
| 1d14760c6d | |||
| baa7af0df0 | |||
| f43c226c67 | |||
| 0e1fa2aefe | |||
| 3d0ce0ebe9 | |||
| b00da987a9 | |||
| 188bdf7768 | |||
| dbd880cc0a | |||
| bf8e0540f8 | |||
| 78b6e8a446 | |||
| b656552d68 | |||
| 1cdfa3b960 | |||
| 16363d8000 | |||
| 92995bbce9 | |||
| b9707d910e | |||
| 5bbd64ac65 | |||
| caeb1bf899 | |||
| 9b4efa73f9 | |||
| 4aaa2f7f6b | |||
| 6290bd4587 | |||
| e9f81b6631 | |||
| afbe073121 | |||
| 7b705df2b7 | |||
| a4c8ac7126 | |||
| e3e2e4436e | |||
| 972c3e9be0 | |||
| feacf608ee | |||
| fe633dd0cf | |||
| fdcc2dbfd3 | |||
| 5ad0c7d0e4 | |||
| 540701a8d8 | |||
| 4d2d70e7fb | |||
| cd28a2e952 | |||
| 59adadca08 | |||
| 497839f583 | |||
| 5487bdb3d1 | |||
| 3ae3ccf3da | |||
| e9b57f9df8 | |||
| 45f47ff6cd | |||
| 0c8b35681e | |||
| a035e88397 | |||
| 3548fe3139 | |||
| 29f9e2665d | |||
| 8d1944851d | |||
| 3e1a6688c3 | |||
| aba9bb2a24 | |||
| 5857c44e0c | |||
| 8adae2fdf2 | |||
| 955551141d | |||
| 94e1a07b28 | |||
| ac73e8877e | |||
| e88dfb734a | |||
| 8d6dc0b9a7 | |||
| acbd7cdf32 | |||
| 035c751076 | |||
| 186a840cd3 | |||
| b09595a3c1 | |||
| f6d98f1472 | |||
| 5279de0e70 | |||
| 8fe77b69e8 | |||
| 1cc6bee4ce | |||
| a8aa193c6b | |||
| e45b013143 | |||
| ea18f4548d | |||
| 57c37a21d1 | |||
| 74fac45f48 | |||
| f0fa5e6376 | |||
| c283bf6035 | |||
| b3c17f3fdc | |||
| 9c06394376 | |||
| 085e3c611f | |||
| 4b35a59c6a | |||
| 7cb03c5ab9 | |||
| 78c7066422 | |||
| 923da410bd | |||
| a87f2fb9e4 | |||
| c27aba4354 | |||
| dd9151e522 | |||
| 3972d2a89b | |||
| cb6f832f38 | |||
| 6022f3df39 | |||
| 7c11531902 | |||
| c6d2549a52 | |||
| bee6060e4b | |||
| 16597e8b52 | |||
| f684f20c99 | |||
| bd04316a89 | |||
| ed36b9da3b | |||
| c925f8a657 | |||
| 4c10d33eb4 | |||
| 9062996a0e | |||
| 411c1ae77e | |||
| d12df0d360 | |||
| d9b58f23f6 | |||
| 03dd8c4f4c | |||
| 48697a2b86 | |||
| 93b777c916 | |||
| 5c70ff72e2 | |||
| 5e663c3dc7 | |||
| 260725efcd | |||
| 4afad1da29 | |||
| eb01fe593d | |||
| fc7834f9ac | |||
| e4303a1f3a | |||
| 1e00db8daa | |||
| aaa0179758 | |||
| f5ef3724ce | |||
| e60601be4f | |||
| e2663f62b0 | |||
| 9f9ed4c5ff | |||
| 66fc268aeb | |||
| 1d966f8a65 | |||
| ddf6f1143f | |||
| 2636105c5e | |||
| c0b557a96c | |||
| 84873e7f4e | |||
| 95fdb1231f | |||
| ef875ad0cf | |||
| 615841a5d3 | |||
| 7d0c256ecd | |||
| 6cbe096dbf | |||
| 21602b5cd6 | |||
| 4ae671ac88 | |||
| 28ed5ba465 | |||
| 02dc81bae0 | |||
| 445680f601 | |||
| bf729d550b | |||
| dc8ffa51b7 | |||
| d7ba5bc83b | |||
| 950d02b4d4 | |||
| 578e38e0af | |||
| af7c51ee1d | |||
| 25d1822bd8 | |||
| 3945f884c5 | |||
| d5ccabce60 | |||
| bb948176aa | |||
| 163c799eff | |||
| 7da70af1ae | |||
| 836e4c1428 | |||
| eabedba34d | |||
| b4add97c17 | |||
| bacc31bea9 | |||
| ad90c3574f | |||
| e28171d5e4 | |||
| ce73ed091b | |||
| 90ac8d57b0 | |||
| 6eb1179505 | |||
| 9b85757102 | |||
| c6c3949b14 | |||
| e175db37c6 | |||
| f38010d3a2 | |||
| 7fc18b263d | |||
| fabc9f77a3 | |||
| c17c731fdc | |||
| 3692885810 | |||
| 5d43439dbe | |||
| a46f2a0db3 | |||
| 3217a249e1 | |||
| 78f394fd17 | |||
| e82e64d57b | |||
| 8978e066b5 | |||
| 833eb3c844 | |||
| 07926ff1ef | |||
| e801faba2e | |||
| ee6af9a978 | |||
| 74379df6c4 | |||
| fe65dd926c | |||
| 669b53ede2 | |||
| b0c3f28e8f | |||
| 9810dc0993 | |||
| ab5df20dfa | |||
| d83a92c121 | |||
| d0425de29e | |||
| ad5e42cf82 | |||
| 9ed1126adb | |||
| 7a19eb84aa | |||
| 718741acab | |||
| ec8bb675b4 | |||
| 8e32f3fd35 | |||
| 02332107e5 | |||
| afc81b554e | |||
| 26e8ab3693 | |||
| 28ccc76aa1 | |||
| b3c4cb7cff | |||
| 4af4378b11 | |||
| 8611ebe6a0 | |||
| 8f46a3c9ac | |||
| 66fdb36ecb | |||
| f0f5ffa9aa | |||
| 2bc7afd3ba | |||
| a4b45397e0 | |||
| de4e06ed73 | |||
| fd822bdaf9 | |||
| 4f78fd692c | |||
| df6d2ba326 | |||
| ccda436f94 | |||
| e86c435349 | |||
| 1942861472 | |||
| b96e978178 | |||
| bda2bba2be | |||
| ca08c004c8 | |||
| 25a62b58db | |||
| 97e3ec4d1b | |||
| 75f11f1fc4 | |||
| e134a8335f | |||
| 8ee32a75f0 |
@@ -11,3 +11,5 @@ performance/temp*.html
|
||||
angular.js.tmproj
|
||||
node_modules
|
||||
jsTestDriver*.conf
|
||||
angular.xcodeproj
|
||||
.idea
|
||||
|
||||
+282
-36
@@ -1,7 +1,223 @@
|
||||
- The Latest Stable Release: <a href="#0.9.19">0.9.19 canine-psychokinesis</a>
|
||||
- The Latest Unstable Release: <a href="#0.10.2">0.10.2 sneaky-seagull</a>
|
||||
- The Latest Unstable Release: <a href="#0.10.6">0.10.6 bubblewrap-cape</a>
|
||||
|
||||
<a name="0.10.2"><a/>
|
||||
<a name="0.10.6"></a>
|
||||
# 0.10.6 bubblewrap-cape (in-progress) #
|
||||
|
||||
## Features:
|
||||
|
||||
- [Dependency injection subsystem][guide2.di] rewrite. This is a huge change to the Angular core
|
||||
that was necessary for many reasons. Please read the full
|
||||
[design doc](https://docs.google.com/document/d/1hJnIqWhSt7wCacmWBB01Bmc6faZ8XdXJAEeiJwjZmqs/edit?hl=en_US)
|
||||
to understand the changes and reasoning behind them.
|
||||
- Added [angular.bootstrap] for manual bootstrapping of the app. Also see
|
||||
[Initializing Angular App][bootstrapping] doc.
|
||||
- Helper functions [inject] and [module] that make testing with DI and jasmine a lot easier.
|
||||
- [jqLite][jqLite2] and jQuery were extended with helper method `injector()` that simplifies the
|
||||
access to the application injector during debugging.
|
||||
- Rewrite of $xhr service and its dependencies, which was replaced with [$http] service.
|
||||
The $browser.xhr and its mock were replaced by [$httpBackend] and its
|
||||
[unit testing][unit-testing $httpBackend] and [end-to-end testing][e2e-testing $httpBackend]
|
||||
mocks. The $resource service api and functionality was preserved, with the exception of caching,
|
||||
which is not happening automatically as it used it in the past (verifyCache has no effect).
|
||||
- [$q] - Q-like deferred/promise implementation
|
||||
([commit](https://github.com/angular/angular.js/commit/1cdfa3b9601c199ec0b45096b38e26350eca744f))
|
||||
- Transparent data-binding to promises in templates. [Example](http://jsfiddle.net/IgorMinar/aNSWu/)
|
||||
([commit](https://github.com/angular/angular.js/commit/78b6e8a446c0e38075c14b724f3cdf345c01fa06))
|
||||
- New [$anchorScroll] service that watches url hash and navigates to the html anchor even if the
|
||||
content was loaded via [ng:view] (for [ng:include] you have to opt into this behavior using
|
||||
autoscroll attribute)
|
||||
- New LRU cache factory - [$cacheFactory] service
|
||||
- jQuery 1.7 compatibility
|
||||
|
||||
|
||||
## Bug Fixes:
|
||||
|
||||
- Directive names are now case insensitive
|
||||
([commit](https://github.com/angular/angular.js/commit/1e00db8daa5c09e7f8f9134f5c94b9a18c7dc425))
|
||||
- $location#url setter fix (Issue [#648](https://github.com/angular/angular.js/issues/648))
|
||||
- [ng:include] - prevent race conditions by ignoring stale http callbacks
|
||||
([commit](https://github.com/angular/angular.js/commit/1d14760c6d3eefb676f5670bc323b2a7cadcdbfa))
|
||||
- [ng:repeat] - support repeating over array with null
|
||||
([commit](https://github.com/angular/angular.js/commit/cd9a7b9608707c34bec2316ee8c789a617d22a7b))
|
||||
- [angular.copy] - throw Error if source and destination are identical
|
||||
([commit](https://github.com/angular/angular.js/commit/08029c7b72a857ffe52f302ed79ae12db9efcc08))
|
||||
- Forms should not prevent POST submission if the action attribute is present
|
||||
([commit](https://github.com/angular/angular.js/commit/c9f2b1eec5e8a9eaf10faae8a8accf0b771096e0))
|
||||
|
||||
|
||||
## Breaking Changes:
|
||||
|
||||
- App bootstrapping works differently (see [angular.bootstrap] and [ng:app] and [bootstrapping])
|
||||
- scope.$service is no more (because injector creates scope and not the other way around),
|
||||
if you really can't get services injected and need to fetch them manually then, get hold of
|
||||
[$injector] service and call $injector.get('serviceId')
|
||||
- angular.service style service registration was replaced with module system, please see
|
||||
[angular.module] api and [DI documentation][guide2.di] for more info.
|
||||
- the $xhr service was replaced with [$http] with promise based apis.
|
||||
- [unit-testing $httpBackend]'s expect method (the replacement for $browser.xhr.expect) is stricter -
|
||||
the order of requests matters and a single request expectation can handle only a single request.
|
||||
- compiler
|
||||
- compiler is a service, so use [$compile] instead of angular.compile to compile templates
|
||||
- $compile (nee angular.compile) returns the linking function which takes one mandatory argument -
|
||||
the scope. previously this argument was optional and if missing, the compiler would create a new
|
||||
root scope, this was a source of bugs and was removed
|
||||
- filters
|
||||
- filters need to be registered either via [moduleName.filter][angular.Module] or
|
||||
[$filterProvider.filter][$filterProvider]
|
||||
- filters don't have access to the dom element
|
||||
- currency filter doesn't make negative values red
|
||||
- json filter doesn't print out stuff in monospace
|
||||
- type augmentation via angular.Array, and angular.Object is gone. As a replacement use filters
|
||||
([filter], [limitTo], [orderBy]) or ES5 apis (e.g. Array#indexOf)
|
||||
- [$browser.defer.flush] now throws an exception when queue is empty
|
||||
([commit](https://github.com/angular/angular.js/commit/63cca9afbcf7a772086eb4582d2f409c39e0ed12))
|
||||
- scope.$apply and scope.$digest throws an exception if called while $apply or $digest is already
|
||||
in progress (this is a programming error, you should never need to do this)
|
||||
([commit](https://github.com/angular/angular.js/commit/0bf611087b2773fd36cf95c938d1cda8e65ffb2b))
|
||||
|
||||
|
||||
<a name="0.10.5"></a>
|
||||
# 0.10.5 steel-fist (11-11-08) #
|
||||
|
||||
## Features:
|
||||
|
||||
- [ng:autobind]: drop angular.js file name restrictions
|
||||
([commit](https://github.com/angular/angular.js/commit/d7ba5bc83ba9a8937384ea677331c5156ed6772d))
|
||||
- [Scope]: better logging of infinite digest error
|
||||
([commit](https://github.com/angular/angular.js/commit/ef875ad0cf4349144cb4674e050dd160564f6dd9),
|
||||
issue [#621](https://github.com/angular/angular.js/issues/621))
|
||||
- enable [widget] styling in IE8 and below using
|
||||
[html5shiv](http://code.google.com/p/html5shiv/)-like approach
|
||||
([commit](https://github.com/angular/angular.js/commit/163c799effd5cfadc57990f4d4127651bae3fbdb),
|
||||
issue [#584](https://github.com/angular/angular.js/issues/584))
|
||||
- [ng:style]: compatibility + perf improvements
|
||||
([commit](https://github.com/angular/angular.js/commit/e2663f62b0fbb8b9ce2e706b821a135e0bc7e885))
|
||||
|
||||
|
||||
## Bug Fixes:
|
||||
- [ng:view]: ignore stale xhr callbacks - fixes issues caused by race-conditions which occured when
|
||||
user navigated to a new route before the current route finished loading
|
||||
(issue [#619](https://github.com/angular/angular.js/issues/619))
|
||||
- [ng:form] should always be a block level (css) element
|
||||
([commit](https://github.com/angular/angular.js/commit/02dc81bae0011b7ae4190363be5fdd5db420aca9))
|
||||
- Fixes for [e2e test runner]'s `$location` dsl
|
||||
([commit](https://github.com/angular/angular.js/commit/dc8ffa51b7ebe5fb9bc1c89087c8b3c9e65d1006))
|
||||
- [ng:repeat] when iterating over arrays ignore non-array properties + when iterating over objects
|
||||
sort keys alphabetically
|
||||
([commit](https://github.com/angular/angular.js/commit/3945f884c5777e629b57c9ab0e93b9d02b9840d0))
|
||||
|
||||
## Docs:
|
||||
- experimental [disqus.com](http://disqus.com/) integration for all docs-next.angularjs.org pages
|
||||
([commit](https://github.com/angular/angular.js/commit/28ed5ba46595a371bd734b92a6e4bb40d1013741),
|
||||
contributed by Dan Doyon)
|
||||
- [e2e test runner] docs were moved to the dev guide
|
||||
|
||||
|
||||
|
||||
<a name="0.10.4"></a>
|
||||
# 0.10.4 human-torch (2011-10-22) #
|
||||
|
||||
## Features:
|
||||
|
||||
- New validation options for
|
||||
[input widgets](http://docs-next.angularjs.org/api/angular.widget.input): `ng:minlength` and
|
||||
`ng:maxlength`
|
||||
([commit](https://github.com/angular/angular.js/commit/78f394fd17be581c84ecd526bb786ed1681d35cb))
|
||||
(contributed by Konstantin Stepanov)
|
||||
- HTML sanitizer was updated to recognize all safe HTML5 elements
|
||||
(Issue [#89](https://github.com/angular/angular.js/issues/89))
|
||||
- [ng:options]' blank option is now compiled and data-bound as any other template
|
||||
(Issue [#562](https://github.com/angular/angular.js/issues/562))
|
||||
(contributed by tehek)
|
||||
- [$defer](http://docs-next.angularjs.org/api/angular.service.$defer) service now exposes `cancel`
|
||||
method for task cancellation
|
||||
([commit](https://github.com/angular/angular.js/commit/ad90c3574f8365ee4a1a973d5e43c64fe9fcda2c))
|
||||
|
||||
|
||||
## Bug Fixes:
|
||||
|
||||
- [ng:options] should select correct element when '?'-option (invalid value) was previously selected
|
||||
(Issue [#599](https://github.com/angular/angular.js/issues/599)) (contributed by Tehek)
|
||||
- Fix data-binding of radio button's value property
|
||||
(Issue [#316](https://github.com/angular/angular.js/issues/316))
|
||||
- Input with type `password` should no be turned into a readable text field
|
||||
([commit](https://github.com/angular/angular.js/commit/e82e64d57b65d9f3c4f2e8831f30b615a069b7f6))
|
||||
(contributed by Konstantin Stepanov)
|
||||
- [ng:repeat] should ignore object properties starting with `$`
|
||||
([commit](https://github.com/angular/angular.js/commit/833eb3c84445110dc1dad238120573f08ed8d102))
|
||||
- Correctly parse out inlined regexp from the input field's `ng:pattern` attribute.
|
||||
([commit](https://github.com/angular/angular.js/commit/5d43439dbe764a4c7227f51b34a81b044f13901b))
|
||||
- $location service in html5 mode should correctly rewrite links that contain nested elements
|
||||
([commit](https://github.com/angular/angular.js/commit/9b85757102fbd44e88d0a3909fdf8b90f191b593))
|
||||
|
||||
|
||||
## Breaking Changes:
|
||||
|
||||
- the [date] filter now uses 'mediumDate' format if none is specified. This was done to deal with
|
||||
browser inconsistencies (each browser used to use different format)
|
||||
(Issue [#605](https://github.com/angular/angular.js/issues/605),
|
||||
[commit](https://github.com/angular/angular.js/commit/c6c3949b14f4003ecab291243edfca61262f2c3d),
|
||||
[commit](https://github.com/angular/angular.js/commit/e175db37c6f52bba4080efeec22a7120a896099e))
|
||||
- calling the linker function returned by [angular.compile][compile] doesn't automatically run
|
||||
`$digest` on the linked scope any more. This behavior was briefly introduced in 0.10.3 but was
|
||||
causing issues and inefficiencies in production apps so we reverted it. See:
|
||||
[commit](https://github.com/angular/angular.js/commit/f38010d3a2f457a53798212ef72418637dabe189)
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="0.10.3"></a>
|
||||
# 0.10.3 shattering-heartbeat (2011-10-13) #
|
||||
|
||||
## Features:
|
||||
|
||||
- New forms, validation, support for HTML5 input widgets. Please check out:
|
||||
- [Forms overview](http://docs-next.angularjs.org/guide/dev_guide.forms)
|
||||
- [form widget](http://docs-next.angularjs.org/api/angular.widget.form)
|
||||
- [input widget](http://docs-next.angularjs.org/api/angular.widget.input)
|
||||
- [$formFactory service](http://docs-next.angularjs.org/api/angular.service.$formFactory)
|
||||
- [angular.inputType](http://docs-next.angularjs.org/api/angular.inputType)
|
||||
- [commit](https://github.com/angular/angular.js/commit/4f78fd692c0ec51241476e6be9a4df06cd62fdd6)
|
||||
|
||||
- [ng:repeat] now has element-model affinity, which makes it more friendly to third-party code that
|
||||
is not aware of angular's DOM manipulation. This is also the pre-requisite for supporting
|
||||
animations.
|
||||
([commit](https://github.com/angular/angular.js/commit/75f11f1fc46c35a28c0905f7316ea6779145e2fb))
|
||||
|
||||
|
||||
## Bug Fixes:
|
||||
|
||||
- The select widget with [ng:options] directive now correctly displays selected option (regression
|
||||
from 0.10.2).
|
||||
- Fix for jqLite's removeClass, which under certain circumstances could clobber class names.
|
||||
([commit](https://github.com/angular/angular.js/commit/b96e978178a6acbf048aa6db466ed845e1395445))
|
||||
- Other small fixes and documentation improvements.
|
||||
|
||||
|
||||
## Breaking Changes:
|
||||
|
||||
- Due to changes in how forms and validation works the following were replaced with new apis:
|
||||
- `angular.formatter` - use `angular.inputType` or form's `$createWidget`
|
||||
- `angular.validator` - use `angular.inputType` or form's `$createWidget`
|
||||
- changes to `<input>` and `<select>` elements
|
||||
- `ng:model` directive is now required for data-binding to kick in
|
||||
- the `name` attribute is now optional and is used only as an alias when accessing the input
|
||||
widget via the form object.
|
||||
- view can't affect the model without a user interaction, so the `value` attribute of the
|
||||
`<input>` element and `selected` attribute of the `<option>` element if specified in the
|
||||
template is ignored.
|
||||
- Removed decoration of DOM elements when:
|
||||
- an exception occurs - when an exception happens, it will be passed to the $exceptionHandler
|
||||
service, which can decide what to do with it.
|
||||
- an input widget contains invalid input - in this case the forms validation apis can be used to
|
||||
display a customized error message.
|
||||
- The $hover service was removed (it was needed only for the DOM decoration described above).
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="0.10.2"></a>
|
||||
# 0.10.2 sneaky-seagull (2011-10-08) #
|
||||
|
||||
## Features:
|
||||
@@ -46,17 +262,20 @@
|
||||
- If Angular is being used with jQuery older than 1.6, some features might not work properly. Please
|
||||
upgrade to jQuery version 1.6.4.
|
||||
|
||||
## Breaking Changes
|
||||
- ng:repeat no longer has ng:repeat-index property. This is because the elements now have
|
||||
affinity to the underlying collection, and moving items around in the collection would move
|
||||
ng:repeat-index property rendering it meaningless.
|
||||
|
||||
|
||||
|
||||
<a name="0.10.1"><a/>
|
||||
<a name="0.10.1"></a>
|
||||
# 0.10.1 inexorable-juggernaut (2011-09-09) #
|
||||
|
||||
## Features
|
||||
|
||||
- complete rewrite of the $location service with HTML5 support, many API and semantic changes.
|
||||
Please see:
|
||||
- [$location service API docs](http://docs-next.angularjs.org/#!/api/angular.service.$location)
|
||||
- [$location service API docs](http://docs-next.angularjs.org/#!/api/angular.module.ng.$location)
|
||||
- [$location service dev guide article](http://docs-next.angularjs.org/#!/guide/dev_guide.services.$location)
|
||||
- [location.js source file](https://github.com/angular/angular.js/blob/master/src/service/location.js)
|
||||
- breaking changes section of this changelog
|
||||
@@ -82,7 +301,7 @@
|
||||
- $location.hashPath -> $location.path()
|
||||
- $location.hashSearch -> $location.search()
|
||||
- $location.search -> no equivalent, use $window.location.search (this is so that we can work in
|
||||
hashBang and html5 mode at the same time, check out the docs)
|
||||
hashBang and html5 mode at the same time, check out the docs)
|
||||
- $location.update() / $location.updateHash() -> use $location.url()
|
||||
- n/a -> $location.replace() - new api for replacing history record instead of creating a new one
|
||||
|
||||
@@ -95,7 +314,7 @@
|
||||
|
||||
|
||||
|
||||
<a name="0.10.0"><a/>
|
||||
<a name="0.10.0"></a>
|
||||
# 0.10.0 chicken-hands (2011-09-02) #
|
||||
|
||||
## Features
|
||||
@@ -150,7 +369,7 @@
|
||||
|
||||
|
||||
|
||||
<a name="0.9.19"><a/>
|
||||
<a name="0.9.19"></a>
|
||||
# 0.9.19 canine-psychokinesis (2011-08-20) #
|
||||
|
||||
## Features
|
||||
@@ -186,7 +405,7 @@
|
||||
`css('display', 'block'/'inline'/..)` instead
|
||||
|
||||
|
||||
<a name="0.9.18"><a/>
|
||||
<a name="0.9.18"></a>
|
||||
# 0.9.18 jiggling-armfat (2011-07-29) #
|
||||
|
||||
### Features
|
||||
@@ -219,7 +438,7 @@
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
- make injector compatible with Rhino (HtmlUnit) (contributed by Mårten Dolk)
|
||||
- make injector compatible with Rhino (HtmlUnit) (contributed by Mårten Dolk)
|
||||
[commit](https://github.com/angular/angular.js/commit/77ba539f630c57b17d71dbf1e9c5667a7eb603b7)
|
||||
- `ie-compat.js` fixes and improvements related to fetching this file on the fly on legacy browsers
|
||||
- [jqLite]
|
||||
@@ -256,7 +475,7 @@
|
||||
|
||||
|
||||
|
||||
<a name="0.9.17"><a/>
|
||||
<a name="0.9.17"></a>
|
||||
# <angular/> 0.9.17 vegetable-reanimation (2011-06-30) #
|
||||
|
||||
### New Features
|
||||
@@ -291,7 +510,7 @@
|
||||
|
||||
|
||||
|
||||
<a name="0.9.16"><a/>
|
||||
<a name="0.9.16"></a>
|
||||
# <angular/> 0.9.16 weather-control (2011-06-07) #
|
||||
|
||||
### Features
|
||||
@@ -322,7 +541,7 @@
|
||||
|
||||
|
||||
|
||||
<a name="0.9.15"><a/>
|
||||
<a name="0.9.15"></a>
|
||||
# <angular/> 0.9.15 lethal-stutter (2011-04-11) #
|
||||
|
||||
### Features
|
||||
@@ -341,7 +560,7 @@
|
||||
|
||||
|
||||
|
||||
<a name="0.9.14"><a/>
|
||||
<a name="0.9.14"></a>
|
||||
# <angular/> 0.9.14 key-maker (2011-04-01) #
|
||||
|
||||
### Performance
|
||||
@@ -362,12 +581,12 @@
|
||||
|
||||
|
||||
|
||||
<a name="0.9.13"><a/>
|
||||
<a name="0.9.13"></a>
|
||||
# <angular/> 0.9.13 curdling-stare (2011-03-13) #
|
||||
|
||||
### New Features
|
||||
- Added XSRF protection for the [$xhr] service. (commit c578f8c3)
|
||||
- Targeted auto-bootstrap — [ng:autobind] now takes an optional value which specifies an element id
|
||||
- Targeted auto-bootstrap – [ng:autobind] now takes an optional value which specifies an element id
|
||||
to be compiled instead of compiling the entire html document. (commit 9d5c5337)
|
||||
|
||||
|
||||
@@ -389,7 +608,7 @@
|
||||
|
||||
|
||||
|
||||
<a name="0.9.12"><a/>
|
||||
<a name="0.9.12"></a>
|
||||
# <angular/> 0.9.12 thought-implanter (2011-03-03) #
|
||||
|
||||
### API
|
||||
@@ -446,7 +665,7 @@
|
||||
|
||||
|
||||
|
||||
<a name="0.9.11"><a/>
|
||||
<a name="0.9.11"></a>
|
||||
# <angular/> 0.9.11 snow-maker (2011-02-08) #
|
||||
|
||||
### Documentation
|
||||
@@ -477,7 +696,7 @@
|
||||
request via the `$xhr` service or remove all unneeded `flush()` calls.
|
||||
|
||||
|
||||
<a name="0.9.10"><a/>
|
||||
<a name="0.9.10"></a>
|
||||
# <angular/> 0.9.10 flea-whisperer (2011-01-26) #
|
||||
|
||||
### Features
|
||||
@@ -501,7 +720,7 @@ with the `$route` service
|
||||
- lots of improvements related to formatting of the content of docs.anguarjs.org
|
||||
|
||||
|
||||
<a name="0.9.9"><a/>
|
||||
<a name="0.9.9"></a>
|
||||
# <angular/> 0.9.9 time-shift (2011-01-13) #
|
||||
|
||||
### Security
|
||||
@@ -532,7 +751,7 @@ with the `$route` service
|
||||
- angular.filter.date now properly handles some corner-cases (issue #159 - fix contributed by Vojta)
|
||||
|
||||
### Breaking changes
|
||||
- API for accessing registered services — `scope.$inject` — was renamed to
|
||||
- API for accessing registered services — `scope.$inject` — was renamed to
|
||||
[`scope.$service`](http://docs.angularjs.org/#!/api/angular.scope.$service). (commit b2631f61)
|
||||
|
||||
- Support for `eager-published` services was removed. This change was done to make explicit
|
||||
@@ -577,7 +796,7 @@ with the `$route` service
|
||||
- The `toString` method of the `angular.service.$location` service was removed. (commit 23875cb3)
|
||||
|
||||
|
||||
<a name="0.9.8"><a/>
|
||||
<a name="0.9.8"></a>
|
||||
# <angular/> 0.9.8 astral-projection (2010-12-23) #
|
||||
|
||||
### Docs/Getting started
|
||||
@@ -591,7 +810,7 @@ with the `$route` service
|
||||
- Ignore input widgets which have no name (issue #153)
|
||||
|
||||
|
||||
<a name="0.9.7"><a/>
|
||||
<a name="0.9.7"></a>
|
||||
# <angular/> 0.9.7 sonic-scream (2010-12-10) #
|
||||
|
||||
### Bug Fixes
|
||||
@@ -610,7 +829,7 @@ with the `$route` service
|
||||
your controllers. (commit e5e69d9b90850eb653883f52c76e28dd870ee067)
|
||||
|
||||
|
||||
<a name="0.9.6"><a/>
|
||||
<a name="0.9.6"></a>
|
||||
# <angular/> 0.9.6 night-vision (2010-12-06) #
|
||||
|
||||
### Security
|
||||
@@ -640,7 +859,7 @@ with the `$route` service
|
||||
- The HTML sanitizer is slightly more strinct now. Please see info in the "Security" section above.
|
||||
|
||||
|
||||
<a name="0.9.5"><a/>
|
||||
<a name="0.9.5"></a>
|
||||
# <angular/> 0.9.5 turkey-blast (2010-11-25) #
|
||||
|
||||
### Docs
|
||||
@@ -650,7 +869,7 @@ with the `$route` service
|
||||
- added `angular.Array.limitTo` to make it easy to select first or last few items of an array
|
||||
|
||||
|
||||
<a name="0.9.4"><a/>
|
||||
<a name="0.9.4"></a>
|
||||
# <angular/> 0.9.4 total-recall (2010-11-18) #
|
||||
|
||||
### Docs
|
||||
@@ -667,7 +886,7 @@ with the `$route` service
|
||||
- Better error handling - compilation exception now contain stack trace (commit b2d63ac4)
|
||||
|
||||
|
||||
<a name="0.9.3"><a/>
|
||||
<a name="0.9.3"></a>
|
||||
# <angular/> 0.9.3 cold-resistance (2010-11-10) #
|
||||
|
||||
### Docs
|
||||
@@ -695,7 +914,7 @@ with the `$route` service
|
||||
simple RegExp validator.
|
||||
|
||||
|
||||
<a name="0.9.2"><a/>
|
||||
<a name="0.9.2"></a>
|
||||
# <angular/> 0.9.2 faunal-mimicry (2010-11-03) #
|
||||
|
||||
### Docs
|
||||
@@ -733,7 +952,7 @@ with the `$route` service
|
||||
implements HEAD
|
||||
|
||||
|
||||
<a name="0.9.1"><a/>
|
||||
<a name="0.9.1"></a>
|
||||
# <angular/> 0.9.1 repulsion-field (2010-10-26) #
|
||||
|
||||
### Security
|
||||
@@ -760,7 +979,7 @@ with the `$route` service
|
||||
- html filter now sanitizes html content for XSS attacks which may result in different behavior
|
||||
|
||||
|
||||
<a name="0.9.0"><a/>
|
||||
<a name="0.9.0"></a>
|
||||
# <angular/> 0.9.0 dragon-breath (2010-10-20) #
|
||||
|
||||
### Security
|
||||
@@ -815,12 +1034,13 @@ with the `$route` service
|
||||
[ng:class]: http://docs.angularjs.org/#!/api/angular.directive.ng:class
|
||||
[ng:src]: http://docs.angularjs.org/#!/api/angular.directive.ng:src
|
||||
[ng:href]: http://docs.angularjs.org/#!/api/angular.directive.ng:href
|
||||
[$defer]: http://docs.angularjs.org/#!/api/angular.service.$defer
|
||||
[$cookies]: http://docs.angularjs.org/#!/api/angular.service.$cookies
|
||||
[$xhr]: http://docs.angularjs.org/#!/api/angular.service.$xhr
|
||||
[$xhr.cache]: http://docs.angularjs.org/#!/api/angular.service.$xhr.cache
|
||||
[$resource]: http://docs.angularjs.org/#!/api/angular.service.$resource
|
||||
[$route]: http://docs.angularjs.org/#!/api/angular.service.$route
|
||||
[ng:style]: http://docs.angularjs.org/#!/api/angular.directive.ng:style
|
||||
[$defer]: http://docs.angularjs.org/#!/api/angular.module.ng.$defer
|
||||
[$cookies]: http://docs.angularjs.org/#!/api/angular.module.ng.$cookies
|
||||
[$xhr]: http://docs.angularjs.org/#!/api/angular.module.ng.$xhr
|
||||
[$xhr.cache]: http://docs.angularjs.org/#!/api/angular.module.ng.$xhr.cache
|
||||
[$resource]: http://docs.angularjs.org/#!/api/angular.module.ng.$resource
|
||||
[$route]: http://docs.angularjs.org/#!/api/angular.module.ng.$route
|
||||
[$orderBy]: http://docs.angularjs.org/#!/api/angular.Array.orderBy
|
||||
[date]: http://docs.angularjs.org/#!/api/angular.filter.date
|
||||
[number]: http://docs.angularjs.org/#!/api/angular.filter.number
|
||||
@@ -835,9 +1055,35 @@ with the `$route` service
|
||||
[Jstd Scenario Adapter]: https://github.com/angular/angular.js/blob/master/src/jstd-scenario-adapter/Adapter.js
|
||||
[i18n]: http://docs-next.angularjs.org/#!/guide/dev_guide.i18n
|
||||
[ng:pluralize]: http://docs-next.angularjs.org/#!/api/angular.widget.ng:pluralize
|
||||
[ng:form]: http://docs-next.angularjs.org/api/angular.widget.form
|
||||
[ng:cloak]: http://docs-next.angularjs.org/#!/api/angular.directive.ng:cloak
|
||||
[$on]: http://docs-next.angularjs.org/#!/api/angular.scope.$on
|
||||
[$emit]: http://docs-next.angularjs.org/#!/api/angular.scope.$emit
|
||||
[$broadcast]: http://docs-next.angularjs.org/#!/api/angular.scope.$broadcast
|
||||
[$limitTo]: http://docs-next.angularjs.org/api/angular.Array.limitTo
|
||||
[$location]: http://docs-next.angularjs.org/api/angular.service.$location
|
||||
[e2e test runner]: http://docs-next.angularjs.org/guide/dev_guide.e2e-testing
|
||||
[$injector]: http://docs-next.angularjs.org/api/angular.module.AUTO.$injector
|
||||
[$http]: http://docs-next.angularjs.org/api/angular.module.ng.$http
|
||||
[$httpBackend]: http://docs-next.angularjs.org/api/angular.module.ng.$httpBackend
|
||||
[unit-testing $httpBackend]: http://docs-next.angularjs.org/api/angular.module.ngMock.$httpBackend
|
||||
[e2e-testing $httpBackend]: http://docs-next.angularjs.org/api/angular.module.ngMockE2E.$httpBackend
|
||||
[$q]: http://docs-next.angularjs.org/api/angular.module.ng.$q
|
||||
[angular.bootstrap]: http://docs-next.angularjs.org/api/angular.bootstrap
|
||||
[$anchorScroll]: http://docs-next.angularjs.org/api/angular.module.ng.$anchorScroll
|
||||
[$cacheFactory]: http://docs-next.angularjs.org/api/angular.module.ng.$cacheFactory
|
||||
[bootstrapping]: http://docs-next.angularjs.org/guide/dev_guide.bootstrap
|
||||
[angular.copy]: http://docs-next.angularjs.org/api/angular.copy
|
||||
[ng:app]: http://docs-next.angularjs.org/api/angular.directive.ng:app
|
||||
[$compile]: http://docs-next.angularjs.org/api/angular.module.ng.$compile
|
||||
[$filterProvider]: http://docs-next.angularjs.org/api/angular.module.ng.$filterProvider
|
||||
[angular.Module]: http://docs-next.angularjs.org/api/angular.Module
|
||||
[angular.module]: http://docs-next.angularjs.org/api/angular.module
|
||||
[filter]: http://docs-next.angularjs.org/api/angular.module.ng.$filter.filter
|
||||
[limitTo]: http://docs-next.angularjs.org/api/angular.module.ng.$filter.limitTo
|
||||
[orderBy]: http://docs-next.angularjs.org/api/angular.module.ng.$filter.orderBy
|
||||
[$browser.defer.flush]: http://docs-next.angularjs.org/api/angular.module.ngMock.$browser#defer.flush
|
||||
[inject]: http://docs-next.angularjs.org/api/angular.mock.inject
|
||||
[module]: http://docs-next.angularjs.org/api/angular.mock.module
|
||||
[guide2.di]: http://docs-next.angularjs.org/guide/dev_guide.di
|
||||
[jqLite2]: http://docs.angularjs.org/#!/api/angular.element
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
Angular
|
||||
======
|
||||
AngularJS
|
||||
=========
|
||||
|
||||
* Web site: http://angularjs.org
|
||||
* Tutorial: http://docs.angularjs.org/#!/tutorial
|
||||
* API Docs: http://docs.angularjs.org
|
||||
* Developer Guide: http://docs.angularjs.org/#!/guide
|
||||
|
||||
Compiling
|
||||
---------
|
||||
@@ -7,6 +12,8 @@ Compiling
|
||||
|
||||
Running Tests
|
||||
-------------
|
||||
rake server:start
|
||||
rake test
|
||||
./server.sh # start the server
|
||||
open http://localhost:9876/capture # capture browser
|
||||
./test.sh # run all unit tests
|
||||
|
||||
|
||||
|
||||
@@ -77,66 +77,8 @@ task :compile_jstd_scenario_adapter => :init do
|
||||
end
|
||||
|
||||
|
||||
desc 'Generate IE css js patch'
|
||||
task :generate_ie_compat => :init do
|
||||
css = File.open('css/angular.css', 'r') {|f| f.read }
|
||||
|
||||
# finds all css rules that contain backround images and extracts the rule name(s), content type of
|
||||
# the image and base64 encoded image data
|
||||
r = /\n([^\{\n]+)\s*\{[^\}]*background-image:\s*url\("data:([^;]+);base64,([^"]+)"\);[^\}]*\}/
|
||||
|
||||
images = css.scan(r)
|
||||
|
||||
# create a js file with multipart header containing the extracted images. the entire file *must*
|
||||
# be CRLF (\r\n) delimited
|
||||
File.open(path_to('angular-ie-compat.js'), 'w') do |f|
|
||||
f.write("/*\r\n" +
|
||||
"Content-Type: multipart/related; boundary=\"_\"\r\n" +
|
||||
"\r\n")
|
||||
|
||||
images.each_index do |idx|
|
||||
f.write("--_\r\n" +
|
||||
"Content-Location:img#{idx}\r\n" +
|
||||
"Content-Transfer-Encoding:base64\r\n" +
|
||||
"\r\n" +
|
||||
images[idx][2] + "\r\n")
|
||||
end
|
||||
|
||||
f.write("--_--\r\n" +
|
||||
"*/\r\n")
|
||||
|
||||
# generate a css string containing *background-image rules for IE that point to the mime type
|
||||
# images in the header
|
||||
cssString = ''
|
||||
images.each_index do |idx|
|
||||
cssString += "#{images[idx][0]}{*background-image:url(\"mhtml:' + jsUri + '!img#{idx}\")}"
|
||||
end
|
||||
|
||||
# generate a javascript closure that contains a function which will append the generated css
|
||||
# string as a stylesheet to the current html document
|
||||
jsString = "(function(){ \r\n" +
|
||||
" var jsUri = document.location.href.replace(/\\/[^\\\/]+(#.*)?$/, '/') + \r\n" +
|
||||
" document.getElementById('ng-ie-compat').src,\r\n" +
|
||||
" css = '#{cssString}',\r\n" +
|
||||
" s = document.createElement('style'); \r\n" +
|
||||
"\r\n" +
|
||||
" s.setAttribute('type', 'text/css'); \r\n" +
|
||||
"\r\n" +
|
||||
" if (s.styleSheet) { \r\n" +
|
||||
" s.styleSheet.cssText = css; \r\n" +
|
||||
" } else { \r\n" +
|
||||
" s.appendChild(document.createTextNode(css)); \r\n" +
|
||||
" } \r\n" +
|
||||
" document.getElementsByTagName('head')[0].appendChild(s); \r\n" +
|
||||
"})();\r\n"
|
||||
|
||||
f.write(jsString)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
desc 'Compile JavaScript'
|
||||
task :compile => [:init, :compile_scenario, :compile_jstd_scenario_adapter, :generate_ie_compat] do
|
||||
task :compile => [:init, :compile_scenario, :compile_jstd_scenario_adapter] do
|
||||
|
||||
deps = [
|
||||
'src/angular.prefix',
|
||||
@@ -167,6 +109,27 @@ task :compile => [:init, :compile_scenario, :compile_jstd_scenario_adapter, :gen
|
||||
--js_output_file #{path_to('angular.min.js')})
|
||||
|
||||
FileUtils.cp_r 'i18n/locale', path_to('i18n')
|
||||
|
||||
File.open(path_to('angular-loader.js'), 'w') do |f|
|
||||
concat = 'cat ' + [
|
||||
'src/loader.prefix',
|
||||
'src/loader.js',
|
||||
'src/loader.suffix'].flatten.join(' ')
|
||||
|
||||
content = %x{#{concat}}.
|
||||
gsub('"NG_VERSION_FULL"', NG_VERSION.full).
|
||||
gsub(/^\s*['"]use strict['"];?\s*$/, '') # remove all file-specific strict mode flags
|
||||
|
||||
f.write(content)
|
||||
end
|
||||
|
||||
%x(java -jar lib/closure-compiler/compiler.jar \
|
||||
--compilation_level SIMPLE_OPTIMIZATIONS \
|
||||
--language_in ECMASCRIPT5_STRICT \
|
||||
--js #{path_to('angular-loader.js')} \
|
||||
--js_output_file #{path_to('angular-loader.min.js')})
|
||||
|
||||
|
||||
end
|
||||
|
||||
|
||||
@@ -192,8 +155,9 @@ task :package => [:clean, :compile, :docs] do
|
||||
|
||||
['src/angular-mocks.js',
|
||||
path_to('angular.js'),
|
||||
path_to('angular-loader.js'),
|
||||
path_to('angular.min.js'),
|
||||
path_to('angular-ie-compat.js'),
|
||||
path_to('angular-loader.min.js'),
|
||||
path_to('angular-scenario.js'),
|
||||
path_to('jstd-scenario-adapter.js'),
|
||||
path_to('jstd-scenario-adapter-config.js'),
|
||||
@@ -205,6 +169,13 @@ task :package => [:clean, :compile, :docs] do
|
||||
FileUtils.cp_r path_to('i18n'), "#{pkg_dir}/i18n-#{NG_VERSION.full}"
|
||||
FileUtils.cp_r path_to('docs'), "#{pkg_dir}/docs-#{NG_VERSION.full}"
|
||||
|
||||
File.open("#{pkg_dir}/angular-mocks-#{NG_VERSION.full}.js", File::RDWR) do |f|
|
||||
text = f.read
|
||||
f.truncate 0
|
||||
f.rewind
|
||||
f.write text.sub('"NG_VERSION_FULL"', NG_VERSION.full)
|
||||
end
|
||||
|
||||
File.open("#{pkg_dir}/docs-#{NG_VERSION.full}/index.html", File::RDWR) do |f|
|
||||
text = f.read
|
||||
f.truncate 0
|
||||
|
||||
Vendored
+41
-25
@@ -1,42 +1,49 @@
|
||||
angularFiles = {
|
||||
'angularSrc': [
|
||||
'src/Angular.js',
|
||||
'src/loader.js',
|
||||
'src/AngularPublic.js',
|
||||
'src/JSON.js',
|
||||
'src/Compiler.js',
|
||||
'src/Scope.js',
|
||||
'src/Injector.js',
|
||||
'src/parser.js',
|
||||
'src/Resource.js',
|
||||
'src/Browser.js',
|
||||
'src/sanitizer.js',
|
||||
'src/jqLite.js',
|
||||
'src/apis.js',
|
||||
'src/filters.js',
|
||||
'src/formatters.js',
|
||||
'src/validators.js',
|
||||
'src/service/anchorScroll.js',
|
||||
'src/service/browser.js',
|
||||
'src/service/cacheFactory.js',
|
||||
'src/service/compiler.js',
|
||||
'src/service/cookieStore.js',
|
||||
'src/service/cookies.js',
|
||||
'src/service/defer.js',
|
||||
'src/service/document.js',
|
||||
'src/service/exceptionHandler.js',
|
||||
'src/service/hover.js',
|
||||
'src/service/invalidWidgets.js',
|
||||
'src/service/filter.js',
|
||||
'src/service/filter/filter.js',
|
||||
'src/service/filter/filters.js',
|
||||
'src/service/filter/limitTo.js',
|
||||
'src/service/filter/orderBy.js',
|
||||
'src/service/formFactory.js',
|
||||
'src/service/interpolate.js',
|
||||
'src/service/location.js',
|
||||
'src/service/log.js',
|
||||
'src/service/resource.js',
|
||||
'src/service/parse.js',
|
||||
'src/service/q.js',
|
||||
'src/service/route.js',
|
||||
'src/service/routeParams.js',
|
||||
'src/service/scope.js',
|
||||
'src/service/sniffer.js',
|
||||
'src/service/window.js',
|
||||
'src/service/xhr.bulk.js',
|
||||
'src/service/xhr.cache.js',
|
||||
'src/service/xhr.error.js',
|
||||
'src/service/xhr.js',
|
||||
'src/service/http.js',
|
||||
'src/service/httpBackend.js',
|
||||
'src/service/locale.js',
|
||||
'src/directives.js',
|
||||
'src/markups.js',
|
||||
'src/widgets.js',
|
||||
'src/AngularPublic.js',
|
||||
'src/widget/form.js',
|
||||
'src/widget/input.js',
|
||||
'src/widget/select.js'
|
||||
],
|
||||
|
||||
'angularScenario': [
|
||||
@@ -57,32 +64,33 @@ angularFiles = {
|
||||
],
|
||||
|
||||
'jstd': [
|
||||
'lib/jasmine-1.0.1/jasmine.js',
|
||||
'lib/jasmine/jasmine.js',
|
||||
'lib/jasmine-jstd-adapter/JasmineAdapter.js',
|
||||
'lib/jquery/jquery.js',
|
||||
'test/jquery_remove.js',
|
||||
'@angularSrc',
|
||||
'example/personalLog/*.js',
|
||||
'test/testabilityPatch.js',
|
||||
'test/matchers.js',
|
||||
'src/scenario/Scenario.js',
|
||||
'src/scenario/output/*.js',
|
||||
'src/jstd-scenario-adapter/*.js',
|
||||
'src/scenario/*.js',
|
||||
'src/angular-mocks.js',
|
||||
'test/mocks.js',
|
||||
'test/scenario/*.js',
|
||||
'test/scenario/output/*.js',
|
||||
'test/jstd-scenario-adapter/*.js',
|
||||
'test/*.js',
|
||||
'test/service/*.js',
|
||||
'test/service/filter/*.js',
|
||||
'test/widget/*.js',
|
||||
'example/personalLog/test/*.js'
|
||||
],
|
||||
|
||||
'jstdExclude': [
|
||||
'test/jquery_alias.js',
|
||||
'src/angular-bootstrap.js',
|
||||
'src/scenario/angular-bootstrap.js',
|
||||
'src/AngularPublic.js'
|
||||
'src/scenario/angular-bootstrap.js'
|
||||
],
|
||||
|
||||
'jstdScenario': [
|
||||
@@ -92,8 +100,17 @@ angularFiles = {
|
||||
'build/docs/docs-scenario.js'
|
||||
],
|
||||
|
||||
'jstdMocks': [
|
||||
'lib/jasmine/jasmine.js',
|
||||
'lib/jasmine-jstd-adapter/JasmineAdapter.js',
|
||||
'build/angular.js',
|
||||
'src/angular-mocks.js',
|
||||
'test/matchers.js',
|
||||
'test/angular-mocksSpec.js'
|
||||
],
|
||||
|
||||
'jstdPerf': [
|
||||
'lib/jasmine-1.0.1/jasmine.js',
|
||||
'lib/jasmine/jasmine.js',
|
||||
'lib/jasmine-jstd-adapter/JasmineAdapter.js',
|
||||
'angularSrc',
|
||||
'src/angular-mocks.js',
|
||||
@@ -104,36 +121,35 @@ angularFiles = {
|
||||
|
||||
'jstdPerfExclude': [
|
||||
'src/angular-bootstrap.js',
|
||||
'src/scenario/angular-bootstrap.js',
|
||||
'src/AngularPublic.js'
|
||||
'src/scenario/angular-bootstrap.js'
|
||||
],
|
||||
|
||||
'jstdJquery': [
|
||||
'lib/jasmine-1.0.1/jasmine.js',
|
||||
'lib/jasmine/jasmine.js',
|
||||
'lib/jasmine-jstd-adapter/JasmineAdapter.js',
|
||||
'lib/jquery/jquery.js',
|
||||
'test/jquery_alias.js',
|
||||
'@angularSrc',
|
||||
'example/personalLog/*.js',
|
||||
'test/testabilityPatch.js',
|
||||
'test/matchers.js',
|
||||
'src/scenario/Scenario.js',
|
||||
'src/scenario/output/*.js',
|
||||
'src/jstd-scenario-adapter/*.js',
|
||||
'src/scenario/*.js',
|
||||
'src/angular-mocks.js',
|
||||
'test/mocks.js',
|
||||
'test/scenario/*.js',
|
||||
'test/scenario/output/*.js',
|
||||
'test/jstd-scenario-adapter/*.js',
|
||||
'test/*.js',
|
||||
'test/service/*.js',
|
||||
'test/widget/*.js',
|
||||
'example/personalLog/test/*.js'
|
||||
],
|
||||
|
||||
'jstdJqueryExclude': [
|
||||
'src/angular-bootstrap.js',
|
||||
'src/AngularPublic.js',
|
||||
'src/scenario/angular-bootstrap.js',
|
||||
'test/jquery_remove.js'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
+2
-84
@@ -4,88 +4,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ng-format-negative {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.ng-exception {
|
||||
border: 2px solid #FF0000;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
font-size: smaller;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.ng-validation-error {
|
||||
border: 2px solid #FF0000;
|
||||
}
|
||||
|
||||
|
||||
/*****************
|
||||
* TIP
|
||||
*****************/
|
||||
#ng-callout {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
font-family: Verdana, Arial, Helvetica, sans-serif;
|
||||
vertical-align: baseline;
|
||||
background: transparent;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#ng-callout .ng-arrow-left{
|
||||
background-image: url("data:image/gif;base64,R0lGODlhCwAXAKIAAMzMzO/v7/f39////////wAAAAAAAAAAACH5BAUUAAQALAAAAAALABcAAAMrSLoc/AG8FeUUIN+sGebWAnbKSJodqqlsOxJtqYooU9vvk+vcJIcTkg+QAAA7");
|
||||
background-repeat: no-repeat;
|
||||
background-position: left top;
|
||||
position: absolute;
|
||||
z-index:101;
|
||||
left:-12px;
|
||||
height:23px;
|
||||
width:10px;
|
||||
top:-3px;
|
||||
}
|
||||
|
||||
#ng-callout .ng-arrow-right{
|
||||
background-image: url("data:image/gif;base64,R0lGODlhCwAXAKIAAMzMzO/v7/f39////////wAAAAAAAAAAACH5BAUUAAQALAAAAAALABcAAAMrCLTcoM29yN6k9socs91e5X3EyJloipYrO4ohTMqA0Fn2XVNswJe+H+SXAAA7");
|
||||
background-repeat: no-repeat;
|
||||
background-position: left top;
|
||||
position: absolute;
|
||||
z-index:101;
|
||||
height:23px;
|
||||
width:11px;
|
||||
top:-2px;
|
||||
}
|
||||
|
||||
#ng-callout {
|
||||
position: absolute;
|
||||
z-index:100;
|
||||
border: 2px solid #CCCCCC;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
#ng-callout .ng-content{
|
||||
padding:10px 10px 10px 10px;
|
||||
color:#333333;
|
||||
}
|
||||
|
||||
#ng-callout .ng-title{
|
||||
background-color: #CCCCCC;
|
||||
text-align: left;
|
||||
padding-left: 8px;
|
||||
padding-bottom: 5px;
|
||||
padding-top: 2px;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
|
||||
/*****************
|
||||
* indicators
|
||||
*****************/
|
||||
.ng-input-indicator-wait {
|
||||
background-image: url("data:image/png;base64,R0lGODlhEAAQAPQAAP///wAAAPDw8IqKiuDg4EZGRnp6egAAAFhYWCQkJKysrL6+vhQUFJycnAQEBDY2NmhoaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAAFdyAgAgIJIeWoAkRCCMdBkKtIHIngyMKsErPBYbADpkSCwhDmQCBethRB6Vj4kFCkQPG4IlWDgrNRIwnO4UKBXDufzQvDMaoSDBgFb886MiQadgNABAokfCwzBA8LCg0Egl8jAggGAA1kBIA1BAYzlyILczULC2UhACH5BAkKAAAALAAAAAAQABAAAAV2ICACAmlAZTmOREEIyUEQjLKKxPHADhEvqxlgcGgkGI1DYSVAIAWMx+lwSKkICJ0QsHi9RgKBwnVTiRQQgwF4I4UFDQQEwi6/3YSGWRRmjhEETAJfIgMFCnAKM0KDV4EEEAQLiF18TAYNXDaSe3x6mjidN1s3IQAh+QQJCgAAACwAAAAAEAAQAAAFeCAgAgLZDGU5jgRECEUiCI+yioSDwDJyLKsXoHFQxBSHAoAAFBhqtMJg8DgQBgfrEsJAEAg4YhZIEiwgKtHiMBgtpg3wbUZXGO7kOb1MUKRFMysCChAoggJCIg0GC2aNe4gqQldfL4l/Ag1AXySJgn5LcoE3QXI3IQAh+QQJCgAAACwAAAAAEAAQAAAFdiAgAgLZNGU5joQhCEjxIssqEo8bC9BRjy9Ag7GILQ4QEoE0gBAEBcOpcBA0DoxSK/e8LRIHn+i1cK0IyKdg0VAoljYIg+GgnRrwVS/8IAkICyosBIQpBAMoKy9dImxPhS+GKkFrkX+TigtLlIyKXUF+NjagNiEAIfkECQoAAAAsAAAAABAAEAAABWwgIAICaRhlOY4EIgjH8R7LKhKHGwsMvb4AAy3WODBIBBKCsYA9TjuhDNDKEVSERezQEL0WrhXucRUQGuik7bFlngzqVW9LMl9XWvLdjFaJtDFqZ1cEZUB0dUgvL3dgP4WJZn4jkomWNpSTIyEAIfkECQoAAAAsAAAAABAAEAAABX4gIAICuSxlOY6CIgiD8RrEKgqGOwxwUrMlAoSwIzAGpJpgoSDAGifDY5kopBYDlEpAQBwevxfBtRIUGi8xwWkDNBCIwmC9Vq0aiQQDQuK+VgQPDXV9hCJjBwcFYU5pLwwHXQcMKSmNLQcIAExlbH8JBwttaX0ABAcNbWVbKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICSRBlOY7CIghN8zbEKsKoIjdFzZaEgUBHKChMJtRwcWpAWoWnifm6ESAMhO8lQK0EEAV3rFopIBCEcGwDKAqPh4HUrY4ICHH1dSoTFgcHUiZjBhAJB2AHDykpKAwHAwdzf19KkASIPl9cDgcnDkdtNwiMJCshACH5BAkKAAAALAAAAAAQABAAAAV3ICACAkkQZTmOAiosiyAoxCq+KPxCNVsSMRgBsiClWrLTSWFoIQZHl6pleBh6suxKMIhlvzbAwkBWfFWrBQTxNLq2RG2yhSUkDs2b63AYDAoJXAcFRwADeAkJDX0AQCsEfAQMDAIPBz0rCgcxky0JRWE1AmwpKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICKZzkqJ4nQZxLqZKv4NqNLKK2/Q4Ek4lFXChsg5ypJjs1II3gEDUSRInEGYAw6B6zM4JhrDAtEosVkLUtHA7RHaHAGJQEjsODcEg0FBAFVgkQJQ1pAwcDDw8KcFtSInwJAowCCA6RIwqZAgkPNgVpWndjdyohACH5BAkKAAAALAAAAAAQABAAAAV5ICACAimc5KieLEuUKvm2xAKLqDCfC2GaO9eL0LABWTiBYmA06W6kHgvCqEJiAIJiu3gcvgUsscHUERm+kaCxyxa+zRPk0SgJEgfIvbAdIAQLCAYlCj4DBw0IBQsMCjIqBAcPAooCBg9pKgsJLwUFOhCZKyQDA3YqIQAh+QQJCgAAACwAAAAAEAAQAAAFdSAgAgIpnOSonmxbqiThCrJKEHFbo8JxDDOZYFFb+A41E4H4OhkOipXwBElYITDAckFEOBgMQ3arkMkUBdxIUGZpEb7kaQBRlASPg0FQQHAbEEMGDSVEAA1QBhAED1E0NgwFAooCDWljaQIQCE5qMHcNhCkjIQAh+QQJCgAAACwAAAAAEAAQAAAFeSAgAgIpnOSoLgxxvqgKLEcCC65KEAByKK8cSpA4DAiHQ/DkKhGKh4ZCtCyZGo6F6iYYPAqFgYy02xkSaLEMV34tELyRYNEsCQyHlvWkGCzsPgMCEAY7Cg04Uk48LAsDhRA8MVQPEF0GAgqYYwSRlycNcWskCkApIyEAOwAAAAAAAAAAAA==");
|
||||
background-position: right;
|
||||
background-repeat: no-repeat;
|
||||
ng\:form {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
@ngdoc overview
|
||||
@name angular.inputType
|
||||
@description
|
||||
|
||||
Angular {@link guide/dev_guide.forms forms} allow you to build complex widgets. However for
|
||||
simple widget which are based on HTML input text element a simpler way of providing the validation
|
||||
and parsing is also provided. `angular.inputType` is a short hand for creating a widget which
|
||||
already has the DOM listeners and `$render` method supplied. The only thing which needs to
|
||||
be provided by the developer are the optional `$validate` listener and
|
||||
`$parseModel` or `$parseModel` methods.
|
||||
|
||||
All `inputType` widgets support:
|
||||
|
||||
- CSS classes:
|
||||
- **`ng-valid`**: when widget is valid.
|
||||
- **`ng-invalid`**: when widget is invalid.
|
||||
- **`ng-pristine`**: when widget has not been modified by user action.
|
||||
- **`ng-dirty`**: when has been modified do to user action.
|
||||
|
||||
- Widget properties:
|
||||
- **`$valid`**: When widget is valid.
|
||||
- **`$invalid`**: When widget is invalid.
|
||||
- **`$pristine`**: When widget has not been modified by user interaction.
|
||||
- **`$dirty`**: When user has been modified do to user interaction.
|
||||
- **`$required`**: When the `<input>` element has `required` attribute. This means that the
|
||||
widget will have `REQUIRED` validation error if empty.
|
||||
- **`$disabled`**: When the `<input>` element has `disabled` attribute.
|
||||
- **`$readonly`**: When the `<input>` element has `readonly` attribute.
|
||||
|
||||
- Widget Attribute Validators:
|
||||
- **`required`**: Sets `REQUIRED` validation error key if the input is empty
|
||||
- **`ng:pattern`** Sets `PATTERN` validation error key if the value does not match the
|
||||
RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
|
||||
patterns defined as scope expressions.
|
||||
|
||||
|
||||
|
||||
# Example
|
||||
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<script>
|
||||
angular.inputType('json', function() {
|
||||
this.$parseView = function() {
|
||||
try {
|
||||
this.$modelValue = angular.fromJson(this.$viewValue);
|
||||
if (this.$error.JSON) {
|
||||
this.$emit('$valid', 'JSON');
|
||||
}
|
||||
} catch (e) {
|
||||
this.$emit('$invalid', 'JSON');
|
||||
}
|
||||
}
|
||||
|
||||
this.$parseModel = function() {
|
||||
this.$viewValue = angular.toJson(this.$modelValue);
|
||||
}
|
||||
});
|
||||
|
||||
function Ctrl() {
|
||||
this.data = {
|
||||
framework:'angular',
|
||||
codenames:'supper-powers'
|
||||
}
|
||||
this.required = false;
|
||||
this.disabled = false;
|
||||
this.readonly = false;
|
||||
}
|
||||
</script>
|
||||
<div ng:controller="Ctrl">
|
||||
<form name="myForm">
|
||||
<input type="json" ng:model="data" size="80"
|
||||
ng:required="{{required}}" ng:disabled="{{disabled}}"
|
||||
ng:readonly="{{readonly}}"/><br/>
|
||||
Required: <input type="checkbox" ng:model="required"> <br/>
|
||||
Disabled: <input type="checkbox" ng:model="disabled"> <br/>
|
||||
Readonly: <input type="checkbox" ng:model="readonly"> <br/>
|
||||
<pre>data={{data}}</pre>
|
||||
<pre>myForm={{myForm}}</pre>
|
||||
</form>
|
||||
</div>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should invalidate on wrong input', function() {
|
||||
expect(element('form[name=myForm]').prop('className')).toMatch('ng-valid');
|
||||
input('data').enter('{}');
|
||||
expect(binding('data')).toEqual('data={\n }');
|
||||
input('data').enter('{');
|
||||
expect(element('form[name=myForm]').prop('className')).toMatch('ng-invalid');
|
||||
});
|
||||
</doc:scenario>
|
||||
</doc:example>
|
||||
@@ -0,0 +1,5 @@
|
||||
@ngdoc overview
|
||||
@name angular.module.ng
|
||||
@description
|
||||
|
||||
The `ng` is an angular module which contains all of the core angular services.
|
||||
@@ -1,28 +0,0 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name angular.service
|
||||
@description
|
||||
|
||||
The services API provides objects for carrying out common web app tasks. Service objects are
|
||||
managed by angular's {@link guide/dev_guide.di dependency injection system}.
|
||||
|
||||
* {@link angular.service.$browser $browser } - Provides an instance of a browser object
|
||||
* {@link angular.service.$cookieStore $cookieStore } - Provides key / value storage backed by
|
||||
session cookies
|
||||
* {@link angular.service.$cookies $cookies } - Provides read / write access to browser cookies
|
||||
* {@link angular.service.$defer $defer } - Defers function execution and try / catch block
|
||||
* {@link angular.service.$document $document } - Provides reference to `window.document` element
|
||||
* {@link angular.service.$exceptionHandler $exceptionHandler } - Receives uncaught angular
|
||||
exceptions
|
||||
* {@link angular.service.$hover $hover } -
|
||||
* {@link angular.service.$invalidWidgets $invalidWidgets } - Holds references to invalid widgets
|
||||
* {@link angular.service.$location $location } - Parses the browser location URL
|
||||
* {@link angular.service.$log $log } - Provides logging service
|
||||
* {@link angular.service.$resource $resource } - Creates objects for interacting with RESTful
|
||||
server-side data sources
|
||||
* {@link angular.service.$route $route } - Provides deep-linking services
|
||||
* {@link angular.service.$window $window } - References the browsers `window` object
|
||||
* {@link angular.service.$xhr $xhr} - Generates an XHR request.
|
||||
|
||||
For information on how angular services work and how to write your own services, see {@link
|
||||
guide/dev_guide.services Angular Services} in the angular Developer Guide.
|
||||
@@ -7,28 +7,25 @@
|
||||
* {@link angular.widget Widgets} - Angular custom DOM element
|
||||
* {@link angular.directive Directives} - Angular DOM element attributes
|
||||
* {@link angular.markup Markup} and {@link angular.attrMarkup Attribute Markup}
|
||||
* {@link angular.filter Filters} - Angular output filters
|
||||
* {@link angular.formatter Formatters} - Angular converters for form elements
|
||||
* {@link angular.validator Validators} - Angular input validators
|
||||
* {@link angular.compile angular.compile()} - Template compiler
|
||||
* {@link angular.module.ng.$filter Filters} - Angular output filters
|
||||
* {@link angular.module.ng.$compile $compile} - Template compiler
|
||||
|
||||
## Angular Scope API
|
||||
|
||||
* {@link angular.scope Scope Object} - Angular scope object
|
||||
* {@link angular.module.ng.$rootScope.Scope Scope Object} - Angular scope object
|
||||
|
||||
|
||||
## Angular Services & Dependency Injection API
|
||||
|
||||
* {@link angular.service Angular Services}
|
||||
* {@link angular.module.ng Angular Services}
|
||||
* {@link angular.injector angular.injector() }
|
||||
|
||||
|
||||
## Angular Testing API
|
||||
|
||||
* {@link angular.mock Testing Mocks API} - Mock objects for testing
|
||||
* {@link
|
||||
https://docs.google.com/document/d/11L8htLKrh6c92foV71ytYpiKkeKpM4_a5-9c3HywfIc/edit?hl=en_US
|
||||
Angular Scenario Runner} - Automated scenario testing documentation
|
||||
* {@link angular.module.ngMock Testing Mocks API} - Mock objects for testing
|
||||
* {@link guide/dev_guide.e2e-testing Angular Scenario Runner} - Automated scenario testing
|
||||
documentation
|
||||
|
||||
|
||||
## Angular Utility Functions
|
||||
@@ -66,9 +63,3 @@ Angular Scenario Runner} - Automated scenario testing documentation
|
||||
|
||||
* {@link angular.fromJson angular.fromJson() }
|
||||
* {@link angular.toJson angular.toJson() }
|
||||
|
||||
|
||||
|
||||
## Utility methods for JavaScript types
|
||||
* {@link angular.Object Object API} - Utility functions for JavaScript objects
|
||||
* {@link angular.Array Array API} - Utility functions for JavaScript arrays
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Cookbook: Advanced Form
|
||||
@description
|
||||
@@ -9,9 +8,7 @@ detection, and preventing invalid form submission.
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<script>
|
||||
UserForm.$inject = ['$invalidWidgets'];
|
||||
function UserForm($invalidWidgets){
|
||||
this.$invalidWidgets = $invalidWidgets;
|
||||
function UserForm() {
|
||||
this.state = /^\w\w$/;
|
||||
this.zip = /^\d\d\d\d\d$/;
|
||||
this.master = {
|
||||
@@ -30,43 +27,67 @@ detection, and preventing invalid form submission.
|
||||
}
|
||||
|
||||
UserForm.prototype = {
|
||||
cancel: function(){
|
||||
cancel: function() {
|
||||
this.form = angular.copy(this.master);
|
||||
},
|
||||
|
||||
save: function(){
|
||||
save: function() {
|
||||
this.master = this.form;
|
||||
this.cancel();
|
||||
},
|
||||
|
||||
addContact: function() {
|
||||
this.form.contacts.push({type:'', value:''});
|
||||
},
|
||||
|
||||
removeContact: function(contact) {
|
||||
for ( var i = 0, ii = this.form.contacts.length; i < ii; i++) {
|
||||
if (contact === this.form.contacts[i]) {
|
||||
this.form.contacts.splice(i, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
isCancelDisabled: function() {
|
||||
return angular.equals(this.master, this.form);
|
||||
},
|
||||
|
||||
isSaveDisabled: function() {
|
||||
return this.myForm.$invalid || angular.equals(this.master, this.form);
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
<div ng:controller="UserForm">
|
||||
|
||||
<label>Name:</label><br/>
|
||||
<input type="text" name="form.name" ng:required/> <br/><br/>
|
||||
<form name="myForm">
|
||||
|
||||
<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>Name:</label><br/>
|
||||
<input type="text" ng:model="form.name" required/> <br/><br/>
|
||||
|
||||
<label>Contacts:</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()" ng:disabled="{{master.$equals(form)}}">Cancel</button>
|
||||
<button ng:click="save()" ng:disabled="{{$invalidWidgets.visible() ||
|
||||
master.$equals(form)}}">Save</button>
|
||||
<label>Address:</label> <br/>
|
||||
<input type="text" ng:model="form.address.line1" size="33" required/> <br/>
|
||||
<input type="text" ng:model="form.address.city" size="12" required/>,
|
||||
<input type="text" ng:model="form.address.state" size="2"
|
||||
ng:pattern="state" required/>
|
||||
<input type="text" ng:model="form.address.zip" size="5"
|
||||
ng:pattern="zip" required/><br/><br/>
|
||||
|
||||
<label>Contacts:</label>
|
||||
[ <a href="" ng:click="addContact()">add</a> ]
|
||||
<div ng:repeat="contact in form.contacts">
|
||||
<select ng:model="contact.type">
|
||||
<option>email</option>
|
||||
<option>phone</option>
|
||||
<option>pager</option>
|
||||
<option>IM</option>
|
||||
</select>
|
||||
<input type="text" ng:model="contact.value" required/>
|
||||
[ <a href="" ng:click="removeContact(contact)">X</a> ]
|
||||
</div>
|
||||
<button ng:click="cancel()" ng:disabled="{{isCancelDisabled()}}">Cancel</button>
|
||||
<button ng:click="save()" ng:disabled="{{isSaveDisabled()}}">Save</button>
|
||||
</form>
|
||||
|
||||
<hr/>
|
||||
Debug View:
|
||||
@@ -75,7 +96,7 @@ master.$equals(form)}}">Save</button>
|
||||
</div>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should enable save button', function(){
|
||||
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();
|
||||
@@ -84,13 +105,13 @@ master.$equals(form)}}">Save</button>
|
||||
element(':button:contains(Save)').click();
|
||||
expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy();
|
||||
});
|
||||
it('should enable cancel button', function(){
|
||||
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');
|
||||
expect(element(':input[ng\\:model="form.name"]').val()).toEqual('John Smith');
|
||||
});
|
||||
</doc:scenario>
|
||||
</doc:example>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Cookbook: Resources - Buzz
|
||||
@description
|
||||
@@ -15,11 +14,12 @@ to retrieve Buzz activity and comments.
|
||||
<script>
|
||||
BuzzController.$inject = ['$resource'];
|
||||
function BuzzController($resource) {
|
||||
this.userId = 'googlebuzz';
|
||||
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'}}
|
||||
{ get: {method: 'JSONP', params: {visibility: '@self'}},
|
||||
replies: {method: 'JSONP', params: {visibility: '@self', comments: '@comments'}}
|
||||
});
|
||||
}
|
||||
BuzzController.prototype = {
|
||||
@@ -32,7 +32,7 @@ to retrieve Buzz activity and comments.
|
||||
};
|
||||
</script>
|
||||
<div ng:controller="BuzzController">
|
||||
<input name="userId" value="googlebuzz"/>
|
||||
<input ng:model="userId"/>
|
||||
<button ng:click="fetch()">fetch</button>
|
||||
<hr/>
|
||||
<div class="buzz" ng:repeat="item in activities.data.items">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Cookbook: Deep Linking
|
||||
@description
|
||||
@@ -38,7 +37,7 @@ The two partials are defined in the following URLs:
|
||||
* <a href="./examples/welcome.html" ng:ext-link>./examples/welcome.html</a>
|
||||
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<doc:source jsfiddle="false">
|
||||
<script>
|
||||
AppCntl.$inject = ['$route']
|
||||
function AppCntl($route) {
|
||||
@@ -56,7 +55,7 @@ The two partials are defined in the following URLs:
|
||||
|
||||
function WelcomeCntl($route){}
|
||||
WelcomeCntl.prototype = {
|
||||
greet: function(){
|
||||
greet: function() {
|
||||
alert("Hello " + this.person.name);
|
||||
}
|
||||
};
|
||||
@@ -67,11 +66,11 @@ The two partials are defined in the following URLs:
|
||||
this.cancel();
|
||||
}
|
||||
SettingsCntl.prototype = {
|
||||
cancel: function(){
|
||||
cancel: function() {
|
||||
this.form = angular.copy(this.person);
|
||||
},
|
||||
|
||||
save: function(){
|
||||
save: function() {
|
||||
angular.copy(this.form, this.person);
|
||||
this.$location.path('/welcome');
|
||||
}
|
||||
@@ -89,7 +88,7 @@ The two partials are defined in the following URLs:
|
||||
</div>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should navigate to URL', function(){
|
||||
it('should navigate to URL', function() {
|
||||
element('a:contains(Welcome)').click();
|
||||
expect(element('ng\\:view').text()).toMatch(/Hello anonymous/);
|
||||
element('a:contains(Settings)').click();
|
||||
@@ -106,9 +105,9 @@ The two partials are defined in the following URLs:
|
||||
# Things to notice
|
||||
|
||||
* Routes are defined in the `AppCntl` class. The initialization of the controller causes the
|
||||
initialization of the {@link api/angular.service.$route $route} service with the proper URL
|
||||
initialization of the {@link api/angular.module.ng.$route $route} service with the proper URL
|
||||
routes.
|
||||
* The {@link api/angular.service.$route $route} service then watches the URL and instantiates the
|
||||
* The {@link api/angular.module.ng.$route $route} service then watches the URL and instantiates the
|
||||
appropriate controller when the URL changes.
|
||||
* The {@link api/angular.widget.ng:view ng:view} widget loads the view when the URL changes. It
|
||||
also
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Cookbook: Form
|
||||
@description
|
||||
@@ -11,7 +10,7 @@ allow a user to enter data.
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<script>
|
||||
function FormController(){
|
||||
function FormController() {
|
||||
this.user = {
|
||||
name: 'John Smith',
|
||||
address:{line1: '123 Main St.', city:'Anytown', state:'AA', zip:'12345'},
|
||||
@@ -19,31 +18,44 @@ allow a user to enter data.
|
||||
};
|
||||
this.state = /^\w\w$/;
|
||||
this.zip = /^\d\d\d\d\d$/;
|
||||
|
||||
this.addContact = function() {
|
||||
this.user.contacts.push({type:'', value:''});
|
||||
};
|
||||
|
||||
this.removeContact = function(contact) {
|
||||
for ( var i = 0, ii = this.user.contacts.length; i < ii; i++) {
|
||||
if (contact === this.user.contacts[i]) {
|
||||
this.user.contacts.splice(i, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<div ng:controller="FormController" class="example">
|
||||
|
||||
<label>Name:</label><br/>
|
||||
<input type="text" name="user.name" ng:required/> <br/><br/>
|
||||
<input type="text" ng:model="user.name" 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/>
|
||||
<input type="text" ng:model="user.address.line1" size="33" required> <br/>
|
||||
<input type="text" ng:model="user.address.city" size="12" required>,
|
||||
<input type="text" ng:model="user.address.state" size="2"
|
||||
ng:pattern="state" required>
|
||||
<input type="text" ng:model="user.address.zip" size="5"
|
||||
ng:pattern="zip" required><br/><br/>
|
||||
|
||||
<label>Phone:</label>
|
||||
[ <a href="" ng:click="user.contacts.$add()">add</a> ]
|
||||
[ <a href="" ng:click="addContact()">add</a> ]
|
||||
<div ng:repeat="contact in user.contacts">
|
||||
<select name="contact.type">
|
||||
<select ng:model="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> ]
|
||||
<input type="text" ng:model="contact.value" required/>
|
||||
[ <a href="" ng:click="removeContact(contact)">X</a> ]
|
||||
</div>
|
||||
<hr/>
|
||||
Debug View:
|
||||
@@ -52,35 +64,37 @@ ng:validate="regexp:zip"/><br/><br/>
|
||||
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should show debug', function(){
|
||||
it('should show debug', function() {
|
||||
expect(binding('user')).toMatch(/John Smith/);
|
||||
});
|
||||
it('should add contact', function(){
|
||||
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(){
|
||||
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"]').prop('className'))
|
||||
.not().toMatch(/ng-validation-error/);
|
||||
it('should validate zip', function() {
|
||||
expect(using('.example').
|
||||
element(':input[ng\\:model="user.address.zip"]').
|
||||
prop('className')).not().toMatch(/ng-invalid/);
|
||||
using('.example').input('user.address.zip').enter('abc');
|
||||
expect(using('.example').element(':input[name="user.address.zip"]').prop('className'))
|
||||
.toMatch(/ng-validation-error/);
|
||||
expect(using('.example').
|
||||
element(':input[ng\\:model="user.address.zip"]').
|
||||
prop('className')).toMatch(/ng-invalid/);
|
||||
});
|
||||
|
||||
it('should validate state', function(){
|
||||
expect(using('.example').element(':input[name="user.address.state"]').prop('className'))
|
||||
.not().toMatch(/ng-validation-error/);
|
||||
it('should validate state', function() {
|
||||
expect(using('.example').element(':input[ng\\:model="user.address.state"]').prop('className'))
|
||||
.not().toMatch(/ng-invalid/);
|
||||
using('.example').input('user.address.state').enter('XXX');
|
||||
expect(using('.example').element(':input[name="user.address.state"]').prop('className'))
|
||||
.toMatch(/ng-validation-error/);
|
||||
expect(using('.example').element(':input[ng\\:model="user.address.state"]').prop('className'))
|
||||
.toMatch(/ng-invalid/);
|
||||
});
|
||||
</doc:scenario>
|
||||
</doc:example>
|
||||
@@ -90,11 +104,11 @@ ng:validate="regexp:zip"/><br/><br/>
|
||||
|
||||
* The user data model is initialized {@link api/angular.directive.ng:controller controller} and is
|
||||
available in
|
||||
the {@link api/angular.scope scope} with the initial data.
|
||||
the {@link api/angular.module.ng.$rootScope.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 api/angular.widget.HTML input widgets} simply refer to the model and are auto bound.
|
||||
* The inputs {@link api/angular.validator validate}. (Try leaving them blank or entering non digits
|
||||
* The {@link api/angular.widget.input input widgets} simply refer to the model and are data-bound.
|
||||
* The inputs {@link guide/dev_guide.forms 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
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Cookbook: Hello World
|
||||
@description
|
||||
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
Your name: <input type="text" name="name" value="World"/>
|
||||
<hr/>
|
||||
Hello {{name}}!
|
||||
<script>
|
||||
function HelloCntl() {
|
||||
this.name = 'World';
|
||||
}
|
||||
</script>
|
||||
<div ng:controller="HelloCntl">
|
||||
Your name: <input type="text" ng:model="name" value="World"/>
|
||||
<hr/>
|
||||
Hello {{name}}!
|
||||
</div>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('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');
|
||||
@@ -23,9 +29,9 @@
|
||||
Take a look through the source and note:
|
||||
|
||||
* The script tag that {@link guide/dev_guide.bootstrap bootstraps} the angular environment.
|
||||
* The text {@link api/angular.widget.HTML input widget} which is bound to the greeting name text.
|
||||
* The text {@link api/angular.widget.input 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 api/angular.scope scope}.
|
||||
* The implicit presence of the `name` variable which is in the root {@link api/angular.module.ng.$rootScope.Scope scope}.
|
||||
* The double curly brace `{{markup}}`, which binds the name variable to the greeting text.
|
||||
* The concept of {@link guide/dev_guide.templates.databinding data binding}, which reflects any
|
||||
changes to the
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Cookbook
|
||||
@description
|
||||
@@ -45,7 +44,7 @@ allowing you to send links to specific screens in your app.
|
||||
|
||||
# Services
|
||||
|
||||
{@link api/angular.service Services}: Services are long lived objects in your applications that are
|
||||
{@link api/angular.module.ng 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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Cookbook: MVC
|
||||
@description
|
||||
@@ -36,7 +35,7 @@ no connection between the controller and the view.
|
||||
this.setUrl();
|
||||
}
|
||||
},
|
||||
reset: function(){
|
||||
reset: function() {
|
||||
this.board = [
|
||||
['', '', ''],
|
||||
['', '', ''],
|
||||
@@ -46,7 +45,7 @@ no connection between the controller and the view.
|
||||
this.winner = '';
|
||||
this.setUrl();
|
||||
},
|
||||
grade: function(){
|
||||
grade: function() {
|
||||
var b = this.board;
|
||||
this.winner =
|
||||
row(0) || row(1) || row(2) ||
|
||||
@@ -57,7 +56,7 @@ no connection between the controller and the view.
|
||||
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(){
|
||||
setUrl: function() {
|
||||
var rows = [];
|
||||
angular.forEach(this.board, function(row){
|
||||
rows.push(row.join(','));
|
||||
@@ -91,7 +90,7 @@ no connection between the controller and the view.
|
||||
</div>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should play a game', function(){
|
||||
it('should play a game', function() {
|
||||
piece(1, 1);
|
||||
expect(binding('nextMove')).toEqual('O');
|
||||
piece(3, 1);
|
||||
@@ -122,4 +121,4 @@ board variable.
|
||||
* 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
|
||||
api/angular.scope.$watch $watch()} to set up a listener that invokes `readUrl()` when needed.
|
||||
api/angular.module.ng.$rootScope.Scope#$watch $watch()} to set up a listener that invokes `readUrl()` when needed.
|
||||
|
||||
@@ -1,89 +1,37 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Initializing Angular: Automatic Initiialization
|
||||
@name Developer Guide: Initializing Angular: Automatic Initialization
|
||||
@description
|
||||
|
||||
Angular initializes automatically when you load the angular script into your page, specifying
|
||||
angular's `ng:autobind` attribute with no arguments:
|
||||
Angular initializes automatically when you load the angular script into your page that contains an element
|
||||
with `ng:app` directive:
|
||||
|
||||
<script src="angular.js" ng:autobind>
|
||||
<pre>
|
||||
<!doctype html>
|
||||
<html ng:app>
|
||||
<head>
|
||||
<script src="angular.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
I can add: {{ 1+2 }}.
|
||||
</body>
|
||||
</html>
|
||||
</pre>
|
||||
|
||||
From a high-level view, this is what happens during angular's automatic initialization process:
|
||||
|
||||
1. The browser loads the page, and then runs the angular script.
|
||||
1. The browser loads the page, and then runs the angular script. Angular waits for the
|
||||
`DOMContentLoaded` (or 'Load') event to attempt to bootstrap.
|
||||
|
||||
The `ng:autobind` attribute tells angular to compile and manage the whole HTML document. The
|
||||
compilation phase is initiated in the page's `onLoad()` handler. Angular doesn't begin processing
|
||||
the page until after the page load is complete.
|
||||
|
||||
2. Angular finds the root of the HTML document and creates the global variable `angular` in the
|
||||
global namespace. Everything that angular subsequently creates is bound to fields in this global
|
||||
object.
|
||||
|
||||
3. Angular walks the DOM looking for angular widgets, directives, and markup (such as `ng:init` or
|
||||
`ng:repeat`). As angular encounters these, it creates child scopes as necessary and attaches them
|
||||
to the DOM, registers listeners on those scopes, associates any controller functions with their
|
||||
data and their part of the view, and ultimately constructs a runnable application. The resulting
|
||||
app features two-way data-binding and a nice separation between data, presentation, and business
|
||||
logic.
|
||||
|
||||
4. For the duration of the application session (while the page is loaded), angular monitors the
|
||||
state of the application, and updates the view and the data model whenever the state of either one
|
||||
changes.
|
||||
|
||||
For details on how the compiler works, see {@link dev_guide.compiler Angular HTML Compiler}.
|
||||
2. Angular looks for the `ng:app` directive. If found it then proceeds to compile the DOM element and its children.
|
||||
Optionally the `ng:app` may specify a {@link api/angular.module module} to load before the compilation. For details on
|
||||
how the compiler works, see {@link dev_guide.compiler Angular HTML Compiler}.
|
||||
|
||||
|
||||
## Initialization Options
|
||||
|
||||
The reason why `ng:autobind` exists is because angular should not assume that the entire HTML
|
||||
The reason why `ng:app` exists is because angular should not assume that the entire HTML
|
||||
document should be processed just because the `angular.js` script is included. In order to compile
|
||||
only a part of the document, specify the ID of the element you want to use for angular's root
|
||||
element as the value of the `ng:autobind` attribute:
|
||||
|
||||
ng:autobind="angularContent"
|
||||
|
||||
|
||||
## Auto-bootstrap with `#autobind`
|
||||
|
||||
In some rare cases you can't define the `ng:` prefix before the script tag's attribute (for
|
||||
example, in some CMS systems). In those situations it is possible to auto-bootstrap angular by
|
||||
appending `#autobind` to the `<script src=...>` URL, like in this snippet:
|
||||
|
||||
<pre>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="text/javascript"
|
||||
src="http://code.angularjs.org/angular.js#autobind"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div xmlns:ng="http://angularjs.org">
|
||||
Hello {{'world'}}!
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</pre>
|
||||
|
||||
As with `ng:autobind`, you can specify an element id that should be exclusively targeted for
|
||||
compilation as the value of the `#autobind`, for example: `#autobind=angularContent`.
|
||||
|
||||
## Filename Restrictions for Auto-bootstrap
|
||||
|
||||
In order for us to find the auto-bootstrap from a script attribute or URL fragment, the value of
|
||||
the `script` `src` attribute that loads the angular script must match one of these naming
|
||||
conventions:
|
||||
|
||||
- `angular.js`
|
||||
- `angular-min.js`
|
||||
- `angular-x.x.x.js`
|
||||
- `angular-x.x.x.min.js`
|
||||
- `angular-x.x.x-xxxxxxxx.js` (dev snapshot)
|
||||
- `angular-x.x.x-xxxxxxxx.min.js` (dev snapshot)
|
||||
- `angular-bootstrap.js` (used for development of angular)
|
||||
|
||||
Optionally, any of the filename formats above can be prepended with a relative or absolute URL that
|
||||
ends with `/`.
|
||||
only a part of the document set the `ng:app` on the root element of this portion.
|
||||
|
||||
## Global Angular Object
|
||||
|
||||
@@ -98,4 +46,4 @@ APIs are bound to fields of this global object.
|
||||
|
||||
## Related API
|
||||
|
||||
{@link api/angular.compile Compiler API}
|
||||
{@link api/angular.module.ng.$compile Compiler API}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Initializing Angular: Manual Initialization
|
||||
@description
|
||||
@@ -8,7 +7,7 @@ angular, but advanced users who want more control over the initialization proces
|
||||
the manual bootstrapping method instead.
|
||||
|
||||
The best way to get started with manual bootstrapping is to look at the what happens when you use
|
||||
{@link api/angular.directive.ng:autobind ng:autobind}, by showing each step of the process
|
||||
{@link api/angular.directive.ng:app ng:app}, by showing each step of the process
|
||||
explicitly.
|
||||
|
||||
<pre>
|
||||
@@ -18,7 +17,7 @@ explicitly.
|
||||
<script src="http://code.angularjs.org/angular.js"></script>
|
||||
<script>
|
||||
angular.element(document).ready(function() {
|
||||
angular.compile(document)();
|
||||
angular.bootstrap(document);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
@@ -32,7 +31,7 @@ This is the sequence that your code should follow if you bootstrap angular on yo
|
||||
|
||||
1. After the page is loaded, find the root of the HTML template, which is typically the root of
|
||||
the document.
|
||||
2. Run angular's {@link dev_guide.compiler Angular HTML compiler}, which converts a template into
|
||||
2. Call {@link api/angular.bootstrap} to {@link dev_guide.compiler compile} the template into
|
||||
an executable, bi-directionally bound application.
|
||||
|
||||
|
||||
@@ -44,4 +43,4 @@ an executable, bi-directionally bound application.
|
||||
|
||||
## Related API
|
||||
|
||||
{@link api/angular.compile Compiler API}
|
||||
{@link api/angular.module.ng.$compile Compiler API}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Initializing Angular
|
||||
@description
|
||||
@@ -8,20 +7,20 @@ angular should process and manage the page. To initialize angular you do the fol
|
||||
|
||||
* Specify the angular namespace in the `<html>` page
|
||||
* Choose which flavor of angular script to load (debug or production)
|
||||
* Specify whether or not angular should process and manage the page automatically (`ng:autobind`)
|
||||
* Specify whether or not angular should process and manage the page automatically (`ng:app`)
|
||||
|
||||
The simplest way to initialize angular is to load the angular script and tell angular to compile
|
||||
and manage the whole page. You do this as follows:
|
||||
|
||||
<pre>
|
||||
<!doctype html>
|
||||
<html xmlns:ng="http://angularjs.org">
|
||||
<html xmlns:ng="http://angularjs.org" ng:app>
|
||||
<head>
|
||||
...
|
||||
</head>
|
||||
<body>
|
||||
...
|
||||
<script src="angular.js" ng:autobind>
|
||||
<script src="angular.js">
|
||||
</body>
|
||||
</pre>
|
||||
|
||||
@@ -32,7 +31,7 @@ and manage the whole page. You do this as follows:
|
||||
|
||||
You need to declare the angular namespace declaration in the following cases:
|
||||
|
||||
* For all types of browser if you are using XHTML.
|
||||
* For all browsers if you are using XHTML.
|
||||
* For Internet Explorer older than version 9 (because older versions of IE do not render widgets
|
||||
properly for either HTML or XHTML).
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Angular HTML Compiler: Directives: Creating Custom Angular Directives
|
||||
@description
|
||||
@@ -24,7 +23,7 @@ angular.directive('ng:bind', function(expression, compiledElement) {
|
||||
The angular compiler exposes methods that you may need to use when writing your own widgets and
|
||||
directives. For example, the `descend()` method lets you control whether the compiler ignores or
|
||||
processes child elements of the element it is compiling. For information on this and other
|
||||
compiler methods, see the {@link api/angular.compile Compiler API doc}.
|
||||
compiler methods, see the {@link api/angular.module.ng.$compile Compiler API doc}.
|
||||
|
||||
|
||||
## Related Docs
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Angular HTML Compiler: Understanding Angular Directives
|
||||
@description
|
||||
@@ -16,11 +15,11 @@ directives per element.
|
||||
You add angular directives to a standard HTML tag as in the following example, in which we have
|
||||
added the {@link api/angular.directive.ng:click ng:click} directive to a button tag:
|
||||
|
||||
<button name="button1" ng:click="foo()">Click This</button>
|
||||
<button ng:click="foo()">Click This</button>
|
||||
|
||||
In the example above, `name` is the standard HTML attribute, and `ng:click` is the angular
|
||||
directive. The `ng:click` directive lets you implement custom behavior in an associated controller
|
||||
function.
|
||||
The `ng:click` directive lets you specify click event handlers directly in the template. Unlike the
|
||||
evil `onclick` attribute, the expression associated with the `ng:click` directive is always executed
|
||||
in the context of the current angular scope.
|
||||
|
||||
In the next example, we add the {@link api/angular.directive.ng:bind ng:bind} directive to a
|
||||
`<span>` tag:
|
||||
@@ -31,8 +30,9 @@ The `ng:bind` directive tells angular to set up {@link dev_guide.templates.datab
|
||||
binding} between the data model and the view for the specified expression. When the angular {@link
|
||||
dev_guide.compiler compiler} encounters an `ng:bind` directive in a template, it passes the
|
||||
attribute value to the `ng:bind` function, which in turn sets up the data binding. On any change to
|
||||
the expression in the model, the view is updated to display the span text with the changed
|
||||
expression value.
|
||||
the model that would change the result of the expression, the view is updated and the text of the
|
||||
span element will reflect the new value. In the example above, the model is represented by two
|
||||
constants, so nothing will ever change - Sorry!
|
||||
|
||||
|
||||
## Related Topics
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Angular HTML Compiler: Comparing Directives and Attribute Widgets
|
||||
@description
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Angular HTML Compiler: Extending the Angular Compiler
|
||||
@description
|
||||
@@ -94,4 +93,4 @@ corresponding spans.
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.compile angular.compile()}
|
||||
* {@link api/angular.module.ng.$compile $compile()}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Angular HTML Compiler: Understanding Angular Markup
|
||||
@description
|
||||
@@ -90,4 +89,4 @@ angular.attrMarkup('extraClass', function(attrValue, attrName, element){
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.compile Compiler API Reference}
|
||||
* {@link api/angular.module.ng.$compile Compiler API Reference}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Angular HTML Compiler
|
||||
@description
|
||||
@@ -24,4 +23,4 @@ All compilation takes place in the web browser; no server is involved.
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.compile Angular Compiler API}
|
||||
* {@link api/angular.module.ng.$compile Angular Compiler API}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Angular HTML Compiler: Testing a New DOM Element
|
||||
@description
|
||||
@@ -15,4 +14,4 @@
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.compile angular.compile()}
|
||||
* {@link api/angular.module.ng.$compile $compile()}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Angular HTML Compiler: Understanding How the Compiler Works
|
||||
@description
|
||||
@@ -7,8 +6,7 @@ Every {@link api/angular.widget widget}, {@link api/angular.directive directive}
|
||||
dev_guide.compiler.markup markup} is defined with a compile function, which the angular compiler
|
||||
executes on each widget or directive it encounters. The compile function optionally returns a link
|
||||
function. This compilation process happens automatically when the page is loaded when you specify
|
||||
`ng:autobind` in the script tag from which you load the angular script file. (See {@link
|
||||
dev_guide.bootstrap Initializing Angular}.)
|
||||
`ng:app` on the root element of the application. (See {@link dev_guide.bootstrap Initializing Angular}.)
|
||||
|
||||
The compile and link functions are related as follows:
|
||||
|
||||
@@ -21,7 +19,7 @@ dataset]"`), the link function gets called to set up a listener on each element
|
||||
|
||||
Note that angular's built-in widgets, directives, and markup have predefined compile and link
|
||||
functions that you don't need to modify. When you create your own widgets, directives, or markup,
|
||||
you must write compile and link functions for them. Refer to the {@link api/angular.compile
|
||||
you must write compile and link functions for them. Refer to the {@link api/angular.module.ng.$compile
|
||||
Compiler API} for details.
|
||||
|
||||
When the angular compiler compiles a page, it proceeds through 3 phases: Compile, Create Root
|
||||
@@ -54,7 +52,7 @@ Note that while the compile function is executed exactly once, the link function
|
||||
multiple times, for example, once for each iteration in a repeater.
|
||||
|
||||
The angular compiler exposes methods that you will need to make use of when writing your own
|
||||
widgets and directives. For information on these methods, see the {@link api/angular.compile
|
||||
widgets and directives. For information on these methods, see the {@link api/angular.module.ng.$compile
|
||||
Compiler API doc}.
|
||||
|
||||
|
||||
@@ -66,4 +64,4 @@ Compiler API doc}.
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.compile angular.compile()}
|
||||
* {@link api/angular.module.ng.$compile $compile()}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Angular HTML Compiler: Widgets: Creating Custom Widgets
|
||||
@description
|
||||
@@ -61,7 +60,7 @@ angular.widget('@my:watch', function(expression, compileElement) {
|
||||
angular.widget('my:time', function(compileElement){
|
||||
compileElement.css('display', 'block');
|
||||
return function(linkElement){
|
||||
function update(){
|
||||
function update() {
|
||||
linkElement.text('Current time is: ' + new Date());
|
||||
setTimeout(update, 1000);
|
||||
}
|
||||
@@ -81,7 +80,7 @@ angular.widget('my:time', function(compileElement){
|
||||
The angular compiler exposes methods that you may need to use of when writing your own widgets and
|
||||
directives. For example, the `descend()` method lets you control whether the compiler ignores or
|
||||
processes child elements of the element it is compiling. For information on this and other
|
||||
compiler methods, see the {@link api/angular.compile Compiler API doc}.
|
||||
compiler methods, see the {@link api/angular.module.ng.$compile Compiler API doc}.
|
||||
|
||||
|
||||
## Related Topics
|
||||
@@ -93,4 +92,4 @@ compiler methods, see the {@link api/angular.compile Compiler API doc}.
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.compile Compiler API}
|
||||
* {@link api/angular.module.ng.$compile Compiler API}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Angular HTML Compiler: Understanding Angular Widgets
|
||||
@description
|
||||
@@ -33,4 +32,4 @@ dev_guide.compiler.widgets.creating_widgets Creating Custom Widgets}.
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.compile Compiler API}
|
||||
* {@link api/angular.module.ng.$compile Compiler API}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: About Dependency Injection (DI)
|
||||
@description
|
||||
@@ -29,5 +28,5 @@ book.
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.service Service API}
|
||||
* {@link api/angular.module.ng Service API}
|
||||
* {@link api/angular.injector Angular Injector API}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: DI: Understanding DI in Angular
|
||||
@description
|
||||
@@ -8,15 +7,15 @@ While DI is widely used in statically typed languages such as Java or C++, it ha
|
||||
used in JavaScript. Angular brings the benefits of DI into JavaScript apps.
|
||||
|
||||
In angular, DI is implemented as a subsystem that manages dependencies between services,
|
||||
controllers, widgets, and filters. The most important of these are {@link api/angular.service
|
||||
services}.
|
||||
controllers, widgets, and filters.
|
||||
|
||||
Services are objects that handle common tasks in web applications. Angular provides several{@link
|
||||
api/angular.service built-in services}, and you can create your own custom services.
|
||||
Services are objects that handle common tasks in web applications. Angular provides several {@link
|
||||
api/angular.module.ng built-in services}, and you can create your
|
||||
{@link dev_guide.services.creating_services own custom services}.
|
||||
|
||||
The main job of angular's DI subsystem is to provide services to angular components that depend on
|
||||
them. The way the DI subsystem provides services is as follows: all services are registered with
|
||||
angular's {@link api/angular.service service API}, and all components that depend on services
|
||||
angular's {@link api/angular.module.ng service API}, and all components that depend on services
|
||||
define those dependencies as a property (`$inject`). With this information, the DI subsystem
|
||||
manages the creation of service objects and the provision of those objects to the components that
|
||||
need them, at the time they need them. The following illustration steps through the sequence of
|
||||
@@ -26,34 +25,23 @@ events:
|
||||
|
||||
In the illustration above, the dependency injection sequence proceeds as follows:
|
||||
|
||||
1. Service factory functions are registered with angular's service factory repository.
|
||||
2. `ng:autobind` triggers angular's bootstrap sequence, during which angular compiles the template,
|
||||
creates the root scope, and creates the dependency injector.
|
||||
3. The `ng:controller` directive implicitly creates a new child scope, augmented by the application
|
||||
of the `PhoneListCtrl` controller function.
|
||||
4. The Injector identifies the `$xhr` service as `PhoneListCtrl` controller's only dependency.
|
||||
5. The Injector checks if the `$xhr` service has already been instantiated, and if not uses the
|
||||
factory function from the service factory repository to construct it.
|
||||
6. DI provides the instance of $xhr service to the PhoneListCtrl controller constructor
|
||||
1. Module "phonecat" is created and all the service providers are registered with this module.
|
||||
(the "ng" module is created by Angular behind the scenes as well)
|
||||
2. `ng:app` triggers bootstrap sequence on given element, during which angular creates injector,
|
||||
loads "phonecat" and "ng" modules and compiles the template.
|
||||
3. The `ng:controller` directive implicitly creates a new child scope and instantiates
|
||||
`PhoneListCtrl` controller.
|
||||
4. Injector identifies the `$http` service as `PhoneListCtrl` controller's only dependency.
|
||||
5. Injector checks its instances cache whether the `$http` service has already been instantiated.
|
||||
If not uses the provider from the available modules to construct it.
|
||||
6. Injector provides the instance of `$http` service to the `PhoneListCtrl` controller constructor.
|
||||
|
||||
|
||||
## How Scope Relates to DI
|
||||
|
||||
The {@link api/angular.injector injector} is responsible for resolving the service dependencies in
|
||||
the application. It gets created and configured with the creation of a root scope. The injector
|
||||
caches instances of services, with the services cache bound to the root scope.
|
||||
The root scope of the application is just a service that is available for injection to any part of
|
||||
the application under the service name "$rootScope".
|
||||
|
||||
Different root scopes have different instances of the injector. While typical angular applications
|
||||
will only have one root scope (and hence the services will act like application singletons), in
|
||||
tests it is important to not share singletons across test invocations for isolation reasons. We
|
||||
achieve the necessary isolation by having each test create its own separate root scope.
|
||||
|
||||
<pre>
|
||||
// create a root scope
|
||||
var rootScope = angular.scope();
|
||||
// access the service locator
|
||||
var myService = rootScope.$service('myService');
|
||||
</pre>
|
||||
|
||||
## Inferring dependencies from the signature of the factory function or constructor
|
||||
|
||||
@@ -75,7 +63,9 @@ equivalent:
|
||||
|
||||
<pre>
|
||||
// given a user defined service
|
||||
angular.service('serviceA', ...);
|
||||
angular.module('module1', [], function($provide) {
|
||||
$provide.factory('serviceA', ...);
|
||||
});
|
||||
|
||||
// inject '$window', 'serviceA', curry 'name';
|
||||
function fnA($window, serviceA, name){};
|
||||
@@ -103,4 +93,4 @@ code and insert the `$inject` into the source code so that it can be minified/ob
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.service Services API}
|
||||
* {@link api/angular.module.ng Services API}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: DI: Using DI in Controllers
|
||||
@description
|
||||
@@ -15,15 +14,15 @@ MyController.$inject = ['$route'];
|
||||
</pre>
|
||||
|
||||
In this example, the `MyController` constructor function takes one argument, the {@link
|
||||
api/angular.service.$route $route} service. Angular is then responsible for supplying the instance
|
||||
api/angular.module.ng.$route $route} service. Angular is then responsible for supplying the instance
|
||||
of `$route` to the controller when the constructor is instantiated. There are two ways to cause
|
||||
controller instantiation – by configuring routes with the `$route` service, or by referencing the
|
||||
controller from the HTML template, as follows:
|
||||
|
||||
<pre>
|
||||
<!doctype html>
|
||||
<html xmlns:ng="http://angularjs.org" ng:controller="MyController">
|
||||
<script src="http://code.angularjs.org/angular.min.js" ng:autobind></script>
|
||||
<html xmlns:ng="http://angularjs.org" ng:controller="MyController" ng:app>
|
||||
<script src="http://code.angularjs.org/angular.min.js"></script>
|
||||
<body>
|
||||
...
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: E2E Testing
|
||||
@description
|
||||
|
||||
As applications grow in size and complexity, it becomes unrealistic to rely on manual testing to
|
||||
verify the correctness of new features, catch bugs and notice regressions.
|
||||
|
||||
To solve this problem, we have built an Angular Scenario Runner which simulates user interactions
|
||||
that will help you verify the health of your Angular application.
|
||||
|
||||
# Overview
|
||||
You will write scenario tests in JavaScript, which describe how your application should behave,
|
||||
given a certain interaction in a specific state. A scenario is comprised of one or more it blocks
|
||||
(you can think of these as the requirements of your application), which in turn are made of
|
||||
**commands** and **expectations**. Commands tell the Runner to do something with the application
|
||||
(such as navigate to a page or click on a button), and expectations tell the Runner to assert
|
||||
something about the state (such as the value of a field or the current URL). If any expectation
|
||||
fails, the runner marks the `it` as "failed" and continues on to the next one. Scenarios may also
|
||||
have **beforeEach** and **afterEach** blocks, which will be run before (or after) each `it` block,
|
||||
regardless of whether they pass or fail.
|
||||
|
||||
<img src="img/guide/scenario_runner.png">
|
||||
|
||||
In addition to the above elements, scenarios may also contain helper functions to avoid duplicating
|
||||
code in the `it` blocks.
|
||||
|
||||
Here is an example of a simple scenario:
|
||||
<pre>
|
||||
describe('Buzz Client', function() {
|
||||
it('should filter results', function() {
|
||||
input('user').enter('jacksparrow');
|
||||
element(':button').click();
|
||||
expect(repeater('ul li').count()).toEqual(10);
|
||||
input('filterText').enter('Bees');
|
||||
expect(repeater('ul li').count()).toEqual(1);
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
This scenario describes the requirements of a Buzz Client, specifically, that it should be able to
|
||||
filter the stream of the user. It starts by entering a value in the 'user' input field, clicking
|
||||
the only button on the page, and then it verifies that there are 10 items listed. It then enters
|
||||
'Bees' in the 'filterText' input field and verifies that the list is reduced to a single item.
|
||||
|
||||
The API section below lists the available commands and expectations for the Runner.
|
||||
|
||||
# API
|
||||
Source: {@link https://github.com/angular/angular.js/blob/master/src/scenario/dsl.js}
|
||||
|
||||
## pause()
|
||||
Pauses the execution of the tests until you call `resume()` in the console (or click the resume
|
||||
link in the Runner UI).
|
||||
|
||||
## sleep(seconds)
|
||||
Pauses the execution of the tests for the specified number of `seconds`.
|
||||
|
||||
## browser().navigateTo(url)
|
||||
Loads the `url` into the test frame.
|
||||
|
||||
## browser().navigateTo(url, fn)
|
||||
Loads the URL returned by `fn` into the testing frame. The given `url` is only used for the test
|
||||
output. Use this when the destination URL is dynamic (that is, the destination is unknown when you
|
||||
write the test).
|
||||
|
||||
## browser().reload()
|
||||
Refreshes the currently loaded page in the test frame.
|
||||
|
||||
## browser().window().href()
|
||||
Returns the window.location.href of the currently loaded page in the test frame.
|
||||
|
||||
## browser().window().path()
|
||||
Returns the window.location.pathname of the currently loaded page in the test frame.
|
||||
|
||||
## browser().window().search()
|
||||
Returns the window.location.search of the currently loaded page in the test frame.
|
||||
|
||||
## browser().window().hash()
|
||||
Returns the window.location.hash (without `#`) of the currently loaded page in the test frame.
|
||||
|
||||
## browser().location().url()
|
||||
Returns the {@link api/angular.module.ng.$location $location.url()} of the currently loaded page in
|
||||
the test frame.
|
||||
|
||||
## browser().location().path()
|
||||
Returns the {@link api/angular.module.ng.$location $location.path()} of the currently loaded page in
|
||||
the test frame.
|
||||
|
||||
## browser().location().search()
|
||||
Returns the {@link api/angular.module.ng.$location $location.search()} of the currently loaded page
|
||||
in the test frame.
|
||||
|
||||
## browser().location().hash()
|
||||
Returns the {@link api/angular.module.ng.$location $location.hash()} of the currently loaded page in
|
||||
the test frame.
|
||||
|
||||
## expect(future).{matcher}
|
||||
Asserts the value of the given `future` satisfies the `matcher`. All API statements return a
|
||||
`future` object, which get a `value` assigned after they are executed. Matchers are defined using
|
||||
`angular.scenario.matcher`, and they use the value of futures to run the expectation. For example:
|
||||
`expect(browser().location().href()).toEqual('http://www.google.com')`
|
||||
|
||||
## expect(future).not().{matcher}
|
||||
Asserts the value of the given `future` satisfies the negation of the `matcher`.
|
||||
|
||||
## using(selector, label)
|
||||
Scopes the next DSL element selection.
|
||||
|
||||
## binding(name)
|
||||
Returns the value of the first binding matching the given `name`.
|
||||
|
||||
## input(name).enter(value)
|
||||
Enters the given `value` in the text field with the given `name`.
|
||||
|
||||
## input(name).check()
|
||||
Checks/unchecks the checkbox with the given `name`.
|
||||
|
||||
## input(name).select(value)
|
||||
Selects the given `value` in the radio button with the given `name`.
|
||||
|
||||
## input(name).val()
|
||||
Returns the current value of an input field with the given `name`.
|
||||
|
||||
## repeater(selector, label).count()
|
||||
Returns the number of rows in the repeater matching the given jQuery `selector`. The `label` is
|
||||
used for test ouput.
|
||||
|
||||
## repeater(selector, label).row(index)
|
||||
Returns an array with the bindings in the row at the given `index` in the repeater matching the
|
||||
given jQuery `selector`. The `label` is used for test output.
|
||||
|
||||
## repeater(selector, label).column(binding)
|
||||
Returns an array with the values in the column with the given `binding` in the repeater matching
|
||||
the given jQuery `selector`. The `label` is used for test output.
|
||||
|
||||
## select(name).option(value)
|
||||
Picks the option with the given `value` on the select with the given `name`.
|
||||
|
||||
## select(name).option(value1, value2...)
|
||||
Picks the options with the given `values` on the multi select with the given `name`.
|
||||
|
||||
## element(selector, label).count()
|
||||
Returns the number of elements that match the given jQuery `selector`. The `label` is used for test
|
||||
output.
|
||||
|
||||
## element(selector, label).click()
|
||||
Clicks on the element matching the given jQuery `selector`. The `label` is used for test output.
|
||||
|
||||
## element(selector, label).query(fn)
|
||||
Executes the function `fn(selectedElements, done)`, where selectedElements are the elements that
|
||||
match the given jQuery `selector` and `done` is a function that is called at the end of the `fn`
|
||||
function. The `label` is used for test output.
|
||||
|
||||
## element(selector, label).{method}()
|
||||
Returns the result of calling `method` on the element matching the given jQuery `selector`, where
|
||||
`method` can be any of the following jQuery methods: `val`, `text`, `html`, `height`,
|
||||
`innerHeight`, `outerHeight`, `width`, `innerWidth`, `outerWidth`, `position`, `scrollLeft`,
|
||||
`scrollTop`, `offset`. The `label` is used for test output.
|
||||
|
||||
## element(selector, label).{method}(value)
|
||||
Executes the `method` passing in `value` on the element matching the given jQuery `selector`, where
|
||||
`method` can be any of the following jQuery methods: `val`, `text`, `html`, `height`,
|
||||
`innerHeight`, `outerHeight`, `width`, `innerWidth`, `outerWidth`, `position`, `scrollLeft`,
|
||||
`scrollTop`, `offset`. The `label` is used for test output.
|
||||
|
||||
## element(selector, label).{method}(key)
|
||||
Returns the result of calling `method` passing in `key` on the element matching the given jQuery
|
||||
`selector`, where `method` can be any of the following jQuery methods: `attr`, `prop`, `css`. The
|
||||
`label` is used for test output.
|
||||
|
||||
## element(selector, label).{method}(key, value)
|
||||
Executes the `method` passing in `key` and `value` on the element matching the given jQuery
|
||||
`selector`, where `method` can be any of the following jQuery methods: `attr`, `prop`, `css`. The
|
||||
`label` is used for test output.
|
||||
|
||||
JavaScript is a dynamically typed language which comes with great power of expression, but it also
|
||||
come with almost no-help from the compiler. For this reason we feel very strongly that any code
|
||||
written in JavaScript needs to come with a strong set of tests. We have built many features into
|
||||
angular which makes testing your angular applications easy. So there is no excuse for not do it.
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Understanding Angular Expressions
|
||||
@description
|
||||
@@ -41,7 +40,7 @@ the `Scope:$eval()` method.
|
||||
1+2={{1+2}}
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should calculate expression in binding', function(){
|
||||
it('should calculate expression in binding', function() {
|
||||
expect(binding('1+2')).toEqual('3');
|
||||
});
|
||||
</doc:scenario>
|
||||
@@ -51,20 +50,37 @@ You can try evaluating different expressions here:
|
||||
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<div ng:init="exprs=[]" class="expressions">
|
||||
<script>
|
||||
function Cntl2() {
|
||||
this.exprs = [];
|
||||
this.expr = '3*10|currency';
|
||||
this.addExp = function(expr) {
|
||||
this.exprs.push(expr);
|
||||
};
|
||||
|
||||
this.removeExp = function(contact) {
|
||||
for ( var i = 0, ii = this.exprs.length; i < ii; i++) {
|
||||
if (contact === this.exprs[i]) {
|
||||
this.exprs.splice(i, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<div ng:controller="Cntl2" class="expressions">
|
||||
Expression:
|
||||
<input type='text' name="expr" value="3*10|currency" size="80"/>
|
||||
<button ng:click="exprs.$add(expr)">Evaluate</button>
|
||||
<input type='text' ng:model="expr" size="80"/>
|
||||
<button ng:click="addExp(expr)">Evaluate</button>
|
||||
<ul>
|
||||
<li ng:repeat="expr in exprs">
|
||||
[ <a href="" ng:click="exprs.$remove(expr)">X</a> ]
|
||||
[ <a href="" ng:click="removeExp(expr)">X</a> ]
|
||||
<tt>{{expr}}</tt> => <span ng:bind="$parent.$eval(expr)"></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should allow user expression testing', function(){
|
||||
it('should allow user expression testing', function() {
|
||||
element('.expressions :button').click();
|
||||
var li = using('.expressions ul').repeater('li');
|
||||
expect(li.count()).toBe(1);
|
||||
@@ -84,13 +100,22 @@ the global state (a common source of subtle bugs).
|
||||
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<div class="example2" ng:init="$window = $service('$window')">
|
||||
Name: <input name="name" type="text" value="World"/>
|
||||
<button ng:click="($window.mockWindow || $window).alert('Hello ' + name)">Greet</button>
|
||||
<script>
|
||||
function Cntl1($window){
|
||||
this.name = 'World';
|
||||
|
||||
this.greet = function() {
|
||||
($window.mockWindow || $window).alert('Hello ' + this.name);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<div class="example2" ng:controller="Cntl1">
|
||||
Name: <input ng:model="name" type="text"/>
|
||||
<button ng:click="greet()">Greet</button>
|
||||
</div>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should calculate expression in binding', function(){
|
||||
it('should calculate expression in binding', function() {
|
||||
var alertText;
|
||||
this.addFutureAction('set mock', function($window, $document, done) {
|
||||
$window.mockWindow = {
|
||||
@@ -140,15 +165,15 @@ JavaScript method instead.
|
||||
Built-in types have methods like `[].push()`, but the richness of these methods is limited.
|
||||
Consider the example below, which allows you to do a simple search over a canned set of contacts.
|
||||
The example would be much more complicated if we did not have the `Array:$filter()`. There is no
|
||||
built-in method on `Array` called {@link api/angular.Array.filter $filter} and angular doesn't add
|
||||
built-in method on `Array` called {@link api/angular.module.ng.$filter.filter $filter} and angular doesn't add
|
||||
it to `Array.prototype` because that could collide with other JavaScript frameworks.
|
||||
|
||||
For this reason the scope expression evaluator augments the built-in types to make them act like
|
||||
they have extra methods. The actual method for `$filter()` is `angular.Array.filter()`. You can
|
||||
they have extra methods. The actual method for `$filter()` is `angular.module.ng.$filter.filter()`. You can
|
||||
call it from JavaScript.
|
||||
|
||||
Extensions: You can further extend the expression vocabulary by adding new methods to
|
||||
`angular.Array` or `angular.String`, etc.
|
||||
`angular.module.ng.$filter` or `angular.String`, etc.
|
||||
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
@@ -158,17 +183,17 @@ Extensions: You can further extend the expression vocabulary by adding new metho
|
||||
{name:'Mike', phone:'555-4321'},
|
||||
{name:'Adam', phone:'555-5678'},
|
||||
{name:'Julie', phone:'555-8765'}]"></div>
|
||||
Search: <input name="searchText"/>
|
||||
Search: <input ng:model="searchText"/>
|
||||
<table class="example3">
|
||||
<tr><th>Name</th><th>Phone</th><tr>
|
||||
<tr ng:repeat="friend in friends.$filter(searchText)">
|
||||
<tr ng:repeat="friend in friends | filter:searchText">
|
||||
<td>{{friend.name}}</td>
|
||||
<td>{{friend.phone}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should filter the list', function(){
|
||||
it('should filter the list', function() {
|
||||
var tr = using('table.example3').repeater('tr.ng-attr-widget');
|
||||
expect(tr.count()).toBe(5);
|
||||
input('searchText').enter('a');
|
||||
@@ -187,7 +212,7 @@ of filters like this:
|
||||
|
||||
name | uppercase
|
||||
|
||||
The expression evaluator simply passes the value of name to angular.filter.uppercase.
|
||||
The expression evaluator simply passes the value of name to angular.module.ng.$filter.uppercase.
|
||||
|
||||
Chain filters using this syntax:
|
||||
|
||||
@@ -218,4 +243,4 @@ so that angular developers and developers who use angular can develop in harmony
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.compile Angular Compiler API}
|
||||
* {@link api/angular.module.ng.$compile Angular Compiler API}
|
||||
|
||||
@@ -0,0 +1,614 @@
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Forms
|
||||
@description
|
||||
|
||||
# Overview
|
||||
|
||||
Forms allow users to enter data into your application. Forms represent the bidirectional data
|
||||
bindings in Angular.
|
||||
|
||||
Forms consist of all of the following:
|
||||
|
||||
- the individual widgets with which users interact
|
||||
- the validation rules for widgets
|
||||
- the form, a collection of widgets that contains aggregated validation information
|
||||
|
||||
|
||||
# Form
|
||||
|
||||
A form groups a set of widgets together into a single logical data-set. A form is created using
|
||||
the {@link api/angular.widget.form <form>} element that calls the
|
||||
{@link api/angular.module.ng.$formFactory $formFactory} service. The form is responsible for managing
|
||||
the widgets and for tracking validation information.
|
||||
|
||||
A form is:
|
||||
|
||||
- The collection which contains widgets or other forms.
|
||||
- Responsible for marshaling data from the model into a widget. This is
|
||||
triggered by {@link api/angular.module.ng.$rootScope.Scope#$watch $watch} of the model expression.
|
||||
- Responsible for marshaling data from the widget into the model. This is
|
||||
triggered by the widget emitting the `$viewChange` event.
|
||||
- Responsible for updating the validation state of the widget, when the widget emits
|
||||
`$valid` / `$invalid` event. The validation state is useful for controlling the validation
|
||||
errors shown to the user in it consist of:
|
||||
|
||||
- `$valid` / `$invalid`: Complementary set of booleans which show if a widget is valid / invalid.
|
||||
- `$error`: an object which has a property for each validation key emited by the widget.
|
||||
The value of the key is always true. If widget is valid, then the `$error`
|
||||
object has no properties. For example if the widget emits
|
||||
`$invalid` event with `REQUIRED` key. The internal state of the `$error` would be
|
||||
updated to `$error.REQUIRED == true`.
|
||||
|
||||
- Responsible for aggregating widget validation information into the form.
|
||||
|
||||
- `$valid` / `$invalid`: Complementary set of booleans which show if all the child widgets
|
||||
(or forms) are valid or if any are invalid.
|
||||
- `$error`: an object which has a property for each validation key emited by the
|
||||
child widget. The value of the key is an array of widgets which fired the invalid
|
||||
event. If all child widgets are valid then, then the `$error` object has no
|
||||
properties. For example if a child widget emits
|
||||
`$invalid` event with `REQUIRED` key. The internal state of the `$error` would be
|
||||
updated to `$error.REQUIRED == [ widgetWhichEmitedInvalid ]`.
|
||||
|
||||
|
||||
# Widgets
|
||||
|
||||
In Angular, a widget is the term used for the UI with which the user input. Examples of
|
||||
bult-in Angular widgets are {@link api/angular.widget.input input} and
|
||||
{@link api/angular.widget.select select}. Widgets provide the rendering and the user
|
||||
interaction logic. Widgets should be declared inside a form, if no form is provided an implicit
|
||||
form {@link api/angular.module.ng.$formFactory $formFactory.rootForm} form is used.
|
||||
|
||||
Widgets are implemented as Angular controllers. A widget controller:
|
||||
|
||||
- implements methods:
|
||||
|
||||
- `$render` - Updates the DOM from the internal state as represented by `$viewValue`.
|
||||
- `$parseView` - Translate `$viewValue` to `$modelValue`. (`$modelValue` will be assigned to
|
||||
the model scope by the form)
|
||||
- `$parseModel` - Translate `$modelValue` to `$viewValue`. (`$viewValue` will be assigned to
|
||||
the DOM inside the `$render` method)
|
||||
|
||||
- responds to events:
|
||||
|
||||
- `$validate` - Emitted by the form when the form determines that the widget needs to validate
|
||||
itself. There may be more then one listener on the `$validate` event. The widget responds
|
||||
by emitting `$valid` / `$invalid` event of its own.
|
||||
|
||||
- emits events:
|
||||
|
||||
- `$viewChange` - Emitted when the user interacts with the widget and it is necessary to update
|
||||
the model.
|
||||
- `$valid` - Emitted when the widget determines that it is valid (usually as a response to
|
||||
`$validate` event or inside `$parseView()` or `$parseModel()` method).
|
||||
- `$invalid` - Emitted when the widget determines that it is invalid (usually as a response to
|
||||
`$validate` event or inside `$parseView()` or `$parseModel()` method).
|
||||
- `$destroy` - Emitted when the widget element is removed from the DOM.
|
||||
|
||||
|
||||
# CSS
|
||||
|
||||
Angular-defined widgets and forms set `ng-valid` and `ng-invalid` classes on themselves to allow
|
||||
the web-designer a way to style them. If you write your own widgets, then their `$render()`
|
||||
methods must set the appropriate CSS classes to allow styling.
|
||||
(See {@link dev_guide.templates.css-styling CSS})
|
||||
|
||||
|
||||
# Example
|
||||
|
||||
The following example demonstrates:
|
||||
|
||||
- How an error is displayed when a required field is empty.
|
||||
- Error highlighting.
|
||||
- How form submission is disabled when the form is invalid.
|
||||
- The internal state of the widget and form in the the 'Debug View' area.
|
||||
|
||||
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<style>
|
||||
.ng-invalid { border: solid 1px red; }
|
||||
.ng-form {display: block;}
|
||||
</style>
|
||||
<script>
|
||||
function UserFormCntl() {
|
||||
this.state = /^\w\w$/;
|
||||
this.zip = /^\d\d\d\d\d$/;
|
||||
this.master = {
|
||||
customer: 'John Smith',
|
||||
address:{
|
||||
line1: '123 Main St.',
|
||||
city:'Anytown',
|
||||
state:'AA',
|
||||
zip:'12345'
|
||||
}
|
||||
};
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
UserFormCntl.prototype = {
|
||||
cancel: function() {
|
||||
this.form = angular.copy(this.master);
|
||||
},
|
||||
|
||||
save: function() {
|
||||
this.master = this.form;
|
||||
this.cancel();
|
||||
},
|
||||
|
||||
isCancelDisabled: function() {
|
||||
return angular.equals(this.master, this.form);
|
||||
},
|
||||
|
||||
isSaveDisabled: function() {
|
||||
return this.userForm.$invalid || angular.equals(this.master, this.form);
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
<div ng:controller="UserFormCntl">
|
||||
|
||||
<form name="userForm">
|
||||
|
||||
<label>Name:</label><br/>
|
||||
<input type="text" name="customer" ng:model="form.customer" required/>
|
||||
<span class="error" ng:show="userForm.customer.$error.REQUIRED">
|
||||
Customer name is required!</span>
|
||||
<br/><br/>
|
||||
|
||||
<ng:form name="addressForm">
|
||||
<label>Address:</label> <br/>
|
||||
<input type="text" name="line1" size="33" required
|
||||
ng:model="form.address.line1"/> <br/>
|
||||
<input type="text" name="city" size="12" required
|
||||
ng:model="form.address.city"/>,
|
||||
<input type="text" name="state" ng:pattern="state" size="2" required
|
||||
ng:model="form.address.state"/>
|
||||
<input type="text" name="zip" ng:pattern="zip" size="5" required
|
||||
ng:model="form.address.zip"/><br/><br/>
|
||||
|
||||
<span class="error" ng:show="addressForm.$invalid">
|
||||
Incomplete address:
|
||||
<span class="error" ng:show="addressForm.state.$error.REQUIRED">
|
||||
Missing state!</span>
|
||||
<span class="error" ng:show="addressForm.state.$error.PATTERN">
|
||||
Invalid state!</span>
|
||||
<span class="error" ng:show="addressForm.zip.$error.REQUIRED">
|
||||
Missing zip!</span>
|
||||
<span class="error" ng:show="addressForm.zip.$error.PATTERN">
|
||||
Invalid zip!</span>
|
||||
</span>
|
||||
</ng:form>
|
||||
|
||||
<button ng:click="cancel()"
|
||||
ng:disabled="{{isCancelDisabled()}}">Cancel</button>
|
||||
<button ng:click="save()"
|
||||
ng:disabled="{{isSaveDisabled()}}">Save</button>
|
||||
</form>
|
||||
|
||||
<hr/>
|
||||
Debug View:
|
||||
<pre>form={{form}}</pre>
|
||||
<pre>master={{master}}</pre>
|
||||
<pre>userForm={{userForm}}</pre>
|
||||
<pre>addressForm={{addressForm}}</pre>
|
||||
</div>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should enable save button', function() {
|
||||
expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy();
|
||||
input('form.customer').enter('');
|
||||
expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy();
|
||||
input('form.customer').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.customer').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[ng\\:model="form.customer"]').val()).toEqual('John Smith');
|
||||
});
|
||||
</doc:scenario>
|
||||
</doc:example>
|
||||
|
||||
# Life-cycle
|
||||
|
||||
- The `<form>` element triggers creation of a new form {@link dev_guide.scopes scope} using the
|
||||
{@link api/angular.module.ng.$formFactory $formfactory}. The new form scope is added to the
|
||||
`<form>` element using the jQuery `.data()` method for later retrieval under the key `$form`.
|
||||
The form also sets up these listeners:
|
||||
|
||||
- `$destroy` - This event is emitted by nested widget when it is removed from the view. It gives
|
||||
the form a chance to clean up any validation references to the destroyed widget.
|
||||
- `$valid` / `$invalid` - This event is emitted by the widget on validation state change.
|
||||
|
||||
- `<input>` element triggers the creation of the widget using the
|
||||
{@link api/angular.module.ng.$formFactory $formfactory.$createWidget()} method. The `$createWidget()`
|
||||
creates new widget instance by calling the current scope {@link api/angular.module.ng.$rootScope.Scope#$new .$new()} and
|
||||
registers these listeners:
|
||||
|
||||
- `$watch` on the model scope.
|
||||
- `$viewChange` event on the widget scope.
|
||||
- `$validate` event on the widget scope.
|
||||
- Element `change` event when the user enters data.
|
||||
|
||||
<img class="center" src="img/form_data_flow.png" border="1" />
|
||||
|
||||
|
||||
- When the user interacts with the widget:
|
||||
|
||||
1. The DOM element fires the `change` event which the widget intercepts. Widget then emits
|
||||
a `$viewChange` event which includes the new user-entered value. (Remember that the DOM events
|
||||
are outside of the Angular environment so the widget must emit its event within the
|
||||
{@link api/angular.module.ng.$rootScope.Scope#$apply $apply} method).
|
||||
2. The form's `$viewChange` listener copies the user-entered value to the widget's `$viewValue`
|
||||
property. Since the `$viewValue` is the raw value as entered by user, it may need to be
|
||||
translated to a different format/type (for example, translating a string to a number).
|
||||
If you need your widget to translate between the internal `$viewValue` and the external
|
||||
`$modelValue` state, you must declare a `$parseView()` method. The `$parseView()` method
|
||||
will copy `$viewValue` to `$modelValue` and perform any necessary translations.
|
||||
3. The `$modelValue` is written into the application model.
|
||||
4. The form then emits a `$validate` event, giving the widget's validators chance to validate the
|
||||
input. There can be any number of validators registered. Each validator may in turn
|
||||
emit a `$valid` / `$invalid` event with the validator's validation key. For example `REQUIRED`.
|
||||
5. Form listens to `$valid`/`$invalid` events and updates both the form as well as the widget
|
||||
scope with the validation state. The validation updates the `$valid` and `$invalid`, property
|
||||
as well as `$error` object. The widget's `$error` object is updated with the validation key
|
||||
such that `$error.REQUIRED == true` when the validation emits `$invalid` with `REQUIRED`
|
||||
validation key. Similarly the form's `$error` object gets updated, but instead of boolean
|
||||
`true` it contains an array of invalid widgets (widgets which fired `$invalid` event with
|
||||
`REQUIRED` validation key).
|
||||
|
||||
- When the model is updated:
|
||||
|
||||
1. The model `$watch` listener assigns the model value to `$modelValue` on the widget.
|
||||
2. The form then calls `$parseModel` method on widget if present. The method converts the
|
||||
value to renderable format and assigns it to `$viewValue` (for example converting number to a
|
||||
string.)
|
||||
3. The form then emits a `$validate` which behaves as described above.
|
||||
4. The form then calls `$render` method on the widget to update the DOM structure from the
|
||||
`$viewValue`.
|
||||
|
||||
|
||||
|
||||
# Writing Your Own Widget
|
||||
|
||||
This example shows how to implement a custom HTML editor widget in Angular.
|
||||
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<script>
|
||||
function EditorCntl() {
|
||||
this.htmlContent = '<b>Hello</b> <i>World</i>!';
|
||||
}
|
||||
|
||||
HTMLEditorWidget.$inject = ['$element', 'htmlFilter'];
|
||||
function HTMLEditorWidget(element, htmlFilter) {
|
||||
var self = this;
|
||||
|
||||
this.$parseModel = function() {
|
||||
// need to protect for script injection
|
||||
try {
|
||||
this.$viewValue = htmlFilter(
|
||||
this.$modelValue || '').get();
|
||||
if (this.$error.HTML) {
|
||||
// we were invalid, but now we are OK.
|
||||
this.$emit('$valid', 'HTML');
|
||||
}
|
||||
} catch (e) {
|
||||
// if HTML not parsable invalidate form.
|
||||
this.$emit('$invalid', 'HTML');
|
||||
}
|
||||
}
|
||||
|
||||
this.$render = function() {
|
||||
element.html(this.$viewValue);
|
||||
}
|
||||
|
||||
element.bind('keyup', function() {
|
||||
self.$apply(function() {
|
||||
self.$emit('$viewChange', element.html());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
angular.directive('ng:html-editor-model', function() {
|
||||
return ['$formFactory', '$element', function ($formFactory, element) {
|
||||
var exp = element.attr('ng:html-editor-model'),
|
||||
form = $formFactory.forElement(element),
|
||||
widget;
|
||||
element.attr('contentEditable', true);
|
||||
widget = form.$createWidget({
|
||||
scope: this,
|
||||
model: exp,
|
||||
controller: HTMLEditorWidget,
|
||||
controllerArgs: {$element: element}});
|
||||
// if the element is destroyed, then we need to
|
||||
// notify the form.
|
||||
element.bind('$destroy', function() {
|
||||
widget.$destroy();
|
||||
});
|
||||
}];
|
||||
});
|
||||
</script>
|
||||
<form name='editorForm' ng:controller="EditorCntl">
|
||||
<div ng:html-editor-model="htmlContent"></div>
|
||||
<hr/>
|
||||
HTML: <br/>
|
||||
<textarea ng:model="htmlContent" cols="80"></textarea>
|
||||
<hr/>
|
||||
<pre>editorForm = {{editorForm}}</pre>
|
||||
</form>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should enter invalid HTML', function() {
|
||||
expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/);
|
||||
input('htmlContent').enter('<');
|
||||
expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/);
|
||||
});
|
||||
</doc:scenario>
|
||||
</doc:example>
|
||||
|
||||
|
||||
|
||||
# HTML Inputs
|
||||
|
||||
The most common widgets you will use will be in the form of the
|
||||
standard HTML set. These widgets are bound using the `name` attribute
|
||||
to an expression. In addition, they can have `required` attribute to further control their
|
||||
validation.
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<script>
|
||||
function Ctrl() {
|
||||
this.input1 = '';
|
||||
this.input2 = '';
|
||||
this.input3 = 'A';
|
||||
this.input4 = false;
|
||||
this.input5 = 'c';
|
||||
this.input6 = [];
|
||||
}
|
||||
</script>
|
||||
<table style="font-size:.9em;" ng:controller="Ctrl">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Format</th>
|
||||
<th>HTML</th>
|
||||
<th>UI</th>
|
||||
<th ng:non-bindable>{{input#}}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>text</th>
|
||||
<td>String</td>
|
||||
<td><tt><input type="text" ng:model="input1"></tt></td>
|
||||
<td><input type="text" ng:model="input1" size="4"></td>
|
||||
<td><tt>{{input1|json}}</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>textarea</th>
|
||||
<td>String</td>
|
||||
<td><tt><textarea ng:model="input2"></textarea></tt></td>
|
||||
<td><textarea ng:model="input2" cols='6'></textarea></td>
|
||||
<td><tt>{{input2|json}}</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>radio</th>
|
||||
<td>String</td>
|
||||
<td><tt>
|
||||
<input type="radio" ng:model="input3" value="A"><br>
|
||||
<input type="radio" ng:model="input3" value="B">
|
||||
</tt></td>
|
||||
<td>
|
||||
<input type="radio" ng:model="input3" value="A">
|
||||
<input type="radio" ng:model="input3" value="B">
|
||||
</td>
|
||||
<td><tt>{{input3|json}}</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>checkbox</th>
|
||||
<td>Boolean</td>
|
||||
<td><tt><input type="checkbox" ng:model="input4"></tt></td>
|
||||
<td><input type="checkbox" ng:model="input4"></td>
|
||||
<td><tt>{{input4|json}}</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>pulldown</th>
|
||||
<td>String</td>
|
||||
<td><tt>
|
||||
<select ng:model="input5"><br>
|
||||
<option value="c">C</option><br>
|
||||
<option value="d">D</option><br>
|
||||
</select><br>
|
||||
</tt></td>
|
||||
<td>
|
||||
<select ng:model="input5">
|
||||
<option value="c">C</option>
|
||||
<option value="d">D</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><tt>{{input5|json}}</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>multiselect</th>
|
||||
<td>Array</td>
|
||||
<td><tt>
|
||||
<select ng:model="input6" multiple size="4"><br>
|
||||
<option value="e">E</option><br>
|
||||
<option value="f">F</option><br>
|
||||
</select><br>
|
||||
</tt></td>
|
||||
<td>
|
||||
<select ng:model="input6" multiple size="4">
|
||||
<option value="e">E</option>
|
||||
<option value="f">F</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><tt>{{input6|json}}</tt></td>
|
||||
</tr>
|
||||
</table>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
|
||||
it('should exercise text', function() {
|
||||
input('input1').enter('Carlos');
|
||||
expect(binding('input1')).toEqual('"Carlos"');
|
||||
});
|
||||
it('should exercise textarea', function() {
|
||||
input('input2').enter('Carlos');
|
||||
expect(binding('input2')).toEqual('"Carlos"');
|
||||
});
|
||||
it('should exercise radio', function() {
|
||||
expect(binding('input3')).toEqual('"A"');
|
||||
input('input3').select('B');
|
||||
expect(binding('input3')).toEqual('"B"');
|
||||
input('input3').select('A');
|
||||
expect(binding('input3')).toEqual('"A"');
|
||||
});
|
||||
it('should exercise checkbox', function() {
|
||||
expect(binding('input4')).toEqual('false');
|
||||
input('input4').check();
|
||||
expect(binding('input4')).toEqual('true');
|
||||
});
|
||||
it('should exercise pulldown', function() {
|
||||
expect(binding('input5')).toEqual('"c"');
|
||||
select('input5').option('d');
|
||||
expect(binding('input5')).toEqual('"d"');
|
||||
});
|
||||
it('should exercise multiselect', function() {
|
||||
expect(binding('input6')).toEqual('[]');
|
||||
select('input6').options('e');
|
||||
expect(binding('input6')).toEqual('["e"]');
|
||||
select('input6').options('e', 'f');
|
||||
expect(binding('input6')).toEqual('["e","f"]');
|
||||
});
|
||||
</doc:scenario>
|
||||
</doc:example>
|
||||
|
||||
#Testing
|
||||
|
||||
When unit-testing a controller it may be desirable to have a reference to form and to simulate
|
||||
different form validation states.
|
||||
|
||||
This example demonstrates a login form, where the login button is enabled only when the form is
|
||||
properly filled out.
|
||||
<pre>
|
||||
<div ng:controller="LoginController">
|
||||
<form name="loginForm">
|
||||
<input type="text" ng:model="username" required/>
|
||||
<input type="password" ng:model="password" required/>
|
||||
<button ng:disabled="{{!disableLogin()}}" ng:click="login()">Login</login>
|
||||
</form>
|
||||
</div>
|
||||
</pre>
|
||||
|
||||
In the unit tests we do not have access to the DOM, and therefore the `loginForm` reference does
|
||||
not get set on the controller. This example shows how it can be unit-tested, by creating a mock
|
||||
form.
|
||||
<pre>
|
||||
function LoginController() {
|
||||
this.disableLogin = function() {
|
||||
return this.loginForm.$invalid;
|
||||
};
|
||||
}
|
||||
|
||||
describe('LoginController', function() {
|
||||
it('should disable login button when form is invalid', inject(function($rootScope) {
|
||||
var loginController = $rootScope.$new(LoginController);
|
||||
|
||||
// In production the 'loginForm' form instance gets set from the view,
|
||||
// but in unit-test we have to set it manually.
|
||||
loginController.loginForm = scope.$service('$formFactory')();
|
||||
|
||||
expect(loginController.disableLogin()).toBe(false);
|
||||
|
||||
// Now simulate an invalid form
|
||||
loginController.loginForm.$emit('$invalid', 'MyReason');
|
||||
expect(loginController.disableLogin()).toBe(true);
|
||||
|
||||
// Now simulate a valid form
|
||||
loginController.loginForm.$emit('$valid', 'MyReason');
|
||||
expect(loginController.disableLogin()).toBe(false);
|
||||
}));
|
||||
});
|
||||
</pre>
|
||||
|
||||
## Custom widgets
|
||||
|
||||
This example demonstrates a login form, where the password has custom validation rules.
|
||||
<pre>
|
||||
<div ng:controller="LoginController">
|
||||
<form name="loginForm">
|
||||
<input type="text" ng:model="username" required/>
|
||||
<input type="@StrongPassword" ng:model="password" required/>
|
||||
<button ng:disabled="{{!disableLogin()}}" ng:click="login()">Login</login>
|
||||
</form>
|
||||
</div>
|
||||
</pre>
|
||||
|
||||
In the unit tests we do not have access to the DOM, and therefore the `loginForm` and custom
|
||||
input type reference does not get set on the controller. This example shows how it can be
|
||||
unit-tested, by creating a mock form and a mock custom input type.
|
||||
<pre>
|
||||
function LoginController(){
|
||||
this.disableLogin = function() {
|
||||
return this.loginForm.$invalid;
|
||||
};
|
||||
|
||||
this.StrongPassword = function(element) {
|
||||
var widget = this;
|
||||
element.attr('type', 'password'); // act as password.
|
||||
this.$on('$validate', function(){
|
||||
widget.$emit(widget.$viewValue.length > 5 ? '$valid' : '$invalid', 'PASSWORD');
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
describe('LoginController', function() {
|
||||
it('should disable login button when form is invalid', inject(function($rootScope) {
|
||||
var loginController = $rootScope.$new(LoginController);
|
||||
var input = angular.element('<input>');
|
||||
|
||||
// In production the 'loginForm' form instance gets set from the view,
|
||||
// but in unit-test we have to set it manually.
|
||||
loginController.loginForm = scope.$service('$formFactory')();
|
||||
|
||||
// now instantiate a custom input type
|
||||
loginController.loginForm.$createWidget({
|
||||
scope: loginController,
|
||||
model: 'password',
|
||||
alias: 'password',
|
||||
controller: loginController.StrongPassword,
|
||||
controllerArgs: [input]
|
||||
});
|
||||
|
||||
// Verify that the custom password input type sets the input type to password
|
||||
expect(input.attr('type')).toEqual('password');
|
||||
|
||||
expect(loginController.disableLogin()).toBe(false);
|
||||
|
||||
// Now simulate an invalid form
|
||||
loginController.loginForm.password.$emit('$invalid', 'PASSWORD');
|
||||
expect(loginController.disableLogin()).toBe(true);
|
||||
|
||||
// Now simulate a valid form
|
||||
loginController.loginForm.password.$emit('$valid', 'PASSWORD');
|
||||
expect(loginController.disableLogin()).toBe(false);
|
||||
|
||||
// Changing model state, should also influence the form validity
|
||||
loginController.password = 'abc'; // too short so it should be invalid
|
||||
scope.$digest();
|
||||
expect(loginController.loginForm.password.$invalid).toBe(true);
|
||||
|
||||
// Changeing model state, should also influence the form validity
|
||||
loginController.password = 'abcdef'; // should be valid
|
||||
scope.$digest();
|
||||
expect(loginController.loginForm.password.$valid).toBe(true);
|
||||
}));
|
||||
});
|
||||
</pre>
|
||||
|
||||
|
||||
@@ -17,15 +17,15 @@ abstracted bits.
|
||||
**What level of support for i18n/l10n is currently in Angular?**
|
||||
|
||||
Currently, Angular supports i18n/l10n for {@link
|
||||
http://docs.angularjs.org/#!/api/angular.filter.date datetime}, {@link
|
||||
http://docs.angularjs.org/#!/api/angular.filter.number number} and {@link
|
||||
http://docs.angularjs.org/#!/api/angular.filter.currency currency} filters.
|
||||
http://docs.angularjs.org/#!/api/angular.module.ng.$filter.date datetime}, {@link
|
||||
http://docs.angularjs.org/#!/api/angular.module.ng.$filter.number number} and {@link
|
||||
http://docs.angularjs.org/#!/api/angular.module.ng.$filter.currency currency} filters.
|
||||
|
||||
Additionally, Angular supports localizable pluralization support provided by the {@link
|
||||
api/angular.widget.ng:pluralize ng:pluralize widget}.
|
||||
|
||||
All localizable Angular components depend on locale-specific rule sets managed by the {@link
|
||||
api/angular.service.$locale $locale service}.
|
||||
api/angular.module.ng.$locale $locale service}.
|
||||
|
||||
For readers who want to jump straight into examples, we have a few web pages that showcase how to
|
||||
use Angular filters with various locale rule sets. You can find these examples either on {@link
|
||||
@@ -67,10 +67,10 @@ You can also include the locale specific js file in the index.html page. For exa
|
||||
requires German locale, you would serve index_de-ge.html which will look something like this:
|
||||
|
||||
<pre>
|
||||
<html>
|
||||
<html ng:app>
|
||||
<head>
|
||||
….
|
||||
<script src="angular.js" ng:autobind></script>
|
||||
<script src="angular.js"></script>
|
||||
<script src="i18n/angular-locale_de-ge.js"></script>
|
||||
….
|
||||
</head>
|
||||
@@ -90,8 +90,8 @@ because an extra script needs to be loaded.
|
||||
|
||||
**Currency symbol "gotcha"**
|
||||
|
||||
Angular's {@link http://docs.angularjs.org/#!/api/angular.filter.currency currency filter} allows
|
||||
you to use the default currency symbol from the {@link api/angular.service.$locale locale service},
|
||||
Angular's {@link http://docs.angularjs.org/#!/api/angular.module.ng.$filter.currency currency filter} allows
|
||||
you to use the default currency symbol from the {@link api/angular.module.ng.$locale locale service},
|
||||
or you can provide the filter with a custom currency symbol. If your app will be used only in one
|
||||
locale, it is fine to rely on the default currency symbol. However, if you anticipate that viewers
|
||||
in other locales might use your app, you should provide your own currency symbol to make sure the
|
||||
@@ -104,7 +104,7 @@ browser will specify the locale as ja, and the balance of '¥1000.00' will be sh
|
||||
will really upset your client.
|
||||
|
||||
In this case, you need to override the default currency symbol by providing the {@link
|
||||
http://docs.angularjs.org/#!/api/angular.filter.currency currency filter} with a currency symbol as
|
||||
http://docs.angularjs.org/#!/api/angular.module.ng.$filter.currency currency filter} with a currency symbol as
|
||||
a parameter when you configure the filter, for example, {{ 1000 | currency:"USD$"}}. This way,
|
||||
Angular will always show a balance of 'USD$1000' and disregard any locale changes.
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Introduction
|
||||
@description
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: About MVC in Angular
|
||||
@description
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: About MVC in Angular: Understanding the Controller Component
|
||||
@description
|
||||
|
||||
In angular, a controller is a JavaScript function (type/class) that is used to augment instances of
|
||||
In angular, a controller is a JavaScript function(type/class) that is used to augment instances of
|
||||
angular {@link dev_guide.scopes Scope}, excluding the root scope. When you or angular create a new
|
||||
child scope object via the {@link api/angular.scope.$new scope.$new} API , there is an
|
||||
child scope object via the {@link api/angular.module.ng.$rootScope.Scope#$new scope.$new} API , there is an
|
||||
option to pass in a controller as a method argument. This will tell angular to associate the
|
||||
controller with the new scope and to augment its behavior.
|
||||
|
||||
@@ -68,7 +67,7 @@ Putting any presentation logic into controllers significantly affects testabilit
|
||||
logic. Angular offers {@link dev_guide.templates.databinding} for automatic DOM manipulation. If
|
||||
you have to perform your own manual DOM manipulation, encapsulate the presentation logic in {@link
|
||||
dev_guide.compiler.widgets widgets} and {@link dev_guide.compiler.directives directives}.
|
||||
- Input formatting — Use {@link dev_guide.templates.formatters angular formatters} instead.
|
||||
- Input formatting — Use {@link dev_guide.forms angular form widgets} instead.
|
||||
- Output filtering — Use {@link dev_guide.templates.filters angular filters} instead.
|
||||
- Run stateless or stateful code shared across controllers — Use {@link dev_guide.services angular
|
||||
services} instead.
|
||||
@@ -78,9 +77,9 @@ instances).
|
||||
|
||||
# Associating Controllers with Angular Scope Objects
|
||||
|
||||
You can associate controllers with scope objects explicitly via the {@link api/angular.scope.$new
|
||||
You can associate controllers with scope objects explicitly via the {@link api/angular.module.ng.$rootScope.Scope#$new
|
||||
scope.$new} api or implicitly via the {@link api/angular.directive.ng:controller ng:controller
|
||||
directive} or {@link api/angular.service.$route $route service}.
|
||||
directive} or {@link api/angular.module.ng.$route $route service}.
|
||||
|
||||
|
||||
## Controller Constructor and Methods Example
|
||||
@@ -128,7 +127,7 @@ starts with capital letter and ends with "Ctrl" or "Controller".
|
||||
controller augments.
|
||||
- Assigning a property to `this` creates or updates the model.
|
||||
- Controller methods can be created through direct assignment to scope (the `chiliSpicy` method) or
|
||||
as prototype methods of the controller constructor function (the `jalapenoSpicy` method)
|
||||
as prototype methods of the controller constructor function(the `jalapenoSpicy` method)
|
||||
- Both controller methods are available in the template (for the `body` element and and its
|
||||
children).
|
||||
|
||||
@@ -139,7 +138,7 @@ previous example.
|
||||
|
||||
<pre>
|
||||
<body ng:controller="SpicyCtrl">
|
||||
<input name="customSpice" value="wasabi">
|
||||
<input ng:model="customSpice" value="wasabi">
|
||||
<button ng:click="spicy('chili')">Chili</button>
|
||||
<button ng:click="spicy(customSpice)">Custom spice</button>
|
||||
<p>The food is {{spice}} spicy!</p>
|
||||
@@ -161,7 +160,7 @@ input box) in the second button.
|
||||
|
||||
## Controller Inheritance Example
|
||||
|
||||
Controller inheritance in angular is based on {@link api/angular.scope Scope} inheritance. Let's
|
||||
Controller inheritance in angular is based on {@link api/angular.module.ng.$rootScope.Scope Scope} inheritance. Let's
|
||||
have a look at an example:
|
||||
|
||||
<pre>
|
||||
@@ -227,7 +226,7 @@ Controller Test:
|
||||
<pre>
|
||||
describe('myController function', function() {
|
||||
|
||||
describe('myController', function(){
|
||||
describe('myController', function() {
|
||||
var ctrl;
|
||||
|
||||
beforeEach(function() {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: About MVC in Angular: Understanding the Model Component
|
||||
@description
|
||||
@@ -41,7 +40,7 @@ when processing the following template constructs:
|
||||
|
||||
* Form input, select, textarea and other form elements:
|
||||
|
||||
<input name="query" value="fluffy cloud">
|
||||
<input ng:model="query" value="fluffy cloud">
|
||||
|
||||
The code above creates a model called "query" on the current scope with the value set to "fluffy
|
||||
cloud".
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: About MVC in Angular: Understanding the View Component
|
||||
@description
|
||||
|
||||
@@ -42,23 +42,31 @@ easier a web developer's life can if they're using angular:
|
||||
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<b>Invoice:</b>
|
||||
<br />
|
||||
<br />
|
||||
<table>
|
||||
<tr><td> </td><td> </td>
|
||||
<tr><td>Quantity</td><td>Cost</td></tr>
|
||||
<tr>
|
||||
<td><input name="qty" value="1" ng:validate="integer:0" ng:required /></td>
|
||||
<td><input name="cost" value="19.95" ng:validate="number" ng:required /></td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr />
|
||||
<b>Total:</b> {{qty * cost | currency}}
|
||||
<script>
|
||||
function InvoiceCntl() {
|
||||
this.qty = 1;
|
||||
this.cost = 19.95;
|
||||
}
|
||||
</script>
|
||||
<div ng:controller="InvoiceCntl">
|
||||
<b>Invoice:</b>
|
||||
<br />
|
||||
<br />
|
||||
<table>
|
||||
<tr><td> </td><td> </td>
|
||||
<tr><td>Quantity</td><td>Cost</td></tr>
|
||||
<tr>
|
||||
<td><input type="integer" min="0" ng:model="qty" required ></td>
|
||||
<td><input type="number" ng:model="cost" required ></td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr />
|
||||
<b>Total:</b> {{qty * cost | currency}}
|
||||
</div>
|
||||
</doc:source>
|
||||
<!--
|
||||
<doc:scenario>
|
||||
it('should show of angular binding', function(){
|
||||
it('should show of angular binding', function() {
|
||||
expect(binding('qty * cost')).toEqual('$19.95');
|
||||
input('qty').enter('2');
|
||||
input('cost').enter('5.00');
|
||||
@@ -71,36 +79,32 @@ easier a web developer's life can if they're using angular:
|
||||
Try out the Live Preview above, and then let's walk through the example and describe what's going
|
||||
on.
|
||||
|
||||
In the `<html>` tag, we add an attribute to let the browser know about the angular namespace:
|
||||
In the `<html>` tag, we add an attribute to let the browser know about the angular namespace.
|
||||
This ensures angular runs nicely in all major browsers. We also specify that it is an angular
|
||||
application with the `ng:app` directive. The `ng:app' will cause the angular to {@link
|
||||
dev_guide.bootstrap auto initialize} your application.
|
||||
|
||||
<html xmlns:ng="http://angularjs.org">
|
||||
<html xmlns:ng="http://angularjs.org" ng:app>
|
||||
|
||||
This ensures angular runs nicely in all major browsers.
|
||||
We load the angular using the `<script>` tag:
|
||||
|
||||
In the `<script>` tag we do two angular setup tasks:
|
||||
`<script src="http://code.angularjs.org/0.9.15/angular-0.9.15.min.js"></script>`
|
||||
|
||||
1. We load `angular.js`.
|
||||
2. The angular {@link api/angular.directive.ng:autobind ng:autobind} directive tells angular to
|
||||
{@link dev_guide.compiler compile} and manage the whole HTML document.
|
||||
|
||||
`<script src="http://code.angularjs.org/0.9.15/angular-0.9.15.min.js"
|
||||
ng:autobind></script>`
|
||||
|
||||
From the `name` attribute of the `<input>` tags, angular automatically sets up two-way data
|
||||
From the `ng:model` attribute of the `<input>` tags, angular automatically sets up two-way data
|
||||
binding, and we also demonstrate some easy input validation:
|
||||
|
||||
Quantity: <input name="qty" value="1" ng:validate="integer:0" ng:required/>
|
||||
Cost: <input name="cost" value="199.95" ng:validate="number" ng:required/>
|
||||
Quantity: <input type="integer" min="0" ng:model="qty" required >
|
||||
Cost: <input type="number" ng:model="cost" required >
|
||||
|
||||
These input widgets look normal enough, but consider these points:
|
||||
|
||||
* When this page loaded, angular bound the names of the input widgets (`qty` and `cost`) to
|
||||
variables of the same name. Think of those variables as the "Model" component of the
|
||||
Model-View-Controller design pattern.
|
||||
* Note the angular directives, {@link api/angular.widget.@ng:validate ng:validate} and {@link
|
||||
api/angular.widget.@ng:required ng:required}. You may have noticed that when you enter invalid data
|
||||
* Note the angular/HTML widget, {@link api/angular.widget.input input}.
|
||||
You may have noticed that when you enter invalid data
|
||||
or leave the the input fields blank, the borders turn red color, and the display value disappears.
|
||||
These `ng:` directives make it easier to implement field validators than coding them in JavaScript,
|
||||
These widgets make it easier to implement field validation than coding them in JavaScript,
|
||||
no? Yes.
|
||||
|
||||
And finally, the mysterious `{{ double curly braces }}`:
|
||||
|
||||
@@ -11,8 +11,8 @@ pattern, a scope's properties comprise both the model and the controller methods
|
||||
|
||||
|
||||
### Scope characteristics
|
||||
- Scopes provide APIs ({@link api/angular.scope.$watch $watch}) to observe model mutations.
|
||||
- Scopes provide APIs ({@link api/angular.scope.$apply $apply}) to propagate any model changes
|
||||
- Scopes provide APIs ({@link api/angular.module.ng.$rootScope.Scope#$watch $watch}) to observe model mutations.
|
||||
- Scopes provide APIs ({@link api/angular.module.ng.$rootScope.Scope#$apply $apply}) to propagate any model changes
|
||||
through the system into the view from outside of the "Angular realm" (controllers, services,
|
||||
Angular event handlers).
|
||||
- Scopes can be nested to isolate application components while providing access to shared model
|
||||
@@ -23,20 +23,18 @@ available as `this` within the given context. (Note: This api will change before
|
||||
|
||||
### Root scope
|
||||
|
||||
Every application has a root scope, which is the ancestor of all other scopes. The root scope is
|
||||
responsible for creating the injector which is assigned to the {@link api/angular.scope.$service
|
||||
$service} property, and initializing the services.
|
||||
Every application has a root scope, which is the ancestor of all other scopes.
|
||||
|
||||
### What is scope used for?
|
||||
|
||||
{@link dev_guide.expressions Expressions} in the view are {@link api/angular.scope.$eval evaluated}
|
||||
{@link dev_guide.expressions Expressions} in the view are {@link api/angular.module.ng.$rootScope.Scope#$eval evaluated}
|
||||
against the current scope. When HTML DOM elements are attached to a scope, expressions in those
|
||||
elements are evaluated against the attached scope.
|
||||
|
||||
There are two kinds of expressions:
|
||||
|
||||
- Binding expressions, which are observations of property changes. Property changes are reflected
|
||||
in the view during the {@link api/angular.scope.$digest digest cycle}.
|
||||
in the view during the {@link api/angular.module.ng.$rootScope.Scope#$digest digest cycle}.
|
||||
- Action expressions, which are expressions with side effects. Typically, the side effects cause
|
||||
execution of a method in a controller in response to a user action, such as clicking on a button.
|
||||
|
||||
@@ -59,41 +57,43 @@ A property write will always write to the current scope. This means that a write
|
||||
property within the scope it writes to, as shown in the following example.
|
||||
|
||||
<pre>
|
||||
var root = angular.scope();
|
||||
var child = root.$new();
|
||||
it('should inherit properties', inject(function($rootScope)) {
|
||||
var root = $rootScope;
|
||||
var child = root.$new();
|
||||
|
||||
root.name = 'angular';
|
||||
expect(child.name).toEqual('angular');
|
||||
expect(root.name).toEqual('angular');
|
||||
root.name = 'angular';
|
||||
expect(child.name).toEqual('angular');
|
||||
expect(root.name).toEqual('angular');
|
||||
|
||||
child.name = 'super-heroic framework';
|
||||
expect(child.name).toEqual('super-heroic framework');
|
||||
expect(root.name).toEqual('angular');
|
||||
child.name = 'super-heroic framework';
|
||||
expect(child.name).toEqual('super-heroic framework');
|
||||
expect(root.name).toEqual('angular');
|
||||
});
|
||||
</pre>
|
||||
|
||||
|
||||
### Scope life cycle
|
||||
1. **Creation**
|
||||
|
||||
* You can create the root scope via {@link api/angular.scope angular.scope()}.
|
||||
* To create a child scopes, you should call {@link api/angular.scope.$new parentScope.$new()}.
|
||||
* The root scope is created by the {@link api/angular.module.ng.$rootScope $rootScope} service.
|
||||
* To create a child scopes, you should call {@link api/angular.module.ng.$rootScope.Scope#$new parentScope.$new()}.
|
||||
|
||||
2. **Watcher registration**
|
||||
|
||||
Watcher registration can happen at any time and on any scope (root or child) via {@link
|
||||
api/angular.scope.$watch scope.$watch()} API.
|
||||
api/angular.module.ng.$rootScope.Scope#$watch scope.$watch()} API.
|
||||
|
||||
3. **Model mutation**
|
||||
|
||||
For mutations to be properly observed, you should make them only within the execution of the
|
||||
function passed into {@link api/angular.scope.$apply scope.$apply()} call. (Angular apis do this
|
||||
function passed into {@link api/angular.module.ng.$rootScope.Scope#$apply scope.$apply()} call. (Angular apis do this
|
||||
implicitly, so no extra `$apply` call is needed when doing synchronous work in controllers, or
|
||||
asynchronous work with {@link api/angular.service.$xhr $xhr} or {@link api/angular.service.$defer
|
||||
asynchronous work with {@link api/angular.module.ng.$http $http} or {@link api/angular.module.ng.$defer
|
||||
$defer} services.
|
||||
|
||||
4. **Mutation observation**
|
||||
|
||||
At the end of each `$apply` call {@link api/angular.scope.$digest $digest} cycle is started on
|
||||
At the end of each `$apply` call {@link api/angular.module.ng.$rootScope.Scope#$digest $digest} cycle is started on
|
||||
the root scope, which then propagates throughout all child scopes.
|
||||
|
||||
During the `$digest` cycle, all `$watch-ers` expressions or functions are checked for model
|
||||
@@ -102,7 +102,7 @@ mutation and if a mutation is detected, the `$watch-er` listener is called.
|
||||
5. **Scope destruction**
|
||||
|
||||
When child scopes are no longer needed, it is the responsibility of the child scope creator to
|
||||
destroy them via {@link api/angular.scope.$destroy scope.$destroy()} API. This will stop
|
||||
destroy them via {@link api/angular.module.ng.$rootScope.Scope#$destroy scope.$destroy()} API. This will stop
|
||||
propagation of `$digest` calls into the child scope and allow for memory used by the child scope
|
||||
models to be reclaimed by the garbage collector.
|
||||
|
||||
@@ -116,29 +116,26 @@ scopes come into play throughout and get a sense of their interactions.
|
||||
|
||||
1. At application compile time, a root scope is created and is attached to the root `<HTML>` DOM
|
||||
element.
|
||||
1. The root scope creates an {@link api/angular.injector injector} which is assigned to the
|
||||
{@link api/angular.scope.$service $service} property of the root scope.
|
||||
2. Any eager {@link api/angular.scope.$service services} are initialized at this point.
|
||||
2. During the compilation phase, the {@link dev_guide.compiler compiler} matches {@link
|
||||
api/angular.directive directives} against the DOM template. The directives usually fall into one of
|
||||
two categories:
|
||||
- Observing {@link api/angular.directive directives}, such as double-curly expressions
|
||||
`{{expression}}`, register listeners using the {@link api/angular.scope.$watch $watch()} method.
|
||||
`{{expression}}`, register listeners using the {@link api/angular.module.ng.$rootScope.Scope#$watch $watch()} method.
|
||||
This type of directive needs to be notified whenever the expression changes so that it can update
|
||||
the view.
|
||||
- Listener directives, such as {@link api/angular.directive.ng:click ng:click}, register a
|
||||
listener with the DOM. When the DOM listener fires, the directive executes the associated
|
||||
expression and updates the view using the {@link api/angular.scope.$apply $apply()} method.
|
||||
expression and updates the view using the {@link api/angular.module.ng.$rootScope.Scope#$apply $apply()} method.
|
||||
3. When an external event (such as a user action, timer or XHR) is received, the associated {@link
|
||||
dev_guide.expressions expression} must be applied to the scope through the {@link
|
||||
api/angular.scope.$apply $apply()} method so that all listeners are updated correctly.
|
||||
api/angular.module.ng.$rootScope.Scope#$apply $apply()} method so that all listeners are updated correctly.
|
||||
|
||||
|
||||
### Directives that create scopes
|
||||
In most cases, {@link api/angular.directive directives} and scopes interact but do not create new
|
||||
instances of scope. However, some directives, such as {@link api/angular.directive.ng:controller
|
||||
ng:controller} and {@link api/angular.widget.@ng:repeat ng:repeat}, create new child scopes using
|
||||
the {@link api/angular.scope.$new $new()} method and then attach the child scope to the
|
||||
the {@link api/angular.module.ng.$rootScope.Scope#$new $new()} method and then attach the child scope to the
|
||||
corresponding DOM element. You can retrieve a scope for any DOM element by using an
|
||||
`angular.element(aDomElement).scope()` method call.)
|
||||
|
||||
@@ -148,13 +145,13 @@ Scopes and controllers interact with each other in the following situations:
|
||||
- Controllers use scopes to expose controller methods to templates (see {@link
|
||||
api/angular.directive.ng:controller ng:controller}).
|
||||
- Controllers define methods (behavior) that can mutate the model (properties on the scope).
|
||||
- Controllers may register {@link api/angular.scope.$watch watches} on the model. These watches
|
||||
- Controllers may register {@link api/angular.module.ng.$rootScope.Scope#$watch watches} on the model. These watches
|
||||
execute immediately after the controller behavior executes, but before the DOM gets updated.
|
||||
|
||||
See the {@link dev_guide.mvc.understanding_controller controller docs} for more information.
|
||||
|
||||
### Updating scope properties
|
||||
You can update a scope by calling its {@link api/angular.scope.$apply $apply()} method with an
|
||||
You can update a scope by calling its {@link api/angular.module.ng.$rootScope.Scope#$apply $apply()} method with an
|
||||
expression or a function as the function argument. However it is typically not necessary to do this
|
||||
explicitly. In most cases, angular intercepts all external events (such as user interactions, XHRs,
|
||||
and timers) and wraps their callbacks into the `$apply()` method call on the scope object for you
|
||||
@@ -177,8 +174,8 @@ doesn't need to worry about propagating the `$digest` call from the parent scope
|
||||
This happens automatically.
|
||||
|
||||
## Scopes in unit-testing
|
||||
You can create scopes, including the root scope, in tests using the {@link api/angular.scope
|
||||
angular.scope()} API. This allows you to mimic the run-time environment and have full control over
|
||||
You can create scopes, including the root scope, in tests by having the $rootScope injected into
|
||||
your spec. This allows you to mimic the run-time environment and have full control over
|
||||
the life cycle of the scope so that you can assert correct model transitions. Since these scopes
|
||||
are created outside the normal compilation process, their life cycles must be managed by the test.
|
||||
|
||||
@@ -188,18 +185,20 @@ within the unit-tests.
|
||||
|
||||
<pre>
|
||||
// example of a test
|
||||
var scope = angular.scope();
|
||||
scope.$watch('name', function(scope, name){
|
||||
scope.greeting = 'Hello ' + name + '!';
|
||||
});
|
||||
it('should trigger a watcher', inject(function($rootScope) {
|
||||
var scope = $rootScope;
|
||||
scope.$watch('name', function(scope, name){
|
||||
scope.greeting = 'Hello ' + name + '!';
|
||||
});
|
||||
|
||||
scope.name = 'angular';
|
||||
// The watch does not fire yet since we have to manually trigger the digest phase.
|
||||
expect(scope.greeting).toEqual(undefined);
|
||||
scope.name = 'angular';
|
||||
// The watch does not fire yet since we have to manually trigger the digest phase.
|
||||
expect(scope.greeting).toEqual(undefined);
|
||||
|
||||
// manually trigger digest phase from the test
|
||||
scope.$digest();
|
||||
expect(scope.greeting).toEqual('Hello Angular!');
|
||||
// manually trigger digest phase from the test
|
||||
scope.$digest();
|
||||
expect(scope.greeting).toEqual('Hello Angular!');
|
||||
}
|
||||
</pre>
|
||||
|
||||
|
||||
@@ -209,9 +208,14 @@ When you find it necessary to inject your own mocks in your tests, use a scope t
|
||||
service instances, as shown in the following example.
|
||||
|
||||
<pre>
|
||||
var myLocation = {};
|
||||
var scope = angular.scope(angular.service, {$location: myLocation});
|
||||
expect(scope.$service('$location')).toEqual(myLocation);
|
||||
it('should allow override of providers', inject(
|
||||
function($provide) {
|
||||
$provide.value('$location', {mode:'I am a mock'});
|
||||
},
|
||||
function($location){
|
||||
expect($location.mode).toBe('I am a mock');
|
||||
}
|
||||
)};
|
||||
</pre>
|
||||
|
||||
## Related Topics
|
||||
@@ -221,5 +225,5 @@ expect(scope.$service('$location')).toEqual(myLocation);
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.scope Angular Scope API}
|
||||
* {@link api/angular.module.ng.$rootScope.Scope Angular Scope API}
|
||||
|
||||
|
||||
@@ -13,15 +13,15 @@ the contexts in which Angular creates data-bindings between the model and the vi
|
||||
|
||||
In addition to providing the context in which data is evaluated, Angular scope objects watch for
|
||||
model changes. The scope objects also notify all components interested in any model changes (for
|
||||
example, functions registered through {@link api/angular.scope.$watch $watch}, bindings created by
|
||||
example, functions registered through {@link api/angular.module.ng.$rootScope.Scope#$watch $watch}, bindings created by
|
||||
{@link api/angular.directive.ng:bind ng:bind}, or HTML input elements).
|
||||
|
||||
Angular scope objects:
|
||||
|
||||
* Link the model, controller and view template together.
|
||||
* Provide the mechanism to watch for model changes ({@link api/angular.scope.$watch $watch}).
|
||||
* Apply model changes to the system ({@link api/angular.scope.$apply $apply}).
|
||||
* Provide the context in which expressions are evaluated ({@link api/angular.scope.$eval $eval}).
|
||||
* Provide the mechanism to watch for model changes ({@link api/angular.module.ng.$rootScope.Scope#$watch $watch}).
|
||||
* Apply model changes to the system ({@link api/angular.module.ng.$rootScope.Scope#$apply $apply}).
|
||||
* Provide the context in which expressions are evaluated ({@link api/angular.module.ng.$rootScope.Scope#$eval $eval}).
|
||||
|
||||
|
||||
## Related Topics
|
||||
@@ -31,5 +31,5 @@ Angular scope objects:
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.scope Angular Scope API}
|
||||
* {@link api/angular.module.ng.$rootScope.Scope Angular Scope API}
|
||||
|
||||
|
||||
@@ -63,4 +63,4 @@ The following illustration shows the DOM and angular scopes for the example abov
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.scope Angular Scope API}
|
||||
* {@link api/angular.module.ng.$rootScope.Scope Angular Scope API}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Angular Services: Using $location
|
||||
@description
|
||||
@@ -55,7 +54,7 @@ changes to $location are reflected into the browser address bar.
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="head">seamless integration with html5 API</td>
|
||||
<td class="head">seamless integration with HTML5 API</td>
|
||||
<td>no</td>
|
||||
<td>yes (with a fallback for legacy browsers)</td>
|
||||
</tr>
|
||||
@@ -89,26 +88,22 @@ setter methods that allow you to get or change the current URL in the browser.
|
||||
|
||||
## $location service configuration
|
||||
|
||||
To configure the `$location` service, you define the `$config` service which is an object with
|
||||
configuration properties:
|
||||
To configure the `$location` service, retrieve the
|
||||
{@link api/angular.module.ng.$locationProvider $locationProvider} and set the parameters as follows:
|
||||
|
||||
- **html5Mode**: {boolean}<br />
|
||||
`true` - see Html5 mode<br />
|
||||
|
||||
- **html5Mode(mode)**: {boolean}<br />
|
||||
`true` - see HTML5 mode<br />
|
||||
`false` - see Hashbang mode<br />
|
||||
default: `false`
|
||||
|
||||
- **hashPrefix**: {string}<br />
|
||||
- **hashPrefix(prefix)**: {string}<br />
|
||||
prefix used for Hashbang URLs (used in Hashbang mode or in legacy browser in Html5 mode)<br />
|
||||
default: `'!'`
|
||||
|
||||
### Example configuration
|
||||
<pre>
|
||||
angular.service('$config', function() {
|
||||
return {
|
||||
html5mode: true,
|
||||
hashPrefix: '!'
|
||||
};
|
||||
});
|
||||
$locationProvider.html5Mode(true).hashPrefix('!');
|
||||
</pre>
|
||||
|
||||
## Getter and setter methods
|
||||
@@ -127,16 +122,25 @@ All of the setter methods return the same `$location` object to allow chaining.
|
||||
change multiple segments in one go, chain setters like this:
|
||||
<pre>$location.path('/newValue').search({key: value});</pre>
|
||||
|
||||
All setter methods take an optional boolean flag parameter, which signifies whether current history
|
||||
record should be replaced or if a new record should be created (default). To change the current URL
|
||||
without creating a new browser history record you can call:
|
||||
<pre>$location.path('/newVal', true);</pre>
|
||||
There is a special `replace` method which can be used to tell the $location service that the next
|
||||
time the $location service is synced with the browser, the last history record should be replaced
|
||||
instead of creating a new one. This is useful when you want to implement redirection, which would
|
||||
otherwise break the back button (navigating back would retrigger the redirection). To change the
|
||||
current URL without creating a new browser history record you can call:
|
||||
<pre>
|
||||
$location.path('/someNewPath');
|
||||
$location.replace();
|
||||
// or you can chain these as: $location.path('/someNewPath').replace();
|
||||
</pre>
|
||||
|
||||
Note that the setters don't update `window.location` immediately. Instead, `$location` service is
|
||||
aware of the {@link api/angular.scope scope} life-cycle and coalesces multiple `$location`
|
||||
mutations into one "commit" to the `window.location` object during the scope `$flush` phase. Since
|
||||
any of the setters can take the replace flag, it's enough for one setter to use this flag in order
|
||||
to make the entire "commit" a replace operation rather than addition to the browser history.
|
||||
aware of the {@link api/angular.module.ng.$rootScope.Scope scope} life-cycle and coalesces multiple `$location`
|
||||
mutations into one "commit" to the `window.location` object during the scope `$digest` phase. Since
|
||||
multiple changes to the $location's state will be pushed to the browser as a single change, it's
|
||||
enough to call the `replace()` method just once to make the entire "commit" a replace operation
|
||||
rather than addition to the browser history. Once the browser is updated, the $location service
|
||||
resets the flag set by `replace()` method and future mutations will create new history records,
|
||||
unless `replace()` is called again.
|
||||
|
||||
### Setters and character encoding
|
||||
You can pass special characters to `$location` service and it will encode them according to rules
|
||||
@@ -151,11 +155,11 @@ encoded.
|
||||
`/path?search=a&b=c#hash`. The segments are encoded as well.
|
||||
|
||||
|
||||
# Hashbang and Html5 Modes
|
||||
# Hashbang and HTML5 Modes
|
||||
|
||||
`$location` service has two configuration modes which control the format of the URL in the browser
|
||||
address bar: **Hashbang mode** (the default) and the **Html5 mode** which is based on using the
|
||||
Html5 {@link http://www.w3.org/TR/html5/history.html History API}. Applications use the same API in
|
||||
address bar: **Hashbang mode** (the default) and the **HTML5 mode** which is based on using the
|
||||
HTML5 {@link http://www.w3.org/TR/html5/history.html History API}. Applications use the same API in
|
||||
both modes and the `$location` service will work with appropriate URL segments and browser APIs to
|
||||
facilitate the browser URL change and history management.
|
||||
|
||||
@@ -167,7 +171,7 @@ facilitate the browser URL change and history management.
|
||||
<tr>
|
||||
<td class="empty-corner-lt"></td>
|
||||
<td>Hashbang mode</td>
|
||||
<td>Html5 mode</td>
|
||||
<td>HTML5 mode</td>
|
||||
</tr>
|
||||
|
||||
</thead>
|
||||
@@ -206,26 +210,27 @@ In this mode, `$location` uses Hashbang URLs in all browsers.
|
||||
### Example
|
||||
|
||||
<pre>
|
||||
angular.service('$config', function() {
|
||||
return {
|
||||
html5Mode: false,
|
||||
hashPrefix: '!'
|
||||
};
|
||||
});
|
||||
it('should show example', inject(
|
||||
function($locationProvider) {
|
||||
$locationProvider.html5mode = false;
|
||||
$locationProvider.hashPrefix = '!';
|
||||
},
|
||||
function($location) {
|
||||
// open http://host.com/base/index.html#!/a
|
||||
$location.absUrl() == 'http://host.com/base/index.html#!/a'
|
||||
$location.path() == '/a'
|
||||
|
||||
// open http://host.com/base/index.html#!/a
|
||||
$location.absUrl() == 'http://host.com/base/index.html#!/a'
|
||||
$location.path() == '/a'
|
||||
$location.path('/foo')
|
||||
$location.absUrl() == 'http://host.com/base/index.html#!/foo'
|
||||
|
||||
$location.path('/foo')
|
||||
$location.absUrl() == 'http://host.com/base/index.html#!/foo'
|
||||
$location.search() == {}
|
||||
$location.search({a: 'b', c: true});
|
||||
$location.absUrl() == 'http://host.com/base/index.html#!/foo?a=b&c'
|
||||
|
||||
$location.search() == {}
|
||||
$location.search({a: 'b', c: true});
|
||||
$location.absUrl() == 'http://host.com/base/index.html#!/foo?a=b&c'
|
||||
|
||||
$location.path('/new').search('x=y');
|
||||
$location.absUrl() == 'http://host.com/base/index.html#!/new?x=y'
|
||||
$location.path('/new').search('x=y');
|
||||
$location.absUrl() == 'http://host.com/base/index.html#!/new?x=y'
|
||||
}
|
||||
));
|
||||
</pre>
|
||||
|
||||
### Crawling your app
|
||||
@@ -254,39 +259,40 @@ having to worry about whether the browser displaying your app supports the histo
|
||||
### Example
|
||||
|
||||
<pre>
|
||||
angular.service('$config', function() {
|
||||
return {
|
||||
html5Mode: true,
|
||||
hashPrefix: '!'
|
||||
};
|
||||
});
|
||||
it('should show example', inject(
|
||||
function($locationProvider) {
|
||||
$locationProvider.html5mode = true;
|
||||
$locationProvider.hashPrefix = '!';
|
||||
},
|
||||
function($location) {
|
||||
// in browser with HTML5 history support:
|
||||
// open http://host.com/#!/a -> rewrite to http://host.com/a
|
||||
// (replacing the http://host.com/#!/a history record)
|
||||
$location.path() == '/a'
|
||||
|
||||
// in browser with html5 history support:
|
||||
// open http://host.com/#!/a -> rewrite to http://host.com/a
|
||||
// (replacing the http://host.com/#!/a history record)
|
||||
$location.path() == '/a'
|
||||
$location.path('/foo');
|
||||
$location.absUrl() == 'http://host.com/foo'
|
||||
|
||||
$location.path('/foo');
|
||||
$location.absUrl() == 'http://host.com/foo'
|
||||
$location.search() == {}
|
||||
$location.search({a: 'b', c: true});
|
||||
$location.absUrl() == 'http://host.com/foo?a=b&c'
|
||||
|
||||
$location.search() == {}
|
||||
$location.search({a: 'b', c: true});
|
||||
$location.absUrl() == 'http://host.com/foo?a=b&c'
|
||||
$location.path('/new').search('x=y');
|
||||
$location.url() == 'new?x=y'
|
||||
$location.absUrl() == 'http://host.com/new?x=y'
|
||||
|
||||
$location.path('/new').search('x=y');
|
||||
$location.url() == 'new?x=y'
|
||||
$location.absUrl() == 'http://host.com/new?x=y'
|
||||
// in browser without html5 history support:
|
||||
// open http://host.com/new?x=y -> redirect to http://host.com/#!/new?x=y
|
||||
// (again replacing the http://host.com/new?x=y history item)
|
||||
$location.path() == '/new'
|
||||
$location.search() == {x: 'y'}
|
||||
|
||||
// in browser without html5 history support:
|
||||
// open http://host.com/new?x=y -> redirect to http://host.com/#!/new?x=y
|
||||
// (again replacing the http://host.com/new?x=y history item)
|
||||
$location.path() == '/new'
|
||||
$location.search() == {x: 'y'}
|
||||
|
||||
$location.path('/foo/bar');
|
||||
$location.path() == '/foo/bar'
|
||||
$location.url() == '/foo/bar?x=y'
|
||||
$location.absUrl() == 'http://host.com/#!/foo/bar?x=y'
|
||||
$location.path('/foo/bar');
|
||||
$location.path() == '/foo/bar'
|
||||
$location.url() == '/foo/bar?x=y'
|
||||
$location.absUrl() == 'http://host.com/#!/foo/bar?x=y'
|
||||
}
|
||||
));
|
||||
</pre>
|
||||
|
||||
### Fallback for legacy browsers
|
||||
@@ -343,7 +349,7 @@ takes care of all relative link issues. **Otherwise you have to specify <base
|
||||
|
||||
### Sending links among different browsers
|
||||
|
||||
Because of rewriting capability in Html5 mode, your users will be able to open regular url links in
|
||||
Because of rewriting capability in HTML5 mode, your users will be able to open regular url links in
|
||||
legacy browsers and hashbang links in modern browser:
|
||||
|
||||
- Modern browser will rewrite hashbang URLs to regular URLs.
|
||||
@@ -360,9 +366,10 @@ redirect to regular / hashbang url, as this conversion happens only during parsi
|
||||
= on page reload.
|
||||
|
||||
In this examples we use `<base href="/base/index.html" />`
|
||||
<doc:example>
|
||||
<doc:source source="false">
|
||||
|
||||
<ul class="doc-example">
|
||||
<li ng:non-bindable class="html5-hashbang-example">
|
||||
<div ng:non-bindable class="html5-hashbang-example">
|
||||
<div id="html5-mode" ng:controller="Html5Cntl">
|
||||
<h3>Browser with History API</h3>
|
||||
<ng:address-bar browser="html5"></ng:address-bar><br /><br />
|
||||
@@ -372,9 +379,9 @@ In this examples we use `<base href="/base/index.html" />`
|
||||
$location.path() = {{$location.path()}}<br />
|
||||
$location.search() = {{$location.search()}}<br />
|
||||
$location.hash() = {{$location.hash()}}<br />
|
||||
<a href="/base/first?a=b">/base/first?a=b</a> | <a
|
||||
href="sec/ond?flag#hash">sec/ond?flag#hash</a> | <a href="/base/another?search"
|
||||
ng:ext-link>external</a>
|
||||
<a href="/base/first?a=b">/base/first?a=b</a> |
|
||||
<a href="sec/ond?flag#hash">sec/ond?flag#hash</a> |
|
||||
<a href="/base/another?search" ng:ext-link>external</a>
|
||||
</div>
|
||||
|
||||
<div id="hashbang-mode" ng:controller="HashbangCntl">
|
||||
@@ -386,12 +393,11 @@ ng:ext-link>external</a>
|
||||
$location.path() = {{$location.path()}}<br />
|
||||
$location.search() = {{$location.search()}}<br />
|
||||
$location.hash() = {{$location.hash()}}<br />
|
||||
<a href="/base/first?a=b">/base/first?a=b</a> | <a
|
||||
href="sec/ond?flag#hash">sec/ond?flag#hash</a> | <a href="/base/another?search"
|
||||
ng:ext-link>external</a>
|
||||
<a href="/base/first?a=b">/base/first?a=b</a> |
|
||||
<a href="sec/ond?flag#hash">sec/ond?flag#hash</a> |
|
||||
<a href="/base/another?search" ng:ext-link>external</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function FakeBrowser(initUrl, baseHref) {
|
||||
@@ -417,8 +423,7 @@ ng:ext-link>external</a>
|
||||
|
||||
var browsers = {
|
||||
html5: new FakeBrowser('http://www.host.com/base/path?a=b#h', '/base/index.html'),
|
||||
hashbang: new FakeBrowser('http://www.host.com/base/index.html#!/path?a=b#h',
|
||||
'/base/index.html')
|
||||
hashbang: new FakeBrowser('http://www.host.com/base/index.html#!/path?a=b#h', '/base/index.html')
|
||||
};
|
||||
|
||||
function Html5Cntl($location) {
|
||||
@@ -456,14 +461,14 @@ ng:ext-link>external</a>
|
||||
|
||||
function initEnv(name) {
|
||||
var root = angular.element(document.getElementById(name + '-mode'));
|
||||
var scope = angular.scope(null, {
|
||||
$config: {html5Mode: true, hashPrefix: '!'},
|
||||
$browser: browsers[name],
|
||||
$document: root,
|
||||
$sniffer: {history: name == 'html5'}
|
||||
});
|
||||
|
||||
angular.compile(root)(scope).$apply();
|
||||
angular.bootstrap(root, [function($locationProvider, $provide){
|
||||
$locationProvider.html5Mode = true;
|
||||
$locationProvider.hashPrefix = '!';
|
||||
|
||||
$provide.value('$browser', browsers[name]);
|
||||
$provide.value('$document', root);
|
||||
$provide.value('$sniffer', {history: name == 'html5'});
|
||||
}]);
|
||||
root.bind('click', function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
@@ -473,6 +478,9 @@ ng:ext-link>external</a>
|
||||
initEnv('hashbang');
|
||||
</script>
|
||||
|
||||
</doc:source>
|
||||
</doc:example>
|
||||
|
||||
|
||||
# Caveats
|
||||
|
||||
@@ -480,11 +488,11 @@ ng:ext-link>external</a>
|
||||
|
||||
The `$location` service allows you to change only the URL; it does not allow you to reload the
|
||||
page. When you need to change the URL and reload the page or navigate to a different page, please
|
||||
use a lower level API, {@link api/angular.service.$window $window.location.href}.
|
||||
use a lower level API, {@link api/angular.module.ng.$window $window.location.href}.
|
||||
|
||||
## Using $location outside of the scope life-cycle
|
||||
|
||||
`$location` knows about Angular's {@link api/angular.scope scope} life-cycle. When a URL changes in
|
||||
`$location` knows about Angular's {@link api/angular.module.ng.$rootScope.Scope scope} life-cycle. When a URL changes in
|
||||
the browser it updates the `$location` and calls `$apply` so that all $watchers / $observers are
|
||||
notified.
|
||||
When you change the `$location` inside the `$digest` phase everything is ok; `$location` will
|
||||
@@ -504,29 +512,23 @@ hashPrefix.
|
||||
# Testing with the $location service
|
||||
|
||||
When using `$location` service during testing, you are outside of the angular's {@link
|
||||
api/angular.scope scope} life-cycle. This means it's your responsibility to call `scope.$apply()`.
|
||||
api/angular.module.ng.$rootScope.Scope scope} life-cycle. This means it's your responsibility to call `scope.$apply()`.
|
||||
|
||||
<pre>
|
||||
angular.service('$serviceUnderTest', function($location) {
|
||||
// whatever it does...
|
||||
};
|
||||
|
||||
describe('$serviceUnderTest', function() {
|
||||
var scope, $location, $sut;
|
||||
|
||||
beforeEach(function() {
|
||||
scope = angular.scope();
|
||||
$location = scope.$service('$location');
|
||||
$sut = scope.$service('$serviceUnderTest');
|
||||
describe('serviceUnderTest', function() {
|
||||
beforeEach(module(function($provide) {
|
||||
$provide.factory('serviceUnderTest', function($location){
|
||||
// whatever it does...
|
||||
});
|
||||
});
|
||||
|
||||
it('should...', function() {
|
||||
it('should...', inject(function($location, $rootScope, serviceUnderTest) {
|
||||
$location.path('/new/path');
|
||||
scope.$apply();
|
||||
$rootScope.$apply();
|
||||
|
||||
// test whatever the service should do...
|
||||
|
||||
});
|
||||
}));
|
||||
});
|
||||
</pre>
|
||||
|
||||
@@ -612,7 +614,7 @@ https://github.com/angular/angular.js/issues/404 issue}). If you should require
|
||||
you will need to specify an extra property that has two watchers. For example:
|
||||
<pre>
|
||||
<!-- html -->
|
||||
<input type="text" name="locationPath" />
|
||||
<input type="text" ng:model="locationPath" />
|
||||
</pre>
|
||||
<pre>
|
||||
// js - controller
|
||||
@@ -628,7 +630,7 @@ this.$watch('$location.path()', function(scope, path) {
|
||||
|
||||
# Related API
|
||||
|
||||
* {@link api/angular.service.$location $location API}
|
||||
* {@link api/angular.module.ng.$location $location API}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,59 +1,104 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Angular Services: Creating Angular Services
|
||||
@name Developer Guide: Angular Services: Creating Services
|
||||
@description
|
||||
|
||||
While angular offers several useful services, for any nontrivial application you'll find it useful
|
||||
to write your own custom services. To do this you begin by registering a service factory function
|
||||
that angular's DI will use to create the service object when it is needed.
|
||||
|
||||
The `angular.service` method accepts three parameters:
|
||||
|
||||
- `{string} name` - Name of the service.
|
||||
- `{function()} factory` - Factory function (called just once by DI).
|
||||
- `{Object} config` - Configuration object with the following properties:
|
||||
- `$inject` - {Array.<string>} - Array of service ids this service depends on. These services
|
||||
will be passed as arguments into the factory function in the same order specified in the `$inject`
|
||||
array. Defaults to `[]`.
|
||||
- `$eager` - {boolean} - If true, the service factory will be called and the service will be
|
||||
instantiated when angular boots. If false, the service will be lazily instantiated when it is first
|
||||
requested during instantiation of a dependant. Defaults to `false`.
|
||||
|
||||
The `this` of the factory function is bound to the root scope of the angular application.
|
||||
with a module either via the {@link api/angular.module Module#factory api} or directly
|
||||
via the {@link api/angular.module.AUTO.$provide $provide} api inside of module config function.
|
||||
|
||||
All angular services participate in {@link dev_guide.di dependency injection (DI)} by registering
|
||||
themselves with angular's DI system (injector) under a `name` (id) as well as by declaring
|
||||
themselves with Angular's DI system (injector) under a `name` (id) as well as by declaring
|
||||
dependencies which need to be provided for the factory function of the registered service. The
|
||||
ability to swap dependencies for mocks/stubs/dummies in tests allows for services to be highly
|
||||
testable.
|
||||
|
||||
|
||||
# Registering Services
|
||||
|
||||
To register a service, you must have a module that this service will be part of. Afterwards, you
|
||||
can register the service with the module either via the {@link api/angular.Module Module api} or
|
||||
by using the {@link api/angular.module.AUTO.$provide $provide} service in the module configuration
|
||||
function.The following pseudo-code shows both approaches:
|
||||
|
||||
Using the angular.Module api:
|
||||
<pre>
|
||||
var myModule = angular.module('myModule', []);
|
||||
myModule.factory('serviceId', function() {
|
||||
var shinyNewServiceInstance;
|
||||
//factory function body that constructs shinyNewServiceInstance
|
||||
return shinyNewServiceInstance;
|
||||
});
|
||||
</pre>
|
||||
|
||||
Using the $provide service:
|
||||
<pre>
|
||||
angular.module('myModule', [], function($provide) {
|
||||
$provide.factory('serviceId', function() {
|
||||
var shinyNewServiceInstance;
|
||||
//factory function body that constructs shinyNewServiceInstance
|
||||
return shinyNewServiceInstance;
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
Note that you are not registering a service instance, but rather a factory function that will
|
||||
create this instance when called.
|
||||
|
||||
|
||||
# Dependencies
|
||||
|
||||
Services can not only be depended upon, but also have its own dependencies. These can be specified
|
||||
as arguments of the factory function. {@link dev_guide.di.understanding_di Read more} about the DI
|
||||
in Angular and the use of array notation and $inject property to make DI annotation
|
||||
minification-proof.
|
||||
|
||||
Following is an example of a very simple service. This service depends on the `$window` service
|
||||
(which is passed as a parameter to the factory function) and is just a function. The service simply
|
||||
stores all notifications; after the third one, the service displays all of the notifications by
|
||||
window alert.
|
||||
|
||||
<pre>
|
||||
angular.service('notify', function(win) {
|
||||
var msgs = [];
|
||||
return function(msg) {
|
||||
msgs.push(msg);
|
||||
if (msgs.length == 3) {
|
||||
win.alert(msgs.join("\n"));
|
||||
msgs = [];
|
||||
}
|
||||
};
|
||||
}, {$inject: ['$window']});
|
||||
angular.module('myModule', [], function($provide) {
|
||||
$provide.factory('notify', ['$window', function(win) {
|
||||
var msgs = [];
|
||||
return function(msg) {
|
||||
msgs.push(msg);
|
||||
if (msgs.length == 3) {
|
||||
win.alert(msgs.join("\n"));
|
||||
msgs = [];
|
||||
}
|
||||
};
|
||||
}]);
|
||||
});
|
||||
</pre>
|
||||
|
||||
|
||||
# Instantiating Angular Services
|
||||
|
||||
All services in Angular are instantiates services lazily, this means that a service will be created
|
||||
only when it is needed for instantiation of a service or an application component that depends on it.
|
||||
In other words, angular won't instantiate lazy services unless they are requested directly or
|
||||
indirectly by the application.
|
||||
|
||||
|
||||
# Services as singletons
|
||||
|
||||
Lastly, it is important to realize that all angular services are application singletons. This means
|
||||
that there is only one instance of a given service per injector. Since angular is lethally allergic
|
||||
to the global state, it is possible to create multiple injectors, each with its own instance of a
|
||||
given service, but that is rarely needed, except in tests where this property is crucially
|
||||
important.
|
||||
|
||||
|
||||
|
||||
## Related Topics
|
||||
|
||||
* {@link dev_guide.services.understanding_services Understanding Angular Services}
|
||||
* {@link dev_guide.services.registering_services Registering Angular Services}
|
||||
* {@link dev_guide.services.managing_dependencies Managing Service Dependencies}
|
||||
* {@link dev_guide.services.injecting_controllers Injecting Services Into Controllers }
|
||||
* {@link dev_guide.services.testing_services Testing Angular Services}
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.service Angular Service API}
|
||||
* {@link api/angular.module.ng Angular Service API}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Angular Services: Injecting Services Into Controllers
|
||||
@description
|
||||
@@ -29,19 +28,21 @@ this.secondMethod = function() {
|
||||
myController.$inject = ['$location', '$log'];
|
||||
</pre>
|
||||
|
||||
<doc:example>
|
||||
<doc:example module="MyServiceModule">
|
||||
<doc:source>
|
||||
<script type="text/javascript">
|
||||
angular.service('notify', function(win) {
|
||||
var msgs = [];
|
||||
return function(msg) {
|
||||
msgs.push(msg);
|
||||
if (msgs.length == 3) {
|
||||
win.alert(msgs.join("\n"));
|
||||
msgs = [];
|
||||
}
|
||||
};
|
||||
}, {$inject: ['$window']});
|
||||
angular.
|
||||
module('MyServiceModule', []).
|
||||
factory('notify', ['$window', function(win) {
|
||||
var msgs = [];
|
||||
return function(msg) {
|
||||
msgs.push(msg);
|
||||
if (msgs.length == 3) {
|
||||
win.alert(msgs.join("\n"));
|
||||
msgs = [];
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
function myController(notifyService) {
|
||||
this.callNotify = function(msg) {
|
||||
@@ -54,13 +55,13 @@ myController.$inject = ['notify'];
|
||||
|
||||
<div ng:controller="myController">
|
||||
<p>Let's try this simple notify service, injected into the controller...</p>
|
||||
<input ng:init="message='test'" type="text" name="message" />
|
||||
<input ng:init="message='test'" type="text" ng:model="message" />
|
||||
<button ng:click="callNotify(message);">NOTIFY</button>
|
||||
</div>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should test service', function(){
|
||||
expect(element(':input[name=message]').val()).toEqual('test');
|
||||
it('should test service', function() {
|
||||
expect(element(':input[ng\\:model="message"]').val()).toEqual('test');
|
||||
});
|
||||
</doc:scenario>
|
||||
</doc:example>
|
||||
@@ -70,10 +71,9 @@ it('should test service', function(){
|
||||
|
||||
{@link dev_guide.services.understanding_services Understanding Angular Services}
|
||||
{@link dev_guide.services.creating_services Creating Angular Services}
|
||||
{@link dev_guide.services.registering_services Registering Angular Services}
|
||||
{@link dev_guide.services.managing_dependencies Managing Service Dependencies}
|
||||
{@link dev_guide.services.testing_services Testing Angular Services}
|
||||
|
||||
## Related API
|
||||
|
||||
{@link api/angular.service Angular Service API}
|
||||
{@link api/angular.module.ng Angular Service API}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Angular Services: Managing Service Dependencies
|
||||
@description
|
||||
@@ -6,80 +5,110 @@
|
||||
Angular allows services to declare other services as dependencies needed for construction of their
|
||||
instances.
|
||||
|
||||
To declare dependencies, you specify them in the factory function signature and via the `$inject`
|
||||
property, as an array of string identifiers. Optionally the `$inject` property declaration can be
|
||||
To declare dependencies, you specify them in the factory function signature and annotate the
|
||||
function with the inject annotations either using by setting the `$inject` property, as an array of
|
||||
string identifiers or using the array notation. Optionally the `$inject` property declaration can be
|
||||
dropped (see "Inferring `$inject`" but note that that is currently an experimental feature).
|
||||
|
||||
Using the array notation:
|
||||
|
||||
<pre>
|
||||
function myModuleCfgFn($provide) {
|
||||
$provide.factory('myService', ['dep1', 'dep2', function(dep1, dep2) {}]);
|
||||
}
|
||||
</pre>
|
||||
|
||||
|
||||
Using the $inject property:
|
||||
|
||||
<pre>
|
||||
function myModuleCfgFn($provide) {
|
||||
var myServiceFactory = function(dep1, dep2) {};
|
||||
myServiceFactory.$inject = ['dep1', 'dep2'];
|
||||
$provide.factory('myService', myServiceFactory);
|
||||
}
|
||||
</pre>
|
||||
|
||||
|
||||
Using DI inference (incompatible with minifiers):
|
||||
|
||||
<pre>
|
||||
function myModuleCfgFn($provide) {
|
||||
$provide.factory('myService', function(dep1, dep2) {});
|
||||
}
|
||||
</pre>
|
||||
|
||||
|
||||
Here is an example of two services that depend on each other, as well as on other services that are
|
||||
provided by angular's web framework:
|
||||
provided by Angular's web framework:
|
||||
|
||||
<pre>
|
||||
/**
|
||||
* batchLog service allows for messages to be queued in memory and flushed
|
||||
* to the console.log every 50 seconds.
|
||||
*
|
||||
* @param {*} message Message to be logged.
|
||||
*/
|
||||
angular.service('batchLog', function($defer, $log) {
|
||||
var messageQueue = [];
|
||||
* batchLog service allows for messages to be queued in memory and flushed
|
||||
* to the console.log every 50 seconds.
|
||||
*
|
||||
* @param {*} message Message to be logged.
|
||||
*/
|
||||
function batchLogModule($provide){
|
||||
$provide.factory('batchLog', ['$defer', '$log', function($defer, $log) {
|
||||
var messageQueue = [];
|
||||
|
||||
function log() {
|
||||
if (messageQueue.length) {
|
||||
$log('batchLog messages: ', messageQueue);
|
||||
messageQueue = [];
|
||||
}
|
||||
$defer(log, 50000);
|
||||
function log() {
|
||||
if (messageQueue.length) {
|
||||
$log('batchLog messages: ', messageQueue);
|
||||
messageQueue = [];
|
||||
}
|
||||
$defer(log, 50000);
|
||||
}
|
||||
|
||||
// start periodic checking
|
||||
log();
|
||||
|
||||
return function(message) {
|
||||
messageQueue.push(message);
|
||||
}
|
||||
}]);
|
||||
|
||||
/**
|
||||
* routeTemplateMonitor monitors each $route change and logs the current
|
||||
* template via the batchLog service.
|
||||
*/
|
||||
$provide.factory('routeTemplateMonitor',
|
||||
['$route', 'batchLog', '$rootScope',
|
||||
function($route, batchLog, $rootScope) {
|
||||
$rootScope.$on('$afterRouteChange', function() {
|
||||
batchLog($route.current ? $route.current.template : null);
|
||||
});
|
||||
}]);
|
||||
}
|
||||
|
||||
// start periodic checking
|
||||
log();
|
||||
|
||||
return function(message) {
|
||||
messageQueue.push(message);
|
||||
}
|
||||
}, {$inject: ['$defer', '$log']});
|
||||
// note how we declared dependency on built-in $defer and $log services above
|
||||
|
||||
/**
|
||||
* routeTemplateMonitor monitors each $route change and logs the current
|
||||
* template via the batchLog service.
|
||||
*/
|
||||
angular.service('routeTemplateMonitor', function($route, batchLog) {
|
||||
this.$on('$afterRouteChange', function() {
|
||||
batchLog($route.current ? $route.current.template : null);
|
||||
});
|
||||
}, {$inject: ['$route', 'batchLog'], $eager: true});
|
||||
// get the main service to kick of the application
|
||||
angular.injector([batchLogModule]).get('routeTemplateMonitor');
|
||||
</pre>
|
||||
|
||||
Things to notice in this example:
|
||||
|
||||
* The `batchLog` service depends on the built-in {@link api/angular.service.$defer $defer} and
|
||||
{@link api/angular.service.$log $log} services, and allows messages to be logged into the
|
||||
* The `batchLog` service depends on the built-in {@link api/angular.module.ng.$defer $defer} and
|
||||
{@link api/angular.module.ng.$log $log} services, and allows messages to be logged into the
|
||||
`console.log` in batches.
|
||||
* The `routeTemplateMonitor` service depends on the built-in {@link api/angular.service.$route
|
||||
* The `routeTemplateMonitor` service depends on the built-in {@link api/angular.module.ng.$route
|
||||
$route} service as well as our custom `batchLog` service.
|
||||
* The `routeTemplateMonitor` service is declared to be eager, so that it is started as soon as the
|
||||
application starts.
|
||||
* To underline the need for the eager instantiation of the `routeTemplateMonitor` service, nothing
|
||||
else in the application depends on this service, and in this particular case the factory function
|
||||
of this service doesn't return anything at all.
|
||||
* Both of our services use the factory function signature as well as the `$inject` property to
|
||||
declare their dependencies. It is important that the order of the string identifiers in the array
|
||||
associated with the `$inject` property is the same as the order of argument names in the signature
|
||||
of the factory function. Unless the dependencies are inferred from the function signature, it is
|
||||
this array with IDs and their order that the injector uses to determine which services and in which
|
||||
order to inject.
|
||||
* Both of our services use the factory function signature and array notation for inject annotations
|
||||
to declare their dependencies. It is important that the order of the string identifiers in the array
|
||||
is the same as the order of argument names in the signature of the factory function. Unless the
|
||||
dependencies are inferred from the function signature, it is this array with IDs and their order
|
||||
that the injector uses to determine which services and in which order to inject.
|
||||
|
||||
|
||||
## Related Topics
|
||||
|
||||
* {@link dev_guide.services.understanding_services Understanding Angular Services}
|
||||
* {@link dev_guide.services.creating_services Creating Angular Services}
|
||||
* {@link dev_guide.services.registering_services Registering Services}
|
||||
* {@link dev_guide.services.injecting_controllers Injecting Services Into Controllers }
|
||||
* {@link dev_guide.services.testing_services Testing Angular Services}
|
||||
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.service Angular Service API}
|
||||
* {@link api/angular.module.ng Angular Service API}
|
||||
* {@link api/angular.injector Angular Injector API}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Angular Services
|
||||
@description
|
||||
@@ -13,11 +12,10 @@ most often used with {@link dev_guide.di dependency injection}, also a key featu
|
||||
|
||||
* {@link dev_guide.services.understanding_services Understanding Angular Services}
|
||||
* {@link dev_guide.services.creating_services Creating Angular Services}
|
||||
* {@link dev_guide.services.registering_services Registering Angular Services}
|
||||
* {@link dev_guide.services.managing_dependencies Managing Service Dependencies}
|
||||
* {@link dev_guide.services.injecting_controllers Injecting Services Into Conrollers}
|
||||
* {@link dev_guide.services.injecting_controllers Injecting Services Into Controllers}
|
||||
* {@link dev_guide.services.testing_services Testing Angular Services}
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.service Angular Service API}
|
||||
* {@link api/angular.module.ng Angular Service API}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Angular Services: Registering Angular Services
|
||||
@description
|
||||
|
||||
To register a service, register a factory function that creates the service with angular's
|
||||
Injector. The Injector is exposed as {@link api/angular.scope.$service scope.$service}. The
|
||||
following pseudo-code shows a simple service registration:
|
||||
|
||||
<pre>
|
||||
angular.service('service id', function() {
|
||||
var shinyNewServiceInstance;
|
||||
//factory function body that constructs shinyNewServiceInstance
|
||||
return shinyNewServiceInstance;
|
||||
});
|
||||
</pre>
|
||||
|
||||
Note that you are not registering a service instance, but rather a factory function that will
|
||||
create this instance when called.
|
||||
|
||||
# Instantiating Angular Services
|
||||
|
||||
A service can be instantiated eagerly or lazily. By default angular instantiates services lazily,
|
||||
which means that a service will be created only when it is needed for instantiation of a service or
|
||||
an application component that depends on it. In other words, angular won't instantiate lazy
|
||||
services unless they are requested directly or indirectly by the application.
|
||||
|
||||
Eager services on the other hand, are instantiated right after the injector itself is created,
|
||||
which happens when the angular {@link dev_guide.bootstrap application initializes}.
|
||||
|
||||
To override the default, you can request that a service is eagerly instantiated as follows:
|
||||
|
||||
<pre>
|
||||
angular.service('service id', function() {
|
||||
var shinyNewServiceInstance;
|
||||
//factory function body that constructs shinyNewServiceInstance
|
||||
return shinyNewServiceInstance;
|
||||
}, {$eager: true});
|
||||
</pre>
|
||||
|
||||
While it is tempting to declare services as eager, only in few cases it is actually useful. If you
|
||||
are unsure whether to make a service eager, it likely doesn't need to be. To be more specific, a
|
||||
service should be declared as eager only if it fits one of these scenarios:
|
||||
|
||||
* Nothing in your application declares this service as its dependency, and this service affects the
|
||||
state or configuration of the application (e.g. a service that configures `$route` or `$resource`
|
||||
services)
|
||||
* A guarantee is needed that the service will be instantiated at application boot time, usually
|
||||
because the service passively observes the application and it is optional for other application
|
||||
components to depend on it. An example of this scenario is a service that monitors and logs
|
||||
application memory usage.
|
||||
|
||||
Lastly, it is important to realize that all angular services are applicaiton singletons. This means
|
||||
that there is only one instance of a given service per injector. Since angular is lethally allergic
|
||||
to the global state, it is possible to create multiple injectors, each with its own instance of a
|
||||
given service, but that is rarely needed, except in tests where this property is crucially
|
||||
important.
|
||||
|
||||
|
||||
## Related Topics
|
||||
|
||||
* {@link dev_guide.services.understanding_services Understanding Angular Services}
|
||||
* {@link dev_guide.services.creating_services Creating Angular Services}
|
||||
* {@link dev_guide.services.managing_dependencies Managing Service Dependencies}
|
||||
* {@link dev_guide.services.injecting_controllers Injecting Services Into Controllers }
|
||||
* {@link dev_guide.services.testing_services Testing Angular Services}
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.service Angular Service API}
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Angular Services: Testing Angular Services
|
||||
@description
|
||||
@@ -12,7 +11,14 @@ var mock, notify;
|
||||
|
||||
beforeEach(function() {
|
||||
mock = {alert: jasmine.createSpy()};
|
||||
notify = angular.service('notify')(mock);
|
||||
|
||||
module(function($provide) {
|
||||
$provide.value('$window', mock);
|
||||
});
|
||||
|
||||
inject(function($injector) {
|
||||
notify = $injector.get('notify');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not alert first two notifications', function() {
|
||||
@@ -48,12 +54,9 @@ it('should clear messages after alert', function() {
|
||||
|
||||
* {@link dev_guide.services.understanding_services Understanding Angular Services}
|
||||
* {@link dev_guide.services.creating_services Creating Angular Services}
|
||||
* {@link dev_guide.services.registering_services Registering Angular Services}
|
||||
* {@link dev_guide.services.managing_dependencies Managing Service Dependencies}
|
||||
* {@link dev_guide.services.injecting_controllers Injecting Services Into Conrollers}
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.service Angular Service API}
|
||||
|
||||
|
||||
* {@link api/angular.module.ng Angular Service API}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Angular Services: Understanding Angular Services
|
||||
@description
|
||||
|
||||
Angular services are singletons that carry out specific tasks common to web apps, such as the
|
||||
{@link api/angular.service.$xhr $xhr service} that provides low level access to the browser's
|
||||
{@link api/angular.module.ng.$http $http service} that provides low level access to the browser's
|
||||
`XMLHttpRequest` object.
|
||||
|
||||
To use an angular service, you identify it as a dependency for the dependent (a controller, or
|
||||
@@ -13,14 +12,14 @@ of the rest. The angular injector subsystem is in charge of service instantiatio
|
||||
dependencies, and provision of dependencies to factory functions as requested.
|
||||
|
||||
Angular injects dependencies using "constructor" injection (the service is passed in via a factory
|
||||
function). Because JavaScript is a dynamically typed language, angular's dependency injection
|
||||
function). Because JavaScript is a dynamically typed language, Angular's dependency injection
|
||||
subsystem cannot use static types to identify service dependencies. For this reason a dependent
|
||||
must explicitly define its dependencies by using the `$inject` property. For example:
|
||||
|
||||
myController.$inject = ['$location'];
|
||||
|
||||
The angular web framework provides a set of services for common operations. Like other core angular
|
||||
variables and identifiers, the built-in services always start with `$` (such as `$xhr` mentioned
|
||||
variables and identifiers, the built-in services always start with `$` (such as `$http` mentioned
|
||||
above). You can also create your own custom services.
|
||||
|
||||
|
||||
@@ -28,11 +27,10 @@ above). You can also create your own custom services.
|
||||
|
||||
* {@link dev_guide.di About Angular Dependency Injection}
|
||||
* {@link dev_guide.services.creating_services Creating Angular Services}
|
||||
* {@link dev_guide.services.registering_services Registering Angular Services}
|
||||
* {@link dev_guide.services.managing_dependencies Managing Service Dependencies}
|
||||
* {@link dev_guide.services.testing_services Testing Angular Services}
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.service Angular Service API}
|
||||
* {@link api/angular.module.ng Angular Service API}
|
||||
* {@link api/angular.injector Injector API}
|
||||
|
||||
@@ -1,51 +1,34 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Templates: Working With CSS in Angular
|
||||
@description
|
||||
|
||||
|
||||
Angular includes built-in CSS classes, which in turn have predefined CSS styles.
|
||||
Angular sets these CSS classes. It is up to your application to provide useful styling.
|
||||
|
||||
# Built-in CSS classes
|
||||
# CSS classes used by angular
|
||||
|
||||
* `ng-exception`
|
||||
* `ng-invalid`, `ng-valid`
|
||||
- **Usage:** angular applies this class to an input widget element if that element's input does
|
||||
notpass validation. (see {@link api/angular.widget.input input} widget).
|
||||
|
||||
**Usage:** angular applies this class to a DOM element if that element contains an Expression that
|
||||
threw an exception when evaluated.
|
||||
* `ng-pristine`, `ng-dirty`
|
||||
- **Usage:** angular {@link api/angular.widget.input input} widget applies `ng-pristine` class
|
||||
to a new input widget element which did not have user interaction. Once the user interacts with
|
||||
the input widget the class is changed to `ng-dirty`.
|
||||
|
||||
**Styling:** The built-in styling of the ng-exception class displays an error message surrounded
|
||||
by a solid red border, for example:
|
||||
# Marking CSS classes
|
||||
|
||||
<div class="ng-exception">Error message</div>
|
||||
* `ng-widget`, `ng-directive`
|
||||
- **Usage:** angular sets these class on elements where {@link api/angular.widget widget} or
|
||||
{@link api/angular.directive directive} has bound to.
|
||||
|
||||
You can try to evaluate malformed expressions in {@link dev_guide.expressions expressions} to see
|
||||
the `ng-exception` class' styling.
|
||||
|
||||
* `ng-validation-error`
|
||||
|
||||
**Usage:** angular applies this class to an input widget element if that element's input does not
|
||||
pass validation. Note that you set the validation criteria on the input widget element using the
|
||||
Ng:validate or Ng:required directives.
|
||||
|
||||
**Styling:** The built-in styling of the ng-validation-error class turns the border of the input
|
||||
box red and includes a hovering UI element that includes more details of the validation error. You
|
||||
can see an example in {@link api/angular.widget.@ng:validate ng:validate example}.
|
||||
|
||||
## Overriding Styles for Angular CSS Classes
|
||||
|
||||
To override the styles for angular's built-in CSS classes, you can do any of the following:
|
||||
|
||||
* Download the source code, edit angular.css, and host the source on your own server.
|
||||
* Create a local CSS file, overriding any styles that you'd like, and link to it from your HTML file
|
||||
as you normally would:
|
||||
|
||||
<pre>
|
||||
<link href="yourfile.css" rel="stylesheet" type="text/css">
|
||||
</pre>
|
||||
|
||||
* Old browser support
|
||||
- Pre v9, IE browsers could not select `ng:include` elements in CSS, because of the `:`
|
||||
character. For this reason angular also sets `ng-include` class on any element which has `:`
|
||||
character in the name by replacing `:` with `-`.
|
||||
|
||||
## Related Topics
|
||||
|
||||
* {@link dev_guide.templates Angular Templates}
|
||||
* {@link dev_guide.templates.formatters Angular Formatters}
|
||||
* {@link dev_guide.templates.formatters.creating_formatters Creating Angular Formatters}
|
||||
* {@link dev_guide.forms Angular Forms}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Templates: Data Binding in Angular
|
||||
@description
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Templates: Filters: Creating Angular Filters
|
||||
@description
|
||||
|
||||
Writing your own filter is very easy: just define a JavaScript function on the `angular.filter`
|
||||
Writing your own filter is very easy: just define a JavaScript function on the `angular.module.ng.$filter`
|
||||
object.
|
||||
The framework passes in the input value as the first argument to your function. Any filter
|
||||
arguments are passed in as additional function arguments.
|
||||
@@ -17,38 +16,42 @@ filter to manipulate the DOM.
|
||||
The following sample filter reverses a text string. In addition, it conditionally makes the
|
||||
text upper-case and assigns color.
|
||||
|
||||
<doc:example>
|
||||
<doc:example module="MyReverseModule">
|
||||
<doc:source>
|
||||
<script type="text/javascript">
|
||||
angular.filter('reverse', function(input, uppercase, color) {
|
||||
var out = "";
|
||||
for (var i = 0; i < input.length; i++) {
|
||||
out = input.charAt(i) + out;
|
||||
}
|
||||
// conditional based on optional argument
|
||||
if (uppercase) {
|
||||
out = out.toUpperCase();
|
||||
}
|
||||
// DOM manipulation using $element
|
||||
if (color) {
|
||||
this.$element.css('color', color);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
angular.module('MyReverseModule', []).
|
||||
filter('reverse', function() {
|
||||
return function(input, uppercase) {
|
||||
var out = "";
|
||||
for (var i = 0; i < input.length; i++) {
|
||||
out = input.charAt(i) + out;
|
||||
}
|
||||
// conditional based on optional argument
|
||||
if (uppercase) {
|
||||
out = out.toUpperCase();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
});
|
||||
|
||||
function Ctrl() {
|
||||
this.greeting = 'hello';
|
||||
}
|
||||
</script>
|
||||
|
||||
<input name="text" type="text" value="hello" /><br>
|
||||
No filter: {{text}}<br>
|
||||
Reverse: {{text|reverse}}<br>
|
||||
Reverse + uppercase: {{text|reverse:true}}<br>
|
||||
Reverse + uppercase + blue: {{text|reverse:true:"blue"}}
|
||||
<div ng:controller="Ctrl">
|
||||
<input ng:model="greeting" type="greeting"><br>
|
||||
No filter: {{greeting}}<br>
|
||||
Reverse: {{greeting|reverse}}<br>
|
||||
Reverse + uppercase: {{greeting|reverse:true}}<br>
|
||||
</div>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should reverse text', function(){
|
||||
expect(binding('text|reverse')).toEqual('olleh');
|
||||
input('text').enter('ABC');
|
||||
expect(binding('text|reverse')).toEqual('CBA');
|
||||
});
|
||||
it('should reverse greeting', function() {
|
||||
expect(binding('greeting|reverse')).toEqual('olleh');
|
||||
input('greeting').enter('ABC');
|
||||
expect(binding('greeting|reverse')).toEqual('CBA');
|
||||
});
|
||||
</doc:scenario>
|
||||
</doc:example>
|
||||
|
||||
@@ -60,4 +63,4 @@ expect(binding('text|reverse')).toEqual('CBA');
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.filter Angular Filter API}
|
||||
* {@link api/angular.module.ng.$filter Angular Filter API}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Templates: Understanding Angular Filters
|
||||
@description
|
||||
@@ -12,7 +11,8 @@ displaying it to the user. You can pass expressions through a chain of filters l
|
||||
|
||||
name | uppercase
|
||||
|
||||
The expression evaluator simply passes the value of name to `angular.filter.uppercase()`.
|
||||
The expression evaluator simply passes the value of name to
|
||||
{@link api/angular.module.ng.$filter.uppercase uppercase filter}.
|
||||
|
||||
In addition to formatting data, filters can also modify the DOM. This allows filters to handle
|
||||
tasks such as conditionally applying CSS styles to filtered output.
|
||||
@@ -25,4 +25,4 @@ tasks such as conditionally applying CSS styles to filtered output.
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.filter Angular Filter API}
|
||||
* {@link api/angular.module.ng.$filter Angular Filter API}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Templates: Filters: Using Angular Filters
|
||||
@description
|
||||
|
||||
Filters can be part of any {@link api/angular.scope} evaluation but are typically used to format
|
||||
Filters can be part of any {@link api/angular.module.ng.$rootScope.Scope} evaluation but are typically used to format
|
||||
expressions in bindings in your templates:
|
||||
|
||||
{{ expression | filter }}
|
||||
@@ -38,4 +37,4 @@ argument that specifies how many digits to display to the right of the decimal p
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.filter Angular Filter API}
|
||||
* {@link api/angular.module.ng.$filter Angular Filter API}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Templates: Angular Formatters: Creating Angular Formatters
|
||||
@description
|
||||
|
||||
To create your own formatter, you can simply register a pair of JavaScript functions with
|
||||
`angular.formatter`. One of your functions is used to parse text from the input widget into the
|
||||
data storage format; the other function is used to format stored data into user-readable text.
|
||||
|
||||
The following example demonstrates a "reverse" formatter. Data is stored in uppercase and in
|
||||
reverse, but it is displayed in lower case and non-reversed. When a user edits the data model via
|
||||
the input widget, the input is automatically parsed into the internal data storage format, and when
|
||||
the data changes in the model, it is automatically formatted to the user-readable form for display
|
||||
in the view.
|
||||
|
||||
<pre>
|
||||
function reverse(text) {
|
||||
var reversed = [];
|
||||
for (var i = 0; i < text.length; i++) {
|
||||
reversed.unshift(text.charAt(i));
|
||||
}
|
||||
return reversed.join('');
|
||||
}
|
||||
|
||||
angular.formatter('reverse', {
|
||||
parse: function(value){
|
||||
return reverse(value||'').toUpperCase();
|
||||
},
|
||||
format: function(value){
|
||||
return reverse(value||'').toLowerCase();
|
||||
}
|
||||
});
|
||||
</pre>
|
||||
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<script type="text/javascript">
|
||||
function reverse(text) {
|
||||
var reversed = [];
|
||||
for (var i = 0; i < text.length; i++) {
|
||||
reversed.unshift(text.charAt(i));
|
||||
}
|
||||
return reversed.join('');
|
||||
}
|
||||
|
||||
angular.formatter('reverse', {
|
||||
parse: function(value){
|
||||
return reverse(value||'').toUpperCase();
|
||||
},
|
||||
format: function(value){
|
||||
return reverse(value||'').toLowerCase();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Templates: Angular Formatters
|
||||
@description
|
||||
|
||||
In angular, formatters are responsible for translating user-readable text entered in an {@link
|
||||
api/angular.widget.HTML input widget} to a JavaScript object in the data model that the application
|
||||
can manipulate.
|
||||
|
||||
You can use formatters in a template, and also in JavaScript. Angular provides built-in
|
||||
formatters, and of course you can create your own formatters.
|
||||
|
||||
## Related Topics
|
||||
|
||||
* {@link dev_guide.templates.formatters.using_formatters Using Angular Formatters}
|
||||
* {@link dev_guide.templates.formatters.creating_formatters Creating Angular Formatters}
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.formatter Angular Formatter API}
|
||||
@@ -1,9 +0,0 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Templates: Angular Formatters: Using Angular Formatters
|
||||
@description
|
||||
|
||||
The following snippet shows how to use a formatter in a template. The formatter below is
|
||||
`ng:format="reverse"`, added as an attribute to an `<input>` tag.
|
||||
|
||||
<pre>
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Understanding Angular Templates
|
||||
@description
|
||||
@@ -18,9 +17,7 @@ is {@link api/angular.widget.@ng:repeat ng:repeat}.
|
||||
* {@link dev_guide.compiler.markup Markup} — Shorthand for a widget or a directive. The double
|
||||
curly brace notation `{{ }}` to bind expressions to elements is built-in angular markup.
|
||||
* {@link dev_guide.templates.filters Filter} — Formats your data for display to the user.
|
||||
* {@link dev_guide.templates.validators Validator} — Lets you validate user input.
|
||||
* {@link dev_guide.templates.formatters Formatter} — Lets you format the input object into a user
|
||||
readable view.
|
||||
* {@link dev_guide.forms Form widgets} — Lets you validate user input.
|
||||
|
||||
Note: In addition to declaring the elements above in templates, you can also access these elements
|
||||
in JavaScript code.
|
||||
@@ -30,15 +27,15 @@ angular {@link dev_guide.compiler.directives directives}, {@link dev_guide.compi
|
||||
and {@link dev_guide.expressions expressions}:
|
||||
|
||||
<pre>
|
||||
<html>
|
||||
<html ng:app>
|
||||
<!-- Body tag augmented with ng:controller directive -->
|
||||
<body ng:controller="MyController">
|
||||
<input name="foo" value="bar">
|
||||
<input ng:model="foo" value="bar">
|
||||
<!-- Button tag with ng:click directive, and
|
||||
string expression 'buttonText'
|
||||
wrapped in "{{ }}" markup -->
|
||||
<button ng:click="changeFoo()">{{buttonText}}</button>
|
||||
<script src="angular.js" ng:autobind>
|
||||
<script src="angular.js">
|
||||
</body>
|
||||
</html>
|
||||
</pre>
|
||||
@@ -46,7 +43,7 @@ and {@link dev_guide.expressions expressions}:
|
||||
In a simple single-page app, the template consists of HTML, CSS, and angular directives contained
|
||||
in just one HTML file (usually `index.html`). In a more complex app, you can display multiple views
|
||||
within one main page using "partials", which are segments of template located in separate HTML
|
||||
files. You "include" the partials in the main page using the {@link api/angular.service.$route
|
||||
files. You "include" the partials in the main page using the {@link api/angular.module.ng.$route
|
||||
$route} service in conjunction with the {@link api/angular.widget.ng:view ng:view} directive. An
|
||||
example of this technique is shown in the {@link tutorial/ angular tutorial}, in steps seven and
|
||||
eight.
|
||||
@@ -55,8 +52,7 @@ eight.
|
||||
## Related Topics
|
||||
|
||||
* {@link dev_guide.templates.filters Angular Filters}
|
||||
* {@link dev_guide.templates.formatters Angular Formatters}
|
||||
* {@link dev_guide.templates.validators Angular Validators}
|
||||
* {@link dev_guide.forms Angular Forms}
|
||||
|
||||
## Related API
|
||||
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Validators: Creating Angular Validators
|
||||
@description
|
||||
|
||||
|
||||
To create a custom validator, you simply add your validator code as a method onto the
|
||||
`angular.validator` object and provide input(s) for the validator function. Each input provided is
|
||||
treated as an argument to the validator function. Any additional inputs should be separated by
|
||||
commas.
|
||||
|
||||
The following bit of pseudo-code shows how to set up a custom validator:
|
||||
|
||||
<pre>
|
||||
angular.validator('your_validator', function(input [,additional params]) {
|
||||
[your validation code];
|
||||
if ( [validation succeeds] ) {
|
||||
return false;
|
||||
} else {
|
||||
return true; // No error message specified
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
|
||||
Note that this validator returns "true" when the user's input is incorrect, as in "Yes, it's true,
|
||||
there was a problem with that input". If you prefer to provide more information when a validator
|
||||
detects a problem with input, you can specify an error message in the validator that angular will
|
||||
display when the user hovers over the input widget.
|
||||
|
||||
To specify an error message, replace "`return true;`" with an error string, for example:
|
||||
|
||||
return "Must be a value between 1 and 5!";
|
||||
|
||||
Following is a sample UPS Tracking Number validator:
|
||||
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<script>
|
||||
angular.validator('upsTrackingNo', function(input, format) {
|
||||
var regexp = new RegExp("^" + format.replace(/9/g, '\\d') + "$");
|
||||
return input.match(regexp)?"":"The format must match " + format;
|
||||
});
|
||||
</script>
|
||||
<input type="text" name="trackNo" size="40"
|
||||
ng:validate="upsTrackingNo:'1Z 999 999 99 9999 999 9'"
|
||||
value="1Z 123 456 78 9012 345 6"/>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should validate correct UPS tracking number', function() {
|
||||
expect(element('input[name=trackNo]').attr('class')).
|
||||
not().toMatch(/ng-validation-error/);
|
||||
});
|
||||
|
||||
it('should not validate in correct UPS tracking number', function() {
|
||||
input('trackNo').enter('foo');
|
||||
expect(element('input[name=trackNo]').attr('class')).
|
||||
toMatch(/ng-validation-error/);
|
||||
});
|
||||
</doc:scenario>
|
||||
</doc:example>
|
||||
|
||||
In this sample validator, we specify a regular expression against which to test the user's input.
|
||||
Note that when the user's input matches `regexp`, the function returns "false" (""); otherwise it
|
||||
returns the specified error message ("true").
|
||||
|
||||
Note: you can also access the current angular scope and DOM element objects in your validator
|
||||
functions as follows:
|
||||
|
||||
* `this` === The current angular scope.
|
||||
* `this.$element` === The DOM element that contains the binding. This allows the filter to
|
||||
manipulate the DOM in addition to transforming the input.
|
||||
|
||||
|
||||
## Related Topics
|
||||
|
||||
* {@link dev_guide.templates Angular Templates}
|
||||
* {@link dev_guide.templates.filters Angular Filters}
|
||||
* {@link dev_guide.templates.formatters Angular Formatters}
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.validator API Validator Reference}
|
||||
@@ -1,131 +0,0 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Templates: Understanding Angular Validators
|
||||
@description
|
||||
|
||||
Angular validators are attributes that test the validity of different types of user input. Angular
|
||||
provides a set of built-in input validators:
|
||||
|
||||
* {@link api/angular.validator.phone phone number}
|
||||
* {@link api/angular.validator.number number}
|
||||
* {@link api/angular.validator.integer integer}
|
||||
* {@link api/angular.validator.date date}
|
||||
* {@link api/angular.validator.email email address}
|
||||
* {@link api/angular.validator.json JSON}
|
||||
* {@link api/angular.validator.regexp regular expressions}
|
||||
* {@link api/angular.validator.url URLs}
|
||||
* {@link api/angular.validator.asynchronous asynchronous}
|
||||
|
||||
You can also create your own custom validators.
|
||||
|
||||
# Using Angular Validators
|
||||
|
||||
You can use angular validators in HTML template bindings, and in JavaScript:
|
||||
|
||||
* Validators in HTML Template Bindings
|
||||
|
||||
<pre>
|
||||
<input ng:validator="validator_type:parameters" [...]>
|
||||
</pre>
|
||||
|
||||
* Validators in JavaScript
|
||||
|
||||
<pre>
|
||||
angular.validator.[validator_type](parameters)
|
||||
</pre>
|
||||
|
||||
The following example shows how to use the built-in angular integer validator:
|
||||
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
Change me: <input type="text" name="number" ng:validate="integer" value="123">
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should validate the default number string', function() {
|
||||
expect(element('input[name=number]').attr('class')).
|
||||
not().toMatch(/ng-validation-error/);
|
||||
});
|
||||
it('should not validate "foo"', function() {
|
||||
input('number').enter('foo');
|
||||
expect(element('input[name=number]').attr('class')).
|
||||
toMatch(/ng-validation-error/);
|
||||
});
|
||||
</doc:scenario>
|
||||
</doc:example>
|
||||
|
||||
# Creating an Angular Validator
|
||||
|
||||
To create a custom validator, you simply add your validator code as a method onto the
|
||||
`angular.validator` object and provide input(s) for the validator function. Each input provided is
|
||||
treated as an argument to the validator function. Any additional inputs should be separated by
|
||||
commas.
|
||||
|
||||
The following bit of pseudo-code shows how to set up a custom validator:
|
||||
|
||||
<pre>
|
||||
angular.validator('your_validator', function(input [,additional params]) {
|
||||
[your validation code];
|
||||
if ( [validation succeeds] ) {
|
||||
return false;
|
||||
} else {
|
||||
return true; // No error message specified
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
|
||||
Note that this validator returns "true" when the user's input is incorrect, as in "Yes, it's true,
|
||||
there was a problem with that input". If you prefer to provide more information when a validator
|
||||
detects a problem with input, you can specify an error message in the validator that angular will
|
||||
display when the user hovers over the input widget.
|
||||
|
||||
To specify an error message, replace "`return true;`" with an error string, for example:
|
||||
|
||||
return "Must be a value between 1 and 5!";
|
||||
|
||||
Following is a sample UPS Tracking Number validator:
|
||||
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<script>
|
||||
angular.validator('upsTrackingNo', function(input, format) {
|
||||
var regexp = new RegExp("^" + format.replace(/9/g, '\\d') + "$");
|
||||
return input.match(regexp)?"":"The format must match " + format;
|
||||
});
|
||||
</script>
|
||||
<input type="text" name="trackNo" size="40"
|
||||
ng:validate="upsTrackingNo:'1Z 999 999 99 9999 999 9'"
|
||||
value="1Z 123 456 78 9012 345 6"/>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should validate correct UPS tracking number', function() {
|
||||
expect(element('input[name=trackNo]').attr('class')).
|
||||
not().toMatch(/ng-validation-error/);
|
||||
});
|
||||
|
||||
it('should not validate in correct UPS tracking number', function() {
|
||||
input('trackNo').enter('foo');
|
||||
expect(element('input[name=trackNo]').attr('class')).
|
||||
toMatch(/ng-validation-error/);
|
||||
});
|
||||
</doc:scenario>
|
||||
</doc:example>
|
||||
|
||||
In this sample validator, we specify a regular expression against which to test the user's input.
|
||||
Note that when the user's input matches `regexp`, the function returns "false" (""); otherwise it
|
||||
returns the specified error message ("true").
|
||||
|
||||
Note: you can also access the current angular scope and DOM element objects in your validator
|
||||
functions as follows:
|
||||
|
||||
* `this` === The current angular scope.
|
||||
* `this.$element` === The DOM element that contains the binding. This allows the filter to
|
||||
manipulate the DOM in addition to transforming the input.
|
||||
|
||||
|
||||
## Related Topics
|
||||
|
||||
* {@link dev_guide.templates Angular Templates}
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link api/angular.validator Validator API}
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide: Unit Testing
|
||||
@description
|
||||
@@ -43,11 +42,11 @@ on a constructor permanently binds the call site to the type. For example lets s
|
||||
trying to instantiate an `XHR` so that we can get some data from the server.
|
||||
|
||||
<pre>
|
||||
function MyClass(){
|
||||
this.doWork = function(){
|
||||
function MyClass() {
|
||||
this.doWork = function() {
|
||||
var xhr = new XHR();
|
||||
xhr.open(method, url, true);
|
||||
xhr.onreadystatechange = function(){...}
|
||||
xhr.onreadystatechange = function() {...}
|
||||
xhr.send();
|
||||
}
|
||||
}
|
||||
@@ -61,7 +60,7 @@ patching, that is a bad idea for many reasons, which is outside the scope of thi
|
||||
The class above is hard to test since we have to resort to monkey patching:
|
||||
<pre>
|
||||
var oldXHR = XHR;
|
||||
XHR = function MockXHR(){};
|
||||
XHR = function MockXHR() {};
|
||||
var myClass = new MyClass();
|
||||
myClass.doWork();
|
||||
// assert that MockXHR got called with the right arguments
|
||||
@@ -73,8 +72,8 @@ XHR = oldXHR; // if you forget this bad things will happen
|
||||
Another way to approach the problem is look for the service in a well known location.
|
||||
|
||||
<pre>
|
||||
function MyClass(){
|
||||
this.doWork = function(){
|
||||
function MyClass() {
|
||||
this.doWork = function() {
|
||||
global.xhr({
|
||||
method:'...',
|
||||
url:'...',
|
||||
@@ -94,7 +93,7 @@ State & Singletons}
|
||||
The class above is hard to test since we have to change global state:
|
||||
<pre>
|
||||
var oldXHR = glabal.xhr;
|
||||
glabal.xhr = function mockXHR(){};
|
||||
glabal.xhr = function mockXHR() {};
|
||||
var myClass = new MyClass();
|
||||
myClass.doWork();
|
||||
// assert that mockXHR got called with the right arguments
|
||||
@@ -110,7 +109,7 @@ having the tests replace the services as needed.
|
||||
<pre>
|
||||
function MyClass() {
|
||||
var serviceRegistry = ????;
|
||||
this.doWork = function(){
|
||||
this.doWork = function() {
|
||||
var xhr = serviceRegistry.get('xhr');
|
||||
xhr({
|
||||
method:'...',
|
||||
@@ -128,7 +127,7 @@ there is only one global variable to be reset).
|
||||
The class above is hard to test since we have to change global state:
|
||||
<pre>
|
||||
var oldServiceLocator = glabal.serviceLocator;
|
||||
glabal.serviceLocator.set('xhr', function mockXHR(){});
|
||||
glabal.serviceLocator.set('xhr', function mockXHR() {});
|
||||
var myClass = new MyClass();
|
||||
myClass.doWork();
|
||||
// assert that mockXHR got called with the right arguments
|
||||
@@ -141,7 +140,7 @@ Lastly the dependency can be passed in.
|
||||
|
||||
<pre>
|
||||
function MyClass(xhr) {
|
||||
this.doWork = function(){
|
||||
this.doWork = function() {
|
||||
xhr({
|
||||
method:'...',
|
||||
url:'...',
|
||||
@@ -174,13 +173,13 @@ for your application is mixed in with DOM manipulation, it will be hard to test
|
||||
below:
|
||||
|
||||
<pre>
|
||||
function PasswordController(){
|
||||
function PasswordController() {
|
||||
// get references to DOM elements
|
||||
var msg = $('.ex1 span');
|
||||
var input = $('.ex1 input');
|
||||
var strength;
|
||||
|
||||
this.grade = function(){
|
||||
this.grade = function() {
|
||||
msg.removeClass(strength);
|
||||
var pwd = input.val();
|
||||
password.text(pwd);
|
||||
@@ -219,9 +218,9 @@ In angular the controllers are strictly separated from the DOM manipulation logi
|
||||
a much easier testability story as can be seen in this example:
|
||||
|
||||
<pre>
|
||||
function PasswordCntrl(){
|
||||
function PasswordCntrl() {
|
||||
this.password = '';
|
||||
this.grade = function(){
|
||||
this.grade = function() {
|
||||
var size = this.password.length;
|
||||
if (size > 8) {
|
||||
this.strength = 'strong';
|
||||
@@ -248,16 +247,18 @@ that such a test tells a story, rather then asserting random bits which don't se
|
||||
|
||||
|
||||
## Filters
|
||||
{@link api/angular.filter Filters} are functions which transform the data into user readable
|
||||
{@link api/angular.module.ng.$filter Filters} are functions which transform the data into user readable
|
||||
format. They are important because they remove the formatting responsibility from the application
|
||||
logic, further simplifying the application logic.
|
||||
|
||||
<pre>
|
||||
angular.filter('length', function(text){
|
||||
return (''+(text||'')).length;
|
||||
myModule.filter('length', function() {
|
||||
return function(text){
|
||||
return (''+(text||'')).length;
|
||||
}
|
||||
});
|
||||
|
||||
var length = angular.filter('length');
|
||||
var length = $filter('length');
|
||||
expect(length(null)).toEqual(0);
|
||||
expect(length('abc')).toEqual(3);
|
||||
</pre>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Developer Guide
|
||||
@description
|
||||
@@ -42,14 +41,12 @@ of the following documents before returning here to the Developer Guide:
|
||||
## {@link dev_guide.templates Angular Templates}
|
||||
|
||||
* {@link dev_guide.templates.filters Understanding Angular Filters}
|
||||
* {@link dev_guide.templates.formatters Understanding Angular Formatters}
|
||||
* {@link dev_guide.templates.validators Understanding Angular Validators}
|
||||
* {@link dev_guide.forms Understanding Angular Forms}
|
||||
|
||||
## {@link dev_guide.services Angular Services}
|
||||
|
||||
* {@link dev_guide.services.understanding_services Understanding Angular Services}
|
||||
* {@link dev_guide.services.creating_services Creating Angular Services}
|
||||
* {@link dev_guide.services.registering_services Registering Angular Services}
|
||||
* {@link dev_guide.services.managing_dependencies Managing Service Dependencies}
|
||||
* {@link dev_guide.services.testing_services Testing Angular Services}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Downloading
|
||||
@description
|
||||
@@ -21,10 +20,10 @@ example points to (non-minified) version 0.9.12:
|
||||
|
||||
<pre>
|
||||
<!doctype html>
|
||||
<html xmlns:ng="http://angularjs.org">
|
||||
<html xmlns:ng="http://angularjs.org" ng:app>
|
||||
<head>
|
||||
<title>My Angular App</title>
|
||||
<script src="http://code.angularjs.org/angular-0.9.12.js" ng:autobind></script>
|
||||
<script src="http://code.angularjs.org/angular-0.9.12.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name FAQ
|
||||
@description
|
||||
@@ -69,7 +68,7 @@ manipulate the DOM.
|
||||
### What is testability like in angular?
|
||||
|
||||
Very testable. It has an integrated dependency injection framework. See
|
||||
{@link api/angular.service service} for details.
|
||||
{@link api/angular.module.ng service} for details.
|
||||
|
||||
### How can I learn more about angular?
|
||||
|
||||
|
||||
@@ -27,18 +27,17 @@ Now let's take a closer look at that code, and see what is going on behind
|
||||
the scenes.
|
||||
|
||||
The first line of interest defines the `ng` namespace, which makes
|
||||
AngularJS work across all browsers (especially important for IE):
|
||||
AngularJS work across all browsers (especially important for IE). The
|
||||
`ng:app` tags tells angular to process the entire HTML when it is loaded:
|
||||
|
||||
<pre>
|
||||
<html xmlns:ng="http://angularjs.org">
|
||||
<html xmlns:ng="http://angularjs.org" ng:app>
|
||||
</pre>
|
||||
|
||||
The next line downloads the angular script, and instructs angular to process
|
||||
the entire HTML page when it is loaded:
|
||||
The next line downloads the angular script:
|
||||
|
||||
<pre>
|
||||
<script type="text/javascript" src="http://code.angularjs.org/angular-?.?.?.min.js"
|
||||
ng:autobind></script>
|
||||
<script type="text/javascript" src="http://code.angularjs.org/angular-?.?.?.min.js"></script>
|
||||
</pre>
|
||||
|
||||
(For details on what happens when angular processes an HTML page,
|
||||
@@ -67,7 +66,7 @@ This example demonstrates angular's two-way data binding:
|
||||
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
Your name: <input type="text" name="yourname" value="World"/>
|
||||
Your name: <input type="text" ng:model="yourname" value="World"/>
|
||||
<hr/>
|
||||
Hello {{yourname}}!
|
||||
</doc:source>
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
@ngdoc overview
|
||||
@name Tutorial
|
||||
@description
|
||||
|
||||
A great way to get introduced to Angular is to work through this tutorial, which walks you through
|
||||
the construction of an AngularJS web app. The app you will build is a catalog that displays a list
|
||||
of Android devices, lets you filter the list to see only devices that interest you, and then view
|
||||
details for any device.
|
||||
|
||||
<img src="img/tutorial/catalog_screen.png">
|
||||
|
||||
Work through the tutorial to see how Angular makes browsers smarter — without the use of extensions
|
||||
or plug-ins. As you work through the tutorial, you will:
|
||||
|
||||
* See examples of how to use client-side data binding and dependency injection to build dynamic
|
||||
views of data that change immediately in response to user actions.
|
||||
* See how Angular creates listeners on your data without the need for DOM manipulation.
|
||||
* Learn a better, easier way to test your web apps.
|
||||
* Learn how to use Angular services to make common web tasks, such as getting data into your app,
|
||||
easier.
|
||||
|
||||
And all of this works in any browser without modification to the browser!
|
||||
|
||||
When you finish the tutorial you will be able to:
|
||||
|
||||
* Create a dynamic application that works in any browser.
|
||||
* Define the differences between Angular and common JavaScript frameworks.
|
||||
* Understand how data binding works in AngularJS.
|
||||
* Use the angular-seed project to quickly boot-strap your own projects.
|
||||
* Create and run tests.
|
||||
* Identify resources for learning more about AngularJS.
|
||||
|
||||
The tutorial guides you through the entire process of building a simple application, including
|
||||
writing and running unit and end-to-end tests. Experiments at the end of each step provide
|
||||
suggestions for you learn more about AngularJS and the application you are building.
|
||||
|
||||
You can go through the whole tutorial in a couple of hours or you may want to spend a pleasant day
|
||||
really digging into it. If you're looking for a shorter introduction to AngularJS, check out the
|
||||
{@link misc/started Getting Started} document.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Working with the code
|
||||
|
||||
You can follow this tutorial and hack on the code in either the Mac/Linux or the Windows
|
||||
environment. Options for working with the tutorial are to use the Git versioning system for source
|
||||
code management or to use scripts that copy snapshots of project files into your workspace
|
||||
(`sandbox`) directory. Select one of the tabs below and follow the instructions for setting up your
|
||||
computer for your preferred option.
|
||||
|
||||
<doc:tutorial-instructions show="true">
|
||||
<doc:tutorial-instruction id="git-mac" title="Git on Mac/Linux">
|
||||
<ol>
|
||||
<li><p>Verify that you have <a href="http://java.com/">Java</a> installed by running the
|
||||
following command in a terminal window:</p>
|
||||
<pre><code>java -version</code></pre>
|
||||
<p>You will need Java to run unit tests.</p></li>
|
||||
<li><p>Download Git from the <a href="http://git-scm.com/download">Git</a> site.</p>
|
||||
<p>You can build Git from source or use the pre-compiled package.</p></li>
|
||||
<li><p>Clone the angular-phonecat repository located at <a
|
||||
href="https://github.com/angular/angular-phonecat">Github</a> by running the following command:</p>
|
||||
<pre><code>git clone git://github.com/angular/angular-phonecat.git</code></pre>
|
||||
<p>This command creates the <code>angular-phonecat</code> directory in your current
|
||||
directory.</p></li>
|
||||
<li><p>Change your current directory to <code>angular-phonecat</code>:</p>
|
||||
<pre><code>cd angular-phonecat</code></pre>
|
||||
<p>The tutorial instructions assume you are running all commands from the angular-phonecat
|
||||
directory.</p></li>
|
||||
<li><p>You will need an http server running on your system. Mac and Linux machines typically
|
||||
have Apache pre-installed, but If you don't already have one installed, you can <a
|
||||
href="https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager">install
|
||||
node.js</a>. Use <code>node</code> to run <code>scripts/web-server.js</code>, a simple bundled
|
||||
http server.</p></li>
|
||||
</ol>
|
||||
</doc:tutorial-instruction>
|
||||
|
||||
<doc:tutorial-instruction id="git-win" title="Git on Windows">
|
||||
<ol>
|
||||
<li><p>You will need Java to run unit tests, so run the following command to verify that you
|
||||
have <a href="http://java.com/">Java</a> installed and that the <code>java</code> executable is on
|
||||
your <code>PATH</code>.</p>
|
||||
<pre><code>java -version</code></pre>
|
||||
<p></p></li>
|
||||
<li><p>Install msysGit from <a href="http://git-scm.com/download">the Git</a> site.</p></li>
|
||||
<li><p>Open msysGit bash and clone the angular-phonecat repository located at <a
|
||||
href="https://github.com/angular/angular-phonecat">Github</a> by running the following command:</p>
|
||||
<pre><code>git clone git://github.com/angular/angular-phonecat.git</code></pre>
|
||||
<p>This command creates the angular-phonecat directory in your current directory.</p></li>
|
||||
<li><p>Change your current directory to angular-phonecat.</p>
|
||||
<pre><code>cd angular-phonecat</code></pre>
|
||||
<p>The tutorial instructions assume you are running all commands from the angular-phonecat
|
||||
directory.</p>
|
||||
<p>You should run all <code>git</code> commands from msysGit bash.</p>
|
||||
<p>Other commands like <code>test-server.bat</code> or <code>test.bat</code> should be
|
||||
executed from the Windows command line.</li>
|
||||
<li><p>You need an http server running on your system. If you don't already have one
|
||||
installed, you can install <a href="http://nodejs.org/">node.js</a>. Download the <a
|
||||
href="http://node-js.prcn.co.cc/">pre-compiled binaries</a>, unzip them, and then add
|
||||
<code>nodejs\bin</code> into your <code>PATH</code>. Use <code>node</code> to run
|
||||
<code>scripts\web-server.js</code>, a simple, bundled http server.</p></li>
|
||||
</ol>
|
||||
</doc:tutorial-instruction>
|
||||
|
||||
<doc:tutorial-instruction id="ss-mac" title="Snapshots on Mac/Linux">
|
||||
<ol>
|
||||
<li><p>You need Java to run unit tests, so verify that you have <a
|
||||
href="http://java.com/">Java</a> installed by running the following command in a terminal
|
||||
window:</p>
|
||||
<pre><code>java -version</code></pre>
|
||||
<li><p>Download the <a href="http://code.angularjs.org/angular-phonecat/">zip archive</a>
|
||||
containing all of the files and unzip them into the [tutorial-dir] directory</p>.</li>
|
||||
<li><p>Change your current directory to [tutorial-dir]/sandbox, as follows:</p>
|
||||
<pre><code>cd [tutorial-dir]/sandbox</code></pre>
|
||||
<p>The tutorial instructions assume you are running all commands from your
|
||||
<code>sandbox</code> directory.</p></li>
|
||||
<li><p>You need an http server running on your system and Mac and Linux machines typically
|
||||
have Apache pre-installed. If you don't have an http server installed, you can <a
|
||||
href="https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager">install
|
||||
node.js</a> and use it to run <code>scripts/web-server.js</code>, a simple bundled http
|
||||
server.</p></li>
|
||||
</ol>
|
||||
</doc:tutorial-instruction>
|
||||
|
||||
<doc:tutorial-instruction id="ss-win" title="Snapshots on Windows">
|
||||
<ol>
|
||||
<li><p>Verify that you have <a href="http://java.com/">Java</a> installed and that the
|
||||
<code>java</code> executable is on your <code>PATH</code> by running the following command in the
|
||||
Windows command line:</p>
|
||||
<pre><code>java -version</code></pre>
|
||||
<p>You need Java to run unit tests, so download the <a
|
||||
href="http://code.angularjs.org/angular-phonecat/">zip archive</a> that contains all of the files
|
||||
and unzip the files into the [tutorial-dir] directory</p></li>
|
||||
<li><p>Change your current directory to [tutorial-dir]/sandbox, as follows:</p>
|
||||
<pre><code>cd [tutorial-dir]/sandbox</code></pre>
|
||||
<p>The tutorial instructions assume you are running all commands from this directory.</p></li>
|
||||
<li><p>You need an http server running on your system, but if you don't already have one
|
||||
already installed, you can install <a href="http://nodejs.org/">node.js</a>. Download the <a
|
||||
href="http://node-js.prcn.co.cc/">pre-compiled binaries</a>, unzip them, and then add
|
||||
<code>nodejs\bin</code> into your <code>PATH</code>. Use <code>node</code> to run
|
||||
<code>scripts\web-server.js</code>, a simple bundled http server.</p></li>
|
||||
</ol>
|
||||
</doc:tutorial-instruction>
|
||||
</doc:tutorial-instructions>
|
||||
|
||||
The last thing to do is to make sure your computer has a web browser and a good text editor
|
||||
installed. Now, let's get going with {@link step_00 step 0}.
|
||||
@@ -1,216 +0,0 @@
|
||||
@ngdoc overview
|
||||
@name Tutorial: 0 - angular-seed
|
||||
@description
|
||||
|
||||
<ul doc:tutorial-nav="0"></ul>
|
||||
|
||||
|
||||
You are now ready to build the Angular phonecat application. In this step, you will become familiar
|
||||
with the most important source code files, learn how to start the development servers bundled with
|
||||
angular-seed, and run the application in the browser.
|
||||
|
||||
|
||||
<doc:tutorial-instructions show="true">
|
||||
<doc:tutorial-instruction id="git-mac" title="Git on Mac/Linux">
|
||||
<ol>
|
||||
<li><p>In angular-phonecat directory, run this command:</p>
|
||||
<pre><code>git checkout -f step-0</code></pre>
|
||||
<p>This resets your workspace to step 0 of the tutorial app.</p>
|
||||
<p>You must repeat this for every future step in the tutorial and change the number to
|
||||
the number of the step you are on. This will cause any changes you made within
|
||||
your working directory to be lost.</p></li>
|
||||
|
||||
<li>To see the app running in a browser, do one of the following:
|
||||
<ul>
|
||||
<li><b>For node.js users:</b>
|
||||
<ol>
|
||||
<li>In a <i>separate</i> terminal tab or window, run
|
||||
<code>./scripts/web-server.js</code> to start the web server.</li>
|
||||
<li>Open a browser window for the app and navigate to <a
|
||||
href="http://localhost:8000/app/index.html">http://localhost:8000/app/index.html</a></li>
|
||||
</ol>
|
||||
</li>
|
||||
<li><b>For other http servers:</b>
|
||||
<ol>
|
||||
<li>Configure the server to serve the files in the <code>angular-phonecat</code>
|
||||
directory.</li>
|
||||
<li>Navigate in your browser to
|
||||
<code>http://localhost:[port-number]/[context-path]/app/index.html</code>.</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</doc:tutorial-instruction>
|
||||
|
||||
|
||||
<doc:tutorial-instruction id="git-win" title="Git on Windows">
|
||||
<ol>
|
||||
<li><p>Open msysGit bash and run this command (in angular-phonecat directory):</p>
|
||||
<pre><code>git checkout -f step-0</code></pre>
|
||||
<p>This resets your workspace to step 0 of the tutorial app.</p>
|
||||
<p>You must repeat this for every future step in the tutorial and change the number to
|
||||
the number of the step you are on. This will cause any changes you made within
|
||||
your working directory to be lost.</p></li>
|
||||
<li>To see the app running in a browser, do one of the following:
|
||||
<ul>
|
||||
<li><b>For node.js users:</b>
|
||||
<ol>
|
||||
<li>In a <i>separate</i> terminal tab or window, run <code>node
|
||||
scripts\web-server.js</code> to start the web server.</li>
|
||||
<li>Open a browser window for the app and navigate to <a
|
||||
href="http://localhost:8000/app/index.html">http://localhost:8000/app/index.html</a></li>
|
||||
</ol>
|
||||
</li>
|
||||
<li><b>For other http servers:</b>
|
||||
<ol>
|
||||
<li>Configure the server to serve the files in the <code>angular-phonecat</code>
|
||||
directory.</li>
|
||||
<li>Navigate in your browser to
|
||||
<code>http://localhost:[port-number]/[context-path]/app/index.html</code>.</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</doc:tutorial-instruction>
|
||||
|
||||
|
||||
<doc:tutorial-instruction id="ss-mac" title="Snapshots on Mac/Linux">
|
||||
<ol>
|
||||
<li><p>In the angular-phonecat directory, run this command:</p>
|
||||
<pre><code>./goto_step.sh 0</code></pre>
|
||||
<p>This resets your workspace to step 0 of the tutorial app.</p>
|
||||
<p>You must repeat this for every future step in the tutorial and change the number to
|
||||
the number of the step you are on. This will cause any changes you made within
|
||||
your working directory to be lost.</p></li>
|
||||
<li>To see the app running in a browser, do one of the following:
|
||||
<ul>
|
||||
<li><b>For node.js users:</b>
|
||||
<ol>
|
||||
<li>In a <i>separate</i> terminal tab or window, run
|
||||
<code>./scripts/web-server.js</code> to start the web server.</li>
|
||||
<li>Open a browser window for the app and navigate to <a
|
||||
href="http://localhost:8000/app/index.html">http://localhost:8000/app/index.html</a></li>
|
||||
</ol>
|
||||
</li>
|
||||
<li><b>For other http servers:</b>
|
||||
<ol>
|
||||
<li>Configure the server to serve the files in the angular-phonecat
|
||||
<code>sandbox</code> directory.</li>
|
||||
<li>Navigate in your browser to
|
||||
<code>http://localhost:[port-number]/[context-path]/app/index.html</code>.</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</doc:tutorial-instruction>
|
||||
|
||||
|
||||
<doc:tutorial-instruction id="ss-win" title="Snapshots on Windows">
|
||||
<ol>
|
||||
<li><p>Open windows command line and run this command (in the angular-phonecat directory):</p>
|
||||
<pre><code>goto_step.bat 0</code></pre>
|
||||
<p>This resets your workspace to step 0 of the tutorial app.</p>
|
||||
<p>You must repeat this for every future step in the tutorial and change the number to
|
||||
the number of the step you are on. This will cause any changes you made within
|
||||
your working directory to be lost.</p></li>
|
||||
<li>To see the app running in a browser, do one of the following:
|
||||
<ul>
|
||||
<li><b>For node.js users:</b>
|
||||
<ol>
|
||||
<li>In a <i>separate</i> terminal tab or window, run <code>node
|
||||
scripts\web-server.js</code> to start the web server.</li>
|
||||
<li>Open a browser window for the app and navigate to <a
|
||||
href="http://localhost:8000/app/index.html">http://localhost:8000/app/index.html</a></li>
|
||||
</ol>
|
||||
</li>
|
||||
<li><b>For other http servers:</b>
|
||||
<ol>
|
||||
<li>Configure the server to serve the files in the angular-phonecat
|
||||
<code>sandbox</code> directory.</li>
|
||||
<li>Navigate in your browser to
|
||||
<code>http://localhost:[port-number]/[context-path]/app/index.html</code>.</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</doc:tutorial-instruction>
|
||||
</doc:tutorial-instructions>
|
||||
|
||||
|
||||
You can now see the page in your browser. It's not very exciting, but that's OK.
|
||||
|
||||
The static HTML page that displays "Nothing here yet!" was constructed with the HTML code shown
|
||||
below. The code contains some key Angular elements that we will need going forward.
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
<!doctype html>
|
||||
<html xmlns:ng="http://angularjs.org/">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>my angular app</title>
|
||||
<link rel="stylesheet" href="css/app.css"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Nothing here yet!
|
||||
|
||||
<script src="lib/angular/angular.js" ng:autobind></script>
|
||||
</body>
|
||||
</html>
|
||||
</pre>
|
||||
|
||||
|
||||
|
||||
## What is the code doing?
|
||||
|
||||
* xmlns declaration
|
||||
|
||||
<html xmlns:ng="http://angularjs.org">
|
||||
|
||||
This `xmlns` declaration for the `ng` namespace must be specified in all Angular applications in
|
||||
order to make Angular work with XHTML and IE versions older than 9 (regardless of whether you are
|
||||
using XHTML or HTML).
|
||||
|
||||
* Angular script tag
|
||||
|
||||
<script src="lib/angular/angular.js" ng:autobind>
|
||||
|
||||
This single line of code is all that is needed to bootstrap an angular application.
|
||||
|
||||
The code downloads the `angular.js` script and registers a callback that will be executed by the
|
||||
browser when the containing HTML page is fully downloaded. When the callback is executed, Angular
|
||||
looks for the {@link api/angular.directive.ng:autobind ng:autobind} attribute. If Angular finds
|
||||
`ng:autobind`, it creates a root scope for the application and associates it with the `<html>`
|
||||
element of the template:
|
||||
|
||||
<img src="img/tutorial/tutorial_00_final.png">
|
||||
|
||||
As you will see shortly, everything in Angular is evaluated within a scope. We'll learn more
|
||||
about this in the next steps.
|
||||
|
||||
|
||||
## What are all these files in my working directory?
|
||||
|
||||
Most of the files in your working directory come from the {@link
|
||||
https://github.com/angular/angular-seed angular-seed project} which is typically used to bootstrap
|
||||
new Angular projects. The seed project includes the latest Angular libraries, test libraries,
|
||||
scripts and a simple example app, all pre-configured for developing a typical web app.
|
||||
|
||||
For the purposes of this tutorial, we modified the angular-seed with the following changes:
|
||||
|
||||
* Removed the example app
|
||||
* Added phone images to `app/img/phones`
|
||||
* Added phone data files (JSON) to `app/phones`
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now let's go to {@link step_01 step 1} and add some content to the web app.
|
||||
|
||||
|
||||
<ul doc:tutorial-nav="0"></ul>
|
||||
@@ -1,57 +0,0 @@
|
||||
@ngdoc overview
|
||||
@name Tutorial: 1 - Static Template
|
||||
@description
|
||||
|
||||
<ul doc:tutorial-nav="1"></ul>
|
||||
|
||||
|
||||
In order to illustrate how angular enhances standard HTML, you will create a purely *static* HTML
|
||||
page and then examine how we can turn this HTML code into a template that angular will use to
|
||||
dynamically display the same result with any set of data.
|
||||
|
||||
In this step you will add some basic information about two cell phones to an HTML page.
|
||||
|
||||
|
||||
<doc:tutorial-instructions step="1" show="true"></doc:tutorial-instructions>
|
||||
|
||||
|
||||
The page now contains a list with information about two phones.
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-0...step-1 GitHub}:
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<ul>
|
||||
<li>
|
||||
<span>Nexus S</span>
|
||||
<p>
|
||||
Fast just got faster with Nexus S.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<span>Motorola XOOM™ with Wi-Fi</span>
|
||||
<p>
|
||||
The Next, Next Generation tablet.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
</pre>
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Try adding more static HTML to `index.html`. For example:
|
||||
|
||||
<p>Total number of phones: 2</p>
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
This addition to your app uses static HTML to display the list. Now, let's go to {@link step_02
|
||||
step 2} to learn how to use angular to dynamically generate the same list.
|
||||
|
||||
|
||||
<ul doc:tutorial-nav="1"></ul>
|
||||
@@ -1,202 +0,0 @@
|
||||
@ngdoc overview
|
||||
@name Tutorial: 2 - Angular Templates
|
||||
@description
|
||||
|
||||
<ul doc:tutorial-nav="2"></ul>
|
||||
|
||||
|
||||
Now it's time to make the web page dynamic -- with Angular. We'll also add a test that verifies the
|
||||
code for the controller we are going to add.
|
||||
|
||||
There are many ways to structure the code for an application. For Angular apps, we encourage the
|
||||
use of {@link http://en.wikipedia.org/wiki/Model–View–Controller the Model-View-Controller (MVC)
|
||||
design pattern} to decouple the code and to separate concerns. With that in mind, let's use a
|
||||
little Angular and JavaScript to add model, view, and controller components to our app.
|
||||
|
||||
|
||||
<doc:tutorial-instructions step="2"></doc:tutorial-instructions>
|
||||
|
||||
|
||||
The app now contains a list with three phones.
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-1...step-2 GitHub}:
|
||||
|
||||
|
||||
## Template for the View
|
||||
|
||||
The __view__ component is constructed by Angular from this template:
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<body ng:controller="PhoneListCtrl">
|
||||
|
||||
<ul>
|
||||
<li ng:repeat="phone in phones">
|
||||
{{phone.name}}
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<script src="lib/angular/angular.js" ng:autobind></script>
|
||||
<script src="js/controllers.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</pre>
|
||||
|
||||
We replaced the hard-coded phone list with the {@link api/angular.widget.@ng:repeat ng:repeat
|
||||
widget} and two {@link guide/dev_guide.expressions Angular expressions} enclosed in curly braces:
|
||||
`{{phone.name}}` and `{{phone.snippet}}`:
|
||||
|
||||
* The `ng:repeat="phone in phones"` statement in the `<li>` tag is an Angular repeater. The
|
||||
repeater tells Angular to create a `<li>` element for each phone in the list using the first `<li>`
|
||||
tag as the template.
|
||||
|
||||
<img src="img/tutorial/tutorial_02_final.png">
|
||||
|
||||
* The curly braces around `phone.name` and `phone.snippet` are examples of {@link
|
||||
guide/dev_guide.compiler.markup Angular markup}. The curly markup is shorthand for the Angular
|
||||
directive {@link api/angular.directive.ng:bind ng:bind}. An `ng:bind` directive indicates a
|
||||
template binding point to Angular. Binding points are locations in a template where Angular creates
|
||||
data-binding between the view and the model.
|
||||
|
||||
In Angular, the view is a projection of the model through the HTML template. This means that
|
||||
whenever the model changes, Angular refreshes the appropriate binding points, which updates the
|
||||
view.
|
||||
|
||||
|
||||
## Model and Controller
|
||||
|
||||
The data __model__ (a simple array of phones in object literal notation) is instantiated within
|
||||
the __controller__ function (`PhoneListCtrl`):
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
<pre>
|
||||
function PhoneListCtrl() {
|
||||
this.phones = [{"name": "Nexus S",
|
||||
"snippet": "Fast just got faster with Nexus S."},
|
||||
{"name": "Motorola XOOM™ with Wi-Fi",
|
||||
"snippet": "The Next, Next Generation tablet."},
|
||||
{"name": "MOTOROLA XOOM™",
|
||||
"snippet": "The Next, Next Generation tablet."}];
|
||||
}
|
||||
</pre>
|
||||
|
||||
|
||||
|
||||
|
||||
Although the controller is not yet doing very much controlling, it is playing a crucial role. By
|
||||
providing context for our data model, the controller allows us to establish data-binding between
|
||||
the model and the view. We connected the dots between the presentation, data, and logic components
|
||||
as follows:
|
||||
|
||||
* The name of our controller function (in the JavaScript file `controllers.js`) matches the {@link
|
||||
api/angular.directive.ng:controller ng:controller} directive in the `<body>` tag (`PhoneListCtrl`).
|
||||
* The data is instantiated within the *scope* of our controller function; our template binding
|
||||
points are located within the block bounded by the `<body ng:controller="PhoneListCtrl">` tag.
|
||||
|
||||
The concept of a scope in Angular is crucial; a scope can be seen as the glue which allows the
|
||||
template, model and controller to work together. Angular uses scopes, along with the information
|
||||
contained in the template, data model, and controller, to keep models and views separate, but in
|
||||
sync. Any changes made to the model are reflected in the view; any changes that occur in the view
|
||||
are reflected in the model.
|
||||
|
||||
To learn more about Angular scopes, see the {@link api/angular.scope angular scope documentation}.
|
||||
|
||||
|
||||
## Tests
|
||||
|
||||
The "Angular way" makes it easy to test code as it is being developed. Take a look at the following
|
||||
unit test for your newly created controller:
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
<pre>
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
describe('PhoneListCtrl', function() {
|
||||
|
||||
it('should create "phones" model with 3 phones', function() {
|
||||
var ctrl = new PhoneListCtrl();
|
||||
expect(ctrl.phones.length).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
The test verifies that we have three records in the phones array and the example demonstrates how
|
||||
easy it is to create a unit test for code in Angular. Since testing is such a critical part of
|
||||
software development, we make it easy to create tests in Angular so that developers are encouraged
|
||||
to write them.
|
||||
|
||||
Angular developers prefer the syntax of Jasmine's Behavior-driven Development (BDD) framework when
|
||||
writing tests. Although Angular does not require you to use Jasmine, we wrote all of the tests in
|
||||
this tutorial in Jasmine. You can learn about Jasmine on the {@link
|
||||
http://pivotal.github.com/jasmine/ Jasmine home page} and on the {@link
|
||||
https://github.com/pivotal/jasmine/wiki Jasmine wiki}.
|
||||
|
||||
The angular-seed project is pre-configured to run all unit tests using {@link
|
||||
http://code.google.com/p/js-test-driver/ JsTestDriver}. To run the test, do the following:
|
||||
|
||||
1. In a _separate_ terminal window or tab, go to the `angular-phonecat` directory and run
|
||||
`./scripts/test-server.sh` to start the test web server.
|
||||
|
||||
2. Open a new browser tab or window and navigate to {@link http://localhost:9876}.
|
||||
|
||||
3. Choose "Capture this browser in strict mode".
|
||||
|
||||
At this point, you can leave this tab open and forget about it. JsTestDriver will use it to
|
||||
execute the tests and report the results in the terminal.
|
||||
|
||||
4. Execute the test by running `./scripts/test.sh`
|
||||
|
||||
You should see the following or similar output:
|
||||
|
||||
Chrome: Runner reset.
|
||||
.
|
||||
Total 1 tests (Passed: 1; Fails: 0; Errors: 0) (2.00 ms)
|
||||
Chrome 11.0.696.57 Mac OS: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (2.00 ms)
|
||||
|
||||
Yay! The test passed! Or not...
|
||||
|
||||
Note: If you see errors after you run the test, close the browser tab and go back to the terminal
|
||||
and kill the script, then repeat the procedure above.
|
||||
|
||||
# Experiments
|
||||
|
||||
* Add another binding to `index.html`. For example:
|
||||
|
||||
<p>Total number of phones: {{phones.length}}</p>
|
||||
|
||||
* Create a new model property in the controller and bind to it from the template. For example:
|
||||
|
||||
this.hello = "Hello, World!"
|
||||
|
||||
Refresh your browser to make sure it says, "Hello, World!"
|
||||
|
||||
* Create a repeater that constructs a simple table:
|
||||
|
||||
<table>
|
||||
<tr><th>row number</th></tr>
|
||||
<tr ng:repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i}}</td></tr>
|
||||
</table>
|
||||
|
||||
Now, make the list 1-based by incrementing `i` by one in the binding:
|
||||
|
||||
<table>
|
||||
<tr><th>row number</th></tr>
|
||||
<tr ng:repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i+1}}</td></tr>
|
||||
</table>
|
||||
|
||||
* Make the unit test fail by changing the `toBe(3)` statement to `toBe(4)`, and rerun the
|
||||
`./scripts/test.sh` script.
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
You now have a dynamic app that features separate model, view, and controller components, and you
|
||||
are testing as you go. Now, let's go to {@link step_03 step 3} to learn how to add full text search
|
||||
to the app.
|
||||
|
||||
|
||||
<ul doc:tutorial-nav="2"></ul>
|
||||
@@ -1,182 +0,0 @@
|
||||
@ngdoc overview
|
||||
@name Tutorial: 3 - Filtering Repeaters
|
||||
@description
|
||||
|
||||
<ul doc:tutorial-nav="3"></ul>
|
||||
|
||||
|
||||
We did a lot of work in laying a foundation for the app in the last step, so now we'll do something
|
||||
simple; we will add full text search (yes, it will be simple!). We will also write an end-to-end
|
||||
test, because a good end-to-end test is a good friend. It stays with your app, keeps an eye on it,
|
||||
and quickly detects regressions.
|
||||
|
||||
|
||||
<doc:tutorial-instructions step="3"></doc:tutorial-instructions>
|
||||
|
||||
|
||||
The app now has a search box. Notice that the phone list on the page changes depending on what a
|
||||
user types into the search box.
|
||||
|
||||
The most important differences between Steps 2 and 3 are listed below. You can see the full diff on
|
||||
{@link https://github.com/angular/angular-phonecat/compare/step-2...step-3
|
||||
GitHub}:
|
||||
|
||||
|
||||
## Controller
|
||||
|
||||
We made no changes to the controller.
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
Fulltext Search: <input name="query"/>
|
||||
|
||||
<ul class="phones">
|
||||
<li ng:repeat="phone in phones.$filter(query)">
|
||||
{{phone.name}}
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
</pre>
|
||||
|
||||
We added a standard HTML `<input>` tag and used angular's {@link api/angular.Array.filter $filter}
|
||||
function to process the input for the `ng:repeater`.
|
||||
|
||||
This lets a user enter search criteria and immediately see the effects of their search on the phone
|
||||
list. This new code demonstrates the following:
|
||||
|
||||
* Data-binding. This is one of the core features in Angular. When the page loads, Angular binds the
|
||||
name of the input box to a variable of the same name in the data model and keeps the two in sync.
|
||||
|
||||
In this code, the data that a user types into the input box (named __`query`__) is immediately
|
||||
available as a filter input in the list repeater (`phone in phones.$filter(`__`query`__`)`). When
|
||||
changes to the data model cause the repeater's input to change, the repeater efficiently updates
|
||||
the DOM to reflect the current state of the model.
|
||||
|
||||
<img src="img/tutorial/tutorial_03_final.png">
|
||||
|
||||
* Use of `$filter`. The {@link api/angular.Array.filter $filter} method uses the `query` value to
|
||||
create a new array that contains only those records that match the `query`.
|
||||
|
||||
`ng:repeat` automatically updates the view in response to the changing number of phones returned
|
||||
by the `$filter`. The process is completely transparent to the developer.
|
||||
|
||||
## Test
|
||||
|
||||
In Step 2, we learned how to write and run unit tests. Unit tests are perfect for testing
|
||||
controllers and other components of our application written in JavaScript, but they can't easily
|
||||
test DOM manipulation or the wiring of our application. For these, an end-to-end test is a much
|
||||
better choice.
|
||||
|
||||
The search feature was fully implemented via templates and data-binding, so we'll write our first
|
||||
end-to-end test, to verify that the feature works.
|
||||
|
||||
__`test/e2e/scenarios.js`:__
|
||||
<pre>
|
||||
describe('PhoneCat App', function() {
|
||||
|
||||
describe('Phone list view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser().navigateTo('../../app/index.html');
|
||||
});
|
||||
|
||||
it('should filter the phone list as user types into the search box', function() {
|
||||
expect(repeater('.phones li').count()).toBe(3);
|
||||
|
||||
input('query').enter('nexus');
|
||||
expect(repeater('.phones li').count()).toBe(1);
|
||||
|
||||
input('query').enter('motorola');
|
||||
expect(repeater('.phones li').count()).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
Even though the syntax of this test looks very much like our controller unit test written with
|
||||
Jasmine, the end-to-end test uses APIs of {@link
|
||||
https://docs.google.com/document/d/11L8htLKrh6c92foV71ytYpiKkeKpM4_a5-9c3HywfIc/edit?hl=en&pli=1#
|
||||
Angular's end-to-end test runner}.
|
||||
|
||||
To run the end-to-end test, open one of the following in a new browser tab:
|
||||
|
||||
* node.js users: {@link http://localhost:8000/test/e2e/runner.html}
|
||||
* users with other http servers:
|
||||
`http://localhost:[port-number]/[context-path]/test/e2e/runner.html`
|
||||
* casual reader: {@link http://angular.github.com/angular-phonecat/step-3/test/e2e/runner.html}
|
||||
|
||||
This test verifies that the search box and the repeater are correctly wired together. Notice how
|
||||
easy it is to write end-to-end tests in Angular. Although this example is for a simple test, it
|
||||
really is that easy to set up any functional, readable, end-to-end test.
|
||||
|
||||
# Experiments
|
||||
|
||||
* Display the current value of the `query` model by adding a `{{query}}` binding into the
|
||||
`index.html` template, and see how it changes when you type in the input box.
|
||||
|
||||
* Let's see how we can get the current value of the `query` model to appear in the HTML page title.
|
||||
|
||||
You might think you could just add the {{query}} to the title tag element as follows:
|
||||
|
||||
<title>Google Phone Gallery: {{query}}</title>
|
||||
|
||||
However, when you reload the page, you won't see the expected result. This is because the "query"
|
||||
model lives in the scope defined by the body element:
|
||||
|
||||
<body ng:controller="PhoneListCtrl">
|
||||
|
||||
If you want to bind to the query model from the `<title>` element, you must __move__ the
|
||||
`ng:controller` declaration to the HTML element because it is the common parent of both the body
|
||||
and title elements:
|
||||
|
||||
<html ng:controller="PhoneListCtrl">
|
||||
|
||||
Be sure to *remove* the `ng:controller` declaration from the body element.
|
||||
|
||||
While using double curlies works fine in within the title element, you might have noticed that
|
||||
for a split second they are actually displayed to the user while the page is loading. A better
|
||||
solution would be to use the {@link api/angular.directive.ng:bind ng:bind} or {@link
|
||||
api/angular.directive.ng:bind-template ng:bind-template} directives, which are invisible to the
|
||||
user while the page is loading:
|
||||
|
||||
<title ng:bind-template="Google Phone Gallery: {{query}}">Google Phone Gallery</title>
|
||||
|
||||
* Add the following end-to-end test into the `describe` block within `test/e2e/scenarios.js`:
|
||||
|
||||
<pre>
|
||||
it('should display the current filter value within an element with id "status"',
|
||||
function() {
|
||||
expect(element('#status').text()).toMatch(/Current filter: \s*$/);
|
||||
|
||||
input('query').enter('nexus');
|
||||
|
||||
expect(element('#status').text()).toMatch(/Current filter: nexus\s*$/);
|
||||
|
||||
//alternative version of the last assertion that tests just the value of the binding
|
||||
using('#status').expect(binding('query')).toBe('nexus');
|
||||
});
|
||||
</pre>
|
||||
|
||||
Refresh the browser tab with the end-to-end test runner to see the test fail. To make the test
|
||||
pass, edit the `index.html` template to add a `div` or `p` element with `id` `"status"` and content
|
||||
with the `query` binding.
|
||||
|
||||
* Add a `pause()` statement into an end-to-end test and rerun it. You'll see the runner pause; this
|
||||
gives you the opportunity to explore the state of your application while it is displayed in the
|
||||
browser. The app is live! You can change the search query to prove it. Notice how useful this is
|
||||
for troubleshooting end-to-end tests.
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
We have now added full text search and included a test to verify that search works! Now let's go on
|
||||
to {@link step_04 step 4} to learn how to add sorting capability to the phone app.
|
||||
|
||||
|
||||
<ul doc:tutorial-nav="3"></ul>
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
@ngdoc overview
|
||||
@name Tutorial: 4 - Two-way Data Binding
|
||||
@description
|
||||
|
||||
<ul doc:tutorial-nav="4"></ul>
|
||||
|
||||
|
||||
In this step, you will add a feature to let your users control the order of the items in the phone
|
||||
list. The dynamic ordering is implemented by creating a new model property, wiring it together with
|
||||
the repeater, and letting the data binding magic do the rest of the work.
|
||||
|
||||
|
||||
<doc:tutorial-instructions step="4"></doc:tutorial-instructions>
|
||||
|
||||
|
||||
You should see that in addition to the search box, the app displays a drop down menu that allows
|
||||
users to control the order in which the phones are listed.
|
||||
|
||||
The most important differences between Steps 3 and 4 are listed below. You can see the full diff on
|
||||
{@link https://github.com/angular/angular-phonecat/compare/step-3...step-4 GitHub}:
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<ul class="controls">
|
||||
<li>
|
||||
Search: <input type="text" name="query"/>
|
||||
</li>
|
||||
<li>
|
||||
Sort by:
|
||||
<select name="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="phones">
|
||||
<li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)">
|
||||
{{phone.name}}
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
</pre>
|
||||
|
||||
We made the following changes to the `index.html` template:
|
||||
|
||||
* First, we added a `<select>` html element named `orderProp`, so that our users can pick from the
|
||||
two provided sorting options.
|
||||
|
||||
<img src="img/tutorial/tutorial_04-06_final.png">
|
||||
|
||||
* We then chained the `$filter` method with {@link api/angular.Array.orderBy `$orderBy`} method to
|
||||
further process the input into the repeater. `$orderBy` is a utility method similar to {@link
|
||||
api/angular.Array.filter `$filter`}, but instead of filtering an array, it reorders it.
|
||||
|
||||
Angular creates a two way data-binding between the select element and the `orderProp` model.
|
||||
`orderProp` is then used as the input for the `$orderBy` method.
|
||||
|
||||
As we discussed in the section about data-binding and the repeater in step 3, whenever the model
|
||||
changes (for example because a user changes the order with the select drop down menu), Angular's
|
||||
data-binding will cause the view to automatically update. No bloated DOM manipulation code is
|
||||
necessary!
|
||||
|
||||
|
||||
|
||||
## Controller
|
||||
|
||||
__`app/js/controller.js`:__
|
||||
<pre>
|
||||
/* App Controllers */
|
||||
|
||||
function PhoneListCtrl() {
|
||||
this.phones = [{"name": "Nexus S",
|
||||
"snippet": "Fast just got faster with Nexus S.",
|
||||
"age": 0},
|
||||
{"name": "Motorola XOOM™ with Wi-Fi",
|
||||
"snippet": "The Next, Next Generation tablet.",
|
||||
"age": 1},
|
||||
{"name": "MOTOROLA XOOM™",
|
||||
"snippet": "The Next, Next Generation tablet.",
|
||||
"age": 2}];
|
||||
|
||||
this.orderProp = 'age';
|
||||
}
|
||||
</pre>
|
||||
|
||||
* We modified the `phones` model - the array of phones - and added an `age` property to each phone
|
||||
record. This property is used to order phones by age.
|
||||
|
||||
* We added a line to the controller that sets the default value of `orderProp` to `age`. If we had
|
||||
not set the default value here, angular would have used the value of the first `<option>` element
|
||||
(`'name'`) when it initialized the data model.
|
||||
|
||||
This is a good time to talk about two-way data-binding. Notice that when the app is loaded in the
|
||||
browser, "Newest" is selected in the drop down menu. This is because we set `orderProp` to `'age'`
|
||||
in the controller. So the binding works in the direction from our model to the UI. Now if you
|
||||
select "Alphabetically" in the drop down menu, the model will be updated as well and the phones
|
||||
will be reordered. That is the data-binding doing its job in the opposite direction — from the UI
|
||||
to the model.
|
||||
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
The changes we made should be verified with both a unit test and an end-to-end test. Let's look at
|
||||
the unit test first.
|
||||
|
||||
__`test/unit/controllerSpec.js`:__
|
||||
<pre>
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, $browser, ctrl;
|
||||
|
||||
beforeEach(function() {
|
||||
ctrl = new PhoneListCtrl();
|
||||
});
|
||||
|
||||
|
||||
it('should create "phones" model with 3 phones', function() {
|
||||
expect(ctrl.phones.length).toBe(3);
|
||||
});
|
||||
|
||||
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(ctrl.orderProp).toBe('age');
|
||||
});
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
|
||||
The unit test now verifies that the default ordering property is set.
|
||||
|
||||
We used Jasmine's API to extract the controller construction into a `beforeEach` block, which is
|
||||
shared by all tests in the parent `describe` block.
|
||||
|
||||
To run the unit tests, once again execute the `./scripts/test.sh` script and you should see the
|
||||
following output.
|
||||
|
||||
Chrome: Runner reset.
|
||||
..
|
||||
Total 2 tests (Passed: 2; Fails: 0; Errors: 0) (3.00 ms)
|
||||
Chrome 11.0.696.57 Mac OS: Run 2 tests (Passed: 2; Fails: 0; Errors 0) (3.00 ms)
|
||||
|
||||
|
||||
Let's turn our attention to the end-to-end test.
|
||||
|
||||
__`test/e2e/scenarios.js`:__
|
||||
<pre>
|
||||
...
|
||||
it('should be possible to control phone order via the drop down select box',
|
||||
function() {
|
||||
|
||||
// narrow the dataset to make the test assertions shorter
|
||||
input('query').enter('tablet');
|
||||
|
||||
expect(repeater('.phones li', 'Phone List').column('a')).
|
||||
toEqual(["Motorola XOOM\u2122 with Wi-Fi",
|
||||
"MOTOROLA XOOM\u2122"]);
|
||||
|
||||
select('orderProp').option('alphabetical');
|
||||
|
||||
expect(repeater('.phones li', 'Phone List').column('a')).
|
||||
toEqual(["MOTOROLA XOOM\u2122",
|
||||
"Motorola XOOM\u2122 with Wi-Fi"]);
|
||||
});
|
||||
...
|
||||
</pre>
|
||||
|
||||
The end-to-end test verifies that the ordering mechanism of the select box is working correctly.
|
||||
|
||||
You can now refresh the browser tab with the end-to-end test runner to see the tests run, or you
|
||||
can see them running on {@link
|
||||
http://angular.github.com/angular-phonecat/step-4/test/e2e/runner.html
|
||||
Angular's server}.
|
||||
|
||||
# Experiments
|
||||
|
||||
* In the `PhoneListCtrl` controller, remove the statement that sets the `orderProp` value and
|
||||
you'll see that the ordering as well as the current selection in the dropdown menu will default to
|
||||
"Alphabetical".
|
||||
|
||||
* Add an `{{orderProp}}` binding into the `index.html` template to display its current value as
|
||||
text.
|
||||
|
||||
# Summary
|
||||
|
||||
Now that you have added list sorting and tested the app, go to {@link step_05 step 5} to learn
|
||||
about Angular services and how Angular uses dependency injection.
|
||||
|
||||
|
||||
<ul doc:tutorial-nav="4"></ul>
|
||||
@@ -1,216 +0,0 @@
|
||||
@ngdoc overview
|
||||
@name Tutorial: 5 - XHRs & Dependency Injection
|
||||
@description
|
||||
|
||||
<ul doc:tutorial-nav="5"></ul>
|
||||
|
||||
|
||||
Enough of building an app with three phones in a hard-coded dataset! Let's fetch a larger dataset
|
||||
from our server using one of angular's built-in {@link api/angular.service services} called {@link
|
||||
api/angular.service.$xhr $xhr}. We will use angular's {@link guide/dev_guide.di dependency
|
||||
injection (DI)} to provide the service to the `PhoneListCtrl` controller.
|
||||
|
||||
|
||||
<doc:tutorial-instructions step="5"></doc:tutorial-instructions>
|
||||
|
||||
|
||||
You should now see a list of 20 phones.
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-4...step-5
|
||||
GitHub}:
|
||||
|
||||
## Data
|
||||
|
||||
The `app/phones/phone.json` file in your project is a dataset that contains a larger list of phones
|
||||
stored in the JSON format.
|
||||
|
||||
Following is a sample of the file:
|
||||
<pre>
|
||||
[
|
||||
{
|
||||
"age": 13,
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
"snippet": "Are you ready for everything life throws your way?"
|
||||
...
|
||||
},
|
||||
...
|
||||
]
|
||||
</pre>
|
||||
|
||||
|
||||
## Controller
|
||||
|
||||
We'll use angular's {@link api/angular.service.$xhr $xhr} service in our controller to make an HTTP
|
||||
request to your web server to fetch the data in the `app/phones/phones.json` file. `$xhr` is just
|
||||
one of several built-in {@link api/angular.service angular services} that handle common operations
|
||||
in web apps. Angular injects these services for you where you need them.
|
||||
|
||||
Services are managed by angular's {@link guide/dev_guide.di DI subsystem}. Dependency injection
|
||||
helps to make your web apps both well-structured (e.g., separate components for presentation, data,
|
||||
and control) and loosely coupled (dependencies between components are not resolved by the
|
||||
components themselves, but by the DI subsystem).
|
||||
|
||||
__`app/js/controllers.js:`__
|
||||
<pre>
|
||||
function PhoneListCtrl($xhr) {
|
||||
var self = this;
|
||||
|
||||
$xhr('GET', 'phones/phones.json', function(code, response) {
|
||||
self.phones = response;
|
||||
});
|
||||
|
||||
self.orderProp = 'age';
|
||||
}
|
||||
|
||||
//PhoneListCtrl.$inject = ['$xhr'];
|
||||
</pre>
|
||||
|
||||
`$xhr` makes an HTTP GET request to our web server, asking for `phone/phones.json` (the url is
|
||||
relative to our `index.html` file). The server responds by providing the data in the json file.
|
||||
(The response might just as well have been dynamically generated by a backend server. To the
|
||||
browser and our app they both look the same. For the sake of simplicity we used a json file in this
|
||||
tutorial.)
|
||||
|
||||
The `$xhr` service takes a callback as the last argument. This callback is used to process the
|
||||
response. We assign the response to the scope controlled by the controller, as a model called
|
||||
`phones`. Notice that angular detected the json response and parsed it for us!
|
||||
|
||||
To use a service in angular, you simply declare the names of the services you need as arguments to
|
||||
the controller's constructor function, as follows:
|
||||
|
||||
function PhoneListCtrl($xhr) {...}
|
||||
|
||||
Angular's dependency injector provides services to your controller when the controller is being
|
||||
constructed. The dependency injector also takes care of creating any transitive dependencies the
|
||||
service may have (services often depend upon other services).
|
||||
|
||||
<img src="img/tutorial/xhr_service_final.png">
|
||||
|
||||
|
||||
### '$' Prefix Naming Convention
|
||||
|
||||
You can create your own services, and in fact we will do exactly that in step 11. As a naming
|
||||
convention, angular's built-in services, Scope methods and a few other angular APIs have a '$'
|
||||
prefix in front of the name. Don't use a '$' prefix when naming your services and models, in order
|
||||
to avoid any possible naming collisions.
|
||||
|
||||
### A Note on Minification
|
||||
|
||||
Since angular infers the controller's dependencies from the names of arguments to the controller's
|
||||
constructor function, if you were to {@link http://en.wikipedia.org/wiki/Minification_(programming)
|
||||
minify} the JavaScript code for `PhoneListCtrl` controller, all of its function arguments would be
|
||||
minified as well, and the dependency injector would not being able to identify services correctly.
|
||||
|
||||
To overcome issues caused by minification, just assign an array with service identifier strings
|
||||
into the `$inject` property of the controller function, just like the last line in the snippet
|
||||
(commented out) suggests:
|
||||
|
||||
PhoneListCtrl.$inject = ['$xhr'];
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
|
||||
Because we started using dependency injection and our controller has dependencies, constructing the
|
||||
controller in our tests is a bit more complicated. We could use the `new` operator and provide the
|
||||
constructor with some kind of fake `$xhr` implementation. However, the recommended (and easier) way
|
||||
is to create a controller in the test environment in the same way that angular does it in the
|
||||
production code behind the scenes, as follows:
|
||||
|
||||
<pre>
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
describe('PhoneListCtrl', function() {
|
||||
var scope, $browser, ctrl;
|
||||
|
||||
beforeEach(function() {
|
||||
scope = angular.scope();
|
||||
$browser = scope.$service('$browser');
|
||||
|
||||
$browser.xhr.expectGET('phones/phones.json')
|
||||
.respond([{name: 'Nexus S'},
|
||||
{name: 'Motorola DROID'}]);
|
||||
ctrl = scope.$new(PhoneListCtrl);
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
We created the controller in the test environment, as follows:
|
||||
|
||||
* We created a root scope object by calling `angular.scope()`
|
||||
|
||||
* We called `scope.$new(PhoneListCtrl)` to get angular to create the child scope associated with
|
||||
the `PhoneListCtrl` controller
|
||||
|
||||
Because our code now uses the `$xhr` service to fetch the phone list data in our controller, before
|
||||
we create the `PhoneListCtrl` child scope, we need to tell the testing harness to expect an
|
||||
incoming request from the controller. To do this we:
|
||||
|
||||
* Use the {@link api/angular.scope.$service `$service`} method to retrieve the `$browser` service,
|
||||
a service that angular uses to represent various browser APIs. In tests, angular automatically uses
|
||||
a mock version of this service that allows you to write tests without having to deal with these
|
||||
native APIs and the global state associated with them.
|
||||
|
||||
* Use the `$browser.xhr.expectGET` method to train the `$browser` object to expect an incoming HTTP
|
||||
request and tell it what to respond with. Note that the responses are not returned before we call
|
||||
the `$browser.xhr.flush` method.
|
||||
|
||||
Now, we will make assertions to verify that the `phones` model doesn't exist on the scope, before
|
||||
the response is received:
|
||||
|
||||
<pre>
|
||||
it('should create "phones" model with 2 phones fetched from xhr', function() {
|
||||
expect(ctrl.phones).toBeUndefined();
|
||||
$browser.xhr.flush();
|
||||
|
||||
expect(ctrl.phones).toEqual([{name: 'Nexus S'},
|
||||
{name: 'Motorola DROID'}]);
|
||||
});
|
||||
</pre>
|
||||
|
||||
* We flush the xhr queue in the browser by calling `$browser.xhr.flush()`. This causes the callback
|
||||
we passed into the `$xhr` service to be executed with the trained response.
|
||||
|
||||
* We make the assertions, verifying that the phone model now exists on the scope.
|
||||
|
||||
Finally, we verify that the default value of `orderProp` is set correctly:
|
||||
|
||||
<pre>
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(ctrl.orderProp).toBe('age');
|
||||
});
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
To run the unit tests, execute the `./scripts/test.sh` script and you should see the following
|
||||
output.
|
||||
|
||||
Chrome: Runner reset.
|
||||
..
|
||||
Total 2 tests (Passed: 2; Fails: 0; Errors: 0) (3.00 ms)
|
||||
Chrome 11.0.696.57 Mac OS: Run 2 tests (Passed: 2; Fails: 0; Errors 0) (3.00 ms)
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* At the bottom of `index.html`, add a `{{phones}}` binding to see the list of phones displayed in
|
||||
json format.
|
||||
|
||||
* In the `PhoneListCtrl` controller, pre-process the xhr response by limiting the number of phones
|
||||
to the first 5 in the list. Use the following code in the xhr callback:
|
||||
|
||||
self.phones = response.splice(0, 5);
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now that you have learned how easy it is to use angular services (thanks to angular's
|
||||
implementation of dependency injection), go to {@link step_06 step 6}, where you will add some
|
||||
thumbnail images of phones and some links.
|
||||
|
||||
|
||||
<ul doc:tutorial-nav="5"></ul>
|
||||
@@ -1,105 +0,0 @@
|
||||
@ngdoc overview
|
||||
@name Tutorial: 6 - Templating Links & Images
|
||||
@description
|
||||
|
||||
<ul doc:tutorial-nav="6"></ul>
|
||||
|
||||
|
||||
In this step, you will add thumbnail images for the phones in the phone list, and links that, for
|
||||
now, will go nowhere. In subsequent steps you will use the links to display additional information
|
||||
about the phones in the catalog.
|
||||
|
||||
|
||||
<doc:tutorial-instructions step="6"></doc:tutorial-instructions>
|
||||
|
||||
|
||||
You should now see links and images of the phones in the list.
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-5...step-6
|
||||
GitHub}:
|
||||
|
||||
|
||||
## Data
|
||||
|
||||
Note that the `phones.json` file contains unique ids and image urls for each of the phones. The
|
||||
urls point to the `app/img/phones/` directory.
|
||||
|
||||
__`app/phones/phones.json`__ (sample snippet):
|
||||
<pre>
|
||||
[
|
||||
{
|
||||
...
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
...
|
||||
},
|
||||
...
|
||||
]
|
||||
</pre>
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<ul class="phones">
|
||||
<li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)">
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<a href="#/phones/{{phone.id}}" class="thumb"><img ng:src="{{phone.imageUrl}}"></a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
</pre>
|
||||
|
||||
To dynamically generate links that will in the future lead to phone detail pages, we used the
|
||||
now-familiar {@link guide/dev_guide.compiler.markup double-curly brace markup} in the `href`
|
||||
attribute values. In step 2, we added the `{{phone.name}}` binding as the element content. In this
|
||||
step the `{{phone.id}}` binding is used in the element attribute.
|
||||
|
||||
We also added phone images next to each record using an image tag with the {@link
|
||||
api/angular.directive.ng:src ng:src} directive. That directive prevents the browser from treating
|
||||
the angular `{{ expression }}` markup literally, which it would have done if we had only specified
|
||||
an attribute binding in a regular `src` attribute (`<img src="{{phone.imageUrl}}">`). Using
|
||||
`ng:src` prevents the browser from making an http request to an invalid location.
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
__`test/e2e/scenarios.js`__:
|
||||
<pre>
|
||||
...
|
||||
it('should render phone specific links', function() {
|
||||
input('query').enter('nexus');
|
||||
element('.phones li a').click();
|
||||
expect(browser().location().hash()).toBe('/phones/nexus-s');
|
||||
});
|
||||
...
|
||||
</pre>
|
||||
|
||||
We added a new end-to-end test to verify that the app is generating correct links to the phone
|
||||
views that we will implement in the upcoming steps.
|
||||
|
||||
You can now refresh the browser tab with the end-to-end test runner to see the tests run, or you
|
||||
can see them running on {@link
|
||||
http://angular.github.com/angular-phonecat/step-6/test/e2e/runner.html
|
||||
angular's server}.
|
||||
|
||||
# Experiments
|
||||
|
||||
* Replace the `ng:src` directive with a plain old `<src>` attribute. Using tools such as Firebug,
|
||||
or Chrome's Web Inspector, or inspecting the webserver access logs, confirm that the app is indeed
|
||||
making an extraneous request to `/app/%7B%7Bphone.imageUrl%7D%7D` (or
|
||||
`/app/index.html/{{phone.imageUrl}}`).
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now that you have added phone images and links, go to {@link step_07 step 7} to learn about angular
|
||||
layout templates and how angular makes it easy to create applications that have multiple views.
|
||||
|
||||
|
||||
<ul doc:tutorial-nav="6"></ul>
|
||||
@@ -1,209 +0,0 @@
|
||||
@ngdoc overview
|
||||
@name Tutorial: 7 - Routing & Multiple Views
|
||||
@description
|
||||
|
||||
<ul doc:tutorial-nav="7"></ul>
|
||||
|
||||
|
||||
In this step, you will learn how to create a layout template and how to build an app that has
|
||||
multiple views by adding routing.
|
||||
|
||||
|
||||
<doc:tutorial-instructions step="7"></doc:tutorial-instructions>
|
||||
|
||||
|
||||
Note that you are redirected to `app/index.html#/phones` and the same phone list appears in the
|
||||
browser. When you click on a phone link the stub of a phone detail page is displayed.
|
||||
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-6...step-7
|
||||
GitHub}:
|
||||
|
||||
|
||||
## Multiple Views, Routing and Layout Template
|
||||
|
||||
Our app is slowly growing and becoming more complex. Before step 7, the app provided our users with
|
||||
a single view (the list of all phones), and all of the template code was located in the
|
||||
`index.html` file. The next step in building the app is to add a view that will show detailed
|
||||
information about each of the devices in our list.
|
||||
|
||||
To add the detailed view, we could expand the `index.html` file to contain template code for both
|
||||
views, but that would get messy very quickly. Instead, we are going to turn the `index.html`
|
||||
template into what we call a "layout template". This is a template that is common for all views in
|
||||
our application. Other "partial templates" are then included into this layout template depending on
|
||||
the current "route" — the view that is currently displayed to the user.
|
||||
|
||||
Application routes in angular are declared via the {@link api/angular.service.$route $route}
|
||||
service. This service makes it easy to wire together controllers, view templates, and the current
|
||||
URL location in the browser. Using this feature we can implement {@link
|
||||
http://en.wikipedia.org/wiki/Deep_linking deep linking}, which lets us utilize the browser's
|
||||
history (back and forward navigation) and bookmarks.
|
||||
|
||||
|
||||
## Controllers
|
||||
|
||||
__`app/js/controller.js`:__
|
||||
<pre>
|
||||
function PhoneCatCtrl($route) {
|
||||
var self = this;
|
||||
|
||||
$route.when('/phones',
|
||||
{template: 'partials/phone-list.html', controller: PhoneListCtrl});
|
||||
$route.when('/phones/:phoneId',
|
||||
{template: 'partials/phone-detail.html', controller: PhoneDetailCtrl});
|
||||
$route.otherwise({redirectTo: '/phones'});
|
||||
|
||||
$route.onChange(function() {
|
||||
self.params = $route.current.params;
|
||||
});
|
||||
|
||||
$route.parent(this);
|
||||
}
|
||||
|
||||
//PhoneCatCtrl.$inject = ['$route'];
|
||||
...
|
||||
</pre>
|
||||
|
||||
We created a new controller called `PhoneCatCtrl`. We declared its dependency on the `$route`
|
||||
service and used this service to declare that our application consists of two different views:
|
||||
|
||||
* The phone list view will be shown when the URL hash fragment is `/phones`. To construct this
|
||||
view, angular will use the `phone-list.html` template and the `PhoneListCtrl` controller.
|
||||
|
||||
* The phone details view will be shown when the URL hash fragment matches '/phone/:phoneId', where
|
||||
`:phoneId` is a variable part of the URL. To construct the phone details view, angular will use the
|
||||
`phone-detail.html` template and the `PhoneDetailCtrl` controller.
|
||||
|
||||
We reused the `PhoneListCtrl` controller that we constructed in previous steps and we added a new,
|
||||
empty `PhoneDetailCtrl` controller to the `app/js/controllers.js` file for the phone details view.
|
||||
|
||||
The statement `$route.otherwise({redirectTo: '/phones'})` triggers a redirection to `/phones` when
|
||||
the browser address doesn't match either of our routes.
|
||||
|
||||
Thanks to the `$route.parent(this);` statement and `ng:controller="PhoneCatCtrl"` declaration in
|
||||
the `index.html` template, the `PhoneCatCtrl` controller has a special role in our app. It is the
|
||||
"root" controller and the parent controller for the other two sub-controllers (`PhoneListCtrl` and
|
||||
`PhoneDetailCtrl`). The sub-controllers inherit the model properties and behavior from the root
|
||||
controller.
|
||||
|
||||
Note the use of the `:phoneId` parameter in the second route declaration. The `$route` service uses
|
||||
the route declaration — `'/phones/:phoneId'` — as a template that is matched against the current
|
||||
URL. All variables defined with the `:` notation are extracted into the `$route.current.params` map.
|
||||
|
||||
The `params` alias created in the {@link api/angular.service.$route `$route.onChange`} callback
|
||||
allows us to use the `phoneId` property of this map in the `phone-details.html` template.
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
The `$route` service is usually used in conjunction with the {@link api/angular.widget.ng:view
|
||||
ng:view} widget. The role of the `ng:view` widget is to include the view template for the current
|
||||
route into the layout template, which makes it a perfect fit for our `index.html` template.
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<body ng:controller="PhoneCatCtrl">
|
||||
|
||||
<ng:view></ng:view>
|
||||
|
||||
<script src="lib/angular/angular.js" ng:autobind></script>
|
||||
<script src="js/controllers.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</pre>
|
||||
|
||||
Note that we removed most of the code in the `index.html` template and replaced it with a single
|
||||
line containing the `ng:view` tag. The code that we removed was placed into the `phone-list.html`
|
||||
template:
|
||||
|
||||
__`app/partials/phone-list.html`:__
|
||||
<pre>
|
||||
<ul class="predicates">
|
||||
<li>
|
||||
Search: <input type="text" name="query"/>
|
||||
</li>
|
||||
<li>
|
||||
Sort by:
|
||||
<select name="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="phones">
|
||||
<li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)">
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<a href="#/phones/{{phone.id}}" class="thumb"><img ng:src="{{phone.imageUrl}}"></a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</pre>
|
||||
|
||||
<img src="img/tutorial/tutorial_07_final.png">
|
||||
|
||||
We also added a placeholder template for the phone details view:
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
<pre>
|
||||
TBD: detail view for {{params.phoneId}}
|
||||
</pre>
|
||||
|
||||
Note how we are using `params` model defined in the `PhoneCatCtrl` controller.
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
To automatically verify that everything is wired properly, we wrote end-to-end tests that navigate
|
||||
to various URLs and verify that the correct view was rendered.
|
||||
|
||||
<pre>
|
||||
...
|
||||
it('should redirect index.html to index.html#/phones', function() {
|
||||
browser().navigateTo('../../app/index.html');
|
||||
expect(browser().location().hash()).toBe('/phones');
|
||||
});
|
||||
...
|
||||
|
||||
describe('Phone detail view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser().navigateTo('../../app/index.html#/phones/nexus-s');
|
||||
});
|
||||
|
||||
|
||||
it('should display placeholder page with phoneId', function() {
|
||||
expect(binding('params.phoneId')).toBe('nexus-s');
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
|
||||
You can now refresh the browser tab with the end-to-end test runner to see the tests run, or you
|
||||
can see them running on {@link
|
||||
http://angular.github.com/angular-phonecat/step-7/test/e2e/runner.html
|
||||
angular's server}.
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Try to add an `{{orderProp}}` binding to `index.html`, and you'll see that nothing happens even
|
||||
when you are in the phone list view. This is because the `orderProp` model is visible only in the
|
||||
scope managed by `PhoneListCtrl`, which is associated with the `<ng:view>` element. If you add the
|
||||
same binding into the `phone-list.html` template, the binding will work as expected.
|
||||
|
||||
* In `PhoneCatCtrl`, create a new model called "`hero`" with `this.hero = 'Zoro'`. In
|
||||
`PhoneListCtrl` let's shadow it with `this.hero = 'Batman'`, and in `PhoneDetailCtrl` we'll use
|
||||
`this.hero = "Captain Proton"`. Then add the `<p>hero = {{hero}}</p>` to all three of our templates
|
||||
(`index.html`, `phone-list.html`, and `phone-detail.html`). Open the app and you'll see scope
|
||||
inheritance and model property shadowing do some wonders.
|
||||
|
||||
# Summary
|
||||
|
||||
With the routing set up and the phone list view implemented, we're ready to go to {@link step_08
|
||||
step 8} to implement the phone details view.
|
||||
|
||||
|
||||
<ul doc:tutorial-nav="7"></ul>
|
||||
@@ -1,188 +0,0 @@
|
||||
@ngdoc overview
|
||||
@name Tutorial: 8 - More Templating
|
||||
@description
|
||||
|
||||
<ul doc:tutorial-nav="8"></ul>
|
||||
|
||||
|
||||
In this step, you will implement the phone details view, which is displayed when a user clicks on a
|
||||
phone in the phone list.
|
||||
|
||||
|
||||
<doc:tutorial-instructions step="8"></doc:tutorial-instructions>
|
||||
|
||||
|
||||
Now when you click on a phone on the list, the phone details page with phone-specific information
|
||||
is displayed.
|
||||
|
||||
To implement the phone details view we will use {@link api/angular.service.$xhr $xhr} to fetch our
|
||||
data, and we'll flesh out the `phone-details.html` view template.
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-7...step-8
|
||||
GitHub}:
|
||||
|
||||
## Data
|
||||
|
||||
In addition to `phones.json`, the `app/phones/` directory also contains one json file for each
|
||||
phone:
|
||||
|
||||
__`app/phones/nexus-s.json`:__ (sample snippet)
|
||||
<pre>
|
||||
{
|
||||
"additionalFeatures": "Contour Display, Near Field Communications (NFC),...",
|
||||
"android": {
|
||||
"os": "Android 2.3",
|
||||
"ui": "Android"
|
||||
},
|
||||
...
|
||||
"images": [
|
||||
"img/phones/nexus-s.0.jpg",
|
||||
"img/phones/nexus-s.1.jpg",
|
||||
"img/phones/nexus-s.2.jpg",
|
||||
"img/phones/nexus-s.3.jpg"
|
||||
],
|
||||
"storage": {
|
||||
"flash": "16384MB",
|
||||
"ram": "512MB"
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
|
||||
|
||||
Each of these files describes various properties of the phone using the same data structure. We'll
|
||||
show this data in the phone detail view.
|
||||
|
||||
|
||||
## Controller
|
||||
|
||||
We'll expand the `PhoneDetailCtrl` by using the `$xhr` service to fetch the json files. This works
|
||||
the same way as the phone list controller.
|
||||
|
||||
__`app/js/controller.js`:__
|
||||
<pre>
|
||||
function PhoneDetailCtrl($xhr) {
|
||||
var self = this;
|
||||
|
||||
$xhr('GET', 'phones/' + self.params.phoneId + '.json', function(code, response) {
|
||||
self.phone = response;
|
||||
});
|
||||
}
|
||||
|
||||
//PhoneDetailCtrl.$inject = ['$xhr'];
|
||||
</pre>
|
||||
|
||||
To construct the URL for the HTTP request, we use `params.phoneId` extracted from the current route
|
||||
in the `PhoneCatCtrl` controller.
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
The TBD placeholder line has been replaced with lists and bindings that comprise the phone details.
|
||||
Note where we use the angular `{{expression}}` markup and `ng:repeater`s to project phone data from
|
||||
our model into the view.
|
||||
|
||||
|
||||
__`app/partials/phone-details.html`:__
|
||||
<pre>
|
||||
<img ng:src="{{phone.images[0]}}" class="phone"/>
|
||||
|
||||
<h1>{{phone.name}}</h1>
|
||||
|
||||
<p>{{phone.description}}</p>
|
||||
|
||||
<ul class="phone-thumbs">
|
||||
<li ng:repeat="img in phone.images">
|
||||
<img ng:src="{{img}}"/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="specs">
|
||||
<li>
|
||||
<span>Availability and Networks</span>
|
||||
<dl>
|
||||
<dt>Availability</dt>
|
||||
<dd ng:repeat="availability in phone.availability">{{availability}}</dd>
|
||||
</dl>
|
||||
</li>
|
||||
...
|
||||
</li>
|
||||
<span>Additional Features</span>
|
||||
<dd>{{phone.additionalFeatures}}</dd>
|
||||
</li>
|
||||
</ul>
|
||||
</pre>
|
||||
|
||||
<img src="img/tutorial/tutorial_08-09_final.png">
|
||||
|
||||
## Test
|
||||
|
||||
We wrote a new unit test that is similar to the one we wrote for the `PhoneListCtrl` controller in
|
||||
step 5.
|
||||
|
||||
__`test/unit/controllerSpec.js`:__
|
||||
<pre>
|
||||
...
|
||||
it('should fetch phone detail', function() {
|
||||
scope.params = {phoneId:'xyz'};
|
||||
$browser.xhr.expectGET('phones/xyz.json').respond({name:'phone xyz'});
|
||||
ctrl = scope.$new(PhoneDetailCtrl);
|
||||
|
||||
expect(ctrl.phone).toBeUndefined();
|
||||
$browser.xhr.flush();
|
||||
|
||||
expect(ctrl.phone).toEqual({name:'phone xyz'});
|
||||
});
|
||||
...
|
||||
</pre>
|
||||
|
||||
To run the unit tests, execute the `./scripts/test.sh` script and you should see the following
|
||||
output.
|
||||
|
||||
Chrome: Runner reset.
|
||||
...
|
||||
Total 3 tests (Passed: 3; Fails: 0; Errors: 0) (5.00 ms)
|
||||
Chrome 11.0.696.57 Mac OS: Run 3 tests (Passed: 3; Fails: 0; Errors 0) (5.00 ms)
|
||||
|
||||
|
||||
We also added a new end-to-end test that navigates to the Nexus S detail page and verifies that the
|
||||
heading on the page is "Nexus S".
|
||||
|
||||
__`test/e2e/scenarios.js`:__
|
||||
<pre>
|
||||
...
|
||||
describe('Phone detail view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser().navigateTo('../../app/index.html#/phones/nexus-s');
|
||||
});
|
||||
|
||||
|
||||
it('should display nexus-s page', function() {
|
||||
expect(binding('phone.name')).toBe('Nexus S');
|
||||
});
|
||||
});
|
||||
...
|
||||
</pre>
|
||||
|
||||
|
||||
You can now refresh the browser tab with the end-to-end test runner to see the tests run, or you
|
||||
can see them running on {@link
|
||||
http://angular.github.com/angular-phonecat/step-8/test/e2e/runner.html
|
||||
angular's server}.
|
||||
|
||||
# Experiments
|
||||
|
||||
* Using the {@link
|
||||
https://docs.google.com/document/d/11L8htLKrh6c92foV71ytYpiKkeKpM4_a5-9c3HywfIc/edit?hl=en&pli=1#
|
||||
end-to-end test runner API}, write a test that verifies that we display 4 thumbnail images on the
|
||||
Nexus S details page.
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now that the phone details view is in place, proceed to {@link step_09 step 9} to learn how to
|
||||
write your own custom display filter.
|
||||
|
||||
|
||||
<ul doc:tutorial-nav="8"></ul>
|
||||
@@ -1,121 +0,0 @@
|
||||
@ngdoc overview
|
||||
@name Tutorial: 9 - Filters
|
||||
@description
|
||||
|
||||
<ul doc:tutorial-nav="9"></ul>
|
||||
|
||||
|
||||
In this step you will learn how to create your own custom display filter.
|
||||
|
||||
|
||||
<doc:tutorial-instructions step="9"></doc:tutorial-instructions>
|
||||
|
||||
|
||||
Navigate to one of the detail pages.
|
||||
|
||||
In the previous step, the details page displayed either "true" or "false" to indicate whether
|
||||
certain phone features were present or not. We have used a custom filter to convert those text
|
||||
strings into glyphs: ✓ for "true", and ✘ for "false". Let's see, what the filter code looks like.
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-8...step-9
|
||||
GitHub}:
|
||||
|
||||
|
||||
## Custom Filter
|
||||
|
||||
In order to create a new filter, simply register your custom filter function with the {@link
|
||||
api/angular.filter `angular.filter`} API.
|
||||
|
||||
__`app/js/filters.js`:__
|
||||
<pre>
|
||||
angular.filter('checkmark', function(input) {
|
||||
return input ? '\u2713' : '\u2718';
|
||||
});
|
||||
</pre>
|
||||
|
||||
The name of our filter is "checkmark". The `input` evaluates to either `true` or `false`, and we
|
||||
return one of two unicode characters we have chosen to represent true or false (`\u2713` and
|
||||
`\u2718`).
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
Since the filter code lives in the `app/js/filters.js` file, we need to include this file in our
|
||||
layout template.
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<script src="js/controllers.js"></script>
|
||||
<script src="js/filters.js"></script>
|
||||
...
|
||||
</pre>
|
||||
|
||||
The syntax for using filters in angular templates is as follows:
|
||||
|
||||
{{ expression | filter }}
|
||||
|
||||
Let's employ the filter in the phone details template:
|
||||
|
||||
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
<pre>
|
||||
...
|
||||
<dl>
|
||||
<dt>Infrared</dt>
|
||||
<dd>{{phone.connectivity.infrared | checkmark}}</dd>
|
||||
<dt>GPS</dt>
|
||||
<dd>{{phone.connectivity.gps | checkmark}}</dd>
|
||||
</dl>
|
||||
...
|
||||
</pre>
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
Filters, like any other component, should be tested and these tests are very easy to write.
|
||||
|
||||
__`test/unit/filtersSpec.js`:__
|
||||
<pre>
|
||||
describe('checkmark filter', function() {
|
||||
|
||||
it('should convert boolean values to unicode checkmark or cross', function() {
|
||||
expect(angular.filter.checkmark(true)).toBe('\u2713');
|
||||
expect(angular.filter.checkmark(false)).toBe('\u2718');
|
||||
});
|
||||
})
|
||||
</pre>
|
||||
|
||||
To run the unit tests, execute the `./scripts/test.sh` script and you should see the following
|
||||
output.
|
||||
|
||||
Chrome: Runner reset.
|
||||
....
|
||||
Total 4 tests (Passed: 4; Fails: 0; Errors: 0) (3.00 ms)
|
||||
Chrome 11.0.696.57 Mac OS: Run 4 tests (Passed: 4; Fails: 0; Errors 0) (3.00 ms)
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Let's experiment with some of the {@link api/angular.filter built-in angular filters} and add the
|
||||
following bindings to `index.html`:
|
||||
* `{{ "lower cap string" | uppercase }}`
|
||||
* `{{ {foo: "bar", baz: 23} | json }}`
|
||||
* `{{ 1304375948024 | date }}`
|
||||
* `{{ 1304375948024 | date:"MM/dd/yyyy @ h:mma" }}`
|
||||
|
||||
* We can also create a model with an input element, and combine it with a filtered binding. Add
|
||||
the following to index.html:
|
||||
|
||||
<input name="userInput"> Uppercased: {{ userInput | uppercase }}
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now that you have learned how to write and test a custom filter, go to {@link step_10 step 10} to
|
||||
learn how we can use angular to enhance the phone details page further.
|
||||
|
||||
|
||||
<ul doc:tutorial-nav="9"></ul>
|
||||
@@ -1,140 +0,0 @@
|
||||
@ngdoc overview
|
||||
@name Tutorial: 10 - Event Handlers
|
||||
@description
|
||||
|
||||
<ul doc:tutorial-nav="10"></ul>
|
||||
|
||||
|
||||
In this step, you will add a clickable phone image swapper to the phone details page.
|
||||
|
||||
|
||||
<doc:tutorial-instructions step="10"></doc:tutorial-instructions>
|
||||
|
||||
|
||||
The phone details view displays one large image of the current phone and several smaller thumbnail
|
||||
images. It would be great if we could replace the large image with any of the thumbnails just by
|
||||
clicking on the desired thumbnail image. Let's have a look at how we can do this with angular.
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-9...step-10
|
||||
GitHub}:
|
||||
|
||||
|
||||
## Controller
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
<pre>
|
||||
...
|
||||
function PhoneDetailCtrl($xhr) {
|
||||
var self = this;
|
||||
|
||||
$xhr('GET', 'phones/' + self.params.phoneId + '.json', function(code, response) {
|
||||
self.phone = response;
|
||||
self.mainImageUrl = response.images[0];
|
||||
});
|
||||
|
||||
self.setImage = function(imageUrl) {
|
||||
self.mainImageUrl = imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
//PhoneDetailCtrl.$inject = ['$xhr'];
|
||||
</pre>
|
||||
|
||||
In the `PhoneDetailCtrl` controller, we created the `mainImageUrl` model property and set its
|
||||
default value to the first phone image url.
|
||||
|
||||
We also created a `setImage` controller method to change the value of `mainImageUrl`.
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
<pre>
|
||||
<img ng:src="{{mainImageUrl}}" class="phone"/>
|
||||
|
||||
...
|
||||
|
||||
<ul class="phone-thumbs">
|
||||
<li ng:repeat="img in phone.images">
|
||||
<img ng:src="{{img}}" ng:click="setImage(img)">
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
</pre>
|
||||
|
||||
We bound the `ng:src` attribute of the large image to the `mainImageUrl` property.
|
||||
|
||||
We also registered an {@link api/angular.directive.ng:click `ng:click`} handler with thumbnail
|
||||
images. When a user clicks on one of the thumbnail images, the handler will use the `setImage`
|
||||
controller method to change the value of the `mainImageUrl` property to the url of the thumbnail
|
||||
image.
|
||||
|
||||
<img src="img/tutorial/tutorial_10-11_final.png">
|
||||
|
||||
## Test
|
||||
|
||||
To verify this new feature, we added two end-to-end tests. One verifies that the main image is set
|
||||
to the first phone image by default. The second test clicks on several thumbnail images and
|
||||
verifies that the main image changed appropriately.
|
||||
|
||||
__`test/e2e/scenarios.js`:__
|
||||
<pre>
|
||||
...
|
||||
describe('Phone detail view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser().navigateTo('../../app/index.html#/phones/nexus-s');
|
||||
});
|
||||
|
||||
|
||||
it('should display the first phone image as the main phone image', function() {
|
||||
expect(element('img.phone').attr('src')).toBe('img/phones/nexus-s.0.jpg');
|
||||
});
|
||||
|
||||
|
||||
it('should swap main image if a thumbnail image is clicked on', function() {
|
||||
element('.phone-thumbs li:nth-child(3) img').click();
|
||||
expect(element('img.phone').attr('src')).toBe('img/phones/nexus-s.2.jpg');
|
||||
|
||||
element('.phone-thumbs li:nth-child(1) img').click();
|
||||
expect(element('img.phone').attr('src')).toBe('img/phones/nexus-s.0.jpg');
|
||||
});
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
You can now refresh the browser tab with the end-to-end test runner to see the tests run, or you
|
||||
can see them running on {@link
|
||||
http://angular.github.com/angular-phonecat/step-8/test/e2e/runner.html
|
||||
angular's server}.
|
||||
|
||||
# Experiments
|
||||
|
||||
* Let's add a new controller method to `PhoneCatCtrl`:
|
||||
|
||||
this.hello = function(name) {
|
||||
alert('Hello ' + (name || 'world') + '!');
|
||||
}
|
||||
|
||||
and add:
|
||||
|
||||
<button ng:click="hello('Elmo')">Hello</button>
|
||||
|
||||
to the `index.html` template.
|
||||
|
||||
The controller methods are inherited between controllers/scopes, so you can use the same snippet
|
||||
in the `phone-list.html` template as well.
|
||||
|
||||
* Move the `hello` method from `PhoneCatCtrl` to `PhoneListCtrl` and you'll see that the button
|
||||
declared in `index.html` will stop working, while the one declared in the `phone-list.html`
|
||||
template remains operational.
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
With the phone image swapper in place, we're ready for {@link step_11 step 11} (the last step!) to
|
||||
learn an even better way to fetch data.
|
||||
|
||||
|
||||
<ul doc:tutorial-nav="10"></ul>
|
||||
@@ -1,208 +0,0 @@
|
||||
@ngdoc overview
|
||||
@name Tutorial: 11 - REST and Custom Services
|
||||
@description
|
||||
|
||||
<ul doc:tutorial-nav="11"></ul>
|
||||
|
||||
|
||||
In this step, you will improve the way our app fetches data.
|
||||
|
||||
|
||||
<doc:tutorial-instructions step="11"></doc:tutorial-instructions>
|
||||
|
||||
|
||||
The last improvement we will make to our app is to define a custom service that represents a {@link
|
||||
http://en.wikipedia.org/wiki/Representational_State_Transfer RESTful} client. Using this client we
|
||||
can make xhr requests for data in an easier way, without having to deal with the lower-level {@link
|
||||
api/angular.service.$xhr $xhr} API, HTTP methods and URLs.
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-10...step-11
|
||||
GitHub}:
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
The custom service is defined in `app/js/services.js` so we need to include this file in our layout
|
||||
template:
|
||||
|
||||
__`app/index.html`.__
|
||||
<pre>
|
||||
...
|
||||
<script src="js/services.js"></script>
|
||||
...
|
||||
</pre>
|
||||
|
||||
## Service
|
||||
|
||||
__`app/js/services.js`.__
|
||||
<pre>
|
||||
angular.service('Phone', function($resource) {
|
||||
return $resource('phones/:phoneId.json', {}, {
|
||||
query: {method: 'GET', params: {phoneId: 'phones'}, isArray: true}
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
We used the {@link api/angular.service} API to register a custom service. We passed in the name of
|
||||
the service - 'Phone' - and a factory function. The factory function is similar to a controller's
|
||||
constructor in that both can declare dependencies via function arguments. The Phone service
|
||||
declared a dependency on the `$resource` service.
|
||||
|
||||
The {@link api/angular.service.$resource `$resource`} service makes it easy to create a {@link
|
||||
http://en.wikipedia.org/wiki/Representational_State_Transfer RESTful} client with just a few lines
|
||||
of code. This client can then be used in our application, instead of the lower-level {@link
|
||||
api/angular.service.$xhr $xhr} service.
|
||||
|
||||
|
||||
## Controller
|
||||
|
||||
We simplified our sub-controllers (`PhoneListCtrl` and `PhoneDetailCtrl`) by factoring out the
|
||||
lower-level {@link api/angular.service.$xhr $xhr} service, replacing it with a new service called
|
||||
`Phone`. Angular's {@link api/angular.service.$resource `$resource`} service is easier to use than
|
||||
{@link api/angular.service.$xhr $xhr} for interacting with data sources exposed as RESTful
|
||||
resources. It is also easier now to understand what the code in our controllers is doing.
|
||||
|
||||
__`app/js/controllers.js`.__
|
||||
<pre>
|
||||
...
|
||||
|
||||
function PhoneListCtrl(Phone) {
|
||||
this.orderProp = 'age';
|
||||
this.phones = Phone.query();
|
||||
}
|
||||
//PhoneListCtrl.$inject = ['Phone'];
|
||||
|
||||
|
||||
function PhoneDetailCtrl(Phone) {
|
||||
var self = this;
|
||||
|
||||
self.phone = Phone.get({phoneId: self.params.phoneId}, function(phone) {
|
||||
self.mainImageUrl = phone.images[0];
|
||||
});
|
||||
|
||||
...
|
||||
}
|
||||
//PhoneDetailCtrl.$inject = ['Phone'];
|
||||
</pre>
|
||||
|
||||
Notice how in `PhoneListCtrl` we replaced:
|
||||
|
||||
$xhr('GET', 'phones/phones.json', function(code, response) {
|
||||
self.phones = response;
|
||||
});
|
||||
|
||||
with:
|
||||
|
||||
this.phones = Phone.query();
|
||||
|
||||
This is a simple statement that we want to query for all phones.
|
||||
|
||||
An important thing to notice in the code above is that we don't pass any callback functions when
|
||||
invoking methods of our Phone service. Although it looks as if the result were returned
|
||||
synchronously, that is not the case at all. What is returned synchronously is a "future" — an
|
||||
object, which will be filled with data when the xhr response returns. Because of the data-binding
|
||||
in angular, we can use this future and bind it to our template. Then, when the data arrives, the
|
||||
view will automatically update.
|
||||
|
||||
Sometimes, relying on the future object and data-binding alone is not sufficient to do everything
|
||||
we require, so in these cases, we can add a callback to process the server response. The
|
||||
`PhoneDetailCtrl` controller illustrates this by setting the `mainImageUrl` in a callback.
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
We have modified our unit tests to verify that our new service is issuing HTTP requests and
|
||||
processing them as expected. The tests also check that our controllers are interacting with the
|
||||
service correctly.
|
||||
|
||||
The {@link api/angular.service.$resource $resource} service augments the response object with
|
||||
methods for updating and deleting the resource. If we were to use the standard `toEqual` matcher,
|
||||
our tests would fail because the test values would not match the responses exactly. To solve the
|
||||
problem, we use a newly-defined `toEqualData` {@link
|
||||
http://pivotal.github.com/jasmine/jsdoc/symbols/jasmine.Matchers.html Jasmine matcher}. When the
|
||||
`toEqualData` matcher compares two objects, it takes only object properties into account and
|
||||
ignores methods.
|
||||
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
<pre>
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
beforeEach(function(){
|
||||
this.addMatchers({
|
||||
toEqualData: function(expected) {
|
||||
return angular.equals(this.actual, expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('PhoneListCtrl', function() {
|
||||
var scope, $browser, ctrl;
|
||||
|
||||
beforeEach(function() {
|
||||
scope = angular.scope();
|
||||
$browser = scope.$service('$browser');
|
||||
|
||||
$browser.xhr.expectGET('phones/phones.json')
|
||||
.respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
ctrl = scope.$new(PhoneListCtrl);
|
||||
});
|
||||
|
||||
it('should create "phones" model with 2 phones fetched from xhr', function() {
|
||||
expect(ctrl.phones).toEqual([]);
|
||||
$browser.xhr.flush();
|
||||
|
||||
expect(ctrl.phones).toEqualData([{name: 'Nexus S'},
|
||||
{name: 'Motorola DROID'}]);
|
||||
});
|
||||
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(ctrl.orderProp).toBe('age');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('PhoneDetailCtrl', function() {
|
||||
var scope, $browser, ctrl;
|
||||
|
||||
beforeEach(function() {
|
||||
scope = angular.scope();
|
||||
$browser = scope.$service('$browser');
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
scope = angular.scope();
|
||||
$browser = scope.$service('$browser');
|
||||
});
|
||||
|
||||
it('should fetch phone detail', function() {
|
||||
scope.params = {phoneId:'xyz'};
|
||||
$browser.xhr.expectGET('phones/xyz.json').respond({name:'phone xyz'});
|
||||
ctrl = scope.$new(PhoneDetailCtrl);
|
||||
|
||||
expect(ctrl.phone).toEqualData({});
|
||||
$browser.xhr.flush();
|
||||
|
||||
expect(ctrl.phone).toEqualData({name:'phone xyz'});
|
||||
});
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
To run the unit tests, execute the `./scripts/test.sh` script and you should see the following
|
||||
output.
|
||||
|
||||
Chrome: Runner reset.
|
||||
....
|
||||
Total 4 tests (Passed: 4; Fails: 0; Errors: 0) (3.00 ms)
|
||||
Chrome 11.0.696.57 Mac OS: Run 4 tests (Passed: 4; Fails: 0; Errors 0) (3.00 ms)
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
There you have it! We have created a web app in a relatively short amount of time. In the {@link
|
||||
the_end closing notes} we'll cover were to go from here.
|
||||
|
||||
|
||||
<ul doc:tutorial-nav="11"></ul>
|
||||
@@ -1,21 +0,0 @@
|
||||
@ngdoc overview
|
||||
@name Tutorial: The End
|
||||
@description
|
||||
|
||||
Our application is now complete. Feel free to experiment with the code further, and jump back to
|
||||
previous steps using the `git checkout` or `goto_step.sh` commands.
|
||||
|
||||
For more details and examples of the angular concepts we touched on in this tutorial, see the
|
||||
{@link guide/ Developer Guide}.
|
||||
|
||||
For several more examples of code, see the {@link cookbook/ Cookbook}.
|
||||
|
||||
When you are ready to start developing a project using angular, we recommend that you bootstrap
|
||||
your development with the {@link https://github.com/angular/angular-seed angular seed} project.
|
||||
|
||||
We hope this tutorial was useful to you and that you learned enough about angular to make you want
|
||||
to learn more. We especially hope you are inspired to go out and develop angular web apps of your
|
||||
own, and that you might be interested in {@link misc/contribute contributing} to angular.
|
||||
|
||||
If you have questions or feedback or just want to say "hi", please post a message at {@link
|
||||
https://groups.google.com/forum/#!forum/angular}.
|
||||
@@ -0,0 +1 @@
|
||||
Hello, $http!
|
||||
@@ -1,13 +1,13 @@
|
||||
<label>Name:</label>
|
||||
<input type="text" name="form.name" ng:required>
|
||||
<input type="text" ng:model="form.name" required>
|
||||
|
||||
<div ng:repeat="contact in form.contacts">
|
||||
<select name="contact.type">
|
||||
<select ng:model="contact.type">
|
||||
<option>url</option>
|
||||
<option>email</option>
|
||||
<option>phone</option>
|
||||
</select>
|
||||
<input type="text" name="contact.url">
|
||||
<input type="text" ng:model="contact.url">
|
||||
[ <a href="" ng:click="form.contacts.$remove(contact)">X</a> ]
|
||||
</div>
|
||||
<div>
|
||||
@@ -15,4 +15,4 @@
|
||||
</div>
|
||||
|
||||
<button ng:click="cancel()">Cancel</button>
|
||||
<button ng:click="save()">Save</button>
|
||||
<button ng:click="save()">Save</button>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -1,15 +1,15 @@
|
||||
var DOM = require('dom.js').DOM;
|
||||
var DOM = require('../src/dom.js').DOM;
|
||||
|
||||
describe('dom', function(){
|
||||
describe('dom', function() {
|
||||
var dom;
|
||||
|
||||
beforeEach(function(){
|
||||
beforeEach(function() {
|
||||
dom = new DOM();
|
||||
});
|
||||
|
||||
describe('h', function(){
|
||||
describe('h', function() {
|
||||
|
||||
it('should render using function', function(){
|
||||
it('should render using function', function() {
|
||||
var cbThis;
|
||||
var cdValue;
|
||||
dom.h('heading', 'content', function(value){
|
||||
@@ -20,11 +20,11 @@ describe('dom', function(){
|
||||
expect(cbValue).toEqual('content');
|
||||
});
|
||||
|
||||
it('should update heading numbers', function(){
|
||||
dom.h('heading', function(){
|
||||
it('should update heading numbers', function() {
|
||||
dom.h('heading', function() {
|
||||
this.html('<h1>sub-heading</h1>');
|
||||
});
|
||||
expect(dom.toString()).toContain('<h1>heading</h1>');
|
||||
expect(dom.toString()).toContain('<h1 id="heading">heading</h1>');
|
||||
expect(dom.toString()).toContain('<h2>sub-heading</h2>');
|
||||
});
|
||||
|
||||
|
||||
+94
-117
@@ -1,11 +1,11 @@
|
||||
var ngdoc = require('ngdoc.js');
|
||||
var DOM = require('dom.js').DOM;
|
||||
var ngdoc = require('../src/ngdoc.js');
|
||||
var DOM = require('../src/dom.js').DOM;
|
||||
|
||||
describe('ngdoc', function(){
|
||||
describe('ngdoc', function() {
|
||||
var Doc = ngdoc.Doc;
|
||||
var dom;
|
||||
|
||||
beforeEach(function(){
|
||||
beforeEach(function() {
|
||||
dom = new DOM();
|
||||
this.addMatchers({
|
||||
toContain: function(text) {
|
||||
@@ -15,15 +15,15 @@ describe('ngdoc', function(){
|
||||
});
|
||||
});
|
||||
|
||||
describe('Doc', function(){
|
||||
describe('metadata', function(){
|
||||
describe('Doc', function() {
|
||||
describe('metadata', function() {
|
||||
|
||||
it('should find keywords', function(){
|
||||
it('should find keywords', function() {
|
||||
expect(new Doc('\nHello: World! @ignore. $abc').keywords()).toEqual('$abc hello world');
|
||||
expect(new Doc('The `ng:class-odd` and').keywords()).toEqual('and ng:class-odd the');
|
||||
});
|
||||
|
||||
it('should have shortName', function(){
|
||||
it('should have shortName', function() {
|
||||
var d1 = new Doc('@name a.b.c').parse();
|
||||
var d2 = new Doc('@name a.b.ng:c').parse();
|
||||
var d3 = new Doc('@name some text: more text').parse();
|
||||
@@ -32,7 +32,7 @@ describe('ngdoc', function(){
|
||||
expect(ngdoc.metadata([d3])[0].shortName).toEqual('more text');
|
||||
});
|
||||
|
||||
it('should have depth information', function(){
|
||||
it('should have depth information', function() {
|
||||
var d1 = new Doc('@name a.b.c').parse();
|
||||
var d2 = new Doc('@name a.b.ng:c').parse();
|
||||
var d3 = new Doc('@name some text: more text').parse();
|
||||
@@ -43,8 +43,8 @@ describe('ngdoc', function(){
|
||||
|
||||
});
|
||||
|
||||
describe('parse', function(){
|
||||
it('should convert @names into properties', function(){
|
||||
describe('parse', function() {
|
||||
it('should convert @names into properties', function() {
|
||||
var doc = new Doc('\n@name name\n@desc\ndesc\ndesc2\n@dep\n');
|
||||
doc.parse();
|
||||
expect(doc.name).toEqual('name');
|
||||
@@ -52,8 +52,9 @@ describe('ngdoc', function(){
|
||||
expect(doc.dep).toEqual('');
|
||||
});
|
||||
|
||||
it('should parse parameters', function(){
|
||||
it('should parse parameters', function() {
|
||||
var doc = new Doc(
|
||||
'@name a\n' +
|
||||
'@param {*} a short\n' +
|
||||
'@param {Type} b med\n' +
|
||||
'@param {Class=} [c=2] long\nline');
|
||||
@@ -65,8 +66,8 @@ describe('ngdoc', function(){
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse return', function(){
|
||||
var doc = new Doc('@returns {Type} text *bold*.');
|
||||
it('should parse return', function() {
|
||||
var doc = new Doc('@name a\n@returns {Type} text *bold*.');
|
||||
doc.parse();
|
||||
expect(doc.returns).toEqual({
|
||||
type: 'Type',
|
||||
@@ -74,31 +75,39 @@ describe('ngdoc', function(){
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse filename', function(){
|
||||
it('should parse filename', function() {
|
||||
var doc = new Doc('@name friendly name', 'docs/a.b.ngdoc', 1);
|
||||
doc.parse(0);
|
||||
expect(doc.id).toEqual('a.b');
|
||||
expect(doc.name).toEqual('friendly name');
|
||||
});
|
||||
|
||||
it('should escape <doc:source> element', function(){
|
||||
var doc = new Doc('@description before <doc:example>' +
|
||||
it('should escape <doc:source> element', function() {
|
||||
var doc = new Doc('@name a\n@description before <doc:example>' +
|
||||
'<doc:source>\n<>\n</doc:source></doc:example> after');
|
||||
doc.parse();
|
||||
expect(doc.description).toContain('<p>before </p><doc:example>' +
|
||||
'<pre class="doc-source">\n<>\n</pre></doc:example><p>after</p>');
|
||||
});
|
||||
|
||||
it('should preserve the jsfiddle attribute', function(){
|
||||
var doc = new Doc('@description before <doc:example>' +
|
||||
it('should preserve the source attribute', function() {
|
||||
var doc = new Doc('@name a\n@description before <doc:example>' +
|
||||
'<doc:source source="false">lala</doc:source></doc:example> after');
|
||||
doc.parse();
|
||||
expect(doc.description).toContain('<p>before </p><doc:example>' +
|
||||
'<pre class="doc-source" source="false">lala</pre></doc:example><p>after</p>');
|
||||
});
|
||||
|
||||
it('should preserve the jsfiddle attribute', function() {
|
||||
var doc = new Doc('@name a\n@description before <doc:example>' +
|
||||
'<doc:source jsfiddle="foo">lala</doc:source></doc:example> after');
|
||||
doc.parse();
|
||||
expect(doc.description).toContain('<p>before </p><doc:example>' +
|
||||
'<pre class="doc-source" jsfiddle="foo">lala</pre></doc:example><p>after</p>');
|
||||
});
|
||||
|
||||
it('should escape <doc:scenario> element', function(){
|
||||
var doc = new Doc('@description before <doc:example>' +
|
||||
it('should escape <doc:scenario> element', function() {
|
||||
var doc = new Doc('@name a\n@description before <doc:example>' +
|
||||
'<doc:scenario>\n<>\n</doc:scenario></doc:example> after');
|
||||
doc.parse();
|
||||
expect(doc.description).toContain('<p>before </p><doc:example>' +
|
||||
@@ -106,7 +115,7 @@ describe('ngdoc', function(){
|
||||
});
|
||||
|
||||
it('should store all links', function() {
|
||||
var doc = new Doc('@description {@link api/angular.link}');
|
||||
var doc = new Doc('@name a\n@description {@link api/angular.link}');
|
||||
doc.parse();
|
||||
|
||||
expect(doc.links).toContain('api/angular.link');
|
||||
@@ -132,11 +141,11 @@ describe('ngdoc', function(){
|
||||
});
|
||||
});
|
||||
|
||||
describe('sorting', function(){
|
||||
describe('sorting', function() {
|
||||
function property(name) {
|
||||
return function(obj) {return obj[name];};
|
||||
}
|
||||
function noop(){}
|
||||
function noop() {}
|
||||
function doc(type, name){
|
||||
return {
|
||||
id: name,
|
||||
@@ -149,7 +158,7 @@ describe('ngdoc', function(){
|
||||
var angular_x = doc('function', 'angular.x');
|
||||
var angular_y = doc('property', 'angular.y');
|
||||
|
||||
it('should put angular.fn() in front of angular.widget, etc', function(){
|
||||
it('should put angular.fn() in front of angular.widget, etc', function() {
|
||||
expect(ngdoc.metadata([angular_widget, angular_y, angular_x]).map(property('id')))
|
||||
.toEqual(['angular.x', 'angular.y', 'angular.widget' ]);
|
||||
});
|
||||
@@ -157,13 +166,13 @@ describe('ngdoc', function(){
|
||||
});
|
||||
});
|
||||
|
||||
describe('markdown', function(){
|
||||
it('should replace angular in markdown', function(){
|
||||
describe('markdown', function() {
|
||||
it('should replace angular in markdown', function() {
|
||||
expect(new Doc().markdown('<angular/>')).
|
||||
toEqual('<p><tt><angular/></tt></p>');
|
||||
});
|
||||
|
||||
it('should not replace anything in <pre>, but escape the html escape the content', function(){
|
||||
it('should not replace anything in <pre>, but escape the html escape the content', function() {
|
||||
expect(new Doc().markdown('bah x\n<pre>\n<b>angular</b>.k\n</pre>\n asdf x')).
|
||||
toEqual(
|
||||
'<p>bah x</p>' +
|
||||
@@ -194,12 +203,12 @@ describe('ngdoc', function(){
|
||||
it('should ignore nested doc widgets', function() {
|
||||
expect(new Doc().markdown(
|
||||
'before<doc:tutorial-instructions>\n' +
|
||||
'<doc:tutorial-instruction id="git-mac" name="Git on Mac/Linux">' +
|
||||
'<doc:tutorial-instruction id="git-mac" ng:model="Git on Mac/Linux">' +
|
||||
'\ngit bla bla\n</doc:tutorial-instruction>\n' +
|
||||
'</doc:tutorial-instructions>')).toEqual(
|
||||
|
||||
'<p>before</p><doc:tutorial-instructions>\n' +
|
||||
'<doc:tutorial-instruction id="git-mac" name="Git on Mac/Linux">\n' +
|
||||
'<doc:tutorial-instruction id="git-mac" ng:model="Git on Mac/Linux">\n' +
|
||||
'git bla bla\n' +
|
||||
'</doc:tutorial-instruction>\n' +
|
||||
'</doc:tutorial-instructions>');
|
||||
@@ -234,30 +243,30 @@ describe('ngdoc', function(){
|
||||
|
||||
});
|
||||
|
||||
describe('trim', function(){
|
||||
describe('trim', function() {
|
||||
var trim = ngdoc.trim;
|
||||
it('should remove leading/trailing space', function(){
|
||||
it('should remove leading/trailing space', function() {
|
||||
expect(trim(' \nabc\n ')).toEqual('abc');
|
||||
});
|
||||
|
||||
it('should remove leading space on every line', function(){
|
||||
it('should remove leading space on every line', function() {
|
||||
expect(trim('\n 1\n 2\n 3\n')).toEqual('1\n 2\n 3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('merge', function(){
|
||||
it('should merge child with parent', function(){
|
||||
var parent = new Doc({id: 'angular.service.abc', name: 'angular.service.abc', section: 'api'});
|
||||
var methodA = new Doc({name: 'methodA', methodOf: 'angular.service.abc'});
|
||||
var methodB = new Doc({name: 'methodB', methodOf: 'angular.service.abc'});
|
||||
var propA = new Doc({name: 'propA', propertyOf: 'angular.service.abc'});
|
||||
var propB = new Doc({name: 'propB', propertyOf: 'angular.service.abc'});
|
||||
var eventA = new Doc({name: 'eventA', eventOf: 'angular.service.abc'});
|
||||
var eventB = new Doc({name: 'eventB', eventOf: 'angular.service.abc'});
|
||||
describe('merge', function() {
|
||||
it('should merge child with parent', function() {
|
||||
var parent = new Doc({id: 'angular.module.ng.abc', name: 'angular.module.ng.abc', section: 'api'});
|
||||
var methodA = new Doc({name: 'methodA', methodOf: 'angular.module.ng.abc'});
|
||||
var methodB = new Doc({name: 'methodB', methodOf: 'angular.module.ng.abc'});
|
||||
var propA = new Doc({name: 'propA', propertyOf: 'angular.module.ng.abc'});
|
||||
var propB = new Doc({name: 'propB', propertyOf: 'angular.module.ng.abc'});
|
||||
var eventA = new Doc({name: 'eventA', eventOf: 'angular.module.ng.abc'});
|
||||
var eventB = new Doc({name: 'eventB', eventOf: 'angular.module.ng.abc'});
|
||||
var docs = [methodB, methodA, eventB, eventA, propA, propB, parent]; // keep wrong order;
|
||||
ngdoc.merge(docs);
|
||||
expect(docs.length).toEqual(1);
|
||||
expect(docs[0].id).toEqual('angular.service.abc');
|
||||
expect(docs[0].id).toEqual('angular.module.ng.abc');
|
||||
expect(docs[0].methods).toEqual([methodA, methodB]);
|
||||
expect(docs[0].events).toEqual([eventA, eventB]);
|
||||
expect(docs[0].properties).toEqual([propA, propB]);
|
||||
@@ -294,10 +303,10 @@ describe('ngdoc', function(){
|
||||
|
||||
////////////////////////////////////////
|
||||
|
||||
describe('TAG', function(){
|
||||
describe('@param', function(){
|
||||
it('should parse with no default', function(){
|
||||
var doc = new Doc('@param {(number|string)} number Number \n to format.');
|
||||
describe('TAG', function() {
|
||||
describe('@param', function() {
|
||||
it('should parse with no default', function() {
|
||||
var doc = new Doc('@name a\n@param {(number|string)} number Number \n to format.');
|
||||
doc.parse();
|
||||
expect(doc.param).toEqual([{
|
||||
type : '(number|string)',
|
||||
@@ -307,8 +316,8 @@ describe('ngdoc', function(){
|
||||
description : '<p>Number \nto format.</p>' }]);
|
||||
});
|
||||
|
||||
it('should parse with default and optional', function(){
|
||||
var doc = new Doc('@param {(number|string)=} [fractionSize=2] desc');
|
||||
it('should parse with default and optional', function() {
|
||||
var doc = new Doc('@name a\n@param {(number|string)=} [fractionSize=2] desc');
|
||||
doc.parse();
|
||||
expect(doc.param).toEqual([{
|
||||
type : '(number|string)',
|
||||
@@ -321,14 +330,14 @@ describe('ngdoc', function(){
|
||||
|
||||
describe('@requires', function() {
|
||||
it('should parse more @requires tag into array', function() {
|
||||
var doc = new Doc('@requires $service for \n`A`\n@requires $another for `B`');
|
||||
var doc = new Doc('@name a\n@requires $service for \n`A`\n@requires $another for `B`');
|
||||
doc.ngdoc = 'service';
|
||||
doc.parse();
|
||||
expect(doc.requires).toEqual([
|
||||
{name:'$service', text:'<p>for \n<code>A</code></p>'},
|
||||
{name:'$another', text:'<p>for <code>B</code></p>'}]);
|
||||
expect(doc.html()).toContain('<a href="api/angular.service.$service">$service</a>');
|
||||
expect(doc.html()).toContain('<a href="api/angular.service.$another">$another</a>');
|
||||
expect(doc.html()).toContain('<a href="api/angular.module.ng.$service">$service</a>');
|
||||
expect(doc.html()).toContain('<a href="api/angular.module.ng.$another">$another</a>');
|
||||
expect(doc.html()).toContain('<p>for \n<code>A</code></p>');
|
||||
expect(doc.html()).toContain('<p>for <code>B</code></p>');
|
||||
});
|
||||
@@ -336,7 +345,7 @@ describe('ngdoc', function(){
|
||||
|
||||
describe('@property', function() {
|
||||
it('should parse @property tags into array', function() {
|
||||
var doc = new Doc("@property {type} name1 desc\n@property {type} name2 desc");
|
||||
var doc = new Doc("@name a\n@property {type} name1 desc\n@property {type} name2 desc");
|
||||
doc.parse();
|
||||
expect(doc.properties.length).toEqual(2);
|
||||
});
|
||||
@@ -348,21 +357,21 @@ describe('ngdoc', function(){
|
||||
});
|
||||
|
||||
it('should parse @property with type', function() {
|
||||
var doc = new Doc("@property {string} name");
|
||||
var doc = new Doc("@name a\n@property {string} name");
|
||||
doc.parse();
|
||||
expect(doc.properties[0].name).toEqual('name');
|
||||
expect(doc.properties[0].type).toEqual('string');
|
||||
});
|
||||
|
||||
it('should parse @property with optional description', function() {
|
||||
var doc = new Doc("@property {string} name desc rip tion");
|
||||
var doc = new Doc("@name a\n@property {string} name desc rip tion");
|
||||
doc.parse();
|
||||
expect(doc.properties[0].name).toEqual('name');
|
||||
expect(doc.properties[0].description).toEqual('<p>desc rip tion</p>');
|
||||
});
|
||||
|
||||
it('should parse @property with type and description both', function() {
|
||||
var doc = new Doc("@property {bool} name desc rip tion");
|
||||
var doc = new Doc("@name a\n@property {bool} name desc rip tion");
|
||||
doc.parse();
|
||||
expect(doc.properties[0].name).toEqual('name');
|
||||
expect(doc.properties[0].type).toEqual('bool');
|
||||
@@ -378,35 +387,35 @@ describe('ngdoc', function(){
|
||||
});
|
||||
|
||||
it('should parse @returns with type and description', function() {
|
||||
var doc = new Doc("@returns {string} descrip tion");
|
||||
var doc = new Doc("@name a\n@returns {string} descrip tion");
|
||||
doc.parse();
|
||||
expect(doc.returns).toEqual({type: 'string', description: '<p>descrip tion</p>'});
|
||||
});
|
||||
|
||||
it('should transform description of @returns with markdown', function() {
|
||||
var doc = new Doc("@returns {string} descrip *tion*");
|
||||
var doc = new Doc("@name a\n@returns {string} descrip *tion*");
|
||||
doc.parse();
|
||||
expect(doc.returns).toEqual({type: 'string', description: '<p>descrip <em>tion</em></p>'});
|
||||
});
|
||||
|
||||
it('should support multiline content', function() {
|
||||
var doc = new Doc("@returns {string} description\n new line\n another line");
|
||||
var doc = new Doc("@name a\n@returns {string} description\n new line\n another line");
|
||||
doc.parse();
|
||||
expect(doc.returns).
|
||||
toEqual({type: 'string', description: '<p>description\nnew line\nanother line</p>'});
|
||||
});
|
||||
});
|
||||
|
||||
describe('@description', function(){
|
||||
it('should support pre blocks', function(){
|
||||
var doc = new Doc("@description <pre><b>abc</b></pre>");
|
||||
describe('@description', function() {
|
||||
it('should support pre blocks', function() {
|
||||
var doc = new Doc("@name a\n@description <pre><b>abc</b></pre>");
|
||||
doc.parse();
|
||||
expect(doc.description).
|
||||
toBe('<div ng:non-bindable><pre class="brush: js; html-script: true;"><b>abc</b></pre></div>');
|
||||
});
|
||||
|
||||
it('should support multiple pre blocks', function() {
|
||||
var doc = new Doc("@description foo \n<pre>abc</pre>\n#bah\nfoo \n<pre>cba</pre>");
|
||||
var doc = new Doc("@name a\n@description foo \n<pre>abc</pre>\n#bah\nfoo \n<pre>cba</pre>");
|
||||
doc.parse();
|
||||
expect(doc.description).
|
||||
toBe('<p>foo </p>' +
|
||||
@@ -418,7 +427,7 @@ describe('ngdoc', function(){
|
||||
});
|
||||
|
||||
it('should support nested @link annotations with or without description', function() {
|
||||
var doc = new Doc("@description " +
|
||||
var doc = new Doc("@name a\n@description " +
|
||||
'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' +
|
||||
@@ -442,8 +451,8 @@ describe('ngdoc', function(){
|
||||
toContain('<a href="./static.html">./static.html</a>');
|
||||
});
|
||||
|
||||
it('should support line breaks in @link', function(){
|
||||
var doc = new Doc("@description " +
|
||||
it('should support line breaks in @link', function() {
|
||||
var doc = new Doc("@name a\n@description " +
|
||||
'{@link\napi/angular.foo\na\nb}');
|
||||
doc.parse();
|
||||
expect(doc.description).
|
||||
@@ -452,15 +461,15 @@ describe('ngdoc', function(){
|
||||
|
||||
});
|
||||
|
||||
describe('@example', function(){
|
||||
it('should not remove {{}}', function(){
|
||||
var doc = new Doc('@example text {{ abc }}');
|
||||
describe('@example', function() {
|
||||
it('should not remove {{}}', function() {
|
||||
var doc = new Doc('@name a\n@example text {{ abc }}');
|
||||
doc.parse();
|
||||
expect(doc.example).toEqual('<p>text {{ abc }}</p>');
|
||||
});
|
||||
|
||||
it('should support doc:example', function(){
|
||||
var doc = new Doc('@ngdoc overview\n@example \n' +
|
||||
it('should support doc:example', function() {
|
||||
var doc = new Doc('@name a\n@ngdoc overview\n@example \n' +
|
||||
'<doc:example>\n' +
|
||||
' <doc:source><escapeme></doc:source>\n' +
|
||||
' <doc:scenario><scenario></doc:scenario>\n' +
|
||||
@@ -474,15 +483,15 @@ describe('ngdoc', function(){
|
||||
|
||||
describe('@deprecated', function() {
|
||||
it('should parse @deprecated', function() {
|
||||
var doc = new Doc('@deprecated Replaced with foo.');
|
||||
var doc = new Doc('@name a\n@deprecated Replaced with foo.');
|
||||
doc.parse();
|
||||
expect(doc.deprecated).toBe('Replaced with foo.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('@this', function(){
|
||||
describe('@this', function() {
|
||||
it('should render @this', function() {
|
||||
var doc = new Doc('@this I am self.');
|
||||
var doc = new Doc('@name a\n@this I am self.');
|
||||
doc.ngdoc = 'filter';
|
||||
doc.parse();
|
||||
expect(doc.html()).toContain('<h3>Method\'s <code>this</code></h3>\n' +
|
||||
@@ -495,9 +504,9 @@ describe('ngdoc', function(){
|
||||
});
|
||||
});
|
||||
|
||||
describe('usage', function(){
|
||||
describe('overview', function(){
|
||||
it('should supress description heading', function(){
|
||||
describe('usage', function() {
|
||||
describe('overview', function() {
|
||||
it('should supress description heading', function() {
|
||||
var doc = new Doc('@ngdoc overview\n@name angular\n@description\n#heading\ntext');
|
||||
doc.parse();
|
||||
expect(doc.html()).toContain('text');
|
||||
@@ -507,8 +516,8 @@ describe('ngdoc', function(){
|
||||
});
|
||||
|
||||
|
||||
describe('function', function(){
|
||||
it('should format', function(){
|
||||
describe('function', function() {
|
||||
it('should format', function() {
|
||||
var doc = new Doc({
|
||||
ngdoc:'function',
|
||||
name:'some.name',
|
||||
@@ -520,15 +529,15 @@ describe('ngdoc', function(){
|
||||
returns: {type: 'number', description: 'return desc'}
|
||||
});
|
||||
doc.html_usage_function(dom);
|
||||
expect(dom).toContain('some.name([a][, b], c)'); //TODO(i) the comma position here is lame
|
||||
expect(dom).toContain('name([a][, b], c)'); //TODO(i) the comma position here is lame
|
||||
expect(dom).toContain('param desc');
|
||||
expect(dom).toContain('(optional="xxx")');
|
||||
expect(dom).toContain('return desc');
|
||||
});
|
||||
});
|
||||
|
||||
describe('filter', function(){
|
||||
it('should format', function(){
|
||||
describe('filter', function() {
|
||||
it('should format', function() {
|
||||
var doc = new Doc({
|
||||
ngdoc:'formatter',
|
||||
shortName:'myFilter',
|
||||
@@ -539,44 +548,12 @@ describe('ngdoc', function(){
|
||||
});
|
||||
doc.html_usage_filter(dom);
|
||||
expect(dom).toContain('myFilter_expression | myFilter:b');
|
||||
expect(dom).toContain('angular.filter.myFilter(a, b)');
|
||||
expect(dom).toContain('$filter(\'myFilter\')(a, b)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validator', function(){
|
||||
it('should format', function(){
|
||||
var doc = new Doc({
|
||||
ngdoc:'validator',
|
||||
shortName:'myValidator',
|
||||
param: [
|
||||
{name:'a'},
|
||||
{name:'b'}
|
||||
]
|
||||
});
|
||||
doc.html_usage_validator(dom);
|
||||
expect(dom).toContain('ng:validate="myValidator:b"');
|
||||
expect(dom).toContain('angular.validator.myValidator(a, b)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatter', function(){
|
||||
it('should format', function(){
|
||||
var doc = new Doc({
|
||||
ngdoc:'formatter',
|
||||
shortName:'myFormatter',
|
||||
param: [
|
||||
{name:'a'},
|
||||
]
|
||||
});
|
||||
doc.html_usage_formatter(dom);
|
||||
expect(dom).toContain('ng:format="myFormatter:a"');
|
||||
expect(dom).toContain('var userInputString = angular.formatter.myFormatter.format(modelValue, a);');
|
||||
expect(dom).toContain('var modelValue = angular.formatter.myFormatter.parse(userInputString, a);');
|
||||
});
|
||||
});
|
||||
|
||||
describe('property', function(){
|
||||
it('should format', function(){
|
||||
describe('property', function() {
|
||||
it('should format', function() {
|
||||
var doc = new Doc({
|
||||
ngdoc:'property',
|
||||
name:'myProp',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
var SiteMap = require('SiteMap.js').SiteMap;
|
||||
var Doc = require('ngdoc.js').Doc;
|
||||
var SiteMap = require('../src/SiteMap.js').SiteMap;
|
||||
var Doc = require('../src/ngdoc.js').Doc;
|
||||
|
||||
|
||||
describe('sitemap', function(){
|
||||
it('should render empty sitemap', function(){
|
||||
describe('sitemap', function() {
|
||||
it('should render empty sitemap', function() {
|
||||
var map = new SiteMap([]);
|
||||
expect(map.render()).toEqual([
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
@@ -11,7 +11,7 @@ describe('sitemap', function(){
|
||||
'</urlset>', ''].join('\n'));
|
||||
});
|
||||
|
||||
it('should render ngdoc url', function(){
|
||||
it('should render ngdoc url', function() {
|
||||
var map = new SiteMap([new Doc({section: 'foo', id: 'a.b.c<>\'"&'})]);
|
||||
expect(map.render()).toContain([
|
||||
' <url>',
|
||||
|
||||
+2
-5
@@ -1,16 +1,13 @@
|
||||
if (global.jasmine) return;
|
||||
|
||||
require.paths.push(__dirname + "/../../lib");
|
||||
require.paths.push(__dirname + '/../src');
|
||||
var jasmine = require('jasmine-1.0.1');
|
||||
var sys = require('util');
|
||||
var jasmine = require('../../lib/jasmine-1.0.1');
|
||||
|
||||
for(var key in jasmine) {
|
||||
global[key] = jasmine[key];
|
||||
}
|
||||
|
||||
//Patch Jasmine for proper stack traces
|
||||
jasmine.Spec.prototype.fail = function (e) {
|
||||
jasmine.Spec.prototype.fail = function(e) {
|
||||
var expectationResult = new jasmine.ExpectationResult({
|
||||
passed: false,
|
||||
message: e ? jasmine.util.formatException(e) : 'Exception'
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
var writer = require('writer.js');
|
||||
describe('writer', function(){
|
||||
describe('toString', function(){
|
||||
var writer = require('../src/writer.js');
|
||||
describe('writer', function() {
|
||||
describe('toString', function() {
|
||||
var toString = writer.toString;
|
||||
|
||||
it('should merge string', function(){
|
||||
it('should merge string', function() {
|
||||
expect(toString('abc')).toEqual('abc');
|
||||
});
|
||||
|
||||
it('should merge obj', function(){
|
||||
it('should merge obj', function() {
|
||||
expect(toString({a:1})).toEqual('{"a":1}');
|
||||
});
|
||||
|
||||
it('should merge array', function(){
|
||||
it('should merge array', function() {
|
||||
expect(toString(['abc',{}])).toEqual('abc{}');
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ exports.SiteMap = SiteMap;
|
||||
* @returns {SiteMap}
|
||||
*/
|
||||
function SiteMap(docs){
|
||||
this.render = function(){
|
||||
this.render = function() {
|
||||
var map = [];
|
||||
map.push('<?xml version="1.0" encoding="UTF-8"?>');
|
||||
map.push('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">');
|
||||
|
||||
+19
-7
@@ -12,7 +12,7 @@ function htmlEscape(text){
|
||||
}
|
||||
|
||||
|
||||
function DOM(){
|
||||
function DOM() {
|
||||
this.out = [];
|
||||
this.headingDepth = 0;
|
||||
}
|
||||
@@ -69,22 +69,34 @@ DOM.prototype = {
|
||||
},
|
||||
|
||||
code: function(text) {
|
||||
this.tag('div', {'ng:non-bindable':''}, function(){
|
||||
this.tag('div', {'ng:non-bindable':''}, function() {
|
||||
this.tag('pre', {'class':"brush: js; html-script: true;"}, text);
|
||||
});
|
||||
},
|
||||
|
||||
div: function(attr, text) {
|
||||
this.tag('div', attr, text);
|
||||
},
|
||||
|
||||
h: function(heading, content, fn){
|
||||
if (content==undefined || (content instanceof Array && content.length == 0)) return;
|
||||
this.headingDepth++;
|
||||
this.tag('h' + this.headingDepth, heading);
|
||||
var className = typeof heading == 'string'
|
||||
? {'class': heading.toLowerCase().replace(/[^\d\w_]/mg, '-').replace(/-+/gm, '-')}
|
||||
: null;
|
||||
var className = null,
|
||||
anchor = null;
|
||||
if (typeof heading == 'string') {
|
||||
var id = heading.
|
||||
replace(/\(.*\)/mg, '').
|
||||
replace(/[^\d\w\$]/mg, '.').
|
||||
replace(/-+/gm, '-').
|
||||
replace(/-*$/gm, '');
|
||||
anchor = {'id': id};
|
||||
className = {'class': id.toLowerCase().replace(/[._]/mg, '-')};
|
||||
}
|
||||
this.tag('h' + this.headingDepth, anchor, heading);
|
||||
if (content instanceof Array) {
|
||||
this.ul(content, className, fn);
|
||||
} else if (fn) {
|
||||
this.tag('div', className, function(){
|
||||
this.tag('div', className, function() {
|
||||
fn.call(this, content);
|
||||
});
|
||||
} else {
|
||||
|
||||
+12
-12
@@ -1,13 +1,11 @@
|
||||
require.paths.push(__dirname);
|
||||
require.paths.push('lib');
|
||||
var reader = require('reader.js'),
|
||||
ngdoc = require('ngdoc.js'),
|
||||
writer = require('writer.js'),
|
||||
SiteMap = require('SiteMap.js').SiteMap,
|
||||
appCache = require('appCache.js').appCache,
|
||||
var reader = require('./reader.js'),
|
||||
ngdoc = require('./ngdoc.js'),
|
||||
writer = require('./writer.js'),
|
||||
SiteMap = require('./SiteMap.js').SiteMap,
|
||||
appCache = require('./appCache.js').appCache,
|
||||
Q = require('qq');
|
||||
|
||||
process.on('uncaughtException', function (err) {
|
||||
process.on('uncaughtException', function(err) {
|
||||
console.error(err.stack || err);
|
||||
});
|
||||
|
||||
@@ -22,7 +20,9 @@ writer.makeDir('build/docs/syntaxhighlighter').then(function() {
|
||||
ngdoc.merge(docs);
|
||||
var fileFutures = [];
|
||||
docs.forEach(function(doc){
|
||||
fileFutures.push(writer.output('partials/' + doc.section + '/' + doc.id + '.html', doc.html()));
|
||||
// this hack is here because on OSX angular.module and angular.Module map to the same file.
|
||||
var id = doc.id.replace('angular.Module', 'angular.IModule');
|
||||
fileFutures.push(writer.output('partials/' + doc.section + '/' + id + '.html', doc.html()));
|
||||
});
|
||||
|
||||
writeTheRest(fileFutures);
|
||||
@@ -46,7 +46,7 @@ function writeTheRest(writesFuture) {
|
||||
var manifest = 'manifest="/build/docs/appcache.manifest"';
|
||||
|
||||
writesFuture.push(writer.copy('docs/src/templates/index.html', 'build/docs/index.html',
|
||||
writer.replace, {'doc:manifest': manifest}));
|
||||
writer.replace, {'doc:manifest': ''})); //manifest //TODO(i): enable
|
||||
|
||||
writesFuture.push(writer.copy('docs/src/templates/index.html', 'build/docs/index-nocache.html',
|
||||
writer.replace, {'doc:manifest': ''}));
|
||||
@@ -93,6 +93,6 @@ function writeTheRest(writesFuture) {
|
||||
}
|
||||
|
||||
|
||||
function now(){ return new Date().getTime(); }
|
||||
function now() { return new Date().getTime(); }
|
||||
|
||||
function noop(){};
|
||||
function noop() {};
|
||||
|
||||
+173
-167
@@ -2,9 +2,9 @@
|
||||
* All parsing/transformation code goes here. All code here should be sync to ease testing.
|
||||
*/
|
||||
|
||||
var Showdown = require('showdown').Showdown;
|
||||
var DOM = require('dom.js').DOM;
|
||||
var htmlEscape = require('dom.js').htmlEscape;
|
||||
var Showdown = require('../../lib/showdown').Showdown;
|
||||
var DOM = require('./dom.js').DOM;
|
||||
var htmlEscape = require('./dom.js').htmlEscape;
|
||||
var NEW_LINE = /\n\r?/;
|
||||
|
||||
exports.trim = trim;
|
||||
@@ -13,6 +13,11 @@ exports.scenarios = scenarios;
|
||||
exports.merge = merge;
|
||||
exports.Doc = Doc;
|
||||
|
||||
var BOOLEAN_ATTR = {};
|
||||
['multiple', 'selected', 'checked', 'disabled', 'readOnly', 'required'].forEach(function(value, key) {
|
||||
BOOLEAN_ATTR[value] = true;
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
function Doc(text, file, line) {
|
||||
if (typeof text == 'object') {
|
||||
@@ -32,14 +37,14 @@ function Doc(text, file, line) {
|
||||
this.events = this.events || [];
|
||||
this.links = this.links || [];
|
||||
}
|
||||
Doc.METADATA_IGNORE = (function(){
|
||||
Doc.METADATA_IGNORE = (function() {
|
||||
var words = require('fs').readFileSync(__dirname + '/ignore.words', 'utf8');
|
||||
return words.toString().split(/[,\s\n\r]+/gm);
|
||||
})();
|
||||
|
||||
|
||||
Doc.prototype = {
|
||||
keywords: function keywords(){
|
||||
keywords: function keywords() {
|
||||
var keywords = {};
|
||||
Doc.METADATA_IGNORE.forEach(function(ignore){ keywords[ignore] = true; });
|
||||
var words = [];
|
||||
@@ -78,12 +83,13 @@ Doc.prototype = {
|
||||
return this.section + '/' + url;
|
||||
},
|
||||
|
||||
markdown: function (text) {
|
||||
markdown: function(text) {
|
||||
if (!text) return text;
|
||||
|
||||
var self = this,
|
||||
IS_URL = /^(https?:\/\/|ftps?:\/\/|mailto:|\.|\/)/,
|
||||
IS_ANGULAR = /^(api\/)?angular\./,
|
||||
IS_HASH = /^#/,
|
||||
parts = trim(text).split(/(<pre>[\s\S]*?<\/pre>|<doc:(\S*).*?>[\s\S]*?<\/doc:\2>)/);
|
||||
|
||||
parts.forEach(function(text, i) {
|
||||
@@ -112,9 +118,9 @@ Doc.prototype = {
|
||||
'</pre></div>';
|
||||
});
|
||||
} else if (isDocWidget('example')) {
|
||||
text = text.replace(/<doc:source(\s+jsfiddle="[^"]+")?>([\s\S]*)<\/doc:source>/mi,
|
||||
function(_, jsfiddle, content){
|
||||
return '<pre class="doc-source"' + (jsfiddle || '') +'>' +
|
||||
text = text.replace(/<doc:source(\s+[^>]*)?>([\s\S]*)<\/doc:source>/mi,
|
||||
function(_, attrs, content){
|
||||
return '<pre class="doc-source"' + (attrs || '') +'>' +
|
||||
htmlEscape(content) +
|
||||
'</pre>';
|
||||
});
|
||||
@@ -129,15 +135,18 @@ Doc.prototype = {
|
||||
function(_all, url, title){
|
||||
var isFullUrl = url.match(IS_URL),
|
||||
isAngular = url.match(IS_ANGULAR),
|
||||
absUrl = isFullUrl ? url : self.convertUrlToAbsolute(url);
|
||||
isHash = url.match(IS_HASH),
|
||||
absUrl = isHash
|
||||
? url
|
||||
: (isFullUrl ? url : self.convertUrlToAbsolute(url));
|
||||
|
||||
if (!isFullUrl) self.links.push(absUrl);
|
||||
|
||||
return '<a href="' + absUrl + '">'
|
||||
+ (isAngular ? '<code>' : '')
|
||||
+ (title || url).replace(/\n/g, ' ')
|
||||
+ (isAngular ? '</code>' : '')
|
||||
+ '</a>';
|
||||
return '<a href="' + absUrl + '">' +
|
||||
(isAngular ? '<code>' : '') +
|
||||
(title || url).replace(/^#/g, '').replace(/\n/g, ' ') +
|
||||
(isAngular ? '</code>' : '') +
|
||||
'</a>';
|
||||
});
|
||||
text = new Showdown.converter().makeHtml(text);
|
||||
}
|
||||
@@ -146,13 +155,13 @@ Doc.prototype = {
|
||||
return parts.join('');
|
||||
},
|
||||
|
||||
parse: function(){
|
||||
parse: function() {
|
||||
var atName;
|
||||
var atText;
|
||||
var match;
|
||||
var self = this;
|
||||
self.text.split(NEW_LINE).forEach(function(line){
|
||||
if (match = line.match(/^\s*@(\w+)(\s+(.*))?/)) {
|
||||
if ((match = line.match(/^\s*@(\w+)(\s+(.*))?/))) {
|
||||
// we found @name ...
|
||||
// if we have existing name
|
||||
flush();
|
||||
@@ -166,20 +175,20 @@ Doc.prototype = {
|
||||
}
|
||||
});
|
||||
flush();
|
||||
this.shortName = (this.name || '').split(/[\.#]/).pop();
|
||||
this.id = this.id // if we have an id just use it
|
||||
|| (((this.file||'').match(/.*\/([^\/]*)\.ngdoc/)||{})[1]) // try to extract it from file name
|
||||
|| this.name; // default to name
|
||||
this.shortName = this.name.split(this.name.match(/#/) ? /#/ : /\./ ).pop();
|
||||
this.id = this.id || // if we have an id just use it
|
||||
(((this.file||'').match(/.*\/([^\/]*)\.ngdoc/)||{})[1]) || // try to extract it from file name
|
||||
this.name; // default to name
|
||||
this.description = this.markdown(this.description);
|
||||
this.example = this.markdown(this.example);
|
||||
this['this'] = this.markdown(this['this']);
|
||||
return this;
|
||||
|
||||
function flush(){
|
||||
function flush() {
|
||||
if (atName) {
|
||||
var text = trim(atText.join('\n'));
|
||||
var text = trim(atText.join('\n')), match;
|
||||
if (atName == 'param') {
|
||||
var match = text.match(/^{([^}=]+)(=)?}\s+(([^\s=]+)|\[(\S+)=([^\]]+)\])\s+(.*)/);
|
||||
match = text.match(/^\{([^}=]+)(=)?\}\s+(([^\s=]+)|\[(\S+)=([^\]]+)\])\s+(.*)/);
|
||||
// 1 12 2 34 4 5 5 6 6 3 7 7
|
||||
if (!match) {
|
||||
throw new Error("Not a valid 'param' format: " + text);
|
||||
@@ -193,7 +202,7 @@ Doc.prototype = {
|
||||
};
|
||||
self.param.push(param);
|
||||
} else if (atName == 'returns') {
|
||||
var match = text.match(/^{([^}=]+)}\s+(.*)/);
|
||||
match = text.match(/^\{([^}=]+)\}\s+(.*)/);
|
||||
if (!match) {
|
||||
throw new Error("Not a valid 'returns' format: " + text);
|
||||
}
|
||||
@@ -202,24 +211,25 @@ Doc.prototype = {
|
||||
description: self.markdown(text.replace(match[0], match[2]))
|
||||
};
|
||||
} else if(atName == 'requires') {
|
||||
var match = text.match(/^([^\s]*)\s*([\S\s]*)/);
|
||||
match = text.match(/^([^\s]*)\s*([\S\s]*)/);
|
||||
self.requires.push({
|
||||
name: match[1],
|
||||
text: self.markdown(match[2])
|
||||
});
|
||||
} else if(atName == 'property') {
|
||||
var match = text.match(/^{(\S+)}\s+(\S+)(\s+(.*))?/);
|
||||
match = text.match(/^\{(\S+)\}\s+(\S+)(\s+(.*))?/);
|
||||
if (!match) {
|
||||
throw new Error("Not a valid 'property' format: " + text);
|
||||
}
|
||||
var property = {
|
||||
var property = new Doc({
|
||||
type: match[1],
|
||||
name: match[2],
|
||||
shortName: match[2],
|
||||
description: self.markdown(text.replace(match[0], match[4]))
|
||||
};
|
||||
});
|
||||
self.properties.push(property);
|
||||
} else if(atName == 'eventType') {
|
||||
var match = text.match(/^([^\s]*)\s+on\s+([\S\s]*)/);
|
||||
match = text.match(/^([^\s]*)\s+on\s+([\S\s]*)/);
|
||||
self.type = match[1];
|
||||
self.target = match[2];
|
||||
} else {
|
||||
@@ -229,26 +239,24 @@ Doc.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
html: function(){
|
||||
html: function() {
|
||||
var dom = new DOM(),
|
||||
self = this;
|
||||
|
||||
dom.h(this.name, function(){
|
||||
notice('workInProgress', 'Work in Progress',
|
||||
'This page is currently being revised. It might be incomplete or contain inaccuracies.');
|
||||
dom.h(this.name, function() {
|
||||
notice('deprecated', 'Deprecated API', self.deprecated);
|
||||
|
||||
if (self.ngdoc != 'overview') {
|
||||
dom.h('Description', self.description, dom.html);
|
||||
}
|
||||
dom.h('Dependencies', self.requires, function(require){
|
||||
dom.tag('code', function(){
|
||||
dom.tag('a', {href: 'api/angular.service.' + require.name}, require.name);
|
||||
dom.tag('code', function() {
|
||||
dom.tag('a', {href: 'api/angular.module.ng.' + require.name}, require.name);
|
||||
});
|
||||
dom.html(require.text);
|
||||
});
|
||||
|
||||
(self['html_usage_' + self.ngdoc] || function(){
|
||||
(self['html_usage_' + self.ngdoc] || function() {
|
||||
throw new Error("Don't know how to format @ngdoc: " + self.ngdoc);
|
||||
}).call(self, dom);
|
||||
|
||||
@@ -271,10 +279,10 @@ Doc.prototype = {
|
||||
|
||||
html_usage_parameters: function(dom) {
|
||||
dom.h('Parameters', this.param, function(param){
|
||||
dom.tag('code', function(){
|
||||
dom.tag('code', function() {
|
||||
dom.text(param.name);
|
||||
if (param.optional) {
|
||||
dom.tag('i', function(){
|
||||
dom.tag('i', function() {
|
||||
dom.text('(optional');
|
||||
if(param['default']) {
|
||||
dom.text('=' + param['default']);
|
||||
@@ -284,6 +292,9 @@ Doc.prototype = {
|
||||
}
|
||||
dom.text(' – {');
|
||||
dom.text(param.type);
|
||||
if (param.optional) {
|
||||
dom.text('=');
|
||||
}
|
||||
dom.text('} – ');
|
||||
});
|
||||
dom.html(param.description);
|
||||
@@ -293,7 +304,7 @@ Doc.prototype = {
|
||||
html_usage_returns: function(dom) {
|
||||
var self = this;
|
||||
if (self.returns) {
|
||||
dom.h('Returns', function(){
|
||||
dom.h('Returns', function() {
|
||||
dom.tag('code', '{' + self.returns.type + '}');
|
||||
dom.text('– ');
|
||||
dom.html(self.returns.description);
|
||||
@@ -314,9 +325,9 @@ Doc.prototype = {
|
||||
|
||||
html_usage_function: function(dom){
|
||||
var self = this;
|
||||
dom.h('Usage', function(){
|
||||
dom.code(function(){
|
||||
dom.text(self.name.split('service.').pop());
|
||||
dom.h('Usage', function() {
|
||||
dom.code(function() {
|
||||
dom.text(self.name.split(/\./).pop());
|
||||
dom.text('(');
|
||||
self.parameters(dom, ', ');
|
||||
dom.text(');');
|
||||
@@ -326,12 +337,13 @@ Doc.prototype = {
|
||||
self.html_usage_this(dom);
|
||||
self.html_usage_returns(dom);
|
||||
});
|
||||
this.method_properties_events(dom);
|
||||
},
|
||||
|
||||
html_usage_property: function(dom){
|
||||
var self = this;
|
||||
dom.h('Usage', function(){
|
||||
dom.code(function(){
|
||||
dom.h('Usage', function() {
|
||||
dom.code(function() {
|
||||
dom.text(self.name);
|
||||
});
|
||||
|
||||
@@ -341,8 +353,8 @@ Doc.prototype = {
|
||||
|
||||
html_usage_directive: function(dom){
|
||||
var self = this;
|
||||
dom.h('Usage', function(){
|
||||
dom.tag('pre', {'class':"brush: js; html-script: true;"}, function(){
|
||||
dom.h('Usage', function() {
|
||||
dom.tag('pre', {'class':"brush: js; html-script: true;"}, function() {
|
||||
dom.text('<' + self.element + ' ');
|
||||
dom.text(self.shortName);
|
||||
if (self.param.length) {
|
||||
@@ -357,9 +369,9 @@ Doc.prototype = {
|
||||
|
||||
html_usage_filter: function(dom){
|
||||
var self = this;
|
||||
dom.h('Usage', function(){
|
||||
dom.h('In HTML Template Binding', function(){
|
||||
dom.tag('code', function(){
|
||||
dom.h('Usage', function() {
|
||||
dom.h('In HTML Template Binding', function() {
|
||||
dom.tag('code', function() {
|
||||
dom.text('{{ ');
|
||||
dom.text(self.shortName);
|
||||
dom.text('_expression | ');
|
||||
@@ -369,11 +381,11 @@ Doc.prototype = {
|
||||
});
|
||||
});
|
||||
|
||||
dom.h('In JavaScript', function(){
|
||||
dom.tag('code', function(){
|
||||
dom.text('angular.filter.');
|
||||
dom.h('In JavaScript', function() {
|
||||
dom.tag('code', function() {
|
||||
dom.text('$filter(\'');
|
||||
dom.text(self.shortName);
|
||||
dom.text('(');
|
||||
dom.text('\')(');
|
||||
self.parameters(dom, ', ');
|
||||
dom.text(')');
|
||||
});
|
||||
@@ -385,77 +397,29 @@ Doc.prototype = {
|
||||
});
|
||||
},
|
||||
|
||||
html_usage_formatter: function(dom){
|
||||
html_usage_inputType: function(dom){
|
||||
var self = this;
|
||||
dom.h('Usage', function(){
|
||||
dom.h('In HTML Template Binding', function(){
|
||||
dom.code(function(){
|
||||
if (self.inputType=='select')
|
||||
dom.text('<select name="bindExpression"');
|
||||
else
|
||||
dom.text('<input type="text" name="bindExpression"');
|
||||
dom.text(' ng:format="');
|
||||
dom.text(self.shortName);
|
||||
self.parameters(dom, ':', false, true);
|
||||
dom.text('">');
|
||||
dom.h('Usage', function() {
|
||||
dom.code(function() {
|
||||
dom.text('<input type="' + self.shortName + '"');
|
||||
(self.param||[]).forEach(function(param){
|
||||
dom.text('\n ');
|
||||
dom.text(param.optional ? ' [' : ' ');
|
||||
dom.text(param.name);
|
||||
dom.text(BOOLEAN_ATTR[param.name] ? '' : '="..."');
|
||||
dom.text(param.optional ? ']' : '');
|
||||
});
|
||||
dom.text('>');
|
||||
});
|
||||
|
||||
dom.h('In JavaScript', function(){
|
||||
dom.code(function(){
|
||||
dom.text('var userInputString = angular.formatter.');
|
||||
dom.text(self.shortName);
|
||||
dom.text('.format(modelValue');
|
||||
self.parameters(dom, ', ', false, true);
|
||||
dom.text(');');
|
||||
dom.text('\n');
|
||||
dom.text('var modelValue = angular.formatter.');
|
||||
dom.text(self.shortName);
|
||||
dom.text('.parse(userInputString');
|
||||
self.parameters(dom, ', ', false, true);
|
||||
dom.text(');');
|
||||
});
|
||||
});
|
||||
|
||||
self.html_usage_parameters(dom);
|
||||
self.html_usage_this(dom);
|
||||
self.html_usage_returns(dom);
|
||||
});
|
||||
},
|
||||
|
||||
html_usage_validator: function(dom){
|
||||
var self = this;
|
||||
dom.h('Usage', function(){
|
||||
dom.h('In HTML Template Binding', function(){
|
||||
dom.code(function(){
|
||||
dom.text('<input type="text" ng:validate="');
|
||||
dom.text(self.shortName);
|
||||
self.parameters(dom, ':', true);
|
||||
dom.text('"/>');
|
||||
});
|
||||
});
|
||||
|
||||
dom.h('In JavaScript', function(){
|
||||
dom.code(function(){
|
||||
dom.text('angular.validator.');
|
||||
dom.text(self.shortName);
|
||||
dom.text('(');
|
||||
self.parameters(dom, ', ');
|
||||
dom.text(')');
|
||||
});
|
||||
});
|
||||
|
||||
self.html_usage_parameters(dom);
|
||||
self.html_usage_this(dom);
|
||||
self.html_usage_returns(dom);
|
||||
});
|
||||
},
|
||||
|
||||
html_usage_widget: function(dom){
|
||||
var self = this;
|
||||
dom.h('Usage', function(){
|
||||
dom.h('In HTML Template Binding', function(){
|
||||
dom.code(function(){
|
||||
dom.h('Usage', function() {
|
||||
dom.h('In HTML Template Binding', function() {
|
||||
dom.code(function() {
|
||||
if (self.shortName.match(/^@/)) {
|
||||
dom.text('<');
|
||||
dom.text(self.element);
|
||||
@@ -473,11 +437,11 @@ Doc.prototype = {
|
||||
dom.text('<');
|
||||
dom.text(self.shortName);
|
||||
(self.param||[]).forEach(function(param){
|
||||
if (param.optional) {
|
||||
dom.text(' [' + param.name + '="..."]');
|
||||
} else {
|
||||
dom.text(' ' + param.name + '="..."');
|
||||
}
|
||||
dom.text('\n ');
|
||||
dom.text(param.optional ? ' [' : ' ');
|
||||
dom.text(param.name);
|
||||
dom.text(BOOLEAN_ATTR[param.name] ? '' : '="..."');
|
||||
dom.text(param.optional ? ']' : '');
|
||||
});
|
||||
dom.text('></');
|
||||
dom.text(self.shortName);
|
||||
@@ -495,12 +459,12 @@ Doc.prototype = {
|
||||
dom.html(this.description);
|
||||
},
|
||||
|
||||
html_usage_service: function(dom){
|
||||
html_usage_interface: function(dom){
|
||||
var self = this;
|
||||
|
||||
if (this.param.length) {
|
||||
dom.h('Usage', function(){
|
||||
dom.code(function(){
|
||||
dom.h('Usage', function() {
|
||||
dom.code(function() {
|
||||
dom.text(self.name.split('.').pop());
|
||||
dom.text('(');
|
||||
self.parameters(dom, ', ');
|
||||
@@ -512,39 +476,73 @@ Doc.prototype = {
|
||||
self.html_usage_returns(dom);
|
||||
});
|
||||
}
|
||||
this.method_properties_events(dom);
|
||||
},
|
||||
|
||||
dom.h('Methods', this.methods, function(method){
|
||||
var signature = (method.param || []).map(property('name'));
|
||||
dom.h(method.shortName + '(' + signature.join(', ') + ')', method, function(){
|
||||
dom.html(method.description);
|
||||
method.html_usage_parameters(dom);
|
||||
self.html_usage_this(dom);
|
||||
method.html_usage_returns(dom);
|
||||
html_usage_service: function(dom) {
|
||||
this.html_usage_interface(dom)
|
||||
},
|
||||
|
||||
dom.h('Example', method.example, dom.html);
|
||||
});
|
||||
});
|
||||
dom.h('Properties', this.properties, function(property){
|
||||
dom.h(property.name, function(){
|
||||
dom.html(property.description);
|
||||
dom.h('Example', property.example, dom.html);
|
||||
});
|
||||
});
|
||||
dom.h('Events', this.events, function(event){
|
||||
dom.h(event.shortName, event, function(){
|
||||
dom.html(event.description);
|
||||
dom.tag('div', {class:'inline'}, function(){
|
||||
dom.h('Type:', event.type);
|
||||
html_usage_object: function(dom) {
|
||||
this.html_usage_interface(dom)
|
||||
},
|
||||
|
||||
method_properties_events: function(dom) {
|
||||
var self = this;
|
||||
if (self.methods.length) {
|
||||
dom.div({class:'member method'}, function(){
|
||||
dom.h('Methods', self.methods, function(method){
|
||||
var signature = (method.param || []).map(property('name'));
|
||||
dom.h(method.shortName + '(' + signature.join(', ') + ')', method, function() {
|
||||
dom.html(method.description);
|
||||
method.html_usage_parameters(dom);
|
||||
self.html_usage_this(dom);
|
||||
method.html_usage_returns(dom);
|
||||
|
||||
dom.h('Example', method.example, dom.html);
|
||||
});
|
||||
});
|
||||
dom.tag('div', {class:'inline'}, function(){
|
||||
dom.h('Target:', event.target);
|
||||
});
|
||||
event.html_usage_parameters(dom);
|
||||
self.html_usage_this(dom);
|
||||
|
||||
dom.h('Example', event.example, dom.html);
|
||||
});
|
||||
});
|
||||
}
|
||||
if (self.properties.length) {
|
||||
dom.div({class:'member property'}, function(){
|
||||
dom.h('Properties', self.properties, function(property){
|
||||
dom.h(property.shortName, function() {
|
||||
dom.html(property.description);
|
||||
if (!property.html_usage_returns) {
|
||||
console.log(property);
|
||||
}
|
||||
property.html_usage_returns(dom);
|
||||
dom.h('Example', property.example, dom.html);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
if (self.events.length) {
|
||||
dom.div({class:'member event'}, function(){
|
||||
dom.h('Events', self.events, function(event){
|
||||
dom.h(event.shortName, event, function() {
|
||||
dom.html(event.description);
|
||||
if (event.type == 'listen') {
|
||||
dom.tag('div', {class:'inline'}, function() {
|
||||
dom.h('Listen on:', event.target);
|
||||
});
|
||||
} else {
|
||||
dom.tag('div', {class:'inline'}, function() {
|
||||
dom.h('Type:', event.type);
|
||||
});
|
||||
dom.tag('div', {class:'inline'}, function() {
|
||||
dom.h('Target:', event.target);
|
||||
});
|
||||
}
|
||||
event.html_usage_parameters(dom);
|
||||
self.html_usage_this(dom);
|
||||
|
||||
dom.h('Example', event.example, dom.html);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
parameters: function(dom, separator, skipFirst, prefix) {
|
||||
@@ -584,8 +582,8 @@ function scenarios(docs){
|
||||
|
||||
function appendSpecs(urlPrefix) {
|
||||
docs.forEach(function(doc){
|
||||
specs.push(' describe("' + doc.section + '/' + doc.id + '", function(){');
|
||||
specs.push(' beforeEach(function(){');
|
||||
specs.push(' describe("' + doc.section + '/' + doc.id + '", function() {');
|
||||
specs.push(' beforeEach(function() {');
|
||||
specs.push(' browser().navigateTo("' + urlPrefix + doc.section + '/' + doc.id + '");');
|
||||
specs.push(' });');
|
||||
specs.push(' ');
|
||||
@@ -628,16 +626,18 @@ var KEYWORD_PRIORITY = {
|
||||
'.index': 1,
|
||||
'.guide': 2,
|
||||
'.angular': 7,
|
||||
'.angular.Array': 7,
|
||||
'.angular.Object': 7,
|
||||
'.angular.directive': 7,
|
||||
'.angular.filter': 7,
|
||||
'.angular.formatter': 7,
|
||||
'.angular.scope': 7,
|
||||
'.angular.service': 7,
|
||||
'.angular.validator': 7,
|
||||
'.angular.widget': 7,
|
||||
'.angular.Module': 7,
|
||||
'.angular.module': 8,
|
||||
'.angular.mock': 9,
|
||||
'.angular.module.ng.$filter': 7,
|
||||
'.angular.module.ng.$filter': 7,
|
||||
'.angular.module.ng.$rootScope.Scope': 7,
|
||||
'.angular.module.ng': 7,
|
||||
'.angular.mock': 8,
|
||||
'.angular.directive': 6,
|
||||
'.angular.inputType': 6,
|
||||
'.angular.widget': 6,
|
||||
'.angular.module.ngMock': 8,
|
||||
'.dev_guide.overview': 1,
|
||||
'.dev_guide.bootstrap': 2,
|
||||
'.dev_guide.mvc': 3,
|
||||
@@ -658,7 +658,7 @@ function keywordSort(a, b){
|
||||
mangled.push(KEYWORD_PRIORITY[partialName] || 5);
|
||||
mangled.push(name);
|
||||
});
|
||||
return doc.section + '/' + mangled.join('.');
|
||||
return (doc.section + '/' + mangled.join('.')).toLowerCase();
|
||||
}
|
||||
var nameA = mangleName(a);
|
||||
var nameB = mangleName(b);
|
||||
@@ -724,7 +724,7 @@ function indent(text, spaceCount) {
|
||||
function merge(docs){
|
||||
var byFullId = {};
|
||||
|
||||
docs.forEach(function (doc) {
|
||||
docs.forEach(function(doc) {
|
||||
byFullId[doc.section + '/' + doc.id] = doc;
|
||||
});
|
||||
|
||||
@@ -733,7 +733,13 @@ function merge(docs){
|
||||
|
||||
// check links - do they exist ?
|
||||
doc.links.forEach(function(link) {
|
||||
if (!byFullId[link]) console.log('WARNING: In ' + doc.section + '/' + doc.id + ', non existing link: "' + link + '"');
|
||||
// convert #id to path#id
|
||||
if (link[0] == '#') {
|
||||
link = doc.section + '/' + doc.id.split('#').shift() + link;
|
||||
}
|
||||
if (!byFullId[link]) {
|
||||
console.log('WARNING: In ' + doc.section + '/' + doc.id + ', non existing link: "' + link + '"');
|
||||
}
|
||||
});
|
||||
|
||||
// merge into parents
|
||||
|
||||
+3
-6
@@ -5,8 +5,7 @@
|
||||
|
||||
exports.collect = collect;
|
||||
|
||||
require.paths.push(__dirname);
|
||||
var ngdoc = require('ngdoc.js'),
|
||||
var ngdoc = require('./ngdoc.js'),
|
||||
Q = require('qq'),
|
||||
qfs = require('q-fs');
|
||||
|
||||
@@ -23,7 +22,6 @@ function collect() {
|
||||
files.forEach(function(file) {
|
||||
var work;
|
||||
if(/\.js$/.test(file)) {
|
||||
console.log("reading " + file + ".......");
|
||||
work = Q.when(qfs.read(file, 'b'), function(content) {
|
||||
processJsFile(content, file).forEach (function(doc) {
|
||||
allDocs.push(doc);
|
||||
@@ -37,14 +35,13 @@ function collect() {
|
||||
return done;
|
||||
});
|
||||
|
||||
//collect all NG Docs in Content Folder
|
||||
//collect all ng Docs in Content Folder
|
||||
var path2 = 'docs/content';
|
||||
var promiseB = Q.when(qfs.listTree(path2), function(files){
|
||||
var done2;
|
||||
files.forEach(function(file) {
|
||||
var work2;
|
||||
if (file.match(/\.ngdoc$/)) {
|
||||
console.log("reading " + file + ".......");
|
||||
work2 = Q.when(qfs.read(file, 'b'), function(content){
|
||||
var section = '@section ' + file.split('/')[2] + '\n';
|
||||
allDocs.push(new ngdoc.Doc(section + content.toString(),file, 1).parse());
|
||||
@@ -96,4 +93,4 @@ function processJsFile(content, file) {
|
||||
}
|
||||
});
|
||||
return docs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ RewriteEngine on
|
||||
RewriteCond %{HTTP_COOKIE} ng-offline="NG_VERSION_FULL"
|
||||
RewriteRule appcache.manifest appcache-offline.manifest
|
||||
|
||||
## Redirect to the latest manifest
|
||||
RewriteCond %{HTTP_HOST} ^docs-next\.angularjs\.org$
|
||||
RewriteRule appcache.manifest http://code.angularjs.org/next/docs/appcache.manifest [R=301]
|
||||
|
||||
## HTML5 URL Support ##
|
||||
RewriteRule ^(guide|api|cookbook|misc|tutorial)(/.*)?$ index.html
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user