Compare commits
510 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00456a8f93 | |||
| 0b3022e8e0 | |||
| 456026eff1 | |||
| 8ec3efd967 | |||
| 8528781f85 | |||
| 98f603722d | |||
| ebece0bcb9 | |||
| 4b7398eeca | |||
| 4e79decc30 | |||
| 888b0f5400 | |||
| aaf9c5e598 | |||
| 893d2f8000 | |||
| effc98fdc9 | |||
| 64cdbf3ae9 | |||
| 5bba892ffd | |||
| ec27deacfd | |||
| c6e4defcb6 | |||
| 74a7afcb31 | |||
| 08cc6edd38 | |||
| 4f3870500d | |||
| cd0507bc3a | |||
| 20eef05195 | |||
| a6d7b4bdbd | |||
| 5811c5c35b | |||
| e37e30e93f | |||
| a0b8ab8d7d | |||
| 129c53730c | |||
| 8695138aec | |||
| ed56872bb2 | |||
| cb183433a0 | |||
| 631fbda6a9 | |||
| 4c33b56612 | |||
| 93b0c2d892 | |||
| d262378b7c | |||
| 7729c84ec7 | |||
| dffeef29d7 | |||
| 2e22588ccf | |||
| 0b0acb0342 | |||
| 18fc43e828 | |||
| ed47d811e2 | |||
| a84f9f6178 | |||
| 4fbbe1152e | |||
| df3d941c57 | |||
| 2bd3214a55 | |||
| fc2abef327 | |||
| 76a0eb89fb | |||
| ee57b4c26b | |||
| 29eaabc000 | |||
| 2a6081057f | |||
| 79538afd7b | |||
| bcfa64e77c | |||
| b7a2deed30 | |||
| 98ff901bda | |||
| 428b81cba9 | |||
| 5dae9c230e | |||
| c3fad1157e | |||
| 9242c580a1 | |||
| c2860944c6 | |||
| e9a00fbdc3 | |||
| d018ac2a9a | |||
| b770c353bc | |||
| 812277c257 | |||
| 61871da9de | |||
| 9ee075518f | |||
| afe93eaff8 | |||
| d8f94d1a3f | |||
| f53b53df22 | |||
| eab5731afc | |||
| 986c446aaf | |||
| 60366c8d0b | |||
| 494c8aa0b3 | |||
| cd9459e129 | |||
| 2862883bd8 | |||
| bbb673a48a | |||
| cd9afd9961 | |||
| e25ed0d48d | |||
| cd6d21e78a | |||
| 9cf5b35e5c | |||
| 5d11e02008 | |||
| 209e600070 | |||
| ceaca57786 | |||
| cbdf0c2afb | |||
| 1111076552 | |||
| 948c86c602 | |||
| cd63ff497d | |||
| 9d6240561b | |||
| 05b5245790 | |||
| e6ebfc87c9 | |||
| 7f2bcc3933 | |||
| 492b0cdf28 | |||
| 51863f80d7 | |||
| 9d4000d689 | |||
| bc036c68ac | |||
| 580135a9cb | |||
| da0c5efa72 | |||
| c280d5ab02 | |||
| 28c8199cd6 | |||
| 0252a9889b | |||
| 1c15cdc2d0 | |||
| 0bd329d4fd | |||
| 8db84a16db | |||
| cffcfc73a0 | |||
| 25eb9b794f | |||
| 6941779543 | |||
| 65f40d2123 | |||
| e159f9626c | |||
| 685a9d040d | |||
| 8eede099cd | |||
| a17d42d706 | |||
| 60af504c18 | |||
| 243d9ac72c | |||
| cc8eb91665 | |||
| 8697c3bf4c | |||
| 3c46c94342 | |||
| 71eb1901f6 | |||
| 4e57e28589 | |||
| 13289c0903 | |||
| afdb4f1b76 | |||
| e7999e7447 | |||
| d175bb0131 | |||
| fe1188daf3 | |||
| e1a3a6251f | |||
| 25d3d3730d | |||
| af5aacce05 | |||
| 3abd0fb93c | |||
| 36d2658b94 | |||
| 6ce5a04d42 | |||
| 929064d9e4 | |||
| d0b873a4b7 | |||
| 2e10659472 | |||
| 2a8493f40c | |||
| 2f960f1530 | |||
| 3f5f20fe77 | |||
| 01387c0a8f | |||
| c0afbfaca5 | |||
| bf13d2683d | |||
| fe01a85a8e | |||
| 6b9a332959 | |||
| f1b28847c8 | |||
| 1b779028fd | |||
| 49b2a1c8cf | |||
| 3e82492fc6 | |||
| 856be44628 | |||
| dbb21b1531 | |||
| 3ce56b739c | |||
| ecbb374826 | |||
| 0e5d31908e | |||
| f1b0d21f97 | |||
| 58e94dcde9 | |||
| eba192b863 | |||
| 75099e6137 | |||
| 172a40931b | |||
| 23e5109b64 | |||
| cc84ce3bf5 | |||
| bee2d1fbb9 | |||
| 7101a02c93 | |||
| 888be00712 | |||
| 05597c24c7 | |||
| bf55f23325 | |||
| ab051e78ea | |||
| 87d46a84c3 | |||
| ae764f1844 | |||
| ad8092ed80 | |||
| 55e1b3e1c8 | |||
| 6f465a2b26 | |||
| 3ecac62251 | |||
| c64610269d | |||
| 5c2302949b | |||
| 7c84e2632f | |||
| 18d18f07db | |||
| 9f5be534fc | |||
| 1cd7912614 | |||
| 514da451fc | |||
| 2788cc4e12 | |||
| 7f6322df6a | |||
| 5bd8613168 | |||
| 32b507890e | |||
| bfedafdede | |||
| cdefbe3425 | |||
| f75f4bce82 | |||
| 4349de3d41 | |||
| df545d7eed | |||
| 3b5f346314 | |||
| 3aab87b381 | |||
| 0973175058 | |||
| 112da45c07 | |||
| e3dc85841d | |||
| ef1c352bc9 | |||
| a5b6444324 | |||
| dd18c00b1d | |||
| a0fad24dc2 | |||
| da0e3c99f5 | |||
| a41c58e285 | |||
| bce5b49133 | |||
| 816b84230c | |||
| 873acf8fab | |||
| 9063a0c2e7 | |||
| 03cbc0d6b1 | |||
| 931789ec14 | |||
| 9bc807783f | |||
| 2d6ee651b1 | |||
| 7ca24a8264 | |||
| 1d8e42070a | |||
| fe6b2fbfc4 | |||
| 5a222244fb | |||
| 4026074aba | |||
| d9a596addd | |||
| 8c0898b21c | |||
| d9b693bb7a | |||
| da94ab2e63 | |||
| 2fd8dc7061 | |||
| 01012a4d7a | |||
| ce84429adf | |||
| a26acb64fe | |||
| 7088ed1ed3 | |||
| 7027844d42 | |||
| 16c584ed7f | |||
| b586bfdfab | |||
| 1d69015e3d | |||
| f13c33bf10 | |||
| ba62e975f1 | |||
| b89d941cdf | |||
| 07fa87a8a8 | |||
| 0af70eb99e | |||
| cb713e6045 | |||
| 89ca859734 | |||
| bc6fb7cc94 | |||
| 0c80df21b6 | |||
| 3141dbf179 | |||
| aad502bad8 | |||
| dca8972367 | |||
| 284de57435 | |||
| f780ccfa1c | |||
| 55f99e0710 | |||
| 1a99ca9c08 | |||
| 37500fca83 | |||
| 4fe4fc5abf | |||
| 74e1cc683b | |||
| a4faa5cde7 | |||
| 32cb40b86d | |||
| d1cd677433 | |||
| 43c735a816 | |||
| ab2e83c8c8 | |||
| e5f454c8af | |||
| 67c11b9a39 | |||
| 5a306b7ba3 | |||
| 8ce61bf178 | |||
| 9d452bc845 | |||
| 192fecc790 | |||
| 32be6369e4 | |||
| 2a45cea0ba | |||
| ea653e4cdd | |||
| 03777445e8 | |||
| 8b25ea129a | |||
| d71f16e745 | |||
| ed59370d80 | |||
| d8e5acfe27 | |||
| af59f4e69a | |||
| 24aee81634 | |||
| f81d56e66c | |||
| f0904cf12e | |||
| 81b7e5ab0e | |||
| 1b1890274e | |||
| 6d418ef5e3 | |||
| 3fa1606c43 | |||
| 8661a9e3d4 | |||
| cf63292742 | |||
| fd420c4061 | |||
| 1382d4e88e | |||
| b9ddef2a49 | |||
| eafba9e2e5 | |||
| 6f1d9f8ca6 | |||
| bb9310974b | |||
| 30279d7b9b | |||
| 8df5f3259a | |||
| 14e797c1a1 | |||
| 82cd6b87f0 | |||
| 6d7cc572b5 | |||
| 2ebbe00eb5 | |||
| 8b86d363aa | |||
| 9b51067516 | |||
| a3208bf66e | |||
| 4e1fb82628 | |||
| ad466412c6 | |||
| 299a32740c | |||
| eb799bcb71 | |||
| 7314c1b69e | |||
| fcfe2b3793 | |||
| 5e140a99c7 | |||
| 4da169d15d | |||
| f37c6f9f73 | |||
| eae658fd96 | |||
| 286f269753 | |||
| 73640a6b7c | |||
| 2dc55ff5c2 | |||
| 98a2563ec4 | |||
| 8c02122837 | |||
| 1e069532fc | |||
| 6f6f7e82a4 | |||
| d852122442 | |||
| 66cb161221 | |||
| 6c14fb1eb6 | |||
| e906aafb0a | |||
| 6acc73f3e0 | |||
| a4367ab00d | |||
| 2e0464fba4 | |||
| 6ffd53ee3c | |||
| 2395bf604d | |||
| 2e5fe846e3 | |||
| b6388b3f1d | |||
| 669e3aeaa8 | |||
| 55b2f0e862 | |||
| ca566d8d81 | |||
| b306babe29 | |||
| d9317cde4f | |||
| 23c8af232f | |||
| 2fcbd39d0b | |||
| 3ffbf202ce | |||
| 09367d88c2 | |||
| 369f69d67a | |||
| 9f43d02af8 | |||
| 08e6b88fb2 | |||
| 9227a5db94 | |||
| fc6ce59cd2 | |||
| 96a314766c | |||
| 6aa31e17ee | |||
| d18d5f57c2 | |||
| 2d9e96772f | |||
| bc2a5aaf05 | |||
| 4547c11dad | |||
| c0b360b993 | |||
| a659049893 | |||
| a3b9b1d205 | |||
| 3305f38db2 | |||
| 9be4e035d1 | |||
| 85ce5d0db9 | |||
| 5c99720934 | |||
| 8d26238664 | |||
| b7cb454546 | |||
| 199825ec26 | |||
| bcdd925c9d | |||
| 0bcace309e | |||
| 46c9c942df | |||
| 2ad439dfc5 | |||
| 3fbfe3f966 | |||
| 8ff671753c | |||
| 24092127d1 | |||
| f1c7240f04 | |||
| fcee3bea1a | |||
| ad08638c0a | |||
| 093e76fa15 | |||
| 28c0497524 | |||
| 88505d8029 | |||
| c241a57761 | |||
| b0e985fb67 | |||
| a0dbd95bb9 | |||
| 9fd5450ee8 | |||
| b5391fae8f | |||
| b635903ec4 | |||
| c9ee20b64b | |||
| 25ae98ca77 | |||
| adb5ee2e0a | |||
| 78954ffcde | |||
| 3b30a4b64a | |||
| 866057233c | |||
| ec1f4a8c9b | |||
| c94190139d | |||
| 4896a0b4d4 | |||
| 32bd990eda | |||
| 23723298f9 | |||
| edab80cddb | |||
| f4bb006e45 | |||
| cf3f709889 | |||
| e9ecd56dca | |||
| f107ef8bd8 | |||
| e7eab501db | |||
| 97fc47f39e | |||
| 6d1c67727a | |||
| 8ba78f08b9 | |||
| 02a3c31c23 | |||
| d4c3d5caaf | |||
| 279f98c4e3 | |||
| 5e548edf67 | |||
| acaf9be685 | |||
| bdd75c97ad | |||
| a3f1cba8ec | |||
| 4195b04072 | |||
| ccc8ec869b | |||
| 6d8abaedd8 | |||
| fd49d6634c | |||
| cecd5214df | |||
| c5e39c688b | |||
| 240608447a | |||
| bcb53deda8 | |||
| 9d4fa33e35 | |||
| de1461da78 | |||
| 2866daf7d6 | |||
| b3de37e418 | |||
| a4cc9e1944 | |||
| f8a1c56cad | |||
| 113850602d | |||
| e76105a320 | |||
| 14e9be202a | |||
| dd5215eceb | |||
| 497ba08775 | |||
| eaaf4967f9 | |||
| f440ac7492 | |||
| d566c4bc61 | |||
| 766b962eac | |||
| 0388eed7e5 | |||
| 82448b86b5 | |||
| fafcd6285a | |||
| 5319621afd | |||
| 32aa491588 | |||
| 31bdb60f0a | |||
| a8aae48bc0 | |||
| 517917f9fa | |||
| 1748abe8ef | |||
| 5f5bf07bb8 | |||
| 3d0b49c07f | |||
| b6aec5642e | |||
| e44e5f447b | |||
| ca273fd9da | |||
| 5ff453d422 | |||
| d9c75bee93 | |||
| e030e64196 | |||
| 7ba19cc355 | |||
| 21428e5cea | |||
| 9321a5f14c | |||
| 72421b2acf | |||
| c3fe170b8b | |||
| ed18b8c9da | |||
| 0cb276f7ac | |||
| cfccb8f64a | |||
| 0069f87007 | |||
| 9599234bae | |||
| d423117158 | |||
| 3c8a940686 | |||
| 9f8e30f550 | |||
| dcd94a23e1 | |||
| 9b79a00294 | |||
| 02058bfbe2 | |||
| 76647d3855 | |||
| 1f1cad8517 | |||
| e1f1d65d0c | |||
| a5df2d4e36 | |||
| b6514d9e1a | |||
| 1b1f94d8fe | |||
| 1362a9b456 | |||
| 6a26b2c38c | |||
| 9ab9bf6b41 | |||
| e7e56fe9bf | |||
| 85ea376da2 | |||
| 050aae3ceb | |||
| 79d9fd9d57 | |||
| 2dc2265e4f | |||
| 9cd33df50c | |||
| 1db20ce90f | |||
| 76cb5ce7c5 | |||
| 2a778d0038 | |||
| b04d3a8ec6 | |||
| 7839330b40 | |||
| 2d7cb14a16 | |||
| f20b06d26d | |||
| 109e5d1d39 | |||
| 227822dac3 | |||
| 92f6b45e02 | |||
| 24f7999bc1 | |||
| e0203660d3 | |||
| b6eb5fdb05 | |||
| 5bf6e50b40 | |||
| a7ccb7531c | |||
| 6bea059109 | |||
| fcdac65aed | |||
| 373078a94c | |||
| db07ad2d4c | |||
| 19d7a127c7 | |||
| f5a04f59cf | |||
| ab92da43b0 | |||
| 6782c45ddc | |||
| c55477fb2b | |||
| 2e2d62ca12 | |||
| e3141fe5f4 | |||
| 1ebed26678 | |||
| e987efd4c0 | |||
| ea72e5f881 | |||
| c550c12738 | |||
| 651caffe45 | |||
| 45855b8ba2 | |||
| feb54d68d2 | |||
| 2d0f6037f7 | |||
| ebd9a2a960 | |||
| fcfa6ebb6b | |||
| cf8ed01c6e | |||
| c352b92c40 | |||
| 369145467c | |||
| c3ec6ea226 | |||
| 291684c29c | |||
| d7615351df | |||
| a97a172ee9 | |||
| d53a787f0d | |||
| 9682bd0c4e | |||
| 34d0740350 | |||
| 4d9efa2f76 | |||
| f9a7b064a0 | |||
| 26ca443c10 | |||
| 0f37e49039 | |||
| 3fdcde73ae | |||
| d2dc77169b | |||
| d99b506885 |
@@ -0,0 +1,21 @@
|
||||
# http://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[src/ngLocale/**]
|
||||
insert_final_newline = false
|
||||
|
||||
[dropdown-toggle.js]
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
|
||||
[htmlparser.js]
|
||||
insert_final_newline = false
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules/**
|
||||
lib/htmlparser/**
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"bitwise": true,
|
||||
"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,
|
||||
"sub": true,
|
||||
"undef": true,
|
||||
"indent": 2
|
||||
}
|
||||
+10
-1
@@ -1,6 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 0.10
|
||||
- '0.10'
|
||||
|
||||
branches:
|
||||
except:
|
||||
@@ -18,6 +18,15 @@ env:
|
||||
- LOGS_DIR=/tmp/angular-build/logs
|
||||
- BROWSER_PROVIDER_READY_FILE=/tmp/sauce-connect-ready
|
||||
|
||||
install:
|
||||
# - npm config set registry http://23.251.144.68
|
||||
# Disable the spinner, it looks bad on Travis
|
||||
- npm config set spin false
|
||||
# Log HTTP requests
|
||||
- npm config set loglevel http
|
||||
- time ./scripts/travis/npm-bundle-deps.sh
|
||||
- time npm install
|
||||
|
||||
before_script:
|
||||
- mkdir -p $LOGS_DIR
|
||||
- ./lib/sauce/sauce_connect_setup.sh
|
||||
|
||||
+1445
-2
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -23,7 +23,7 @@ discussion list or [StackOverflow][stackoverflow]. We are also available on [IRC
|
||||
|
||||
## <a name="issue"></a> Found an Issue?
|
||||
If you find a bug in the source code or a mistake in the documentation, you can help us by
|
||||
submitting and issue to our [GitHub Repository][github]. Even better you can submit a Pull Request
|
||||
submitting an issue to our [GitHub Repository][github]. Even better you can submit a Pull Request
|
||||
with a fix.
|
||||
|
||||
***Localization Issue:*** *Angular.js uses the [Google Closure I18N library], to generate its own I18N files. This means that
|
||||
@@ -119,7 +119,7 @@ Before you submit your pull request consider the following guidelines:
|
||||
```
|
||||
|
||||
* In GitHub, send a pull request to `angular:master`.
|
||||
* If we suggest changes then
|
||||
* If we suggest changes then
|
||||
* Make the required updates.
|
||||
* Re-run the Angular test suite to ensure tests are still passing.
|
||||
* Rebase your branch and force push to your GitHub repository (this will update your Pull Request):
|
||||
@@ -237,7 +237,7 @@ reference GitHub issues that this commit **Closes**.
|
||||
|
||||
A detailed explanation can be found in this [document][commit-message-format].
|
||||
|
||||
## <a name="cla"></a> Signing the CLA
|
||||
## <a name="cla"></a> Signing the CLA
|
||||
|
||||
Please sign our Contributor License Agreement (CLA) before sending pull requests. For any code
|
||||
changes to be accepted, the CLA must be signed. It's a quick process, we promise!
|
||||
@@ -252,7 +252,7 @@ You can find out more detailed information about contributing in the
|
||||
|
||||
|
||||
|
||||
[Google Closure I18N library]: https://code.google.com/p/closure-library/source/browse/closure/goog/i18n/
|
||||
[Google Closure I18N library]: https://github.com/google/closure-library/tree/master/closure/goog/i18n
|
||||
[angular-dev]: https://groups.google.com/forum/?fromgroups#!forum/angular-dev
|
||||
[coc]: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md
|
||||
[commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#
|
||||
@@ -269,6 +269,6 @@ You can find out more detailed information about contributing in the
|
||||
[ngDocs]: https://github.com/angular/angular.js/wiki/Writing-AngularJS-Documentation
|
||||
[plunker]: http://plnkr.co/edit
|
||||
[stackoverflow]: http://stackoverflow.com/questions/tagged/angularjs
|
||||
[unit-testing]: http://docs.angularjs.org/guide/dev_guide.unit-testing
|
||||
[unit-testing]: https://docs.angularjs.org/guide/unit-testing
|
||||
|
||||
[](https://github.com/igrigorik/ga-beacon)
|
||||
|
||||
+17
-2
@@ -1,3 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
var files = require('./angularFiles').files;
|
||||
var util = require('./lib/grunt/utils.js');
|
||||
var versionInfo = require('./lib/versions/version-info');
|
||||
@@ -107,6 +109,12 @@ module.exports = function(grunt) {
|
||||
options: {
|
||||
jshintrc: true,
|
||||
},
|
||||
node: {
|
||||
files: { src: ['*.js', 'lib/**/*.js'] },
|
||||
},
|
||||
tests: {
|
||||
files: { src: 'test/**/*.js' },
|
||||
},
|
||||
ng: {
|
||||
files: { src: files['angularSrc'] },
|
||||
},
|
||||
@@ -220,8 +228,11 @@ module.exports = function(grunt) {
|
||||
|
||||
"ddescribe-iit": {
|
||||
files: [
|
||||
'src/**/*.js',
|
||||
'test/**/*.js',
|
||||
'!test/ngScenario/DescribeSpec.js'
|
||||
'!test/ngScenario/DescribeSpec.js',
|
||||
'!src/ng/directive/booleanAttrs.js', // legitimate xit here
|
||||
'!src/ngScenario/**/*.js'
|
||||
]
|
||||
},
|
||||
|
||||
@@ -246,7 +257,11 @@ module.exports = function(grunt) {
|
||||
compress: {
|
||||
build: {
|
||||
options: {archive: 'build/' + dist +'.zip', mode: 'zip'},
|
||||
src: ['**'], cwd: 'build', expand: true, dot: true, dest: dist + '/'
|
||||
src: ['**'],
|
||||
cwd: 'build',
|
||||
expand: true,
|
||||
dot: true,
|
||||
dest: dist + '/'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
+5
-5
@@ -34,7 +34,7 @@ This process based on the idea of minimizing user pain
|
||||
* Check if there are comments that link to a dupe. If so verify that this is indeed a dupe, [close it][], and go to the last step.
|
||||
1. Bugs:
|
||||
* Label `Type: Bug`
|
||||
* Reproducible? - Steps to reproduce the bug are clear. If they are not,
|
||||
* Reproducible? - Steps to reproduce the bug are clear. If they are not, ask for a clarification. If there's no reply after a week, [close it][].
|
||||
* Reproducible on master? - <http://code.angularjs.org/snapshot/>
|
||||
|
||||
1. Non bugs:
|
||||
@@ -44,11 +44,11 @@ This process based on the idea of minimizing user pain
|
||||
* Label `needs: breaking change` - if needed
|
||||
* Label `needs: public api` - if the issue requires introduction of a new public API
|
||||
1. Label `browser: *` - if the issue **only** affects a certain browser
|
||||
1. Label `frequency: *` – How often does this issue come up? How many developers does this affect?
|
||||
1. Label `frequency: *` – How often does this issue come up? How many developers does this affect? Chose just one of the following:
|
||||
* low - obscure issue affecting a handful of developers
|
||||
* moderate - impacts a common usage pattern
|
||||
* high - impacts most or all Angular apps
|
||||
1. Label `severity: *` - How bad is the issue?
|
||||
1. Label `severity: *` - How bad is the issue? Chose just one of the following:
|
||||
* security issue
|
||||
* regression
|
||||
* memory leak
|
||||
@@ -61,9 +61,9 @@ This process based on the idea of minimizing user pain
|
||||
1. Label `origin: google` for issues from Google
|
||||
|
||||
1. Assign a milestone:
|
||||
* Backlog - triaged fixes and features, should be the default choice
|
||||
* Backlog - triaged fixes and features, should be the default choice
|
||||
* Current 1.x.y milestone (e.g. 1.3.0-beta-2) - regressions and urgent bugs only
|
||||
|
||||
|
||||
|
||||
1. Unassign yourself from the issue
|
||||
|
||||
|
||||
Vendored
+3
-1
@@ -1,4 +1,6 @@
|
||||
angularFiles = {
|
||||
'use strict';
|
||||
|
||||
var angularFiles = {
|
||||
'angularSrc': [
|
||||
'src/minErr.js',
|
||||
'src/Angular.js',
|
||||
|
||||
+4
-2
@@ -3,6 +3,8 @@
|
||||
// TODO(vojta): pre-commit hook for validating messages
|
||||
// TODO(vojta): report errors, currently Q silence everything which really sucks
|
||||
|
||||
'use strict';
|
||||
|
||||
var child = require('child_process');
|
||||
var fs = require('fs');
|
||||
var util = require('util');
|
||||
@@ -164,7 +166,7 @@ var writeChangelog = function(stream, commits, version) {
|
||||
hash: commit.hash,
|
||||
closes: []
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
stream.write(util.format(HEADER_TPL, version, version, currentDate()));
|
||||
@@ -172,7 +174,7 @@ var writeChangelog = function(stream, commits, version) {
|
||||
printSection(stream, 'Features', sections.feat);
|
||||
printSection(stream, 'Performance Improvements', sections.perf);
|
||||
printSection(stream, 'Breaking Changes', sections.breaks, false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var getPreviousTag = function() {
|
||||
|
||||
+5
-1
@@ -1,3 +1,7 @@
|
||||
/* global describe: false, it: false, expect: false */
|
||||
|
||||
'use strict';
|
||||
|
||||
describe('changelog.js', function() {
|
||||
var ch = require('./changelog');
|
||||
|
||||
@@ -13,7 +17,7 @@ describe('changelog.js', function() {
|
||||
expect(msg.hash).toBe('9b1aff905b638aa274a5fc8f88662df446d374bd');
|
||||
expect(msg.subject).toBe('broadcast $destroy event on scope destruction');
|
||||
expect(msg.body).toBe('perf testing shows that in chrome this change adds 5-15% overhead\n' +
|
||||
'when destroying 10k nested scopes where each scope has a $destroy listener\n')
|
||||
'when destroying 10k nested scopes where each scope has a $destroy listener\n');
|
||||
expect(msg.component).toBe('scope');
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/local/bin/node
|
||||
|
||||
'use strict';
|
||||
|
||||
var util = require('util');
|
||||
var cp = require('child_process');
|
||||
|
||||
@@ -143,7 +145,7 @@ then(allInSeries(function (branch) {
|
||||
return sha + (msg.toLowerCase().indexOf('fix') === -1 ? ' ' : ' * ') + msg;
|
||||
});
|
||||
branch.log = log.map(function (line) {
|
||||
return line.substr(41)
|
||||
return line.substr(41);
|
||||
});
|
||||
return branch;
|
||||
});
|
||||
|
||||
@@ -14,3 +14,9 @@ ng\:form {
|
||||
transition:0s all!important;
|
||||
-webkit-transition:0s all!important;
|
||||
}
|
||||
|
||||
/* show the element during a show/hide animation when the
|
||||
* animation is ongoing, but the .ng-hide class is active */
|
||||
.ng-hide-add-active, .ng-hide-remove {
|
||||
display: block!important;
|
||||
}
|
||||
|
||||
@@ -184,10 +184,6 @@ h1,h2,h3,h4,h5,h6 {
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
display: block;
|
||||
white-space: pre-wrap;
|
||||
word-break: normal;
|
||||
}
|
||||
@@ -215,6 +211,10 @@ code.highlighted {
|
||||
color:maroon;
|
||||
}
|
||||
|
||||
ul + p {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.docs-version-jump {
|
||||
min-width:100%;
|
||||
max-width:100%;
|
||||
@@ -257,6 +257,9 @@ code.highlighted {
|
||||
z-index: 99;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
-moz-appearance: none;
|
||||
text-indent: 0.01px;
|
||||
text-overflow: '';
|
||||
}
|
||||
|
||||
.picker:after {
|
||||
@@ -576,6 +579,15 @@ ul.events > li {
|
||||
margin-bottom:40px;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 769px) and (max-width: 991px) {
|
||||
.main-body-grid {
|
||||
margin-top: 160px;
|
||||
}
|
||||
.main-body-grid > .grid-left {
|
||||
top: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width : 768px) {
|
||||
.picker, .picker select {
|
||||
width:auto;
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.lang-text * {
|
||||
color: #333333!important;
|
||||
}
|
||||
|
||||
.pln {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ pre.prettyprint.linenums {
|
||||
}
|
||||
ol.linenums {
|
||||
margin: 0 0 0 33px; /* IE indents via margin-left */
|
||||
}
|
||||
}
|
||||
ol.linenums li {
|
||||
padding-left: 12px;
|
||||
font-size:12px;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 141 KiB |
+3
-3
@@ -11,7 +11,7 @@ directive.runnableExample = ['$templateCache', '$document', function($templateCa
|
||||
'ng-repeat="tab in tabs track by $index" ' +
|
||||
'href="" ' +
|
||||
'class="btn"' +
|
||||
'ng-click="setTab($index)">' +
|
||||
'ng-click="setTab($index)">' +
|
||||
' {{ tab }}' +
|
||||
' </a>' +
|
||||
'</nav>';
|
||||
@@ -103,7 +103,7 @@ directive.syntax = function() {
|
||||
restrict: 'A',
|
||||
link: function(scope, element, attrs) {
|
||||
function makeLink(type, text, link, icon) {
|
||||
return '<a href="' + link + '" class="btn syntax-' + type + '" target="_blank" rel="nofollow">' +
|
||||
return '<a href="' + link + '" class="btn syntax-' + type + '" target="_blank" rel="nofollow">' +
|
||||
'<span class="' + icon + '"></span> ' + text +
|
||||
'</a>';
|
||||
};
|
||||
@@ -307,7 +307,7 @@ var popoverElement = function() {
|
||||
return this.titleElement.html(value);
|
||||
},
|
||||
|
||||
content : function(value) {
|
||||
content : function(value) {
|
||||
if(value && value.length > 0) {
|
||||
value = marked(value);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
describe('docs.angularjs.org', function () {
|
||||
describe('App', function () {
|
||||
// it('should filter the module list when searching', function () {
|
||||
// browser.get();
|
||||
// browser.waitForAngular();
|
||||
|
||||
// var search = element(by.input('q'));
|
||||
// var search = element(by.model('q'));
|
||||
// search.clear();
|
||||
// search.sendKeys('ngBind');
|
||||
|
||||
@@ -27,15 +29,15 @@ describe('docs.angularjs.org', function () {
|
||||
it('should show the functioning input directive example', function () {
|
||||
browser.get('index-debug.html#!/api/ng/directive/input');
|
||||
|
||||
//Wait for animation
|
||||
browser.sleep(500);
|
||||
// Ensure that the page is loaded before trying to switch frames.
|
||||
browser.waitForAngular();
|
||||
|
||||
browser.switchTo().frame('example-input-directive');
|
||||
|
||||
var nameInput = element(by.input('user.name'));
|
||||
var nameInput = element(by.model('user.name'));
|
||||
nameInput.sendKeys('!!!');
|
||||
|
||||
var code = element(by.css('tt'));
|
||||
var code = element.all(by.css('tt')).first();
|
||||
expect(code.getText()).toContain('guest!!!');
|
||||
});
|
||||
|
||||
@@ -66,4 +68,21 @@ describe('docs.angularjs.org', function () {
|
||||
expect(element(by.css('.minerr-errmsg')).getText()).toEqual("Argument 'Missing' is not a function, got undefined");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("templates", function() {
|
||||
it("should show parameter defaults", function() {
|
||||
browser.get('index-debug.html#!/api/ng/service/$timeout');
|
||||
expect(element.all(by.css('.input-arguments p em')).first().getText()).toContain('(default: 0)');
|
||||
});
|
||||
});
|
||||
|
||||
describe("API pages", function() {
|
||||
it("should display links to code on GitHub", function() {
|
||||
browser.get('index-debug.html#!/api/ng/service/$http');
|
||||
expect(element(by.css('.improve-docs')).getAttribute('href')).toMatch(/https?:\/\/github\.com\/angular\/angular\.js\/edit\/.+\/src\/ng\/http\.js/);
|
||||
|
||||
browser.get('index-debug.html#!/api/ng/service/$http');
|
||||
expect(element(by.css('.view-source')).getAttribute('href')).toMatch(/https?:\/\/github\.com\/angular\/angular\.js\/tree\/.+\/src\/ng\/http\.js#L\d+/);
|
||||
});
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -20,4 +20,4 @@ angular.module('docsApp', [
|
||||
|
||||
.config(function($locationProvider) {
|
||||
$locationProvider.html5Mode(true).hashPrefix('!');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
angular.module('directives', [])
|
||||
|
||||
.directive('code', function() {
|
||||
return { restrict:'E', terminal: true };
|
||||
})
|
||||
|
||||
/**
|
||||
* backToTop Directive
|
||||
* @param {Function} $anchorScroll
|
||||
@@ -25,7 +21,7 @@ angular.module('directives', [])
|
||||
restrict: 'E',
|
||||
terminal: true,
|
||||
compile: function(element) {
|
||||
var linenums = element.hasClass('linenum') || element.parent()[0].nodeName === 'PRE';
|
||||
var linenums = element.hasClass('linenum');// || element.parent()[0].nodeName === 'PRE';
|
||||
var match = /lang-(\S)+/.exec(element.className);
|
||||
var lang = match && match[1];
|
||||
var html = element.html();
|
||||
|
||||
+10
-3
@@ -1,9 +1,16 @@
|
||||
angular.module('DocsController', [])
|
||||
|
||||
.controller('DocsController', function($scope, $rootScope, $location, $window, $cookies, NG_PAGES, NG_NAVIGATION, NG_VERSION) {
|
||||
.controller('DocsController', [
|
||||
'$scope', '$rootScope', '$location', '$window', '$cookies', 'openPlunkr',
|
||||
'NG_PAGES', 'NG_NAVIGATION', 'NG_VERSION',
|
||||
function($scope, $rootScope, $location, $window, $cookies, openPlunkr,
|
||||
NG_PAGES, NG_NAVIGATION, NG_VERSION) {
|
||||
|
||||
|
||||
$scope.openPlunkr = openPlunkr;
|
||||
|
||||
$scope.docsVersion = NG_VERSION.isSnapshot ? 'snapshot' : NG_VERSION.version;
|
||||
|
||||
|
||||
$scope.fold = function(url) {
|
||||
if(url) {
|
||||
$scope.docs_fold = '/notes/' + url;
|
||||
@@ -120,4 +127,4 @@ angular.module('DocsController', [])
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -59,4 +59,4 @@ angular.module('errors', ['ngSanitize'])
|
||||
element.html(errorLinkFilter(interpolate.apply(null, formatArgs), '_blank'));
|
||||
}
|
||||
};
|
||||
}]);
|
||||
}]);
|
||||
|
||||
+53
-242
@@ -1,67 +1,13 @@
|
||||
angular.module('examples', [])
|
||||
|
||||
.directive('sourceEdit', function(getEmbeddedTemplate) {
|
||||
return {
|
||||
template: '<div class="btn-group pull-right">' +
|
||||
'<a class="btn dropdown-toggle btn-primary" data-toggle="dropdown" href>' +
|
||||
' <i class="icon-pencil icon-white"></i> Edit<span class="caret"></span>' +
|
||||
'</a>' +
|
||||
'<ul class="dropdown-menu">' +
|
||||
' <li><a ng-click="plunkr($event)" href="">In Plunkr</a></li>' +
|
||||
' <li><a ng-click="fiddle($event)" href="">In JsFiddle</a></li>' +
|
||||
'</ul>' +
|
||||
'</div>',
|
||||
scope: true,
|
||||
controller: function($scope, $attrs, openJsFiddle, openPlunkr) {
|
||||
var sources = {
|
||||
module: $attrs.sourceEdit,
|
||||
deps: read($attrs.sourceEditDeps),
|
||||
html: read($attrs.sourceEditHtml),
|
||||
css: read($attrs.sourceEditCss),
|
||||
js: read($attrs.sourceEditJs),
|
||||
json: read($attrs.sourceEditJson),
|
||||
unit: read($attrs.sourceEditUnit),
|
||||
scenario: read($attrs.sourceEditScenario)
|
||||
};
|
||||
$scope.fiddle = function(e) {
|
||||
e.stopPropagation();
|
||||
openJsFiddle(sources);
|
||||
};
|
||||
$scope.plunkr = function(e) {
|
||||
e.stopPropagation();
|
||||
openPlunkr(sources);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function read(text) {
|
||||
var files = [];
|
||||
angular.forEach(text ? text.split(' ') : [], function(refId) {
|
||||
// refId is index.html-343, so we need to strip the unique ID when exporting the name
|
||||
files.push({name: refId.replace(/-\d+$/, ''), content: getEmbeddedTemplate(refId)});
|
||||
});
|
||||
return files;
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
.factory('angularUrls', function($document) {
|
||||
var urls = {};
|
||||
|
||||
angular.forEach($document.find('script'), function(script) {
|
||||
var match = script.src.match(/^.*\/(angular[^\/]*\.js)$/);
|
||||
if (match) {
|
||||
urls[match[1].replace(/(\-\d.*)?(\.min)?\.js$/, '.js')] = match[0];
|
||||
}
|
||||
});
|
||||
|
||||
return urls;
|
||||
})
|
||||
|
||||
|
||||
.factory('formPostData', function($document) {
|
||||
.factory('formPostData', ['$document', function($document) {
|
||||
return function(url, fields) {
|
||||
var form = angular.element('<form style="display: none;" method="post" action="' + url + '" target="_blank"></form>');
|
||||
/**
|
||||
* Form previously posted to target="_blank", but pop-up blockers were causing this to not work.
|
||||
* If a user chose to bypass pop-up blocker one time and click the link, they would arrive at
|
||||
* a new default plnkr, not a plnkr with the desired template.
|
||||
*/
|
||||
var form = angular.element('<form style="display: none;" method="post" action="' + url + '"></form>');
|
||||
angular.forEach(fields, function(value, name) {
|
||||
var input = angular.element('<input type="hidden" name="' + name + '">');
|
||||
input.attr('value', value);
|
||||
@@ -71,196 +17,61 @@ angular.module('examples', [])
|
||||
form[0].submit();
|
||||
form.remove();
|
||||
};
|
||||
})
|
||||
}])
|
||||
|
||||
|
||||
.factory('prepareDefaultAppModule', function() {
|
||||
return function(content) {
|
||||
var deps = [];
|
||||
angular.forEach(content.deps, function(file) {
|
||||
if(file.name == 'angular-animate.js') {
|
||||
deps.push('ngAnimate');
|
||||
}
|
||||
});
|
||||
.factory('openPlunkr', ['formPostData', '$http', '$q', function(formPostData, $http, $q) {
|
||||
return function(exampleFolder) {
|
||||
|
||||
var moduleName = 'App';
|
||||
return {
|
||||
module : moduleName,
|
||||
script : "angular.module('" + moduleName + "', [" +
|
||||
(deps.length ? "'" + deps.join("','") + "'" : "") + "]);\n\n"
|
||||
};
|
||||
};
|
||||
})
|
||||
var exampleName = 'AngularJS Example';
|
||||
|
||||
.factory('prepareEditorAssetTags', function(angularUrls) {
|
||||
return function(content, options) {
|
||||
options = options || {};
|
||||
var includeLocalFiles = options.includeLocalFiles;
|
||||
var html = makeScriptTag(angularUrls['angular.js']);
|
||||
// Load the manifest for the example
|
||||
$http.get(exampleFolder + '/manifest.json')
|
||||
.then(function(response) {
|
||||
return response.data;
|
||||
})
|
||||
.then(function(manifest) {
|
||||
var filePromises = [];
|
||||
|
||||
var allFiles = [].concat(content.js, content.css, content.html, content.json);
|
||||
angular.forEach(content.deps, function(file) {
|
||||
if (file.name !== 'angular.js') {
|
||||
var isLocal = false;
|
||||
for(var i=0;i<allFiles.length;i++) {
|
||||
if(allFiles[i].name == file.name) {
|
||||
isLocal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!(isLocal && !includeLocalFiles)) {
|
||||
var assetUrl = angularUrls[file.name] || file.name;
|
||||
html += makeScriptTag(assetUrl);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(includeLocalFiles) {
|
||||
angular.forEach(content.css, function(file, index) {
|
||||
html += makeCssLinkTag(file.name);
|
||||
});
|
||||
}
|
||||
|
||||
return html;
|
||||
|
||||
|
||||
function makeScriptTag(src) {
|
||||
return '<script type="text/javascript" src="' + src + '"></script>\n';
|
||||
}
|
||||
|
||||
function makeCssLinkTag(src) {
|
||||
return '<link rel="stylesheet" type="text/css" href="' + src + '" />\n';
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
|
||||
.factory('openPlunkr', function(templateMerge, formPostData, prepareEditorAssetTags, prepareDefaultAppModule) {
|
||||
return function(content) {
|
||||
var hasRouting = false;
|
||||
angular.forEach(content.deps, function(file) {
|
||||
hasRouting = hasRouting || file.name == 'angular-route.js';
|
||||
});
|
||||
var indexHtmlContent = '<!doctype html>\n' +
|
||||
'<html ng-app="{{module}}">\n' +
|
||||
' <head>\n' +
|
||||
'{{scriptDeps}}';
|
||||
|
||||
if(hasRouting) {
|
||||
indexHtmlContent += '<script type="text/javascript">\n' +
|
||||
'//this is here to make plunkr work with AngularJS routing\n' +
|
||||
'angular.element(document.getElementsByTagName(\'head\')).append(' +
|
||||
'angular.element(\'<base href="\' + window.location.pathname + \'" />\')' +
|
||||
');\n' +
|
||||
'</script>\n';
|
||||
}
|
||||
|
||||
indexHtmlContent += '</head>\n' +
|
||||
' <body>\n\n' +
|
||||
'{{indexContents}}\n\n' +
|
||||
' </body>\n' +
|
||||
'</html>\n';
|
||||
|
||||
indexProp = {
|
||||
module: content.module,
|
||||
scriptDeps: prepareEditorAssetTags(content, { includeLocalFiles : true }),
|
||||
indexContents: content.html[0].content
|
||||
};
|
||||
|
||||
var allFiles = [].concat(content.js, content.css, content.html, content.json);
|
||||
|
||||
if(!content.module) {
|
||||
var moduleData = prepareDefaultAppModule(content);
|
||||
indexProp.module = moduleData.module;
|
||||
|
||||
var found = false;
|
||||
angular.forEach(content.js, function(file) {
|
||||
if(file.name == 'script.js') {
|
||||
file.content = moduleData.script + file.content;
|
||||
found = true;
|
||||
}
|
||||
});
|
||||
if(!found) {
|
||||
indexProp.scriptDeps += '<script type="text/javascript" src="script.js"></script>\n';
|
||||
allFiles.push({
|
||||
name : 'script.js',
|
||||
content : moduleData.script
|
||||
// Build a pretty title for the Plunkr
|
||||
var exampleNameParts = manifest.name.split('-');
|
||||
exampleNameParts.unshift('AngularJS');
|
||||
angular.forEach(exampleNameParts, function(part, index) {
|
||||
exampleNameParts[index] = part.charAt(0).toUpperCase() + part.substr(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
exampleName = exampleNameParts.join(' - ');
|
||||
|
||||
var postData = {};
|
||||
angular.forEach(manifest.files, function(filename) {
|
||||
filePromises.push($http.get(exampleFolder + '/' + filename, { transformResponse: [] })
|
||||
.then(function(response) {
|
||||
|
||||
angular.forEach(allFiles, function(file, index) {
|
||||
if (file.content && file.name != 'index.html') {
|
||||
postData['files[' + file.name + ']'] = file.content;
|
||||
}
|
||||
});
|
||||
// The manifests provide the production index file but Plunkr wants
|
||||
// a straight index.html
|
||||
if (filename === "index-production.html") {
|
||||
filename = "index.html"
|
||||
}
|
||||
|
||||
postData['files[index.html]'] = templateMerge(indexHtmlContent, indexProp);
|
||||
postData['tags[]'] = "angularjs";
|
||||
return {
|
||||
name: filename,
|
||||
content: response.data
|
||||
};
|
||||
}));
|
||||
});
|
||||
return $q.all(filePromises);
|
||||
})
|
||||
.then(function(files) {
|
||||
var postData = {};
|
||||
|
||||
postData.private = true;
|
||||
postData.description = 'AngularJS Example Plunkr';
|
||||
angular.forEach(files, function(file) {
|
||||
postData['files[' + file.name + ']'] = file.content;
|
||||
});
|
||||
|
||||
formPostData('http://plnkr.co/edit/?p=preview', postData);
|
||||
postData['tags[0]'] = "angularjs";
|
||||
postData['tags[1]'] = "example";
|
||||
postData.private = true;
|
||||
postData.description = exampleName;
|
||||
|
||||
formPostData('http://plnkr.co/edit/?p=preview', postData);
|
||||
});
|
||||
};
|
||||
})
|
||||
|
||||
.factory('openJsFiddle', function(templateMerge, formPostData, prepareEditorAssetTags, prepareDefaultAppModule) {
|
||||
var HTML = '<div ng-app=\"{{module}}\">\n{{html:2}}</div>',
|
||||
CSS = '</style> <!-- Ugly Hack to make remote files preload in jsFiddle --> \n' +
|
||||
'{{head:0}}<style>{{css}}',
|
||||
SCRIPT = '{{script}}',
|
||||
SCRIPT_CACHE = '\n\n<!-- {{name}} -->\n<script type="text/ng-template" id="{{name}}">\n{{content:2}}</script>',
|
||||
BASE_HREF_TAG = '<!-- Ugly Hack to make AngularJS routing work inside of jsFiddle -->\n' +
|
||||
'<base href="/" />\n\n';
|
||||
|
||||
return function(content) {
|
||||
var prop = {
|
||||
module: content.module,
|
||||
html: '',
|
||||
css: '',
|
||||
script: ''
|
||||
};
|
||||
if(!prop.module) {
|
||||
var moduleData = prepareDefaultAppModule(content);
|
||||
prop.script = moduleData.script;
|
||||
prop.module = moduleData.module;
|
||||
}
|
||||
|
||||
angular.forEach(content.html, function(file, index) {
|
||||
if (index) {
|
||||
prop.html += templateMerge(SCRIPT_CACHE, file);
|
||||
} else {
|
||||
prop.html += file.content;
|
||||
}
|
||||
});
|
||||
|
||||
prop.head = prepareEditorAssetTags(content, { includeLocalFiles : false });
|
||||
|
||||
angular.forEach(content.js, function(file, index) {
|
||||
prop.script += file.content;
|
||||
});
|
||||
|
||||
angular.forEach(content.css, function(file, index) {
|
||||
prop.css += file.content;
|
||||
});
|
||||
|
||||
var hasRouting = false;
|
||||
angular.forEach(content.deps, function(file) {
|
||||
hasRouting = hasRouting || file.name == 'angular-route.js';
|
||||
});
|
||||
|
||||
var compiledHTML = templateMerge(HTML, prop);
|
||||
if(hasRouting) {
|
||||
compiledHTML = BASE_HREF_TAG + compiledHTML;
|
||||
}
|
||||
formPostData("http://jsfiddle.net/api/post/library/pure/", {
|
||||
title: 'AngularJS Example',
|
||||
html: compiledHTML,
|
||||
js: templateMerge(SCRIPT, prop),
|
||||
css: templateMerge(CSS, prop)
|
||||
});
|
||||
};
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -21,4 +21,4 @@ angular.module('docsApp.navigationService', [])
|
||||
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ angular.module('search', [])
|
||||
}
|
||||
|
||||
$scope.search = function(q) {
|
||||
var MIN_SEARCH_LENGTH = 3;
|
||||
var MIN_SEARCH_LENGTH = 2;
|
||||
if(q.length >= MIN_SEARCH_LENGTH) {
|
||||
var results = docsSearch(q);
|
||||
var totalAreas = 0;
|
||||
@@ -35,7 +35,7 @@ angular.module('search', [])
|
||||
}
|
||||
}
|
||||
if(result) {
|
||||
$location.path(result.url);
|
||||
$location.path(result.path);
|
||||
$scope.hideResults();
|
||||
}
|
||||
};
|
||||
|
||||
+15
-23
@@ -22,7 +22,7 @@ angular.module('tutorials', [])
|
||||
element.addClass('tutorial-nav');
|
||||
element.append(templateMerge(
|
||||
'<a href="tutorial/{{prev}}"><li class="btn btn-primary"><i class="glyphicon glyphicon-step-backward"></i> Previous</li></a>\n' +
|
||||
'<a href="http://angular.github.com/angular-phonecat/step-{{seq}}/app"><li class="btn btn-primary"><i class="glyphicon glyphicon-play"></i> Live Demo</li></a>\n' +
|
||||
'<a href="http://angular.github.io/angular-phonecat/step-{{seq}}/app"><li class="btn btn-primary"><i class="glyphicon glyphicon-play"></i> Live Demo</li></a>\n' +
|
||||
'<a href="https://github.com/angular/angular-phonecat/compare/step-{{diffLo}}...step-{{diffHi}}"><li class="btn btn-primary"><i class="glyphicon glyphicon-search"></i> Code Diff</li></a>\n' +
|
||||
'<a href="tutorial/{{next}}"><li class="btn btn-primary">Next <i class="glyphicon glyphicon-step-forward"></i></li></a>', props));
|
||||
}
|
||||
@@ -31,28 +31,20 @@ angular.module('tutorials', [])
|
||||
|
||||
|
||||
.directive('docTutorialReset', function() {
|
||||
function tab(name, command, id, step) {
|
||||
return '' +
|
||||
' <div class=\'tab-pane well\' title="' + name + '" value="' + id + '">\n' +
|
||||
' <ol>\n' +
|
||||
' <li><p>Reset the workspace to step ' + step + '.</p>' +
|
||||
' <pre>' + command + '</pre></li>\n' +
|
||||
' <li><p>Refresh your browser or check the app out on <a href="http://angular.github.com/angular-phonecat/step-' + step + '/app">Angular\'s server</a>.</p></li>\n' +
|
||||
' </ol>\n' +
|
||||
' </div>\n';
|
||||
}
|
||||
|
||||
return {
|
||||
compile: function(element, attrs) {
|
||||
var step = attrs.docTutorialReset;
|
||||
element.html(
|
||||
'<div ng-hide="show">' +
|
||||
'<p><a href="" ng-click="show=true;$event.stopPropagation()">Workspace Reset Instructions ➤</a></p>' +
|
||||
'</div>\n' +
|
||||
'<div class="tabbable" ng-show="show" ng-model="$cookies.platformPreference">\n' +
|
||||
tab('Git on Mac/Linux', 'git checkout -f step-' + step, 'gitUnix', step) +
|
||||
tab('Git on Windows', 'git checkout -f step-' + step, 'gitWin', step) +
|
||||
'</div>\n');
|
||||
}
|
||||
scope: {
|
||||
'step': '@docTutorialReset'
|
||||
},
|
||||
template:
|
||||
'<p><a href="" ng-click="show=!show;$event.stopPropagation()">Workspace Reset Instructions ➤</a></p>\n' +
|
||||
'<div class="alert alert-info" ng-show="show">\n' +
|
||||
' <p>Reset the workspace to step {{step}}.</p>' +
|
||||
' <p><pre>git checkout -f step-{{step}}</pre></p>\n' +
|
||||
' <p>Refresh your browser or check out this step online: '+
|
||||
'<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}}">GitHub</a>\n' +
|
||||
'</p>'
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
"use strict";
|
||||
|
||||
angular.module('versions', [])
|
||||
|
||||
.controller('DocsVersionsCtrl', ['$scope', '$location', '$window', 'NG_VERSIONS', function($scope, $location, $window, NG_VERSIONS) {
|
||||
$scope.docs_versions = NG_VERSIONS;
|
||||
$scope.docs_version = NG_VERSIONS[0];
|
||||
|
||||
for(var i=0, minor = NaN; i < NG_VERSIONS.length; i++) {
|
||||
var version = NG_VERSIONS[i];
|
||||
// NaN will give false here
|
||||
if (minor <= version.minor) {
|
||||
continue;
|
||||
}
|
||||
version.isLatest = true;
|
||||
minor = version.minor;
|
||||
}
|
||||
|
||||
$scope.docs_versions = NG_VERSIONS;
|
||||
$scope.getGroupName = function(v) {
|
||||
return v.isLatest ? 'Latest' : (v.isStable ? 'Stable' : 'Unstable');
|
||||
};
|
||||
|
||||
$scope.jumpToDocsVersion = function(version) {
|
||||
var currentPagePath = $location.path();
|
||||
|
||||
// TODO: We need to do some munging of the path for different versions of the API...
|
||||
|
||||
|
||||
|
||||
$window.location = version.docsUrl + currentPagePath;
|
||||
};
|
||||
}]);
|
||||
}]);
|
||||
|
||||
@@ -3,6 +3,7 @@ describe("DocsController", function() {
|
||||
|
||||
angular.module('fake', [])
|
||||
.value('$cookies', {})
|
||||
.value('openPlunkr', function() {})
|
||||
.value('NG_PAGES', {})
|
||||
.value('NG_NAVIGATION', {})
|
||||
.value('NG_VERSION', {});
|
||||
@@ -30,4 +31,4 @@ describe("DocsController", function() {
|
||||
expect($window._gaq.pop()).toEqual(['_trackPageview', 'x/y/z']);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ module.exports = function(config) {
|
||||
|
||||
config = basePackage(config);
|
||||
config = examplesPackage(config);
|
||||
|
||||
|
||||
config.append('processing.processors', [
|
||||
require('./processors/git-data'),
|
||||
require('./processors/error-docs'),
|
||||
@@ -25,6 +25,10 @@ module.exports = function(config) {
|
||||
require('./tag-defs/tutorial-step')
|
||||
]);
|
||||
|
||||
config.append('processing.defaultTagTransforms', [
|
||||
require('dgeni-packages/jsdoc/tag-defs/transforms/trim-whitespace')
|
||||
]);
|
||||
|
||||
config.append('processing.inlineTagDefinitions', [
|
||||
require('./inline-tag-defs/type')
|
||||
]);
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
var writer = require('dgeni/lib/utils/doc-writer');
|
||||
var fs = require('q-io/fs');
|
||||
var log = require('winston');
|
||||
var util = require("util");
|
||||
|
||||
var filter, outputPath, depth;
|
||||
|
||||
module.exports = {
|
||||
name: 'debug-dump',
|
||||
runBefore: ['write-files'],
|
||||
description: 'This processor dumps docs that match a filter to a file',
|
||||
init: function(config, injectables) {
|
||||
process: function(docs, config) {
|
||||
|
||||
var filter, outputPath, depth;
|
||||
|
||||
filter = config.get('processing.debug-dump.filter');
|
||||
outputPath = config.get('processing.debug-dump.outputPath');
|
||||
depth = config.get('processing.debug-dump.depth', 2);
|
||||
},
|
||||
process: function(docs) {
|
||||
|
||||
|
||||
if ( filter && outputPath ) {
|
||||
log.info('Dumping docs:', filter, outputPath);
|
||||
var filteredDocs = filter(docs);
|
||||
var dumpedDocs = util.inspect(filteredDocs, depth);
|
||||
return writer.writeFile(outputPath, dumpedDocs).then(function() {
|
||||
return writeFile(outputPath, dumpedDocs).then(function() {
|
||||
return docs;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function writeFile(file, content) {
|
||||
return fs.makeTree(fs.directory(file)).then(function() {
|
||||
return fs.write(file, content, 'wb');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,16 +5,18 @@ var path = require('canonical-path');
|
||||
module.exports = {
|
||||
name: 'error-docs',
|
||||
description: 'Compute the various fields for docs in the Error area',
|
||||
runAfter: ['tags-extracted'],
|
||||
init: function(config, injectables) {
|
||||
injectables.value('errorNamespaces', {});
|
||||
|
||||
var minerrInfoPath = config.get('processing.errors.minerrInfoPath');
|
||||
if ( !minerrInfoPath ) {
|
||||
throw new Error('Error in configuration: Please provide a path to the minerr info file (errors.json) ' +
|
||||
'in the `config.processing.errors.minerrInfoPath` property');
|
||||
}
|
||||
injectables.value('minerrInfo', require(minerrInfoPath));
|
||||
runAfter: ['tags-extracted', 'compute-path'],
|
||||
runBefore: ['extra-docs-added'],
|
||||
exports: {
|
||||
errorNamespaces: ['factory', function() { return {}; }],
|
||||
minerrInfo: ['factory', function(config) {
|
||||
var minerrInfoPath = config.get('processing.errors.minerrInfoPath');
|
||||
if ( !minerrInfoPath ) {
|
||||
throw new Error('Error in configuration: Please provide a path to the minerr info file (errors.json) ' +
|
||||
'in the `config.processing.errors.minerrInfoPath` property');
|
||||
}
|
||||
return require(minerrInfoPath);
|
||||
}]
|
||||
},
|
||||
process: function(docs, partialNames, errorNamespaces, minerrInfo) {
|
||||
|
||||
@@ -54,4 +56,4 @@ module.exports = {
|
||||
|
||||
return docs.concat(_.values(errorNamespaces));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,16 +3,18 @@ var versionInfo = require('../../../lib/versions/version-info');
|
||||
|
||||
module.exports = {
|
||||
name: 'git-data',
|
||||
runBefore: ['loading-files'],
|
||||
runBefore: ['reading-files'],
|
||||
description: 'This processor adds information from the local git repository to the extraData injectable',
|
||||
init: function(config, injectables) {
|
||||
injectables.value('gitData', {
|
||||
version: versionInfo.currentVersion,
|
||||
versions: versionInfo.previousVersions,
|
||||
info: versionInfo.gitRepoInfo
|
||||
});
|
||||
exports: {
|
||||
gitData: ['factory', function() {
|
||||
return {
|
||||
version: versionInfo.currentVersion,
|
||||
versions: versionInfo.previousVersions,
|
||||
info: versionInfo.gitRepoInfo
|
||||
};
|
||||
}]
|
||||
},
|
||||
process: function(extraData, gitData) {
|
||||
extraData.git = gitData;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
var _ = require('lodash');
|
||||
var log = require('winston');
|
||||
var path = require('canonical-path');
|
||||
var deployment;
|
||||
|
||||
module.exports = {
|
||||
name: 'index-page',
|
||||
runAfter: ['adding-extra-docs'],
|
||||
runBefore: ['extra-docs-added'],
|
||||
description: 'This processor creates docs that will be rendered as the index page for the app',
|
||||
init: function(config) {
|
||||
deployment = config.deployment;
|
||||
process: function(docs, config) {
|
||||
|
||||
var deployment = config.deployment;
|
||||
if ( !deployment || !deployment.environments ) {
|
||||
throw new Error('No deployment environments found in the config.');
|
||||
}
|
||||
},
|
||||
process: function(docs) {
|
||||
|
||||
// Collect up all the areas in the docs
|
||||
var areas = {};
|
||||
|
||||
@@ -3,20 +3,20 @@ var log = require('winston');
|
||||
var fs = require('fs');
|
||||
var path = require('canonical-path');
|
||||
|
||||
// Keywords to ignore
|
||||
var wordsToIgnore = [];
|
||||
var propertiesToIgnore;
|
||||
var areasToSearch;
|
||||
|
||||
// Keywords start with "ng:" or one of $, _ or a letter
|
||||
var KEYWORD_REGEX = /^((ng:|[\$_a-z])[\w\-_]+)/;
|
||||
|
||||
module.exports = {
|
||||
name: 'keywords',
|
||||
runAfter: ['docs-processed'],
|
||||
runBefore: ['adding-extra-docs'],
|
||||
description: 'This processor extracts all the keywords from the document',
|
||||
init: function(config) {
|
||||
process: function(docs, config) {
|
||||
|
||||
// Keywords to ignore
|
||||
var wordsToIgnore = [];
|
||||
var propertiesToIgnore;
|
||||
var areasToSearch;
|
||||
|
||||
// Keywords start with "ng:" or one of $, _ or a letter
|
||||
var KEYWORD_REGEX = /^((ng:|[\$_a-z])[\w\-_]+)/;
|
||||
|
||||
// Load up the keywords to ignore, if specified in the config
|
||||
if ( config.processing.search && config.processing.search.ignoreWordsFile ) {
|
||||
@@ -34,9 +34,6 @@ module.exports = {
|
||||
propertiesToIgnore = _.indexBy(config.get('processing.search.propertiesToIgnore', []));
|
||||
log.debug('Properties to ignore', propertiesToIgnore);
|
||||
|
||||
},
|
||||
process: function(docs) {
|
||||
|
||||
var ignoreWordsMap = _.indexBy(wordsToIgnore);
|
||||
|
||||
// If the title contains a name starting with ng, e.g. "ngController", then add the module name
|
||||
@@ -88,4 +85,4 @@ module.exports = {
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -129,18 +129,15 @@ var navGroupMappers = {
|
||||
}
|
||||
};
|
||||
|
||||
var outputFolder;
|
||||
|
||||
module.exports = {
|
||||
name: 'pages-data',
|
||||
description: 'This plugin will create a new doc that will be rendered as an angularjs module ' +
|
||||
'which will contain meta information about the pages and navigation',
|
||||
runAfter: ['adding-extra-docs', 'component-groups-generate'],
|
||||
runAfter: ['adding-extra-docs', 'component-groups-generate', 'compute-path'],
|
||||
runBefore: ['extra-docs-added'],
|
||||
init: function(config) {
|
||||
outputFolder = config.rendering.outputFolder;
|
||||
},
|
||||
process: function(docs) {
|
||||
process: function(docs, config) {
|
||||
|
||||
var outputFolder = config.rendering.outputFolder;
|
||||
|
||||
_(docs)
|
||||
.filter(function(doc) { return doc.area === 'api'; })
|
||||
@@ -191,7 +188,7 @@ module.exports = {
|
||||
area.navGroups = navGroupMapper(pages, area);
|
||||
});
|
||||
|
||||
// Extract a list of basic page information for mapping paths to paritals and for client side searching
|
||||
// Extract a list of basic page information for mapping paths to partials and for client side searching
|
||||
var pages = _(docs)
|
||||
.map(function(doc) {
|
||||
var page = _.pick(doc, [
|
||||
|
||||
@@ -1,45 +1,20 @@
|
||||
var _ = require('lodash');
|
||||
var log = require('winston');
|
||||
var path = require('canonical-path');
|
||||
var trimIndentation = require('dgeni/lib/utils/trim-indentation');
|
||||
var code = require('dgeni/lib/utils/code');
|
||||
var protractorFolder;
|
||||
|
||||
function createProtractorDoc(example, file, env) {
|
||||
var protractorDoc = {
|
||||
docType: 'e2e-test',
|
||||
id: 'protractorTest' + '-' + example.id,
|
||||
template: 'protractorTests.template.js',
|
||||
outputPath: path.join(protractorFolder, example.id, env + '_test.js'),
|
||||
innerTest: file.fileContents,
|
||||
pathPrefix: '.', // Hold for if we test with full jQuery
|
||||
exampleId: example.id,
|
||||
description: example.doc.id
|
||||
};
|
||||
|
||||
if (env === 'jquery') {
|
||||
protractorDoc.examplePath = example.outputFolder + '/index-jquery.html'
|
||||
} else {
|
||||
protractorDoc.examplePath = example.outputFolder + '/index.html'
|
||||
}
|
||||
return protractorDoc;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: 'protractor-generate',
|
||||
description: 'Generate a protractor test file from the e2e tests in the examples',
|
||||
runAfter: ['adding-extra-docs'],
|
||||
runBefore: ['extra-docs-added'],
|
||||
init: function(config, injectables) {
|
||||
protractorFolder = config.get('rendering.protractor.outputFolder', 'ptore2e');
|
||||
},
|
||||
process: function(docs, examples) {
|
||||
process: function(docs, examples, config) {
|
||||
var protractorFolder = config.get('rendering.protractor.outputFolder', 'ptore2e');
|
||||
|
||||
_.forEach(examples, function(example) {
|
||||
|
||||
_.forEach(example.files, function(file) {
|
||||
|
||||
// Check if it's a Protractor test.
|
||||
if (!(file.type == 'protractor')) {
|
||||
if (file.type !== 'protractor') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -48,5 +23,28 @@ module.exports = {
|
||||
docs.push(createProtractorDoc(example, file, 'jqlite'));
|
||||
});
|
||||
});
|
||||
|
||||
function createProtractorDoc(example, file, env) {
|
||||
var protractorDoc = {
|
||||
docType: 'e2e-test',
|
||||
id: 'protractorTest' + '-' + example.id,
|
||||
template: 'protractorTests.template.js',
|
||||
outputPath: path.join(protractorFolder, example.id, env + '_test.js'),
|
||||
innerTest: file.fileContents,
|
||||
pathPrefix: '.', // Hold for if we test with full jQuery
|
||||
exampleId: example.id,
|
||||
description: example.doc.id,
|
||||
'ng-app-included': example['ng-app-included']
|
||||
};
|
||||
|
||||
if (env === 'jquery') {
|
||||
protractorDoc.examplePath = example.outputFolder + '/index-jquery.html';
|
||||
} else {
|
||||
protractorDoc.examplePath = example.outputFolder + '/index.html';
|
||||
}
|
||||
return protractorDoc;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
@@ -35,4 +35,4 @@ module.exports = {
|
||||
|
||||
docs.push(versionDoc);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
module.exports = {
|
||||
name: 'step',
|
||||
transformFn: function(doc, tag) {
|
||||
transforms: function(doc, tag, value) {
|
||||
if ( doc.docType !== 'tutorial' ) {
|
||||
throw new Error('Invalid tag, step. You should only use this tag on tutorial docs');
|
||||
}
|
||||
return parseInt(tag.description,10);
|
||||
return parseInt(value,10);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,4 +14,4 @@
|
||||
{$ doc.description | marked $}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -25,4 +25,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
<div class="container main-grid main-header-grid">
|
||||
<div class="grid-left">
|
||||
<div ng-controller="DocsVersionsCtrl" class="picker version-picker">
|
||||
<select ng-options="v as ('v' + v.version + (v.isSnapshot ? ' (snapshot)' : '')) group by (v.isStable?'Stable':'Unstable') for v in docs_versions"
|
||||
<select ng-options="v as ('v' + v.version + (v.isSnapshot ? ' (snapshot)' : '')) group by getGroupName(v) for v in docs_versions"
|
||||
ng-model="docs_version"
|
||||
ng-change="jumpToDocsVersion(docs_version)"
|
||||
class="docs-version-jump">
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
describe("{$ doc.description $}", function() {
|
||||
var rootEl;
|
||||
beforeEach(function() {
|
||||
rootEl = browser.rootEl;{% if doc['ng-app-included'] %}
|
||||
browser.rootEl = '[ng-app]';{% endif %}
|
||||
browser.get("{$ doc.pathPrefix $}/{$ doc.examplePath $}");
|
||||
});
|
||||
|
||||
{% if doc['ng-app-included'] %}afterEach(function() { browser.rootEl = rootEl; });{% endif %}
|
||||
{$ doc.innerTest $}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
is HTML and wrap each line in a <p> - thus breaking the HTML #}
|
||||
|
||||
<div>
|
||||
<a ng-href="http://plnkr.co/edit/ngdoc:{$ doc.example.id $}@{{docsVersion}}?p=preview" class="btn pull-right" target="_blank">
|
||||
<a ng-click="openPlunkr('{$ doc.example.outputFolder $}')" class="btn pull-right">
|
||||
<i class="glyphicon glyphicon-edit"> </i>
|
||||
Edit in Plunker</a>
|
||||
<div class="runnable-example"
|
||||
@@ -15,7 +15,7 @@
|
||||
{$ attrName $}="{$ attrValue $}"{% endfor %}>
|
||||
{% code -%}
|
||||
{$ file.fileContents $}
|
||||
{%- endcode %}
|
||||
{%- endcode %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -24,4 +24,4 @@
|
||||
</div>
|
||||
|
||||
{# Be aware that we need these extra new lines here or marked will not realise that the <div>
|
||||
above is HTML and wrap each line in a <p> - thus breaking the HTML #}
|
||||
above is HTML and wrap each line in a <p> - thus breaking the HTML #}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{% include 'overview.template.html' %}
|
||||
{% include 'overview.template.html' %}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
Welcome to the AngularJS API docs page. These pages contain the AngularJS reference materials for version <strong ng-bind="version"></strong>.
|
||||
|
||||
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/dev_guide.services services}, {@link guide/filter filters}, {@link guide/providers providers}, {@link guide/templates types}, global APIs and testing mocks.
|
||||
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.
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Angular Namespaces `$` and `$$`**
|
||||
@@ -212,7 +212,7 @@ Use ngTouch when developing for mobile browsers/devices.
|
||||
{@link ngTouch#service Services / Factories}
|
||||
</td>
|
||||
<td>
|
||||
The {@link ngTouch.$swipe $swipe} service is used to register and manage mobile DOM events.
|
||||
The {@link ngTouch.$swipe $swipe} service is used to register and manage mobile DOM events.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -252,7 +252,7 @@ Use ngSanitize to securely parse and manipulate HTML data in your application.
|
||||
|
||||
## {@link ngMock ngMock}
|
||||
|
||||
Use ngMock to inject and mock modules, factories, services and providers within your unit tests
|
||||
Use ngMock to inject and mock modules, factories, services and providers within your unit tests
|
||||
|
||||
<div class="alert alert-info">Include the **angular-mocks.js** file into your test runner for this to work.</div>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ but the required directive controller is not present on the current DOM element
|
||||
|
||||
To resolve this error ensure that there is no typo in the required controller name and that the required directive controller is present on the current element.
|
||||
|
||||
If the required controller is expected to be on a ancestor element, make ensure that you prefix the controller name in the `require` definition with `^`.
|
||||
If the required controller is expected to be on a ancestor element, make sure that you prefix the controller name in the `require` definition with `^`.
|
||||
|
||||
If the required controller is optionally requested, use `?` or `^?` to specify that.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ myModule.directive('myDirective', function factory() {
|
||||
return {
|
||||
...
|
||||
scope: {
|
||||
'bind': '=localValue'
|
||||
localValue: '=bind'
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
@@ -37,3 +37,17 @@ elements. For example:
|
||||
```
|
||||
<b>Hello</b> World!
|
||||
```
|
||||
|
||||
Watch out for html comments at the beginning or end of templates, as these can cause this error as
|
||||
well. Consider the following template:
|
||||
|
||||
```
|
||||
<div class='container'>
|
||||
<div class='wrapper>
|
||||
...
|
||||
</div> <!-- wrapper -->
|
||||
</div> <!-- container -->
|
||||
```
|
||||
|
||||
The `<!-- container -->` comment is interpreted as a second root element and causes the template to
|
||||
be invalid.
|
||||
|
||||
@@ -23,4 +23,4 @@ When an instance of `MyCtrl` is created, the service `myService` will be created
|
||||
by the `$injector`. `myService` depends on itself, which causes the `$injector`
|
||||
to detect a circular dependency and throw the error.
|
||||
|
||||
For more information, see the {@link guide/di Dependency Injection Guide}.
|
||||
For more information, see the {@link guide/di Dependency Injection Guide}.
|
||||
|
||||
@@ -23,4 +23,4 @@ To avoid the error, always use string literals for dependency injection annotati
|
||||
tokens.
|
||||
|
||||
For an explanation of what injection annotations are and how to use them, refer
|
||||
to the {@link guide/di Dependency Injection Guide}.
|
||||
to the {@link guide/di Dependency Injection Guide}.
|
||||
|
||||
@@ -23,4 +23,4 @@ angular.module("myApp", [])
|
||||
```
|
||||
|
||||
For more information, refer to the {@link auto.$provide#provider
|
||||
$provide.provider} api doc.
|
||||
$provide.provider} api doc.
|
||||
|
||||
@@ -14,8 +14,9 @@ angular.module('myApp', [])
|
||||
}]);
|
||||
```
|
||||
|
||||
This code will fail with `$injector:unpr` if `myService` is not defined. Making
|
||||
sure each dependency is defined will fix the problem.
|
||||
The above code will fail with `$injector:unpr` if `myService` is not defined.
|
||||
|
||||
Making sure each dependency is defined will fix the problem, as noted below.
|
||||
|
||||
```
|
||||
angular.module('myApp', [])
|
||||
@@ -23,4 +24,34 @@ angular.module('myApp', [])
|
||||
.controller('MyController', ['myService', function (myService) {
|
||||
// Do something with myService
|
||||
}]);
|
||||
```
|
||||
|
||||
An unknown provider error can also be caused by accidentally redefining a
|
||||
module using the `angular.module` API, as shown in the following example.
|
||||
|
||||
```
|
||||
angular.module('myModule', [])
|
||||
.service('myCoolService', function () { /* ... */ });
|
||||
|
||||
angular.module('myModule', [])
|
||||
// myModule has already been created! This is not what you want!
|
||||
.directive('myDirective', ['myCoolService', function (myCoolService) {
|
||||
// This directive definition throws unknown provider, because myCoolService
|
||||
// has been destroyed.
|
||||
}]);
|
||||
```
|
||||
|
||||
To fix this problem, make sure you only define each module with the
|
||||
`angular.module(name, [requires])` syntax once across your entire project.
|
||||
Retrieve it for subsequent use with `angular.module(name)`. The fixed example
|
||||
is shown below.
|
||||
|
||||
```
|
||||
angular.module('myModule', [])
|
||||
.service('myCoolService', function () { /* ... */ });
|
||||
|
||||
angular.module('myModule')
|
||||
.directive('myDirective', ['myCoolService', function (myCoolService) {
|
||||
// This directive definition does not throw unknown provider.
|
||||
}]);
|
||||
```
|
||||
@@ -9,4 +9,4 @@ it hard to reason about whether some combination of concatenated values are
|
||||
unsafe to use and could easily lead to XSS.
|
||||
|
||||
For more information about how AngularJS helps keep your app secure, refer to
|
||||
the {@link ng.$sce $sce} API doc.
|
||||
the {@link ng.$sce $sce} API doc.
|
||||
|
||||
@@ -14,3 +14,34 @@ perform this check - it's up to the developer to not expose such sensitive and p
|
||||
directly on the scope chain.
|
||||
|
||||
To resolve this error, avoid access to DOM nodes.
|
||||
|
||||
|
||||
# Event Handlers and Return Values
|
||||
|
||||
The `$parse:isecdom` error also occurs when an event handler invokes a function that returns a DOM
|
||||
node.
|
||||
|
||||
```html
|
||||
<button ng-click="iWillReturnDOM()">click me</button>
|
||||
```
|
||||
|
||||
```js
|
||||
$scope.iWillReturnDOM = function() {
|
||||
return someDomNode;
|
||||
}
|
||||
```
|
||||
|
||||
To fix this issue, avoid returning DOM nodes from event handlers.
|
||||
|
||||
*Note: This error often means that you are accessing DOM from your controllers, which is usually
|
||||
a sign of poor coding style that violates separation of concerns.*
|
||||
|
||||
|
||||
# Implicit Returns in CoffeeScript
|
||||
|
||||
This error can occur more frequently when using CoffeeScript, which has a feature called implicit
|
||||
returns. This language feature returns the last dereferenced object in the function when the
|
||||
function has no explicit return statement.
|
||||
|
||||
The solution in this scenario is to add an explicit return statement. For example `return false` to
|
||||
the function.
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
@ngdoc error
|
||||
@name $parse:isecff
|
||||
@fullName Referencing 'call', 'apply' and 'bind' Disallowed
|
||||
@description
|
||||
|
||||
Occurs when an expression attempts to invoke Function's 'call', 'apply' or 'bind'.
|
||||
|
||||
Angular bans the invocation of 'call', 'apply' and 'bind' from within expressions
|
||||
since access is a known way to modify the behaviour of existing functions.
|
||||
|
||||
To resolve this error, avoid using these methods in expressions.
|
||||
|
||||
Example expression that would result in this error:
|
||||
|
||||
```
|
||||
<div>{{user.sendInfo.call({}, true)}}</div>
|
||||
```
|
||||
@@ -1,18 +1,27 @@
|
||||
@ngdoc error
|
||||
@name $parse:isecfld
|
||||
@fullName Referencing 'constructor' Field in Expression
|
||||
@fullName Referencing Disallowed Field in Expression
|
||||
@description
|
||||
|
||||
Occurs when an expression attempts to access an objects constructor field.
|
||||
Occurs when an expression attempts to access one of the following fields:
|
||||
|
||||
AngularJS bans constructor access from within expressions since constructor
|
||||
access is a known way to execute arbitrary Javascript code.
|
||||
* __proto__
|
||||
* __defineGetter__
|
||||
* __defineSetter__
|
||||
* __lookupGetter__
|
||||
* __lookupSetter__
|
||||
|
||||
To resolve this error, avoid constructor access. As a last resort, alias
|
||||
the constructor and access it through the alias instead.
|
||||
AngularJS bans access to these fields from within expressions since
|
||||
access is a known way to mess with native objects or
|
||||
to execute arbitrary Javascript code.
|
||||
|
||||
Example expression that would result in this error:
|
||||
To resolve this error, avoid using these fields in expressions. As a last resort,
|
||||
alias their value and access them through the alias instead.
|
||||
|
||||
Example expressions that would result in this error:
|
||||
|
||||
```
|
||||
<div>{{user.constructor.name}}</div>
|
||||
<div>{{user.__proto__.hasOwnProperty = $emit}}</div>
|
||||
|
||||
<div>{{user.__defineGetter__('name', noop)}}</div>
|
||||
```
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
@ngdoc error
|
||||
@name $parse:isecobj
|
||||
@fullName Referencing Object Disallowed
|
||||
@description
|
||||
|
||||
Occurs when an expression attempts to access the 'Object' object (Root object in JavaScript).
|
||||
|
||||
Angular bans access to Object from within expressions since access is a known way to modify
|
||||
the behaviour of existing objects.
|
||||
|
||||
To resolve this error, avoid Object access.
|
||||
@@ -5,4 +5,4 @@
|
||||
|
||||
Occurs when you try to use the name `hasOwnProperty` as a name of a parameter.
|
||||
Generally, a name cannot be `hasOwnProperty` because it is used, internally, on a object
|
||||
and allowing such a name would break lookups on this object.
|
||||
and allowing such a name would break lookups on this object.
|
||||
|
||||
@@ -3,72 +3,310 @@
|
||||
@fullName Action Already In Progress
|
||||
@description
|
||||
|
||||
At any point in time there can be only one `$digest` or $apply operation in progress.
|
||||
The stack trace of this error allows you to trace the origin of the currently executing $apply or $digest call.
|
||||
At any point in time there can be only one `$digest` or `$apply` operation in progress. This is to
|
||||
prevent very hard to detect bugs from entering your application. The stack trace of this error
|
||||
allows you to trace the origin of the currently executing `$apply` or `$digest` call, which caused
|
||||
the error.
|
||||
|
||||
`$digest` or `$apply` are processing operational states of the Scope - data-structure in Angular that provides context for models and enables model mutation observation.
|
||||
## Background
|
||||
|
||||
Trying to reenter a `$digest` or `$apply` while one of them is already in progress is typically a sign of programming error that needs to be fixed.
|
||||
Angular uses a dirty-checking digest mechanism to monitor and update values of the scope during
|
||||
the processing of your application. The digest works by checking all the values that are being
|
||||
watched against their previous value and running any watch handlers that have been defined for those
|
||||
values that have changed.
|
||||
|
||||
This digest mechanism is triggered by calling `$digest` on a scope object. Normally you do not need
|
||||
to trigger a digest manually, because every external action that can trigger changes in your
|
||||
application, such as mouse events, timeouts or server responses, wrap the Angular application code
|
||||
in a block of code that will run `$digest` when the code completes.
|
||||
|
||||
You wrap Angular code in a block that will be followed by a `$digest` by calling `$apply` on a scope
|
||||
object. So, in pseudo-code, the process looks like this:
|
||||
|
||||
```
|
||||
element.on('mouseup', function() {
|
||||
scope.$apply(function() {
|
||||
$scope.doStuff();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
where `$apply()` looks something like:
|
||||
|
||||
```
|
||||
$apply = function(fn) {
|
||||
try {
|
||||
fn();
|
||||
} finally() {
|
||||
$digest();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Digest Phases
|
||||
|
||||
Angular keeps track of what phase of processing we are in, the relevant ones being `$apply` and
|
||||
`$digest`. Trying to reenter a `$digest` or `$apply` while one of them is already in progress is
|
||||
typically a sign of programming error that needs to be fixed. So Angular will throw this error when
|
||||
that occurs.
|
||||
|
||||
In most situations it should be well defined whether a piece of code will be run inside an `$apply`,
|
||||
in which case you should not be calling `$apply` or `$digest`, or it will be run outside, in which
|
||||
case you should wrap any code that will be interacting with Angular scope or services, in a call to
|
||||
`$apply`.
|
||||
|
||||
As an example, all Controller code should expect to be run within Angular, so it should have no need
|
||||
to call `$apply` or `$digest`. Conversely, code that is being trigger directly as a call back to
|
||||
some external event, from the DOM or 3rd party library, should expect that it is never called from
|
||||
within Angular, and so any Angular application code that it calls should first be wrapped in a call
|
||||
to $apply.
|
||||
|
||||
## Common Causes
|
||||
|
||||
Apart from simply incorrect calls to `$apply` or `$digest` there are some cases when you may get
|
||||
this error through no fault of your own.
|
||||
|
||||
### Inconsistent API (Sync/Async)
|
||||
|
||||
This error is often seen when interacting with an API that is sometimes sync and sometimes async.
|
||||
|
||||
For example:
|
||||
For example, imagine a 3rd party library that has a method which will retrieve data for us. Since it
|
||||
may be making an asynchronous call to a server, it accepts a callback function, which will be called
|
||||
when the data arrives.
|
||||
|
||||
```
|
||||
function MyController() {
|
||||
function MyController($scope, thirdPartyComponent) {
|
||||
thirdPartyComponent.getData(function(someData) {
|
||||
scope.$apply(function() {
|
||||
scope.someData = someData;
|
||||
$scope.$apply(function() {
|
||||
$scope.someData = someData;
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The controller constructor is always instantiated from within an $apply cycle, so if the third-party component called our callback synchronously, we'd be trying to enter the $apply again.
|
||||
We expect that our callback will be called asynchronously, and so from outside Angular. Therefore, we
|
||||
correctly wrap our application code that interacts with Angular in a call to `$apply`.
|
||||
|
||||
To resolve this type of issue, either fix the api to be always synchronous or asynchronous or wrap the call to the api with setTimeout call to make it always asynchronous.
|
||||
The problem comes if `getData()` decides to call the callback handler synchronously; perhaps it has
|
||||
the data already cached in memory and so it immediately calls the callback to return the data,
|
||||
synchronously.
|
||||
|
||||
Since, the `MyController` constructor is always instantiated from within an `$apply` call, our
|
||||
handler is trying to enter a new `$apply` block from within one.
|
||||
|
||||
Other situation that leads to this error is when you are trying to reuse a function to by using it as a callback for code that is called by various apis inside and outside of $apply.
|
||||
This is not an ideal design choice on the part of the 3rd party library.
|
||||
|
||||
For example:
|
||||
To resolve this type of issue, either fix the api to be always synchronous or asynchronous or force
|
||||
your callback handler to always run asynchronously by using the `$timeout` service.
|
||||
|
||||
```
|
||||
myApp.directive('myDirective', function() {
|
||||
function MyController($scope, thirdPartyComponent) {
|
||||
thirdPartyComponent.getData(function(someData) {
|
||||
$timeout(function() {
|
||||
$scope.someData = someData;
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Here we have used `$timeout` to schedule the changes to the scope in a future call stack.
|
||||
By providing a timeout period of 0ms, this will occur as soon as possible and `$timeout` will ensure
|
||||
that the code will be called in a single `$apply` block.
|
||||
|
||||
### Triggering Events Programmatically
|
||||
|
||||
The other situation that often leads to this error is when you trigger code (such as a DOM event)
|
||||
programmatically (from within Angular), which is normally called by an external trigger.
|
||||
|
||||
For example, consider a directive that will set focus on an input control when a value in the scope
|
||||
is true:
|
||||
|
||||
```
|
||||
myApp.directive('setFocusIf', function() {
|
||||
return {
|
||||
link: function($scope, $element) {
|
||||
function doSomeWork() {
|
||||
$scope.$apply(function() {
|
||||
// do work here, and update the model
|
||||
};
|
||||
}
|
||||
|
||||
$element.on('click', doSomeWork);
|
||||
doSomeWork(); // << this will throw an exception because templates are compiled within $apply
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
The fix for the example above looks like this:
|
||||
```
|
||||
myApp.directive('myDirective', function() {
|
||||
return {
|
||||
link: function($scope, $element) {
|
||||
function doSomeWork() {
|
||||
// do work here, and update the model
|
||||
}
|
||||
|
||||
$element.on('click', function() {
|
||||
$scope.$apply(doSomeWork); // <<< the $apply call was moved to the callsite that doesn't execute in $apply call already
|
||||
link: function($scope, $element, $attr) {
|
||||
$scope.$watch($attr.setFocusIf, function(value) {
|
||||
if ( value ) { $element[0].focus(); }
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
doSomeWork();
|
||||
If we applied this directive to an input which also used the `ngFocus` directive to trigger some
|
||||
work when the element receives focus we will have a problem:
|
||||
|
||||
```
|
||||
<input set-focus-if="hasFocus" ng-focus="msg='has focus'">
|
||||
<button ng-click="hasFocus = true">Focus</button>
|
||||
```
|
||||
|
||||
In this setup, there are two ways to trigger ngFocus. First from a user interaction:
|
||||
|
||||
* Click on the input control
|
||||
* The input control gets focus
|
||||
* The `ngFocus` directive is triggered, setting `$scope.msg='has focus'` from within a new call to
|
||||
`$apply()`
|
||||
|
||||
Second programmatically:
|
||||
|
||||
* Click the button
|
||||
* The `ngClick` directive sets the value of `$scope.hasFocus` to true inside a call to `$apply`
|
||||
* The `$digest` runs, which triggers the watch inside the `setFocusIf` directive
|
||||
* The watch's handle runs, which gives the focus to the input
|
||||
* The `ngFocus` directive is triggered, setting `$scope.msg='has focus'` from within a new call to
|
||||
`$apply()`
|
||||
|
||||
In this second scenario, we are already inside a `$digest` when the ngFocus directive makes another
|
||||
call to `$apply()`, causing this error to be thrown.
|
||||
|
||||
It is possible to workaround this problem by moving the call to set the focus outside of the digest,
|
||||
by using `$timeout(fn, 0, false)`, where the `false` value tells Angular not to wrap this `fn` in a
|
||||
`$apply` block:
|
||||
|
||||
```
|
||||
myApp.directive('setFocusIf', function($timeout) {
|
||||
return {
|
||||
link: function($scope, $element, $attr) {
|
||||
$scope.$watch($attr.setFocusIf, function(value) {
|
||||
if ( value ) {
|
||||
$timeout(function() {
|
||||
// We must reevaluate the value in case it was changed by a subsequent
|
||||
// watch handler in the digest.
|
||||
if ( $scope.$eval($attr.setFocusIf) ) {
|
||||
$element[0].focus();
|
||||
}
|
||||
}, 0, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
To learn more about Angular processing model please check out the {@link guide/concepts concepts doc} as well as the {@link ng.$rootScope.Scope api} doc.
|
||||
## Diagnosing This Error
|
||||
|
||||
When you get this error it can be rather daunting to diagnose the cause of the issue. The best
|
||||
course of action is to investigate the stack trace from the error. You need to look for places
|
||||
where `$apply` or `$digest` have been called and find the context in which this occurred.
|
||||
|
||||
There should be two calls:
|
||||
|
||||
* The first call is the good `$apply`/`$digest` and would normally be triggered by some event near
|
||||
the top of the call stack.
|
||||
|
||||
* The second call is the bad `$apply`/`$digest` and this is the one to investigate.
|
||||
|
||||
Once you have identified this call you work your way up the stack to see what the problem is.
|
||||
|
||||
* If the second call was made in your application code then you should look at why this code has been
|
||||
called from within a `$apply`/`$digest`. It may be a simple oversight or maybe it fits with the
|
||||
sync/async scenario described earlier.
|
||||
|
||||
* If the second call was made inside an Angular directive then it is likely that it matches the second
|
||||
programmatic event trigger scenario described earlier. In this case you may need to look further up
|
||||
the tree to what triggered the event in the first place.
|
||||
|
||||
### Example Problem
|
||||
|
||||
Let's look at how to investigate this error using the `setFocusIf` example from above. This example
|
||||
defines a new `setFocusIf` directive that sets the focus on the element where it is defined when the
|
||||
value of its attribute becomes true.
|
||||
|
||||
<example name="error-$rootScope-inprog" module="app">
|
||||
<file name="index.html">
|
||||
<button ng-click="focusInput = true">Focus</button>
|
||||
<input ng-focus="count = count + 1" set-focus-if="focusInput" />
|
||||
</file>
|
||||
<file name="app.js">
|
||||
angular.module('app', []).directive('setFocusIf', function() {
|
||||
return function link($scope, $element, $attr) {
|
||||
$scope.$watch($attr.setFocusIf, function(value) {
|
||||
if ( value ) { $element[0].focus(); }
|
||||
});
|
||||
};
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
|
||||
When you click on the button to cause the focus to occur we get our `$rootScope:inprog` error. The
|
||||
stacktrace looks like this:
|
||||
|
||||
```
|
||||
Error: [$rootScope:inprog]
|
||||
at Error (native)
|
||||
at angular.min.js:6:467
|
||||
at n (angular.min.js:105:60)
|
||||
at g.$get.g.$apply (angular.min.js:113:195)
|
||||
at HTMLInputElement.<anonymous> (angular.min.js:198:401)
|
||||
at angular.min.js:32:32
|
||||
at Array.forEach (native)
|
||||
at q (angular.min.js:7:295)
|
||||
at HTMLInputElement.c (angular.min.js:32:14)
|
||||
at Object.fn (app.js:12:38) angular.js:10111
|
||||
(anonymous function) angular.js:10111
|
||||
$get angular.js:7412
|
||||
$get.g.$apply angular.js:12738 <--- $apply
|
||||
(anonymous function) angular.js:19833 <--- called here
|
||||
(anonymous function) angular.js:2890
|
||||
q angular.js:320
|
||||
c angular.js:2889
|
||||
(anonymous function) app.js:12
|
||||
$get.g.$digest angular.js:12469
|
||||
$get.g.$apply angular.js:12742 <--- $apply
|
||||
(anonymous function) angular.js:19833 <--- called here
|
||||
(anonymous function) angular.js:2890
|
||||
q angular.js:320
|
||||
```
|
||||
|
||||
We can see (even though the Angular code is minified) that there were two calls to `$apply`, first
|
||||
on line `19833`, then on line `12738` of `angular.js`.
|
||||
|
||||
It is this second call that caused the error. If we look at the angular.js code, we can see that
|
||||
this call is made by an Angular directive.
|
||||
|
||||
```
|
||||
var ngEventDirectives = {};
|
||||
forEach(
|
||||
'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
|
||||
function(name) {
|
||||
var directiveName = directiveNormalize('ng-' + name);
|
||||
ngEventDirectives[directiveName] = ['$parse', function($parse) {
|
||||
return {
|
||||
compile: function($element, attr) {
|
||||
var fn = $parse(attr[directiveName]);
|
||||
return function(scope, element, attr) {
|
||||
element.on(lowercase(name), function(event) {
|
||||
scope.$apply(function() {
|
||||
fn(scope, {$event:event});
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
}];
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
It is not possible to tell which from the stack trace, but we happen to know in this case that it is
|
||||
the `ngFocus` directive.
|
||||
|
||||
Now look up the stack to see that our application code is only entered once in `app.js` at line `12`.
|
||||
This is where our problem is:
|
||||
|
||||
```
|
||||
10: link: function($scope, $element, $attr) {
|
||||
11: $scope.$watch($attr.setFocusIf, function(value) {
|
||||
12: if ( value ) { $element[0].focus(); } <---- This is the source of the problem
|
||||
13: });
|
||||
14: }
|
||||
```
|
||||
|
||||
We can now see that the second `$apply` was caused by us programmatically triggering a DOM event
|
||||
(i.e. focus) to occur. We must fix this by moving the code outside of the $apply block using
|
||||
`$timeout` as described above.
|
||||
|
||||
## Further Reading
|
||||
To learn more about Angular processing model please check out the
|
||||
{@link guide/concepts concepts doc} as well as the {@link ng.$rootScope.Scope api} doc.
|
||||
|
||||
@@ -15,9 +15,9 @@ By default, only URLs that belong to the same origin are trusted. These are urls
|
||||
The {@link ng.directive:ngInclude ngInclude} directive and {@link guide/directive directives} that specify a `templateUrl` require a trusted resource URL.
|
||||
|
||||
To load templates from other domains and/or protocols, either adjust the {@link
|
||||
api/ng.$sceDelegateProvider#resourceUrlWhitelist whitelist}/ {@link
|
||||
api/ng.$sceDelegateProvider#resourceUrlBlacklist blacklist} or wrap the URL with a call to {@link
|
||||
api/ng.$sce#trustAsResourceUrl $sce.trustAsResourceUrl}.
|
||||
ng.$sceDelegateProvider#resourceUrlWhitelist whitelist}/ {@link
|
||||
ng.$sceDelegateProvider#resourceUrlBlacklist blacklist} or wrap the URL with a call to {@link
|
||||
ng.$sce#trustAsResourceUrl $sce.trustAsResourceUrl}.
|
||||
|
||||
**Note**: The browser's [Same Origin
|
||||
Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest) and
|
||||
|
||||
@@ -13,3 +13,4 @@ Angular template from a URL requires that the URL is one considered safe for loa
|
||||
This helps prevent XSS and other security issues. Read more at {@link
|
||||
api/ng.$sce Strict Contextual Escaping (SCE)}
|
||||
|
||||
You may want to include the ngSanitize module to use the automatic sanitizing.
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
|
||||
AngularJS often asserts that certain values will be present and truthy using a
|
||||
helper function. If the assertion fails, this error is thrown. To fix this problem,
|
||||
make sure that the value the assertion expects is defined and truthy.
|
||||
make sure that the value the assertion expects is defined and truthy.
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
|
||||
Occurs when you try to use the name `hasOwnProperty` in a context where it is not allow.
|
||||
Generally, a name cannot be `hasOwnProperty` because it is used, internally, on a object
|
||||
and allowing such a name would break lookups on this object.
|
||||
and allowing such a name would break lookups on this object.
|
||||
|
||||
@@ -49,4 +49,4 @@ You can also get this error if you accidentally load AngularJS itself more than
|
||||
<script src="angular.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
```
|
||||
|
||||
@@ -7,4 +7,4 @@ This error occurs when attempting to copy an object to itself. Calling {@link
|
||||
api/angular.copy angular.copy} with a `destination` object deletes
|
||||
all of the elements or properties on `destination` before copying to it. Copying
|
||||
an object to itself is not supported. Make sure to check your calls to
|
||||
`angular.copy` and avoid copying objects or arrays to themselves.
|
||||
`angular.copy` and avoid copying objects or arrays to themselves.
|
||||
|
||||
@@ -7,4 +7,4 @@ Copying Window or Scope instances is not supported because of cyclical and self
|
||||
references. Avoid copying windows and scopes, as well as any other cyclical or
|
||||
self-referential structures. Note that trying to deep copy an object containing
|
||||
cyclical references that is neither a window nor a scope will cause infinite
|
||||
recursion and a stack overflow.
|
||||
recursion and a stack overflow.
|
||||
|
||||
+320
-141
@@ -49,7 +49,7 @@ changes to $location are reflected into the browser address bar.
|
||||
<tr>
|
||||
<td class="head">integration with angular application life-cycle</td>
|
||||
<td>none</td>
|
||||
<td>knows about all internal life-cycle phases, integrates with $watch, ...</td>
|
||||
<td>knows about all internal life-cycle phases, integrates with {@link ng.$rootScope.Scope#$watch $watch}, ...</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
@@ -60,7 +60,7 @@ changes to $location are reflected into the browser address bar.
|
||||
|
||||
<tr>
|
||||
<td class="head">aware of docroot/context from which the application is loaded</td>
|
||||
<td>no - window.location.path returns "/docroot/actual/path"</td>
|
||||
<td>no - window.location.pathname returns "/docroot/actual/path"</td>
|
||||
<td>yes - $location.path() returns "/actual/path"</td>
|
||||
</tr>
|
||||
|
||||
@@ -237,20 +237,6 @@ it('should show example', inject(
|
||||
));
|
||||
```
|
||||
|
||||
### Crawling your app
|
||||
|
||||
To allow indexing of your AJAX application, you have to add special meta tag in the head section of
|
||||
your document:
|
||||
|
||||
```html
|
||||
<meta name="fragment" content="!" />
|
||||
```
|
||||
|
||||
This will cause crawler bot to request links with `_escaped_fragment_` param so that your server
|
||||
can recognize the crawler and serve a HTML snapshots. For more information about this technique,
|
||||
see [Making AJAX Applications
|
||||
Crawlable](http://code.google.com/web/ajaxcrawling/docs/specification.html).
|
||||
|
||||
## HTML5 mode
|
||||
|
||||
In HTML5 mode, the `$location` service getters and setters interact with the browser URL address
|
||||
@@ -344,20 +330,6 @@ are not prefixed with `.` and will not be intercepted by the `otherwise` rule in
|
||||
Using this mode requires URL rewriting on server side, basically you have to rewrite all your links
|
||||
to entry point of your application (e.g. index.html)
|
||||
|
||||
### Crawling your app
|
||||
|
||||
If you want your AJAX application to be indexed by web crawlers, you will need to add the following
|
||||
meta tag to the HEAD section of your document:
|
||||
|
||||
```html
|
||||
<meta name="fragment" content="!" />
|
||||
```
|
||||
|
||||
This statement causes a crawler to request links with an empty `_escaped_fragment_` parameter so that
|
||||
your server can recognize the crawler and serve it HTML snapshots. For more information about this
|
||||
technique, see [Making AJAX
|
||||
Applications Crawlable](http://code.google.com/web/ajaxcrawling/docs/specification.html).
|
||||
|
||||
### Relative links
|
||||
|
||||
Be sure to check all relative links, images, scripts etc. You must either specify the url base in
|
||||
@@ -386,118 +358,308 @@ Note that when you type hashbang url into first browser (or vice versa) it doesn
|
||||
redirect to regular / hashbang url, as this conversion happens only during parsing the initial URL
|
||||
= on page reload.
|
||||
|
||||
In this examples we use `<base href="/base/index.html" />`
|
||||
<example>
|
||||
In these examples we use `<base href="/base/index.html" />`
|
||||
|
||||
#### Browser in HTML5 mode
|
||||
<example module="html5-mode" name="location-html5-mode">
|
||||
<file name="index.html">
|
||||
<div id="html5-mode" ng-controller="Html5Cntl">
|
||||
<h3>Browser with History API</h3>
|
||||
<div ng-address-bar browser="html5"></div><br><br>
|
||||
$location.protocol() = {{$location.protocol()}}<br>
|
||||
$location.host() = {{$location.host()}}<br>
|
||||
$location.port() = {{$location.port()}}<br>
|
||||
$location.path() = {{$location.path()}}<br>
|
||||
$location.search() = {{$location.search()}}<br>
|
||||
$location.hash() = {{$location.hash()}}<br>
|
||||
<a href="http://www.example.com/base/first?a=b">/base/first?a=b</a> |
|
||||
<a href="http://www.example.com/base/sec/ond?flag#hash">sec/ond?flag#hash</a> |
|
||||
<a href="/other-base/another?search">external</a>
|
||||
</div>
|
||||
|
||||
<div id="hashbang-mode" ng-controller="HashbangCntl">
|
||||
<h3>Browser without History API</h3>
|
||||
<div ng-address-bar browser="hashbang"></div><br><br>
|
||||
$location.protocol() = {{$location.protocol()}}<br>
|
||||
$location.host() = {{$location.host()}}<br>
|
||||
$location.port() = {{$location.port()}}<br>
|
||||
$location.path() = {{$location.path()}}<br>
|
||||
$location.search() = {{$location.search()}}<br>
|
||||
$location.hash() = {{$location.hash()}}<br>
|
||||
<a href="http://www.example.com/base/first?a=b">/base/first?a=b</a> |
|
||||
<a href="http://www.example.com/base/sec/ond?flag#hash">sec/ond?flag#hash</a> |
|
||||
<a href="/other-base/another?search">external</a>
|
||||
<div ng-controller="LocationController">
|
||||
<div ng-address-bar></div><br><br>
|
||||
<div>
|
||||
$location.protocol() = <span ng-bind="$location.protocol()"></span> <br>
|
||||
$location.host() = <span ng-bind="$location.host()"></span> <br>
|
||||
$location.port() = <span ng-bind="$location.port()"></span> <br>
|
||||
$location.path() = <span ng-bind="$location.path()"></span> <br>
|
||||
$location.search() = <span ng-bind="$location.search()"></span> <br>
|
||||
$location.hash() = <span ng-bind="$location.hash()"></span> <br>
|
||||
</div>
|
||||
<div id="navigation">
|
||||
<a href="http://www.example.com/base/first?a=b">/base/first?a=b</a> |
|
||||
<a href="http://www.example.com/base/sec/ond?flag#hash">sec/ond?flag#hash</a> |
|
||||
<a href="/other-base/another?search">external</a>
|
||||
</div>
|
||||
</div>
|
||||
</file>
|
||||
<file name="app.js">
|
||||
angular.module('html5-mode', ['fake-browser', 'address-bar'])
|
||||
|
||||
<file name="script.js">
|
||||
function FakeBrowser(initUrl, baseHref) {
|
||||
this.onUrlChange = function(fn) {
|
||||
this.urlChange = fn;
|
||||
.constant('initUrl', 'http://www.example.com/base/path?a=b#h')
|
||||
.constant('baseHref', '/base/index.html')
|
||||
.value('$sniffer', { history: true })
|
||||
|
||||
.controller("LocationController", function($scope, $location) {
|
||||
$scope.$location = {};
|
||||
angular.forEach("protocol host port path search hash".split(" "), function(method){
|
||||
$scope.$location[method] = function(){
|
||||
var result = $location[method].call($location);
|
||||
return angular.isObject(result) ? angular.toJson(result) : result;
|
||||
};
|
||||
});
|
||||
})
|
||||
|
||||
.config(function($locationProvider) {
|
||||
$locationProvider.html5Mode(true).hashPrefix('!');
|
||||
})
|
||||
|
||||
.run(function($rootElement) {
|
||||
$rootElement.on('click', function(e) { e.stopPropagation(); });
|
||||
});
|
||||
</file>
|
||||
|
||||
<file name="fakeBrowser.js">
|
||||
angular.module('fake-browser', [])
|
||||
|
||||
.config(function($provide) {
|
||||
$provide.decorator('$browser', function($delegate, baseHref, initUrl) {
|
||||
|
||||
$delegate.onUrlChange = function(fn) {
|
||||
this.urlChange = fn;
|
||||
};
|
||||
|
||||
$delegate.url = function() {
|
||||
return initUrl;
|
||||
};
|
||||
|
||||
this.url = function() {
|
||||
return initUrl;
|
||||
};
|
||||
$delegate.defer = function(fn, delay) {
|
||||
setTimeout(function() { fn(); }, delay || 0);
|
||||
};
|
||||
|
||||
this.defer = function(fn, delay) {
|
||||
setTimeout(function() { fn(); }, delay || 0);
|
||||
};
|
||||
$delegate.baseHref = function() {
|
||||
return baseHref;
|
||||
};
|
||||
|
||||
this.baseHref = function() {
|
||||
return baseHref;
|
||||
};
|
||||
return $delegate;
|
||||
});
|
||||
});
|
||||
</file>
|
||||
|
||||
this.notifyWhenOutstandingRequests = angular.noop;
|
||||
}
|
||||
<file name="addressBar.js">
|
||||
angular.module('address-bar', [])
|
||||
.directive('ngAddressBar', function($browser, $timeout) {
|
||||
return {
|
||||
template: 'Address: <input id="addressBar" type="text" style="width: 400px" >',
|
||||
link: function(scope, element, attrs){
|
||||
var input = element.children("input"), delay;
|
||||
|
||||
var browsers = {
|
||||
html5: new FakeBrowser('http://www.example.com/base/path?a=b#h', '/base/index.html'),
|
||||
hashbang: new FakeBrowser('http://www.example.com/base/index.html#!/path?a=b#h', '/base/index.html')
|
||||
};
|
||||
input.on('keypress keyup keydown', function(event) {
|
||||
delay = (!delay ? $timeout(fireUrlChange, 250) : null);
|
||||
event.stopPropagation();
|
||||
})
|
||||
.val($browser.url());
|
||||
|
||||
function Html5Cntl($scope, $location) {
|
||||
$scope.$location = $location;
|
||||
}
|
||||
|
||||
function HashbangCntl($scope, $location) {
|
||||
$scope.$location = $location;
|
||||
}
|
||||
|
||||
function initEnv(name) {
|
||||
var root = angular.element(document.getElementById(name + '-mode'));
|
||||
// We must kill a link to the injector for this element otherwise angular will
|
||||
// complain that it has been bootstrapped already.
|
||||
root.data('$injector', null);
|
||||
angular.bootstrap(root, [function($compileProvider, $locationProvider, $provide){
|
||||
$locationProvider.html5Mode(true).hashPrefix('!');
|
||||
|
||||
$provide.value('$browser', browsers[name]);
|
||||
$provide.value('$sniffer', {history: name == 'html5'});
|
||||
|
||||
$compileProvider.directive('ngAddressBar', function() {
|
||||
return function(scope, elm, attrs) {
|
||||
var browser = browsers[attrs.browser],
|
||||
input = angular.element('<input type="text" style="width: 400px">').val(browser.url()),
|
||||
delay;
|
||||
|
||||
input.on('keypress keyup keydown', function() {
|
||||
if (!delay) {
|
||||
delay = setTimeout(fireUrlChange, 250);
|
||||
}
|
||||
});
|
||||
|
||||
browser.url = function(url) {
|
||||
return input.val(url);
|
||||
};
|
||||
|
||||
elm.append('Address: ').append(input);
|
||||
|
||||
function fireUrlChange() {
|
||||
delay = null;
|
||||
browser.urlChange(input.val());
|
||||
}
|
||||
$browser.url = function(url) {
|
||||
return url ? input.val(url) : input.val();
|
||||
};
|
||||
});
|
||||
}]);
|
||||
root.on('click', function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
initEnv('html5');
|
||||
initEnv('hashbang');
|
||||
function fireUrlChange() {
|
||||
delay = null;
|
||||
$browser.urlChange(input.val());
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
|
||||
var addressBar = element(by.css("#addressBar")),
|
||||
url = 'http://www.example.com/base/path?a=b#h';
|
||||
|
||||
|
||||
it("should show fake browser info on load", function(){
|
||||
expect(addressBar.getAttribute('value')).toBe(url);
|
||||
|
||||
expect(element(by.binding('$location.protocol')).getText()).toBe('http');
|
||||
expect(element(by.binding('$location.host')).getText()).toBe('www.example.com');
|
||||
expect(element(by.binding('$location.port')).getText()).toBe('80');
|
||||
expect(element(by.binding('$location.path')).getText()).toBe('/path');
|
||||
expect(element(by.binding('$location.search')).getText()).toBe('{"a":"b"}');
|
||||
expect(element(by.binding('$location.hash')).getText()).toBe('h');
|
||||
|
||||
});
|
||||
|
||||
it("should change $location accordingly", function(){
|
||||
var navigation = element.all(by.css("#navigation a"));
|
||||
|
||||
navigation.get(0).click();
|
||||
|
||||
expect(addressBar.getAttribute('value')).toBe("http://www.example.com/base/first?a=b");
|
||||
|
||||
expect(element(by.binding('$location.protocol')).getText()).toBe('http');
|
||||
expect(element(by.binding('$location.host')).getText()).toBe('www.example.com');
|
||||
expect(element(by.binding('$location.port')).getText()).toBe('80');
|
||||
expect(element(by.binding('$location.path')).getText()).toBe('/first');
|
||||
expect(element(by.binding('$location.search')).getText()).toBe('{"a":"b"}');
|
||||
expect(element(by.binding('$location.hash')).getText()).toBe('');
|
||||
|
||||
|
||||
navigation.get(1).click();
|
||||
|
||||
expect(addressBar.getAttribute('value')).toBe("http://www.example.com/base/sec/ond?flag#hash");
|
||||
|
||||
expect(element(by.binding('$location.protocol')).getText()).toBe('http');
|
||||
expect(element(by.binding('$location.host')).getText()).toBe('www.example.com');
|
||||
expect(element(by.binding('$location.port')).getText()).toBe('80');
|
||||
expect(element(by.binding('$location.path')).getText()).toBe('/sec/ond');
|
||||
expect(element(by.binding('$location.search')).getText()).toBe('{"flag":true}');
|
||||
expect(element(by.binding('$location.hash')).getText()).toBe('hash');
|
||||
});
|
||||
|
||||
</file>
|
||||
|
||||
</example>
|
||||
|
||||
####Browser in HTML5 Fallback mode (Hashbang mode)
|
||||
<example module="hashbang-mode" name="location-hashbang-mode">
|
||||
<file name="index.html">
|
||||
<div ng-controller="LocationController">
|
||||
<div ng-address-bar></div><br><br>
|
||||
<div>
|
||||
$location.protocol() = <span ng-bind="$location.protocol()"></span> <br>
|
||||
$location.host() = <span ng-bind="$location.host()"></span> <br>
|
||||
$location.port() = <span ng-bind="$location.port()"></span> <br>
|
||||
$location.path() = <span ng-bind="$location.path()"></span> <br>
|
||||
$location.search() = <span ng-bind="$location.search()"></span> <br>
|
||||
$location.hash() = <span ng-bind="$location.hash()"></span> <br>
|
||||
</div>
|
||||
<div id="navigation">
|
||||
<a href="http://www.example.com/base/first?a=b">/base/first?a=b</a> |
|
||||
<a href="http://www.example.com/base/sec/ond?flag#hash">sec/ond?flag#hash</a> |
|
||||
<a href="/other-base/another?search">external</a>
|
||||
</div>
|
||||
</div>
|
||||
</file>
|
||||
<file name="app.js">
|
||||
angular.module('hashbang-mode', ['fake-browser', 'address-bar'])
|
||||
|
||||
.constant('initUrl', 'http://www.example.com/base/index.html#!/path?a=b#h')
|
||||
.constant('baseHref', '/base/index.html')
|
||||
.value('$sniffer', { history: false })
|
||||
|
||||
.config(function($locationProvider) {
|
||||
$locationProvider.html5Mode(true).hashPrefix('!');
|
||||
})
|
||||
|
||||
.controller("LocationController", function($scope, $location) {
|
||||
$scope.$location = {};
|
||||
angular.forEach("protocol host port path search hash".split(" "), function(method){
|
||||
$scope.$location[method] = function(){
|
||||
var result = $location[method].call($location);
|
||||
return angular.isObject(result) ? angular.toJson(result) : result;
|
||||
};
|
||||
});
|
||||
})
|
||||
|
||||
.run(function($rootElement) {
|
||||
$rootElement.on('click', function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
});
|
||||
|
||||
</file>
|
||||
|
||||
<file name="fakeBrowser.js">
|
||||
angular.module('fake-browser', [])
|
||||
|
||||
.config(function($provide) {
|
||||
$provide.decorator('$browser', function($delegate, baseHref, initUrl) {
|
||||
|
||||
$delegate.onUrlChange = function(fn) {
|
||||
this.urlChange = fn;
|
||||
};
|
||||
|
||||
$delegate.url = function() {
|
||||
return initUrl;
|
||||
};
|
||||
|
||||
$delegate.defer = function(fn, delay) {
|
||||
setTimeout(function() { fn(); }, delay || 0);
|
||||
};
|
||||
|
||||
$delegate.baseHref = function() {
|
||||
return baseHref;
|
||||
};
|
||||
|
||||
return $delegate;
|
||||
});
|
||||
});
|
||||
</file>
|
||||
|
||||
|
||||
<file name="addressBar.js">
|
||||
angular.module('address-bar', [])
|
||||
.directive('ngAddressBar', function($browser, $timeout) {
|
||||
return {
|
||||
template: 'Address: <input id="addressBar" type="text" style="width: 400px" >',
|
||||
link: function(scope, element, attrs){
|
||||
var input = element.children("input"), delay;
|
||||
|
||||
input.on('keypress keyup keydown', function(event) {
|
||||
delay = (!delay ? $timeout(fireUrlChange, 250) : null);
|
||||
event.stopPropagation();
|
||||
})
|
||||
.val($browser.url());
|
||||
|
||||
$browser.url = function(url) {
|
||||
return url ? input.val(url) : input.val();
|
||||
};
|
||||
|
||||
function fireUrlChange() {
|
||||
delay = null;
|
||||
$browser.urlChange(input.val());
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
|
||||
var addressBar = element(by.css("#addressBar")),
|
||||
url = 'http://www.example.com/base/index.html#!/path?a=b#h';
|
||||
|
||||
it("should show fake browser info on load", function(){
|
||||
expect(addressBar.getAttribute('value')).toBe(url);
|
||||
|
||||
expect(element(by.binding('$location.protocol')).getText()).toBe('http');
|
||||
expect(element(by.binding('$location.host')).getText()).toBe('www.example.com');
|
||||
expect(element(by.binding('$location.port')).getText()).toBe('80');
|
||||
expect(element(by.binding('$location.path')).getText()).toBe('/path');
|
||||
expect(element(by.binding('$location.search')).getText()).toBe('{"a":"b"}');
|
||||
expect(element(by.binding('$location.hash')).getText()).toBe('h');
|
||||
|
||||
});
|
||||
|
||||
it("should change $location accordingly", function(){
|
||||
var navigation = element.all(by.css("#navigation a"));
|
||||
|
||||
navigation.get(0).click();
|
||||
|
||||
expect(addressBar.getAttribute('value')).toBe("http://www.example.com/base/index.html#!/first?a=b");
|
||||
|
||||
expect(element(by.binding('$location.protocol')).getText()).toBe('http');
|
||||
expect(element(by.binding('$location.host')).getText()).toBe('www.example.com');
|
||||
expect(element(by.binding('$location.port')).getText()).toBe('80');
|
||||
expect(element(by.binding('$location.path')).getText()).toBe('/first');
|
||||
expect(element(by.binding('$location.search')).getText()).toBe('{"a":"b"}');
|
||||
expect(element(by.binding('$location.hash')).getText()).toBe('');
|
||||
|
||||
|
||||
navigation.get(1).click();
|
||||
|
||||
expect(addressBar.getAttribute('value')).toBe("http://www.example.com/base/index.html#!/sec/ond?flag#hash");
|
||||
|
||||
expect(element(by.binding('$location.protocol')).getText()).toBe('http');
|
||||
expect(element(by.binding('$location.host')).getText()).toBe('www.example.com');
|
||||
expect(element(by.binding('$location.port')).getText()).toBe('80');
|
||||
expect(element(by.binding('$location.path')).getText()).toBe('/sec/ond');
|
||||
expect(element(by.binding('$location.search')).getText()).toBe('{"flag":true}');
|
||||
expect(element(by.binding('$location.hash')).getText()).toBe('hash');
|
||||
|
||||
});
|
||||
</file>
|
||||
|
||||
</example>
|
||||
|
||||
# Caveats
|
||||
|
||||
@@ -510,10 +672,12 @@ use a lower level API, {@link ng.$window $window.location.href}.
|
||||
## Using $location outside of the scope life-cycle
|
||||
|
||||
`$location` knows about Angular's {@link ng.$rootScope.Scope scope} life-cycle. When a URL changes in
|
||||
the browser it updates the `$location` and calls `$apply` so that all $watchers / $observers are
|
||||
notified.
|
||||
the browser it updates the `$location` and calls `$apply` so that all
|
||||
{@link ng.$rootScope.Scope#$watch $watchers} /
|
||||
{@link ng.$compile.directive.Attributes#$observe $observers} are notified.
|
||||
When you change the `$location` inside the `$digest` phase everything is ok; `$location` will
|
||||
propagate this change into browser and will notify all the $watchers / $observers.
|
||||
propagate this change into browser and will notify all the {@link ng.$rootScope.Scope#$watch $watchers} /
|
||||
{@link ng.$compile.directive.Attributes#$observe $observers}.
|
||||
When you want to change the `$location` from outside Angular (for example, through a DOM Event or
|
||||
during testing) - you must call `$apply` to propagate the changes.
|
||||
|
||||
@@ -525,6 +689,20 @@ forward slash if it is missing.
|
||||
Note that the `!` prefix in the hashbang mode is not part of `$location.path()`; it is actually
|
||||
hashPrefix.
|
||||
|
||||
## Crawling your app
|
||||
|
||||
To allow indexing of your AJAX application, you have to add special meta tag in the head section of
|
||||
your document:
|
||||
|
||||
```html
|
||||
<meta name="fragment" content="!" />
|
||||
```
|
||||
|
||||
This will cause crawler bot to request links with `_escaped_fragment_` param so that your server
|
||||
can recognize the crawler and serve a HTML snapshots. For more information about this technique,
|
||||
see [Making AJAX Applications
|
||||
Crawlable](http://code.google.com/web/ajaxcrawling/docs/specification.html).
|
||||
|
||||
|
||||
# Testing with the $location service
|
||||
|
||||
@@ -632,25 +810,26 @@ then uses the information it obtains to compose hashbang URLs (such as
|
||||
|
||||
The Angular's compiler currently does not support two-way binding for methods (see [issue](https://github.com/angular/angular.js/issues/404)). If you should require two-way binding
|
||||
to the $location object (using {@link input[text] ngModel} directive on an input
|
||||
field), you will need to specify an extra model property (e.g. `locationPath`) with two watchers
|
||||
field), you will need to specify an extra model property (e.g. `locationPath`) with two {@link ng.$rootScope.Scope#$watch $watchers}
|
||||
which push $location updates in both directions. For example:
|
||||
<example>
|
||||
<example module="locationExample">
|
||||
<file name="index.html">
|
||||
<div ng-controller="LocationController">
|
||||
<input type="text" ng-model="locationPath" />
|
||||
</div>
|
||||
</file>
|
||||
<file name="script.js">
|
||||
function LocationController($scope, $location) {
|
||||
$scope.$watch('locationPath', function(path) {
|
||||
$location.path(path);
|
||||
});
|
||||
$scope.$watch(function() {
|
||||
return $location.path();
|
||||
}, function(path) {
|
||||
$scope.locationPath = path;
|
||||
});
|
||||
}
|
||||
angular.module('locationExample', [])
|
||||
.controller('LocationController', ['$scope', '$location', function ($scope, $location) {
|
||||
$scope.$watch('locationPath', function(path) {
|
||||
$location.path(path);
|
||||
});
|
||||
$scope.$watch(function() {
|
||||
return $location.path();
|
||||
}, function(path) {
|
||||
$scope.locationPath = path;
|
||||
});
|
||||
}]);
|
||||
</file>
|
||||
</example>
|
||||
|
||||
|
||||
@@ -251,15 +251,18 @@ Although the CSS is a little different then what we saw before, the idea is the
|
||||
A handful of common AngularJS directives support and trigger animation hooks whenever any major event occurs during its life cycle.
|
||||
The table below explains in detail which animation events are triggered
|
||||
|
||||
| Directive | Supported Animations |
|
||||
|-------------------------------------------------------------------------------------|------------------------------------------|
|
||||
| {@link ng.directive:ngRepeat#usage_animations ngRepeat} | enter, leave, and move |
|
||||
| {@link ngRoute.directive:ngView#usage_animations ngView} | enter and leave |
|
||||
| {@link ng.directive:ngInclude#usage_animations ngInclude} | enter and leave |
|
||||
| {@link ng.directive:ngSwitch#usage_animations ngSwitch} | enter and leave |
|
||||
| {@link ng.directive:ngIf#usage_animations ngIf} | enter and leave |
|
||||
| {@link ng.directive:ngClass#usage_animations ngClass or {{class}}} | add and remove |
|
||||
| {@link ng.directive:ngShow#usage_animations ngShow & ngHide} | add and remove (the ng-hide class value) |
|
||||
-| Directive | Supported Animations |
|
||||
-|-----------------------------------------------------------------|--------------------------------------------------------------------------|
|
||||
-| {@link ng.directive:ngRepeat#usage_animations ngRepeat} | enter, leave and move |
|
||||
-| {@link ngRoute.directive:ngView#usage_animations ngView} | enter and leave |
|
||||
-| {@link ng.directive:ngInclude#usage_animations ngInclude} | enter and leave |
|
||||
-| {@link ng.directive:ngSwitch#usage_animations ngSwitch} | enter and leave |
|
||||
-| {@link ng.directive:ngIf#usage_animations ngIf} | enter and leave |
|
||||
-| {@link ng.directive:ngClass#usage_animations ngClass} | add and remove |
|
||||
-| {@link ng.directive:ngShow#usage_animations ngShow & ngHide} | add and remove (the ng-hide class value) |
|
||||
-| {@link ng.directive:form#usage_animations form} | add and remove (dirty, pristine, valid, invalid & all other validations) |
|
||||
-| {@link ng.directive:ngModel#usage_animations ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) |
|
||||
|
||||
|
||||
For a full breakdown of the steps involved during each animation event, refer to the {@link ngAnimate.$animate API docs}.
|
||||
|
||||
|
||||
@@ -90,8 +90,8 @@ Here is an example of manually initializing Angular:
|
||||
<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
Hello {{'World'}}!
|
||||
<script src="http://code.angularjs.org/angular.js"></script>
|
||||
Hello {{greetMe}}!
|
||||
<script src="http://code.angularjs.org/snapshot/angular.js"></script>
|
||||
|
||||
<script>
|
||||
angular.module('myApp', [])
|
||||
|
||||
@@ -198,7 +198,7 @@ This should help give you an idea of what Angular does internally.
|
||||
|
||||
// Step 3: link the compiled template with the scope.
|
||||
var element = linkFn(scope);
|
||||
|
||||
|
||||
// Step 4: Append to DOM (optional)
|
||||
parent.appendChild(element);
|
||||
```
|
||||
@@ -226,7 +226,7 @@ moved to the compile function for performance reasons.
|
||||
To understand, let's look at a real-world example with `ngRepeat`:
|
||||
|
||||
```html
|
||||
Hello {{user}}, you have these actions:
|
||||
Hello {{user.name}}, you have these actions:
|
||||
<ul>
|
||||
<li ng-repeat="action in user.actions">
|
||||
{{action.description}}
|
||||
@@ -236,7 +236,7 @@ Hello {{user}}, you have these actions:
|
||||
|
||||
When the above example is compiled, the compiler visits every node and looks for directives.
|
||||
|
||||
`{{user}}` matches the {@link ng.$interpolate interpolation directive}
|
||||
`{{user.name}}` matches the {@link ng.$interpolate interpolation directive}
|
||||
and `ng-repeat` matches the {@link ng.directive:ngRepeat `ngRepeat` directive}.
|
||||
|
||||
But {@link ng.directive:ngRepeat ngRepeat} has a dilemma.
|
||||
|
||||
@@ -11,7 +11,7 @@ For a more in-depth explanation, see the {@link tutorial/ tutorial}.
|
||||
|------------------|------------------------------------------|
|
||||
|{@link concepts#template Template} | HTML with additional markup |
|
||||
|{@link concepts#directive Directives} | extend HTML with custom attributes and elements |
|
||||
|{@link concepts#model Model} | the data that is shown to the user and with which the user interacts |
|
||||
|{@link concepts#model Model} | the data shown to the user in the view and with which the user interacts |
|
||||
|{@link concepts#scope Scope} | context where the model is stored so that controllers, directives and expressions can access it |
|
||||
|{@link concepts#expression Expressions} | access variables and functions from the scope |
|
||||
|{@link concepts#compiler Compiler} | parses the template and instantiates directives and expressions |
|
||||
@@ -19,9 +19,9 @@ For a more in-depth explanation, see the {@link tutorial/ tutorial}.
|
||||
|{@link concepts#view View} | what the user sees (the DOM) |
|
||||
|{@link concepts#databinding Data Binding} | sync data between the model and the view |
|
||||
|{@link concepts#controller Controller} | the business logic behind views |
|
||||
|{@link concepts#di Dependency Injection} | Creates and wires objects / functions |
|
||||
|{@link concepts#di Dependency Injection} | Creates and wires objects and functions |
|
||||
|{@link concepts#injector Injector} | dependency injection container |
|
||||
|{@link concepts#module Module} | configures the Injector |
|
||||
|{@link concepts#module Module} | a container for the different parts of an app including controllers, services, filters, directives which configures the Injector |
|
||||
|{@link concepts#service Service} | reusable business logic independent of views |
|
||||
|
||||
|
||||
@@ -37,10 +37,10 @@ Let's start with input fields for quantity and cost whose values are multiplied
|
||||
<div ng-app ng-init="qty=1;cost=2">
|
||||
<b>Invoice:</b>
|
||||
<div>
|
||||
Quantity: <input type="number" ng-model="qty" required >
|
||||
Quantity: <input type="number" ng-model="qty">
|
||||
</div>
|
||||
<div>
|
||||
Costs: <input type="number" ng-model="cost" required >
|
||||
Costs: <input type="number" ng-model="cost">
|
||||
</div>
|
||||
<div>
|
||||
<b>Total:</b> {{qty * cost | currency}}
|
||||
@@ -62,11 +62,8 @@ The first kind of new markup are the so called <a name="directive">"{@link direc
|
||||
They apply special behavior to attributes or elements in the HTML. In the example above we use the
|
||||
{@link ng.directive:ngApp `ng-app`} attribute, which is linked to a directive that automatically
|
||||
initializes our application. Angular also defines a directive for the {@link ng.directive:input `input`}
|
||||
element that adds extra behavior to the element. E.g. it is able to automatically validate that the entered
|
||||
text is non empty by evaluating the `required` attribute.
|
||||
The {@link ng.directive:ngModel `ng-model`} directive stores/updates
|
||||
the value of the input field into/from a variable and shows the validation state of the input field by
|
||||
adding css classes. In the example we use these css classes to mark an empty input field with a red border.
|
||||
element that adds extra behavior to the element. The {@link ng.directive:ngModel `ng-model`} directive
|
||||
stores/updates the value of the input field into/from a variable.
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Custom directives to access the DOM**: In Angular, the only place where an application touches the DOM is
|
||||
@@ -254,7 +251,7 @@ Let's refactor our example and move the currency conversion into a service in an
|
||||
|
||||
What changed?
|
||||
We moved the `convertCurrency` function and the definition of the existing currencies
|
||||
into the new file `finance.js`. But how does the controller
|
||||
into the new file `finance2.js`. But how does the controller
|
||||
get a hold of the now separated function?
|
||||
|
||||
This is where <a name="di">"{@link di Dependency Injection}"</a> comes into play.
|
||||
@@ -323,7 +320,7 @@ The following example shows how this is done with Angular:
|
||||
angular.module('finance3', [])
|
||||
.factory('currencyConverter', ['$http', function($http) {
|
||||
var YAHOO_FINANCE_URL_PATTERN =
|
||||
'http://query.yahooapis.com/v1/public/yql?q=select * from '+
|
||||
'//query.yahooapis.com/v1/public/yql?q=select * from '+
|
||||
'yahoo.finance.xchange where pair in ("PAIRS")&format=json&'+
|
||||
'env=store://datatables.org/alltableswithkeys&callback=JSON_CALLBACK';
|
||||
var currencies = ['USD', 'EUR', 'CNY'];
|
||||
|
||||
@@ -37,27 +37,8 @@ The properties contain the **view model** (the model that will be presented by t
|
||||
`$scope` properties will be available to the template at the point in the DOM where the Controller
|
||||
is registered.
|
||||
|
||||
The following example shows a very simple constructor function for a Controller, `GreetingController`,
|
||||
which attaches a `greeting` property containing the string `'Hola!'` to the `$scope`:
|
||||
|
||||
```js
|
||||
function GreetingController($scope) {
|
||||
$scope.greeting = 'Hola!';
|
||||
}
|
||||
```
|
||||
|
||||
Once the Controller has been attached to the DOM, the `greeting` property can be data-bound to the
|
||||
template:
|
||||
|
||||
```js
|
||||
<div ng-controller="GreetingController">
|
||||
{{ greeting }}
|
||||
</div>
|
||||
```
|
||||
|
||||
**NOTE**: Although Angular allows you to create Controller functions in the global scope, this is
|
||||
not recommended. In a real application you should use the `.controller` method of your
|
||||
{@link module Angular Module} for your application as follows:
|
||||
The following example demonstrates creating a `GreetingController`, which attaches a `greeting`
|
||||
property containing the string `'Hola!'` to the `$scope`:
|
||||
|
||||
```js
|
||||
var myApp = angular.module('myApp',[]);
|
||||
@@ -67,9 +48,24 @@ myApp.controller('GreetingController', ['$scope', function($scope) {
|
||||
}]);
|
||||
```
|
||||
|
||||
We create an {@link module Angular Module}, `myApp`, for our application. Then we add the controller's
|
||||
constructor function to the module using the `.controller()` method. This keeps the controller's
|
||||
constructor function out of the global scope.
|
||||
|
||||
<div class="alert alert-info">
|
||||
We have used an **inline injection annotation** to explicitly specify the dependency
|
||||
of the Controller on the `$scope` service provided by Angular. See the guide on
|
||||
[Dependency Injection](http://docs.angularjs.org/guide/di) for more information.
|
||||
{@link guide/di Dependency Injection} for more information.
|
||||
</div>
|
||||
|
||||
We attach our controller to the DOM using the `ng-controller` directive. The `greeting` property can
|
||||
now be data-bound to the template:
|
||||
|
||||
```js
|
||||
<div ng-controller="GreetingController">
|
||||
{{ greeting }}
|
||||
</div>
|
||||
```
|
||||
|
||||
|
||||
# Adding Behavior to a Scope Object
|
||||
@@ -243,7 +239,7 @@ more information about scope inheritance.
|
||||
}]);
|
||||
myApp.controller('GrandChildController', ['$scope', function($scope) {
|
||||
$scope.timeOfDay = 'evening';
|
||||
$scope.name = 'Gingerbreak Baby';
|
||||
$scope.name = 'Gingerbread Baby';
|
||||
}]);
|
||||
</file>
|
||||
</example>
|
||||
@@ -273,8 +269,8 @@ involves injecting the {@link ng.$rootScope $rootScope} and {@link ng.$controlle
|
||||
|
||||
myApp.controller('MyController', function($scope) {
|
||||
$scope.spices = [{"name":"pasilla", "spiciness":"mild"},
|
||||
{"name":"jalapeno", "spiceiness":"hot hot hot!"},
|
||||
{"name":"habanero", "spiceness":"LAVA HOT!!"}];
|
||||
{"name":"jalapeno", "spiciness":"hot hot hot!"},
|
||||
{"name":"habanero", "spiciness":"LAVA HOT!!"}];
|
||||
$scope.spice = "habanero";
|
||||
});
|
||||
```
|
||||
@@ -333,6 +329,3 @@ describe('state', function() {
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Angular sets these CSS classes. It is up to your application to provide useful s
|
||||
# CSS classes used by angular
|
||||
|
||||
* `ng-scope`
|
||||
- **Usage:** angular applies this class to any element that where a new {@link ng.$rootScope.Scope scope}
|
||||
- **Usage:** angular applies this class to any element for which a new {@link api/ng.$rootScope.Scope scope}
|
||||
is defined. (see {@link guide/scope scope} guide for more information about scopes)
|
||||
|
||||
* `ng-binding`
|
||||
|
||||
@@ -9,7 +9,7 @@ When the model changes, the view reflects the change, and vice versa.
|
||||
|
||||
## Data Binding in Classical Template Systems
|
||||
|
||||
<img class="right" src="img/One_Way_Data_Binding.png"/>
|
||||
<img class="right" src="img/One_Way_Data_Binding.png"/><br />
|
||||
Most templating systems bind data in only one direction: they merge template and model components
|
||||
together into a view. After the merge occurs, changes to the model
|
||||
or related sections of the view are NOT automatically reflected in the view. Worse, any changes
|
||||
@@ -18,7 +18,7 @@ to write code that constantly syncs the view with the model and the model with t
|
||||
|
||||
## Data Binding in Angular Templates
|
||||
|
||||
<img class="right" src="img/Two_Way_Data_Binding.png"/>
|
||||
<img class="right" src="img/Two_Way_Data_Binding.png"/><br />
|
||||
Angular templates work differently. First the template (which is the uncompiled HTML along with
|
||||
any additional markup or directives) is compiled on the browser. The compilation step produces a
|
||||
live view. Any changes to the view are immediately reflected in the model, and any changes in
|
||||
|
||||
+144
-85
@@ -4,30 +4,29 @@
|
||||
|
||||
# Dependency Injection
|
||||
|
||||
Dependency Injection (DI) is a software design pattern that deals with how code gets hold of its
|
||||
dependencies.
|
||||
Dependency Injection (DI) is a software design pattern that deals with how components get hold of
|
||||
their dependencies.
|
||||
|
||||
The Angular injector subsystem is in charge of service instantiation, resolution
|
||||
of dependencies, and provision of dependencies to components as requested.
|
||||
The Angular injector subsystem is in charge of creating components, resolving their dependencies,
|
||||
and providing them to other components as requested.
|
||||
|
||||
For in-depth discussion about DI, see
|
||||
[Dependency Injection](http://en.wikipedia.org/wiki/Dependency_injection) at Wikipedia,
|
||||
[Inversion of Control](http://martinfowler.com/articles/injection.html) by Martin Fowler,
|
||||
or read about DI in your favorite software design pattern book.
|
||||
|
||||
## DI in a nutshell
|
||||
## DI in a Nutshell
|
||||
|
||||
There are only three ways an object or a function can get a hold of its dependencies:
|
||||
|
||||
1. The dependency can be created, typically using the `new` operator.
|
||||
2. The dependency can be looked up by referring to a global variable.
|
||||
3. The dependency can be passed in to where it is needed.
|
||||
There are only three ways a component (object or function) can get a hold of its dependencies:
|
||||
|
||||
1. The component can create the dependency, typically using the `new` operator.
|
||||
2. The component can look up the dependency, by referring to a global variable.
|
||||
3. The component can have the dependency passed to it where it is needed.
|
||||
|
||||
The first two options of creating or looking up dependencies are not optimal because they hard
|
||||
code the dependency. This makes it difficult, if not impossible, to modify the dependencies.
|
||||
This is especially problematic in tests, where it is often desirable to provide mock dependencies
|
||||
for test isolation.
|
||||
code the dependency to the component. This makes it difficult, if not impossible, to modify the
|
||||
dependencies. This is especially problematic in tests, where it is often desirable to provide mock
|
||||
dependencies for test isolation.
|
||||
|
||||
The third option is the most viable, since it removes the responsibility of locating the
|
||||
dependency from the component. The dependency is simply handed to the component.
|
||||
@@ -42,8 +41,8 @@ SomeClass.prototype.doSomething = function(name) {
|
||||
}
|
||||
```
|
||||
|
||||
In the above example `SomeClass` is not concerned with locating the `greeter` dependency, it
|
||||
is simply handed the `greeter` at runtime.
|
||||
In the above example `SomeClass` is not concerned with creating or locating the `greeter`
|
||||
dependency, it is simply handed the `greeter` when it is instantiated.
|
||||
|
||||
This is desirable, but it puts the responsibility of getting hold of the dependency on the
|
||||
code that constructs `SomeClass`.
|
||||
@@ -51,76 +50,92 @@ code that constructs `SomeClass`.
|
||||
<img class="pull-right" style="padding-left: 3em; padding-bottom: 1em;" src="img/guide/concepts-module-injector.png">
|
||||
|
||||
To manage the responsibility of dependency creation, each Angular application has an {@link
|
||||
angular.injector injector}. The injector is a service locator that is responsible for
|
||||
angular.injector injector}. The injector is a
|
||||
[service locator](http://en.wikipedia.org/wiki/Service_locator_pattern) that is responsible for
|
||||
construction and lookup of dependencies.
|
||||
|
||||
Here is an example of using the injector service:
|
||||
|
||||
```js
|
||||
// Provide the wiring information in a module
|
||||
angular.module('myModule', []).
|
||||
var myModule = angular.module('myModule', []);
|
||||
```
|
||||
|
||||
// Teach the injector how to build a 'greeter'
|
||||
// Notice that greeter itself is dependent on '$window'
|
||||
factory('greeter', function($window) {
|
||||
// This is a factory function, and is responsible for
|
||||
// creating the 'greet' service.
|
||||
return {
|
||||
greet: function(text) {
|
||||
$window.alert(text);
|
||||
}
|
||||
};
|
||||
});
|
||||
Teach the injector how to build a `greeter` service. Notice that `greeter` is dependent on the
|
||||
`$window` service. The `greeter` service is an object that contains a `greet` method.
|
||||
|
||||
// New injector is created from the module.
|
||||
// (This is usually done automatically by angular bootstrap)
|
||||
```js
|
||||
myModule.factory('greeter', function($window) {
|
||||
return {
|
||||
greet: function(text) {
|
||||
$window.alert(text);
|
||||
}
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
Create a new injector that can provide components defined in our `myModule` module and request our
|
||||
`greeter` service from the injector. (This is usually done automatically by angular bootstrap).
|
||||
|
||||
```js
|
||||
var injector = angular.injector(['myModule', 'ng']);
|
||||
|
||||
// Request any dependency from the injector
|
||||
var greeter = injector.get('greeter');
|
||||
```
|
||||
|
||||
Asking for dependencies solves the issue of hard coding, but it also means that the injector needs
|
||||
to be passed throughout the application. Passing the injector breaks the [Law of Demeter](http://en.wikipedia.org/wiki/Law_of_Demeter). To remedy this, we turn the
|
||||
dependency lookup responsibility to the injector by declaring the dependencies as in this example:
|
||||
to be passed throughout the application. Passing the injector breaks the
|
||||
[Law of Demeter](http://en.wikipedia.org/wiki/Law_of_Demeter). To remedy this, we use a declarative
|
||||
notation in our HTML templates, to hand the responsibility of creating components over to the
|
||||
injector, as in this example:
|
||||
|
||||
```html
|
||||
<!-- Given this HTML -->
|
||||
<div ng-controller="MyController">
|
||||
<button ng-click="sayHello()">Hello</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
// And this controller definition
|
||||
function MyController($scope, greeter) {
|
||||
$scope.sayHello = function() {
|
||||
greeter.greet('Hello World');
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
// The 'ng-controller' directive does this behind the scenes
|
||||
When Angular compiles the HTML, it processes the `ng-controller` directive, which in turn
|
||||
asks the injector to create an instance of the controller and its dependencies.
|
||||
|
||||
```js
|
||||
injector.instantiate(MyController);
|
||||
```
|
||||
|
||||
Notice that by having the `ng-controller` instantiate the class, it can satisfy all of the
|
||||
dependencies of `MyController` without the controller ever knowing about the injector. This is
|
||||
the best outcome. The application code simply asks for the dependencies it needs, without having to
|
||||
deal with the injector. This setup does not break the Law of Demeter.
|
||||
This is all done behinds the scenes. Notice that by having the `ng-controller` ask the injector to
|
||||
instantiate the class, it can satisfy all of the dependencies of `MyController` without the
|
||||
controller ever knowing about the injector.
|
||||
|
||||
This is the best outcome. The application code simply declares the dependencies it needs, without
|
||||
having to deal with the injector. This setup does not break the Law of Demeter.
|
||||
|
||||
|
||||
## Dependency Annotation
|
||||
|
||||
How does the injector know what service needs to be injected?
|
||||
**How does the injector know what components need to be injected?**
|
||||
|
||||
The application developer needs to provide annotation information that the injector uses in order
|
||||
to resolve the dependencies. Throughout Angular, certain API functions are invoked using the
|
||||
injector, as per the API documentation. The injector needs to know what services to inject into
|
||||
the function. Below are three equivalent ways of annotating your code with service name
|
||||
information. These can be used interchangeably as you see fit and are equivalent.
|
||||
the function. There are three equivalent ways of annotating your code with service name
|
||||
information:
|
||||
|
||||
### Inferring Dependencies
|
||||
- Implicitly from the function parameter names
|
||||
- Using the `$inject` property annotation
|
||||
- Using the inline array annotation
|
||||
|
||||
The simplest way to get hold of the dependencies, is to assume that the function parameter names
|
||||
These can be used interchangeably as you see fit and are equivalent.
|
||||
|
||||
### Implicit Dependencies
|
||||
|
||||
The simplest way to get hold of the dependencies is to assume that the function parameter names
|
||||
are the names of the dependencies.
|
||||
|
||||
```js
|
||||
@@ -129,16 +144,17 @@ function MyController($scope, greeter) {
|
||||
}
|
||||
```
|
||||
|
||||
Given a function the injector can infer the names of the service to inject by examining the
|
||||
Given a function the injector can infer the names of the services to inject by examining the
|
||||
function declaration and extracting the parameter names. In the above example `$scope`, and
|
||||
`greeter` are two services which need to be injected into the function.
|
||||
|
||||
While straightforward, this method will not work with JavaScript minifiers/obfuscators as they
|
||||
rename the method parameter names. This makes this way of annotating only useful for [pretotyping](http://www.pretotyping.org/), and demo applications.
|
||||
rename the method parameter names. This makes this way of annotating only useful for
|
||||
[pretotyping](http://www.pretotyping.org/), and demo applications.
|
||||
|
||||
### `$inject` Annotation
|
||||
### `$inject` Property Annotation
|
||||
|
||||
To allow the minifers to rename the function parameters and still be able to inject right services
|
||||
To allow the minifiers to rename the function parameters and still be able to inject the right services,
|
||||
the function needs to be annotated with the `$inject` property. The `$inject` property is an array
|
||||
of service names to inject.
|
||||
|
||||
@@ -149,18 +165,18 @@ var MyController = function(renamed$scope, renamedGreeter) {
|
||||
MyController['$inject'] = ['$scope', 'greeter'];
|
||||
```
|
||||
|
||||
In this scenario the ordering of the values in the '$inject' array must match the ordering of the arguments to inject.
|
||||
Using above code snippet as an example, '$scope' will be injected into 'renamed$scope' and 'greeter' into 'renamedGreeter'.
|
||||
Care must be taken that the `$inject` annotation is kept in sync with the actual arguments in the
|
||||
function declaration.
|
||||
In this scenario the ordering of the values in the `$inject` array must match the ordering of the
|
||||
arguments to inject. Using the above code snippet as an example, `$scope` will be injected into
|
||||
`renamed$scope` and `greeter` into `renamedGreeter`. Care must be taken that the `$inject`
|
||||
annotation is kept in sync with the actual arguments in the function declaration.
|
||||
|
||||
This method of annotation is useful for controller declarations since it assigns the annotation
|
||||
information with the function.
|
||||
|
||||
### Inline Annotation
|
||||
### Inline Array Annotation
|
||||
|
||||
Sometimes using the `$inject` annotation style is not convenient such as when annotating
|
||||
directives.
|
||||
directives or services defined inline by a factory function.
|
||||
|
||||
For example:
|
||||
|
||||
@@ -190,18 +206,76 @@ someModule.factory('greeter', ['$window', function(renamed$window) {
|
||||
}]);
|
||||
```
|
||||
|
||||
Here, instead of simply providing the factory function, we pass an array whose elements consist of
|
||||
a list of strings (the names of the dependencies) followed by the function itself.
|
||||
|
||||
Keep in mind that all of the annotation styles are equivalent and can be used anywhere in Angular
|
||||
where injection is supported.
|
||||
|
||||
## Where can I use DI?
|
||||
## Where Can I Use DI?
|
||||
|
||||
DI is pervasive throughout Angular. You can use it in controllers, services, directives, filters,
|
||||
animations, and `run` and `config` blocks.
|
||||
DI is pervasive throughout Angular. You can use it when defining components or when providing `run`
|
||||
and `config` blocks for a module.
|
||||
|
||||
### DI in controllers
|
||||
- Components such as services, directives, filters and animations are defined by an injectable factory
|
||||
method or constructor function. These components can be injected with "service" and "value"
|
||||
components as dependencies.
|
||||
|
||||
Controllers are classes which are responsible for application behavior. The recommended way of
|
||||
declaring controllers is using the array notation:
|
||||
- The `run` method accepts a function, which can be injected with "service", "value" and "constant"
|
||||
components as dependencies. Note that you cannot inject "providers" into `run` blocks.
|
||||
|
||||
- The `config` method accepts a function, which can be injected with "provider" and "constant"
|
||||
components as dependencies. Note that you cannot inject "service" or "value" components into
|
||||
configuration
|
||||
|
||||
- Controllers are defined by a constructor function, which can be injected with any of the "service"
|
||||
and "value" components as dependencies, but they can also be provided with special dependencies. See
|
||||
{@link di#controllers Controllers} below for a list of these special dependencies.
|
||||
|
||||
See {@link module#module-loading-dependencies Modules} for more details about injecting dependencies
|
||||
into `run` and `config` blocks.
|
||||
|
||||
|
||||
### Factory Methods
|
||||
|
||||
Factory methods are responsible for creating most objects in Angular. Examples are directives,
|
||||
services, and filters. The factory methods are registered with the module, and the recommended way
|
||||
of declaring factories is:
|
||||
|
||||
```js
|
||||
angular.module('myModule', [])
|
||||
.factory('serviceId', ['depService', function(depService) {
|
||||
...
|
||||
}])
|
||||
.directive('directiveName', ['depService', function(depService) {
|
||||
...
|
||||
}])
|
||||
.filter('filterName', ['depService', function(depService) {
|
||||
...
|
||||
}]);
|
||||
```
|
||||
|
||||
### Module Methods
|
||||
|
||||
We can specify functions to run at configuration and run time for a module by calling the `run` and
|
||||
`config` methods. These functions are injectable with dependencies just like the factory functions
|
||||
above.
|
||||
|
||||
```js
|
||||
angular.module('myModule', [])
|
||||
.config(['depProvider', function(depProvider){
|
||||
...
|
||||
}])
|
||||
.run(['depService', function(depService) {
|
||||
...
|
||||
}]);
|
||||
```
|
||||
|
||||
### Controllers
|
||||
|
||||
Controllers are "classes" or "constructor functions" that are responsible for providing the
|
||||
application behavior that supports the declarative markup in the template. The recommended way of
|
||||
declaring Controllers is using the array notation:
|
||||
|
||||
```js
|
||||
someModule.controller('MyController', ['$scope', 'dep1', 'dep2', function($scope, dep1, dep2) {
|
||||
@@ -215,28 +289,13 @@ someModule.controller('MyController', ['$scope', 'dep1', 'dep2', function($scope
|
||||
|
||||
This avoids the creation of global functions for controllers and also protects against minification.
|
||||
|
||||
Controllers are special in that, unlike services, there can be many instances of them in the
|
||||
application. For example, there would be one instance for every `ng-controller` directive in the template.
|
||||
|
||||
### Factory methods
|
||||
Moreover, additional dependencies are made available to Controllers:
|
||||
|
||||
Factory methods are responsible for creating most objects in Angular. Examples are directives,
|
||||
services, and filters. The factory methods are registered with the module, and the recommended way
|
||||
of declaring factories is:
|
||||
|
||||
```js
|
||||
angular.module('myModule', []).
|
||||
config(['depProvider', function(depProvider){
|
||||
...
|
||||
}]).
|
||||
factory('serviceId', ['depService', function(depService) {
|
||||
...
|
||||
}]).
|
||||
directive('directiveName', ['depService', function(depService) {
|
||||
...
|
||||
}]).
|
||||
filter('filterName', ['depService', function(depService) {
|
||||
...
|
||||
}]).
|
||||
run(['depService', function(depService) {
|
||||
...
|
||||
}]);
|
||||
```
|
||||
* {@link scope `$scope`}: Controllers are always associated with a point in the DOM and so are provided with
|
||||
access to the {@link scope scope} at that point. Other components, such as services only have access to the
|
||||
singleton {@link $rootScope} service.
|
||||
* {@link $route} resolves: If a controller is instantiated as part of a route, then any values that
|
||||
are resolved as part of the route are made available for injection into the controller.
|
||||
|
||||
@@ -18,7 +18,7 @@ how to implement them.
|
||||
## What are Directives?
|
||||
|
||||
At a high level, directives are markers on a DOM element (such as an attribute, element
|
||||
name, or CSS class) that tell AngularJS's **HTML compiler** ({@link ng.$compile `$compile`}) to
|
||||
name, comment or CSS class) that tell AngularJS's **HTML compiler** ({@link ng.$compile `$compile`}) to
|
||||
attach a specified behavior to that DOM element or even transform the DOM element and its children.
|
||||
|
||||
Angular comes with a set of these directives built-in, like `ngBind`, `ngModel`, and `ngView`.
|
||||
@@ -282,7 +282,7 @@ using `templateUrl` instead:
|
||||
Great! But what if we wanted to have our directive match the tag name `<my-customer>` instead?
|
||||
If we simply put a `<my-customer>` element into the HTML, it doesn't work.
|
||||
|
||||
<div class="alert alert-waring">
|
||||
<div class="alert alert-warning">
|
||||
**Note:** When you create a directive, it is restricted to attribute only by default. In order to
|
||||
create directives that are triggered by element or class name, you need to use the `restrict` option.
|
||||
</div>
|
||||
@@ -352,7 +352,7 @@ element as a customer component.
|
||||
Our `myCustomer` directive above is great, but it has a fatal flaw. We can only use it once within a
|
||||
given scope.
|
||||
|
||||
In its current implementation, we'd need to create a different controller each time In order to
|
||||
In its current implementation, we'd need to create a different controller each time in order to
|
||||
re-use such a directive:
|
||||
|
||||
<example module="docsScopeProblemExample">
|
||||
@@ -475,7 +475,6 @@ within our directive's template:
|
||||
angular.module('docsIsolationExample', [])
|
||||
.controller('Controller', ['$scope', function($scope) {
|
||||
$scope.naomi = { name: 'Naomi', address: '1600 Amphitheatre' };
|
||||
|
||||
$scope.vojta = { name: 'Vojta', address: '3456 Somewhere Else' };
|
||||
}])
|
||||
.directive('myCustomer', function() {
|
||||
@@ -510,8 +509,8 @@ that you explicitly pass in.
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Note:** Normally, a scope prototypically inherits from its parent. An isolated scope does not.
|
||||
See the {@link guide/directive#isolating-the-scope-of-a-directive
|
||||
"Isolating the Scope of a Directive"} section for more information about isolate scopes.
|
||||
See the {@link api/ng/service/$compile#directive-definition-object
|
||||
"Directive Definition Object - scope"} section for more information about isolate scopes.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success">
|
||||
@@ -537,7 +536,7 @@ where:
|
||||
In our `link` function, we want to update the displayed time once a second, or whenever a user
|
||||
changes the time formatting string that our directive binds to. We will use the `$interval` service
|
||||
to call a handler on a regular basis. This is easier than using `$timeout` but also works better with
|
||||
end 2 end testing, where we want to ensure that all $timeouts have completed before completing the test.
|
||||
end-to-end testing, where we want to ensure that all `$timeout`s have completed before completing the test.
|
||||
We also want to remove the `$interval` if the directive is deleted so we don't introduce a memory leak.
|
||||
|
||||
<example module="docsTimeDirective">
|
||||
@@ -901,10 +900,39 @@ So where does this `myTabs` controller come from? Directives can specify control
|
||||
the unsurprisingly named `controller` option. As you can see, the `myTabs` directive uses this
|
||||
option. Just like `ngController`, this option attaches a controller to the template of the directive.
|
||||
|
||||
If it is necessary to reference the controller or any functions bound to the controller's scope in
|
||||
the template, you can use the option `controllerAs` to specify the name of the controller as an alias.
|
||||
The directive needs to define a scope for this configuration to be used. This is particularly useful
|
||||
in the case when the directive is used as a component.
|
||||
|
||||
Looking back at `myPane`'s definition, notice the last argument in its `link` function: `tabsCtrl`.
|
||||
When a directive requires a controller, it receives that controller as the fourth argument of its
|
||||
`link` function. Taking advantage of this, `myPane` can call the `addPane` function of `myTabs`.
|
||||
|
||||
If multiple controllers are required, the `require` option of the directive can take an array argument.
|
||||
The corresponding parameter being sent to the `link` function will also be an array.
|
||||
|
||||
```js
|
||||
angular.module('docsTabsExample', [])
|
||||
.directive('myPane', function() {
|
||||
return {
|
||||
require: ['^myTabs', '^ngModel'],
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
scope: {
|
||||
title: '@'
|
||||
},
|
||||
link: function(scope, element, attrs, controllers) {
|
||||
var tabsCtrl = controllers[0],
|
||||
modelCtrl = controllers[1];
|
||||
|
||||
tabsCtrl.addPane(scope);
|
||||
},
|
||||
templateUrl: 'my-pane.html'
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
Savvy readers may be wondering what the difference is between `link` and `controller`.
|
||||
The basic difference is that `controller` can expose an API, and `link` functions can interact with
|
||||
controllers using `require`.
|
||||
|
||||
@@ -3,319 +3,83 @@
|
||||
@name E2E Testing
|
||||
@description
|
||||
|
||||
# E2E Testing
|
||||
|
||||
<div class="alert alert-danger">
|
||||
**Note:** Angular Scenario Runner is depricated. If you're starting a new Angular project,
|
||||
consider using [Protractor](https://github.com/angular/protractor).
|
||||
**Note:** In the past, end to end testing could be done with a deprecated tool called
|
||||
[Angular Scenario Runner](http://code.angularjs.org/1.2.16/docs/guide/e2e-testing). That tool
|
||||
is now in maintenance mode.
|
||||
</div>
|
||||
|
||||
# E2E Testing with the Angular Scenario Runner
|
||||
|
||||
As applications grow in size and complexity, it becomes unrealistic to rely on manual testing to
|
||||
verify the correctness of new features, catch bugs and notice regressions.
|
||||
verify the correctness of new features, catch bugs and notice regressions. End to end tests
|
||||
are the first line of defense for catching bugs, but sometimes issues come up with integration
|
||||
between components which can't be captured in a unit test. End to end tests are made to find
|
||||
these problems.
|
||||
|
||||
To solve this problem, we have built an Angular Scenario Runner which simulates user interactions
|
||||
that will help you verify the health of your Angular application.
|
||||
We have built [Protractor](https://github.com/angular/protractor), an end
|
||||
to end test runner which simulates user interactions that will help you verify the health of your
|
||||
Angular application.
|
||||
|
||||
## Overview
|
||||
## Using Protractor
|
||||
|
||||
You write scenario tests in JavaScript. These tests describe how your application should behave
|
||||
given a certain interaction in a specific state.
|
||||
Protractor is a [Node.js](http://nodejs.org) program, and runs end to end tests that are also
|
||||
written in JavaScript and run with node. Protractor uses [WebDriver](https://code.google.com/p/selenium/wiki/GettingStarted)
|
||||
to control browsers and simulate user actions.
|
||||
|
||||
A scenario is comprised of one or more `it` blocks that describe the requirements of your
|
||||
application. `it` blocks are made of **commands** and **expectations**. Commands tell the Runner
|
||||
to do something with the application such as navigate to a page or click on a button. Expectations
|
||||
tell the Runner to assert something about the application's state, such as the value of a field or
|
||||
the current URL.
|
||||
For more information on Protractor, view [getting started](https://github.com/angular/protractor/blob/master/docs/getting-started.md)
|
||||
or the [api docs](https://github.com/angular/protractor/blob/master/docs/api.md).
|
||||
|
||||
Protractor uses [Jasmine](http://jasmine.github.io/1.3/introduction.html) for its test syntax.
|
||||
As in unit testing, a test file is comprised of one or
|
||||
more `it` blocks that describe the requirements of your application. `it` blocks are made of
|
||||
**commands** and **expectations**. Commands tell Protractor to do something with the application
|
||||
such as navigate to a page or click on a button. Expectations tell Protractor to assert something
|
||||
about the application's state, such as the value of a field or the current URL.
|
||||
|
||||
If any expectation within an `it` block fails, the runner marks the `it` as "failed" and continues
|
||||
on to the next block.
|
||||
|
||||
Scenarios may also have `beforeEach` and `afterEach` blocks, which will be run before or after
|
||||
Test files may also have `beforeEach` and `afterEach` blocks, which will be run before or after
|
||||
each `it` block regardless of whether the block passes or fails.
|
||||
|
||||
<img src="img/guide/scenario_runner.png">
|
||||
|
||||
In addition to the above elements, scenarios may also contain helper functions to avoid duplicating
|
||||
In addition to the above elements, tests may also contain helper functions to avoid duplicating
|
||||
code in the `it` blocks.
|
||||
|
||||
Here is an example of a simple scenario:
|
||||
Here is an example of a simple test:
|
||||
```js
|
||||
describe('Buzz Client', function() {
|
||||
it('should filter results', function() {
|
||||
input('user').enter('jacksparrow');
|
||||
element(':button').click();
|
||||
expect(repeater('ul li').count()).toEqual(10);
|
||||
input('filterText').enter('Bees');
|
||||
expect(repeater('ul li').count()).toEqual(1);
|
||||
});
|
||||
describe('TODO list', function() {
|
||||
it('should filter results', function() {
|
||||
|
||||
// Find the element with ng-model="user" and type "jacksparrow" into it
|
||||
element(by.model('user')).sendKeys('jacksparrow');
|
||||
|
||||
// Find the first (and only) button on the page and click it
|
||||
element(by.css(':button')).click();
|
||||
|
||||
// Verify that there are 10 tasks
|
||||
expect(element.all(by.repeater('task in tasks')).count()).toEqual(10);
|
||||
|
||||
// Enter 'groceries' into the element with ng-model="filterText"
|
||||
element(by.model('filterText')).sendKeys('groceries');
|
||||
|
||||
// Verify that now there is only one item in the task list
|
||||
expect(element.all(by.repeater('task in tasks')).count()).toEqual(1);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Note that
|
||||
[`input('user')`](https://github.com/angular/angular.js/blob/master/docs/content/guide/dev_guide.e2e-testing.ngdoc#L119)
|
||||
finds the `<input>` element with `ng-model="user"` not `name="user"`.
|
||||
|
||||
This scenario describes the requirements of a Buzz Client, specifically, that it should be able to
|
||||
filter the stream of the user. It starts by entering a value in the input field with ng-model="user", clicking
|
||||
the only button on the page, and then it verifies that there are 10 items listed. It then enters
|
||||
'Bees' in the input field with ng-model='filterText' and verifies that the list is reduced to a single item.
|
||||
|
||||
The API section below lists the available commands and expectations for the Runner.
|
||||
|
||||
## API
|
||||
Source: https://github.com/angular/angular.js/blob/master/src/ngScenario/dsl.js
|
||||
|
||||
### `pause()`
|
||||
Pauses the execution of the tests until you call `resume()` in the console (or click the resume
|
||||
link in the Runner UI).
|
||||
|
||||
### `sleep(seconds)`
|
||||
Pauses the execution of the tests for the specified number of `seconds`.
|
||||
|
||||
### `browser().navigateTo(url)`
|
||||
Loads the `url` into the test frame.
|
||||
|
||||
### `browser().navigateTo(url, fn)`
|
||||
Loads the URL returned by `fn` into the testing frame. The given `url` is only used for the test
|
||||
output. Use this when the destination URL is dynamic (that is, the destination is unknown when you
|
||||
write the test).
|
||||
|
||||
### `browser().reload()`
|
||||
Refreshes the currently loaded page in the test frame.
|
||||
|
||||
### `browser().window().href()`
|
||||
Returns the window.location.href of the currently loaded page in the test frame.
|
||||
|
||||
### `browser().window().path()`
|
||||
Returns the window.location.pathname of the currently loaded page in the test frame.
|
||||
|
||||
### `browser().window().search()`
|
||||
Returns the window.location.search of the currently loaded page in the test frame.
|
||||
|
||||
### `browser().window().hash()`
|
||||
Returns the window.location.hash (without `#`) of the currently loaded page in the test frame.
|
||||
|
||||
### `browser().location().url()`
|
||||
Returns the {@link ng.$location $location.url()} of the currently loaded page in
|
||||
the test frame.
|
||||
|
||||
### `browser().location().path()`
|
||||
Returns the {@link ng.$location $location.path()} of the currently loaded page in
|
||||
the test frame.
|
||||
|
||||
### `browser().location().search()`
|
||||
Returns the {@link ng.$location $location.search()} of the currently loaded page
|
||||
in the test frame.
|
||||
|
||||
### `browser().location().hash()`
|
||||
Returns the {@link ng.$location $location.hash()} of the currently loaded page in
|
||||
the test frame.
|
||||
|
||||
### `expect(future).{matcher}`
|
||||
Asserts the value of the given `future` satisfies the `matcher`. All API statements return a
|
||||
`future` object, which get a `value` assigned after they are executed. Matchers are defined using
|
||||
`angular.scenario.matcher`, and they use the value of futures to run the expectation. For example:
|
||||
`expect(browser().location().href()).toEqual('http://www.google.com')`. Available matchers
|
||||
are presented further down this document.
|
||||
|
||||
### `expect(future).not().{matcher}`
|
||||
Asserts the value of the given `future` satisfies the negation of the `matcher`.
|
||||
|
||||
### `using(selector, label)`
|
||||
Scopes the next DSL element selection.
|
||||
|
||||
### `binding(name)`
|
||||
Returns the value of the first binding matching the given `name`.
|
||||
|
||||
### `input(name).enter(value)`
|
||||
Enters the given `value` in the text field with the corresponding ng-model `name`.
|
||||
|
||||
### `input(name).check()`
|
||||
Checks/unchecks the checkbox with the corresponding ng-model `name`.
|
||||
|
||||
### `input(name).select(value)`
|
||||
Selects the given `value` in the radio button with the corresponding ng-model `name`.
|
||||
|
||||
### `input(name).val()`
|
||||
Returns the current value of an input field with the corresponding ng-model `name`.
|
||||
|
||||
### `repeater(selector, label).count()`
|
||||
Returns the number of rows in the repeater matching the given jQuery `selector`. The `label` is
|
||||
used for test output.
|
||||
|
||||
### `repeater(selector, label).row(index)`
|
||||
Returns an array with the bindings in the row at the given `index` in the repeater matching the
|
||||
given jQuery `selector`. The `label` is used for test output.
|
||||
|
||||
### `repeater(selector, label).column(binding)`
|
||||
Returns an array with the values in the column with the given `binding` in the repeater matching
|
||||
the given jQuery `selector`. The `label` is used for test output.
|
||||
|
||||
### `select(name).option(value)`
|
||||
Picks the option with the given `value` on the select with the given ng-model `name`.
|
||||
|
||||
### `select(name).options(value1, value2...)`
|
||||
Picks the options with the given `values` on the multi select with the given ng-model `name`.
|
||||
|
||||
### `element(selector, label).count()`
|
||||
Returns the number of elements that match the given jQuery `selector`. The `label` is used for test
|
||||
output.
|
||||
|
||||
### `element(selector, label).click()`
|
||||
Clicks on the element matching the given jQuery `selector`. The `label` is used for test output.
|
||||
|
||||
### `element(selector, label).query(fn)`
|
||||
Executes the function `fn(selectedElements, done)`, where selectedElements are the elements that
|
||||
match the given jQuery `selector` and `done` is a function that is called at the end of the `fn`
|
||||
function. The `label` is used for test output.
|
||||
|
||||
### `element(selector, label).{method}()`
|
||||
Returns the result of calling `method` on the element matching the given jQuery `selector`, where
|
||||
`method` can be any of the following jQuery methods: `val`, `text`, `html`, `height`,
|
||||
`innerHeight`, `outerHeight`, `width`, `innerWidth`, `outerWidth`, `position`, `scrollLeft`,
|
||||
`scrollTop`, `offset`. The `label` is used for test output.
|
||||
|
||||
### `element(selector, label).{method}(value)`
|
||||
Executes the `method` passing in `value` on the element matching the given jQuery `selector`, where
|
||||
`method` can be any of the following jQuery methods: `val`, `text`, `html`, `height`,
|
||||
`innerHeight`, `outerHeight`, `width`, `innerWidth`, `outerWidth`, `position`, `scrollLeft`,
|
||||
`scrollTop`, `offset`. The `label` is used for test output.
|
||||
|
||||
### `element(selector, label).{method}(key)`
|
||||
Returns the result of calling `method` passing in `key` on the element matching the given jQuery
|
||||
`selector`, where `method` can be any of the following jQuery methods: `attr`, `prop`, `css`. The
|
||||
`label` is used for test output.
|
||||
|
||||
### `element(selector, label).{method}(key, value)`
|
||||
Executes the `method` passing in `key` and `value` on the element matching the given jQuery
|
||||
`selector`, where `method` can be any of the following jQuery methods: `attr`, `prop`, `css`. The
|
||||
`label` is used for test output.
|
||||
|
||||
## Matchers
|
||||
|
||||
Matchers are used in combination with the `expect(...)` function as described above and can
|
||||
be negated with `not()`. For instance: `expect(element('h1').text()).not().toEqual('Error')`.
|
||||
|
||||
Source: https://github.com/angular/angular.js/blob/master/src/ngScenario/matchers.js
|
||||
|
||||
```js
|
||||
// value and Object comparison following the rules of angular.equals().
|
||||
expect(value).toEqual(value)
|
||||
|
||||
// a simpler value comparison using ===
|
||||
expect(value).toBe(value)
|
||||
|
||||
// checks that the value is defined by checking its type.
|
||||
expect(value).toBeDefined()
|
||||
|
||||
// the following two matchers are using JavaScript's standard truthiness rules
|
||||
expect(value).toBeTruthy()
|
||||
expect(value).toBeFalsy()
|
||||
|
||||
// verify that the value matches the given regular expression. The regular
|
||||
// expression may be passed in form of a string or a regular expression
|
||||
// object.
|
||||
expect(value).toMatch(expectedRegExp)
|
||||
|
||||
// a check for null using ===
|
||||
expect(value).toBeNull()
|
||||
|
||||
// Array.indexOf(...) is used internally to check whether the element is
|
||||
// contained within the array.
|
||||
expect(value).toContain(expected)
|
||||
|
||||
// number comparison using < and >
|
||||
expect(value).toBeLessThan(expected)
|
||||
expect(value).toBeGreaterThan(expected)
|
||||
```
|
||||
This test describes the requirements of a ToDo list, specifically, that it should be able to
|
||||
filter the list of items.
|
||||
|
||||
## Example
|
||||
See the [angular-seed](https://github.com/angular/angular-seed) project for more examples.
|
||||
|
||||
### Conditional actions with element(...).query(fn)
|
||||
|
||||
E2E testing with angular scenario is highly asynchronous and hides a lot of complexity by
|
||||
queueing actions and expectations that can handle futures. From time to time, you might need
|
||||
conditional assertions or element selection. Even though you should generally try to avoid this
|
||||
(as it is can be sign for unstable tests), you can add conditional behavior with
|
||||
`element(...).query(fn)`. The following code listing shows how this function can be used to delete
|
||||
added entries (where an entry is some domain object) using the application's web interface.
|
||||
|
||||
Imagine the application to be structured into two views:
|
||||
|
||||
1. *Overview view* which lists all the added entries in a table and
|
||||
2. a *detail view* which shows the entries' details and contains a delete button. When clicking the
|
||||
delete button, the user is redirected back to the *overview page*.
|
||||
|
||||
```js
|
||||
beforeEach(function () {
|
||||
var deleteEntry = function () {
|
||||
browser().navigateTo('/entries');
|
||||
|
||||
// we need to select the <tbody> element as it might be the case that there
|
||||
// are no entries (and therefore no rows). When the selector does not
|
||||
// result in a match, the test would be marked as a failure.
|
||||
element('table tbody').query(function (tbody, done) {
|
||||
// ngScenario gives us a jQuery lite wrapped element. We call the
|
||||
// `children()` function to retrieve the table body's rows
|
||||
var children = tbody.children();
|
||||
|
||||
if (children.length > 0) {
|
||||
// if there is at least one entry in the table, click on the link to
|
||||
// the entry's detail view
|
||||
element('table tbody a').click();
|
||||
// and, after a route change, click the delete button
|
||||
element('.btn-danger').click();
|
||||
}
|
||||
|
||||
// if there is more than one entry shown in the table, queue another
|
||||
// delete action.
|
||||
if (children.length > 1) {
|
||||
deleteEntry();
|
||||
}
|
||||
|
||||
// remember to call `done()` so that ngScenario can continue
|
||||
// test execution.
|
||||
done();
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
// start deleting entries
|
||||
deleteEntry();
|
||||
});
|
||||
```
|
||||
|
||||
In order to understand what is happening, we should emphasize that ngScenario calls are not
|
||||
immediately executed, but queued (in ngScenario terms, we would be talking about adding
|
||||
future actions). If we had only one entry in our table, then the following future actions
|
||||
would be queued:
|
||||
|
||||
```js
|
||||
// delete entry 1
|
||||
browser().navigateTo('/entries');
|
||||
element('table tbody').query(function (tbody, done) { ... });
|
||||
element('table tbody a');
|
||||
element('.btn-danger').click();
|
||||
```
|
||||
|
||||
For two entries, ngScenario would have to work on the following queue:
|
||||
|
||||
```js
|
||||
// delete entry 1
|
||||
browser().navigateTo('/entries');
|
||||
element('table tbody').query(function (tbody, done) { ... });
|
||||
element('table tbody a');
|
||||
element('.btn-danger').click();
|
||||
|
||||
// delete entry 2
|
||||
// indented to represent "recursion depth"
|
||||
browser().navigateTo('/entries');
|
||||
element('table tbody').query(function (tbody, done) { ... });
|
||||
element('table tbody a');
|
||||
element('.btn-danger').click();
|
||||
```
|
||||
See the [angular-seed](https://github.com/angular/angular-seed) project for more examples, or look
|
||||
at the embedded examples in the Angular documentation (For example, [$http](http://docs.angularjs.org/api/ng/service/$http)
|
||||
has an end to end test in the example under the `protractor.js` tag).
|
||||
|
||||
## Caveats
|
||||
|
||||
`ngScenario` does not work with apps that manually bootstrap using `angular.bootstrap`. You must use the `ng-app` directive.
|
||||
Protractor does not work out-of-the-box with apps that bootstrap manually using
|
||||
`angular.bootstrap`. You must use the `ng-app` directive.
|
||||
|
||||
@@ -50,9 +50,9 @@ the method from your view. If you want to `eval()` an Angular expression yoursel
|
||||
|
||||
You can try evaluating different expressions here:
|
||||
|
||||
<example>
|
||||
<example module="expressionExample">
|
||||
<file name="index.html">
|
||||
<div ng-controller="Cntl2" class="expressions">
|
||||
<div ng-controller="ExampleController" class="expressions">
|
||||
Expression:
|
||||
<input type='text' ng-model="expr" size="80"/>
|
||||
<button ng-click="addExp(expr)">Evaluate</button>
|
||||
@@ -66,23 +66,24 @@ You can try evaluating different expressions here:
|
||||
</file>
|
||||
|
||||
<file name="script.js">
|
||||
function Cntl2($scope) {
|
||||
var exprs = $scope.exprs = [];
|
||||
$scope.expr = '3*10|currency';
|
||||
$scope.addExp = function(expr) {
|
||||
exprs.push(expr);
|
||||
};
|
||||
angular.module('expressionExample', [])
|
||||
.controller('ExampleController', ['$scope', function($scope) {
|
||||
var exprs = $scope.exprs = [];
|
||||
$scope.expr = '3*10|currency';
|
||||
$scope.addExp = function(expr) {
|
||||
exprs.push(expr);
|
||||
};
|
||||
|
||||
$scope.removeExp = function(index) {
|
||||
exprs.splice(index, 1);
|
||||
};
|
||||
}
|
||||
$scope.removeExp = function(index) {
|
||||
exprs.splice(index, 1);
|
||||
};
|
||||
}]);
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should allow user expression testing', function() {
|
||||
element(by.css('.expressions button')).click();
|
||||
var lis = element(by.css('.expressions ul')).element.all(by.repeater('expr in exprs'));
|
||||
var lis = element(by.css('.expressions ul')).all(by.repeater('expr in exprs'));
|
||||
expect(lis.count()).toBe(1);
|
||||
expect(lis.get(0).getText()).toEqual('[ X ] 3*10|currency => $30.00');
|
||||
});
|
||||
@@ -95,27 +96,30 @@ You can try evaluating different expressions here:
|
||||
Angular does not use JavaScript's `eval()` to evaluate expressions. Instead Angular's
|
||||
{@link ng.$parse $parse} service processes these expressions.
|
||||
|
||||
Unlike JavaScript, where names default to global `window` properties, Angular expressions must use
|
||||
{@link ng.$window `$window`} explicitly to refer to the global `window` object. For example, if you
|
||||
want to call `alert()` in an expression you must use `$window.alert()`. This restriction is
|
||||
intentional. It prevents accidental access to the global state – a common source of subtle bugs.
|
||||
Angular expressions do not have access to global variables like `window`, `document` or `location`.
|
||||
This restriction is intentional. It prevents accidental access to the global state – a common source of subtle bugs.
|
||||
|
||||
<example>
|
||||
Instead use services like `$window` and `$location` in functions called from expressions. Such services
|
||||
provide mockable access to globals.
|
||||
|
||||
<example module="expressionExample">
|
||||
<file name="index.html">
|
||||
<div class="example2" ng-controller="Cntl1">
|
||||
<div class="example2" ng-controller="ExampleController">
|
||||
Name: <input ng-model="name" type="text"/>
|
||||
<button ng-click="greet()">Greet</button>
|
||||
<button ng-click="window.alert('Should not see me')">Won't greet</button>
|
||||
</div>
|
||||
</file>
|
||||
|
||||
<file name="script.js">
|
||||
function Cntl1($window, $scope){
|
||||
$scope.name = 'World';
|
||||
angular.module('expressionExample', [])
|
||||
.controller('ExampleController', ['$window', '$scope', function($window, $scope) {
|
||||
$scope.name = 'World';
|
||||
|
||||
$scope.greet = function() {
|
||||
$window.alert('Hello ' + $scope.name);
|
||||
};
|
||||
}
|
||||
$scope.greet = function() {
|
||||
$window.alert('Hello ' + $scope.name);
|
||||
};
|
||||
}]);
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
@@ -152,9 +156,10 @@ Similarly, invoking a function `a.b.c()` on `undefined` or `null` simply returns
|
||||
|
||||
## No Control Flow Statements
|
||||
|
||||
You cannot write a control flow statement in an expression. The reason behind this is core to the
|
||||
Angular philosophy that application logic should be in controllers, not the views. If you need a
|
||||
conditional, loop, or to throw from a view expression, delegate to a JavaScript method instead.
|
||||
Apart from the ternary operator (`a ? b : c`), you cannot write a control flow statement in an
|
||||
expression. The reason behind this is core to the Angular philosophy that application logic should
|
||||
be in controllers, not the views. If you need a real conditional, loop, or to throw from a view
|
||||
expression, delegate to a JavaScript method instead.
|
||||
|
||||
## `$event`
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
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.
|
||||
|
||||
The underlying API is the {@link ng.$filterProvider filterProvider}.
|
||||
The underlying API is the {@link ng.$filterProvider `filterProvider`}.
|
||||
|
||||
## Using filters in view templates
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ The key directive in understanding two-way data-binding is {@link ng.directive:n
|
||||
The `ngModel` directive provides the two-way data-binding by synchronizing the model to the view, as well as view to the model.
|
||||
In addition it provides an {@link ngModel.NgModelController API} for other directives to augment its behavior.
|
||||
|
||||
<example>
|
||||
<example module="formExample">
|
||||
<file name="index.html">
|
||||
<div ng-controller="Controller">
|
||||
<div ng-controller="ExampleController">
|
||||
<form novalidate class="simple-form">
|
||||
Name: <input type="text" ng-model="user.name" /><br />
|
||||
E-mail: <input type="email" ng-model="user.email" /><br />
|
||||
@@ -32,19 +32,20 @@ In addition it provides an {@link ngModel.NgModelController API} for other direc
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function Controller($scope) {
|
||||
$scope.master = {};
|
||||
angular.module('formExample', [])
|
||||
.controller('ExampleController', ['$scope', function($scope) {
|
||||
$scope.master = {};
|
||||
|
||||
$scope.update = function(user) {
|
||||
$scope.master = angular.copy(user);
|
||||
};
|
||||
$scope.update = function(user) {
|
||||
$scope.master = angular.copy(user);
|
||||
};
|
||||
|
||||
$scope.reset = function() {
|
||||
$scope.user = angular.copy($scope.master);
|
||||
};
|
||||
$scope.reset = function() {
|
||||
$scope.user = angular.copy($scope.master);
|
||||
};
|
||||
|
||||
$scope.reset();
|
||||
}
|
||||
$scope.reset();
|
||||
}]);
|
||||
</script>
|
||||
</file>
|
||||
</example>
|
||||
@@ -67,9 +68,9 @@ The following example uses the CSS to display validity of each form control.
|
||||
In the example both `user.name` and `user.email` are required, but are rendered with red background only when they are dirty.
|
||||
This ensures that the user is not distracted with an error until after interacting with the control, and failing to satisfy its validity.
|
||||
|
||||
<example>
|
||||
<example module="formExample">
|
||||
<file name="index.html">
|
||||
<div ng-controller="Controller">
|
||||
<div ng-controller="ExampleController">
|
||||
<form novalidate class="css-form">
|
||||
Name:
|
||||
<input type="text" ng-model="user.name" required /><br />
|
||||
@@ -92,19 +93,20 @@ This ensures that the user is not distracted with an error until after interacti
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function Controller($scope) {
|
||||
$scope.master = {};
|
||||
angular.module('formExample', [])
|
||||
.controller('ExampleController', ['$scope', function($scope) {
|
||||
$scope.master = {};
|
||||
|
||||
$scope.update = function(user) {
|
||||
$scope.master = angular.copy(user);
|
||||
};
|
||||
$scope.update = function(user) {
|
||||
$scope.master = angular.copy(user);
|
||||
};
|
||||
|
||||
$scope.reset = function() {
|
||||
$scope.user = angular.copy($scope.master);
|
||||
};
|
||||
$scope.reset = function() {
|
||||
$scope.user = angular.copy($scope.master);
|
||||
};
|
||||
|
||||
$scope.reset();
|
||||
}
|
||||
$scope.reset();
|
||||
}]);
|
||||
</script>
|
||||
</file>
|
||||
</example>
|
||||
@@ -130,9 +132,9 @@ This allows us to extend the above example with these features:
|
||||
- SAVE button is enabled only if form has some changes and is valid
|
||||
- custom error messages for `user.email` and `user.agree`
|
||||
|
||||
<example>
|
||||
<example module="formExample">
|
||||
<file name="index.html">
|
||||
<div ng-controller="Controller">
|
||||
<div ng-controller="ExampleController">
|
||||
<form name="form" class="css-form" novalidate>
|
||||
Name:
|
||||
<input type="text" ng-model="user.name" name="uName" required /><br />
|
||||
@@ -159,23 +161,24 @@ This allows us to extend the above example with these features:
|
||||
</file>
|
||||
|
||||
<file name="script.js">
|
||||
function Controller($scope) {
|
||||
$scope.master = {};
|
||||
angular.module('formExample', [])
|
||||
.controller('ExampleController', ['$scope', function($scope) {
|
||||
$scope.master = {};
|
||||
|
||||
$scope.update = function(user) {
|
||||
$scope.master = angular.copy(user);
|
||||
};
|
||||
$scope.update = function(user) {
|
||||
$scope.master = angular.copy(user);
|
||||
};
|
||||
|
||||
$scope.reset = function() {
|
||||
$scope.user = angular.copy($scope.master);
|
||||
};
|
||||
$scope.reset = function() {
|
||||
$scope.user = angular.copy($scope.master);
|
||||
};
|
||||
|
||||
$scope.isUnchanged = function(user) {
|
||||
return angular.equals(user, $scope.master);
|
||||
};
|
||||
$scope.isUnchanged = function(user) {
|
||||
return angular.equals(user, $scope.master);
|
||||
};
|
||||
|
||||
$scope.reset();
|
||||
}
|
||||
$scope.reset();
|
||||
}]);
|
||||
</file>
|
||||
</example>
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ You write the following binding using the currency filter:
|
||||
|
||||
If your app is currently in the `en-US` locale, the browser will show `$1000.00`. If someone in the
|
||||
Japanese locale (`ja`) views your app, their browser will show a balance of `¥1000.00` instead.
|
||||
This is problematinc because $1000 is not the same as ¥1000.
|
||||
This is problematic because $1000 is not the same as ¥1000.
|
||||
|
||||
In this case, you need to override the default currency symbol by providing the
|
||||
{@link ng.filter:currency} currency filter with a currency symbol as a parameter.
|
||||
|
||||
@@ -21,7 +21,7 @@ integration server runs all the tests against IE9, IE10, and IE11. See
|
||||
[ci.angularjs.org](http://ci.angularjs.org).
|
||||
|
||||
We do not run tests on IE8 and below. A subset of the AngularJS functionality may work on these
|
||||
browesers, but it is up to you to test and decide whether it works for your particular app.
|
||||
browsers, but it is up to you to test and decide whether it works for your particular app.
|
||||
|
||||
|
||||
## Short Version
|
||||
|
||||
@@ -47,7 +47,7 @@ In Angular applications, you move the job of filling page templates with data fr
|
||||
|
||||
### Testing
|
||||
|
||||
* **Unit testing:** [Using Karma (video)](http://www.youtube.com/watch?v=YG5DEzaQBIc), {@link guide/dev_guide.unit-testing Unit testing}, {@link guide/dev_guide.services.testing_services Testing services}, [Karma in Webstorm](http://blog.jetbrains.com/webstorm/2013/10/running-javascript-tests-with-karma-in-webstorm-7/)
|
||||
* **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)
|
||||
|
||||
## Specific Topics
|
||||
@@ -73,15 +73,16 @@ This is a short list of libraries with specific support and documentation for wo
|
||||
* **Internationalization:** [angular-translate](http://angular-translate.github.io), [angular-gettext](http://angular-gettext.rocketeer.be/)
|
||||
* **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/)
|
||||
* **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
|
||||
|
||||
* **Javascript minification: **[Background](http://thegreenpizza.github.io/2013/05/25/building-minification-safe-angular.js-applications/), [ngmin automation tool](http://www.thinkster.io/pick/XlWneEZCqY/angularjs-ngmin)
|
||||
* **Tracking:** [Angularyitcs (Google Analytics)](http://ngmodules.org/modules/angularytics), [Logging Client-Side Errors](http://www.bennadel.com/blog/2542-Logging-Client-Side-Errors-With-AngularJS-And-Stacktrace-js.htm)
|
||||
* **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
|
||||
@@ -112,6 +113,7 @@ This is a short list of libraries with specific support and documentation for wo
|
||||
* **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:**
|
||||
[Pluralsite (3 courses)](http://www.pluralsight.com/training/Courses/Find?highlight=true&searchTerm=angularjs),
|
||||
[Tuts+](https://tutsplus.com/course/easier-js-apps-with-angular/),
|
||||
|
||||
@@ -22,7 +22,7 @@ The impedance mismatch between dynamic applications and static documents is ofte
|
||||
in charge and it calls into the library when it sees fit. E.g., `jQuery`.
|
||||
* **frameworks** - a particular implementation of a web application, where your code fills in
|
||||
the details. The framework is in charge and it calls into your code when it needs something
|
||||
app specific. E.g., `knockout`, `ember`, etc.
|
||||
app specific. E.g., `durandal`, `ember`, etc.
|
||||
|
||||
|
||||
Angular takes another approach. It attempts to minimize the impedance mismatch between document
|
||||
|
||||
@@ -382,8 +382,6 @@ See [80739409](https://github.com/angular/angular.js/commit/807394095b991357225a
|
||||
|
||||
## ngBindHtmlUnsafe has been removed and replaced by ngBindHtml
|
||||
|
||||
`ngBindHtml` which has been moved from `ngSanitize` module to the core `ng` module.
|
||||
|
||||
`ngBindHtml` provides `ngBindHtmlUnsafe` like
|
||||
behavior (evaluate an expression and innerHTML the result into the DOM) when bound to the result
|
||||
of `$sce.trustAsHtml(string)`. When bound to a plain string, the string is sanitized via
|
||||
@@ -391,6 +389,10 @@ of `$sce.trustAsHtml(string)`. When bound to a plain string, the string is sanit
|
||||
module is not loaded) and the bound expression evaluates to a value that is not trusted an
|
||||
exception is thrown.
|
||||
|
||||
When using this directive you can either include `ngSanitize` in your module's dependencis (See the
|
||||
example at the {@link ngBindHtml} reference) or use the {@link $sce} service to set the value as
|
||||
trusted.
|
||||
|
||||
See [dae69473](https://github.com/angular/angular.js/commit/dae694739b9581bea5dbc53522ec00d87b26ae55).
|
||||
|
||||
|
||||
|
||||
@@ -26,10 +26,12 @@ should be bootstrapped. There are several advantages to this approach:
|
||||
|
||||
I'm in a hurry. How do I get a Hello World module working?
|
||||
|
||||
<example module='myApp'>
|
||||
<example ng-app-included="true">
|
||||
<file name="index.html">
|
||||
<div>
|
||||
{{ 'World' | greet }}
|
||||
<div ng-app="myApp">
|
||||
<div>
|
||||
{{ 'World' | greet }}
|
||||
</div>
|
||||
</div>
|
||||
</file>
|
||||
|
||||
@@ -45,12 +47,18 @@ I'm in a hurry. How do I get a Hello World module working?
|
||||
};
|
||||
});
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should add Hello to the name', function() {
|
||||
expect(element(by.binding("{{ 'World' | greet }}")).getText()).toEqual('Hello, World!');
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
|
||||
Important things to notice:
|
||||
|
||||
* The {@link angular.Module Module} API
|
||||
* The reference to `myApp` module in `<html ng-app="myApp">`.
|
||||
* The reference to `myApp` module in `<div ng-app="myApp">`.
|
||||
This is what bootstraps the app using your module.
|
||||
* The empty array in `angular.module('myApp', [])`.
|
||||
This array is the list of modules `myApp` depends on.
|
||||
@@ -75,13 +83,14 @@ The above is a suggestion. Tailor it to your needs.
|
||||
<example module='xmpl'>
|
||||
<file name="index.html">
|
||||
<div ng-controller="XmplController">
|
||||
{{ greeting }}!
|
||||
{{ greeting }}
|
||||
</div>
|
||||
</file>
|
||||
|
||||
<file name="script.js">
|
||||
angular.module('xmpl.service', []).
|
||||
value('greeter', {
|
||||
angular.module('xmpl.service', [])
|
||||
|
||||
.value('greeter', {
|
||||
salutation: 'Hello',
|
||||
localize: function(localization) {
|
||||
this.salutation = localization.salutation;
|
||||
@@ -89,8 +98,9 @@ The above is a suggestion. Tailor it to your needs.
|
||||
greet: function(name) {
|
||||
return this.salutation + ' ' + name + '!';
|
||||
}
|
||||
}).
|
||||
value('user', {
|
||||
})
|
||||
|
||||
.value('user', {
|
||||
load: function(name) {
|
||||
this.name = name;
|
||||
}
|
||||
@@ -100,21 +110,28 @@ The above is a suggestion. Tailor it to your needs.
|
||||
|
||||
angular.module('xmpl.filter', []);
|
||||
|
||||
angular.module('xmpl', ['xmpl.service', 'xmpl.directive', 'xmpl.filter']).
|
||||
run(function(greeter, user) {
|
||||
angular.module('xmpl', ['xmpl.service', 'xmpl.directive', 'xmpl.filter'])
|
||||
|
||||
.run(function(greeter, user) {
|
||||
// This is effectively part of the main method initialization code
|
||||
greeter.localize({
|
||||
salutation: 'Bonjour'
|
||||
});
|
||||
user.load('World');
|
||||
})
|
||||
|
||||
.controller('XmplController', function($scope, greeter, user){
|
||||
$scope.greeting = greeter.greet(user.name);
|
||||
});
|
||||
|
||||
|
||||
// A Controller for your app
|
||||
var XmplController = function($scope, greeter, user) {
|
||||
$scope.greeting = greeter.greet(user.name);
|
||||
};
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should add Hello to the name', function() {
|
||||
expect(element(by.binding("{{ greeting }}")).getText()).toEqual('Bonjour World!');
|
||||
});
|
||||
</file>
|
||||
|
||||
</example>
|
||||
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ On to more complex examples!
|
||||
## Factory Recipe
|
||||
|
||||
The Value recipe is very simple to write, but lacks some important features we often need when
|
||||
creating services. Let's now look at the Value recipe's more powerful sibling, the Factory.The
|
||||
creating services. Let's now look at the Value recipe's more powerful sibling, the Factory. The
|
||||
Factory recipe adds the following abilities:
|
||||
|
||||
* ability to use other services (have dependencies)
|
||||
@@ -97,7 +97,7 @@ created by this recipe.
|
||||
Note: All services in Angular are singletons. That means that the injector uses each recipe at most
|
||||
once to create the object. The injector then caches the reference for all future needs.
|
||||
|
||||
Since Factory is more powerful version of Value recipe, you can construct the same service with it.
|
||||
Since Factory is more a powerful version of the Value recipe, you can construct the same service with it.
|
||||
Using our previous `clientId` Value recipe example, we can rewrite it as a Factory recipe like
|
||||
this:
|
||||
|
||||
@@ -111,8 +111,8 @@ But given that the token is just a string literal, sticking with the Value recip
|
||||
appropriate as it makes the code easier to follow.
|
||||
|
||||
Let's say, however, that we would also like to create a service that computes a token used for
|
||||
authentication against a remote API. This token will be called 'apiToken' and will be computed
|
||||
based on the `clientId` value and a secret stored in browser's local storage:
|
||||
authentication against a remote API. This token will be called `apiToken` and will be computed
|
||||
based on the `clientId` value and a secret stored in the browser's local storage:
|
||||
|
||||
```javascript
|
||||
myApp.factory('apiToken', ['clientId', function apiTokenFactory(clientId) {
|
||||
@@ -129,12 +129,14 @@ myApp.factory('apiToken', ['clientId', function apiTokenFactory(clientId) {
|
||||
```
|
||||
|
||||
In the code above, we see how the `apiToken` service is defined via the Factory recipe that depends
|
||||
on `clientId` service. The factory service then uses NSA-proof encryption to produce an authentication
|
||||
on the `clientId` service. The factory service then uses NSA-proof encryption to produce an authentication
|
||||
token.
|
||||
|
||||
Note: It is a best practice to name the factory functions as "<serviceId>Factory"
|
||||
(e.g. apiTokenFactory). While this names are not required, they help when navigating the code base
|
||||
<div class="alert alert-success">
|
||||
**Best Practice:** name the factory functions as `<serviceId>Factory`
|
||||
(e.g. apiTokenFactory). While this naming convention is not required, it helps when navigating the code base
|
||||
or looking at stack traces in the debugger.
|
||||
</div>
|
||||
|
||||
Just like with Value recipe, Factory recipe can create a service of any type, whether it be a
|
||||
primitive, object literal, function, or even an instance of a custom type.
|
||||
@@ -143,7 +145,7 @@ primitive, object literal, function, or even an instance of a custom type.
|
||||
## Service Recipe
|
||||
|
||||
JavaScript developers often use custom types to write object-oriented code. Let's explore how we
|
||||
could launch a unicorn into space via our `unicornLauncher` service that is an instance of
|
||||
could launch a unicorn into space via our `unicornLauncher` service which is an instance of a
|
||||
custom type:
|
||||
|
||||
```javascript
|
||||
@@ -187,14 +189,13 @@ myApp.service('unicornLauncher', ["apiToken", UnicornLauncher]);
|
||||
Much simpler!
|
||||
|
||||
Note: Yes, we have called one of our service recipes 'Service'. We regret this and know that we'll
|
||||
be somehow punished for our mis-deed. It's like we named one of our offspring 'Children'. Boy,
|
||||
be somehow punished for our mis-deed. It's like we named one of our offspring 'Child'. Boy,
|
||||
that would mess with the teachers.
|
||||
|
||||
|
||||
## Provider Recipe
|
||||
|
||||
There are two more recipe types left to cover. They are both fairly specialized and are used
|
||||
infrequently. As already mentioned in the intro, the Provider recipe is the core recipe type and
|
||||
As already mentioned in the intro, the Provider recipe is the core recipe type and
|
||||
all the other recipe types are just syntactic sugar on top of it. It is the most verbose recipe
|
||||
with the most abilities, but for most services it's overkill.
|
||||
|
||||
|
||||
@@ -42,15 +42,16 @@ arrangement isolates the controller from the directive as well as from DOM. This
|
||||
point since it makes the controllers view agnostic, which greatly improves the testing story of
|
||||
the applications.
|
||||
|
||||
<example>
|
||||
<example module="scopeExample">
|
||||
<file name="script.js">
|
||||
function MyController($scope) {
|
||||
$scope.username = 'World';
|
||||
angular.module('scopeExample', [])
|
||||
.controller('MyController', ['$scope', function($scope) {
|
||||
$scope.username = 'World';
|
||||
|
||||
$scope.sayHello = function() {
|
||||
$scope.greeting = 'Hello ' + $scope.username + '!';
|
||||
};
|
||||
}
|
||||
$scope.sayHello = function() {
|
||||
$scope.greeting = 'Hello ' + $scope.username + '!';
|
||||
};
|
||||
}]);
|
||||
</file>
|
||||
<file name="index.html">
|
||||
<div ng-controller="MyController">
|
||||
@@ -112,7 +113,7 @@ may have several child scopes.
|
||||
The application can have multiple scopes, because some {@link guide/directive directives} create
|
||||
new child scopes (refer to directive documentation to see which directives create new scopes).
|
||||
When new scopes are created, they are added as children of their parent scope. This creates a tree
|
||||
structure which parallels the DOM where they're attached
|
||||
structure which parallels the DOM where they're attached.
|
||||
|
||||
When Angular evaluates `{{name}}`, it first looks at the scope associated with the given
|
||||
element for the `name` property. If no such property is found, it searches the parent scope
|
||||
@@ -122,13 +123,13 @@ inheritance, and child scopes prototypically inherit from their parents.
|
||||
This example illustrates scopes in application, and prototypical inheritance of properties. The example is followed by
|
||||
a diagram depicting the scope boundaries.
|
||||
|
||||
<example>
|
||||
<example module="scopeExample">
|
||||
<file name="index.html">
|
||||
<div class="show-scope-demo">
|
||||
<div ng-controller="GreetCtrl">
|
||||
<div ng-controller="GreetController">
|
||||
Hello {{name}}!
|
||||
</div>
|
||||
<div ng-controller="ListCtrl">
|
||||
<div ng-controller="ListController">
|
||||
<ol>
|
||||
<li ng-repeat="name in names">{{name}} from {{department}}</li>
|
||||
</ol>
|
||||
@@ -136,14 +137,14 @@ a diagram depicting the scope boundaries.
|
||||
</div>
|
||||
</file>
|
||||
<file name="script.js">
|
||||
function GreetCtrl($scope, $rootScope) {
|
||||
$scope.name = 'World';
|
||||
$rootScope.department = 'Angular';
|
||||
}
|
||||
|
||||
function ListCtrl($scope) {
|
||||
$scope.names = ['Igor', 'Misko', 'Vojta'];
|
||||
}
|
||||
angular.module('scopeExample', [])
|
||||
.controller('GreetController', ['$scope', '$rootScope', function($scope, $rootScope) {
|
||||
$scope.name = 'World';
|
||||
$rootScope.department = 'Angular';
|
||||
}])
|
||||
.controller('ListController', ['$scope', function($scope) {
|
||||
$scope.names = ['Igor', 'Misko', 'Vojta'];
|
||||
}]);
|
||||
</file>
|
||||
<file name="style.css">
|
||||
.show-scope-demo.ng-scope,
|
||||
@@ -190,14 +191,15 @@ Scopes can propagate events in similar fashion to DOM events. The event can be {
|
||||
ng.$rootScope.Scope#$broadcast broadcasted} to the scope children or {@link
|
||||
ng.$rootScope.Scope#$emit emitted} to scope parents.
|
||||
|
||||
<example>
|
||||
<example module="eventExample">
|
||||
<file name="script.js">
|
||||
function EventController($scope) {
|
||||
$scope.count = 0;
|
||||
$scope.$on('MyEvent', function() {
|
||||
$scope.count++;
|
||||
});
|
||||
}
|
||||
angular.module('eventExample', [])
|
||||
.controller('EventController', ['$scope', function($scope) {
|
||||
$scope.count = 0;
|
||||
$scope.$on('MyEvent', function() {
|
||||
$scope.count++;
|
||||
});
|
||||
}]);
|
||||
</file>
|
||||
<file name="index.html">
|
||||
<div ng-controller="EventController">
|
||||
@@ -268,7 +270,7 @@ the `$digest` phase. This delay is desirable, since it coalesces multiple model
|
||||
|
||||
4. **Mutation observation**
|
||||
|
||||
At the end `$apply`, Angular performs a {@link ng.$rootScope.Scope#$digest
|
||||
At the end of `$apply`, Angular performs a {@link ng.$rootScope.Scope#$digest
|
||||
$digest} cycle on the root scope, which then propagates throughout all child scopes. During
|
||||
the `$digest` cycle, all `$watch`ed expressions or functions are checked for model mutation
|
||||
and if a mutation is detected, the `$watch` listener is called.
|
||||
@@ -350,15 +352,15 @@ The diagram and the example below describe how Angular interacts with the browse
|
||||
|
||||
Angular modifies the normal JavaScript flow by providing its own event processing loop. This
|
||||
splits the JavaScript into classical and Angular execution context. Only operations which are
|
||||
applied in Angular execution context will benefit from Angular data-binding, exception handling,
|
||||
property watching, etc... You can also use $apply() to enter Angular execution context from JavaScript. Keep in
|
||||
applied in the Angular execution context will benefit from Angular data-binding, exception handling,
|
||||
property watching, etc... You can also use $apply() to enter the Angular execution context from JavaScript. Keep in
|
||||
mind that in most places (controllers, services) $apply has already been called for you by the
|
||||
directive which is handling the event. An explicit call to $apply is needed only when
|
||||
implementing custom event callbacks, or when working with third-party library callbacks.
|
||||
|
||||
1. Enter Angular execution context by calling {@link guide/scope scope}`.`{@link
|
||||
ng.$rootScope.Scope#$apply $apply}`(stimulusFn)`. Where `stimulusFn` is
|
||||
the work you wish to do in Angular execution context.
|
||||
1. Enter the Angular execution context by calling {@link guide/scope scope}`.`{@link
|
||||
ng.$rootScope.Scope#$apply $apply}`(stimulusFn)`, where `stimulusFn` is
|
||||
the work you wish to do in the Angular execution context.
|
||||
2. Angular executes the `stimulusFn()`, which typically modifies application state.
|
||||
3. Angular enters the {@link ng.$rootScope.Scope#$digest $digest} loop. The
|
||||
loop is made up of two smaller loops which process {@link
|
||||
|
||||
@@ -130,8 +130,14 @@ injection of `$window`, `$scope`, and our `notify` service:
|
||||
</example>
|
||||
|
||||
<div class="alert alert-danger">
|
||||
**Careful:** If you plan to [minify](http://en.wikipedia.org/wiki/Minification_(programming) your code,
|
||||
your variable names will get renamed unless you use one of the annotation techniques above.
|
||||
**Careful:** If you plan to [minify](http://en.wikipedia.org/wiki/Minification_(programming)) your
|
||||
code, your variable names will get renamed unless you use one of the annotation techniques above.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
If you use a tool like [ngmin](https://github.com/btford/ngmin#ngmin) in your workflow you can
|
||||
use implicit dependency notation within your codebase and let **ngmin** automatically convert such
|
||||
injectable functions to the array notation prior to minifying.
|
||||
</div>
|
||||
|
||||
|
||||
@@ -293,5 +299,5 @@ it('should clear messages after alert', function() {
|
||||
|
||||
## Related API
|
||||
|
||||
* {@link ./ng Angular Service API}
|
||||
* {@link ./api/ng/service Angular Service API}
|
||||
* {@link angular.injector Injector API}
|
||||
|
||||
@@ -49,7 +49,7 @@ Out of the four options in the list above, only the last one is testable. Let's
|
||||
### Using the `new` operator
|
||||
|
||||
While there is nothing wrong with the `new` operator fundamentally, a problem arises when calling `new`
|
||||
on a constructor. This permanently binds the call site to the type. For example, lets say that we try to
|
||||
on a constructor. This permanently binds the call site to the type. For example, let's say that we try to
|
||||
instantiate an `XHR` that will retrieve data from the server.
|
||||
|
||||
```js
|
||||
@@ -65,7 +65,7 @@ function MyClass() {
|
||||
|
||||
A problem surfaces in tests when we would like to instantiate a `MockXHR` that would
|
||||
allow us to return fake data and simulate network failures. By calling `new XHR()` we are
|
||||
permanently bound to the actual XHR and there is no way to replace it. Yes, we could monkey
|
||||
permanently bound to the actual XHR and there is no way to replace it. Yes, we could monkey
|
||||
patch, but that is a bad idea for many reasons which are outside the scope of this document.
|
||||
|
||||
Here's an example of how the class above becomes hard to test when resorting to monkey patching:
|
||||
@@ -133,7 +133,7 @@ function MyClass() {
|
||||
|
||||
However, where does the serviceRegistry come from? If it is:
|
||||
* `new`-ed up, the test has no chance to reset the services for testing.
|
||||
* a global look-up then the service returned is global as well (but resetting is easier, since
|
||||
* a global look-up then the service returned is global as well (but resetting is easier, since
|
||||
only one global variable exists to be reset).
|
||||
|
||||
The class above is hard to test since we have to change the global state:
|
||||
@@ -296,7 +296,7 @@ Now we can add a directive to our app.
|
||||
app.directive('aGreatEye', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
replace: true,
|
||||
template: '<h1>lidless, wreathed in flame, {{1 + 1}} times</h1>'
|
||||
};
|
||||
});
|
||||
@@ -337,6 +337,80 @@ We inject the $compile service and $rootScope before each jasmine test. The $com
|
||||
to render the aGreatEye directive. After rendering the directive we ensure that the directive has
|
||||
replaced the content and "lidless, wreathed in flame, 2 times" is present.
|
||||
|
||||
### Testing Transclusion Directives
|
||||
|
||||
Directives that use transclusion are treated specially by the compiler. Before their compile
|
||||
function is called, the contents of the directive's element are removed from the element and
|
||||
provided via a transclusion function. The directive's template is then appended to the directive's
|
||||
element, to which it can then insert the transcluded content into its template.
|
||||
|
||||
|
||||
Before compilation:
|
||||
```html
|
||||
<div translude-directive>
|
||||
Some transcluded content
|
||||
</div>
|
||||
```
|
||||
|
||||
After transclusion extraction:
|
||||
```html
|
||||
<div transclude-directive></div>
|
||||
```
|
||||
|
||||
After compilation:
|
||||
```html
|
||||
<div transclude-directive>
|
||||
Some Template
|
||||
<span ng-transclude>Some transcluded content</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
If the directive is using 'element' transclusion, the compiler will actually remove the
|
||||
directive's entire element from the DOM and replace it with a comment node. The compiler then
|
||||
inserts the directive's template "after" this comment node, as a sibling.
|
||||
|
||||
Before compilation
|
||||
```html
|
||||
<div element-transclude>
|
||||
Some Content
|
||||
</div>
|
||||
```
|
||||
|
||||
After transclusion extraction
|
||||
```html
|
||||
<!-- elementTransclude -->
|
||||
```
|
||||
|
||||
After compilation:
|
||||
```html
|
||||
<!-- elementTransclude -->
|
||||
<div element-transclude>
|
||||
Some Template
|
||||
<span ng-transclude>Some transcluded content</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
It is important to be aware of this when writing tests for directives that use 'element'
|
||||
transclusion. If you place the directive on the root element of the DOM fragment that you
|
||||
pass to {@link $compile}, then the DOM node returned from the linking function will be the
|
||||
comment node and you will lose the ability to access the template and transcluded content.
|
||||
|
||||
```javascript
|
||||
var node = $compile('<div element-transclude></div>')($rootScope);
|
||||
expect(node[0].nodeType).toEqual(node.COMMENT_NODE);
|
||||
expect(node[1]).toBeUndefined();
|
||||
```
|
||||
|
||||
To cope with this you simply ensure that your 'element' transclude directive is wrapped in an
|
||||
element, such as a `<div>`.
|
||||
|
||||
```javascript
|
||||
var node = $compile('<div><div element-transclude></div></div>')($rootScope);
|
||||
var contents = node.contents();
|
||||
expect(contents[0].nodeType).toEqual(node.COMMENT_NODE);
|
||||
expect(contents[1].nodeType).toEqual(node.ELEMENT_NODE);
|
||||
```
|
||||
|
||||
### Testing Directives With External Templates
|
||||
|
||||
If your directive uses `templateUrl`, consider using
|
||||
|
||||
@@ -62,13 +62,13 @@ minified AngularJS files:
|
||||
|
||||
```shell
|
||||
# Clone your Github repository:
|
||||
git clone git@github.com:<github username>/angular.js.git
|
||||
git clone "git@github.com:<github username>/angular.js.git"
|
||||
|
||||
# Go to the AngularJS directory:
|
||||
cd angular.js
|
||||
|
||||
# Add the main AngularJS repository as an upstream remote to your repository:
|
||||
git remote add upstream https://github.com/angular/angular.js.git
|
||||
git remote add upstream "https://github.com/angular/angular.js.git"
|
||||
|
||||
# Install node.js dependencies:
|
||||
npm install
|
||||
@@ -126,13 +126,13 @@ made available a local web server based on Node.js.
|
||||
```
|
||||
|
||||
2. To access the local server, enter the following URL into your web browser:
|
||||
```
|
||||
```text
|
||||
http://localhost:8000/
|
||||
```
|
||||
By default, it serves the contents of the AngularJS project directory.
|
||||
|
||||
3. To access the locally served docs, visit this URL:
|
||||
```
|
||||
```text
|
||||
http://localhost:8000/build/docs/
|
||||
```
|
||||
|
||||
@@ -165,7 +165,7 @@ change. To execute tests in this mode run:
|
||||
2. To capture more browsers, open this URL in the desired browser (URL might be different if you have multiple instance
|
||||
of Karma running, read Karma's console output for the correct URL):
|
||||
|
||||
```shell
|
||||
```text
|
||||
http://localhost:9876/
|
||||
```
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ 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.2.0:
|
||||
|
||||
<pre>
|
||||
```
|
||||
<!doctype html>
|
||||
<html ng-app>
|
||||
<head>
|
||||
@@ -27,7 +27,7 @@ example points to the minified version 1.2.0:
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
</pre>
|
||||
```
|
||||
|
||||
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
|
||||
@@ -83,4 +83,4 @@ after the core `angular.js` file:
|
||||
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.
|
||||
defined in the `ng` module.
|
||||
|
||||
@@ -16,7 +16,7 @@ Because HTML has Angular brackets and "ng" sounds like "Angular".
|
||||
AngularJS fits the definition of a framework the best, even though it's much more lightweight than
|
||||
a typical framework and that's why many confuse it with a library.
|
||||
|
||||
AngularJS is 100% JavaScript, 100% client side and compatible with both desktop and mobile browsers.
|
||||
AngularJS is 100% JavaScript, 100% client-side and compatible with both desktop and mobile browsers.
|
||||
So it's definitely not a plugin or some other native browser extension.
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ application is being bootstrapped. If jQuery is not present in your script path,
|
||||
to its own implementation of the subset of jQuery that we call {@link angular.element jQLite}.
|
||||
|
||||
Due to a change to use `on()`/`off()` rather than `bind()`/`unbind()`, Angular 1.2 only operates with
|
||||
jQuery 1.7.1 or above.
|
||||
jQuery 1.7.1 or above. However, Angular does not currently support jQuery 2.x or above.
|
||||
|
||||
|
||||
### What is testability like in Angular?
|
||||
@@ -114,7 +114,7 @@ make our schwag will be happy to do a custom run for you, based on our existing
|
||||
they'll waive the setup costs, and you can order any quantity you need.
|
||||
|
||||
**Stickers**
|
||||
For orders of 250 stickers or more within Canada or the United States, contact Tom Witting (or anyone in sales) via email at tom@stickergiant.com, and tell him you want to order some AngularJS
|
||||
For orders of 250 stickers or more within Canada or the United States, contact Tom Witting (or anyone in sales) via email at <tom@stickergiant.com>, and tell him you want to order some AngularJS
|
||||
stickers just like the ones in job #42711. You'll have to give them your own info for billing and shipping.
|
||||
|
||||
As long as the design stays exactly the same, [StickerGiant](http://www.stickergiant.com) will give you a reorder discount.
|
||||
|
||||
@@ -10,7 +10,7 @@ becoming an Angular expert.
|
||||
1. Read the {@link guide/concepts conceptual overview}.<br/>Understand Angular's vocabulary and how all the Angular
|
||||
components work together.
|
||||
1. Do the {@link tutorial/ AngularJS Tutorial}.<br/>Walk end-to-end through building an application complete with tests
|
||||
on top of a node.js web server. Covers every major AngularJS feature and show you how to set up your development
|
||||
on top of a node.js web server. Covers every major AngularJS feature and shows you how to set up your development
|
||||
environment.
|
||||
1. Download or clone the [Seed App project template](https://github.com/angular/angular-seed).<br/>Gives you a
|
||||
starter app with a directory layout, test harness, and scripts to begin building your application.
|
||||
|
||||
@@ -13,23 +13,25 @@ details for any device.
|
||||
<img class="diagram" src="img/tutorial/catalog_screen.png" width="488" height="413" alt="demo
|
||||
application running in the browser">
|
||||
|
||||
Work through the tutorial to see how Angular makes browsers smarter — without the use of native
|
||||
extensions or plug-ins. As you work through the tutorial, you will:
|
||||
Follow the tutorial to see how Angular makes browsers smarter — without the use of native
|
||||
extensions or plug-ins:
|
||||
|
||||
* See examples of how to use client-side data binding and dependency injection to build dynamic
|
||||
views of data that change immediately in response to user actions.
|
||||
* See how Angular creates listeners on your data without the need for DOM manipulation.
|
||||
* Learn a better, easier way to test your web apps.
|
||||
* Learn how to use Angular services to make common web tasks, such as getting data into your app,
|
||||
easier.
|
||||
* See examples of how to use client-side data binding to build dynamic views of data that change
|
||||
immediately in response to user actions.
|
||||
* See how Angular keeps your views in sync with your data without the need for DOM manipulation.
|
||||
* Learn a better, easier way to test your web apps, with Karma and Protractor.
|
||||
* Learn how to use dependency injection and services to make common web tasks, such as getting data
|
||||
into your app, easier.
|
||||
|
||||
When you finish the tutorial you will be able to:
|
||||
|
||||
* Create a dynamic application that works in all modern browsers.
|
||||
* Define the differences between Angular and common JavaScript frameworks.
|
||||
* Understand how data binding works in AngularJS.
|
||||
* Create and run unit tests.
|
||||
* Create and run end to end tests.
|
||||
* 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.
|
||||
* Get data from a server using Angular services.
|
||||
* Apply animations to your application, using ngAnimate.
|
||||
* Identify resources for learning more about AngularJS.
|
||||
|
||||
The tutorial guides you through the entire process of building a simple application, including
|
||||
@@ -40,31 +42,46 @@ 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
|
||||
|
||||
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:
|
||||
[Step 0 - Bootstrapping](tutorial/step_00).
|
||||
|
||||
# Working with the code
|
||||
|
||||
You can follow along with this tutorial and hack on the code in either the Mac/Linux or the Windows
|
||||
environment. The tutorial relies on the use of the [Git][git] versioning system for source code
|
||||
management.
|
||||
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
|
||||
recommended testing tools.
|
||||
|
||||
You don't need to know anything about Git to follow the tutorial. Select one of the tabs below
|
||||
and follow the instructions for setting up your computer.
|
||||
The tutorial relies on the use of the [Git][git] versioning system for source code management.
|
||||
You don't need to know anything about Git to follow the tutorial other than how to install and run
|
||||
a few git commands.
|
||||
|
||||
|
||||
## Install Git
|
||||
### Install Git
|
||||
|
||||
You'll need Git, which you can get from [the Git site][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:
|
||||
|
||||
- `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
|
||||
|
||||
Clone the [angular-phonecat repository][angular-phonecat] located at GitHub by running the following
|
||||
command:
|
||||
|
||||
```
|
||||
git clone https://github.com/angular/angular-phonecat.git
|
||||
git clone --depth=14 https://github.com/angular/angular-phonecat.git
|
||||
```
|
||||
|
||||
This command creates the `angular-phonecat` 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>
|
||||
|
||||
Change your current directory to `angular-phonecat`.
|
||||
|
||||
```
|
||||
@@ -75,119 +92,183 @@ The tutorial instructions, from now on, assume you are running all commands from
|
||||
`angular-phonecat` directory.
|
||||
|
||||
|
||||
## Install Node.js
|
||||
### Install Node.js
|
||||
|
||||
If you want to run the built-in web-server and the test tools then you will also need
|
||||
Node.js v0.10, or later.
|
||||
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].
|
||||
|
||||
You can download Node.js from the [node.js website][node].
|
||||
You can download a Node.js installer for your operating system from http://nodejs.org/download/.
|
||||
|
||||
|
||||
You can check the version of Node.js that you have installed by running the following command:
|
||||
Check the version of Node.js that you have installed by running the following command:
|
||||
|
||||
```
|
||||
node --version
|
||||
```
|
||||
|
||||
<div class="alert alert-info"><strong>Helpful note:<strong> If you need to run a different versions of node.js
|
||||
In Debian based distributions, there is 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`.
|
||||
|
||||
```
|
||||
apt-get install nodejs-legacy
|
||||
nodejs --version
|
||||
```
|
||||
|
||||
|
||||
<div class="alert alert-info">If you need to run a 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>
|
||||
|
||||
Once you have Node.js installed you can install the tool dependencies by running:
|
||||
Once you have Node.js installed on your machine you can download the tool dependencies by running:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
This command will download the following tools, into the `node_modules` directive:
|
||||
This command will download the following tools, into the `node_modules` directory:
|
||||
|
||||
- [Bower][bower] - client-side code package manager
|
||||
- [http-server][http-server] - simple local static web server
|
||||
- [Http-Server][http-server] - simple local static web server
|
||||
- [Karma][karma] - unit test runner
|
||||
- [protractor][protractor] - end 2 end test runner
|
||||
- [Protractor][protractor] - end to end (E2E) test runner
|
||||
|
||||
Running `npm install` will also automatically run `bower install`, which will download the Angular
|
||||
framework into the `bower_components` directory.
|
||||
Running `npm install` will also automatically use bower to download the Angular 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.
|
||||
</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.
|
||||
tasks that you will need while developing:
|
||||
|
||||
## Running Development Web Server
|
||||
- `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
|
||||
|
||||
The project is preconfigured to provide a simple static web server for hosting the application.
|
||||
Start the web server by running:
|
||||
### Install Helper Tools (optional)
|
||||
|
||||
The Bower, Http-Server, Karma and Protractor modules are also executables, which can be installed
|
||||
globally and run directly from a terminal/command prompt. You don't need to do this to follow the
|
||||
tutorial, but if you decide you do want to run them directly, you can install these modules globally
|
||||
using, `sudo npm install -g ...`.
|
||||
|
||||
For instance to install the Bower command line executable you would do:
|
||||
|
||||
```
|
||||
sudo npm install -g bower
|
||||
```
|
||||
|
||||
*(Omit the sudo if running on Windows)*
|
||||
|
||||
Then you can run the bower tool directly, such as:
|
||||
|
||||
```
|
||||
bower install
|
||||
```
|
||||
|
||||
|
||||
### Running 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
|
||||
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.
|
||||
|
||||
The angular-phonecat project is configured with a simple static web server for hosting the
|
||||
application during development. Start the web server by running:
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
Now you can browse to the application at:
|
||||
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
|
||||
```
|
||||
|
||||
## Running Unit Tests
|
||||
### Running Unit Tests
|
||||
|
||||
The project is preconfigured to use [Karma][karma] to run the unit tests for the application. Start
|
||||
Karma by running:
|
||||
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.
|
||||
|
||||
The angular-phonecat project is configured to use [Karma][karma] to run the unit tests for the
|
||||
application. Start Karma by running:
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
This will start the Karma unit test runner, open up a Chrome browser and execute all the unit tests
|
||||
in this browser. Karma will then sit and watch all the source and test JavaScript files.
|
||||
Whenever one of these files changes Karma re-runs all the unit tests.
|
||||
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:
|
||||
|
||||
It is good to leave this running all the time as you will get immediate feedback from Karma as you
|
||||
are working on the code.
|
||||
- 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
|
||||
|
||||
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 End to End Tests
|
||||
|
||||
The project is preconfigured to use [Protractor][protractor] to run the end to end tests for the
|
||||
application. Set up the binaries protractor needs to run by running:
|
||||
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.
|
||||
|
||||
The End to End tests are kept in the `test/e2e` 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:
|
||||
|
||||
```
|
||||
npm run update-webdriver
|
||||
```
|
||||
(You will only need to do this once) Execute the Protractor test scripts against your application
|
||||
by running:
|
||||
|
||||
*(You should only need to do this once.)*
|
||||
|
||||
Since Protractor works by interacting with a running application, we need to start our web server:
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
Then in a separate terminal/command line window, we can run the Protractor test scripts against the
|
||||
application by running:
|
||||
|
||||
```
|
||||
npm run protractor
|
||||
```
|
||||
|
||||
This will start the Protractor end to end test runner, open up a Chrome browser and execute all the
|
||||
end to end test scripts in this browser. Once the test scripts finish, the browser will close down
|
||||
and Protractor will exit.
|
||||
Protractor will read the configuration file at `test/protractor-conf.js`. This configuration 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
|
||||
|
||||
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.
|
||||
|
||||
<div class="alert alert-info"><strong>Helpful note:</strong> Protractor requires that a web server is running
|
||||
and serving up the application at the default URL: `http://localhost:8000/app/index.html`. You can
|
||||
start the provided development server by running `npm start`.
|
||||
</div>
|
||||
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.
|
||||
|
||||
|
||||
# Get Started
|
||||
|
||||
Now your environment is ready, it is time to get started developing the Angular PhoneCat
|
||||
application.
|
||||
|
||||
<a href="tutorial/step_00" title="Next Step"><span class="btn btn-primary">Step 0 - Bootstrapping</span></a>
|
||||
|
||||
|
||||
|
||||
[git]: http://git-scm.com/download
|
||||
[angular-phonecat]: https://github.com/angular/angular-phonecat
|
||||
[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/
|
||||
[http-server]: https://github.com/nodeapps/http-server
|
||||
[karma]: https://github.com/karma-runner/karma
|
||||
[karma]: https://github.com/karma-runner/karma
|
||||
|
||||
@@ -11,65 +11,27 @@ with the most important source code files, learn how to start the development se
|
||||
angular-seed, and run the application in the browser.
|
||||
|
||||
|
||||
<div class="tabbable" show="true" ng-model="$cookies.platformPreference">
|
||||
<div class="tab-pane well" id="git-mac" title="Git on Mac/Linux" value="gitUnix">
|
||||
<ol>
|
||||
<li><p>In <code>angular-phonecat</code> directory, run this command:</p>
|
||||
<pre>git checkout -f step-0</pre>
|
||||
<p>This resets your workspace to step 0 of the tutorial app.</p>
|
||||
<p>You must repeat this for every future step in the tutorial and change the number to
|
||||
the number of the step you are on. This will cause any changes you made within
|
||||
your working directory to be lost.</p></li>
|
||||
In `angular-phonecat` directory, run this command:
|
||||
|
||||
<li>To see the app running in a browser, do one of the following:
|
||||
<ul>
|
||||
<li><b>For node.js users:</b>
|
||||
<ol>
|
||||
<li>In a <i>separate</i> terminal tab or window, run <code>npm start</code> to start the web server.</li>
|
||||
<li>Open a browser window for the app and navigate to <a
|
||||
href="http://localhost:8000/app/index.html" target="_blank">`http://localhost:8000/app/index.html`</a></li>
|
||||
</ol>
|
||||
</li>
|
||||
<li><b>For other http servers:</b>
|
||||
<ol>
|
||||
<li>Configure the server to serve the files in the <code>angular-phonecat</code> directory.</li>
|
||||
<li>Navigate in your browser to <code>http://localhost:[port-number]/[context-path]/app/index.html</code>.</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
```
|
||||
git checkout -f step-0
|
||||
```
|
||||
|
||||
|
||||
<div class="tab-pane well" id="git-win" title="Git on Windows" value="gitWin">
|
||||
<ol>
|
||||
<li><p>Open Git bash and run this command (in <code>angular-phonecat</code> directory):</p>
|
||||
<pre>git checkout -f step-0</pre>
|
||||
<p>This resets your workspace to step 0 of the tutorial app.</p>
|
||||
<p>You must repeat this for every future step in the tutorial and change the number to
|
||||
the number of the step you are on. This will cause any changes you made within
|
||||
your working directory to be lost.</p></li>
|
||||
<li>To see the app running in a browser, do one of the following:
|
||||
<ul>
|
||||
<li><b>For node.js users:</b>
|
||||
<ol>
|
||||
<li>In a <i>separate</i> terminal tab or window, run <code>node scripts\web-server.js</code> to start the web server.</li>
|
||||
<li>Open a browser window for the app and navigate to <a href="http://localhost:8000/app/index.html" target="_blank">`http://localhost:8000/app/index.html`</a></li>
|
||||
</ol>
|
||||
</li>
|
||||
<li><b>For other http servers:</b>
|
||||
<ol>
|
||||
<li>Configure the server to serve the files in the <code>angular-phonecat</code> directory.</li>
|
||||
<li>Navigate in your browser to <code>http://localhost:[port-number]/[context-path]/app/index.html</code>.</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
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:
|
||||
|
||||
```
|
||||
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">`http://localhost:8000/app/`</a>
|
||||
|
||||
You can now see the page in your browser. It's not very exciting, but that's OK.
|
||||
|
||||
@@ -84,9 +46,9 @@ __`app/index.html`:__
|
||||
<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">
|
||||
<link rel="stylesheet" href="css/bootstrap.css">
|
||||
<script src="lib/angular/angular.js"></script>
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -114,7 +76,7 @@ __`app/index.html`:__
|
||||
|
||||
* AngularJS script tag:
|
||||
|
||||
<script src="lib/angular/angular.js">
|
||||
<script src="bower_components/angular/angular.js">
|
||||
|
||||
This code downloads the `angular.js` script and registers a callback that will be executed by the
|
||||
browser when the containing HTML page is fully downloaded. When the callback is executed, Angular
|
||||
@@ -126,8 +88,10 @@ being the element on which the `ngApp` directive was defined.
|
||||
|
||||
Nothing here {{'yet' + '!'}}
|
||||
|
||||
This line demonstrates the core feature of Angular's templating capabilities – a binding, denoted
|
||||
by double-curlies `{{ }}` as well as a simple expression `'yet' + '!'` used in this binding.
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -171,16 +135,18 @@ 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](https://github.com/angular/angular-seed) which is typically used to bootstrap
|
||||
new Angular projects. The seed project includes the latest Angular libraries, test libraries,
|
||||
scripts and a simple example app, all pre-configured for developing a typical web app.
|
||||
|
||||
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`).
|
||||
|
||||
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/`
|
||||
* Added [Bootstrap](http://getbootstrap.com) files to `app/css/` and `app/img/`
|
||||
* Added a dependency on [Bootstrap](http://getbootstrap.com) in the `bower.json` file.
|
||||
|
||||
|
||||
|
||||
@@ -199,9 +165,5 @@ Now let's go to {@link step_01 step 1} and add some content to the web app.
|
||||
|
||||
<ul doc-tutorial-nav="0"></ul>
|
||||
|
||||
<div style="display: none">
|
||||
Note: During the bootstrap the injector and the root scope will then be associated with the
|
||||
element on which the `ngApp` directive was declared, so when debugging the app you can retrieve
|
||||
them from browser console via `angular.element(rootElement).scope()` and
|
||||
`angular.element(rootElement).injector()`.
|
||||
</div>
|
||||
|
||||
[angular-seed]: https://github.com/angular/angular-seed
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user