Compare commits
319 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22b669407c | |||
| 99a063f9cb | |||
| 27db9f018e | |||
| 2ed2399d49 | |||
| 9d2de344f5 | |||
| 9c0659ca27 | |||
| 3b9963c54a | |||
| 26db937377 | |||
| 1d0e582b3f | |||
| a6b58019e1 | |||
| 38fd896b74 | |||
| f96d553822 | |||
| f4149b70bc | |||
| 8a8c94c3d5 | |||
| d9de526059 | |||
| 0318761f7a | |||
| 74f7b57823 | |||
| 995ccfc9f6 | |||
| 72c244564a | |||
| 4bfe7e8663 | |||
| 63fe144397 | |||
| 0f551e6109 | |||
| 541f4ba21f | |||
| 6e7d04ddb2 | |||
| 06fb391687 | |||
| 203dd4d1e3 | |||
| c991f93817 | |||
| 52ddc13bee | |||
| 1645924d49 | |||
| 6bee655679 | |||
| acd455181d | |||
| a89b6e27a9 | |||
| 46d559dc8d | |||
| 26212fb204 | |||
| 244cfc7cd5 | |||
| c6074dc34c | |||
| 4930ef2bfe | |||
| 1778d347cd | |||
| 4724d56c93 | |||
| 70fcffc375 | |||
| 181e44019e | |||
| 5ce859bcdd | |||
| fb994e4d90 | |||
| 55f26fc246 | |||
| 1161002857 | |||
| 7ccfe92bed | |||
| a126fcfee3 | |||
| f22af2b874 | |||
| 2ca4170903 | |||
| fd57df1235 | |||
| 3010ed4ee5 | |||
| de59ca7107 | |||
| fa3214cfb8 | |||
| a8cacfe938 | |||
| 1a387ba5dd | |||
| 98e4a220fe | |||
| 9001ae2822 | |||
| c405f88bbc | |||
| 159a68ec7b | |||
| b9a56d588f | |||
| f797f83cd6 | |||
| 6df970eb3c | |||
| f88b4be92f | |||
| a7bcc01581 | |||
| 40ed8094d1 | |||
| 1a722f7b19 | |||
| 4991207c7c | |||
| 33514ec384 | |||
| 28bd20ed30 | |||
| c3bfd7f59d | |||
| 553cccdb85 | |||
| c7dd2c24f4 | |||
| fbf5aebda2 | |||
| ab35c16ccf | |||
| 37b6a71678 | |||
| e79ebff223 | |||
| 36b38e1355 | |||
| 3168d95793 | |||
| 538c63f057 | |||
| a9a1461351 | |||
| 29190f9318 | |||
| ca7d2b3b3f | |||
| 81676f8323 | |||
| 6f9d07d9c7 | |||
| 5201b8146a | |||
| 6aa83d04b7 | |||
| cfbe1a701a | |||
| aa38ded59d | |||
| b03957f004 | |||
| 56f33af890 | |||
| 41bea59eb7 | |||
| 6d0dcca6f1 | |||
| f423dc6b51 | |||
| 9fca87ca5b | |||
| ebbaa4ac5e | |||
| 617137231e | |||
| 71b966ace7 | |||
| 488d5b4288 | |||
| 01b8772cca | |||
| 05ac0ba926 | |||
| 399e872228 | |||
| 3115408be7 | |||
| aea39e9fdc | |||
| 51f6752cec | |||
| db58801a55 | |||
| 2e33d170be | |||
| cc6dcb4bc2 | |||
| 294d6793fd | |||
| cefcc46e1b | |||
| c10dba144e | |||
| f05ffd9bf1 | |||
| e208d81293 | |||
| b51fca1a2b | |||
| 6685043ad4 | |||
| f176a2dbb7 | |||
| 3a366bd42b | |||
| 1c5db02638 | |||
| ffd5308d86 | |||
| aed9de9d37 | |||
| aab6825e85 | |||
| c4fad6ad61 | |||
| 2a7c37cf01 | |||
| 194c3adfed | |||
| 371957d9d5 | |||
| 1e5d36d4b0 | |||
| 3749c85829 | |||
| 886b59d317 | |||
| 2048b6df24 | |||
| 5ec0a50d39 | |||
| 60689afe1c | |||
| 881fddaaba | |||
| fda338e8e2 | |||
| 05c8d88126 | |||
| aa57f57ccb | |||
| 51b78f61f9 | |||
| 739100e8a8 | |||
| 20e0c7f966 | |||
| f34e3413c7 | |||
| 4d28598b3d | |||
| 16d6e89f3b | |||
| 6e220ab612 | |||
| ce14e4a6b7 | |||
| b5dbc9834a | |||
| 57f10251db | |||
| 48cd05a1ad | |||
| 6a71ba17f0 | |||
| cf1b175508 | |||
| 5bba68c264 | |||
| e5a4345aac | |||
| 49ef92041f | |||
| 081ae8bfca | |||
| 4f8aa6f5bb | |||
| d7e888b8c6 | |||
| 491b4af92e | |||
| 5930b8ee27 | |||
| 69d9072d5b | |||
| 9b5de0ac97 | |||
| 21b76aed06 | |||
| 363fb16c10 | |||
| e00f9f62e3 | |||
| 507c666d16 | |||
| 743b33ab9d | |||
| 9f8fa79f25 | |||
| fe3c1d44a5 | |||
| 5ae72dc7af | |||
| 47bc98a1ea | |||
| 10920a2857 | |||
| 90a9270dfa | |||
| d4b05dd13a | |||
| 59802bc790 | |||
| b0a35a794a | |||
| a9545e3963 | |||
| 269c15c74b | |||
| 9f48025733 | |||
| 1d0ccd0d73 | |||
| 03e5164528 | |||
| 3047964712 | |||
| 225afb9eb4 | |||
| b377d6b043 | |||
| ac650cb2e6 | |||
| 926088ba77 | |||
| f94cf634d8 | |||
| 85b3786f55 | |||
| 1116a96df6 | |||
| 1c1c9b27cb | |||
| c5e7a5bc9b | |||
| 7550b7bda8 | |||
| 43b97b211b | |||
| 76405897fc | |||
| bb730704f2 | |||
| 8965d57aa0 | |||
| 951a2743d5 | |||
| a2a907e439 | |||
| 308f22ba9a | |||
| 32e2643d90 | |||
| 3edaf0922f | |||
| 1095596962 | |||
| ee0dac4d1d | |||
| 6f1fae8a91 | |||
| f96485085a | |||
| 147650b3be | |||
| 10c426ffa3 | |||
| 785647eb9d | |||
| a2a6279157 | |||
| b50d2a4a1f | |||
| f539fb1f7c | |||
| 2dcb5acb7b | |||
| 905a8fd4a4 | |||
| df9b57a481 | |||
| fee2077d0b | |||
| 691aafc981 | |||
| da7af937d6 | |||
| 1c9b9e24ed | |||
| 46a5b2c298 | |||
| 28e4707dc4 | |||
| 420b5211da | |||
| 04bccee099 | |||
| a404839795 | |||
| 25d4e5cca4 | |||
| c67112563f | |||
| 2b2ec26a75 | |||
| 41f90e5fb0 | |||
| a85c591d36 | |||
| f276ad0d51 | |||
| 33f817b99c | |||
| 6a4124d0fb | |||
| 26d1b34321 | |||
| 7fba6b603f | |||
| c115b37c33 | |||
| d20ba95e28 | |||
| 7945e5010a | |||
| 21d148aedc | |||
| 4c8aeefb62 | |||
| d384834fde | |||
| f975d8d448 | |||
| 2602daf993 | |||
| 9f681c459a | |||
| d9448dcb9f | |||
| e9c718a465 | |||
| 4dc44f7205 | |||
| 499e1b2adf | |||
| 28c6c4dae2 | |||
| 791148a328 | |||
| 01b1845088 | |||
| 56c861c9e1 | |||
| 451e0f6175 | |||
| aee7f1c72f | |||
| 5fe28a0422 | |||
| f56586d9a9 | |||
| ec0baadcb6 | |||
| 9147d5c04a | |||
| cb801378c9 | |||
| 79604f4628 | |||
| bf6cb8ab0d | |||
| de74b3fd85 | |||
| c7a92d2a9a | |||
| da284fc354 | |||
| 90da3059ce | |||
| 651fd05eca | |||
| 85d7e09bef | |||
| 243a57648b | |||
| 158dec330c | |||
| a1e63c8d4a | |||
| 8cc9978d15 | |||
| 78a404da0e | |||
| f1bb8369b5 | |||
| 76b6941b4c | |||
| e72990dc37 | |||
| f338e96ccc | |||
| 0ad2b70862 | |||
| 5261a374a9 | |||
| 832eba5fc9 | |||
| 2b569fc0b2 | |||
| ee6aeb08bf | |||
| 8d43d8b8e7 | |||
| d08f5c6986 | |||
| 3e7fa19197 | |||
| 5d379db72b | |||
| 514639b585 | |||
| 9cd9956dcb | |||
| c7813e9ebf | |||
| ef91b04cdd | |||
| 1acd97e18f | |||
| 513199ee9f | |||
| 33f3c40e93 | |||
| 696cb95d5e | |||
| 457fd21a1a | |||
| 3c6dfbf67d | |||
| 3277b885c4 | |||
| 48a256d04b | |||
| 0579430799 | |||
| 39ac68dac1 | |||
| 87fb44a5d3 | |||
| 5c76b406f7 | |||
| f665968daf | |||
| 5d1291c29d | |||
| ce7f400011 | |||
| 0fc8516b0c | |||
| 28b2a9b583 | |||
| e8549372fc | |||
| 5a434eb74e | |||
| 318de4db66 | |||
| a6c79bf15d | |||
| c6a10a755e | |||
| 1d7bd5bf4f | |||
| 94572e89c2 | |||
| fee7bac392 | |||
| dbb3b0f561 | |||
| b8bfed6a52 | |||
| 7215a89e7d | |||
| b7cca56091 | |||
| 7338e433f8 | |||
| 119ed07d5a | |||
| 5f98ae8323 | |||
| 8a598b43bb | |||
| e5dff4cfbe | |||
| 6c7a9cdd5f | |||
| 3b34f762fe | |||
| fa167ba747 |
@@ -1,19 +1,24 @@
|
||||
{
|
||||
"bitwise": true,
|
||||
"esversion": 6,
|
||||
"immed": true,
|
||||
"newcap": true,
|
||||
"noarg": true,
|
||||
"noempty": true,
|
||||
"nonew": true,
|
||||
"trailing": true,
|
||||
"maxlen": 200,
|
||||
"boss": true,
|
||||
"eqnull": true,
|
||||
"expr": true,
|
||||
"globalstrict": true,
|
||||
"laxbreak": true,
|
||||
"loopfunc": true,
|
||||
"strict": "global",
|
||||
"sub": true,
|
||||
"undef": true,
|
||||
"indent": 2
|
||||
"indent": 2,
|
||||
|
||||
"globals": {
|
||||
"ArrayBuffer": false,
|
||||
"Uint8Array": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
language: node_js
|
||||
sudo: false
|
||||
node_js:
|
||||
- '4.2'
|
||||
- '4.4'
|
||||
|
||||
cache:
|
||||
directories:
|
||||
@@ -19,6 +19,7 @@ env:
|
||||
- JOB=unit BROWSER_PROVIDER=saucelabs
|
||||
- JOB=docs-e2e BROWSER_PROVIDER=saucelabs
|
||||
- JOB=e2e TEST_TARGET=jqlite BROWSER_PROVIDER=saucelabs
|
||||
- JOB=e2e TEST_TARGET=jquery-2.1 BROWSER_PROVIDER=saucelabs
|
||||
- JOB=e2e TEST_TARGET=jquery BROWSER_PROVIDER=saucelabs
|
||||
global:
|
||||
- CXX=g++-4.8 # node 4 likes the G++ v4.8 compiler
|
||||
|
||||
@@ -1,3 +1,574 @@
|
||||
<a name="1.5.8"></a>
|
||||
# 1.5.8 arbitrary-fallbacks (2016-07-22)
|
||||
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **$animate:** do not get affected by custom, enumerable properties on `Object.prototype`
|
||||
([181e4401](https://github.com/angular/angular.js/commit/181e44019e850e5253378e29415cddf8d768bbef),
|
||||
[#14804](https://github.com/angular/angular.js/issues/14804), [#14830](https://github.com/angular/angular.js/issues/14830))
|
||||
- **$compile:** ensure `$doCheck` hooks can be defined in the controller constructor
|
||||
([3010ed4e](https://github.com/angular/angular.js/commit/3010ed4ee5c2c18b9848b5664639971b9fdc8919),
|
||||
[#14811](https://github.com/angular/angular.js/issues/14811))
|
||||
- **$injector:** fix class detection RegExp
|
||||
([4724d56c](https://github.com/angular/angular.js/commit/4724d56c939d02675433e7fe554608dff97ecf81),
|
||||
[#14533](https://github.com/angular/angular.js/issues/14533))
|
||||
- **$jsonpCallbacks:** do not overwrite callbacks added by other apps
|
||||
([1778d347](https://github.com/angular/angular.js/commit/1778d347cd3c91335e3840eaa49f1e387db8602f),
|
||||
[#14824](https://github.com/angular/angular.js/issues/14824))
|
||||
- **$timeout:** make $flush handle new $timeouts added in $timeout callbacks
|
||||
([1a387ba5](https://github.com/angular/angular.js/commit/1a387ba5dd1a8a831486fce23f6795bd1eef3d8b),
|
||||
[#5420](https://github.com/angular/angular.js/issues/5420), [#14686](https://github.com/angular/angular.js/issues/14686))
|
||||
- **copy:** fix handling of typed subarrays
|
||||
([1645924d](https://github.com/angular/angular.js/commit/1645924d49a7905ce55cced4c4276654970bb226),
|
||||
[#14842](https://github.com/angular/angular.js/issues/14842), [#14845](https://github.com/angular/angular.js/issues/14845))
|
||||
- **modules:** allow modules to be loaded in any order when using `angular-loader`
|
||||
([98e4a220](https://github.com/angular/angular.js/commit/98e4a220fe8301cec35498ae592adc0266f12437),
|
||||
[#9140](https://github.com/angular/angular.js/issues/9140), [#14794](https://github.com/angular/angular.js/issues/14794))
|
||||
- **ngAnimate:** allow removal of class that is scheduled to be added with requestAnimationFrame
|
||||
([7ccfe92b](https://github.com/angular/angular.js/commit/7ccfe92bed7361832f1b8d25b1a8411eb24d3fb5),
|
||||
[#14582](https://github.com/angular/angular.js/issues/14582))
|
||||
- **ngMocks:** allow `ErrorAddingDeclarationLocationStack` to be recognized as an `Error`
|
||||
([c6074dc3](https://github.com/angular/angular.js/commit/c6074dc34c31a07269bf7f628b971ef6dc805f17),
|
||||
[#13821](https://github.com/angular/angular.js/issues/13821), [#14344](https://github.com/angular/angular.js/issues/14344))
|
||||
- **ngOptions:** don't duplicate groups with falsy values
|
||||
([c3bfd7f5](https://github.com/angular/angular.js/commit/c3bfd7f59d0ecbf4ba3253fb407e683c7bb0766c))
|
||||
- **ngTransclude:**
|
||||
- ensure that fallback content is compiled and linked correctly
|
||||
([c405f88b](https://github.com/angular/angular.js/commit/c405f88bbc743f41591f6f3cfc022eea3c6c34ad),
|
||||
[#14787](https://github.com/angular/angular.js/issues/14787))
|
||||
- only compile fallback content if necessary
|
||||
([159a68ec](https://github.com/angular/angular.js/commit/159a68ec7ba77e9128b0d0516b813ed3d223b6e3),
|
||||
[#14768](https://github.com/angular/angular.js/issues/14768), [#14765](https://github.com/angular/angular.js/issues/14765), [#14775](https://github.com/angular/angular.js/issues/14775))
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- **$compile:** backport $doCheck
|
||||
([de59ca71](https://github.com/angular/angular.js/commit/de59ca71072eac95ee68de308f92bc5f921dd07b),
|
||||
[#14656](https://github.com/angular/angular.js/issues/14656))
|
||||
- **$jsonpCallbacks:** new service to abstract how JSONP callbacks are handled
|
||||
([a8cacfe9](https://github.com/angular/angular.js/commit/a8cacfe938287c54ce7099125cb735ad53f4c7c2),
|
||||
[#14795](https://github.com/angular/angular.js/issues/14795))
|
||||
- **$q:** implement $q.race
|
||||
([b9a56d58](https://github.com/angular/angular.js/commit/b9a56d588f8b597b1dff30d8e184b7c37d94cdcf),
|
||||
[#12929](https://github.com/angular/angular.js/issues/12929), [#14757](https://github.com/angular/angular.js/issues/14757))
|
||||
- **$resource:** pass the resource to a dynamic param functions
|
||||
([a126fcfe](https://github.com/angular/angular.js/commit/a126fcfee3bd8b02869bd2542c73e1eb21afe927),
|
||||
[#4899](https://github.com/angular/angular.js/issues/4899))
|
||||
- **$swipe:** add pointer support
|
||||
([f797f83c](https://github.com/angular/angular.js/commit/f797f83cd66f1fd11b3c9399e7894217ffa06c38),
|
||||
[#14061](https://github.com/angular/angular.js/issues/14061), [#14791](https://github.com/angular/angular.js/issues/14791))
|
||||
- **filterFilter:** allow overwriting the special `$` property name
|
||||
([33514ec3](https://github.com/angular/angular.js/commit/33514ec384d676d84b2a445bc15bae38c8c3ac8d),
|
||||
[#13313](https://github.com/angular/angular.js/issues/13313))
|
||||
|
||||
|
||||
## Performance Improvements
|
||||
|
||||
- **$compile:** wrap try/catch of collect comment directives into a function to avoid V8 deopt
|
||||
([acd45518](https://github.com/angular/angular.js/commit/acd455181de1cfa6b34d75f8d71a6c0b6995a777),
|
||||
[#14848](https://github.com/angular/angular.js/issues/14848))
|
||||
|
||||
|
||||
<a name="1.2.30"></a>
|
||||
# 1.2.30 patronal-resurrection (2016-07-21)
|
||||
|
||||
|
||||
_**Note:** This release contains some security fixes that required breaking changes. Since the
|
||||
legacy 1.2.x branch is the only version branch that supports IE8, it was necessary to introduce a
|
||||
couple of low-impact breaking changes in a patch release - something we generally avoid - in order
|
||||
to make the fixes available to people that still need IE8 support._
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **$compile:**
|
||||
- secure `link[href]` as a `RESOURCE_URL`s in `$sce`
|
||||
([f35f334b](https://github.com/angular/angular.js/commit/f35f334bd3197585bdf034f4b6d9ffa3122dac62),
|
||||
[#14687](https://github.com/angular/angular.js/issues/14687))
|
||||
- properly sanitize `xlink:href` attribute interoplation
|
||||
([f2fa1ed8](https://github.com/angular/angular.js/commit/f2fa1ed83d18d4e79a36f8c0db1c2524d762e513),
|
||||
[2687c261](https://github.com/angular/angular.js/commit/2687c26140585d9e3716f9f559390f5d8d598fdf))
|
||||
- **ngSanitize:** blacklist the attribute `usemap` as it can be used as a security exploit
|
||||
([ac0d5286](https://github.com/angular/angular.js/commit/ac0d5286b8931633d774080d6396fb4825d8be33),
|
||||
[#14903](https://github.com/angular/angular.js/issues/14903))
|
||||
- **ngAnimate:** do not use event.timeStamp anymore for time tracking
|
||||
([8d83b563](https://github.com/angular/angular.js/commit/8d83b5633471c847d58f337426fe069797dd49d9),
|
||||
[#13494](https://github.com/angular/angular.js/issues/13494), [#13495](https://github.com/angular/angular.js/issues/13495))
|
||||
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
- **$compile:** due to [f35f334b](https://github.com/angular/angular.js/commit/f35f334bd3197585bdf034f4b6d9ffa3122dac62),
|
||||
|
||||
`link[href]` attributes are now protected via `$sce`, which prevents interpolated values that fail
|
||||
the `RESOURCE_URL` context tests from being used in interpolation. For example if the application is
|
||||
running at `https://mydomain.org/` then the following will fail:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="{{ 'https://otherdomain.org/unsafe.css' }}" />
|
||||
```
|
||||
|
||||
By default, `RESOURCE_URL` safe URLs are only allowed from the same domain and protocol as the
|
||||
application document. To use URLs from other domains and/or protocols, you may either whitelist them
|
||||
using `$sceDelegateProvider.resourceUrlWhitelist(...)` or wrap them into a trusted value by calling
|
||||
`$sce.trustAsResourceUrl(url)`.
|
||||
|
||||
- **ngSanitize:** due to [234053fc](https://github.com/angular/angular.js/commit/234053fc9ad90e0d05be7e8359c6af66be94c094),
|
||||
|
||||
The `$sanitize` service will now remove instances of the `usemap` attribute from any elements passed
|
||||
to it.
|
||||
|
||||
This attribute is used to reference another element by `name` or `id`. Since the `name` and `id`
|
||||
attributes are already blacklisted, a sanitized `usemap` attribute could only reference unsanitized
|
||||
content, which is a security risk.
|
||||
|
||||
|
||||
<a name="1.5.7"></a>
|
||||
# 1.5.7 hexagonal-circumvolution (2016-06-15)
|
||||
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **$compile:**
|
||||
- don't add merged attributes twice to $attrs
|
||||
([ebbaa4ac](https://github.com/angular/angular.js/commit/ebbaa4ac5e3559df9267203438a0bb18c2d3b7d8),
|
||||
[#8159](https://github.com/angular/angular.js/issues/8159), [#14737](https://github.com/angular/angular.js/issues/14737))
|
||||
- cope with `$onChanges` hooks throwing
|
||||
([3749c858](https://github.com/angular/angular.js/commit/3749c85829406ca57cc5729e80696c7f34134068),
|
||||
[#14444](https://github.com/angular/angular.js/issues/14444), [#14463](https://github.com/angular/angular.js/issues/14463))
|
||||
- **$parse:** allow arguments to contain filter chains
|
||||
([cc6dcb4b](https://github.com/angular/angular.js/commit/cc6dcb4bc28aadff4f62d76d6451b0f80b928e69),
|
||||
[#4175](https://github.com/angular/angular.js/issues/4175), [#4168](https://github.com/angular/angular.js/issues/4168), [#14720](https://github.com/angular/angular.js/issues/14720))
|
||||
- **$routeProvider:** do not deep-copy route definition objects
|
||||
([6d0dcca6](https://github.com/angular/angular.js/commit/6d0dcca6f18a353a12c356dc96e05475d351b795),
|
||||
[#14478](https://github.com/angular/angular.js/issues/14478), [#14699](https://github.com/angular/angular.js/issues/14699), [#14750](https://github.com/angular/angular.js/issues/14750))
|
||||
- **input[email]:** improve email address validation
|
||||
([f423dc6b](https://github.com/angular/angular.js/commit/f423dc6b51a9b4a09728a287a2cabda7d45f192e),
|
||||
[#14719](https://github.com/angular/angular.js/issues/14719))
|
||||
- **ngMessages:** create new scope for ngMessage, clean it up correctly
|
||||
([56f33af8](https://github.com/angular/angular.js/commit/56f33af89045e2ec18d144d9d1ef73affbe51959),
|
||||
[#14307](https://github.com/angular/angular.js/issues/14307))
|
||||
- **ngMessagesInclude:** don't break on empty (or whitespace-only) templates
|
||||
([01b8772c](https://github.com/angular/angular.js/commit/01b8772cca55916376355a2ae58d3ab7832a4bc2),
|
||||
[#12941](https://github.com/angular/angular.js/issues/12941), [#14726](https://github.com/angular/angular.js/issues/14726))
|
||||
- **ngMock#$controller:** properly assign bindings to all types of controllers (e.g. class-based)
|
||||
([db58801a](https://github.com/angular/angular.js/commit/db58801a55c91df755414387dc00fee5902bb5f3),
|
||||
[#14437](https://github.com/angular/angular.js/issues/14437), [#14439](https://github.com/angular/angular.js/issues/14439))
|
||||
- **ngMockE2E:** allow $httpBackend.passThrough() to work when ngMock is loaded
|
||||
([6685043a](https://github.com/angular/angular.js/commit/6685043ad40acc50d7088f87e2a71f76d2265306),
|
||||
[#1434](https://github.com/angular/angular.js/issues/1434), [#13124](https://github.com/angular/angular.js/issues/13124))
|
||||
- **ngSanitize:** call attribute setter in linky for all links
|
||||
([c4fad6ad](https://github.com/angular/angular.js/commit/c4fad6ad617af025984ca401054f7b402aa28f1d),
|
||||
[#14707](https://github.com/angular/angular.js/issues/14707))
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- **limitTo:** add support for array-like objects
|
||||
([b03957f0](https://github.com/angular/angular.js/commit/b03957f0047fcfe996abc8efe97f9e2be2c2e13a),
|
||||
[#14657](https://github.com/angular/angular.js/issues/14657), [#14694](https://github.com/angular/angular.js/issues/14694))
|
||||
- **orderBy:** add support for custom comparators
|
||||
([2e33d170](https://github.com/angular/angular.js/commit/2e33d170be7eb72f40fd57a8f66631583572fbf2),
|
||||
[#13238](https://github.com/angular/angular.js/issues/13238), [#14455](https://github.com/angular/angular.js/issues/14455), [#5123](https://github.com/angular/angular.js/issues/5123), [#8112](https://github.com/angular/angular.js/issues/8112), [#10368](https://github.com/angular/angular.js/issues/10368), [#14468](https://github.com/angular/angular.js/issues/14468))
|
||||
|
||||
|
||||
<a name="1.5.6"></a>
|
||||
# 1.5.6 arrow-stringification (2016-05-27)
|
||||
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **$browser:** set the url even if the browser transforms it
|
||||
([743b33ab](https://github.com/angular/angular.js/commit/743b33ab9d8cdb8aa6ff28028a54ac0023f4dc15),
|
||||
[#14427](https://github.com/angular/angular.js/issues/14427), [#14499](https://github.com/angular/angular.js/issues/14499))
|
||||
- **$compile:**
|
||||
- properly bind context to linking functions for directives with `templateUrl`
|
||||
([5ae72dc7](https://github.com/angular/angular.js/commit/5ae72dc7affb252774b7c4cbf278269c5e178f38))
|
||||
- always use the DDO as `this` in pre-/post-linking functions
|
||||
([47bc98a1](https://github.com/angular/angular.js/commit/47bc98a1eafd7853babc1a7dc54f13363a3f4a43),
|
||||
[#9306](https://github.com/angular/angular.js/issues/9306))
|
||||
- don't run unnecessary update to one-way bindings
|
||||
([30479647](https://github.com/angular/angular.js/commit/304796471292f9805b9cf77e51aacc9cfbb09921),
|
||||
[#14546](https://github.com/angular/angular.js/issues/14546), [#14580](https://github.com/angular/angular.js/issues/14580))
|
||||
- removing unnecessary white space in element-transclusion comments
|
||||
([1116a96d](https://github.com/angular/angular.js/commit/1116a96df662c95aa83ab527c479b644a14532b6),
|
||||
[#14549](https://github.com/angular/angular.js/issues/14549), [#14550](https://github.com/angular/angular.js/issues/14550))
|
||||
- properly handle setting `srcset` to undefined
|
||||
([308f22ba](https://github.com/angular/angular.js/commit/308f22ba9a803967ce84c995d810990c80478f6f),
|
||||
[#14470](https://github.com/angular/angular.js/issues/14470), [#14493](https://github.com/angular/angular.js/issues/14493))
|
||||
- **$injector:**
|
||||
- add workaround for class stringification in Chrome v50/51
|
||||
([7550b7bd](https://github.com/angular/angular.js/commit/7550b7bda8d992cc6ed3c65b1f9f7e323a09cae4),
|
||||
[#14531](https://github.com/angular/angular.js/issues/14531))
|
||||
- add workaround for fat-arrow stringification in Chrome v50/51
|
||||
([bb730704](https://github.com/angular/angular.js/commit/bb730704f2f3dea4620e7b40083dcd65f208e2b2),
|
||||
[#14487](https://github.com/angular/angular.js/issues/14487), [#14495](https://github.com/angular/angular.js/issues/14495))
|
||||
- **$templateRequest:** trust empty templates in `$templateCache` as well
|
||||
([10955969](https://github.com/angular/angular.js/commit/10955969620248bd31a0bd37bc9fd7cc3b1f5e88),
|
||||
[#14479](https://github.com/angular/angular.js/issues/14479), [#14496](https://github.com/angular/angular.js/issues/14496))
|
||||
- **filters:** always call `splice()` with 2 arguments or more
|
||||
([6f1fae8a](https://github.com/angular/angular.js/commit/6f1fae8a916cc6f26725f64869f86fcd4991e819),
|
||||
[#14467](https://github.com/angular/angular.js/issues/14467), [#14489](https://github.com/angular/angular.js/issues/14489))
|
||||
- **ng-bind-html:** watch the unwrapped value using `$sce.valueOf()` (instead of `toString()`)
|
||||
([1c1c9b27](https://github.com/angular/angular.js/commit/1c1c9b27cbb57b3219d4c9765eeea8a11553d297),
|
||||
[#14526](https://github.com/angular/angular.js/issues/14526), [#14527](https://github.com/angular/angular.js/issues/14527))
|
||||
- **ngAnimate:**
|
||||
- don't break on anchored animations without duration
|
||||
([21b76aed](https://github.com/angular/angular.js/commit/21b76aed06d9cc04bd25a99c23ba852af782b357),
|
||||
[#14641](https://github.com/angular/angular.js/issues/14641), [#14645](https://github.com/angular/angular.js/issues/14645))
|
||||
- properly handle empty jqLite collections
|
||||
([9f480257](https://github.com/angular/angular.js/commit/9f4802573348401ee24090f815f3138fa17c161d),
|
||||
[#14558](https://github.com/angular/angular.js/issues/14558), [#14559](https://github.com/angular/angular.js/issues/14559))
|
||||
- **ngMessages:** do not compile ngMessagesInclude template if scope is destroyed
|
||||
([69d9072d](https://github.com/angular/angular.js/commit/69d9072d5b5aba988ac8a9717c92ce1fac465cbe),
|
||||
[#12695](https://github.com/angular/angular.js/issues/12695), [#14640](https://github.com/angular/angular.js/issues/14640))
|
||||
- **ngMock:**
|
||||
- match HTTP request regardless of the order of query params
|
||||
([363fb16c](https://github.com/angular/angular.js/commit/363fb16c1076a806bb31006daaf583ab5c82e467),
|
||||
[#12762](https://github.com/angular/angular.js/issues/12762))
|
||||
- pass eventHandlers to $httpBackend if passThrough is active
|
||||
([147650b3](https://github.com/angular/angular.js/commit/147650b3beae7ad8b556a930129887d187a0f5d3),
|
||||
[#14471](https://github.com/angular/angular.js/issues/14471))
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- **$compile:** support omitting required controller name if same as the local name
|
||||
([8965d57a](https://github.com/angular/angular.js/commit/8965d57aa0788c96859cf3cfa0f78b46d2cebedb),
|
||||
[#14513](https://github.com/angular/angular.js/issues/14513))
|
||||
- **$parse:** Add support for ES6 object initializers
|
||||
([1c9b9e24](https://github.com/angular/angular.js/commit/1c9b9e24ede7efc10ce4d53c6ab5528cc77e79d7))
|
||||
|
||||
|
||||
## Performance Improvements
|
||||
|
||||
- **$rootScope:** make queues more efficient
|
||||
([51b78f61](https://github.com/angular/angular.js/commit/51b78f61f954d94dc937f57190c9f881df9ab3ad),
|
||||
[#14545](https://github.com/angular/angular.js/issues/14545))
|
||||
- **ngAnimate:** listen for document visibility changes
|
||||
([b377d6b0](https://github.com/angular/angular.js/commit/b377d6b043db9c8da4e68493892f75a0f9759464),
|
||||
[#14568](https://github.com/angular/angular.js/issues/14568))
|
||||
- **ngClass:** improve even-odd checking
|
||||
([cf1b1755](https://github.com/angular/angular.js/commit/cf1b175508a72d4b7e3d64b8506903096c6ec25f))
|
||||
|
||||
|
||||
<a name="1.5.5"></a>
|
||||
# 1.5.5 material-conspiration (2016-04-18)
|
||||
|
||||
|
||||
## Reverts
|
||||
|
||||
- **$compile:** move setting of controller data to single location
|
||||
Reverted commit [21d148ae](https://github.com/angular/angular.js/commit/21d148aedc29c7efba4131ff2ef6383b4700868c)
|
||||
since it caused the Angular Material tabs directive to fail.
|
||||
|
||||
- **ngRoute:** allow `ngView` to be included in an asynchronously loaded template
|
||||
Eagerly loading `$route`, could break tests, because it might request the root or default route
|
||||
template (something `$httpBackend` would know nothing about).
|
||||
|
||||
It will be re-applied for `v1.6.x`, with a breaking change notice and possibly a way to disable
|
||||
the feature in tests.
|
||||
|
||||
([8237482d](https://github.com/angular/angular.js/commit/8237482d49e76e2c4994fe6207e3c9799ef04163),
|
||||
[#1213](https://github.com/angular/angular.js/issues/1213), [#6812](https://github.com/angular/angular.js/issues/6812),
|
||||
[#14088](https://github.com/angular/angular.js/issues/14088))
|
||||
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **$compile:**
|
||||
- do not use `noop()` as controller for multiple components
|
||||
([4c8aeefb](https://github.com/angular/angular.js/commit/4c8aeefb624de7436ad95f3cd525405e0c3f493e),
|
||||
[#14391](https://github.com/angular/angular.js/issues/14391), [#14402](https://github.com/angular/angular.js/issues/14402))
|
||||
- still trigger `$onChanges` even if the inner value already matches the new value
|
||||
([d9448dcb](https://github.com/angular/angular.js/commit/d9448dcb9f901ceb04deda1d5f3d5aac8442a718),
|
||||
[#14406](https://github.com/angular/angular.js/issues/14406))
|
||||
- handle boolean attributes in `@` bindings
|
||||
([499e1b2a](https://github.com/angular/angular.js/commit/499e1b2adf27f32d671123f8dceadb3df2ad84a9),
|
||||
[#14070](https://github.com/angular/angular.js/issues/14070))
|
||||
- don't throw if controller is named
|
||||
([e72990dc](https://github.com/angular/angular.js/commit/e72990dc3714c8b847185ddb64fd5fd00e5cceab))
|
||||
- ensure that `$onChanges` hook is called correctly
|
||||
([0ad2b708](https://github.com/angular/angular.js/commit/0ad2b70862d49ecc4355a16d767c0ca9358ecc3e),
|
||||
[#14355](https://github.com/angular/angular.js/issues/14355), [#14359](https://github.com/angular/angular.js/issues/14359))
|
||||
- **$injector:** ensure functions with overridden `toString()` are annotated properly
|
||||
([d384834f](https://github.com/angular/angular.js/commit/d384834fdee140a716298bd065f304f8fba4725e),
|
||||
[#14361](https://github.com/angular/angular.js/issues/14361))
|
||||
- **ngAnimate:**
|
||||
- remove event listeners only after all listeners have been called
|
||||
([79604f46](https://github.com/angular/angular.js/commit/79604f462899c118a99d610995083ff82d38aa35),
|
||||
[#14321](https://github.com/angular/angular.js/issues/14321))
|
||||
- fire callbacks when document is hidden
|
||||
([c7a92d2a](https://github.com/angular/angular.js/commit/c7a92d2a9a436dddd65de721c9837a93e915d939),
|
||||
[#14120](https://github.com/angular/angular.js/issues/14120))
|
||||
- fire callbacks in the correct order for certain skipped animations
|
||||
([90da3059](https://github.com/angular/angular.js/commit/90da3059cecfefaecf136b01cd87aee6775a8778))
|
||||
- **ngClass:** fix watching of an array expression containing an object
|
||||
([f975d8d4](https://github.com/angular/angular.js/commit/f975d8d4481e0b8cdba553f0e5ad9ec1688adae8),
|
||||
[#14405](https://github.com/angular/angular.js/issues/14405))
|
||||
- **ngMock:** fix collecting stack trace in `inject()` on IE10+, PhantomJS
|
||||
([e9c718a4](https://github.com/angular/angular.js/commit/e9c718a465d28b9f2691e3acab944f7c31aa9fb6),
|
||||
[#13591](https://github.com/angular/angular.js/issues/13591), [#13592](https://github.com/angular/angular.js/issues/13592), [#13593](https://github.com/angular/angular.js/issues/13593))
|
||||
- **ngOptions:** set select value when model matches disabled option
|
||||
([832eba5f](https://github.com/angular/angular.js/commit/832eba5fc952312e6b99127123e6e75bdf729006),
|
||||
[#12756](https://github.com/angular/angular.js/issues/12756))
|
||||
- **$http:** pass event object to `eventHandlers`/`uploadEventHandlers`
|
||||
([25d4e5cc](https://github.com/angular/angular.js/commit/25d4e5cca4fa615e49d65976223c6deb5b485b4c),
|
||||
[#14436](https://github.com/angular/angular.js/issues/14436))
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- **$compile:**
|
||||
- put custom annotations on DDO
|
||||
([f338e96c](https://github.com/angular/angular.js/commit/f338e96ccc739efc4b24022eae406c3d5451d422),
|
||||
[#14369](https://github.com/angular/angular.js/issues/14369), [#14279](https://github.com/angular/angular.js/issues/14279), [#14284](https://github.com/angular/angular.js/issues/14284))
|
||||
- add `isFirstChange()` method to onChanges object
|
||||
([8d43d8b8](https://github.com/angular/angular.js/commit/8d43d8b8e7aacf97ddb9aa48bff25db57249cdd5),
|
||||
[#14318](https://github.com/angular/angular.js/issues/14318), [#14323](https://github.com/angular/angular.js/issues/14323))
|
||||
- **$componentController:** provide isolated scope if none is passed (#14425)
|
||||
([33f817b9](https://github.com/angular/angular.js/commit/33f817b99cb20e566b381e7202235fe99b4a742a),
|
||||
[#14425](https://github.com/angular/angular.js/issues/14425))
|
||||
- **$http:**
|
||||
- support handling additional XHR events
|
||||
([01b18450](https://github.com/angular/angular.js/commit/01b18450882da9bb9c903d43c0daddbc03c2c35d) and
|
||||
[56c861c9](https://github.com/angular/angular.js/commit/56c861c9e114c45790865e5635eaae8d32eb649a),
|
||||
[#14367](https://github.com/angular/angular.js/issues/14367), [#11547](https://github.com/angular/angular.js/issues/11547), [#1934](https://github.com/angular/angular.js/issues/1934))
|
||||
- **$parse:** add the ability to define the identifier characters
|
||||
([3e7fa191](https://github.com/angular/angular.js/commit/3e7fa19197c54a764225ad27c0c0bf72263daa8d))
|
||||
- **ngAnimate:** let $animate.off() remove all listeners for an element
|
||||
([bf6cb8ab](https://github.com/angular/angular.js/commit/bf6cb8ab0d157083a1ed55743e3fffe728daa6f3))
|
||||
- **ngAria:** add support for aria-readonly based on ngReadonly
|
||||
([ec0baadc](https://github.com/angular/angular.js/commit/ec0baadcb68a4fa8da27d76b7e6a4e0840acd7fa),
|
||||
[#14140](https://github.com/angular/angular.js/issues/14140), [#14077](https://github.com/angular/angular.js/issues/14077))
|
||||
- **ngParseExt:** new ngParseExt module
|
||||
([d08f5c69](https://github.com/angular/angular.js/commit/d08f5c698624f6243685b16f2d458cb9a980ebde))
|
||||
|
||||
|
||||
## Performance Improvements
|
||||
|
||||
- **$compile:** use createMap() for directive bindings to allow fast `forEach`
|
||||
([c115b37c](https://github.com/angular/angular.js/commit/c115b37c336f3a5936187279057b29c76078caf2),
|
||||
[#12529](https://github.com/angular/angular.js/issues/12529))
|
||||
- **ngOptions:** use `documentFragment` to populate `select` options
|
||||
([6a4124d0](https://github.com/angular/angular.js/commit/6a4124d0fb17cd7fc0e8bf5a1ca4d785a1d11c1c),
|
||||
[#13607](https://github.com/angular/angular.js/issues/13607), [#13239](https://github.com/angular/angular.js/issues/13239), [#12076](https://github.com/angular/angular.js/issues/12076))
|
||||
|
||||
|
||||
<a name="1.5.4"></a>
|
||||
# 1.5.4 graduated-sophistry (2016-04-14)
|
||||
|
||||
This was a partially published release that you should ignore.
|
||||
|
||||
<a name="1.5.3"></a>
|
||||
# 1.5.3 diplohaplontic-meiosis (2016-03-25)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **$compile:** workaround a GC bug in Chrome < 50
|
||||
([513199ee](https://github.com/angular/angular.js/commit/513199ee9f1c8eef1240983d6e52c824404adb98),
|
||||
[#14041](https://github.com/angular/angular.js/issues/14041), [#14286](https://github.com/angular/angular.js/issues/14286))
|
||||
- **$sniffer:** fix history sniffing in Chrome Packaged Apps
|
||||
([457fd21a](https://github.com/angular/angular.js/commit/457fd21a1a0c10c66245c32a73602f3a09038bda),
|
||||
[#11932](https://github.com/angular/angular.js/issues/11932), [#13945](https://github.com/angular/angular.js/issues/13945))
|
||||
- **formatNumber:** handle small numbers correctly when `gSize` !== `lgSize`
|
||||
([3277b885](https://github.com/angular/angular.js/commit/3277b885c4dec3edd51b8e8c3d1776057d6d4d1d),
|
||||
[#14289](https://github.com/angular/angular.js/issues/14289), [#14290](https://github.com/angular/angular.js/issues/14290))
|
||||
- **ngAnimate:** run structural animations with cancelled out class changes
|
||||
([c7813e9e](https://github.com/angular/angular.js/commit/c7813e9ebf793fe89380dcad54e8e002fafdd985),
|
||||
[#14249](https://github.com/angular/angular.js/issues/14249))
|
||||
- **ngMessages:** don't crash when nested messages are removed
|
||||
([ef91b04c](https://github.com/angular/angular.js/commit/ef91b04cdd794f308617bca7ebd0b1b747e4f7de),
|
||||
[#14183](https://github.com/angular/angular.js/issues/14183), [#14242](https://github.com/angular/angular.js/issues/14242))
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- **$compile:** add more lifecycle hooks to directive controllers
|
||||
([9cd9956d](https://github.com/angular/angular.js/commit/9cd9956dcbc8382e8e8757a805398bd251bbc67e),
|
||||
[#14127](https://github.com/angular/angular.js/issues/14127), [#14030](https://github.com/angular/angular.js/issues/14030), [#14020](https://github.com/angular/angular.js/issues/14020), [#13991](https://github.com/angular/angular.js/issues/13991), [#14302](https://github.com/angular/angular.js/issues/14302))
|
||||
|
||||
|
||||
|
||||
<a name="1.5.2"></a>
|
||||
# 1.5.2 differential-recovery (2016-03-18)
|
||||
|
||||
This release reverts a breaking change that accidentally made it into the 1.5.1 release. See
|
||||
[fee7bac3](https://github.com/angular/angular.js/commit/fee7bac392db24b6006d6a57ba71526f3afa102c)
|
||||
for more info.
|
||||
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **ngAnimate.$animate:** remove animation callbacks when the element is removed
|
||||
([ce7f4000](https://github.com/angular/angular.js/commit/ce7f400011e1e2e1b9316f18ce87b87b79d878b4))
|
||||
|
||||
|
||||
<a name="1.5.1"></a>
|
||||
# 1.5.1 equivocal-sophistication (2016-03-16)
|
||||
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **core:** only call `console.log` when `window.console` exists
|
||||
([ce138f3c](https://github.com/angular/angular.js/commit/ce138f3c552f8bf741721ab8d10994ed35a4b2f5),
|
||||
[#14006](https://github.com/angular/angular.js/issues/14006), [#14007](https://github.com/angular/angular.js/issues/14007), [#14047](https://github.com/angular/angular.js/issues/14047))
|
||||
- **$compile:** allow directives to have decorators
|
||||
([0728cc2f](https://github.com/angular/angular.js/commit/0728cc2f2bb04d5dbdfca41f3afacea16c75ee07))
|
||||
- **$resource:** fix parse errors on older Android WebViews
|
||||
([df8db7b4](https://github.com/angular/angular.js/commit/df8db7b446b5bae83afef457d706d2805e597f29),
|
||||
[#13989](https://github.com/angular/angular.js/issues/13989))
|
||||
- **$routeProvider:** properly handle optional eager path named groups
|
||||
([c0797c68](https://github.com/angular/angular.js/commit/c0797c68866c9ef8ff3c2f6985e6eb9374346151),
|
||||
[#14011](https://github.com/angular/angular.js/issues/14011))
|
||||
- **copy:** add support for copying `Blob` objects
|
||||
([e9d579b6](https://github.com/angular/angular.js/commit/e9d579b608c2be8fdcf0326d0679a76bb9ae5b6e),
|
||||
[#9669](https://github.com/angular/angular.js/issues/9669), [#14064](https://github.com/angular/angular.js/issues/14064))
|
||||
- **dateFilter:** correctly format BC years
|
||||
([e36205f5](https://github.com/angular/angular.js/commit/e36205f5af82b69362def7d2b6eeeb038f592311))
|
||||
- **formatNumber:** allow negative fraction size
|
||||
([e046c170](https://github.com/angular/angular.js/commit/e046c170bcf677f26e61af6470cb5fd2f751c969),
|
||||
[#13913](https://github.com/angular/angular.js/issues/13913))
|
||||
- **input:** re-validate when partially editing date-family inputs
|
||||
([e383804c](https://github.com/angular/angular.js/commit/e383804c4ab62278fbaf4fdfaa03caeacff77fc4),
|
||||
[#12207](https://github.com/angular/angular.js/issues/12207), [#13886](https://github.com/angular/angular.js/issues/13886))
|
||||
- **input\[date\]:** support years with more than 4 digits
|
||||
([d76951f1](https://github.com/angular/angular.js/commit/d76951f1747abd2da6e320d4ff9019f170d9793f),
|
||||
[#13735](https://github.com/angular/angular.js/issues/13735), [#13905](https://github.com/angular/angular.js/issues/13905))
|
||||
- **ngOptions:** always set the 'selected' attribute for selected options
|
||||
([9f5a1722](https://github.com/angular/angular.js/commit/9f5a172291ff6926dcd246f0972288916a4c9bf6),
|
||||
[#14115](https://github.com/angular/angular.js/issues/14115))
|
||||
- **ngRoute:** allow `ngView` to be included in an asynchronously loaded template
|
||||
([8237482d](https://github.com/angular/angular.js/commit/8237482d49e76e2c4994fe6207e3c9799ef04163),
|
||||
[#1213](https://github.com/angular/angular.js/issues/1213), [#6812](https://github.com/angular/angular.js/issues/6812), [#14088](https://github.com/angular/angular.js/issues/14088))
|
||||
- **ngMock:**
|
||||
- attach `$injector` to `$rootElement` and prevent memory leak due to attached data
|
||||
([75373dd4](https://github.com/angular/angular.js/commit/75373dd4bdae6c6035272942c69444c386f824cd),
|
||||
[#14022](https://github.com/angular/angular.js/issues/14022), [#14094](https://github.com/angular/angular.js/issues/14094), [#14098](https://github.com/angular/angular.js/issues/14098))
|
||||
- don't break if `$rootScope.$destroy()` is not a function
|
||||
([50ed8712](https://github.com/angular/angular.js/commit/50ed8712566d601c9fb76b71f7b534b5bc803a36),
|
||||
[#14106](https://github.com/angular/angular.js/issues/14106), [#14107](https://github.com/angular/angular.js/issues/14107))
|
||||
- **ngMockE2E:** pass `responseType` to `$delegate` when using `passThrough`
|
||||
([d16faf9f](https://github.com/angular/angular.js/commit/d16faf9f2b9bd2b85d95e71d902cec0269282f2c),
|
||||
[#5415](https://github.com/angular/angular.js/issues/5415), [#5783](https://github.com/angular/angular.js/issues/5783))
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- **$compile:** add custom annotations to the controller
|
||||
([0c800930](https://github.com/angular/angular.js/commit/0c8009300b819c39c5e4892856724a731a8dcda6),
|
||||
[#14114](https://github.com/angular/angular.js/issues/14114))
|
||||
- **$controllerProvider:** add a `has()` method for checking the existence of a controller
|
||||
([bb9575db](https://github.com/angular/angular.js/commit/bb9575dbd3428176216355df7b2933d2a72783cd),
|
||||
[#13951](https://github.com/angular/angular.js/issues/13951), [#14109](https://github.com/angular/angular.js/issues/14109))
|
||||
- **dateFilter:** add support for STANDALONEMONTH in format (`LLLL`)
|
||||
([3e5b25b3](https://github.com/angular/angular.js/commit/3e5b25b33f278376def432698c704b1807fdb8c0),
|
||||
[#13999](https://github.com/angular/angular.js/issues/13999), [#14013](https://github.com/angular/angular.js/issues/14013))
|
||||
- **ngMock:** add `sharedInjector()` to `angular.mock.module`
|
||||
([a46ab60f](https://github.com/angular/angular.js/commit/a46ab60fd5bf94896f0761e858ef38b998eb0f80),
|
||||
[#14093](https://github.com/angular/angular.js/issues/14093), [#10238](https://github.com/angular/angular.js/issues/10238))
|
||||
|
||||
|
||||
## Performance Improvements
|
||||
|
||||
- **ngRepeat:** avoid duplicate jqLite wrappers
|
||||
([632e15a3](https://github.com/angular/angular.js/commit/632e15a3afdcd30168700cec1367bd81966400d4))
|
||||
- **ngAnimate:**
|
||||
- avoid jqLite/jQuery for upward DOM traversal
|
||||
([35251bd4](https://github.com/angular/angular.js/commit/35251bd4ce23251b5e9a2860cf414726c194721e))
|
||||
- avoid `$.fn.data` overhead with jQuery
|
||||
([15915e60](https://github.com/angular/angular.js/commit/15915e606fdf5114592db1a0a5e3f12e639d7cdb))
|
||||
|
||||
|
||||
<a name="1.4.10"></a>
|
||||
# 1.4.10 benignant-oscillation (2016-03-16)
|
||||
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **core:** only call `console.log` when `window.console` exists
|
||||
([beb00e44](https://github.com/angular/angular.js/commit/beb00e44de947981dbe35d5cf7a116e10ea8dc67),
|
||||
[#14006](https://github.com/angular/angular.js/issues/14006), [#14007](https://github.com/angular/angular.js/issues/14007), [#14047](https://github.com/angular/angular.js/issues/14047))
|
||||
- **$animateCss:** cancel fallback timeout when animation ends normally
|
||||
([a60bbc12](https://github.com/angular/angular.js/commit/a60bbc12e8c5170e70d95f1b2c3e309b3b95cb84),
|
||||
[#13787](https://github.com/angular/angular.js/issues/13787))
|
||||
- **$compile:**
|
||||
- allow directives to have decorators
|
||||
([77cdc37c](https://github.com/angular/angular.js/commit/77cdc37c65491b551fcf01a18ab848a693c293d7))
|
||||
- properly denormalize templates when only one of the start/end symbols is different
|
||||
([2d44a681](https://github.com/angular/angular.js/commit/2d44a681eb912a81a8bc8e16a278c45dae91fa24),
|
||||
[#13848](https://github.com/angular/angular.js/issues/13848))
|
||||
- handle boolean attributes in `@` bindings
|
||||
([2ffbfb0a](https://github.com/angular/angular.js/commit/2ffbfb0ad0647d103ff339ee4b772b62d4823bf3),
|
||||
[#13767](https://github.com/angular/angular.js/issues/13767), [#13769](https://github.com/angular/angular.js/issues/13769))
|
||||
- **$parse:**
|
||||
- prevent assignment on constructor properties
|
||||
([f47e2180](https://github.com/angular/angular.js/commit/f47e218006029f39b4785d820b430de3a0eebcb0),
|
||||
[#13417](https://github.com/angular/angular.js/issues/13417))
|
||||
- preserve expensive checks when runnning `$eval` inside an expression
|
||||
([96d62cc0](https://github.com/angular/angular.js/commit/96d62cc0fc77248d7e3ec4aa458bac0d3e072629))
|
||||
- copy `inputs` for expressions with expensive checks
|
||||
([0b7fff30](https://github.com/angular/angular.js/commit/0b7fff303f46202bbae1ff3ca9d0e5fa76e0fc9a))
|
||||
- **$rootScope:** set no context when calling helper functions for `$watch`
|
||||
([ab5c7698](https://github.com/angular/angular.js/commit/ab5c7698bb106669ca31b5f79a95afa54d65c5f1))
|
||||
- **$route:** allow preventing a route reload
|
||||
([4bc30314](https://github.com/angular/angular.js/commit/4bc3031497447ad527356f12bd0ceee1d7d09db5),
|
||||
[#9824](https://github.com/angular/angular.js/issues/9824), [#13894](https://github.com/angular/angular.js/issues/13894))
|
||||
- **$routeProvider:** properly handle optional eager path named groups
|
||||
([6a4403a1](https://github.com/angular/angular.js/commit/6a4403a11845173d6a96232f77d73aa544b182af),
|
||||
[#14011](https://github.com/angular/angular.js/issues/14011))
|
||||
- **copy:** add support for copying `Blob` objects
|
||||
([863a4232](https://github.com/angular/angular.js/commit/863a4232a6faa92428df45cd54d5a519be2434de),
|
||||
[#9669](https://github.com/angular/angular.js/issues/9669), [#14064](https://github.com/angular/angular.js/issues/14064))
|
||||
- **dateFilter:** follow the CLDR on pattern escape sequences
|
||||
([f476060d](https://github.com/angular/angular.js/commit/f476060de6cc016380c0343490a184543f853652),
|
||||
[#12839](https://github.com/angular/angular.js/issues/12839))
|
||||
- **dateFilter, input:** fix Date parsing in IE/Edge when timezone offset contains `:`
|
||||
([571afd65](https://github.com/angular/angular.js/commit/571afd6558786d7b99e2aebd307b4a94c9f2bb87),
|
||||
[#13880](https://github.com/angular/angular.js/issues/13880), [#13887](https://github.com/angular/angular.js/issues/13887))
|
||||
- **input:** re-validate when partially editing date-family inputs
|
||||
([02929f82](https://github.com/angular/angular.js/commit/02929f82f30449301ff18fea84a6396a017683b1),
|
||||
[#12207](https://github.com/angular/angular.js/issues/12207), [#13886](https://github.com/angular/angular.js/issues/13886))
|
||||
- **select:** handle corner case of adding options via a custom directive
|
||||
([df6e7315](https://github.com/angular/angular.js/commit/df6e731506831a3dc7f44c9a90abe17515450b3e),
|
||||
[#13874](https://github.com/angular/angular.js/issues/13874), [#13878](https://github.com/angular/angular.js/issues/13878))
|
||||
- **ngOptions:** always set the 'selected' attribute for selected options
|
||||
([f87e8288](https://github.com/angular/angular.js/commit/f87e8288fb69526fd240a66a046f5de52ed204de),
|
||||
[#14115](https://github.com/angular/angular.js/issues/14115))
|
||||
- **ngAnimate:** properly cancel previously running class-based animations
|
||||
([3b27dd37](https://github.com/angular/angular.js/commit/3b27dd37a2cc8a52992784ece6b371023dadf792),
|
||||
[#10156](https://github.com/angular/angular.js/issues/10156), [#13822](https://github.com/angular/angular.js/issues/13822))
|
||||
- **ngAnimateChildren:** make it compatible with `ngIf`
|
||||
([dc158e7e](https://github.com/angular/angular.js/commit/dc158e7e40624ef94c66560386522ef7e991a9ce),
|
||||
[#13865](https://github.com/angular/angular.js/issues/13865), [#13876](https://github.com/angular/angular.js/issues/13876))
|
||||
- **ngMockE2E:** pass `responseType` to `$delegate` when using `passThrough`
|
||||
([947cb4d1](https://github.com/angular/angular.js/commit/947cb4d1451afa4f5090a693df5b1968dd0df70c),
|
||||
[#5415](https://github.com/angular/angular.js/issues/5415), [#5783](https://github.com/angular/angular.js/issues/5783))
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- **$locale:** Include original locale ID in $locale
|
||||
([e69f3550](https://github.com/angular/angular.js/commit/e69f35507e10c994708ce4f1efba7573951d1acd),
|
||||
[#13390](https://github.com/angular/angular.js/issues/13390))
|
||||
- **ngAnimate:** provide ng-[event]-prepare class for structural animations
|
||||
([796f7ab4](https://github.com/angular/angular.js/commit/796f7ab41487e124b5b0c02dbf0a03bd581bf073))
|
||||
|
||||
|
||||
## Performance Improvements
|
||||
|
||||
- **$compile:** avoid needless overhead when wrapping text nodes
|
||||
([946d9ae9](https://github.com/angular/angular.js/commit/946d9ae90bb31fe911ebbe1b80cd4c8af5a665c6))
|
||||
- **ngRepeat:** avoid duplicate jqLite wrappers
|
||||
([d04c38c4](https://github.com/angular/angular.js/commit/d04c38c48968db777c3ea6a177ce2ff0116df7b4))
|
||||
- **ngAnimate:**
|
||||
- avoid jqLite/jQuery for upward DOM traversal
|
||||
([ab95ba65](https://github.com/angular/angular.js/commit/ab95ba65c08b38cace83de6717b7681079182b45))
|
||||
- avoid `$.fn.data` overhead with jQuery
|
||||
([86416bcb](https://github.com/angular/angular.js/commit/86416bcbee2192fa31c017163c5d856763182ade))
|
||||
|
||||
|
||||
<a name="1.5.0"></a>
|
||||
# 1.5.0 ennoblement-facilitation (2016-02-05)
|
||||
|
||||
@@ -33,6 +604,9 @@
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
### Upgrade to 1.5.1
|
||||
This version of AngularJS is problematic due to a issue during its release. Please upgrade to version [1.5.2](#1.5.2).
|
||||
|
||||
- **ngAria:** due to [d06431e5](https://github.com/angular/angular.js/commit/d06431e5309bb0125588877451dc79b935808134),
|
||||
Where appropriate, ngAria now applies ARIA to custom controls only, not native inputs. Because of this, support for `aria-multiline` on textareas has been removed.
|
||||
|
||||
@@ -113,7 +687,8 @@ changes section for more information
|
||||
|
||||
- **ngSanitize:** due to [234053fc](https://github.com/angular/angular.js/commit/234053fc9ad90e0d05be7e8359c6af66be94c094),
|
||||
|
||||
The `$sanitize` service will now remove instances of the `usemap` attribute from any elements passed to it.
|
||||
The `$sanitize` service will now remove instances of the `usemap` attribute from any elements passed
|
||||
to it.
|
||||
|
||||
This attribute is used to reference another element by `name` or `id`. Since the `name` and `id`
|
||||
attributes are already blacklisted, a sanitized `usemap` attribute could only reference unsanitized
|
||||
@@ -689,9 +1264,6 @@ is `$locals`.
|
||||
[#13236](https://github.com/angular/angular.js/issues/13236))
|
||||
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
|
||||
<a name="1.5.0-beta.2"></a>
|
||||
# 1.5.0-beta.2 effective-delegation (2015-11-17)
|
||||
|
||||
@@ -1011,9 +1583,6 @@ requirement more strict and alerts the developer explicitly.
|
||||
[#2318](https://github.com/angular/angular.js/issues/2318), [#9319](https://github.com/angular/angular.js/issues/9319), [#12159](https://github.com/angular/angular.js/issues/12159))
|
||||
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
|
||||
<a name="1.3.20"></a>
|
||||
# 1.3.20 shallow-translucence (2015-09-29)
|
||||
|
||||
@@ -1024,9 +1593,6 @@ requirement more strict and alerts the developer explicitly.
|
||||
([d434f3db](https://github.com/angular/angular.js/commit/d434f3db53d6209eb140b904e83bbde401686c16))
|
||||
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
|
||||
<a name="1.2.29"></a>
|
||||
# 1.2.29 ultimate-deprecation (2015-09-29)
|
||||
|
||||
@@ -1053,7 +1619,15 @@ requirement more strict and alerts the developer explicitly.
|
||||
[#9936](https://github.com/angular/angular.js/issues/9936))
|
||||
|
||||
|
||||
## Breaking Changes
|
||||
<a name="1.5.0-beta.0"></a>
|
||||
|
||||
# 1.5.0-beta.0 intialization-processation (2015-09-17)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **jqLite:**: properly handle dash-delimited node names in `jqLiteBuildFragment`
|
||||
([cdd1227a3](https://github.com/angular/angular.js/commit/cdd1227a308edd34d31b67f338083b6e0c4c0db9),
|
||||
[#10617](https://github.com/angular/angular.js/issues/10617))
|
||||
|
||||
|
||||
<a name="1.4.6"></a>
|
||||
@@ -1606,6 +2180,44 @@ describe('$q.when', function() {
|
||||
[#11580](https://github.com/angular/angular.js/issues/11580), [#12234](https://github.com/angular/angular.js/issues/12234))
|
||||
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
- **ngInclude:** due to [3c6e8ce044446735eb2e70d0061db8c6db050289](https://github.com/angular/angular.js/commit/3c6e8ce044446735eb2e70d0061db8c6db050289), the `src` attribute of ngInclude no longer accepts an
|
||||
expression that returns the result of `$sce.trustAsResourceUrl`. This will now cause an infinite digest:
|
||||
|
||||
Before:
|
||||
```html
|
||||
<div ng-include="findTemplate('https://example.com/myTemplate.html')"></div>
|
||||
```
|
||||
|
||||
```js
|
||||
$scope.findTemplate = function(templateName) {
|
||||
return $sce.trustAsResourceUrl(templateName);
|
||||
};
|
||||
```
|
||||
|
||||
To migrate, either cache the result of `trustAsResourceUrl()`, or put the template url in the resource
|
||||
whitelist in the `config()` function:
|
||||
|
||||
After:
|
||||
|
||||
```js
|
||||
var templateCache = {};
|
||||
$scope.findTemplate = function(templateName) {
|
||||
if (!templateCache[templateName]) {
|
||||
templateCache[templateName] = $sce.trustAsResourceUrl(templateName);
|
||||
}
|
||||
|
||||
return templateCache[templateName];
|
||||
};
|
||||
|
||||
// Alternatively, use `$sceDelegateProvider.resourceUrlWhitelist()`:
|
||||
|
||||
angular.module('myApp', []).config(function($sceDelegateProvider) {
|
||||
$sceDelegateProvider.resourceUrlWhitelist(['self', 'https://example.com/**'])
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
<a name="1.3.17"></a>
|
||||
# 1.3.17 tsktskskly-euouae (2015-07-06)
|
||||
@@ -1884,9 +2496,7 @@ describe('$q.when', function() {
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
### ngAnimate
|
||||
|
||||
- **$animateCss:** due to [d5683d21](https://github.com/angular/angular.js/commit/d5683d21165e725bc5a850e795f681b0a8a008f5),
|
||||
- **ngAnimate** - $animateCss: due to [d5683d21](https://github.com/angular/angular.js/commit/d5683d21165e725bc5a850e795f681b0a8a008f5),
|
||||
The $animateCss service will now always return an
|
||||
object even if the animation is not set to run. If your code is using
|
||||
$animateCss then please consider the following code change:
|
||||
@@ -1970,9 +2580,7 @@ $animateProvider.classNameFilter(/ng-animate-special/);
|
||||
```
|
||||
|
||||
|
||||
### ngOptions
|
||||
|
||||
- ** due to [dfa722a8](https://github.com/angular/angular.js/commit/dfa722a8a6864793fd9580d8ae704a06d10b5509),
|
||||
- **ngOptions**: due to [dfa722a8](https://github.com/angular/angular.js/commit/dfa722a8a6864793fd9580d8ae704a06d10b5509),
|
||||
|
||||
|
||||
Although it is unlikely that anyone is using it in this way, this change does change the
|
||||
|
||||
@@ -280,7 +280,7 @@ You can find out more detailed information about contributing in the
|
||||
[groups]: https://groups.google.com/forum/?fromgroups#!forum/angular
|
||||
[individual-cla]: http://code.google.com/legal/individual-cla-v1.0.html
|
||||
[irc]: http://webchat.freenode.net/?channels=angularjs&uio=d4
|
||||
[js-style-guide]: http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml
|
||||
[js-style-guide]: https://google.github.io/styleguide/javascriptguide.xml
|
||||
[jsfiddle]: http://jsfiddle.net/
|
||||
[list]: https://groups.google.com/forum/?fromgroups#!forum/angular
|
||||
[ngDocs]: https://github.com/angular/angular.js/wiki/Writing-AngularJS-Documentation
|
||||
|
||||
@@ -17,6 +17,10 @@ module.exports = function(grunt) {
|
||||
NG_VERSION.cdn = versionInfo.cdnVersion;
|
||||
var dist = 'angular-'+ NG_VERSION.full;
|
||||
|
||||
if (versionInfo.cdnVersion == null) {
|
||||
throw new Error('Unable to read CDN version, are you offline or has the CDN not been properly pushed?');
|
||||
}
|
||||
|
||||
//config
|
||||
grunt.initConfig({
|
||||
NG_VERSION: NG_VERSION,
|
||||
@@ -79,6 +83,8 @@ module.exports = function(grunt) {
|
||||
tests: {
|
||||
jqlite: 'karma-jqlite.conf.js',
|
||||
jquery: 'karma-jquery.conf.js',
|
||||
'jquery-2.2': 'karma-jquery-2.2.conf.js',
|
||||
'jquery-2.1': 'karma-jquery-2.1.conf.js',
|
||||
docs: 'karma-docs.conf.js',
|
||||
modules: 'karma-modules.conf.js'
|
||||
},
|
||||
@@ -87,6 +93,8 @@ module.exports = function(grunt) {
|
||||
autotest: {
|
||||
jqlite: 'karma-jqlite.conf.js',
|
||||
jquery: 'karma-jquery.conf.js',
|
||||
'jquery-2.2': 'karma-jquery-2.2.conf.js',
|
||||
'jquery-2.1': 'karma-jquery-2.1.conf.js',
|
||||
modules: 'karma-modules.conf.js',
|
||||
docs: 'karma-docs.conf.js'
|
||||
},
|
||||
@@ -135,6 +143,9 @@ module.exports = function(grunt) {
|
||||
ngMock: {
|
||||
files: { src: 'src/ngMock/**/*.js' },
|
||||
},
|
||||
ngParseExt: {
|
||||
files: { src: 'src/ngParseExt/**/*.js' },
|
||||
},
|
||||
ngResource: {
|
||||
files: { src: 'src/ngResource/**/*.js' },
|
||||
},
|
||||
@@ -231,7 +242,11 @@ module.exports = function(grunt) {
|
||||
dest: 'build/angular-aria.js',
|
||||
src: util.wrap(files['angularModules']['ngAria'], 'module')
|
||||
},
|
||||
'promises-aplus-adapter': {
|
||||
parseext: {
|
||||
dest: 'build/angular-parse-ext.js',
|
||||
src: util.wrap(files['angularModules']['ngParseExt'], 'module')
|
||||
},
|
||||
"promises-aplus-adapter": {
|
||||
dest:'tmp/promises-aplus-adapter++.js',
|
||||
src:['src/ng/q.js', 'lib/promises-aplus/promises-aplus-test-adapter.js']
|
||||
}
|
||||
@@ -249,7 +264,8 @@ module.exports = function(grunt) {
|
||||
resource: 'build/angular-resource.js',
|
||||
route: 'build/angular-route.js',
|
||||
sanitize: 'build/angular-sanitize.js',
|
||||
aria: 'build/angular-aria.js'
|
||||
aria: 'build/angular-aria.js',
|
||||
parseext: 'build/angular-parse-ext.js'
|
||||
},
|
||||
|
||||
|
||||
@@ -264,12 +280,17 @@ module.exports = function(grunt) {
|
||||
],
|
||||
options: {
|
||||
disallowed: [
|
||||
'fit',
|
||||
'iit',
|
||||
'xit',
|
||||
'fthey',
|
||||
'tthey',
|
||||
'xthey',
|
||||
'fdescribe',
|
||||
'ddescribe',
|
||||
'xdescribe'
|
||||
'xdescribe',
|
||||
'it.only',
|
||||
'describe.only'
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -344,10 +365,12 @@ module.exports = function(grunt) {
|
||||
//alias tasks
|
||||
grunt.registerTask('test', 'Run unit, docs and e2e tests with Karma', ['jshint', 'jscs', 'package', 'test:unit', 'test:promises-aplus', 'tests:docs', 'test:protractor']);
|
||||
grunt.registerTask('test:jqlite', 'Run the unit tests with Karma' , ['tests:jqlite']);
|
||||
grunt.registerTask('test:jquery', 'Run the jQuery unit tests with Karma', ['tests:jquery']);
|
||||
grunt.registerTask('test:jquery', 'Run the jQuery (latest) unit tests with Karma', ['tests:jquery']);
|
||||
grunt.registerTask('test:jquery-2.2', 'Run the jQuery 2.2 unit tests with Karma', ['tests:jquery-2.2']);
|
||||
grunt.registerTask('test:jquery-2.1', 'Run the jQuery 2.1 unit tests with Karma', ['tests:jquery-2.1']);
|
||||
grunt.registerTask('test:modules', 'Run the Karma module tests with Karma', ['build', 'tests:modules']);
|
||||
grunt.registerTask('test:docs', 'Run the doc-page tests with Karma', ['package', 'tests:docs']);
|
||||
grunt.registerTask('test:unit', 'Run unit, jQuery and Karma module tests with Karma', ['test:jqlite', 'test:jquery', 'test:modules']);
|
||||
grunt.registerTask('test:unit', 'Run unit, jQuery and Karma module tests with Karma', ['test:jqlite', 'test:jquery', 'test:jquery-2.2', 'test:jquery-2.1', 'test:modules']);
|
||||
grunt.registerTask('test:protractor', 'Run the end to end tests with Protractor and keep a test server running in the background', ['webdriver', 'connect:testserver', 'protractor:normal']);
|
||||
grunt.registerTask('test:travis-protractor', 'Run the end to end tests with Protractor for Travis CI builds', ['connect:testserver', 'protractor:travis']);
|
||||
grunt.registerTask('test:ci-protractor', 'Run the end to end tests with Protractor for Jenkins CI builds', ['webdriver', 'connect:testserver', 'protractor:jenkins']);
|
||||
|
||||
@@ -8,20 +8,22 @@ synchronizes data from your UI (view) with your JavaScript objects (model) throu
|
||||
binding. To help you structure your application better and make it easy to test, AngularJS teaches
|
||||
the browser how to do dependency injection and inversion of control.
|
||||
|
||||
Oh yeah and it helps with server-side communication, taming async callbacks with promises and
|
||||
deferreds. It also makes client-side navigation and deeplinking with hashbang urls or HTML5 pushState a
|
||||
piece of cake. Best of all?? It makes development fun!
|
||||
It also helps with server-side communication, taming async callbacks with promises and deferreds,
|
||||
and it makes client-side navigation and deeplinking with hashbang urls or HTML5 pushState a
|
||||
piece of cake. Best of all? It makes development fun!
|
||||
|
||||
* Web site: http://angularjs.org
|
||||
* Tutorial: http://docs.angularjs.org/tutorial
|
||||
* API Docs: http://docs.angularjs.org/api
|
||||
* Developer Guide: http://docs.angularjs.org/guide
|
||||
* Web site: https://angularjs.org
|
||||
* Tutorial: https://docs.angularjs.org/tutorial
|
||||
* API Docs: https://docs.angularjs.org/api
|
||||
* Developer Guide: https://docs.angularjs.org/guide
|
||||
* Contribution guidelines: [CONTRIBUTING.md](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md)
|
||||
* Dashboard: http://dashboard.angularjs.org
|
||||
|
||||
* Dashboard: https://dashboard.angularjs.org
|
||||
|
||||
##### Looking for Angular 2? Go here: https://github.com/angular/angular
|
||||
|
||||
Building AngularJS
|
||||
---------
|
||||
[Once you have your environment set up](http://docs.angularjs.org/misc/contribute) just run:
|
||||
[Once you have set up your environment](https://docs.angularjs.org/misc/contribute), just run:
|
||||
|
||||
grunt package
|
||||
|
||||
@@ -37,8 +39,12 @@ To execute end-to-end (e2e) tests, use:
|
||||
grunt package
|
||||
grunt test:e2e
|
||||
|
||||
To learn more about the grunt tasks, run `grunt --help` and also read our
|
||||
[contribution guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md).
|
||||
To learn more about the grunt tasks, run `grunt --help`
|
||||
|
||||
Contribute & Develop
|
||||
--------------------
|
||||
|
||||
We've set up a separate document for our [contribution guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md).
|
||||
|
||||
|
||||
[](https://github.com/igrigorik/ga-beacon)
|
||||
@@ -48,7 +54,7 @@ What to use AngularJS for and when to use it
|
||||
AngularJS is the next generation framework where each component is designed to work with every other component in an interconnected way like a well-oiled machine. AngularJS is JavaScript MVC made easy and done right. (Well it is not really MVC, read on, to understand what this means.)
|
||||
|
||||
#### MVC, no, MV* done the right way!
|
||||
MVC, short for Model-View-Controller, is a design pattern, i.e. how the code should be organized and how the different parts of an application separated for proper readability and debugging. Model is the data and the database. View is the user interface and what the user sees. Controller is the main link between Model and View. These are the three pillars of major programming frameworks present on the market today. On the other hand AngularJS works on MV*, short for Model-View-_Whatever_. The _Whatever_ is AngularJS's way of telling that you may create any kind of linking between the Model and the View here.
|
||||
MVC, short for Model-View-Controller, is a design pattern, i.e. how the code should be organized and how the different parts of an application separated for proper readability and debugging. Model is the data and the database. View is the user interface and what the user sees. Controller is the main link between Model and View. These are the three pillars of major programming frameworks present on the market today. On the other hand AngularJS works on MV*, short for Model-View-_Whatever_. The _Whatever_ is AngularJS's way of telling that you may create any kind of linking between the Model and the View here.
|
||||
|
||||
Unlike other frameworks in any programming language, where MVC, the three separate components, each one has to be written and then connected by the programmer, AngularJS helps the programmer by asking him/her to just create these and everything else will be taken care of by AngularJS.
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ var angularFiles = {
|
||||
'src/minErr.js',
|
||||
'src/Angular.js',
|
||||
'src/loader.js',
|
||||
'src/shallowCopy.js',
|
||||
'src/stringify.js',
|
||||
'src/AngularPublic.js',
|
||||
'src/jqLite.js',
|
||||
@@ -27,6 +28,7 @@ var angularFiles = {
|
||||
'src/ng/httpBackend.js',
|
||||
'src/ng/interpolate.js',
|
||||
'src/ng/interval.js',
|
||||
'src/ng/jsonpCallbacks.js',
|
||||
'src/ng/locale.js',
|
||||
'src/ng/location.js',
|
||||
'src/ng/log.js',
|
||||
@@ -120,10 +122,15 @@ var angularFiles = {
|
||||
'ngMessages': [
|
||||
'src/ngMessages/messages.js'
|
||||
],
|
||||
'ngParseExt': [
|
||||
'src/ngParseExt/ucd.js',
|
||||
'src/ngParseExt/module.js'
|
||||
],
|
||||
'ngResource': [
|
||||
'src/ngResource/resource.js'
|
||||
],
|
||||
'ngRoute': [
|
||||
'src/shallowCopy.js',
|
||||
'src/ngRoute/route.js',
|
||||
'src/ngRoute/routeParams.js',
|
||||
'src/ngRoute/directive/ngView.js'
|
||||
@@ -171,6 +178,7 @@ var angularFiles = {
|
||||
'test/auto/*.js',
|
||||
'test/ng/**/*.js',
|
||||
'test/ngAnimate/*.js',
|
||||
'test/ngMessageFormat/*.js',
|
||||
'test/ngMessages/*.js',
|
||||
'test/ngCookies/*.js',
|
||||
'test/ngResource/*.js',
|
||||
@@ -205,9 +213,12 @@ var angularFiles = {
|
||||
"karmaModules": [
|
||||
'build/angular.js',
|
||||
'@angularSrcModules',
|
||||
'test/modules/no_bootstrap.js',
|
||||
'src/ngScenario/browserTrigger.js',
|
||||
'test/helpers/*.js',
|
||||
'test/ngAnimate/*.js',
|
||||
'test/ngMessageFormat/*.js',
|
||||
'test/ngMessages/*.js',
|
||||
'test/ngMock/*.js',
|
||||
'test/ngCookies/*.js',
|
||||
'test/ngRoute/**/*.js',
|
||||
@@ -234,6 +245,17 @@ var angularFiles = {
|
||||
]
|
||||
};
|
||||
|
||||
['2.1', '2.2'].forEach(function (jQueryVersion) {
|
||||
angularFiles['karmaJquery' + jQueryVersion] = []
|
||||
.concat(angularFiles.karmaJquery)
|
||||
.map(function (path) {
|
||||
if (path.startsWith('bower_components/jquery')) {
|
||||
return path.replace(/^bower_components\/jquery/, 'bower_components/jquery-' + jQueryVersion);
|
||||
}
|
||||
return path;
|
||||
});
|
||||
});
|
||||
|
||||
angularFiles['angularSrcModules'] = [].concat(
|
||||
angularFiles['angularModules']['ngAnimate'],
|
||||
angularFiles['angularModules']['ngMessageFormat'],
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"name": "AngularJS",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"jquery": "2.1.1",
|
||||
"jquery": "3.1.0",
|
||||
"jquery-2.2": "jquery#2.2.4",
|
||||
"jquery-2.1": "jquery#2.1.4",
|
||||
"closure-compiler": "https://dl.google.com/closure-compiler/compiler-20140814.zip",
|
||||
"ng-closure-runner": "https://raw.github.com/angular/ng-closure-runner/v0.2.3/assets/ng-closure-runner.zip"
|
||||
}
|
||||
|
||||
@@ -148,8 +148,6 @@ var writeChangelog = function(stream, commits, version) {
|
||||
breaks: {}
|
||||
};
|
||||
|
||||
sections.breaks[EMPTY_COMPONENT] = [];
|
||||
|
||||
commits.forEach(function(commit) {
|
||||
var section = sections[commit.type];
|
||||
var component = commit.component || EMPTY_COMPONENT;
|
||||
|
||||
@@ -647,6 +647,11 @@ ul.events > li {
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
.diagram {
|
||||
margin-bottom: 10px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 769px) and (max-width: 991px) {
|
||||
.main-body-grid {
|
||||
margin-top: 160px;
|
||||
|
||||
@@ -19,9 +19,12 @@
|
||||
"dump": false,
|
||||
|
||||
/* e2e */
|
||||
"protractor": false,
|
||||
"browser": false,
|
||||
"element": false,
|
||||
"by": false,
|
||||
"$": false,
|
||||
"$$": false,
|
||||
|
||||
/* testabilityPatch / matchers */
|
||||
"inject": false,
|
||||
@@ -39,4 +42,4 @@
|
||||
"browserTrigger": false,
|
||||
"jqLiteCacheSize": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('docs.angularjs.org', function () {
|
||||
|
||||
|
||||
it('should change the page content when clicking a link to a service', function () {
|
||||
browser.get('build/docs/index.html');
|
||||
browser.get('build/docs/index-production.html');
|
||||
|
||||
var ngBindLink = element(by.css('.definition-table td a[href="api/ng/directive/ngClick"]'));
|
||||
ngBindLink.click();
|
||||
@@ -51,33 +51,33 @@ describe('docs.angularjs.org', function () {
|
||||
|
||||
|
||||
it('should be resilient to trailing slashes', function() {
|
||||
browser.get('build/docs/index.html#!/api/ng/function/angular.noop/');
|
||||
browser.get('build/docs/index-production.html#!/api/ng/function/angular.noop/');
|
||||
var pageBody = element(by.css('h1'));
|
||||
expect(pageBody.getText()).toEqual('angular.noop');
|
||||
});
|
||||
|
||||
|
||||
it('should be resilient to trailing "index"', function() {
|
||||
browser.get('build/docs/index.html#!/api/ng/function/angular.noop/index');
|
||||
browser.get('build/docs/index-production.html#!/api/ng/function/angular.noop/index');
|
||||
var pageBody = element(by.css('h1'));
|
||||
expect(pageBody.getText()).toEqual('angular.noop');
|
||||
});
|
||||
|
||||
|
||||
it('should be resilient to trailing "index/"', function() {
|
||||
browser.get('build/docs/index.html#!/api/ng/function/angular.noop/index/');
|
||||
browser.get('build/docs/index-production.html#!/api/ng/function/angular.noop/index/');
|
||||
var pageBody = element(by.css('h1'));
|
||||
expect(pageBody.getText()).toEqual('angular.noop');
|
||||
});
|
||||
|
||||
|
||||
it('should display formatted error messages on error doc pages', function() {
|
||||
browser.get('build/docs/index.html#!error/ng/areq?p0=Missing&p1=not%20a%20function,%20got%20undefined');
|
||||
browser.get('build/docs/index-production.html#!error/ng/areq?p0=Missing&p1=not%20a%20function,%20got%20undefined');
|
||||
expect(element(by.css('.minerr-errmsg')).getText()).toEqual("Argument 'Missing' is not a function, got undefined");
|
||||
});
|
||||
|
||||
it("should display an error if the page does not exist", function() {
|
||||
browser.get('build/docs/index.html#!/api/does/not/exist');
|
||||
browser.get('build/docs/index-production.html#!/api/does/not/exist');
|
||||
expect(element(by.css('h1')).getText()).toBe('Oops!');
|
||||
});
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ angular.module('examples', [])
|
||||
|
||||
postData.description = ctrl.example.name;
|
||||
|
||||
formPostData('http://plnkr.co/edit/?p=preview', newWindow, postData);
|
||||
formPostData('https://plnkr.co/edit/?p=preview', newWindow, postData);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
@@ -5,7 +5,8 @@ angular.module('tutorials', [])
|
||||
'',
|
||||
'step_00', 'step_01', 'step_02', 'step_03', 'step_04',
|
||||
'step_05', 'step_06', 'step_07', 'step_08', 'step_09',
|
||||
'step_10', 'step_11', 'step_12', 'the_end'
|
||||
'step_10', 'step_11', 'step_12', 'step_13', 'step_14',
|
||||
'the_end'
|
||||
];
|
||||
return {
|
||||
scope: {},
|
||||
@@ -43,7 +44,7 @@ angular.module('tutorials', [])
|
||||
'<a href="http://angular.github.io/angular-phonecat/step-{{step}}/app">Step {{step}} Live Demo</a>.</p>\n' +
|
||||
'</div>\n' +
|
||||
'<p>The most important changes are listed below. You can see the full diff on ' +
|
||||
'<a ng-href="https://github.com/angular/angular-phonecat/compare/step-{{step ? (step - 1): \'0~1\'}}...step-{{step}}" title="See diff on Github">GitHub</a>\n' +
|
||||
'<a ng-href="https://github.com/angular/angular-phonecat/compare/step-{{step ? (step - 1): \'0~1\'}}...step-{{step}}" title="See diff on Github">GitHub</a>.\n' +
|
||||
'</p>'
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ describe("DocsController", function() {
|
||||
|
||||
it("should update the Google Analytics with $location.path if currentPage is missing", inject(function($window, $location) {
|
||||
$window._gaq = [];
|
||||
spyOn($location, 'path').andReturn('x/y/z');
|
||||
spyOn($location, 'path').and.returnValue('x/y/z');
|
||||
$scope.$broadcast('$includeContentLoaded');
|
||||
expect($window._gaq.pop()).toEqual(['_trackPageview', 'x/y/z']);
|
||||
}));
|
||||
|
||||
@@ -4,7 +4,7 @@ describe('errors', function() {
|
||||
// Mock `ngSanitize` module
|
||||
angular.
|
||||
module('ngSanitize', []).
|
||||
value('$sanitize', jasmine.createSpy('$sanitize').andCallFake(angular.identity));
|
||||
value('$sanitize', jasmine.createSpy('$sanitize').and.callFake(angular.identity));
|
||||
|
||||
beforeEach(module('errors'));
|
||||
|
||||
@@ -103,12 +103,12 @@ describe('errors', function() {
|
||||
|
||||
|
||||
it('should pass the final string through `$sanitize`', function() {
|
||||
$sanitize.reset();
|
||||
$sanitize.calls.reset();
|
||||
|
||||
var input = 'start https://foo/bar?baz#qux end';
|
||||
var output = errorLinkFilter(input);
|
||||
|
||||
expect($sanitize.callCount).toBe(1);
|
||||
expect($sanitize).toHaveBeenCalledTimes(1);
|
||||
expect($sanitize).toHaveBeenCalledWith(output);
|
||||
});
|
||||
});
|
||||
@@ -123,7 +123,7 @@ describe('errors', function() {
|
||||
beforeEach(module(function($provide) {
|
||||
$provide.decorator('errorLinkFilter', function() {
|
||||
errorLinkFilter = jasmine.createSpy('errorLinkFilter');
|
||||
errorLinkFilter.andCallFake(angular.identity);
|
||||
errorLinkFilter.and.callFake(angular.identity);
|
||||
|
||||
return errorLinkFilter;
|
||||
});
|
||||
@@ -142,7 +142,7 @@ describe('errors', function() {
|
||||
|
||||
|
||||
it('should interpolate the contents against `$location.search()`', function() {
|
||||
spyOn($location, 'search').andReturn({p0: 'foo', p1: 'bar'});
|
||||
spyOn($location, 'search').and.returnValue({p0: 'foo', p1: 'bar'});
|
||||
|
||||
var elem = $compile('<span error-display="foo = {0}, bar = {1}"></span>')($rootScope);
|
||||
expect(elem.html()).toBe('foo = foo, bar = bar');
|
||||
@@ -150,10 +150,10 @@ describe('errors', function() {
|
||||
|
||||
|
||||
it('should pass the interpolated text through `errorLinkFilter`', function() {
|
||||
$location.search = jasmine.createSpy('search').andReturn({p0: 'foo'});
|
||||
$location.search = jasmine.createSpy('search').and.returnValue({p0: 'foo'});
|
||||
|
||||
var elem = $compile('<span error-display="foo = {0}"></span>')($rootScope);
|
||||
expect(errorLinkFilter.callCount).toBe(1);
|
||||
expect(errorLinkFilter).toHaveBeenCalledTimes(1);
|
||||
expect(errorLinkFilter).toHaveBeenCalledWith('foo = foo', '_blank');
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "AngularJS-docs-app",
|
||||
"dependencies": {
|
||||
"jquery": "2.1.1",
|
||||
"jquery": "2.2.3",
|
||||
"lunr.js": "0.5.12",
|
||||
"open-sans-fontface": "1.0.4",
|
||||
"google-code-prettify": "1.0.1",
|
||||
|
||||
@@ -54,6 +54,7 @@ module.exports = new Package('angularjs', [
|
||||
.config(function(parseTagsProcessor) {
|
||||
parseTagsProcessor.tagDefinitions.push(require('./tag-defs/tutorial-step'));
|
||||
parseTagsProcessor.tagDefinitions.push(require('./tag-defs/sortOrder'));
|
||||
parseTagsProcessor.tagDefinitions.push(require('./tag-defs/installation'));
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
name: 'installation'
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
{% extends "base.template.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<a href='https://github.com/{$ git.info.owner $}/{$ git.info.repo $}/tree/{$ git.version.isSnapshot and 'master' or git.version.raw $}/{$ doc.fileInfo.projectRelativePath $}#L{$ doc.startingLine $}' class='view-source pull-right btn btn-primary'>
|
||||
<i class="glyphicon glyphicon-zoom-in"> </i>View Source
|
||||
</a>
|
||||
|
||||
{% block header %}
|
||||
<header class="api-profile-header">
|
||||
<h1 class="api-profile-header-heading">{$ doc.name $}</h1>
|
||||
<ol class="api-profile-header-structure naked-list step-list">
|
||||
{% block related_components %}{% endblock %}
|
||||
<li>
|
||||
- {$ doc.docType $} in module <a href="{$ doc.moduleDoc.path $}">{$ doc.moduleDoc.name $}</a>
|
||||
</li>
|
||||
</ol>
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
||||
{% block description %}
|
||||
<div class="api-profile-description">
|
||||
{$ doc.description | marked $}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% if doc.knownIssues %}
|
||||
<h2 id="known-issues">Known Issues</h2>
|
||||
{% for issue in doc.knownIssues -%}
|
||||
<div class="known-issue">
|
||||
{$ issue | marked $} {% if not loop.last %}<hr>{% endif %}
|
||||
</div>
|
||||
{% endfor -%}
|
||||
{% endif %}
|
||||
|
||||
{% if doc.deprecated %}
|
||||
<fieldset class="deprecated">
|
||||
<legend>Deprecated API</legend>
|
||||
{$ doc.deprecated| marked $}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
{% block dependencies %}
|
||||
{%- if doc.requires %}
|
||||
<h2 id="dependencies">Dependencies</h2>
|
||||
<ul>
|
||||
{% for require in doc.requires %}<li>{$ require | link $}</li>{% endfor %}
|
||||
</ul>
|
||||
{% endif -%}
|
||||
{% endblock %}
|
||||
|
||||
{% block additional %}
|
||||
{% endblock %}
|
||||
|
||||
{% block examples %}
|
||||
{%- if doc.examples %}
|
||||
<h2 id="example">Example</h2>
|
||||
{%- for example in doc.examples -%}
|
||||
{$ example | marked $}
|
||||
{%- endfor -%}
|
||||
{% endif -%}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,100 @@
|
||||
{% extends "base.template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% if doc.title %}{$ doc.title | marked $}{% else %}{$ doc.name | code $}{% endif %}
|
||||
</h1>
|
||||
|
||||
<h2>Installation</h2>
|
||||
{% if doc.installation or doc.installation == '' %}
|
||||
{$ doc.installation | marked $}
|
||||
{% else %}
|
||||
|
||||
<p>First, get the file:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://developers.google.com/speed/libraries/devguide#angularjs">Google CDN</a> e.g.
|
||||
{% code %}"//ajax.googleapis.com/ajax/libs/angularjs/X.Y.Z/{$ doc.packageFile $}"{% endcode %}
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.npmjs.com/">NPM</a> e.g.
|
||||
{% code %}npm install {$ doc.packageName $}@X.Y.Z{% endcode %}
|
||||
</li>
|
||||
<li>
|
||||
<a href="http://bower.io">Bower</a> e.g.
|
||||
{% code %}bower install {$ doc.packageName $}#X.Y.Z{% endcode %}
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://code.angularjs.org/">code.angularjs.org</a>
|
||||
(discouraged for production use) e.g.
|
||||
{% code %}"//code.angularjs.org/X.Y.Z/{$ doc.packageFile $}"{% endcode %}
|
||||
</li>
|
||||
</ul>
|
||||
<p>where X.Y.Z is the AngularJS version you are running.</p>
|
||||
|
||||
<p>Then, include {$ doc.packageFile | code $} in your HTML:</p>
|
||||
|
||||
{% code %}
|
||||
<script src="path/to/angular.js"></script>
|
||||
<script src="path/to/{$ doc.packageFile $}"></script>
|
||||
{% endcode %}
|
||||
|
||||
<p>Finally, load the module in your application by adding it as a dependent module:</p>
|
||||
{% code %}
|
||||
angular.module('app', ['{$ doc.name $}']);
|
||||
{% endcode %}
|
||||
|
||||
<p>With that you're ready to get started!</p>
|
||||
{% endif %}
|
||||
|
||||
{$ doc.description | marked $}
|
||||
|
||||
{% if doc.knownIssueDocs %}
|
||||
<div class="known-issues">
|
||||
<h2 id="known-issues">Known Issues</h2>
|
||||
<table class="definition-table">
|
||||
<tr><th>Name</th><th>Description</th></tr>
|
||||
{% for issueDoc in doc.knownIssueDocs -%}
|
||||
<tr>
|
||||
<td>{$ issueDoc.id | link(issueDoc.name, issueDoc) $}</td>
|
||||
<td>
|
||||
{% for issue in issueDoc.knownIssues -%}
|
||||
{$ issue | marked $} {% if not loop.last %}<hr>{% endif %}
|
||||
{% endfor -%}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor -%}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if doc.componentGroups.length %}
|
||||
<div class="component-breakdown">
|
||||
<h2>Module Components</h2>
|
||||
{% for componentGroup in doc.componentGroups %}
|
||||
<div>
|
||||
<h3 class="component-heading" id="{$ componentGroup.groupType | dashCase $}">{$ componentGroup.groupType | title $}</h3>
|
||||
<table class="definition-table">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
{% for component in componentGroup.components %}
|
||||
<tr>
|
||||
<td>{$ component.id | link(component.name, component) $}</td>
|
||||
<td>{$ component.description | firstParagraph | marked $}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if doc.usage %}
|
||||
<h2>Usage</h2>
|
||||
{$ doc.usage | marked $}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -228,10 +228,10 @@
|
||||
)
|
||||
</p>
|
||||
<p>
|
||||
Code licensed under the
|
||||
<a href="https://github.com/angular/angular.js/blob/master/LICENSE" target="_blank">The
|
||||
MIT License</a>. Documentation licensed under <a
|
||||
href="http://creativecommons.org/licenses/by/3.0/">CC BY 3.0</a>.
|
||||
Code licensed under
|
||||
<a href="https://github.com/angular/angular.js/blob/master/LICENSE" target="_blank">The MIT License</a>.
|
||||
Documentation licensed under
|
||||
<a href="http://creativecommons.org/licenses/by/3.0/" target="_blank">CC BY 3.0</a>.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -8,6 +8,8 @@ Welcome to the AngularJS API docs page. These pages contain the AngularJS refere
|
||||
The documentation is organized into **{@link guide/module modules}** which contain various components of an AngularJS application.
|
||||
These components are {@link guide/directive directives}, {@link guide/services services}, {@link guide/filter filters}, {@link guide/providers providers}, {@link guide/templates templates}, global APIs, and testing mocks.
|
||||
|
||||
There is also a {@link guide/index guide} with articles on various topics, and a list of external resources.
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Angular Prefixes `$` and `$$`**:
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@ngdoc error
|
||||
@name $compile:baddir
|
||||
@fullName Invalid Directive Name
|
||||
@fullName Invalid Directive/Component Name
|
||||
@description
|
||||
|
||||
This error occurs when the name of a directive is not valid.
|
||||
This error occurs when the name of a directive or component is not valid.
|
||||
|
||||
Directives must start with a lowercase character and must not contain leading or trailing whitespaces.
|
||||
Directives and Components must start with a lowercase character and must not contain leading or trailing whitespaces.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
@ngdoc error
|
||||
@name $compile:infchng
|
||||
@fullName Unstable `$onChanges` hooks
|
||||
@description
|
||||
|
||||
This error occurs when the application's model becomes unstable because some `$onChanges` hooks are causing updates which then trigger
|
||||
further calls to `$onChanges` that can never complete.
|
||||
Angular detects this situation and prevents an infinite loop from causing the browser to become unresponsive.
|
||||
|
||||
For example, the situation can occur by setting up a `$onChanges()` hook which triggers an event on the component, which subsequently
|
||||
triggers the component's bound inputs to be updated:
|
||||
|
||||
```html
|
||||
<c1 prop="a" on-change="a = -a"></c1>
|
||||
```
|
||||
|
||||
```js
|
||||
function Controller1() {}
|
||||
Controller1.$onChanges = function() {
|
||||
this.onChange();
|
||||
};
|
||||
|
||||
mod.component('c1', {
|
||||
controller: Controller1,
|
||||
bindings: {'prop': '<', onChange: '&'}
|
||||
}
|
||||
```
|
||||
|
||||
The maximum number of allowed iterations of the `$onChanges` hooks is controlled via TTL setting which can be configured via
|
||||
{@link ng.$compileProvider#onChangesTtl `$compileProvider.onChangesTtl`}.
|
||||
@@ -0,0 +1,47 @@
|
||||
@ngdoc error
|
||||
@name $compile:reqslot
|
||||
@fullName Required transclusion slot
|
||||
@description
|
||||
|
||||
This error occurs when a directive or component try to transclude a slot that is not provided.
|
||||
|
||||
Transcluded elements must contain something. This error could happen when you try to transclude a self closing tag element.
|
||||
Also you can make a transclusion slot optional with a `?` prefix.
|
||||
|
||||
```js
|
||||
// In this example the <my-component> must have an <important-component> inside to transclude it.
|
||||
// If not, a reqslot error will be generated.
|
||||
|
||||
var componentConfig = {
|
||||
template: 'path/to/template.html',
|
||||
tranclude: {
|
||||
importantSlot: 'importantComponent', // mandatory transclusion
|
||||
optionalSlot: '?optionalComponent', // optional transclusion
|
||||
}
|
||||
};
|
||||
|
||||
angular
|
||||
.module('doc')
|
||||
.component('myComponent', componentConfig)
|
||||
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- Will not work because <important-component> is missing -->
|
||||
<my-component>
|
||||
</my-component>
|
||||
|
||||
<my-component>
|
||||
<optional-component></optional-component>
|
||||
</my-component>
|
||||
|
||||
<!-- Will work -->
|
||||
<my-component>
|
||||
<important-component></important-component>
|
||||
</my-component>
|
||||
|
||||
<my-component>
|
||||
<optional-component></optional-component>
|
||||
<important-component></important-component>
|
||||
</my-component>
|
||||
```
|
||||
@@ -43,7 +43,7 @@ well. Consider the following template:
|
||||
|
||||
```
|
||||
<div class='container'>
|
||||
<div class='wrapper>
|
||||
<div class='wrapper'>
|
||||
...
|
||||
</div> <!-- wrapper -->
|
||||
</div> <!-- container -->
|
||||
|
||||
@@ -350,7 +350,7 @@ base and the path that should be handled by the application.
|
||||
### Base href constraints
|
||||
|
||||
The `$location` service is not able to function properly if the current URL is outside the URL given
|
||||
as the base href. This can have subtle confusing consequencies...
|
||||
as the base href. This can have subtle confusing consequences...
|
||||
|
||||
Consider a base href set as follows: `<base href="/base/">` (i.e. the application exists in the "folder"
|
||||
called `/base`). The URL `/base` is actually outside the application (it refers to the `base` file found
|
||||
|
||||
@@ -10,7 +10,7 @@ The goal of ngAria is to improve Angular's default accessibility by enabling com
|
||||
[ARIA](http://www.w3.org/TR/wai-aria/) attributes that convey state or semantic information for
|
||||
assistive technologies used by persons with disabilities.
|
||||
|
||||
##Including ngAria
|
||||
## Including ngAria
|
||||
|
||||
Using {@link ngAria ngAria} is as simple as requiring the ngAria module in your application. ngAria hooks into
|
||||
standard AngularJS directives and quietly injects accessibility support into your application
|
||||
@@ -20,7 +20,7 @@ at runtime.
|
||||
angular.module('myApp', ['ngAria'])...
|
||||
```
|
||||
|
||||
###Using ngAria
|
||||
### Using ngAria
|
||||
Most of what ngAria does is only visible "under the hood". To see the module in action, once you've
|
||||
added it as a dependency, you can test a few things:
|
||||
* Using your favorite element inspector, look for attributes added by ngAria in your own code.
|
||||
@@ -28,12 +28,13 @@ added it as a dependency, you can test a few things:
|
||||
* Fire up a screen reader such as VoiceOver or NVDA to check for ARIA support.
|
||||
[Helpful screen reader tips.](http://webaim.org/articles/screenreader_testing/)
|
||||
|
||||
##Supported directives
|
||||
## Supported directives
|
||||
Currently, ngAria interfaces with the following directives:
|
||||
|
||||
* {@link guide/accessibility#ngmodel ngModel}
|
||||
* {@link guide/accessibility#ngdisabled ngDisabled}
|
||||
* {@link guide/accessibility#ngrequired ngRequired}
|
||||
* {@link guide/accessibility#ngreadonly ngReadonly}
|
||||
* {@link guide/accessibility#ngvaluechecked ngChecked}
|
||||
* {@link guide/accessibility#ngvaluechecked ngValue}
|
||||
* {@link guide/accessibility#ngshow ngShow}
|
||||
@@ -57,12 +58,62 @@ attributes (if they have not been explicitly specified by the developer):
|
||||
* aria-valuenow
|
||||
* aria-invalid
|
||||
* aria-required
|
||||
* aria-readonly
|
||||
|
||||
###Example
|
||||
### Example
|
||||
|
||||
<example module="ngAria_ngModelExample" deps="angular-aria.js">
|
||||
<file name="index.html">
|
||||
<style>
|
||||
<file name="index.html">
|
||||
<form ng-controller="formsController">
|
||||
<some-checkbox role="checkbox" ng-model="checked" ng-class="{active: checked}"
|
||||
ng-disabled="isDisabled" ng-click="toggleCheckbox()"
|
||||
aria-label="Custom Checkbox" show-attrs>
|
||||
<span class="icon" aria-hidden="true"></span>
|
||||
Custom Checkbox
|
||||
</some-checkbox>
|
||||
</form>
|
||||
</file>
|
||||
<file name="script.js">
|
||||
var app = angular.module('ngAria_ngModelExample', ['ngAria'])
|
||||
.controller('formsController', function($scope){
|
||||
$scope.checked = false;
|
||||
$scope.toggleCheckbox = function(){
|
||||
$scope.checked = !$scope.checked;
|
||||
};
|
||||
})
|
||||
.directive('someCheckbox', function(){
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function($scope, $el, $attrs) {
|
||||
$el.on('keypress', function(event){
|
||||
event.preventDefault();
|
||||
if(event.keyCode === 32 || event.keyCode === 13){
|
||||
$scope.toggleCheckbox();
|
||||
$scope.$apply();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
.directive('showAttrs', function() {
|
||||
return function($scope, $el, $attrs) {
|
||||
var pre = document.createElement('pre');
|
||||
$el.after(pre);
|
||||
$scope.$watch(function() {
|
||||
var $attrs = {};
|
||||
Array.prototype.slice.call($el[0].attributes, 0).forEach(function(item) {
|
||||
if (item.name !== 'show-$attrs') {
|
||||
$attrs[item.name] = item.value;
|
||||
}
|
||||
});
|
||||
return $attrs;
|
||||
}, function(newAttrs, oldAttrs) {
|
||||
pre.textContent = JSON.stringify(newAttrs, null, 2);
|
||||
}, true);
|
||||
};
|
||||
});
|
||||
</file>
|
||||
<file name="style.css">
|
||||
[role=checkbox] {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
@@ -81,58 +132,7 @@ attributes (if they have not been explicitly specified by the developer):
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
<form ng-controller="formsController">
|
||||
<some-checkbox role="checkbox" ng-model="checked" ng-class="{active: checked}"
|
||||
ng-disabled="isDisabled" ng-click="toggleCheckbox()"
|
||||
aria-label="Custom Checkbox" show-attrs>
|
||||
<span class="icon" aria-hidden="true"></span>
|
||||
Custom Checkbox
|
||||
</some-checkbox>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
var app = angular.module('ngAria_ngModelExample', ['ngAria'])
|
||||
.controller('formsController', function($scope){
|
||||
$scope.checked = false;
|
||||
$scope.toggleCheckbox = function(){
|
||||
$scope.checked = !$scope.checked;
|
||||
}
|
||||
})
|
||||
.directive('someCheckbox', function(){
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function($scope, $el, $attrs) {
|
||||
$el.on('keypress', function(event){
|
||||
event.preventDefault();
|
||||
if(event.keyCode === 32 || event.keyCode === 13){
|
||||
$scope.toggleCheckbox();
|
||||
$scope.$apply();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.directive('showAttrs', function() {
|
||||
return function($scope, $el, $attrs) {
|
||||
var pre = document.createElement('pre');
|
||||
$el.after(pre);
|
||||
$scope.$watch(function() {
|
||||
var $attrs = {};
|
||||
Array.prototype.slice.call($el[0].attributes, 0).forEach(function(item) {
|
||||
if (item.name !== 'show-$attrs') {
|
||||
$attrs[item.name] = item.value;
|
||||
}
|
||||
});
|
||||
return $attrs;
|
||||
}, function(newAttrs, oldAttrs) {
|
||||
pre.textContent = JSON.stringify(newAttrs, null, 2);
|
||||
}, true);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</file>
|
||||
</file>
|
||||
</example>
|
||||
|
||||
ngAria will also add `tabIndex`, ensuring custom elements with these roles will be reachable from
|
||||
@@ -147,7 +147,7 @@ To ease the transition between native inputs and custom controls, ngAria now sup
|
||||
The original directives were created for native inputs only, so ngAria extends
|
||||
support to custom elements by managing `aria-checked` for accessibility.
|
||||
|
||||
###Example
|
||||
### Example
|
||||
|
||||
```html
|
||||
<custom-checkbox ng-checked="val"></custom-checkbox>
|
||||
@@ -169,7 +169,7 @@ using ngAria with {@link ng.ngDisabled ngDisabled} will also
|
||||
add `aria-disabled`. This tells assistive technologies when a non-native input is disabled, helping
|
||||
custom controls to be more accessible.
|
||||
|
||||
###Example
|
||||
### Example
|
||||
|
||||
```html
|
||||
<md-checkbox ng-disabled="disabled"></md-checkbox>
|
||||
@@ -181,8 +181,10 @@ Becomes:
|
||||
<md-checkbox disabled aria-disabled="true"></md-checkbox>
|
||||
```
|
||||
|
||||
>You can check whether a control is legitimately disabled for a screen reader by visiting
|
||||
<div class="alert alert-info">
|
||||
You can check whether a control is legitimately disabled for a screen reader by visiting
|
||||
[chrome://accessibility](chrome://accessibility) and inspecting [the accessibility tree](http://www.paciellogroup.com/blog/2015/01/the-browser-accessibility-tree/).
|
||||
</div>
|
||||
|
||||
<h2 id="ngrequired">ngRequired</h2>
|
||||
|
||||
@@ -191,7 +193,7 @@ The boolean `required` attribute is only valid for native form controls such as
|
||||
as required, using ngAria with {@link ng.ngRequired ngRequired} will also add
|
||||
`aria-required`. This tells accessibility APIs when a custom control is required.
|
||||
|
||||
###Example
|
||||
### Example
|
||||
|
||||
```html
|
||||
<md-checkbox ng-required="val"></md-checkbox>
|
||||
@@ -203,9 +205,28 @@ Becomes:
|
||||
<md-checkbox ng-required="val" aria-required="true"></md-checkbox>
|
||||
```
|
||||
|
||||
<h2 id="ngreadonly">ngReadonly</h2>
|
||||
|
||||
The boolean `readonly` attribute is only valid for native form controls such as `input` and
|
||||
`textarea`. To properly indicate custom element directives such as `<md-checkbox>` or `<custom-input>`
|
||||
as required, using ngAria with {@link ng.ngReadonly ngReadonly} will also add
|
||||
`aria-readonly`. This tells accessibility APIs when a custom control is read-only.
|
||||
|
||||
### Example
|
||||
|
||||
```html
|
||||
<md-checkbox ng-readonly="val"></md-checkbox>
|
||||
```
|
||||
|
||||
Becomes:
|
||||
|
||||
```html
|
||||
<md-checkbox ng-readonly="val" aria-readonly="true"></md-checkbox>
|
||||
```
|
||||
|
||||
<h2 id="ngshow">ngShow</h2>
|
||||
|
||||
>The {@link ng.ngShow ngShow} directive shows or hides the
|
||||
The {@link ng.ngShow ngShow} directive shows or hides the
|
||||
given HTML element based on the expression provided to the `ngShow` attribute. The element is
|
||||
shown or hidden by removing or adding the `.ng-hide` CSS class onto the element.
|
||||
|
||||
@@ -222,7 +243,7 @@ screen reader users won't accidentally focus on "mystery elements". Managing tab
|
||||
child control can be complex and affect performance, so it’s best to just stick with the default
|
||||
`display: none` CSS. See the [fourth rule of ARIA use](http://www.w3.org/TR/aria-in-html/#fourth-rule-of-aria-use).
|
||||
|
||||
###Example
|
||||
### Example
|
||||
```css
|
||||
.ng-hide {
|
||||
display: block;
|
||||
@@ -242,7 +263,7 @@ Becomes:
|
||||
|
||||
<h2 id="nghide">ngHide</h2>
|
||||
|
||||
>The {@link ng.ngHide ngHide} directive shows or hides the
|
||||
The {@link ng.ngHide ngHide} directive shows or hides the
|
||||
given HTML element based on the expression provided to the `ngHide` attribute. The element is
|
||||
shown or hidden by removing or adding the `.ng-hide` CSS class onto the element.
|
||||
|
||||
@@ -283,11 +304,11 @@ Becomes:
|
||||
|
||||
<h2 id="ngmessages">ngMessages</h2>
|
||||
|
||||
The new ngMessages module makes it easy to display form validation or other messages with priority
|
||||
The ngMessages module makes it easy to display form validation or other messages with priority
|
||||
sequencing and animation. To expose these visual messages to screen readers,
|
||||
ngAria injects `aria-live="assertive"`, causing them to be read aloud any time a message is shown,
|
||||
regardless of the user's focus location.
|
||||
###Example
|
||||
### Example
|
||||
|
||||
```html
|
||||
<div ng-messages="myForm.myName.$error">
|
||||
@@ -305,7 +326,7 @@ Becomes:
|
||||
</div>
|
||||
```
|
||||
|
||||
##Disabling attributes
|
||||
## Disabling attributes
|
||||
The attribute magic of ngAria may not work for every scenario. To disable individual attributes,
|
||||
you can use the {@link ngAria.$ariaProvider#config config} method. Just keep in mind this will
|
||||
tell ngAria to ignore the attribute globally.
|
||||
@@ -343,7 +364,7 @@ tell ngAria to ignore the attribute globally.
|
||||
</file>
|
||||
</example>
|
||||
|
||||
##Common Accessibility Patterns
|
||||
## Common Accessibility Patterns
|
||||
|
||||
Accessibility best practices that apply to web apps in general also apply to Angular.
|
||||
|
||||
|
||||
@@ -12,6 +12,13 @@ triggered, will attempt to perform a CSS Transition, CSS Keyframe Animation or a
|
||||
placed on the given directive). Animations can be placed using vanilla CSS by following the naming conventions set in place by AngularJS
|
||||
or with JavaScript code when it's defined as a factory.
|
||||
|
||||
<div class="alert alert-info">
|
||||
Note that we have used non-prefixed CSS transition properties in our examples as the major browsers now support non-prefixed
|
||||
properties. If you intend to support older browsers or certain mobile browsers then you will need to include prefixed
|
||||
versions of the transition properties. Take a look at http://caniuse.com/#feat=css-transitions for what browsers require prefixes,
|
||||
and https://github.com/postcss/autoprefixer for a tool that can automatically generate the prefixes for you.
|
||||
</div>
|
||||
|
||||
Animations are not available unless you include the {@link ngAnimate `ngAnimate` module} as a dependency within your application.
|
||||
|
||||
Below is a quick example of animations being enabled for `ngShow` and `ngHide`:
|
||||
@@ -29,18 +36,17 @@ Below is a quick example of animations being enabled for `ngShow` and `ngHide`:
|
||||
</file>
|
||||
<file name="animations.css">
|
||||
.sample-show-hide {
|
||||
padding:10px;
|
||||
border:1px solid black;
|
||||
background:white;
|
||||
padding: 10px;
|
||||
border: 1px solid black;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.sample-show-hide {
|
||||
-webkit-transition:all linear 0.5s;
|
||||
transition:all linear 0.5s;
|
||||
transition: all linear 0.5s;
|
||||
}
|
||||
|
||||
.sample-show-hide.ng-hide {
|
||||
opacity:0;
|
||||
opacity: 0;
|
||||
}
|
||||
</file>
|
||||
</example>
|
||||
@@ -80,11 +86,8 @@ occur when ngRepeat triggers them:
|
||||
class
|
||||
*/
|
||||
.repeated-item.ng-enter, .repeated-item.ng-move {
|
||||
-webkit-transition:0.5s linear all;
|
||||
-moz-transition:0.5s linear all;
|
||||
-o-transition:0.5s linear all;
|
||||
transition:0.5s linear all;
|
||||
opacity:0;
|
||||
transition: all 0.5s linear;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -95,7 +98,7 @@ occur when ngRepeat triggers them:
|
||||
*/
|
||||
.repeated-item.ng-enter.ng-enter-active,
|
||||
.repeated-item.ng-move.ng-move-active {
|
||||
opacity:1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -104,10 +107,7 @@ occur when ngRepeat triggers them:
|
||||
that has the .repeated-item class
|
||||
*/
|
||||
.repeated-item.ng-leave {
|
||||
-webkit-animation:0.5s my_animation;
|
||||
-moz-animation:0.5s my_animation;
|
||||
-o-animation:0.5s my_animation;
|
||||
animation:0.5s my_animation;
|
||||
animation: 0.5s my_animation;
|
||||
}
|
||||
|
||||
@keyframes my_animation {
|
||||
@@ -115,24 +115,6 @@ occur when ngRepeat triggers them:
|
||||
to { opacity:0; }
|
||||
}
|
||||
|
||||
/*
|
||||
Unfortunately each browser vendor requires
|
||||
its own definition of keyframe animation code...
|
||||
*/
|
||||
@-webkit-keyframes my_animation {
|
||||
from { opacity:1; }
|
||||
to { opacity:0; }
|
||||
}
|
||||
|
||||
@-moz-keyframes my_animation {
|
||||
from { opacity:1; }
|
||||
to { opacity:0; }
|
||||
}
|
||||
|
||||
@-o-keyframes my_animation {
|
||||
from { opacity:1; }
|
||||
to { opacity:0; }
|
||||
}
|
||||
```
|
||||
|
||||
The same approach to animation can be used using JavaScript code (**jQuery is used within to perform animations**):
|
||||
@@ -217,10 +199,7 @@ The example below shows how to perform animations during class changes:
|
||||
</file>
|
||||
<file name="style.css">
|
||||
.css-class-add, .css-class-remove {
|
||||
-webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||
-moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||
-o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||
transition: all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||
}
|
||||
|
||||
.css-class,
|
||||
@@ -231,7 +210,7 @@ The example below shows how to perform animations during class changes:
|
||||
|
||||
.css-class-remove.css-class-remove-active {
|
||||
font-size:1.0em;
|
||||
color:black;
|
||||
color: black;
|
||||
}
|
||||
</file>
|
||||
</example>
|
||||
@@ -317,8 +296,8 @@ app.config(function($animateProvider) {
|
||||
```css
|
||||
/* prefixed with animate- */
|
||||
.animate-fade-add.animate-fade-add-active {
|
||||
transition:1s linear all;
|
||||
opacity:0;
|
||||
transition: all 1s linear;
|
||||
opacity: 0;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -334,8 +313,8 @@ This function can be used to enable / disable animations in two different ways:
|
||||
With a single `boolean` argument, it enables / disables animations globally: `$animate.enabled(false)`
|
||||
disables all animations in your app.
|
||||
|
||||
When the second argument is a native DOM or jQuery element, the function enables / disables
|
||||
animations on this element *and all its children*: `$animate.enabled(false, myElement)`. This is the
|
||||
When the first argument is a native DOM or jqLite/jQuery element, the function enables / disables
|
||||
animations on this element *and all its children*: `$animate.enabled(myElement, false)`. This is the
|
||||
most flexible way to change the animation state. For example, even if you have used it to disable
|
||||
animations on a parent element, you can still re-enable it for a child element. And compared to the
|
||||
`classNameFilter`, you can change the animation status at runtime instead of during the config phase.
|
||||
@@ -368,6 +347,7 @@ By setting `transition: 0s`, ngAnimate will ignore the existing transition style
|
||||
animations will still execute, though). This can be used to prevent {@link guide/animations#preventing-collisions-with-existing-animations-and-third-party-libraries
|
||||
issues with existing animations interfering with ngAnimate}.
|
||||
|
||||
|
||||
## Preventing flicker before an animation starts
|
||||
|
||||
When nesting elements with structural animations such as `ngIf` into elements that have class-based
|
||||
@@ -430,18 +410,37 @@ You can prevent this unwanted behavior by adding CSS to the `.ng-animate` class
|
||||
for the whole duration of an animation. Simply overwrite the transition / animation duration. In the
|
||||
case of the spinner, this would be:
|
||||
|
||||
```css
|
||||
.spinner.ng-animate {
|
||||
transition: 0s none;
|
||||
animation: 0s none;
|
||||
}
|
||||
```
|
||||
|
||||
If you do have CSS transitions / animations defined for the animation events, make sure they have higher priority
|
||||
than any styles that are independent from ngAnimate.
|
||||
|
||||
You can also use one of the two other {@link guide/animations#how-to-selectively-enable-disable-and-skip-animations strategies to disable animations}.
|
||||
|
||||
|
||||
### Enable animations for elements outside of the Angular application DOM tree: {@link ng.$animate#pin $animate.pin()}
|
||||
|
||||
Before animating, `ngAnimate` checks to see if the element being animated is inside the application DOM tree,
|
||||
and if it is not, no animation is run. Usually, this is not a problem as most apps use the `ngApp`
|
||||
attribute / bootstrap the app on the `html` or `body` element.
|
||||
|
||||
Problems arise when the application is bootstrapped on a different element, and animations are
|
||||
attempted on elements that are outside the application tree, e.g. when libraries append popup and modal
|
||||
elements as the last child in the body tag.
|
||||
|
||||
You can use {@link ng.$animate#pin `$animate.pin(elementToAnimate, parentHost)`} to specify that an
|
||||
element belongs to your application. Simply call it before the element is added to the DOM / before
|
||||
the animation starts, with the element you want to animate, and the element which should be its
|
||||
assumed parent.
|
||||
|
||||
|
||||
## More about animations
|
||||
|
||||
For a full breakdown of each method available on `$animate`, see the {@link ng.$animate API documentation}.
|
||||
|
||||
To see a complete demo, see the {@link tutorial/step_12 animation step within the AngularJS phonecat tutorial}.
|
||||
To see a complete demo, see the {@link tutorial/step_14 animation step within the AngularJS phonecat tutorial}.
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
|
||||
# Component Router
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Deprecation Notice:** In an effort to keep synchronized with router changes in Angular 2, this implementation of the Component Router (ngComponentRouter module) has been deprecated and will not receive further updates.
|
||||
We are investigating backporting the Angular 2 Router to Angular 1, but alternatively, use the {@link ngRoute} module or community developed projects (e.g. [ui-router](https://github.com/angular-ui/ui-router)).
|
||||
</div>
|
||||
|
||||
This guide describes the new Component Router for AngularJS 1.5.
|
||||
|
||||
<div class="alert alert-info">
|
||||
@@ -33,7 +38,7 @@ Here is a table of the main concepts used in the Component Router.
|
||||
|
||||
## Component-based Applications
|
||||
|
||||
It recommended to develop AngularJS applications as a hierarchy of Components. Each Component
|
||||
It is recommended to develop AngularJS applications as a hierarchy of Components. Each Component
|
||||
is an isolated part of the application, which is responsible for its own user interface and has
|
||||
a well defined programmatic interface to the Component that contains it. Take a look at the
|
||||
{@link guide/component component guide} for more information.
|
||||
@@ -105,7 +110,7 @@ Here we have specified that the **Root Component** is the component directive wi
|
||||
Remember to instantiate this **Root Component** in our `index.html` file.
|
||||
|
||||
```html
|
||||
<my-app><my-app>
|
||||
<my-app></my-app>
|
||||
```
|
||||
|
||||
## Route Matching
|
||||
@@ -124,9 +129,9 @@ This process continues until we run out of **Routing Components** or consume the
|
||||
|
||||

|
||||
|
||||
In the previous diagram can see that the URL `/heros/2` has been matched against the `App`, `Heroes` and
|
||||
In the previous diagram, we can see that the URL `/heros/4` has been matched against the `App`, `Heroes` and
|
||||
`HeroDetail` **Routing Components**. The **Routers** for each of the **Routing Components** consumed a part
|
||||
of the URL: "/", "/heroes" and "/2" respectively.
|
||||
of the URL: "/", "/heroes" and "/4" respectively.
|
||||
|
||||
The result is that we end up with a hierarchy of **Routing Components** rendered in **Outlets**, via the
|
||||
{@link ngOutlet} directive, in each **Routing Component's** template, as you can see in the following diagram.
|
||||
@@ -462,13 +467,13 @@ to display list and detail views of Heroes and Crises.
|
||||
|
||||
## Install the libraries
|
||||
|
||||
It is simplest to use npm to install the **Component Router** module. For this guide we will also install
|
||||
It is easier to use npm to install the **Component Router** module. For this guide we will also install
|
||||
AngularJS itself via npm:
|
||||
|
||||
```bash
|
||||
npm init
|
||||
npm install@1.5.x angular --save
|
||||
npm install @angular/router --save
|
||||
npm install angular@1.5.x --save
|
||||
npm install @angular/router@0.2.0 --save
|
||||
```
|
||||
|
||||
|
||||
@@ -482,10 +487,18 @@ Just like any Angular application, we load the JavaScript files into our `index.
|
||||
<script src="/app/app.js"></script>
|
||||
```
|
||||
|
||||
You also need to include ES6 shims for browsers that do not support ES6 code (Internet Explorer,
|
||||
iOs < 8, Android < 5.0, Windows Mobile < 10):
|
||||
```html
|
||||
<!-- IE required polyfills, in this exact order -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.33.3/es6-shim.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.20/system-polyfills.js"></script>
|
||||
<script src="https://npmcdn.com/angular2/es6/dev/src/testing/shims_for_IE.js"></script>
|
||||
```
|
||||
|
||||
## Create the `app` module
|
||||
|
||||
In the app.js file, create the main application module `app` which depends upon the `ngComponentRouter`
|
||||
In the app.js file, create the main application module `app` which depends on the `ngComponentRouter`
|
||||
module, which is provided by the **Component Router** script.
|
||||
|
||||
```js
|
||||
@@ -494,10 +507,10 @@ angular.module('app', ['ngComponentRouter'])
|
||||
|
||||
We must choose what **Location Mode** the **Router** should use. We are going to use HTML5 mode locations,
|
||||
so that we will not have hash-based paths. We must rely on the browser to provide `pushState` support,
|
||||
which is true of most modern browsers. See {@link $locationProvider#html5Mode} for more information.
|
||||
which is true for most modern browsers. See {@link $locationProvider#html5Mode} for more information.
|
||||
|
||||
<div class="alert alert-info">
|
||||
Using HTML5 mode means that we can have clean URLs for our application routes but it does require that our
|
||||
Using HTML5 mode means that we can have clean URLs for our application routes. However, HTML5 mode does require that our
|
||||
web server, which hosts the application, understands that it must respond with the index.html file for
|
||||
requests to URLs that represent all our application routes. We are going to use the `lite-server` web server
|
||||
to do this for us.
|
||||
@@ -550,7 +563,7 @@ Bootstrap the Angular application and add the top level App Component.
|
||||
|
||||
# Implementing the AppComponent
|
||||
|
||||
In the previous section we created a single top level **App Component**. Let's now create some more
|
||||
In the previous section we have created a single top level **App Component**. Let's now create some more
|
||||
**Routing Components** and wire up **Route Config** for those. We start with a Heroes Feature, which
|
||||
will display one of two views.
|
||||
|
||||
@@ -590,7 +603,7 @@ of this view will be rendered.
|
||||
### ngLink
|
||||
|
||||
We have used the `ng-link` directive to create a link to navigate to the Heroes Component. By using this
|
||||
directive we don't need to know what the actual URL will be. We can leave the Router to generate that for us.
|
||||
directive we don't need to know what the actual URL will be. We can let the Router generate that for us.
|
||||
|
||||
We have included a link to the Crisis Center but have not included the `ng-link` directive as we have not yet
|
||||
implemented the CrisisCenter component.
|
||||
@@ -658,7 +671,7 @@ because the `HeroList` and `HeroDetail` will not contain any child routes.
|
||||
|
||||
The `component` property in a **Route Definition** defines the **Component** directive that will be rendered
|
||||
into the DOM via the **Outlet**. For example the `heroDetail` **Component** will be rendered into the page
|
||||
where the `<ng-outlet></ng-outlet>` lives as `<hero-detail></hero-detail>.
|
||||
where the `<ng-outlet></ng-outlet>` lives as `<hero-detail></hero-detail>`.
|
||||
|
||||
The `name` property is used to reference the **Route Definition** when generating URLs or navigating to
|
||||
**Routes**. For example this link will `<a ng-link="['Heroes']">Heroes</a>` navigate the **Route Definition**
|
||||
@@ -765,7 +778,7 @@ function HeroListComponent(heroService) {
|
||||
Running the application should update the browser's location to `/heroes` and display the list of heroes
|
||||
returned from the `heroService`.
|
||||
|
||||
By returning a promise for the list of heroes from `$routerOnActivate()` we can delay activation of the
|
||||
By returning a promise for the list of heroes from `$routerOnActivate()` we can delay the activation of the
|
||||
Route until the heroes have arrived successfully. This is similar to how a `resolve` works in {@link ngRoute}.
|
||||
|
||||
|
||||
@@ -854,7 +867,6 @@ Router itself, which was made available by the binding in the **Component Defini
|
||||
function HeroDetailComponent(heroService) {
|
||||
...
|
||||
this.gotoHeroes = function() {
|
||||
var heroId = this.hero && this.hero.id;
|
||||
this.$router.navigate(['HeroList']);
|
||||
};
|
||||
```
|
||||
@@ -956,9 +968,9 @@ respectively.
|
||||
|
||||
**How do I prevent navigation from occurring?**
|
||||
|
||||
Each **Component** can provide the `$routerCanActivate` and `$routerCanDeactivate` **Lifecycle Hooks**. The
|
||||
`$routerCanDeactivate` hook is an instance method on the **Component**. The `$routerCanActivate` hook is a
|
||||
static method defined on either the **Component Definition Object** or the **Component's** constructor function.
|
||||
Each **Component** can provide the `$canActivate` and `$routerCanDeactivate` **Lifecycle Hooks**. The
|
||||
`$routerCanDeactivate` hook is an instance method on the **Component**. The `$canActivate` hook is used as a
|
||||
static method defined on the **Component Definition Object**.
|
||||
|
||||
The **Router** will call these hooks to control navigation from one **Route** to another. Each of these hooks can
|
||||
return a `boolean` or a Promise that will resolve to a `boolean`.
|
||||
@@ -966,7 +978,7 @@ return a `boolean` or a Promise that will resolve to a `boolean`.
|
||||
During a navigation, some **Components** will become inactive and some will become active. Before the navigation
|
||||
can complete, all the **Components** must agree that they can be deactivated or activated, respectively.
|
||||
|
||||
The **Router** will call the `$routerCanDeactivate` and `$routerCanActivate` hooks, if they are provided. If any
|
||||
The **Router** will call the `$routerCanDeactivate` and `$canActivate` hooks, if they are provided. If any
|
||||
of the hooks resolve to `false` then the navigation is cancelled.
|
||||
|
||||
### Dialog Box Service
|
||||
|
||||
@@ -29,7 +29,7 @@ and link functions are unavailable
|
||||
Components can be registered using the `.component()` method of an Angular module (returned by {@link module `angular.module()`}). The method takes two arguments:
|
||||
|
||||
* The name of the Component (as string).
|
||||
* The Component config object (note that, unlike the `.directive()` method, this method does **not** take a factory function.
|
||||
* The Component config object. (Note that, unlike the `.directive()` method, this method does **not** take a factory function.)
|
||||
|
||||
<example name="heroComponentSimple" module="heroApp">
|
||||
<file name="index.js">
|
||||
@@ -133,6 +133,8 @@ components should follow a few simple conventions:
|
||||
For a deletion, that means the component doesn't delete the `hero` itself, but sends it back to
|
||||
the owner component via the correct event.
|
||||
```html
|
||||
<!-- note that we use kebab-case for bindings in the template as usual -->
|
||||
<editable-field on-update="$ctrl.update('location', value)"></editable-field><br>
|
||||
<button ng-click="$ctrl.onDelete({hero: $ctrl.hero})">Delete</button>
|
||||
```
|
||||
- That way, the parent component can decide what to do with the event (e.g. delete an item or update the properties)
|
||||
@@ -147,6 +149,30 @@ components should follow a few simple conventions:
|
||||
}
|
||||
```
|
||||
|
||||
- **Components have a well-defined lifecycle**
|
||||
Each component can implement "lifecycle hooks". These are methods that will be called at certain points in the life
|
||||
of the component. The following hook methods can be implemented:
|
||||
|
||||
* `$onInit()` - Called on each controller after all the controllers on an element have been constructed and
|
||||
had their bindings initialized (and before the pre & post linking functions for the directives on
|
||||
this element). This is a good place to put initialization code for your controller.
|
||||
* `$onChanges(changesObj)` - Called whenever one-way bindings are updated. The `changesObj` is a hash whose keys
|
||||
are the names of the bound properties that have changed, and the values are an object of the form
|
||||
`{ currentValue, previousValue, isFirstChange() }`. Use this hook to trigger updates within a component such as
|
||||
cloning the bound value to prevent accidental mutation of the outer value.
|
||||
* `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing
|
||||
external resources, watches and event handlers.
|
||||
* `$postLink()` - Called after this controller's element and its children have been linked. Similar to the post-link
|
||||
function this hook can be used to set up DOM event handlers and do direct DOM manipulation.
|
||||
Note that child elements that contain `templateUrl` directives will not have been compiled and linked since
|
||||
they are waiting for their template to load asynchronously and their own compilation and linking has been
|
||||
suspended until that occurs.
|
||||
This hook can be considered analogous to the `ngAfterViewInit` and `ngAfterContentInit` hooks in Angular 2.
|
||||
Since the compilation process is rather different in Angular 1 there is no direct mapping and care should
|
||||
be taken when upgrading.
|
||||
|
||||
By implementing these methods, your component can hook into its lifecycle.
|
||||
|
||||
- **An application is a tree of components:**
|
||||
Ideally, the whole application should be a tree of components that implement clearly defined inputs
|
||||
and outputs, and minimize two-way data binding. That way, it's easier to predict when data changes and what the state
|
||||
@@ -207,9 +233,13 @@ it upwards to the heroList component, which updates the original data.
|
||||
</file>
|
||||
|
||||
<file name="heroDetail.js">
|
||||
function HeroDetailController($scope, $element, $attrs) {
|
||||
function HeroDetailController() {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.delete = function() {
|
||||
ctrl.onDelete({hero: ctrl.hero});
|
||||
};
|
||||
|
||||
ctrl.update = function(prop, value) {
|
||||
ctrl.onUpdate({hero: ctrl.hero, prop: prop, value: value});
|
||||
};
|
||||
@@ -277,7 +307,7 @@ it upwards to the heroList component, which updates the original data.
|
||||
<div>
|
||||
Name: {{$ctrl.hero.name}}<br>
|
||||
Location: <editable-field field-value="$ctrl.hero.location" field-type="text" on-update="$ctrl.update('location', value)"></editable-field><br>
|
||||
<button ng-click="$ctrl.onDelete({hero: $ctrl.hero})">Delete</button>
|
||||
<button ng-click="$ctrl.delete()">Delete</button>
|
||||
</div>
|
||||
</file>
|
||||
<file name="editableField.html">
|
||||
@@ -417,54 +447,52 @@ angular.module('docsTabsExample', [])
|
||||
|
||||
# Unit-testing Component Controllers
|
||||
|
||||
The easiest way to unit-test a component controller is by using the {@link ngMock.$componentController $componentController}
|
||||
that is included in {@link ngMock}. The advantage of this method is that you do not have
|
||||
to create any DOM elements. The following example shows how to do this for the `heroDetail` component
|
||||
from above.
|
||||
The easiest way to unit-test a component controller is by using the
|
||||
{@link ngMock.$componentController $componentController} that is included in {@link ngMock}. The
|
||||
advantage of this method is that you do not have to create any DOM elements. The following example
|
||||
shows how to do this for the `heroDetail` component from above.
|
||||
|
||||
The examples use the [Jasmine](http://jasmine.github.io/) testing framework.
|
||||
|
||||
**Controller Test:**
|
||||
```js
|
||||
describe('component: heroDetail', function() {
|
||||
var component, scope, hero, $componentController;
|
||||
var $componentController;
|
||||
|
||||
beforeEach(module('simpleComponent'));
|
||||
|
||||
beforeEach(inject(function($rootScope, _$componentController_) {
|
||||
scope = $rootScope.$new();
|
||||
beforeEach(module('heroApp'));
|
||||
beforeEach(inject(function(_$componentController_) {
|
||||
$componentController = _$componentController_;
|
||||
hero = {name: 'Wolverine'};
|
||||
}));
|
||||
|
||||
it('should set the default values of the hero', function() {
|
||||
// It's necessary to always pass the scope in the locals, so that the controller instance can be bound to it
|
||||
component = $componentController('heroDetail', {$scope: scope});
|
||||
|
||||
expect(component.hero).toEqual({
|
||||
name: undefined,
|
||||
location: 'unknown'
|
||||
});
|
||||
});
|
||||
|
||||
it('should assign the name bindings to the hero object', function() {
|
||||
it('should expose a `hero` object', function() {
|
||||
// Here we are passing actual bindings to the component
|
||||
var bindings = {hero: {name: 'Wolverine'}};
|
||||
var ctrl = $componentController('heroDetail', null, bindings);
|
||||
|
||||
component = $componentController('heroDetail',
|
||||
{$scope: scope},
|
||||
{hero: hero}
|
||||
);
|
||||
expect(component.hero.name).toBe('Wolverine');
|
||||
expect(ctrl.hero).toBeDefined();
|
||||
expect(ctrl.hero.name).toBe('Wolverine');
|
||||
});
|
||||
|
||||
it('should call the onDelete binding when a hero is deleted', function() {
|
||||
component = $componentController('heroDetail',
|
||||
{$scope: scope},
|
||||
{hero: hero, onDelete: jasmine.createSpy('deleteSpy')}
|
||||
);
|
||||
it('should call the `onDelete` binding, when deleting the hero', function() {
|
||||
var onDeleteSpy = jasmine.createSpy('onDelete');
|
||||
var bindings = {hero: {}, onDelete: onDeleteSpy};
|
||||
var ctrl = $componentController('heroDetail', null, bindings);
|
||||
|
||||
component.onDelete({hero: component.hero});
|
||||
expect(spy('deleteSpy')).toHaveBeenCalledWith(component.hero);
|
||||
ctrl.delete();
|
||||
expect(onDeleteSpy).toHaveBeenCalledWith({hero: ctrl.hero});
|
||||
});
|
||||
|
||||
it('should call the `onUpdate` binding, when updating a property', function() {
|
||||
var onUpdateSpy = jasmine.createSpy('onUpdate');
|
||||
var bindings = {hero: {}, onUpdate: onUpdateSpy};
|
||||
var ctrl = $componentController('heroDetail', null, bindings);
|
||||
|
||||
ctrl.update('foo', 'bar');
|
||||
expect(onUpdateSpy).toHaveBeenCalledWith({
|
||||
hero: ctrl.hero,
|
||||
prop: 'foo',
|
||||
value: 'bar'
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -277,8 +277,8 @@ Now that Angular knows of all the parts of the application, it needs to create t
|
||||
In the previous section we saw that controllers are created using a factory function.
|
||||
For services there are multiple ways to define their factory
|
||||
(see the {@link services service guide}).
|
||||
In the example above, we are using a function that returns the `currencyConverter` function as the factory
|
||||
for the service.
|
||||
In the example above, we are using an anonymous function as the factory function for `currencyConverter` service.
|
||||
This function should return the `currencyConverter` service instance.
|
||||
|
||||
Back to the initial question: How does the `InvoiceController` get a reference to the `currencyConverter` function?
|
||||
In Angular, this is done by simply defining arguments on the constructor function. With this, the injector
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
@ngdoc overview
|
||||
@name Decorators
|
||||
@sortOrder 345
|
||||
@description
|
||||
|
||||
# Decorators in AngularJS
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**NOTE:** This guide is targeted towards developers who are already familiar with AngularJS basics.
|
||||
If you're just getting started, we recommend the {@link tutorial/ tutorial} first.
|
||||
</div>
|
||||
|
||||
## What are decorators?
|
||||
|
||||
Decorators are a design pattern that is used to separate modification or *decoration* of a class without modifying the
|
||||
original source code. In Angular, decorators are functions that allow a service, directive or filter to be modified
|
||||
prior to its usage.
|
||||
|
||||
## How to use decorators
|
||||
|
||||
There are two ways to register decorators
|
||||
|
||||
- `$provide.decorator`, and
|
||||
- `module.decorator`
|
||||
|
||||
Each provide access to a `$delegate`, which is the instantiated service/directive/filter, prior to being passed to the
|
||||
service that required it.
|
||||
|
||||
### $provide.decorator
|
||||
|
||||
The {@link api/auto/service/$provide#decorator decorator function} allows access to a $delegate of the service once it
|
||||
has been instantiated. For example:
|
||||
|
||||
```js
|
||||
angular.module('myApp', [])
|
||||
|
||||
.config([ '$provide', function($provide) {
|
||||
|
||||
$provide.decorator('$log', [
|
||||
'$delegate',
|
||||
function $logDecorator($delegate) {
|
||||
|
||||
var originalWarn = $delegate.warn;
|
||||
$delegate.warn = function decoratedWarn(msg) {
|
||||
msg = 'Decorated Warn: ' + msg;
|
||||
originalWarn.apply($delegate, arguments);
|
||||
};
|
||||
|
||||
return $delegate;
|
||||
}
|
||||
]);
|
||||
}]);
|
||||
```
|
||||
|
||||
After the `$log` service has been instantiated the decorator is fired. The decorator function has a `$delegate` object
|
||||
injected to provide access to the service that matches the selector in the decorator. This `$delegate` will be the
|
||||
service you are decorating. The return value of the function *provided to the decorator* will take place of the service,
|
||||
directive, or filter being decorated.
|
||||
|
||||
<hr>
|
||||
|
||||
The `$delegate` may be either modified or completely replaced. Given a service `myService` with a method `someFn`, the
|
||||
following could all be viable solutions:
|
||||
|
||||
|
||||
#### Completely Replace the $delegate
|
||||
```js
|
||||
angular.module('myApp', [])
|
||||
|
||||
.config([ '$provide', function($provide) {
|
||||
|
||||
$provide.decorator('myService', [
|
||||
'$delegate',
|
||||
function myServiceDecorator($delegate) {
|
||||
|
||||
var myDecoratedService = {
|
||||
// new service object to replace myService
|
||||
};
|
||||
return myDecoratedService;
|
||||
}
|
||||
]);
|
||||
}]);
|
||||
```
|
||||
|
||||
#### Patch the $delegate
|
||||
```js
|
||||
angular.module('myApp', [])
|
||||
|
||||
.config([ '$provide', function($provide) {
|
||||
|
||||
$provide.decorator('myService', [
|
||||
'$delegate',
|
||||
function myServiceDecorator($delegate) {
|
||||
|
||||
var someFn = $delegate.someFn;
|
||||
|
||||
function aNewFn() {
|
||||
// new service function
|
||||
someFn.apply($delegate, arguments);
|
||||
}
|
||||
|
||||
$delegate.someFn = aNewFn;
|
||||
return $delegate;
|
||||
}
|
||||
]);
|
||||
}]);
|
||||
```
|
||||
|
||||
#### Augment the $delegate
|
||||
```js
|
||||
angular.module('myApp', [])
|
||||
|
||||
.config([ '$provide', function($provide) {
|
||||
|
||||
$provide.decorator('myService', [
|
||||
'$delegate',
|
||||
function myServiceDecorator($delegate) {
|
||||
|
||||
function helperFn() {
|
||||
// an additional fn to add to the service
|
||||
}
|
||||
|
||||
$delegate.aHelpfulAddition = helperFn;
|
||||
return $delegate;
|
||||
}
|
||||
]);
|
||||
}]);
|
||||
```
|
||||
|
||||
<div class="alert alert-info">
|
||||
Note that whatever is returned by the decorator function will replace that which is being decorated. For example, a
|
||||
missing return statement will wipe out the entire object being decorated.
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
Decorators have different rules for different services. This is because services are registered in different ways.
|
||||
Services are selected by name, however filters and directives are selected by appending `"Filter"` or `"Directive"` to
|
||||
the end of the name. The `$delegate` provided is dictated by the type of service.
|
||||
|
||||
| Service Type | Selector | $delegate |
|
||||
|--------------|-------------------------------|-----------------------------------------------------------------------|
|
||||
| Service | `serviceName` | The `object` or `function` returned by the service |
|
||||
| Directive | `directiveName + 'Directive'` | An `Array.<DirectiveObject>`<sub>{@link guide/decorators#drtvArray 1}</sub> |
|
||||
| Filter | `filterName + 'Filter'` | The `function` returned by the filter |
|
||||
|
||||
<small id="drtvArray">1. Multiple directives may be registered to the same selector/name</small>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**NOTE:** Developers should take care in how and why they are modifying the `$delegate` for the service. Not only
|
||||
should expectations for the consumer be kept, but some functionality (such as directive registration) does not take
|
||||
place after decoration, but during creation/registration of the original service. This means, for example, that
|
||||
an action such as pushing a directive object to a directive `$delegate` will likely result in unexpected behavior.
|
||||
|
||||
Furthermore, great care should be taken when decorating core services, directives, or filters as this may unexpectedly
|
||||
or adversely affect the functionality of the framework.
|
||||
</div>
|
||||
|
||||
### module.decorator
|
||||
|
||||
This {@link api/ng/type/angular.Module#decorator function} is the same as the `$provide.decorator` function except it is
|
||||
exposed through the module API. This allows you to separate your decorator patterns from your module config blocks. The
|
||||
main caveat here is that you will need to take note the order in which you create your decorators.
|
||||
|
||||
Unlike in the module config block (which allows configuration of services prior to their creation), the service must be
|
||||
registered prior to the decorator (see {@link guide/providers#provider-recipe Provider Recipe}). For example, the
|
||||
following would not work because you are attempting to decorate outside of the configuration phase and the service
|
||||
hasn't been created yet:
|
||||
|
||||
```js
|
||||
// will cause an error since 'someService' hasn't been registered
|
||||
angular.module('myApp').decorator('someService', ...);
|
||||
|
||||
angular.module('myApp').factory('someService', ...);
|
||||
```
|
||||
|
||||
## Example Applications
|
||||
|
||||
The following sections provide examples each of a service decorator, a directive decorator, and a filter decorator.
|
||||
|
||||
### Service Decorator Example
|
||||
|
||||
This example shows how we can replace the $log service with our own to display log messages.
|
||||
|
||||
<example module="myServiceDecorator" name="service-decorator">
|
||||
<file name="script.js">
|
||||
angular.module('myServiceDecorator', []).
|
||||
|
||||
controller('Ctrl', [
|
||||
'$scope',
|
||||
'$log',
|
||||
'$timeout',
|
||||
function($scope, $log, $timeout) {
|
||||
var types = ['error', 'warn', 'log', 'info' ,'debug'], i;
|
||||
|
||||
for (i = 0; i < types.length; i++) {
|
||||
$log[types[i]](types[i] + ': message ' + (i + 1));
|
||||
}
|
||||
|
||||
$timeout(function() {
|
||||
$log.info('info: message logged in timeout');
|
||||
});
|
||||
}
|
||||
]).
|
||||
|
||||
directive('myLog', [
|
||||
'$log',
|
||||
function($log) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: '<ul id="myLog"><li ng-repeat="l in myLog" class="{{l.type}}">{{l.message}}</li></ul>',
|
||||
scope: {},
|
||||
compile: function() {
|
||||
return function(scope) {
|
||||
scope.myLog = $log.stack;
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
]).
|
||||
|
||||
config([
|
||||
'$provide',
|
||||
function($provide) {
|
||||
|
||||
$provide.decorator('$log', [
|
||||
'$delegate',
|
||||
function logDecorator($delegate) {
|
||||
|
||||
var myLog = {
|
||||
warn: function(msg) {
|
||||
log(msg, 'warn');
|
||||
},
|
||||
error: function(msg) {
|
||||
log(msg, 'error');
|
||||
},
|
||||
info: function(msg) {
|
||||
log(msg, 'info');
|
||||
},
|
||||
debug: function(msg) {
|
||||
log(msg, 'debug');
|
||||
},
|
||||
log: function(msg) {
|
||||
log(msg, 'log');
|
||||
},
|
||||
stack: []
|
||||
};
|
||||
|
||||
function log(msg, type) {
|
||||
myLog.stack.push({ type: type, message: msg.toString() });
|
||||
if (console && console[type]) console[type](msg);
|
||||
}
|
||||
|
||||
return myLog;
|
||||
|
||||
}
|
||||
]);
|
||||
|
||||
}
|
||||
]);
|
||||
</file>
|
||||
|
||||
<file name="index.html">
|
||||
<div ng-controller="Ctrl">
|
||||
<h1>Logs</h1>
|
||||
<my-log></my-log>
|
||||
</div>
|
||||
</file>
|
||||
|
||||
<file name="style.css">
|
||||
li.warn { color: yellow; }
|
||||
li.error { color: red; }
|
||||
li.info { color: blue }
|
||||
li.log { color: black }
|
||||
li.debug { color: green }
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should display log messages in dom', function() {
|
||||
element.all(by.repeater('l in myLog')).count().then(function(count) {
|
||||
expect(count).toEqual(6);
|
||||
});
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
|
||||
### Directive Decorator Example
|
||||
|
||||
Failed interpolated expressions in `ng-href` attributes can easily go unnoticed. We can decorate `ngHref` to warn us of
|
||||
those conditions.
|
||||
|
||||
<example module="urlDecorator" name="directive-decorator">
|
||||
<file name="script.js">
|
||||
angular.module('urlDecorator', []).
|
||||
|
||||
controller('Ctrl', ['$scope', function ($scope) {
|
||||
$scope.id = 3;
|
||||
$scope.warnCount = 0; // for testing
|
||||
}]).
|
||||
|
||||
config(['$provide', function($provide) {
|
||||
|
||||
// matchExpressions looks for interpolation markup in the directive attribute, extracts the expressions
|
||||
// from that markup (if they exist) and returns an array of those expressions
|
||||
function matchExpressions(str) {
|
||||
var exps = str.match(/{{([^}]+)}}/g);
|
||||
|
||||
// if there isn't any, get out of here
|
||||
if (exps === null) return;
|
||||
|
||||
exps = exps.map(function(exp) {
|
||||
var prop = exp.match(/[^{}]+/);
|
||||
return prop === null ? null : prop[0];
|
||||
});
|
||||
|
||||
return exps;
|
||||
}
|
||||
|
||||
// remember: directives must be selected by appending 'Directive' to the directive selector
|
||||
$provide.decorator('ngHrefDirective', [
|
||||
'$delegate',
|
||||
'$log',
|
||||
'$parse',
|
||||
function($delegate, $log, $parse) {
|
||||
|
||||
// store the original link fn
|
||||
var originalLinkFn = $delegate[0].link;
|
||||
|
||||
// replace the compile fn
|
||||
$delegate[0].compile = function(tElem, tAttr) {
|
||||
|
||||
// store the original exp in the directive attribute for our warning message
|
||||
var originalExp = tAttr.ngHref;
|
||||
|
||||
// get the interpolated expressions
|
||||
var exps = matchExpressions(originalExp);
|
||||
|
||||
// create and store the getters using $parse
|
||||
var getters = exps.map(function(el) {
|
||||
if (el) return $parse(el);
|
||||
});
|
||||
|
||||
return function newLinkFn(scope, elem, attr) {
|
||||
// fire the originalLinkFn
|
||||
originalLinkFn.apply($delegate[0], arguments);
|
||||
|
||||
// observe the directive attr and check the expressions
|
||||
attr.$observe('ngHref', function(val) {
|
||||
|
||||
// if we have getters and getters is an array...
|
||||
if (getters && angular.isArray(getters)) {
|
||||
|
||||
// loop through the getters and process them
|
||||
angular.forEach(getters, function(g, idx) {
|
||||
|
||||
// if val is truthy, then the warning won't log
|
||||
var val = angular.isFunction(g) ? g(scope) : true;
|
||||
if (!val) {
|
||||
$log.warn('NgHref Warning: "' + exps[idx] + '" in the expression "' + originalExp +
|
||||
'" is falsy!');
|
||||
|
||||
scope.warnCount++; // for testing
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
// get rid of the old link function since we return a link function in compile
|
||||
delete $delegate[0].link;
|
||||
|
||||
// return the $delegate
|
||||
return $delegate;
|
||||
|
||||
}
|
||||
|
||||
]);
|
||||
|
||||
}]);
|
||||
</file>
|
||||
|
||||
<file name="index.html">
|
||||
<div ng-controller="Ctrl">
|
||||
<a ng-href="/products/{{ id }}/view" id="id3">View Product {{ id }}</a>
|
||||
- <strong>id == 3</strong>, so no warning<br>
|
||||
<a ng-href="/products/{{ id + 5 }}/view" id="id8">View Product {{ id + 5 }}</a>
|
||||
- <strong>id + 5 == 8</strong>, so no warning<br>
|
||||
<a ng-href="/products/{{ someOtherId }}/view" id="someOtherId">View Product {{ someOtherId }}</a>
|
||||
- <strong style="background-color: #ffff00;">someOtherId == undefined</strong>, so warn<br>
|
||||
<a ng-href="/products/{{ someOtherId + 5 }}/view" id="someOtherId5">View Product {{ someOtherId + 5 }}</a>
|
||||
- <strong>someOtherId + 5 == 5</strong>, so no warning<br>
|
||||
<div>Warn Count: {{ warnCount }}</div>
|
||||
</div>
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should warn when an expression in the interpolated value is falsy', function() {
|
||||
var id3 = element(by.id('id3'));
|
||||
var id8 = element(by.id('id8'));
|
||||
var someOther = element(by.id('someOtherId'));
|
||||
var someOther5 = element(by.id('someOtherId5'));
|
||||
|
||||
expect(id3.getText()).toEqual('View Product 3');
|
||||
expect(id3.getAttribute('href')).toContain('/products/3/view');
|
||||
|
||||
expect(id8.getText()).toEqual('View Product 8');
|
||||
expect(id8.getAttribute('href')).toContain('/products/8/view');
|
||||
|
||||
expect(someOther.getText()).toEqual('View Product');
|
||||
expect(someOther.getAttribute('href')).toContain('/products//view');
|
||||
|
||||
expect(someOther5.getText()).toEqual('View Product 5');
|
||||
expect(someOther5.getAttribute('href')).toContain('/products/5/view');
|
||||
|
||||
expect(element(by.binding('warnCount')).getText()).toEqual('Warn Count: 1');
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
|
||||
### Filter Decorator Example
|
||||
|
||||
Let's say we have created an app that uses the default format for many of our `Date` filters. Suddenly requirements have
|
||||
changed (that never happens) and we need all of our default dates to be `'shortDate'` instead of `'mediumDate'`.
|
||||
|
||||
<example module="filterDecorator" name="filter-decorator">
|
||||
<file name="script.js">
|
||||
angular.module('filterDecorator', []).
|
||||
|
||||
controller('Ctrl', ['$scope', function ($scope) {
|
||||
$scope.genesis = new Date(2010, 0, 5);
|
||||
$scope.ngConf = new Date(2016, 4, 4);
|
||||
}]).
|
||||
|
||||
config(['$provide', function($provide) {
|
||||
|
||||
$provide.decorator('dateFilter', [
|
||||
'$delegate',
|
||||
function dateDecorator($delegate) {
|
||||
|
||||
// store the original filter
|
||||
var originalFilter = $delegate;
|
||||
|
||||
// return our filter
|
||||
return shortDateDefault;
|
||||
|
||||
// shortDateDefault sets the format to shortDate if it is falsy
|
||||
function shortDateDefault(date, format, timezone) {
|
||||
if (!format) format = 'shortDate';
|
||||
|
||||
// return the result of the original filter
|
||||
return originalFilter(date, format, timezone);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
]);
|
||||
|
||||
}]);
|
||||
</file>
|
||||
|
||||
<file name="index.html">
|
||||
<div ng-controller="Ctrl">
|
||||
<div id="genesis">Initial Commit default to short date: {{ genesis | date }}</div>
|
||||
<div>ng-conf 2016 default short date: {{ ngConf | date }}</div>
|
||||
<div id="ngConf">ng-conf 2016 with full date format: {{ ngConf | date:'fullDate' }}</div>
|
||||
</div>
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should default date filter to short date format', function() {
|
||||
expect(element(by.id('genesis')).getText())
|
||||
.toMatch(/Initial Commit default to short date: \d{1,2}\/\d{1,2}\/\d{2}/);
|
||||
});
|
||||
|
||||
it('should still allow dates to be formatted', function() {
|
||||
expect(element(by.id('ngConf')).getText())
|
||||
.toMatch(/ng-conf 2016 with full date format\: [A-Za-z]+, [A-Za-z]+ \d{1,2}, \d{4}/);
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
@@ -162,10 +162,6 @@ initialization work here. The function is invoked using
|
||||
{@link auto.$injector#invoke $injector.invoke} which makes it injectable just like a
|
||||
controller.
|
||||
|
||||
<div class="alert alert-success">
|
||||
**Best Practice:** Prefer using the definition object over returning a function.
|
||||
</div>
|
||||
|
||||
|
||||
We'll go over a few common examples of directives, then dive deep into the different options
|
||||
and compilation process.
|
||||
|
||||
@@ -27,6 +27,9 @@ Angular expressions are like JavaScript expressions with the following differenc
|
||||
* **Forgiving:** In JavaScript, trying to evaluate undefined properties generates `ReferenceError`
|
||||
or `TypeError`. In Angular, expression evaluation is forgiving to `undefined` and `null`.
|
||||
|
||||
* **Filters:** You can use {@link guide/filter filters} within expressions to format data before
|
||||
displaying it.
|
||||
|
||||
* **No Control Flow Statements:** You cannot use the following in an Angular expression:
|
||||
conditionals, loops, or exceptions.
|
||||
|
||||
@@ -38,10 +41,10 @@ Angular expressions are like JavaScript expressions with the following differenc
|
||||
|
||||
* **No Object Creation With New Operator:** You cannot use `new` operator in an Angular expression.
|
||||
|
||||
* **No Comma And Void Operators:** You cannot use `,` or `void` operators in an Angular expression.
|
||||
* **No Bitwise, Comma, And Void Operators:** You cannot use
|
||||
[Bitwise](https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators),
|
||||
`,` or `void` operators in an Angular expression.
|
||||
|
||||
* **Filters:** You can use {@link guide/filter filters} within expressions to format data before
|
||||
displaying it.
|
||||
|
||||
If you want to run more complex JavaScript code, you should make it a controller method and call
|
||||
the method from your view. If you want to `eval()` an Angular expression yourself, use the
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
@ngdoc overview
|
||||
@name External Resources
|
||||
@sortOrder 205
|
||||
@description
|
||||
|
||||
# External Angular 1 Resources
|
||||
|
||||
This is a collection of external, 3rd party resources for learning and developing Angular.
|
||||
|
||||
## Articles, Videos, and Projects
|
||||
|
||||
### Introductory Material
|
||||
|
||||
* [10 Reasons Why You Should Use AngularJS](http://www.sitepoint.com/10-reasons-use-angularjs/)
|
||||
* [10 Reasons Why Developers Should Learn AngularJS](http://wintellect.com/blogs/jlikness/10-reasons-web-developers-should-learn-angularjs)
|
||||
* [Design Principles of AngularJS (video)](https://www.youtube.com/watch?v=HCR7i5F5L8c)
|
||||
* [Fundamentals in 60 Minutes (video)](http://www.youtube.com/watch?v=i9MHigUZKEM)
|
||||
* [For folks with a jQuery background](http://stackoverflow.com/questions/14994391/how-do-i-think-in-angularjs-if-i-have-a-jquery-background)
|
||||
|
||||
### Specific Topics
|
||||
|
||||
#### Application Structure & Style Guides
|
||||
|
||||
* [Angular Styleguide](https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md)
|
||||
* [Architecture, file structure, components, one-way dataflow and best practices](https://github.com/toddmotto/angular-styleguide)
|
||||
* [When to use directives, controllers or services](http://kirkbushell.me/when-to-use-directives-controllers-or-services-in-angular/)
|
||||
* [Service vs Factory](http://blog.thoughtram.io/angular/2015/07/07/service-vs-factory-once-and-for-all.html)
|
||||
|
||||
#### Testing
|
||||
|
||||
* **Unit testing:** [Using Karma (video)](http://www.youtube.com/watch?v=YG5DEzaQBIc), [Karma in Webstorm](http://blog.jetbrains.com/webstorm/2013/10/running-javascript-tests-with-karma-in-webstorm-7/)
|
||||
|
||||
#### Mobile
|
||||
|
||||
* [Angular on Mobile Guide](http://www.ng-newsletter.com/posts/angular-on-mobile.html)
|
||||
* [Angular and Cordova](http://devgirl.org/2013/06/10/quick-start-guide-phonegap-and-angularjs/)
|
||||
* [Ionic Framework](http://ionicframework.com/)
|
||||
|
||||
#### Deployment
|
||||
|
||||
##### General
|
||||
|
||||
* **Javascript minification: **[Background](http://thegreenpizza.github.io/2013/05/25/building-minification-safe-angular.js-applications/), [ng-annotate automation tool](https://github.com/olov/ng-annotate)
|
||||
* **Analytics and Logging:** [Angularyitcs (Google Analytics)](http://ngmodules.org/modules/angularytics), [Angulartics (Analytics)](https://github.com/luisfarzati/angulartics), [Logging Client-Side Errors](http://www.bennadel.com/blog/2542-Logging-Client-Side-Errors-With-AngularJS-And-Stacktrace-js.htm)
|
||||
* **SEO:** [By hand](http://www.yearofmoo.com/2012/11/angularjs-and-seo.html), [prerender.io](http://prerender.io/), [Brombone](http://www.brombone.com/), [SEO.js](http://getseojs.com/), [SEO4Ajax](http://www.seo4ajax.com/)
|
||||
|
||||
##### Server-Specific
|
||||
|
||||
* **Django:** [Tutorial](http://blog.mourafiq.com/post/55034504632/end-to-end-web-app-with-django-rest-framework), [Integrating AngularJS with Django](http://django-angular.readthedocs.org/en/latest/integration.html), [Getting Started with Django Rest Framework and AngularJS](http://blog.kevinastone.com/getting-started-with-django-rest-framework-and-angularjs.html)
|
||||
* **FireBase:** [AngularFire](http://angularfire.com/), [Firebase Foundations for AngularJS](http://blog.watchandcode.com/firebase-foundations/), [Realtime Apps with AngularJS and FireBase (video)](http://www.youtube.com/watch?v=C7ZI7z7qnHU)
|
||||
* **Google Cloud Platform: **[with Cloud Endpoints](https://cloud.google.com/developers/articles/angularjs-cloud-endpoints-recipe-for-building-modern-web-applications/), [with Go](https://github.com/GoogleCloudPlatform/appengine-angular-gotodos)
|
||||
* **Hood.ie:** [60 Minutes to Awesome](http://www.roberthorvick.com/2013/06/30/todomvc-angularjs-hood-ie-60-minutes-to-awesome/)
|
||||
* **MEAN Stack: **[Blog post](http://blog.mongodb.org/post/49262866911/the-mean-stack-mongodb-expressjs-angularjs-and), [Setup](http://thecodebarbarian.wordpress.com/2013/07/22/introduction-to-the-mean-stack-part-one-setting-up-your-tools/), [GDL Video](https://developers.google.com/live/shows/913996610)
|
||||
* **Rails: **[Tutorial](http://coderberry.me/blog/2013/04/22/angularjs-on-rails-4-part-1/), [AngularJS with Rails4](https://shellycloud.com/blog/2013/10/how-to-integrate-angularjs-with-rails-4), [angularjs-rails](https://github.com/hiravgandhi/angularjs-rails)
|
||||
* **PHP: **[Building a RESTful web service](http://blog.brunoscopelliti.com/building-a-restful-web-service-with-angularjs-and-php-more-power-with-resource), [End to End with Laravel 4 (video)](http://www.youtube.com/watch?v=hqAyiqUs93c)
|
||||
* **Meteor: **[angular-meteor package](https://github.com/Urigo/angular-meteor)
|
||||
|
||||
### Other Languages
|
||||
* [ES6, Webpack, and JSPM Starter Project](https://github.com/AngularClass/NG6-starter)
|
||||
* [ES6/Typescript Best Practices](https://codepen.io/martinmcwhorter/post/angularjs-1-x-with-typescript-or-es6-best-practices)
|
||||
* [Dart](https://github.com/angular/angular.dart.tutorial/wiki)
|
||||
* [CoffeeScript Tutorial](http://www.coffeescriptlove.com/2013/08/angularjs-and-coffeescript-tutorials.html)
|
||||
|
||||
### More Topics
|
||||
|
||||
* **Security:** [video](https://www.youtube.com/watch?v=18ifoT-Id54)
|
||||
* **Internationalization and Localization:** [Creating multilingual support](http://www.novanet.no/blog/hallstein-brotan/dates/2013/10/creating-multilingual-support-using-angularjs/)
|
||||
* **Authentication/Login: **[Google example](https://developers.google.com/+/photohunt/python), [AngularJS Facebook library](https://github.com/pc035860/angular-easyfb), [Facebook example](http://blog.brunoscopelliti.com/facebook-authentication-in-your-angularjs-web-app), [authentication strategy](http://blog.brunoscopelliti.com/deal-with-users-authentication-in-an-angularjs-web-app), [unix-style authorization](http://frederiknakstad.com/authentication-in-single-page-applications-with-angular-js/)
|
||||
* **Visualization:** [SVG](http://gaslight.co/blog/angular-backed-svgs), [D3.js](http://www.ng-newsletter.com/posts/d3-on-angular.html)
|
||||
* **Realtime Communication: **[Socket.io](http://www.creativebloq.com/javascript/angularjs-collaboration-board-socketio-2132885), [OmniBinder](https://github.com/jeffbcross/omnibinder)
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
* **Getting Started:** [Comparison of the options for starting a new project](http://www.dancancro.com/comparison-of-angularjs-application-starters/)
|
||||
* **Debugging:** [Batarang](https://chrome.google.com/webstore/detail/angularjs-batarang/ighdmehidhipcmcojjgiloacoafjmpfk?hl=en)
|
||||
* **Editor support:** [Webstorm](http://plugins.jetbrains.com/plugin/6971) (and [video](http://www.youtube.com/watch?v=LJOyrSh1kDU)), [Sublime Text](https://github.com/angular-ui/AngularJS-sublime-package), [Visual Studio](http://madskristensen.net/post/angularjs-intellisense-in-visual-studio-2012)
|
||||
* **Workflow:** [Yeoman.io](https://github.com/yeoman/generator-angular) and [Angular Yeoman Tutorial](http://www.sitepoint.com/kickstart-your-angularjs-development-with-yeoman-grunt-and-bower/)
|
||||
|
||||
## Complementary Libraries
|
||||
|
||||
This is a list of libraries that enhance Angular, add common UI components or integrate with other libraries.
|
||||
You can find a larger list of Angular external libraries at [ngmodules.org](http://ngmodules.org/).
|
||||
|
||||
* **Advanced Routing:** [UI-Router](https://github.com/angular-ui/ui-router)
|
||||
* **Authentication:** [Http Auth Interceptor](https://github.com/witoldsz/angular-http-auth)
|
||||
* **Internationalization:**
|
||||
- [angular-translate](http://angular-translate.github.io)
|
||||
- [angular-gettext](http://angular-gettext.rocketeer.be/)
|
||||
- [angular-localization](http://doshprompt.github.io/angular-localization/)
|
||||
* **RESTful services:** [Restangular](https://github.com/mgonto/restangular)
|
||||
* **SQL and NoSQL backends:**
|
||||
- [BreezeJS](http://www.breezejs.com/)
|
||||
- [AngularFire](http://angularfire.com/)
|
||||
* **Data Handling**
|
||||
- Local Storage and session: [ngStorage](https://github.com/gsklee/ngStorage)
|
||||
- [angular-cache](https://github.com/jmdobry/angular-cache)
|
||||
- Data Modeling [JS-Data-Angular](https://github.com/js-data/js-data-angular)
|
||||
* **Fileupload:**
|
||||
- [ng-file-upload](https://github.com/danialfarid/ng-file-upload)
|
||||
- [blueimp-fileupload for Angular](https://blueimp.github.io/jQuery-File-Upload/angularjs.html)
|
||||
* **General UI Libraries:**
|
||||
- [Angular Material](https://material.angularjs.org/latest/)
|
||||
- [Angular UI Bootstrap](http://angular-ui.github.io/)
|
||||
- [AngularStrap for Bootstrap 3](http://mgcrea.github.io/angular-strap/)
|
||||
- [KendoUI](http://kendo-labs.github.io/angular-kendo/#/)
|
||||
- [Wijmo](http://wijmo.com/tag/angularjs-2/)
|
||||
* **Specific UI Elements:**
|
||||
- [ngInfiniteScroll](https://sroze.github.io/ngInfiniteScroll/)
|
||||
- [ngTable](https://github.com/esvit/ng-table)
|
||||
- [Angular UI Grid](http://angular-ui.github.io/grid)
|
||||
- [Toaster Notifications](https://github.com/jirikavi/AngularJS-Toaster)
|
||||
- [textAngular Rich Text Editor / contenteditable](http://textangular.com/) (Rich Text Editor /
|
||||
binding to contenteditable)
|
||||
- [Angular UI Map (Google Maps)](https://github.com/angular-ui/ui-map)
|
||||
|
||||
## General Learning Resources
|
||||
|
||||
### Books
|
||||
* [AngularJS: Up and Running](http://www.amazon.com/AngularJS-Running-Enhanced-Productivity-Structured/dp/1491901942) by Brad Green and Shyam Seshadri
|
||||
* [Mastering Web App Development](http://www.amazon.com/Mastering-Web-Application-Development-AngularJS/dp/1782161821) by Pawel Kozlowski and Pete Bacon Darwin
|
||||
* [AngularJS Directives](http://www.amazon.com/AngularJS-Directives-Alex-Vanston/dp/1783280336) by Alex Vanston
|
||||
* [Recipes With AngularJS](http://www.amazon.co.uk/Recipes-Angular-js-Frederik-Dietz-ebook/dp/B00DK95V48) by Frederik Dietz
|
||||
* [Developing an AngularJS Edge](http://www.amazon.com/Developing-AngularJS-Edge-Christopher-Hiller-ebook/dp/B00CJLFF8K) by Christopher Hiller
|
||||
* [ng-book: The Complete Book on AngularJS](http://ng-book.com/) by Ari Lerner
|
||||
* [AngularJS : Novice to Ninja](http://www.amazon.in/AngularJS-Novice-Ninja-Sandeep-Panda/dp/0992279453) by Sandeep Panda
|
||||
* [AngularJS UI Development](http://www.amazon.com/AngularJS-UI-Development-Amit-Ghart-ebook/dp/B00OXVAK7A) by Amit Gharat and Matthias Nehlsen
|
||||
* [Responsive Web Design with AngularJS](http://www.amazon.com/Responsive-Design-AngularJS-Sandeep-Kumar/dp/178439842X) by Sandeep Kumar Patel
|
||||
* [Professional AngularJS](http://www.amazon.com/Professional-AngularJS-Valeri-Karpov/dp/1118832078/)
|
||||
|
||||
### Videos:
|
||||
* [egghead.io](http://egghead.io/)
|
||||
|
||||
### Courses
|
||||
* **Free online:**
|
||||
[thinkster.io](http://thinkster.io),
|
||||
[CodeAcademy](http://www.codecademy.com/courses/javascript-advanced-en-2hJ3J/0/1),
|
||||
[CodeSchool](https://www.codeschool.com/courses/shaping-up-with-angular-js)
|
||||
* **Paid online:**
|
||||
[The Angular Course (115 videos that show you how to build a full app)](http://watchandcode.com/courses/angular-course/),
|
||||
[Pluralsite (3 courses)](http://www.pluralsight.com/training/Courses/Find?highlight=true&searchTerm=angularjs),
|
||||
[Tuts+](https://tutsplus.com/course/easier-js-apps-with-angular/),
|
||||
[lynda.com](http://www.lynda.com/AngularJS-tutorials/Up-Running-AngularJS/133318-2.html),
|
||||
[WintellectNOW (4 lessons)](http://www.wintellectnow.com/Course/Detail/mastering-angularjs)
|
||||
* **Paid onsite:**
|
||||
[angularbootcamp.com](http://angularbootcamp.com/)
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
# Filters
|
||||
|
||||
A filter formats the value of an expression for display to the user. They can be used in view templates,
|
||||
controllers or services and it is easy to define your own filter.
|
||||
Filters format the value of an expression for display to the user. They can be used in view
|
||||
templates, controllers or services. Angular comes with a collection of
|
||||
[built-in filters](api/ng/filter), but it is easy to define your own as well.
|
||||
|
||||
The underlying API is the {@link ng.$filterProvider `filterProvider`}.
|
||||
The underlying API is the {@link ng.$filterProvider}.
|
||||
|
||||
## Using filters in view templates
|
||||
|
||||
@@ -31,6 +32,21 @@ Filters may have arguments. The syntax for this is
|
||||
E.g. the markup `{{ 1234 | number:2 }}` formats the number 1234 with 2 decimal points using the
|
||||
{@link ng.filter:number `number`} filter. The resulting value is `1,234.00`.
|
||||
|
||||
### When filters are executed
|
||||
|
||||
In templates, filters are only executed when their inputs have changed. This is more performant than executing
|
||||
a filter on each {@link ng.$rootScope.Scope#$digest `$digest`} as is the case with {@link guide/expression expressions}.
|
||||
|
||||
There are two exceptions to this rule:
|
||||
|
||||
1. In general, this applies only to filters that take [primitive values](https://developer.mozilla.org/docs/Glossary/Primitive)
|
||||
as inputs. Filters that receive [Objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Objects)
|
||||
as input are executed on each `$digest`, as it would be too costly to track if the inputs have changed.
|
||||
|
||||
2. Filters that are marked as `$stateful` are also executed on each $digest.
|
||||
See {@link guide/filter#stateful-filters Stateful filters} for more information. Note that no Angular
|
||||
core filters are $stateful.
|
||||
|
||||
|
||||
## Using filters in controllers, services, and directives
|
||||
|
||||
@@ -93,8 +109,9 @@ as the first argument. Any filter arguments are passed in as additional argument
|
||||
function.
|
||||
|
||||
The filter function should be a [pure function](http://en.wikipedia.org/wiki/Pure_function), which
|
||||
means that it should be stateless and idempotent. Angular relies on these properties and executes
|
||||
the filter only when the inputs to the function change.
|
||||
means that it should be stateless and idempotent, and not rely for example on other Angular services.
|
||||
Angular relies on this contract and will by default execute a filter only when the inputs to the function change.
|
||||
{@link guide/filter#stateful-filters Stateful filters} are possible, but less performant.
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`.
|
||||
@@ -141,7 +158,7 @@ text upper-case.
|
||||
</example>
|
||||
|
||||
|
||||
## Stateful filters
|
||||
### Stateful filters
|
||||
|
||||
It is strongly discouraged to write filters that are stateful, because the execution of those can't
|
||||
be optimized by Angular, which often leads to performance issues. Many stateful filters can be
|
||||
@@ -183,4 +200,4 @@ means that it will be executed one or more times during the each `$digest` cycle
|
||||
|
||||
## Testing custom filters
|
||||
|
||||
See the [phonecat tutorial](http://docs.angularjs.org/tutorial/step_09#test) for an example.
|
||||
See the [phonecat tutorial](http://docs.angularjs.org/tutorial/step_11#testing) for an example.
|
||||
|
||||
@@ -29,6 +29,13 @@ To ensure your Angular application works on IE please consider:
|
||||
|
||||
1. Use `ng-style` tags instead of `style="{{ someCss }}"`. The latter works in Chrome and Firefox
|
||||
but does not work in Internet Explorer <= 11 (the most recent version at time of writing).
|
||||
2. For the `type` attribute of buttons, use `ng-attr-type` tags instead of
|
||||
`type="{{ someExpression }}"`. If using the latter, Internet Explorer overwrites the expression
|
||||
2. For the `type` attribute of buttons, use `ng-attr-type` tags instead of
|
||||
`type="{{ someExpression }}"`. If using the latter, Internet Explorer overwrites the expression
|
||||
with `type="submit"` before Angular has a chance to interpolate it.
|
||||
3. For the `value` attribute of progress, use `ng-attr-value` tags instead of
|
||||
`value="{{ someExpression}}"`. If using the latter, Internet Explorer overwrites the expression
|
||||
with `value="0"` before Angular has a chance to interpolate it.
|
||||
4. For the `placeholder` attribute of textarea, use `ng-attr-placeholder` tags instead
|
||||
of `placeholder="{{ someExpression }}"`. If using the latter, Internet Explorer will error
|
||||
on accessing the `nodeValue` on a parentless `TextNode` in Internet Explorer 10 & 11
|
||||
(see [issue 5025](https://github.com/angular/angular.js/issues/5025)).
|
||||
|
||||
@@ -2,22 +2,21 @@
|
||||
@name Developer Guide
|
||||
@description
|
||||
|
||||
# Guide to AngularJS Documentation
|
||||
# Guide to Angular 1 Documentation
|
||||
|
||||
Everything you need to know about AngularJS
|
||||
On this page, you will find a list of official Angular resources on various topics.
|
||||
|
||||
* {@link guide/introduction What is AngularJS?}
|
||||
Just starting out with Angular 1? Try working through our step by step tutorial or try
|
||||
building on our seed project.
|
||||
|
||||
* {@link tutorial/index Official Angular 1 Tutorial}
|
||||
* [Angular Seed](https://github.com/angular/angular-seed)
|
||||
|
||||
Ready to find out more about Angular 1?
|
||||
|
||||
* {@link guide/introduction What is Angular 1?}
|
||||
* {@link guide/concepts Conceptual Overview}
|
||||
|
||||
## Tutorials
|
||||
|
||||
* {@link tutorial/index Official AngularJS Tutorial}
|
||||
* [10 Reasons Why You Should Use AngularJS](http://www.sitepoint.com/10-reasons-use-angularjs/)
|
||||
* [10 Reasons Why Developers Should Learn AngularJS](http://wintellect.com/blogs/jlikness/10-reasons-web-developers-should-learn-angularjs)
|
||||
* [Design Principles of AngularJS (video)](https://www.youtube.com/watch?v=HCR7i5F5L8c)
|
||||
* [Fundamentals in 60 Minutes (video)](http://www.youtube.com/watch?v=i9MHigUZKEM)
|
||||
* [For folks with a jQuery background](http://stackoverflow.com/questions/14994391/how-do-i-think-in-angularjs-if-i-have-a-jquery-background)
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Templates
|
||||
@@ -26,134 +25,68 @@ In Angular applications, you move the job of filling page templates with data fr
|
||||
|
||||
* {@link guide/databinding Data binding}
|
||||
* {@link guide/expression Expressions}
|
||||
* {@link guide/interpolation Interpolation}
|
||||
* {@link guide/directive Directives}
|
||||
* {@link ngRoute.$route Views and routes (see the example)}
|
||||
* {@link guide/filter Filters}
|
||||
* {@link guide/forms Forms} and [Concepts of AngularJS Forms](http://mrbool.com/the-concepts-of-angularjs-forms/29117)
|
||||
* {@link guide/compiler HTML compiler}
|
||||
* {@link guide/forms Forms}
|
||||
|
||||
### Application Structure
|
||||
|
||||
* **Blog post: **[When to use directives, controllers or services](http://kirkbushell.me/when-to-use-directives-controllers-or-services-in-angular/)
|
||||
* **App wiring:** {@link guide/di Dependency injection}
|
||||
* **Exposing model to templates:** {@link guide/scope Scopes}
|
||||
* **Bootstrap:** {@link guide/bootstrap Bootstrapping an app}
|
||||
* **Communicating with servers:** {@link ng.$http $http}, {@link ngResource.$resource $resource}
|
||||
|
||||
### Other AngularJS Features
|
||||
### Other Features
|
||||
|
||||
* **Animation:** {@link guide/animations Core concepts}, {@link ngAnimate ngAnimate API}, and [Animation in AngularJS 1.2](http://www.yearofmoo.com/2013/08/remastered-animation-in-angularjs-1-2.html)
|
||||
* **Animation:** {@link guide/animations Core concepts}, {@link ngAnimate ngAnimate API}
|
||||
* **Security:** {@link guide/security Security Docs}, {@link ng.$sce Strict Contextual Escaping}, {@link ng.directive:ngCsp Content Security Policy}, {@link ngSanitize.$sanitize $sanitize}, [video](https://www.youtube.com/watch?v=18ifoT-Id54)
|
||||
* **Internationalization and Localization:** {@link guide/i18n Angular Guide to i18n and l10n}, {@link ng.filter:date date filter}, {@link ng.filter:currency currency filter}, [Creating multilingual support](http://www.novanet.no/blog/hallstein-brotan/dates/2013/10/creating-multilingual-support-using-angularjs/)
|
||||
* **Mobile:** {@link ngTouch Touch events}
|
||||
* **Touch events:** {@link ngTouch Touch events}
|
||||
* **Accessibility:** {@link guide/accessibility ngAria}
|
||||
|
||||
### Testing
|
||||
|
||||
* **Unit testing:** [Using Karma (video)](http://www.youtube.com/watch?v=YG5DEzaQBIc), {@link guide/unit-testing Unit testing}, {@link guide/services#unit-testing Testing services}, [Karma in Webstorm](http://blog.jetbrains.com/webstorm/2013/10/running-javascript-tests-with-karma-in-webstorm-7/)
|
||||
* **Scenario testing:** [Protractor](https://github.com/angular/protractor)
|
||||
* **Unit testing:** [Karma](http://karma-runner.github.io), {@link guide/unit-testing Unit testing}, {@link guide/services#unit-testing Testing services},
|
||||
* **End-to-End Testing:** [Protractor](https://github.com/angular/protractor), {@link guide/e2e-testing e2e testing guide}
|
||||
|
||||
## Specific Topics
|
||||
|
||||
* **Login: **[Google example](https://developers.google.com/+/photohunt/python), [AngularJS Facebook library](https://github.com/pc035860/angular-easyfb), [Facebook example](http://blog.brunoscopelliti.com/facebook-authentication-in-your-angularjs-web-app), [authentication strategy](http://blog.brunoscopelliti.com/deal-with-users-authentication-in-an-angularjs-web-app), [unix-style authorization](http://frederiknakstad.com/authentication-in-single-page-applications-with-angular-js/)
|
||||
* **Mobile:** [Angular on Mobile Guide](http://www.ng-newsletter.com/posts/angular-on-mobile.html), [PhoneGap](http://devgirl.org/2013/06/10/quick-start-guide-phonegap-and-angularjs/)
|
||||
* **Other Languages:** [CoffeeScript](http://www.coffeescriptlove.com/2013/08/angularjs-and-coffeescript-tutorials.html), [Dart](https://github.com/angular/angular.dart.tutorial/wiki)
|
||||
* **Realtime: **[Socket.io](http://www.creativebloq.com/javascript/angularjs-collaboration-board-socketio-2132885), [OmniBinder](https://github.com/jeffbcross/omnibinder)
|
||||
* **Visualization:** [SVG](http://gaslight.co/blog/angular-backed-svgs), [D3.js](http://www.ng-newsletter.com/posts/d3-on-angular.html)
|
||||
* **Local Storage and session:** [ngStorage](https://github.com/gsklee/ngStorage)
|
||||
## Community Resources
|
||||
|
||||
## Tools
|
||||
We have set up a guide to many resources provided by the community, where you can find lots
|
||||
of additional information and material on these topics, a list of complimentary libraries, and much more.
|
||||
|
||||
* **Getting Started:** [Comparison of the options for starting a new project](http://www.dancancro.com/comparison-of-angularjs-application-starters/)
|
||||
* **Debugging:** [Batarang](https://chrome.google.com/webstore/detail/angularjs-batarang/ighdmehidhipcmcojjgiloacoafjmpfk?hl=en)
|
||||
* **Testing:** [Karma](http://karma-runner.github.io), [Protractor](https://github.com/angular/protractor)
|
||||
* **Editor support:** [Webstorm](http://plugins.jetbrains.com/plugin/6971) (and [video](http://www.youtube.com/watch?v=LJOyrSh1kDU)), [Sublime Text](https://github.com/angular-ui/AngularJS-sublime-package), [Visual Studio](http://madskristensen.net/post/angularjs-intellisense-in-visual-studio-2012)
|
||||
* **Workflow:** [Yeoman.io](https://github.com/yeoman/generator-angular) and [Angular Yeoman Tutorial](http://www.sitepoint.com/kickstart-your-angularjs-development-with-yeoman-grunt-and-bower/)
|
||||
* {@link guide/external-resources External Angular 1 resources}
|
||||
|
||||
## Complementary Libraries
|
||||
|
||||
This is a short list of libraries with specific support and documentation for working with Angular. You can find a full list of all known Angular external libraries at [ngmodules.org](http://ngmodules.org/).
|
||||
|
||||
* **Internationalization:** [angular-translate](http://angular-translate.github.io), [angular-gettext](http://angular-gettext.rocketeer.be/), [angular-localization](http://doshprompt.github.io/angular-localization/)
|
||||
* **RESTful services:** [Restangular](https://github.com/mgonto/restangular)
|
||||
* **SQL and NoSQL backends:** [BreezeJS](http://www.breezejs.com/), [AngularFire](http://angularfire.com/)
|
||||
* **UI Widgets: **[KendoUI](http://kendo-labs.github.io/angular-kendo/#/), [UI Bootstrap](http://angular-ui.github.io/bootstrap/), [Wijmo](http://wijmo.com/tag/angularjs-2/), [ngTagsInput](https://github.com/mbenford/ngTagsInput)
|
||||
* **Advanced Routing:** [UI-Router](https://github.com/angular-ui/ui-router)
|
||||
* **Maps:** [UI-Map (Google Maps)](https://github.com/angular-ui/ui-map)
|
||||
|
||||
## Deployment
|
||||
|
||||
### General
|
||||
|
||||
* **Docs Page:** {@link guide/production Running an AngularJS App in Production}
|
||||
* **Javascript minification: **[Background](http://thegreenpizza.github.io/2013/05/25/building-minification-safe-angular.js-applications/), [ng-annotate automation tool](https://github.com/olov/ng-annotate)
|
||||
* **Analytics and Logging:** [Angularyitcs (Google Analytics)](http://ngmodules.org/modules/angularytics), [Angulartics (Analytics)](https://github.com/luisfarzati/angulartics), [Logging Client-Side Errors](http://www.bennadel.com/blog/2542-Logging-Client-Side-Errors-With-AngularJS-And-Stacktrace-js.htm)
|
||||
* **SEO:** [By hand](http://www.yearofmoo.com/2012/11/angularjs-and-seo.html), [prerender.io](http://prerender.io/), [Brombone](http://www.brombone.com/), [SEO.js](http://getseojs.com/), [SEO4Ajax](http://www.seo4ajax.com/)
|
||||
|
||||
### Server-Specific
|
||||
|
||||
* **Django:** [Tutorial](http://blog.mourafiq.com/post/55034504632/end-to-end-web-app-with-django-rest-framework), [Integrating AngularJS with Django](http://django-angular.readthedocs.org/en/latest/integration.html), [Getting Started with Django Rest Framework and AngularJS](http://blog.kevinastone.com/getting-started-with-django-rest-framework-and-angularjs.html)
|
||||
* **FireBase:** [AngularFire](http://angularfire.com/), [Firebase Foundations for AngularJS](http://blog.watchandcode.com/firebase-foundations/), [Realtime Apps with AngularJS and FireBase (video)](http://www.youtube.com/watch?v=C7ZI7z7qnHU)
|
||||
* **Google Cloud Platform: **[with Cloud Endpoints](https://cloud.google.com/developers/articles/angularjs-cloud-endpoints-recipe-for-building-modern-web-applications/), [with Go](https://github.com/GoogleCloudPlatform/appengine-angular-gotodos)
|
||||
* **Hood.ie:** [60 Minutes to Awesome](http://www.roberthorvick.com/2013/06/30/todomvc-angularjs-hood-ie-60-minutes-to-awesome/)
|
||||
* **MEAN Stack: **[Blog post](http://blog.mongodb.org/post/49262866911/the-mean-stack-mongodb-expressjs-angularjs-and), [Setup](http://thecodebarbarian.wordpress.com/2013/07/22/introduction-to-the-mean-stack-part-one-setting-up-your-tools/), [GDL Video](https://developers.google.com/live/shows/913996610)
|
||||
* **Rails: **[Tutorial](http://coderberry.me/blog/2013/04/22/angularjs-on-rails-4-part-1/), [AngularJS with Rails4](https://shellycloud.com/blog/2013/10/how-to-integrate-angularjs-with-rails-4), [angularjs-rails](https://github.com/hiravgandhi/angularjs-rails)
|
||||
* **PHP: **[Building a RESTful web service](http://blog.brunoscopelliti.com/building-a-restful-web-service-with-angularjs-and-php-more-power-with-resource), [End to End with Laravel 4 (video)](http://www.youtube.com/watch?v=hqAyiqUs93c)
|
||||
* **Meteor: **[angular-meteor package](https://github.com/Urigo/angular-meteor)
|
||||
|
||||
## Learning Resources
|
||||
|
||||
###Books
|
||||
* [AngularJS: Up and Running](http://www.amazon.com/AngularJS-Running-Enhanced-Productivity-Structured/dp/1491901942) by Brad Green and Shyam Seshadri
|
||||
* [Mastering Web App Development](http://www.amazon.com/Mastering-Web-Application-Development-AngularJS/dp/1782161821) by Pawel Kozlowski and Pete Bacon Darwin
|
||||
* [AngularJS Directives](http://www.amazon.com/AngularJS-Directives-Alex-Vanston/dp/1783280336) by Alex Vanston
|
||||
* [Recipes With AngularJS](http://www.amazon.co.uk/Recipes-Angular-js-Frederik-Dietz-ebook/dp/B00DK95V48) by Frederik Dietz
|
||||
* [Developing an AngularJS Edge](http://www.amazon.com/Developing-AngularJS-Edge-Christopher-Hiller-ebook/dp/B00CJLFF8K) by Christopher Hiller
|
||||
* [ng-book: The Complete Book on AngularJS](http://ng-book.com/) by Ari Lerner
|
||||
* [AngularJS : Novice to Ninja](http://www.amazon.in/AngularJS-Novice-Ninja-Sandeep-Panda/dp/0992279453) by Sandeep Panda
|
||||
* [AngularJS UI Development](http://www.amazon.com/AngularJS-UI-Development-Amit-Ghart-ebook/dp/B00OXVAK7A) by Amit Gharat and Matthias Nehlsen
|
||||
* [Responsive Web Design with AngularJS](http://www.amazon.com/Responsive-Design-AngularJS-Sandeep-Kumar/dp/178439842X) by Sandeep Kumar Patel
|
||||
* [Professional AngularJS](http://www.amazon.com/Professional-AngularJS-Valeri-Karpov/dp/1118832078/)
|
||||
|
||||
###Videos:
|
||||
* [egghead.io](http://egghead.io/)
|
||||
* [Angular on YouTube](http://youtube.com/angularjs)
|
||||
* [Firebase Foundations for AngularJS](http://blog.watchandcode.com/firebase-foundations/)
|
||||
|
||||
### Courses
|
||||
* **Free online:**
|
||||
[thinkster.io](http://thinkster.io),
|
||||
[CodeAcademy](http://www.codecademy.com/courses/javascript-advanced-en-2hJ3J/0/1),
|
||||
[CodeSchool](https://www.codeschool.com/courses/shaping-up-with-angular-js)
|
||||
* **Paid online:**
|
||||
[The Angular Course (115 videos that show you how to build a full app)](http://watchandcode.com/courses/angular-course/),
|
||||
[Pluralsite (3 courses)](http://www.pluralsight.com/training/Courses/Find?highlight=true&searchTerm=angularjs),
|
||||
[Tuts+](https://tutsplus.com/course/easier-js-apps-with-angular/),
|
||||
[lynda.com](http://www.lynda.com/AngularJS-tutorials/Up-Running-AngularJS/133318-2.html),
|
||||
[WintellectNOW (4 lessons)](http://www.wintellectnow.com/Course/Detail/mastering-angularjs)
|
||||
* **Paid onsite:**
|
||||
[angularbootcamp.com](http://angularbootcamp.com/)
|
||||
|
||||
## Getting Help
|
||||
|
||||
The recipe for getting help on your unique issue is to create an example that could work (even if it doesn't) in a shareable example on [Plunker](http://plnkr.co/), [JSFiddle](http://jsfiddle.net/), or similar site and then post to one of the following:
|
||||
|
||||
* [Stackoverflow.com](http://stackoverflow.com/search?q=angularjs)
|
||||
* [AngularJS mailing list](https://groups.google.com/forum/#!forum/angular)
|
||||
* [AngularJS IRC channel](http://webchat.freenode.net/?channels=angularjs&uio=d4)
|
||||
* [Angular 1 mailing list](https://groups.google.com/forum/#!forum/angular)
|
||||
* [Angular 1 IRC channel](http://webchat.freenode.net/?channels=angularjs&uio=d4)
|
||||
|
||||
## Social Channels
|
||||
## Official Communications
|
||||
|
||||
* **Daily updates:** [Google+](https://plus.google.com/u/0/+AngularJS) or [Twitter](https://twitter.com/angularjs)
|
||||
* **Weekly newsletter:** [ng-newsletter](http://www.ng-newsletter.com/)
|
||||
* **Meetups: **[meetup.com](http://www.meetup.com/find/?keywords=angularJS&radius=Infinity&userFreeform=San+Francisco%2C+CA&mcId=z94108&mcName=San+Francisco%2C+CA&sort=member_count&eventFilter=mysugg)
|
||||
* **Official news and releases: **[AngularJS Blog](http://blog.angularjs.org/)
|
||||
Official announcements, news and releases are posted to our blog, G+ and Twitter:
|
||||
|
||||
## Contributing to AngularJS
|
||||
* [Angular Blog](http://blog.angularjs.org/)
|
||||
* [Google+](https://plus.google.com/u/0/+AngularJS)
|
||||
* [Twitter](https://twitter.com/angularjs)
|
||||
* [Angular on YouTube](http://youtube.com/angularjs)
|
||||
|
||||
Though we have a core group of core contributors at Google, Angular is an open source project with hundreds of contributors. We'd love you to be one of them. When you're ready, please read the {@link misc/contribute Guide for contributing to AngularJS}.
|
||||
## Contributing to Angular 1
|
||||
|
||||
## Final Bits
|
||||
Though we have a core group of core contributors at Google, Angular is an open source project with hundreds of contributors.
|
||||
We'd love you to be one of them. When you're ready, please read the {@link misc/contribute Guide for contributing to Angular}.
|
||||
|
||||
Didn't find what you're looking for here? Check out [AngularJS-Learning](https://github.com/jmcunningham/AngularJS-Learning) for an even more comprehensive list of links to videos, tutorials, and blog posts.
|
||||
## Something Missing?
|
||||
|
||||
If you have awesome AngularJS resources that belong on this page, please tell us about them on [Google+](https://plus.google.com/u/0/+AngularJS) or [Twitter](https://twitter.com/angularjs).
|
||||
Didn't find what you're looking for here? Check out the {@link guide/external-resources External Angular 1 resources guide}.
|
||||
|
||||
If you have awesome Angular 1 resources that belong on that page, please tell us about them on
|
||||
[Google+](https://plus.google.com/u/0/+AngularJS) or [Twitter](https://twitter.com/angularjs).
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ would be lost, because the browser ignores the attribute value.
|
||||
In the following example, the interpolation information would be ignored and the browser would simply
|
||||
interpret the attribute as present, meaning that the button would always be disabled.
|
||||
|
||||
```html
|
||||
```html
|
||||
Disabled: <input type="checkbox" ng-model="isDisabled" />
|
||||
<button disabled="{{isDisabled}}">Disabled</button>
|
||||
```
|
||||
@@ -49,7 +49,7 @@ For this reason, Angular provides special `ng`-prefixed directives for the follo
|
||||
These directives take an expression inside the attribute, and set the corresponding boolean attribute
|
||||
to true when the expression evaluates to truthy.
|
||||
|
||||
```html
|
||||
```html
|
||||
Disabled: <input type="checkbox" ng-model="isDisabled" />
|
||||
<button ng-disabled="isDisabled">Disabled</button>
|
||||
```
|
||||
@@ -104,7 +104,20 @@ can be used with `ngAttr` instead. The following is a list of known problematic
|
||||
- **size** in `<select>` elements (see [issue 1619](https://github.com/angular/angular.js/issues/1619))
|
||||
- **placeholder** in `<textarea>` in Internet Explorer 10/11 (see [issue 5025](https://github.com/angular/angular.js/issues/5025))
|
||||
- **type** in `<button>` in Internet Explorer 11 (see [issue 14117](https://github.com/angular/angular.js/issues/5025))
|
||||
- **value** in `<progress>` in Internet Explorer = 11 (see [issue 7218](https://github.com/angular/angular.js/issues/7218))
|
||||
|
||||
## Known Issues
|
||||
|
||||
### Dynamically changing an interpolated value
|
||||
|
||||
You should avoid dynamically changing the content of an interpolated string (e.g. attribute value
|
||||
or text node). Your changes are likely to be overwriten, when the original string gets evaluated.
|
||||
This restriction applies to both directly changing the content via JavaScript or indirectly using a
|
||||
directive.
|
||||
|
||||
For example, you should not use interpolation in the value of the `style` attribute (e.g.
|
||||
`style="color: {{ 'orange' }}; font-weight: {{ 'bold' }};"`) **and** at the same time use a
|
||||
directive that changes the content of that attributte, such as `ngStyle`.
|
||||
|
||||
### Embedding interpolation markup inside expressions
|
||||
|
||||
|
||||
@@ -514,7 +514,7 @@ angular.module('myApp').directive('form', function() {
|
||||
});
|
||||
```
|
||||
|
||||
### Templating (`ngRepeat`, `$compile`)
|
||||
### Templating (`ngRepeat`, `$compile`, `ngInclude`)
|
||||
|
||||
#### ngRepeat
|
||||
|
||||
@@ -545,6 +545,44 @@ Due to [62d514b](https://github.com/angular/angular.js/commit/62d514b06937cc7dd8
|
||||
returning an object from a controller constructor function will now override the scope. Views that use the
|
||||
controllerAs method will no longer get the this reference, but the returned object.
|
||||
|
||||
#### ngInclude
|
||||
Due to [3c6e8ce044446735eb2e70d0061db8c6db050289](https://github.com/angular/angular.js/commit/3c6e8ce044446735eb2e70d0061db8c6db050289), the `src` attribute of ngInclude no longer accepts an
|
||||
expression that returns the result of `$sce.trustAsResourceUrl`. This will now cause an infinite digest:
|
||||
|
||||
Before:
|
||||
```html
|
||||
<div ng-include="findTemplate('https://example.com/templates/myTemplate.html')"></div>
|
||||
```
|
||||
|
||||
```js
|
||||
$scope.findTemplate = function(templateName) {
|
||||
return $sce.trustAsResourceUrl(templateName);
|
||||
};
|
||||
```
|
||||
|
||||
To migrate, either cache the result of `trustAsResourceUrl()`, or put the template url in the resource
|
||||
whitelist in the `config()` function:
|
||||
|
||||
After:
|
||||
|
||||
```js
|
||||
var templateCache = {};
|
||||
$scope.findTemplate = function(templateName) {
|
||||
if (!templateCache[templateName]) {
|
||||
templateCache[templateName] = $sce.trustAsResourceUrl(templateName);
|
||||
}
|
||||
|
||||
return templateCache[templateName];
|
||||
};
|
||||
|
||||
// Alternatively, use `$sceDelegateProvider.resourceUrlWhitelist()`, which means you don't
|
||||
// have to use `$sce.trustAsResourceUrl()` at all:
|
||||
|
||||
angular.module('myApp', []).config(function($sceDelegateProvider) {
|
||||
$sceDelegateProvider.resourceUrlWhitelist(['self', 'https://example.com/templates/**'])
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
### Cookies (`ngCookies`)
|
||||
|
||||
@@ -583,8 +621,6 @@ has been moved to `$cookies`, to which `$cookieStore` now simply
|
||||
delegates calls.
|
||||
|
||||
|
||||
|
||||
|
||||
### Server Requests (`$http`)
|
||||
|
||||
Due to [5da1256](https://github.com/angular/angular.js/commit/5da1256fc2812d5b28fb0af0de81256054856369),
|
||||
@@ -616,8 +652,6 @@ $http.get(url, {
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
### Filters (`filter`, `limitTo`)
|
||||
|
||||
#### `filter` filter
|
||||
@@ -636,7 +670,6 @@ Now, instead of returning empty object/array, it returns unchanged input.
|
||||
|
||||
|
||||
|
||||
|
||||
## Migrating from 1.2 to 1.3
|
||||
|
||||
### Controllers
|
||||
|
||||
@@ -75,8 +75,8 @@ that you break your application to multiple modules like this:
|
||||
* And an application level module which depends on the above modules and contains any
|
||||
initialization code.
|
||||
|
||||
You can find a community
|
||||
[style guide](https://github.com/johnpapa/angular-styleguide) to help yourself when application grows.
|
||||
You can find a community [style guide](https://github.com/johnpapa/angular-styleguide) to help
|
||||
yourself when application grows.
|
||||
|
||||
The above is a suggestion. Tailor it to your needs.
|
||||
|
||||
|
||||
@@ -257,7 +257,7 @@ the `$digest` phase. This delay is desirable, since it coalesces multiple model
|
||||
|
||||
2. **Watcher registration**
|
||||
|
||||
During template linking directives register {@link
|
||||
During template linking, directives register {@link
|
||||
ng.$rootScope.Scope#$watch watches} on the scope. These watches will be
|
||||
used to propagate model values to the DOM.
|
||||
|
||||
|
||||
@@ -87,6 +87,10 @@ Protection from JSON Hijacking is provided if the server prefixes all JSON reque
|
||||
Angular will automatically strip the prefix before processing it as JSON.
|
||||
For more information please visit {@link $http#json-vulnerability-protection JSON Hijacking Protection}.
|
||||
|
||||
Bear in mind that calling `$http.jsonp`, like in [our Yahoo! finance example](https://docs.angularjs.org/guide/concepts#accessing-the-backend),
|
||||
gives the remote server (and, if the request is not secured, any Man-in-the-Middle attackers)
|
||||
instant remote code execution in your application: the result of these requests is handed off
|
||||
to the browser as regular `<script>` tag.
|
||||
|
||||
## Strict Contextual Escaping
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ subsystem takes care of the rest.
|
||||
<file name="script.js">
|
||||
angular.
|
||||
module('myServiceModule', []).
|
||||
controller('MyController', ['$scope','notify', function ($scope, notify) {
|
||||
controller('MyController', ['$scope', 'notify', function ($scope, notify) {
|
||||
$scope.callNotify = function(msg) {
|
||||
notify(msg);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
@sortOrder 410
|
||||
@description
|
||||
|
||||
# Unit Testing
|
||||
|
||||
JavaScript is a dynamically typed language which comes with great power of expression, but it also
|
||||
comes 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
|
||||
|
||||
@@ -26,9 +26,9 @@ machine:
|
||||
* [Git](http://git-scm.com/): The [Github Guide to
|
||||
Installing Git](https://help.github.com/articles/set-up-git) is a good source of information.
|
||||
|
||||
* [Node.js](http://nodejs.org): We use Node to generate the documentation, run a
|
||||
* [Node.js v4.x](http://nodejs.org): We use Node to generate the documentation, run a
|
||||
development web server, run tests, and generate distributable files. Depending on your system, you can install Node either from source or as a
|
||||
pre-packaged bundle.
|
||||
pre-packaged bundle. (Currently our build does not work properly on Node v5 or greater - please use v4.x.)
|
||||
|
||||
* [Java](http://www.java.com): We minify JavaScript using our
|
||||
[Closure Tools](https://developers.google.com/closure/) jar. Make sure you have Java (version 7 or higher) installed
|
||||
@@ -40,15 +40,6 @@ and included in your [PATH](http://docs.oracle.com/javase/tutorial/essential/env
|
||||
npm install -g grunt-cli
|
||||
```
|
||||
|
||||
* [Bower](http://bower.io/): We use Bower to manage client-side packages for the docs. Install the `bower` command-line tool globally with:
|
||||
|
||||
```shell
|
||||
npm install -g bower
|
||||
```
|
||||
|
||||
**Note:** You may need to use sudo (for OSX, *nix, BSD etc) or run your command shell as Administrator (for Windows) to install Grunt &
|
||||
Bower globally.
|
||||
|
||||
## Forking Angular on Github
|
||||
|
||||
To create a Github account, follow the instructions [here](https://github.com/signup/free).
|
||||
@@ -62,7 +53,7 @@ minified AngularJS files:
|
||||
|
||||
```shell
|
||||
# Clone your Github repository:
|
||||
git clone "git@github.com:<github username>/angular.js.git"
|
||||
git clone https://github.com/<github username>/angular.js.git
|
||||
|
||||
# Go to the AngularJS directory:
|
||||
cd angular.js
|
||||
|
||||
@@ -2,85 +2,122 @@
|
||||
@name Downloading
|
||||
@description
|
||||
|
||||
# Including angular scripts from the Google CDN
|
||||
The quickest way to get started is to point your html `<script>` tag to a Google CDN URL.
|
||||
# Including Angular scripts from the Google CDN
|
||||
The quickest way to get started is to point your html `<script>` tag to a
|
||||
[Google CDN](https://developers.google.com/speed/libraries/#angularjs) URL.
|
||||
This way, you don't have to download anything or maintain a local copy.
|
||||
|
||||
There are two types of angular script URLs you can point to, one for development and one for
|
||||
There are two types of Angular script URLs you can point to, one for development and one for
|
||||
production:
|
||||
|
||||
* __angular.js__ — This is the human-readable, non-minified version, suitable for web
|
||||
development.
|
||||
* __angular.js__ — This is the human-readable, non-minified version, suitable for web development.
|
||||
* __angular.min.js__ — This is the minified version, which we strongly suggest you use in
|
||||
production.
|
||||
production.
|
||||
|
||||
To point your code to an angular script on the Google CDN server, use the following template. This
|
||||
example points to the minified version 1.4.5:
|
||||
To point your code to an angular script on the Google CDN server, use the following template. This
|
||||
example points to the minified version 1.5.6:
|
||||
|
||||
```
|
||||
```html
|
||||
<!doctype html>
|
||||
<html ng-app>
|
||||
<head>
|
||||
<title>My Angular App</title>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Note that only versions 1.0.1 and above are available on the CDN, if you need an earlier version
|
||||
you can use the <http://code.angularjs.org/> URL which was the previous recommended location for
|
||||
hosted code source. If you're still using the angular server you should switch to the CDN version
|
||||
for even faster loading times.
|
||||
<div class="alert alert-info">
|
||||
Note that only versions 1.0.1 and above are available on the CDN. If you need an earlier version
|
||||
(which you shouldn't) you can use the https://code.angularjs.org/ URL, which was the previous
|
||||
recommended location for hosted code source. If you're still using the Angular server you should
|
||||
switch to the CDN version for even faster loading times.
|
||||
</div>
|
||||
|
||||
<br />
|
||||
# Downloading and hosting angular files locally
|
||||
This option is for those who want to work with angular offline, or those who want to host the
|
||||
angular files on their own servers.
|
||||
This option is for those who want to work with Angular offline, or those who want to host the
|
||||
Angular files on their own servers.
|
||||
|
||||
If you navigate to <http://code.angularjs.org/>, you'll see a directory listing with all of the
|
||||
angular versions since we started releasing versioned build artifacts (quite late in the project
|
||||
lifetime). Each directory contains all artifacts that we released for a particular version.
|
||||
Download the version you want and have fun.
|
||||
If you navigate to https://code.angularjs.org/, you'll see a directory listing with all of the
|
||||
Angular versions since we started releasing versioned build artifacts. Each directory contains all
|
||||
artifacts that we released for a particular version. Download the version you want and have fun.
|
||||
|
||||
Each directory under <http://code.angularjs.org/> includes the following set of files:
|
||||
<div class="alert alert-warning">
|
||||
You can ignore directories starting with `2.` (e.g. `2.0.0-beta.17`) — they are not related to
|
||||
AngularJS. They contain build artifacts from [Angular 2](https://angular.io) versions.
|
||||
</div>
|
||||
|
||||
* __`angular.js`__ — This file is non-obfuscated, non-minified, and human-readable by
|
||||
opening it in any editor or browser. In order to get better error messages during development, you
|
||||
should always use this non-minified angular script.
|
||||
<br />
|
||||
Each directory under https://code.angularjs.org/ includes a set of files that comprise the
|
||||
corresponding version. All JavaScript files (except for `angular-mocks` which is only used during
|
||||
development) come in two flavors — one suitable for development, the other for production:
|
||||
|
||||
* __`angular.min.js`__ — This is a minified and obfuscated version of
|
||||
`angular.js` created with the Closure compiler. Use this version for production in order
|
||||
to minimize the size of the application that is downloaded by your user's browser.
|
||||
* __`<filename>.js`__ — These files are non-obfuscated, non-minified, and human-readable by opening
|
||||
them in any editor or browser. In order to get better error messages during development, you
|
||||
should always use these non-minified scripts.
|
||||
|
||||
* __`angular.zip`__ — This is a zip archive that contains all of the files released
|
||||
for this angular version. Use this file to get everything in a single download.
|
||||
* __`<filename>.min.js`__ — These are minified and obfuscated versions, created with the
|
||||
[Closure compiler](https://developers.google.com/closure/compiler/). Use these versions for
|
||||
production in order to minimize the size of the application that is downloaded by your user's
|
||||
browser.
|
||||
|
||||
* __`angular-mocks.js`__ — This file contains an implementation of mocks that makes
|
||||
testing angular apps even easier. Your unit/integration test harness should load this file after
|
||||
`angular.js` is loaded.
|
||||
* __`<filename>.min.js.map`__ — These are
|
||||
[sourcemap files](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/). You can
|
||||
serve them alongside the `.min.js` files in order to be able to debug the minified code (e.g. on a
|
||||
production deployment) more easily, but without impacting performance.
|
||||
|
||||
* __`angular-scenario.js`__ — This file is a very nifty JavaScript file that allows you
|
||||
to write and execute end-to-end tests for angular applications.
|
||||
<br />
|
||||
The set of files included in each version directory are:
|
||||
|
||||
* __`angular-loader.min.js`__ — Module loader for Angular modules. If you are loading multiple script files containing
|
||||
Angular modules, you can load them asynchronously and in any order as long as you load this file first. Often the
|
||||
contents of this file are copy&pasted into the `index.html` to avoid even the initial request to `angular-loader.min.js`.
|
||||
See [angular-seed](https://github.com/angular/angular-seed/blob/master/app/index-async.html) for an example of usage.
|
||||
* __`angular.zip`__ — This is a zip archive that contains all of the files released for this Angular
|
||||
version. Use this file to get everything in a single download.
|
||||
|
||||
* __Additional Angular modules:__ optional modules with additional functionality. These files should be loaded
|
||||
after the core `angular.js` file:
|
||||
* __`angular-animate.js`__ - Enable animation support
|
||||
* __`angular-cookies.js`__ - A convenient wrapper for reading and writing browser cookies
|
||||
* __`angular-resource.js`__ - Interaction support with RESTful services via the $resource service
|
||||
* __`angular-route.js`__ - Routing and deeplinking services and directives for angular apps
|
||||
* __`angular-sanitize.js`__ - Functionality to sanitize HTML
|
||||
* __`angular-touch.js`__ - Touch events and other helpers for touch-enabled devices
|
||||
* __`angular.js`__ — The core Angular framework. This is all you need to get your Angular app
|
||||
running.
|
||||
|
||||
* __`angular-csp.css`__ — You only need this file if you are using
|
||||
[CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP). See
|
||||
{@link directive:ngCsp here} for more info.
|
||||
|
||||
* __`angular-mocks.js`__ — This file contains an implementation of mocks that makes testing angular
|
||||
apps even easier. Your unit/integration test harness should load this file after `angular.js` is
|
||||
loaded.
|
||||
|
||||
* __`angular-loader.js`__ — Module loader for Angular modules. If you are loading multiple
|
||||
script files containing Angular modules, you can load them asynchronously and in any order as long
|
||||
as you load this file first. Often the contents of this file are copy&pasted into the `index.html`
|
||||
to avoid even the initial request to `angular-loader[.min].js`.
|
||||
See [angular-seed](https://github.com/angular/angular-seed/blob/master/app/index-async.html) for
|
||||
an example of usage.
|
||||
|
||||
* __Additional Angular modules:__ Optional modules with additional functionality. These files
|
||||
should be loaded after the core `angular[.min].js` file:
|
||||
* __`angular-animate.js`__ — Enable animation support. ({@link module:ngAnimate API docs})
|
||||
* __`angular-aria.js`__ — Make your apps [accessible](http://www.w3.org/TR/wai-aria/) to users of
|
||||
assistive technologies. ({@link module:ngAria API docs})
|
||||
* __`angular-cookies.js`__ — A convenient wrapper for reading and writing browser cookies.
|
||||
({@link module:ngCookies API docs})
|
||||
* __`angular-message-format.js`__ — Enhanced support for pluralization and gender specific
|
||||
messages in interpolated text. ({@link module:ngMessageFormat API docs})
|
||||
* __`angular-messages.js`__ — Enhanced support for displaying validation messages.
|
||||
({@link module:ngMessages API docs})
|
||||
* __`angular-parse-ext.js`__ — Allow Unicode characters in identifiers inside Angular expressions.
|
||||
({@link module:ngParseExt API docs})
|
||||
* __`angular-resource.js`__ — Easy interaction with RESTful services.
|
||||
({@link module:ngResource API docs})
|
||||
* __`angular-route.js`__ — Routing and deep-linking services and directives for Angular apps.
|
||||
({@link module:ngRoute API docs})
|
||||
* __`angular-sanitize.js`__ — Functionality to sanitize HTML. ({@link module:ngSanitize API docs})
|
||||
* __`angular-touch.js`__ — Touch events and other helpers for touch-enabled devices.
|
||||
({@link module:ngTouch API docs})
|
||||
|
||||
|
||||
* __`docs`__ — this directory contains all the files that compose the
|
||||
<http://docs.angularjs.org/> documentation app. These files are handy to see the older version of
|
||||
our docs, or even more importantly, view the docs offline.
|
||||
* __`docs/`__ — This directory contains all the files that compose the https://docs.angularjs.org/
|
||||
documentation app. These files are handy to see the older versions of our docs, or even more
|
||||
importantly, view the docs offline.
|
||||
|
||||
* __`i18n`__ - this directory contains locale specific `ngLocale` angular modules to override the defaults
|
||||
defined in the `ng` module.
|
||||
* __`i18n/`__ - This directory contains [locale specific](https://docs.angularjs.org/guide/i18n)
|
||||
`ngLocale` Angular modules to override the defaults defined in the main `ng` module.
|
||||
|
||||
@@ -16,9 +16,9 @@ becoming an Angular expert.
|
||||
starter app with a directory layout, test harness, and scripts to begin building your application.
|
||||
|
||||
|
||||
#Further Steps
|
||||
# Further Steps
|
||||
|
||||
##Watch Videos
|
||||
## Watch Videos
|
||||
|
||||
If you haven’t had a chance to watch the videos from the homepage, please check out:
|
||||
|
||||
@@ -29,13 +29,13 @@ If you haven’t had a chance to watch the videos from the homepage, please chec
|
||||
And visit our [YouTube channel](http://www.youtube.com/user/angularjs) for more AngularJS video presentations and
|
||||
tutorials.
|
||||
|
||||
##Subscribe
|
||||
## Subscribe
|
||||
|
||||
* Subscribe to the [mailing list](http://groups.google.com/forum/?fromgroups#!forum/angular). Ask questions here!
|
||||
* Follow us on [Twitter](https://twitter.com/intent/follow?original_referer=http%3A%2F%2Fangularjs.org%2F®ion=follow_link&screen_name=angularjs&source=followbutton&variant=2.0)
|
||||
* Add us to your circles on [Google+](https://plus.google.com/110323587230527980117/posts)
|
||||
|
||||
##Read more
|
||||
## Read more
|
||||
|
||||
The AngularJS documentation includes the {@link guide/index Developer Guide} covering concepts and the
|
||||
{@link ./api API Reference} for syntax and usage.
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
# PhoneCat Tutorial App
|
||||
|
||||
A great way to get introduced to AngularJS 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
|
||||
the construction of an Angular 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 class="diagram" src="img/tutorial/catalog_screen.png" width="488" height="413" alt="demo
|
||||
application running in the browser">
|
||||
<img class="diagram" src="img/tutorial/catalog_screen.png" width="488" height="413"
|
||||
alt="demo application running in the browser">
|
||||
|
||||
Follow the tutorial to see how Angular makes browsers smarter — without the use of native
|
||||
extensions or plug-ins:
|
||||
@@ -28,10 +28,11 @@ When you finish the tutorial you will be able to:
|
||||
* Create a dynamic application that works in all modern browsers.
|
||||
* Use data binding to wire up your data model to your views.
|
||||
* Create and run unit tests, with Karma.
|
||||
* Create and run end to end tests, with Protractor.
|
||||
* Move application logic out of the template and into Controllers.
|
||||
* Create and run end-to-end tests, with Protractor.
|
||||
* Move application logic out of the template and into components and controllers.
|
||||
* Get data from a server using Angular services.
|
||||
* Apply animations to your application, using ngAnimate.
|
||||
* Apply animations to your application, using the `ngAnimate` module.
|
||||
* Structure your Angular applications in a modular way that scales well for larger projects.
|
||||
* Identify resources for learning more about AngularJS.
|
||||
|
||||
The tutorial guides you through the entire process of building a simple application, including
|
||||
@@ -42,16 +43,18 @@ You can go through the whole tutorial in a couple of hours or you may want to sp
|
||||
really digging into it. If you're looking for a shorter introduction to AngularJS, check out the
|
||||
{@link misc/started Getting Started} document.
|
||||
|
||||
# Get Started
|
||||
|
||||
# Environment Setup
|
||||
|
||||
The rest of this page explains how you can set up your local machine for development.
|
||||
If you just want to read the tutorial then you can just go straight to the first step:
|
||||
If you just want to _read_ the tutorial, you can go straight to the first step:
|
||||
[Step 0 - Bootstrapping](tutorial/step_00).
|
||||
|
||||
# Working with the code
|
||||
|
||||
## Working with the Code
|
||||
|
||||
You can follow along with this tutorial and hack on the code in the comfort of your own computer.
|
||||
In this way you can get hands-on practice of really writing AngularJS code and also on using the
|
||||
This way, you can get hands-on practice of really writing Angular code and also on using the
|
||||
recommended testing tools.
|
||||
|
||||
The tutorial relies on the use of the [Git][git] versioning system for source code management.
|
||||
@@ -62,10 +65,11 @@ a few git commands.
|
||||
### Install Git
|
||||
|
||||
You can download and install Git from http://git-scm.com/download. Once installed, you should have
|
||||
access to the `git` command line tool. The main commands that you will need to use are:
|
||||
access to the `git` command line tool. The main commands that you will need to use are:
|
||||
|
||||
* `git clone ...`: Clone a remote repository onto your local machine.
|
||||
* `git checkout ...`: Check out a particular branch or a tagged version of the code to hack on.
|
||||
|
||||
- `git clone ...` : clone a remote repository onto your local machine
|
||||
- `git checkout ...` : check out a particular branch or a tagged version of the code to hack on
|
||||
|
||||
### Download angular-phonecat
|
||||
|
||||
@@ -73,13 +77,14 @@ Clone the [angular-phonecat repository][angular-phonecat] located at GitHub by r
|
||||
command:
|
||||
|
||||
```
|
||||
git clone --depth=14 https://github.com/angular/angular-phonecat.git
|
||||
git clone --depth=16 https://github.com/angular/angular-phonecat.git
|
||||
```
|
||||
|
||||
This command creates the `angular-phonecat` directory in your current directory.
|
||||
This command creates an `angular-phonecat` sub-directory in your current directory.
|
||||
|
||||
<div class="alert alert-info">The `--depth=14` option just tells Git to pull down only the last 14 commits. This makes the
|
||||
download much smaller and faster.
|
||||
<div class="alert alert-info">
|
||||
The `--depth=16` option tells Git to pull down only the last 16 commits.
|
||||
This makes the download much smaller and faster.
|
||||
</div>
|
||||
|
||||
Change your current directory to `angular-phonecat`.
|
||||
@@ -88,16 +93,16 @@ Change your current directory to `angular-phonecat`.
|
||||
cd angular-phonecat
|
||||
```
|
||||
|
||||
The tutorial instructions, from now on, assume you are running all commands from the
|
||||
The tutorial instructions, from now on, assume you are running all commands from within the
|
||||
`angular-phonecat` directory.
|
||||
|
||||
|
||||
### Install Node.js
|
||||
|
||||
If you want to run the preconfigured local web-server and the test tools then you will also need
|
||||
[Node.js v0.10.27+][node].
|
||||
If you want to run the preconfigured local web server and the test tools then you will also need
|
||||
[Node.js v4+][node].
|
||||
|
||||
You can download a Node.js installer for your operating system from http://nodejs.org/download/.
|
||||
You can download a Node.js installer for your operating system from https://nodejs.org/en/download/.
|
||||
|
||||
Check the version of Node.js that you have installed by running the following command:
|
||||
|
||||
@@ -105,7 +110,7 @@ Check the version of Node.js that you have installed by running the following co
|
||||
node --version
|
||||
```
|
||||
|
||||
In Debian based distributions, there is a name clash with another utility called `node`. The
|
||||
In Debian based distributions, there might be a name clash with another utility called `node`. The
|
||||
suggested solution is to also install the `nodejs-legacy` apt package, which renames `node` to
|
||||
`nodejs`.
|
||||
|
||||
@@ -115,12 +120,9 @@ nodejs --version
|
||||
npm --version
|
||||
```
|
||||
|
||||
|
||||
<div class="alert alert-info">If you need to run different versions of node.js
|
||||
in your local environment, consider installing
|
||||
<a href="https://github.com/creationix/nvm" title="Node Version Manager Github Repo link">
|
||||
Node Version Manager (nvm)
|
||||
</a>.
|
||||
<div class="alert alert-info">
|
||||
If you need to run different versions of Node.js in your local environment, consider installing
|
||||
[Node Version Manager (nvm)][nvm] or [Node Version Manager (nvm) for Windows][nvm-windows].
|
||||
</div>
|
||||
|
||||
Once you have Node.js installed on your machine, you can download the tool dependencies by running:
|
||||
@@ -129,30 +131,32 @@ Once you have Node.js installed on your machine, you can download the tool depen
|
||||
npm install
|
||||
```
|
||||
|
||||
This command reads angular-phonecat's `package.json` file and downloads the following tools
|
||||
into the `node_modules` directory:
|
||||
This command reads angular-phonecat's `package.json` file and downloads the following tools into the
|
||||
`node_modules` directory:
|
||||
|
||||
- [Bower][bower] - client-side code package manager
|
||||
- [Http-Server][http-server] - simple local static web server
|
||||
- [Karma][karma] - unit test runner
|
||||
- [Protractor][protractor] - end to end (E2E) test runner
|
||||
* [Bower][bower] - client-side code package manager
|
||||
* [Http-Server][http-server] - simple local static web server
|
||||
* [Karma][karma] - unit test runner
|
||||
* [Protractor][protractor] - end-to-end (E2E) test runner
|
||||
|
||||
Running `npm install` will also automatically use bower to download the Angular framework into the
|
||||
Running `npm install` will also automatically use bower to download the AngularJS framework into the
|
||||
`app/bower_components` directory.
|
||||
|
||||
<div class="alert alert-info">
|
||||
Note the angular-phonecat project is setup to install and run these utilities via npm scripts.
|
||||
This means that you do not have to have any of these utilities installed globally on your system
|
||||
to follow the tutorial. See **Installing Helper Tools** below for more information.
|
||||
to follow the tutorial. See [Installing Helper Tools](tutorial/#install-helper-tools-optional-)
|
||||
below for more information.
|
||||
</div>
|
||||
|
||||
The project is preconfigured with a number of npm helper scripts to make it easy to run the common
|
||||
tasks that you will need while developing:
|
||||
|
||||
- `npm start` : start a local development web-server
|
||||
- `npm test` : start the Karma unit test runner
|
||||
- `npm run protractor` : run the Protractor end to end (E2E) tests
|
||||
- `npm run update-webdriver` : install the drivers needed by Protractor
|
||||
* `npm start`: Start a local development web server.
|
||||
* `npm test`: Start the Karma unit test runner.
|
||||
* `npm run protractor`: Run the Protractor end-to-end (E2E) tests.
|
||||
* `npm run update-webdriver`: Install the drivers needed by Protractor.
|
||||
|
||||
|
||||
### Install Helper Tools (optional)
|
||||
|
||||
@@ -167,7 +171,7 @@ For instance, to install the Bower command line executable you would do:
|
||||
sudo npm install -g bower
|
||||
```
|
||||
|
||||
*(Omit the sudo if running on Windows)*
|
||||
_(Omit the sudo if running on Windows)_
|
||||
|
||||
Then you can run the bower tool directly, such as:
|
||||
|
||||
@@ -176,10 +180,10 @@ bower install
|
||||
```
|
||||
|
||||
|
||||
### Running Development Web Server
|
||||
### Running the Development Web Server
|
||||
|
||||
While Angular applications are purely client-side code, and it is possible to open them in a web
|
||||
browser directly from the file system, it is better to serve them from a HTTP web server. In
|
||||
browser directly from the file system, it is better to serve them from an HTTP web server. In
|
||||
particular, for security reasons, most modern browsers will not allow JavaScript to make server
|
||||
requests if the page is loaded directly from the file system.
|
||||
|
||||
@@ -190,70 +194,64 @@ application during development. Start the web server by running:
|
||||
npm start
|
||||
```
|
||||
|
||||
This will create a local webserver that is listening to port 8000 on your local machine.
|
||||
You can now browse to the application at:
|
||||
|
||||
```
|
||||
http://localhost:8000/app/index.html
|
||||
```
|
||||
This will create a local web server that is listening to port 8000 on your local machine.
|
||||
You can now browse to the application at http://localhost:8000/index.html.
|
||||
|
||||
<div class="alert alert-info">
|
||||
To serve the web app on a different IP address or port, edit the "start" script within package.json.
|
||||
You can use `-a` to set the address and `-p` to set the port.
|
||||
To serve the web app on a different IP address or port, edit the "start" script within
|
||||
`package.json`. You can use `-a` to set the address and `-p` to set the port. You also need to
|
||||
update the `baseUrl` configuration property in `e2e-test/protractor.conf.js`.
|
||||
</div>
|
||||
|
||||
|
||||
### Running Unit Tests
|
||||
|
||||
We use unit tests to ensure that the JavaScript code in our application is operating correctly.
|
||||
Unit tests focus on testing small isolated parts of the application. The unit tests are kept in the
|
||||
`test/unit` directory.
|
||||
Unit tests focus on testing small isolated parts of the application. The unit tests are kept in test
|
||||
files (specs) side-by-side with the application code. This way it's easier to find them and keep
|
||||
them up-to-date with the code under test. It also makes refactoring our app structure easier, since
|
||||
tests are moved together with the source code.
|
||||
|
||||
The angular-phonecat project is configured to use [Karma][karma] to run the unit tests for the
|
||||
application. Start Karma by running:
|
||||
application. Start Karma by running:
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
This will start the Karma unit test runner. Karma will read the configuration file at
|
||||
`test/karma.conf.js`. This configuration file tells Karma to:
|
||||
This will start the Karma unit test runner. Karma will read the configuration file `karma.conf.js`,
|
||||
located at the root of the project directory. This configuration file tells Karma to:
|
||||
|
||||
- open up a Chrome browser and connect it to Karma
|
||||
- execute all the unit tests in this browser
|
||||
- report the results of these tests in the terminal/command line window
|
||||
- watch all the project's JavaScript files and re-run the tests whenever any of these change
|
||||
* Open up instances of the Chrome and Firefox browsers and connect them to Karma.
|
||||
* Execute all the unit tests in these browsers.
|
||||
* Report the results of these tests in the terminal/command line window.
|
||||
* Watch all the project's JavaScript files and re-run the tests whenever any of these change.
|
||||
|
||||
It is good to leave this running all the time, in the background, as it will give you immediate
|
||||
feedback about whether your changes pass the unit tests while you are working on the code.
|
||||
|
||||
|
||||
### Running End to End Tests
|
||||
### Running E2E Tests
|
||||
|
||||
We use End to End tests to ensure that the application as a whole operates as expected.
|
||||
End to End tests are designed to test the whole client side application, in particular that the
|
||||
views are displaying and behaving correctly. It does this by simulating real user interaction with
|
||||
the real application running in the browser.
|
||||
We use E2E (end-to-end) tests to ensure that the application as a whole operates as expected.
|
||||
E2E tests are designed to test the whole client-side application, in particular that the views are
|
||||
displaying and behaving correctly. It does this by simulating real user interaction with the real
|
||||
application running in the browser.
|
||||
|
||||
The End to End tests are kept in the `test/e2e` directory.
|
||||
The E2E tests are kept in the `e2e-tests` directory.
|
||||
|
||||
The angular-phonecat project is configured to use [Protractor][protractor] to run the End to End
|
||||
tests for the application. Protractor relies upon a set of drivers to allow it to interact with
|
||||
the browser. You can install these drivers by running:
|
||||
The angular-phonecat project is configured to use [Protractor][protractor] to run the E2E tests for
|
||||
the application. Protractor relies upon a set of drivers to allow it to interact with the browser.
|
||||
You can install these drivers by running:
|
||||
|
||||
```
|
||||
npm run update-webdriver
|
||||
```
|
||||
|
||||
*(You should only need to do this once.)*
|
||||
|
||||
You will need to have Java present on your dev machine to allow the Selenium standalone to be started.
|
||||
Check if you already have java installed by opening a terminal/command line window and typing
|
||||
'''
|
||||
java -version
|
||||
'''
|
||||
If java is already installed and exists in the PATH then you will be shown the version installed,
|
||||
if, however you receive a message that "java is not recognized as an internal command or external
|
||||
command" you will need to install [java].
|
||||
<div class="alert alert-info">
|
||||
You don't have to manually run this command. Our npm scripts are configured so that it will be
|
||||
automatically executed as part of the command that runs the E2E tests.
|
||||
</div>
|
||||
|
||||
Since Protractor works by interacting with a running application, we need to start our web server:
|
||||
|
||||
@@ -261,32 +259,79 @@ Since Protractor works by interacting with a running application, we need to sta
|
||||
npm start
|
||||
```
|
||||
|
||||
Then in a separate terminal/command line window, we can run the Protractor test scripts against the
|
||||
application by running:
|
||||
Then, in a _separate_ terminal/command line window, we can run the Protractor test scripts against
|
||||
the application by running:
|
||||
|
||||
```
|
||||
npm run protractor
|
||||
```
|
||||
|
||||
Protractor will read the configuration file at `test/protractor-conf.js`. This configuration tells
|
||||
Protractor to:
|
||||
Protractor will read the configuration file at `e2e-tests/protractor.conf.js`. This configuration
|
||||
file tells Protractor to:
|
||||
|
||||
- open up a Chrome browser and connect it to the application
|
||||
- execute all the End to End tests in this browser
|
||||
- report the results of these tests in the terminal/command line window
|
||||
- close down the browser and exit
|
||||
* Open up a Chrome browser and connect it to the application.
|
||||
* Execute all the E2E tests in this browser.
|
||||
* Report the results of these tests in the terminal/command line window.
|
||||
* Close the browser and exit.
|
||||
|
||||
It is good to run the end to end tests whenever you make changes to the HTML views or want to check
|
||||
that the application as a whole is executing correctly. It is very common to run End to End tests
|
||||
before pushing a new commit of changes to a remote repository.
|
||||
It is good to run the E2E tests whenever you make changes to the HTML views or want to check that
|
||||
the application as a whole is executing correctly. It is very common to run E2E tests before pushing
|
||||
a new commit of changes to a remote repository.
|
||||
|
||||
|
||||
### Common Issues
|
||||
|
||||
<br />
|
||||
**Firewall / Proxy issues**
|
||||
|
||||
Git and other tools, often use the `git:` protocol for accessing files in remote repositories.
|
||||
Some firewall configurations are blocking `git://` URLs, which leads to errors when trying to clone
|
||||
repositories or download dependencies. (For example corporate firewalls are "notorious" for blocking
|
||||
`git:`.)
|
||||
|
||||
If you run into this issue, you can force the use of `https:` instead, by running the following
|
||||
command: `git config --global url."https://".insteadOf git://`
|
||||
|
||||
<br />
|
||||
**Updating WebDriver takes too long**
|
||||
|
||||
Running `update-webdriver` for the first time may take from several seconds up to a few minutes
|
||||
(depending on your hardware and network connection). If you cancel the operation (e.g. using
|
||||
`Ctrl+C`), you might get errors, when trying to run Protractor later.
|
||||
|
||||
In that case, you can delete the `node_modules/` directory and run `npm install` again.
|
||||
|
||||
<br />
|
||||
**Protractor dependencies**
|
||||
|
||||
Under the hood, Protractor uses the [Selenium Stadalone Server][selenium], which in turn requires
|
||||
the [Java Development Kit (JDK)][jdk] to be installed on your local machine. Check this by running
|
||||
`java -version` from the command line.
|
||||
|
||||
If JDK is not already installed, you can download it [here][jdk-download].
|
||||
|
||||
<br />
|
||||
**Error running the web server**
|
||||
|
||||
The web server is configured to use port 8000. If the port is already in use (for example by another
|
||||
instance of a running web server) you will get an `EADDRINUSE` error. Make sure the port is
|
||||
available, before running `npm start`.
|
||||
|
||||
<hr />
|
||||
|
||||
Now that you have set up your local machine, let's get started with the tutorial:
|
||||
{@link step_00 Step 0 - Bootstrapping}
|
||||
|
||||
Now that you have set up your local machine, let's get started with the tutorial: {@link step_00 Step 0 - Bootstrapping}
|
||||
|
||||
[git]: http://git-scm.com/
|
||||
[node]: http://nodejs.org/
|
||||
[angular-phonecat]: https://github.com/angular/angular-phonecat
|
||||
[protractor]: https://github.com/angular/protractor
|
||||
[bower]: http://bower.io/
|
||||
[git]: http://git-scm.com/
|
||||
[http-server]: https://github.com/nodeapps/http-server
|
||||
[karma]: https://github.com/karma-runner/karma
|
||||
[java]: https://www.java.com/en/download/help/download_options.xml
|
||||
[jdk]: https://en.wikipedia.org/wiki/Java_Development_Kit
|
||||
[jdk-download]: http://www.oracle.com/technetwork/java/javase/downloads/index.html
|
||||
[karma]: https://karma-runner.github.io/
|
||||
[node]: http://nodejs.org/
|
||||
[nvm]: https://github.com/creationix/nvm
|
||||
[nvm-windows]: https://github.com/coreybutler/nvm-windows
|
||||
[protractor]: https://github.com/angular/protractor
|
||||
[selenium]: http://docs.seleniumhq.org/
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
|
||||
|
||||
In this step of the tutorial, you will become familiar with the most important source code files of
|
||||
the AngularJS phonecat app. You will also learn how to start the development servers bundled with
|
||||
angular-seed, and run the application in the browser.
|
||||
the AngularJS Phonecat App. You will also learn how to start the development servers bundled with
|
||||
[angular-seed][angular-seed], and run the application in the browser.
|
||||
|
||||
Before you continue, make sure you have set up your development environment and installed all necessary
|
||||
dependencies, as described in {@link index#get-started Get Started}.
|
||||
Before you continue, make sure you have set up your development environment and installed all
|
||||
necessary dependencies, as described in the {@link tutorial/#environment-setup Environment Setup}
|
||||
section.
|
||||
|
||||
In the `angular-phonecat` directory, run this command:
|
||||
|
||||
@@ -19,118 +20,130 @@ In the `angular-phonecat` directory, run this command:
|
||||
git checkout -f step-0
|
||||
```
|
||||
|
||||
|
||||
This resets your workspace to step 0 of the tutorial app.
|
||||
|
||||
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.
|
||||
|
||||
If you haven't already done so you need to install the dependencies by running:
|
||||
If you haven't already done so, you need to install the dependencies by running:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
To see the app running in a browser, open a *separate* terminal/command line tab or window, then
|
||||
run `npm start` to start the web server. Now, open a browser window for the app and navigate to
|
||||
<a href="http://localhost:8000/app/" target="_blank" title="Open app on localhost">`http://localhost:8000/app/`</a>
|
||||
To see the app running in a browser, open a _separate_ terminal/command line tab or window, then run
|
||||
`npm start` to start the web server. Now, open a browser window for the app and navigate to
|
||||
http://localhost:8000/index.html.
|
||||
|
||||
Note that if you already ran the master branch app prior to checking out step-0, you may see the cached
|
||||
master version of the app in your browser window at this point. Just hit refresh to re-load the page.
|
||||
Note that if you already ran the master branch app prior to checking out step-0, you may see the
|
||||
cached master version of the app in your browser window at this point. Just hit refresh to re-load
|
||||
the page.
|
||||
|
||||
You can now see the page in your browser. It's not very exciting, but that's OK.
|
||||
|
||||
The 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 as we progress.
|
||||
|
||||
__`app/index.html`:__
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en" ng-app>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My HTML File</title>
|
||||
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
|
||||
<link rel="stylesheet" href="css/app.css">
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My HTML File</title>
|
||||
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css" />
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>Nothing here {{'yet' + '!'}}</p>
|
||||
<p>Nothing here {{'yet' + '!'}}</p>
|
||||
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
|
||||
|
||||
## What is the code doing?
|
||||
|
||||
**`ng-app` directive:**
|
||||
<br />
|
||||
**`ng-app` attribute:**
|
||||
|
||||
<html ng-app>
|
||||
```html
|
||||
<html ng-app>
|
||||
```
|
||||
|
||||
The `ng-app` attribute represents an Angular directive named `ngApp` (Angular uses
|
||||
`spinal-case` for its custom attributes and `camelCase` for the corresponding directives
|
||||
which implement them).
|
||||
This directive is used to flag the html element that Angular should consider to be the root element
|
||||
of our application.
|
||||
This gives application developers the freedom to tell Angular if the entire html page or only a
|
||||
portion of it should be treated as the Angular application.
|
||||
The `ng-app` attribute represents an Angular directive, named `ngApp` (Angular uses `kebab-case` for
|
||||
its custom attributes and `camelCase` for the corresponding directives which implement them). This
|
||||
directive is used to flag the HTML element that Angular should consider to be the root element of
|
||||
our application. This gives application developers the freedom to tell Angular if the entire HTML
|
||||
page or only a portion of it should be treated as the AngularJS application.
|
||||
|
||||
**AngularJS script tag:**
|
||||
For more info on `ngApp`, check out the {@link ngApp API Reference}.
|
||||
|
||||
<script src="bower_components/angular/angular.js">
|
||||
<br />
|
||||
**`angular.js` script tag:**
|
||||
|
||||
This code downloads the `angular.js` script which registers a callback that will be executed by the
|
||||
```html
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
```
|
||||
|
||||
This code downloads the `angular.js` script which 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 ng.directive:ngApp ngApp} directive. If
|
||||
Angular finds the directive, it will bootstrap the application with the root of the application DOM
|
||||
being the element on which the `ngApp` directive was defined.
|
||||
looks for the {@link ngApp ngApp} directive. If Angular finds the directive, it will bootstrap the
|
||||
application with the root of the application DOM being the element on which the `ngApp` directive
|
||||
was defined.
|
||||
|
||||
For more info on bootstrapping your app, checkout the [Bootstrap](guide/bootstrap) section of the
|
||||
Developer Guide.
|
||||
|
||||
<br />
|
||||
**Double-curly binding with an expression:**
|
||||
|
||||
Nothing here {{'yet' + '!'}}
|
||||
```html
|
||||
Nothing here {{'yet' + '!'}}
|
||||
```
|
||||
|
||||
This line demonstrates two core features of Angular's templating capabilities:
|
||||
|
||||
* a binding, denoted by double-curlies `{{ }}`
|
||||
* a simple expression `'yet' + '!'` used in this binding.
|
||||
* A binding, denoted by double-curlies: `{{ }}`
|
||||
* A simple expression used in this binding: `'yet' + '!'`
|
||||
|
||||
The binding tells Angular that it should evaluate an expression and insert the result into the
|
||||
DOM in place of the binding. Rather than a one-time insert, as we'll see in the next steps, a
|
||||
binding will result in efficient continuous updates whenever the result of the expression
|
||||
evaluation changes.
|
||||
The binding tells Angular that it should evaluate an expression and insert the result into the DOM
|
||||
in place of the binding. As we will see in the next steps, rather than a one-time insert, a binding
|
||||
will result in efficient continuous updates whenever the result of the expression evaluation
|
||||
changes.
|
||||
|
||||
{@link guide/expression Angular expression} is a JavaScript-like code snippet that is
|
||||
evaluated by Angular in the context of the current model scope, rather than within the scope of
|
||||
the global context (`window`).
|
||||
{@link guide/expression Angular expressions} are JavaScript-like code snippets that are evaluated by
|
||||
Angular in the context of the current model scope, rather than within the scope of the global
|
||||
context (`window`).
|
||||
|
||||
As expected, once this template is processed by Angular, the html page contains the text:
|
||||
"Nothing here yet!".
|
||||
As expected, once this template is processed by Angular, the HTML page contains the text:
|
||||
|
||||
## Bootstrapping AngularJS apps
|
||||
```
|
||||
Nothing here yet!
|
||||
```
|
||||
|
||||
Bootstrapping AngularJS apps automatically using the `ngApp` directive is very easy and suitable
|
||||
for most cases. In advanced cases, such as when using script loaders, you can use the
|
||||
{@link guide/bootstrap imperative / manual way} to bootstrap the app.
|
||||
## Bootstrapping Angular Applications
|
||||
|
||||
There are 3 important things that happen during the app bootstrap:
|
||||
Bootstrapping Angular applications automatically using the `ngApp` directive is very easy and
|
||||
suitable for most cases. In advanced cases, such as when using script loaders, you can use the
|
||||
{@link guide/bootstrap#manual-initialization imperative/manual way} to bootstrap the application.
|
||||
|
||||
There are 3 important things that happen during the bootstrap phase:
|
||||
|
||||
1. The {@link auto.$injector injector} that will be used for dependency injection is created.
|
||||
|
||||
2. The injector will then create the {@link ng.$rootScope root scope} that will
|
||||
become the context for the model of our application.
|
||||
2. The injector will then create the {@link ng.$rootScope root scope} that will become the context
|
||||
for the model of our application.
|
||||
|
||||
3. Angular will then "compile" the DOM starting at the `ngApp` root element, processing any
|
||||
directives and bindings found along the way.
|
||||
|
||||
|
||||
Once an application is bootstrapped, it will then wait for incoming browser events (such as mouse
|
||||
click, key press or incoming HTTP response) that might change the model. Once such an event occurs,
|
||||
Angular detects if it caused any model changes and if changes are found, Angular will reflect them
|
||||
in the view by updating all of the affected bindings.
|
||||
clicks, key presses or incoming HTTP responses) that might change the model. Once such an event
|
||||
occurs, Angular detects if it caused any model changes and if changes are found, Angular will
|
||||
reflect them in the view by updating all of the affected bindings.
|
||||
|
||||
The structure of our application is currently very simple. The template contains just one directive
|
||||
and one static binding, and our model is empty. That will soon change!
|
||||
@@ -140,27 +153,29 @@ and one static binding, and our model is empty. That will soon change!
|
||||
|
||||
## What are all these files in my working directory?
|
||||
|
||||
|
||||
Most of the files in your working directory come from the [angular-seed project][angular-seed] which
|
||||
is typically used to bootstrap new Angular projects. The seed project is pre-configured to install
|
||||
the angular framework (via `bower` into the `app/bower_components/` folder) and tools for developing
|
||||
a typical web app (via `npm`).
|
||||
Most of the files in your working directory come from the [angular-seed project][angular-seed],
|
||||
which is typically used to bootstrap new AngularJS projects. The seed project is pre-configured to
|
||||
install the AngularJS framework (via `bower` into the `app/bower_components/` directory) and tools
|
||||
for developing and testing a typical web application (via `npm`).
|
||||
|
||||
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/`
|
||||
* Removed the example app.
|
||||
* Removed unused dependencies.
|
||||
* Added phone images to `app/img/phones/`.
|
||||
* Added phone data files (JSON) to `app/phones/`.
|
||||
* Added a dependency on [Bootstrap](http://getbootstrap.com) in the `bower.json` file.
|
||||
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Try adding a new expression to the `index.html` that will do some math:
|
||||
<div></div>
|
||||
|
||||
<p>1 + 2 = {{ 1 + 2 }}</p>
|
||||
* Try adding a new expression to `index.html` that will do some math:
|
||||
|
||||
```html
|
||||
<p>1 + 2 = {{1 + 2}}</p>
|
||||
```
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
@@ -12,11 +12,11 @@ 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.
|
||||
|
||||
- The page now contains a list with information about two phones.
|
||||
* The page now contains a list with information about two phones.
|
||||
|
||||
<div doc-tutorial-reset="1"></div>
|
||||
|
||||
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
@@ -39,15 +39,19 @@ In this step you will add some basic information about two cell phones to an HTM
|
||||
|
||||
# Experiments
|
||||
|
||||
<div></div>
|
||||
|
||||
* Try adding more static HTML to `index.html`. For example:
|
||||
|
||||
<p>Total number of phones: 2</p>
|
||||
```html
|
||||
<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 AngularJS to dynamically generate the same list.
|
||||
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>
|
||||
|
||||
@@ -6,37 +6,38 @@
|
||||
<ul doc-tutorial-nav="2"></ul>
|
||||
|
||||
|
||||
Now it's time to make the web page dynamic — with AngularJS. We'll also add a test that verifies the
|
||||
code for the controller we are going to add.
|
||||
Now, it's time to make the web page dynamic — with AngularJS. We will 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
|
||||
[the Model-View-Controller (MVC) design pattern](http://en.wikipedia.org/wiki/Model–View–Controller)
|
||||
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.
|
||||
There are many ways to structure the code for an application. For Angular applications, we encourage
|
||||
the use of the [Model-View-Controller (MVC) design pattern][mvc-pattern] to decouple the code and
|
||||
separate concerns. With that in mind, let's use a little Angular and JavaScript to add models,
|
||||
views, and controllers to our app.
|
||||
|
||||
- The list of three phones is now generated dynamically from data
|
||||
* The list of three phones is now generated dynamically from data
|
||||
|
||||
<div doc-tutorial-reset="2"></div>
|
||||
|
||||
|
||||
## View and Template
|
||||
|
||||
In Angular, the __view__ is a projection of the model through the HTML __template__. This means that
|
||||
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.
|
||||
|
||||
The view component is constructed by Angular from this template:
|
||||
The view is constructed by Angular from this template.
|
||||
|
||||
__`app/index.html`:__
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
<html ng-app="phonecatApp">
|
||||
<head>
|
||||
...
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="js/controllers.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</head>
|
||||
<body ng-controller="PhoneListCtrl">
|
||||
<body ng-controller="PhoneListController">
|
||||
|
||||
<ul>
|
||||
<li ng-repeat="phone in phones">
|
||||
@@ -49,97 +50,117 @@ __`app/index.html`:__
|
||||
</html>
|
||||
```
|
||||
|
||||
We replaced the hard-coded phone list with the {@link ng.directive:ngRepeat ngRepeat directive}
|
||||
and two {@link guide/expression Angular expressions}:
|
||||
We replaced the hard-coded phone list with the {@link ngRepeat ngRepeat} directive and two
|
||||
{@link guide/expression Angular expressions}:
|
||||
|
||||
* The `ng-repeat="phone in phones"` attribute in the `<li>` tag is an Angular repeater directive.
|
||||
The repeater tells Angular to create a `<li>` element for each phone in the list using the `<li>`
|
||||
tag as the template.
|
||||
* The expressions wrapped in curly braces (`{{phone.name}}` and `{{phone.snippet}}`) will be replaced
|
||||
by the value of the expressions.
|
||||
* The `ng-repeat="phone in phones"` attribute on the `<li>` tag is an Angular repeater directive.
|
||||
The repeater tells Angular to create a `<li>` element for each phone in the list, using the `<li>`
|
||||
tag as the template.
|
||||
* The expressions wrapped in curly braces (`{{phone.name}}` and `{{phone.snippet}}`) will be
|
||||
replaced by the values of the expressions.
|
||||
|
||||
We have added a new directive, called `ng-controller`, which attaches a `PhoneListCtrl`
|
||||
__controller__ to the <body> tag. At this point:
|
||||
We have also added a new directive, called {@link ngController ngController}, which attaches a
|
||||
`PhoneListController` **controller** to the `<body>` tag. At this point:
|
||||
|
||||
* The expressions in curly braces (`{{phone.name}}` and `{{phone.snippet}}`) denote
|
||||
bindings, which are referring to our application model, which is set up in our `PhoneListCtrl`
|
||||
controller.
|
||||
* `PhoneListController` is in charge of the DOM sub-tree under (and including) the `<body>` element.
|
||||
* The expressions in curly braces (`{{phone.name}}` and `{{phone.snippet}}`) denote bindings, which
|
||||
are referring to our application model, which is set up in our `PhoneListController` controller.
|
||||
|
||||
<div class="alert alert-info">
|
||||
Note: We have specified an {@link angular.Module Angular Module} to load using `ng-app="phonecatApp"`,
|
||||
where `phonecatApp` is the name of our module. This module will contain the `PhoneListCtrl`.
|
||||
Note: We have specified an {@link angular.Module Angular Module} to load using
|
||||
`ng-app="phonecatApp"`, where `phonecatApp` is the name of our module. This module will contain
|
||||
the `PhoneListController`.
|
||||
</div>
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_02.png">
|
||||
|
||||
## Model and Controller
|
||||
|
||||
The data __model__ (a simple array of phones in object literal notation) is now instantiated within
|
||||
the `PhoneListCtrl` __controller__. The __controller__ is simply a constructor function that takes a
|
||||
`$scope` parameter:
|
||||
The data **model** (a simple array of phones in object literal notation) is now instantiated within
|
||||
the `PhoneListController` **controller**. The **controller** is simply a constructor function that
|
||||
takes a `$scope` parameter:
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
<br />
|
||||
**`app/app.js`:**
|
||||
|
||||
```js
|
||||
|
||||
// Define the `phonecatApp` module
|
||||
var phonecatApp = angular.module('phonecatApp', []);
|
||||
|
||||
phonecatApp.controller('PhoneListCtrl', function ($scope) {
|
||||
// Define the `PhoneListController` controller on the `phonecatApp` module
|
||||
phonecatApp.controller('PhoneListController', function PhoneListController($scope) {
|
||||
$scope.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.'}
|
||||
{
|
||||
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.'
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
Here we declared a controller called `PhoneListCtrl` and registered it in an AngularJS
|
||||
module, `phonecatApp`. Notice that our `ng-app` directive (on the `<html>` tag) now specifies the `phonecatApp`
|
||||
module name as the module to load when bootstrapping the Angular application.
|
||||
Here we declared a controller called `PhoneListController` and registered it in an Angular module,
|
||||
`phonecatApp`. Notice that our `ngApp` directive (on the `<html>` tag) now specifies the
|
||||
`phonecatApp` module name as the module to load when bootstrapping the application.
|
||||
|
||||
Although the controller is not yet doing very much, it plays 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:
|
||||
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 {@link ng.directive:ngController ngController} directive, located on the `<body>` tag,
|
||||
references the name of our controller, `PhoneListCtrl` (located in the JavaScript file
|
||||
`controllers.js`).
|
||||
* The {@link ngController ngController} directive, located on the `<body>` tag, references the name
|
||||
of our controller, `PhoneListController` (located in the JavaScript file `app.js`).
|
||||
|
||||
* The `PhoneListController` controller attaches the phone data to the `$scope` that was injected
|
||||
into our controller function. This _scope_ is a prototypal descendant of the _root scope_ that was
|
||||
created when the application was defined. This controller scope is available to all bindings
|
||||
located within the `<body ng-controller="PhoneListController">` tag.
|
||||
|
||||
* The `PhoneListCtrl` controller attaches the phone data to the `$scope` that was injected into our
|
||||
controller function. This *scope* is a prototypical descendant of the *root scope* that was created
|
||||
when the application was defined. This controller scope is available to all bindings located within
|
||||
the `<body ng-controller="PhoneListCtrl">` tag.
|
||||
|
||||
### Scope
|
||||
|
||||
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
|
||||
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 ng.$rootScope.Scope angular scope documentation}.
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_02.png">
|
||||
|
||||
## Tests
|
||||
<div class="alert alert-warning">
|
||||
<p>
|
||||
Angular scopes prototypally inherit from their parent scope, all the way up to the *root scope*
|
||||
of the application. As a result, assigning values directly on the scope makes it easy to share
|
||||
data across different parts of the page and create interactive applications.
|
||||
While this approach works for prototypes and smaller applications, it quickly leads to tight
|
||||
coupling and difficulty to reason about changes in our data model.
|
||||
</p>
|
||||
<p>
|
||||
In the next step, we will learn how to better organize our code, by "packaging" related pieces
|
||||
of application and presentation logic into isolated, reusable entities, called components.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
The "Angular way" of separating controller from the view, makes it easy to test code as it is being
|
||||
developed. If our controller is available on the global namespace then we could simply instantiate it
|
||||
with a mock `scope` object:
|
||||
|
||||
__`test/e2e/scenarios.js`:__
|
||||
developed. If our controller were available on the global namespace, we could simply instantiate it
|
||||
with a mock scope object:
|
||||
|
||||
<br />
|
||||
```js
|
||||
describe('PhoneListCtrl', function(){
|
||||
describe('PhoneListController', function() {
|
||||
|
||||
it('should create "phones" model with 3 phones', function() {
|
||||
var scope = {},
|
||||
ctrl = new PhoneListCtrl(scope);
|
||||
it('should create a `phones` model with 3 phones', function() {
|
||||
var scope = {};
|
||||
var ctrl = new PhoneListController(scope);
|
||||
|
||||
expect(scope.phones.length).toBe(3);
|
||||
});
|
||||
@@ -147,30 +168,31 @@ describe('PhoneListCtrl', function(){
|
||||
});
|
||||
```
|
||||
|
||||
The test instantiates `PhoneListCtrl` and verifies that the phones array property on the scope
|
||||
contains three records. This 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.
|
||||
The test instantiates `PhoneListController` and verifies that the phones array property on the
|
||||
scope contains three records. This 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.
|
||||
|
||||
### Testing non-Global Controllers
|
||||
|
||||
In practice, you will not want to have your controller functions in the global namespace. Instead,
|
||||
you can see that we have registered it via an anonymous constructor function on the `phonecatApp`
|
||||
module.
|
||||
## Testing non-global Controllers
|
||||
|
||||
In practice, you will not want to have your controller functions in the global namespace. Instead,
|
||||
you can see that we have registered it via a constructor function on the `phonecatApp` module.
|
||||
|
||||
In this case Angular provides a service, `$controller`, which will retrieve your controller by name.
|
||||
Here is the same test using `$controller`:
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
<br />
|
||||
**`app/app.spec.js`:**
|
||||
|
||||
```js
|
||||
describe('PhoneListCtrl', function(){
|
||||
describe('PhoneListController', function() {
|
||||
|
||||
beforeEach(module('phonecatApp'));
|
||||
|
||||
it('should create "phones" model with 3 phones', inject(function($controller) {
|
||||
var scope = {},
|
||||
ctrl = $controller('PhoneListCtrl', {$scope:scope});
|
||||
it('should create a `phones` model with 3 phones', inject(function($controller) {
|
||||
var scope = {};
|
||||
var ctrl = $controller('PhoneListController', {$scope: scope});
|
||||
|
||||
expect(scope.phones.length).toBe(3);
|
||||
}));
|
||||
@@ -179,29 +201,46 @@ describe('PhoneListCtrl', function(){
|
||||
```
|
||||
|
||||
* Before each test we tell Angular to load the `phonecatApp` module.
|
||||
* We ask Angular to `inject` the `$controller` service into our test function
|
||||
* We use `$controller` to create an instance of the `PhoneListCtrl`
|
||||
* We ask Angular to `inject` the `$controller` service into our test function.
|
||||
* We use `$controller` to create an instance of the `PhoneListController`.
|
||||
* With this instance, we verify that the phones array property on the scope contains three records.
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>**A note on file naming:**</p>
|
||||
<p>
|
||||
As already mentioned in the [introduction](tutorial/#running-unit-tests), the unit test files
|
||||
(specs) are kept side-by-side with the application code. We name our specs after the file
|
||||
containing the code to be tested plus a specific suffix to distinguish them from files
|
||||
containing application code. Note that test files are still plain JavaScript files, so they have
|
||||
a `.js` file extension.
|
||||
</p>
|
||||
<p>
|
||||
In this tutorial, we are using the `.spec` suffix. So the test file corresponding to
|
||||
`something.js` would be called `something.spec.js`.
|
||||
(Another common convention is to use a `_spec` or `_test` suffix.)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
### Writing and Running Tests
|
||||
|
||||
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 v1.3. You can learn about Jasmine on the [Jasmine home page][jasmine] and
|
||||
at the [Jasmine docs][jasmine-docs].
|
||||
## Writing and Running Tests
|
||||
|
||||
The angular-seed project is pre-configured to run unit tests using [Karma][karma] but you will need
|
||||
Many Angular developers prefer the syntax of
|
||||
[Jasmine's Behavior-Driven Development (BDD) framework][jasmine-home], when writing tests. Although
|
||||
Angular does not require you to use Jasmine, we wrote all of the tests in this tutorial in Jasmine
|
||||
v2.4. You can learn about Jasmine on the [Jasmine home page][jasmine-home] and at the
|
||||
[Jasmine docs][jasmine-docs].
|
||||
|
||||
The angular-seed project is pre-configured to run unit tests using [Karma][karma], but you will need
|
||||
to ensure that Karma and its necessary plugins are installed. You can do this by running
|
||||
`npm install`.
|
||||
|
||||
To run the tests, and then watch the files for changes: `npm test`.
|
||||
To run the tests, and then watch the files for changes execute: `npm test`
|
||||
|
||||
* Karma will start new instances of Chrome and Firefox browsers automatically. Just ignore them and
|
||||
let them run in the background. Karma will use these browsers for test execution.
|
||||
* If you only have one of the browsers installed on your machine (either Chrome or Firefox), make
|
||||
sure to update the karma configuration file before running the test. Locate the configuration file
|
||||
in `test/karma.conf.js`, then update the `browsers` property.
|
||||
sure to update the karma configuration file (`karma.conf.js`), before running the test. Locate the
|
||||
configuration file in the root directory and update the `browsers` property.
|
||||
|
||||
E.g. if you only have Chrome installed:
|
||||
<pre>
|
||||
@@ -213,23 +252,27 @@ To run the tests, and then watch the files for changes: `npm test`.
|
||||
* You should see the following or similar output in the terminal:
|
||||
|
||||
<pre>
|
||||
info: Karma server started at http://localhost:9876/
|
||||
info (launcher): Starting browser "Chrome"
|
||||
info (Chrome 22.0): Connected on socket id tPUm9DXcLHtZTKbAEO-n
|
||||
Chrome 22.0: Executed 1 of 1 SUCCESS (0.093 secs / 0.004 secs)
|
||||
INFO [karma]: Karma server started at http://localhost:9876/
|
||||
INFO [launcher]: Starting browser Chrome
|
||||
INFO [Chrome 49.0]: Connected on socket ... with id ...
|
||||
Chrome 49.0: Executed 1 of 1 SUCCESS (0.05 secs / 0.04 secs)
|
||||
</pre>
|
||||
|
||||
Yay! The test passed! Or not...
|
||||
* To rerun the tests, just change any of the source or test .js files. Karma will notice the change
|
||||
|
||||
* To rerun the tests, just change any of the source or test `.js` files. Karma will notice the change
|
||||
and will rerun the tests for you. Now isn't that sweet?
|
||||
|
||||
<div class="alert alert-info">
|
||||
Make sure you don't minimize the browser that Karma opened. On some OS, memory assigned to a minimized
|
||||
browser is limited, which results in your karma tests running extremely slow.
|
||||
Make sure you don't minimize the browser that Karma opened. On some OS, memory assigned to a
|
||||
minimized browser is limited, which results in your karma tests running extremely slow.
|
||||
</div>
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
<div></div>
|
||||
|
||||
* Add another binding to `index.html`. For example:
|
||||
|
||||
```html
|
||||
@@ -238,46 +281,60 @@ browser is limited, which results in your karma tests running extremely slow.
|
||||
|
||||
* Create a new model property in the controller and bind to it from the template. For example:
|
||||
|
||||
$scope.name = "World";
|
||||
```js
|
||||
$scope.name = 'world';
|
||||
```
|
||||
|
||||
Then add a new binding to `index.html`:
|
||||
|
||||
<p>Hello, {{name}}!</p>
|
||||
```html
|
||||
<p>Hello, {{name}}!</p>
|
||||
```
|
||||
|
||||
Refresh your browser and verify that it says "Hello, World!".
|
||||
Refresh your browser and verify that it says 'Hello, world!'.
|
||||
|
||||
* Update the unit test for the controller in `./test/unit/controllersSpec.js` to reflect the previous change. For example by adding:
|
||||
* Update the unit test for the controller in `app/app.spec.js` to reflect the previous change.
|
||||
For example by adding:
|
||||
|
||||
expect(scope.name).toBe('World');
|
||||
```js
|
||||
expect(scope.name).toBe('world');
|
||||
```
|
||||
|
||||
* Create a repeater in `index.html` 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>
|
||||
```html
|
||||
<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>
|
||||
```html
|
||||
<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>
|
||||
```
|
||||
|
||||
Extra points: try and make an 8x8 table using an additional `ng-repeat`.
|
||||
Extra points: Try and make an 8x8 table using an additional `ng-repeat`.
|
||||
|
||||
* Make the unit test fail by changing `expect(scope.phones.length).toBe(3)` to instead use `toBe(4)`.
|
||||
* Make the unit test fail by changing `expect(scope.phones.length).toBe(3)` to instead use
|
||||
`toBe(4)`.
|
||||
|
||||
|
||||
# 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.
|
||||
We now have a dynamic application which separates models, views, and controllers, and we are testing
|
||||
as we go. Let's go to {@link step_03 step 3} to learn how to improve our application's architecture,
|
||||
by utilizing components.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="2"></ul>
|
||||
|
||||
[jasmine]: http://jasmine.github.io/
|
||||
[jasmine-docs]: http://jasmine.github.io/1.3/introduction.html
|
||||
[karma]: http://karma-runner.github.io/
|
||||
|
||||
[jasmine-docs]: http://jasmine.github.io/2.4/introduction.html
|
||||
[jasmine-home]: http://jasmine.github.io/
|
||||
[karma]: https://karma-runner.github.io/
|
||||
[mvc-pattern]: http://en.wikipedia.org/wiki/Model–View–Controller
|
||||
|
||||
@@ -1,223 +1,282 @@
|
||||
@ngdoc tutorial
|
||||
@name 3 - Filtering Repeaters
|
||||
@name 3 - Components
|
||||
@step 3
|
||||
@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.
|
||||
In the previous step, we saw how a controller and a template worked together to convert a static
|
||||
HTML page into a dynamic view. This is a very common pattern in Single-Page Applications in general
|
||||
(and Angular applications in particular):
|
||||
|
||||
* Instead of creating a static HTML page on the server, the client-side code "takes over" and
|
||||
interacts dynamically with the view, updating it instantly to reflect changes in model data or
|
||||
state, usually as a result of user interaction (we'll see an example shortly in
|
||||
{@link step_05 step 5}).
|
||||
|
||||
The **template** (the part of the view containing the bindings and presentation logic) acts as a
|
||||
blueprint for how our data should be organized and presented to the user.
|
||||
The **controller** provides the context in which the bindings are evaluated and applies behavior
|
||||
and logic to our template.
|
||||
|
||||
There are still a couple of areas we can do better:
|
||||
|
||||
1. What if we want to reuse the same functionality in a different part of our application ?<br />
|
||||
We would need to duplicate the whole template (including the controller). This is error-prone and
|
||||
hurts maintainability.
|
||||
2. The scope, that glues our controller and template together into a dynamic view, is not isolated
|
||||
from other parts of the page. What this means is that a random, unrelated change in a different
|
||||
part of the page (e.g. a property-name conflict) could have unexpected and hard-to-debug side
|
||||
effects on our view.
|
||||
|
||||
(OK, this might not be a real concern in our minimal example, but it **is** a valid concern for
|
||||
bigger, real-world applications.)
|
||||
|
||||
* 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.
|
||||
|
||||
<div doc-tutorial-reset="3"></div>
|
||||
|
||||
|
||||
## Controller
|
||||
## Components to the rescue!
|
||||
|
||||
We made no changes to the controller.
|
||||
Since this combination (template + controller) is such a common and recurring pattern, Angular
|
||||
provides an easy and concise way to combine them together into reusable and isolated entities,
|
||||
known as _components_.
|
||||
Additionally, Angular will create a so called _isolate scope_ for each instance of our component,
|
||||
which means no prototypal inheritance and no risk of our component affecting other parts of the
|
||||
application or vice versa.
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
Since this is an introductory tutorial, we are not going to dive deep into all features provided
|
||||
by Angular **components**. You can read more about components and their usage patterns in the
|
||||
[Components](guide/component) section of the Developer Guide.
|
||||
</p>
|
||||
<p>
|
||||
In fact, one could think of components as an opinionated and stripped-down version of their more
|
||||
complex and verbose (but powerful) siblings, **directives**, which are Angular's way of teaching
|
||||
HTML new tricks. You can read all about them in the [Directives](guide/directive) section of the
|
||||
Developer Guide.
|
||||
</p>
|
||||
<p>
|
||||
(**Note:** Directives are an advanced topic, so you might want to postpone studying them, until
|
||||
you have mastered the basics.)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## Template
|
||||
To create a component, we use the {@link angular.Module#component .component()} method of an
|
||||
{@link module Angular module}. We must provide the name of the component and the Component
|
||||
Definition Object (CDO for short).
|
||||
|
||||
__`app/index.html`:__
|
||||
Remember that (since components are also directives) the name of the component is in `camelCase`,
|
||||
but we will use `kebab-case`, when referring to it in our HTML.
|
||||
|
||||
```html
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<!--Sidebar content-->
|
||||
In its simplest form, the CDO will just contain a template and a controller. (We can actually omit
|
||||
the controller and Angular will create a dummy controller for us. This is useful for simple
|
||||
"presentational" components, that don't attach any behavior to the template.)
|
||||
|
||||
Search: <input ng-model="query">
|
||||
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<!--Body content-->
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query">
|
||||
{{phone.name}}
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
We added a standard HTML `<input>` tag and used Angular's
|
||||
{@link ng.filter:filter filter} function to process the input for the
|
||||
{@link ng.directive:ngRepeat ngRepeat} directive.
|
||||
|
||||
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 class="diagram" src="img/tutorial/tutorial_03.png">
|
||||
|
||||
* Use of the `filter` filter: The {@link ng.filter:filter filter} function uses the
|
||||
`query` value to create a new array that contains only those records that match the `query`.
|
||||
|
||||
`ngRepeat` automatically updates the view in response to the changing number of phones returned
|
||||
by the `filter` 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`:__
|
||||
Let's see an example:
|
||||
|
||||
```js
|
||||
describe('PhoneCat App', function() {
|
||||
|
||||
describe('Phone list view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('app/index.html');
|
||||
angular.
|
||||
module('myApp').
|
||||
component('greetUser', {
|
||||
template: 'Hello, {{$ctrl.user}}!',
|
||||
controller: function GreetUserController() {
|
||||
this.user = 'world';
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Now, every time we include `<greet-user></greet-user>` in our view, Angular will expand it into a
|
||||
DOM sub-tree constructed using the provided `template` and managed by an instance of the specified
|
||||
controller.
|
||||
|
||||
But wait, where did that `$ctrl` come from and what does it refer to ?
|
||||
|
||||
For reasons already mentioned (and for other reasons that are out of the scope of this tutorial), it
|
||||
is considered a good practice to avoid using the scope directly. We can (and should) use our
|
||||
controller instance; i.e. assign our data and methods on properties of our controller (the "`this`"
|
||||
inside the controller constructor), instead of directly to the scope.
|
||||
|
||||
From the template, we can refer to our controller instance using an alias. This way, the context of
|
||||
evaluation for our expressions is even more clear. By default, components use `$ctrl` as the
|
||||
controller alias, but we can override it, should the need arise.
|
||||
|
||||
There are more options available, so make sure you check out the
|
||||
{@link ng.$compileProvider#component API Reference}, before using `.component()` in your own
|
||||
applications.
|
||||
|
||||
|
||||
it('should filter the phone list as a user types into the search box', function() {
|
||||
## Using Components
|
||||
|
||||
var phoneList = element.all(by.repeater('phone in phones'));
|
||||
var query = element(by.model('query'));
|
||||
Now that we know how to create components, let's refactor the HTML page to make use of our newly
|
||||
acquired skill.
|
||||
|
||||
expect(phoneList.count()).toBe(3);
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
query.sendKeys('nexus');
|
||||
expect(phoneList.count()).toBe(1);
|
||||
```html
|
||||
<html ng-app="phonecatApp">
|
||||
<head>
|
||||
...
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<script src="phone-list.component.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
query.clear();
|
||||
query.sendKeys('motorola');
|
||||
expect(phoneList.count()).toBe(2);
|
||||
});
|
||||
<!-- Use a custom component to render a list of phones -->
|
||||
<phone-list></phone-list>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/app.js`:**
|
||||
|
||||
```js
|
||||
// Define the `phonecatApp` module
|
||||
angular.module('phonecatApp', []);
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/phone-list.component.js`:**
|
||||
|
||||
```js
|
||||
// Register `phoneList` component, along with its associated controller and template
|
||||
angular.
|
||||
module('phonecatApp').
|
||||
component('phoneList', {
|
||||
template:
|
||||
'<ul>' +
|
||||
'<li ng-repeat="phone in $ctrl.phones">' +
|
||||
'<span>{{phone.name}}</span>' +
|
||||
'<p>{{phone.snippet}}</p>' +
|
||||
'</li>' +
|
||||
'</ul>',
|
||||
controller: function PhoneListController() {
|
||||
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.'
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Voilà! The resulting output should look the same, but let's see what we have gained:
|
||||
|
||||
* Our phone list is reusable. Just drop `<phone-list></phone-list>` anywhere in the page to get a
|
||||
list of phones.
|
||||
* Our main view (`index.html`) is cleaner and more declarative. Just by looking at it, we know there
|
||||
is a list of phones. We are not bothered with implementation details.
|
||||
* Our component is isolated and safe from "external influences". Likewise, we don't have to worry,
|
||||
that we might accidentally break something in some other part of the application. What happens
|
||||
inside our component, stays inside our component.
|
||||
* It's easier to test our component in isolation.
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_03.png">
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>**A note on file naming:**</p>
|
||||
<p>
|
||||
It is a good practice to distinguish different types of entities by suffix. In this tutorial, we
|
||||
are using the `.component` suffix for components, so the definition of a `someComponent`
|
||||
component would be in a file named `some-component.component.js`.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
Although we have combined our controller with a template into a component, we still can (and should)
|
||||
unit test the controller separately, since this is where our application logic and data reside.
|
||||
|
||||
In order to retrieve and instantiate a component's controller, Angular provides the
|
||||
{@link ngMock.$componentController $componentController} service.
|
||||
|
||||
<div class="alert alert-info">
|
||||
The `$controller` service that we used in the previous step can only instantiate controllers that
|
||||
were registered by name, using the `.controller()` method. We could have registered our component
|
||||
controller this way, too, if we wanted to. Instead, we chose to define it inline — inside
|
||||
the CDO — to keep things localized, but either way works equally well.
|
||||
</div>
|
||||
|
||||
<br />
|
||||
**`app/phone-list.component.spec.js`:**
|
||||
|
||||
```js
|
||||
describe('phoneList', function() {
|
||||
|
||||
// Load the module that contains the `phoneList` component before each test
|
||||
beforeEach(module('phonecatApp'));
|
||||
|
||||
// Test the controller
|
||||
describe('PhoneListController', function() {
|
||||
|
||||
it('should create a `phones` model with 3 phones', inject(function($componentController) {
|
||||
var ctrl = $componentController('phoneList');
|
||||
|
||||
expect(ctrl.phones.length).toBe(3);
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
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.
|
||||
The test retrieves the controller associated with the `phoneList` component, instantiates it and
|
||||
verifies that the phones array property on it contains three records. Note that the data is now on
|
||||
the controller instance itself, not on a `scope`.
|
||||
|
||||
### Running End to End Tests with Protractor
|
||||
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 [Protractor](https://github.com/angular/protractor). Read
|
||||
about the Protractor APIs at http://angular.github.io/protractor/#/api.
|
||||
|
||||
Much like Karma is the test runner for unit tests, we use Protractor to run end-to-end tests.
|
||||
Try it with `npm run protractor`. End-to-end tests are slow, so unlike with unit tests, Protractor
|
||||
will exit after the test run and will not automatically rerun the test suite on every file change.
|
||||
To rerun the test suite, execute `npm run protractor` again.
|
||||
## Running Tests
|
||||
|
||||
<div class="alert alert-info">
|
||||
Note: You must ensure your application is being served via a web-server to test with protractor.
|
||||
You can do this using `npm start`.
|
||||
You also need to ensure you've installed the protractor and updated webdriver prior to running the
|
||||
`npm run protractor`. You can do this by issuing `npm install` and `npm run update-webdriver` into
|
||||
your terminal.
|
||||
</div>
|
||||
Same as before, execute `npm test` to run the tests and then watch the files for changes.
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
### Display Current Query
|
||||
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.
|
||||
<div></div>
|
||||
|
||||
### Display Query in Title
|
||||
Let's see how we can get the current value of the `query` model to appear in the HTML page title.
|
||||
* Try the experiments from the previous step, this time on the `phoneList` component.
|
||||
|
||||
* Add an end-to-end test into the `describe` block, `test/e2e/scenarios.js` should look like this:
|
||||
* Add a couple more phone lists on the page, by just adding more `<phone-list></phone-list>`
|
||||
elements in `index.html`. Now add another binding to the `phoneList` component's template:
|
||||
|
||||
```js
|
||||
describe('PhoneCat App', function() {
|
||||
|
||||
describe('Phone list view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('app/index.html');
|
||||
});
|
||||
|
||||
var phoneList = element.all(by.repeater('phone in phones'));
|
||||
var query = element(by.model('query'));
|
||||
|
||||
it('should filter the phone list as a user types into the search box', function() {
|
||||
expect(phoneList.count()).toBe(3);
|
||||
|
||||
query.sendKeys('nexus');
|
||||
expect(phoneList.count()).toBe(1);
|
||||
|
||||
query.clear();
|
||||
query.sendKeys('motorola');
|
||||
expect(phoneList.count()).toBe(2);
|
||||
});
|
||||
|
||||
it('should display the current filter value in the title bar', function() {
|
||||
query.clear();
|
||||
expect(browser.getTitle()).toMatch(/Google Phone Gallery:\s*$/);
|
||||
|
||||
query.sendKeys('nexus');
|
||||
expect(browser.getTitle()).toMatch(/Google Phone Gallery: nexus$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
template:
|
||||
'<p>Total number of phones: {{$ctrl.phones.length}}</p>' +
|
||||
'<ul>' +
|
||||
...
|
||||
```
|
||||
|
||||
Run protractor (`npm run protractor`) to see this test fail.
|
||||
|
||||
|
||||
* 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 `ng-controller="PhoneListCtrl"` directive, on the body
|
||||
element:
|
||||
|
||||
<body ng-controller="PhoneListCtrl">
|
||||
|
||||
If you want to bind to the query model from the `<title>` element, you must __move__ the
|
||||
`ngController` declaration to the HTML element because it is the common parent of both the body
|
||||
and title elements:
|
||||
|
||||
<html ng-app="phonecatApp" ng-controller="PhoneListCtrl">
|
||||
|
||||
Be sure to __remove__ the `ng-controller` declaration from the body element.
|
||||
|
||||
* Re-run `npm run protractor` to see the test now pass.
|
||||
|
||||
* While using double curlies works fine 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 ng.directive:ngBind ngBind} or
|
||||
{@link ng.directive:ngBindTemplate ngBindTemplate} directives, which are invisible to the user
|
||||
while the page is loading:
|
||||
|
||||
<title ng-bind-template="Google Phone Gallery: {{query}}">Google Phone Gallery</title>
|
||||
Reload the page and watch the new "feature" propagate to all phone lists. In real-world
|
||||
applications, where the phone lists could appear on several different pages, being able to change
|
||||
or add something in one place (e.g. a component's template) and have that change propagate
|
||||
throughout the application, is a big win.
|
||||
|
||||
|
||||
# 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.
|
||||
You have learned how to organize your application and presentation logic into isolated, reusable
|
||||
components. Let's go to {@link step_04 step 4} to learn how to organize our code in directories and
|
||||
files, so it remains easy to locate as our application grows.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="3"></ul>
|
||||
|
||||
|
||||
[jasmine-docs]: http://jasmine.github.io/2.4/introduction.html
|
||||
[jasmine-home]: http://jasmine.github.io/
|
||||
[karma]: https://karma-runner.github.io/
|
||||
[mvc-pattern]: http://en.wikipedia.org/wiki/Model–View–Controller
|
||||
|
||||
@@ -1,199 +1,315 @@
|
||||
@ngdoc tutorial
|
||||
@name 4 - Two-way Data Binding
|
||||
@name 4 - Directory and File Organization
|
||||
@step 4
|
||||
@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.
|
||||
In this step, we will not be adding any new functionality to our application. Instead, we are going
|
||||
to take a step back, refactor our codebase and move files and code around, in order to make our
|
||||
application more easily expandable and maintainable.
|
||||
|
||||
* 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.
|
||||
In the previous step, we saw how to architect our application to be modular and testable. What's
|
||||
equally important though, is organizing our codebase in a way that makes it easy (both for us and
|
||||
other developers on our team) to navigate through the code and quickly locate the pieces that are
|
||||
relevant to a specific feature or section of the application.
|
||||
|
||||
To that end, we will explain why and how we:
|
||||
|
||||
* Put each entity in its **own file**.
|
||||
* Organize our code by **feature area**, instead of by function.
|
||||
* Split our code into **modules** that other modules can depend on.
|
||||
|
||||
<div class="alert alert-info">
|
||||
We will keep it short, not going into great detail on every good practice and convention. These
|
||||
principles are explained in great detail in the [Angular Style Guide][styleguide], which also
|
||||
contains many more techniques for effectively organizing Angular codebases.
|
||||
</div>
|
||||
|
||||
|
||||
<div doc-tutorial-reset="4"></div>
|
||||
|
||||
|
||||
## Template
|
||||
## One Feature per File
|
||||
|
||||
__`app/index.html`:__
|
||||
It might be tempting, for the sake of simplicity, to put everything in one file, or have one file
|
||||
per type; e.g. all controllers is one file, all components in another file, all services in a third
|
||||
file, and so on.
|
||||
This might seem to work well in the beginning, but as our application grows it becomes a burden to
|
||||
maintain. As we add more and more features, our files will get bigger and bigger and it will be
|
||||
difficult to navigate and find the code we are looking for.
|
||||
|
||||
```html
|
||||
Search: <input ng-model="query">
|
||||
Sort by:
|
||||
<select ng-model="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
Instead we should put each feature/entity in its own file. Each stand-alone controller will be
|
||||
defined in its own file, each component will be defined in each own file, etc.
|
||||
|
||||
Luckily, we don't need to change anything with respect to that guideline in our code, since we have
|
||||
already defined our `phoneList` component in its own `phone-list.component.js` file. Good job!
|
||||
|
||||
We will keep this in mind though, as we add more features.
|
||||
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp">
|
||||
<span>{{phone.name}}</span>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
## Organizing by Feature
|
||||
|
||||
So, now that we learned we should put everything in its own file, our `app/` directory will soon be
|
||||
full with dozens of files and specs (remember we keep our unit test files next to the corresponding
|
||||
source code files). What's more important, logically related files will not be grouped together; it
|
||||
will be really difficult of locate all files related to a specific section of the application and
|
||||
make a change or fix a bug.
|
||||
|
||||
So, what shall we do?
|
||||
|
||||
Well, we are going to group our files into directories _by feature_. For example, since we have a
|
||||
section in our application that lists phones, we will put all related files into a `phone-list/`
|
||||
directory under `app/`. We are soon to find out that certain features are used across different
|
||||
parts of the application. We will put those inside `app/core/`.
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
Other typical names for our `core` directory are `shared`, `common` and `components`. The last
|
||||
one is kind of misleading though, as it will contain other things than components as well.
|
||||
</p>
|
||||
<p>
|
||||
(This is mostly a relic of the past, when "components" just meant the generic building blocks of
|
||||
an application.)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Based on what we have discussed so far, here is our directory/file layout for the `phoneList`
|
||||
"feature":
|
||||
|
||||
```
|
||||
app/
|
||||
phone-list/
|
||||
phone-list.component.js
|
||||
phone-list.component.spec.js
|
||||
app.js
|
||||
```
|
||||
|
||||
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.
|
||||
## Using Modules
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_04.png">
|
||||
As previously mentioned, one of the benefits of having a modular architecture is code reuse —
|
||||
not only inside the same application, but across applications too. There is one final step in making
|
||||
this code reuse frictionless:
|
||||
|
||||
* We then chained the `filter` filter with {@link ng.filter:orderBy `orderBy`}
|
||||
filter to further process the input into the repeater. `orderBy` is a filter that takes an input
|
||||
array, copies it and reorders the copy which is then returned.
|
||||
* Each feature/section should declare its own module and all related entities should register
|
||||
themselves on that module.
|
||||
|
||||
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` filter.
|
||||
|
||||
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/controllers.js`:__
|
||||
Let's take the `phoneList` feature as an example. Previously, the `phoneList` component would
|
||||
register itself on the `phonecatApp` module:
|
||||
|
||||
```js
|
||||
var phonecatApp = angular.module('phonecatApp', []);
|
||||
|
||||
phonecatApp.controller('PhoneListCtrl', function ($scope) {
|
||||
$scope.phones = [
|
||||
{'name': 'Nexus S',
|
||||
'snippet': 'Fast just got faster with Nexus S.',
|
||||
'age': 1},
|
||||
{'name': 'Motorola XOOM™ with Wi-Fi',
|
||||
'snippet': 'The Next, Next Generation tablet.',
|
||||
'age': 2},
|
||||
{'name': 'MOTOROLA XOOM™',
|
||||
'snippet': 'The Next, Next Generation tablet.',
|
||||
'age': 3}
|
||||
];
|
||||
|
||||
$scope.orderProp = 'age';
|
||||
});
|
||||
angular.
|
||||
module('phonecatApp').
|
||||
component('phoneList', ...);
|
||||
```
|
||||
|
||||
* 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.
|
||||
Similarly, the accompanying spec file loads the `phonecatApp` module before each test (because
|
||||
that's where our component is registered). Now, imagine that we need a list of phones on another
|
||||
project that we are working on. Thanks to our modular architecture, we don't have to reinvent the
|
||||
wheel; we simply copy the `phone-list/` directory on our other project and add the necessary script
|
||||
tags in our `index.html` file and we are done, right?
|
||||
|
||||
* We added a line to the controller that sets the default value of `orderProp` to `age`. If we had
|
||||
not set a default value here, the `orderBy` filter would remain uninitialized until our
|
||||
user picked an option from the drop down menu.
|
||||
Well, not so fast. The new project doesn't know anything about a `phonecatApp` module. So, we would
|
||||
have to replace all references to `phonecatApp` with the name of this project's main module. As you
|
||||
can imagine this is both laborious and error-prone.
|
||||
|
||||
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.
|
||||
Yeah, you guessed it: There is a better way!
|
||||
|
||||
Each feature/section, will declare its own module and have all related entities registered there.
|
||||
The main module (`phonecatApp`) will declare a dependency on each feature/section module. Now,
|
||||
all it takes to reuse the same code on a new project is copying the feature directory over and
|
||||
adding the feature module as a dependency in the new project's main module.
|
||||
|
||||
Here is what our `phoneList` feature will look like after this change:
|
||||
|
||||
## Test
|
||||
<br />
|
||||
**`/`:**
|
||||
|
||||
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.
|
||||
```
|
||||
app/
|
||||
phone-list/
|
||||
phone-list.module.js
|
||||
phone-list.component.js
|
||||
phone-list.component.spec.js
|
||||
app.module.js
|
||||
```
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
<br />
|
||||
**`app/phone-list/phone-list.module.js`:**
|
||||
|
||||
```js
|
||||
describe('PhoneCat controllers', function() {
|
||||
// Define the `phoneList` module
|
||||
angular.module('phoneList', []);
|
||||
```
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, ctrl;
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.js`:**
|
||||
|
||||
beforeEach(module('phonecatApp'));
|
||||
```js
|
||||
// Register the `phoneList` component on the `phoneList` module,
|
||||
angular.
|
||||
module('phoneList').
|
||||
component('phoneList', {...});
|
||||
```
|
||||
|
||||
beforeEach(inject(function($controller) {
|
||||
scope = {};
|
||||
ctrl = $controller('PhoneListCtrl', {$scope:scope});
|
||||
}));
|
||||
<br />
|
||||
**`app/app.module.js`:**<br />
|
||||
_(since `app/app.js` now only contains the main module declaration, we gave it a `.module` suffix)_
|
||||
|
||||
it('should create "phones" model with 3 phones', function() {
|
||||
expect(scope.phones.length).toBe(3);
|
||||
});
|
||||
```js
|
||||
// Define the `phonecatApp` module
|
||||
angular.module('phonecatApp', [
|
||||
// ...which depends on the `phoneList` module
|
||||
'phoneList'
|
||||
]);
|
||||
```
|
||||
|
||||
By passing `phoneList` inside the dependencies array when defining the `phonecatApp` module, Angular
|
||||
will make all entities registered on `phoneList` available on `phonecatApp` as well.
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
Don't forget to also update your `index.html` adding a `<script>` tag for each JavaScript file
|
||||
we have created. This might seem tedious, but is totally worth it.
|
||||
</p>
|
||||
<p>
|
||||
In a production-ready application, you would concatenate and minify all your JavaScript files
|
||||
anyway (for performance reasons), so this won't be an issue any more.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
Note that files defining a module (i.e. `.module.js`) need to be included before other files that
|
||||
add features (e.g. components, controllers, services, filters) to that module.
|
||||
</div>
|
||||
|
||||
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(scope.orderProp).toBe('age');
|
||||
});
|
||||
## External Templates
|
||||
|
||||
Since we are at refactoring, let's do one more thing. As we learned, components have templates,
|
||||
which are basically fragments of HTML code that dictate how our data is laid out and presented to
|
||||
the user. In {@link step_03 step 3}, we saw how we can specify the template for a component as a
|
||||
string using the `template` property of the CDO (Component Definition Object).
|
||||
Having HTML code in a string isn't ideal, especially for bigger templates. It would be much better,
|
||||
if we could have our HTML code in `.html` files. This way, we would get all the support our
|
||||
IDE/editor has to offer (e.g. HTML-specific color-highlighting and auto-completion) and also keep
|
||||
our component definitions cleaner.
|
||||
|
||||
So, while it's perfectly fine to keep our component templates inline (using the `template` property
|
||||
of the CDO), we are going to use an external template for our `phoneList` component. In order to
|
||||
denote that we are using an external template, we use the `templateUrl` property and specify the URL
|
||||
that our template will be loaded from. Since we want to keep our template close to where the
|
||||
component is defined, we place it inside `app/phone-list/`.
|
||||
|
||||
We copied the contents of the `template` property (the HTML code) into
|
||||
`app/phone-list/phone-list.template.html` and modified our CDO like this:
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phoneList').
|
||||
component('phoneList', {
|
||||
// Note: The URL is relative to our `index.html` file
|
||||
templateUrl: 'phone-list/phone-list.template.html',
|
||||
controller: ...
|
||||
});
|
||||
```
|
||||
|
||||
At runtime, when Angular needs to create an instance of the `phoneList` component, it will make an
|
||||
HTTP request to get the template from `app/phone-list/phone-list.template.html`.
|
||||
|
||||
<div class="alert alert-info">
|
||||
Keeping inline with our convention, we will be using the `.template` suffix for external
|
||||
templates. Another common convention is to just have the `.html` extension
|
||||
(e.g. `phone-list.html`).
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<p>
|
||||
Using an external template like this, will result in more HTTP requests to the server (one for
|
||||
each external template). Although Angular takes care not to make extraneous requests (e.g.
|
||||
fetching the templates lazily, caching the results, etc), additional requests do have a cost
|
||||
(especially on mobile devices and data-plan connections).
|
||||
</p>
|
||||
<p>
|
||||
Luckily, there are ways to avoid the extra costs (while still keeping your templates external).
|
||||
A detailed discussion of the subject is outside the scope of this tutorial, but you can take a
|
||||
look at the {@link ng.$templateRequest $templateRequest} and
|
||||
{@link ng.$templateCache $templateCache} services for more info on how Angular manages external
|
||||
templates.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
## Final Directory/File Layout
|
||||
|
||||
After all the refactorings that took place, this is how our application looks from the outside:
|
||||
|
||||
<br />
|
||||
**`/`:**
|
||||
|
||||
```
|
||||
app/
|
||||
phone-list/
|
||||
phone-list.component.js
|
||||
phone-list.component.spec.js
|
||||
phone-list.module.js
|
||||
phone-list.template.html
|
||||
app.css
|
||||
app.module.js
|
||||
index.html
|
||||
```
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
Since this was just a refactoring step (no actual code addition/deletions), we shouldn't need to
|
||||
change much (if anything) as far as our specs are concerned.
|
||||
|
||||
One thing that we can (and should) change is the name of the module to be loaded before each test in
|
||||
`app/phone-list/phone-list.component.spec.js`. We don't need to pull in the whole `phonecatApp`
|
||||
module (which will soon grow to depend on more stuff). All we want to test is already included in
|
||||
the much smaller `phoneList` module, so it suffices to just load that.
|
||||
This is one extra benefit that we get out of our modular architecture for free.
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.spec.js`:**
|
||||
|
||||
```js
|
||||
describe('phoneList', function() {
|
||||
|
||||
// Load the module that contains the `phoneList` component before each test
|
||||
beforeEach(module('phoneList'));
|
||||
|
||||
...
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
If not already done so, run the tests (using the `npm test` command) and verify that they still
|
||||
pass.
|
||||
|
||||
The unit test now verifies that the default ordering property is set.
|
||||
<div class="alert alert-success">
|
||||
One of the great things about tests is the confidence they provide, when refactoring your
|
||||
application. It's easy to break something as you start moving files around and re-arranging
|
||||
modules. Having good test coverage is the quickest, easiest and most reliable way of knowing that
|
||||
your application will continue to work as expected.
|
||||
</div>
|
||||
|
||||
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.
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
<pre>Chrome 22.0: Executed 2 of 2 SUCCESS (0.021 secs / 0.001 secs)</pre>
|
||||
|
||||
|
||||
Let's turn our attention to the end-to-end test.
|
||||
|
||||
__`test/e2e/scenarios.js`:__
|
||||
|
||||
```js
|
||||
...
|
||||
it('should be possible to control phone order via the drop down select box', function() {
|
||||
|
||||
var phoneNameColumn = element.all(by.repeater('phone in phones').column('phone.name'));
|
||||
var query = element(by.model('query'));
|
||||
|
||||
function getNames() {
|
||||
return phoneNameColumn.map(function(elm) {
|
||||
return elm.getText();
|
||||
});
|
||||
}
|
||||
|
||||
query.sendKeys('tablet'); //let's narrow the dataset to make the test assertions shorter
|
||||
|
||||
expect(getNames()).toEqual([
|
||||
"Motorola XOOM\u2122 with Wi-Fi",
|
||||
"MOTOROLA XOOM\u2122"
|
||||
]);
|
||||
|
||||
element(by.model('orderProp')).element(by.css('option[value="name"]')).click();
|
||||
|
||||
expect(getNames()).toEqual([
|
||||
"MOTOROLA XOOM\u2122",
|
||||
"Motorola XOOM\u2122 with Wi-Fi"
|
||||
]);
|
||||
});...
|
||||
```
|
||||
|
||||
The end-to-end test verifies that the ordering mechanism of the select box is working correctly.
|
||||
|
||||
You can now rerun `npm run protractor` to see the tests run.
|
||||
|
||||
# Experiments
|
||||
|
||||
* In the `PhoneListCtrl` controller, remove the statement that sets the `orderProp` value and
|
||||
you'll see that Angular will temporarily add a new blank ("unknown") option to the drop-down list and the
|
||||
ordering will default to unordered/natural order.
|
||||
|
||||
* Add an `{{orderProp}}` binding into the `index.html` template to display its current value as
|
||||
text.
|
||||
|
||||
* Reverse the sort order by adding a `-` symbol before the sorting value: `<option value="-age">Oldest</option>`
|
||||
|
||||
# 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.
|
||||
Even if we didn't add any new and exciting functionality to our application, we have made a great
|
||||
step towards a well-architected and maintainable application. Time to spice things up. Let's go to
|
||||
{@link step_05 step 5} to learn how to add full-text search to the application.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="4"></ul>
|
||||
|
||||
|
||||
[angular-seed]: https://github.com/angular/angular-seed
|
||||
[styleguide]: https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md
|
||||
|
||||
@@ -1,277 +1,168 @@
|
||||
@ngdoc tutorial
|
||||
@name 5 - XHRs & Dependency Injection
|
||||
@name 5 - Filtering Repeaters
|
||||
@step 5
|
||||
@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 guide/services services} called {@link
|
||||
ng.$http $http}. We will use Angular's {@link guide/di dependency
|
||||
injection (DI)} to provide the service to the `PhoneListCtrl` controller.
|
||||
We did a lot of work in laying a foundation for the app in the previous steps, 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 (E2E) test, because a good E2E test is a good friend. It stays with your app, keeps an
|
||||
eye on it, and quickly detects regressions.
|
||||
|
||||
* 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.
|
||||
|
||||
* There is now a list of 20 phones, loaded from the server.
|
||||
|
||||
<div doc-tutorial-reset="5"></div>
|
||||
|
||||
|
||||
## Data
|
||||
The `app/phones/phones.json` file in your project is a dataset that contains a larger list of phones
|
||||
stored in the JSON format.
|
||||
## Component Controller
|
||||
|
||||
Following is a sample of the file:
|
||||
We made no changes to the component's controller.
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
"age": 13,
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
"snippet": "Are you ready for everything life throws your way?"
|
||||
...
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
## Component Template
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.template.html`:**
|
||||
|
||||
```html
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<!--Sidebar content-->
|
||||
|
||||
Search: <input ng-model="$ctrl.query" />
|
||||
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<!--Body content-->
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query">
|
||||
<span>{{phone.name}}</span>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
We added a standard HTML `<input>` tag and used Angular's {@link ng.filter:filter filter} function
|
||||
to process the input for the {@link ngRepeat ngRepeat} directive.
|
||||
|
||||
## Controller
|
||||
By virtue of the {@link ngModel ngModel} directive, 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:
|
||||
|
||||
We'll use Angular's {@link ng.$http $http} service in our controller to make an HTTP
|
||||
request to your web server to fetch the data in the `app/phones/phones.json` file. `$http` is just
|
||||
one of several built-in {@link guide/services Angular services} that handle common operations
|
||||
in web apps. Angular injects these services for you where you need them.
|
||||
* Data-binding: This is one of the core features in Angular. When the page loads, Angular binds the
|
||||
value of the input box to the data model variable specified with `ngModel` and keeps the two in
|
||||
sync.
|
||||
|
||||
Services are managed by Angular's {@link 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).
|
||||
In this code, the data that a user types into the input box (bound to **`$ctrl.query`**) is
|
||||
immediately available as a filter input in the list repeater
|
||||
(`phone in $ctrl.phones | filter:`**`$ctrl.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.
|
||||
|
||||
__`app/js/controllers.js:`__
|
||||
<img class="diagram" src="img/tutorial/tutorial_05.png">
|
||||
|
||||
* Use of the `filter` filter: The {@link ng.filter:filter filter} function uses the `$ctrl.query`
|
||||
value to create a new array that contains only those records that match the query.
|
||||
|
||||
`ngRepeat` automatically updates the view in response to the changing number of phones returned
|
||||
by the `filter` filter. The process is completely transparent to the developer.
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
In previous steps, we learned how to write and run unit tests. Unit tests are perfect for testing
|
||||
controllers and other parts of our application written in JavaScript, but they can't easily
|
||||
test templates, DOM manipulation or interoperability of components and services. For these, an
|
||||
end-to-end (E2E) test is a much better choice.
|
||||
|
||||
The search feature was fully implemented via templates and data-binding, so we'll write our first
|
||||
E2E test, to verify that the feature works.
|
||||
|
||||
<br />
|
||||
**`e2e-tests/scenarios.js`:**
|
||||
|
||||
```js
|
||||
var phonecatApp = angular.module('phonecatApp', []);
|
||||
describe('PhoneCat Application', function() {
|
||||
|
||||
describe('phoneList', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('index.html');
|
||||
});
|
||||
|
||||
it('should filter the phone list as a user types into the search box', function() {
|
||||
var phoneList = element.all(by.repeater('phone in $ctrl.phones'));
|
||||
var query = element(by.model('$ctrl.query'));
|
||||
|
||||
expect(phoneList.count()).toBe(3);
|
||||
|
||||
query.sendKeys('nexus');
|
||||
expect(phoneList.count()).toBe(1);
|
||||
|
||||
query.clear();
|
||||
query.sendKeys('motorola');
|
||||
expect(phoneList.count()).toBe(2);
|
||||
});
|
||||
|
||||
phonecatApp.controller('PhoneListCtrl', function ($scope, $http) {
|
||||
$http.get('phones/phones.json').success(function(data) {
|
||||
$scope.phones = data;
|
||||
});
|
||||
|
||||
$scope.orderProp = 'age';
|
||||
});
|
||||
```
|
||||
|
||||
`$http` makes an HTTP GET request to our web server, asking for `phones/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 `$http` service returns a {@link ng.$q promise object} with a `success`
|
||||
method. We call this method to handle the asynchronous response and assign the phone data to the
|
||||
scope controlled by this 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 dependencies you need as arguments
|
||||
to the controller's constructor function, as follows:
|
||||
|
||||
phonecatApp.controller('PhoneListCtrl', function ($scope, $http) {...}
|
||||
|
||||
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).
|
||||
|
||||
Note that the names of arguments are significant, because the injector uses these to look up the
|
||||
dependencies.
|
||||
This test verifies that the search box and the repeater are correctly wired together. Notice how
|
||||
easy it is to write E2E tests in Angular. Although this example is for a simple test, it really is
|
||||
that easy to set up any functional, readable, E2E test.
|
||||
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_05.png">
|
||||
## Running E2E Tests with Protractor
|
||||
|
||||
Even though the syntax of this test looks very much like our controller unit test written with
|
||||
Jasmine, the E2E test uses APIs of [Protractor][protractor]. Read about the Protractor APIs in the
|
||||
[Protractor API Docs][protractor-docs].
|
||||
|
||||
### `$` 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.
|
||||
|
||||
The `$` prefix is there to namespace Angular-provided services.
|
||||
To prevent collisions it's best to avoid naming your services and models anything that begins with a `$`.
|
||||
|
||||
If you inspect a Scope, you may also notice some properties that begin with `$$`. These
|
||||
properties are considered private, and should not be accessed or modified.
|
||||
|
||||
|
||||
### 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 [minify](http://goo.gl/SAnnsm) the JavaScript code for
|
||||
`PhoneListCtrl` controller, all of its function arguments would be minified as well, and the
|
||||
dependency injector would not be able to identify services correctly.
|
||||
|
||||
We can overcome this problem by annotating the function with the names of the dependencies, provided
|
||||
as strings, which will not get minified. There are two ways to provide these injection annotations:
|
||||
|
||||
* Create a `$inject` property on the controller function which holds an array of strings.
|
||||
Each string in the array is the name of the service to inject for the corresponding parameter.
|
||||
In our example, we would write:
|
||||
|
||||
```js
|
||||
function PhoneListCtrl($scope, $http) {...}
|
||||
PhoneListCtrl.$inject = ['$scope', '$http'];
|
||||
phonecatApp.controller('PhoneListCtrl', PhoneListCtrl);
|
||||
```
|
||||
|
||||
* Use an inline annotation where, instead of just providing the function, you provide an array.
|
||||
This array contains a list of the service names, followed by the function itself.
|
||||
|
||||
```js
|
||||
function PhoneListCtrl($scope, $http) {...}
|
||||
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', PhoneListCtrl]);
|
||||
```
|
||||
|
||||
Both of these methods work with any function that can be injected by Angular, so it's up to your
|
||||
project's style guide to decide which one you use.
|
||||
|
||||
When using the second method, it is common to provide the constructor function inline as an
|
||||
anonymous function when registering the controller:
|
||||
|
||||
```js
|
||||
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', function($scope, $http) {...}]);
|
||||
```
|
||||
|
||||
From this point onward, we're going to use the inline method in the tutorial. With that in mind,
|
||||
let's add the annotations to our `PhoneListCtrl`:
|
||||
|
||||
__`app/js/controllers.js:`__
|
||||
|
||||
```js
|
||||
var phonecatApp = angular.module('phonecatApp', []);
|
||||
|
||||
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http',
|
||||
function ($scope, $http) {
|
||||
$http.get('phones/phones.json').success(function(data) {
|
||||
$scope.phones = data;
|
||||
});
|
||||
|
||||
$scope.orderProp = 'age';
|
||||
}]);
|
||||
```
|
||||
|
||||
## 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 `$http` implementation. However, Angular provides a mock `$http`
|
||||
service that we can use in unit tests. We configure "fake" responses to server requests by calling
|
||||
methods on a service called `$httpBackend`:
|
||||
|
||||
```js
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, ctrl, $httpBackend;
|
||||
|
||||
// Load our app module definition before each test.
|
||||
beforeEach(module('phonecatApp'));
|
||||
|
||||
// The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).
|
||||
// This allows us to inject a service but then attach it to a variable
|
||||
// with the same name as the service in order to avoid a name conflict.
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/phones.json').
|
||||
respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller('PhoneListCtrl', {$scope: scope});
|
||||
}));
|
||||
```
|
||||
|
||||
Note: Because we loaded Jasmine and `angular-mocks.js` in our test environment, we got two helper
|
||||
methods {@link angular.mock.module module} and {@link angular.mock.inject inject} that we'll
|
||||
use to access and configure the injector.
|
||||
|
||||
We created the controller in the test environment, as follows:
|
||||
|
||||
* We used the `inject` helper method to inject instances of
|
||||
{@link ng.$rootScope $rootScope},
|
||||
{@link ng.$controller $controller} and
|
||||
{@link ng.$httpBackend $httpBackend} services into the Jasmine's `beforeEach`
|
||||
function. These instances come from an injector which is recreated from scratch for every single
|
||||
test. This guarantees that each test starts from a well known starting point and each test is
|
||||
isolated from the work done in other tests.
|
||||
|
||||
* We created a new scope for our controller by calling `$rootScope.$new()`
|
||||
|
||||
* We called the injected `$controller` function passing the name of the `PhoneListCtrl` controller
|
||||
and the created scope as parameters.
|
||||
|
||||
Because our code now uses the `$http` 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:
|
||||
|
||||
* Request `$httpBackend` service to be injected into our `beforeEach` function. This is a mock
|
||||
version of the service that in a production environment facilitates all XHR and JSONP requests.
|
||||
The mock version of this service allows you to write tests without having to deal with
|
||||
native APIs and the global state associated with them — both of which make testing a nightmare.
|
||||
|
||||
* Use the `$httpBackend.expectGET` method to train the `$httpBackend` service to expect an incoming
|
||||
HTTP request and tell it what to respond with. Note that the responses are not returned until we call
|
||||
the `$httpBackend.flush` method.
|
||||
|
||||
Now we will make assertions to verify that the `phones` model doesn't exist on `scope` before
|
||||
the response is received:
|
||||
|
||||
```js
|
||||
it('should create "phones" model with 2 phones fetched from xhr', function() {
|
||||
expect(scope.phones).toBeUndefined();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(scope.phones).toEqual([{name: 'Nexus S'},
|
||||
{name: 'Motorola DROID'}]);
|
||||
});
|
||||
```
|
||||
|
||||
* We flush the request queue in the browser by calling `$httpBackend.flush()`. This causes the
|
||||
promise returned by the `$http` service to be resolved with the trained response. See
|
||||
'Flushing HTTP requests' in the {@link ngMock.$httpBackend mock $httpBackend} documentation for
|
||||
a full explanation of why this is necessary.
|
||||
|
||||
* 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:
|
||||
|
||||
```js
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(scope.orderProp).toBe('age');
|
||||
});
|
||||
```
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
<pre>Chrome 22.0: Executed 2 of 2 SUCCESS (0.028 secs / 0.007 secs)</pre>
|
||||
Much like Karma is the test runner for unit tests, we use Protractor to run E2E tests. Try it with
|
||||
`npm run protractor`. E2E tests take time, so unlike with unit tests, Protractor will exit after the
|
||||
tests run and will not automatically rerun the test suite on every file change.
|
||||
To rerun the test suite, execute `npm run protractor` again.
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** In order for protractor to access and run tests against your application, it must be
|
||||
served via a web server. In a different terminal/command line window, run `npm start` to fire up
|
||||
the web server.
|
||||
</div>
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* At the bottom of `index.html`, add a `<pre>{{phones | filter:query | orderBy:orderProp | json}}</pre>`
|
||||
binding to see the list of phones displayed in json format.
|
||||
<div></div>
|
||||
|
||||
* In the `PhoneListCtrl` controller, pre-process the http response by limiting the number of phones
|
||||
to the first 5 in the list. Use the following code in the `$http` callback:
|
||||
* Display the current value of the `query` model by adding a `{{$ctrl.query}}` binding into the
|
||||
`phone-list.template.html` template and see how it changes, when you type in the input box.
|
||||
|
||||
$scope.phones = data.splice(0, 5);
|
||||
You might also try to add the `{{$ctrl.query}}` binding to `index.html`. 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 `<phone-list>` component.<br />
|
||||
Component isolation at work!
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now that you have learned how easy it is to use Angular services (thanks to Angular's dependency
|
||||
injection), go to {@link step_06 step 6}, where you will add some
|
||||
thumbnail images of phones and some links.
|
||||
We have now added full-text search and included a test to verify that it works! Now let's go on to
|
||||
{@link step_06 step 6} to learn how to add sorting capabilities to the PhoneCat application.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="5"></ul>
|
||||
|
||||
|
||||
[protractor]: https://github.com/angular/protractor
|
||||
[protractor-docs]: https://angular.github.io/protractor/#/api
|
||||
|
||||
@@ -1,108 +1,241 @@
|
||||
@ngdoc tutorial
|
||||
@name 6 - Templating Links & Images
|
||||
@name 6 - Two-way Data Binding
|
||||
@step 6
|
||||
@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.
|
||||
In this step, we will add a feature to let our 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.
|
||||
|
||||
* In addition to the search box, the application displays a drop-down menu that allows users to
|
||||
control the order in which the phones are listed.
|
||||
|
||||
* There are now links and images of the phones in the list.
|
||||
|
||||
<div doc-tutorial-reset="6"></div>
|
||||
|
||||
## 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.
|
||||
## Component Template
|
||||
|
||||
__`app/phones/phones.json`__ (sample snippet):
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
...
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
...
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
__`app/index.html`:__
|
||||
<br />
|
||||
**`app/phone-list/phone-list.template.html`:**
|
||||
|
||||
```html
|
||||
...
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<!--Sidebar content-->
|
||||
|
||||
<p>
|
||||
Search:
|
||||
<input ng-model="$ctrl.query">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Sort by:
|
||||
<select ng-model="$ctrl.orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<!--Body content-->
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
|
||||
<a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}" alt="{{phone.name}}"></a>
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp">
|
||||
<span>{{phone.name}}</span>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
To dynamically generate links that will in the future lead to phone detail pages, we used the
|
||||
now-familiar double-curly brace binding 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 made the following changes to the `phone-list.template.html` template:
|
||||
|
||||
We also added phone images next to each record using an image tag with the {@link
|
||||
ng.directive:ngSrc ngSrc} directive. That directive prevents the
|
||||
browser from treating the Angular `{{ expression }}` markup literally, and initiating a request to
|
||||
an invalid URL `http://localhost:8000/app/{{phone.imageUrl}}`, which it would have done if we had
|
||||
only specified an attribute binding in a regular `src` attribute (`<img src="{{phone.imageUrl}}">`).
|
||||
Using the `ngSrc` directive prevents the browser from making an http request to an invalid location.
|
||||
* First, we added a `<select>` element bound to `$ctrl.orderProp`, so that our users can pick from
|
||||
the two provided sorting options.
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_06.png">
|
||||
|
||||
* We then chained the `filter` filter with the {@link orderBy orderBy} filter to further process the
|
||||
input for the repeater. `orderBy` is a filter that takes an input array, copies it and reorders
|
||||
the copy which is then returned.
|
||||
|
||||
Angular creates a two way data-binding between the select element and the `$ctrl.orderProp` model.
|
||||
`$ctrl.orderProp` is then used as the input for the `orderBy` filter.
|
||||
|
||||
As we discussed in the section about data-binding and the repeater in {@link step_05 step 5},
|
||||
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!
|
||||
|
||||
|
||||
## Test
|
||||
## Component Controller
|
||||
|
||||
__`test/e2e/scenarios.js`__:
|
||||
<br />
|
||||
**`app/phone-list/phone-list.components.js`:**
|
||||
|
||||
```js
|
||||
...
|
||||
it('should render phone specific links', function() {
|
||||
var query = element(by.model('query'));
|
||||
query.sendKeys('nexus');
|
||||
element.all(by.css('.phones li a')).first().click();
|
||||
browser.getLocationAbsUrl().then(function(url) {
|
||||
expect(url).toBe('/phones/nexus-s');
|
||||
});
|
||||
angular.
|
||||
module('phoneList').
|
||||
component('phoneList', {
|
||||
templateUrl: 'phone-list/phone-list.template.html',
|
||||
controller: function PhoneListController() {
|
||||
this.phones = [
|
||||
{
|
||||
name: 'Nexus S',
|
||||
snippet: 'Fast just got faster with Nexus S.',
|
||||
age: 1
|
||||
}, {
|
||||
name: 'Motorola XOOM™ with Wi-Fi',
|
||||
snippet: 'The Next, Next Generation tablet.',
|
||||
age: 2
|
||||
}, {
|
||||
name: 'MOTOROLA XOOM™',
|
||||
snippet: 'The Next, Next Generation tablet.',
|
||||
age: 3
|
||||
}
|
||||
];
|
||||
|
||||
this.orderProp = 'age';
|
||||
}
|
||||
});
|
||||
...
|
||||
```
|
||||
|
||||
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.
|
||||
* We modified the `phones` model - the array of phones - and added an `age` property to each phone
|
||||
record. This property is used to order the phones by age.
|
||||
|
||||
* We added a line to the controller that sets the default value of `orderProp` to `age`. If we had
|
||||
not set a default value here, the `orderBy` filter would remain uninitialized until the user
|
||||
picked an option from the drop-down menu.
|
||||
|
||||
This is a good time to talk about two-way data-binding. Notice that when the application 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.
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
The changes we made should be verified with both a unit test and an E2E test. Let's look at the unit
|
||||
test first.
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.spec.js`:**
|
||||
|
||||
```js
|
||||
describe('phoneList', function() {
|
||||
|
||||
// Load the module that contains the `phoneList` component before each test
|
||||
beforeEach(module('phoneList'));
|
||||
|
||||
// Test the controller
|
||||
describe('PhoneListController', function() {
|
||||
var ctrl;
|
||||
|
||||
beforeEach(inject(function($componentController) {
|
||||
ctrl = $componentController('phoneList');
|
||||
}));
|
||||
|
||||
it('should create a `phones` model with 3 phones', function() {
|
||||
expect(ctrl.phones.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should set a default value for the `orderProp` model', function() {
|
||||
expect(ctrl.orderProp).toBe('age');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
```
|
||||
Chrome 49.0: Executed 2 of 2 SUCCESS (0.136 secs / 0.08 secs)
|
||||
```
|
||||
|
||||
Let's turn our attention to the E2E tests.
|
||||
|
||||
<br />
|
||||
**`e2e-tests/scenarios.js`:**
|
||||
|
||||
```js
|
||||
describe('PhoneCat Application', function() {
|
||||
|
||||
describe('phoneList', function() {
|
||||
|
||||
...
|
||||
|
||||
it('should be possible to control phone order via the drop-down menu', function() {
|
||||
var queryField = element(by.model('$ctrl.query'));
|
||||
var orderSelect = element(by.model('$ctrl.orderProp'));
|
||||
var nameOption = orderSelect.element(by.css('option[value="name"]'));
|
||||
var phoneNameColumn = element.all(by.repeater('phone in $ctrl.phones').column('phone.name'));
|
||||
|
||||
function getNames() {
|
||||
return phoneNameColumn.map(function(elem) {
|
||||
return elem.getText();
|
||||
});
|
||||
}
|
||||
|
||||
queryField.sendKeys('tablet'); // Let's narrow the dataset to make the assertions shorter
|
||||
|
||||
expect(getNames()).toEqual([
|
||||
'Motorola XOOM\u2122 with Wi-Fi',
|
||||
'MOTOROLA XOOM\u2122'
|
||||
]);
|
||||
|
||||
nameOption.click();
|
||||
|
||||
expect(getNames()).toEqual([
|
||||
'MOTOROLA XOOM\u2122',
|
||||
'Motorola XOOM\u2122 with Wi-Fi'
|
||||
]);
|
||||
});
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
The E2E test verifies that the ordering mechanism of the select box is working correctly.
|
||||
|
||||
You can now rerun `npm run protractor` to see the tests run.
|
||||
|
||||
|
||||
# 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/{{phone.imageUrl}}`).
|
||||
<div></div>
|
||||
|
||||
The issue here is that the browser will fire a request for that invalid image address as soon as
|
||||
it hits the `img` tag, which is before Angular has a chance to evaluate the expression and inject
|
||||
the valid address.
|
||||
* In the `phoneList` component's controller, remove the statement that sets the `orderProp` value
|
||||
and you'll see that Angular will temporarily add a new blank ("unknown") option to the drop-down
|
||||
list and the ordering will default to unordered/natural order.
|
||||
|
||||
* Add a `{{$ctrl.orderProp}}` binding into the `phone-list.template.html` template to display its
|
||||
current value as text.
|
||||
|
||||
* Reverse the sort order by adding a `-` symbol before the sorting value:
|
||||
`<option value="-age">Oldest</option>`
|
||||
|
||||
|
||||
# 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.
|
||||
Now that you have added list sorting and tested the application, go to {@link step_07 step 7} to
|
||||
learn about Angular services and how Angular uses dependency injection.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="6"></ul>
|
||||
|
||||
@@ -1,380 +1,312 @@
|
||||
@ngdoc tutorial
|
||||
@name 7 - Routing & Multiple Views
|
||||
@name 7 - XHR & Dependency Injection
|
||||
@step 7
|
||||
@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, using an Angular module called 'ngRoute'.
|
||||
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 guide/services services} called
|
||||
{@link ng.$http $http}. We will use Angular's {@link guide/di dependency injection (DI)} to provide
|
||||
the service to the `phoneList` component's controller.
|
||||
|
||||
* There is now a list of 20 phones, loaded from the server.
|
||||
|
||||
* When you now navigate to `app/index.html`, you are redirected to `app/index.html/#/phones`
|
||||
and the phone list appears in the browser.
|
||||
* When you click on a phone link, the url changes to that specific phone and the stub of a
|
||||
phone detail page is displayed.
|
||||
|
||||
<div doc-tutorial-reset="7"></div>
|
||||
|
||||
## Dependencies
|
||||
|
||||
The routing functionality added by this step is provided by angular in the `ngRoute` module, which
|
||||
is distributed separately from the core Angular framework.
|
||||
## Data
|
||||
|
||||
We are using [Bower][bower] to install client-side dependencies. This step updates the
|
||||
`bower.json` configuration file to include the new dependency:
|
||||
The `app/phones/phones.json` file in our project is a dataset that contains a larger list of phones,
|
||||
stored in JSON format.
|
||||
|
||||
Following is a sample of the file:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "angular-phonecat",
|
||||
"description": "A starter project for AngularJS",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/angular/angular-phonecat",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "1.4.x",
|
||||
"angular-mocks": "1.4.x",
|
||||
"jquery": "~2.1.1",
|
||||
"bootstrap": "~3.1.1",
|
||||
"angular-route": "1.4.x"
|
||||
}
|
||||
}
|
||||
[
|
||||
{
|
||||
"age": 13,
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
"snippet": "Are you ready for everything life throws your way?"
|
||||
...
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
The new dependency `"angular-route": "1.4.x"` tells bower to install a version of the
|
||||
angular-route component that is compatible with version 1.4.x. We must tell bower to download
|
||||
and install this dependency.
|
||||
|
||||
If you have bower installed globally, then you can run `bower install` but for this project, we have
|
||||
preconfigured npm to run bower install for us:
|
||||
## Component Controller
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
We will use Angular's {@link ng.$http $http} service in our controller for making an HTTP request to
|
||||
our web server to fetch the data in the `app/phones/phones.json` file. `$http` is just one of
|
||||
several built-in {@link guide/services Angular services} that handle common operations in web
|
||||
applications. Angular injects these services for you, right where you need them.
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then you may have a
|
||||
problem with the `bower install` due to a conflict between the versions of angular.js that need to
|
||||
be installed. If you get this then simply delete your `app/bower_components` folder before running
|
||||
`npm install`.
|
||||
</div>
|
||||
Services are managed by Angular's {@link guide/di DI subsystem}. Dependency injection helps to make
|
||||
your web applications both well-structured (e.g. separate entities for presentation, data, and
|
||||
control) and loosely coupled (dependencies between entities are not resolved by the entities
|
||||
themselves, but by the DI subsystem). As a result, applications are easier to test as well.
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** If you have bower installed globally then you can run `bower install` but for this project we have
|
||||
preconfigured `npm install` to run bower for us.
|
||||
</div>
|
||||
|
||||
|
||||
## 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 ngRoute.$routeProvider $routeProvider},
|
||||
which is the provider of the {@link ngRoute.$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 [deep linking](http://en.wikipedia.org/wiki/Deep_linking), which lets us
|
||||
utilize the browser's history (back and forward navigation) and bookmarks.
|
||||
|
||||
|
||||
### A Note About DI, Injector and Providers
|
||||
|
||||
As you {@link tutorial/step_05 noticed}, {@link guide/di dependency injection} (DI) is at the core of
|
||||
AngularJS, so it's important for you to understand a thing or two about how it works.
|
||||
|
||||
When the application bootstraps, Angular creates an injector that will be used to find and inject all
|
||||
of the services that are required by your app. The injector itself doesn't know anything about what
|
||||
`$http` or `$route` services do. In fact, the injector doesn't even know about the existence of these services
|
||||
unless it is configured with proper module definitions.
|
||||
|
||||
The injector only carries out the following steps :
|
||||
|
||||
* load the module definition(s) that you specify in your app
|
||||
* register all Providers defined in these module definitions
|
||||
* when asked to do so, inject a specified function and any necessary dependencies (services) that
|
||||
it lazily instantiates via their Providers.
|
||||
|
||||
Providers are objects that provide (create) instances of services and expose configuration APIs
|
||||
that can be used to control the creation and runtime behavior of a service. In case of the `$route`
|
||||
service, the `$routeProvider` exposes APIs that allow you to define routes for your application.
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Note:** Providers can only be injected into `config` functions. Thus you could not inject
|
||||
`$routeProvider` into `PhoneListCtrl`.
|
||||
</div>
|
||||
|
||||
Angular modules solve the problem of removing global state from the application and provide a way
|
||||
of configuring the injector. As opposed to AMD or require.js modules, Angular modules don't try to
|
||||
solve the problem of script load ordering or lazy script fetching. These goals are totally independent and
|
||||
both module systems can live side by side and fulfill their goals.
|
||||
|
||||
To deepen your understanding of DI on Angular, see
|
||||
[Understanding Dependency Injection](https://github.com/angular/angular.js/wiki/Understanding-Dependency-Injection).
|
||||
|
||||
## Template
|
||||
|
||||
The `$route` service is usually used in conjunction with the {@link ngRoute.directive:ngView
|
||||
ngView} directive. The role of the `ngView` directive is to include the view template for the current
|
||||
route into the layout template. This makes it a perfect fit for our `index.html` template.
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** Starting with AngularJS version 1.2, `ngRoute` is in its own module and must be loaded by
|
||||
loading the additional `angular-route.js` file, which we download via Bower above.
|
||||
</div>
|
||||
|
||||
__`app/index.html`:__
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en" ng-app="phonecatApp">
|
||||
<head>
|
||||
...
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="bower_components/angular-route/angular-route.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
<script src="js/controllers.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div ng-view></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
We have added two new `<script>` tags in our index file to load up extra JavaScript files into our
|
||||
application:
|
||||
|
||||
- `angular-route.js` : defines the Angular `ngRoute` module, which provides us with routing.
|
||||
- `app.js` : this file now holds the root module of our application.
|
||||
|
||||
Note that we removed most of the code in the `index.html` template and replaced it with a single
|
||||
line containing a div with the `ng-view` attribute. The code that we removed was placed into the
|
||||
`phone-list.html` template:
|
||||
|
||||
__`app/partials/phone-list.html`:__
|
||||
|
||||
```html
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<!--Sidebar content-->
|
||||
|
||||
Search: <input ng-model="query">
|
||||
Sort by:
|
||||
<select ng-model="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<!--Body content-->
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
|
||||
<a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
<div style="display:none">
|
||||
TODO!
|
||||
<img class="diagram" src="img/tutorial/tutorial_07_final.png">
|
||||
</div>
|
||||
|
||||
We also added a placeholder template for the phone details view:
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
|
||||
```html
|
||||
TBD: detail view for <span>{{phoneId}}</span>
|
||||
```
|
||||
|
||||
Note how we are using the `phoneId` expression which will be defined in the `PhoneDetailCtrl` controller.
|
||||
|
||||
## The App Module
|
||||
|
||||
To improve the organization of the app, we are making use of Angular's `ngRoute` module and we've
|
||||
moved the controllers into their own module `phonecatControllers` (as shown below).
|
||||
|
||||
We added `angular-route.js` to `index.html` and created a new `phonecatControllers` module in
|
||||
`controllers.js`. That's not all we need to do to be able to use their code, however. We also have
|
||||
to add the modules as dependencies of our app. By listing these two modules as dependencies of
|
||||
`phonecatApp`, we can use the directives and services they provide.
|
||||
|
||||
|
||||
__`app/js/app.js`:__
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.js:`**
|
||||
|
||||
```js
|
||||
var phonecatApp = angular.module('phonecatApp', [
|
||||
'ngRoute',
|
||||
'phonecatControllers'
|
||||
]);
|
||||
angular.
|
||||
module('phoneList').
|
||||
component('phoneList', {
|
||||
templateUrl: 'phone-list/phone-list.template.html',
|
||||
controller: function PhoneListController($http) {
|
||||
var self = this;
|
||||
self.orderProp = 'age';
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Notice the second argument passed to `angular.module`, `['ngRoute', 'phonecatControllers']`. This
|
||||
array lists the modules that `phonecatApp` depends on.
|
||||
|
||||
|
||||
```js
|
||||
...
|
||||
|
||||
phonecatApp.config(['$routeProvider',
|
||||
function($routeProvider) {
|
||||
$routeProvider.
|
||||
when('/phones', {
|
||||
templateUrl: 'partials/phone-list.html',
|
||||
controller: 'PhoneListCtrl'
|
||||
}).
|
||||
when('/phones/:phoneId', {
|
||||
templateUrl: 'partials/phone-detail.html',
|
||||
controller: 'PhoneDetailCtrl'
|
||||
}).
|
||||
otherwise({
|
||||
redirectTo: '/phones'
|
||||
});
|
||||
}]);
|
||||
```
|
||||
|
||||
Using the `phonecatApp.config()` method, we request the `$routeProvider` to be injected into our
|
||||
config function and use the {@link ngRoute.$routeProvider#when `$routeProvider.when()`} method to
|
||||
define our routes.
|
||||
|
||||
Our application routes are defined as follows:
|
||||
|
||||
* `when('/phones')`: 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.
|
||||
|
||||
* `when('/phones/:phoneId')`: The phone details view will be shown when the URL hash fragment
|
||||
matches '/phones/: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.
|
||||
|
||||
* `otherwise({redirectTo: '/phones'})`: triggers a redirection to `/phones` when the browser
|
||||
address doesn't match either of our routes.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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
|
||||
{@link ngRoute.$routeParams `$routeParams`} object.
|
||||
|
||||
|
||||
## Controllers
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
|
||||
```js
|
||||
var phonecatControllers = angular.module('phonecatControllers', []);
|
||||
|
||||
phonecatControllers.controller('PhoneListCtrl', ['$scope', '$http',
|
||||
function ($scope, $http) {
|
||||
$http.get('phones/phones.json').success(function(data) {
|
||||
$scope.phones = data;
|
||||
$http.get('phones/phones.json').then(function(response) {
|
||||
self.phones = response.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$scope.orderProp = 'age';
|
||||
}]);
|
||||
|
||||
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams',
|
||||
function($scope, $routeParams) {
|
||||
$scope.phoneId = $routeParams.phoneId;
|
||||
}]);
|
||||
```
|
||||
|
||||
Again, note that we created a new module called `phonecatControllers`. For small AngularJS
|
||||
applications, it's common to create just one module for all of your controllers if there are just a
|
||||
few. As your application grows, it is quite common to refactor your code into additional modules.
|
||||
For larger apps, you will probably want to create separate modules for each major feature of
|
||||
your app.
|
||||
`$http` makes an HTTP GET request to our web server, asking for `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 will use JSON files
|
||||
in this tutorial.)
|
||||
|
||||
Because our example app is relatively small, we'll just add all of our controllers to the
|
||||
`phonecatControllers` module.
|
||||
The `$http` service returns a {@link ng.$q promise object}, which has a `then()` method. We call
|
||||
this method to handle the asynchronous response and assign the phone data to the controller, as a
|
||||
property called `phones`. Notice that Angular detected the JSON response and parsed it for us into
|
||||
the `data` property of the `response` object passed to our callback!
|
||||
|
||||
Since we are making the assignment of the `phones` property in a callback function, where the `this`
|
||||
value is not defined, we also introduce a local variable called `self` that points back to the
|
||||
controller instance.
|
||||
|
||||
## 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.
|
||||
To use a service in Angular, you simply declare the names of the dependencies you need as arguments
|
||||
to the controller's constructor function, as follows:
|
||||
|
||||
```js
|
||||
...
|
||||
it('should redirect index.html to index.html#/phones', function() {
|
||||
browser.get('app/index.html');
|
||||
browser.getLocationAbsUrl().then(function(url) {
|
||||
expect(url).toEqual('/phones');
|
||||
});
|
||||
function PhoneListController($http) {...}
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
Note that the names of arguments are significant, because the injector uses these to look up the
|
||||
dependencies.
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_05.png">
|
||||
|
||||
|
||||
### `$`-prefix Naming Convention
|
||||
|
||||
You can create your own services, and in fact we will do exactly that a few steps down the road. 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.
|
||||
|
||||
The `$` prefix is there to namespace Angular-provided services. To prevent collisions it's best to
|
||||
avoid naming your services and models anything that begins with a `$`.
|
||||
|
||||
If you inspect a Scope, you may also notice some properties that begin with `$$`. These properties
|
||||
are considered private, and should not be accessed or modified.
|
||||
|
||||
|
||||
### 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 [minify][minification] the JavaScript code for the
|
||||
`PhoneListController` controller, all of its function arguments would be minified as well, and the
|
||||
dependency injector would not be able to identify services correctly.
|
||||
|
||||
We can overcome this problem by annotating the function with the names of the dependencies, provided
|
||||
as strings, which will not get minified. There are two ways to provide these injection annotations:
|
||||
|
||||
* Create an `$inject` property on the controller function which holds an array of strings.
|
||||
Each string in the array is the name of the service to inject for the corresponding parameter.
|
||||
In our example, we would write:
|
||||
|
||||
```js
|
||||
function PhoneListController($http) {...}
|
||||
PhoneListController.$inject = ['$http'];
|
||||
...
|
||||
.component('phoneList', {..., controller: PhoneListController});
|
||||
```
|
||||
|
||||
* Use an inline annotation where, instead of just providing the function, you provide an array.
|
||||
This array contains a list of the service names, followed by the function itself as the last item
|
||||
of the array.
|
||||
|
||||
```js
|
||||
function PhoneListController($http) {...}
|
||||
...
|
||||
.component('phoneList', {..., controller: ['$http', PhoneListController]});
|
||||
```
|
||||
|
||||
Both of these methods work with any function that can be injected by Angular, so it's up to your
|
||||
project's style guide to decide which one you use.
|
||||
|
||||
When using the second method, it is common to provide the constructor function inline, when
|
||||
registering the controller:
|
||||
|
||||
```js
|
||||
.component('phoneList', {..., controller: ['$http', function PhoneListController($http) {...}]});
|
||||
```
|
||||
|
||||
From this point onwards, we are going to use the inline method in the tutorial. With that in mind,
|
||||
let's add the annotations to our `PhoneListController`:
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.js`**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phoneList').
|
||||
component('phoneList', {
|
||||
templateUrl: 'phone-list/phone-list.template.html',
|
||||
controller: ['$http',
|
||||
function PhoneListController($http) {
|
||||
var self = this;
|
||||
self.orderProp = 'age';
|
||||
|
||||
$http.get('phones/phones.json').then(function(response) {
|
||||
self.phones = response.data;
|
||||
});
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
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 `$http` implementation. However, Angular provides a mock `$http`
|
||||
service that we can use in unit tests. We configure "fake" responses to server requests by calling
|
||||
methods on a service called `$httpBackend`:
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.spec.js`:**
|
||||
|
||||
```js
|
||||
describe('phoneList', function() {
|
||||
|
||||
beforeEach(module('phoneList'));
|
||||
|
||||
describe('controller', function() {
|
||||
var $httpBackend, ctrl;
|
||||
|
||||
// The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).
|
||||
// This allows us to inject a service and assign it to a variable with the same name
|
||||
// as the service while avoiding a name conflict.
|
||||
beforeEach(inject(function($componentController, _$httpBackend_) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/phones.json')
|
||||
.respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
|
||||
ctrl = $componentController('phoneList');
|
||||
}));
|
||||
|
||||
...
|
||||
|
||||
});
|
||||
|
||||
describe('Phone list view', function() {
|
||||
beforeEach(function() {
|
||||
browser.get('app/index.html#/phones');
|
||||
});
|
||||
...
|
||||
|
||||
describe('Phone detail view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('app/index.html#/phones/nexus-s');
|
||||
});
|
||||
|
||||
|
||||
it('should display placeholder page with phoneId', function() {
|
||||
expect(element(by.binding('phoneId')).getText()).toBe('nexus-s');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** Because we loaded Jasmine and `angular-mocks.js` in our test environment, we got two
|
||||
helper methods {@link angular.mock.module module} and {@link angular.mock.inject inject} that we
|
||||
can use to access and configure the injector.
|
||||
</div>
|
||||
|
||||
You can now rerun `npm run protractor` to see the tests run.
|
||||
We created the controller in the test environment, as follows:
|
||||
|
||||
* We used the `inject()` helper method to inject instances of
|
||||
{@link ngMock.$componentController $componentController} and {@link ng.$httpBackend $httpBackend}
|
||||
services into Jasmine's `beforeEach()` function. These instances come from an injector which is
|
||||
recreated from scratch for every single test. This guarantees that each test starts from a well
|
||||
known starting point and each test is isolated from the work done in other tests.
|
||||
|
||||
* We called the injected `$componentController` function passing the name of the `phoneList`
|
||||
component (whose controller we wanted to instantiate) as a parameter.
|
||||
|
||||
Because our code now uses the `$http` service to fetch the phone list data in our controller, before
|
||||
we create the `PhoneListController`, we need to tell the testing harness to expect an incoming
|
||||
request from the controller. To do this we:
|
||||
|
||||
* Inject the `$httpBackend` service into the `beforeEach()` function. This is a
|
||||
{@link ngMock.$httpBackend mock version} of the service that in a production environment
|
||||
facilitates all XHR and JSONP requests. The mock version of this service allows us to write tests
|
||||
without having to deal with native APIs and the global state associated with them — both of which
|
||||
make testing a nightmare. It also overcomes the asynchronous nature of these calls, which would
|
||||
slow down unit tests.
|
||||
|
||||
* Use the `$httpBackend.expectGET()` method to train the `$httpBackend` service to expect an
|
||||
incoming HTTP request and tell it what to respond with. Note that the responses are not returned
|
||||
until we call the `$httpBackend.flush()` method.
|
||||
|
||||
Now we will make assertions to verify that the `phones` property doesn't exist on the controller
|
||||
before the response is received:
|
||||
|
||||
```js
|
||||
it('should create a `phones` property with 2 phones fetched with `$http`', function() {
|
||||
expect(ctrl.phones).toBeUndefined();
|
||||
|
||||
$httpBackend.flush();
|
||||
expect(ctrl.phones).toEqual([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
});
|
||||
```
|
||||
|
||||
* We flush the request queue in the browser by calling `$httpBackend.flush()`. This causes the
|
||||
promise returned by the `$http` service to be resolved with the trained response. See
|
||||
{@link ngMock.$httpBackend#flushing-http-requests Flushing HTTP requests} in the mock
|
||||
`$httpBackend` documentation for a full explanation of why this is necessary.
|
||||
|
||||
* We make the assertions, verifying that the `phones` property now exists on the controller.
|
||||
|
||||
Finally, we verify that the default value of `orderProp` is set correctly:
|
||||
|
||||
```js
|
||||
it('should set a default value for the `orderProp` property', function() {
|
||||
expect(ctrl.orderProp).toBe('age');
|
||||
});
|
||||
```
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
```
|
||||
Chrome 49.0: Executed 2 of 2 SUCCESS (0.133 secs / 0.097 secs)
|
||||
```
|
||||
|
||||
|
||||
# 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 `<div ng-view>` element. If you add
|
||||
the same binding into the `phone-list.html` template, the binding will work as expected.
|
||||
<div></div>
|
||||
|
||||
<div style="display: none">
|
||||
* In `PhoneCatCtrl`, create a new model called "`hero`" with `this.hero = 'Zoro'`. In
|
||||
`PhoneListCtrl`, let's shadow it with `this.hero = 'Batman'`. 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.
|
||||
</div>
|
||||
* At the bottom of `phone-list.template.html`, add a
|
||||
`<pre>{{$ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp | json}}</pre>` binding to see
|
||||
the list of phones displayed in JSON format.
|
||||
|
||||
* In the `PhoneListController` controller, pre-process the HTTP response by limiting the number of
|
||||
phones to the first 5 in the list. Use the following code in the `$http` callback:
|
||||
|
||||
```js
|
||||
self.phones = response.data.slice(0, 5);
|
||||
```
|
||||
|
||||
|
||||
# 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.
|
||||
Now that you have learned how easy it is to use Angular services (thanks to Angular's dependency
|
||||
injection), go to {@link step_08 step 8}, where you will add some thumbnail images of phones and
|
||||
some links.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="7"></ul>
|
||||
|
||||
|
||||
[bower]: http://bower.io
|
||||
[minification]: https://en.wikipedia.org/wiki/Minification_(programming)
|
||||
|
||||
@@ -1,197 +1,117 @@
|
||||
@ngdoc tutorial
|
||||
@name 8 - More Templating
|
||||
@name 8 - Templating Links & Images
|
||||
@step 8
|
||||
@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.
|
||||
In this step, we will add thumbnail images for the phones in the phone list, and links that, for
|
||||
now, will go nowhere. In subsequent steps, we will use the links to display additional information
|
||||
about the phones in the catalog.
|
||||
|
||||
* When you click on a phone on the list, the phone details page with phone-specific information
|
||||
is displayed.
|
||||
* There are now links and images of the phones in the list.
|
||||
|
||||
To implement the phone details view we are going to use {@link ng.$http $http} to fetch our data,
|
||||
and then flesh out the `phone-detail.html` view template.
|
||||
|
||||
<div doc-tutorial-reset="8"></div>
|
||||
|
||||
|
||||
## Data
|
||||
|
||||
In addition to `phones.json`, the `app/phones/` directory also contains one JSON file for each
|
||||
phone:
|
||||
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/nexus-s.json`:__ (sample snippet)
|
||||
<br />
|
||||
**`app/phones/phones.json`** (sample snippet):
|
||||
|
||||
```js
|
||||
{
|
||||
"additionalFeatures": "Contour Display, Near Field Communications (NFC),...",
|
||||
"android": {
|
||||
"os": "Android 2.3",
|
||||
"ui": "Android"
|
||||
```json
|
||||
[
|
||||
{
|
||||
...
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
...
|
||||
},
|
||||
...
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
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.
|
||||
## Component Template
|
||||
|
||||
|
||||
## Controller
|
||||
|
||||
We'll expand the `PhoneDetailCtrl` by using the `$http` service to fetch the JSON files. This works
|
||||
the same way as the phone list controller.
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
|
||||
```js
|
||||
var phonecatControllers = angular.module('phonecatControllers',[]);
|
||||
|
||||
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', '$http',
|
||||
function($scope, $routeParams, $http) {
|
||||
$http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) {
|
||||
$scope.phone = data;
|
||||
});
|
||||
}]);
|
||||
```
|
||||
|
||||
To construct the URL for the HTTP request, we use `$routeParams.phoneId` extracted from the current
|
||||
route by the `$route` service.
|
||||
|
||||
|
||||
## 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 `ngRepeat` to project phone data from
|
||||
our model into the view.
|
||||
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
<br />
|
||||
**`app/phone-list/phone-list.template.html`:**
|
||||
|
||||
```html
|
||||
<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}}">
|
||||
...
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp" class="thumbnail">
|
||||
<a href="#/phones/{{phone.id}}" class="thumb">
|
||||
<img ng-src="{{phone.imageUrl}}" alt="{{phone.name}}" />
|
||||
</a>
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</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>
|
||||
To dynamically generate links that will in the future lead to phone detail pages, we used the
|
||||
now-familiar double-curly brace binding 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 ngSrc ngSrc}
|
||||
directive. That directive prevents the browser from treating the Angular `{{ expression }}` markup
|
||||
literally, and initiating a request to an invalid URL (`http://localhost:8000/{{phone.imageUrl}}`),
|
||||
which it would have done if we had only specified an attribute binding in a regular `src` attribute
|
||||
(`<img src="{{phone.imageUrl}}">`). Using the `ngSrc` directive, prevents the browser from making an
|
||||
HTTP request to an invalid location.
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
<br />
|
||||
**`e2e-tests/scenarios.js`**:
|
||||
|
||||
```js
|
||||
...
|
||||
<li>
|
||||
<span>Additional Features</span>
|
||||
<dd>{{phone.additionalFeatures}}</dd>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
<div style="display: none">
|
||||
TODO!
|
||||
<img class="diagram" src="img/tutorial/tutorial_08-09_final.png">
|
||||
</div>
|
||||
it('should render phone specific links', function() {
|
||||
var query = element(by.model('$ctrl.query'));
|
||||
query.sendKeys('nexus');
|
||||
|
||||
## Test
|
||||
|
||||
We wrote a new unit test that is similar to the one we wrote for the `PhoneListCtrl` controller in
|
||||
step 5.
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
|
||||
```js
|
||||
|
||||
beforeEach(module('phonecatApp'));
|
||||
|
||||
...
|
||||
|
||||
describe('PhoneDetailCtrl', function(){
|
||||
var scope, $httpBackend, ctrl;
|
||||
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/xyz.json').respond({name:'phone xyz'});
|
||||
|
||||
$routeParams.phoneId = 'xyz';
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller('PhoneDetailCtrl', {$scope: scope});
|
||||
}));
|
||||
|
||||
|
||||
it('should fetch phone detail', function() {
|
||||
expect(scope.phone).toBeUndefined();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(scope.phone).toEqual({name:'phone xyz'});
|
||||
});
|
||||
});
|
||||
...
|
||||
```
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
<pre>Chrome 22.0: Executed 3 of 3 SUCCESS (0.039 secs / 0.012 secs)</pre>
|
||||
|
||||
|
||||
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`:__
|
||||
|
||||
```js
|
||||
...
|
||||
describe('Phone detail view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('app/index.html#/phones/nexus-s');
|
||||
element.all(by.css('.phones li a')).first().click();
|
||||
expect(browser.getLocationAbsUrl()).toBe('/phones/nexus-s');
|
||||
});
|
||||
|
||||
|
||||
it('should display nexus-s page', function() {
|
||||
expect(element(by.binding('phone.name')).getText()).toBe('Nexus S');
|
||||
});
|
||||
});
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
We added a new E2E test to verify that the application is generating correct links to the phone
|
||||
views, that we will implement in the upcoming steps.
|
||||
|
||||
You can now rerun `npm run protractor` to see the tests run.
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Using the [Protractor API](http://angular.github.io/protractor/#/api),
|
||||
write a test that verifies that we display 4 thumbnail images on the Nexus S details page.
|
||||
<div></div>
|
||||
|
||||
* Replace the `ngSrc` directive with a plain old `src` attribute. Using tools such as your browser's
|
||||
developer tools or inspecting the web server access logs, confirm that the application is indeed
|
||||
making an extraneous request to `%7B%7Bphone.imageUrl%7D%7D` (or `{{phone.imageUrl}}`).
|
||||
|
||||
The issue here is that the browser will fire a request for that invalid image address as soon as
|
||||
it hits the `<img>` tag, which is before Angular has a chance to evaluate the expression and
|
||||
inject the valid address.
|
||||
|
||||
|
||||
# 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.
|
||||
Now that you have added phone images and links, go to {@link step_09 step 9} to learn about Angular
|
||||
layout templates and how Angular makes it easy to create applications that have multiple views.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="8"></ul>
|
||||
|
||||
@@ -1,144 +1,429 @@
|
||||
@ngdoc tutorial
|
||||
@name 9 - Filters
|
||||
@name 9 - Routing & Multiple Views
|
||||
@step 9
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="9"></ul>
|
||||
|
||||
In this step you will learn how to create your own custom display filter.
|
||||
|
||||
* 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.
|
||||
In this step, you will learn how to create a layout template and how to build an application that
|
||||
has multiple views by adding routing, using an Angular module called {@link ngRoute ngRoute}.
|
||||
|
||||
* When you now navigate to `/index.html`, you are redirected to `/index.html#!/phones` and the phone
|
||||
list appears in the browser.
|
||||
* When you click on a phone link, the URL changes to that specific phone and the stub of a phone
|
||||
detail page is displayed.
|
||||
|
||||
|
||||
<div doc-tutorial-reset="9"></div>
|
||||
|
||||
|
||||
## Custom Filter
|
||||
## Dependencies
|
||||
|
||||
In order to create a new filter, you are going to create a `phonecatFilters` module and register
|
||||
your custom filter with this module:
|
||||
The routing functionality added in this step is provided by Angular in the `ngRoute` module, which
|
||||
is distributed separately from the core Angular framework.
|
||||
|
||||
__`app/js/filters.js`:__
|
||||
Since we are using [Bower][bower] to install client-side dependencies, this step updates the
|
||||
`bower.json` configuration file to include the new dependency:
|
||||
|
||||
```js
|
||||
angular.module('phonecatFilters', []).filter('checkmark', function() {
|
||||
return function(input) {
|
||||
return input ? '\u2713' : '\u2718';
|
||||
};
|
||||
});
|
||||
<br />
|
||||
**`bower.json`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "angular-phonecat",
|
||||
"description": "A starter project for AngularJS",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/angular/angular-phonecat",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "1.5.x",
|
||||
"angular-mocks": "1.5.x",
|
||||
"angular-route": "1.5.x",
|
||||
"bootstrap": "3.3.x"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The name of our filter is "checkmark". The `input` evaluates to either `true` or `false`, and we
|
||||
return one of the two unicode characters we have chosen to represent true (`\u2713` -> ✓) or false (`\u2718` -> ✘).
|
||||
The new dependency `"angular-route": "1.5.x"` tells bower to install a version of the angular-route
|
||||
module that is compatible with version 1.5.x of Angular. We must tell bower to download and install
|
||||
this dependency.
|
||||
|
||||
Now that our filter is ready, we need to register the `phonecatFilters` module as a dependency for
|
||||
our main `phonecatApp` module.
|
||||
|
||||
__`app/js/app.js`:__
|
||||
|
||||
```js
|
||||
...
|
||||
angular.module('phonecatApp', ['ngRoute','phonecatControllers','phonecatFilters']);
|
||||
...
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** If you have bower installed globally, you can run `bower install`, but for this project
|
||||
we have preconfigured `npm install` to run bower for us.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then
|
||||
you may have a problem with the `bower install` due to a conflict between the versions of
|
||||
angular.js that need to be installed. If you run into this issue, simply delete your
|
||||
`app/bower_components` directory and then run `npm install`.
|
||||
</div>
|
||||
|
||||
|
||||
## Multiple Views, Routing and Layout Templates
|
||||
|
||||
Our app is slowly growing and becoming more complex. Prior to this step, the app provided our users
|
||||
with a single view (including the list of all phones), and all of the template code was located in
|
||||
the `phone-list.template.html` file. The next step in building the application is to add a view that
|
||||
will show detailed information about each of the devices in our list.
|
||||
|
||||
To add the detailed view, we are going to turn `index.html` 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 ngRoute.$routeProvider $routeProvider},
|
||||
which is the provider of the {@link ngRoute.$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 [deep linking][deep-linking], which lets us utilize the browser's history
|
||||
(back and forward navigation) and bookmarks.
|
||||
|
||||
<div class="alert alert-success">
|
||||
<p>
|
||||
`ngRoute` lets us associate a controller and a template with a specific URL (or URL
|
||||
pattern). This is pretty close to what we did with `ngController` and `index.html` back in
|
||||
{@link step_02 step 2}.
|
||||
</p>
|
||||
<p>
|
||||
Since we have already learned that components allow us to combine controllers with templates in
|
||||
a modular, testable way, we are going to use components for routing as well.
|
||||
Each route will be associated with a component and that component will be in charge of providing
|
||||
the view template and the controller.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
### A Note about DI, Injector and Providers
|
||||
|
||||
As you {@link step_07 noticed}, {@link guide/di dependency injection} (DI) is at the core of
|
||||
AngularJS, so it's important for you to understand a thing or two about how it works.
|
||||
|
||||
When the application bootstraps, Angular creates an injector that will be used to find and inject
|
||||
all of the services that are required by your application. The injector itself doesn't know anything
|
||||
about what the `$http` or `$route` services do. In fact, the injector doesn't even know about the
|
||||
existence of these services, unless it is configured with proper module definitions.
|
||||
|
||||
The injector only carries out the following steps:
|
||||
|
||||
* Load the module definition(s) that you specify in your application.
|
||||
* Register all Providers defined in these module definition(s).
|
||||
* When asked to do so, lazily instantiate services and their dependencies, via their Providers, as
|
||||
parameters to an injectable function.
|
||||
|
||||
Providers are objects that provide (create) instances of services and expose configuration APIs,
|
||||
that can be used to control the creation and runtime behavior of a service. In case of the `$route`
|
||||
service, the `$routeProvider` exposes APIs that allow you to define routes for your application.
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Note:** Providers can only be injected into `config` functions. Thus you could not inject
|
||||
`$routeProvider` into `PhoneListController` at runtime.
|
||||
</div>
|
||||
|
||||
Angular modules solve the problem of removing global variables from the application and provide a
|
||||
way of configuring the injector. As opposed to AMD or require.js modules, Angular modules don't try
|
||||
to solve the problem of script load ordering or lazy script fetching. These goals are totally
|
||||
independent and both module systems can live side-by-side and fulfill their goals.
|
||||
|
||||
To deepen your understanding on Angular's DI, see [Understanding Dependency Injection][wiki-di].
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
Since the filter code lives in the `app/js/filters.js` file, we need to include this file in our
|
||||
layout template.
|
||||
The `$route` service is usually used in conjunction with the {@link ngRoute.directive:ngView ngView}
|
||||
directive. The role of the `ngView` directive is to include the view template for the current route
|
||||
into the layout template. This makes it a perfect fit for our `index.html` template.
|
||||
|
||||
__`app/index.html`:__
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
...
|
||||
<script src="js/controllers.js"></script>
|
||||
<script src="js/filters.js"></script>
|
||||
...
|
||||
<head>
|
||||
...
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="bower_components/angular-route/angular-route.js"></script>
|
||||
<script src="app.module.js"></script>
|
||||
<script src="app.config.js"></script>
|
||||
...
|
||||
<script src="phone-detail/phone-detail.module.js"></script>
|
||||
<script src="phone-detail/phone-detail.component.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div ng-view></div>
|
||||
|
||||
</body>
|
||||
```
|
||||
|
||||
The syntax for using filters in Angular templates is as follows:
|
||||
We have added four new `<script>` tags in our `index.html` file to load some extra JavaScript files
|
||||
into our application:
|
||||
|
||||
{{ expression | filter }}
|
||||
* `angular-route.js`: Defines the Angular `ngRoute` module, which provides us with routing.
|
||||
* `app.config.js`: Configures the providers available to our main module (see
|
||||
[below](tutorial/step_09#configuring-a-module)).
|
||||
* `phone-detail.module.js`: Defines a new module containing a `phoneDetail` component.
|
||||
* `phone-detail.component.js`: Defines a dummy `phoneDetail` component (see
|
||||
[below](tutorial/step_09#the-phonedetail-component)).
|
||||
|
||||
Let's employ the filter in the phone details template:
|
||||
Note that we removed the `<phone-list></phone-list>` line from the `index.html` template and
|
||||
replaced it with a `<div>` with the `ng-view` attribute.
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_09.png">
|
||||
|
||||
|
||||
## Configuring a Module
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
A module's {@link ng.angular.Module#config .config()} method gives us access to the available
|
||||
providers for configuration. To make the providers, services and directives defined in `ngRoute`
|
||||
available to our application, we need to add `ngRoute` as a dependency of our `phonecatApp` module.
|
||||
|
||||
```html
|
||||
...
|
||||
<dl>
|
||||
<dt>Infrared</dt>
|
||||
<dd>{{phone.connectivity.infrared | checkmark}}</dd>
|
||||
<dt>GPS</dt>
|
||||
<dd>{{phone.connectivity.gps | checkmark}}</dd>
|
||||
</dl>
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
Filters, like any other component, should be tested and these tests are very easy to write.
|
||||
|
||||
__`test/unit/filtersSpec.js`:__
|
||||
<br />
|
||||
**`app/app.module.js`:**
|
||||
|
||||
```js
|
||||
describe('filter', function() {
|
||||
|
||||
beforeEach(module('phonecatFilters'));
|
||||
|
||||
describe('checkmark', function() {
|
||||
|
||||
it('should convert boolean values to unicode checkmark or cross',
|
||||
inject(function(checkmarkFilter) {
|
||||
expect(checkmarkFilter(true)).toBe('\u2713');
|
||||
expect(checkmarkFilter(false)).toBe('\u2718');
|
||||
}));
|
||||
});
|
||||
});
|
||||
angular.module('phonecatApp', [
|
||||
'ngRoute',
|
||||
...
|
||||
]);
|
||||
```
|
||||
|
||||
We must call `beforeEach(module('phonecatFilters'))` before any of
|
||||
our filter tests execute. This call loads our `phonecatFilters` module into the injector
|
||||
for this test run.
|
||||
Now, in addition to the core services and directives, we can also configure the `$route` service
|
||||
(using it's provider) for our application. In order to be able to quickly locate the configuration
|
||||
code, we put it into a separate file and used the `.config` suffix.
|
||||
|
||||
Note that we call the helper function, `inject(function(checkmarkFilter) { ... })`, to get
|
||||
access to the filter that we want to test. See {@link angular.mock.inject angular.mock.inject()}.
|
||||
<br />
|
||||
**`app/app.config.js`:**
|
||||
|
||||
Notice that the suffix 'Filter' is appended to your filter name when injected.
|
||||
See the {@link guide/filter#using-filters-in-controllers-services-and-directives Filter Guide}
|
||||
section where this is outlined.
|
||||
```js
|
||||
angular.
|
||||
module('phonecatApp').
|
||||
config(['$locationProvider', '$routeProvider',
|
||||
function config($locationProvider, $routeProvider) {
|
||||
$locationProvider.hashPrefix('!');
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
$routeProvider.
|
||||
when('/phones', {
|
||||
template: '<phone-list></phone-list>'
|
||||
}).
|
||||
when('/phones/:phoneId', {
|
||||
template: '<phone-detail></phone-detail>'
|
||||
}).
|
||||
otherwise('/phones');
|
||||
}
|
||||
]);
|
||||
```
|
||||
|
||||
<pre>Chrome 22.0: Executed 4 of 4 SUCCESS (0.034 secs / 0.012 secs)</pre>
|
||||
Using the `.config()` method, we request the necessary providers (for example the `$routeProvider`)
|
||||
to be injected into our configuration function and then use their methods to specify the behavior of
|
||||
the corresponding services. Here, we use the
|
||||
{@link ngRoute.$routeProvider#when $routeProvider.when()} and
|
||||
{@link ngRoute.$routeProvider#otherwise $routeProvider.otherwise()} methods to define our
|
||||
application routes.
|
||||
|
||||
<div class="alert alert-success">
|
||||
<p>
|
||||
We also used {@link $locationProvider#hashPrefix $locationProvider.hashPrefix()} to set the
|
||||
hash-prefix to `!`. This prefix will appear in the links to our client-side routes, right after
|
||||
the hash (`#`) symbol and before the actual path (e.g. `index.html#!/some/path`).
|
||||
</p>
|
||||
<p>
|
||||
Setting a prefix is not necessary, but it is considered a good practice (for reasons that are
|
||||
outside the scope of this tutorial). `!` is the most commonly used prefix.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Our routes are defined as follows:
|
||||
|
||||
* `when('/phones')`: Determines the view that will be shown, when the URL hash fragment is
|
||||
`/phones`. According to the specified template, Angular will create an instance of the `phoneList`
|
||||
component to manage the view. Note that this is the same markup that we used to have in the
|
||||
`index.html` file.
|
||||
|
||||
* `when('/phones/:phoneId')`: Determines the view that will be shown, when the URL hash fragment
|
||||
matches `/phones/<phoneId>`, where `<phoneId>` is a variable part of the URL. In charge of the
|
||||
view will be the `phoneDetail` component.
|
||||
|
||||
* `otherwise('/phones')`: Defines a fallback route to redirect to, when no route definition matches
|
||||
the current URL.(Here it will redirect to `/phones`.)
|
||||
|
||||
We reused the `phoneList` component that we have already built and a new "dummy" `phoneDetail`
|
||||
component. For now, the `phoneDetail` component will just display the selected phone's ID.
|
||||
(Not too impressive, but we will enhance it in the {@link step_10 next step}.)
|
||||
|
||||
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 `:` prefix are extracted into the (injectable)
|
||||
{@link ngRoute.$routeParams $routeParams} object.
|
||||
|
||||
|
||||
## The `phoneDetail` Component
|
||||
|
||||
We created a `phoneDetail` component to handle the phone details view. We followed the same
|
||||
conventions as with `phoneList`, using a separate directory and creating a `phoneDetail` module,
|
||||
which we added as a dependency of the `phonecatApp` module.
|
||||
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.module.js`:**
|
||||
|
||||
```js
|
||||
angular.module('phoneDetail', [
|
||||
'ngRoute'
|
||||
]);
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.component.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phoneDetail').
|
||||
component('phoneDetail', {
|
||||
template: 'TBD: Detail view for <span>{{$ctrl.phoneId}}</span>',
|
||||
controller: ['$routeParams',
|
||||
function PhoneDetailController($routeParams) {
|
||||
this.phoneId = $routeParams.phoneId;
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/app.module.js`:**
|
||||
|
||||
```js
|
||||
angular.module('phonecatApp', [
|
||||
...
|
||||
'phoneDetail',
|
||||
...
|
||||
]);
|
||||
```
|
||||
|
||||
### A Note on Sub-module Dependencies
|
||||
|
||||
The `phoneDetail` module depends on the `ngRoute` module for providing the `$routeParams` object,
|
||||
which is used in the `phoneDetail` component's controller. Since `ngRoute` is also a dependency of
|
||||
the main `phonecatApp` module, its services and directives are already available everywhere in the
|
||||
application (including the `phoneDetail` component).
|
||||
|
||||
This means that our application would continue to work even if we didn't include `ngRoute` in the
|
||||
list of dependencies for the `phoneDetail` component. Although it might be tempting to omit
|
||||
dependencies of a sub-module that are already imported by the main module, it breaks our hard-earned
|
||||
modularity.
|
||||
|
||||
<div class="alert alert-warning">
|
||||
Imagine what would happen if we decided to copy the `phoneDetail` feature over to another project
|
||||
that does not declare a dependency on `ngRoute`. The injector would not be able to provide
|
||||
`$routeParams` and our application would break.
|
||||
</div>
|
||||
|
||||
The takeaway here is:
|
||||
|
||||
* Always be explicit about the dependecies of a sub-module. Do not rely on dependencies inherited
|
||||
from a parent module (because that parent module might not be there some day).
|
||||
|
||||
<div class="alert alert-success">
|
||||
Declaring the same dependency in multiple modules does not incur extra "cost", because Angular
|
||||
will still load each dependency once. For more info on modules and their dependencies take a look
|
||||
at the [Modules](guide/module) section of the Developer Guide.
|
||||
</div>
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
Since some of our modules depend on {@link ngRoute ngRoute} now, it is necessary to update the Karma
|
||||
configuration file with angular-route. Other than that, the unit tests should (still) pass without
|
||||
any modification.
|
||||
|
||||
<br />
|
||||
**`karma.conf.js`:**
|
||||
|
||||
```js
|
||||
files: [
|
||||
'bower_components/angular/angular.js',
|
||||
'bower_components/angular-route/angular-route.js',
|
||||
...
|
||||
],
|
||||
```
|
||||
|
||||
<br />
|
||||
To automatically verify that everything is wired properly, we wrote E2E tests for navigating to
|
||||
various URLs and verifying that the correct view was rendered.
|
||||
|
||||
<br />
|
||||
**`e2e-tests/scenarios.js`**
|
||||
|
||||
```js
|
||||
...
|
||||
|
||||
it('should redirect `index.html` to `index.html#!/phones', function() {
|
||||
browser.get('index.html');
|
||||
expect(browser.getLocationAbsUrl()).toBe('/phones');
|
||||
});
|
||||
|
||||
...
|
||||
|
||||
describe('View: Phone list', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('index.html#!/phones');
|
||||
});
|
||||
|
||||
...
|
||||
|
||||
});
|
||||
|
||||
...
|
||||
|
||||
describe('View: Phone details', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('index.html#!/phones/nexus-s');
|
||||
});
|
||||
|
||||
it('should display placeholder page with `phoneId`', function() {
|
||||
expect(element(by.binding('$ctrl.phoneId')).getText()).toBe('nexus-s');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
You can now rerun `npm run protractor` to see the tests run (and hopefully pass).
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Let's experiment with some of the {@link api/ng/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" }}`
|
||||
<div></div>
|
||||
|
||||
* We can also create a model with an input element, and combine it with a filtered binding. Add
|
||||
the following to index.html:
|
||||
* Try to add a `{{$ctrl.phoneId}` binding in the template string for the phone details view:
|
||||
|
||||
```html
|
||||
<input ng-model="userInput"> Uppercased: {{ userInput | uppercase }}
|
||||
```js
|
||||
when('/phones/:phoneId', {
|
||||
template: '{{$ctrl.phoneId}} <phone-detail></phone-detail>'
|
||||
...
|
||||
```
|
||||
|
||||
You will see that nothing happens, even when you are in the phone details view. This is because
|
||||
the `phoneId` model is visible only in the context set by the `phoneDetail` component. Again,
|
||||
component isolation at work!
|
||||
|
||||
|
||||
# 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.
|
||||
With the routing set up and the phone list view implemented, we are ready to go to
|
||||
{@link step_10 step 10} and implement a proper phone details view.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="9"></ul>
|
||||
|
||||
|
||||
[bower]: http://bower.io
|
||||
[deep-linking]: https://en.wikipedia.org/wiki/Deep_linking
|
||||
[wiki-di]: https://github.com/angular/angular.js/wiki/Understanding-Dependency-Injection
|
||||
|
||||
@@ -1,180 +1,214 @@
|
||||
@ngdoc tutorial
|
||||
@name 10 - Event Handlers
|
||||
@name 10 - More Templating
|
||||
@step 10
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="10"></ul>
|
||||
|
||||
|
||||
In this step, you will add a clickable phone image swapper to the phone details page.
|
||||
In this step, we will implement the phone details view, which is displayed when a user clicks on a
|
||||
phone in the phone list.
|
||||
|
||||
* 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 are going to use {@link ng.$http $http} to fetch our data,
|
||||
and then flesh out the `phoneDetail` component's template.
|
||||
|
||||
* 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.
|
||||
|
||||
<div doc-tutorial-reset="10"></div>
|
||||
|
||||
## Controller
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
## Data
|
||||
|
||||
```js
|
||||
...
|
||||
var phonecatControllers = angular.module('phonecatControllers',[]);
|
||||
In addition to `phones.json`, the `app/phones/` directory also contains one JSON file for each
|
||||
phone:
|
||||
|
||||
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', '$http',
|
||||
function($scope, $routeParams, $http) {
|
||||
$http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) {
|
||||
$scope.phone = data;
|
||||
$scope.mainImageUrl = data.images[0];
|
||||
});
|
||||
<br />
|
||||
**`app/phones/nexus-s.json`:** (sample snippet)
|
||||
|
||||
$scope.setImage = function(imageUrl) {
|
||||
$scope.mainImageUrl = imageUrl;
|
||||
};
|
||||
}]);
|
||||
```json
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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` event handler function that will change the value of `mainImageUrl`.
|
||||
Each of these files describes various properties of the phone using the same data structure. We will
|
||||
show this data in the phone details view.
|
||||
|
||||
|
||||
## Template
|
||||
## Component Controller
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
We will expand the `phoneDetail` component's controller by using the `$http` service to fetch the
|
||||
appropriate JSON files. This works the same way as the `phoneList` component's controller.
|
||||
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.component.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phoneDetail').
|
||||
component('phoneDetail', {
|
||||
templateUrl: 'phone-detail/phone-detail.template.html',
|
||||
controller: ['$http', '$routeParams',
|
||||
function PhoneDetailController($http, $routeParams) {
|
||||
var self = this;
|
||||
|
||||
$http.get('phones/' + $routeParams.phoneId + '.json').then(function(response) {
|
||||
self.phone = response.data;
|
||||
});
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
To construct the URL for the HTTP request, we use `$routeParams.phoneId`, which is extracted from
|
||||
the current route by the `$route` service.
|
||||
|
||||
|
||||
## Component Template
|
||||
|
||||
The inline, TBD placeholder template has been replaced with a full blown external template,
|
||||
including lists and bindings that comprise the phone details. Note how we use the Angular
|
||||
`{{expression}}` markup and `ngRepeat` to project phone data from our model into the view.
|
||||
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.template.html`:**
|
||||
|
||||
```html
|
||||
<img ng-src="{{mainImageUrl}}" class="phone">
|
||||
<img ng-src="{{$ctrl.phone.images[0]}}" class="phone" />
|
||||
|
||||
...
|
||||
<h1>{{$ctrl.phone.name}}</h1>
|
||||
|
||||
<p>{{$ctrl.phone.description}}</p>
|
||||
|
||||
<ul class="phone-thumbs">
|
||||
<li ng-repeat="img in phone.images">
|
||||
<img ng-src="{{img}}" ng-click="setImage(img)">
|
||||
<li ng-repeat="img in $ctrl.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 $ctrl.phone.availability">{{availability}}</dd>
|
||||
</dl>
|
||||
</li>
|
||||
...
|
||||
<li>
|
||||
<span>Additional Features</span>
|
||||
<dd>{{$ctrl.phone.additionalFeatures}}</dd>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
```
|
||||
|
||||
We bound the `ngSrc` directive of the large image to the `mainImageUrl` property.
|
||||
<img class="diagram" src="img/tutorial/tutorial_10.png">
|
||||
|
||||
We also registered an {@link ng.directive:ngClick `ngClick`}
|
||||
handler with thumbnail images. When a user clicks on one of the thumbnail images, the handler will
|
||||
use the `setImage` event handler function to change the value of the `mainImageUrl` property to the
|
||||
URL of the thumbnail image.
|
||||
|
||||
<div style="display: none">
|
||||
TODO!
|
||||
<img class="diagram" src="img/tutorial/tutorial_10-11_final.png">
|
||||
</div>
|
||||
# Testing
|
||||
|
||||
## Test
|
||||
We wrote a new unit test that is similar to the one we wrote for the `phoneList` component's
|
||||
controller in {@link step_07#testing step 7}.
|
||||
|
||||
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`:__
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.component.spec.js`:**
|
||||
|
||||
```js
|
||||
...
|
||||
describe('Phone detail view', function() {
|
||||
describe('phoneDetail', function() {
|
||||
|
||||
...
|
||||
// Load the module that contains the `phoneDetail` component before each test
|
||||
beforeEach(module('phoneDetail'));
|
||||
|
||||
it('should display the first phone image as the main phone image', function() {
|
||||
expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
|
||||
});
|
||||
// Test the controller
|
||||
describe('PhoneDetailController', function() {
|
||||
var $httpBackend, ctrl;
|
||||
|
||||
|
||||
it('should swap main image if a thumbnail image is clicked on', function() {
|
||||
element(by.css('.phone-thumbs li:nth-child(3) img')).click();
|
||||
expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.2.jpg/);
|
||||
|
||||
element(by.css('.phone-thumbs li:nth-child(1) img')).click();
|
||||
expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
You can now rerun `npm run protractor` to see the tests run.
|
||||
|
||||
|
||||
You also have to refactor one of your unit tests because of the addition of the `mainImageUrl`
|
||||
model property to the `PhoneDetailCtrl` controller. Below, we create the function `xyzPhoneData`
|
||||
which returns the appropriate json with the `images` attribute in order to get the test to pass.
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
|
||||
```js
|
||||
...
|
||||
beforeEach(module('phonecatApp'));
|
||||
|
||||
...
|
||||
|
||||
describe('PhoneDetailCtrl', function(){
|
||||
var scope, $httpBackend, ctrl,
|
||||
xyzPhoneData = function() {
|
||||
return {
|
||||
name: 'phone xyz',
|
||||
images: ['image/url1.png', 'image/url2.png']
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) {
|
||||
beforeEach(inject(function($componentController, _$httpBackend_, $routeParams) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData());
|
||||
$httpBackend.expectGET('phones/xyz.json').respond({name: 'phone xyz'});
|
||||
|
||||
$routeParams.phoneId = 'xyz';
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller('PhoneDetailCtrl', {$scope: scope});
|
||||
|
||||
ctrl = $componentController('phoneDetail');
|
||||
}));
|
||||
|
||||
it('should fetch the phone details', function() {
|
||||
expect(ctrl.phone).toBeUndefined();
|
||||
|
||||
it('should fetch phone detail', function() {
|
||||
expect(scope.phone).toBeUndefined();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(scope.phone).toEqual(xyzPhoneData());
|
||||
expect(ctrl.phone).toEqual({name: 'phone xyz'});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
Your unit tests should now be passing.
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
```
|
||||
Chrome 49.0: Executed 3 of 3 SUCCESS (0.159 secs / 0.136 secs)
|
||||
```
|
||||
|
||||
We also added a new E2E test that navigates to the 'Nexus S' details page and verifies that the
|
||||
heading on the page is "Nexus S".
|
||||
|
||||
<br />
|
||||
**`e2e-tests/scenarios.js`**
|
||||
|
||||
```js
|
||||
...
|
||||
|
||||
describe('View: Phone detail', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('index.html#!/phones/nexus-s');
|
||||
});
|
||||
|
||||
it('should display the `nexus-s` page', function() {
|
||||
expect(element(by.binding('$ctrl.phone.name')).getText()).toBe('Nexus S');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
You can run the tests with `npm run protractor`.
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Let's add a new controller method to `PhoneDetailCtrl`:
|
||||
<div></div>
|
||||
|
||||
$scope.hello = function(name) {
|
||||
alert('Hello ' + (name || 'world') + '!');
|
||||
}
|
||||
|
||||
and add:
|
||||
|
||||
<button ng-click="hello('Elmo')">Hello</button>
|
||||
|
||||
to the `phone-detail.html` template.
|
||||
|
||||
<div style="display: none">
|
||||
TODO!
|
||||
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.
|
||||
</div>
|
||||
* Using [Protractor's API][protractor-docs], write a test that verifies that we display 4 thumbnail
|
||||
images on the 'Nexus S' details page.
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
With the phone image swapper in place, we're ready for {@link step_11 step 11} to
|
||||
learn an even better way to fetch data.
|
||||
Now that the phone details view is in place, proceed to {@link step_11 step 11} to learn how to
|
||||
write your own custom display filter.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="10"></ul>
|
||||
|
||||
|
||||
[protractor-docs]: https://angular.github.io/protractor/#/api
|
||||
|
||||
@@ -1,293 +1,176 @@
|
||||
@ngdoc tutorial
|
||||
@name 11 - REST and Custom Services
|
||||
@name 11 - Custom Filters
|
||||
@step 11
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="11"></ul>
|
||||
|
||||
|
||||
In this step, you will change the way our app fetches data.
|
||||
In this step you will learn how to create your own custom display filter.
|
||||
|
||||
* We define a custom service that represents a [RESTful][restful] client. Using this client we
|
||||
can make requests to the server for data in an easier way, without having to deal with the
|
||||
lower-level {@link ng.$http $http} API, HTTP methods and URLs.
|
||||
* In the previous step, the details page displayed either "true" or "false" to indicate whether
|
||||
certain phone features were present or not. In this step, we are using a custom filter to convert
|
||||
those text strings into glyphs: ✓ for "true", and ✘ for "false".
|
||||
|
||||
Let's see what the filter code looks like.
|
||||
|
||||
|
||||
<div doc-tutorial-reset="11"></div>
|
||||
|
||||
## Dependencies
|
||||
|
||||
The RESTful functionality is provided by Angular in the `ngResource` module, which is distributed
|
||||
separately from the core Angular framework.
|
||||
## The `checkmark` Filter
|
||||
|
||||
We are using [Bower][bower] to install client side dependencies. This step updates the
|
||||
`bower.json` configuration file to include the new dependency:
|
||||
Since this filter is generic (i.e. it is not specific to any view or component), we are going to
|
||||
register it in a `core` module, which contains "application-wide" features.
|
||||
|
||||
```
|
||||
{
|
||||
"name": "angular-seed",
|
||||
"description": "A starter project for AngularJS",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/angular/angular-seed",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "1.4.x",
|
||||
"angular-mocks": "1.4.x",
|
||||
"jquery": "~2.1.1",
|
||||
"bootstrap": "~3.1.1",
|
||||
"angular-route": "1.4.x",
|
||||
"angular-resource": "1.4.x"
|
||||
}
|
||||
}
|
||||
<br />
|
||||
**`app/core/core.module.js`:**
|
||||
|
||||
```js
|
||||
angular.module('core', []);
|
||||
```
|
||||
|
||||
The new dependency `"angular-resource": "1.4.x"` tells bower to install a version of the
|
||||
angular-resource component that is compatible with version 1.4.x. We must ask bower to download
|
||||
and install this dependency. We can do this by running:
|
||||
<br />
|
||||
**`app/core/checkmark/checkmark.filter.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('core').
|
||||
filter('checkmark', function() {
|
||||
return function(input) {
|
||||
return input ? '\u2713' : '\u2718';
|
||||
};
|
||||
});
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then you may have a
|
||||
problem with the `bower install` due to a conflict between the versions of angular.js that need to
|
||||
be installed. If you get this then simply delete your `app/bower_components` folder before running
|
||||
`npm install`.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** If you have bower installed globally then you can run `bower install` but for this project we have
|
||||
preconfigured `npm install` to run bower for us.
|
||||
As you may have noticed, we (unsurprisingly) gave our file a `.filter` suffix.
|
||||
</div>
|
||||
|
||||
The name of our filter is "checkmark". The `input` evaluates to either `true` or `false`, and we
|
||||
return one of the two unicode characters we have chosen to represent true (`\u2713` -> ✓) and false
|
||||
(`\u2718` -> ✘).
|
||||
|
||||
## Template
|
||||
Now that our filter is ready, we need to register the `core` module as a dependency of our main
|
||||
`phonecatApp` module.
|
||||
|
||||
Our custom resource service will be defined in `app/js/services.js` so we need to include this file
|
||||
in our layout template. Additionally, we also need to load the `angular-resource.js` file, which
|
||||
contains the {@link module:ngResource ngResource} module:
|
||||
<br />
|
||||
**`app/app.module.js`:**
|
||||
|
||||
__`app/index.html`.__
|
||||
```js
|
||||
angular.module('phonecatApp', [
|
||||
...
|
||||
'core',
|
||||
...
|
||||
]);
|
||||
```
|
||||
|
||||
|
||||
## Templates
|
||||
|
||||
Since we have created two new files (**core.module.js**, **checkmark.filter.js**), we need to
|
||||
include them in our layout template.
|
||||
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
...
|
||||
<script src="bower_components/angular-resource/angular-resource.js"></script>
|
||||
<script src="js/services.js"></script>
|
||||
...
|
||||
...
|
||||
<script src="core/core.module.js"></script>
|
||||
<script src="core/checkmark/checkmark.filter.js"></script>
|
||||
...
|
||||
```
|
||||
|
||||
## Service
|
||||
The syntax for using filters in Angular templates is as follows:
|
||||
|
||||
We create our own service to provide access to the phone data on the server:
|
||||
|
||||
__`app/js/services.js`.__
|
||||
|
||||
```js
|
||||
var phonecatServices = angular.module('phonecatServices', ['ngResource']);
|
||||
|
||||
phonecatServices.factory('Phone', ['$resource',
|
||||
function($resource){
|
||||
return $resource('phones/:phoneId.json', {}, {
|
||||
query: {method:'GET', params:{phoneId:'phones'}, isArray:true}
|
||||
});
|
||||
}]);
|
||||
```
|
||||
{{expression | filter}}
|
||||
```
|
||||
|
||||
We used the module API to register a custom service using a factory function. We passed in the name
|
||||
of the service - 'Phone' - and the factory function. The factory function is similar to a
|
||||
controller's constructor in that both can declare dependencies to be injected via function
|
||||
arguments. The Phone service declared a dependency on the `$resource` service.
|
||||
Let's employ the filter in the phone details template:
|
||||
|
||||
The {@link ngResource.$resource `$resource`} service makes it easy to create a
|
||||
[RESTful][restful] client with just a few lines of code. This client can then be used in our
|
||||
application, instead of the lower-level {@link ng.$http $http} service.
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.template.html`:**
|
||||
|
||||
__`app/js/app.js`.__
|
||||
|
||||
```js
|
||||
...
|
||||
angular.module('phonecatApp', ['ngRoute', 'phonecatControllers','phonecatFilters', 'phonecatServices']).
|
||||
...
|
||||
```html
|
||||
...
|
||||
<dl>
|
||||
<dt>Infrared</dt>
|
||||
<dd>{{$ctrl.phone.connectivity.infrared | checkmark}}</dd>
|
||||
<dt>GPS</dt>
|
||||
<dd>{{$ctrl.phone.connectivity.gps | checkmark}}</dd>
|
||||
</dl>
|
||||
...
|
||||
```
|
||||
|
||||
We need to add the 'phonecatServices' module dependency to 'phonecatApp' module's requires array.
|
||||
|
||||
# Testing
|
||||
|
||||
## Controller
|
||||
Filters, like any other code, should be tested. Luckily, these tests are very easy to write.
|
||||
|
||||
We simplified our sub-controllers (`PhoneListCtrl` and `PhoneDetailCtrl`) by factoring out the
|
||||
lower-level {@link ng.$http $http} service, replacing it with a new service called
|
||||
`Phone`. Angular's {@link ngResource.$resource `$resource`} service is easier to
|
||||
use than `$http` 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`.__
|
||||
<br />
|
||||
**`app/core/checkmark/checkmark.filter.spec.js`:**
|
||||
|
||||
```js
|
||||
var phonecatControllers = angular.module('phonecatControllers', []);
|
||||
describe('checkmark', function() {
|
||||
|
||||
...
|
||||
beforeEach(module('core'));
|
||||
|
||||
phonecatControllers.controller('PhoneListCtrl', ['$scope', 'Phone', function($scope, Phone) {
|
||||
$scope.phones = Phone.query();
|
||||
$scope.orderProp = 'age';
|
||||
}]);
|
||||
it('should convert boolean values to unicode checkmark or cross',
|
||||
inject(function(checkmarkFilter) {
|
||||
expect(checkmarkFilter(true)).toBe('\u2713');
|
||||
expect(checkmarkFilter(false)).toBe('\u2718');
|
||||
})
|
||||
);
|
||||
|
||||
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', 'Phone', function($scope, $routeParams, Phone) {
|
||||
$scope.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) {
|
||||
$scope.mainImageUrl = phone.images[0];
|
||||
});
|
||||
|
||||
$scope.setImage = function(imageUrl) {
|
||||
$scope.mainImageUrl = imageUrl;
|
||||
}
|
||||
}]);
|
||||
```
|
||||
|
||||
Notice how in `PhoneListCtrl` we replaced:
|
||||
|
||||
$http.get('phones/phones.json').success(function(data) {
|
||||
$scope.phones = data;
|
||||
});
|
||||
|
||||
with:
|
||||
|
||||
$scope.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
|
||||
|
||||
Because we're now using the {@link ngResource ngResource} module, it's necessary to
|
||||
update the Karma config file with angular-resource so the new tests will pass.
|
||||
|
||||
__`test/karma.conf.js`:__
|
||||
|
||||
```js
|
||||
files : [
|
||||
'app/bower_components/angular/angular.js',
|
||||
'app/bower_components/angular-route/angular-route.js',
|
||||
'app/bower_components/angular-resource/angular-resource.js',
|
||||
'app/bower_components/angular-mocks/angular-mocks.js',
|
||||
'app/js/**/*.js',
|
||||
'test/unit/**/*.js'
|
||||
],
|
||||
```
|
||||
|
||||
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 ngResource.$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` [Jasmine matcher][jasmine-matchers]. When
|
||||
the `toEqualData` matcher compares two objects, it takes only object properties into account and
|
||||
ignores methods.
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
|
||||
|
||||
```js
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
beforeEach(function(){
|
||||
this.addMatchers({
|
||||
toEqualData: function(expected) {
|
||||
return angular.equals(this.actual, expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(module('phonecatApp'));
|
||||
beforeEach(module('phonecatServices'));
|
||||
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, ctrl, $httpBackend;
|
||||
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/phones.json').
|
||||
respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller('PhoneListCtrl', {$scope: scope});
|
||||
}));
|
||||
|
||||
|
||||
it('should create "phones" model with 2 phones fetched from xhr', function() {
|
||||
expect(scope.phones).toEqualData([]);
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(scope.phones).toEqualData(
|
||||
[{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
});
|
||||
|
||||
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(scope.orderProp).toBe('age');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('PhoneDetailCtrl', function(){
|
||||
var scope, $httpBackend, ctrl,
|
||||
xyzPhoneData = function() {
|
||||
return {
|
||||
name: 'phone xyz',
|
||||
images: ['image/url1.png', 'image/url2.png']
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData());
|
||||
|
||||
$routeParams.phoneId = 'xyz';
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller('PhoneDetailCtrl', {$scope: scope});
|
||||
}));
|
||||
|
||||
|
||||
it('should fetch phone detail', function() {
|
||||
expect(scope.phone).toEqualData({});
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(scope.phone).toEqualData(xyzPhoneData());
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
The call to `beforeEach(module('core'))` loads the `core` module (which contains the `checkmark`
|
||||
filter) into the injector, before every test.
|
||||
|
||||
Note that we call the helper function `inject(function(checkmarkFilter) {...})`, to get access to
|
||||
the filter that we want to test. See also {@link angular.mock.inject angular.mock.inject()}.
|
||||
|
||||
<div class="alert alert-info">
|
||||
When injecting a filter, we need to suffix the filter name with 'Filter'. For example, our
|
||||
`checkmark` filter is injected as `checkmarkFilter`.
|
||||
See the [Filters](guide/filter#using-filters-in-controllers-services-and-directives) section of
|
||||
the Developer Guide for more info.
|
||||
</div>
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
<pre>Chrome 22.0: Executed 5 of 5 SUCCESS (0.038 secs / 0.01 secs)</pre>
|
||||
```
|
||||
Chrome 49.0: Executed 4 of 4 SUCCESS (0.091 secs / 0.075 secs)
|
||||
```
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
<div></div>
|
||||
|
||||
* Let's experiment with some of the {@link api/ng/filter built-in Angular filters}.
|
||||
Add the following bindings to `index.html`:
|
||||
|
||||
* `{{'lower cap string' | uppercase}}`
|
||||
* `{{{foo: 'bar', baz: 42} | json}}`
|
||||
* `{{1459461289000 | date}}`
|
||||
* `{{1459461289000 | 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`:
|
||||
|
||||
```html
|
||||
<input ng-model="userInput" /> Uppercased: {{userInput | uppercase}}
|
||||
```
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now that we've seen how to build a custom service as a RESTful client, we're ready for {@link step_12 step 12} (the last step!) to
|
||||
learn how to improve this application with animations.
|
||||
Now that we have learned how to write and test a custom filter, let's go to {@link step_12 step 12}
|
||||
to learn how we can use Angular to enhance the phone details page further.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="11"></ul>
|
||||
|
||||
[restful]: http://en.wikipedia.org/wiki/Representational_State_Transfer
|
||||
[jasmine-matchers]: http://jasmine.github.io/1.3/introduction.html#section-Matchers
|
||||
[bower]: http://bower.io/
|
||||
|
||||
@@ -1,538 +1,185 @@
|
||||
@ngdoc tutorial
|
||||
@name 12 - Applying Animations
|
||||
@name 12 - Event Handlers
|
||||
@step 12
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="12"></ul>
|
||||
|
||||
|
||||
In this final step, we will enhance our phonecat web application by attaching CSS and JavaScript
|
||||
animations on top of the template code we created before.
|
||||
In this step, you will add a clickable phone image swapper to the phone details page.
|
||||
|
||||
* 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.
|
||||
|
||||
* We now use the `ngAnimate` module to enable animations throughout the application.
|
||||
* We also use common `ng` directives to automatically trigger hooks for animations to tap into.
|
||||
* When an animation is found then the animation will run in between the standard DOM operation that
|
||||
is being issued on the element at the given time (e.g. inserting and removing nodes on
|
||||
{@link ngRepeat `ngRepeat`} or adding and removing classes on
|
||||
{@link ngClass `ngClass`}).
|
||||
|
||||
<div doc-tutorial-reset="12"></div>
|
||||
|
||||
## Dependencies
|
||||
|
||||
The animation functionality is provided by Angular in the `ngAnimate` module, which is distributed
|
||||
separately from the core Angular framework. In addition we will use `jQuery` in this project to do
|
||||
extra JavaScript animations.
|
||||
## Component Controller
|
||||
|
||||
We are using [Bower][bower] to install client side dependencies. This step updates the
|
||||
`bower.json` configuration file to include the new dependency:
|
||||
|
||||
```
|
||||
{
|
||||
"name": "angular-seed",
|
||||
"description": "A starter project for AngularJS",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/angular/angular-seed",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "1.4.x",
|
||||
"angular-mocks": "1.4.x",
|
||||
"jquery": "~2.1.1",
|
||||
"bootstrap": "~3.1.1",
|
||||
"angular-route": "1.4.x",
|
||||
"angular-resource": "1.4.x",
|
||||
"angular-animate": "1.4.x"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* `"angular-animate": "1.4.x"` tells bower to install a version of the
|
||||
angular-animate component that is compatible with version 1.4.x.
|
||||
* `"jquery": "~2.1.1"` tells bower to install the 2.1.1 version of jQuery. Note that this is not an
|
||||
Angular library, it is the standard jQuery library. We can use bower to install a wide range of 3rd
|
||||
party libraries.
|
||||
|
||||
We must ask bower to download and install this dependency. We can do this by running:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then you may have a
|
||||
problem with the `bower install` due to a conflict between the versions of angular.js that need to
|
||||
be installed. If you get this then simply delete your `app/bower_components` folder before running
|
||||
`npm install`.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** If you have bower installed globally then you can run `bower install` but for this project we have
|
||||
preconfigured `npm install` to run bower for us.
|
||||
</div>
|
||||
|
||||
|
||||
## How Animations work with `ngAnimate`
|
||||
|
||||
To get an idea of how animations work with AngularJS, please read the
|
||||
{@link guide/animations AngularJS Animation Guide} first.
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
The changes required within the HTML template code is to link the asset files which define the animations as
|
||||
well as the `angular-animate.js` file. The animation module, known as {@link module:ngAnimate `ngAnimate`}, is
|
||||
defined within `angular-animate.js` and contains the code necessary to make your application become animation
|
||||
aware.
|
||||
|
||||
Here's what needs to be changed in the index file:
|
||||
|
||||
__`app/index.html`.__
|
||||
|
||||
```html
|
||||
...
|
||||
<!-- for CSS Transitions and/or Keyframe Animations -->
|
||||
<link rel="stylesheet" href="css/animations.css">
|
||||
|
||||
...
|
||||
|
||||
<!-- jQuery is used for JavaScript animations (include this before angular.js) -->
|
||||
<script src="bower_components/jquery/dist/jquery.js"></script>
|
||||
|
||||
...
|
||||
|
||||
<!-- required module to enable animation support in AngularJS -->
|
||||
<script src="bower_components/angular-animate/angular-animate.js"></script>
|
||||
|
||||
<!-- for JavaScript Animations -->
|
||||
<script src="js/animations.js"></script>
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
<div class="alert alert-error">
|
||||
**Important:** Be sure to use jQuery version 2.1 or newer when using Angular 1.4; jQuery 1.x is
|
||||
not officially supported.
|
||||
Be sure to load jQuery before all AngularJS scripts, otherwise AngularJS won't detect jQuery and
|
||||
animations will not work as expected.
|
||||
</div>
|
||||
|
||||
Animations can now be created within the CSS code (`animations.css`) as well as the JavaScript code (`animations.js`).
|
||||
But before we start, let's create a new module which uses the ngAnimate module as a dependency just like we did before
|
||||
with `ngResource`.
|
||||
|
||||
## Module & Animations
|
||||
|
||||
__`app/js/animations.js`.__
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.component.js`:**
|
||||
|
||||
```js
|
||||
angular.module('phonecatAnimations', ['ngAnimate']);
|
||||
// ...
|
||||
// this module will later be used to define animations
|
||||
// ...
|
||||
...
|
||||
controller: ['$http', '$routeParams',
|
||||
function PhoneDetailController($http, $routeParams) {
|
||||
var self = this;
|
||||
|
||||
self.setImage = function setImage(imageUrl) {
|
||||
self.mainImageUrl = imageUrl;
|
||||
};
|
||||
|
||||
$http.get('phones/' + $routeParams.phoneId + '.json').then(function(response) {
|
||||
self.phone = response.data;
|
||||
self.setImage(self.phone.images[0]);
|
||||
});
|
||||
}
|
||||
]
|
||||
...
|
||||
```
|
||||
|
||||
And now let's attach this module to our application module...
|
||||
In the `phoneDetail` component's controller, we created the `mainImageUrl` model property and set
|
||||
its default value to the first phone image URL.
|
||||
|
||||
__`app/js/app.js`.__
|
||||
|
||||
```js
|
||||
// ...
|
||||
angular.module('phonecatApp', [
|
||||
'ngRoute',
|
||||
|
||||
'phonecatAnimations',
|
||||
'phonecatControllers',
|
||||
'phonecatFilters',
|
||||
'phonecatServices',
|
||||
]);
|
||||
// ...
|
||||
```
|
||||
|
||||
Now, the phonecat module is animation aware. Let's make some animations!
|
||||
We also created a `setImage()` method (to be used as event handler), that will change the value of
|
||||
`mainImageUrl`.
|
||||
|
||||
|
||||
## Animating ngRepeat with CSS Transition Animations
|
||||
## Component Template
|
||||
|
||||
We'll start off by adding CSS transition animations to our `ngRepeat` directive present on the `phone-list.html` page.
|
||||
First let's add an extra CSS class to our repeated element so that we can hook into it with our CSS animation code.
|
||||
|
||||
__`app/partials/phone-list.html`.__
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.template.html`:**
|
||||
|
||||
```html
|
||||
<!--
|
||||
Let's change the repeater HTML to include a new CSS class
|
||||
which we will later use for animations:
|
||||
-->
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp"
|
||||
class="thumbnail phone-listing">
|
||||
<a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
```
|
||||
|
||||
Notice how we added the `phone-listing` CSS class? This is all we need in our HTML code to get animations working.
|
||||
|
||||
Now for the actual CSS transition animation code:
|
||||
|
||||
__`app/css/animations.css`__
|
||||
|
||||
```css
|
||||
.phone-listing.ng-enter,
|
||||
.phone-listing.ng-leave,
|
||||
.phone-listing.ng-move {
|
||||
-webkit-transition: 0.5s linear all;
|
||||
-moz-transition: 0.5s linear all;
|
||||
-o-transition: 0.5s linear all;
|
||||
transition: 0.5s linear all;
|
||||
}
|
||||
|
||||
.phone-listing.ng-enter,
|
||||
.phone-listing.ng-move {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.phone-listing.ng-move.ng-move-active,
|
||||
.phone-listing.ng-enter.ng-enter-active {
|
||||
opacity: 1;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.phone-listing.ng-leave {
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.phone-listing.ng-leave.ng-leave-active {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
```
|
||||
|
||||
As you can see our `phone-listing` CSS class is combined together with the animation hooks that occur when items are
|
||||
inserted into and removed from the list:
|
||||
|
||||
* The `ng-enter` class is applied to the element when a new phone is added to the list and rendered on the page.
|
||||
* The `ng-move` class is applied when items are moved around in the list.
|
||||
* The `ng-leave` class is applied when they're removed from the list.
|
||||
|
||||
The phone listing items are added and removed depending on the data passed to the `ng-repeat` attribute.
|
||||
For example, if the filter data changes, the items will be animated in and out of the repeat list.
|
||||
|
||||
Something important to note is that when an animation occurs, two sets of CSS classes
|
||||
are added to the element:
|
||||
|
||||
1. a "starting" class that represents the style at the beginning of the animation
|
||||
2. an "active" class that represents the style at the end of the animation
|
||||
|
||||
The name of the starting class is the name of the event that is fired (like `enter`, `move` or `leave`) prefixed with
|
||||
`ng-`. So an `enter` event will result in a class called `ng-enter`.
|
||||
|
||||
The active class name is the same as the starting class's but with an `-active` suffix.
|
||||
This two-class CSS naming convention allows the developer to craft an animation, beginning to end.
|
||||
|
||||
In our example above, elements are expanded from a height of **0** to **120 pixels** when they're added to the
|
||||
list and are collapsed back down to **0 pixels** before being removed from the list.
|
||||
There's also a nice fade-in and fade-out effect that occurs at the same time. All of this is handled
|
||||
by the CSS transition declarations at the top of the example code above.
|
||||
|
||||
Although most modern browsers have good support for [CSS transitions](http://caniuse.com/#feat=css-transitions)
|
||||
and [CSS animations](http://caniuse.com/#feat=css-animation), IE9 and earlier do not.
|
||||
If you want animations that are backwards-compatible with older browsers, consider using JavaScript-based animations,
|
||||
which are described in detail below.
|
||||
|
||||
|
||||
## Animating `ngView` with CSS Keyframe Animations
|
||||
|
||||
Next let's add an animation for transitions between route changes in {@link ngRoute.directive:ngView `ngView`}.
|
||||
|
||||
To start, let's add a new CSS class to our HTML like we did in the example above.
|
||||
This time, instead of the `ng-repeat` element, let's add it to the element containing the `ng-view` directive.
|
||||
In order to do this, we'll have to make some small changes to the HTML code so that we can have more control over our
|
||||
animations between view changes.
|
||||
|
||||
__`app/index.html`.__
|
||||
|
||||
```html
|
||||
<div class="view-container">
|
||||
<div ng-view class="view-frame"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
With this change, the `ng-view` directive is nested inside a parent element with
|
||||
a `view-container` CSS class. This class adds a `position: relative` style so that the positioning of the `ng-view`
|
||||
is relative to this parent as it animates transitions.
|
||||
|
||||
With this in place, let's add the CSS for this transition animation to our `animations.css` file:
|
||||
|
||||
__`app/css/animations.css`.__
|
||||
|
||||
```css
|
||||
.view-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.view-frame.ng-enter, .view-frame.ng-leave {
|
||||
background: white;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.view-frame.ng-enter {
|
||||
-webkit-animation: 0.5s fade-in;
|
||||
-moz-animation: 0.5s fade-in;
|
||||
-o-animation: 0.5s fade-in;
|
||||
animation: 0.5s fade-in;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.view-frame.ng-leave {
|
||||
-webkit-animation: 0.5s fade-out;
|
||||
-moz-animation: 0.5s fade-out;
|
||||
-o-animation: 0.5s fade-out;
|
||||
animation: 0.5s fade-out;
|
||||
z-index:99;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@-moz-keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@-webkit-keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
@-moz-keyframes fade-out {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
@-webkit-keyframes fade-out {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
/* don't forget about the vendor-prefixes! */
|
||||
```
|
||||
|
||||
Nothing crazy here! Just a simple fade in and fade out effect between pages. The only out of the
|
||||
ordinary thing here is that we're using absolute positioning to position the next page (identified
|
||||
via `ng-enter`) on top of the previous page (the one that has the `ng-leave` class) while performing
|
||||
a cross fade animation in between. So as the previous page is just about to be removed, it fades out
|
||||
while the new page fades in right on top of it.
|
||||
|
||||
Once the leave animation is over then element is removed and once the enter animation is complete
|
||||
then the `ng-enter` and `ng-enter-active` CSS classes are removed from the element, causing it to rerender and
|
||||
reposition itself with its default CSS code (so no more absolute positioning once the animation is
|
||||
over). This works fluidly so that pages flow naturally between route changes without anything
|
||||
jumping around.
|
||||
|
||||
The CSS classes applied (the start and end classes) are much the same as with `ng-repeat`. Each time
|
||||
a new page is loaded the `ng-view` directive will create a copy of itself, download the template and
|
||||
append the contents. This ensures that all views are contained within a single HTML element which
|
||||
allows for easy animation control.
|
||||
|
||||
For more on CSS animations, see the
|
||||
[Web Platform documentation](http://docs.webplatform.org/wiki/css/properties/animations).
|
||||
|
||||
|
||||
## Animating `ngClass` with JavaScript
|
||||
|
||||
Let's add another animation to our application. Switching to our `phone-detail.html` page,
|
||||
we see that we have a nice thumbnail swapper. By hovering over the thumbnails listed on the page,
|
||||
the profile phone image changes. But how can we change this around to add animations?
|
||||
|
||||
Let's think about it first. Basically, when you hover over a thumbnail image, you're changing the
|
||||
state of the profile image to reflect the newly selected thumbnail image.
|
||||
The best way to specify state changes within HTML is to use classes.
|
||||
Much like before, how we used a CSS class to specify an animation, this time the animation will
|
||||
occur whenever the CSS class itself changes.
|
||||
|
||||
Whenever a new phone thumbnail is selected, the state changes and the `.active` CSS class is added
|
||||
to the matching profile image and the animation plays.
|
||||
|
||||
Let's get started and tweak our HTML code on the `phone-detail.html` page first. Notice that we
|
||||
have changed the way we display our large image:
|
||||
|
||||
__`app/partials/phone-detail.html`.__
|
||||
|
||||
```html
|
||||
<!-- We're only changing the top of the file -->
|
||||
<div class="phone-images">
|
||||
<img ng-src="{{img}}"
|
||||
class="phone"
|
||||
ng-repeat="img in phone.images"
|
||||
ng-class="{active:mainImageUrl==img}">
|
||||
</div>
|
||||
|
||||
<h1>{{phone.name}}</h1>
|
||||
|
||||
<p>{{phone.description}}</p>
|
||||
|
||||
<img ng-src="{{$ctrl.mainImageUrl}}" class="phone" />
|
||||
...
|
||||
<ul class="phone-thumbs">
|
||||
<li ng-repeat="img in phone.images">
|
||||
<img ng-src="{{img}}" ng-mouseenter="setImage(img)">
|
||||
<li ng-repeat="img in $ctrl.phone.images">
|
||||
<img ng-src="{{img}}" ng-click="$ctrl.setImage(img)" />
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Just like with the thumbnails, we're using a repeater to display **all** the profile images as a
|
||||
list, however we're not animating any repeat-related animations. Instead, we're keeping our eye on
|
||||
the ng-class directive since whenever the `active` class is true then it will be applied to the
|
||||
element and will render as visible. Otherwise, the profile image is hidden. In our case, there is
|
||||
always one element that has the active class, and, therefore, there will always be one phone profile
|
||||
image visible on screen at all times.
|
||||
|
||||
When the active class is added to the element, the `active-add` and the `active-add-active` classes
|
||||
are added just before to signal AngularJS to fire off an animation. When removed, the
|
||||
`active-remove` and the `active-remove-active` classes are applied to the element which in turn
|
||||
trigger another animation.
|
||||
|
||||
To ensure that the phone images are displayed correctly when the page is first loaded we also tweak
|
||||
the detail page CSS styles:
|
||||
|
||||
__`app/css/app.css`__
|
||||
```css
|
||||
.phone-images {
|
||||
background-color: white;
|
||||
width: 450px;
|
||||
height: 450px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
float: left;
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
img.phone {
|
||||
float: left;
|
||||
margin-right: 3em;
|
||||
margin-bottom: 2em;
|
||||
background-color: white;
|
||||
padding: 2em;
|
||||
height: 400px;
|
||||
width: 400px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
img.phone:first-child {
|
||||
display: block;
|
||||
}
|
||||
```
|
||||
|
||||
We bound the `ngSrc` directive of the large image to the `$ctrl.mainImageUrl` property.
|
||||
|
||||
You may be thinking that we're just going to create another CSS-enabled animation.
|
||||
Although we could do that, let's take the opportunity to learn how to create JavaScript-enabled
|
||||
animations with the `animation()` module method.
|
||||
We also registered an {@link ng.directive:ngClick ngClick} handler with thumbnail images. When a
|
||||
user clicks on one of the thumbnail images, the handler will use the `$ctrl.setImage()` method
|
||||
callback to change the value of the `$ctrl.mainImageUrl` property to the URL of the clicked
|
||||
thumbnail image.
|
||||
|
||||
__`app/js/animations.js`.__
|
||||
<img class="diagram" src="img/tutorial/tutorial_12.png">
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
To verify this new feature, we added two E2E tests. One verifies that `mainImageUrl` is set to the
|
||||
first phone image URL by default. The second test clicks on several thumbnail images and verifies
|
||||
that the main image URL changes accordingly.
|
||||
|
||||
<br />
|
||||
**`e2e-tests/scenarios.js`:**
|
||||
|
||||
```js
|
||||
var phonecatAnimations = angular.module('phonecatAnimations', ['ngAnimate']);
|
||||
...
|
||||
|
||||
phonecatAnimations.animation('.phone', function() {
|
||||
describe('View: Phone detail', function() {
|
||||
|
||||
var animateUp = function(element, className, done) {
|
||||
if(className != 'active') {
|
||||
return;
|
||||
}
|
||||
element.css({
|
||||
position: 'absolute',
|
||||
top: 500,
|
||||
left: 0,
|
||||
display: 'block'
|
||||
...
|
||||
|
||||
it('should display the first phone image as the main phone image', function() {
|
||||
var mainImage = element(by.css('img.phone'));
|
||||
|
||||
expect(mainImage.getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
|
||||
});
|
||||
|
||||
jQuery(element).animate({
|
||||
top: 0
|
||||
}, done);
|
||||
it('should swap the main image when clicking on a thumbnail image', function() {
|
||||
var mainImage = element(by.css('img.phone'));
|
||||
var thumbnails = element.all(by.css('.phone-thumbs img'));
|
||||
|
||||
return function(cancel) {
|
||||
if(cancel) {
|
||||
element.stop();
|
||||
}
|
||||
};
|
||||
}
|
||||
thumbnails.get(2).click();
|
||||
expect(mainImage.getAttribute('src')).toMatch(/img\/phones\/nexus-s.2.jpg/);
|
||||
|
||||
var animateDown = function(element, className, done) {
|
||||
if(className != 'active') {
|
||||
return;
|
||||
}
|
||||
element.css({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0
|
||||
thumbnails.get(0).click();
|
||||
expect(mainImage.getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
|
||||
});
|
||||
|
||||
jQuery(element).animate({
|
||||
top: -500
|
||||
}, done);
|
||||
});
|
||||
|
||||
return function(cancel) {
|
||||
if(cancel) {
|
||||
element.stop();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
addClass: animateUp,
|
||||
removeClass: animateDown
|
||||
};
|
||||
});
|
||||
...
|
||||
```
|
||||
|
||||
Note that we're using [jQuery](http://jquery.com/) to implement the animation. jQuery
|
||||
isn't required to do JavaScript animations with AngularJS, but we're going to use it because writing
|
||||
your own JavaScript animation library is beyond the scope of this tutorial. For more on
|
||||
`jQuery.animate`, see the [jQuery documentation](http://api.jquery.com/animate/).
|
||||
You can now rerun the tests with `npm run protractor`.
|
||||
|
||||
The `addClass` and `removeClass` callback functions are called whenever a class is added or removed
|
||||
on the element that contains the class we registered, which is in this case `.phone`. When the `.active`
|
||||
class is added to the element (via the `ng-class` directive) the `addClass` JavaScript callback will
|
||||
be fired with `element` passed in as a parameter to that callback. The last parameter passed in is the
|
||||
`done` callback function. The purpose of `done` is so you can let Angular know when the JavaScript
|
||||
animation has ended by calling it.
|
||||
We also have to refactor one of our unit tests, because of the addition of the `mainImageUrl` model
|
||||
property to the controller. As previously, we will use a mocked response.
|
||||
|
||||
The `removeClass` callback works the same way, but instead gets triggered when a class is removed
|
||||
from the element.
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.component.spec.js`:**
|
||||
|
||||
Within your JavaScript callback, you create the animation by manipulating the DOM. In the code above,
|
||||
that's what the `element.css()` and the `element.animate()` are doing. The callback positions the next
|
||||
element with an offset of `500 pixels` and animates both the previous and the new items together by
|
||||
shifting each item up `500 pixels`. This results in a conveyor-belt like animation. After the `animate`
|
||||
function does its business, it calls `done`.
|
||||
```js
|
||||
...
|
||||
|
||||
Notice that `addClass` and `removeClass` each return a function. This is an **optional** function that's
|
||||
called when the animation is cancelled (when another animation takes place on the same element)
|
||||
as well as when the animation has completed. A boolean parameter is passed into the function which
|
||||
lets the developer know if the animation was cancelled or not. This function can be used to
|
||||
do any cleanup necessary for when the animation finishes.
|
||||
describe('controller', function() {
|
||||
var $httpBackend, ctrl
|
||||
var xyzPhoneData = {
|
||||
name: 'phone xyz',
|
||||
images: ['image/url1.png', 'image/url2.png']
|
||||
};
|
||||
|
||||
beforeEach(inject(function($componentController, _$httpBackend_, _$routeParams_) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData);
|
||||
|
||||
...
|
||||
}));
|
||||
|
||||
it('should fetch phone details', function() {
|
||||
expect(ctrl.phone).toBeUndefined();
|
||||
|
||||
$httpBackend.flush();
|
||||
expect(ctrl.phone).toEqual(xyzPhoneData);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Our unit tests should now be passing again.
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
<div></div>
|
||||
|
||||
* Similar to the `ngClick` directive, which binds an Angular expression to the `click` event, there
|
||||
are built-in directives for all native events, such as `dblclick`, `focus`/`blur`, mouse and key
|
||||
events, etc.
|
||||
|
||||
Let's add a new controller method to the `phoneDetail` component's controller:
|
||||
|
||||
```js
|
||||
self.onDblclick = function onDblclick(imageUrl) {
|
||||
alert('You double-clicked image: ' + imageUrl);
|
||||
};
|
||||
```
|
||||
|
||||
and add the following to the `<img>` element in `phone-detail.template.html`:
|
||||
|
||||
```html
|
||||
<img ... ng-dblclick="$ctrl.onDblclick(img)" />
|
||||
```
|
||||
|
||||
Now, whenever you double-click on a thumbnail, an alert pops-up. Pretty annoying!
|
||||
|
||||
|
||||
# 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 where to go from here.
|
||||
With the phone image swapper in place, we are ready for {@link step_13 step 13} to learn an even
|
||||
better way to fetch data.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="12"></ul>
|
||||
|
||||
[bower]: http://bower.io/
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
@ngdoc tutorial
|
||||
@name 13 - REST and Custom Services
|
||||
@step 13
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="13"></ul>
|
||||
|
||||
|
||||
In this step, we will change the way our application fetches data.
|
||||
|
||||
* We define a custom service that represents a [RESTful][restful] client. Using this client we can
|
||||
make requests for data to the server in an easier way, without having to deal with the lower-level
|
||||
{@link ng.$http $http} API, HTTP methods and URLs.
|
||||
|
||||
|
||||
<div doc-tutorial-reset="13"></div>
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
The RESTful functionality is provided by Angular in the {@link ngResource ngResource} module, which
|
||||
is distributed separately from the core Angular framework.
|
||||
|
||||
Since we are using [Bower][bower] to install client-side dependencies, this step updates the
|
||||
`bower.json` configuration file to include the new dependency:
|
||||
|
||||
<br />
|
||||
**`bower.json`:**
|
||||
|
||||
```
|
||||
{
|
||||
"name": "angular-phonecat",
|
||||
"description": "A starter project for AngularJS",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/angular/angular-phonecat",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "1.5.x",
|
||||
"angular-mocks": "1.5.x",
|
||||
"angular-resource": "1.5.x",
|
||||
"angular-route": "1.5.x",
|
||||
"bootstrap": "3.3.x"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The new dependency `"angular-resource": "1.5.x"` tells bower to install a version of the
|
||||
angular-resource module that is compatible with version 1.5.x of Angular. We must tell bower to
|
||||
download and install this dependency.
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** If you have bower installed globally, you can run `bower install`, but for this project
|
||||
we have preconfigured `npm install` to run bower for us.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then
|
||||
you may have a problem with the `bower install` due to a conflict between the versions of
|
||||
angular.js that need to be installed. If you run into this issue, simply delete your
|
||||
`app/bower_components` directory and then run `npm install`.
|
||||
</div>
|
||||
|
||||
|
||||
## Service
|
||||
|
||||
We create our own service to provide access to the phone data on the server. We will put the service
|
||||
in its own module, under `core`, so we can explicitly declare its dependency on `ngResource`:
|
||||
|
||||
<br />
|
||||
**`app/core/phone/phone.module.js`:**
|
||||
|
||||
```js
|
||||
angular.module('core.phone', ['ngResource']);
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/core/phone/phone.service.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('core.phone').
|
||||
factory('Phone', ['$resource',
|
||||
function($resource) {
|
||||
return $resource('phones/:phoneId.json', {}, {
|
||||
query: {
|
||||
method: 'GET',
|
||||
params: {phoneId: 'phones'},
|
||||
isArray: true
|
||||
}
|
||||
});
|
||||
}
|
||||
]);
|
||||
```
|
||||
|
||||
We used the {@link angular.Module module API} to register a custom service using a factory function.
|
||||
We passed in the name of the service — `'Phone'` — and the factory function. The factory
|
||||
function is similar to a controller's constructor in that both can declare dependencies to be
|
||||
injected via function arguments. The `Phone` service declares a dependency on the `$resource`
|
||||
service, provided by the `ngResource` module.
|
||||
|
||||
The {@link ngResource.$resource $resource} service makes it easy to create a [RESTful][restful]
|
||||
client with just a few lines of code. This client can then be used in our application, instead of
|
||||
the lower-level {@link ng.$http $http} service.
|
||||
|
||||
<br />
|
||||
**`app/core/core.module.js`:**
|
||||
|
||||
```js
|
||||
angular.module('core', ['core.phone']);
|
||||
```
|
||||
|
||||
We need to add the `core.phone` module as a dependency of the `core` module.
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
Our custom resource service will be defined in `app/core/phone/phone.service.js`, so we need to
|
||||
include this file and the associated `.module.js` file in our layout template. Additionally, we also
|
||||
need to load the `angular-resource.js` file, which contains the `ngResource` module:
|
||||
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
<head>
|
||||
...
|
||||
<script src="bower_components/angular-resource/angular-resource.js"></script>
|
||||
...
|
||||
<script src="core/phone/phone.module.js"></script>
|
||||
<script src="core/phone/phone.service.js"></script>
|
||||
...
|
||||
</head>
|
||||
```
|
||||
|
||||
|
||||
## Component Controllers
|
||||
|
||||
We can now simplify our component controllers (`PhoneListController` and `PhoneDetailController`) by
|
||||
factoring out the lower-level `$http` service, replacing it with the new `Phone` service. Angular's
|
||||
`$resource` service is easier to use than `$http` for interacting with data sources exposed as
|
||||
RESTful resources. It is also easier now to understand what the code in our controllers is doing.
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.module.js`:**
|
||||
|
||||
```js
|
||||
angular.module('phoneList', ['core.phone']);
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phoneList').
|
||||
component('phoneList', {
|
||||
templateUrl: 'phone-list/phone-list.template.html',
|
||||
controller: ['Phone',
|
||||
function PhoneListController(Phone) {
|
||||
this.phones = Phone.query();
|
||||
this.orderProp = 'age';
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.module.js`:**
|
||||
|
||||
```js
|
||||
angular.module('phoneDetail', ['core.phone']);
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.component.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phoneDetail').
|
||||
component('phoneDetail', {
|
||||
templateUrl: 'phone-detail/phone-detail.template.html',
|
||||
controller: ['$routeParams', 'Phone',
|
||||
function PhoneDetailController($routeParams, Phone) {
|
||||
var self = this;
|
||||
self.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) {
|
||||
self.setImage(phone.images[0]);
|
||||
});
|
||||
|
||||
self.setImage = function setImage(imageUrl) {
|
||||
self.mainImageUrl = imageUrl;
|
||||
};
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
Notice how in `PhoneListController` we replaced:
|
||||
|
||||
```js
|
||||
$http.get('phones/phones.json').then(function(response) {
|
||||
self.phones = response.data;
|
||||
});
|
||||
```
|
||||
|
||||
with just:
|
||||
|
||||
```js
|
||||
this.phones = Phone.query();
|
||||
```
|
||||
|
||||
This is a simple and declarative 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 results 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 is received. 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 be updated automatically.
|
||||
|
||||
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
|
||||
`phoneDetail` component's controller illustrates this by setting the `mainImageUrl` in a callback.
|
||||
|
||||
|
||||
## Testing
|
||||
|
||||
Because we are now using the {@link ngResource ngResource} module, it is necessary to update the
|
||||
Karma configuration file with angular-resource.
|
||||
|
||||
<br />
|
||||
**`karma.conf.js`:**
|
||||
|
||||
```js
|
||||
files: [
|
||||
'bower_components/angular/angular.js',
|
||||
'bower_components/angular-resource/angular-resource.js',
|
||||
...
|
||||
],
|
||||
```
|
||||
|
||||
We have added a unit test to verify that our new service is issuing HTTP requests and returns the
|
||||
expected "future" objects/arrays.
|
||||
|
||||
The {@link ngResource.$resource $resource} service augments the response object with extra methods
|
||||
— e.g. for updating and deleting the resource — and properties (some of which are only
|
||||
meant to be accessed by Angular). If we were to use Jasmine's standard `.toEqual()` matcher, our
|
||||
tests would fail, because the test values would not match the responses exactly.
|
||||
|
||||
To solve the problem, we instruct Jasmine to use a [custom equality tester][jasmine-equality] for
|
||||
comparing objects. We specify {@link angular.equals angular.equals} as our equality tester, which
|
||||
ignores functions and `$`-prefixed properties, such as those added by the `$resource` service.<br />
|
||||
(Remember that Angular uses the `$` prefix for its proprietary API.)
|
||||
|
||||
<br />
|
||||
**`app/core/phone/phone.service.spec.js`:**
|
||||
|
||||
```js
|
||||
describe('Phone', function() {
|
||||
...
|
||||
var phonesData = [...];
|
||||
|
||||
// Add a custom equality tester before each test
|
||||
beforeEach(function() {
|
||||
jasmine.addCustomEqualityTester(angular.equals);
|
||||
});
|
||||
|
||||
// Load the module that contains the `Phone` service before each test
|
||||
...
|
||||
|
||||
// Instantiate the service and "train" `$httpBackend` before each test
|
||||
...
|
||||
|
||||
// Verify that there are no outstanding expectations or requests after each test
|
||||
afterEach(function () {
|
||||
$httpBackend.verifyNoOutstandingExpectation();
|
||||
$httpBackend.verifyNoOutstandingRequest();
|
||||
});
|
||||
|
||||
it('should fetch the phones data from `/phones/phones.json`', function() {
|
||||
var phones = Phone.query();
|
||||
|
||||
expect(phones).toEqual([]);
|
||||
|
||||
$httpBackend.flush();
|
||||
expect(phones).toEqual(phonesData);
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
Here we are using `$httpBackend`'s
|
||||
{@link ngMock.$httpBackend#verifyNoOutstandingExpectation verifyNoOutstandingExpectation()} and
|
||||
{@link ngMock.$httpBackend#verifyNoOutstandingExpectation verifyNoOutstandingRequest()} methods to
|
||||
verify that all expected requests have been sent and that no extra request is scheduled for later.
|
||||
|
||||
Note that we have also modified our component tests to use the custom matcher when appropriate.
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
```
|
||||
Chrome 49.0: Executed 5 of 5 SUCCESS (0.123 secs / 0.104 secs)
|
||||
```
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now that we have seen how to build a custom service as a RESTful client, we are ready for
|
||||
{@link step_14 step 14} to learn how to enhance the user experience with animations.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="13"></ul>
|
||||
|
||||
|
||||
[bower]: http://bower.io/
|
||||
[jasmine-equality]: https://jasmine.github.io/2.4/custom_equality.html
|
||||
[restful]: https://en.wikipedia.org/wiki/Representational_State_Transfer
|
||||
@@ -0,0 +1,564 @@
|
||||
@ngdoc tutorial
|
||||
@name 14 - Animations
|
||||
@step 14
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="14"></ul>
|
||||
|
||||
|
||||
In this step, we will enhance our web application by adding CSS and JavaScript animations on top of
|
||||
the template code we created earlier.
|
||||
|
||||
* We now use the {@link ngAnimate ngAnimate} module to enable animations throughout the application.
|
||||
* We also rely on built-in directives to automatically trigger hooks for animations to tap into.
|
||||
* When an animation is found, it will run along with the actual DOM operation that is being issued
|
||||
on the element at the given time (e.g. inserting/removing nodes on {@link ngRepeat ngRepeat} or
|
||||
adding/removing classes on {@link ngClass ngClass}).
|
||||
|
||||
|
||||
<div doc-tutorial-reset="14"></div>
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
The animation functionality is provided by Angular in the `ngAnimate` module, which is distributed
|
||||
separately from the core Angular framework. In addition we will use [jQuery][jquery] in this project
|
||||
to do extra JavaScript animations.
|
||||
|
||||
Since we are using [Bower][bower] to install client-side dependencies, this step updates the
|
||||
`bower.json` configuration file to include the new dependencies:
|
||||
|
||||
<br />
|
||||
**`bower.json`:**
|
||||
|
||||
```
|
||||
{
|
||||
"name": "angular-phonecat",
|
||||
"description": "A starter project for AngularJS",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/angular/angular-phonecat",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "1.5.x",
|
||||
"angular-animate": "1.5.x",
|
||||
"angular-mocks": "1.5.x",
|
||||
"angular-resource": "1.5.x",
|
||||
"angular-route": "1.5.x",
|
||||
"bootstrap": "3.3.x",
|
||||
"jquery": "2.2.x"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* `"angular-animate": "1.5.x"` tells bower to install a version of the angular-animate module that
|
||||
is compatible with version 1.5.x of Angular.
|
||||
* `"jquery": "2.2.x"` tells bower to install the latest patch release of the 2.2 version of jQuery.
|
||||
Note that this is not an Angular library; it is the standard jQuery library. We can use bower to
|
||||
install a wide range of 3rd party libraries.
|
||||
|
||||
Now, we must tell bower to download and install these dependencies.
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** If you have bower installed globally, you can run `bower install`, but for this project
|
||||
we have preconfigured `npm install` to run bower for us.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then
|
||||
you may have a problem with the `bower install` due to a conflict between the versions of
|
||||
angular.js that need to be installed. If you run into this issue, simply delete your
|
||||
`app/bower_components` directory and then run `npm install`.
|
||||
</div>
|
||||
|
||||
|
||||
## How Animations work with `ngAnimate`
|
||||
|
||||
To get an idea of how animations work with AngularJS, you might want to read the
|
||||
[Animations](guide/animations) section of the Developer Guide first.
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
In order to enable animations, we need to update `index.html`, loading the necessary dependencies
|
||||
(**angular-animate.js** and **jquery.js**) and the files that contain the CSS and JavaScript code
|
||||
used in CSS/JavaScript animations. The animation module, {@link ngAnimate ngAnimate}, contains the
|
||||
code necessary to make your application "animation aware".
|
||||
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
...
|
||||
|
||||
<!-- Defines CSS necessary for animations -->
|
||||
<link rel="stylesheet" href="app.animations.css" />
|
||||
|
||||
...
|
||||
|
||||
<!-- Used for JavaScript animations (include this before angular.js) -->
|
||||
<script src="bower_components/jquery/dist/jquery.js"></script>
|
||||
|
||||
...
|
||||
|
||||
<!-- Adds animation support in AngularJS -->
|
||||
<script src="bower_components/angular-animate/angular-animate.js"></script>
|
||||
|
||||
<!-- Defines JavaScript animations -->
|
||||
<script src="app.animations.js"></script>
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
<div class="alert alert-error">
|
||||
**Important:** Be sure to use jQuery version 2.1 or newer, when using Angular 1.5; jQuery 1.x is
|
||||
not officially supported.
|
||||
In order for Angular to detect jQuery and take advantage of it, make sure to include `jquery.js`
|
||||
before `angular.js`.
|
||||
</div>
|
||||
|
||||
Animations can now be created within the CSS code (`app.animations.css`) as well as the JavaScript
|
||||
code (`app.animations.js`).
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
We need to add a dependency on `ngAnimate` to our main module first:
|
||||
|
||||
<br />
|
||||
**`app/app.module.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phonecatApp', [
|
||||
'ngAnimate',
|
||||
...
|
||||
]);
|
||||
```
|
||||
|
||||
Now that our application is "animation aware", let's create some fancy animations!
|
||||
|
||||
|
||||
## CSS Transition Animations: Animating `ngRepeat`
|
||||
|
||||
We will start off by adding CSS transition animations to our `ngRepeat` directive present on the
|
||||
`phoneList` component's template. We need to add an extra CSS class to our repeated element, in
|
||||
order to be able to hook into it with our CSS animation code.
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.template.html`:**
|
||||
|
||||
```html
|
||||
...
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp"
|
||||
class="thumbnail phone-list-item">
|
||||
<a href="#!/phones/{{phone.id}}" class="thumb">
|
||||
<img ng-src="{{phone.imageUrl}}" alt="{{phone.name}}" />
|
||||
</a>
|
||||
<a href="#!/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
```
|
||||
|
||||
Did you notice the added `phone-list-item` CSS class? This is all we need in our HTML code to get
|
||||
animations working.
|
||||
|
||||
Now for the actual CSS transition animation code:
|
||||
|
||||
<br />
|
||||
**`app/app.animations.css`:**
|
||||
|
||||
```css
|
||||
.phone-list-item.ng-enter,
|
||||
.phone-list-item.ng-leave,
|
||||
.phone-list-item.ng-move {
|
||||
transition: 0.5s linear all;
|
||||
}
|
||||
|
||||
.phone-list-item.ng-enter,
|
||||
.phone-list-item.ng-move {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.phone-list-item.ng-enter.ng-enter-active,
|
||||
.phone-list-item.ng-move.ng-move-active {
|
||||
height: 120px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.phone-list-item.ng-leave {
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.phone-list-item.ng-leave.ng-leave-active {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, our `phone-list-item` CSS class is combined together with the animation hooks that
|
||||
occur when items are inserted into and removed from the list:
|
||||
|
||||
* The `ng-enter` class is applied to the element when a new phone is added to the list and rendered
|
||||
on the page.
|
||||
* The `ng-move` class is applied to the element when a phone's relative position in the list
|
||||
changes.
|
||||
* The `ng-leave` class is applied to the element when a phone is removed from the list.
|
||||
|
||||
The phone list items are added and removed based on the data passed to the `ngRepeat` directive.
|
||||
For example, if the filter data changes, the items will be animated in and out of the repeat list.
|
||||
|
||||
Something important to note is that, when an animation occurs, two sets of CSS classes are added to
|
||||
the element:
|
||||
|
||||
1. A "starting" class that represents the style at the beginning of the animation.
|
||||
2. An "active" class that represents the style at the end of the animation.
|
||||
|
||||
The name of the starting class is the name of the event that is fired (like `enter`, `move` or
|
||||
`leave`) prefixed with `ng-`. So an `enter` event will result in adding the `ng-enter` class.
|
||||
|
||||
The active class name is derived from the starting class by appending an `-active` suffix.
|
||||
This two-class CSS naming convention allows the developer to craft an animation, beginning to end.
|
||||
|
||||
In the example above, animated elements are expanded from a height of **0px** to **120px** when they
|
||||
are added to the list and are collapsed back down to **0px** before being removed from the list.
|
||||
There is also a catchy fade-in/fade-out effect that occurs at the same time. All this is handled by
|
||||
the CSS transition declaration at the top of the CSS file.
|
||||
|
||||
<div class="alert alert-warning">
|
||||
Although all modern browsers have good support for [CSS transitions][caniuse-css-transitions] and
|
||||
[CSS animations][caniuse-css-animation], IE9 and earlier IE versions do not.
|
||||
If you want animations that are backwards-compatible with older browsers, consider using
|
||||
JavaScript-based animations, which are demonstrated below.
|
||||
</div>
|
||||
|
||||
|
||||
## CSS Keyframe Animations: Animating `ngView`
|
||||
|
||||
Next, let's add an animation for transitions between route changes in
|
||||
{@link ngRoute.directive:ngView ngView}.
|
||||
|
||||
Again, we need to prepare our HTML template by adding a new CSS class, this time to the `ng-view`
|
||||
element. In order to gain more "expressive power" for our animations, we will also wrap the
|
||||
`[ng-view]` element in a container element.
|
||||
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
<div class="view-container">
|
||||
<div ng-view class="view-frame"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
We have applied a `position: relative` CSS style to the `.view-container` wrapper, so that it is
|
||||
easier for us to manage the `.view-frame` element's positioning during the animation.
|
||||
|
||||
With our preparation code in place, let's move on to the actual CSS styles for this transition
|
||||
animation.
|
||||
|
||||
<br />
|
||||
**`app/app.animations.css`:**
|
||||
|
||||
```css
|
||||
...
|
||||
|
||||
.view-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.view-frame.ng-enter,
|
||||
.view-frame.ng-leave {
|
||||
background: white;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.view-frame.ng-enter {
|
||||
animation: 1s fade-in;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.view-frame.ng-leave {
|
||||
animation: 1s fade-out;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
/* Older browsers might need vendor-prefixes for keyframes and animation! */
|
||||
```
|
||||
|
||||
Nothing fancy here! Just a simple fade-in/fade-out effect between pages. The only thing out of the
|
||||
ordinary here is that we are using absolute positioning to position the entering page (identified
|
||||
by the `ng-enter` class) on top of the leaving page (identified by the `ng-leave` class). At the
|
||||
same time a cross-fade animation is performed. So, as the previous page is just about to be removed,
|
||||
it fades out, while the new page fades in right on top of it.
|
||||
|
||||
Once the `leave` animation is over, the element is removed from the DOM. Likewise, once the `enter`
|
||||
animation is complete, the `ng-enter` and `ng-enter-active` CSS classes are removed from the
|
||||
element, causing it to rerender and reposition itself with its default CSS styles (so no more
|
||||
absolute positioning once the animation is over). This works fluidly and the pages flow naturally
|
||||
between route changes, without anything jumping around.
|
||||
|
||||
The applied CSS classes are much the same as with `ngRepeat`. Each time a new page is loaded the
|
||||
`ngView` directive will create a copy of itself, download the template and append the contents. This
|
||||
ensures that all views are contained within a single HTML element, which allows for easy animation
|
||||
control.
|
||||
|
||||
For more on CSS animations, see the [Web Platform documentation][webplatform-animations].
|
||||
|
||||
|
||||
## Animating `ngClass` with JavaScript
|
||||
|
||||
Let's add another animation to our application. On our `phone-detail.template.html` view, we have a
|
||||
nice thumbnail swapper. By clicking on the thumbnails listed on the page, the profile phone image
|
||||
changes. But how can we incorporate animations?
|
||||
|
||||
Let's give it some thought first. Basically, when a user clicks on a thumbnail image, they are
|
||||
changing the state of the profile image to reflect the newly selected thumbnail image. The best way
|
||||
to specify state changes within HTML is to use classes. Much like before — when we used a CSS
|
||||
class to drive the animation — this time the animation will occur when the CSS class itself
|
||||
changes.
|
||||
|
||||
Every time a phone thumbnail is selected, the state changes and the `.selected` CSS class is added
|
||||
to the matching profile image. This will trigger the animation.
|
||||
|
||||
We will start by tweaking our HTML code in `phone-detail.template.html`. Notice that we have changed
|
||||
the way we display our large image:
|
||||
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.template.html`:**
|
||||
|
||||
```html
|
||||
<div class="phone-images">
|
||||
<img ng-src="{{img}}" class="phone"
|
||||
ng-class="{selected: img === $ctrl.mainImageUrl}"
|
||||
ng-repeat="img in $ctrl.phone.images" />
|
||||
</div>
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Just like with the thumbnails, we are using a repeater to display **all** the profile images as a
|
||||
list, however we're not animating any repeat-related transitions. Instead, we will be keeping our
|
||||
eye on each element's classes and especially the `selected` class, since its presence or absence
|
||||
will determine if the element is visible or hidden. The addition/removal of the `selected` class is
|
||||
managed by the {@link ngClass ngClass} directive, based on the specified condition
|
||||
(`img === $ctrl.mainImageUrl`).
|
||||
In our case, there is always exactly one element that has the `selected` class, and therefore there
|
||||
will be exactly one phone profile image visible on the screen at all times.
|
||||
|
||||
When the `selected` class is added to an element, the `selected-add` and `selected-add-active`
|
||||
classes are added just before to signal AngularJS to fire off an animation. When the `selected`
|
||||
class is removed from an element, the `selected-remove` and `selected-remove-active` classes are
|
||||
applied to the element, triggering another animation.
|
||||
|
||||
Finally, in order to ensure that the phone images are displayed correctly when the page is first
|
||||
loaded, we also tweak the detail page CSS styles:
|
||||
|
||||
<br />
|
||||
**`app/app.css`:**
|
||||
|
||||
```css
|
||||
...
|
||||
|
||||
.phone {
|
||||
background-color: white;
|
||||
display: none;
|
||||
float: left;
|
||||
height: 400px;
|
||||
margin-bottom: 2em;
|
||||
margin-right: 3em;
|
||||
padding: 2em;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.phone:first-child {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.phone-images {
|
||||
background-color: white;
|
||||
float: left;
|
||||
height: 450px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 450px;
|
||||
}
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
You may be thinking that we are just going to create another CSS-based animation. Although we could
|
||||
do that, let's take the opportunity to learn how to create JavaScript-based animations with the
|
||||
{@link ng.angular.Module#animation .animation()} module method.
|
||||
|
||||
<br />
|
||||
**`app/app.animations.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phonecatApp').
|
||||
animation('.phone', function phoneAnimationFactory() {
|
||||
return {
|
||||
addClass: animateIn,
|
||||
removeClass: animateOut
|
||||
};
|
||||
|
||||
function animateIn(element, className, done) {
|
||||
if (className !== 'selected') return;
|
||||
|
||||
element.
|
||||
css({
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
top: 500,
|
||||
left: 0
|
||||
}).
|
||||
animate({
|
||||
top: 0
|
||||
}, done);
|
||||
|
||||
return function animateInEnd(wasCanceled) {
|
||||
if (wasCanceled) element.stop();
|
||||
};
|
||||
}
|
||||
|
||||
function animateOut(element, className, done) {
|
||||
if (className !== 'selected') return;
|
||||
|
||||
element.
|
||||
css({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0
|
||||
}).
|
||||
animate({
|
||||
top: -500
|
||||
}, done);
|
||||
|
||||
return function animateOutEnd(wasCanceled) {
|
||||
if (wasCanceled) element.stop();
|
||||
};
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
We are creating a custom animation by specifying the target elements via a CSS class selector (here
|
||||
`.phone`) and an animation _factory_ function (here `phoneAnimationFactory()`). The factory function
|
||||
returns an object associating specific _events_ (object keys) to animation _callbacks_ (object
|
||||
values). The _events_ correspond to DOM actions that `ngAnimate` recognizes and can hook into, such
|
||||
as `addClass`/`removeClass`/`setClass`, `enter`/`move`/`leave` and `animate`. The associated
|
||||
callbacks are called by `ngAnimate` at appropriate times.
|
||||
|
||||
For more info on animation factories, check out the
|
||||
{@link ng.$animateProvider#register API Reference}.
|
||||
|
||||
In this case, we are interested in a class getting added to/removed from a `.phone` element, thus we
|
||||
specify callbacks for the `addClass` and `removeClass` events. When the `selected` class is added to
|
||||
an element (via the `ngClass` directive), the `addClass` JavaScript callback will be executed with
|
||||
`element` passed in as a parameter. The last parameter passed in is the `done` callback function. We
|
||||
call `done()` to let Angular know that our custom JavaScript animation has ended. The `removeClass`
|
||||
callback works the same way, but instead gets executed when a class is removed.
|
||||
|
||||
Note that we are using [jQuery][jquery]'s `animate()` helper to implement the animation. jQuery
|
||||
isn't required to do JavaScript animations with AngularJS, but we use it here anyway in order to
|
||||
keep the example simple. More info on `jQuery.animate()` can be found in the
|
||||
[jQuery documentation][jquery-animate].
|
||||
|
||||
Within the event callbacks, we create the animation by manipulating the DOM. In the code above,
|
||||
this is achieved using `element.css()` and `element.animate()`. As a result the new element is
|
||||
positioned with an offset of **500px** and then both elements — the previous and the new
|
||||
— are animated together by shifting each one up by **500px**. The outcome is a conveyor-belt
|
||||
like animation. After the `animate()` function has completed the animation, it calls `done` to
|
||||
notify Angular.
|
||||
|
||||
You may have noticed that each animation callback returns a function. This is an **optional**
|
||||
function, which (if provided) will be called when the animation ends, either because it ran to
|
||||
completion or because it was canceled (for example another animation took place on the same
|
||||
element). A boolean parameter (`wasCanceled`) is passed to the function, letting the developer know
|
||||
if the animation was canceled or not. Use this function to do any necessary clean-up.
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
<div></div>
|
||||
|
||||
* Reverse the animation, so that the elements animate downwards.
|
||||
|
||||
* Make the animation run faster or slower, by passing a `duration` argument to `.animate()`:
|
||||
|
||||
```js
|
||||
element.css({...}).animate({...}, 1000 /* 1 second */, done);
|
||||
```
|
||||
|
||||
* Make the animations "asymmetrical". For example, have the previous element fade out, while the new
|
||||
element zooms in:
|
||||
|
||||
```js
|
||||
// animateIn()
|
||||
element.css({
|
||||
display: 'block',
|
||||
opacity: 1,
|
||||
position: 'absolute',
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 200,
|
||||
left: 200
|
||||
}).animate({
|
||||
width: 400,
|
||||
height: 400,
|
||||
top: 0,
|
||||
left: 0
|
||||
}, done);
|
||||
|
||||
// animateOut()
|
||||
element.animate({
|
||||
opacity: 0
|
||||
}, done);
|
||||
```
|
||||
|
||||
* Go crazy and come up with your own funky animations!
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Our application is now much more pleasant to use, thanks to the smooth transitions between pages
|
||||
and UI states.
|
||||
|
||||
There you have it! We have created a web application in a relatively short amount of time. In the
|
||||
{@link the_end closing notes} we will cover where to go from here.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="14"></ul>
|
||||
|
||||
|
||||
[bower]: http://bower.io/
|
||||
[caniuse-css-animation]: http://caniuse.com/#feat=css-animation
|
||||
[caniuse-css-transitions]: http://caniuse.com/#feat=css-transitions
|
||||
[jquery]: https://jquery.com/
|
||||
[jquery-animate]: https://api.jquery.com/animate/
|
||||
[webplatform-animations]: https://docs.webplatform.org/wiki/css/properties/animations
|
||||
@@ -9,11 +9,18 @@ previous steps using the `git checkout` command.
|
||||
For more details and examples of the Angular concepts we touched on in this tutorial, see the
|
||||
{@link guide/ Developer Guide}.
|
||||
|
||||
When you are ready to start developing a project using Angular, we recommend that you bootstrap
|
||||
your development with the [angular-seed](https://github.com/angular/angular-seed) project.
|
||||
When you are ready to start developing a project using AngularJS, we recommend that you bootstrap
|
||||
your development with the [angular-seed project][angular-seed].
|
||||
|
||||
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.
|
||||
We hope this tutorial was useful to you and that you learned enough about AngularJS to make you want
|
||||
to learn more. We especially hope you are inspired to go out and develop Angular web applications of
|
||||
your own, and that you might be interested in {@link misc/contribute contributing} to AngularJS.
|
||||
|
||||
If you have questions or feedback or just want to say "hi", please post a message at (https://groups.google.com/forum/#!forum/angular).
|
||||
If you have questions or feedback or just want to say "hi", please post a message to the
|
||||
[mailing list][mailing-list]. You can also find us on [IRC][irc] or [Gitter][gitter].
|
||||
|
||||
|
||||
[angular-seed]: https://github.com/angular/angular-seed
|
||||
[gitter]: https://gitter.im/angular/angular.js
|
||||
[irc]: http://webchat.freenode.net/?channels=angularjs&uio=d4
|
||||
[mailing-list]: https://groups.google.com/forum/#!forum/angular
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"use strict";
|
||||
|
||||
var fs = require('fs');
|
||||
var _ = require('lodash');
|
||||
var stripJsonComments = require('strip-json-comments');
|
||||
|
||||
var gulp = require('gulp');
|
||||
var log = require('gulp-util').log;
|
||||
var concat = require('gulp-concat');
|
||||
@@ -25,6 +29,23 @@ var ignoredFiles = '!src/angular.bind.js';
|
||||
var assets = 'app/assets/**/*';
|
||||
|
||||
|
||||
var getJshintConfig = function(filepath) {
|
||||
return JSON.parse(stripJsonComments(fs.readFileSync(filepath, {encoding: 'utf-8'})));
|
||||
};
|
||||
|
||||
var getMergedJshintConfig = function(filepath) {
|
||||
// "extends" doesn't work in configuration passed by an object, we need to do the extending ourselves.
|
||||
var config = getJshintConfig(filepath);
|
||||
var baseConfig = getJshintConfig('../.jshintrc-base');
|
||||
_.merge(config, baseConfig);
|
||||
delete config.extends;
|
||||
|
||||
// Examples don't run in strict mode; accept that for now.
|
||||
config.strict = false;
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
var copyComponent = function(component, pattern, sourceFolder, packageFile) {
|
||||
pattern = pattern || '/**/*';
|
||||
sourceFolder = sourceFolder || bowerFolder;
|
||||
@@ -90,17 +111,35 @@ gulp.task('assets', ['bower'], function() {
|
||||
|
||||
gulp.task('doc-gen', ['bower'], function() {
|
||||
var dgeni = new Dgeni([require('./config')]);
|
||||
return dgeni.generate().catch(function(error) {
|
||||
return dgeni.generate().catch(function() {
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
||||
// JSHint the example and protractor test files
|
||||
gulp.task('jshint', ['doc-gen'], function() {
|
||||
gulp.src([outputFolder + '/ptore2e/**/*.js', outputFolder + '/examples/**/*.js'])
|
||||
.pipe(jshint())
|
||||
.pipe(jshint.reporter('jshint-stylish'))
|
||||
.pipe(jshint.reporter('fail'));
|
||||
var examplesConfig = getMergedJshintConfig('../docs/app/test/.jshintrc');
|
||||
// Some tests use `alert` which is not assumed to be available even with `"browser": true`.
|
||||
examplesConfig.globals.alert = false;
|
||||
|
||||
var protractorConfig = getMergedJshintConfig('../docs/app/e2e/.jshintrc');
|
||||
|
||||
return merge(
|
||||
gulp.src([
|
||||
outputFolder + '/examples/**/*.js',
|
||||
'!' + outputFolder + '/examples/**/protractor.js',
|
||||
])
|
||||
.pipe(jshint(examplesConfig))
|
||||
.pipe(jshint.reporter('jshint-stylish'))
|
||||
.pipe(jshint.reporter('fail')),
|
||||
gulp.src([
|
||||
outputFolder + '/ptore2e/**/*.js',
|
||||
outputFolder + '/examples/**/protractor.js',
|
||||
])
|
||||
.pipe(jshint(protractorConfig))
|
||||
.pipe(jshint.reporter('jshint-stylish'))
|
||||
.pipe(jshint.reporter('fail'))
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 205 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 141 KiB |
@@ -9,4 +9,8 @@ npm run test-i18n
|
||||
|
||||
node src/closureSlurper.js
|
||||
|
||||
npm run test-i18n-ucd
|
||||
|
||||
echo "Generating ngParseExt"
|
||||
node ucd/src/extract.js
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
var extractValues = require('../src/extractValues.js').extractValues;
|
||||
var stream = require('stream');
|
||||
|
||||
function StringStream(str) {
|
||||
return new stream.Readable({
|
||||
read: function(n) {
|
||||
this.push(str);
|
||||
str = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe('extractValues', function() {
|
||||
it('should extract the values from the xml', function(done) {
|
||||
var str = '<ucd><repertoire><char cp="0000" IDS="N"></char><char cp="0001" IDS="Y"></char><char cp="0002" IDS="Y"></char><char cp="0003" IDS="N"></char></repertoire></ucd>';
|
||||
extractValues(StringStream(str), {'IDS': 'Y'}, function(values) {
|
||||
expect(values).toEqual({ IDS_Y : [ [ '0001', '0002' ] ] });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract the values from the xml if the last element matches', function(done) {
|
||||
var str = '<ucd><repertoire><char cp="0000" IDS="N"></char><char cp="0001" IDS="Y"></char><char cp="0002" IDS="Y"></char><char cp="0003" IDS="Y"></char></repertoire></ucd>';
|
||||
extractValues(StringStream(str), {'IDS': 'Y'}, function(values) {
|
||||
expect(values).toEqual({ IDS_Y : [ [ '0001', '0003' ] ] });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support `reserved`', function(done) {
|
||||
var str = '<ucd><repertoire><char cp="0000" IDS="N"></char><char cp="0001" IDS="Y"></char><reserved first-cp="0002" last-cp="0005" IDS="N"></reserved><char cp="0006" IDS="Y"></char></repertoire></ucd>';
|
||||
extractValues(StringStream(str), {'IDS': 'Y'}, function(values) {
|
||||
expect(values).toEqual({ IDS_Y : [ [ '0001', '0001' ], [ '0006', '0006' ] ] });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support `surrogate`', function(done) {
|
||||
var str = '<ucd><repertoire><char cp="0000" IDS="N"></char><char cp="0001" IDS="Y"></char><surrogate first-cp="0002" last-cp="0005" IDS="N"></surrogate><char cp="0006" IDS="Y"></char></repertoire></ucd>';
|
||||
extractValues(StringStream(str), {'IDS': 'Y'}, function(values) {
|
||||
expect(values).toEqual({ IDS_Y : [ [ '0001', '0001' ], [ '0006', '0006' ] ] });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support `noncharactere`', function(done) {
|
||||
var str = '<ucd><repertoire><char cp="0000" IDS="N"></char><char cp="0001" IDS="Y"></char><noncharacter first-cp="0002" last-cp="0005" IDS="N"></noncharacter><char cp="0006" IDS="Y"></char></repertoire></ucd>';
|
||||
extractValues(StringStream(str), {'IDS': 'Y'}, function(values) {
|
||||
expect(values).toEqual({ IDS_Y : [ [ '0001', '0001' ], [ '0006', '0006' ] ] });
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
var generateCodeModule = require('../src/generateCode.js');
|
||||
var generateCode = generateCodeModule.generateCode;
|
||||
var generateFunction = generateCodeModule.generateFunction;
|
||||
|
||||
describe('generateFunction', function() {
|
||||
it('should generate function with ranges', function() {
|
||||
expect(generateFunction([ [ '0001', '0003' ] ], 'IDS_Y')).toEqual('\
|
||||
function IDS_Y(cp) {\n\
|
||||
if (0x0001 <= cp && cp <= 0x0003) return true;\n\
|
||||
return false;\n\
|
||||
}\n');
|
||||
});
|
||||
|
||||
it('should generate function with multiple ranges', function() {
|
||||
expect(generateFunction([ [ '0001', '0003' ], [ '0005', '0009'] ], 'IDS_Y')).toEqual('\
|
||||
function IDS_Y(cp) {\n\
|
||||
if (0x0001 <= cp && cp <= 0x0003) return true;\n\
|
||||
if (0x0005 <= cp && cp <= 0x0009) return true;\n\
|
||||
return false;\n\
|
||||
}\n');
|
||||
});
|
||||
|
||||
it('should generate function with unique values', function() {
|
||||
expect(generateFunction([ [ '0001', '0001' ], [ '0005', '0009'] ], 'IDS_Y')).toEqual('\
|
||||
function IDS_Y(cp) {\n\
|
||||
if (cp === 0x0001) return true;\n\
|
||||
if (0x0005 <= cp && cp <= 0x0009) return true;\n\
|
||||
return false;\n\
|
||||
}\n');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateCode', function() {
|
||||
it('should generate the function for all the values', function() {
|
||||
expect(generateCode({ IDS_Y : [ [ '0001', '0001' ], [ '0006', '0006' ] ], IDC_Y : [ [ '0002', '0002' ], [ '0007', '0007' ] ] })).toEqual('\
|
||||
/******************************************************\n\
|
||||
* Generated file, do not modify *\n\
|
||||
* *\n\
|
||||
*****************************************************/\n\
|
||||
"use strict";\n\
|
||||
function IDS_Y(cp) {\n\
|
||||
if (cp === 0x0001) return true;\n\
|
||||
if (cp === 0x0006) return true;\n\
|
||||
return false;\n\
|
||||
}\n\
|
||||
function IDC_Y(cp) {\n\
|
||||
if (cp === 0x0002) return true;\n\
|
||||
if (cp === 0x0007) return true;\n\
|
||||
return false;\n\
|
||||
}\n\
|
||||
');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
"use strict";
|
||||
|
||||
var fs = require('fs');
|
||||
var zlib = require('zlib');
|
||||
var extractValues = require('./extractValues').extractValues;
|
||||
var generateCode = require('./generateCode').generateCode;
|
||||
var generateextractValues = require('./extractValues').extractValues;
|
||||
// ID_Start and ID_Continue
|
||||
var propertiesToExtract = {'IDS': 'Y', 'IDC': 'Y'};
|
||||
|
||||
function main() {
|
||||
extractValues(
|
||||
fs.createReadStream('./ucd/src/ucd.all.flat.xml.gz').pipe(zlib.createGunzip()),
|
||||
propertiesToExtract,
|
||||
writeFile);
|
||||
|
||||
function writeFile(validRanges) {
|
||||
var code = generateCode(validRanges);
|
||||
try {
|
||||
var stats = fs.lstatSync('../src/ngParseExt');
|
||||
} catch (e) {
|
||||
fs.mkdirSync('../src/ngParseExt');
|
||||
}
|
||||
fs.writeFile('../src/ngParseExt/ucd.js', code);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Extract values from a stream.
|
||||
*/
|
||||
|
||||
exports.extractValues = extractValues;
|
||||
|
||||
var sax = require('sax/lib/sax');
|
||||
var saxStrict = true;
|
||||
var saxOptions = {};
|
||||
var validXMLTagNames = { char: 'Y', reserved: 'Y', surrogate: 'Y', noncharacter: 'Y'};
|
||||
|
||||
function extractValues(stream, propertiesToExtract, callback) {
|
||||
var saxStream = sax.createStream(saxStrict, saxOptions);
|
||||
var firstValid = {};
|
||||
var lastValid = {};
|
||||
var keys = Object.keys(propertiesToExtract);
|
||||
var keyValues = keys.map(function(k) { return propertiesToExtract[k]; });
|
||||
var validRanges = {};
|
||||
|
||||
for (var i in keys) {
|
||||
validRanges[keys[i] + '_' + keyValues[i]] = [];
|
||||
}
|
||||
saxStream.onopentag = onOpenTag;
|
||||
stream
|
||||
.pipe(saxStream)
|
||||
.on('end', doCallback);
|
||||
|
||||
function onOpenTag(node) {
|
||||
var property;
|
||||
if (validXMLTagNames[node.name]) {
|
||||
for (var i in keys) {
|
||||
property = keyValues[i];
|
||||
if (node.attributes[keys[i]] === property) validProperty(keys[i] + '_' + property, node);
|
||||
else invalidProperty(keys[i] + '_' + property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validProperty(property, node) {
|
||||
if (!firstValid[property]) firstValid[property] =
|
||||
node.attributes.cp || node.attributes['first-cp'];
|
||||
lastValid[property] = node.attributes.cp || node.attributes['last-cp'];
|
||||
}
|
||||
|
||||
function invalidProperty(property) {
|
||||
if (!firstValid[property]) return;
|
||||
validRanges[property].push([firstValid[property], lastValid[property]]);
|
||||
firstValid[property] = null;
|
||||
}
|
||||
|
||||
function doCallback() {
|
||||
for (var i in keys) {
|
||||
property = keys[i] + '_' + keyValues[i];
|
||||
invalidProperty(property);
|
||||
}
|
||||
callback(validRanges);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
exports.generateCode = generateCode;
|
||||
exports.generateFunction = generateFunction;
|
||||
|
||||
function generateCode(validRanges) {
|
||||
var code = '/******************************************************\n' +
|
||||
' * Generated file, do not modify *\n' +
|
||||
' * *\n' +
|
||||
' *****************************************************/\n' +
|
||||
'"use strict";\n';
|
||||
var keys = Object.keys(validRanges);
|
||||
for (var i in keys) {
|
||||
code += generateFunction(validRanges[keys[i]], keys[i]);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
|
||||
function generateFunction(positiveElements, functionName) {
|
||||
var result = [];
|
||||
result.push('function ', functionName, '(cp) {\n');
|
||||
positiveElements.forEach(function(range) {
|
||||
if (range[0] === range[1]) {
|
||||
result.push(' if (cp === 0x', range[0], ')');
|
||||
} else {
|
||||
result.push(' if (0x', range[0], ' <= cp && cp <= 0x', range[1], ')');
|
||||
}
|
||||
result.push(' return true;\n');
|
||||
});
|
||||
result.push(' return false;\n}\n');
|
||||
return result.join('');
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 576 733" width="48pc" height="733pt" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata> Produced by OmniGraffle 6.5.2 <dc:date>2016-04-12 13:18:31 +0000</dc:date></metadata><defs><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Notes</title><rect fill="white" width="576" height="733"/><g><title>Layer 1</title><text transform="translate(57 78)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="bold" x="0" y="11" textLength="106.01367">Guiding Principles</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="0" y="25" textLength="126.73828">* Convention over Confi</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="126.73828" y="25" textLength="43.365234">guration</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="0" y="39" textLength="158.73047">* Declarative / Self Describing</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="0" y="53" textLength="8.0039062">* </tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="7.7929688" y="53" textLength="7.330078">T</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="13.792969" y="53" textLength="38.695312">estable</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="0" y="67" textLength="25.335938">* DR</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="25.125" y="67" textLength="8.0039062">Y</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="32.917969" y="67" textLength="80.33203"> (Don't Repeat </tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="113.03906" y="67" textLength="8.0039062">Y</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="119.941406" y="67" textLength="40.013672">ourself)</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="0" y="81" textLength="171.38086">* CRUD ~ 80% -> make it trivial </tspan></text></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 12 KiB |