Compare commits
842 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22b669407c | |||
| 99a063f9cb | |||
| 27db9f018e | |||
| 2ed2399d49 | |||
| 9d2de344f5 | |||
| 9c0659ca27 | |||
| 3b9963c54a | |||
| 26db937377 | |||
| 1d0e582b3f | |||
| a6b58019e1 | |||
| 38fd896b74 | |||
| f96d553822 | |||
| f4149b70bc | |||
| 8a8c94c3d5 | |||
| d9de526059 | |||
| 0318761f7a | |||
| 74f7b57823 | |||
| 995ccfc9f6 | |||
| 72c244564a | |||
| 4bfe7e8663 | |||
| 63fe144397 | |||
| 0f551e6109 | |||
| 541f4ba21f | |||
| 6e7d04ddb2 | |||
| 06fb391687 | |||
| 203dd4d1e3 | |||
| c991f93817 | |||
| 52ddc13bee | |||
| 1645924d49 | |||
| 6bee655679 | |||
| acd455181d | |||
| a89b6e27a9 | |||
| 46d559dc8d | |||
| 26212fb204 | |||
| 244cfc7cd5 | |||
| c6074dc34c | |||
| 4930ef2bfe | |||
| 1778d347cd | |||
| 4724d56c93 | |||
| 70fcffc375 | |||
| 181e44019e | |||
| 5ce859bcdd | |||
| fb994e4d90 | |||
| 55f26fc246 | |||
| 1161002857 | |||
| 7ccfe92bed | |||
| a126fcfee3 | |||
| f22af2b874 | |||
| 2ca4170903 | |||
| fd57df1235 | |||
| 3010ed4ee5 | |||
| de59ca7107 | |||
| fa3214cfb8 | |||
| a8cacfe938 | |||
| 1a387ba5dd | |||
| 98e4a220fe | |||
| 9001ae2822 | |||
| c405f88bbc | |||
| 159a68ec7b | |||
| b9a56d588f | |||
| f797f83cd6 | |||
| 6df970eb3c | |||
| f88b4be92f | |||
| a7bcc01581 | |||
| 40ed8094d1 | |||
| 1a722f7b19 | |||
| 4991207c7c | |||
| 33514ec384 | |||
| 28bd20ed30 | |||
| c3bfd7f59d | |||
| 553cccdb85 | |||
| c7dd2c24f4 | |||
| fbf5aebda2 | |||
| ab35c16ccf | |||
| 37b6a71678 | |||
| e79ebff223 | |||
| 36b38e1355 | |||
| 3168d95793 | |||
| 538c63f057 | |||
| a9a1461351 | |||
| 29190f9318 | |||
| ca7d2b3b3f | |||
| 81676f8323 | |||
| 6f9d07d9c7 | |||
| 5201b8146a | |||
| 6aa83d04b7 | |||
| cfbe1a701a | |||
| aa38ded59d | |||
| b03957f004 | |||
| 56f33af890 | |||
| 41bea59eb7 | |||
| 6d0dcca6f1 | |||
| f423dc6b51 | |||
| 9fca87ca5b | |||
| ebbaa4ac5e | |||
| 617137231e | |||
| 71b966ace7 | |||
| 488d5b4288 | |||
| 01b8772cca | |||
| 05ac0ba926 | |||
| 399e872228 | |||
| 3115408be7 | |||
| aea39e9fdc | |||
| 51f6752cec | |||
| db58801a55 | |||
| 2e33d170be | |||
| cc6dcb4bc2 | |||
| 294d6793fd | |||
| cefcc46e1b | |||
| c10dba144e | |||
| f05ffd9bf1 | |||
| e208d81293 | |||
| b51fca1a2b | |||
| 6685043ad4 | |||
| f176a2dbb7 | |||
| 3a366bd42b | |||
| 1c5db02638 | |||
| ffd5308d86 | |||
| aed9de9d37 | |||
| aab6825e85 | |||
| c4fad6ad61 | |||
| 2a7c37cf01 | |||
| 194c3adfed | |||
| 371957d9d5 | |||
| 1e5d36d4b0 | |||
| 3749c85829 | |||
| 886b59d317 | |||
| 2048b6df24 | |||
| 5ec0a50d39 | |||
| 60689afe1c | |||
| 881fddaaba | |||
| fda338e8e2 | |||
| 05c8d88126 | |||
| aa57f57ccb | |||
| 51b78f61f9 | |||
| 739100e8a8 | |||
| 20e0c7f966 | |||
| f34e3413c7 | |||
| 4d28598b3d | |||
| 16d6e89f3b | |||
| 6e220ab612 | |||
| ce14e4a6b7 | |||
| b5dbc9834a | |||
| 57f10251db | |||
| 48cd05a1ad | |||
| 6a71ba17f0 | |||
| cf1b175508 | |||
| 5bba68c264 | |||
| e5a4345aac | |||
| 49ef92041f | |||
| 081ae8bfca | |||
| 4f8aa6f5bb | |||
| d7e888b8c6 | |||
| 491b4af92e | |||
| 5930b8ee27 | |||
| 69d9072d5b | |||
| 9b5de0ac97 | |||
| 21b76aed06 | |||
| 363fb16c10 | |||
| e00f9f62e3 | |||
| 507c666d16 | |||
| 743b33ab9d | |||
| 9f8fa79f25 | |||
| fe3c1d44a5 | |||
| 5ae72dc7af | |||
| 47bc98a1ea | |||
| 10920a2857 | |||
| 90a9270dfa | |||
| d4b05dd13a | |||
| 59802bc790 | |||
| b0a35a794a | |||
| a9545e3963 | |||
| 269c15c74b | |||
| 9f48025733 | |||
| 1d0ccd0d73 | |||
| 03e5164528 | |||
| 3047964712 | |||
| 225afb9eb4 | |||
| b377d6b043 | |||
| ac650cb2e6 | |||
| 926088ba77 | |||
| f94cf634d8 | |||
| 85b3786f55 | |||
| 1116a96df6 | |||
| 1c1c9b27cb | |||
| c5e7a5bc9b | |||
| 7550b7bda8 | |||
| 43b97b211b | |||
| 76405897fc | |||
| bb730704f2 | |||
| 8965d57aa0 | |||
| 951a2743d5 | |||
| a2a907e439 | |||
| 308f22ba9a | |||
| 32e2643d90 | |||
| 3edaf0922f | |||
| 1095596962 | |||
| ee0dac4d1d | |||
| 6f1fae8a91 | |||
| f96485085a | |||
| 147650b3be | |||
| 10c426ffa3 | |||
| 785647eb9d | |||
| a2a6279157 | |||
| b50d2a4a1f | |||
| f539fb1f7c | |||
| 2dcb5acb7b | |||
| 905a8fd4a4 | |||
| df9b57a481 | |||
| fee2077d0b | |||
| 691aafc981 | |||
| da7af937d6 | |||
| 1c9b9e24ed | |||
| 46a5b2c298 | |||
| 28e4707dc4 | |||
| 420b5211da | |||
| 04bccee099 | |||
| a404839795 | |||
| 25d4e5cca4 | |||
| c67112563f | |||
| 2b2ec26a75 | |||
| 41f90e5fb0 | |||
| a85c591d36 | |||
| f276ad0d51 | |||
| 33f817b99c | |||
| 6a4124d0fb | |||
| 26d1b34321 | |||
| 7fba6b603f | |||
| c115b37c33 | |||
| d20ba95e28 | |||
| 7945e5010a | |||
| 21d148aedc | |||
| 4c8aeefb62 | |||
| d384834fde | |||
| f975d8d448 | |||
| 2602daf993 | |||
| 9f681c459a | |||
| d9448dcb9f | |||
| e9c718a465 | |||
| 4dc44f7205 | |||
| 499e1b2adf | |||
| 28c6c4dae2 | |||
| 791148a328 | |||
| 01b1845088 | |||
| 56c861c9e1 | |||
| 451e0f6175 | |||
| aee7f1c72f | |||
| 5fe28a0422 | |||
| f56586d9a9 | |||
| ec0baadcb6 | |||
| 9147d5c04a | |||
| cb801378c9 | |||
| 79604f4628 | |||
| bf6cb8ab0d | |||
| de74b3fd85 | |||
| c7a92d2a9a | |||
| da284fc354 | |||
| 90da3059ce | |||
| 651fd05eca | |||
| 85d7e09bef | |||
| 243a57648b | |||
| 158dec330c | |||
| a1e63c8d4a | |||
| 8cc9978d15 | |||
| 78a404da0e | |||
| f1bb8369b5 | |||
| 76b6941b4c | |||
| e72990dc37 | |||
| f338e96ccc | |||
| 0ad2b70862 | |||
| 5261a374a9 | |||
| 832eba5fc9 | |||
| 2b569fc0b2 | |||
| ee6aeb08bf | |||
| 8d43d8b8e7 | |||
| d08f5c6986 | |||
| 3e7fa19197 | |||
| 5d379db72b | |||
| 514639b585 | |||
| 9cd9956dcb | |||
| c7813e9ebf | |||
| ef91b04cdd | |||
| 1acd97e18f | |||
| 513199ee9f | |||
| 33f3c40e93 | |||
| 696cb95d5e | |||
| 457fd21a1a | |||
| 3c6dfbf67d | |||
| 3277b885c4 | |||
| 48a256d04b | |||
| 0579430799 | |||
| 39ac68dac1 | |||
| 87fb44a5d3 | |||
| 5c76b406f7 | |||
| f665968daf | |||
| 5d1291c29d | |||
| ce7f400011 | |||
| 0fc8516b0c | |||
| 28b2a9b583 | |||
| e8549372fc | |||
| 5a434eb74e | |||
| 318de4db66 | |||
| a6c79bf15d | |||
| c6a10a755e | |||
| 1d7bd5bf4f | |||
| 94572e89c2 | |||
| fee7bac392 | |||
| dbb3b0f561 | |||
| b8bfed6a52 | |||
| 7215a89e7d | |||
| b7cca56091 | |||
| 7338e433f8 | |||
| 119ed07d5a | |||
| 5f98ae8323 | |||
| 8a598b43bb | |||
| e5dff4cfbe | |||
| 6c7a9cdd5f | |||
| 3b34f762fe | |||
| fa167ba747 | |||
| a4e60cb697 | |||
| 81150ac77d | |||
| 7ecfa5deba | |||
| 567f9b0136 | |||
| 0c2d3988ab | |||
| efd448d7d3 | |||
| db281c133d | |||
| 6a336ba6a0 | |||
| 67a98112e4 | |||
| f1aea54a9b | |||
| 663788d8c1 | |||
| af0574ebaa | |||
| 4883e95797 | |||
| 9c7c494c3e | |||
| 321180af13 | |||
| e2898c9436 | |||
| c52d0957de | |||
| 7bdc6cb358 | |||
| 2c54a3c081 | |||
| 5078c76c5d | |||
| 30a7e3a144 | |||
| a6afa780b7 | |||
| 3faa01fb15 | |||
| 0f5bcb7356 | |||
| db1cf6d293 | |||
| 6253de3913 | |||
| 32feb2b45f | |||
| 5e37b2a7fd | |||
| 0749eb44e5 | |||
| c900b9c531 | |||
| 6a4597b47d | |||
| d7cb37032b | |||
| 76f47d5632 | |||
| 871bebf7dc | |||
| 7e112c1fc3 | |||
| 59aef48281 | |||
| 2ffda41ab0 | |||
| f70237a3e8 | |||
| 3c86212710 | |||
| 85ef70f428 | |||
| a7244fdcb0 | |||
| 565391d30a | |||
| a88473db8a | |||
| 68f528aa04 | |||
| 3671adbba6 | |||
| 1edb13f784 | |||
| 113a946a99 | |||
| 70caf84634 | |||
| 1b8590a7c5 | |||
| 9955bd05ed | |||
| 02a9543189 | |||
| eda7ef66f7 | |||
| 2d6c218327 | |||
| 7324804bf5 | |||
| 84c04b0b68 | |||
| 735be18344 | |||
| 3ea4477266 | |||
| 0b1b9112a3 | |||
| 489835dd0b | |||
| adbc2b10d2 | |||
| ea6c2473c1 | |||
| b43768a345 | |||
| e47957248f | |||
| 019900d7c2 | |||
| 88bb5518eb | |||
| 3940edced4 | |||
| 7c60e19eb8 | |||
| 632fa30fe3 | |||
| bf2a76d32f | |||
| 9421674dad | |||
| ece8266b01 | |||
| b8b5b885f7 | |||
| e4e30961ca | |||
| 1061c56fe1 | |||
| 7617c08da6 | |||
| a021a376fc | |||
| a7d69c9d42 | |||
| f0f6da304c | |||
| 9425015a69 | |||
| 7b592f9edd | |||
| c966876e57 | |||
| bfce0675e2 | |||
| 2e3c6404f2 | |||
| b04871b43f | |||
| 4ba8e3463a | |||
| 9881e77ccb | |||
| 17ba2a6e7c | |||
| 3bfeda3b2b | |||
| 4fed66da6c | |||
| 7de7059f95 | |||
| cff232a8a2 | |||
| 614ecb7aa6 | |||
| 77b1407e0d | |||
| c4e47e491f | |||
| 87ac4443b6 | |||
| 75f23f0b87 | |||
| 39eecd136f | |||
| 260cfe6dc9 | |||
| 455af41a19 | |||
| f3c8aa2790 | |||
| e2312e3efd | |||
| a460ce4665 | |||
| d06431e530 | |||
| 34ec0d9629 | |||
| 4ac23c0ac5 | |||
| 507cf31ff9 | |||
| 91414918d7 | |||
| 52c21fe51d | |||
| 2f0a50b526 | |||
| 4a80a2e8bc | |||
| 446fac84d9 | |||
| 43f72066e1 | |||
| a909ed1a5c | |||
| 1c6edd416b | |||
| 8bda5ec735 | |||
| 23395ce07d | |||
| c81c9e7cf4 | |||
| 344b4bcb95 | |||
| 34ba294f76 | |||
| 50ecc370b9 | |||
| 622c421696 | |||
| db1180f3b3 | |||
| ca5b27bd0b | |||
| 3d158f629b | |||
| 8aecf46ad2 | |||
| 2072641537 | |||
| 63154c3d1a | |||
| 37cb9ad436 | |||
| 5a3504abdc | |||
| 0dfc1dfebf | |||
| e9c406b246 | |||
| 79b6d55792 | |||
| 8f94b5b277 | |||
| b11120be0a | |||
| bfba95ce46 | |||
| 310f80e78e | |||
| acfda1022d | |||
| 04d4d93e5b | |||
| 8348365df9 | |||
| 16bcdcb61d | |||
| adcfa74327 | |||
| d641901be6 | |||
| eae0a1121f | |||
| 173c9063e7 | |||
| b461551b81 | |||
| 63492a0261 | |||
| adb0e1746b | |||
| 3be79cd6a1 | |||
| e4c6e01791 | |||
| fb76d96009 | |||
| f322c4f3c3 | |||
| 081f6ec7f2 | |||
| 9bb6a30417 | |||
| 97e97d1eb7 | |||
| 1ab4e44443 | |||
| 99eeec358c | |||
| ce8a7525cc | |||
| 5df2e5ce29 | |||
| ef03dfc4a4 | |||
| 4ff9c027b0 | |||
| 20b8ece444 | |||
| 6406e3b01d | |||
| 234053fc9a | |||
| a277bcf0f7 | |||
| 7985416d39 | |||
| dc3013e848 | |||
| 0f2926db38 | |||
| bd59335eba | |||
| 6aa111b333 | |||
| 42b5ce99fb | |||
| 2cb1989d12 | |||
| 1137d91abd | |||
| 288e4e33c3 | |||
| d98c5f03a4 | |||
| 8d20b04f1c | |||
| 56c3666fe5 | |||
| cd21216ff7 | |||
| 3ffdf380c5 | |||
| db5e0ffe12 | |||
| 92e4801d88 | |||
| 6a92e9111f | |||
| ba6d37756e | |||
| 683bd92f56 | |||
| 7700e2df09 | |||
| 3fb809e412 | |||
| 52ea4110d3 | |||
| a3a7afd3aa | |||
| 20bc37fbc8 | |||
| 7761b6c3b0 | |||
| b77e14beea | |||
| a1f461e429 | |||
| 13587193a5 | |||
| a1ff35850c | |||
| 95b3e1ce6c | |||
| b78b12976a | |||
| a31c082de6 | |||
| 5afd54514d | |||
| 72b96ef57a | |||
| 90975db5f9 | |||
| 25bc531802 | |||
| 4e1b36c216 | |||
| d4fa331308 | |||
| a801df719e | |||
| cb74999b17 | |||
| 2764536e8f | |||
| 8ccc0547a8 | |||
| dd14e0c44d | |||
| 7e24590fa3 | |||
| 8b6360338d | |||
| d91cf16796 | |||
| f31c5a3924 | |||
| 16ccac91d0 | |||
| 91b080e6e6 | |||
| feeb19787c | |||
| 98c2db7f9c | |||
| 06aa52efff | |||
| fabc6ab5b0 | |||
| 93c7251f5f | |||
| e1def1b8fe | |||
| e5cab951f4 | |||
| f7eab8d8fe | |||
| 495d40d802 | |||
| d28ae2126e | |||
| 8955cfb646 | |||
| 776972ed9c | |||
| 1358b3ca9b | |||
| e020b8993e | |||
| b2b896f949 | |||
| 3c5827b6f5 | |||
| 0e03644dad | |||
| 112024271b | |||
| bca0a1f786 | |||
| 959f2bbb2d | |||
| 6a47c0d75d | |||
| b0248b7894 | |||
| 6fd41e7f59 | |||
| 3297bbd188 | |||
| 8863836cd4 | |||
| b8fb0c4573 | |||
| 17b700a339 | |||
| 811b20e3b9 | |||
| 5ded3d3e73 | |||
| 986647a968 | |||
| 7b51243be5 | |||
| 798fb18542 | |||
| 99d601a048 | |||
| a6e9174a27 | |||
| dec8a0eb72 | |||
| 98776487a0 | |||
| b3ef5e0852 | |||
| 04efdd5bfa | |||
| 9f5d76e16b | |||
| 525be5b7d4 | |||
| 042e0f1f0a | |||
| 03872983a4 | |||
| 62f79e820f | |||
| e5c26e92cc | |||
| 4bcb307abc | |||
| 0e729e1dd5 | |||
| 9bb184d181 | |||
| a7a053f5be | |||
| 9630159444 | |||
| e3be5d6efa | |||
| c2173c1298 | |||
| 0b94e8a8bf | |||
| 983374c574 | |||
| 05d3ed0d9b | |||
| 1b25f80cd2 | |||
| 6cdbda7cf1 | |||
| c9e6cf9be0 | |||
| c7ed8a33af | |||
| 6988667e5e | |||
| e57cf13d5d | |||
| e4e5677fbd | |||
| 8b6b428271 | |||
| 8f0b482596 | |||
| 6428ed5bb5 | |||
| bc41ad8aa8 | |||
| 6858caf251 | |||
| 20604e7fc4 | |||
| b7b06d8477 | |||
| de9777d819 | |||
| 1b06f33f30 | |||
| ca6e266869 | |||
| 23c4ae522a | |||
| 3112f8e910 | |||
| 21ab82906e | |||
| 1dd206ef85 | |||
| da5db4b1b3 | |||
| 3694390c90 | |||
| 36a3c81177 | |||
| b0e7d548d0 | |||
| e4bb838795 | |||
| 0ea535035a | |||
| e9aba90f7f | |||
| f50b0cb393 | |||
| 8b3bec7e07 | |||
| 6752337629 | |||
| 48ad7486d9 | |||
| 341b834229 | |||
| 7a668cdd7d | |||
| 5a674f3bb9 | |||
| e94b37e20e | |||
| f5ebcbacf8 | |||
| 0812af49bd | |||
| c3ae6ed78e | |||
| c3a2691115 | |||
| 6976d6d8d8 | |||
| b75c0d8d05 | |||
| df6fade6e6 | |||
| 983b059812 | |||
| 25e8c5927c | |||
| 6628b4f1e5 | |||
| 6e18b50a5b | |||
| 77419cf19f | |||
| 193153c3d3 | |||
| 6a0686d434 | |||
| 08c9a5e9e7 | |||
| a72c12bd70 | |||
| 73e38658c4 | |||
| b641181b93 | |||
| 7ffb2d3c17 | |||
| 023b777a56 | |||
| 0c9480de8c | |||
| 0bc275461c | |||
| 0b5ecc64f0 | |||
| b183eae7ae | |||
| 7ddbc9aa35 | |||
| b2a937d425 | |||
| 4473b81cda | |||
| 98528be311 | |||
| 9190d4c3ad | |||
| d9ec9951e2 | |||
| fedafdc677 | |||
| 26c36bb4d1 | |||
| c7a2028ab3 | |||
| 596af70101 | |||
| 96c73a0672 | |||
| 1537651c8c | |||
| d59aeb4e0b | |||
| 551a33db56 | |||
| cd91640146 | |||
| ab5824ee12 | |||
| 5c50723535 | |||
| 546a277d65 | |||
| a5ff651a59 | |||
| ccd47ec904 | |||
| 2e23a3cdbc | |||
| ca7f4a387c | |||
| 898a3fd3b9 | |||
| 9473371343 | |||
| e5275590db | |||
| 78297d252d | |||
| e5e0884eaf | |||
| 551d1c20cf | |||
| f5aa207960 | |||
| 4412fe238f | |||
| 8088284f66 | |||
| 25f1bbaad1 | |||
| bd7b217729 | |||
| 50557a6cd3 | |||
| 1c13a4f45d | |||
| 1a98c0ee34 | |||
| b837fc3116 | |||
| aff74ec87b | |||
| 077ee37942 | |||
| 0efef2385f | |||
| 7da22e6685 | |||
| 92bdd7627f | |||
| 39ebb06baf | |||
| 9cf6b197ab | |||
| 4971ef12d4 | |||
| 7c792f4cc9 | |||
| 4daafd3dbe | |||
| fe11265fdc | |||
| 75292a6cb5 | |||
| 4ff6c85792 | |||
| e26bc2370b | |||
| 33713deeb8 | |||
| 17715fa366 | |||
| 96288d02d3 | |||
| 8be98e4fdf | |||
| cf83b4f445 | |||
| 15bfea8339 | |||
| 9b90c32f31 | |||
| f4bf744516 | |||
| 0d96995fcc | |||
| 2a85a634f8 | |||
| 00d2b2c4cf | |||
| e688f07023 | |||
| 8e2f7d37e4 | |||
| f5bc3ed9b4 | |||
| 11d60af3dc | |||
| 865f6065e7 | |||
| 91c0b364af | |||
| fce07f55e5 | |||
| c8768d12f2 | |||
| af6342d6fb | |||
| a179757fad | |||
| 35eada68c4 | |||
| bfad2a4f4a | |||
| 54e816552f | |||
| 4fc734665e | |||
| b8736e65b0 | |||
| b9bed7d9da | |||
| b2fc39d2dd | |||
| 7c0731edb2 | |||
| 8fe781fbe7 | |||
| 23932a26ff | |||
| 3ca4ca463c | |||
| 395f3ec638 | |||
| 964a901bd8 | |||
| a995ee17ee | |||
| 794d1c1ebe | |||
| 662fb282c1 | |||
| ffb6b2fb56 | |||
| 941c1c35f1 | |||
| 29a05984fe | |||
| 4038aabffa | |||
| 773efd0812 | |||
| 80881949fc | |||
| 2c8d87e064 | |||
| 33c67ce785 | |||
| ad4296d966 | |||
| 469b14a525 | |||
| 1caf0b6bee | |||
| 9f716dd590 | |||
| 914a934b6f | |||
| a4ada8ba9c | |||
| 40c974ab14 | |||
| 06f002b161 | |||
| 8226ff8b8e | |||
| d67e999dfb | |||
| a8e03b3a90 | |||
| 6f3e26c404 | |||
| 1c75ea613d | |||
| c8e1db2050 | |||
| 74ed28665d | |||
| 4ad0ca130d | |||
| b51dd3010d | |||
| 6e6f31943c | |||
| 66fee7e22a | |||
| beea571660 | |||
| 0c8a9a0e1a | |||
| fd83d3724a | |||
| 2fcfd75a14 | |||
| 2d40507547 | |||
| ecf9304811 | |||
| b9ab88776b | |||
| 00bf218304 | |||
| 95fbf168d1 | |||
| 51a27c0f1a | |||
| f02811f0bb | |||
| bcf78ebb18 | |||
| 3050dd1b47 | |||
| 1efdb4745a | |||
| d73f7dff45 | |||
| f047ad2628 | |||
| 049d3def80 | |||
| 00778aa239 | |||
| 6b123a0419 | |||
| 4079eea6b3 | |||
| d277ac2eb8 | |||
| 9deb123d04 | |||
| 1c2d2e8ba0 | |||
| 144bcc84ab | |||
| 693021c449 | |||
| dc818e1165 | |||
| e51174bf13 | |||
| 7f3f3dd3eb | |||
| d077966ff1 | |||
| 0df4ff800a | |||
| 6208b76afa | |||
| 70dac5ae82 | |||
| aafbd94439 | |||
| e732f8e579 | |||
| 9f67da6252 | |||
| b3a3c6a72e | |||
| e8cdabe129 | |||
| 3af71bee75 | |||
| f2724b2bbc | |||
| 38500669f6 | |||
| 862c9d8bb2 | |||
| 240d5896ec | |||
| 64ef084b91 | |||
| 8b27c3f064 | |||
| 8366622bed | |||
| d3de0066b0 | |||
| b7e5133b2e | |||
| 42c97c5db5 | |||
| e1f4f23f78 | |||
| 630280c7fb | |||
| 496a67c10e | |||
| 7dcfe5e03e | |||
| 9e83b8355e | |||
| 03726f7fbd | |||
| 75893ae9e7 | |||
| d9ca245917 | |||
| 3259aabdf3 | |||
| befeeb3689 | |||
| 79577c5d31 | |||
| b71d7c3f3c | |||
| fa4c7b7f1d | |||
| 652b83eb22 | |||
| 20cf7d5e3a | |||
| ded2518756 | |||
| b366f0352a | |||
| 76c2491a31 | |||
| ef3df93afe | |||
| 106f90aafa | |||
| 86e8088b38 | |||
| a654bdfed9 | |||
| 2c22c57e58 | |||
| a4dfa4d061 | |||
| bc0d8c4eea | |||
| 9b84fcad76 | |||
| f33ce173c9 | |||
| 181fc567d8 | |||
| 94207f8fb6 | |||
| 35a21532b7 | |||
| 1c97a6057b | |||
| 658a865c5b | |||
| f8cf28a788 |
@@ -0,0 +1,27 @@
|
||||
***Note*: for support questions, please use one of these channels: https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#question. This repository's issues are reserved for feature requests and bug reports.**
|
||||
|
||||
**Do you want to request a *feature* or report a *bug*?**
|
||||
|
||||
|
||||
|
||||
**What is the current behavior?**
|
||||
|
||||
|
||||
|
||||
**If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://plnkr.co or similar (template: http://plnkr.co/edit/tpl:yBpEi4).**
|
||||
|
||||
|
||||
|
||||
**What is the expected behavior?**
|
||||
|
||||
|
||||
|
||||
**What is the motivation / use case for changing the behavior?**
|
||||
|
||||
|
||||
|
||||
**Which versions of Angular, and which browser / OS are affected by this issue? Did this work in previous versions of Angular? Please also test with the latest stable and snapshot (https://code.angularjs.org/snapshot/) versions.**
|
||||
|
||||
|
||||
|
||||
**Other information (e.g. stacktraces, related issues, suggestions how to fix)**
|
||||
@@ -0,0 +1,23 @@
|
||||
**What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)**
|
||||
|
||||
|
||||
|
||||
**What is the current behavior? (You can also link to an open issue here)**
|
||||
|
||||
|
||||
|
||||
**What is the new behavior (if this is a feature change)?**
|
||||
|
||||
|
||||
|
||||
**Does this PR introduce a breaking change?**
|
||||
|
||||
|
||||
|
||||
**Please check if the PR fulfills these requirements**
|
||||
- [ ] The commit message follows our guidelines: https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit-message-format
|
||||
- [ ] Tests for the changes have been added (for bug fixes / features)
|
||||
- [ ] Docs have been added / updated (for bug fixes / features)
|
||||
|
||||
**Other information**:
|
||||
|
||||
+8
-3
@@ -1,19 +1,24 @@
|
||||
{
|
||||
"bitwise": true,
|
||||
"esversion": 6,
|
||||
"immed": true,
|
||||
"newcap": true,
|
||||
"noarg": true,
|
||||
"noempty": true,
|
||||
"nonew": true,
|
||||
"trailing": true,
|
||||
"maxlen": 200,
|
||||
"boss": true,
|
||||
"eqnull": true,
|
||||
"expr": true,
|
||||
"globalstrict": true,
|
||||
"laxbreak": true,
|
||||
"loopfunc": true,
|
||||
"strict": "global",
|
||||
"sub": true,
|
||||
"undef": true,
|
||||
"indent": 2
|
||||
"indent": 2,
|
||||
|
||||
"globals": {
|
||||
"ArrayBuffer": false,
|
||||
"Uint8Array": false
|
||||
}
|
||||
}
|
||||
|
||||
+20
-23
@@ -1,7 +1,7 @@
|
||||
language: node_js
|
||||
sudo: false
|
||||
node_js:
|
||||
- '0.10'
|
||||
- '4.4'
|
||||
|
||||
cache:
|
||||
directories:
|
||||
@@ -15,28 +15,27 @@ branches:
|
||||
|
||||
env:
|
||||
matrix:
|
||||
- JOB=ci-checks
|
||||
- JOB=unit BROWSER_PROVIDER=saucelabs
|
||||
- JOB=docs-e2e BROWSER_PROVIDER=saucelabs
|
||||
- JOB=e2e TEST_TARGET=jqlite BROWSER_PROVIDER=saucelabs
|
||||
- JOB=e2e TEST_TARGET=jquery-2.1 BROWSER_PROVIDER=saucelabs
|
||||
- JOB=e2e TEST_TARGET=jquery BROWSER_PROVIDER=saucelabs
|
||||
- JOB=unit BROWSER_PROVIDER=browserstack
|
||||
- JOB=docs-e2e BROWSER_PROVIDER=browserstack
|
||||
- JOB=e2e TEST_TARGET=jqlite BROWSER_PROVIDER=browserstack
|
||||
- JOB=e2e TEST_TARGET=jquery BROWSER_PROVIDER=browserstack
|
||||
global:
|
||||
- CXX=g++-4.8 # node 4 likes the G++ v4.8 compiler
|
||||
- SAUCE_USERNAME=angular-ci
|
||||
- SAUCE_ACCESS_KEY=9b988f434ff8-fbca-8aa4-4ae3-35442987
|
||||
- BROWSER_STACK_USERNAME=VojtaJina
|
||||
- BROWSER_STACK_ACCESS_KEY=QCQJ1ZpWXpBkSwEdD8ev
|
||||
- LOGS_DIR=/tmp/angular-build/logs
|
||||
- BROWSER_PROVIDER_READY_FILE=/tmp/browsersprovider-tunnel-ready
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- env: "JOB=unit BROWSER_PROVIDER=browserstack"
|
||||
- env: "JOB=docs-e2e BROWSER_PROVIDER=browserstack"
|
||||
- env: "JOB=e2e TEST_TARGET=jqlite BROWSER_PROVIDER=browserstack"
|
||||
- env: "JOB=e2e TEST_TARGET=jquery BROWSER_PROVIDER=browserstack"
|
||||
# node 4 likes the G++ v4.8 compiler
|
||||
# see https://docs.travis-ci.com/user/languages/javascript-with-nodejs#Node.js-v4-(or-io.js-v3)-compiler-requirements
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
|
||||
install:
|
||||
# Check the size of caches
|
||||
@@ -46,27 +45,25 @@ install:
|
||||
- npm config set spin false
|
||||
# Log HTTP requests
|
||||
- npm config set loglevel http
|
||||
- npm install -g npm@2.5
|
||||
# Instal npm dependecies and ensure that npm cache is not stale
|
||||
- scripts/npm/install-dependencies.sh
|
||||
#- npm install -g npm@2.5
|
||||
# Install npm dependencies and ensure that npm cache is not stale
|
||||
- npm install
|
||||
|
||||
before_script:
|
||||
- mkdir -p $LOGS_DIR
|
||||
- ./scripts/travis/start_browser_provider.sh
|
||||
- npm install -g grunt-cli
|
||||
- grunt package
|
||||
- ./scripts/travis/wait_for_browser_provider.sh
|
||||
- ./scripts/travis/before_build.sh
|
||||
|
||||
script:
|
||||
- ./scripts/travis/build.sh
|
||||
|
||||
after_script:
|
||||
- ./scripts/travis/tear_down_browser_provider.sh
|
||||
- ./scripts/travis/print_logs.sh
|
||||
|
||||
notifications:
|
||||
webhooks:
|
||||
urls:
|
||||
- https://webhooks.gitter.im/e/d2120f3f2bb39a4531b2
|
||||
on_success: change # options: [always|never|change] default: always
|
||||
- http://104.197.9.155:8484/hubot/travis/activity #hubot-server
|
||||
on_success: always # options: [always|never|change] default: always
|
||||
on_failure: always # options: [always|never|change] default: always
|
||||
on_start: false # default: false
|
||||
on_start: always # default: false
|
||||
|
||||
+1821
-65
File diff suppressed because it is too large
Load Diff
+11
-3
@@ -71,7 +71,7 @@ chances of your issue being dealt with quickly:
|
||||
* **Angular Version(s)** - is it a regression?
|
||||
* **Browsers and Operating System** - is this a problem with all browsers or only IE8?
|
||||
* **Reproduce the Error** - provide a live example (using [Plunker][plunker] or
|
||||
[JSFiddle][jsfiddle]) or a unambiguous set of steps.
|
||||
[JSFiddle][jsfiddle]) or an unambiguous set of steps.
|
||||
* **Related Issues** - has a similar issue been reported before?
|
||||
* **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be
|
||||
causing the problem (line of code or commit)
|
||||
@@ -123,13 +123,19 @@ Before you submit your pull request consider the following guidelines:
|
||||
* 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):
|
||||
* Commit your changes to your branch (e.g. `my-fix-branch`).
|
||||
* Push the changes to your GitHub repository (this will update your Pull Request).
|
||||
|
||||
If the PR gets too outdated we may ask you to rebase and force push to update the PR:
|
||||
|
||||
```shell
|
||||
git rebase master -i
|
||||
git push origin my-fix-branch -f
|
||||
```
|
||||
|
||||
*WARNING. Squashing or reverting commits and forced push thereafter may remove GitHub comments
|
||||
on code that were previously made by you and others in your commits.*
|
||||
|
||||
That's it! Thank you for your contribution!
|
||||
|
||||
#### After your pull request is merged
|
||||
@@ -187,6 +193,8 @@ We have very precise rules over how our git commit messages can be formatted. T
|
||||
readable messages** that are easy to follow when looking through the **project history**. But also,
|
||||
we use the git commit messages to **generate the AngularJS change log**.
|
||||
|
||||
The commit message formatting can be added using a typical git workflow or through the use of a CLI wizard ([Commitizen](https://github.com/commitizen/cz-cli)). To use the wizard, run `npm run commit` in your terminal after staging your changes in git.
|
||||
|
||||
### Commit Message Format
|
||||
Each commit message consists of a **header**, a **body** and a **footer**. The header has a special
|
||||
format that includes a **type**, a **scope** and a **subject**:
|
||||
@@ -272,7 +280,7 @@ You can find out more detailed information about contributing in the
|
||||
[groups]: https://groups.google.com/forum/?fromgroups#!forum/angular
|
||||
[individual-cla]: http://code.google.com/legal/individual-cla-v1.0.html
|
||||
[irc]: http://webchat.freenode.net/?channels=angularjs&uio=d4
|
||||
[js-style-guide]: http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml
|
||||
[js-style-guide]: https://google.github.io/styleguide/javascriptguide.xml
|
||||
[jsfiddle]: http://jsfiddle.net/
|
||||
[list]: https://groups.google.com/forum/?fromgroups#!forum/angular
|
||||
[ngDocs]: https://github.com/angular/angular.js/wiki/Writing-AngularJS-Documentation
|
||||
|
||||
+40
-15
@@ -17,6 +17,10 @@ module.exports = function(grunt) {
|
||||
NG_VERSION.cdn = versionInfo.cdnVersion;
|
||||
var dist = 'angular-'+ NG_VERSION.full;
|
||||
|
||||
if (versionInfo.cdnVersion == null) {
|
||||
throw new Error('Unable to read CDN version, are you offline or has the CDN not been properly pushed?');
|
||||
}
|
||||
|
||||
//config
|
||||
grunt.initConfig({
|
||||
NG_VERSION: NG_VERSION,
|
||||
@@ -79,6 +83,8 @@ module.exports = function(grunt) {
|
||||
tests: {
|
||||
jqlite: 'karma-jqlite.conf.js',
|
||||
jquery: 'karma-jquery.conf.js',
|
||||
'jquery-2.2': 'karma-jquery-2.2.conf.js',
|
||||
'jquery-2.1': 'karma-jquery-2.1.conf.js',
|
||||
docs: 'karma-docs.conf.js',
|
||||
modules: 'karma-modules.conf.js'
|
||||
},
|
||||
@@ -87,6 +93,8 @@ module.exports = function(grunt) {
|
||||
autotest: {
|
||||
jqlite: 'karma-jqlite.conf.js',
|
||||
jquery: 'karma-jquery.conf.js',
|
||||
'jquery-2.2': 'karma-jquery-2.2.conf.js',
|
||||
'jquery-2.1': 'karma-jquery-2.1.conf.js',
|
||||
modules: 'karma-modules.conf.js',
|
||||
docs: 'karma-docs.conf.js'
|
||||
},
|
||||
@@ -135,6 +143,9 @@ module.exports = function(grunt) {
|
||||
ngMock: {
|
||||
files: { src: 'src/ngMock/**/*.js' },
|
||||
},
|
||||
ngParseExt: {
|
||||
files: { src: 'src/ngParseExt/**/*.js' },
|
||||
},
|
||||
ngResource: {
|
||||
files: { src: 'src/ngResource/**/*.js' },
|
||||
},
|
||||
@@ -162,7 +173,7 @@ module.exports = function(grunt) {
|
||||
'!src/angular.bind.js' // we ignore this file since contains an early return statement
|
||||
],
|
||||
options: {
|
||||
config: ".jscsrc"
|
||||
config: '.jscsrc'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -231,9 +242,13 @@ module.exports = function(grunt) {
|
||||
dest: 'build/angular-aria.js',
|
||||
src: util.wrap(files['angularModules']['ngAria'], 'module')
|
||||
},
|
||||
parseext: {
|
||||
dest: 'build/angular-parse-ext.js',
|
||||
src: util.wrap(files['angularModules']['ngParseExt'], 'module')
|
||||
},
|
||||
"promises-aplus-adapter": {
|
||||
dest:'tmp/promises-aplus-adapter++.js',
|
||||
src:['src/ng/q.js','lib/promises-aplus/promises-aplus-test-adapter.js']
|
||||
src:['src/ng/q.js', 'lib/promises-aplus/promises-aplus-test-adapter.js']
|
||||
}
|
||||
},
|
||||
|
||||
@@ -249,11 +264,12 @@ module.exports = function(grunt) {
|
||||
resource: 'build/angular-resource.js',
|
||||
route: 'build/angular-route.js',
|
||||
sanitize: 'build/angular-sanitize.js',
|
||||
aria: 'build/angular-aria.js'
|
||||
aria: 'build/angular-aria.js',
|
||||
parseext: 'build/angular-parse-ext.js'
|
||||
},
|
||||
|
||||
|
||||
"ddescribe-iit": {
|
||||
'ddescribe-iit': {
|
||||
files: [
|
||||
'src/**/*.js',
|
||||
'test/**/*.js',
|
||||
@@ -264,17 +280,22 @@ module.exports = function(grunt) {
|
||||
],
|
||||
options: {
|
||||
disallowed: [
|
||||
'fit',
|
||||
'iit',
|
||||
'xit',
|
||||
'fthey',
|
||||
'tthey',
|
||||
'xthey',
|
||||
'fdescribe',
|
||||
'ddescribe',
|
||||
'xdescribe'
|
||||
'xdescribe',
|
||||
'it.only',
|
||||
'describe.only'
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"merge-conflict": {
|
||||
'merge-conflict': {
|
||||
files: [
|
||||
'src/**/*',
|
||||
'test/**/*',
|
||||
@@ -304,11 +325,11 @@ module.exports = function(grunt) {
|
||||
},
|
||||
|
||||
shell: {
|
||||
"npm-install": {
|
||||
command: path.normalize('scripts/npm/install-dependencies.sh')
|
||||
'npm-install': {
|
||||
command: 'node scripts/npm/check-node-modules.js'
|
||||
},
|
||||
|
||||
"promises-aplus-tests": {
|
||||
'promises-aplus-tests': {
|
||||
options: {
|
||||
stdout: false,
|
||||
stderr: true,
|
||||
@@ -339,22 +360,26 @@ module.exports = function(grunt) {
|
||||
grunt.task.run('shell:npm-install');
|
||||
}
|
||||
|
||||
|
||||
|
||||
//alias tasks
|
||||
grunt.registerTask('test', 'Run unit, docs and e2e tests with Karma', ['jshint', 'jscs', 'package','test:unit','test:promises-aplus', 'tests:docs', 'test:protractor']);
|
||||
grunt.registerTask('test', 'Run unit, docs and e2e tests with Karma', ['jshint', 'jscs', 'package', 'test:unit', 'test:promises-aplus', 'tests:docs', 'test:protractor']);
|
||||
grunt.registerTask('test:jqlite', 'Run the unit tests with Karma' , ['tests:jqlite']);
|
||||
grunt.registerTask('test:jquery', 'Run the jQuery unit tests with Karma', ['tests:jquery']);
|
||||
grunt.registerTask('test:jquery', 'Run the jQuery (latest) unit tests with Karma', ['tests:jquery']);
|
||||
grunt.registerTask('test:jquery-2.2', 'Run the jQuery 2.2 unit tests with Karma', ['tests:jquery-2.2']);
|
||||
grunt.registerTask('test:jquery-2.1', 'Run the jQuery 2.1 unit tests with Karma', ['tests:jquery-2.1']);
|
||||
grunt.registerTask('test:modules', 'Run the Karma module tests with Karma', ['build', 'tests:modules']);
|
||||
grunt.registerTask('test:docs', 'Run the doc-page tests with Karma', ['package', 'tests:docs']);
|
||||
grunt.registerTask('test:unit', 'Run unit, jQuery and Karma module tests with Karma', ['test:jqlite', 'test:jquery', 'test:modules']);
|
||||
grunt.registerTask('test:unit', 'Run unit, jQuery and Karma module tests with Karma', ['test:jqlite', 'test:jquery', 'test:jquery-2.2', 'test:jquery-2.1', 'test:modules']);
|
||||
grunt.registerTask('test:protractor', 'Run the end to end tests with Protractor and keep a test server running in the background', ['webdriver', 'connect:testserver', 'protractor:normal']);
|
||||
grunt.registerTask('test:travis-protractor', 'Run the end to end tests with Protractor for Travis CI builds', ['connect:testserver', 'protractor:travis']);
|
||||
grunt.registerTask('test:ci-protractor', 'Run the end to end tests with Protractor for Jenkins CI builds', ['webdriver', 'connect:testserver', 'protractor:jenkins']);
|
||||
grunt.registerTask('test:e2e', 'Alias for test:protractor', ['test:protractor']);
|
||||
grunt.registerTask('test:promises-aplus',['build:promises-aplus-adapter','shell:promises-aplus-tests']);
|
||||
grunt.registerTask('test:promises-aplus',['build:promises-aplus-adapter', 'shell:promises-aplus-tests']);
|
||||
|
||||
grunt.registerTask('minify', ['bower','clean', 'build', 'minall']);
|
||||
grunt.registerTask('minify', ['bower', 'clean', 'build', 'minall']);
|
||||
grunt.registerTask('webserver', ['connect:devserver']);
|
||||
grunt.registerTask('package', ['bower','clean', 'buildall', 'minall', 'collect-errors', 'docs', 'copy', 'write', 'compress']);
|
||||
grunt.registerTask('package', ['bower', 'validate-angular-files', 'clean', 'buildall', 'minall', 'collect-errors', 'docs', 'copy', 'write', 'compress']);
|
||||
grunt.registerTask('ci-checks', ['ddescribe-iit', 'merge-conflict', 'jshint', 'jscs']);
|
||||
grunt.registerTask('default', ['package']);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2015 Google, Inc. http://angularjs.org
|
||||
Copyright (c) 2010-2016 Google, Inc. http://angularjs.org
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -8,25 +8,27 @@ synchronizes data from your UI (view) with your JavaScript objects (model) throu
|
||||
binding. To help you structure your application better and make it easy to test, AngularJS teaches
|
||||
the browser how to do dependency injection and inversion of control.
|
||||
|
||||
Oh yeah and it helps with server-side communication, taming async callbacks with promises and
|
||||
deferreds. It also makes client-side navigation and deeplinking with hashbang urls or HTML5 pushState a
|
||||
piece of cake. Best of all?? It makes development fun!
|
||||
It also helps with server-side communication, taming async callbacks with promises and deferreds,
|
||||
and it makes client-side navigation and deeplinking with hashbang urls or HTML5 pushState a
|
||||
piece of cake. Best of all? It makes development fun!
|
||||
|
||||
* Web site: http://angularjs.org
|
||||
* Tutorial: http://docs.angularjs.org/tutorial
|
||||
* API Docs: http://docs.angularjs.org/api
|
||||
* Developer Guide: http://docs.angularjs.org/guide
|
||||
* Web site: https://angularjs.org
|
||||
* Tutorial: https://docs.angularjs.org/tutorial
|
||||
* API Docs: https://docs.angularjs.org/api
|
||||
* Developer Guide: https://docs.angularjs.org/guide
|
||||
* Contribution guidelines: [CONTRIBUTING.md](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md)
|
||||
* Dashboard: http://dashboard.angularjs.org
|
||||
* Dashboard: https://dashboard.angularjs.org
|
||||
|
||||
##### Looking for Angular 2? Go here: https://github.com/angular/angular
|
||||
|
||||
Building AngularJS
|
||||
---------
|
||||
[Once you have your environment setup](http://docs.angularjs.org/misc/contribute) just run:
|
||||
[Once you have set up your environment](https://docs.angularjs.org/misc/contribute), just run:
|
||||
|
||||
grunt package
|
||||
|
||||
|
||||
Running Tests
|
||||
Running tests
|
||||
-------------
|
||||
To execute all unit tests, use:
|
||||
|
||||
@@ -37,9 +39,36 @@ To execute end-to-end (e2e) tests, use:
|
||||
grunt package
|
||||
grunt test:e2e
|
||||
|
||||
To learn more about the grunt tasks, run `grunt --help` and also read our
|
||||
[contribution guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md).
|
||||
To learn more about the grunt tasks, run `grunt --help`
|
||||
|
||||
Contribute & Develop
|
||||
--------------------
|
||||
|
||||
We've set up a separate document for our [contribution guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md).
|
||||
|
||||
|
||||
[](https://github.com/igrigorik/ga-beacon)
|
||||
|
||||
What to use AngularJS for and when to use it
|
||||
---------
|
||||
AngularJS is the next generation framework where each component is designed to work with every other component in an interconnected way like a well-oiled machine. AngularJS is JavaScript MVC made easy and done right. (Well it is not really MVC, read on, to understand what this means.)
|
||||
|
||||
#### MVC, no, MV* done the right way!
|
||||
MVC, short for Model-View-Controller, is a design pattern, i.e. how the code should be organized and how the different parts of an application separated for proper readability and debugging. Model is the data and the database. View is the user interface and what the user sees. Controller is the main link between Model and View. These are the three pillars of major programming frameworks present on the market today. On the other hand AngularJS works on MV*, short for Model-View-_Whatever_. The _Whatever_ is AngularJS's way of telling that you may create any kind of linking between the Model and the View here.
|
||||
|
||||
Unlike other frameworks in any programming language, where MVC, the three separate components, each one has to be written and then connected by the programmer, AngularJS helps the programmer by asking him/her to just create these and everything else will be taken care of by AngularJS.
|
||||
|
||||
#### Interconnection with HTML at the root level
|
||||
AngularJS uses HTML to define the user's interface. AngularJS also enables the programmer to write new HTML tags (AngularJS Directives) and increase the readability and understandability of the HTML code. Directives are AngularJS’s way of bringing additional functionality to HTML. Directives achieve this by enabling us to invent our own HTML elements. This also helps in making the code DRY (Don't Repeat Yourself), which means once created, a new directive can be used anywhere within the application.
|
||||
|
||||
#### Data Handling made simple
|
||||
Data and Data Models in AngularJS are plain JavaScript objects and one can add and change properties directly on it and loop over objects and arrays at will.
|
||||
|
||||
#### Two-way Data Binding
|
||||
One of AngularJS's strongest features. Two-way Data Binding means that if something changes in the Model, the change gets reflected in the View instantaneously, and the same happens the other way around. This is also referred to as Reactive Programming, i.e. suppose `a = b + c` is being programmed and after this, if the value of `b` and/or `c` is changed then the value of `a` will be automatically updated to reflect the change. AngularJS uses its "scopes" as a glue between the Model and View and makes these updates in one available for the other.
|
||||
|
||||
#### Less Written Code and Easily Maintainable Code
|
||||
Everything in AngularJS is created to enable the programmer to end up writing less code that is easily maintainable and readable by any other new person on the team. Believe it or not, one can write a complete working two-way data binded application in less than 10 lines of code. Try and see for yourself!
|
||||
|
||||
#### Testing Ready
|
||||
AngularJS has Dependency Injection, i.e. it takes care of providing all the necessary dependencies to its controllers whenever required. This helps in making the AngularJS code ready for unit testing by making use of mock dependencies created and injected. This makes AngularJS more modular and easily testable thus in turn helping a team create more robust applications.
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ The following is done automatically so you don't have to worry about it:
|
||||
This process based on the idea of minimizing user pain
|
||||
[from this blog post](http://www.lostgarden.com/2008/05/improving-bug-triage-with-user-pain.html).
|
||||
|
||||
1. Open the list of [non triaged issues](https://github.com/angular/angular.js/issues?direction=desc&milestone=none&page=1&sort=created&state=open)
|
||||
1. Open the list of [non triaged issues](https://github.com/angular/angular.js/issues?q=is%3Aopen+sort%3Acreated-desc+no%3Amilestone)
|
||||
* Sort by submit date, with the newest issues first
|
||||
* You don't have to do issues in order; feel free to pick and choose issues as you please.
|
||||
* You can triage older issues as well
|
||||
|
||||
Vendored
+26
-3
@@ -5,6 +5,7 @@ var angularFiles = {
|
||||
'src/minErr.js',
|
||||
'src/Angular.js',
|
||||
'src/loader.js',
|
||||
'src/shallowCopy.js',
|
||||
'src/stringify.js',
|
||||
'src/AngularPublic.js',
|
||||
'src/jqLite.js',
|
||||
@@ -14,6 +15,7 @@ var angularFiles = {
|
||||
|
||||
'src/ng/anchorScroll.js',
|
||||
'src/ng/animate.js',
|
||||
'src/ng/animateRunner.js',
|
||||
'src/ng/animateCss.js',
|
||||
'src/ng/browser.js',
|
||||
'src/ng/cacheFactory.js',
|
||||
@@ -26,6 +28,7 @@ var angularFiles = {
|
||||
'src/ng/httpBackend.js',
|
||||
'src/ng/interpolate.js',
|
||||
'src/ng/interval.js',
|
||||
'src/ng/jsonpCallbacks.js',
|
||||
'src/ng/locale.js',
|
||||
'src/ng/location.js',
|
||||
'src/ng/log.js',
|
||||
@@ -33,6 +36,7 @@ var angularFiles = {
|
||||
'src/ng/q.js',
|
||||
'src/ng/raf.js',
|
||||
'src/ng/rootScope.js',
|
||||
'src/ng/rootElement.js',
|
||||
'src/ng/sanitizeUri.js',
|
||||
'src/ng/sce.js',
|
||||
'src/ng/sniffer.js',
|
||||
@@ -84,7 +88,7 @@ var angularFiles = {
|
||||
],
|
||||
|
||||
'angularLoader': [
|
||||
'stringify.js',
|
||||
'src/stringify.js',
|
||||
'src/minErr.js',
|
||||
'src/loader.js'
|
||||
],
|
||||
@@ -92,7 +96,6 @@ var angularFiles = {
|
||||
'angularModules': {
|
||||
'ngAnimate': [
|
||||
'src/ngAnimate/shared.js',
|
||||
'src/ngAnimate/body.js',
|
||||
'src/ngAnimate/rafScheduler.js',
|
||||
'src/ngAnimate/animateChildrenDirective.js',
|
||||
'src/ngAnimate/animateCss.js',
|
||||
@@ -100,8 +103,8 @@ var angularFiles = {
|
||||
'src/ngAnimate/animateJs.js',
|
||||
'src/ngAnimate/animateJsDriver.js',
|
||||
'src/ngAnimate/animateQueue.js',
|
||||
'src/ngAnimate/animateRunner.js',
|
||||
'src/ngAnimate/animation.js',
|
||||
'src/ngAnimate/ngAnimateSwap.js',
|
||||
'src/ngAnimate/module.js'
|
||||
],
|
||||
'ngCookies': [
|
||||
@@ -119,10 +122,15 @@ var angularFiles = {
|
||||
'ngMessages': [
|
||||
'src/ngMessages/messages.js'
|
||||
],
|
||||
'ngParseExt': [
|
||||
'src/ngParseExt/ucd.js',
|
||||
'src/ngParseExt/module.js'
|
||||
],
|
||||
'ngResource': [
|
||||
'src/ngResource/resource.js'
|
||||
],
|
||||
'ngRoute': [
|
||||
'src/shallowCopy.js',
|
||||
'src/ngRoute/route.js',
|
||||
'src/ngRoute/routeParams.js',
|
||||
'src/ngRoute/directive/ngView.js'
|
||||
@@ -170,6 +178,7 @@ var angularFiles = {
|
||||
'test/auto/*.js',
|
||||
'test/ng/**/*.js',
|
||||
'test/ngAnimate/*.js',
|
||||
'test/ngMessageFormat/*.js',
|
||||
'test/ngMessages/*.js',
|
||||
'test/ngCookies/*.js',
|
||||
'test/ngResource/*.js',
|
||||
@@ -204,9 +213,12 @@ var angularFiles = {
|
||||
"karmaModules": [
|
||||
'build/angular.js',
|
||||
'@angularSrcModules',
|
||||
'test/modules/no_bootstrap.js',
|
||||
'src/ngScenario/browserTrigger.js',
|
||||
'test/helpers/*.js',
|
||||
'test/ngAnimate/*.js',
|
||||
'test/ngMessageFormat/*.js',
|
||||
'test/ngMessages/*.js',
|
||||
'test/ngMock/*.js',
|
||||
'test/ngCookies/*.js',
|
||||
'test/ngRoute/**/*.js',
|
||||
@@ -233,6 +245,17 @@ var angularFiles = {
|
||||
]
|
||||
};
|
||||
|
||||
['2.1', '2.2'].forEach(function (jQueryVersion) {
|
||||
angularFiles['karmaJquery' + jQueryVersion] = []
|
||||
.concat(angularFiles.karmaJquery)
|
||||
.map(function (path) {
|
||||
if (path.startsWith('bower_components/jquery')) {
|
||||
return path.replace(/^bower_components\/jquery/, 'bower_components/jquery-' + jQueryVersion);
|
||||
}
|
||||
return path;
|
||||
});
|
||||
});
|
||||
|
||||
angularFiles['angularSrcModules'] = [].concat(
|
||||
angularFiles['angularModules']['ngAnimate'],
|
||||
angularFiles['angularModules']['ngMessageFormat'],
|
||||
|
||||
@@ -8,20 +8,20 @@
|
||||
Large table rendered with AngularJS
|
||||
</p>
|
||||
|
||||
<div>none: <input type="radio" ng-model="benchmarkType" value="none"></div>
|
||||
<div>baseline binding: <input type="radio" ng-model="benchmarkType" value="baselineBinding"></div>
|
||||
<div>baseline interpolation: <input type="radio" ng-model="benchmarkType" value="baselineInterpolation"></div>
|
||||
<div>ngBind: <input type="radio" ng-model="benchmarkType" value="ngBind"></div>
|
||||
<div>ngBindOnce: <input type="radio" ng-model="benchmarkType" value="ngBindOnce"></div>
|
||||
<div>interpolation: <input type="radio" ng-model="benchmarkType" value="interpolation"></div>
|
||||
<div>interpolation + bind-once: <input type="radio" ng-model="benchmarkType" value="bindOnceInterpolation"></div>
|
||||
<div>attribute interpolation: <input type="radio" ng-model="benchmarkType" value="interpolationAttr"></div>
|
||||
<div>ngBind + fnInvocation: <input type="radio" ng-model="benchmarkType" value="ngBindFn"></div>
|
||||
<div>interpolation + fnInvocation: <input type="radio" ng-model="benchmarkType" value="interpolationFn"></div>
|
||||
<div>ngBind + filter: <input type="radio" ng-model="benchmarkType" value="ngBindFilter"></div>
|
||||
<div>interpolation + filter: <input type="radio" ng-model="benchmarkType" value="interpolationFilter"></div>
|
||||
<div>ngModel (const name): <input type="radio" ng-model="benchmarkType" value="ngModelConstName"></div>
|
||||
<div>ngModel (interp name): <input type="radio" ng-model="benchmarkType" value="ngModelInterpName"></div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" value="none">none: </label></div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" value="baselineBinding">baseline binding: </label></div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" value="baselineInterpolation">baseline interpolation: </label></div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" value="ngBind">ngBind: </label></div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" value="ngBindOnce">ngBindOnce: </label></div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" value="interpolation">interpolation: </label></div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" value="bindOnceInterpolation">interpolation + bind-once: </label></div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" value="interpolationAttr">attribute interpolation: </label></div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" value="ngBindFn">ngBind + fnInvocation: </label></div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" value="interpolationFn">interpolation + fnInvocation: </label></div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" value="ngBindFilter">ngBind + filter: </label></div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" value="interpolationFilter">interpolation + filter: </label></div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" value="ngModelConstName">ngModel (const name): </label></div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" value="ngModelInterpName">ngModel (interp name): </label></div>
|
||||
|
||||
<ng-switch on="benchmarkType">
|
||||
<baseline-binding-table ng-switch-when="baselineBinding">
|
||||
|
||||
+4
-1
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "AngularJS",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"jquery": "2.1.1",
|
||||
"jquery": "3.1.0",
|
||||
"jquery-2.2": "jquery#2.2.4",
|
||||
"jquery-2.1": "jquery#2.1.4",
|
||||
"closure-compiler": "https://dl.google.com/closure-compiler/compiler-20140814.zip",
|
||||
"ng-closure-runner": "https://raw.github.com/angular/ng-closure-runner/v0.2.3/assets/ng-closure-runner.zip"
|
||||
}
|
||||
|
||||
@@ -148,8 +148,6 @@ var writeChangelog = function(stream, commits, version) {
|
||||
breaks: {}
|
||||
};
|
||||
|
||||
sections.breaks[EMPTY_COMPONENT] = [];
|
||||
|
||||
commits.forEach(function(commit) {
|
||||
var section = sections[commit.type];
|
||||
var component = commit.component || EMPTY_COMPONENT;
|
||||
|
||||
@@ -124,7 +124,7 @@ h1,h2,h3,h4,h5,h6 {
|
||||
font-size:1.2em;
|
||||
padding:0;
|
||||
margin:0;
|
||||
border-bottom:1px soild #aaa;
|
||||
border-bottom:1px solid #aaa;
|
||||
margin-bottom:5px;
|
||||
}
|
||||
|
||||
@@ -315,8 +315,13 @@ iframe.example {
|
||||
color:white;
|
||||
}
|
||||
|
||||
.search-results-group .search-results {
|
||||
padding: 0 5px 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.search-results-frame > .search-results-group:first-child > .search-results {
|
||||
border-right:1px solid #050505;
|
||||
border-right:1px solid #222;
|
||||
}
|
||||
|
||||
.search-results-group.col-group-api { width:30%; }
|
||||
@@ -325,10 +330,57 @@ iframe.example {
|
||||
.search-results-group.col-group-misc,
|
||||
.search-results-group.col-group-error { width:15%; float: right; }
|
||||
|
||||
@supports ((column-count: 2) or (-moz-column-count: 2) or (-ms-column-count: 2) or (-webkit-column-count: 2)) {
|
||||
.search-results-group.col-group-api .search-results {
|
||||
-moz-column-count: 2;
|
||||
-ms-column-count: 2;
|
||||
-webkit-column-count: 2;
|
||||
column-count: 2;
|
||||
/* Prevent bullets in the second column from being hidden in Chrome and IE */
|
||||
-webkit-column-gap: 2em;
|
||||
-ms-column-gap: 2em;
|
||||
column-gap: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.search-results-group .search-result {
|
||||
word-wrap: break-word;
|
||||
-webkit-hyphens: auto;
|
||||
-moz-hyphens: auto;
|
||||
-ms-hyphens: auto;
|
||||
hyphens: auto;
|
||||
-ms-column-break-inside: avoid;
|
||||
-webkit-column-break-inside: avoid;
|
||||
-moz-column-break-inside: avoid; /* Unsupported */
|
||||
column-break-inside: avoid;
|
||||
text-indent: -0.65em; /* Make sure line wrapped words are aligned vertically */
|
||||
}
|
||||
|
||||
@supports (-moz-column-count: 2) {
|
||||
.search-results-group .search-result {
|
||||
/* Prevents column breaks inside words in FF, but has adverse effects in IE11 and Chrome */
|
||||
overflow: hidden;
|
||||
padding-left: 1em; /* In FF the list item bullet is otherwise hidden */
|
||||
margin-left: -1em; /* offset the padding left */
|
||||
}
|
||||
}
|
||||
|
||||
.search-result:before {
|
||||
content: "\002D\00A0"; /* Dash and non-breaking space as List item type */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-results-group.col-group-api .search-result {
|
||||
width:48%;
|
||||
display:inline-block;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
@supports ((column-count: 2) or (-moz-column-count: 2) or (-ms-column-count: 2) or (-webkit-column-count: 2)) {
|
||||
.search-results-group.col-group-api .search-result {
|
||||
width:auto;
|
||||
display: list-item;
|
||||
}
|
||||
}
|
||||
|
||||
.search-close {
|
||||
@@ -589,6 +641,17 @@ ul.events > li {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.table > tbody > tr.head > td,
|
||||
.table > tbody > tr.head > th {
|
||||
border-bottom: 2px solid #ddd;
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
.diagram {
|
||||
margin-bottom: 10px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 769px) and (max-width: 991px) {
|
||||
.main-body-grid {
|
||||
margin-top: 160px;
|
||||
@@ -682,6 +745,11 @@ ul.events > li {
|
||||
padding-bottom:60px;
|
||||
text-align:left;
|
||||
}
|
||||
|
||||
.search-results-frame > .search-results-group:first-child > .search-results {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.search-results-group {
|
||||
float:none!important;
|
||||
display:block!important;
|
||||
@@ -689,14 +757,42 @@ ul.events > li {
|
||||
border:0!important;
|
||||
padding:0!important;
|
||||
}
|
||||
|
||||
@supports ((column-count: 2) or (-moz-column-count: 2) or (-ms-column-count: 2) or (-webkit-column-count: 2)) {
|
||||
.search-results-group .search-results {
|
||||
-moz-column-count: 2;
|
||||
-ms-column-count: 2;
|
||||
-webkit-column-count: 2;
|
||||
column-count: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.search-results-group .search-result {
|
||||
display:inline-block!important;
|
||||
padding:0 5px;
|
||||
width:auto!important;
|
||||
text-indent: initial;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.search-results-group .search-result:after {
|
||||
content:", ";
|
||||
}
|
||||
|
||||
.search-results-group .search-result:before {
|
||||
content: "";
|
||||
}
|
||||
|
||||
@supports ((column-count: 2) or (-moz-column-count: 2) or (-ms-column-count: 2) or (-webkit-column-count: 2)) {
|
||||
.search-results-group .search-result {
|
||||
display: list-item !important;
|
||||
}
|
||||
|
||||
.search-results-group .search-result:after {
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
padding-bottom:0px;
|
||||
}
|
||||
|
||||
-442
@@ -1,442 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var directive = {};
|
||||
|
||||
directive.runnableExample = ['$templateCache', '$document', function($templateCache, $document) {
|
||||
var exampleClassNameSelector = '.runnable-example-file';
|
||||
var doc = $document[0];
|
||||
var tpl =
|
||||
'<nav class="runnable-example-tabs" ng-if="tabs">' +
|
||||
' <a ng-class="{active:$index==activeTabIndex}"' +
|
||||
'ng-repeat="tab in tabs track by $index" ' +
|
||||
'href="" ' +
|
||||
'class="btn"' +
|
||||
'ng-click="setTab($index)">' +
|
||||
' {{ tab }}' +
|
||||
' </a>' +
|
||||
'</nav>';
|
||||
|
||||
return {
|
||||
restrict: 'C',
|
||||
scope : true,
|
||||
controller : ['$scope', function($scope) {
|
||||
$scope.setTab = function(index) {
|
||||
var tab = $scope.tabs[index];
|
||||
$scope.activeTabIndex = index;
|
||||
$scope.$broadcast('tabChange', index, tab);
|
||||
};
|
||||
}],
|
||||
compile : function(element) {
|
||||
element.html(tpl + element.html());
|
||||
return function(scope, element) {
|
||||
var node = element[0];
|
||||
var examples = node.querySelectorAll(exampleClassNameSelector);
|
||||
var tabs = [], now = Date.now();
|
||||
angular.forEach(examples, function(child, index) {
|
||||
tabs.push(child.getAttribute('name'));
|
||||
});
|
||||
|
||||
if(tabs.length > 0) {
|
||||
scope.tabs = tabs;
|
||||
scope.$on('tabChange', function(e, index, title) {
|
||||
angular.forEach(examples, function(child) {
|
||||
child.style.display = 'none';
|
||||
});
|
||||
var selected = examples[index];
|
||||
selected.style.display = 'block';
|
||||
});
|
||||
scope.setTab(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}];
|
||||
|
||||
directive.dropdownToggle =
|
||||
['$document', '$location', '$window',
|
||||
function ($document, $location, $window) {
|
||||
var openElement = null, close;
|
||||
return {
|
||||
restrict: 'C',
|
||||
link: function(scope, element, attrs) {
|
||||
scope.$watch(function dropdownTogglePathWatch(){return $location.path();}, function dropdownTogglePathWatchAction() {
|
||||
close && close();
|
||||
});
|
||||
|
||||
element.parent().on('click', function(event) {
|
||||
close && close();
|
||||
});
|
||||
|
||||
element.on('click', function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
var iWasOpen = false;
|
||||
|
||||
if (openElement) {
|
||||
iWasOpen = openElement === element;
|
||||
close();
|
||||
}
|
||||
|
||||
if (!iWasOpen){
|
||||
element.parent().addClass('open');
|
||||
openElement = element;
|
||||
|
||||
close = function (event) {
|
||||
event && event.preventDefault();
|
||||
event && event.stopPropagation();
|
||||
$document.off('click', close);
|
||||
element.parent().removeClass('open');
|
||||
close = null;
|
||||
openElement = null;
|
||||
}
|
||||
|
||||
$document.on('click', close);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}];
|
||||
|
||||
directive.syntax = function() {
|
||||
return {
|
||||
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">' +
|
||||
'<span class="' + icon + '"></span> ' + text +
|
||||
'</a>';
|
||||
};
|
||||
|
||||
var html = '';
|
||||
var types = {
|
||||
'github' : {
|
||||
text : 'View on Github',
|
||||
key : 'syntaxGithub',
|
||||
icon : 'icon-github'
|
||||
},
|
||||
'plunkr' : {
|
||||
text : 'View on Plunkr',
|
||||
key : 'syntaxPlunkr',
|
||||
icon : 'icon-arrow-down'
|
||||
},
|
||||
'jsfiddle' : {
|
||||
text : 'View on JSFiddle',
|
||||
key : 'syntaxFiddle',
|
||||
icon : 'icon-cloud'
|
||||
}
|
||||
};
|
||||
for(var type in types) {
|
||||
var data = types[type];
|
||||
var link = attrs[data.key];
|
||||
if(link) {
|
||||
html += makeLink(type, data.text, link, data.icon);
|
||||
}
|
||||
};
|
||||
|
||||
var nav = document.createElement('nav');
|
||||
nav.className = 'syntax-links';
|
||||
nav.innerHTML = html;
|
||||
|
||||
var node = element[0];
|
||||
var par = node.parentNode;
|
||||
par.insertBefore(nav, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
directive.tabbable = function() {
|
||||
return {
|
||||
restrict: 'C',
|
||||
compile: function(element) {
|
||||
var navTabs = angular.element('<ul class="nav nav-tabs"></ul>'),
|
||||
tabContent = angular.element('<div class="tab-content"></div>');
|
||||
|
||||
tabContent.append(element.contents());
|
||||
element.append(navTabs).append(tabContent);
|
||||
},
|
||||
controller: ['$scope', '$element', function($scope, $element) {
|
||||
var navTabs = $element.contents().eq(0),
|
||||
ngModel = $element.controller('ngModel') || {},
|
||||
tabs = [],
|
||||
selectedTab;
|
||||
|
||||
ngModel.$render = function() {
|
||||
var $viewValue = this.$viewValue;
|
||||
|
||||
if (selectedTab ? (selectedTab.value != $viewValue) : $viewValue) {
|
||||
if(selectedTab) {
|
||||
selectedTab.paneElement.removeClass('active');
|
||||
selectedTab.tabElement.removeClass('active');
|
||||
selectedTab = null;
|
||||
}
|
||||
if($viewValue) {
|
||||
for(var i = 0, ii = tabs.length; i < ii; i++) {
|
||||
if ($viewValue == tabs[i].value) {
|
||||
selectedTab = tabs[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selectedTab) {
|
||||
selectedTab.paneElement.addClass('active');
|
||||
selectedTab.tabElement.addClass('active');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
this.addPane = function(element, attr) {
|
||||
var li = angular.element('<li><a href></a></li>'),
|
||||
a = li.find('a'),
|
||||
tab = {
|
||||
paneElement: element,
|
||||
paneAttrs: attr,
|
||||
tabElement: li
|
||||
};
|
||||
|
||||
tabs.push(tab);
|
||||
|
||||
attr.$observe('value', update)();
|
||||
attr.$observe('title', function(){ update(); a.text(tab.title); })();
|
||||
|
||||
function update() {
|
||||
tab.title = attr.title;
|
||||
tab.value = attr.value || attr.title;
|
||||
if (!ngModel.$setViewValue && (!ngModel.$viewValue || tab == selectedTab)) {
|
||||
// we are not part of angular
|
||||
ngModel.$viewValue = tab.value;
|
||||
}
|
||||
ngModel.$render();
|
||||
}
|
||||
|
||||
navTabs.append(li);
|
||||
li.on('click', function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (ngModel.$setViewValue) {
|
||||
$scope.$apply(function() {
|
||||
ngModel.$setViewValue(tab.value);
|
||||
ngModel.$render();
|
||||
});
|
||||
} else {
|
||||
// we are not part of angular
|
||||
ngModel.$viewValue = tab.value;
|
||||
ngModel.$render();
|
||||
}
|
||||
});
|
||||
|
||||
return function() {
|
||||
tab.tabElement.remove();
|
||||
for(var i = 0, ii = tabs.length; i < ii; i++ ) {
|
||||
if (tab == tabs[i]) {
|
||||
tabs.splice(i, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
directive.table = function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function(scope, element, attrs) {
|
||||
if (!attrs['class']) {
|
||||
element.addClass('table table-bordered table-striped code-table');
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
var popoverElement = function() {
|
||||
var object = {
|
||||
init : function() {
|
||||
this.element = angular.element(
|
||||
'<div class="popover popover-incode top">' +
|
||||
'<div class="arrow"></div>' +
|
||||
'<div class="popover-inner">' +
|
||||
'<div class="popover-title"><code></code></div>' +
|
||||
'<div class="popover-content"></div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
this.node = this.element[0];
|
||||
this.element.css({
|
||||
'display':'block',
|
||||
'position':'absolute'
|
||||
});
|
||||
angular.element(document.body).append(this.element);
|
||||
|
||||
var inner = this.element.children()[1];
|
||||
this.titleElement = angular.element(inner.childNodes[0].firstChild);
|
||||
this.contentElement = angular.element(inner.childNodes[1]);
|
||||
|
||||
//stop the click on the tooltip
|
||||
this.element.on('click', function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
var self = this;
|
||||
angular.element(document.body).on('click',function(event) {
|
||||
if(self.visible()) self.hide();
|
||||
});
|
||||
},
|
||||
|
||||
show : function(x,y) {
|
||||
this.element.addClass('visible');
|
||||
this.position(x || 0, y || 0);
|
||||
},
|
||||
|
||||
hide : function() {
|
||||
this.element.removeClass('visible');
|
||||
this.position(-9999,-9999);
|
||||
},
|
||||
|
||||
visible : function() {
|
||||
return this.position().y >= 0;
|
||||
},
|
||||
|
||||
isSituatedAt : function(element) {
|
||||
return this.besideElement ? element[0] == this.besideElement[0] : false;
|
||||
},
|
||||
|
||||
title : function(value) {
|
||||
return this.titleElement.html(value);
|
||||
},
|
||||
|
||||
content : function(value) {
|
||||
if(value && value.length > 0) {
|
||||
value = marked(value);
|
||||
}
|
||||
return this.contentElement.html(value);
|
||||
},
|
||||
|
||||
positionArrow : function(position) {
|
||||
this.node.className = 'popover ' + position;
|
||||
},
|
||||
|
||||
positionAway : function() {
|
||||
this.besideElement = null;
|
||||
this.hide();
|
||||
},
|
||||
|
||||
positionBeside : function(element) {
|
||||
this.besideElement = element;
|
||||
|
||||
var elm = element[0];
|
||||
var x = elm.offsetLeft;
|
||||
var y = elm.offsetTop;
|
||||
x -= 30;
|
||||
y -= this.node.offsetHeight + 10;
|
||||
this.show(x,y);
|
||||
},
|
||||
|
||||
position : function(x,y) {
|
||||
if(x != null && y != null) {
|
||||
this.element.css('left',x + 'px');
|
||||
this.element.css('top', y + 'px');
|
||||
}
|
||||
else {
|
||||
return {
|
||||
x : this.node.offsetLeft,
|
||||
y : this.node.offsetTop
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
object.init();
|
||||
object.hide();
|
||||
|
||||
return object;
|
||||
};
|
||||
|
||||
directive.popover = ['popoverElement', function(popover) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
priority : 500,
|
||||
link: function(scope, element, attrs) {
|
||||
element.on('click',function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if(popover.isSituatedAt(element) && popover.visible()) {
|
||||
popover.title('');
|
||||
popover.content('');
|
||||
popover.positionAway();
|
||||
}
|
||||
else {
|
||||
popover.title(attrs.title);
|
||||
popover.content(attrs.content);
|
||||
popover.positionBeside(element);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
directive.tabPane = function() {
|
||||
return {
|
||||
require: '^tabbable',
|
||||
restrict: 'C',
|
||||
link: function(scope, element, attrs, tabsCtrl) {
|
||||
element.on('$remove', tabsCtrl.addPane(element, attrs));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
directive.foldout = ['$http', '$animate','$window', function($http, $animate, $window) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
priority : 500,
|
||||
link: function(scope, element, attrs) {
|
||||
var container, loading, url = attrs.url;
|
||||
if(/\/build\//.test($window.location.href)) {
|
||||
url = '/build/docs' + url;
|
||||
}
|
||||
element.on('click',function() {
|
||||
scope.$apply(function() {
|
||||
if(!container) {
|
||||
if(loading) return;
|
||||
|
||||
loading = true;
|
||||
var par = element.parent();
|
||||
container = angular.element('<div class="foldout">loading...</div>');
|
||||
$animate.enter(container, null, par);
|
||||
|
||||
$http.get(url, { cache : true }).success(function(html) {
|
||||
loading = false;
|
||||
|
||||
html = '<div class="foldout-inner">' +
|
||||
'<div calss="foldout-arrow"></div>' +
|
||||
html +
|
||||
'</div>';
|
||||
container.html(html);
|
||||
|
||||
//avoid showing the element if the user has already closed it
|
||||
if(container.css('display') == 'block') {
|
||||
container.css('display','none');
|
||||
$animate.addClass(container, 'ng-hide');
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
container.hasClass('ng-hide') ? $animate.removeClass(container, 'ng-hide') : $animate.addClass(container, 'ng-hide');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
angular.module('bootstrap', [])
|
||||
.directive(directive)
|
||||
.factory('popoverElement', popoverElement)
|
||||
.run(function() {
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
tables: true
|
||||
});
|
||||
});
|
||||
+3
-1
@@ -54,7 +54,9 @@ angular.module('ui.bootstrap.dropdown', [])
|
||||
}
|
||||
};
|
||||
|
||||
var closeDropdown = function() {
|
||||
var closeDropdown = function(evt) {
|
||||
if (evt && evt.which === 3) return;
|
||||
|
||||
openScope.$apply(function() {
|
||||
openScope.isOpen = false;
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/* global importScripts, onmessage: true, postMessage, lunr */
|
||||
|
||||
// Load up the lunr library
|
||||
importScripts('../components/lunr.js-0.4.2/lunr.min.js');
|
||||
importScripts('../components/lunr.js-0.5.12/lunr.min.js');
|
||||
|
||||
// Create the lunr index - the docs should be an array of object, each object containing
|
||||
// the path and search terms for a page
|
||||
|
||||
@@ -19,9 +19,12 @@
|
||||
"dump": false,
|
||||
|
||||
/* e2e */
|
||||
"protractor": false,
|
||||
"browser": false,
|
||||
"element": false,
|
||||
"by": false,
|
||||
"$": false,
|
||||
"$$": false,
|
||||
|
||||
/* testabilityPatch / matchers */
|
||||
"inject": false,
|
||||
@@ -39,4 +42,4 @@
|
||||
"browserTrigger": false,
|
||||
"jqLiteCacheSize": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('docs.angularjs.org', function () {
|
||||
|
||||
|
||||
it('should change the page content when clicking a link to a service', function () {
|
||||
browser.get('build/docs/index.html');
|
||||
browser.get('build/docs/index-production.html');
|
||||
|
||||
var ngBindLink = element(by.css('.definition-table td a[href="api/ng/directive/ngClick"]'));
|
||||
ngBindLink.click();
|
||||
@@ -51,33 +51,33 @@ describe('docs.angularjs.org', function () {
|
||||
|
||||
|
||||
it('should be resilient to trailing slashes', function() {
|
||||
browser.get('build/docs/index.html#!/api/ng/function/angular.noop/');
|
||||
browser.get('build/docs/index-production.html#!/api/ng/function/angular.noop/');
|
||||
var pageBody = element(by.css('h1'));
|
||||
expect(pageBody.getText()).toEqual('angular.noop');
|
||||
});
|
||||
|
||||
|
||||
it('should be resilient to trailing "index"', function() {
|
||||
browser.get('build/docs/index.html#!/api/ng/function/angular.noop/index');
|
||||
browser.get('build/docs/index-production.html#!/api/ng/function/angular.noop/index');
|
||||
var pageBody = element(by.css('h1'));
|
||||
expect(pageBody.getText()).toEqual('angular.noop');
|
||||
});
|
||||
|
||||
|
||||
it('should be resilient to trailing "index/"', function() {
|
||||
browser.get('build/docs/index.html#!/api/ng/function/angular.noop/index/');
|
||||
browser.get('build/docs/index-production.html#!/api/ng/function/angular.noop/index/');
|
||||
var pageBody = element(by.css('h1'));
|
||||
expect(pageBody.getText()).toEqual('angular.noop');
|
||||
});
|
||||
|
||||
|
||||
it('should display formatted error messages on error doc pages', function() {
|
||||
browser.get('build/docs/index.html#!error/ng/areq?p0=Missing&p1=not%20a%20function,%20got%20undefined');
|
||||
browser.get('build/docs/index-production.html#!error/ng/areq?p0=Missing&p1=not%20a%20function,%20got%20undefined');
|
||||
expect(element(by.css('.minerr-errmsg')).getText()).toEqual("Argument 'Missing' is not a function, got undefined");
|
||||
});
|
||||
|
||||
it("should display an error if the page does not exist", function() {
|
||||
browser.get('build/docs/index.html#!/api/does/not/exist');
|
||||
browser.get('build/docs/index-production.html#!/api/does/not/exist');
|
||||
expect(element(by.css('h1')).getText()).toBe('Oops!');
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ angular.module('docsApp', [
|
||||
'search',
|
||||
'tutorials',
|
||||
'versions',
|
||||
'bootstrap',
|
||||
'ui.bootstrap.dropdown'
|
||||
])
|
||||
|
||||
|
||||
@@ -34,4 +34,15 @@ angular.module('directives', [])
|
||||
return function(scope, element) {
|
||||
$anchorScroll.yOffset = element;
|
||||
};
|
||||
}]);
|
||||
}])
|
||||
|
||||
.directive('table', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function(scope, element, attrs) {
|
||||
if (!attrs['class']) {
|
||||
element.addClass('table table-bordered table-striped code-table');
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
angular.module('DocsController', [])
|
||||
|
||||
.controller('DocsController', [
|
||||
'$scope', '$rootScope', '$location', '$window', '$cookies', 'openPlunkr',
|
||||
'$scope', '$rootScope', '$location', '$window', '$cookies',
|
||||
'NG_PAGES', 'NG_NAVIGATION', 'NG_VERSION',
|
||||
function($scope, $rootScope, $location, $window, $cookies, openPlunkr,
|
||||
function($scope, $rootScope, $location, $window, $cookies,
|
||||
NG_PAGES, NG_NAVIGATION, NG_VERSION) {
|
||||
|
||||
$scope.openPlunkr = openPlunkr;
|
||||
|
||||
$scope.docsVersion = NG_VERSION.isSnapshot ? 'snapshot' : NG_VERSION.version;
|
||||
|
||||
$scope.navClass = function(navItem) {
|
||||
|
||||
+10
-3
@@ -13,10 +13,10 @@ angular.module('errors', ['ngSanitize'])
|
||||
};
|
||||
|
||||
return function (text, target) {
|
||||
var targetHtml = target ? ' target="' + target + '"' : '';
|
||||
|
||||
if (!text) return text;
|
||||
|
||||
var targetHtml = target ? ' target="' + target + '"' : '';
|
||||
|
||||
return $sanitize(text.replace(LINKY_URL_REGEXP, function (url) {
|
||||
if (STACK_TRACE_REGEXP.test(url)) {
|
||||
return url;
|
||||
@@ -34,6 +34,10 @@ angular.module('errors', ['ngSanitize'])
|
||||
|
||||
|
||||
.directive('errorDisplay', ['$location', 'errorLinkFilter', function ($location, errorLinkFilter) {
|
||||
var encodeAngleBrackets = function (text) {
|
||||
return text.replace(/</g, '<').replace(/>/g, '>');
|
||||
};
|
||||
|
||||
var interpolate = function (formatString) {
|
||||
var formatArgs = arguments;
|
||||
return formatString.replace(/\{\d+\}/g, function (match) {
|
||||
@@ -51,12 +55,15 @@ angular.module('errors', ['ngSanitize'])
|
||||
link: function (scope, element, attrs) {
|
||||
var search = $location.search(),
|
||||
formatArgs = [attrs.errorDisplay],
|
||||
formattedText,
|
||||
i;
|
||||
|
||||
for (i = 0; angular.isDefined(search['p'+i]); i++) {
|
||||
formatArgs.push(search['p'+i]);
|
||||
}
|
||||
element.html(errorLinkFilter(interpolate.apply(null, formatArgs), '_blank'));
|
||||
|
||||
formattedText = encodeAngleBrackets(interpolate.apply(null, formatArgs));
|
||||
element.html(errorLinkFilter(formattedText, '_blank'));
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
+149
-28
@@ -1,5 +1,55 @@
|
||||
angular.module('examples', [])
|
||||
|
||||
.directive('runnableExample', ['$templateCache', '$document', function($templateCache, $document) {
|
||||
var exampleClassNameSelector = '.runnable-example-file';
|
||||
var doc = $document[0];
|
||||
var tpl =
|
||||
'<nav class="runnable-example-tabs" ng-if="tabs">' +
|
||||
' <a ng-class="{active:$index==activeTabIndex}"' +
|
||||
'ng-repeat="tab in tabs track by $index" ' +
|
||||
'href="" ' +
|
||||
'class="btn"' +
|
||||
'ng-click="setTab($index)">' +
|
||||
' {{ tab }}' +
|
||||
' </a>' +
|
||||
'</nav>';
|
||||
|
||||
return {
|
||||
restrict: 'C',
|
||||
scope : true,
|
||||
controller : ['$scope', function($scope) {
|
||||
$scope.setTab = function(index) {
|
||||
var tab = $scope.tabs[index];
|
||||
$scope.activeTabIndex = index;
|
||||
$scope.$broadcast('tabChange', index, tab);
|
||||
};
|
||||
}],
|
||||
compile : function(element) {
|
||||
element.html(tpl + element.html());
|
||||
return function(scope, element) {
|
||||
var node = element[0];
|
||||
var examples = node.querySelectorAll(exampleClassNameSelector);
|
||||
var tabs = [], now = Date.now();
|
||||
angular.forEach(examples, function(child, index) {
|
||||
tabs.push(child.getAttribute('name'));
|
||||
});
|
||||
|
||||
if(tabs.length > 0) {
|
||||
scope.tabs = tabs;
|
||||
scope.$on('tabChange', function(e, index, title) {
|
||||
angular.forEach(examples, function(child) {
|
||||
child.style.display = 'none';
|
||||
});
|
||||
var selected = examples[index];
|
||||
selected.style.display = 'block';
|
||||
});
|
||||
scope.setTab(0);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('formPostData', ['$document', function($document) {
|
||||
return function(url, newWindow, fields) {
|
||||
/**
|
||||
@@ -22,29 +72,110 @@ angular.module('examples', [])
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('createCopyrightNotice', function() {
|
||||
var COPYRIGHT = 'Copyright ' + (new Date()).getFullYear() + ' Google Inc. All Rights Reserved.\n'
|
||||
+ 'Use of this source code is governed by an MIT-style license that\n'
|
||||
+ 'can be found in the LICENSE file at http://angular.io/license';
|
||||
var COPYRIGHT_JS_CSS = '\n\n/*\n' + COPYRIGHT + '\n*/';
|
||||
var COPYRIGHT_HTML = '\n\n<!-- \n' + COPYRIGHT + '\n-->';
|
||||
|
||||
.factory('openPlunkr', ['formPostData', '$http', '$q', function(formPostData, $http, $q) {
|
||||
return function(exampleFolder, clickEvent) {
|
||||
return function getCopyright(filename) {
|
||||
switch (filename.substr(filename.lastIndexOf('.'))) {
|
||||
case '.html':
|
||||
return COPYRIGHT_HTML;
|
||||
case '.js':
|
||||
case '.css':
|
||||
return COPYRIGHT_JS_CSS;
|
||||
case '.md':
|
||||
return COPYRIGHT;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
})
|
||||
|
||||
var exampleName = 'AngularJS Example';
|
||||
var newWindow = clickEvent.ctrlKey || clickEvent.metaKey;
|
||||
.directive('plnkrOpener', ['$q', 'getExampleData', 'formPostData', 'createCopyrightNotice', function($q, getExampleData, formPostData, createCopyrightNotice) {
|
||||
return {
|
||||
scope: {},
|
||||
bindToController: {
|
||||
'examplePath': '@'
|
||||
},
|
||||
controllerAs: 'plnkr',
|
||||
template: '<button ng-click="plnkr.open($event)" class="btn pull-right"> <i class="glyphicon glyphicon-edit"> </i> Edit in Plunker</button> ',
|
||||
controller: [function() {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.example = {
|
||||
path: ctrl.examplePath,
|
||||
manifest: undefined,
|
||||
files: undefined,
|
||||
name: 'AngularJS Example'
|
||||
};
|
||||
|
||||
ctrl.prepareExampleData = function() {
|
||||
if (ctrl.example.manifest) {
|
||||
return $q.when(ctrl.example);
|
||||
}
|
||||
|
||||
return getExampleData(ctrl.examplePath).then(function(data) {
|
||||
ctrl.example.files = data.files;
|
||||
ctrl.example.manifest = data.manifest;
|
||||
|
||||
// Build a pretty title for the Plunkr
|
||||
var exampleNameParts = data.manifest.name.split('-');
|
||||
exampleNameParts.unshift('AngularJS');
|
||||
angular.forEach(exampleNameParts, function(part, index) {
|
||||
exampleNameParts[index] = part.charAt(0).toUpperCase() + part.substr(1);
|
||||
});
|
||||
ctrl.example.name = exampleNameParts.join(' - ');
|
||||
|
||||
return ctrl.example;
|
||||
});
|
||||
};
|
||||
|
||||
ctrl.open = function(clickEvent) {
|
||||
|
||||
var newWindow = clickEvent.ctrlKey || clickEvent.metaKey;
|
||||
|
||||
var postData = {
|
||||
'tags[0]': "angularjs",
|
||||
'tags[1]': "example",
|
||||
'private': true
|
||||
};
|
||||
|
||||
// Make sure the example data is available.
|
||||
// If an XHR must be made, this might break some pop-up blockers when
|
||||
// new window is requested
|
||||
ctrl.prepareExampleData()
|
||||
.then(function() {
|
||||
angular.forEach(ctrl.example.files, function(file) {
|
||||
postData['files[' + file.name + ']'] = file.content + createCopyrightNotice(file.name);
|
||||
});
|
||||
|
||||
postData.description = ctrl.example.name;
|
||||
|
||||
formPostData('https://plnkr.co/edit/?p=preview', newWindow, postData);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
// Initialize the example data, so it's ready when clicking the open button.
|
||||
// Otherwise pop-up blockers will prevent a new window from opening
|
||||
ctrl.prepareExampleData(ctrl.example.path);
|
||||
|
||||
}]
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('getExampleData', ['$http', '$q', function($http, $q) {
|
||||
return function(exampleFolder){
|
||||
// Load the manifest for the example
|
||||
$http.get(exampleFolder + '/manifest.json')
|
||||
return $http.get(exampleFolder + '/manifest.json')
|
||||
.then(function(response) {
|
||||
return response.data;
|
||||
})
|
||||
.then(function(manifest) {
|
||||
var filePromises = [];
|
||||
|
||||
// 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(' - ');
|
||||
|
||||
angular.forEach(manifest.files, function(filename) {
|
||||
filePromises.push($http.get(exampleFolder + '/' + filename, { transformResponse: [] })
|
||||
.then(function(response) {
|
||||
@@ -52,7 +183,7 @@ angular.module('examples', [])
|
||||
// The manifests provide the production index file but Plunkr wants
|
||||
// a straight index.html
|
||||
if (filename === "index-production.html") {
|
||||
filename = "index.html"
|
||||
filename = "index.html";
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -61,21 +192,11 @@ angular.module('examples', [])
|
||||
};
|
||||
}));
|
||||
});
|
||||
return $q.all(filePromises);
|
||||
})
|
||||
.then(function(files) {
|
||||
var postData = {};
|
||||
|
||||
angular.forEach(files, function(file) {
|
||||
postData['files[' + file.name + ']'] = file.content;
|
||||
return $q.all({
|
||||
manifest: manifest,
|
||||
files: $q.all(filePromises)
|
||||
});
|
||||
|
||||
postData['tags[0]'] = "angularjs";
|
||||
postData['tags[1]'] = "example";
|
||||
postData.private = true;
|
||||
postData.description = exampleName;
|
||||
|
||||
formPostData('http://plnkr.co/edit/?p=preview', newWindow, postData);
|
||||
});
|
||||
};
|
||||
}]);
|
||||
}]);
|
||||
@@ -11,7 +11,15 @@ angular.module('search', [])
|
||||
var MIN_SEARCH_LENGTH = 2;
|
||||
if(q.length >= MIN_SEARCH_LENGTH) {
|
||||
docsSearch(q).then(function(hits) {
|
||||
var results = {};
|
||||
// Make sure the areas are always in the same order
|
||||
var results = {
|
||||
api: [],
|
||||
guide: [],
|
||||
tutorial: [],
|
||||
error: [],
|
||||
misc: []
|
||||
};
|
||||
|
||||
angular.forEach(hits, function(hit) {
|
||||
var area = hit.area;
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ angular.module('tutorials', [])
|
||||
'',
|
||||
'step_00', 'step_01', 'step_02', 'step_03', 'step_04',
|
||||
'step_05', 'step_06', 'step_07', 'step_08', 'step_09',
|
||||
'step_10', 'step_11', 'step_12', 'the_end'
|
||||
'step_10', 'step_11', 'step_12', 'step_13', 'step_14',
|
||||
'the_end'
|
||||
];
|
||||
return {
|
||||
scope: {},
|
||||
@@ -43,7 +44,7 @@ angular.module('tutorials', [])
|
||||
'<a href="http://angular.github.io/angular-phonecat/step-{{step}}/app">Step {{step}} Live Demo</a>.</p>\n' +
|
||||
'</div>\n' +
|
||||
'<p>The most important changes are listed below. You can see the full diff on ' +
|
||||
'<a ng-href="https://github.com/angular/angular-phonecat/compare/step-{{step ? (step - 1): \'0~1\'}}...step-{{step}}" title="See diff on Github">GitHub</a>\n' +
|
||||
'<a ng-href="https://github.com/angular/angular-phonecat/compare/step-{{step ? (step - 1): \'0~1\'}}...step-{{step}}" title="See diff on Github">GitHub</a>.\n' +
|
||||
'</p>'
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "../../../.jshintrc-base",
|
||||
"browser": true,
|
||||
"globals": {
|
||||
// AngularJS
|
||||
"angular": false,
|
||||
|
||||
// ngMocks
|
||||
"module": false,
|
||||
"inject": true,
|
||||
|
||||
// Jasmine
|
||||
"jasmine": false,
|
||||
"describe": false,
|
||||
"ddescribe": false,
|
||||
"xdescribe": false,
|
||||
"it": false,
|
||||
"iit": false,
|
||||
"xit": false,
|
||||
"beforeEach": false,
|
||||
"afterEach": false,
|
||||
"spyOn": false,
|
||||
"expect": false
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ describe("DocsController", function() {
|
||||
|
||||
angular.module('fake', [])
|
||||
.value('$cookies', {})
|
||||
.value('openPlunkr', function() {})
|
||||
.value('NG_PAGES', {})
|
||||
.value('NG_NAVIGATION', {})
|
||||
.value('NG_VERSION', {});
|
||||
@@ -26,7 +25,7 @@ describe("DocsController", function() {
|
||||
|
||||
it("should update the Google Analytics with $location.path if currentPage is missing", inject(function($window, $location) {
|
||||
$window._gaq = [];
|
||||
spyOn($location, 'path').andReturn('x/y/z');
|
||||
spyOn($location, 'path').and.returnValue('x/y/z');
|
||||
$scope.$broadcast('$includeContentLoaded');
|
||||
expect($window._gaq.pop()).toEqual(['_trackPageview', 'x/y/z']);
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
'use strict';
|
||||
|
||||
describe('errors', function() {
|
||||
// Mock `ngSanitize` module
|
||||
angular.
|
||||
module('ngSanitize', []).
|
||||
value('$sanitize', jasmine.createSpy('$sanitize').and.callFake(angular.identity));
|
||||
|
||||
beforeEach(module('errors'));
|
||||
|
||||
|
||||
describe('errorDisplay', function() {
|
||||
var $sanitize;
|
||||
var errorLinkFilter;
|
||||
|
||||
beforeEach(inject(function(_$sanitize_, _errorLinkFilter_) {
|
||||
$sanitize = _$sanitize_;
|
||||
errorLinkFilter = _errorLinkFilter_;
|
||||
}));
|
||||
|
||||
|
||||
it('should return empty input unchanged', function() {
|
||||
var inputs = [undefined, null, false, 0, ''];
|
||||
var remaining = inputs.length;
|
||||
|
||||
inputs.forEach(function(falsyValue) {
|
||||
expect(errorLinkFilter(falsyValue)).toBe(falsyValue);
|
||||
remaining--;
|
||||
});
|
||||
|
||||
expect(remaining).toBe(0);
|
||||
});
|
||||
|
||||
|
||||
it('should recognize URLs and convert them to `<a>`', function() {
|
||||
var urls = [
|
||||
['ftp://foo/bar?baz#qux'],
|
||||
['http://foo/bar?baz#qux'],
|
||||
['https://foo/bar?baz#qux'],
|
||||
['mailto:foo_bar@baz.qux', null, 'foo_bar@baz.qux'],
|
||||
['foo_bar@baz.qux', 'mailto:foo_bar@baz.qux', 'foo_bar@baz.qux']
|
||||
];
|
||||
var remaining = urls.length;
|
||||
|
||||
urls.forEach(function(values) {
|
||||
var actualUrl = values[0];
|
||||
var expectedUrl = values[1] || actualUrl;
|
||||
var expectedText = values[2] || expectedUrl;
|
||||
var anchor = '<a href="' + expectedUrl + '">' + expectedText + '</a>';
|
||||
|
||||
var input = 'start ' + actualUrl + ' end';
|
||||
var output = 'start ' + anchor + ' end';
|
||||
|
||||
expect(errorLinkFilter(input)).toBe(output);
|
||||
remaining--;
|
||||
});
|
||||
|
||||
expect(remaining).toBe(0);
|
||||
});
|
||||
|
||||
|
||||
it('should not recognize stack-traces as URLs', function() {
|
||||
var urls = [
|
||||
'ftp://foo/bar?baz#qux:4:2',
|
||||
'http://foo/bar?baz#qux:4:2',
|
||||
'https://foo/bar?baz#qux:4:2',
|
||||
'mailto:foo_bar@baz.qux:4:2',
|
||||
'foo_bar@baz.qux:4:2'
|
||||
];
|
||||
var remaining = urls.length;
|
||||
|
||||
urls.forEach(function(url) {
|
||||
var input = 'start ' + url + ' end';
|
||||
|
||||
expect(errorLinkFilter(input)).toBe(input);
|
||||
remaining--;
|
||||
});
|
||||
|
||||
expect(remaining).toBe(0);
|
||||
});
|
||||
|
||||
|
||||
it('should should set `[target]` if specified', function() {
|
||||
var url = 'https://foo/bar?baz#qux';
|
||||
var target = '_blank';
|
||||
var outputWithoutTarget = '<a href="' + url + '">' + url + '</a>';
|
||||
var outputWithTarget = '<a target="' + target + '" href="' + url + '">' + url + '</a>';
|
||||
|
||||
expect(errorLinkFilter(url)).toBe(outputWithoutTarget);
|
||||
expect(errorLinkFilter(url, target)).toBe(outputWithTarget);
|
||||
});
|
||||
|
||||
|
||||
it('should truncate the contents of the generated `<a>` to 60 characters', function() {
|
||||
var looongUrl = 'https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo';
|
||||
var truncatedUrl = 'https://foooooooooooooooooooooooooooooooooooooooooooooooo...';
|
||||
var output = '<a href="' + looongUrl + '">' + truncatedUrl + '</a>';
|
||||
|
||||
expect(looongUrl.length).toBeGreaterThan(60);
|
||||
expect(truncatedUrl.length).toBe(60);
|
||||
expect(errorLinkFilter(looongUrl)).toBe(output);
|
||||
});
|
||||
|
||||
|
||||
it('should pass the final string through `$sanitize`', function() {
|
||||
$sanitize.calls.reset();
|
||||
|
||||
var input = 'start https://foo/bar?baz#qux end';
|
||||
var output = errorLinkFilter(input);
|
||||
|
||||
expect($sanitize).toHaveBeenCalledTimes(1);
|
||||
expect($sanitize).toHaveBeenCalledWith(output);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('errorDisplay', function() {
|
||||
var $compile;
|
||||
var $location;
|
||||
var $rootScope;
|
||||
var errorLinkFilter;
|
||||
|
||||
beforeEach(module(function($provide) {
|
||||
$provide.decorator('errorLinkFilter', function() {
|
||||
errorLinkFilter = jasmine.createSpy('errorLinkFilter');
|
||||
errorLinkFilter.and.callFake(angular.identity);
|
||||
|
||||
return errorLinkFilter;
|
||||
});
|
||||
}));
|
||||
beforeEach(inject(function(_$compile_, _$location_, _$rootScope_) {
|
||||
$compile = _$compile_;
|
||||
$location = _$location_;
|
||||
$rootScope = _$rootScope_;
|
||||
}));
|
||||
|
||||
|
||||
it('should set the element\s HTML', function() {
|
||||
var elem = $compile('<span error-display="bar">foo</span>')($rootScope);
|
||||
expect(elem.html()).toBe('bar');
|
||||
});
|
||||
|
||||
|
||||
it('should interpolate the contents against `$location.search()`', function() {
|
||||
spyOn($location, 'search').and.returnValue({p0: 'foo', p1: 'bar'});
|
||||
|
||||
var elem = $compile('<span error-display="foo = {0}, bar = {1}"></span>')($rootScope);
|
||||
expect(elem.html()).toBe('foo = foo, bar = bar');
|
||||
});
|
||||
|
||||
|
||||
it('should pass the interpolated text through `errorLinkFilter`', function() {
|
||||
$location.search = jasmine.createSpy('search').and.returnValue({p0: 'foo'});
|
||||
|
||||
var elem = $compile('<span error-display="foo = {0}"></span>')($rootScope);
|
||||
expect(errorLinkFilter).toHaveBeenCalledTimes(1);
|
||||
expect(errorLinkFilter).toHaveBeenCalledWith('foo = foo', '_blank');
|
||||
});
|
||||
|
||||
|
||||
it('should encode `<` and `>`', function() {
|
||||
var elem = $compile('<span error-display="<xyz>"></span>')($rootScope);
|
||||
expect(elem.text()).toBe('<xyz>');
|
||||
});
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "AngularJS-docs-app",
|
||||
"dependencies": {
|
||||
"jquery": "2.1.1",
|
||||
"lunr.js": "0.4.3",
|
||||
"jquery": "2.2.3",
|
||||
"lunr.js": "0.5.12",
|
||||
"open-sans-fontface": "1.0.4",
|
||||
"google-code-prettify": "1.0.1",
|
||||
"bootstrap": "3.1.1"
|
||||
|
||||
@@ -54,6 +54,7 @@ module.exports = new Package('angularjs', [
|
||||
.config(function(parseTagsProcessor) {
|
||||
parseTagsProcessor.tagDefinitions.push(require('./tag-defs/tutorial-step'));
|
||||
parseTagsProcessor.tagDefinitions.push(require('./tag-defs/sortOrder'));
|
||||
parseTagsProcessor.tagDefinitions.push(require('./tag-defs/installation'));
|
||||
})
|
||||
|
||||
|
||||
@@ -170,4 +171,8 @@ module.exports = new Package('angularjs', [
|
||||
jqueryDeployment,
|
||||
productionDeployment
|
||||
];
|
||||
})
|
||||
|
||||
.config(function(generateKeywordsProcessor) {
|
||||
generateKeywordsProcessor.docTypesToIgnore = ['componentGroup'];
|
||||
});
|
||||
|
||||
@@ -16,9 +16,11 @@ module.exports = function generateKeywordsProcessor(log, readFilesProcessor) {
|
||||
ignoreWordsFile: undefined,
|
||||
areasToSearch: ['api', 'guide', 'misc', 'error', 'tutorial'],
|
||||
propertiesToIgnore: [],
|
||||
docTypesToIgnore: [],
|
||||
$validate: {
|
||||
ignoreWordsFile: { },
|
||||
areasToSearch: { presence: true },
|
||||
docTypesToIgnore: { },
|
||||
propertiesToIgnore: { }
|
||||
},
|
||||
$runAfter: ['memberDocsProcessor'],
|
||||
@@ -28,6 +30,7 @@ module.exports = function generateKeywordsProcessor(log, readFilesProcessor) {
|
||||
// Keywords to ignore
|
||||
var wordsToIgnore = [];
|
||||
var propertiesToIgnore;
|
||||
var docTypesToIgnore;
|
||||
var areasToSearch;
|
||||
|
||||
// Keywords start with "ng:" or one of $, _ or a letter
|
||||
@@ -47,6 +50,8 @@ module.exports = function generateKeywordsProcessor(log, readFilesProcessor) {
|
||||
areasToSearch = _.indexBy(this.areasToSearch);
|
||||
propertiesToIgnore = _.indexBy(this.propertiesToIgnore);
|
||||
log.debug('Properties to ignore', propertiesToIgnore);
|
||||
docTypesToIgnore = _.indexBy(this.docTypesToIgnore);
|
||||
log.debug('Doc types to ignore', docTypesToIgnore);
|
||||
|
||||
var ignoreWordsMap = _.indexBy(wordsToIgnore);
|
||||
|
||||
@@ -78,34 +83,36 @@ module.exports = function generateKeywordsProcessor(log, readFilesProcessor) {
|
||||
|
||||
// We are only interested in docs that live in the right area
|
||||
docs = _.filter(docs, function(doc) { return areasToSearch[doc.area]; });
|
||||
docs = _.filter(docs, function(doc) { return !docTypesToIgnore[doc.docType]; });
|
||||
|
||||
_.forEach(docs, function(doc) {
|
||||
|
||||
var words = [];
|
||||
var keywordMap = _.clone(ignoreWordsMap);
|
||||
var members = [];
|
||||
var membersMap = {};
|
||||
|
||||
// Search each top level property of the document for search terms
|
||||
_.forEach(doc, function(value, key) {
|
||||
var words = [];
|
||||
var keywordMap = _.clone(ignoreWordsMap);
|
||||
var members = [];
|
||||
var membersMap = {};
|
||||
|
||||
if ( _.isString(value) && !propertiesToIgnore[key] ) {
|
||||
extractWords(value, words, keywordMap);
|
||||
}
|
||||
// Search each top level property of the document for search terms
|
||||
_.forEach(doc, function(value, key) {
|
||||
|
||||
if ( key === 'methods' || key === 'properties' || key === 'events' ) {
|
||||
_.forEach(value, function(member) {
|
||||
extractWords(member.name, members, membersMap);
|
||||
});
|
||||
}
|
||||
});
|
||||
if ( _.isString(value) && !propertiesToIgnore[key] ) {
|
||||
extractWords(value, words, keywordMap);
|
||||
}
|
||||
|
||||
if ( key === 'methods' || key === 'properties' || key === 'events' ) {
|
||||
_.forEach(value, function(member) {
|
||||
extractWords(member.name, members, membersMap);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
doc.searchTerms = {
|
||||
titleWords: extractTitleWords(doc.name),
|
||||
keywords: _.sortBy(words).join(' '),
|
||||
members: _.sortBy(members).join(' ')
|
||||
};
|
||||
doc.searchTerms = {
|
||||
titleWords: extractTitleWords(doc.name),
|
||||
keywords: _.sortBy(words).join(' '),
|
||||
members: _.sortBy(members).join(' ')
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ module.exports = function debugDeployment(getVersion) {
|
||||
'../angular-touch.js',
|
||||
'../angular-animate.js',
|
||||
'components/marked-' + getVersion('marked', 'node_modules', 'package.json') + '/lib/marked.js',
|
||||
'js/angular-bootstrap/bootstrap.js',
|
||||
'js/angular-bootstrap/dropdown-toggle.js',
|
||||
'components/lunr.js-' + getVersion('lunr.js') + '/lunr.js',
|
||||
'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/prettify.js',
|
||||
|
||||
@@ -18,7 +18,6 @@ module.exports = function defaultDeployment(getVersion) {
|
||||
'../angular-touch.min.js',
|
||||
'../angular-animate.min.js',
|
||||
'components/marked-' + getVersion('marked', 'node_modules', 'package.json') + '/lib/marked.js',
|
||||
'js/angular-bootstrap/bootstrap.min.js',
|
||||
'js/angular-bootstrap/dropdown-toggle.min.js',
|
||||
'components/lunr.js-' + getVersion('lunr.js') + '/lunr.min.js',
|
||||
'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/prettify.js',
|
||||
|
||||
-1
@@ -22,7 +22,6 @@ module.exports = function jqueryDeployment(getVersion) {
|
||||
'../angular-touch.min.js',
|
||||
'../angular-animate.min.js',
|
||||
'components/marked-' + getVersion('marked', 'node_modules', 'package.json') + '/lib/marked.js',
|
||||
'js/angular-bootstrap/bootstrap.min.js',
|
||||
'js/angular-bootstrap/dropdown-toggle.min.js',
|
||||
'components/lunr.js-' + getVersion('lunr.js') + '/lunr.min.js',
|
||||
'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/prettify.js',
|
||||
|
||||
@@ -21,7 +21,6 @@ module.exports = function productionDeployment(getVersion) {
|
||||
cdnUrl + '/angular-touch.min.js',
|
||||
cdnUrl + '/angular-animate.min.js',
|
||||
'components/marked-' + getVersion('marked', 'node_modules', 'package.json') + '/lib/marked.js',
|
||||
'js/angular-bootstrap/bootstrap.min.js',
|
||||
'js/angular-bootstrap/dropdown-toggle.min.js',
|
||||
'components/lunr.js-' + getVersion('lunr.js') + '/lunr.min.js',
|
||||
'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/prettify.js',
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
name: 'installation'
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
{% extends "base.template.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<a href='https://github.com/{$ git.info.owner $}/{$ git.info.repo $}/tree/{$ git.version.isSnapshot and 'master' or git.version.raw $}/{$ doc.fileInfo.projectRelativePath $}#L{$ doc.startingLine $}' class='view-source pull-right btn btn-primary'>
|
||||
<i class="glyphicon glyphicon-zoom-in"> </i>View Source
|
||||
</a>
|
||||
|
||||
{% block header %}
|
||||
<header class="api-profile-header">
|
||||
<h1 class="api-profile-header-heading">{$ doc.name $}</h1>
|
||||
<ol class="api-profile-header-structure naked-list step-list">
|
||||
{% block related_components %}{% endblock %}
|
||||
<li>
|
||||
- {$ doc.docType $} in module <a href="{$ doc.moduleDoc.path $}">{$ doc.moduleDoc.name $}</a>
|
||||
</li>
|
||||
</ol>
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
||||
{% block description %}
|
||||
<div class="api-profile-description">
|
||||
{$ doc.description | marked $}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% if doc.knownIssues %}
|
||||
<h2 id="known-issues">Known Issues</h2>
|
||||
{% for issue in doc.knownIssues -%}
|
||||
<div class="known-issue">
|
||||
{$ issue | marked $} {% if not loop.last %}<hr>{% endif %}
|
||||
</div>
|
||||
{% endfor -%}
|
||||
{% endif %}
|
||||
|
||||
{% if doc.deprecated %}
|
||||
<fieldset class="deprecated">
|
||||
<legend>Deprecated API</legend>
|
||||
{$ doc.deprecated| marked $}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
{% block dependencies %}
|
||||
{%- if doc.requires %}
|
||||
<h2 id="dependencies">Dependencies</h2>
|
||||
<ul>
|
||||
{% for require in doc.requires %}<li>{$ require | link $}</li>{% endfor %}
|
||||
</ul>
|
||||
{% endif -%}
|
||||
{% endblock %}
|
||||
|
||||
{% block additional %}
|
||||
{% endblock %}
|
||||
|
||||
{% block examples %}
|
||||
{%- if doc.examples %}
|
||||
<h2 id="example">Example</h2>
|
||||
{%- for example in doc.examples -%}
|
||||
{$ example | marked $}
|
||||
{%- endfor -%}
|
||||
{% endif -%}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,100 @@
|
||||
{% extends "base.template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% if doc.title %}{$ doc.title | marked $}{% else %}{$ doc.name | code $}{% endif %}
|
||||
</h1>
|
||||
|
||||
<h2>Installation</h2>
|
||||
{% if doc.installation or doc.installation == '' %}
|
||||
{$ doc.installation | marked $}
|
||||
{% else %}
|
||||
|
||||
<p>First, get the file:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://developers.google.com/speed/libraries/devguide#angularjs">Google CDN</a> e.g.
|
||||
{% code %}"//ajax.googleapis.com/ajax/libs/angularjs/X.Y.Z/{$ doc.packageFile $}"{% endcode %}
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.npmjs.com/">NPM</a> e.g.
|
||||
{% code %}npm install {$ doc.packageName $}@X.Y.Z{% endcode %}
|
||||
</li>
|
||||
<li>
|
||||
<a href="http://bower.io">Bower</a> e.g.
|
||||
{% code %}bower install {$ doc.packageName $}#X.Y.Z{% endcode %}
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://code.angularjs.org/">code.angularjs.org</a>
|
||||
(discouraged for production use) e.g.
|
||||
{% code %}"//code.angularjs.org/X.Y.Z/{$ doc.packageFile $}"{% endcode %}
|
||||
</li>
|
||||
</ul>
|
||||
<p>where X.Y.Z is the AngularJS version you are running.</p>
|
||||
|
||||
<p>Then, include {$ doc.packageFile | code $} in your HTML:</p>
|
||||
|
||||
{% code %}
|
||||
<script src="path/to/angular.js"></script>
|
||||
<script src="path/to/{$ doc.packageFile $}"></script>
|
||||
{% endcode %}
|
||||
|
||||
<p>Finally, load the module in your application by adding it as a dependent module:</p>
|
||||
{% code %}
|
||||
angular.module('app', ['{$ doc.name $}']);
|
||||
{% endcode %}
|
||||
|
||||
<p>With that you're ready to get started!</p>
|
||||
{% endif %}
|
||||
|
||||
{$ doc.description | marked $}
|
||||
|
||||
{% if doc.knownIssueDocs %}
|
||||
<div class="known-issues">
|
||||
<h2 id="known-issues">Known Issues</h2>
|
||||
<table class="definition-table">
|
||||
<tr><th>Name</th><th>Description</th></tr>
|
||||
{% for issueDoc in doc.knownIssueDocs -%}
|
||||
<tr>
|
||||
<td>{$ issueDoc.id | link(issueDoc.name, issueDoc) $}</td>
|
||||
<td>
|
||||
{% for issue in issueDoc.knownIssues -%}
|
||||
{$ issue | marked $} {% if not loop.last %}<hr>{% endif %}
|
||||
{% endfor -%}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor -%}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if doc.componentGroups.length %}
|
||||
<div class="component-breakdown">
|
||||
<h2>Module Components</h2>
|
||||
{% for componentGroup in doc.componentGroups %}
|
||||
<div>
|
||||
<h3 class="component-heading" id="{$ componentGroup.groupType | dashCase $}">{$ componentGroup.groupType | title $}</h3>
|
||||
<table class="definition-table">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
{% for component in componentGroup.components %}
|
||||
<tr>
|
||||
<td>{$ component.id | link(component.name, component) $}</td>
|
||||
<td>{$ component.description | firstParagraph | marked $}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if doc.usage %}
|
||||
<h2>Usage</h2>
|
||||
{$ doc.usage | marked $}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -147,13 +147,13 @@
|
||||
<div class="search-results-container" ng-show="hasResults">
|
||||
<div class="container">
|
||||
<div class="search-results-frame">
|
||||
<div ng-repeat="(key, value) in results" class="search-results-group" ng-class="colClassName + ' col-group-' + key">
|
||||
<div ng-repeat="(key, value) in results track by key" class="search-results-group" ng-class="colClassName + ' col-group-' + key" ng-show="value.length > 0">
|
||||
<h4 class="search-results-group-heading">{{ key }}</h4>
|
||||
<div class="search-results">
|
||||
<div ng-repeat="item in value" class="search-result">
|
||||
- <a ng-click="hideResults()" ng-href="{{ item.path }}">{{ item.name }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="search-results">
|
||||
<!-- Do not insert a line break between li and a. Chrome will insert an actual line-break, which breaks the list item view.
|
||||
TODO: use a html minifier instead -->
|
||||
<li ng-repeat="item in value" class="search-result"><a ng-click="hideResults()" ng-href="{{ item.path }}">{{ item.name }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<a href="" ng-click="hideResults()" class="search-close">
|
||||
@@ -220,7 +220,7 @@
|
||||
<p class="pull-right"><a back-to-top>Back to top</a></p>
|
||||
|
||||
<p>
|
||||
Super-powered by Google ©2010-2015
|
||||
Super-powered by Google ©2010-2016
|
||||
( <a id="version"
|
||||
ng-href="https://github.com/angular/angular.js/blob/master/CHANGELOG.md#{{versionNumber}}"
|
||||
ng-bind-template="v{{version}}" title="Changelog of this version of Angular JS">
|
||||
@@ -228,10 +228,10 @@
|
||||
)
|
||||
</p>
|
||||
<p>
|
||||
Code licensed under the
|
||||
<a href="https://github.com/angular/angular.js/blob/master/LICENSE" target="_blank">The
|
||||
MIT License</a>. Documentation licensed under <a
|
||||
href="http://creativecommons.org/licenses/by/3.0/">CC BY 3.0</a>.
|
||||
Code licensed under
|
||||
<a href="https://github.com/angular/angular.js/blob/master/LICENSE" target="_blank">The MIT License</a>.
|
||||
Documentation licensed under
|
||||
<a href="http://creativecommons.org/licenses/by/3.0/" target="_blank">CC BY 3.0</a>.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
{# Be aware that we need these extra new lines here or marked will not realise that the <div>
|
||||
{# Be aware that we need these extra new lines here or marked will not realize that the <div>
|
||||
is HTML and wrap each line in a <p> - thus breaking the HTML #}
|
||||
|
||||
<div>
|
||||
<a ng-click="openPlunkr('{$ doc.path $}', $event)" class="btn pull-right">
|
||||
<i class="glyphicon glyphicon-edit"> </i>
|
||||
Edit in Plunker</a>
|
||||
<plnkr-opener example-path="{$ doc.path $}"></plnkr-opener>
|
||||
|
||||
<div class="runnable-example"
|
||||
path="{$ doc.example.deployments.default.path $}"
|
||||
@@ -24,5 +22,5 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Be aware that we need these extra new lines here or marked will not realise that the <div>
|
||||
{# Be aware that we need these extra new lines here or marked will not realize that the <div>
|
||||
above is HTML and wrap each line in a <p> - thus breaking the HTML #}
|
||||
|
||||
@@ -8,6 +8,8 @@ Welcome to the AngularJS API docs page. These pages contain the AngularJS refere
|
||||
The documentation is organized into **{@link guide/module modules}** which contain various components of an AngularJS application.
|
||||
These components are {@link guide/directive directives}, {@link guide/services services}, {@link guide/filter filters}, {@link guide/providers providers}, {@link guide/templates templates}, global APIs, and testing mocks.
|
||||
|
||||
There is also a {@link guide/index guide} with articles on various topics, and a list of external resources.
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Angular Prefixes `$` and `$$`**:
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@ngdoc error
|
||||
@name $compile:baddir
|
||||
@fullName Invalid Directive Name
|
||||
@fullName Invalid Directive/Component Name
|
||||
@description
|
||||
|
||||
This error occurs when the name of a directive is not valid.
|
||||
This error occurs when the name of a directive or component is not valid.
|
||||
|
||||
Directives must start with a lowercase character and must not contain leading or trailing whitespaces.
|
||||
Directives and Components must start with a lowercase character and must not contain leading or trailing whitespaces.
|
||||
|
||||
@@ -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 sure that you prefix the controller name in the `require` definition with `^`.
|
||||
If the required controller is expected to be on an 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.
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
@ngdoc error
|
||||
@name $compile:infchng
|
||||
@fullName Unstable `$onChanges` hooks
|
||||
@description
|
||||
|
||||
This error occurs when the application's model becomes unstable because some `$onChanges` hooks are causing updates which then trigger
|
||||
further calls to `$onChanges` that can never complete.
|
||||
Angular detects this situation and prevents an infinite loop from causing the browser to become unresponsive.
|
||||
|
||||
For example, the situation can occur by setting up a `$onChanges()` hook which triggers an event on the component, which subsequently
|
||||
triggers the component's bound inputs to be updated:
|
||||
|
||||
```html
|
||||
<c1 prop="a" on-change="a = -a"></c1>
|
||||
```
|
||||
|
||||
```js
|
||||
function Controller1() {}
|
||||
Controller1.$onChanges = function() {
|
||||
this.onChange();
|
||||
};
|
||||
|
||||
mod.component('c1', {
|
||||
controller: Controller1,
|
||||
bindings: {'prop': '<', onChange: '&'}
|
||||
}
|
||||
```
|
||||
|
||||
The maximum number of allowed iterations of the `$onChanges` hooks is controlled via TTL setting which can be configured via
|
||||
{@link ng.$compileProvider#onChangesTtl `$compileProvider.onChangesTtl`}.
|
||||
@@ -3,7 +3,7 @@
|
||||
@fullName Invalid Isolate Scope Definition
|
||||
@description
|
||||
|
||||
When declaring isolate scope the scope definition object must be in specific format which starts with mode character (`@&=`) with an optional local name.
|
||||
When declaring isolate scope the scope definition object must be in specific format which starts with mode character (`@&=<`), after which comes an optional `?`, and it ends with an optional local name.
|
||||
|
||||
```
|
||||
myModule.directive('directiveName', function factory() {
|
||||
@@ -12,9 +12,11 @@ myModule.directive('directiveName', function factory() {
|
||||
scope: {
|
||||
'attrName': '@', // OK
|
||||
'attrName2': '=localName', // OK
|
||||
'attrName3': 'name', // ERROR: missing mode @&=
|
||||
'attrName4': ' = name', // ERROR: extra spaces
|
||||
'attrName5': 'name=', // ERROR: must be prefixed with @&=
|
||||
'attrName3': '<?localName', // OK
|
||||
'attrName4': ' = name', // OK
|
||||
'attrName5': 'name', // ERROR: missing mode @&=
|
||||
'attrName6': 'name=', // ERROR: must be prefixed with @&=
|
||||
'attrName7': '=name?', // ERROR: ? must come directly after the mode
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
@ngdoc error
|
||||
@name $compile:reqslot
|
||||
@fullName Required transclusion slot
|
||||
@description
|
||||
|
||||
This error occurs when a directive or component try to transclude a slot that is not provided.
|
||||
|
||||
Transcluded elements must contain something. This error could happen when you try to transclude a self closing tag element.
|
||||
Also you can make a transclusion slot optional with a `?` prefix.
|
||||
|
||||
```js
|
||||
// In this example the <my-component> must have an <important-component> inside to transclude it.
|
||||
// If not, a reqslot error will be generated.
|
||||
|
||||
var componentConfig = {
|
||||
template: 'path/to/template.html',
|
||||
tranclude: {
|
||||
importantSlot: 'importantComponent', // mandatory transclusion
|
||||
optionalSlot: '?optionalComponent', // optional transclusion
|
||||
}
|
||||
};
|
||||
|
||||
angular
|
||||
.module('doc')
|
||||
.component('myComponent', componentConfig)
|
||||
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- Will not work because <important-component> is missing -->
|
||||
<my-component>
|
||||
</my-component>
|
||||
|
||||
<my-component>
|
||||
<optional-component></optional-component>
|
||||
</my-component>
|
||||
|
||||
<!-- Will work -->
|
||||
<my-component>
|
||||
<important-component></important-component>
|
||||
</my-component>
|
||||
|
||||
<my-component>
|
||||
<optional-component></optional-component>
|
||||
<important-component></important-component>
|
||||
</my-component>
|
||||
```
|
||||
@@ -31,7 +31,7 @@ single root element, like the `div` element in this template:
|
||||
<div><b>Hello</b> World!</div>
|
||||
```
|
||||
|
||||
An an invalid template to be used with this directive is one that defines multiple root nodes or
|
||||
An invalid template to be used with this directive is one that defines multiple root nodes or
|
||||
elements. For example:
|
||||
|
||||
```
|
||||
@@ -43,7 +43,7 @@ well. Consider the following template:
|
||||
|
||||
```
|
||||
<div class='container'>
|
||||
<div class='wrapper>
|
||||
<div class='wrapper'>
|
||||
...
|
||||
</div> <!-- wrapper -->
|
||||
</div> <!-- container -->
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
This error occurs when a module fails to load due to some exception. The error
|
||||
message above should provide additional context.
|
||||
|
||||
A common reason why the module fails to load is that you've forgotten to
|
||||
include the file with the defined module or that the file couldn't be loaded.
|
||||
|
||||
### Using `ngRoute`
|
||||
|
||||
In AngularJS `1.2.0` and later, `ngRoute` has been moved to its own module.
|
||||
@@ -24,4 +27,4 @@ angular.module('ng').filter('tel', function (){});
|
||||
|
||||
Instead create your own module and add it as a dependency to your application's top-level module.
|
||||
See [#9692](https://github.com/angular/angular.js/issues/9692) and
|
||||
[#7709](https://github.com/angular/angular.js/issues/7709) for more information
|
||||
[#7709](https://github.com/angular/angular.js/issues/7709) for more information
|
||||
|
||||
@@ -81,3 +81,6 @@ angular.module('myModule', [])
|
||||
// a scope object cannot be injected into a service.
|
||||
}]);
|
||||
```
|
||||
|
||||
If you encounter this error only with minified code, consider using `ngStrictDi` (see
|
||||
{@link ng.directive:ngApp ngApp}) to provoke the error with the non-minified source.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@ngdoc error
|
||||
@name $location:nobase
|
||||
@fullName $location in HTML5 mode requires a <base> tag to be present!
|
||||
@fullName $location in HTML5 mode requires a <base> tag to be present!
|
||||
@description
|
||||
|
||||
If you configure {@link ng.$location `$location`} to use
|
||||
@@ -15,7 +15,7 @@ $locationProvider.html5Mode({
|
||||
});
|
||||
```
|
||||
|
||||
Note that removing the requirement for a <base> tag will have adverse side effects when resolving
|
||||
Note that removing the requirement for a `<base>` tag will have adverse side effects when resolving
|
||||
relative paths with `$location` in IE9.
|
||||
|
||||
The base URL is then used to resolve all relative URLs throughout the application regardless of the
|
||||
|
||||
@@ -14,3 +14,32 @@ 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 Window access.
|
||||
|
||||
### Common CoffeeScript Issue
|
||||
|
||||
Be aware that if you are using CoffeeScript, it automatically returns the value of the last statement in a
|
||||
function. So for instance
|
||||
|
||||
```coffeescript
|
||||
scope.foo = ->
|
||||
window.open 'https://example.com'
|
||||
```
|
||||
|
||||
compiles to something like
|
||||
|
||||
```js
|
||||
scope.foo = function() {
|
||||
return window.open('https://example.com');
|
||||
};
|
||||
```
|
||||
|
||||
You can see that this function will return the result of calling `window.open`, which is a `Window`
|
||||
object.
|
||||
|
||||
You can avoid this by explicitly returning something else from the function:
|
||||
|
||||
```coffeescript
|
||||
scope.foo = ->
|
||||
window.open 'https://example.com'
|
||||
return true;
|
||||
```
|
||||
|
||||
@@ -100,7 +100,7 @@ To resolve this type of issue, either fix the api to be always synchronous or as
|
||||
your callback handler to always run asynchronously by using the `$timeout` service.
|
||||
|
||||
```
|
||||
function MyController($scope, thirdPartyComponent) {
|
||||
function MyController($scope, $timeout, thirdPartyComponent) {
|
||||
thirdPartyComponent.getData(function(someData) {
|
||||
$timeout(function() {
|
||||
$scope.someData = someData;
|
||||
@@ -161,7 +161,7 @@ In this second scenario, we are already inside a `$digest` when the ngFocus dire
|
||||
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
|
||||
by using `$timeout(fn, 0, false)`, where the `false` value tells Angular not to wrap this `fn` in an
|
||||
`$apply` block:
|
||||
|
||||
```
|
||||
@@ -200,7 +200,7 @@ the top of the call stack.
|
||||
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
|
||||
called from within an `$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
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
@ngdoc error
|
||||
@name $sanitize:badparse
|
||||
@fullName Parsing Error while Sanitizing
|
||||
@description
|
||||
|
||||
This error occurs when the HTML string passed to '$sanitize' can't be parsed by the sanitizer.
|
||||
The error contains part of the html string that can't be parsed.
|
||||
|
||||
The parser is more strict than a typical browser parser, so it's possible that some obscure input would produce this error despite the string being recognized as valid HTML by a browser.
|
||||
|
||||
If a valid html code results in this error, please file a bug.
|
||||
@@ -0,0 +1,10 @@
|
||||
@ngdoc error
|
||||
@name $sanitize:noinert
|
||||
@fullName Can't create an inert html document
|
||||
@description
|
||||
|
||||
This error occurs when `$sanitize` sanitizer determines that `document.implementation.createHTMLDocument ` api is not supported by the current browser.
|
||||
|
||||
This api is necessary for safe parsing of HTML strings into DOM trees and without it the sanitizer can't sanitize the input.
|
||||
|
||||
The api is present in all supported browsers including IE 9.0, so the presence of this error usually indicates that Angular's `$sanitize` is being used on an unsupported platform.
|
||||
@@ -0,0 +1,13 @@
|
||||
@ngdoc error
|
||||
@name $sanitize:uinput
|
||||
@fullName Failed to sanitize html because the input is unstable
|
||||
@description
|
||||
|
||||
This error occurs when `$sanitize` sanitizer tries to check the input for possible mXSS payload and the verification
|
||||
errors due to the input mutating indefinitely. This could be a sign that the payload contains code exploiting an mXSS
|
||||
vulnerability in the browser.
|
||||
|
||||
mXSS attack exploit browser bugs that cause some browsers parse a certain html strings into DOM, which once serialized
|
||||
doesn't match the original input. These browser bugs can be exploited by attackers to create payload which looks
|
||||
harmless to sanitizers, but due to mutations caused by the browser are turned into dangerous code once processed after
|
||||
sanitization.
|
||||
@@ -0,0 +1,16 @@
|
||||
@ngdoc error
|
||||
@name linky:notstring
|
||||
@fullName Not a string
|
||||
@description
|
||||
|
||||
This error occurs when {@link ngSanitize.linky linky} is used with a non-empty, non-string value:
|
||||
```html
|
||||
<div ng-bind-html="42 | linky"></div>
|
||||
```
|
||||
|
||||
`linky` is supposed to be used with string values only, and therefore assumes that several methods
|
||||
(such as `.match()`) are available on the passed in value.
|
||||
The value can be initialized asynchronously and therefore null or undefined won't throw this error.
|
||||
|
||||
If you want to pass non-string values to `linky` (e.g. Objects whose `.toString()` should be
|
||||
utilized), you need to manually convert them to strings.
|
||||
@@ -0,0 +1,28 @@
|
||||
@ngdoc error
|
||||
@name ngModel:nopromise
|
||||
@fullName No promise
|
||||
@description
|
||||
|
||||
The return value of an async validator, must always be a promise. If you want to return a
|
||||
non-promise value, you can convert it to a promise using {@link ng.$q#resolve `$q.resolve()`} or
|
||||
{@link ng.$q#reject `$q.reject()`}.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
.directive('asyncValidator', function($q) {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, elem, attrs, ngModel) {
|
||||
ngModel.$asyncValidators.myAsyncValidation = function(modelValue, viewValue) {
|
||||
if (/* I don't need to hit the backend API */) {
|
||||
return $q.resolve(); // to mark as valid or
|
||||
// return $q.reject(); // to mark as invalid
|
||||
} else {
|
||||
// ...send a request to the backend and return a promise
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,52 @@
|
||||
@ngdoc error
|
||||
@name orderBy:notarray
|
||||
@fullName Value is not array-like
|
||||
@description
|
||||
|
||||
This error occurs when {@link ng.orderBy orderBy} is not passed an array-like value:
|
||||
```html
|
||||
<div ng-repeat="(key, value) in myObj | orderBy:someProp">
|
||||
{{ key }} : {{ value }}
|
||||
</div>
|
||||
```
|
||||
|
||||
`orderBy` must be used with an array-like value so a subset of items can be returned.
|
||||
The array can be initialized asynchronously and therefore `null` or `undefined` won't throw this error.
|
||||
|
||||
To use `orderBy` to order the properties of an object, you can create your own array based on that object:
|
||||
```js
|
||||
angular.module('aModule', [])
|
||||
.controller('aController', function($scope) {
|
||||
var myObj = {
|
||||
one: {id: 1, name: 'Some thing'},
|
||||
two: {id: 2, name: 'Another thing'},
|
||||
three: {id: 3, name: 'A third thing'}
|
||||
};
|
||||
|
||||
$scope.arrFromMyObj = Object.keys(myObj).map(function(key) {
|
||||
return myObj[key];
|
||||
});
|
||||
});
|
||||
```
|
||||
That can be used as:
|
||||
```html
|
||||
<label>
|
||||
Order by:
|
||||
<select ng-model="orderProp" ng-options="prop for prop in ['id', 'name']"></select>
|
||||
</label>
|
||||
<div ng-repeat="item in arrFromMyObj | orderBy:orderProp">
|
||||
[{{ item.id }}] {{ item.name }}
|
||||
</div>
|
||||
```
|
||||
|
||||
You could as well convert the object to an array using a filter such as
|
||||
[toArrayFilter](https://github.com/petebacondarwin/angular-toArrayFilter):
|
||||
```html
|
||||
<label>
|
||||
Order by:
|
||||
<select ng-model="orderProp" ng-options="prop for prop in ['id', 'name']"></select>
|
||||
</label>
|
||||
<div ng-repeat="item in myObj | toArray:false | orderBy:orderProp">
|
||||
[{{ item.id }}] {{ item.name }}
|
||||
</div>
|
||||
```
|
||||
@@ -330,8 +330,8 @@ reload to the original link.
|
||||
### Relative links
|
||||
|
||||
Be sure to check all relative links, images, scripts etc. Angular requires you to specify the url
|
||||
base in the head of your main html file (`<base href="/my-base">`) unless `html5Mode.requireBase` is
|
||||
set to `false` in the html5Mode definition object passed to `$locationProvider.html5Mode()`. With
|
||||
base in the head of your main html file (`<base href="/my-base/index.html">`) unless `html5Mode.requireBase`
|
||||
is set to `false` in the html5Mode definition object passed to `$locationProvider.html5Mode()`. With
|
||||
that, relative urls will always be resolved to this base url, even if the initial url of the
|
||||
document was different.
|
||||
|
||||
@@ -339,6 +339,7 @@ There is one exception: Links that only contain a hash fragment (e.g. `<a href="
|
||||
will only change `$location.hash()` and not modify the url otherwise. This is useful for scrolling
|
||||
to anchors on the same page without needing to know on which page the user currently is.
|
||||
|
||||
|
||||
### Server side
|
||||
|
||||
Using this mode requires URL rewriting on server side, basically you have to rewrite all your links
|
||||
@@ -346,6 +347,20 @@ to entry point of your application (e.g. index.html). Requiring a `<base>` tag i
|
||||
this case, as it allows Angular to differentiate between the part of the url that is the application
|
||||
base and the path that should be handled by the application.
|
||||
|
||||
### Base href constraints
|
||||
|
||||
The `$location` service is not able to function properly if the current URL is outside the URL given
|
||||
as the base href. This can have subtle confusing consequences...
|
||||
|
||||
Consider a base href set as follows: `<base href="/base/">` (i.e. the application exists in the "folder"
|
||||
called `/base`). The URL `/base` is actually outside the application (it refers to the `base` file found
|
||||
in the root `/` folder).
|
||||
|
||||
If you wish to be able to navigate to the application via a URL such as `/base` then you should ensure that
|
||||
you server is setup to redirect such requests to `/base/`.
|
||||
|
||||
See https://github.com/angular/angular.js/issues/14018 for more information.
|
||||
|
||||
### Sending links among different browsers
|
||||
|
||||
Because of rewriting capability in HTML5 mode, your users will be able to open regular url links in
|
||||
@@ -356,15 +371,15 @@ legacy browsers and hashbang links in modern browser:
|
||||
|
||||
### Example
|
||||
|
||||
Here you can see two `$location` instances, both in **Html5 mode**, but on different browsers, so
|
||||
that you can see the differences. These `$location` services are connected to a fake browsers. Each
|
||||
input represents the address bar of the browser.
|
||||
Here you can see two `$location` instances that show the difference between **Html5 mode** and **Html5 Fallback mode**.
|
||||
Note that to simulate different levels of browser support, the `$location` instances are connected to
|
||||
a fakeBrowser service, which you don't have to set up in actual projects.
|
||||
|
||||
Note that when you type hashbang url into first browser (or vice versa) it doesn't rewrite /
|
||||
Note that when you type hashbang url into the first browser (or vice versa) it doesn't rewrite /
|
||||
redirect to regular / hashbang url, as this conversion happens only during parsing the initial URL
|
||||
= on page reload.
|
||||
|
||||
In these examples we use `<base href="/base/index.html" />`
|
||||
In these examples we use `<base href="/base/index.html" />`. The inputs represent the address bar of the browser.
|
||||
|
||||
#### Browser in HTML5 mode
|
||||
<example module="html5-mode" name="location-html5-mode">
|
||||
@@ -389,6 +404,7 @@ In these examples we use `<base href="/base/index.html" />`
|
||||
<file name="app.js">
|
||||
angular.module('html5-mode', ['fake-browser', 'address-bar'])
|
||||
|
||||
// Configure the fakeBrowser. Do not set these values in actual projects.
|
||||
.constant('initUrl', 'http://www.example.com/base/path?a=b#h')
|
||||
.constant('baseHref', '/base/index.html')
|
||||
.value('$sniffer', { history: true })
|
||||
@@ -538,6 +554,7 @@ In these examples we use `<base href="/base/index.html" />`
|
||||
<file name="app.js">
|
||||
angular.module('hashbang-mode', ['fake-browser', 'address-bar'])
|
||||
|
||||
// Configure the fakeBrowser. Do not set these values in actual projects.
|
||||
.constant('initUrl', 'http://www.example.com/base/index.html#!/path?a=b#h')
|
||||
.constant('baseHref', '/base/index.html')
|
||||
.value('$sniffer', { history: false })
|
||||
@@ -769,8 +786,8 @@ then uses the information it obtains to compose hashbang URLs (such as
|
||||
</tr>
|
||||
|
||||
<tr class="head">
|
||||
<td>Navigation outside the app</td>
|
||||
<td>Use lower level API</td>
|
||||
<th>Navigation outside the app</td>
|
||||
<th>Use lower level API</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
@@ -784,8 +801,8 @@ then uses the information it obtains to compose hashbang URLs (such as
|
||||
</tr>
|
||||
|
||||
<tr class="head">
|
||||
<td>Read access</td>
|
||||
<td>Change to</td>
|
||||
<th>Read access</td>
|
||||
<th>Change to</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
||||
@@ -10,7 +10,7 @@ The goal of ngAria is to improve Angular's default accessibility by enabling com
|
||||
[ARIA](http://www.w3.org/TR/wai-aria/) attributes that convey state or semantic information for
|
||||
assistive technologies used by persons with disabilities.
|
||||
|
||||
##Including ngAria
|
||||
## Including ngAria
|
||||
|
||||
Using {@link ngAria ngAria} is as simple as requiring the ngAria module in your application. ngAria hooks into
|
||||
standard AngularJS directives and quietly injects accessibility support into your application
|
||||
@@ -20,7 +20,7 @@ at runtime.
|
||||
angular.module('myApp', ['ngAria'])...
|
||||
```
|
||||
|
||||
###Using ngAria
|
||||
### Using ngAria
|
||||
Most of what ngAria does is only visible "under the hood". To see the module in action, once you've
|
||||
added it as a dependency, you can test a few things:
|
||||
* Using your favorite element inspector, look for attributes added by ngAria in your own code.
|
||||
@@ -28,11 +28,15 @@ added it as a dependency, you can test a few things:
|
||||
* Fire up a screen reader such as VoiceOver or NVDA to check for ARIA support.
|
||||
[Helpful screen reader tips.](http://webaim.org/articles/screenreader_testing/)
|
||||
|
||||
##Supported directives
|
||||
## Supported directives
|
||||
Currently, ngAria interfaces with the following directives:
|
||||
|
||||
* {@link guide/accessibility#ngmodel ngModel}
|
||||
* {@link guide/accessibility#ngdisabled ngDisabled}
|
||||
* {@link guide/accessibility#ngrequired ngRequired}
|
||||
* {@link guide/accessibility#ngreadonly ngReadonly}
|
||||
* {@link guide/accessibility#ngvaluechecked ngChecked}
|
||||
* {@link guide/accessibility#ngvaluechecked ngValue}
|
||||
* {@link guide/accessibility#ngshow ngShow}
|
||||
* {@link guide/accessibility#nghide ngHide}
|
||||
* {@link guide/accessibility#ngclick ngClick}
|
||||
@@ -41,7 +45,7 @@ Currently, ngAria interfaces with the following directives:
|
||||
|
||||
<h2 id="ngmodel">ngModel</h2>
|
||||
|
||||
Much of ngAria's heavy lifting happens in the {@link ngModel ngModel}
|
||||
Much of ngAria's heavy lifting happens in the {@link ng.ngModel ngModel}
|
||||
directive. For elements using ngModel, special attention is paid by ngAria if that element also
|
||||
has a role or type of `checkbox`, `radio`, `range` or `textbox`.
|
||||
|
||||
@@ -54,12 +58,62 @@ attributes (if they have not been explicitly specified by the developer):
|
||||
* aria-valuenow
|
||||
* aria-invalid
|
||||
* aria-required
|
||||
* aria-readonly
|
||||
|
||||
###Example
|
||||
### Example
|
||||
|
||||
<example module="ngAria_ngModelExample" deps="angular-aria.js">
|
||||
<file name="index.html">
|
||||
<style>
|
||||
<file name="index.html">
|
||||
<form ng-controller="formsController">
|
||||
<some-checkbox role="checkbox" ng-model="checked" ng-class="{active: checked}"
|
||||
ng-disabled="isDisabled" ng-click="toggleCheckbox()"
|
||||
aria-label="Custom Checkbox" show-attrs>
|
||||
<span class="icon" aria-hidden="true"></span>
|
||||
Custom Checkbox
|
||||
</some-checkbox>
|
||||
</form>
|
||||
</file>
|
||||
<file name="script.js">
|
||||
var app = angular.module('ngAria_ngModelExample', ['ngAria'])
|
||||
.controller('formsController', function($scope){
|
||||
$scope.checked = false;
|
||||
$scope.toggleCheckbox = function(){
|
||||
$scope.checked = !$scope.checked;
|
||||
};
|
||||
})
|
||||
.directive('someCheckbox', function(){
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function($scope, $el, $attrs) {
|
||||
$el.on('keypress', function(event){
|
||||
event.preventDefault();
|
||||
if(event.keyCode === 32 || event.keyCode === 13){
|
||||
$scope.toggleCheckbox();
|
||||
$scope.$apply();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
.directive('showAttrs', function() {
|
||||
return function($scope, $el, $attrs) {
|
||||
var pre = document.createElement('pre');
|
||||
$el.after(pre);
|
||||
$scope.$watch(function() {
|
||||
var $attrs = {};
|
||||
Array.prototype.slice.call($el[0].attributes, 0).forEach(function(item) {
|
||||
if (item.name !== 'show-$attrs') {
|
||||
$attrs[item.name] = item.value;
|
||||
}
|
||||
});
|
||||
return $attrs;
|
||||
}, function(newAttrs, oldAttrs) {
|
||||
pre.textContent = JSON.stringify(newAttrs, null, 2);
|
||||
}, true);
|
||||
};
|
||||
});
|
||||
</file>
|
||||
<file name="style.css">
|
||||
[role=checkbox] {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
@@ -78,91 +132,101 @@ attributes (if they have not been explicitly specified by the developer):
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
<form ng-controller="formsController">
|
||||
<some-checkbox role="checkbox" ng-model="checked" ng-class="{active: checked}"
|
||||
ng-disabled="isDisabled" ng-click="toggleCheckbox()"
|
||||
aria-label="Custom Checkbox" show-attrs>
|
||||
<span class="icon" aria-hidden="true"></span>
|
||||
Custom Checkbox
|
||||
</some-checkbox>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
var app = angular.module('ngAria_ngModelExample', ['ngAria'])
|
||||
.controller('formsController', function($scope){
|
||||
$scope.checked = false;
|
||||
$scope.toggleCheckbox = function(){
|
||||
$scope.checked = !$scope.checked;
|
||||
}
|
||||
})
|
||||
.directive('someCheckbox', function(){
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function($scope, $el, $attrs) {
|
||||
$el.on('keypress', function(event){
|
||||
event.preventDefault();
|
||||
if(event.keyCode === 32 || event.keyCode === 13){
|
||||
$scope.toggleCheckbox();
|
||||
$scope.$apply();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.directive('showAttrs', function() {
|
||||
return function($scope, $el, $attrs) {
|
||||
var pre = document.createElement('pre');
|
||||
$el.after(pre);
|
||||
$scope.$watch(function() {
|
||||
var $attrs = {};
|
||||
Array.prototype.slice.call($el[0].attributes, 0).forEach(function(item) {
|
||||
if (item.name !== 'show-$attrs') {
|
||||
$attrs[item.name] = item.value;
|
||||
}
|
||||
});
|
||||
return $attrs;
|
||||
}, function(newAttrs, oldAttrs) {
|
||||
pre.textContent = JSON.stringify(newAttrs, null, 2);
|
||||
}, true);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</file>
|
||||
</file>
|
||||
</example>
|
||||
|
||||
ngAria will also add `tabIndex`, ensuring custom elements with these roles will be reachable from
|
||||
the keyboard. It is still up to **you** as a developer to **ensure custom controls will be
|
||||
accessible**. As a rule, any time you create a widget involving user interaction, be sure to test
|
||||
accessible**. As a rule, any time you create a widget involving user interaction, be sure to test
|
||||
it with your keyboard and at least one mobile and desktop screen reader.
|
||||
|
||||
<h2 id="ngdisabled">ngDisabled</h2>
|
||||
<h2 id="ngvaluechecked">ngValue and ngChecked</h2>
|
||||
|
||||
The `disabled` attribute is only valid for certain elements such as `button`, `input` and
|
||||
`textarea`. To properly disable custom element directives such as `<md-checkbox>` or `<taco-tab>`,
|
||||
using ngAria with [ngDisabled](https://docs.angularjs.org/api/ng/directive/ngDisabled) will also
|
||||
add `aria-disabled`. This tells assistive technologies when a non-native input is disabled, helping
|
||||
custom controls to be more accessible.
|
||||
To ease the transition between native inputs and custom controls, ngAria now supports
|
||||
{@link ng.ngValue ngValue} and {@link ng.ngChecked ngChecked}.
|
||||
The original directives were created for native inputs only, so ngAria extends
|
||||
support to custom elements by managing `aria-checked` for accessibility.
|
||||
|
||||
###Example
|
||||
### Example
|
||||
|
||||
```html
|
||||
<md-checkbox ng-disabled="disabled">
|
||||
<custom-checkbox ng-checked="val"></custom-checkbox>
|
||||
<custom-radio-button ng-value="val"></custom-radio-button>
|
||||
```
|
||||
|
||||
Becomes:
|
||||
|
||||
```html
|
||||
<md-checkbox disabled aria-disabled="true">
|
||||
<custom-checkbox ng-checked="val" aria-checked="true"></custom-checkbox>
|
||||
<custom-radio-button ng-value="val" aria-checked="true"></custom-radio-button>
|
||||
```
|
||||
|
||||
>You can check whether a control is legitimately disabled for a screen reader by visiting
|
||||
<h2 id="ngdisabled">ngDisabled</h2>
|
||||
|
||||
The `disabled` attribute is only valid for certain elements such as `button`, `input` and
|
||||
`textarea`. To properly disable custom element directives such as `<md-checkbox>` or `<taco-tab>`,
|
||||
using ngAria with {@link ng.ngDisabled ngDisabled} will also
|
||||
add `aria-disabled`. This tells assistive technologies when a non-native input is disabled, helping
|
||||
custom controls to be more accessible.
|
||||
|
||||
### Example
|
||||
|
||||
```html
|
||||
<md-checkbox ng-disabled="disabled"></md-checkbox>
|
||||
```
|
||||
|
||||
Becomes:
|
||||
|
||||
```html
|
||||
<md-checkbox disabled aria-disabled="true"></md-checkbox>
|
||||
```
|
||||
|
||||
<div class="alert alert-info">
|
||||
You can check whether a control is legitimately disabled for a screen reader by visiting
|
||||
[chrome://accessibility](chrome://accessibility) and inspecting [the accessibility tree](http://www.paciellogroup.com/blog/2015/01/the-browser-accessibility-tree/).
|
||||
</div>
|
||||
|
||||
<h2 id="ngrequired">ngRequired</h2>
|
||||
|
||||
The boolean `required` attribute is only valid for native form controls such as `input` and
|
||||
`textarea`. To properly indicate custom element directives such as `<md-checkbox>` or `<custom-input>`
|
||||
as required, using ngAria with {@link ng.ngRequired ngRequired} will also add
|
||||
`aria-required`. This tells accessibility APIs when a custom control is required.
|
||||
|
||||
### Example
|
||||
|
||||
```html
|
||||
<md-checkbox ng-required="val"></md-checkbox>
|
||||
```
|
||||
|
||||
Becomes:
|
||||
|
||||
```html
|
||||
<md-checkbox ng-required="val" aria-required="true"></md-checkbox>
|
||||
```
|
||||
|
||||
<h2 id="ngreadonly">ngReadonly</h2>
|
||||
|
||||
The boolean `readonly` attribute is only valid for native form controls such as `input` and
|
||||
`textarea`. To properly indicate custom element directives such as `<md-checkbox>` or `<custom-input>`
|
||||
as required, using ngAria with {@link ng.ngReadonly ngReadonly} will also add
|
||||
`aria-readonly`. This tells accessibility APIs when a custom control is read-only.
|
||||
|
||||
### Example
|
||||
|
||||
```html
|
||||
<md-checkbox ng-readonly="val"></md-checkbox>
|
||||
```
|
||||
|
||||
Becomes:
|
||||
|
||||
```html
|
||||
<md-checkbox ng-readonly="val" aria-readonly="true"></md-checkbox>
|
||||
```
|
||||
|
||||
<h2 id="ngshow">ngShow</h2>
|
||||
|
||||
>The [ngShow](https://docs.angularjs.org/api/ng/directive/ngShow) directive shows or hides the
|
||||
The {@link ng.ngShow ngShow} directive shows or hides the
|
||||
given HTML element based on the expression provided to the `ngShow` attribute. The element is
|
||||
shown or hidden by removing or adding the `.ng-hide` CSS class onto the element.
|
||||
|
||||
@@ -179,7 +243,7 @@ screen reader users won't accidentally focus on "mystery elements". Managing tab
|
||||
child control can be complex and affect performance, so it’s best to just stick with the default
|
||||
`display: none` CSS. See the [fourth rule of ARIA use](http://www.w3.org/TR/aria-in-html/#fourth-rule-of-aria-use).
|
||||
|
||||
###Example
|
||||
### Example
|
||||
```css
|
||||
.ng-hide {
|
||||
display: block;
|
||||
@@ -199,7 +263,7 @@ Becomes:
|
||||
|
||||
<h2 id="nghide">ngHide</h2>
|
||||
|
||||
>The [ngHide](https://docs.angularjs.org/api/ng/directive/ngHide) directive shows or hides the
|
||||
The {@link ng.ngHide ngHide} directive shows or hides the
|
||||
given HTML element based on the expression provided to the `ngHide` attribute. The element is
|
||||
shown or hidden by removing or adding the `.ng-hide` CSS class onto the element.
|
||||
|
||||
@@ -208,7 +272,7 @@ The default CSS for `ngHide`, the inverse method to `ngShow`, makes ngAria redun
|
||||
`display: none`. See explanation for {@link guide/accessibility#ngshow ngShow} when overriding the default CSS.
|
||||
|
||||
<h2><span id="ngclick">ngClick</span> and <span id="ngdblclick">ngDblclick</span></h2>
|
||||
If `ng-click` or `ng-dblclick` is encountered, ngAria will add `tabindex="0"` to any element not in
|
||||
If `ng-click` or `ng-dblclick` is encountered, ngAria will add `tabindex="0"` to any element not in
|
||||
a node blacklist:
|
||||
|
||||
* Button
|
||||
@@ -218,14 +282,14 @@ a node blacklist:
|
||||
* Select
|
||||
* Details/Summary
|
||||
|
||||
To fix widespread accessibility problems with `ng-click` on `div` elements, ngAria will
|
||||
To fix widespread accessibility problems with `ng-click` on `div` elements, ngAria will
|
||||
dynamically bind a keypress event by default as long as the element isn't in the node blacklist.
|
||||
You can turn this functionality on or off with the `bindKeypress` configuration option.
|
||||
You can turn this functionality on or off with the `bindKeypress` configuration option.
|
||||
|
||||
ngAria will also add the `button` role to communicate to users of assistive technologies. This can
|
||||
be disabled with the `bindRoleForClick` configuration option.
|
||||
|
||||
For `ng-dblclick`, you must still manually add `ng-keypress` and a role to non-interactive elements
|
||||
For `ng-dblclick`, you must still manually add `ng-keypress` and a role to non-interactive elements
|
||||
such as `div` or `taco-button` to enable keyboard access.
|
||||
|
||||
<h3>Example</h3>
|
||||
@@ -240,11 +304,11 @@ Becomes:
|
||||
|
||||
<h2 id="ngmessages">ngMessages</h2>
|
||||
|
||||
The new ngMessages module makes it easy to display form validation or other messages with priority
|
||||
The ngMessages module makes it easy to display form validation or other messages with priority
|
||||
sequencing and animation. To expose these visual messages to screen readers,
|
||||
ngAria injects `aria-live="assertive"`, causing them to be read aloud any time a message is shown,
|
||||
regardless of the user's focus location.
|
||||
###Example
|
||||
### Example
|
||||
|
||||
```html
|
||||
<div ng-messages="myForm.myName.$error">
|
||||
@@ -262,7 +326,7 @@ Becomes:
|
||||
</div>
|
||||
```
|
||||
|
||||
##Disabling attributes
|
||||
## Disabling attributes
|
||||
The attribute magic of ngAria may not work for every scenario. To disable individual attributes,
|
||||
you can use the {@link ngAria.$ariaProvider#config config} method. Just keep in mind this will
|
||||
tell ngAria to ignore the attribute globally.
|
||||
@@ -300,7 +364,7 @@ tell ngAria to ignore the attribute globally.
|
||||
</file>
|
||||
</example>
|
||||
|
||||
##Common Accessibility Patterns
|
||||
## Common Accessibility Patterns
|
||||
|
||||
Accessibility best practices that apply to web apps in general also apply to Angular.
|
||||
|
||||
|
||||
@@ -6,12 +6,19 @@
|
||||
|
||||
# Animations
|
||||
|
||||
AngularJS 1.3 provides animation hooks for common directives such as `ngRepeat`, `ngSwitch`, and `ngView`, as well as custom directives
|
||||
AngularJS provides animation hooks for common directives such as `ngRepeat`, `ngSwitch`, and `ngView`, as well as custom directives
|
||||
via the `$animate` service. These animation hooks are set in place to trigger animations during the life cycle of various directives and when
|
||||
triggered, will attempt to perform a CSS Transition, CSS Keyframe Animation or a JavaScript callback Animation (depending on if an animation is
|
||||
placed on the given directive). Animations can be placed using vanilla CSS by following the naming conventions set in place by AngularJS
|
||||
or with JavaScript code when it's defined as a factory.
|
||||
|
||||
<div class="alert alert-info">
|
||||
Note that we have used non-prefixed CSS transition properties in our examples as the major browsers now support non-prefixed
|
||||
properties. If you intend to support older browsers or certain mobile browsers then you will need to include prefixed
|
||||
versions of the transition properties. Take a look at http://caniuse.com/#feat=css-transitions for what browsers require prefixes,
|
||||
and https://github.com/postcss/autoprefixer for a tool that can automatically generate the prefixes for you.
|
||||
</div>
|
||||
|
||||
Animations are not available unless you include the {@link ngAnimate `ngAnimate` module} as a dependency within your application.
|
||||
|
||||
Below is a quick example of animations being enabled for `ngShow` and `ngHide`:
|
||||
@@ -29,18 +36,17 @@ Below is a quick example of animations being enabled for `ngShow` and `ngHide`:
|
||||
</file>
|
||||
<file name="animations.css">
|
||||
.sample-show-hide {
|
||||
padding:10px;
|
||||
border:1px solid black;
|
||||
background:white;
|
||||
padding: 10px;
|
||||
border: 1px solid black;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.sample-show-hide {
|
||||
-webkit-transition:all linear 0.5s;
|
||||
transition:all linear 0.5s;
|
||||
transition: all linear 0.5s;
|
||||
}
|
||||
|
||||
.sample-show-hide.ng-hide {
|
||||
opacity:0;
|
||||
opacity: 0;
|
||||
}
|
||||
</file>
|
||||
</example>
|
||||
@@ -80,11 +86,8 @@ occur when ngRepeat triggers them:
|
||||
class
|
||||
*/
|
||||
.repeated-item.ng-enter, .repeated-item.ng-move {
|
||||
-webkit-transition:0.5s linear all;
|
||||
-moz-transition:0.5s linear all;
|
||||
-o-transition:0.5s linear all;
|
||||
transition:0.5s linear all;
|
||||
opacity:0;
|
||||
transition: all 0.5s linear;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -95,7 +98,7 @@ occur when ngRepeat triggers them:
|
||||
*/
|
||||
.repeated-item.ng-enter.ng-enter-active,
|
||||
.repeated-item.ng-move.ng-move-active {
|
||||
opacity:1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -104,10 +107,7 @@ occur when ngRepeat triggers them:
|
||||
that has the .repeated-item class
|
||||
*/
|
||||
.repeated-item.ng-leave {
|
||||
-webkit-animation:0.5s my_animation;
|
||||
-moz-animation:0.5s my_animation;
|
||||
-o-animation:0.5s my_animation;
|
||||
animation:0.5s my_animation;
|
||||
animation: 0.5s my_animation;
|
||||
}
|
||||
|
||||
@keyframes my_animation {
|
||||
@@ -115,24 +115,6 @@ occur when ngRepeat triggers them:
|
||||
to { opacity:0; }
|
||||
}
|
||||
|
||||
/*
|
||||
Unfortunately each browser vendor requires
|
||||
its own definition of keyframe animation code...
|
||||
*/
|
||||
@-webkit-keyframes my_animation {
|
||||
from { opacity:1; }
|
||||
to { opacity:0; }
|
||||
}
|
||||
|
||||
@-moz-keyframes my_animation {
|
||||
from { opacity:1; }
|
||||
to { opacity:0; }
|
||||
}
|
||||
|
||||
@-o-keyframes my_animation {
|
||||
from { opacity:1; }
|
||||
to { opacity:0; }
|
||||
}
|
||||
```
|
||||
|
||||
The same approach to animation can be used using JavaScript code (**jQuery is used within to perform animations**):
|
||||
@@ -217,10 +199,7 @@ The example below shows how to perform animations during class changes:
|
||||
</file>
|
||||
<file name="style.css">
|
||||
.css-class-add, .css-class-remove {
|
||||
-webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||
-moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||
-o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||
transition: all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||
}
|
||||
|
||||
.css-class,
|
||||
@@ -231,7 +210,7 @@ The example below shows how to perform animations during class changes:
|
||||
|
||||
.css-class-remove.css-class-remove-active {
|
||||
font-size:1.0em;
|
||||
color:black;
|
||||
color: black;
|
||||
}
|
||||
</file>
|
||||
</example>
|
||||
@@ -274,8 +253,194 @@ myModule.directive('my-directive', ['$animate', function($animate) {
|
||||
}]);
|
||||
```
|
||||
|
||||
## Animations on app bootstrap / page load
|
||||
|
||||
By default, animations are disabled when the Angular app {@link guide/bootstrap bootstraps}. If you are using the {@link ngApp} directive,
|
||||
this happens in the `DOMContentLoaded` event, so immediately after the page has been loaded.
|
||||
Animations are disabled, so that UI and content are instantly visible. Otherwise, with many animations on
|
||||
the page, the loading process may become too visually overwhelming, and the performance may suffer.
|
||||
|
||||
Internally, `ngAnimate` waits until all template downloads that are started right after bootstrap have finished.
|
||||
Then, it waits for the currently running {@link ng.$rootScope.Scope#$digest} and the one after that to finish.
|
||||
This ensures that the whole app has been compiled fully before animations are attempted.
|
||||
|
||||
If you do want your animations to play when the app bootstraps, you can enable animations globally in
|
||||
your main module's {@link angular.Module#run run} function:
|
||||
|
||||
```js
|
||||
myModule.run(function($animate) {
|
||||
$animate.enabled(true);
|
||||
});
|
||||
```
|
||||
|
||||
## How to (selectively) enable, disable and skip animations
|
||||
|
||||
There are three different ways to disable animations, both globally and for specific animations.
|
||||
Disabling specific animations can help to speed up the render performance, for example for large `ngRepeat`
|
||||
lists that don't actually have animations. Because ngAnimate checks at runtime if animations are present,
|
||||
performance will take a hit even if an element has no animation.
|
||||
|
||||
### In the config: {@link $animateProvider#classNameFilter $animateProvider.classNameFilter()}
|
||||
|
||||
This function can be called in the {@link angular.Module#config config} phase of an app. It takes a regex as the only argument,
|
||||
which will then be matched against the classes of any element that is about to be animated. The regex
|
||||
allows a lot of flexibility - you can either allow animations only for specific classes (useful when
|
||||
you are working with 3rd party animations), or exclude specific classes from getting animated.
|
||||
|
||||
```js
|
||||
app.config(function($animateProvider) {
|
||||
$animateProvider.classNameFilter(/animate-/);
|
||||
});
|
||||
```
|
||||
|
||||
```css
|
||||
/* prefixed with animate- */
|
||||
.animate-fade-add.animate-fade-add-active {
|
||||
transition: all 1s linear;
|
||||
opacity: 0;
|
||||
}
|
||||
```
|
||||
|
||||
The classNameFilter approach generally applies the biggest speed boost, because the matching is
|
||||
done before any other animation disabling strategies are checked. However, that also means it is not
|
||||
possible to override class name matching with the two following strategies. It's of course still possible
|
||||
to enable / disable animations by changing an element's class name at runtime.
|
||||
|
||||
### At runtime: {@link ng.$animate#enabled $animate.enabled()}
|
||||
|
||||
This function can be used to enable / disable animations in two different ways:
|
||||
|
||||
With a single `boolean` argument, it enables / disables animations globally: `$animate.enabled(false)`
|
||||
disables all animations in your app.
|
||||
|
||||
When the first argument is a native DOM or jqLite/jQuery element, the function enables / disables
|
||||
animations on this element *and all its children*: `$animate.enabled(myElement, false)`. This is the
|
||||
most flexible way to change the animation state. For example, even if you have used it to disable
|
||||
animations on a parent element, you can still re-enable it for a child element. And compared to the
|
||||
`classNameFilter`, you can change the animation status at runtime instead of during the config phase.
|
||||
|
||||
Note however that the `$animate.enabled()` state for individual elements does not overwrite disabling
|
||||
rules that have been set in the {@link $animateProvider#classNameFilter classNameFilter}.
|
||||
|
||||
### Via CSS styles: overwriting styles in the `ng-animate` CSS class
|
||||
Whenever an animation is started, ngAnimate applies the `ng-animate` class to the element for the
|
||||
whole duration of the animation. By applying CSS transition / animation styling to the class,
|
||||
you can skip an animation:
|
||||
|
||||
```css
|
||||
|
||||
.my-class{
|
||||
transition: transform 2s;
|
||||
}
|
||||
|
||||
.my-class:hover {
|
||||
transform: translateX(50px);
|
||||
}
|
||||
|
||||
my-class.ng-animate {
|
||||
transition: 0s;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
By setting `transition: 0s`, ngAnimate will ignore the existing transition styles, and not try to animate them (Javascript
|
||||
animations will still execute, though). This can be used to prevent {@link guide/animations#preventing-collisions-with-existing-animations-and-third-party-libraries
|
||||
issues with existing animations interfering with ngAnimate}.
|
||||
|
||||
|
||||
## Preventing flicker before an animation starts
|
||||
|
||||
When nesting elements with structural animations such as `ngIf` into elements that have class-based
|
||||
animations such as `ngClass`, it sometimes happens that before the actual animation starts, there is a brief flicker or flash of content
|
||||
where the animated element is briefly visible.
|
||||
|
||||
To prevent this, you can apply styles to the `ng-[event]-prepare` class, which is added as soon as an animation is initialized,
|
||||
but removed before the actual animation starts (after waiting for a $digest). This class is only added for *structural*
|
||||
animations (`enter`, `move`, and `leave`).
|
||||
|
||||
Here's an example where you might see flickering:
|
||||
|
||||
```html
|
||||
<div ng-class="{red: myProp}">
|
||||
<div ng-class="{blue: myProp}">
|
||||
<div class="message" ng-if="myProp"></div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
It is possible that during the `enter` event, the `.message` div will be briefly visible before it starts animating.
|
||||
In that case, you can add styles to the CSS that make sure the element stays hidden before the animation starts:
|
||||
|
||||
```css
|
||||
.message.ng-enter-prepare {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Other animation styles ... */
|
||||
```
|
||||
|
||||
## Preventing Collisions with Existing Animations and Third Party Libraries
|
||||
By default, any `ngAnimate` enabled directives will assume any transition / animation styles on the
|
||||
element are part of an `ngAnimate` animation. This can lead to problems when the styles are actually
|
||||
for animations that are independent of `ngAnimate`.
|
||||
|
||||
For example, an element acts as a loading spinner. It has an inifinite css animation on it, and also an
|
||||
{@link ngIf `ngIf`} directive, for which no animations are defined:
|
||||
|
||||
```css
|
||||
@keyframes rotating {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.spinner {
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
```
|
||||
|
||||
Now, when the `ngIf` changes, `ngAnimate` will see the spinner animation and use
|
||||
it to animate the `enter`/`leave` event, which doesn't work because
|
||||
the animation is infinite. The element will still be added / removed after a timeout, but there will be a
|
||||
noticable delay.
|
||||
|
||||
This might also happen because some third-party frameworks place animation duration defaults
|
||||
across many element or className selectors in order to make their code small and reuseable.
|
||||
|
||||
You can prevent this unwanted behavior by adding CSS to the `.ng-animate` class that is added
|
||||
for the whole duration of an animation. Simply overwrite the transition / animation duration. In the
|
||||
case of the spinner, this would be:
|
||||
|
||||
```css
|
||||
.spinner.ng-animate {
|
||||
transition: 0s none;
|
||||
animation: 0s none;
|
||||
}
|
||||
```
|
||||
|
||||
If you do have CSS transitions / animations defined for the animation events, make sure they have higher priority
|
||||
than any styles that are independent from ngAnimate.
|
||||
|
||||
You can also use one of the two other {@link guide/animations#how-to-selectively-enable-disable-and-skip-animations strategies to disable animations}.
|
||||
|
||||
|
||||
### Enable animations for elements outside of the Angular application DOM tree: {@link ng.$animate#pin $animate.pin()}
|
||||
|
||||
Before animating, `ngAnimate` checks to see if the element being animated is inside the application DOM tree,
|
||||
and if it is not, no animation is run. Usually, this is not a problem as most apps use the `ngApp`
|
||||
attribute / bootstrap the app on the `html` or `body` element.
|
||||
|
||||
Problems arise when the application is bootstrapped on a different element, and animations are
|
||||
attempted on elements that are outside the application tree, e.g. when libraries append popup and modal
|
||||
elements as the last child in the body tag.
|
||||
|
||||
You can use {@link ng.$animate#pin `$animate.pin(elementToAnimate, parentHost)`} to specify that an
|
||||
element belongs to your application. Simply call it before the element is added to the DOM / before
|
||||
the animation starts, with the element you want to animate, and the element which should be its
|
||||
assumed parent.
|
||||
|
||||
|
||||
## More about animations
|
||||
|
||||
For a full breakdown of each method available on `$animate`, see the {@link ng.$animate API documentation}.
|
||||
|
||||
To see a complete demo, see the {@link tutorial/step_12 animation step within the AngularJS phonecat tutorial}.
|
||||
To see a complete demo, see the {@link tutorial/step_14 animation step within the AngularJS phonecat tutorial}.
|
||||
|
||||
@@ -53,13 +53,13 @@ initialization.
|
||||
|
||||
Angular initializes automatically upon `DOMContentLoaded` event or when the `angular.js` script is
|
||||
evaluated if at that time `document.readyState` is set to `'complete'`. At this point Angular looks
|
||||
for the {@link ng.directive:ngApp `ng-app`} directive which designates your application root.
|
||||
If the {@link ng.directive:ngApp `ng-app`} directive is found then Angular will:
|
||||
for the {@link ng.directive:ngApp `ngApp`} directive which designates your application root.
|
||||
If the {@link ng.directive:ngApp `ngApp`} directive is found then Angular will:
|
||||
|
||||
* load the {@link guide/module module} associated with the directive.
|
||||
* create the application {@link auto.$injector injector}
|
||||
* compile the DOM treating the {@link ng.directive:ngApp
|
||||
`ng-app`} directive as the root of the compilation. This allows you to tell it to treat only a
|
||||
`ngApp`} directive as the root of the compilation. This allows you to tell it to treat only a
|
||||
portion of the DOM as an Angular application.
|
||||
|
||||
|
||||
@@ -142,6 +142,17 @@ This is the sequence that your code should follow:
|
||||
2. Call {@link angular.bootstrap} to {@link compiler compile} the element into an
|
||||
executable, bi-directionally bound application.
|
||||
|
||||
## Things to keep in mind
|
||||
|
||||
There a few things to keep in mind regardless of automatic or manual bootstrapping:
|
||||
|
||||
- While it's possible to bootstrap more than one AngularJS application per page, we don't actively
|
||||
test against this scenario. It's possible that you'll run into problems, especially with complex apps, so
|
||||
caution is advised.
|
||||
- Do not bootstrap your app on an element with a directive that uses {@link ng.$compile#transclusion transclusion}, such as
|
||||
{@link ng.ngIf `ngIf`}, {@link ng.ngInclude `ngInclude`} and {@link ngRoute.ngView `ngView`}.
|
||||
Doing this misplaces the app {@link ng.$rootElement `$rootElement`} and the app's {@link auto.$injector injector},
|
||||
causing animations to stop working and making the injector inaccessible from outside the app.
|
||||
|
||||
## Deferred Bootstrap
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,499 @@
|
||||
@ngdoc overview
|
||||
@name Components
|
||||
@sortOrder 305
|
||||
@description
|
||||
|
||||
# Understanding Components
|
||||
|
||||
In Angular, a Component is a special kind of {@link guide/directive directive} that uses a simpler
|
||||
configuration which is suitable for a component-based application structure.
|
||||
|
||||
This makes it easier to write an app in a way that's similar to using Web Components or using Angular
|
||||
2's style of application architecture.
|
||||
|
||||
Advantages of Components:
|
||||
- simpler configuration than plain directives
|
||||
- promote sane defaults and best practices
|
||||
- optimized for component-based architecture
|
||||
- writing component directives will make it easier to upgrade to Angular 2
|
||||
|
||||
When not to use Components:
|
||||
|
||||
- for directives that rely on DOM manipulation, adding event listeners etc, because the compile
|
||||
and link functions are unavailable
|
||||
- when you need advanced directive definition options like priority, terminal, multi-element
|
||||
- when you want a directive that is triggered by an attribute or CSS class, rather than an element
|
||||
|
||||
## Creating and configuring a Component
|
||||
|
||||
Components can be registered using the `.component()` method of an Angular module (returned by {@link module `angular.module()`}). The method takes two arguments:
|
||||
|
||||
* The name of the Component (as string).
|
||||
* The Component config object. (Note that, unlike the `.directive()` method, this method does **not** take a factory function.)
|
||||
|
||||
<example name="heroComponentSimple" module="heroApp">
|
||||
<file name="index.js">
|
||||
angular.module('heroApp', []).controller('mainCtrl', function() {
|
||||
this.hero = {
|
||||
name: 'Spawn'
|
||||
};
|
||||
});
|
||||
</file>
|
||||
<file name="heroDetail.js">
|
||||
|
||||
function HeroDetailController() {
|
||||
|
||||
}
|
||||
|
||||
angular.module('heroApp').component('heroDetail', {
|
||||
templateUrl: 'heroDetail.html',
|
||||
controller: HeroDetailController,
|
||||
bindings: {
|
||||
hero: '='
|
||||
}
|
||||
});
|
||||
</file>
|
||||
<file name="index.html">
|
||||
<!-- components match only elements -->
|
||||
<div ng-controller="mainCtrl as ctrl">
|
||||
<b>Hero</b><br>
|
||||
<hero-detail hero="ctrl.hero"></hero-detail>
|
||||
</div>
|
||||
</file>
|
||||
<file name="heroDetail.html">
|
||||
<span>Name: {{$ctrl.hero.name}}</span>
|
||||
</file>
|
||||
</example>
|
||||
|
||||
It's also possible to add components via {@link $compileProvider#component} in a module's config phase.
|
||||
|
||||
### Comparison between Directive definition and Component definition
|
||||
|
||||
| | Directive | Component |
|
||||
|-------------------|----------------------|-----------------|
|
||||
| bindings | No | Yes (binds to controller) |
|
||||
| bindToController | Yes (default: false) | No (use bindings instead) |
|
||||
| compile function | Yes | No |
|
||||
| controller | Yes | Yes (default `function() {}`) |
|
||||
| controllerAs | Yes (default: false) | Yes (default: `$ctrl`) |
|
||||
| link functions | Yes | No |
|
||||
| multiElement | Yes | No |
|
||||
| priority | Yes | No |
|
||||
| require | Yes | Yes |
|
||||
| restrict | Yes | No (restricted to elements only) |
|
||||
| scope | Yes (default: false) | No (scope is always isolate) |
|
||||
| template | Yes | Yes, injectable |
|
||||
| templateNamespace | Yes | No |
|
||||
| templateUrl | Yes | Yes, injectable |
|
||||
| terminal | Yes | No |
|
||||
| transclude | Yes (default: false) | Yes (default: false) |
|
||||
|
||||
|
||||
## Component-based application architecture
|
||||
|
||||
As already mentioned, the component helper makes it easier to structure your application with
|
||||
a component-based architecture. But what makes a component beyond the options that
|
||||
the component helper has?
|
||||
|
||||
- **Components only control their own View and Data:**
|
||||
Components should never modify any data or DOM that is out of their own scope. Normally, in Angular
|
||||
it is possible to modify data anywhere in the application through scope inheritance and watches. This
|
||||
is practical, but can also lead to problems when it is not clear which part of the application is
|
||||
responsible for modifying the data. That is why component directives use an isolate scope, so a whole
|
||||
class of scope manipulation is not possible.
|
||||
|
||||
- **Components have a well-defined public API - Inputs and Outputs:**
|
||||
However, scope isolation only goes so far, because Angular uses two-way binding. So if you pass
|
||||
an object to a component like this - `bindings: {item: '='}`, and modify one of its properties, the
|
||||
change will be reflected in the parent component. For components however, only the component that owns
|
||||
the data should modify it, to make it easy to reason about what data is changed, and when. For that reason,
|
||||
components should follow a few simple conventions:
|
||||
|
||||
- Inputs should be using `<` and `@` bindings. The `<` symbol denotes {@link $compile#-scope- one-way bindings} which are
|
||||
available since 1.5. The difference to `=` is that the bound properties in the component scope are not watched, which means
|
||||
if you assign a new value to the property in the component scope, it will not update the parent scope. Note however, that both parent
|
||||
and component scope reference the same object, so if you are changing object properties or array elements in the
|
||||
component, the parent will still reflect that change.
|
||||
The general rule should therefore be to never change an object or array property in the component scope.
|
||||
`@` bindings can be used when the input is a string, especially when the value of the binding doesn't change.
|
||||
```js
|
||||
bindings: {
|
||||
hero: '<',
|
||||
comment: '@'
|
||||
}
|
||||
```
|
||||
- Outputs are realized with `&` bindings, which function as callbacks to component events.
|
||||
```js
|
||||
bindings: {
|
||||
onDelete: '&',
|
||||
onUpdate: '&'
|
||||
}
|
||||
```
|
||||
- Instead of manipulating Input Data, the component calls the correct Output Event with the changed data.
|
||||
For a deletion, that means the component doesn't delete the `hero` itself, but sends it back to
|
||||
the owner component via the correct event.
|
||||
```html
|
||||
<!-- note that we use kebab-case for bindings in the template as usual -->
|
||||
<editable-field on-update="$ctrl.update('location', value)"></editable-field><br>
|
||||
<button ng-click="$ctrl.onDelete({hero: $ctrl.hero})">Delete</button>
|
||||
```
|
||||
- That way, the parent component can decide what to do with the event (e.g. delete an item or update the properties)
|
||||
```js
|
||||
ctrl.deleteHero(hero) {
|
||||
$http.delete(...).then(function() {
|
||||
var idx = ctrl.list.indexOf(hero);
|
||||
if (idx >= 0) {
|
||||
ctrl.list.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- **Components have a well-defined lifecycle**
|
||||
Each component can implement "lifecycle hooks". These are methods that will be called at certain points in the life
|
||||
of the component. The following hook methods can be implemented:
|
||||
|
||||
* `$onInit()` - Called on each controller after all the controllers on an element have been constructed and
|
||||
had their bindings initialized (and before the pre & post linking functions for the directives on
|
||||
this element). This is a good place to put initialization code for your controller.
|
||||
* `$onChanges(changesObj)` - Called whenever one-way bindings are updated. The `changesObj` is a hash whose keys
|
||||
are the names of the bound properties that have changed, and the values are an object of the form
|
||||
`{ currentValue, previousValue, isFirstChange() }`. Use this hook to trigger updates within a component such as
|
||||
cloning the bound value to prevent accidental mutation of the outer value.
|
||||
* `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing
|
||||
external resources, watches and event handlers.
|
||||
* `$postLink()` - Called after this controller's element and its children have been linked. Similar to the post-link
|
||||
function this hook can be used to set up DOM event handlers and do direct DOM manipulation.
|
||||
Note that child elements that contain `templateUrl` directives will not have been compiled and linked since
|
||||
they are waiting for their template to load asynchronously and their own compilation and linking has been
|
||||
suspended until that occurs.
|
||||
This hook can be considered analogous to the `ngAfterViewInit` and `ngAfterContentInit` hooks in Angular 2.
|
||||
Since the compilation process is rather different in Angular 1 there is no direct mapping and care should
|
||||
be taken when upgrading.
|
||||
|
||||
By implementing these methods, your component can hook into its lifecycle.
|
||||
|
||||
- **An application is a tree of components:**
|
||||
Ideally, the whole application should be a tree of components that implement clearly defined inputs
|
||||
and outputs, and minimize two-way data binding. That way, it's easier to predict when data changes and what the state
|
||||
of a component is.
|
||||
|
||||
## Example of a component tree
|
||||
|
||||
The following example expands on the simple component example and incorporates the concepts we introduced
|
||||
above:
|
||||
|
||||
Instead of an ngController, we now have a heroList component that holds the data of
|
||||
different heroes, and creates a heroDetail for each of them.
|
||||
|
||||
The heroDetail component now contains new functionality:
|
||||
- a delete button that calls the bound `onDelete` function of the heroList component
|
||||
- an input to change the hero location, in the form of a reusable editableField component. Instead
|
||||
of manipulating the hero object itself, it sends a changeset upwards to the heroDetail, which sends
|
||||
it upwards to the heroList component, which updates the original data.
|
||||
|
||||
<example name="heroComponentTree" module="heroApp">
|
||||
<file name="index.js">
|
||||
var mode = angular.module('heroApp', []);
|
||||
</file>
|
||||
|
||||
<file name="heroList.js">
|
||||
function HeroListController($scope, $element, $attrs) {
|
||||
var ctrl = this;
|
||||
|
||||
// This would be loaded by $http etc.
|
||||
ctrl.list = [
|
||||
{
|
||||
name: 'Superman',
|
||||
location: ''
|
||||
},
|
||||
{
|
||||
name: 'Batman',
|
||||
location: 'Wayne Manor'
|
||||
}
|
||||
];
|
||||
|
||||
ctrl.updateHero = function(hero, prop, value) {
|
||||
hero[prop] = value;
|
||||
};
|
||||
|
||||
ctrl.deleteHero = function(hero) {
|
||||
var idx = ctrl.list.indexOf(hero);
|
||||
if (idx >= 0) {
|
||||
ctrl.list.splice(idx, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
angular.module('heroApp').component('heroList', {
|
||||
templateUrl: 'heroList.html',
|
||||
controller: HeroListController
|
||||
});
|
||||
|
||||
</file>
|
||||
|
||||
<file name="heroDetail.js">
|
||||
function HeroDetailController() {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.delete = function() {
|
||||
ctrl.onDelete({hero: ctrl.hero});
|
||||
};
|
||||
|
||||
ctrl.update = function(prop, value) {
|
||||
ctrl.onUpdate({hero: ctrl.hero, prop: prop, value: value});
|
||||
};
|
||||
}
|
||||
|
||||
angular.module('heroApp').component('heroDetail', {
|
||||
templateUrl: 'heroDetail.html',
|
||||
controller: HeroDetailController,
|
||||
bindings: {
|
||||
hero: '<',
|
||||
onDelete: '&',
|
||||
onUpdate: '&'
|
||||
}
|
||||
});
|
||||
</file>
|
||||
|
||||
<file name="editableField.js">
|
||||
|
||||
function EditableFieldController($scope, $element, $attrs) {
|
||||
var ctrl = this;
|
||||
ctrl.editMode = false;
|
||||
|
||||
ctrl.handleModeChange = function() {
|
||||
if (ctrl.editMode) {
|
||||
ctrl.onUpdate({value: ctrl.fieldValue});
|
||||
ctrl.fieldValueCopy = ctrl.fieldValue;
|
||||
}
|
||||
ctrl.editMode = !ctrl.editMode;
|
||||
};
|
||||
|
||||
ctrl.reset = function() {
|
||||
ctrl.fieldValue = ctrl.fieldValueCopy;
|
||||
};
|
||||
|
||||
ctrl.$onInit = function() {
|
||||
// Make a copy of the initial value to be able to reset it later
|
||||
ctrl.fieldValueCopy = ctrl.fieldValue;
|
||||
|
||||
// Set a default fieldType
|
||||
if (!ctrl.fieldType) {
|
||||
ctrl.fieldType = 'text';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
angular.module('heroApp').component('editableField', {
|
||||
templateUrl: 'editableField.html',
|
||||
controller: EditableFieldController,
|
||||
bindings: {
|
||||
fieldValue: '<',
|
||||
fieldType: '@?',
|
||||
onUpdate: '&'
|
||||
}
|
||||
});
|
||||
</file>
|
||||
<file name="index.html">
|
||||
<hero-list></hero-list>
|
||||
</file>
|
||||
<file name="heroList.html">
|
||||
<b>Heroes</b><br>
|
||||
<hero-detail ng-repeat="hero in $ctrl.list" hero="hero" on-delete="$ctrl.deleteHero(hero)" on-update="$ctrl.updateHero(hero, prop, value)"></hero-detail>
|
||||
</file>
|
||||
<file name="heroDetail.html">
|
||||
<hr>
|
||||
<div>
|
||||
Name: {{$ctrl.hero.name}}<br>
|
||||
Location: <editable-field field-value="$ctrl.hero.location" field-type="text" on-update="$ctrl.update('location', value)"></editable-field><br>
|
||||
<button ng-click="$ctrl.delete()">Delete</button>
|
||||
</div>
|
||||
</file>
|
||||
<file name="editableField.html">
|
||||
<span ng-switch="$ctrl.editMode">
|
||||
<input ng-switch-when="true" type="{{$ctrl.fieldType}}" ng-model="$ctrl.fieldValue">
|
||||
<span ng-switch-default>{{$ctrl.fieldValue}}</span>
|
||||
</span>
|
||||
<button ng-click="$ctrl.handleModeChange()">{{$ctrl.editMode ? 'Save' : 'Edit'}}</button>
|
||||
<button ng-if="$ctrl.editMode" ng-click="$ctrl.reset()">Reset</button>
|
||||
</file>
|
||||
</example>
|
||||
|
||||
## Components as route templates
|
||||
Components are also useful as route templates (e.g. when using {@link ngRoute ngRoute}). In a component-based
|
||||
application, every view is a component:
|
||||
|
||||
```js
|
||||
var myMod = angular.module('myMod', ['ngRoute']);
|
||||
myMod.component('home', {
|
||||
template: '<h1>Home</h1><p>Hello, {{ $ctrl.user.name }} !</p>',
|
||||
controller: function() {
|
||||
this.user = {name: 'world'};
|
||||
}
|
||||
});
|
||||
myMod.config(function($routeProvider) {
|
||||
$routeProvider.when('/', {
|
||||
template: '<home></home>'
|
||||
});
|
||||
});
|
||||
```
|
||||
<br />
|
||||
When using {@link ngRoute.$routeProvider $routeProvider}, you can often avoid some
|
||||
boilerplate, by passing the resolved route dependencies directly to the component. Since 1.5,
|
||||
ngRoute automatically assigns the resolves to the route scope property `$resolve` (you can also
|
||||
configure the property name via `resolveAs`). When using components, you can take advantage of this and pass resolves
|
||||
directly into your component without creating an extra route controller:
|
||||
|
||||
```js
|
||||
var myMod = angular.module('myMod', ['ngRoute']);
|
||||
myMod.component('home', {
|
||||
template: '<h1>Home</h1><p>Hello, {{ $ctrl.user.name }} !</p>',
|
||||
bindings: {
|
||||
user: '<'
|
||||
}
|
||||
});
|
||||
myMod.config(function($routeProvider) {
|
||||
$routeProvider.when('/', {
|
||||
template: '<home user="$resolve.user"></home>',
|
||||
resolve: {
|
||||
user: function($http) { return $http.get('...'); }
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Intercomponent Communication
|
||||
|
||||
Directives can require the controllers of other directives to enable communication
|
||||
between each other. This can be achieved in a component by providing an
|
||||
object mapping for the `require` property. The object keys specify the property names under which
|
||||
the required controllers (object values) will be bound to the requiring component's controller.
|
||||
|
||||
<div class="alert alert-warning">
|
||||
Note that the required controllers will not be available during the instantiation of the controller,
|
||||
but they are guaranteed to be available just before the `$onInit` method is executed!
|
||||
</div>
|
||||
|
||||
Here is a tab pane example built from components:
|
||||
|
||||
<example module="docsTabsExample">
|
||||
<file name="script.js">
|
||||
angular.module('docsTabsExample', [])
|
||||
.component('myTabs', {
|
||||
transclude: true,
|
||||
controller: function() {
|
||||
var panes = this.panes = [];
|
||||
this.select = function(pane) {
|
||||
angular.forEach(panes, function(pane) {
|
||||
pane.selected = false;
|
||||
});
|
||||
pane.selected = true;
|
||||
};
|
||||
this.addPane = function(pane) {
|
||||
if (panes.length === 0) {
|
||||
this.select(pane);
|
||||
}
|
||||
panes.push(pane);
|
||||
};
|
||||
},
|
||||
templateUrl: 'my-tabs.html'
|
||||
})
|
||||
.component('myPane', {
|
||||
transclude: true,
|
||||
require: {
|
||||
tabsCtrl: '^myTabs'
|
||||
},
|
||||
bindings: {
|
||||
title: '@'
|
||||
},
|
||||
controller: function() {
|
||||
this.$onInit = function() {
|
||||
this.tabsCtrl.addPane(this);
|
||||
console.log(this);
|
||||
};
|
||||
},
|
||||
templateUrl: 'my-pane.html'
|
||||
});
|
||||
</file>
|
||||
<file name="index.html">
|
||||
<my-tabs>
|
||||
<my-pane title="Hello">
|
||||
<h4>Hello</h4>
|
||||
<p>Lorem ipsum dolor sit amet</p>
|
||||
</my-pane>
|
||||
<my-pane title="World">
|
||||
<h4>World</h4>
|
||||
<em>Mauris elementum elementum enim at suscipit.</em>
|
||||
<p><a href ng-click="i = i + 1">counter: {{i || 0}}</a></p>
|
||||
</my-pane>
|
||||
</my-tabs>
|
||||
</file>
|
||||
<file name="my-tabs.html">
|
||||
<div class="tabbable">
|
||||
<ul class="nav nav-tabs">
|
||||
<li ng-repeat="pane in $ctrl.panes" ng-class="{active:pane.selected}">
|
||||
<a href="" ng-click="$ctrl.select(pane)">{{pane.title}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" ng-transclude></div>
|
||||
</div>
|
||||
</file>
|
||||
<file name="my-pane.html">
|
||||
<div class="tab-pane" ng-show="$ctrl.selected" ng-transclude></div>
|
||||
</file>
|
||||
</example>
|
||||
|
||||
|
||||
# Unit-testing Component Controllers
|
||||
|
||||
The easiest way to unit-test a component controller is by using the
|
||||
{@link ngMock.$componentController $componentController} that is included in {@link ngMock}. The
|
||||
advantage of this method is that you do not have to create any DOM elements. The following example
|
||||
shows how to do this for the `heroDetail` component from above.
|
||||
|
||||
The examples use the [Jasmine](http://jasmine.github.io/) testing framework.
|
||||
|
||||
**Controller Test:**
|
||||
```js
|
||||
describe('component: heroDetail', function() {
|
||||
var $componentController;
|
||||
|
||||
beforeEach(module('heroApp'));
|
||||
beforeEach(inject(function(_$componentController_) {
|
||||
$componentController = _$componentController_;
|
||||
}));
|
||||
|
||||
it('should expose a `hero` object', function() {
|
||||
// Here we are passing actual bindings to the component
|
||||
var bindings = {hero: {name: 'Wolverine'}};
|
||||
var ctrl = $componentController('heroDetail', null, bindings);
|
||||
|
||||
expect(ctrl.hero).toBeDefined();
|
||||
expect(ctrl.hero.name).toBe('Wolverine');
|
||||
});
|
||||
|
||||
it('should call the `onDelete` binding, when deleting the hero', function() {
|
||||
var onDeleteSpy = jasmine.createSpy('onDelete');
|
||||
var bindings = {hero: {}, onDelete: onDeleteSpy};
|
||||
var ctrl = $componentController('heroDetail', null, bindings);
|
||||
|
||||
ctrl.delete();
|
||||
expect(onDeleteSpy).toHaveBeenCalledWith({hero: ctrl.hero});
|
||||
});
|
||||
|
||||
it('should call the `onUpdate` binding, when updating a property', function() {
|
||||
var onUpdateSpy = jasmine.createSpy('onUpdate');
|
||||
var bindings = {hero: {}, onUpdate: onUpdateSpy};
|
||||
var ctrl = $componentController('heroDetail', null, bindings);
|
||||
|
||||
ctrl.update('foo', 'bar');
|
||||
expect(onUpdateSpy).toHaveBeenCalledWith({
|
||||
hero: ctrl.hero,
|
||||
prop: 'foo',
|
||||
value: 'bar'
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
@@ -76,7 +76,7 @@ stores/updates the value of the input field into/from a variable.
|
||||
The second kind of new markup are the double curly braces `{{ expression | filter }}`:
|
||||
When the compiler encounters this markup, it will replace it with the evaluated value of the markup.
|
||||
An <a name="expression">{@link expression expression}</a> in a template is a JavaScript-like code snippet that allows
|
||||
to read and write variables. Note that those variables are not global variables.
|
||||
Angular to read and write variables. Note that those variables are not global variables.
|
||||
Just like variables in a JavaScript function live in a scope,
|
||||
Angular provides a <a name="scope">{@link scope scope}</a> for the variables accessible to expressions.
|
||||
The values that are stored in variables on the scope are referred to as the <a name="model"></a>*model*
|
||||
@@ -277,8 +277,8 @@ Now that Angular knows of all the parts of the application, it needs to create t
|
||||
In the previous section we saw that controllers are created using a factory function.
|
||||
For services there are multiple ways to define their factory
|
||||
(see the {@link services service guide}).
|
||||
In the example above, we are using a function that returns the `currencyConverter` function as the factory
|
||||
for the service.
|
||||
In the example above, we are using an anonymous function as the factory function for `currencyConverter` service.
|
||||
This function should return the `currencyConverter` service instance.
|
||||
|
||||
Back to the initial question: How does the `InvoiceController` get a reference to the `currencyConverter` function?
|
||||
In Angular, this is done by simply defining arguments on the constructor function. With this, the injector
|
||||
@@ -334,9 +334,9 @@ The following example shows how this is done with Angular:
|
||||
var refresh = function() {
|
||||
var url = YAHOO_FINANCE_URL_PATTERN.
|
||||
replace('PAIRS', 'USD' + currencies.join('","USD'));
|
||||
return $http.jsonp(url).success(function(data) {
|
||||
return $http.jsonp(url).then(function(response) {
|
||||
var newUsdToForeignRates = {};
|
||||
angular.forEach(data.query.results.rate, function(rate) {
|
||||
angular.forEach(response.data.query.results.rate, function(rate) {
|
||||
var currency = rate.id.substring(3,6);
|
||||
newUsdToForeignRates[currency] = window.parseFloat(rate.Rate);
|
||||
});
|
||||
@@ -348,8 +348,7 @@ The following example shows how this is done with Angular:
|
||||
|
||||
return {
|
||||
currencies: currencies,
|
||||
convert: convert,
|
||||
refresh: refresh
|
||||
convert: convert
|
||||
};
|
||||
}]);
|
||||
</file>
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
@ngdoc overview
|
||||
@name Decorators
|
||||
@sortOrder 345
|
||||
@description
|
||||
|
||||
# Decorators in AngularJS
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**NOTE:** This guide is targeted towards developers who are already familiar with AngularJS basics.
|
||||
If you're just getting started, we recommend the {@link tutorial/ tutorial} first.
|
||||
</div>
|
||||
|
||||
## What are decorators?
|
||||
|
||||
Decorators are a design pattern that is used to separate modification or *decoration* of a class without modifying the
|
||||
original source code. In Angular, decorators are functions that allow a service, directive or filter to be modified
|
||||
prior to its usage.
|
||||
|
||||
## How to use decorators
|
||||
|
||||
There are two ways to register decorators
|
||||
|
||||
- `$provide.decorator`, and
|
||||
- `module.decorator`
|
||||
|
||||
Each provide access to a `$delegate`, which is the instantiated service/directive/filter, prior to being passed to the
|
||||
service that required it.
|
||||
|
||||
### $provide.decorator
|
||||
|
||||
The {@link api/auto/service/$provide#decorator decorator function} allows access to a $delegate of the service once it
|
||||
has been instantiated. For example:
|
||||
|
||||
```js
|
||||
angular.module('myApp', [])
|
||||
|
||||
.config([ '$provide', function($provide) {
|
||||
|
||||
$provide.decorator('$log', [
|
||||
'$delegate',
|
||||
function $logDecorator($delegate) {
|
||||
|
||||
var originalWarn = $delegate.warn;
|
||||
$delegate.warn = function decoratedWarn(msg) {
|
||||
msg = 'Decorated Warn: ' + msg;
|
||||
originalWarn.apply($delegate, arguments);
|
||||
};
|
||||
|
||||
return $delegate;
|
||||
}
|
||||
]);
|
||||
}]);
|
||||
```
|
||||
|
||||
After the `$log` service has been instantiated the decorator is fired. The decorator function has a `$delegate` object
|
||||
injected to provide access to the service that matches the selector in the decorator. This `$delegate` will be the
|
||||
service you are decorating. The return value of the function *provided to the decorator* will take place of the service,
|
||||
directive, or filter being decorated.
|
||||
|
||||
<hr>
|
||||
|
||||
The `$delegate` may be either modified or completely replaced. Given a service `myService` with a method `someFn`, the
|
||||
following could all be viable solutions:
|
||||
|
||||
|
||||
#### Completely Replace the $delegate
|
||||
```js
|
||||
angular.module('myApp', [])
|
||||
|
||||
.config([ '$provide', function($provide) {
|
||||
|
||||
$provide.decorator('myService', [
|
||||
'$delegate',
|
||||
function myServiceDecorator($delegate) {
|
||||
|
||||
var myDecoratedService = {
|
||||
// new service object to replace myService
|
||||
};
|
||||
return myDecoratedService;
|
||||
}
|
||||
]);
|
||||
}]);
|
||||
```
|
||||
|
||||
#### Patch the $delegate
|
||||
```js
|
||||
angular.module('myApp', [])
|
||||
|
||||
.config([ '$provide', function($provide) {
|
||||
|
||||
$provide.decorator('myService', [
|
||||
'$delegate',
|
||||
function myServiceDecorator($delegate) {
|
||||
|
||||
var someFn = $delegate.someFn;
|
||||
|
||||
function aNewFn() {
|
||||
// new service function
|
||||
someFn.apply($delegate, arguments);
|
||||
}
|
||||
|
||||
$delegate.someFn = aNewFn;
|
||||
return $delegate;
|
||||
}
|
||||
]);
|
||||
}]);
|
||||
```
|
||||
|
||||
#### Augment the $delegate
|
||||
```js
|
||||
angular.module('myApp', [])
|
||||
|
||||
.config([ '$provide', function($provide) {
|
||||
|
||||
$provide.decorator('myService', [
|
||||
'$delegate',
|
||||
function myServiceDecorator($delegate) {
|
||||
|
||||
function helperFn() {
|
||||
// an additional fn to add to the service
|
||||
}
|
||||
|
||||
$delegate.aHelpfulAddition = helperFn;
|
||||
return $delegate;
|
||||
}
|
||||
]);
|
||||
}]);
|
||||
```
|
||||
|
||||
<div class="alert alert-info">
|
||||
Note that whatever is returned by the decorator function will replace that which is being decorated. For example, a
|
||||
missing return statement will wipe out the entire object being decorated.
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
Decorators have different rules for different services. This is because services are registered in different ways.
|
||||
Services are selected by name, however filters and directives are selected by appending `"Filter"` or `"Directive"` to
|
||||
the end of the name. The `$delegate` provided is dictated by the type of service.
|
||||
|
||||
| Service Type | Selector | $delegate |
|
||||
|--------------|-------------------------------|-----------------------------------------------------------------------|
|
||||
| Service | `serviceName` | The `object` or `function` returned by the service |
|
||||
| Directive | `directiveName + 'Directive'` | An `Array.<DirectiveObject>`<sub>{@link guide/decorators#drtvArray 1}</sub> |
|
||||
| Filter | `filterName + 'Filter'` | The `function` returned by the filter |
|
||||
|
||||
<small id="drtvArray">1. Multiple directives may be registered to the same selector/name</small>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**NOTE:** Developers should take care in how and why they are modifying the `$delegate` for the service. Not only
|
||||
should expectations for the consumer be kept, but some functionality (such as directive registration) does not take
|
||||
place after decoration, but during creation/registration of the original service. This means, for example, that
|
||||
an action such as pushing a directive object to a directive `$delegate` will likely result in unexpected behavior.
|
||||
|
||||
Furthermore, great care should be taken when decorating core services, directives, or filters as this may unexpectedly
|
||||
or adversely affect the functionality of the framework.
|
||||
</div>
|
||||
|
||||
### module.decorator
|
||||
|
||||
This {@link api/ng/type/angular.Module#decorator function} is the same as the `$provide.decorator` function except it is
|
||||
exposed through the module API. This allows you to separate your decorator patterns from your module config blocks. The
|
||||
main caveat here is that you will need to take note the order in which you create your decorators.
|
||||
|
||||
Unlike in the module config block (which allows configuration of services prior to their creation), the service must be
|
||||
registered prior to the decorator (see {@link guide/providers#provider-recipe Provider Recipe}). For example, the
|
||||
following would not work because you are attempting to decorate outside of the configuration phase and the service
|
||||
hasn't been created yet:
|
||||
|
||||
```js
|
||||
// will cause an error since 'someService' hasn't been registered
|
||||
angular.module('myApp').decorator('someService', ...);
|
||||
|
||||
angular.module('myApp').factory('someService', ...);
|
||||
```
|
||||
|
||||
## Example Applications
|
||||
|
||||
The following sections provide examples each of a service decorator, a directive decorator, and a filter decorator.
|
||||
|
||||
### Service Decorator Example
|
||||
|
||||
This example shows how we can replace the $log service with our own to display log messages.
|
||||
|
||||
<example module="myServiceDecorator" name="service-decorator">
|
||||
<file name="script.js">
|
||||
angular.module('myServiceDecorator', []).
|
||||
|
||||
controller('Ctrl', [
|
||||
'$scope',
|
||||
'$log',
|
||||
'$timeout',
|
||||
function($scope, $log, $timeout) {
|
||||
var types = ['error', 'warn', 'log', 'info' ,'debug'], i;
|
||||
|
||||
for (i = 0; i < types.length; i++) {
|
||||
$log[types[i]](types[i] + ': message ' + (i + 1));
|
||||
}
|
||||
|
||||
$timeout(function() {
|
||||
$log.info('info: message logged in timeout');
|
||||
});
|
||||
}
|
||||
]).
|
||||
|
||||
directive('myLog', [
|
||||
'$log',
|
||||
function($log) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: '<ul id="myLog"><li ng-repeat="l in myLog" class="{{l.type}}">{{l.message}}</li></ul>',
|
||||
scope: {},
|
||||
compile: function() {
|
||||
return function(scope) {
|
||||
scope.myLog = $log.stack;
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
]).
|
||||
|
||||
config([
|
||||
'$provide',
|
||||
function($provide) {
|
||||
|
||||
$provide.decorator('$log', [
|
||||
'$delegate',
|
||||
function logDecorator($delegate) {
|
||||
|
||||
var myLog = {
|
||||
warn: function(msg) {
|
||||
log(msg, 'warn');
|
||||
},
|
||||
error: function(msg) {
|
||||
log(msg, 'error');
|
||||
},
|
||||
info: function(msg) {
|
||||
log(msg, 'info');
|
||||
},
|
||||
debug: function(msg) {
|
||||
log(msg, 'debug');
|
||||
},
|
||||
log: function(msg) {
|
||||
log(msg, 'log');
|
||||
},
|
||||
stack: []
|
||||
};
|
||||
|
||||
function log(msg, type) {
|
||||
myLog.stack.push({ type: type, message: msg.toString() });
|
||||
if (console && console[type]) console[type](msg);
|
||||
}
|
||||
|
||||
return myLog;
|
||||
|
||||
}
|
||||
]);
|
||||
|
||||
}
|
||||
]);
|
||||
</file>
|
||||
|
||||
<file name="index.html">
|
||||
<div ng-controller="Ctrl">
|
||||
<h1>Logs</h1>
|
||||
<my-log></my-log>
|
||||
</div>
|
||||
</file>
|
||||
|
||||
<file name="style.css">
|
||||
li.warn { color: yellow; }
|
||||
li.error { color: red; }
|
||||
li.info { color: blue }
|
||||
li.log { color: black }
|
||||
li.debug { color: green }
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should display log messages in dom', function() {
|
||||
element.all(by.repeater('l in myLog')).count().then(function(count) {
|
||||
expect(count).toEqual(6);
|
||||
});
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
|
||||
### Directive Decorator Example
|
||||
|
||||
Failed interpolated expressions in `ng-href` attributes can easily go unnoticed. We can decorate `ngHref` to warn us of
|
||||
those conditions.
|
||||
|
||||
<example module="urlDecorator" name="directive-decorator">
|
||||
<file name="script.js">
|
||||
angular.module('urlDecorator', []).
|
||||
|
||||
controller('Ctrl', ['$scope', function ($scope) {
|
||||
$scope.id = 3;
|
||||
$scope.warnCount = 0; // for testing
|
||||
}]).
|
||||
|
||||
config(['$provide', function($provide) {
|
||||
|
||||
// matchExpressions looks for interpolation markup in the directive attribute, extracts the expressions
|
||||
// from that markup (if they exist) and returns an array of those expressions
|
||||
function matchExpressions(str) {
|
||||
var exps = str.match(/{{([^}]+)}}/g);
|
||||
|
||||
// if there isn't any, get out of here
|
||||
if (exps === null) return;
|
||||
|
||||
exps = exps.map(function(exp) {
|
||||
var prop = exp.match(/[^{}]+/);
|
||||
return prop === null ? null : prop[0];
|
||||
});
|
||||
|
||||
return exps;
|
||||
}
|
||||
|
||||
// remember: directives must be selected by appending 'Directive' to the directive selector
|
||||
$provide.decorator('ngHrefDirective', [
|
||||
'$delegate',
|
||||
'$log',
|
||||
'$parse',
|
||||
function($delegate, $log, $parse) {
|
||||
|
||||
// store the original link fn
|
||||
var originalLinkFn = $delegate[0].link;
|
||||
|
||||
// replace the compile fn
|
||||
$delegate[0].compile = function(tElem, tAttr) {
|
||||
|
||||
// store the original exp in the directive attribute for our warning message
|
||||
var originalExp = tAttr.ngHref;
|
||||
|
||||
// get the interpolated expressions
|
||||
var exps = matchExpressions(originalExp);
|
||||
|
||||
// create and store the getters using $parse
|
||||
var getters = exps.map(function(el) {
|
||||
if (el) return $parse(el);
|
||||
});
|
||||
|
||||
return function newLinkFn(scope, elem, attr) {
|
||||
// fire the originalLinkFn
|
||||
originalLinkFn.apply($delegate[0], arguments);
|
||||
|
||||
// observe the directive attr and check the expressions
|
||||
attr.$observe('ngHref', function(val) {
|
||||
|
||||
// if we have getters and getters is an array...
|
||||
if (getters && angular.isArray(getters)) {
|
||||
|
||||
// loop through the getters and process them
|
||||
angular.forEach(getters, function(g, idx) {
|
||||
|
||||
// if val is truthy, then the warning won't log
|
||||
var val = angular.isFunction(g) ? g(scope) : true;
|
||||
if (!val) {
|
||||
$log.warn('NgHref Warning: "' + exps[idx] + '" in the expression "' + originalExp +
|
||||
'" is falsy!');
|
||||
|
||||
scope.warnCount++; // for testing
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
// get rid of the old link function since we return a link function in compile
|
||||
delete $delegate[0].link;
|
||||
|
||||
// return the $delegate
|
||||
return $delegate;
|
||||
|
||||
}
|
||||
|
||||
]);
|
||||
|
||||
}]);
|
||||
</file>
|
||||
|
||||
<file name="index.html">
|
||||
<div ng-controller="Ctrl">
|
||||
<a ng-href="/products/{{ id }}/view" id="id3">View Product {{ id }}</a>
|
||||
- <strong>id == 3</strong>, so no warning<br>
|
||||
<a ng-href="/products/{{ id + 5 }}/view" id="id8">View Product {{ id + 5 }}</a>
|
||||
- <strong>id + 5 == 8</strong>, so no warning<br>
|
||||
<a ng-href="/products/{{ someOtherId }}/view" id="someOtherId">View Product {{ someOtherId }}</a>
|
||||
- <strong style="background-color: #ffff00;">someOtherId == undefined</strong>, so warn<br>
|
||||
<a ng-href="/products/{{ someOtherId + 5 }}/view" id="someOtherId5">View Product {{ someOtherId + 5 }}</a>
|
||||
- <strong>someOtherId + 5 == 5</strong>, so no warning<br>
|
||||
<div>Warn Count: {{ warnCount }}</div>
|
||||
</div>
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should warn when an expression in the interpolated value is falsy', function() {
|
||||
var id3 = element(by.id('id3'));
|
||||
var id8 = element(by.id('id8'));
|
||||
var someOther = element(by.id('someOtherId'));
|
||||
var someOther5 = element(by.id('someOtherId5'));
|
||||
|
||||
expect(id3.getText()).toEqual('View Product 3');
|
||||
expect(id3.getAttribute('href')).toContain('/products/3/view');
|
||||
|
||||
expect(id8.getText()).toEqual('View Product 8');
|
||||
expect(id8.getAttribute('href')).toContain('/products/8/view');
|
||||
|
||||
expect(someOther.getText()).toEqual('View Product');
|
||||
expect(someOther.getAttribute('href')).toContain('/products//view');
|
||||
|
||||
expect(someOther5.getText()).toEqual('View Product 5');
|
||||
expect(someOther5.getAttribute('href')).toContain('/products/5/view');
|
||||
|
||||
expect(element(by.binding('warnCount')).getText()).toEqual('Warn Count: 1');
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
|
||||
### Filter Decorator Example
|
||||
|
||||
Let's say we have created an app that uses the default format for many of our `Date` filters. Suddenly requirements have
|
||||
changed (that never happens) and we need all of our default dates to be `'shortDate'` instead of `'mediumDate'`.
|
||||
|
||||
<example module="filterDecorator" name="filter-decorator">
|
||||
<file name="script.js">
|
||||
angular.module('filterDecorator', []).
|
||||
|
||||
controller('Ctrl', ['$scope', function ($scope) {
|
||||
$scope.genesis = new Date(2010, 0, 5);
|
||||
$scope.ngConf = new Date(2016, 4, 4);
|
||||
}]).
|
||||
|
||||
config(['$provide', function($provide) {
|
||||
|
||||
$provide.decorator('dateFilter', [
|
||||
'$delegate',
|
||||
function dateDecorator($delegate) {
|
||||
|
||||
// store the original filter
|
||||
var originalFilter = $delegate;
|
||||
|
||||
// return our filter
|
||||
return shortDateDefault;
|
||||
|
||||
// shortDateDefault sets the format to shortDate if it is falsy
|
||||
function shortDateDefault(date, format, timezone) {
|
||||
if (!format) format = 'shortDate';
|
||||
|
||||
// return the result of the original filter
|
||||
return originalFilter(date, format, timezone);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
]);
|
||||
|
||||
}]);
|
||||
</file>
|
||||
|
||||
<file name="index.html">
|
||||
<div ng-controller="Ctrl">
|
||||
<div id="genesis">Initial Commit default to short date: {{ genesis | date }}</div>
|
||||
<div>ng-conf 2016 default short date: {{ ngConf | date }}</div>
|
||||
<div id="ngConf">ng-conf 2016 with full date format: {{ ngConf | date:'fullDate' }}</div>
|
||||
</div>
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should default date filter to short date format', function() {
|
||||
expect(element(by.id('genesis')).getText())
|
||||
.toMatch(/Initial Commit default to short date: \d{1,2}\/\d{1,2}\/\d{2}/);
|
||||
});
|
||||
|
||||
it('should still allow dates to be formatted', function() {
|
||||
expect(element(by.id('ngConf')).getText())
|
||||
.toMatch(/ng-conf 2016 with full date format\: [A-Za-z]+, [A-Za-z]+ \d{1,2}, \d{4}/);
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
@@ -43,8 +43,7 @@ mirrors the process of compiling source code in
|
||||
Before we can write a directive, we need to know how Angular's {@link guide/compiler HTML compiler}
|
||||
determines when to use a given directive.
|
||||
|
||||
Similar to the terminology used when an [element **matches** a selector]
|
||||
(https://developer.mozilla.org/en-US/docs/Web/API/Element.matches), we say an element **matches** a
|
||||
Similar to the terminology used when an [element **matches** a selector](https://developer.mozilla.org/en-US/docs/Web/API/Element.matches), we say an element **matches** a
|
||||
directive when the directive is part of its declaration.
|
||||
|
||||
In the following example, we say that the `<input>` element **matches** the `ngModel` directive
|
||||
@@ -100,8 +99,13 @@ For example, the following forms are all equivalent and match the {@link ngBind}
|
||||
</file>
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should show off bindings', function() {
|
||||
expect(element(by.css('div[ng-controller="Controller"] span[ng-bind]')).getText())
|
||||
.toBe('Max Karl Ernst Ludwig Planck (April 23, 1858 – October 4, 1947)');
|
||||
var containerElm = element(by.css('div[ng-controller="Controller"]'));
|
||||
var nameBindings = containerElm.all(by.binding('name'));
|
||||
|
||||
expect(nameBindings.count()).toBe(5);
|
||||
nameBindings.each(function(elem) {
|
||||
expect(elem.getText()).toEqual('Max Karl Ernst Ludwig Planck (April 23, 1858 – October 4, 1947)');
|
||||
});
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
@@ -142,63 +146,6 @@ directives when possible.
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
### Text and attribute bindings
|
||||
|
||||
During the compilation process the {@link ng.$compile compiler} matches text and attributes
|
||||
using the {@link ng.$interpolate $interpolate} service to see if they contain embedded
|
||||
expressions. These expressions are registered as {@link ng.$rootScope.Scope#$watch watches}
|
||||
and will update as part of normal {@link ng.$rootScope.Scope#$digest digest} cycle. An
|
||||
example of interpolation is shown below:
|
||||
|
||||
```html
|
||||
<a ng-href="img/{{username}}.jpg">Hello {{username}}!</a>
|
||||
```
|
||||
|
||||
|
||||
### `ngAttr` attribute bindings
|
||||
|
||||
Web browsers are sometimes picky about what values they consider valid for attributes.
|
||||
|
||||
For example, considering this template:
|
||||
|
||||
```html
|
||||
<svg>
|
||||
<circle cx="{{cx}}"></circle>
|
||||
</svg>
|
||||
```
|
||||
|
||||
We would expect Angular to be able to bind to this, but when we check the console we see
|
||||
something like `Error: Invalid value for attribute cx="{{cx}}"`. Because of the SVG DOM API's
|
||||
restrictions, you cannot simply write `cx="{{cx}}"`.
|
||||
|
||||
With `ng-attr-cx` you can work around this problem.
|
||||
|
||||
If an attribute with a binding is prefixed with the `ngAttr` prefix (denormalized as `ng-attr-`)
|
||||
then during the binding it will be applied to the corresponding unprefixed attribute. This allows
|
||||
you to bind to attributes that would otherwise be eagerly processed by browsers
|
||||
(e.g. an SVG element's `circle[cx]` attributes). When using `ngAttr`, the `allOrNothing` flag of
|
||||
{@link ng.$interpolate $interpolate} is used, so if any expression in the interpolated string
|
||||
results in `undefined`, the attribute is removed and not added to the element.
|
||||
|
||||
For example, we could fix the example above by instead writing:
|
||||
|
||||
```html
|
||||
<svg>
|
||||
<circle ng-attr-cx="{{cx}}"></circle>
|
||||
</svg>
|
||||
```
|
||||
|
||||
If one wants to modify a camelcased attribute (SVG elements have valid camelcased attributes), such as `viewBox` on the `svg` element, one can use underscores to denote that the attribute to bind to is naturally camelcased.
|
||||
|
||||
For example, to bind to `viewBox`, we can write:
|
||||
|
||||
```html
|
||||
<svg ng-attr-view_box="{{viewBox}}">
|
||||
</svg>
|
||||
```
|
||||
|
||||
|
||||
## Creating Directives
|
||||
|
||||
First let's talk about the {@link ng.$compileProvider#directive API for registering directives}. Much like
|
||||
@@ -215,10 +162,6 @@ initialization work here. The function is invoked using
|
||||
{@link auto.$injector#invoke $injector.invoke} which makes it injectable just like a
|
||||
controller.
|
||||
|
||||
<div class="alert alert-success">
|
||||
**Best Practice:** Prefer using the definition object over returning a function.
|
||||
</div>
|
||||
|
||||
|
||||
We'll go over a few common examples of directives, then dive deep into the different options
|
||||
and compilation process.
|
||||
@@ -356,6 +299,7 @@ The `restrict` option is typically set to:
|
||||
* `'A'` - only matches attribute name
|
||||
* `'E'` - only matches element name
|
||||
* `'C'` - only matches class name
|
||||
* `'M'` - only matches comment
|
||||
|
||||
These restrictions can all be combined as needed:
|
||||
|
||||
@@ -459,7 +403,7 @@ This is clearly not a great solution.
|
||||
|
||||
What we want to be able to do is separate the scope inside a directive from the scope
|
||||
outside, and then map the outer scope to a directive's inner scope. We can do this by creating what
|
||||
we call an **isolate scope**. To do this, we can use a directive's `scope` option:
|
||||
we call an **isolate scope**. To do this, we can use a {@link $compile#-scope- directive's `scope`} option:
|
||||
|
||||
<example module="docsIsolateScopeDirective">
|
||||
<file name="script.js">
|
||||
@@ -588,14 +532,24 @@ want to reuse throughout your app.
|
||||
In this example we will build a directive that displays the current time.
|
||||
Once a second, it updates the DOM to reflect the current time.
|
||||
|
||||
Directives that want to modify the DOM typically use the `link` option.
|
||||
`link` takes a function with the following signature, `function link(scope, element, attrs) { ... }`
|
||||
where:
|
||||
Directives that want to modify the DOM typically use the `link` option to register DOM listeners
|
||||
as well as update the DOM. It is executed after the template has been cloned and is where
|
||||
directive logic will be put.
|
||||
|
||||
`link` takes a function with the following signature,
|
||||
`function link(scope, element, attrs, controller, transcludeFn) { ... }`, where:
|
||||
|
||||
* `scope` is an Angular scope object.
|
||||
* `element` is the jqLite-wrapped element that this directive matches.
|
||||
* `attrs` is a hash object with key-value pairs of normalized attribute names and their
|
||||
corresponding attribute values.
|
||||
* `controller` is the directive's required controller instance(s) or its own controller (if any).
|
||||
The exact value depends on the directive's require property.
|
||||
* `transcludeFn` is a transclude linking function pre-bound to the correct transclusion scope.
|
||||
|
||||
<div class="alert alert-info">
|
||||
For more details on the `link` option refer to the {@link ng.$compile#-link- `$compile` API} page.
|
||||
</div>
|
||||
|
||||
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
|
||||
@@ -689,6 +643,7 @@ To do this, we need to use the `transclude` option.
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
scope: {},
|
||||
templateUrl: 'my-dialog.html'
|
||||
};
|
||||
});
|
||||
@@ -699,8 +654,7 @@ To do this, we need to use the `transclude` option.
|
||||
</div>
|
||||
</file>
|
||||
<file name="my-dialog.html">
|
||||
<div class="alert" ng-transclude>
|
||||
</div>
|
||||
<div class="alert" ng-transclude></div>
|
||||
</file>
|
||||
</example>
|
||||
|
||||
@@ -722,7 +676,7 @@ that redefines `name` as `Jeff`. What do you think the `{{name}}` binding will r
|
||||
transclude: true,
|
||||
scope: {},
|
||||
templateUrl: 'my-dialog.html',
|
||||
link: function (scope, element) {
|
||||
link: function (scope) {
|
||||
scope.name = 'Jeff';
|
||||
}
|
||||
};
|
||||
@@ -734,8 +688,7 @@ that redefines `name` as `Jeff`. What do you think the `{{name}}` binding will r
|
||||
</div>
|
||||
</file>
|
||||
<file name="my-dialog.html">
|
||||
<div class="alert" ng-transclude>
|
||||
</div>
|
||||
<div class="alert" ng-transclude></div>
|
||||
</file>
|
||||
</example>
|
||||
|
||||
@@ -746,7 +699,7 @@ The `transclude` option changes the way scopes are nested. It makes it so that t
|
||||
transcluded directive have whatever scope is outside the directive, rather than whatever scope is on
|
||||
the inside. In doing so, it gives the contents access to the outside scope.
|
||||
|
||||
Note that if the directive did not create its own scope, then `scope` in `scope.name = 'Jeff';` would
|
||||
Note that if the directive did not create its own scope, then `scope` in `scope.name = 'Jeff'` would
|
||||
reference the outside scope and we would see `Jeff` in the output.
|
||||
|
||||
This behavior makes sense for a directive that wraps some content, because otherwise you'd have to
|
||||
@@ -819,9 +772,9 @@ function.
|
||||
|
||||
Often it's desirable to pass data from the isolate scope via an expression to the
|
||||
parent scope, this can be done by passing a map of local variable names and values into the expression
|
||||
wrapper fn. For example, the hideDialog function takes a message to display when the dialog is hidden.
|
||||
This is specified in the directive by calling `close({message: 'closing for now'})`. Then the local
|
||||
variable `message` will be available within the `on-close` expression.
|
||||
wrapper function. For example, the `hideDialog` function takes a message to display when the dialog
|
||||
is hidden. This is specified in the directive by calling `close({message: 'closing for now'})`.
|
||||
Then the local variable `message` will be available within the `on-close` expression.
|
||||
|
||||
<div class="alert alert-success">
|
||||
**Best Practice:** use `&attr` in the `scope` option when you want your directive
|
||||
@@ -880,7 +833,7 @@ element?
|
||||
}]);
|
||||
</file>
|
||||
<file name="index.html">
|
||||
<span my-draggable>Drag ME</span>
|
||||
<span my-draggable>Drag Me</span>
|
||||
</file>
|
||||
</example>
|
||||
|
||||
@@ -903,7 +856,7 @@ to which tab is active.
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
scope: {},
|
||||
controller: function($scope) {
|
||||
controller: ['$scope', function($scope) {
|
||||
var panes = $scope.panes = [];
|
||||
|
||||
$scope.select = function(pane) {
|
||||
@@ -919,13 +872,13 @@ to which tab is active.
|
||||
}
|
||||
panes.push(pane);
|
||||
};
|
||||
},
|
||||
}],
|
||||
templateUrl: 'my-tabs.html'
|
||||
};
|
||||
})
|
||||
.directive('myPane', function() {
|
||||
return {
|
||||
require: '^myTabs',
|
||||
require: '^^myTabs',
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
scope: {
|
||||
@@ -941,11 +894,9 @@ to which tab is active.
|
||||
<file name="index.html">
|
||||
<my-tabs>
|
||||
<my-pane title="Hello">
|
||||
<h4>Hello</h4>
|
||||
<p>Lorem ipsum dolor sit amet</p>
|
||||
</my-pane>
|
||||
<my-pane title="World">
|
||||
<h4>World</h4>
|
||||
<em>Mauris elementum elementum enim at suscipit.</em>
|
||||
<p><a href ng-click="i = i + 1">counter: {{i || 0}}</a></p>
|
||||
</my-pane>
|
||||
@@ -962,22 +913,25 @@ to which tab is active.
|
||||
</div>
|
||||
</file>
|
||||
<file name="my-pane.html">
|
||||
<div class="tab-pane" ng-show="selected" ng-transclude>
|
||||
<div class="tab-pane" ng-show="selected">
|
||||
<h4>{{title}}</h4>
|
||||
<div ng-transclude></div>
|
||||
</div>
|
||||
</file>
|
||||
</example>
|
||||
|
||||
The `myPane` directive has a `require` option with value `^myTabs`. When a directive uses this
|
||||
option, `$compile` will throw an error unless the specified controller is found. The `^` prefix
|
||||
means that this directive searches for the controller on its parents (without the `^` prefix, the
|
||||
directive would look for the controller on just its own element).
|
||||
The `myPane` directive has a `require` option with value `^^myTabs`. When a directive uses this
|
||||
option, `$compile` will throw an error unless the specified controller is found. The `^^` prefix
|
||||
means that this directive searches for the controller on its parents. (A `^` prefix would make the
|
||||
directive look for the controller on its own element or its parents; without any prefix, the
|
||||
directive would look on its own element only.)
|
||||
|
||||
So where does this `myTabs` controller come from? Directives can specify controllers using
|
||||
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.
|
||||
If it is necessary to reference the controller or any functions bound to the controller from 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.
|
||||
|
||||
@@ -992,7 +946,7 @@ The corresponding parameter being sent to the `link` function will also be an ar
|
||||
angular.module('docsTabsExample', [])
|
||||
.directive('myPane', function() {
|
||||
return {
|
||||
require: ['^myTabs', '^ngModel'],
|
||||
require: ['^^myTabs', 'ngModel'],
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
scope: {
|
||||
@@ -1028,4 +982,3 @@ available in the {@link guide/compiler compiler guide}.
|
||||
|
||||
The {@link ng.$compile `$compile` API} page has a comprehensive list of directive options for
|
||||
reference.
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
|
||||
# Angular Expressions
|
||||
|
||||
Angular expressions are JavaScript-like code snippets that are usually placed in bindings such as
|
||||
`{{ expression }}`.
|
||||
Angular expressions are JavaScript-like code snippets that are mainly placed in
|
||||
interpolation bindings such as `<span title="{{ attrBinding }}">{{ textBinding }}</span>`,
|
||||
but also used directly in directive attributes such as `ng-click="functionExpression()"`.
|
||||
|
||||
For example, these are valid expressions in Angular:
|
||||
|
||||
@@ -26,6 +27,9 @@ Angular expressions are like JavaScript expressions with the following differenc
|
||||
* **Forgiving:** In JavaScript, trying to evaluate undefined properties generates `ReferenceError`
|
||||
or `TypeError`. In Angular, expression evaluation is forgiving to `undefined` and `null`.
|
||||
|
||||
* **Filters:** You can use {@link guide/filter filters} within expressions to format data before
|
||||
displaying it.
|
||||
|
||||
* **No Control Flow Statements:** You cannot use the following in an Angular expression:
|
||||
conditionals, loops, or exceptions.
|
||||
|
||||
@@ -35,10 +39,12 @@ Angular expressions are like JavaScript expressions with the following differenc
|
||||
* **No RegExp Creation With Literal Notation:** You cannot create regular expressions
|
||||
in an Angular expression.
|
||||
|
||||
* **No Comma And Void Operators:** You cannot use `,` or `void` in an Angular expression.
|
||||
* **No Object Creation With New Operator:** You cannot use `new` operator in an Angular expression.
|
||||
|
||||
* **No Bitwise, Comma, And Void Operators:** You cannot use
|
||||
[Bitwise](https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators),
|
||||
`,` or `void` operators in an Angular expression.
|
||||
|
||||
* **Filters:** You can use {@link guide/filter filters} within expressions to format data before
|
||||
displaying it.
|
||||
|
||||
If you want to run more complex JavaScript code, you should make it a controller method and call
|
||||
the method from your view. If you want to `eval()` an Angular expression yourself, use the
|
||||
@@ -113,6 +119,9 @@ This restriction is intentional. It prevents accidental access to the global sta
|
||||
Instead use services like `$window` and `$location` in functions called from expressions. Such services
|
||||
provide mockable access to globals.
|
||||
|
||||
It is possible to access the context object using the identifier `this` and the locals object using the
|
||||
identifier `$locals`.
|
||||
|
||||
<example module="expressionExample">
|
||||
<file name="index.html">
|
||||
<div class="example2" ng-controller="ExampleController">
|
||||
@@ -280,7 +289,7 @@ result is a non-undefined value (see value stabilization algorithm below).
|
||||
</example>
|
||||
|
||||
|
||||
### Why this feature
|
||||
### Reasons for using one-time binding
|
||||
|
||||
The main purpose of one-time binding expression is to provide a way to create a binding
|
||||
that gets deregistered and frees up resources once the binding is stabilized.
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
@ngdoc overview
|
||||
@name External Resources
|
||||
@sortOrder 205
|
||||
@description
|
||||
|
||||
# External Angular 1 Resources
|
||||
|
||||
This is a collection of external, 3rd party resources for learning and developing Angular.
|
||||
|
||||
## Articles, Videos, and Projects
|
||||
|
||||
### Introductory Material
|
||||
|
||||
* [10 Reasons Why You Should Use AngularJS](http://www.sitepoint.com/10-reasons-use-angularjs/)
|
||||
* [10 Reasons Why Developers Should Learn AngularJS](http://wintellect.com/blogs/jlikness/10-reasons-web-developers-should-learn-angularjs)
|
||||
* [Design Principles of AngularJS (video)](https://www.youtube.com/watch?v=HCR7i5F5L8c)
|
||||
* [Fundamentals in 60 Minutes (video)](http://www.youtube.com/watch?v=i9MHigUZKEM)
|
||||
* [For folks with a jQuery background](http://stackoverflow.com/questions/14994391/how-do-i-think-in-angularjs-if-i-have-a-jquery-background)
|
||||
|
||||
### Specific Topics
|
||||
|
||||
#### Application Structure & Style Guides
|
||||
|
||||
* [Angular Styleguide](https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md)
|
||||
* [Architecture, file structure, components, one-way dataflow and best practices](https://github.com/toddmotto/angular-styleguide)
|
||||
* [When to use directives, controllers or services](http://kirkbushell.me/when-to-use-directives-controllers-or-services-in-angular/)
|
||||
* [Service vs Factory](http://blog.thoughtram.io/angular/2015/07/07/service-vs-factory-once-and-for-all.html)
|
||||
|
||||
#### Testing
|
||||
|
||||
* **Unit testing:** [Using Karma (video)](http://www.youtube.com/watch?v=YG5DEzaQBIc), [Karma in Webstorm](http://blog.jetbrains.com/webstorm/2013/10/running-javascript-tests-with-karma-in-webstorm-7/)
|
||||
|
||||
#### Mobile
|
||||
|
||||
* [Angular on Mobile Guide](http://www.ng-newsletter.com/posts/angular-on-mobile.html)
|
||||
* [Angular and Cordova](http://devgirl.org/2013/06/10/quick-start-guide-phonegap-and-angularjs/)
|
||||
* [Ionic Framework](http://ionicframework.com/)
|
||||
|
||||
#### Deployment
|
||||
|
||||
##### General
|
||||
|
||||
* **Javascript minification: **[Background](http://thegreenpizza.github.io/2013/05/25/building-minification-safe-angular.js-applications/), [ng-annotate automation tool](https://github.com/olov/ng-annotate)
|
||||
* **Analytics and Logging:** [Angularyitcs (Google Analytics)](http://ngmodules.org/modules/angularytics), [Angulartics (Analytics)](https://github.com/luisfarzati/angulartics), [Logging Client-Side Errors](http://www.bennadel.com/blog/2542-Logging-Client-Side-Errors-With-AngularJS-And-Stacktrace-js.htm)
|
||||
* **SEO:** [By hand](http://www.yearofmoo.com/2012/11/angularjs-and-seo.html), [prerender.io](http://prerender.io/), [Brombone](http://www.brombone.com/), [SEO.js](http://getseojs.com/), [SEO4Ajax](http://www.seo4ajax.com/)
|
||||
|
||||
##### Server-Specific
|
||||
|
||||
* **Django:** [Tutorial](http://blog.mourafiq.com/post/55034504632/end-to-end-web-app-with-django-rest-framework), [Integrating AngularJS with Django](http://django-angular.readthedocs.org/en/latest/integration.html), [Getting Started with Django Rest Framework and AngularJS](http://blog.kevinastone.com/getting-started-with-django-rest-framework-and-angularjs.html)
|
||||
* **FireBase:** [AngularFire](http://angularfire.com/), [Firebase Foundations for AngularJS](http://blog.watchandcode.com/firebase-foundations/), [Realtime Apps with AngularJS and FireBase (video)](http://www.youtube.com/watch?v=C7ZI7z7qnHU)
|
||||
* **Google Cloud Platform: **[with Cloud Endpoints](https://cloud.google.com/developers/articles/angularjs-cloud-endpoints-recipe-for-building-modern-web-applications/), [with Go](https://github.com/GoogleCloudPlatform/appengine-angular-gotodos)
|
||||
* **Hood.ie:** [60 Minutes to Awesome](http://www.roberthorvick.com/2013/06/30/todomvc-angularjs-hood-ie-60-minutes-to-awesome/)
|
||||
* **MEAN Stack: **[Blog post](http://blog.mongodb.org/post/49262866911/the-mean-stack-mongodb-expressjs-angularjs-and), [Setup](http://thecodebarbarian.wordpress.com/2013/07/22/introduction-to-the-mean-stack-part-one-setting-up-your-tools/), [GDL Video](https://developers.google.com/live/shows/913996610)
|
||||
* **Rails: **[Tutorial](http://coderberry.me/blog/2013/04/22/angularjs-on-rails-4-part-1/), [AngularJS with Rails4](https://shellycloud.com/blog/2013/10/how-to-integrate-angularjs-with-rails-4), [angularjs-rails](https://github.com/hiravgandhi/angularjs-rails)
|
||||
* **PHP: **[Building a RESTful web service](http://blog.brunoscopelliti.com/building-a-restful-web-service-with-angularjs-and-php-more-power-with-resource), [End to End with Laravel 4 (video)](http://www.youtube.com/watch?v=hqAyiqUs93c)
|
||||
* **Meteor: **[angular-meteor package](https://github.com/Urigo/angular-meteor)
|
||||
|
||||
### Other Languages
|
||||
* [ES6, Webpack, and JSPM Starter Project](https://github.com/AngularClass/NG6-starter)
|
||||
* [ES6/Typescript Best Practices](https://codepen.io/martinmcwhorter/post/angularjs-1-x-with-typescript-or-es6-best-practices)
|
||||
* [Dart](https://github.com/angular/angular.dart.tutorial/wiki)
|
||||
* [CoffeeScript Tutorial](http://www.coffeescriptlove.com/2013/08/angularjs-and-coffeescript-tutorials.html)
|
||||
|
||||
### More Topics
|
||||
|
||||
* **Security:** [video](https://www.youtube.com/watch?v=18ifoT-Id54)
|
||||
* **Internationalization and Localization:** [Creating multilingual support](http://www.novanet.no/blog/hallstein-brotan/dates/2013/10/creating-multilingual-support-using-angularjs/)
|
||||
* **Authentication/Login: **[Google example](https://developers.google.com/+/photohunt/python), [AngularJS Facebook library](https://github.com/pc035860/angular-easyfb), [Facebook example](http://blog.brunoscopelliti.com/facebook-authentication-in-your-angularjs-web-app), [authentication strategy](http://blog.brunoscopelliti.com/deal-with-users-authentication-in-an-angularjs-web-app), [unix-style authorization](http://frederiknakstad.com/authentication-in-single-page-applications-with-angular-js/)
|
||||
* **Visualization:** [SVG](http://gaslight.co/blog/angular-backed-svgs), [D3.js](http://www.ng-newsletter.com/posts/d3-on-angular.html)
|
||||
* **Realtime Communication: **[Socket.io](http://www.creativebloq.com/javascript/angularjs-collaboration-board-socketio-2132885), [OmniBinder](https://github.com/jeffbcross/omnibinder)
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
* **Getting Started:** [Comparison of the options for starting a new project](http://www.dancancro.com/comparison-of-angularjs-application-starters/)
|
||||
* **Debugging:** [Batarang](https://chrome.google.com/webstore/detail/angularjs-batarang/ighdmehidhipcmcojjgiloacoafjmpfk?hl=en)
|
||||
* **Editor support:** [Webstorm](http://plugins.jetbrains.com/plugin/6971) (and [video](http://www.youtube.com/watch?v=LJOyrSh1kDU)), [Sublime Text](https://github.com/angular-ui/AngularJS-sublime-package), [Visual Studio](http://madskristensen.net/post/angularjs-intellisense-in-visual-studio-2012)
|
||||
* **Workflow:** [Yeoman.io](https://github.com/yeoman/generator-angular) and [Angular Yeoman Tutorial](http://www.sitepoint.com/kickstart-your-angularjs-development-with-yeoman-grunt-and-bower/)
|
||||
|
||||
## Complementary Libraries
|
||||
|
||||
This is a list of libraries that enhance Angular, add common UI components or integrate with other libraries.
|
||||
You can find a larger list of Angular external libraries at [ngmodules.org](http://ngmodules.org/).
|
||||
|
||||
* **Advanced Routing:** [UI-Router](https://github.com/angular-ui/ui-router)
|
||||
* **Authentication:** [Http Auth Interceptor](https://github.com/witoldsz/angular-http-auth)
|
||||
* **Internationalization:**
|
||||
- [angular-translate](http://angular-translate.github.io)
|
||||
- [angular-gettext](http://angular-gettext.rocketeer.be/)
|
||||
- [angular-localization](http://doshprompt.github.io/angular-localization/)
|
||||
* **RESTful services:** [Restangular](https://github.com/mgonto/restangular)
|
||||
* **SQL and NoSQL backends:**
|
||||
- [BreezeJS](http://www.breezejs.com/)
|
||||
- [AngularFire](http://angularfire.com/)
|
||||
* **Data Handling**
|
||||
- Local Storage and session: [ngStorage](https://github.com/gsklee/ngStorage)
|
||||
- [angular-cache](https://github.com/jmdobry/angular-cache)
|
||||
- Data Modeling [JS-Data-Angular](https://github.com/js-data/js-data-angular)
|
||||
* **Fileupload:**
|
||||
- [ng-file-upload](https://github.com/danialfarid/ng-file-upload)
|
||||
- [blueimp-fileupload for Angular](https://blueimp.github.io/jQuery-File-Upload/angularjs.html)
|
||||
* **General UI Libraries:**
|
||||
- [Angular Material](https://material.angularjs.org/latest/)
|
||||
- [Angular UI Bootstrap](http://angular-ui.github.io/)
|
||||
- [AngularStrap for Bootstrap 3](http://mgcrea.github.io/angular-strap/)
|
||||
- [KendoUI](http://kendo-labs.github.io/angular-kendo/#/)
|
||||
- [Wijmo](http://wijmo.com/tag/angularjs-2/)
|
||||
* **Specific UI Elements:**
|
||||
- [ngInfiniteScroll](https://sroze.github.io/ngInfiniteScroll/)
|
||||
- [ngTable](https://github.com/esvit/ng-table)
|
||||
- [Angular UI Grid](http://angular-ui.github.io/grid)
|
||||
- [Toaster Notifications](https://github.com/jirikavi/AngularJS-Toaster)
|
||||
- [textAngular Rich Text Editor / contenteditable](http://textangular.com/) (Rich Text Editor /
|
||||
binding to contenteditable)
|
||||
- [Angular UI Map (Google Maps)](https://github.com/angular-ui/ui-map)
|
||||
|
||||
## General Learning Resources
|
||||
|
||||
### Books
|
||||
* [AngularJS: Up and Running](http://www.amazon.com/AngularJS-Running-Enhanced-Productivity-Structured/dp/1491901942) by Brad Green and Shyam Seshadri
|
||||
* [Mastering Web App Development](http://www.amazon.com/Mastering-Web-Application-Development-AngularJS/dp/1782161821) by Pawel Kozlowski and Pete Bacon Darwin
|
||||
* [AngularJS Directives](http://www.amazon.com/AngularJS-Directives-Alex-Vanston/dp/1783280336) by Alex Vanston
|
||||
* [Recipes With AngularJS](http://www.amazon.co.uk/Recipes-Angular-js-Frederik-Dietz-ebook/dp/B00DK95V48) by Frederik Dietz
|
||||
* [Developing an AngularJS Edge](http://www.amazon.com/Developing-AngularJS-Edge-Christopher-Hiller-ebook/dp/B00CJLFF8K) by Christopher Hiller
|
||||
* [ng-book: The Complete Book on AngularJS](http://ng-book.com/) by Ari Lerner
|
||||
* [AngularJS : Novice to Ninja](http://www.amazon.in/AngularJS-Novice-Ninja-Sandeep-Panda/dp/0992279453) by Sandeep Panda
|
||||
* [AngularJS UI Development](http://www.amazon.com/AngularJS-UI-Development-Amit-Ghart-ebook/dp/B00OXVAK7A) by Amit Gharat and Matthias Nehlsen
|
||||
* [Responsive Web Design with AngularJS](http://www.amazon.com/Responsive-Design-AngularJS-Sandeep-Kumar/dp/178439842X) by Sandeep Kumar Patel
|
||||
* [Professional AngularJS](http://www.amazon.com/Professional-AngularJS-Valeri-Karpov/dp/1118832078/)
|
||||
|
||||
### Videos:
|
||||
* [egghead.io](http://egghead.io/)
|
||||
|
||||
### Courses
|
||||
* **Free online:**
|
||||
[thinkster.io](http://thinkster.io),
|
||||
[CodeAcademy](http://www.codecademy.com/courses/javascript-advanced-en-2hJ3J/0/1),
|
||||
[CodeSchool](https://www.codeschool.com/courses/shaping-up-with-angular-js)
|
||||
* **Paid online:**
|
||||
[The Angular Course (115 videos that show you how to build a full app)](http://watchandcode.com/courses/angular-course/),
|
||||
[Pluralsite (3 courses)](http://www.pluralsight.com/training/Courses/Find?highlight=true&searchTerm=angularjs),
|
||||
[Tuts+](https://tutsplus.com/course/easier-js-apps-with-angular/),
|
||||
[lynda.com](http://www.lynda.com/AngularJS-tutorials/Up-Running-AngularJS/133318-2.html),
|
||||
[WintellectNOW (4 lessons)](http://www.wintellectnow.com/Course/Detail/mastering-angularjs)
|
||||
* **Paid onsite:**
|
||||
[angularbootcamp.com](http://angularbootcamp.com/)
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
@sortOrder 280
|
||||
@description
|
||||
|
||||
A filter formats the value of an expression for display to the user. They can be used in view templates,
|
||||
controllers or services and it is easy to define your own filter.
|
||||
# Filters
|
||||
|
||||
The underlying API is the {@link ng.$filterProvider `filterProvider`}.
|
||||
Filters format the value of an expression for display to the user. They can be used in view
|
||||
templates, controllers or services. Angular comes with a collection of
|
||||
[built-in filters](api/ng/filter), but it is easy to define your own as well.
|
||||
|
||||
The underlying API is the {@link ng.$filterProvider}.
|
||||
|
||||
## Using filters in view templates
|
||||
|
||||
@@ -29,13 +32,31 @@ Filters may have arguments. The syntax for this is
|
||||
E.g. the markup `{{ 1234 | number:2 }}` formats the number 1234 with 2 decimal points using the
|
||||
{@link ng.filter:number `number`} filter. The resulting value is `1,234.00`.
|
||||
|
||||
### When filters are executed
|
||||
|
||||
In templates, filters are only executed when their inputs have changed. This is more performant than executing
|
||||
a filter on each {@link ng.$rootScope.Scope#$digest `$digest`} as is the case with {@link guide/expression expressions}.
|
||||
|
||||
There are two exceptions to this rule:
|
||||
|
||||
1. In general, this applies only to filters that take [primitive values](https://developer.mozilla.org/docs/Glossary/Primitive)
|
||||
as inputs. Filters that receive [Objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Objects)
|
||||
as input are executed on each `$digest`, as it would be too costly to track if the inputs have changed.
|
||||
|
||||
2. Filters that are marked as `$stateful` are also executed on each $digest.
|
||||
See {@link guide/filter#stateful-filters Stateful filters} for more information. Note that no Angular
|
||||
core filters are $stateful.
|
||||
|
||||
|
||||
## Using filters in controllers, services, and directives
|
||||
|
||||
You can also use filters in controllers, services, and directives. For this, inject a dependency
|
||||
with the name `<filterName>Filter` to your controller/service/directive. E.g. using the dependency
|
||||
`numberFilter` will inject the number filter. The injected argument is a function that takes the
|
||||
value to format as first argument and filter parameters starting with the second argument.
|
||||
You can also use filters in controllers, services, and directives.
|
||||
|
||||
<div class="alert alert-info">
|
||||
For this, inject a dependency with the name `<filterName>Filter` into your controller/service/directive.
|
||||
E.g. a filter called `number` is injected by using the dependency `numberFilter`. The injected argument
|
||||
is a function that takes the value to format as first argument, and filter parameters starting with the second argument.
|
||||
</div>
|
||||
|
||||
The example below uses the filter called {@link ng.filter:filter `filter`}.
|
||||
This filter reduces arrays into sub arrays based on
|
||||
@@ -88,8 +109,9 @@ as the first argument. Any filter arguments are passed in as additional argument
|
||||
function.
|
||||
|
||||
The filter function should be a [pure function](http://en.wikipedia.org/wiki/Pure_function), which
|
||||
means that it should be stateless and idempotent. Angular relies on these properties and executes
|
||||
the filter only when the inputs to the function change.
|
||||
means that it should be stateless and idempotent, and not rely for example on other Angular services.
|
||||
Angular relies on this contract and will by default execute a filter only when the inputs to the function change.
|
||||
{@link guide/filter#stateful-filters Stateful filters} are possible, but less performant.
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`.
|
||||
@@ -108,6 +130,7 @@ text upper-case.
|
||||
No filter: {{greeting}}<br>
|
||||
Reverse: {{greeting|reverse}}<br>
|
||||
Reverse + uppercase: {{greeting|reverse:true}}<br>
|
||||
Reverse, filtered in controller: {{filteredGreeting}}<br>
|
||||
</div>
|
||||
</file>
|
||||
|
||||
@@ -127,14 +150,15 @@ text upper-case.
|
||||
return out;
|
||||
};
|
||||
})
|
||||
.controller('MyController', ['$scope', function($scope) {
|
||||
.controller('MyController', ['$scope', 'reverseFilter', function($scope, reverseFilter) {
|
||||
$scope.greeting = 'hello';
|
||||
$scope.filteredGreeting = reverseFilter($scope.greeting);
|
||||
}]);
|
||||
</file>
|
||||
</example>
|
||||
|
||||
|
||||
## Stateful filters
|
||||
### Stateful filters
|
||||
|
||||
It is strongly discouraged to write filters that are stateful, because the execution of those can't
|
||||
be optimized by Angular, which often leads to performance issues. Many stateful filters can be
|
||||
@@ -176,4 +200,4 @@ means that it will be executed one or more times during the each `$digest` cycle
|
||||
|
||||
## Testing custom filters
|
||||
|
||||
See the [phonecat tutorial](http://docs.angularjs.org/tutorial/step_09#test) for an example.
|
||||
See the [phonecat tutorial](http://docs.angularjs.org/tutorial/step_11#testing) for an example.
|
||||
|
||||
@@ -383,7 +383,7 @@ In the following example we create two directives:
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, elm, attrs, ctrl) {
|
||||
var usernames = ['Jim', 'John', 'Jill', 'Jackie'];
|
||||
var usernames = ['Jim', 'John', 'Jill', 'Jackie'];
|
||||
|
||||
ctrl.$asyncValidators.username = function(modelValue, viewValue) {
|
||||
|
||||
@@ -440,8 +440,7 @@ Note that you can alternatively use `ng-pattern` to further restrict the validat
|
||||
var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@example\.com$/i;
|
||||
|
||||
return {
|
||||
require: 'ngModel',
|
||||
restrict: '',
|
||||
require: '?ngModel',
|
||||
link: function(scope, elm, attrs, ctrl) {
|
||||
// only apply the validator if ngModel is present and Angular has added the email validator
|
||||
if (ctrl && ctrl.$validators.email) {
|
||||
|
||||
@@ -28,4 +28,14 @@ browsers, but it is up to you to test and decide whether it works for your parti
|
||||
To ensure your Angular application works on IE please consider:
|
||||
|
||||
1. Use `ng-style` tags instead of `style="{{ someCss }}"`. The latter works in Chrome and Firefox
|
||||
but does not work in Internet Explorer <= 11 (the most recent version at time of writing).
|
||||
but does not work in Internet Explorer <= 11 (the most recent version at time of writing).
|
||||
2. For the `type` attribute of buttons, use `ng-attr-type` tags instead of
|
||||
`type="{{ someExpression }}"`. If using the latter, Internet Explorer overwrites the expression
|
||||
with `type="submit"` before Angular has a chance to interpolate it.
|
||||
3. For the `value` attribute of progress, use `ng-attr-value` tags instead of
|
||||
`value="{{ someExpression}}"`. If using the latter, Internet Explorer overwrites the expression
|
||||
with `value="0"` before Angular has a chance to interpolate it.
|
||||
4. For the `placeholder` attribute of textarea, use `ng-attr-placeholder` tags instead
|
||||
of `placeholder="{{ someExpression }}"`. If using the latter, Internet Explorer will error
|
||||
on accessing the `nodeValue` on a parentless `TextNode` in Internet Explorer 10 & 11
|
||||
(see [issue 5025](https://github.com/angular/angular.js/issues/5025)).
|
||||
|
||||
+40
-104
@@ -2,22 +2,21 @@
|
||||
@name Developer Guide
|
||||
@description
|
||||
|
||||
# Guide to AngularJS Documentation
|
||||
# Guide to Angular 1 Documentation
|
||||
|
||||
Everything you need to know about AngularJS
|
||||
On this page, you will find a list of official Angular resources on various topics.
|
||||
|
||||
* {@link guide/introduction What is AngularJS?}
|
||||
Just starting out with Angular 1? Try working through our step by step tutorial or try
|
||||
building on our seed project.
|
||||
|
||||
* {@link tutorial/index Official Angular 1 Tutorial}
|
||||
* [Angular Seed](https://github.com/angular/angular-seed)
|
||||
|
||||
Ready to find out more about Angular 1?
|
||||
|
||||
* {@link guide/introduction What is Angular 1?}
|
||||
* {@link guide/concepts Conceptual Overview}
|
||||
|
||||
## Tutorials
|
||||
|
||||
* {@link tutorial/index Official AngularJS Tutorial}
|
||||
* [10 Reasons Why You Should Use AngularJS](http://www.sitepoint.com/10-reasons-use-angularjs/)
|
||||
* [10 Reasons Why Developers Should Learn AngularJS](http://wintellect.com/blogs/jlikness/10-reasons-web-developers-should-learn-angularjs)
|
||||
* [Design Principles of AngularJS (video)](https://www.youtube.com/watch?v=HCR7i5F5L8c)
|
||||
* [Fundamentals in 60 Minutes (video)](http://www.youtube.com/watch?v=i9MHigUZKEM)
|
||||
* [For folks with a jQuery background](http://stackoverflow.com/questions/14994391/how-do-i-think-in-angularjs-if-i-have-a-jquery-background)
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Templates
|
||||
@@ -26,131 +25,68 @@ In Angular applications, you move the job of filling page templates with data fr
|
||||
|
||||
* {@link guide/databinding Data binding}
|
||||
* {@link guide/expression Expressions}
|
||||
* {@link guide/interpolation Interpolation}
|
||||
* {@link guide/directive Directives}
|
||||
* {@link ngRoute.$route Views and routes (see the example)}
|
||||
* {@link guide/filter Filters}
|
||||
* {@link guide/forms Forms} and [Concepts of AngularJS Forms](http://mrbool.com/the-concepts-of-angularjs-forms/29117)
|
||||
* {@link guide/compiler HTML compiler}
|
||||
* {@link guide/forms Forms}
|
||||
|
||||
### Application Structure
|
||||
|
||||
* **Blog post: **[When to use directives, controllers or services](http://kirkbushell.me/when-to-use-directives-controllers-or-services-in-angular/)
|
||||
* **App wiring:** {@link guide/di Dependency injection}
|
||||
* **Exposing model to templates:** {@link guide/scope Scopes}
|
||||
* **Bootstrap:** {@link guide/bootstrap Bootstrapping an app}
|
||||
* **Communicating with servers:** {@link ng.$http $http}, {@link ngResource.$resource $resource}
|
||||
|
||||
### Other AngularJS Features
|
||||
### Other Features
|
||||
|
||||
* **Animation:** {@link guide/animations Core concepts}, {@link ngAnimate ngAnimate API}, and [Animation in AngularJS 1.2](http://www.yearofmoo.com/2013/08/remastered-animation-in-angularjs-1-2.html)
|
||||
* **Animation:** {@link guide/animations Core concepts}, {@link ngAnimate ngAnimate API}
|
||||
* **Security:** {@link guide/security Security Docs}, {@link ng.$sce Strict Contextual Escaping}, {@link ng.directive:ngCsp Content Security Policy}, {@link ngSanitize.$sanitize $sanitize}, [video](https://www.youtube.com/watch?v=18ifoT-Id54)
|
||||
* **Internationalization and Localization:** {@link guide/i18n Angular Guide to i18n and l10n}, {@link ng.filter:date date filter}, {@link ng.filter:currency currency filter}, [Creating multilingual support](http://www.novanet.no/blog/hallstein-brotan/dates/2013/10/creating-multilingual-support-using-angularjs/)
|
||||
* **Mobile:** {@link ngTouch Touch events}
|
||||
* **Touch events:** {@link ngTouch Touch events}
|
||||
* **Accessibility:** {@link guide/accessibility ngAria}
|
||||
|
||||
### Testing
|
||||
|
||||
* **Unit testing:** [Using Karma (video)](http://www.youtube.com/watch?v=YG5DEzaQBIc), {@link guide/unit-testing Unit testing}, {@link guide/services#unit-testing Testing services}, [Karma in Webstorm](http://blog.jetbrains.com/webstorm/2013/10/running-javascript-tests-with-karma-in-webstorm-7/)
|
||||
* **Scenario testing:** [Protractor](https://github.com/angular/protractor)
|
||||
* **Unit testing:** [Karma](http://karma-runner.github.io), {@link guide/unit-testing Unit testing}, {@link guide/services#unit-testing Testing services},
|
||||
* **End-to-End Testing:** [Protractor](https://github.com/angular/protractor), {@link guide/e2e-testing e2e testing guide}
|
||||
|
||||
## Specific Topics
|
||||
|
||||
* **Login: **[Google example](https://developers.google.com/+/photohunt/python), [AngularJS Facebook library](https://github.com/pc035860/angular-easyfb), [Facebook example](http://blog.brunoscopelliti.com/facebook-authentication-in-your-angularjs-web-app), [authentication strategy](http://blog.brunoscopelliti.com/deal-with-users-authentication-in-an-angularjs-web-app), [unix-style authorization](http://frederiknakstad.com/authentication-in-single-page-applications-with-angular-js/)
|
||||
* **Mobile:** [Angular on Mobile Guide](http://www.ng-newsletter.com/posts/angular-on-mobile.html), [PhoneGap](http://devgirl.org/2013/06/10/quick-start-guide-phonegap-and-angularjs/)
|
||||
* **Other Languages:** [CoffeeScript](http://www.coffeescriptlove.com/2013/08/angularjs-and-coffeescript-tutorials.html), [Dart](https://github.com/angular/angular.dart.tutorial/wiki)
|
||||
* **Realtime: **[Socket.io](http://www.creativebloq.com/javascript/angularjs-collaboration-board-socketio-2132885), [OmniBinder](https://github.com/jeffbcross/omnibinder)
|
||||
* **Visualization:** [SVG](http://gaslight.co/blog/angular-backed-svgs), [D3.js](http://www.ng-newsletter.com/posts/d3-on-angular.html)
|
||||
* **Local Storage and session:** [ngStorage](https://github.com/gsklee/ngStorage)
|
||||
## Community Resources
|
||||
|
||||
## Tools
|
||||
We have set up a guide to many resources provided by the community, where you can find lots
|
||||
of additional information and material on these topics, a list of complimentary libraries, and much more.
|
||||
|
||||
* **Getting Started:** [Comparison of the options for starting a new project](http://www.dancancro.com/comparison-of-angularjs-application-starters/)
|
||||
* **Debugging:** [Batarang](https://chrome.google.com/webstore/detail/angularjs-batarang/ighdmehidhipcmcojjgiloacoafjmpfk?hl=en)
|
||||
* **Testing:** [Karma](http://karma-runner.github.io), [Protractor](https://github.com/angular/protractor)
|
||||
* **Editor support:** [Webstorm](http://plugins.jetbrains.com/plugin/6971) (and [video](http://www.youtube.com/watch?v=LJOyrSh1kDU)), [Sublime Text](https://github.com/angular-ui/AngularJS-sublime-package), [Visual Studio](http://madskristensen.net/post/angularjs-intellisense-in-visual-studio-2012)
|
||||
* **Workflow:** [Yeoman.io](https://github.com/yeoman/generator-angular) and [Angular Yeoman Tutorial](http://www.sitepoint.com/kickstart-your-angularjs-development-with-yeoman-grunt-and-bower/)
|
||||
* {@link guide/external-resources External Angular 1 resources}
|
||||
|
||||
## Complementary Libraries
|
||||
|
||||
This is a short list of libraries with specific support and documentation for working with Angular. You can find a full list of all known Angular external libraries at [ngmodules.org](http://ngmodules.org/).
|
||||
|
||||
* **Internationalization:** [angular-translate](http://angular-translate.github.io), [angular-gettext](http://angular-gettext.rocketeer.be/), [angular-localization](http://doshprompt.github.io/angular-localization/)
|
||||
* **RESTful services:** [Restangular](https://github.com/mgonto/restangular)
|
||||
* **SQL and NoSQL backends:** [BreezeJS](http://www.breezejs.com/), [AngularFire](http://angularfire.com/)
|
||||
* **UI Widgets: **[KendoUI](http://kendo-labs.github.io/angular-kendo/#/), [UI Bootstrap](http://angular-ui.github.io/bootstrap/), [Wijmo](http://wijmo.com/tag/angularjs-2/), [ngTagsInput](https://github.com/mbenford/ngTagsInput)
|
||||
* **Advanced Routing:** [UI-Router](https://github.com/angular-ui/ui-router)
|
||||
* **Maps:** [UI-Map (Google Maps)](https://github.com/angular-ui/ui-map)
|
||||
|
||||
## Deployment
|
||||
|
||||
### General
|
||||
|
||||
* **Docs Page:** {@link guide/production Running an AngularJS App in Production}
|
||||
* **Javascript minification: **[Background](http://thegreenpizza.github.io/2013/05/25/building-minification-safe-angular.js-applications/), [ng-annotate automation tool](https://github.com/olov/ng-annotate)
|
||||
* **Analytics and Logging:** [Angularyitcs (Google Analytics)](http://ngmodules.org/modules/angularytics), [Angulartics (Analytics)](https://github.com/luisfarzati/angulartics), [Logging Client-Side Errors](http://www.bennadel.com/blog/2542-Logging-Client-Side-Errors-With-AngularJS-And-Stacktrace-js.htm)
|
||||
* **SEO:** [By hand](http://www.yearofmoo.com/2012/11/angularjs-and-seo.html), [prerender.io](http://prerender.io/), [Brombone](http://www.brombone.com/), [SEO.js](http://getseojs.com/), [SEO4Ajax](http://www.seo4ajax.com/)
|
||||
|
||||
### Server-Specific
|
||||
|
||||
* **Django:** [Tutorial](http://blog.mourafiq.com/post/55034504632/end-to-end-web-app-with-django-rest-framework), [Integrating AngularJS with Django](http://django-angular.readthedocs.org/en/latest/integration.html), [Getting Started with Django Rest Framework and AngularJS](http://blog.kevinastone.com/getting-started-with-django-rest-framework-and-angularjs.html)
|
||||
* **FireBase:** [AngularFire](http://angularfire.com/), [Realtime Apps with AngularJS and FireBase (video)](http://www.youtube.com/watch?v=C7ZI7z7qnHU)
|
||||
* **Google Cloud Platform: **[with Cloud Endpoints](https://cloud.google.com/developers/articles/angularjs-cloud-endpoints-recipe-for-building-modern-web-applications/), [with Go](https://github.com/GoogleCloudPlatform/appengine-angular-gotodos)
|
||||
* **Hood.ie:** [60 Minutes to Awesome](http://www.roberthorvick.com/2013/06/30/todomvc-angularjs-hood-ie-60-minutes-to-awesome/)
|
||||
* **MEAN Stack: **[Blog post](http://blog.mongodb.org/post/49262866911/the-mean-stack-mongodb-expressjs-angularjs-and), [Setup](http://thecodebarbarian.wordpress.com/2013/07/22/introduction-to-the-mean-stack-part-one-setting-up-your-tools/), [GDL Video](https://developers.google.com/live/shows/913996610)
|
||||
* **Rails: **[Tutorial](http://coderberry.me/blog/2013/04/22/angularjs-on-rails-4-part-1/), [AngularJS with Rails4](https://shellycloud.com/blog/2013/10/how-to-integrate-angularjs-with-rails-4), [angularjs-rails](https://github.com/hiravgandhi/angularjs-rails)
|
||||
* **PHP: **[Building a RESTful web service](http://blog.brunoscopelliti.com/building-a-restful-web-service-with-angularjs-and-php-more-power-with-resource), [End to End with Laravel 4 (video)](http://www.youtube.com/watch?v=hqAyiqUs93c)
|
||||
* **Meteor: **[angular-meteor package](https://github.com/Urigo/angular-meteor)
|
||||
|
||||
## Learning Resources
|
||||
|
||||
###Books
|
||||
* [AngularJS: Up and Running](http://www.amazon.com/AngularJS-Running-Enhanced-Productivity-Structured/dp/1491901942) by Brad Green and Shyam Seshadri
|
||||
* [Mastering Web App Development](http://www.amazon.com/Mastering-Web-Application-Development-AngularJS/dp/1782161821) by Pawel Kozlowski and Pete Bacon Darwin
|
||||
* [AngularJS Directives](http://www.amazon.com/AngularJS-Directives-Alex-Vanston/dp/1783280336) by Alex Vanston
|
||||
* [Recipes With AngularJS](http://www.amazon.co.uk/Recipes-Angular-js-Frederik-Dietz-ebook/dp/B00DK95V48) by Frederik Dietz
|
||||
* [Developing an AngularJS Edge](http://www.amazon.com/Developing-AngularJS-Edge-Christopher-Hiller-ebook/dp/B00CJLFF8K) by Christopher Hiller
|
||||
* [ng-book: The Complete Book on AngularJS](http://ng-book.com/) by Ari Lerner
|
||||
* [AngularJS : Novice to Ninja](http://www.amazon.in/AngularJS-Novice-Ninja-Sandeep-Panda/dp/0992279453) by Sandeep Panda
|
||||
* [AngularJS UI Development](http://www.amazon.com/AngularJS-UI-Development-Amit-Ghart-ebook/dp/B00OXVAK7A) by Amit Gharat and Matthias Nehlsen
|
||||
* [Responsive Web Design with AngularJS](http://www.amazon.com/Responsive-Design-AngularJS-Sandeep-Kumar/dp/178439842X) by Sandeep Kumar Patel
|
||||
|
||||
###Videos:
|
||||
* [egghead.io](http://egghead.io/)
|
||||
* [Angular on YouTube](http://youtube.com/angularjs)
|
||||
|
||||
### Courses
|
||||
* **Free online:**
|
||||
[thinkster.io](http://thinkster.io),
|
||||
[CodeAcademy](http://www.codecademy.com/courses/javascript-advanced-en-2hJ3J/0/1),
|
||||
[CodeSchool](https://www.codeschool.com/courses/shaping-up-with-angular-js)
|
||||
* **Paid online:**
|
||||
[Pluralsite (3 courses)](http://www.pluralsight.com/training/Courses/Find?highlight=true&searchTerm=angularjs),
|
||||
[Tuts+](https://tutsplus.com/course/easier-js-apps-with-angular/),
|
||||
[lynda.com](http://www.lynda.com/AngularJS-tutorials/Up-Running-AngularJS/133318-2.html),
|
||||
[WintellectNOW (4 lessons)](http://www.wintellectnow.com/Course/Detail/mastering-angularjs)
|
||||
* **Paid onsite:**
|
||||
[angularbootcamp.com](http://angularbootcamp.com/)
|
||||
|
||||
## Getting Help
|
||||
|
||||
The recipe for getting help on your unique issue is to create an example that could work (even if it doesn't) in a shareable example on [Plunker](http://plnkr.co/), [JSFiddle](http://jsfiddle.net/), or similar site and then post to one of the following:
|
||||
|
||||
* [Stackoverflow.com](http://stackoverflow.com/search?q=angularjs)
|
||||
* [AngularJS mailing list](https://groups.google.com/forum/#!forum/angular)
|
||||
* [AngularJS IRC channel](http://webchat.freenode.net/?channels=angularjs&uio=d4)
|
||||
* [Angular 1 mailing list](https://groups.google.com/forum/#!forum/angular)
|
||||
* [Angular 1 IRC channel](http://webchat.freenode.net/?channels=angularjs&uio=d4)
|
||||
|
||||
## Social Channels
|
||||
## Official Communications
|
||||
|
||||
* **Daily updates:** [Google+](https://plus.google.com/u/0/+AngularJS) or [Twitter](https://twitter.com/angularjs)
|
||||
* **Weekly newsletter:** [ng-newsletter](http://www.ng-newsletter.com/)
|
||||
* **Meetups: **[meetup.com](http://www.meetup.com/find/?keywords=angularJS&radius=Infinity&userFreeform=San+Francisco%2C+CA&mcId=z94108&mcName=San+Francisco%2C+CA&sort=member_count&eventFilter=mysugg)
|
||||
* **Official news and releases: **[AngularJS Blog](http://blog.angularjs.org/)
|
||||
Official announcements, news and releases are posted to our blog, G+ and Twitter:
|
||||
|
||||
## Contributing to AngularJS
|
||||
* [Angular Blog](http://blog.angularjs.org/)
|
||||
* [Google+](https://plus.google.com/u/0/+AngularJS)
|
||||
* [Twitter](https://twitter.com/angularjs)
|
||||
* [Angular on YouTube](http://youtube.com/angularjs)
|
||||
|
||||
Though we have a core group of core contributors at Google, Angular is an open source project with hundreds of contributors. We'd love you to be one of them. When you're ready, please read the {@link misc/contribute Guide for contributing to AngularJS}.
|
||||
## Contributing to Angular 1
|
||||
|
||||
## Final Bits
|
||||
Though we have a core group of core contributors at Google, Angular is an open source project with hundreds of contributors.
|
||||
We'd love you to be one of them. When you're ready, please read the {@link misc/contribute Guide for contributing to Angular}.
|
||||
|
||||
Didn't find what you're looking for here? Check out [AngularJS-Learning](https://github.com/jmcunningham/AngularJS-Learning) for an even more comprehensive list of links to videos, tutorials, and blog posts.
|
||||
## Something Missing?
|
||||
|
||||
If you have awesome AngularJS resources that belong on this page, please tell us about them on [Google+](https://plus.google.com/u/0/+AngularJS) or [Twitter](https://twitter.com/angularjs).
|
||||
Didn't find what you're looking for here? Check out the {@link guide/external-resources External Angular 1 resources guide}.
|
||||
|
||||
If you have awesome Angular 1 resources that belong on that page, please tell us about them on
|
||||
[Google+](https://plus.google.com/u/0/+AngularJS) or [Twitter](https://twitter.com/angularjs).
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
@ngdoc overview
|
||||
@name Interpolation
|
||||
@sortOrder 275
|
||||
@description
|
||||
|
||||
# Interpolation and data-binding
|
||||
|
||||
Interpolation markup with embedded {@link guide/expression expressions} is used by Angular to
|
||||
provide data-binding to text nodes and attribute values.
|
||||
|
||||
An example of interpolation is shown below:
|
||||
|
||||
```html
|
||||
<a ng-href="img/{{username}}.jpg">Hello {{username}}!</a>
|
||||
```
|
||||
|
||||
### How text and attribute bindings work
|
||||
|
||||
During the compilation process the {@link ng.$compile compiler} uses the {@link ng.$interpolate $interpolate}
|
||||
service to see if text nodes and element attributes contain interpolation markup with embedded expressions.
|
||||
|
||||
If that is the case, the compiler adds an interpolateDirective to the node and
|
||||
registers {@link ng.$rootScope.Scope#$watch watches} on the computed interpolation function,
|
||||
which will update the corresponding text nodes or attribute values as part of the
|
||||
normal {@link ng.$rootScope.Scope#$digest digest} cycle.
|
||||
|
||||
Note that the interpolateDirective has a priority of 100 and sets up the watch in the preLink function.
|
||||
|
||||
### Binding to boolean attributes
|
||||
|
||||
Attributes such as `disabled` are called `boolean` attributes, because their presence means `true` and
|
||||
their absence means `false`. We cannot use normal attribute bindings with them, because the HTML
|
||||
specification does not require browsers to preserve the values of boolean attributes. This means that
|
||||
if we put an Angular interpolation expression into such an attribute then the binding information
|
||||
would be lost, because the browser ignores the attribute value.
|
||||
|
||||
In the following example, the interpolation information would be ignored and the browser would simply
|
||||
interpret the attribute as present, meaning that the button would always be disabled.
|
||||
|
||||
```html
|
||||
Disabled: <input type="checkbox" ng-model="isDisabled" />
|
||||
<button disabled="{{isDisabled}}">Disabled</button>
|
||||
```
|
||||
|
||||
For this reason, Angular provides special `ng`-prefixed directives for the following boolean attributes:
|
||||
{@link ngDisabled `disabled`}, {@link ngRequired `required`}, {@link ngSelected `selected`},
|
||||
{@link ngChecked `checked`}, {@link ngReadonly `readOnly`} , and {@link ngOpen `open`}.
|
||||
|
||||
These directives take an expression inside the attribute, and set the corresponding boolean attribute
|
||||
to true when the expression evaluates to truthy.
|
||||
|
||||
```html
|
||||
Disabled: <input type="checkbox" ng-model="isDisabled" />
|
||||
<button ng-disabled="isDisabled">Disabled</button>
|
||||
```
|
||||
|
||||
### `ngAttr` for binding to arbitrary attributes
|
||||
|
||||
Web browsers are sometimes picky about what values they consider valid for attributes.
|
||||
|
||||
For example, considering this template:
|
||||
|
||||
```html
|
||||
<svg>
|
||||
<circle cx="{{cx}}"></circle>
|
||||
</svg>
|
||||
```
|
||||
|
||||
We would expect Angular to be able to bind to this, but when we check the console we see
|
||||
something like `Error: Invalid value for attribute cx="{{cx}}"`. Because of the SVG DOM API's
|
||||
restrictions, you cannot simply write `cx="{{cx}}"`.
|
||||
|
||||
With `ng-attr-cx` you can work around this problem.
|
||||
|
||||
If an attribute with a binding is prefixed with the `ngAttr` prefix (denormalized as `ng-attr-`)
|
||||
then during the binding it will be applied to the corresponding unprefixed attribute. This allows
|
||||
you to bind to attributes that would otherwise be eagerly processed by browsers
|
||||
(e.g. an SVG element's `circle[cx]` attributes). When using `ngAttr`, the `allOrNothing` flag of
|
||||
{@link ng.$interpolate $interpolate} is used, so if any expression in the interpolated string
|
||||
results in `undefined`, the attribute is removed and not added to the element.
|
||||
|
||||
For example, we could fix the example above by instead writing:
|
||||
|
||||
```html
|
||||
<svg>
|
||||
<circle ng-attr-cx="{{cx}}"></circle>
|
||||
</svg>
|
||||
```
|
||||
|
||||
If one wants to modify a camelcased attribute (SVG elements have valid camelcased attributes),
|
||||
such as `viewBox` on the `svg` element, one can use underscores to denote that the attribute to bind
|
||||
to is naturally camelcased.
|
||||
|
||||
For example, to bind to `viewBox`, we can write:
|
||||
|
||||
```html
|
||||
<svg ng-attr-view_box="{{viewBox}}">
|
||||
</svg>
|
||||
```
|
||||
|
||||
Other attributes may also not work as expected when they contain interpolation markup, and
|
||||
can be used with `ngAttr` instead. The following is a list of known problematic attributes:
|
||||
|
||||
- **size** in `<select>` elements (see [issue 1619](https://github.com/angular/angular.js/issues/1619))
|
||||
- **placeholder** in `<textarea>` in Internet Explorer 10/11 (see [issue 5025](https://github.com/angular/angular.js/issues/5025))
|
||||
- **type** in `<button>` in Internet Explorer 11 (see [issue 14117](https://github.com/angular/angular.js/issues/5025))
|
||||
- **value** in `<progress>` in Internet Explorer = 11 (see [issue 7218](https://github.com/angular/angular.js/issues/7218))
|
||||
|
||||
## Known Issues
|
||||
|
||||
### Dynamically changing an interpolated value
|
||||
|
||||
You should avoid dynamically changing the content of an interpolated string (e.g. attribute value
|
||||
or text node). Your changes are likely to be overwriten, when the original string gets evaluated.
|
||||
This restriction applies to both directly changing the content via JavaScript or indirectly using a
|
||||
directive.
|
||||
|
||||
For example, you should not use interpolation in the value of the `style` attribute (e.g.
|
||||
`style="color: {{ 'orange' }}; font-weight: {{ 'bold' }};"`) **and** at the same time use a
|
||||
directive that changes the content of that attributte, such as `ngStyle`.
|
||||
|
||||
### Embedding interpolation markup inside expressions
|
||||
|
||||
<div class="alert alert-danger">
|
||||
**Note:** Angular directive attributes take either expressions *or* interpolation markup with embedded expressions.
|
||||
It is considered **bad practice** to embed interpolation markup inside an expression:
|
||||
</div>
|
||||
|
||||
```html
|
||||
<div ng-show="form{{$index}}.$invalid"></div>
|
||||
```
|
||||
|
||||
You should instead delegate the computation of complex expressions to the scope, like this:
|
||||
|
||||
```html
|
||||
<div ng-show="getForm($index).$invalid"></div>
|
||||
```
|
||||
|
||||
```js
|
||||
function getForm(index) {
|
||||
return $scope['form' + index];
|
||||
}
|
||||
```
|
||||
|
||||
You can also access the `scope` with `this` in your templates:
|
||||
|
||||
```html
|
||||
<div ng-show="this['form' + $index].$invalid"></div>
|
||||
```
|
||||
|
||||
#### Why mixing interpolation and expressions is bad practice:
|
||||
|
||||
- It increases the complexity of the markup
|
||||
- There is no guarantee that it works for every directive, because interpolation itself is a directive.
|
||||
If another directive accesses attribute data before interpolation has run, it will get the raw
|
||||
interpolation markup and not data.
|
||||
- It impacts performance, as interpolation adds another watcher to the scope.
|
||||
- Since this is not recommended usage, we do not test for this, and changes to
|
||||
Angular core may break your code.
|
||||
@@ -3,17 +3,246 @@
|
||||
@sortOrder 550
|
||||
@description
|
||||
|
||||
# Migrating an App to a newer version
|
||||
|
||||
Minor version releases in AngularJS introduce several breaking changes that may require changes to your
|
||||
application's source code; for instance from 1.0 to 1.2 and from 1.2 to 1.3.
|
||||
|
||||
Although we try to avoid breaking changes, there are some cases where it is unavoidable.
|
||||
Although we try to avoid breaking changes, there are some cases where it is unavoidable:
|
||||
|
||||
* AngularJS has undergone thorough security reviews to make applications safer by default,
|
||||
which drives many of these changes.
|
||||
* Several new features, especially animations, would not be possible without a few changes.
|
||||
* Finally, some outstanding bugs were best fixed by changing an existing API.
|
||||
|
||||
# Migrating from 1.3 to 1.4
|
||||
|
||||
|
||||
## Contents
|
||||
|
||||
<ul class="nav nav-list">
|
||||
<li>{@link guide/migration#migrating-from-1-4-to-1-5 Migrating from 1.4 to 1.5}</li>
|
||||
<li>{@link guide/migration#migrating-from-1-3-to-1-4 Migrating from 1.3 to 1.4}</li>
|
||||
<li>{@link guide/migration#migrating-from-1-2-to-1-3 Migrating from 1.2 to 1.3}</li>
|
||||
<li>{@link guide/migration#migrating-from-1-0-to-1-2 Migrating from 1.0 to 1.2}</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Migrating from 1.4 to 1.5
|
||||
|
||||
Angular 1.5 takes a big step towards preparing developers for a smoother transition to Angular 2 in
|
||||
the future. Architecturing your applications using components, multi-slot transclusion, one-way
|
||||
bindings in isolate scopes, using lifecycle hooks in directive controllers and relying on native ES6
|
||||
features (such as classes and arrow functions) are now all possible with Angular 1.5.
|
||||
|
||||
|
||||
This release includes numerous bug and security fixes, as well as performance improvements to core
|
||||
services, directives, filters and helper functions. Existing applications can start enjoying the
|
||||
benefits of such changes in `$compile`, `$parse`, `$animate`, `$animateCss`, `$sanitize`, `ngOptions`,
|
||||
`currencyFilter`, `numberFilter`, `copy()` (to name but a few) without any change in code.
|
||||
|
||||
New features have been added to more than a dozen services, directives and filters across 8 modules.
|
||||
Among them, a few stand out:
|
||||
|
||||
* `angular.component()`: Introducing "components", a special sort of directive that are easy to
|
||||
configure and promote best practices (plus can bring Angular 1 applications closer to Angular 2's
|
||||
style of architecture).
|
||||
* Multi-slot transclusion: Enabling the design of more powerful and complex UI elements with a much
|
||||
simpler configuration and reduced boilerplate.
|
||||
* `$onInit` lifecycle hook: Introducing a new lifecycle hook for directive controllers, called after
|
||||
all required controllers have been constructed. This enables access to required controllers from
|
||||
a directive's controller, without having to rely on the linking function.
|
||||
* `ngAnimateSwap`: A new directive in `ngAnimate`, making it super easy to create rotating
|
||||
banner-like components.
|
||||
* Testing helpers: New helper functions in `ngMock`, simplifying testing for animations, component
|
||||
controllers and routing.
|
||||
|
||||
Also, notable is the improved support for ES6 features, such as classes and arrow functions. These
|
||||
features are now more reliably detected and correctly handled within the core.
|
||||
|
||||
|
||||
All this goodness doesn't come without a price, though. Below is a list of breaking changes (grouped
|
||||
by module) that need to be taken into account while migrating from 1.4. Fortunately, the majority of
|
||||
them should have a pretty low impact on most applications.
|
||||
|
||||
|
||||
### Core
|
||||
|
||||
We tried to keep the breaking changes inside the core components to a bare minimum. Still, a few of
|
||||
them were unavoidable.
|
||||
|
||||
#### Services (`$parse`)
|
||||
|
||||
Due to [0ea53503](https://github.com/angular/angular.js/commit/0ea535035a3a1a992948490c3533bffb83235052),
|
||||
a new special property, `$locals`, will be available for accessing the locals from an expression.
|
||||
This is a breaking change, only if a `$locals` property does already exist (and needs to be
|
||||
referenced) either on the `scope` or on the `locals` object. Your expressions should be changed to
|
||||
access such existing properties as `this.$locals` and `$locals.$locals` respectively.
|
||||
|
||||
|
||||
#### Directives (`ngOptions`)
|
||||
|
||||
A fair amount of work has been put into the `ngOptions` directive, fixing bugs and corner-cases and
|
||||
neutralizing browser quirks. A couple of breaking changes were made in the process:
|
||||
|
||||
Due to [b71d7c3f](https://github.com/angular/angular.js/commit/b71d7c3f3c04e65b02d88b33c22dd90ae3cdfc27),
|
||||
falsy values (`''`, `0`, `false` and `null`) are properly recognized as option group identifiers for
|
||||
options passed to `ngOptions`. Previously, all of these values were ignored and the option was not
|
||||
assigned to any group. `undefined` is still interpreted as "no group".
|
||||
If you have options with falsy group indentifiers that should still not be assigned to any group,
|
||||
then you must filter the values before passing them to `ngOptions`, converting falsy values to
|
||||
`undefined`.
|
||||
|
||||
Due to [ded25187](https://github.com/angular/angular.js/commit/ded2518756d4409fdfda0d4af243f2125bea01b5),
|
||||
`ngOptions` now explicitly requires `ngModel` on the same element, thus an error will be thrown if
|
||||
`ngModel` is not found. Previously, `ngOptions` would silently fail, which could lead to
|
||||
hard-to-debug errors.
|
||||
This is not expected to have any significant impact on applications, since `ngOptions` didn't work
|
||||
without `ngModel` before either. The main difference is that now it will fail with a more
|
||||
informative error message.
|
||||
|
||||
|
||||
#### Filters (`orderBy`)
|
||||
|
||||
Due to [2a85a634](https://github.com/angular/angular.js/commit/2a85a634f86c84f15b411ce009a3515fca7ba580),
|
||||
passing a non-array-like value (other than `undefined` or `null`) through the `orderBy` filter will
|
||||
throw an error. Previously, the input was returned unchanged, which could lead to hard-to-spot bugs
|
||||
and was not consistent with other filters (e.g. `filter`).
|
||||
Objects considered array-like include: arrays, array subclasses, strings, NodeLists,
|
||||
jqLite/jQuery collections
|
||||
|
||||
|
||||
### ngAria
|
||||
|
||||
Due to [d06431e](https://github.com/angular/angular.js/commit/d06431e5309bb0125588877451dc79b935808134),
|
||||
the `ngAria`-enhanced directives (e.g. `ngModel`, `ngDisabled` etc) will not apply ARIA attributes
|
||||
to native inputs, unless necessary. Previously, ARIA attributes were always applied to native
|
||||
inputs, despite this being unnecessary in most cases.
|
||||
In the context of `ngAria`, elements considered "native inputs" include:
|
||||
`<a>`, `<button>`, `<details>`, `<input>`, `<select>`, `<summary>`, `<textarea>`
|
||||
|
||||
This change will not affect the accessibility of your applications (since native inputs are
|
||||
accessible by default), but if you relied on ARIA attributes being present on native inputs (for
|
||||
whatever reason), you'll have to add and update them manually.
|
||||
|
||||
Additionally, the `aria-multiline` attribute, which was previously added to elements with a `type`
|
||||
or `role` of `textbox`, will not be added anymore, since there is no way `ngAria` can tell if the
|
||||
textbox element is multiline or not.
|
||||
If you relied on `aria-multiline="true"` being automatically added by `ngAria`, you need to apply it
|
||||
yourself. E.g. change your code from `<div role="textbox" ng-model="..." ...>` to
|
||||
`<div role="textbox" ng-model="..." ... aria-multiline="true">`.
|
||||
|
||||
|
||||
### ngMessages (`ngMessage`)
|
||||
|
||||
Due to [4971ef12](https://github.com/angular/angular.js/commit/4971ef12d4c2c268cb8d26f90385dc96eba19db8),
|
||||
the `ngMessage` directive is now compiled with a priority of 1, which means directives on the same
|
||||
element as `ngMessage` with a priority lower than 1 will be applied when `ngMessage` calls its
|
||||
`$transclude` function. Previously, they were applied during the initial compile phase and were
|
||||
passed the comment element created by the transclusion of `ngMessage`.
|
||||
If you have custom directives that relied on the previous behavior, you need to give them a priority
|
||||
of 1 or greater.
|
||||
|
||||
|
||||
### ngResource (`$resource`)
|
||||
|
||||
The `$resource` service underwent a minor internal refactoring to finally solve a long-standing bug
|
||||
preventing requests from being cancelled using promises. Due to the nature of `$resource`'s
|
||||
configuration, it was not possible to follow the `$http` convention. A new `$cancelRequest()` method
|
||||
was introduced instead.
|
||||
|
||||
Due to [98528be3](https://github.com/angular/angular.js/commit/98528be311b48269ba0e15ba4e3e2ad9b89693a9),
|
||||
using a promise as `timeout` in `$resource` is no longer supported and will log a warning. This is
|
||||
hardly expected to affect the behavior of your application, since a promise as `timeout` didn't work
|
||||
before either, but it will now warn you explicitly when trying to pass one.
|
||||
If you need to be able to cancel pending requests, you can now use the new `$cancelRequest()` that
|
||||
will be available on `$resource` instances.
|
||||
|
||||
|
||||
### ngRoute (`ngView`)
|
||||
|
||||
Due to [983b0598](https://github.com/angular/angular.js/commit/983b0598121a8c5a3a51a30120e114d7e3085d4d),
|
||||
a new property will be available on the scope of the route, allowing easy access to the route's
|
||||
resolved values from the view's template. The default name for this property is `$resolve`. This is
|
||||
a breaking change, only if a `$resolve` property is already available on the scope, in which case
|
||||
the existing property will be hidden or overwritten.
|
||||
To fix this, you should choose a custom name for this property, that does not collide with other
|
||||
properties on the scope, by specifying the `resolveAs` property on the route.
|
||||
|
||||
|
||||
### ngSanitize (`$sanitize`, `linky`)
|
||||
|
||||
The HTML sanitizer has been re-implemented using inert documents, increasing security, fixing some
|
||||
corner-cases that were difficult to handle and reducing its size by about 20% (in terms of loc). In
|
||||
order to make it more secure by default, a couple of breaking changes have been introduced:
|
||||
|
||||
Due to [181fc567](https://github.com/angular/angular.js/commit/181fc567d873df065f1e84af7225deb70a8d2eb9),
|
||||
SVG support in `$sanitize` is now an opt-in feature (i.e. disabled by default), as it could make
|
||||
an application vulnerable to click-hijacking attacks. If your application relies on it, you can
|
||||
still turn it on with `$sanitizeProvider.enableSvg(true)`, but you extra precautions need to be
|
||||
taken in order to keep your application secure. Read the documentation for more information about
|
||||
the dangers and ways to mitigate them.
|
||||
|
||||
Due to [7a668cdd](https://github.com/angular/angular.js/commit/7a668cdd7d08a7016883eb3c671cbcd586223ae8),
|
||||
the `$sanitize` service will now remove instances of the `<use>` tag from the content passed to it.
|
||||
This element is used to import external SVG resources, which is a security risk as the `$sanitize`
|
||||
service does not have access to the resource in order to sanitize it.
|
||||
|
||||
Similarly, due to [234053fc](https://github.com/angular/angular.js/commit/234053fc9ad90e0d05be7e8359c6af66be94c094),
|
||||
the `$sanitize` service will now also remove instances of the `usemap` attribute from any elements
|
||||
passed to it. This attribute is used to reference another element by `name` or `id`. Since the
|
||||
`name` and `id` attributes are already blacklisted, a sanitized `usemap` attribute could only
|
||||
reference unsanitized content, which is a security risk.
|
||||
|
||||
Due to [98c2db7f](https://github.com/angular/angular.js/commit/98c2db7f9c2d078a408576e722407d518c7ee10a),
|
||||
passing a non-string value (other than `undefined` or `null`) through the `linky` filter will throw
|
||||
an error. This is not expected to have any significant impact on applications, since the input was
|
||||
always assumed to be of type 'string', so passing non-string values never worked correctly anyway.
|
||||
The main difference is that now it will fail faster and with a more informative error message.
|
||||
|
||||
|
||||
## ngTouch (`ngClick`)
|
||||
|
||||
Due to [0dfc1dfe](https://github.com/angular/angular.js/commit/0dfc1dfebf26af7f951f301c4e3848ac46f05d7f),
|
||||
the `ngClick` override directive from the `ngTouch` module is **deprecated and disabled by default**.
|
||||
This means that on touch-based devices, users might now experience a 300ms delay before a click
|
||||
event is fired.
|
||||
|
||||
If you rely on this directive, you can still enable it using
|
||||
`$touchProvider.ngClickOverrideEnabled()`:
|
||||
|
||||
```js
|
||||
angular.module('myApp').config(function($touchProvider) {
|
||||
$touchProvider.ngClickOverrideEnabled(true);
|
||||
});
|
||||
```
|
||||
|
||||
Going forward, we recommend using [FastClick](https://github.com/ftlabs/fastclick) or perhaps one of
|
||||
the [Angular 3rd party touch-related modules](http://ngmodules.org/tags/touch) that provide similar
|
||||
functionality.
|
||||
|
||||
Also note that modern browsers already remove the 300ms delay under some circumstances:
|
||||
|
||||
- **Chrome and Firefox for Android** remove the 300ms delay when the well-known
|
||||
`<meta name="viewport" content="width=device-width">` is set.
|
||||
- **Internet Explorer** removes the delay, when the `touch-action` css property is set to `none` or
|
||||
`manipulation`.
|
||||
- Since **iOS 8, Safari** removes the delay on so-called "slow taps".
|
||||
|
||||
For more info on the topic, you can take a look at this
|
||||
[article by Telerik](http://developer.telerik.com/featured/300-ms-click-delay-ios-8/).
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Note:** This change does **not** affect the `ngSwipe` directive.
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Migrating from 1.3 to 1.4
|
||||
|
||||
Angular 1.4 fixes major animation issues and introduces a new API for `ngCookies`. Further, there
|
||||
are changes to `ngMessages`, `$compile`, `ngRepeat`, `ngOptions `and some fixes to core filters:
|
||||
@@ -37,7 +266,7 @@ relatively straightforward otherwise.
|
||||
|
||||
|
||||
|
||||
## Animation (`ngAnimate`)
|
||||
### Animation (`ngAnimate`)
|
||||
|
||||
Animations in 1.4 have been refactored internally, but the API has stayed much the same. There are, however,
|
||||
some breaking changes that need to be addressed when upgrading to 1.4.
|
||||
@@ -136,9 +365,9 @@ class based animations (animations triggered via ngClass) in order to ensure tha
|
||||
|
||||
|
||||
|
||||
## Forms (`ngMessages`, `ngOptions`, `select`)
|
||||
### Forms (`ngMessages`, `ngOptions`, `select`)
|
||||
|
||||
### ngMessages
|
||||
#### ngMessages
|
||||
The ngMessages module has also been subject to an internal refactor to allow it to be more flexible
|
||||
and compatible with dynamic message data. The `ngMessage` directive now supports a new attribute
|
||||
called `ng-message-exp` which will evaluate an expression and will keep track of that expression
|
||||
@@ -170,7 +399,26 @@ other inline messages situated as children within the `ngMessages` container dir
|
||||
Depending on where the `ngMessagesInclude` directive is placed it will be prioritized inline with the other messages
|
||||
before and after it.
|
||||
|
||||
### ngOptions
|
||||
Also due to [c9a4421f](https://github.com/angular/angular.js/commit/c9a4421fc3c97448527eadef1f42eb2f487ec2e0),
|
||||
it is no longer possible to use interpolation inside the `ngMessages` attribute expression. This technique
|
||||
is generally not recommended, and can easily break when a directive implementation changes. In cases
|
||||
where a simple expression is not possible, you can delegate accessing the object to a function:
|
||||
|
||||
```html
|
||||
<div ng-messages="ctrl.form['field_{{$index}}'].$error">...</div>
|
||||
```
|
||||
would become
|
||||
```html
|
||||
<div ng-messages="ctrl.getMessages($index)">...</div>
|
||||
```
|
||||
where `ctrl.getMessages()`
|
||||
```javascript
|
||||
ctrl.getMessages = function($index) {
|
||||
return ctrl.form['field_' + $index].$error;
|
||||
}
|
||||
```
|
||||
|
||||
#### ngOptions
|
||||
|
||||
The `ngOptions` directive has also been refactored and as a result some long-standing bugs
|
||||
have been fixed. The breaking changes are comparatively minor and should not affect most applications.
|
||||
@@ -189,8 +437,12 @@ But in practice this is not what people want and so this change iterates over pr
|
||||
in the order they are returned by Object.keys(obj), which is almost always the order
|
||||
in which the properties were defined.
|
||||
|
||||
Also due to [7fda214c](https://github.com/angular/angular.js/commit/7fda214c4f65a6a06b25cf5d5aff013a364e9cef),
|
||||
setting the ngOptions attribute expression after the element is compiled, will no longer trigger the ngOptions behavior.
|
||||
This worked previously because the ngOptions logic was part of the select directive, while
|
||||
it is now implemented in the ngOptions directive itself.
|
||||
|
||||
### select
|
||||
#### select
|
||||
|
||||
Due to [7fda214c](https://github.com/angular/angular.js/commit/7fda214c4f65a6a06b25cf5d5aff013a364e9cef),
|
||||
the `select` directive will now use strict comparison of the `ngModel` scope value against `option`
|
||||
@@ -221,9 +473,50 @@ ngModelCtrl.$formatters.push(function(value) {
|
||||
});
|
||||
```
|
||||
|
||||
## Templating (`ngRepeat`, `$compile`)
|
||||
|
||||
### ngRepeat
|
||||
### form
|
||||
|
||||
Due to [94533e57](https://github.com/angular/angular.js/commit/94533e570673e6b2eb92073955541fa289aabe02),
|
||||
the `name` attribute of `form` elements can now only contain characters that can be evaluated as part
|
||||
of an Angular expression. This is because Angular uses the value of `name` as an assignable expression
|
||||
to set the form on the `$scope`. For example, `name="myForm"` assigns the form to `$scope.myForm` and
|
||||
`name="myObj.myForm"` assigns it to `$scope.myObj.myForm`.
|
||||
|
||||
Previously, it was possible to also use names such `name="my:name"`, because Angular used a special setter
|
||||
function for the form name. Now the general, more robust `$parse` setter is used.
|
||||
|
||||
The easiest way to migrate your code is therefore to remove all special characters from the `name` attribute.
|
||||
|
||||
If you need to keep the special characters, you can use the following directive, which will replace
|
||||
the `name` with a value that can be evaluated as an expression in the compile function, and then
|
||||
re-set the original name in the postLink function. This ensures that (1), the form is published on
|
||||
the scope, and (2), the form has the original name, which might be important if you are doing server-side
|
||||
form submission.
|
||||
|
||||
```js
|
||||
angular.module('myApp').directive('form', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
priority: 1000,
|
||||
compile: function(element, attrs) {
|
||||
var unsupportedCharacter = ':'; // change accordingly
|
||||
var originalName = attrs.name;
|
||||
if (attrs.name && attrs.name.indexOf(unsupportedCharacter) > 0) {
|
||||
attrs.$set('name', 'this["' + originalName + '"]');
|
||||
}
|
||||
|
||||
return postLinkFunction(scope, element) {
|
||||
// Don't trigger $observers
|
||||
element.setAttribute('name', originalName);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### Templating (`ngRepeat`, `$compile`, `ngInclude`)
|
||||
|
||||
#### ngRepeat
|
||||
|
||||
Due to [c260e738](https://github.com/angular/angular.js/commit/c260e7386391877625eda086480de73e8a0ba921),
|
||||
previously, the order of items when using ngRepeat to iterate over object properties was guaranteed to be consistent
|
||||
@@ -242,7 +535,7 @@ https://github.com/petebacondarwin/angular-toArrayFilter
|
||||
or some other mechanism, and then sort them manually in the order you need.
|
||||
|
||||
|
||||
### $compile
|
||||
#### $compile
|
||||
|
||||
Due to [6a38dbfd](https://github.com/angular/angular.js/commit/6a38dbfd3c34c8f9efff503d17eb3cbeb666d422),
|
||||
previously, '&' expressions would always set up a function in the isolate scope. Now, if the binding
|
||||
@@ -252,8 +545,46 @@ Due to [62d514b](https://github.com/angular/angular.js/commit/62d514b06937cc7dd8
|
||||
returning an object from a controller constructor function will now override the scope. Views that use the
|
||||
controllerAs method will no longer get the this reference, but the returned object.
|
||||
|
||||
#### ngInclude
|
||||
Due to [3c6e8ce044446735eb2e70d0061db8c6db050289](https://github.com/angular/angular.js/commit/3c6e8ce044446735eb2e70d0061db8c6db050289), the `src` attribute of ngInclude no longer accepts an
|
||||
expression that returns the result of `$sce.trustAsResourceUrl`. This will now cause an infinite digest:
|
||||
|
||||
## Cookies (`ngCookies`)
|
||||
Before:
|
||||
```html
|
||||
<div ng-include="findTemplate('https://example.com/templates/myTemplate.html')"></div>
|
||||
```
|
||||
|
||||
```js
|
||||
$scope.findTemplate = function(templateName) {
|
||||
return $sce.trustAsResourceUrl(templateName);
|
||||
};
|
||||
```
|
||||
|
||||
To migrate, either cache the result of `trustAsResourceUrl()`, or put the template url in the resource
|
||||
whitelist in the `config()` function:
|
||||
|
||||
After:
|
||||
|
||||
```js
|
||||
var templateCache = {};
|
||||
$scope.findTemplate = function(templateName) {
|
||||
if (!templateCache[templateName]) {
|
||||
templateCache[templateName] = $sce.trustAsResourceUrl(templateName);
|
||||
}
|
||||
|
||||
return templateCache[templateName];
|
||||
};
|
||||
|
||||
// Alternatively, use `$sceDelegateProvider.resourceUrlWhitelist()`, which means you don't
|
||||
// have to use `$sce.trustAsResourceUrl()` at all:
|
||||
|
||||
angular.module('myApp', []).config(function($sceDelegateProvider) {
|
||||
$sceDelegateProvider.resourceUrlWhitelist(['self', 'https://example.com/templates/**'])
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
### Cookies (`ngCookies`)
|
||||
|
||||
Due to [38fbe3ee](https://github.com/angular/angular.js/commit/38fbe3ee8370fc449b82d80df07b5c2ed2cd5fbe),
|
||||
`$cookies` will no longer expose properties that represent the current browser cookie
|
||||
@@ -290,9 +621,7 @@ has been moved to `$cookies`, to which `$cookieStore` now simply
|
||||
delegates calls.
|
||||
|
||||
|
||||
|
||||
|
||||
## Server Requests (`$http`)
|
||||
### Server Requests (`$http`)
|
||||
|
||||
Due to [5da1256](https://github.com/angular/angular.js/commit/5da1256fc2812d5b28fb0af0de81256054856369),
|
||||
`transformRequest` functions can no longer modify request headers.
|
||||
@@ -323,11 +652,9 @@ $http.get(url, {
|
||||
```
|
||||
|
||||
|
||||
### Filters (`filter`, `limitTo`)
|
||||
|
||||
|
||||
## Filters (`filter`, `limitTo`)
|
||||
|
||||
### `filter` filter
|
||||
#### `filter` filter
|
||||
Due to [cea8e751](https://github.com/angular/angular.js/commit/cea8e75144e6910b806b63a6ec2a6d118316fddd),
|
||||
the `filter` filter will throw an error when used with a non-array. Beforehand it would silently
|
||||
return an empty array.
|
||||
@@ -335,7 +662,7 @@ return an empty array.
|
||||
If necessary, this can be worked around by converting an object to an array,
|
||||
using a filter such as https://github.com/petebacondarwin/angular-toArrayFilter.
|
||||
|
||||
### `limitTo` filter
|
||||
#### `limitTo` filter
|
||||
Due to [a3c3bf33](https://github.com/angular/angular.js/commit/a3c3bf3332e5685dc319c46faef882cb6ac246e1),
|
||||
the limitTo filter has changed behavior when the provided limit value is invalid.
|
||||
Now, instead of returning empty object/array, it returns unchanged input.
|
||||
@@ -343,10 +670,9 @@ Now, instead of returning empty object/array, it returns unchanged input.
|
||||
|
||||
|
||||
|
||||
## Migrating from 1.2 to 1.3
|
||||
|
||||
# Migrating from 1.2 to 1.3
|
||||
|
||||
## Controllers
|
||||
### Controllers
|
||||
|
||||
Due to [3f2232b5](https://github.com/angular/angular.js/commit/3f2232b5a181512fac23775b1df4a6ebda67d018),
|
||||
`$controller` will no longer look for controllers on `window`.
|
||||
@@ -384,7 +710,7 @@ angular.module('myModule').config(['$controllerProvider', function($controllerPr
|
||||
}]);
|
||||
```
|
||||
|
||||
## Angular Expression Parsing (`$parse` + `$interpolate`)
|
||||
### Angular Expression Parsing (`$parse` + `$interpolate`)
|
||||
|
||||
- due to [77ada4c8](https://github.com/angular/angular.js/commit/77ada4c82d6b8fc6d977c26f3cdb48c2f5fbe5a5),
|
||||
|
||||
@@ -442,7 +768,7 @@ expression parser; there are six of them: false, null, undefined, NaN, 0 and "".
|
||||
|
||||
|
||||
|
||||
## Miscellaneous Angular helpers
|
||||
### Miscellaneous Angular helpers
|
||||
|
||||
- **Angular.copy:** due to [b59b04f9](https://github.com/angular/angular.js/commit/b59b04f98a0b59eead53f6a53391ce1bbcbe9b57),
|
||||
|
||||
@@ -473,15 +799,14 @@ This change also makes our forEach behave more like Array#forEach.
|
||||
|
||||
|
||||
- **angular.toJson:** due to [c054288c](https://github.com/angular/angular.js/commit/c054288c9722875e3595e6e6162193e0fb67a251),
|
||||
|
||||
If you expected `toJson` to strip these types of properties before, you will have to
|
||||
manually do this yourself now.
|
||||
`toJson()` will no longer strip properties starting with a single `$`. If you relied on
|
||||
`toJson()`'s stripping these types of properties before, you will have to do it manually now.
|
||||
It will still strip properties starting with `$$` though.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## jqLite / JQuery
|
||||
### jqLite / JQuery
|
||||
|
||||
- **jqLite:** due to [a196c8bc](https://github.com/angular/angular.js/commit/a196c8bca82a28c08896d31f1863cf4ecd11401c),
|
||||
previously it was possible to set jqLite data on Text/Comment
|
||||
@@ -497,7 +822,7 @@ jQuery. We don't expect that app code actually depends on this accidental featur
|
||||
|
||||
|
||||
|
||||
## Angular HTML Compiler (`$compile`)
|
||||
### Angular HTML Compiler (`$compile`)
|
||||
|
||||
|
||||
- due to [2ee29c5d](https://github.com/angular/angular.js/commit/2ee29c5da81ffacdc1cabb438f5d125d5e116cb9),
|
||||
@@ -561,10 +886,40 @@ After:
|
||||
};
|
||||
});
|
||||
|
||||
- due to [531a8de7](https://github.com/angular/angular.js/commit/531a8de72c439d8ddd064874bf364c00cedabb11),
|
||||
`$observe` no longer registers on undefined attributes. For example, if you were using `$observe` on
|
||||
an absent optional attribute to set a default value, the following would not work anymore:
|
||||
|
||||
```html
|
||||
<my-dir></my-dir>
|
||||
```
|
||||
|
||||
```js
|
||||
// Link function for directive myDir
|
||||
link: function(scope, element, attr) {
|
||||
attr.$observe('myAttr', function(newVal) {
|
||||
scope.myValue = newVal ? newVal : 'myDefaultValue';
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Instead, check if the attribute is set before registering the observer:
|
||||
|
||||
```js
|
||||
link: function(scope, element, attr) {
|
||||
if (attr.myAttr) {
|
||||
// register the observer
|
||||
} else {
|
||||
// set the default
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Forms, Inputs and ngModel
|
||||
|
||||
|
||||
### Forms, Inputs and ngModel
|
||||
|
||||
- due to [1be9bb9d](https://github.com/angular/angular.js/commit/1be9bb9d3527e0758350c4f7417a4228d8571440),
|
||||
|
||||
@@ -633,11 +988,18 @@ $scope.resetWithCancel = function (e) {
|
||||
[#5864](https://github.com/angular/angular.js/issues/5864))
|
||||
|
||||
|
||||
- {@link input[checkbox] `input[checkbox]`} now supports constant expressions in `ngTrueValue` and
|
||||
`ngFalseValue`, making it now possible to e.g. use boolean and integer values. Previously, these attributes would
|
||||
always be treated as strings, whereas they are now parsed as expressions, and will throw if an expression
|
||||
is non-constant. To convert non-constant strings into constant expressions, simply wrap them in an
|
||||
extra pair of quotes, like so:
|
||||
|
||||
`<input type="checkbox" ng-model="..." ng-true-value="'truthyValue'">`
|
||||
|
||||
See [c90cefe1614](https://github.com/angular/angular.js/commit/c90cefe16142d973a123e945fc9058e8a874c357)
|
||||
|
||||
|
||||
|
||||
|
||||
## Scopes and Digests (`$scope`)
|
||||
### Scopes and Digests (`$scope`)
|
||||
|
||||
- due to [8c6a8171](https://github.com/angular/angular.js/commit/8c6a8171f9bdaa5cdabc0cc3f7d3ce10af7b434d),
|
||||
Scope#$id is now of type number rather than string. Since the
|
||||
@@ -657,7 +1019,7 @@ anyone.
|
||||
|
||||
|
||||
|
||||
## Server Requests (`$http`, `$resource`)
|
||||
### Server Requests (`$http`, `$resource`)
|
||||
- **$http:** due to [ad4336f9](https://github.com/angular/angular.js/commit/ad4336f9359a073e272930f8f9bcd36587a8648f),
|
||||
|
||||
|
||||
@@ -724,7 +1086,7 @@ More details on the new interceptors API (which has been around as of v1.1.4) ca
|
||||
|
||||
|
||||
|
||||
## Modules and Injector (`$inject`)
|
||||
### Modules and Injector (`$inject`)
|
||||
|
||||
- due to [c0b4e2db](https://github.com/angular/angular.js/commit/c0b4e2db9cbc8bc3164cedc4646145d3ab72536e),
|
||||
|
||||
@@ -766,7 +1128,7 @@ app. This is no longer possible within a single module.
|
||||
|
||||
|
||||
|
||||
## Animation (`ngAnimate`)
|
||||
### Animation (`ngAnimate`)
|
||||
|
||||
|
||||
- due to [1cb8584e](https://github.com/angular/angular.js/commit/1cb8584e8490ecdb1b410a8846c4478c6c2c0e53),
|
||||
@@ -788,7 +1150,7 @@ to:
|
||||
|
||||
Any class-based animation code that makes use of transitions
|
||||
and uses the setup CSS classes (such as class-add and class-remove) must now
|
||||
provide a empty transition value to ensure that its styling is applied right
|
||||
provide an empty transition value to ensure that its styling is applied right
|
||||
away. In other words if your animation code is expecting any styling to be
|
||||
applied that is defined in the setup class then it will not be applied
|
||||
"instantly" unless a `transition:0s none` value is present in the styling
|
||||
@@ -819,7 +1181,7 @@ After:
|
||||
Please view the documentation for ngAnimate for more info.
|
||||
|
||||
|
||||
## Testing
|
||||
### Testing
|
||||
|
||||
- due to [85880a64](https://github.com/angular/angular.js/commit/85880a64900fa22a61feb926bf52de0965332ca5), some deprecated features of
|
||||
Protractor tests no longer work.
|
||||
@@ -858,7 +1220,7 @@ or simply use:
|
||||
var el = element(by.repeater('foo in foos').row(2))
|
||||
|
||||
|
||||
## Internet Explorer 8
|
||||
### Internet Explorer 8
|
||||
|
||||
- due to [eaa1d00b](https://github.com/angular/angular.js/commit/eaa1d00b24008f590b95ad099241b4003688cdda),
|
||||
As communicated before, IE8 is no longer supported.
|
||||
@@ -868,7 +1230,7 @@ or simply use:
|
||||
|
||||
|
||||
|
||||
# Migrating from 1.0 to 1.2
|
||||
## Migrating from 1.0 to 1.2
|
||||
|
||||
|
||||
<div class="alert alert-warning">
|
||||
@@ -911,7 +1273,7 @@ below should still apply, but you may want to consult the
|
||||
</ul>
|
||||
|
||||
|
||||
## ngRoute has been moved into its own module
|
||||
### ngRoute has been moved into its own module
|
||||
|
||||
Just like `ngResource`, `ngRoute` is now its own module.
|
||||
|
||||
@@ -942,7 +1304,7 @@ var myApp = angular.module('myApp', ['ngRoute', 'someOtherModule']);
|
||||
See [5599b55b](https://github.com/angular/angular.js/commit/5599b55b04788c2e327d7551a4a699d75516dd21).
|
||||
|
||||
|
||||
## Templates no longer automatically unwrap promises
|
||||
### Templates no longer automatically unwrap promises
|
||||
|
||||
`$parse` and templates in general will no longer automatically unwrap promises.
|
||||
|
||||
@@ -976,7 +1338,7 @@ See [5dc35b52](https://github.com/angular/angular.js/commit/5dc35b527b3c99f6544b
|
||||
[b6a37d11](https://github.com/angular/angular.js/commit/b6a37d112b3e1478f4d14a5f82faabf700443748).
|
||||
|
||||
|
||||
## Syntax for named wildcard parameters changed in `$route`
|
||||
### Syntax for named wildcard parameters changed in `$route`
|
||||
|
||||
To migrate the code, follow the example below. Here, `*highlight` becomes `:highlight*`
|
||||
|
||||
@@ -997,7 +1359,7 @@ $routeProvider.when('/Book1/:book/Chapter/:chapter/:highlight*/edit',
|
||||
See [04cebcc1](https://github.com/angular/angular.js/commit/04cebcc133c8b433a3ac5f72ed19f3631778142b).
|
||||
|
||||
|
||||
## You can only bind one expression to `*[src]`, `*[ng-src]` or `action`
|
||||
### You can only bind one expression to `*[src]`, `*[ng-src]` or `action`
|
||||
|
||||
With the exception of `<a>` and `<img>` elements, you cannot bind more than one expression to the
|
||||
`src` or `action` attribute of elements.
|
||||
@@ -1072,7 +1434,7 @@ scope.getIframeSrc = function() {
|
||||
See [38deedd6](https://github.com/angular/angular.js/commit/38deedd6e3d806eb8262bb43f26d47245f6c2739).
|
||||
|
||||
|
||||
## Interpolations inside DOM event handlers are now disallowed
|
||||
### Interpolations inside DOM event handlers are now disallowed
|
||||
|
||||
DOM event handlers execute arbitrary Javascript code. Using an interpolation for such handlers
|
||||
means that the interpolated value is a JS string that is evaluated. Storing or generating such
|
||||
@@ -1099,7 +1461,7 @@ HTML: <div ng-click="foo()">
|
||||
See [39841f2e](https://github.com/angular/angular.js/commit/39841f2ec9b17b3b2920fd1eb548d444251f4f56).
|
||||
|
||||
|
||||
## Directives cannot end with -start or -end
|
||||
### Directives cannot end with -start or -end
|
||||
|
||||
This change was necessary to enable multi-element directives. The best fix is to rename existing
|
||||
directives so that they don't end with these suffixes.
|
||||
@@ -1107,7 +1469,7 @@ directives so that they don't end with these suffixes.
|
||||
See [e46100f7](https://github.com/angular/angular.js/commit/e46100f7097d9a8f174bdb9e15d4c6098395c3f2).
|
||||
|
||||
|
||||
## In $q, promise.always has been renamed promise.finally
|
||||
### In $q, promise.always has been renamed promise.finally
|
||||
|
||||
The reason for this change is to align `$q` with the [Q promise
|
||||
library](https://github.com/kriskowal/q), despite the fact that this makes it a bit more difficult
|
||||
@@ -1139,7 +1501,7 @@ $http.get('/foo')['finally'](doSomething);
|
||||
See [f078762d](https://github.com/angular/angular.js/commit/f078762d48d0d5d9796dcdf2cb0241198677582c).
|
||||
|
||||
|
||||
## ngMobile is now ngTouch
|
||||
### ngMobile is now ngTouch
|
||||
|
||||
Many touch-enabled devices are not mobile devices, so we decided to rename this module to better
|
||||
reflect its concerns.
|
||||
@@ -1150,7 +1512,7 @@ To migrate, replace all references to `ngMobile` with `ngTouch` and `angular-mob
|
||||
See [94ec84e7](https://github.com/angular/angular.js/commit/94ec84e7b9c89358dc00e4039009af9e287bbd05).
|
||||
|
||||
|
||||
## resource.$then has been removed
|
||||
### resource.$then has been removed
|
||||
|
||||
Resource instances do not have a `$then` function anymore. Use the `$promise.then` instead.
|
||||
|
||||
@@ -1169,7 +1531,7 @@ Resource.query().$promise.then(callback);
|
||||
See [05772e15](https://github.com/angular/angular.js/commit/05772e15fbecfdc63d4977e2e8839d8b95d6a92d).
|
||||
|
||||
|
||||
## Resource methods return the promise
|
||||
### Resource methods return the promise
|
||||
|
||||
Methods of a resource instance return the promise rather than the instance itself.
|
||||
|
||||
@@ -1189,7 +1551,7 @@ resource.chaining = true;
|
||||
See [05772e15](https://github.com/angular/angular.js/commit/05772e15fbecfdc63d4977e2e8839d8b95d6a92d).
|
||||
|
||||
|
||||
## Resource promises are resolved with the resource instance
|
||||
### Resource promises are resolved with the resource instance
|
||||
|
||||
On success, the resource promise is resolved with the resource instance rather than HTTP response object.
|
||||
|
||||
@@ -1220,7 +1582,7 @@ var Resource = $resource('/url', {}, {
|
||||
See [05772e15](https://github.com/angular/angular.js/commit/05772e15fbecfdc63d4977e2e8839d8b95d6a92d).
|
||||
|
||||
|
||||
## $location.search supports multiple keys
|
||||
### $location.search supports multiple keys
|
||||
|
||||
{@link ng.$location#search `$location.search`} now supports multiple keys with the
|
||||
same value provided that the values are stored in an array.
|
||||
@@ -1237,7 +1599,7 @@ passing it to `$location`.
|
||||
See [80739409](https://github.com/angular/angular.js/commit/807394095b991357225a03d5fed81fea5c9a1abe).
|
||||
|
||||
|
||||
## ngBindHtmlUnsafe has been removed and replaced by ngBindHtml
|
||||
### ngBindHtmlUnsafe has been removed and replaced by ngBindHtml
|
||||
|
||||
`ngBindHtml` provides `ngBindHtmlUnsafe` like
|
||||
behavior (evaluate an expression and innerHTML the result into the DOM) when bound to the result
|
||||
@@ -1253,7 +1615,7 @@ trusted.
|
||||
See [dae69473](https://github.com/angular/angular.js/commit/dae694739b9581bea5dbc53522ec00d87b26ae55).
|
||||
|
||||
|
||||
## Form names that are expressions are evaluated
|
||||
### Form names that are expressions are evaluated
|
||||
|
||||
If you have form names that will evaluate as an expression:
|
||||
|
||||
@@ -1285,7 +1647,7 @@ Supporting the previous behavior offers no benefit.
|
||||
See [8ea802a1](https://github.com/angular/angular.js/commit/8ea802a1d23ad8ecacab892a3a451a308d9c39d7).
|
||||
|
||||
|
||||
## hasOwnProperty disallowed as an input name
|
||||
### hasOwnProperty disallowed as an input name
|
||||
|
||||
Inputs with name equal to `hasOwnProperty` are not allowed inside form or ngForm directives.
|
||||
|
||||
@@ -1296,7 +1658,7 @@ and bad practice. To migrate, change your input name.
|
||||
See [7a586e5c](https://github.com/angular/angular.js/commit/7a586e5c19f3d1ecc3fefef084ce992072ee7f60).
|
||||
|
||||
|
||||
## Directives: Order of postLink functions reversed
|
||||
### Directives: Order of postLink functions reversed
|
||||
|
||||
The order of postLink fn is now mirror opposite of the order in which corresponding preLinking and compile functions execute.
|
||||
|
||||
@@ -1352,7 +1714,7 @@ attribute interpolation directive was adjusted.
|
||||
See [31f190d4](https://github.com/angular/angular.js/commit/31f190d4d53921d32253ba80d9ebe57d6c1de82b).
|
||||
|
||||
|
||||
## Directive priority
|
||||
### Directive priority
|
||||
|
||||
the priority of ngRepeat, ngSwitchWhen, ngIf, ngInclude and ngView has changed. This could affect directives that explicitly specify their priority.
|
||||
|
||||
@@ -1370,7 +1732,7 @@ ngView | 1000 | 400
|
||||
See [b7af76b4](https://github.com/angular/angular.js/commit/b7af76b4c5aa77648cc1bfd49935b48583419023).
|
||||
|
||||
|
||||
## ngScenario
|
||||
### ngScenario
|
||||
|
||||
browserTrigger now uses an eventData object instead of direct parameters for mouse events.
|
||||
To migrate, place the `keys`,`x` and `y` parameters inside of an object and place that as the
|
||||
@@ -1379,7 +1741,7 @@ third parameter for the browserTrigger function.
|
||||
See [28f56a38](https://github.com/angular/angular.js/commit/28f56a383e9d1ff378e3568a3039e941c7ffb1d8).
|
||||
|
||||
|
||||
## ngInclude and ngView replace its entire element on update
|
||||
### ngInclude and ngView replace its entire element on update
|
||||
|
||||
Previously `ngInclude` and `ngView` only updated its element's content. Now these directives will
|
||||
recreate the element every time a new content is included.
|
||||
@@ -1391,7 +1753,7 @@ See [7d69d52a](https://github.com/angular/angular.js/commit/7d69d52acff8578e0f7d
|
||||
[aa2133ad](https://github.com/angular/angular.js/commit/aa2133ad818d2e5c27cbd3933061797096356c8a).
|
||||
|
||||
|
||||
## URLs are now sanitized against a whitelist
|
||||
### URLs are now sanitized against a whitelist
|
||||
|
||||
A whitelist configured via `$compileProvider` can be used to configure what URLs are considered safe.
|
||||
By default all common protocol prefixes are whitelisted including `data:` URIs with mime types `image/*`.
|
||||
@@ -1401,7 +1763,7 @@ See [1adf29af](https://github.com/angular/angular.js/commit/1adf29af13890d612868
|
||||
[3e39ac7e](https://github.com/angular/angular.js/commit/3e39ac7e1b10d4812a44dad2f959a93361cd823b).
|
||||
|
||||
|
||||
## Isolate scope only exposed to directives with `scope` property
|
||||
### Isolate scope only exposed to directives with `scope` property
|
||||
|
||||
If you declare a scope option on a directive, that directive will have an
|
||||
[isolate scope](https://github.com/angular/angular.js/wiki/Understanding-Scopes). In Angular 1.0, if a
|
||||
@@ -1474,7 +1836,7 @@ See [909cabd3](https://github.com/angular/angular.js/commit/909cabd36d779598763c
|
||||
[#2500](https://github.com/angular/angular.js/issues/2500).
|
||||
|
||||
|
||||
## Change to interpolation priority
|
||||
### Change to interpolation priority
|
||||
|
||||
Previously, the interpolation priority was `-100` in 1.2.0-rc.2, and `100` before 1.2.0-rc.2.
|
||||
Before this change the binding was setup in the post-linking phase.
|
||||
@@ -1487,7 +1849,7 @@ See [79223eae](https://github.com/angular/angular.js/commit/79223eae502283889334
|
||||
[#4528](https://github.com/angular/angular.js/issues/4528), and
|
||||
[#4649](https://github.com/angular/angular.js/issues/4649)
|
||||
|
||||
## Underscore-prefixed/suffixed properties are non-bindable
|
||||
### Underscore-prefixed/suffixed properties are non-bindable
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>**Reverted**: This breaking change has been reverted in 1.2.1, and so can be ignored if you're using **version 1.2.1 or higher**</p>
|
||||
@@ -1526,7 +1888,7 @@ are actually needed by the expressions.
|
||||
See [3d6a89e8](https://github.com/angular/angular.js/commit/3d6a89e8888b14ae5cb5640464e12b7811853c7e).
|
||||
|
||||
|
||||
## You cannot bind to select[multiple]
|
||||
### You cannot bind to select[multiple]
|
||||
|
||||
Switching between `select[single]` and `select[multiple]` has always been odd due to browser quirks.
|
||||
This feature never worked with two-way data-binding so it's not expected that anyone is using it.
|
||||
@@ -1536,7 +1898,7 @@ If you are interested in properly adding this feature, please submit a pull requ
|
||||
See [d87fa004](https://github.com/angular/angular.js/commit/d87fa0042375b025b98c40bff05e5f42c00af114).
|
||||
|
||||
|
||||
## Uncommon region-specific local files were removed from i18n
|
||||
### Uncommon region-specific local files were removed from i18n
|
||||
|
||||
AngularJS uses the Google Closure library's locale files. The following locales were removed from
|
||||
Closure, so Angular is not able to continue to support them:
|
||||
@@ -1552,7 +1914,7 @@ load and use your copy of the locale file provided that you maintain it yourself
|
||||
|
||||
See [6382e21f](https://github.com/angular/angular.js/commit/6382e21fb28541a2484ac1a241d41cf9fbbe9d2c).
|
||||
|
||||
## Services can now return functions
|
||||
### Services can now return functions
|
||||
|
||||
Previously, the service constructor only returned objects regardless of whether a function was returned.
|
||||
|
||||
|
||||
@@ -75,9 +75,8 @@ that you break your application to multiple modules like this:
|
||||
* And an application level module which depends on the above modules and contains any
|
||||
initialization code.
|
||||
|
||||
We've also
|
||||
[written a document](http://angularjs.blogspot.com/2014/02/an-angularjs-style-guide-and-best.html)
|
||||
on how we organize large apps at Google.
|
||||
You can find a community [style guide](https://github.com/johnpapa/angular-styleguide) to help
|
||||
yourself when application grows.
|
||||
|
||||
The above is a suggestion. Tailor it to your needs.
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ and {@link angular.reloadWithDebugInfo `angular.reloadWithDebugInfo`}.
|
||||
|
||||
## Strict DI Mode
|
||||
|
||||
Using strict di mode in your production application will throw errors when a injectable
|
||||
Using strict di mode in your production application will throw errors when an injectable
|
||||
function is not
|
||||
{@link di#dependency-annotation annotated explicitly}. Strict di mode is intended to help
|
||||
you make sure that your code will work when minified. However, it also will force you to
|
||||
|
||||
@@ -257,7 +257,7 @@ the `$digest` phase. This delay is desirable, since it coalesces multiple model
|
||||
|
||||
2. **Watcher registration**
|
||||
|
||||
During template linking directives register {@link
|
||||
During template linking, directives register {@link
|
||||
ng.$rootScope.Scope#$watch watches} on the scope. These watches will be
|
||||
used to propagate model values to the DOM.
|
||||
|
||||
@@ -391,7 +391,7 @@ implementing custom event callbacks, or when working with third-party library ca
|
||||
5. The {@link ng.$rootScope.Scope#$watch $watch} list is a set of expressions
|
||||
which may have changed since last iteration. If a change is detected then the `$watch`
|
||||
function is called which typically updates the DOM with the new value.
|
||||
6. Once the Angular {@link ng.$rootScope.Scope#$digest $digest} loop finishes
|
||||
6. Once the Angular {@link ng.$rootScope.Scope#$digest $digest} loop finishes,
|
||||
the execution leaves the Angular and JavaScript context. This is followed by the browser
|
||||
re-rendering the DOM to reflect any changes.
|
||||
|
||||
@@ -419,4 +419,4 @@ user enters text into the text field.
|
||||
which in turn updates the DOM.
|
||||
6. Angular exits the execution context, which in turn exits the `keydown` event and with it
|
||||
the JavaScript execution context.
|
||||
7. The browser re-renders the view with update text.
|
||||
7. The browser re-renders the view with the updated text.
|
||||
|
||||
@@ -9,6 +9,27 @@ This document explains some of AngularJS's security features and best practices
|
||||
keep in mind as you build your application.
|
||||
|
||||
|
||||
## Reporting a security issue
|
||||
|
||||
Email us at [security@angularjs.org](mailto:security@angularjs.org) to report any potential
|
||||
security issues in AngularJS.
|
||||
|
||||
Please keep in mind the points below about Angular's expression language.
|
||||
|
||||
|
||||
## Use the latest AngularJS possible
|
||||
|
||||
Like any software library, it is critical to keep AngularJS up to date. Please track the
|
||||
[CHANGELOG](https://github.com/angular/angular.js/blob/master/CHANGELOG.md) and make sure you are aware
|
||||
of upcoming security patches and other updates.
|
||||
|
||||
Be ready to update rapidly when new security-centric patches are available.
|
||||
|
||||
Those that stray from Angular standards (such as modifying Angular's core) may have difficulty updating,
|
||||
so keeping to AngularJS standards is not just a functionality issue, it's also critical in order to
|
||||
facilitate rapid security updates.
|
||||
|
||||
|
||||
## Expression Sandboxing
|
||||
|
||||
AngularJS's expressions are sandboxed not for security reasons, but instead to maintain a proper
|
||||
@@ -25,7 +46,8 @@ But if an attacker can change arbitrary HTML templates, there's nothing stopping
|
||||
<script>somethingEvil();</script>
|
||||
```
|
||||
|
||||
It's better to design your application in such a way that users cannot change client-side templates.
|
||||
**It's better to design your application in such a way that users cannot change client-side templates.**
|
||||
|
||||
For instance:
|
||||
|
||||
* Do not mix client and server templates
|
||||
@@ -33,7 +55,8 @@ For instance:
|
||||
* Do not run user input through `$scope.$eval`
|
||||
* Consider using {@link ng.directive:ngCsp CSP} (but don't rely only on CSP)
|
||||
|
||||
## Mixing client-side and server-side templates
|
||||
|
||||
### Mixing client-side and server-side templates
|
||||
|
||||
In general, we recommend against this because it can create unintended XSS vectors.
|
||||
|
||||
@@ -41,20 +64,66 @@ However, it's ok to mix server-side templating in the bootstrap template (`index
|
||||
as user input cannot be used on the server to output html that would then be processed by Angular
|
||||
in a way that would allow for arbitrary code execution.
|
||||
|
||||
For instance, you can use server-side templating to dynamically generate CSS, URLs, etc, but not
|
||||
for generating templates that are bootstrapped/compiled by Angular.
|
||||
**For instance, you can use server-side templating to dynamically generate CSS, URLs, etc, but not
|
||||
for generating templates that are bootstrapped/compiled by Angular.**
|
||||
|
||||
|
||||
## Reporting a security issue
|
||||
## HTTP Requests
|
||||
|
||||
Email us at [security@angularjs.org](mailto:security@angularjs.org) to report any potential
|
||||
security issues in AngularJS.
|
||||
Whenever your application makes requests to a server there are potential security issues that need
|
||||
to be blocked. Both server and the client must cooperate in order to eliminate these threats.
|
||||
Angular comes pre-configured with strategies that address these issues, but for this to work backend
|
||||
server cooperation is required.
|
||||
|
||||
Please keep in mind the above points about Angular's expression language.
|
||||
|
||||
### Cross Site Request Forgery (XSRF/CSRF)
|
||||
|
||||
Protection from XSRF is provided by using the double-submit cookie defense pattern.
|
||||
For more information please visit {@link $http#cross-site-request-forgery-xsrf-protection XSRF protection}.
|
||||
|
||||
### JSON Hijacking Protection
|
||||
|
||||
Protection from JSON Hijacking is provided if the server prefixes all JSON requests with following string `")]}',\n"`.
|
||||
Angular will automatically strip the prefix before processing it as JSON.
|
||||
For more information please visit {@link $http#json-vulnerability-protection JSON Hijacking Protection}.
|
||||
|
||||
Bear in mind that calling `$http.jsonp`, like in [our Yahoo! finance example](https://docs.angularjs.org/guide/concepts#accessing-the-backend),
|
||||
gives the remote server (and, if the request is not secured, any Man-in-the-Middle attackers)
|
||||
instant remote code execution in your application: the result of these requests is handed off
|
||||
to the browser as regular `<script>` tag.
|
||||
|
||||
## Strict Contextual Escaping
|
||||
|
||||
Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain contexts to require
|
||||
a value that is marked as safe to use for that context.
|
||||
|
||||
This mode is implemented by the {@link $sce} service and various core directives.
|
||||
|
||||
One example of such a context is rendering arbitrary content via the {@link ngBindHtml} directive. If the content is
|
||||
provided by a user there is a chance of Cross Site Scripting (XSS) attacks. The {@link ngBindHtml} directive will not
|
||||
render content that is not marked as safe by {@link $sce}. The {@link ngSanitize} module can be used to clean such
|
||||
user provided content and mark the content as safe.
|
||||
|
||||
**Be aware that marking untrusted data as safe via calls to {@link $sce#trustAsHtml `$sce.trustAsHtml`}, etc is
|
||||
dangerous and will lead to Cross Site Scripting exploits.**
|
||||
|
||||
For more information please visit {@link $sce} and {@link ngSanitize.$sanitize}.
|
||||
|
||||
## Using Local Caches
|
||||
|
||||
There are various places that the browser can store (or cache) data. Within Angular there are objects created by
|
||||
the {@link $cacheFactory}. These objects, such as {@link $templateCache} are used to store and retrieve data,
|
||||
primarily used by {@link $http} and the {@link script} directive to cache templates and other data.
|
||||
|
||||
Similarly the browser itself offers `localStorage` and `sessionStorage` objects for caching data.
|
||||
|
||||
**Attackers with local access can retrieve sensitive data from this cache even when users are not authenticated.**
|
||||
|
||||
For instance in a long running Single Page Application (SPA), one user may "log out", but then another user
|
||||
may access the application without refreshing, in which case all the cached data is still available.
|
||||
|
||||
For more information please visit [Web Storage Security](https://www.whitehatsec.com/blog/web-storage-security/).
|
||||
|
||||
## See also
|
||||
|
||||
* {@link ng.directive:ngCsp Content Security Policy}
|
||||
* {@link ng.$sce Strict Contextual Escaping}
|
||||
* {@link ngSanitize.$sanitize $sanitize}
|
||||
|
||||
@@ -43,7 +43,7 @@ subsystem takes care of the rest.
|
||||
<file name="script.js">
|
||||
angular.
|
||||
module('myServiceModule', []).
|
||||
controller('MyController', ['$scope','notify', function ($scope, notify) {
|
||||
controller('MyController', ['$scope', 'notify', function ($scope, notify) {
|
||||
$scope.callNotify = function(msg) {
|
||||
notify(msg);
|
||||
};
|
||||
@@ -138,9 +138,13 @@ batchModule.factory('batchLog', ['$interval', '$log', function($interval, $log)
|
||||
*/
|
||||
batchModule.factory('routeTemplateMonitor', ['$route', 'batchLog', '$rootScope',
|
||||
function($route, batchLog, $rootScope) {
|
||||
$rootScope.$on('$routeChangeSuccess', function() {
|
||||
batchLog($route.current ? $route.current.template : null);
|
||||
});
|
||||
return {
|
||||
startMonitoring: function() {
|
||||
$rootScope.$on('$routeChangeSuccess', function() {
|
||||
batchLog($route.current ? $route.current.template : null);
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
```
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
@sortOrder 260
|
||||
@description
|
||||
|
||||
# Templates
|
||||
|
||||
In Angular, templates are written with HTML that contains Angular-specific elements and attributes.
|
||||
Angular combines the template with information from the model and controller to render the dynamic
|
||||
view that a user sees in the browser.
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
@sortOrder 410
|
||||
@description
|
||||
|
||||
# Unit Testing
|
||||
|
||||
JavaScript is a dynamically typed language which comes with great power of expression, but it also
|
||||
comes with almost no help from the compiler. For this reason we feel very strongly that any code
|
||||
written in JavaScript needs to come with a strong set of tests. We have built many features into
|
||||
Angular which makes testing your Angular applications easy. So there is no excuse for not testing.
|
||||
Angular which make testing your Angular applications easy. With Angular, there is no excuse for not testing.
|
||||
|
||||
## Separation of Concerns
|
||||
|
||||
@@ -20,13 +22,13 @@ related pieces such as the DOM elements, or making any XHR calls to fetch the da
|
||||
|
||||
While this may seem obvious it can be very difficult to call an individual function on a
|
||||
typical project. The reason is that the developers often mix concerns resulting in a
|
||||
piece of code which does everything. It makes an XHR request, it sorts the response data and then it
|
||||
piece of code which does everything. It makes an XHR request, it sorts the response data, and then it
|
||||
manipulates the DOM.
|
||||
|
||||
With Angular we try to make it easy for you to do the right thing, and so we
|
||||
provide dependency injection for your XHR requests, which can be mocked, and we provide abstractions which
|
||||
allow you to test your model without having to resort to manipulating the DOM. The test can then
|
||||
assert that the data has been sorted without having to create or look at the state of the DOM or
|
||||
With Angular, we try to make it easy for you to do the right thing. For your XHR requests, we
|
||||
provide dependency injection, so your requests can be simulated. For the DOM, we abstract it, so you can
|
||||
test your model without having to manipulate the DOM directly. Your tests can then
|
||||
assert that the data has been sorted without having to create or look at the state of the DOM or to
|
||||
wait for any XHR requests to return data. The individual sort function can be tested in isolation.
|
||||
|
||||
## With great power comes great responsibility
|
||||
@@ -359,7 +361,7 @@ element, to which it can then insert the transcluded content into its template.
|
||||
|
||||
Before compilation:
|
||||
```html
|
||||
<div translude-directive>
|
||||
<div transclude-directive>
|
||||
Some transcluded content
|
||||
</div>
|
||||
```
|
||||
@@ -430,5 +432,50 @@ If your directive uses `templateUrl`, consider using
|
||||
to pre-compile HTML templates and thus avoid having to load them over HTTP during test execution.
|
||||
Otherwise you may run into issues if the test directory hierarchy differs from the application's.
|
||||
|
||||
## Testing Promises
|
||||
|
||||
When testing promises, it's important to know that the resolution of promises is tied to the {@link ng.$rootScope.Scope#$digest digest cycle}.
|
||||
That means a promise's `then`, `catch` and `finally` callback functions are only called after a digest has run.
|
||||
In tests, you can trigger a digest by calling a scope's {@link ng.$rootScope.Scope#$apply `$apply` function}.
|
||||
If you don't have a scope in your test, you can inject the {@link ng.$rootScope $rootScope} and call `$apply` on it.
|
||||
There is also an example of testing promises in the {@link ng.$q#testing `$q` service documentation}.
|
||||
|
||||
## Using `beforeAll()`
|
||||
|
||||
Jasmine's `beforeAll()` and mocha's `before()` hooks are often useful for sharing test setup - either to reduce test run-time or simply to make for more focussed test cases.
|
||||
|
||||
By default, ngMock will create an injector per test case to ensure your tests do not affect each other. However, if we want to use `beforeAll()`, ngMock will have to create the injector before any test cases are run, and share that injector through all the cases for that `describe`. That is where {@link angular.mock.module.sharedInjector module.sharedInjector()} comes in. When it's called within a `describe` block, a single injector is shared between all hooks and test cases run in that block.
|
||||
|
||||
In the example below we are testing a service that takes a long time to generate its answer. To avoid having all of the assertions we want to write in a single test case, {@link angular.mock.module.sharedInjector module.sharedInjector()} and Jasmine's `beforeAll()` are used to run the service only once. The test cases then all make assertions about the properties added to the service instance.
|
||||
|
||||
```javascript
|
||||
describe("Deep Thought", function() {
|
||||
|
||||
module.sharedInjector();
|
||||
|
||||
beforeAll(module("UltimateQuestion"));
|
||||
|
||||
beforeAll(inject(function(DeepThought) {
|
||||
expect(DeepThought.answer).toBeUndefined();
|
||||
DeepThought.generateAnswer();
|
||||
}));
|
||||
|
||||
it("has calculated the answer correctly", inject(function(DeepThought) {
|
||||
// Because of sharedInjector, we have access to the instance of the DeepThought service
|
||||
// that was provided to the beforeAll() hook. Therefore we can test the generated answer
|
||||
expect(DeepThought.answer).toBe(42);
|
||||
}));
|
||||
|
||||
it("has calculated the answer within the expected time", inject(function(DeepThought) {
|
||||
expect(DeepThought.runTimeMillennia).toBeLessThan(8000);
|
||||
}));
|
||||
|
||||
it("has double checked the answer", inject(function(DeepThought) {
|
||||
expect(DeepThought.absolutelySureItIsTheRightAnswer).toBe(true);
|
||||
}));
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
## Sample project
|
||||
See the [angular-seed](https://github.com/angular/angular-seed) project for an example.
|
||||
|
||||
@@ -26,9 +26,9 @@ machine:
|
||||
* [Git](http://git-scm.com/): The [Github Guide to
|
||||
Installing Git](https://help.github.com/articles/set-up-git) is a good source of information.
|
||||
|
||||
* [Node.js](http://nodejs.org): We use Node to generate the documentation, run a
|
||||
* [Node.js v4.x](http://nodejs.org): We use Node to generate the documentation, run a
|
||||
development web server, run tests, and generate distributable files. Depending on your system, you can install Node either from source or as a
|
||||
pre-packaged bundle.
|
||||
pre-packaged bundle. (Currently our build does not work properly on Node v5 or greater - please use v4.x.)
|
||||
|
||||
* [Java](http://www.java.com): We minify JavaScript using our
|
||||
[Closure Tools](https://developers.google.com/closure/) jar. Make sure you have Java (version 7 or higher) installed
|
||||
@@ -40,15 +40,6 @@ and included in your [PATH](http://docs.oracle.com/javase/tutorial/essential/env
|
||||
npm install -g grunt-cli
|
||||
```
|
||||
|
||||
* [Bower](http://bower.io/): We use Bower to manage client-side packages for the docs. Install the `bower` command-line tool globally with:
|
||||
|
||||
```shell
|
||||
npm install -g bower
|
||||
```
|
||||
|
||||
**Note:** You may need to use sudo (for OSX, *nix, BSD etc) or run your command shell as Administrator (for Windows) to install Grunt &
|
||||
Bower globally.
|
||||
|
||||
## Forking Angular on Github
|
||||
|
||||
To create a Github account, follow the instructions [here](https://github.com/signup/free).
|
||||
@@ -62,7 +53,7 @@ minified AngularJS files:
|
||||
|
||||
```shell
|
||||
# Clone your Github repository:
|
||||
git clone "git@github.com:<github username>/angular.js.git"
|
||||
git clone https://github.com/<github username>/angular.js.git
|
||||
|
||||
# Go to the AngularJS directory:
|
||||
cd angular.js
|
||||
|
||||
@@ -2,85 +2,122 @@
|
||||
@name Downloading
|
||||
@description
|
||||
|
||||
# Including angular scripts from the Google CDN
|
||||
The quickest way to get started is to point your html `<script>` tag to a Google CDN URL.
|
||||
# Including Angular scripts from the Google CDN
|
||||
The quickest way to get started is to point your html `<script>` tag to a
|
||||
[Google CDN](https://developers.google.com/speed/libraries/#angularjs) URL.
|
||||
This way, you don't have to download anything or maintain a local copy.
|
||||
|
||||
There are two types of angular script URLs you can point to, one for development and one for
|
||||
There are two types of Angular script URLs you can point to, one for development and one for
|
||||
production:
|
||||
|
||||
* __angular.js__ — This is the human-readable, non-minified version, suitable for web
|
||||
development.
|
||||
* __angular.js__ — This is the human-readable, non-minified version, suitable for web development.
|
||||
* __angular.min.js__ — This is the minified version, which we strongly suggest you use in
|
||||
production.
|
||||
production.
|
||||
|
||||
To point your code to an angular script on the Google CDN server, use the following template. This
|
||||
example points to the minified version 1.4.5:
|
||||
To point your code to an angular script on the Google CDN server, use the following template. This
|
||||
example points to the minified version 1.5.6:
|
||||
|
||||
```
|
||||
```html
|
||||
<!doctype html>
|
||||
<html ng-app>
|
||||
<head>
|
||||
<title>My Angular App</title>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Note that only versions 1.0.1 and above are available on the CDN, if you need an earlier version
|
||||
you can use the <http://code.angularjs.org/> URL which was the previous recommended location for
|
||||
hosted code source. If you're still using the angular server you should switch to the CDN version
|
||||
for even faster loading times.
|
||||
<div class="alert alert-info">
|
||||
Note that only versions 1.0.1 and above are available on the CDN. If you need an earlier version
|
||||
(which you shouldn't) you can use the https://code.angularjs.org/ URL, which was the previous
|
||||
recommended location for hosted code source. If you're still using the Angular server you should
|
||||
switch to the CDN version for even faster loading times.
|
||||
</div>
|
||||
|
||||
<br />
|
||||
# Downloading and hosting angular files locally
|
||||
This option is for those who want to work with angular offline, or those who want to host the
|
||||
angular files on their own servers.
|
||||
This option is for those who want to work with Angular offline, or those who want to host the
|
||||
Angular files on their own servers.
|
||||
|
||||
If you navigate to <http://code.angularjs.org/>, you'll see a directory listing with all of the
|
||||
angular versions since we started releasing versioned build artifacts (quite late in the project
|
||||
lifetime). Each directory contains all artifacts that we released for a particular version.
|
||||
Download the version you want and have fun.
|
||||
If you navigate to https://code.angularjs.org/, you'll see a directory listing with all of the
|
||||
Angular versions since we started releasing versioned build artifacts. Each directory contains all
|
||||
artifacts that we released for a particular version. Download the version you want and have fun.
|
||||
|
||||
Each directory under <http://code.angularjs.org/> includes the following set of files:
|
||||
<div class="alert alert-warning">
|
||||
You can ignore directories starting with `2.` (e.g. `2.0.0-beta.17`) — they are not related to
|
||||
AngularJS. They contain build artifacts from [Angular 2](https://angular.io) versions.
|
||||
</div>
|
||||
|
||||
* __`angular.js`__ — This file is non-obfuscated, non-minified, and human-readable by
|
||||
opening it it any editor or browser. In order to get better error messages during development, you
|
||||
should always use this non-minified angular script.
|
||||
<br />
|
||||
Each directory under https://code.angularjs.org/ includes a set of files that comprise the
|
||||
corresponding version. All JavaScript files (except for `angular-mocks` which is only used during
|
||||
development) come in two flavors — one suitable for development, the other for production:
|
||||
|
||||
* __`angular.min.js`__ — This is a minified and obfuscated version of
|
||||
`angular.js` created with the Closure compiler. Use this version for production in order
|
||||
to minimize the size of the application that is downloaded by your user's browser.
|
||||
* __`<filename>.js`__ — These files are non-obfuscated, non-minified, and human-readable by opening
|
||||
them in any editor or browser. In order to get better error messages during development, you
|
||||
should always use these non-minified scripts.
|
||||
|
||||
* __`angular.zip`__ — This is a zip archive that contains all of the files released
|
||||
for this angular version. Use this file to get everything in a single download.
|
||||
* __`<filename>.min.js`__ — These are minified and obfuscated versions, created with the
|
||||
[Closure compiler](https://developers.google.com/closure/compiler/). Use these versions for
|
||||
production in order to minimize the size of the application that is downloaded by your user's
|
||||
browser.
|
||||
|
||||
* __`angular-mocks.js`__ — This file contains an implementation of mocks that makes
|
||||
testing angular apps even easier. Your unit/integration test harness should load this file after
|
||||
`angular.js` is loaded.
|
||||
* __`<filename>.min.js.map`__ — These are
|
||||
[sourcemap files](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/). You can
|
||||
serve them alongside the `.min.js` files in order to be able to debug the minified code (e.g. on a
|
||||
production deployment) more easily, but without impacting performance.
|
||||
|
||||
* __`angular-scenario.js`__ — This file is a very nifty JavaScript file that allows you
|
||||
to write and execute end-to-end tests for angular applications.
|
||||
<br />
|
||||
The set of files included in each version directory are:
|
||||
|
||||
* __`angular-loader.min.js`__ — Module loader for Angular modules. If you are loading multiple script files containing
|
||||
Angular modules, you can load them asynchronously and in any order as long as you load this file first. Often the
|
||||
contents of this file are copy&pasted into the `index.html` to avoid even the initial request to `angular-loader.min.js`.
|
||||
See [angular-seed](https://github.com/angular/angular-seed/blob/master/app/index-async.html) for an example of usage.
|
||||
* __`angular.zip`__ — This is a zip archive that contains all of the files released for this Angular
|
||||
version. Use this file to get everything in a single download.
|
||||
|
||||
* __Additional Angular modules:__ optional modules with additional functionality. These files should be loaded
|
||||
after the core `angular.js` file:
|
||||
* __`angular-animate.js`__ - Enable animation support
|
||||
* __`angular-cookies.js`__ - A convenient wrapper for reading and writing browser cookies
|
||||
* __`angular-resource.js`__ - Interaction support with RESTful services via the $resource service
|
||||
* __`angular-route.js`__ - Routing and deeplinking services and directives for angular apps
|
||||
* __`angular-sanitize.js`__ - Functionality to sanitize HTML
|
||||
* __`angular-touch.js`__ - Touch events and other helpers for touch-enabled devices
|
||||
* __`angular.js`__ — The core Angular framework. This is all you need to get your Angular app
|
||||
running.
|
||||
|
||||
* __`angular-csp.css`__ — You only need this file if you are using
|
||||
[CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP). See
|
||||
{@link directive:ngCsp here} for more info.
|
||||
|
||||
* __`angular-mocks.js`__ — This file contains an implementation of mocks that makes testing angular
|
||||
apps even easier. Your unit/integration test harness should load this file after `angular.js` is
|
||||
loaded.
|
||||
|
||||
* __`angular-loader.js`__ — Module loader for Angular modules. If you are loading multiple
|
||||
script files containing Angular modules, you can load them asynchronously and in any order as long
|
||||
as you load this file first. Often the contents of this file are copy&pasted into the `index.html`
|
||||
to avoid even the initial request to `angular-loader[.min].js`.
|
||||
See [angular-seed](https://github.com/angular/angular-seed/blob/master/app/index-async.html) for
|
||||
an example of usage.
|
||||
|
||||
* __Additional Angular modules:__ Optional modules with additional functionality. These files
|
||||
should be loaded after the core `angular[.min].js` file:
|
||||
* __`angular-animate.js`__ — Enable animation support. ({@link module:ngAnimate API docs})
|
||||
* __`angular-aria.js`__ — Make your apps [accessible](http://www.w3.org/TR/wai-aria/) to users of
|
||||
assistive technologies. ({@link module:ngAria API docs})
|
||||
* __`angular-cookies.js`__ — A convenient wrapper for reading and writing browser cookies.
|
||||
({@link module:ngCookies API docs})
|
||||
* __`angular-message-format.js`__ — Enhanced support for pluralization and gender specific
|
||||
messages in interpolated text. ({@link module:ngMessageFormat API docs})
|
||||
* __`angular-messages.js`__ — Enhanced support for displaying validation messages.
|
||||
({@link module:ngMessages API docs})
|
||||
* __`angular-parse-ext.js`__ — Allow Unicode characters in identifiers inside Angular expressions.
|
||||
({@link module:ngParseExt API docs})
|
||||
* __`angular-resource.js`__ — Easy interaction with RESTful services.
|
||||
({@link module:ngResource API docs})
|
||||
* __`angular-route.js`__ — Routing and deep-linking services and directives for Angular apps.
|
||||
({@link module:ngRoute API docs})
|
||||
* __`angular-sanitize.js`__ — Functionality to sanitize HTML. ({@link module:ngSanitize API docs})
|
||||
* __`angular-touch.js`__ — Touch events and other helpers for touch-enabled devices.
|
||||
({@link module:ngTouch API docs})
|
||||
|
||||
|
||||
* __`docs`__ — this directory contains all the files that compose the
|
||||
<http://docs.angularjs.org/> documentation app. These files are handy to see the older version of
|
||||
our docs, or even more importantly, view the docs offline.
|
||||
* __`docs/`__ — This directory contains all the files that compose the https://docs.angularjs.org/
|
||||
documentation app. These files are handy to see the older versions of our docs, or even more
|
||||
importantly, view the docs offline.
|
||||
|
||||
* __`i18n`__ - this directory contains locale specific `ngLocale` angular modules to override the defaults
|
||||
defined in the `ng` module.
|
||||
* __`i18n/`__ - This directory contains [locale specific](https://docs.angularjs.org/guide/i18n)
|
||||
`ngLocale` Angular modules to override the defaults defined in the main `ng` module.
|
||||
|
||||
@@ -16,9 +16,9 @@ becoming an Angular expert.
|
||||
starter app with a directory layout, test harness, and scripts to begin building your application.
|
||||
|
||||
|
||||
#Further Steps
|
||||
# Further Steps
|
||||
|
||||
##Watch Videos
|
||||
## Watch Videos
|
||||
|
||||
If you haven’t had a chance to watch the videos from the homepage, please check out:
|
||||
|
||||
@@ -29,13 +29,13 @@ If you haven’t had a chance to watch the videos from the homepage, please chec
|
||||
And visit our [YouTube channel](http://www.youtube.com/user/angularjs) for more AngularJS video presentations and
|
||||
tutorials.
|
||||
|
||||
##Subscribe
|
||||
## Subscribe
|
||||
|
||||
* Subscribe to the [mailing list](http://groups.google.com/forum/?fromgroups#!forum/angular). Ask questions here!
|
||||
* Follow us on [Twitter](https://twitter.com/intent/follow?original_referer=http%3A%2F%2Fangularjs.org%2F®ion=follow_link&screen_name=angularjs&source=followbutton&variant=2.0)
|
||||
* Add us to your circles on [Google+](https://plus.google.com/110323587230527980117/posts)
|
||||
|
||||
##Read more
|
||||
## Read more
|
||||
|
||||
The AngularJS documentation includes the {@link guide/index Developer Guide} covering concepts and the
|
||||
{@link ./api API Reference} for syntax and usage.
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
# PhoneCat Tutorial App
|
||||
|
||||
A great way to get introduced to AngularJS is to work through this tutorial, which walks you through
|
||||
the construction of an AngularJS web app. The app you will build is a catalog that displays a list
|
||||
the construction of an Angular web app. The app you will build is a catalog that displays a list
|
||||
of Android devices, lets you filter the list to see only devices that interest you, and then view
|
||||
details for any device.
|
||||
|
||||
<img class="diagram" src="img/tutorial/catalog_screen.png" width="488" height="413" alt="demo
|
||||
application running in the browser">
|
||||
<img class="diagram" src="img/tutorial/catalog_screen.png" width="488" height="413"
|
||||
alt="demo application running in the browser">
|
||||
|
||||
Follow the tutorial to see how Angular makes browsers smarter — without the use of native
|
||||
extensions or plug-ins:
|
||||
@@ -28,10 +28,11 @@ When you finish the tutorial you will be able to:
|
||||
* Create a dynamic application that works in all modern browsers.
|
||||
* Use data binding to wire up your data model to your views.
|
||||
* Create and run unit tests, with Karma.
|
||||
* Create and run end to end tests, with Protractor.
|
||||
* Move application logic out of the template and into Controllers.
|
||||
* Create and run end-to-end tests, with Protractor.
|
||||
* Move application logic out of the template and into components and controllers.
|
||||
* Get data from a server using Angular services.
|
||||
* Apply animations to your application, using ngAnimate.
|
||||
* Apply animations to your application, using the `ngAnimate` module.
|
||||
* Structure your Angular applications in a modular way that scales well for larger projects.
|
||||
* Identify resources for learning more about AngularJS.
|
||||
|
||||
The tutorial guides you through the entire process of building a simple application, including
|
||||
@@ -42,16 +43,18 @@ You can go through the whole tutorial in a couple of hours or you may want to sp
|
||||
really digging into it. If you're looking for a shorter introduction to AngularJS, check out the
|
||||
{@link misc/started Getting Started} document.
|
||||
|
||||
# Get Started
|
||||
|
||||
# Environment Setup
|
||||
|
||||
The rest of this page explains how you can set up your local machine for development.
|
||||
If you just want to read the tutorial then you can just go straight to the first step:
|
||||
If you just want to _read_ the tutorial, you can go straight to the first step:
|
||||
[Step 0 - Bootstrapping](tutorial/step_00).
|
||||
|
||||
# Working with the code
|
||||
|
||||
## Working with the Code
|
||||
|
||||
You can follow along with this tutorial and hack on the code in the comfort of your own computer.
|
||||
In this way you can get hands-on practice of really writing AngularJS code and also on using the
|
||||
This way, you can get hands-on practice of really writing Angular code and also on using the
|
||||
recommended testing tools.
|
||||
|
||||
The tutorial relies on the use of the [Git][git] versioning system for source code management.
|
||||
@@ -61,11 +64,12 @@ a few git commands.
|
||||
|
||||
### Install Git
|
||||
|
||||
You can download and install Git from http://git-scm.com/download. Once installed you should have
|
||||
access to the `git` command line tool. The main commands that you will need to use are:
|
||||
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.
|
||||
|
||||
- `git clone ...` : clone a remote repository onto your local machine
|
||||
- `git checkout ...` : check out a particular branch or a tagged version of the code to hack on
|
||||
|
||||
### Download angular-phonecat
|
||||
|
||||
@@ -73,13 +77,14 @@ Clone the [angular-phonecat repository][angular-phonecat] located at GitHub by r
|
||||
command:
|
||||
|
||||
```
|
||||
git clone --depth=14 https://github.com/angular/angular-phonecat.git
|
||||
git clone --depth=16 https://github.com/angular/angular-phonecat.git
|
||||
```
|
||||
|
||||
This command creates the `angular-phonecat` directory in your current directory.
|
||||
This command creates an `angular-phonecat` sub-directory in your current directory.
|
||||
|
||||
<div class="alert alert-info">The `--depth=14` option just tells Git to pull down only the last 14 commits. This makes the
|
||||
download much smaller and faster.
|
||||
<div class="alert alert-info">
|
||||
The `--depth=16` option tells Git to pull down only the last 16 commits.
|
||||
This makes the download much smaller and faster.
|
||||
</div>
|
||||
|
||||
Change your current directory to `angular-phonecat`.
|
||||
@@ -88,16 +93,16 @@ Change your current directory to `angular-phonecat`.
|
||||
cd angular-phonecat
|
||||
```
|
||||
|
||||
The tutorial instructions, from now on, assume you are running all commands from the
|
||||
The tutorial instructions, from now on, assume you are running all commands from within the
|
||||
`angular-phonecat` directory.
|
||||
|
||||
|
||||
### Install Node.js
|
||||
|
||||
If you want to run the preconfigured local web-server and the test tools then you will also need
|
||||
[Node.js v0.10.27+][node].
|
||||
If you want to run the preconfigured local web server and the test tools then you will also need
|
||||
[Node.js v4+][node].
|
||||
|
||||
You can download a Node.js installer for your operating system from http://nodejs.org/download/.
|
||||
You can download a Node.js installer for your operating system from https://nodejs.org/en/download/.
|
||||
|
||||
Check the version of Node.js that you have installed by running the following command:
|
||||
|
||||
@@ -105,7 +110,7 @@ Check the version of Node.js that you have installed by running the following co
|
||||
node --version
|
||||
```
|
||||
|
||||
In Debian based distributions, there is a name clash with another utility called `node`. The
|
||||
In Debian based distributions, there might be a name clash with another utility called `node`. The
|
||||
suggested solution is to also install the `nodejs-legacy` apt package, which renames `node` to
|
||||
`nodejs`.
|
||||
|
||||
@@ -115,44 +120,43 @@ nodejs --version
|
||||
npm --version
|
||||
```
|
||||
|
||||
|
||||
<div class="alert alert-info">If you need to run different versions of node.js
|
||||
in your local environment, consider installing
|
||||
<a href="https://github.com/creationix/nvm" title="Node Version Manager Github Repo link">
|
||||
Node Version Manager (nvm)
|
||||
</a>.
|
||||
<div class="alert alert-info">
|
||||
If you need to run different versions of Node.js in your local environment, consider installing
|
||||
[Node Version Manager (nvm)][nvm] or [Node Version Manager (nvm) for Windows][nvm-windows].
|
||||
</div>
|
||||
|
||||
Once you have Node.js installed on your machine you can download the tool dependencies by running:
|
||||
Once you have Node.js installed on your machine, you can download the tool dependencies by running:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
This command reads angular-phonecat's `package.json` file and downloads the following tools
|
||||
into the `node_modules` directory:
|
||||
This command reads angular-phonecat's `package.json` file and downloads the following tools into the
|
||||
`node_modules` directory:
|
||||
|
||||
- [Bower][bower] - client-side code package manager
|
||||
- [Http-Server][http-server] - simple local static web server
|
||||
- [Karma][karma] - unit test runner
|
||||
- [Protractor][protractor] - end to end (E2E) test runner
|
||||
* [Bower][bower] - client-side code package manager
|
||||
* [Http-Server][http-server] - simple local static web server
|
||||
* [Karma][karma] - unit test runner
|
||||
* [Protractor][protractor] - end-to-end (E2E) test runner
|
||||
|
||||
Running `npm install` will also automatically use bower to download the Angular framework into the
|
||||
Running `npm install` will also automatically use bower to download the AngularJS framework into the
|
||||
`app/bower_components` directory.
|
||||
|
||||
<div class="alert alert-info">
|
||||
Note the angular-phonecat project is setup to install and run these utilities via npm scripts.
|
||||
This means that you do not have to have any of these utilities installed globally on your system
|
||||
to follow the tutorial. See **Installing Helper Tools** below for more information.
|
||||
to follow the tutorial. See [Installing Helper Tools](tutorial/#install-helper-tools-optional-)
|
||||
below for more information.
|
||||
</div>
|
||||
|
||||
The project is preconfigured with a number of npm helper scripts to make it easy to run the common
|
||||
tasks that you will need while developing:
|
||||
|
||||
- `npm start` : start a local development web-server
|
||||
- `npm test` : start the Karma unit test runner
|
||||
- `npm run protractor` : run the Protractor end to end (E2E) tests
|
||||
- `npm run update-webdriver` : install the drivers needed by Protractor
|
||||
* `npm start`: Start a local development web server.
|
||||
* `npm test`: Start the Karma unit test runner.
|
||||
* `npm run protractor`: Run the Protractor end-to-end (E2E) tests.
|
||||
* `npm run update-webdriver`: Install the drivers needed by Protractor.
|
||||
|
||||
|
||||
### Install Helper Tools (optional)
|
||||
|
||||
@@ -167,7 +171,7 @@ For instance, to install the Bower command line executable you would do:
|
||||
sudo npm install -g bower
|
||||
```
|
||||
|
||||
*(Omit the sudo if running on Windows)*
|
||||
_(Omit the sudo if running on Windows)_
|
||||
|
||||
Then you can run the bower tool directly, such as:
|
||||
|
||||
@@ -176,10 +180,10 @@ bower install
|
||||
```
|
||||
|
||||
|
||||
### Running Development Web Server
|
||||
### Running the Development Web Server
|
||||
|
||||
While Angular applications are purely client-side code, and it is possible to open them in a web
|
||||
browser directly from the file system, it is better to serve them from a HTTP web server. In
|
||||
browser directly from the file system, it is better to serve them from an HTTP web server. In
|
||||
particular, for security reasons, most modern browsers will not allow JavaScript to make server
|
||||
requests if the page is loaded directly from the file system.
|
||||
|
||||
@@ -190,61 +194,64 @@ application during development. Start the web server by running:
|
||||
npm start
|
||||
```
|
||||
|
||||
This will create a local webserver that is listening to port 8000 on your local machine.
|
||||
You can now browse to the application at:
|
||||
|
||||
```
|
||||
http://localhost:8000/app/index.html
|
||||
```
|
||||
This will create a local web server that is listening to port 8000 on your local machine.
|
||||
You can now browse to the application at http://localhost:8000/index.html.
|
||||
|
||||
<div class="alert alert-info">
|
||||
To serve the web app on a different ip address or port, edit the "start" script within package.json.
|
||||
You can use `-a` to set the address and `-p` to set the port.
|
||||
To serve the web app on a different IP address or port, edit the "start" script within
|
||||
`package.json`. You can use `-a` to set the address and `-p` to set the port. You also need to
|
||||
update the `baseUrl` configuration property in `e2e-test/protractor.conf.js`.
|
||||
</div>
|
||||
|
||||
|
||||
### Running Unit Tests
|
||||
|
||||
We use unit tests to ensure that the JavaScript code in our application is operating correctly.
|
||||
Unit tests focus on testing small isolated parts of the application. The unit tests are kept in the
|
||||
`test/unit` directory.
|
||||
Unit tests focus on testing small isolated parts of the application. The unit tests are kept in test
|
||||
files (specs) side-by-side with the application code. This way it's easier to find them and keep
|
||||
them up-to-date with the code under test. It also makes refactoring our app structure easier, since
|
||||
tests are moved together with the source code.
|
||||
|
||||
The angular-phonecat project is configured to use [Karma][karma] to run the unit tests for the
|
||||
application. Start Karma by running:
|
||||
application. Start Karma by running:
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
This will start the Karma unit test runner. Karma will read the configuration file at
|
||||
`test/karma.conf.js`. This configuration file tells Karma to:
|
||||
This will start the Karma unit test runner. Karma will read the configuration file `karma.conf.js`,
|
||||
located at the root of the project directory. This configuration file tells Karma to:
|
||||
|
||||
- open up a Chrome browser and connect it to Karma
|
||||
- execute all the unit tests in this browser
|
||||
- report the results of these tests in the terminal/command line window
|
||||
- watch all the project's JavaScript files and re-run the tests whenever any of these change
|
||||
* Open up instances of the Chrome and Firefox browsers and connect them to Karma.
|
||||
* Execute all the unit tests in these browsers.
|
||||
* Report the results of these tests in the terminal/command line window.
|
||||
* Watch all the project's JavaScript files and re-run the tests whenever any of these change.
|
||||
|
||||
It is good to leave this running all the time, in the background, as it will give you immediate
|
||||
feedback about whether your changes pass the unit tests while you are working on the code.
|
||||
|
||||
|
||||
### Running End to End Tests
|
||||
### Running E2E Tests
|
||||
|
||||
We use End to End tests to ensure that the application as a whole operates as expected.
|
||||
End to End tests are designed to test the whole client side application, in particular that the
|
||||
views are displaying and behaving correctly. It does this by simulating real user interaction with
|
||||
the real application running in the browser.
|
||||
We use E2E (end-to-end) tests to ensure that the application as a whole operates as expected.
|
||||
E2E tests are designed to test the whole client-side application, in particular that the views are
|
||||
displaying and behaving correctly. It does this by simulating real user interaction with the real
|
||||
application running in the browser.
|
||||
|
||||
The End to End tests are kept in the `test/e2e` directory.
|
||||
The E2E tests are kept in the `e2e-tests` directory.
|
||||
|
||||
The angular-phonecat project is configured to use [Protractor][protractor] to run the End to End
|
||||
tests for the application. Protractor relies upon a set of drivers to allow it to interact with
|
||||
the browser. You can install these drivers by running:
|
||||
The angular-phonecat project is configured to use [Protractor][protractor] to run the E2E tests for
|
||||
the application. Protractor relies upon a set of drivers to allow it to interact with the browser.
|
||||
You can install these drivers by running:
|
||||
|
||||
```
|
||||
npm run update-webdriver
|
||||
```
|
||||
|
||||
*(You should only need to do this once.)*
|
||||
<div class="alert alert-info">
|
||||
You don't have to manually run this command. Our npm scripts are configured so that it will be
|
||||
automatically executed as part of the command that runs the E2E tests.
|
||||
</div>
|
||||
|
||||
Since Protractor works by interacting with a running application, we need to start our web server:
|
||||
|
||||
@@ -252,31 +259,79 @@ Since Protractor works by interacting with a running application, we need to sta
|
||||
npm start
|
||||
```
|
||||
|
||||
Then in a separate terminal/command line window, we can run the Protractor test scripts against the
|
||||
application by running:
|
||||
Then, in a _separate_ terminal/command line window, we can run the Protractor test scripts against
|
||||
the application by running:
|
||||
|
||||
```
|
||||
npm run protractor
|
||||
```
|
||||
|
||||
Protractor will read the configuration file at `test/protractor-conf.js`. This configuration tells
|
||||
Protractor to:
|
||||
Protractor will read the configuration file at `e2e-tests/protractor.conf.js`. This configuration
|
||||
file tells Protractor to:
|
||||
|
||||
- open up a Chrome browser and connect it to the application
|
||||
- execute all the End to End tests in this browser
|
||||
- report the results of these tests in the terminal/command line window
|
||||
- close down the browser and exit
|
||||
* Open up a Chrome browser and connect it to the application.
|
||||
* Execute all the E2E tests in this browser.
|
||||
* Report the results of these tests in the terminal/command line window.
|
||||
* Close the browser and exit.
|
||||
|
||||
It is good to run the end to end tests whenever you make changes to the HTML views or want to check
|
||||
that the application as a whole is executing correctly. It is very common to run End to End tests
|
||||
before pushing a new commit of changes to a remote repository.
|
||||
It is good to run the E2E tests whenever you make changes to the HTML views or want to check that
|
||||
the application as a whole is executing correctly. It is very common to run E2E tests before pushing
|
||||
a new commit of changes to a remote repository.
|
||||
|
||||
|
||||
### Common Issues
|
||||
|
||||
<br />
|
||||
**Firewall / Proxy issues**
|
||||
|
||||
Git and other tools, often use the `git:` protocol for accessing files in remote repositories.
|
||||
Some firewall configurations are blocking `git://` URLs, which leads to errors when trying to clone
|
||||
repositories or download dependencies. (For example corporate firewalls are "notorious" for blocking
|
||||
`git:`.)
|
||||
|
||||
If you run into this issue, you can force the use of `https:` instead, by running the following
|
||||
command: `git config --global url."https://".insteadOf git://`
|
||||
|
||||
<br />
|
||||
**Updating WebDriver takes too long**
|
||||
|
||||
Running `update-webdriver` for the first time may take from several seconds up to a few minutes
|
||||
(depending on your hardware and network connection). If you cancel the operation (e.g. using
|
||||
`Ctrl+C`), you might get errors, when trying to run Protractor later.
|
||||
|
||||
In that case, you can delete the `node_modules/` directory and run `npm install` again.
|
||||
|
||||
<br />
|
||||
**Protractor dependencies**
|
||||
|
||||
Under the hood, Protractor uses the [Selenium Stadalone Server][selenium], which in turn requires
|
||||
the [Java Development Kit (JDK)][jdk] to be installed on your local machine. Check this by running
|
||||
`java -version` from the command line.
|
||||
|
||||
If JDK is not already installed, you can download it [here][jdk-download].
|
||||
|
||||
<br />
|
||||
**Error running the web server**
|
||||
|
||||
The web server is configured to use port 8000. If the port is already in use (for example by another
|
||||
instance of a running web server) you will get an `EADDRINUSE` error. Make sure the port is
|
||||
available, before running `npm start`.
|
||||
|
||||
<hr />
|
||||
|
||||
Now that you have set up your local machine, let's get started with the tutorial:
|
||||
{@link step_00 Step 0 - Bootstrapping}
|
||||
|
||||
Now that you have set up your local machine, let's get started with the tutorial: {@link step_00 Step 0 - Bootstrapping}
|
||||
|
||||
[git]: http://git-scm.com/
|
||||
[node]: http://nodejs.org/
|
||||
[angular-phonecat]: https://github.com/angular/angular-phonecat
|
||||
[protractor]: https://github.com/angular/protractor
|
||||
[bower]: http://bower.io/
|
||||
[git]: http://git-scm.com/
|
||||
[http-server]: https://github.com/nodeapps/http-server
|
||||
[karma]: https://github.com/karma-runner/karma
|
||||
[jdk]: https://en.wikipedia.org/wiki/Java_Development_Kit
|
||||
[jdk-download]: http://www.oracle.com/technetwork/java/javase/downloads/index.html
|
||||
[karma]: https://karma-runner.github.io/
|
||||
[node]: http://nodejs.org/
|
||||
[nvm]: https://github.com/creationix/nvm
|
||||
[nvm-windows]: https://github.com/coreybutler/nvm-windows
|
||||
[protractor]: https://github.com/angular/protractor
|
||||
[selenium]: http://docs.seleniumhq.org/
|
||||
|
||||
@@ -6,129 +6,144 @@
|
||||
<ul doc-tutorial-nav="0"></ul>
|
||||
|
||||
|
||||
You are now ready to build the AngularJS phonecat app. In this step, you will become familiar
|
||||
with the most important source code files, learn how to start the development servers bundled with
|
||||
angular-seed, and run the application in the browser.
|
||||
In this step of the tutorial, you will become familiar with the most important source code files of
|
||||
the AngularJS Phonecat App. You will also learn how to start the development servers bundled with
|
||||
[angular-seed][angular-seed], and run the application in the browser.
|
||||
|
||||
Before you continue, make sure you have set up your development environment and installed all
|
||||
necessary dependencies, as described in the {@link tutorial/#environment-setup Environment Setup}
|
||||
section.
|
||||
|
||||
In `angular-phonecat` directory, run this command:
|
||||
In the `angular-phonecat` directory, run this command:
|
||||
|
||||
```
|
||||
git checkout -f step-0
|
||||
```
|
||||
|
||||
|
||||
This resets your workspace to step 0 of the tutorial app.
|
||||
|
||||
You must repeat this for every future step in the tutorial and change the number to the number of
|
||||
the step you are on. This will cause any changes you made within your working directory to be lost.
|
||||
|
||||
If you haven't already done so you need to install the dependencies by running:
|
||||
If you haven't already done so, you need to install the dependencies by running:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
To see the app running in a browser, open a *separate* terminal/command line tab or window, then
|
||||
run `npm start` to start the web server. Now, open a browser window for the app and navigate to
|
||||
<a href="http://localhost:8000/app/" target="_blank" title="Open app on localhost">`http://localhost:8000/app/`</a>
|
||||
To see the app running in a browser, open a _separate_ terminal/command line tab or window, then run
|
||||
`npm start` to start the web server. Now, open a browser window for the app and navigate to
|
||||
http://localhost:8000/index.html.
|
||||
|
||||
Note that if you already ran the master branch app prior to checking out step-0, you may see the cached
|
||||
master version of the app in your browser window at this point. Just hit refresh to re-load the page.
|
||||
Note that if you already ran the master branch app prior to checking out step-0, you may see the
|
||||
cached master version of the app in your browser window at this point. Just hit refresh to re-load
|
||||
the page.
|
||||
|
||||
You can now see the page in your browser. It's not very exciting, but that's OK.
|
||||
|
||||
The HTML page that displays "Nothing here yet!" was constructed with the HTML code shown below.
|
||||
The code contains some key Angular elements that we will need as we progress.
|
||||
|
||||
__`app/index.html`:__
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en" ng-app>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My HTML File</title>
|
||||
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
|
||||
<link rel="stylesheet" href="css/app.css">
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My HTML File</title>
|
||||
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css" />
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>Nothing here {{'yet' + '!'}}</p>
|
||||
<p>Nothing here {{'yet' + '!'}}</p>
|
||||
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
|
||||
|
||||
## What is the code doing?
|
||||
|
||||
**`ng-app` directive:**
|
||||
<br />
|
||||
**`ng-app` attribute:**
|
||||
|
||||
<html ng-app>
|
||||
```html
|
||||
<html ng-app>
|
||||
```
|
||||
|
||||
The `ng-app` attribute represents an Angular directive named `ngApp` (Angular uses
|
||||
`spinal-case` for its custom attributes and `camelCase` for the corresponding directives
|
||||
which implement them).
|
||||
This directive is used to flag the html element that Angular should consider to be the root element
|
||||
of our application.
|
||||
This gives application developers the freedom to tell Angular if the entire html page or only a
|
||||
portion of it should be treated as the Angular application.
|
||||
The `ng-app` attribute represents an Angular directive, named `ngApp` (Angular uses `kebab-case` for
|
||||
its custom attributes and `camelCase` for the corresponding directives which implement them). This
|
||||
directive is used to flag the HTML element that Angular should consider to be the root element of
|
||||
our application. This gives application developers the freedom to tell Angular if the entire HTML
|
||||
page or only a portion of it should be treated as the AngularJS application.
|
||||
|
||||
**AngularJS script tag:**
|
||||
For more info on `ngApp`, check out the {@link ngApp API Reference}.
|
||||
|
||||
<script src="bower_components/angular/angular.js">
|
||||
<br />
|
||||
**`angular.js` script tag:**
|
||||
|
||||
This code downloads the `angular.js` script which registers a callback that will be executed by the
|
||||
```html
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
```
|
||||
|
||||
This code downloads the `angular.js` script which registers a callback that will be executed by the
|
||||
browser when the containing HTML page is fully downloaded. When the callback is executed, Angular
|
||||
looks for the {@link ng.directive:ngApp ngApp} directive. If
|
||||
Angular finds the directive, it will bootstrap the application with the root of the application DOM
|
||||
being the element on which the `ngApp` directive was defined.
|
||||
looks for the {@link ngApp ngApp} directive. If Angular finds the directive, it will bootstrap the
|
||||
application with the root of the application DOM being the element on which the `ngApp` directive
|
||||
was defined.
|
||||
|
||||
For more info on bootstrapping your app, checkout the [Bootstrap](guide/bootstrap) section of the
|
||||
Developer Guide.
|
||||
|
||||
<br />
|
||||
**Double-curly binding with an expression:**
|
||||
|
||||
Nothing here {{'yet' + '!'}}
|
||||
```html
|
||||
Nothing here {{'yet' + '!'}}
|
||||
```
|
||||
|
||||
This line demonstrates two core features of Angular's templating capabilities:
|
||||
|
||||
* a binding, denoted by double-curlies `{{ }}`
|
||||
* a simple expression `'yet' + '!'` used in this binding.
|
||||
* A binding, denoted by double-curlies: `{{ }}`
|
||||
* A simple expression used in this binding: `'yet' + '!'`
|
||||
|
||||
The binding tells Angular that it should evaluate an expression and insert the result into the
|
||||
DOM in place of the binding. Rather than a one-time insert, as we'll see in the next steps, a
|
||||
binding will result in efficient continuous updates whenever the result of the expression
|
||||
evaluation changes.
|
||||
The binding tells Angular that it should evaluate an expression and insert the result into the DOM
|
||||
in place of the binding. As we will see in the next steps, rather than a one-time insert, a binding
|
||||
will result in efficient continuous updates whenever the result of the expression evaluation
|
||||
changes.
|
||||
|
||||
{@link guide/expression Angular expression} is a JavaScript-like code snippet that is
|
||||
evaluated by Angular in the context of the current model scope, rather than within the scope of
|
||||
the global context (`window`).
|
||||
{@link guide/expression Angular expressions} are JavaScript-like code snippets that are evaluated by
|
||||
Angular in the context of the current model scope, rather than within the scope of the global
|
||||
context (`window`).
|
||||
|
||||
As expected, once this template is processed by Angular, the html page contains the text:
|
||||
"Nothing here yet!".
|
||||
As expected, once this template is processed by Angular, the HTML page contains the text:
|
||||
|
||||
## Bootstrapping AngularJS apps
|
||||
```
|
||||
Nothing here yet!
|
||||
```
|
||||
|
||||
Bootstrapping AngularJS apps automatically using the `ngApp` directive is very easy and suitable
|
||||
for most cases. In advanced cases, such as when using script loaders, you can use the
|
||||
{@link guide/bootstrap imperative / manual way} to bootstrap the app.
|
||||
## Bootstrapping Angular Applications
|
||||
|
||||
There are 3 important things that happen during the app bootstrap:
|
||||
Bootstrapping Angular applications automatically using the `ngApp` directive is very easy and
|
||||
suitable for most cases. In advanced cases, such as when using script loaders, you can use the
|
||||
{@link guide/bootstrap#manual-initialization imperative/manual way} to bootstrap the application.
|
||||
|
||||
There are 3 important things that happen during the bootstrap phase:
|
||||
|
||||
1. The {@link auto.$injector injector} that will be used for dependency injection is created.
|
||||
|
||||
2. The injector will then create the {@link ng.$rootScope root scope} that will
|
||||
become the context for the model of our application.
|
||||
2. The injector will then create the {@link ng.$rootScope root scope} that will become the context
|
||||
for the model of our application.
|
||||
|
||||
3. Angular will then "compile" the DOM starting at the `ngApp` root element, processing any
|
||||
directives and bindings found along the way.
|
||||
|
||||
|
||||
Once an application is bootstrapped, it will then wait for incoming browser events (such as mouse
|
||||
click, key press or incoming HTTP response) that might change the model. Once such an event occurs,
|
||||
Angular detects if it caused any model changes and if changes are found, Angular will reflect them
|
||||
in the view by updating all of the affected bindings.
|
||||
clicks, key presses or incoming HTTP responses) that might change the model. Once such an event
|
||||
occurs, Angular detects if it caused any model changes and if changes are found, Angular will
|
||||
reflect them in the view by updating all of the affected bindings.
|
||||
|
||||
The structure of our application is currently very simple. The template contains just one directive
|
||||
and one static binding, and our model is empty. That will soon change!
|
||||
@@ -138,27 +153,29 @@ and one static binding, and our model is empty. That will soon change!
|
||||
|
||||
## What are all these files in my working directory?
|
||||
|
||||
|
||||
Most of the files in your working directory come from the [angular-seed project][angular-seed] which
|
||||
is typically used to bootstrap new Angular projects. The seed project is pre-configured to install
|
||||
the angular framework (via `bower` into the `app/bower_components/` folder) and tools for developing
|
||||
a typical web app (via `npm`).
|
||||
Most of the files in your working directory come from the [angular-seed project][angular-seed],
|
||||
which is typically used to bootstrap new AngularJS projects. The seed project is pre-configured to
|
||||
install the AngularJS framework (via `bower` into the `app/bower_components/` directory) and tools
|
||||
for developing and testing a typical web application (via `npm`).
|
||||
|
||||
For the purposes of this tutorial, we modified the angular-seed with the following changes:
|
||||
|
||||
* Removed the example app
|
||||
* Added phone images to `app/img/phones/`
|
||||
* Added phone data files (JSON) to `app/phones/`
|
||||
* Removed the example app.
|
||||
* Removed unused dependencies.
|
||||
* Added phone images to `app/img/phones/`.
|
||||
* Added phone data files (JSON) to `app/phones/`.
|
||||
* Added a dependency on [Bootstrap](http://getbootstrap.com) in the `bower.json` file.
|
||||
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Try adding a new expression to the `index.html` that will do some math:
|
||||
<div></div>
|
||||
|
||||
<p>1 + 2 = {{ 1 + 2 }}</p>
|
||||
* Try adding a new expression to `index.html` that will do some math:
|
||||
|
||||
```html
|
||||
<p>1 + 2 = {{1 + 2}}</p>
|
||||
```
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
@@ -12,11 +12,11 @@ dynamically display the same result with any set of data.
|
||||
|
||||
In this step you will add some basic information about two cell phones to an HTML page.
|
||||
|
||||
- The page now contains a list with information about two phones.
|
||||
* The page now contains a list with information about two phones.
|
||||
|
||||
<div doc-tutorial-reset="1"></div>
|
||||
|
||||
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
@@ -39,15 +39,19 @@ In this step you will add some basic information about two cell phones to an HTM
|
||||
|
||||
# Experiments
|
||||
|
||||
<div></div>
|
||||
|
||||
* Try adding more static HTML to `index.html`. For example:
|
||||
|
||||
<p>Total number of phones: 2</p>
|
||||
```html
|
||||
<p>Total number of phones: 2</p>
|
||||
```
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
This addition to your app uses static HTML to display the list. Now, let's go to {@link step_02
|
||||
step 2} to learn how to use AngularJS to dynamically generate the same list.
|
||||
This addition to your app uses static HTML to display the list. Now, let's go to
|
||||
{@link step_02 step 2} to learn how to use Angular to dynamically generate the same list.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="1"></ul>
|
||||
|
||||
+183
-113
@@ -6,37 +6,38 @@
|
||||
<ul doc-tutorial-nav="2"></ul>
|
||||
|
||||
|
||||
Now it's time to make the web page dynamic — with AngularJS. We'll also add a test that verifies the
|
||||
code for the controller we are going to add.
|
||||
Now, it's time to make the web page dynamic — with AngularJS. We will also add a test that verifies
|
||||
the code for the controller we are going to add.
|
||||
|
||||
There are many ways to structure the code for an application. For Angular apps, we encourage the use of
|
||||
[the Model-View-Controller (MVC) design pattern](http://en.wikipedia.org/wiki/Model–View–Controller)
|
||||
to decouple the code and to separate concerns. With that in mind, let's use a little Angular and
|
||||
JavaScript to add model, view, and controller components to our app.
|
||||
There are many ways to structure the code for an application. For Angular applications, we encourage
|
||||
the use of the [Model-View-Controller (MVC) design pattern][mvc-pattern] to decouple the code and
|
||||
separate concerns. With that in mind, let's use a little Angular and JavaScript to add models,
|
||||
views, and controllers to our app.
|
||||
|
||||
- The list of three phones is now generated dynamically from data
|
||||
* The list of three phones is now generated dynamically from data
|
||||
|
||||
<div doc-tutorial-reset="2"></div>
|
||||
|
||||
|
||||
## View and Template
|
||||
|
||||
In Angular, the __view__ is a projection of the model through the HTML __template__. This means that
|
||||
In Angular, the **view** is a projection of the model through the HTML **template**. This means that
|
||||
whenever the model changes, Angular refreshes the appropriate binding points, which updates the
|
||||
view.
|
||||
|
||||
The view component is constructed by Angular from this template:
|
||||
The view is constructed by Angular from this template.
|
||||
|
||||
__`app/index.html`:__
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
<html ng-app="phonecatApp">
|
||||
<head>
|
||||
...
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="js/controllers.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</head>
|
||||
<body ng-controller="PhoneListCtrl">
|
||||
<body ng-controller="PhoneListController">
|
||||
|
||||
<ul>
|
||||
<li ng-repeat="phone in phones">
|
||||
@@ -49,95 +50,117 @@ __`app/index.html`:__
|
||||
</html>
|
||||
```
|
||||
|
||||
We replaced the hard-coded phone list with the {@link ng.directive:ngRepeat ngRepeat directive}
|
||||
and two {@link guide/expression Angular expressions}:
|
||||
We replaced the hard-coded phone list with the {@link ngRepeat ngRepeat} directive and two
|
||||
{@link guide/expression Angular expressions}:
|
||||
|
||||
* The `ng-repeat="phone in phones"` attribute in the `<li>` tag is an Angular repeater directive.
|
||||
The repeater tells Angular to create a `<li>` element for each phone in the list using the `<li>`
|
||||
tag as the template.
|
||||
* The expressions wrapped in curly braces (`{{phone.name}}` and `{{phone.snippet}}`) will be replaced
|
||||
by the value of the expressions.
|
||||
* The `ng-repeat="phone in phones"` attribute on the `<li>` tag is an Angular repeater directive.
|
||||
The repeater tells Angular to create a `<li>` element for each phone in the list, using the `<li>`
|
||||
tag as the template.
|
||||
* The expressions wrapped in curly braces (`{{phone.name}}` and `{{phone.snippet}}`) will be
|
||||
replaced by the values of the expressions.
|
||||
|
||||
We have added a new directive, called `ng-controller`, which attaches a `PhoneListCtrl`
|
||||
__controller__ to the <body> tag. At this point:
|
||||
We have also added a new directive, called {@link ngController ngController}, which attaches a
|
||||
`PhoneListController` **controller** to the `<body>` tag. At this point:
|
||||
|
||||
* The expressions in curly braces (`{{phone.name}}` and `{{phone.snippet}}` denote
|
||||
bindings, which are referring to our application model, which is set up in our `PhoneListCtrl`
|
||||
controller.
|
||||
* `PhoneListController` is in charge of the DOM sub-tree under (and including) the `<body>` element.
|
||||
* The expressions in curly braces (`{{phone.name}}` and `{{phone.snippet}}`) denote bindings, which
|
||||
are referring to our application model, which is set up in our `PhoneListController` controller.
|
||||
|
||||
<div class="alert alert-info">
|
||||
Note: We have specified an {@link angular.Module Angular Module} to load using `ng-app="phonecatApp"`,
|
||||
where `phonecatApp` is the name of our module. This module will contain the `PhoneListCtrl`.
|
||||
Note: We have specified an {@link angular.Module Angular Module} to load using
|
||||
`ng-app="phonecatApp"`, where `phonecatApp` is the name of our module. This module will contain
|
||||
the `PhoneListController`.
|
||||
</div>
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_02.png">
|
||||
|
||||
## Model and Controller
|
||||
|
||||
The data __model__ (a simple array of phones in object literal notation) is now instantiated within
|
||||
the `PhoneListCtrl` __controller__. The __controller__ is simply a constructor function that takes a
|
||||
`$scope` parameter:
|
||||
The data **model** (a simple array of phones in object literal notation) is now instantiated within
|
||||
the `PhoneListController` **controller**. The **controller** is simply a constructor function that
|
||||
takes a `$scope` parameter:
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
<br />
|
||||
**`app/app.js`:**
|
||||
|
||||
```js
|
||||
|
||||
// Define the `phonecatApp` module
|
||||
var phonecatApp = angular.module('phonecatApp', []);
|
||||
|
||||
phonecatApp.controller('PhoneListCtrl', function ($scope) {
|
||||
// Define the `PhoneListController` controller on the `phonecatApp` module
|
||||
phonecatApp.controller('PhoneListController', function PhoneListController($scope) {
|
||||
$scope.phones = [
|
||||
{'name': 'Nexus S',
|
||||
'snippet': 'Fast just got faster with Nexus S.'},
|
||||
{'name': 'Motorola XOOM™ with Wi-Fi',
|
||||
'snippet': 'The Next, Next Generation tablet.'},
|
||||
{'name': 'MOTOROLA XOOM™',
|
||||
'snippet': 'The Next, Next Generation tablet.'}
|
||||
{
|
||||
name: 'Nexus S',
|
||||
snippet: 'Fast just got faster with Nexus S.'
|
||||
}, {
|
||||
name: 'Motorola XOOM™ with Wi-Fi',
|
||||
snippet: 'The Next, Next Generation tablet.'
|
||||
}, {
|
||||
name: 'MOTOROLA XOOM™',
|
||||
snippet: 'The Next, Next Generation tablet.'
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
Here we declared a controller called `PhoneListCtrl` and registered it in an AngularJS
|
||||
module, `phonecatApp`. Notice that our `ng-app` directive (on the `<html>` tag) now specifies the `phonecatApp`
|
||||
module name as the module to load when bootstrapping the Angular application.
|
||||
Here we declared a controller called `PhoneListController` and registered it in an Angular module,
|
||||
`phonecatApp`. Notice that our `ngApp` directive (on the `<html>` tag) now specifies the
|
||||
`phonecatApp` module name as the module to load when bootstrapping the application.
|
||||
|
||||
Although the controller is not yet doing very much, it plays a crucial role. By providing context
|
||||
for our data model, the controller allows us to establish data-binding between
|
||||
the model and the view. We connected the dots between the presentation, data, and logic components
|
||||
as follows:
|
||||
for our data model, the controller allows us to establish data-binding between the model and the
|
||||
view. We connected the dots between the presentation, data, and logic components as follows:
|
||||
|
||||
* The {@link ng.directive:ngController ngController} directive, located on the `<body>` tag,
|
||||
references the name of our controller, `PhoneListCtrl` (located in the JavaScript file
|
||||
`controllers.js`).
|
||||
* The {@link ngController ngController} directive, located on the `<body>` tag, references the name
|
||||
of our controller, `PhoneListController` (located in the JavaScript file `app.js`).
|
||||
|
||||
* The `PhoneListController` controller attaches the phone data to the `$scope` that was injected
|
||||
into our controller function. This _scope_ is a prototypal descendant of the _root scope_ that was
|
||||
created when the application was defined. This controller scope is available to all bindings
|
||||
located within the `<body ng-controller="PhoneListController">` tag.
|
||||
|
||||
* The `PhoneListCtrl` controller attaches the phone data to the `$scope` that was injected into our
|
||||
controller function. This *scope* is a prototypical descendant of the *root scope* that was created
|
||||
when the application was defined. This controller scope is available to all bindings located within
|
||||
the `<body ng-controller="PhoneListCtrl">` tag.
|
||||
|
||||
### Scope
|
||||
|
||||
The concept of a scope in Angular is crucial. A scope can be seen as the glue which allows the
|
||||
template, model and controller to work together. Angular uses scopes, along with the information
|
||||
template, model, and controller to work together. Angular uses scopes, along with the information
|
||||
contained in the template, data model, and controller, to keep models and views separate, but in
|
||||
sync. Any changes made to the model are reflected in the view; any changes that occur in the view
|
||||
are reflected in the model.
|
||||
|
||||
To learn more about Angular scopes, see the {@link ng.$rootScope.Scope angular scope documentation}.
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_02.png">
|
||||
|
||||
## Tests
|
||||
<div class="alert alert-warning">
|
||||
<p>
|
||||
Angular scopes prototypally inherit from their parent scope, all the way up to the *root scope*
|
||||
of the application. As a result, assigning values directly on the scope makes it easy to share
|
||||
data across different parts of the page and create interactive applications.
|
||||
While this approach works for prototypes and smaller applications, it quickly leads to tight
|
||||
coupling and difficulty to reason about changes in our data model.
|
||||
</p>
|
||||
<p>
|
||||
In the next step, we will learn how to better organize our code, by "packaging" related pieces
|
||||
of application and presentation logic into isolated, reusable entities, called components.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
The "Angular way" of separating controller from the view, makes it easy to test code as it is being
|
||||
developed. If our controller is available on the global namespace then we could simply instantiate it
|
||||
with a mock `scope` object:
|
||||
developed. If our controller were available on the global namespace, we could simply instantiate it
|
||||
with a mock scope object:
|
||||
|
||||
<br />
|
||||
```js
|
||||
describe('PhoneListCtrl', function(){
|
||||
describe('PhoneListController', function() {
|
||||
|
||||
it('should create "phones" model with 3 phones', function() {
|
||||
var scope = {},
|
||||
ctrl = new PhoneListCtrl(scope);
|
||||
it('should create a `phones` model with 3 phones', function() {
|
||||
var scope = {};
|
||||
var ctrl = new PhoneListController(scope);
|
||||
|
||||
expect(scope.phones.length).toBe(3);
|
||||
});
|
||||
@@ -145,30 +168,31 @@ describe('PhoneListCtrl', function(){
|
||||
});
|
||||
```
|
||||
|
||||
The test instantiates `PhoneListCtrl` and verifies that the phones array property on the scope
|
||||
contains three records. This example demonstrates how easy it is to create a unit test for code in
|
||||
Angular. Since testing is such a critical part of software development, we make it easy to create
|
||||
tests in Angular so that developers are encouraged to write them.
|
||||
The test instantiates `PhoneListController` and verifies that the phones array property on the
|
||||
scope contains three records. This example demonstrates how easy it is to create a unit test for
|
||||
code in Angular. Since testing is such a critical part of software development, we make it easy to
|
||||
create tests in Angular so that developers are encouraged to write them.
|
||||
|
||||
### Testing non-Global Controllers
|
||||
|
||||
In practice, you will not want to have your controller functions in the global namespace. Instead,
|
||||
you can see that we have registered it via an anonymous constructor function on the `phonecatApp`
|
||||
module.
|
||||
## Testing non-global Controllers
|
||||
|
||||
In practice, you will not want to have your controller functions in the global namespace. Instead,
|
||||
you can see that we have registered it via a constructor function on the `phonecatApp` module.
|
||||
|
||||
In this case Angular provides a service, `$controller`, which will retrieve your controller by name.
|
||||
Here is the same test using `$controller`:
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
<br />
|
||||
**`app/app.spec.js`:**
|
||||
|
||||
```js
|
||||
describe('PhoneListCtrl', function(){
|
||||
describe('PhoneListController', function() {
|
||||
|
||||
beforeEach(module('phonecatApp'));
|
||||
|
||||
it('should create "phones" model with 3 phones', inject(function($controller) {
|
||||
var scope = {},
|
||||
ctrl = $controller('PhoneListCtrl', {$scope:scope});
|
||||
it('should create a `phones` model with 3 phones', inject(function($controller) {
|
||||
var scope = {};
|
||||
var ctrl = $controller('PhoneListController', {$scope: scope});
|
||||
|
||||
expect(scope.phones.length).toBe(3);
|
||||
}));
|
||||
@@ -177,46 +201,78 @@ describe('PhoneListCtrl', function(){
|
||||
```
|
||||
|
||||
* Before each test we tell Angular to load the `phonecatApp` module.
|
||||
* We ask Angular to `inject` the `$controller` service into our test function
|
||||
* We use `$controller` to create an instance of the `PhoneListCtrl`
|
||||
* We ask Angular to `inject` the `$controller` service into our test function.
|
||||
* We use `$controller` to create an instance of the `PhoneListController`.
|
||||
* With this instance, we verify that the phones array property on the scope contains three records.
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>**A note on file naming:**</p>
|
||||
<p>
|
||||
As already mentioned in the [introduction](tutorial/#running-unit-tests), the unit test files
|
||||
(specs) are kept side-by-side with the application code. We name our specs after the file
|
||||
containing the code to be tested plus a specific suffix to distinguish them from files
|
||||
containing application code. Note that test files are still plain JavaScript files, so they have
|
||||
a `.js` file extension.
|
||||
</p>
|
||||
<p>
|
||||
In this tutorial, we are using the `.spec` suffix. So the test file corresponding to
|
||||
`something.js` would be called `something.spec.js`.
|
||||
(Another common convention is to use a `_spec` or `_test` suffix.)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
### Writing and Running Tests
|
||||
|
||||
Angular developers prefer the syntax of Jasmine's Behavior-driven Development (BDD) framework when
|
||||
writing tests. Although Angular does not require you to use Jasmine, we wrote all of the tests in
|
||||
this tutorial in Jasmine v1.3. You can learn about Jasmine on the [Jasmine home page][jasmine] and
|
||||
at the [Jasmine docs][jasmine-docs].
|
||||
## Writing and Running Tests
|
||||
|
||||
The angular-seed project is pre-configured to run unit tests using [Karma][karma] but you will need
|
||||
Many Angular developers prefer the syntax of
|
||||
[Jasmine's Behavior-Driven Development (BDD) framework][jasmine-home], when writing tests. Although
|
||||
Angular does not require you to use Jasmine, we wrote all of the tests in this tutorial in Jasmine
|
||||
v2.4. You can learn about Jasmine on the [Jasmine home page][jasmine-home] and at the
|
||||
[Jasmine docs][jasmine-docs].
|
||||
|
||||
The angular-seed project is pre-configured to run unit tests using [Karma][karma], but you will need
|
||||
to ensure that Karma and its necessary plugins are installed. You can do this by running
|
||||
`npm install`.
|
||||
|
||||
To run the tests, and then watch the files for changes: `npm test`.
|
||||
To run the tests, and then watch the files for changes execute: `npm test`
|
||||
|
||||
* Karma will start new instances of Chrome and Firefox browsers automatically. Just ignore them and
|
||||
let them run in the background. Karma will use these browsers for test execution.
|
||||
* If you only have one of the browsers installed on your machine (either Chrome or Firefox), make
|
||||
sure to update the karma configuration file (`karma.conf.js`), before running the test. Locate the
|
||||
configuration file in the root directory and update the `browsers` property.
|
||||
|
||||
E.g. if you only have Chrome installed:
|
||||
<pre>
|
||||
...
|
||||
browsers: ['Chrome'],
|
||||
...
|
||||
</pre>
|
||||
|
||||
* Karma will start a new instance of Chrome browser automatically. Just ignore it and let it run in
|
||||
the background. Karma will use this browser for test execution.
|
||||
* You should see the following or similar output in the terminal:
|
||||
|
||||
<pre>
|
||||
info: Karma server started at http://localhost:9876/
|
||||
info (launcher): Starting browser "Chrome"
|
||||
info (Chrome 22.0): Connected on socket id tPUm9DXcLHtZTKbAEO-n
|
||||
Chrome 22.0: Executed 1 of 1 SUCCESS (0.093 secs / 0.004 secs)
|
||||
INFO [karma]: Karma server started at http://localhost:9876/
|
||||
INFO [launcher]: Starting browser Chrome
|
||||
INFO [Chrome 49.0]: Connected on socket ... with id ...
|
||||
Chrome 49.0: Executed 1 of 1 SUCCESS (0.05 secs / 0.04 secs)
|
||||
</pre>
|
||||
|
||||
Yay! The test passed! Or not...
|
||||
* To rerun the tests, just change any of the source or test .js files. Karma will notice the change
|
||||
|
||||
* To rerun the tests, just change any of the source or test `.js` files. Karma will notice the change
|
||||
and will rerun the tests for you. Now isn't that sweet?
|
||||
|
||||
<div class="alert alert-info">
|
||||
Make sure you don't minimize the browser that Karma opened. On some OS, memory assigned to a minimized
|
||||
browser is limited, which results in your karma tests running extremely slow.
|
||||
Make sure you don't minimize the browser that Karma opened. On some OS, memory assigned to a
|
||||
minimized browser is limited, which results in your karma tests running extremely slow.
|
||||
</div>
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
<div></div>
|
||||
|
||||
* Add another binding to `index.html`. For example:
|
||||
|
||||
```html
|
||||
@@ -225,46 +281,60 @@ browser is limited, which results in your karma tests running extremely slow.
|
||||
|
||||
* Create a new model property in the controller and bind to it from the template. For example:
|
||||
|
||||
$scope.name = "World";
|
||||
```js
|
||||
$scope.name = 'world';
|
||||
```
|
||||
|
||||
Then add a new binding to `index.html`:
|
||||
|
||||
<p>Hello, {{name}}!</p>
|
||||
```html
|
||||
<p>Hello, {{name}}!</p>
|
||||
```
|
||||
|
||||
Refresh your browser and verify that it says "Hello, World!".
|
||||
Refresh your browser and verify that it says 'Hello, world!'.
|
||||
|
||||
* Update the unit test for the controller in `./test/unit/controllersSpec.js` to reflect the previous change. For example by adding:
|
||||
* Update the unit test for the controller in `app/app.spec.js` to reflect the previous change.
|
||||
For example by adding:
|
||||
|
||||
expect(scope.name).toBe('World');
|
||||
```js
|
||||
expect(scope.name).toBe('world');
|
||||
```
|
||||
|
||||
* Create a repeater in `index.html` that constructs a simple table:
|
||||
|
||||
<table>
|
||||
<tr><th>row number</th></tr>
|
||||
<tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i}}</td></tr>
|
||||
</table>
|
||||
```html
|
||||
<table>
|
||||
<tr><th>Row number</th></tr>
|
||||
<tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i}}</td></tr>
|
||||
</table>
|
||||
```
|
||||
|
||||
Now, make the list 1-based by incrementing `i` by one in the binding:
|
||||
|
||||
<table>
|
||||
<tr><th>row number</th></tr>
|
||||
<tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i+1}}</td></tr>
|
||||
</table>
|
||||
|
||||
Extra points: try and make an 8x8 table using an additional `ng-repeat`.
|
||||
```html
|
||||
<table>
|
||||
<tr><th>Row number</th></tr>
|
||||
<tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i+1}}</td></tr>
|
||||
</table>
|
||||
```
|
||||
|
||||
* Make the unit test fail by changing `expect(scope.phones.length).toBe(3)` to instead use `toBe(4)`.
|
||||
Extra points: Try and make an 8x8 table using an additional `ng-repeat`.
|
||||
|
||||
* Make the unit test fail by changing `expect(scope.phones.length).toBe(3)` to instead use
|
||||
`toBe(4)`.
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
You now have a dynamic app that features separate model, view, and controller components, and you
|
||||
are testing as you go. Now, let's go to {@link step_03 step 3} to learn how to add full text search
|
||||
to the app.
|
||||
We now have a dynamic application which separates models, views, and controllers, and we are testing
|
||||
as we go. Let's go to {@link step_03 step 3} to learn how to improve our application's architecture,
|
||||
by utilizing components.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="2"></ul>
|
||||
|
||||
[jasmine]: http://jasmine.github.io/
|
||||
[jasmine-docs]: http://jasmine.github.io/1.3/introduction.html
|
||||
[karma]: http://karma-runner.github.io/
|
||||
|
||||
[jasmine-docs]: http://jasmine.github.io/2.4/introduction.html
|
||||
[jasmine-home]: http://jasmine.github.io/
|
||||
[karma]: https://karma-runner.github.io/
|
||||
[mvc-pattern]: http://en.wikipedia.org/wiki/Model–View–Controller
|
||||
|
||||
+230
-171
@@ -1,223 +1,282 @@
|
||||
@ngdoc tutorial
|
||||
@name 3 - Filtering Repeaters
|
||||
@name 3 - Components
|
||||
@step 3
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="3"></ul>
|
||||
|
||||
|
||||
We did a lot of work in laying a foundation for the app in the last step, so now we'll do something
|
||||
simple; we will add full text search (yes, it will be simple!). We will also write an end-to-end
|
||||
test, because a good end-to-end test is a good friend. It stays with your app, keeps an eye on it,
|
||||
and quickly detects regressions.
|
||||
In the previous step, we saw how a controller and a template worked together to convert a static
|
||||
HTML page into a dynamic view. This is a very common pattern in Single-Page Applications in general
|
||||
(and Angular applications in particular):
|
||||
|
||||
* Instead of creating a static HTML page on the server, the client-side code "takes over" and
|
||||
interacts dynamically with the view, updating it instantly to reflect changes in model data or
|
||||
state, usually as a result of user interaction (we'll see an example shortly in
|
||||
{@link step_05 step 5}).
|
||||
|
||||
The **template** (the part of the view containing the bindings and presentation logic) acts as a
|
||||
blueprint for how our data should be organized and presented to the user.
|
||||
The **controller** provides the context in which the bindings are evaluated and applies behavior
|
||||
and logic to our template.
|
||||
|
||||
There are still a couple of areas we can do better:
|
||||
|
||||
1. What if we want to reuse the same functionality in a different part of our application ?<br />
|
||||
We would need to duplicate the whole template (including the controller). This is error-prone and
|
||||
hurts maintainability.
|
||||
2. The scope, that glues our controller and template together into a dynamic view, is not isolated
|
||||
from other parts of the page. What this means is that a random, unrelated change in a different
|
||||
part of the page (e.g. a property-name conflict) could have unexpected and hard-to-debug side
|
||||
effects on our view.
|
||||
|
||||
(OK, this might not be a real concern in our minimal example, but it **is** a valid concern for
|
||||
bigger, real-world applications.)
|
||||
|
||||
* The app now has a search box. Notice that the phone list on the page changes depending on what a
|
||||
user types into the search box.
|
||||
|
||||
<div doc-tutorial-reset="3"></div>
|
||||
|
||||
|
||||
## Controller
|
||||
## Components to the rescue!
|
||||
|
||||
We made no changes to the controller.
|
||||
Since this combination (template + controller) is such a common and recurring pattern, Angular
|
||||
provides an easy and concise way to combine them together into reusable and isolated entities,
|
||||
known as _components_.
|
||||
Additionally, Angular will create a so called _isolate scope_ for each instance of our component,
|
||||
which means no prototypal inheritance and no risk of our component affecting other parts of the
|
||||
application or vice versa.
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
Since this is an introductory tutorial, we are not going to dive deep into all features provided
|
||||
by Angular **components**. You can read more about components and their usage patterns in the
|
||||
[Components](guide/component) section of the Developer Guide.
|
||||
</p>
|
||||
<p>
|
||||
In fact, one could think of components as an opinionated and stripped-down version of their more
|
||||
complex and verbose (but powerful) siblings, **directives**, which are Angular's way of teaching
|
||||
HTML new tricks. You can read all about them in the [Directives](guide/directive) section of the
|
||||
Developer Guide.
|
||||
</p>
|
||||
<p>
|
||||
(**Note:** Directives are an advanced topic, so you might want to postpone studying them, until
|
||||
you have mastered the basics.)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## Template
|
||||
To create a component, we use the {@link angular.Module#component .component()} method of an
|
||||
{@link module Angular module}. We must provide the name of the component and the Component
|
||||
Definition Object (CDO for short).
|
||||
|
||||
__`app/index.html`:__
|
||||
Remember that (since components are also directives) the name of the component is in `camelCase`,
|
||||
but we will use `kebab-case`, when referring to it in our HTML.
|
||||
|
||||
```html
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<!--Sidebar content-->
|
||||
In its simplest form, the CDO will just contain a template and a controller. (We can actually omit
|
||||
the controller and Angular will create a dummy controller for us. This is useful for simple
|
||||
"presentational" components, that don't attach any behavior to the template.)
|
||||
|
||||
Search: <input ng-model="query">
|
||||
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<!--Body content-->
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query">
|
||||
{{phone.name}}
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
We added a standard HTML `<input>` tag and used Angular's
|
||||
{@link ng.filter:filter filter} function to process the input for the
|
||||
{@link ng.directive:ngRepeat ngRepeat} directive.
|
||||
|
||||
This lets a user enter search criteria and immediately see the effects of their search on the phone
|
||||
list. This new code demonstrates the following:
|
||||
|
||||
* Data-binding: This is one of the core features in Angular. When the page loads, Angular binds the
|
||||
name of the input box to a variable of the same name in the data model and keeps the two in sync.
|
||||
|
||||
In this code, the data that a user types into the input box (named __`query`__) is immediately
|
||||
available as a filter input in the list repeater (`phone in phones | filter:`__`query`__). When
|
||||
changes to the data model cause the repeater's input to change, the repeater efficiently updates
|
||||
the DOM to reflect the current state of the model.
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_03.png">
|
||||
|
||||
* Use of the `filter` filter: The {@link ng.filter:filter filter} function uses the
|
||||
`query` value to create a new array that contains only those records that match the `query`.
|
||||
|
||||
`ngRepeat` automatically updates the view in response to the changing number of phones returned
|
||||
by the `filter` filter. The process is completely transparent to the developer.
|
||||
|
||||
## Test
|
||||
|
||||
In Step 2, we learned how to write and run unit tests. Unit tests are perfect for testing
|
||||
controllers and other components of our application written in JavaScript, but they can't easily
|
||||
test DOM manipulation or the wiring of our application. For these, an end-to-end test is a much
|
||||
better choice.
|
||||
|
||||
The search feature was fully implemented via templates and data-binding, so we'll write our first
|
||||
end-to-end test, to verify that the feature works.
|
||||
|
||||
__`test/e2e/scenarios.js`:__
|
||||
Let's see an example:
|
||||
|
||||
```js
|
||||
describe('PhoneCat App', function() {
|
||||
|
||||
describe('Phone list view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('app/index.html');
|
||||
angular.
|
||||
module('myApp').
|
||||
component('greetUser', {
|
||||
template: 'Hello, {{$ctrl.user}}!',
|
||||
controller: function GreetUserController() {
|
||||
this.user = 'world';
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Now, every time we include `<greet-user></greet-user>` in our view, Angular will expand it into a
|
||||
DOM sub-tree constructed using the provided `template` and managed by an instance of the specified
|
||||
controller.
|
||||
|
||||
But wait, where did that `$ctrl` come from and what does it refer to ?
|
||||
|
||||
For reasons already mentioned (and for other reasons that are out of the scope of this tutorial), it
|
||||
is considered a good practice to avoid using the scope directly. We can (and should) use our
|
||||
controller instance; i.e. assign our data and methods on properties of our controller (the "`this`"
|
||||
inside the controller constructor), instead of directly to the scope.
|
||||
|
||||
From the template, we can refer to our controller instance using an alias. This way, the context of
|
||||
evaluation for our expressions is even more clear. By default, components use `$ctrl` as the
|
||||
controller alias, but we can override it, should the need arise.
|
||||
|
||||
There are more options available, so make sure you check out the
|
||||
{@link ng.$compileProvider#component API Reference}, before using `.component()` in your own
|
||||
applications.
|
||||
|
||||
|
||||
it('should filter the phone list as a user types into the search box', function() {
|
||||
## Using Components
|
||||
|
||||
var phoneList = element.all(by.repeater('phone in phones'));
|
||||
var query = element(by.model('query'));
|
||||
Now that we know how to create components, let's refactor the HTML page to make use of our newly
|
||||
acquired skill.
|
||||
|
||||
expect(phoneList.count()).toBe(3);
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
query.sendKeys('nexus');
|
||||
expect(phoneList.count()).toBe(1);
|
||||
```html
|
||||
<html ng-app="phonecatApp">
|
||||
<head>
|
||||
...
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<script src="phone-list.component.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
query.clear();
|
||||
query.sendKeys('motorola');
|
||||
expect(phoneList.count()).toBe(2);
|
||||
});
|
||||
<!-- Use a custom component to render a list of phones -->
|
||||
<phone-list></phone-list>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/app.js`:**
|
||||
|
||||
```js
|
||||
// Define the `phonecatApp` module
|
||||
angular.module('phonecatApp', []);
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/phone-list.component.js`:**
|
||||
|
||||
```js
|
||||
// Register `phoneList` component, along with its associated controller and template
|
||||
angular.
|
||||
module('phonecatApp').
|
||||
component('phoneList', {
|
||||
template:
|
||||
'<ul>' +
|
||||
'<li ng-repeat="phone in $ctrl.phones">' +
|
||||
'<span>{{phone.name}}</span>' +
|
||||
'<p>{{phone.snippet}}</p>' +
|
||||
'</li>' +
|
||||
'</ul>',
|
||||
controller: function PhoneListController() {
|
||||
this.phones = [
|
||||
{
|
||||
name: 'Nexus S',
|
||||
snippet: 'Fast just got faster with Nexus S.'
|
||||
}, {
|
||||
name: 'Motorola XOOM™ with Wi-Fi',
|
||||
snippet: 'The Next, Next Generation tablet.'
|
||||
}, {
|
||||
name: 'MOTOROLA XOOM™',
|
||||
snippet: 'The Next, Next Generation tablet.'
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Voilà! The resulting output should look the same, but let's see what we have gained:
|
||||
|
||||
* Our phone list is reusable. Just drop `<phone-list></phone-list>` anywhere in the page to get a
|
||||
list of phones.
|
||||
* Our main view (`index.html`) is cleaner and more declarative. Just by looking at it, we know there
|
||||
is a list of phones. We are not bothered with implementation details.
|
||||
* Our component is isolated and safe from "external influences". Likewise, we don't have to worry,
|
||||
that we might accidentally break something in some other part of the application. What happens
|
||||
inside our component, stays inside our component.
|
||||
* It's easier to test our component in isolation.
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_03.png">
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>**A note on file naming:**</p>
|
||||
<p>
|
||||
It is a good practice to distinguish different types of entities by suffix. In this tutorial, we
|
||||
are using the `.component` suffix for components, so the definition of a `someComponent`
|
||||
component would be in a file named `some-component.component.js`.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
Although we have combined our controller with a template into a component, we still can (and should)
|
||||
unit test the controller separately, since this is where our application logic and data reside.
|
||||
|
||||
In order to retrieve and instantiate a component's controller, Angular provides the
|
||||
{@link ngMock.$componentController $componentController} service.
|
||||
|
||||
<div class="alert alert-info">
|
||||
The `$controller` service that we used in the previous step can only instantiate controllers that
|
||||
were registered by name, using the `.controller()` method. We could have registered our component
|
||||
controller this way, too, if we wanted to. Instead, we chose to define it inline — inside
|
||||
the CDO — to keep things localized, but either way works equally well.
|
||||
</div>
|
||||
|
||||
<br />
|
||||
**`app/phone-list.component.spec.js`:**
|
||||
|
||||
```js
|
||||
describe('phoneList', function() {
|
||||
|
||||
// Load the module that contains the `phoneList` component before each test
|
||||
beforeEach(module('phonecatApp'));
|
||||
|
||||
// Test the controller
|
||||
describe('PhoneListController', function() {
|
||||
|
||||
it('should create a `phones` model with 3 phones', inject(function($componentController) {
|
||||
var ctrl = $componentController('phoneList');
|
||||
|
||||
expect(ctrl.phones.length).toBe(3);
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
This test verifies that the search box and the repeater are correctly wired together. Notice how
|
||||
easy it is to write end-to-end tests in Angular. Although this example is for a simple test, it
|
||||
really is that easy to set up any functional, readable, end-to-end test.
|
||||
The test retrieves the controller associated with the `phoneList` component, instantiates it and
|
||||
verifies that the phones array property on it contains three records. Note that the data is now on
|
||||
the controller instance itself, not on a `scope`.
|
||||
|
||||
### Running End to End Tests with Protractor
|
||||
Even though the syntax of this test looks very much like our controller unit test written with
|
||||
Jasmine, the end-to-end test uses APIs of [Protractor](https://github.com/angular/protractor). Read
|
||||
about the Protractor APIs at http://angular.github.io/protractor/#/api.
|
||||
|
||||
Much like Karma is the test runner for unit tests, we use Protractor to run end-to-end tests.
|
||||
Try it with `npm run protractor`. End-to-end tests are slow, so unlike with unit tests, Protractor
|
||||
will exit after the test run and will not automatically rerun the test suite on every file change.
|
||||
To rerun the test suite, execute `npm run protractor` again.
|
||||
## Running Tests
|
||||
|
||||
<div class="alert alert-info">
|
||||
Note: You must ensure your application is being served via a web-server to test with protractor.
|
||||
You can do this using `npm start`.
|
||||
You also need to ensure you've installed the protractor and updated webdriver prior to running the
|
||||
`npm run protractor`. You can do this by issuing `npm install` and `npm run update-webdriver` into
|
||||
your terminal.
|
||||
</div>
|
||||
Same as before, execute `npm test` to run the tests and then watch the files for changes.
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
### Display Current Query
|
||||
Display the current value of the `query` model by adding a `{{query}}` binding into the
|
||||
`index.html` template, and see how it changes when you type in the input box.
|
||||
<div></div>
|
||||
|
||||
### Display Query in Title
|
||||
Let's see how we can get the current value of the `query` model to appear in the HTML page title.
|
||||
* Try the experiments from the previous step, this time on the `phoneList` component.
|
||||
|
||||
* Add an end-to-end test into the `describe` block, `test/e2e/scenarios.js` should look like this:
|
||||
* Add a couple more phone lists on the page, by just adding more `<phone-list></phone-list>`
|
||||
elements in `index.html`. Now add another binding to the `phoneList` component's template:
|
||||
|
||||
```js
|
||||
describe('PhoneCat App', function() {
|
||||
|
||||
describe('Phone list view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('app/index.html');
|
||||
});
|
||||
|
||||
var phoneList = element.all(by.repeater('phone in phones'));
|
||||
var query = element(by.model('query'));
|
||||
|
||||
it('should filter the phone list as a user types into the search box', function() {
|
||||
expect(phoneList.count()).toBe(3);
|
||||
|
||||
query.sendKeys('nexus');
|
||||
expect(phoneList.count()).toBe(1);
|
||||
|
||||
query.clear();
|
||||
query.sendKeys('motorola');
|
||||
expect(phoneList.count()).toBe(2);
|
||||
});
|
||||
|
||||
it('should display the current filter value in the title bar', function() {
|
||||
query.clear();
|
||||
expect(browser.getTitle()).toMatch(/Google Phone Gallery:\s*$/);
|
||||
|
||||
query.sendKeys('nexus');
|
||||
expect(browser.getTitle()).toMatch(/Google Phone Gallery: nexus$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
template:
|
||||
'<p>Total number of phones: {{$ctrl.phones.length}}</p>' +
|
||||
'<ul>' +
|
||||
...
|
||||
```
|
||||
|
||||
Run protractor (`npm run protractor`) to see this test fail.
|
||||
|
||||
|
||||
* You might think you could just add the `{{query}}` to the title tag element as follows:
|
||||
|
||||
<title>Google Phone Gallery: {{query}}</title>
|
||||
|
||||
However, when you reload the page, you won't see the expected result. This is because the "query"
|
||||
model lives in the scope, defined by the `ng-controller="PhoneListCtrl"` directive, on the body
|
||||
element:
|
||||
|
||||
<body ng-controller="PhoneListCtrl">
|
||||
|
||||
If you want to bind to the query model from the `<title>` element, you must __move__ the
|
||||
`ngController` declaration to the HTML element because it is the common parent of both the body
|
||||
and title elements:
|
||||
|
||||
<html ng-app="phonecatApp" ng-controller="PhoneListCtrl">
|
||||
|
||||
Be sure to __remove__ the `ng-controller` declaration from the body element.
|
||||
|
||||
* Re-run `npm run protractor` to see the test now pass.
|
||||
|
||||
* While using double curlies works fine within the title element, you might have noticed that
|
||||
for a split second they are actually displayed to the user while the page is loading. A better
|
||||
solution would be to use the {@link ng.directive:ngBind ngBind} or
|
||||
{@link ng.directive:ngBindTemplate ngBindTemplate} directives, which are invisible to the user
|
||||
while the page is loading:
|
||||
|
||||
<title ng-bind-template="Google Phone Gallery: {{query}}">Google Phone Gallery</title>
|
||||
Reload the page and watch the new "feature" propagate to all phone lists. In real-world
|
||||
applications, where the phone lists could appear on several different pages, being able to change
|
||||
or add something in one place (e.g. a component's template) and have that change propagate
|
||||
throughout the application, is a big win.
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
We have now added full text search and included a test to verify that search works! Now let's go on
|
||||
to {@link step_04 step 4} to learn how to add sorting capability to the phone app.
|
||||
You have learned how to organize your application and presentation logic into isolated, reusable
|
||||
components. Let's go to {@link step_04 step 4} to learn how to organize our code in directories and
|
||||
files, so it remains easy to locate as our application grows.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="3"></ul>
|
||||
|
||||
|
||||
[jasmine-docs]: http://jasmine.github.io/2.4/introduction.html
|
||||
[jasmine-home]: http://jasmine.github.io/
|
||||
[karma]: https://karma-runner.github.io/
|
||||
[mvc-pattern]: http://en.wikipedia.org/wiki/Model–View–Controller
|
||||
|
||||
+261
-145
@@ -1,199 +1,315 @@
|
||||
@ngdoc tutorial
|
||||
@name 4 - Two-way Data Binding
|
||||
@name 4 - Directory and File Organization
|
||||
@step 4
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="4"></ul>
|
||||
|
||||
|
||||
In this step, you will add a feature to let your users control the order of the items in the phone
|
||||
list. The dynamic ordering is implemented by creating a new model property, wiring it together with
|
||||
the repeater, and letting the data binding magic do the rest of the work.
|
||||
In this step, we will not be adding any new functionality to our application. Instead, we are going
|
||||
to take a step back, refactor our codebase and move files and code around, in order to make our
|
||||
application more easily expandable and maintainable.
|
||||
|
||||
* In addition to the search box, the app displays a drop down menu that allows users to control the
|
||||
order in which the phones are listed.
|
||||
In the previous step, we saw how to architect our application to be modular and testable. What's
|
||||
equally important though, is organizing our codebase in a way that makes it easy (both for us and
|
||||
other developers on our team) to navigate through the code and quickly locate the pieces that are
|
||||
relevant to a specific feature or section of the application.
|
||||
|
||||
To that end, we will explain why and how we:
|
||||
|
||||
* Put each entity in its **own file**.
|
||||
* Organize our code by **feature area**, instead of by function.
|
||||
* Split our code into **modules** that other modules can depend on.
|
||||
|
||||
<div class="alert alert-info">
|
||||
We will keep it short, not going into great detail on every good practice and convention. These
|
||||
principles are explained in great detail in the [Angular Style Guide][styleguide], which also
|
||||
contains many more techniques for effectively organizing Angular codebases.
|
||||
</div>
|
||||
|
||||
|
||||
<div doc-tutorial-reset="4"></div>
|
||||
|
||||
|
||||
## Template
|
||||
## One Feature per File
|
||||
|
||||
__`app/index.html`:__
|
||||
It might be tempting, for the sake of simplicity, to put everything in one file, or have one file
|
||||
per type; e.g. all controllers is one file, all components in another file, all services in a third
|
||||
file, and so on.
|
||||
This might seem to work well in the beginning, but as our application grows it becomes a burden to
|
||||
maintain. As we add more and more features, our files will get bigger and bigger and it will be
|
||||
difficult to navigate and find the code we are looking for.
|
||||
|
||||
```html
|
||||
Search: <input ng-model="query">
|
||||
Sort by:
|
||||
<select ng-model="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
Instead we should put each feature/entity in its own file. Each stand-alone controller will be
|
||||
defined in its own file, each component will be defined in each own file, etc.
|
||||
|
||||
Luckily, we don't need to change anything with respect to that guideline in our code, since we have
|
||||
already defined our `phoneList` component in its own `phone-list.component.js` file. Good job!
|
||||
|
||||
We will keep this in mind though, as we add more features.
|
||||
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp">
|
||||
<span>{{phone.name}}</span>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
## Organizing by Feature
|
||||
|
||||
So, now that we learned we should put everything in its own file, our `app/` directory will soon be
|
||||
full with dozens of files and specs (remember we keep our unit test files next to the corresponding
|
||||
source code files). What's more important, logically related files will not be grouped together; it
|
||||
will be really difficult of locate all files related to a specific section of the application and
|
||||
make a change or fix a bug.
|
||||
|
||||
So, what shall we do?
|
||||
|
||||
Well, we are going to group our files into directories _by feature_. For example, since we have a
|
||||
section in our application that lists phones, we will put all related files into a `phone-list/`
|
||||
directory under `app/`. We are soon to find out that certain features are used across different
|
||||
parts of the application. We will put those inside `app/core/`.
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
Other typical names for our `core` directory are `shared`, `common` and `components`. The last
|
||||
one is kind of misleading though, as it will contain other things than components as well.
|
||||
</p>
|
||||
<p>
|
||||
(This is mostly a relic of the past, when "components" just meant the generic building blocks of
|
||||
an application.)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Based on what we have discussed so far, here is our directory/file layout for the `phoneList`
|
||||
"feature":
|
||||
|
||||
```
|
||||
app/
|
||||
phone-list/
|
||||
phone-list.component.js
|
||||
phone-list.component.spec.js
|
||||
app.js
|
||||
```
|
||||
|
||||
We made the following changes to the `index.html` template:
|
||||
|
||||
* First, we added a `<select>` html element named `orderProp`, so that our users can pick from the
|
||||
two provided sorting options.
|
||||
## Using Modules
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_04.png">
|
||||
As previously mentioned, one of the benefits of having a modular architecture is code reuse —
|
||||
not only inside the same application, but across applications too. There is one final step in making
|
||||
this code reuse frictionless:
|
||||
|
||||
* We then chained the `filter` filter with {@link ng.filter:orderBy `orderBy`}
|
||||
filter to further process the input into the repeater. `orderBy` is a filter that takes an input
|
||||
array, copies it and reorders the copy which is then returned.
|
||||
* Each feature/section should declare its own module and all related entities should register
|
||||
themselves on that module.
|
||||
|
||||
Angular creates a two way data-binding between the select element and the `orderProp` model.
|
||||
`orderProp` is then used as the input for the `orderBy` filter.
|
||||
|
||||
As we discussed in the section about data-binding and the repeater in step 3, whenever the model
|
||||
changes (for example because a user changes the order with the select drop down menu), Angular's
|
||||
data-binding will cause the view to automatically update. No bloated DOM manipulation code is
|
||||
necessary!
|
||||
|
||||
|
||||
|
||||
## Controller
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
Let's take the `phoneList` feature as an example. Previously, the `phoneList` component would
|
||||
register itself on the `phonecatApp` module:
|
||||
|
||||
```js
|
||||
var phonecatApp = angular.module('phonecatApp', []);
|
||||
|
||||
phonecatApp.controller('PhoneListCtrl', function ($scope) {
|
||||
$scope.phones = [
|
||||
{'name': 'Nexus S',
|
||||
'snippet': 'Fast just got faster with Nexus S.',
|
||||
'age': 1},
|
||||
{'name': 'Motorola XOOM™ with Wi-Fi',
|
||||
'snippet': 'The Next, Next Generation tablet.',
|
||||
'age': 2},
|
||||
{'name': 'MOTOROLA XOOM™',
|
||||
'snippet': 'The Next, Next Generation tablet.',
|
||||
'age': 3}
|
||||
];
|
||||
|
||||
$scope.orderProp = 'age';
|
||||
});
|
||||
angular.
|
||||
module('phonecatApp').
|
||||
component('phoneList', ...);
|
||||
```
|
||||
|
||||
* We modified the `phones` model - the array of phones - and added an `age` property to each phone
|
||||
record. This property is used to order phones by age.
|
||||
Similarly, the accompanying spec file loads the `phonecatApp` module before each test (because
|
||||
that's where our component is registered). Now, imagine that we need a list of phones on another
|
||||
project that we are working on. Thanks to our modular architecture, we don't have to reinvent the
|
||||
wheel; we simply copy the `phone-list/` directory on our other project and add the necessary script
|
||||
tags in our `index.html` file and we are done, right?
|
||||
|
||||
* We added a line to the controller that sets the default value of `orderProp` to `age`. If we had
|
||||
not set a default value here, the `orderBy` filter would remain uninitialized until our
|
||||
user picked an option from the drop down menu.
|
||||
Well, not so fast. The new project doesn't know anything about a `phonecatApp` module. So, we would
|
||||
have to replace all references to `phonecatApp` with the name of this project's main module. As you
|
||||
can imagine this is both laborious and error-prone.
|
||||
|
||||
This is a good time to talk about two-way data-binding. Notice that when the app is loaded in the
|
||||
browser, "Newest" is selected in the drop down menu. This is because we set `orderProp` to `'age'`
|
||||
in the controller. So the binding works in the direction from our model to the UI. Now if you
|
||||
select "Alphabetically" in the drop down menu, the model will be updated as well and the phones
|
||||
will be reordered. That is the data-binding doing its job in the opposite direction — from the UI
|
||||
to the model.
|
||||
Yeah, you guessed it: There is a better way!
|
||||
|
||||
Each feature/section, will declare its own module and have all related entities registered there.
|
||||
The main module (`phonecatApp`) will declare a dependency on each feature/section module. Now,
|
||||
all it takes to reuse the same code on a new project is copying the feature directory over and
|
||||
adding the feature module as a dependency in the new project's main module.
|
||||
|
||||
Here is what our `phoneList` feature will look like after this change:
|
||||
|
||||
## Test
|
||||
<br />
|
||||
**`/`:**
|
||||
|
||||
The changes we made should be verified with both a unit test and an end-to-end test. Let's look at
|
||||
the unit test first.
|
||||
```
|
||||
app/
|
||||
phone-list/
|
||||
phone-list.module.js
|
||||
phone-list.component.js
|
||||
phone-list.component.spec.js
|
||||
app.module.js
|
||||
```
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
<br />
|
||||
**`app/phone-list/phone-list.module.js`:**
|
||||
|
||||
```js
|
||||
describe('PhoneCat controllers', function() {
|
||||
// Define the `phoneList` module
|
||||
angular.module('phoneList', []);
|
||||
```
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, ctrl;
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.js`:**
|
||||
|
||||
beforeEach(module('phonecatApp'));
|
||||
```js
|
||||
// Register the `phoneList` component on the `phoneList` module,
|
||||
angular.
|
||||
module('phoneList').
|
||||
component('phoneList', {...});
|
||||
```
|
||||
|
||||
beforeEach(inject(function($controller) {
|
||||
scope = {};
|
||||
ctrl = $controller('PhoneListCtrl', {$scope:scope});
|
||||
}));
|
||||
<br />
|
||||
**`app/app.module.js`:**<br />
|
||||
_(since `app/app.js` now only contains the main module declaration, we gave it a `.module` suffix)_
|
||||
|
||||
it('should create "phones" model with 3 phones', function() {
|
||||
expect(scope.phones.length).toBe(3);
|
||||
});
|
||||
```js
|
||||
// Define the `phonecatApp` module
|
||||
angular.module('phonecatApp', [
|
||||
// ...which depends on the `phoneList` module
|
||||
'phoneList'
|
||||
]);
|
||||
```
|
||||
|
||||
By passing `phoneList` inside the dependencies array when defining the `phonecatApp` module, Angular
|
||||
will make all entities registered on `phoneList` available on `phonecatApp` as well.
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
Don't forget to also update your `index.html` adding a `<script>` tag for each JavaScript file
|
||||
we have created. This might seem tedious, but is totally worth it.
|
||||
</p>
|
||||
<p>
|
||||
In a production-ready application, you would concatenate and minify all your JavaScript files
|
||||
anyway (for performance reasons), so this won't be an issue any more.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
Note that files defining a module (i.e. `.module.js`) need to be included before other files that
|
||||
add features (e.g. components, controllers, services, filters) to that module.
|
||||
</div>
|
||||
|
||||
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(scope.orderProp).toBe('age');
|
||||
});
|
||||
## External Templates
|
||||
|
||||
Since we are at refactoring, let's do one more thing. As we learned, components have templates,
|
||||
which are basically fragments of HTML code that dictate how our data is laid out and presented to
|
||||
the user. In {@link step_03 step 3}, we saw how we can specify the template for a component as a
|
||||
string using the `template` property of the CDO (Component Definition Object).
|
||||
Having HTML code in a string isn't ideal, especially for bigger templates. It would be much better,
|
||||
if we could have our HTML code in `.html` files. This way, we would get all the support our
|
||||
IDE/editor has to offer (e.g. HTML-specific color-highlighting and auto-completion) and also keep
|
||||
our component definitions cleaner.
|
||||
|
||||
So, while it's perfectly fine to keep our component templates inline (using the `template` property
|
||||
of the CDO), we are going to use an external template for our `phoneList` component. In order to
|
||||
denote that we are using an external template, we use the `templateUrl` property and specify the URL
|
||||
that our template will be loaded from. Since we want to keep our template close to where the
|
||||
component is defined, we place it inside `app/phone-list/`.
|
||||
|
||||
We copied the contents of the `template` property (the HTML code) into
|
||||
`app/phone-list/phone-list.template.html` and modified our CDO like this:
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phoneList').
|
||||
component('phoneList', {
|
||||
// Note: The URL is relative to our `index.html` file
|
||||
templateUrl: 'phone-list/phone-list.template.html',
|
||||
controller: ...
|
||||
});
|
||||
```
|
||||
|
||||
At runtime, when Angular needs to create an instance of the `phoneList` component, it will make an
|
||||
HTTP request to get the template from `app/phone-list/phone-list.template.html`.
|
||||
|
||||
<div class="alert alert-info">
|
||||
Keeping inline with our convention, we will be using the `.template` suffix for external
|
||||
templates. Another common convention is to just have the `.html` extension
|
||||
(e.g. `phone-list.html`).
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<p>
|
||||
Using an external template like this, will result in more HTTP requests to the server (one for
|
||||
each external template). Although Angular takes care not to make extraneous requests (e.g.
|
||||
fetching the templates lazily, caching the results, etc), additional requests do have a cost
|
||||
(especially on mobile devices and data-plan connections).
|
||||
</p>
|
||||
<p>
|
||||
Luckily, there are ways to avoid the extra costs (while still keeping your templates external).
|
||||
A detailed discussion of the subject is outside the scope of this tutorial, but you can take a
|
||||
look at the {@link ng.$templateRequest $templateRequest} and
|
||||
{@link ng.$templateCache $templateCache} services for more info on how Angular manages external
|
||||
templates.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
## Final Directory/File Layout
|
||||
|
||||
After all the refactorings that took place, this is how our application looks from the outside:
|
||||
|
||||
<br />
|
||||
**`/`:**
|
||||
|
||||
```
|
||||
app/
|
||||
phone-list/
|
||||
phone-list.component.js
|
||||
phone-list.component.spec.js
|
||||
phone-list.module.js
|
||||
phone-list.template.html
|
||||
app.css
|
||||
app.module.js
|
||||
index.html
|
||||
```
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
Since this was just a refactoring step (no actual code addition/deletions), we shouldn't need to
|
||||
change much (if anything) as far as our specs are concerned.
|
||||
|
||||
One thing that we can (and should) change is the name of the module to be loaded before each test in
|
||||
`app/phone-list/phone-list.component.spec.js`. We don't need to pull in the whole `phonecatApp`
|
||||
module (which will soon grow to depend on more stuff). All we want to test is already included in
|
||||
the much smaller `phoneList` module, so it suffices to just load that.
|
||||
This is one extra benefit that we get out of our modular architecture for free.
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.spec.js`:**
|
||||
|
||||
```js
|
||||
describe('phoneList', function() {
|
||||
|
||||
// Load the module that contains the `phoneList` component before each test
|
||||
beforeEach(module('phoneList'));
|
||||
|
||||
...
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
If not already done so, run the tests (using the `npm test` command) and verify that they still
|
||||
pass.
|
||||
|
||||
The unit test now verifies that the default ordering property is set.
|
||||
<div class="alert alert-success">
|
||||
One of the great things about tests is the confidence they provide, when refactoring your
|
||||
application. It's easy to break something as you start moving files around and re-arranging
|
||||
modules. Having good test coverage is the quickest, easiest and most reliable way of knowing that
|
||||
your application will continue to work as expected.
|
||||
</div>
|
||||
|
||||
We used Jasmine's API to extract the controller construction into a `beforeEach` block, which is
|
||||
shared by all tests in the parent `describe` block.
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
<pre>Chrome 22.0: Executed 2 of 2 SUCCESS (0.021 secs / 0.001 secs)</pre>
|
||||
|
||||
|
||||
Let's turn our attention to the end-to-end test.
|
||||
|
||||
__`test/e2e/scenarios.js`:__
|
||||
|
||||
```js
|
||||
...
|
||||
it('should be possible to control phone order via the drop down select box', function() {
|
||||
|
||||
var phoneNameColumn = element.all(by.repeater('phone in phones').column('phone.name'));
|
||||
var query = element(by.model('query'));
|
||||
|
||||
function getNames() {
|
||||
return phoneNameColumn.map(function(elm) {
|
||||
return elm.getText();
|
||||
});
|
||||
}
|
||||
|
||||
query.sendKeys('tablet'); //let's narrow the dataset to make the test assertions shorter
|
||||
|
||||
expect(getNames()).toEqual([
|
||||
"Motorola XOOM\u2122 with Wi-Fi",
|
||||
"MOTOROLA XOOM\u2122"
|
||||
]);
|
||||
|
||||
element(by.model('orderProp')).element(by.css('option[value="name"]')).click();
|
||||
|
||||
expect(getNames()).toEqual([
|
||||
"MOTOROLA XOOM\u2122",
|
||||
"Motorola XOOM\u2122 with Wi-Fi"
|
||||
]);
|
||||
});...
|
||||
```
|
||||
|
||||
The end-to-end test verifies that the ordering mechanism of the select box is working correctly.
|
||||
|
||||
You can now rerun `npm run protractor` to see the tests run.
|
||||
|
||||
# Experiments
|
||||
|
||||
* In the `PhoneListCtrl` controller, remove the statement that sets the `orderProp` value and
|
||||
you'll see that Angular will temporarily add a new blank ("unknown") option to the drop-down list and the
|
||||
ordering will default to unordered/natural order.
|
||||
|
||||
* Add an `{{orderProp}}` binding into the `index.html` template to display its current value as
|
||||
text.
|
||||
|
||||
* Reverse the sort order by adding a `-` symbol before the sorting value: `<option value="-age">Oldest</option>`
|
||||
|
||||
# Summary
|
||||
|
||||
Now that you have added list sorting and tested the app, go to {@link step_05 step 5} to learn
|
||||
about Angular services and how Angular uses dependency injection.
|
||||
Even if we didn't add any new and exciting functionality to our application, we have made a great
|
||||
step towards a well-architected and maintainable application. Time to spice things up. Let's go to
|
||||
{@link step_05 step 5} to learn how to add full-text search to the application.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="4"></ul>
|
||||
|
||||
|
||||
[angular-seed]: https://github.com/angular/angular-seed
|
||||
[styleguide]: https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md
|
||||
|
||||
+122
-231
@@ -1,277 +1,168 @@
|
||||
@ngdoc tutorial
|
||||
@name 5 - XHRs & Dependency Injection
|
||||
@name 5 - Filtering Repeaters
|
||||
@step 5
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="5"></ul>
|
||||
|
||||
|
||||
Enough of building an app with three phones in a hard-coded dataset! Let's fetch a larger dataset
|
||||
from our server using one of Angular's built-in {@link guide/services services} called {@link
|
||||
ng.$http $http}. We will use Angular's {@link guide/di dependency
|
||||
injection (DI)} to provide the service to the `PhoneListCtrl` controller.
|
||||
We did a lot of work in laying a foundation for the app in the previous steps, so now we'll do
|
||||
something simple; we will add full-text search (yes, it will be simple!). We will also write an
|
||||
end-to-end (E2E) test, because a good E2E test is a good friend. It stays with your app, keeps an
|
||||
eye on it, and quickly detects regressions.
|
||||
|
||||
* The app now has a search box. Notice that the phone list on the page changes depending on what a
|
||||
user types into the search box.
|
||||
|
||||
* There is now a list of 20 phones, loaded from the server.
|
||||
|
||||
<div doc-tutorial-reset="5"></div>
|
||||
|
||||
|
||||
## Data
|
||||
The `app/phones/phones.json` file in your project is a dataset that contains a larger list of phones
|
||||
stored in the JSON format.
|
||||
## Component Controller
|
||||
|
||||
Following is a sample of the file:
|
||||
We made no changes to the component's controller.
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
"age": 13,
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
"snippet": "Are you ready for everything life throws your way?"
|
||||
...
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
## Component Template
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.template.html`:**
|
||||
|
||||
```html
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<!--Sidebar content-->
|
||||
|
||||
Search: <input ng-model="$ctrl.query" />
|
||||
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<!--Body content-->
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query">
|
||||
<span>{{phone.name}}</span>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
We added a standard HTML `<input>` tag and used Angular's {@link ng.filter:filter filter} function
|
||||
to process the input for the {@link ngRepeat ngRepeat} directive.
|
||||
|
||||
## Controller
|
||||
By virtue of the {@link ngModel ngModel} directive, this lets a user enter search criteria and
|
||||
immediately see the effects of their search on the phone list. This new code demonstrates the
|
||||
following:
|
||||
|
||||
We'll use Angular's {@link ng.$http $http} service in our controller to make an HTTP
|
||||
request to your web server to fetch the data in the `app/phones/phones.json` file. `$http` is just
|
||||
one of several built-in {@link guide/services Angular services} that handle common operations
|
||||
in web apps. Angular injects these services for you where you need them.
|
||||
* Data-binding: This is one of the core features in Angular. When the page loads, Angular binds the
|
||||
value of the input box to the data model variable specified with `ngModel` and keeps the two in
|
||||
sync.
|
||||
|
||||
Services are managed by Angular's {@link guide/di DI subsystem}. Dependency injection
|
||||
helps to make your web apps both well-structured (e.g., separate components for presentation, data,
|
||||
and control) and loosely coupled (dependencies between components are not resolved by the
|
||||
components themselves, but by the DI subsystem).
|
||||
In this code, the data that a user types into the input box (bound to **`$ctrl.query`**) is
|
||||
immediately available as a filter input in the list repeater
|
||||
(`phone in $ctrl.phones | filter:`**`$ctrl.query`**). When changes to the data model cause the
|
||||
repeater's input to change, the repeater efficiently updates the DOM to reflect the current state
|
||||
of the model.
|
||||
|
||||
__`app/js/controllers.js:`__
|
||||
<img class="diagram" src="img/tutorial/tutorial_05.png">
|
||||
|
||||
* Use of the `filter` filter: The {@link ng.filter:filter filter} function uses the `$ctrl.query`
|
||||
value to create a new array that contains only those records that match the query.
|
||||
|
||||
`ngRepeat` automatically updates the view in response to the changing number of phones returned
|
||||
by the `filter` filter. The process is completely transparent to the developer.
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
In previous steps, we learned how to write and run unit tests. Unit tests are perfect for testing
|
||||
controllers and other parts of our application written in JavaScript, but they can't easily
|
||||
test templates, DOM manipulation or interoperability of components and services. For these, an
|
||||
end-to-end (E2E) test is a much better choice.
|
||||
|
||||
The search feature was fully implemented via templates and data-binding, so we'll write our first
|
||||
E2E test, to verify that the feature works.
|
||||
|
||||
<br />
|
||||
**`e2e-tests/scenarios.js`:**
|
||||
|
||||
```js
|
||||
var phonecatApp = angular.module('phonecatApp', []);
|
||||
describe('PhoneCat Application', function() {
|
||||
|
||||
describe('phoneList', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('index.html');
|
||||
});
|
||||
|
||||
it('should filter the phone list as a user types into the search box', function() {
|
||||
var phoneList = element.all(by.repeater('phone in $ctrl.phones'));
|
||||
var query = element(by.model('$ctrl.query'));
|
||||
|
||||
expect(phoneList.count()).toBe(3);
|
||||
|
||||
query.sendKeys('nexus');
|
||||
expect(phoneList.count()).toBe(1);
|
||||
|
||||
query.clear();
|
||||
query.sendKeys('motorola');
|
||||
expect(phoneList.count()).toBe(2);
|
||||
});
|
||||
|
||||
phonecatApp.controller('PhoneListCtrl', function ($scope, $http) {
|
||||
$http.get('phones/phones.json').success(function(data) {
|
||||
$scope.phones = data;
|
||||
});
|
||||
|
||||
$scope.orderProp = 'age';
|
||||
});
|
||||
```
|
||||
|
||||
`$http` makes an HTTP GET request to our web server, asking for `phones/phones.json` (the url is
|
||||
relative to our `index.html` file). The server responds by providing the data in the json file.
|
||||
(The response might just as well have been dynamically generated by a backend server. To the
|
||||
browser and our app they both look the same. For the sake of simplicity we used a json file in this
|
||||
tutorial.)
|
||||
|
||||
The `$http` service returns a {@link ng.$q promise object} with a `success`
|
||||
method. We call this method to handle the asynchronous response and assign the phone data to the
|
||||
scope controlled by this controller, as a model called `phones`. Notice that Angular detected the
|
||||
json response and parsed it for us!
|
||||
|
||||
To use a service in Angular, you simply declare the names of the dependencies you need as arguments
|
||||
to the controller's constructor function, as follows:
|
||||
|
||||
phonecatApp.controller('PhoneListCtrl', function ($scope, $http) {...}
|
||||
|
||||
Angular's dependency injector provides services to your controller when the controller is being
|
||||
constructed. The dependency injector also takes care of creating any transitive dependencies the
|
||||
service may have (services often depend upon other services).
|
||||
|
||||
Note that the names of arguments are significant, because the injector uses these to look up the
|
||||
dependencies.
|
||||
This test verifies that the search box and the repeater are correctly wired together. Notice how
|
||||
easy it is to write E2E tests in Angular. Although this example is for a simple test, it really is
|
||||
that easy to set up any functional, readable, E2E test.
|
||||
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_05.png">
|
||||
## Running E2E Tests with Protractor
|
||||
|
||||
Even though the syntax of this test looks very much like our controller unit test written with
|
||||
Jasmine, the E2E test uses APIs of [Protractor][protractor]. Read about the Protractor APIs in the
|
||||
[Protractor API Docs][protractor-docs].
|
||||
|
||||
### `$` Prefix Naming Convention
|
||||
|
||||
You can create your own services, and in fact we will do exactly that in step 11. As a naming
|
||||
convention, Angular's built-in services, Scope methods and a few other Angular APIs have a `$`
|
||||
prefix in front of the name.
|
||||
|
||||
The `$` prefix is there to namespace Angular-provided services.
|
||||
To prevent collisions it's best to avoid naming your services and models anything that begins with a `$`.
|
||||
|
||||
If you inspect a Scope, you may also notice some properties that begin with `$$`. These
|
||||
properties are considered private, and should not be accessed or modified.
|
||||
|
||||
|
||||
### A Note on Minification
|
||||
|
||||
Since Angular infers the controller's dependencies from the names of arguments to the controller's
|
||||
constructor function, if you were to [minify](http://goo.gl/SAnnsm) the JavaScript code for
|
||||
`PhoneListCtrl` controller, all of its function arguments would be minified as well, and the
|
||||
dependency injector would not be able to identify services correctly.
|
||||
|
||||
We can overcome this problem by annotating the function with the names of the dependencies, provided
|
||||
as strings, which will not get minified. There are two ways to provide these injection annotations:
|
||||
|
||||
* Create a `$inject` property on the controller function which holds an array of strings.
|
||||
Each string in the array is the name of the service to inject for the corresponding parameter.
|
||||
In our example we would write:
|
||||
|
||||
```js
|
||||
function PhoneListCtrl($scope, $http) {...}
|
||||
PhoneListCtrl.$inject = ['$scope', '$http'];
|
||||
phonecatApp.controller('PhoneListCtrl', PhoneListCtrl);
|
||||
```
|
||||
|
||||
* Use an inline annotation where, instead of just providing the function, you provide an array.
|
||||
This array contains a list of the service names, followed by the function itself.
|
||||
|
||||
```js
|
||||
function PhoneListCtrl($scope, $http) {...}
|
||||
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', PhoneListCtrl]);
|
||||
```
|
||||
|
||||
Both of these methods work with any function that can be injected by Angular, so it's up to your
|
||||
project's style guide to decide which one you use.
|
||||
|
||||
When using the second method, it is common to provide the constructor function inline as an
|
||||
anonymous function when registering the controller:
|
||||
|
||||
```js
|
||||
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', function($scope, $http) {...}]);
|
||||
```
|
||||
|
||||
From this point onward, we're going to use the inline method in the tutorial. With that in mind,
|
||||
let's add the annotations to our `PhoneListCtrl`:
|
||||
|
||||
__`app/js/controllers.js:`__
|
||||
|
||||
```js
|
||||
var phonecatApp = angular.module('phonecatApp', []);
|
||||
|
||||
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http',
|
||||
function ($scope, $http) {
|
||||
$http.get('phones/phones.json').success(function(data) {
|
||||
$scope.phones = data;
|
||||
});
|
||||
|
||||
$scope.orderProp = 'age';
|
||||
}]);
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
|
||||
Because we started using dependency injection and our controller has dependencies, constructing the
|
||||
controller in our tests is a bit more complicated. We could use the `new` operator and provide the
|
||||
constructor with some kind of fake `$http` implementation. However, Angular provides a mock `$http`
|
||||
service that we can use in unit tests. We configure "fake" responses to server requests by calling
|
||||
methods on a service called `$httpBackend`:
|
||||
|
||||
```js
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, ctrl, $httpBackend;
|
||||
|
||||
// Load our app module definition before each test.
|
||||
beforeEach(module('phonecatApp'));
|
||||
|
||||
// The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).
|
||||
// This allows us to inject a service but then attach it to a variable
|
||||
// with the same name as the service in order to avoid a name conflict.
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/phones.json').
|
||||
respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller('PhoneListCtrl', {$scope: scope});
|
||||
}));
|
||||
```
|
||||
|
||||
Note: Because we loaded Jasmine and `angular-mocks.js` in our test environment, we got two helper
|
||||
methods {@link angular.mock.module module} and {@link angular.mock.inject inject} that we'll
|
||||
use to access and configure the injector.
|
||||
|
||||
We created the controller in the test environment, as follows:
|
||||
|
||||
* We used the `inject` helper method to inject instances of
|
||||
{@link ng.$rootScope $rootScope},
|
||||
{@link ng.$controller $controller} and
|
||||
{@link ng.$httpBackend $httpBackend} services into the Jasmine's `beforeEach`
|
||||
function. These instances come from an injector which is recreated from scratch for every single
|
||||
test. This guarantees that each test starts from a well known starting point and each test is
|
||||
isolated from the work done in other tests.
|
||||
|
||||
* We created a new scope for our controller by calling `$rootScope.$new()`
|
||||
|
||||
* We called the injected `$controller` function passing the name of the `PhoneListCtrl` controller
|
||||
and the created scope as parameters.
|
||||
|
||||
Because our code now uses the `$http` service to fetch the phone list data in our controller, before
|
||||
we create the `PhoneListCtrl` child scope, we need to tell the testing harness to expect an
|
||||
incoming request from the controller. To do this we:
|
||||
|
||||
* Request `$httpBackend` service to be injected into our `beforeEach` function. This is a mock
|
||||
version of the service that in a production environment facilitates all XHR and JSONP requests.
|
||||
The mock version of this service allows you to write tests without having to deal with
|
||||
native APIs and the global state associated with them — both of which make testing a nightmare.
|
||||
|
||||
* Use the `$httpBackend.expectGET` method to train the `$httpBackend` service to expect an incoming
|
||||
HTTP request and tell it what to respond with. Note that the responses are not returned until we call
|
||||
the `$httpBackend.flush` method.
|
||||
|
||||
Now we will make assertions to verify that the `phones` model doesn't exist on `scope` before
|
||||
the response is received:
|
||||
|
||||
```js
|
||||
it('should create "phones" model with 2 phones fetched from xhr', function() {
|
||||
expect(scope.phones).toBeUndefined();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(scope.phones).toEqual([{name: 'Nexus S'},
|
||||
{name: 'Motorola DROID'}]);
|
||||
});
|
||||
```
|
||||
|
||||
* We flush the request queue in the browser by calling `$httpBackend.flush()`. This causes the
|
||||
promise returned by the `$http` service to be resolved with the trained response. See
|
||||
'Flushing HTTP requests' in the {@link ngMock.$httpBackend mock $httpBackend} documentation for
|
||||
a full explanation of why this is necessary.
|
||||
|
||||
* We make the assertions, verifying that the phone model now exists on the scope.
|
||||
|
||||
Finally, we verify that the default value of `orderProp` is set correctly:
|
||||
|
||||
```js
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(scope.orderProp).toBe('age');
|
||||
});
|
||||
```
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
<pre>Chrome 22.0: Executed 2 of 2 SUCCESS (0.028 secs / 0.007 secs)</pre>
|
||||
Much like Karma is the test runner for unit tests, we use Protractor to run E2E tests. Try it with
|
||||
`npm run protractor`. E2E tests take time, so unlike with unit tests, Protractor will exit after the
|
||||
tests run and will not automatically rerun the test suite on every file change.
|
||||
To rerun the test suite, execute `npm run protractor` again.
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** In order for protractor to access and run tests against your application, it must be
|
||||
served via a web server. In a different terminal/command line window, run `npm start` to fire up
|
||||
the web server.
|
||||
</div>
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* At the bottom of `index.html`, add a `<pre>{{phones | filter:query | orderBy:orderProp | json}}</pre>`
|
||||
binding to see the list of phones displayed in json format.
|
||||
<div></div>
|
||||
|
||||
* In the `PhoneListCtrl` controller, pre-process the http response by limiting the number of phones
|
||||
to the first 5 in the list. Use the following code in the `$http` callback:
|
||||
* Display the current value of the `query` model by adding a `{{$ctrl.query}}` binding into the
|
||||
`phone-list.template.html` template and see how it changes, when you type in the input box.
|
||||
|
||||
$scope.phones = data.splice(0, 5);
|
||||
You might also try to add the `{{$ctrl.query}}` binding to `index.html`. However, when you reload
|
||||
the page, you won't see the expected result. This is because the `query` model lives in the scope
|
||||
defined by the `<phone-list>` component.<br />
|
||||
Component isolation at work!
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now that you have learned how easy it is to use Angular services (thanks to Angular's dependency
|
||||
injection), go to {@link step_06 step 6}, where you will add some
|
||||
thumbnail images of phones and some links.
|
||||
We have now added full-text search and included a test to verify that it works! Now let's go on to
|
||||
{@link step_06 step 6} to learn how to add sorting capabilities to the PhoneCat application.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="5"></ul>
|
||||
|
||||
|
||||
[protractor]: https://github.com/angular/protractor
|
||||
[protractor-docs]: https://angular.github.io/protractor/#/api
|
||||
|
||||
@@ -1,108 +1,241 @@
|
||||
@ngdoc tutorial
|
||||
@name 6 - Templating Links & Images
|
||||
@name 6 - Two-way Data Binding
|
||||
@step 6
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="6"></ul>
|
||||
|
||||
|
||||
In this step, you will add thumbnail images for the phones in the phone list, and links that, for
|
||||
now, will go nowhere. In subsequent steps you will use the links to display additional information
|
||||
about the phones in the catalog.
|
||||
In this step, we will add a feature to let our users control the order of the items in the phone
|
||||
list. The dynamic ordering is implemented by creating a new model property, wiring it together with
|
||||
the repeater, and letting the data binding magic do the rest of the work.
|
||||
|
||||
* In addition to the search box, the application displays a drop-down menu that allows users to
|
||||
control the order in which the phones are listed.
|
||||
|
||||
* There are now links and images of the phones in the list.
|
||||
|
||||
<div doc-tutorial-reset="6"></div>
|
||||
|
||||
## Data
|
||||
|
||||
Note that the `phones.json` file contains unique IDs and image URLs for each of the phones. The
|
||||
URLs point to the `app/img/phones/` directory.
|
||||
## Component Template
|
||||
|
||||
__`app/phones/phones.json`__ (sample snippet):
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
...
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
...
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
__`app/index.html`:__
|
||||
<br />
|
||||
**`app/phone-list/phone-list.template.html`:**
|
||||
|
||||
```html
|
||||
...
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<!--Sidebar content-->
|
||||
|
||||
<p>
|
||||
Search:
|
||||
<input ng-model="$ctrl.query">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Sort by:
|
||||
<select ng-model="$ctrl.orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<!--Body content-->
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
|
||||
<a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp">
|
||||
<span>{{phone.name}}</span>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
To dynamically generate links that will in the future lead to phone detail pages, we used the
|
||||
now-familiar double-curly brace binding in the `href` attribute values. In step 2, we added the
|
||||
`{{phone.name}}` binding as the element content. In this step the `{{phone.id}}` binding is used in
|
||||
the element attribute.
|
||||
We made the following changes to the `phone-list.template.html` template:
|
||||
|
||||
We also added phone images next to each record using an image tag with the {@link
|
||||
ng.directive:ngSrc ngSrc} directive. That directive prevents the
|
||||
browser from treating the Angular `{{ expression }}` markup literally, and initiating a request to
|
||||
invalid URL `http://localhost:8000/app/{{phone.imageUrl}}`, which it would have done if we had only
|
||||
specified an attribute binding in a regular `src` attribute (`<img src="{{phone.imageUrl}}">`).
|
||||
Using the `ngSrc` directive prevents the browser from making an http request to an invalid location.
|
||||
* First, we added a `<select>` element bound to `$ctrl.orderProp`, so that our users can pick from
|
||||
the two provided sorting options.
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_06.png">
|
||||
|
||||
* We then chained the `filter` filter with the {@link orderBy orderBy} filter to further process the
|
||||
input for the repeater. `orderBy` is a filter that takes an input array, copies it and reorders
|
||||
the copy which is then returned.
|
||||
|
||||
Angular creates a two way data-binding between the select element and the `$ctrl.orderProp` model.
|
||||
`$ctrl.orderProp` is then used as the input for the `orderBy` filter.
|
||||
|
||||
As we discussed in the section about data-binding and the repeater in {@link step_05 step 5},
|
||||
whenever the model changes (for example because a user changes the order with the select drop-down
|
||||
menu), Angular's data-binding will cause the view to automatically update. No bloated DOM
|
||||
manipulation code is necessary!
|
||||
|
||||
|
||||
## Test
|
||||
## Component Controller
|
||||
|
||||
__`test/e2e/scenarios.js`__:
|
||||
<br />
|
||||
**`app/phone-list/phone-list.components.js`:**
|
||||
|
||||
```js
|
||||
...
|
||||
it('should render phone specific links', function() {
|
||||
var query = element(by.model('query'));
|
||||
query.sendKeys('nexus');
|
||||
element.all(by.css('.phones li a')).first().click();
|
||||
browser.getLocationAbsUrl().then(function(url) {
|
||||
expect(url).toBe('/phones/nexus-s');
|
||||
});
|
||||
angular.
|
||||
module('phoneList').
|
||||
component('phoneList', {
|
||||
templateUrl: 'phone-list/phone-list.template.html',
|
||||
controller: function PhoneListController() {
|
||||
this.phones = [
|
||||
{
|
||||
name: 'Nexus S',
|
||||
snippet: 'Fast just got faster with Nexus S.',
|
||||
age: 1
|
||||
}, {
|
||||
name: 'Motorola XOOM™ with Wi-Fi',
|
||||
snippet: 'The Next, Next Generation tablet.',
|
||||
age: 2
|
||||
}, {
|
||||
name: 'MOTOROLA XOOM™',
|
||||
snippet: 'The Next, Next Generation tablet.',
|
||||
age: 3
|
||||
}
|
||||
];
|
||||
|
||||
this.orderProp = 'age';
|
||||
}
|
||||
});
|
||||
...
|
||||
```
|
||||
|
||||
We added a new end-to-end test to verify that the app is generating correct links to the phone
|
||||
views that we will implement in the upcoming steps.
|
||||
* We modified the `phones` model - the array of phones - and added an `age` property to each phone
|
||||
record. This property is used to order the phones by age.
|
||||
|
||||
* We added a line to the controller that sets the default value of `orderProp` to `age`. If we had
|
||||
not set a default value here, the `orderBy` filter would remain uninitialized until the user
|
||||
picked an option from the drop-down menu.
|
||||
|
||||
This is a good time to talk about two-way data-binding. Notice that when the application is loaded
|
||||
in the browser, "Newest" is selected in the drop-down menu. This is because we set `orderProp` to
|
||||
`'age'` in the controller. So the binding works in the direction from our model to the UI. Now if
|
||||
you select "Alphabetically" in the drop-down menu, the model will be updated as well and the phones
|
||||
will be reordered. That is the data-binding doing its job in the opposite direction — from the UI to
|
||||
the model.
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
The changes we made should be verified with both a unit test and an E2E test. Let's look at the unit
|
||||
test first.
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.spec.js`:**
|
||||
|
||||
```js
|
||||
describe('phoneList', function() {
|
||||
|
||||
// Load the module that contains the `phoneList` component before each test
|
||||
beforeEach(module('phoneList'));
|
||||
|
||||
// Test the controller
|
||||
describe('PhoneListController', function() {
|
||||
var ctrl;
|
||||
|
||||
beforeEach(inject(function($componentController) {
|
||||
ctrl = $componentController('phoneList');
|
||||
}));
|
||||
|
||||
it('should create a `phones` model with 3 phones', function() {
|
||||
expect(ctrl.phones.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should set a default value for the `orderProp` model', function() {
|
||||
expect(ctrl.orderProp).toBe('age');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
The unit test now verifies that the default ordering property is set.
|
||||
|
||||
We used Jasmine's API to extract the controller construction into a `beforeEach` block, which is
|
||||
shared by all tests in the parent `describe` block.
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
```
|
||||
Chrome 49.0: Executed 2 of 2 SUCCESS (0.136 secs / 0.08 secs)
|
||||
```
|
||||
|
||||
Let's turn our attention to the E2E tests.
|
||||
|
||||
<br />
|
||||
**`e2e-tests/scenarios.js`:**
|
||||
|
||||
```js
|
||||
describe('PhoneCat Application', function() {
|
||||
|
||||
describe('phoneList', function() {
|
||||
|
||||
...
|
||||
|
||||
it('should be possible to control phone order via the drop-down menu', function() {
|
||||
var queryField = element(by.model('$ctrl.query'));
|
||||
var orderSelect = element(by.model('$ctrl.orderProp'));
|
||||
var nameOption = orderSelect.element(by.css('option[value="name"]'));
|
||||
var phoneNameColumn = element.all(by.repeater('phone in $ctrl.phones').column('phone.name'));
|
||||
|
||||
function getNames() {
|
||||
return phoneNameColumn.map(function(elem) {
|
||||
return elem.getText();
|
||||
});
|
||||
}
|
||||
|
||||
queryField.sendKeys('tablet'); // Let's narrow the dataset to make the assertions shorter
|
||||
|
||||
expect(getNames()).toEqual([
|
||||
'Motorola XOOM\u2122 with Wi-Fi',
|
||||
'MOTOROLA XOOM\u2122'
|
||||
]);
|
||||
|
||||
nameOption.click();
|
||||
|
||||
expect(getNames()).toEqual([
|
||||
'MOTOROLA XOOM\u2122',
|
||||
'Motorola XOOM\u2122 with Wi-Fi'
|
||||
]);
|
||||
});
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
The E2E test verifies that the ordering mechanism of the select box is working correctly.
|
||||
|
||||
You can now rerun `npm run protractor` to see the tests run.
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Replace the `ng-src` directive with a plain old `src` attribute. Using tools such as Firebug,
|
||||
or Chrome's Web Inspector, or inspecting the webserver access logs, confirm that the app is indeed
|
||||
making an extraneous request to `/app/%7B%7Bphone.imageUrl%7D%7D` (or
|
||||
`/app/{{phone.imageUrl}}`).
|
||||
<div></div>
|
||||
|
||||
The issue here is that the browser will fire a request for that invalid image address as soon as
|
||||
it hits the `img` tag, which is before Angular has a chance to evaluate the expression and inject
|
||||
the valid address.
|
||||
* In the `phoneList` component's controller, remove the statement that sets the `orderProp` value
|
||||
and you'll see that Angular will temporarily add a new blank ("unknown") option to the drop-down
|
||||
list and the ordering will default to unordered/natural order.
|
||||
|
||||
* Add a `{{$ctrl.orderProp}}` binding into the `phone-list.template.html` template to display its
|
||||
current value as text.
|
||||
|
||||
* Reverse the sort order by adding a `-` symbol before the sorting value:
|
||||
`<option value="-age">Oldest</option>`
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now that you have added phone images and links, go to {@link step_07 step 7} to learn about Angular
|
||||
layout templates and how Angular makes it easy to create applications that have multiple views.
|
||||
Now that you have added list sorting and tested the application, go to {@link step_07 step 7} to
|
||||
learn about Angular services and how Angular uses dependency injection.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="6"></ul>
|
||||
|
||||
+242
-298
@@ -1,368 +1,312 @@
|
||||
@ngdoc tutorial
|
||||
@name 7 - Routing & Multiple Views
|
||||
@name 7 - XHR & Dependency Injection
|
||||
@step 7
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="7"></ul>
|
||||
|
||||
|
||||
In this step, you will learn how to create a layout template and how to build an app that has
|
||||
multiple views by adding routing, using an Angular module called 'ngRoute'.
|
||||
Enough of building an app with three phones in a hard-coded dataset! Let's fetch a larger dataset
|
||||
from our server using one of Angular's built-in {@link guide/services services} called
|
||||
{@link ng.$http $http}. We will use Angular's {@link guide/di dependency injection (DI)} to provide
|
||||
the service to the `phoneList` component's controller.
|
||||
|
||||
* There is now a list of 20 phones, loaded from the server.
|
||||
|
||||
* When you now navigate to `app/index.html`, you are redirected to `app/index.html/#/phones`
|
||||
and the phone list appears in the browser.
|
||||
* When you click on a phone link, the url changes to that specific phone and the stub of a
|
||||
phone detail page is displayed.
|
||||
|
||||
<div doc-tutorial-reset="7"></div>
|
||||
|
||||
## Dependencies
|
||||
|
||||
The routing functionality added by this step is provided by angular in the `ngRoute` module, which
|
||||
is distributed separately from the core Angular framework.
|
||||
## Data
|
||||
|
||||
We are using [Bower][bower] to install client-side dependencies. This step updates the
|
||||
`bower.json` configuration file to include the new dependency:
|
||||
The `app/phones/phones.json` file in our project is a dataset that contains a larger list of phones,
|
||||
stored in JSON format.
|
||||
|
||||
Following is a sample of the file:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "angular-phonecat",
|
||||
"description": "A starter project for AngularJS",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/angular/angular-phonecat",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "1.4.x",
|
||||
"angular-mocks": "1.4.x",
|
||||
"jquery": "~2.1.1",
|
||||
"bootstrap": "~3.1.1",
|
||||
"angular-route": "~1.4.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The new dependency `"angular-route": "~1.4.0"` tells bower to install a version of the
|
||||
angular-route component that is compatible with version 1.4.x. We must tell bower to download
|
||||
and install this dependency.
|
||||
|
||||
If you have bower installed globally, then you can run `bower install` but for this project, we have
|
||||
preconfigured npm to run bower install for us:
|
||||
|
||||
```
|
||||
npm install
|
||||
[
|
||||
{
|
||||
"age": 13,
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
"snippet": "Are you ready for everything life throws your way?"
|
||||
...
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
## Multiple Views, Routing and Layout Template
|
||||
## Component Controller
|
||||
|
||||
Our app is slowly growing and becoming more complex. Before step 7, the app provided our users with
|
||||
a single view (the list of all phones), and all of the template code was located in the
|
||||
`index.html` file. The next step in building the app is to add a view that will show detailed
|
||||
information about each of the devices in our list.
|
||||
We will use Angular's {@link ng.$http $http} service in our controller for making an HTTP request to
|
||||
our web server to fetch the data in the `app/phones/phones.json` file. `$http` is just one of
|
||||
several built-in {@link guide/services Angular services} that handle common operations in web
|
||||
applications. Angular injects these services for you, right where you need them.
|
||||
|
||||
To add the detailed view, we could expand the `index.html` file to contain template code for both
|
||||
views, but that would get messy very quickly. Instead, we are going to turn the `index.html`
|
||||
template into what we call a "layout template". This is a template that is common for all views in
|
||||
our application. Other "partial templates" are then included into this layout template depending on
|
||||
the current "route" — the view that is currently displayed to the user.
|
||||
Services are managed by Angular's {@link guide/di DI subsystem}. Dependency injection helps to make
|
||||
your web applications both well-structured (e.g. separate entities for presentation, data, and
|
||||
control) and loosely coupled (dependencies between entities are not resolved by the entities
|
||||
themselves, but by the DI subsystem). As a result, applications are easier to test as well.
|
||||
|
||||
Application routes in Angular are declared via the {@link ngRoute.$routeProvider $routeProvider},
|
||||
which is the provider of the {@link ngRoute.$route $route service}. This service makes it easy to
|
||||
wire together controllers, view templates, and the current URL location in the browser. Using this
|
||||
feature, we can implement [deep linking](http://en.wikipedia.org/wiki/Deep_linking), which lets us
|
||||
utilize the browser's history (back and forward navigation) and bookmarks.
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.js:`**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phoneList').
|
||||
component('phoneList', {
|
||||
templateUrl: 'phone-list/phone-list.template.html',
|
||||
controller: function PhoneListController($http) {
|
||||
var self = this;
|
||||
self.orderProp = 'age';
|
||||
|
||||
$http.get('phones/phones.json').then(function(response) {
|
||||
self.phones = response.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
`$http` makes an HTTP GET request to our web server, asking for `phones.json` (the URL is relative
|
||||
to our `index.html` file). The server responds by providing the data in the JSON file.
|
||||
(The response might just as well have been dynamically generated by a backend server. To the
|
||||
browser and our app, they both look the same. For the sake of simplicity, we will use JSON files
|
||||
in this tutorial.)
|
||||
|
||||
The `$http` service returns a {@link ng.$q promise object}, which has a `then()` method. We call
|
||||
this method to handle the asynchronous response and assign the phone data to the controller, as a
|
||||
property called `phones`. Notice that Angular detected the JSON response and parsed it for us into
|
||||
the `data` property of the `response` object passed to our callback!
|
||||
|
||||
Since we are making the assignment of the `phones` property in a callback function, where the `this`
|
||||
value is not defined, we also introduce a local variable called `self` that points back to the
|
||||
controller instance.
|
||||
|
||||
To use a service in Angular, you simply declare the names of the dependencies you need as arguments
|
||||
to the controller's constructor function, as follows:
|
||||
|
||||
```js
|
||||
function PhoneListController($http) {...}
|
||||
```
|
||||
|
||||
Angular's dependency injector provides services to your controller, when the controller is being
|
||||
constructed. The dependency injector also takes care of creating any transitive dependencies the
|
||||
service may have (services often depend upon other services).
|
||||
|
||||
Note that the names of arguments are significant, because the injector uses these to look up the
|
||||
dependencies.
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_05.png">
|
||||
|
||||
|
||||
### A Note About DI, Injector and Providers
|
||||
### `$`-prefix Naming Convention
|
||||
|
||||
As you {@link tutorial/step_05 noticed}, {@link guide/di dependency injection} (DI) is at the core of
|
||||
AngularJS, so it's important for you to understand a thing or two about how it works.
|
||||
You can create your own services, and in fact we will do exactly that a few steps down the road. As
|
||||
a naming convention, Angular's built-in services, Scope methods and a few other Angular APIs have a
|
||||
`$` prefix in front of the name.
|
||||
|
||||
When the application bootstraps, Angular creates an injector that will be used to find and inject all
|
||||
of the services that are required by your app. The injector itself doesn't know anything about what
|
||||
`$http` or `$route` services do. In fact, the injector doesn't even know about the existence of these services
|
||||
unless it is configured with proper module definitions.
|
||||
The `$` prefix is there to namespace Angular-provided services. To prevent collisions it's best to
|
||||
avoid naming your services and models anything that begins with a `$`.
|
||||
|
||||
The injector only carries out the following steps :
|
||||
If you inspect a Scope, you may also notice some properties that begin with `$$`. These properties
|
||||
are considered private, and should not be accessed or modified.
|
||||
|
||||
* load the module definition(s) that you specify in your app
|
||||
* register all Providers defined in these module definitions
|
||||
* when asked to do so, inject a specified function and any necessary dependencies (services) that
|
||||
it lazily instantiates via their Providers.
|
||||
|
||||
Providers are objects that provide (create) instances of services and expose configuration APIs
|
||||
that can be used to control the creation and runtime behavior of a service. In case of the `$route`
|
||||
service, the `$routeProvider` exposes APIs that allow you to define routes for your application.
|
||||
### A Note on Minification
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Note:** Providers can only be injected into `config` functions. Thus you could not inject
|
||||
`$routeProvider` into `PhoneListCtrl`.
|
||||
</div>
|
||||
Since Angular infers the controller's dependencies from the names of arguments to the controller's
|
||||
constructor function, if you were to [minify][minification] the JavaScript code for the
|
||||
`PhoneListController` controller, all of its function arguments would be minified as well, and the
|
||||
dependency injector would not be able to identify services correctly.
|
||||
|
||||
Angular modules solve the problem of removing global state from the application and provide a way
|
||||
of configuring the injector. As opposed to AMD or require.js modules, Angular modules don't try to
|
||||
solve the problem of script load ordering or lazy script fetching. These goals are totally independent and
|
||||
both module systems can live side by side and fulfill their goals.
|
||||
We can overcome this problem by annotating the function with the names of the dependencies, provided
|
||||
as strings, which will not get minified. There are two ways to provide these injection annotations:
|
||||
|
||||
To deepen your understanding of DI on Angular, see
|
||||
[Understanding Dependency Injection](https://github.com/angular/angular.js/wiki/Understanding-Dependency-Injection).
|
||||
* Create an `$inject` property on the controller function which holds an array of strings.
|
||||
Each string in the array is the name of the service to inject for the corresponding parameter.
|
||||
In our example, we would write:
|
||||
|
||||
## Template
|
||||
```js
|
||||
function PhoneListController($http) {...}
|
||||
PhoneListController.$inject = ['$http'];
|
||||
...
|
||||
.component('phoneList', {..., controller: PhoneListController});
|
||||
```
|
||||
|
||||
The `$route` service is usually used in conjunction with the {@link ngRoute.directive:ngView
|
||||
ngView} directive. The role of the `ngView` directive is to include the view template for the current
|
||||
route into the layout template. This makes it a perfect fit for our `index.html` template.
|
||||
* Use an inline annotation where, instead of just providing the function, you provide an array.
|
||||
This array contains a list of the service names, followed by the function itself as the last item
|
||||
of the array.
|
||||
|
||||
```js
|
||||
function PhoneListController($http) {...}
|
||||
...
|
||||
.component('phoneList', {..., controller: ['$http', PhoneListController]});
|
||||
```
|
||||
|
||||
Both of these methods work with any function that can be injected by Angular, so it's up to your
|
||||
project's style guide to decide which one you use.
|
||||
|
||||
When using the second method, it is common to provide the constructor function inline, when
|
||||
registering the controller:
|
||||
|
||||
```js
|
||||
.component('phoneList', {..., controller: ['$http', function PhoneListController($http) {...}]});
|
||||
```
|
||||
|
||||
From this point onwards, we are going to use the inline method in the tutorial. With that in mind,
|
||||
let's add the annotations to our `PhoneListController`:
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.js`**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phoneList').
|
||||
component('phoneList', {
|
||||
templateUrl: 'phone-list/phone-list.template.html',
|
||||
controller: ['$http',
|
||||
function PhoneListController($http) {
|
||||
var self = this;
|
||||
self.orderProp = 'age';
|
||||
|
||||
$http.get('phones/phones.json').then(function(response) {
|
||||
self.phones = response.data;
|
||||
});
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
Because we started using dependency injection and our controller has dependencies, constructing the
|
||||
controller in our tests is a bit more complicated. We could use the `new` operator and provide the
|
||||
constructor with some kind of fake `$http` implementation. However, Angular provides a mock `$http`
|
||||
service that we can use in unit tests. We configure "fake" responses to server requests by calling
|
||||
methods on a service called `$httpBackend`:
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.component.spec.js`:**
|
||||
|
||||
```js
|
||||
describe('phoneList', function() {
|
||||
|
||||
beforeEach(module('phoneList'));
|
||||
|
||||
describe('controller', function() {
|
||||
var $httpBackend, ctrl;
|
||||
|
||||
// The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).
|
||||
// This allows us to inject a service and assign it to a variable with the same name
|
||||
// as the service while avoiding a name conflict.
|
||||
beforeEach(inject(function($componentController, _$httpBackend_) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/phones.json')
|
||||
.respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
|
||||
ctrl = $componentController('phoneList');
|
||||
}));
|
||||
|
||||
...
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** Starting with AngularJS version 1.2, `ngRoute` is in its own module and must be loaded by
|
||||
loading the additional `angular-route.js` file, which we download via Bower above.
|
||||
**Note:** Because we loaded Jasmine and `angular-mocks.js` in our test environment, we got two
|
||||
helper methods {@link angular.mock.module module} and {@link angular.mock.inject inject} that we
|
||||
can use to access and configure the injector.
|
||||
</div>
|
||||
|
||||
__`app/index.html`:__
|
||||
We created the controller in the test environment, as follows:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en" ng-app="phonecatApp">
|
||||
<head>
|
||||
...
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="bower_components/angular-route/angular-route.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
<script src="js/controllers.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
* We used the `inject()` helper method to inject instances of
|
||||
{@link ngMock.$componentController $componentController} and {@link ng.$httpBackend $httpBackend}
|
||||
services into Jasmine's `beforeEach()` function. These instances come from an injector which is
|
||||
recreated from scratch for every single test. This guarantees that each test starts from a well
|
||||
known starting point and each test is isolated from the work done in other tests.
|
||||
|
||||
<div ng-view></div>
|
||||
* We called the injected `$componentController` function passing the name of the `phoneList`
|
||||
component (whose controller we wanted to instantiate) as a parameter.
|
||||
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
Because our code now uses the `$http` service to fetch the phone list data in our controller, before
|
||||
we create the `PhoneListController`, we need to tell the testing harness to expect an incoming
|
||||
request from the controller. To do this we:
|
||||
|
||||
We have added two new `<script>` tags in our index file to load up extra JavaScript files into our
|
||||
application:
|
||||
* Inject the `$httpBackend` service into the `beforeEach()` function. This is a
|
||||
{@link ngMock.$httpBackend mock version} of the service that in a production environment
|
||||
facilitates all XHR and JSONP requests. The mock version of this service allows us to write tests
|
||||
without having to deal with native APIs and the global state associated with them — both of which
|
||||
make testing a nightmare. It also overcomes the asynchronous nature of these calls, which would
|
||||
slow down unit tests.
|
||||
|
||||
- `angular-route.js` : defines the Angular `ngRoute` module, which provides us with routing.
|
||||
- `app.js` : this file now holds the root module of our application.
|
||||
* Use the `$httpBackend.expectGET()` method to train the `$httpBackend` service to expect an
|
||||
incoming HTTP request and tell it what to respond with. Note that the responses are not returned
|
||||
until we call the `$httpBackend.flush()` method.
|
||||
|
||||
Note that we removed most of the code in the `index.html` template and replaced it with a single
|
||||
line containing a div with the `ng-view` attribute. The code that we removed was placed into the
|
||||
`phone-list.html` template:
|
||||
|
||||
__`app/partials/phone-list.html`:__
|
||||
|
||||
```html
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<!--Sidebar content-->
|
||||
|
||||
Search: <input ng-model="query">
|
||||
Sort by:
|
||||
<select ng-model="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<!--Body content-->
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
|
||||
<a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
<div style="display:none">
|
||||
TODO!
|
||||
<img class="diagram" src="img/tutorial/tutorial_07_final.png">
|
||||
</div>
|
||||
|
||||
We also added a placeholder template for the phone details view:
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
|
||||
```html
|
||||
TBD: detail view for <span>{{phoneId}}</span>
|
||||
```
|
||||
|
||||
Note how we are using the `phoneId` expression which will be defined in the `PhoneDetailCtrl` controller.
|
||||
|
||||
## The App Module
|
||||
|
||||
To improve the organization of the app, we are making use of Angular's `ngRoute` module and we've
|
||||
moved the controllers into their own module `phonecatControllers` (as shown below).
|
||||
|
||||
We added `angular-route.js` to `index.html` and created a new `phonecatControllers` module in
|
||||
`controllers.js`. That's not all we need to do to be able to use their code, however. We also have
|
||||
to add the modules as dependencies of our app. By listing these two modules as dependencies of
|
||||
`phonecatApp`, we can use the directives and services they provide.
|
||||
|
||||
|
||||
__`app/js/app.js`:__
|
||||
Now we will make assertions to verify that the `phones` property doesn't exist on the controller
|
||||
before the response is received:
|
||||
|
||||
```js
|
||||
var phonecatApp = angular.module('phonecatApp', [
|
||||
'ngRoute',
|
||||
'phonecatControllers'
|
||||
]);
|
||||
it('should create a `phones` property with 2 phones fetched with `$http`', function() {
|
||||
expect(ctrl.phones).toBeUndefined();
|
||||
|
||||
...
|
||||
$httpBackend.flush();
|
||||
expect(ctrl.phones).toEqual([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
});
|
||||
```
|
||||
|
||||
Notice the second argument passed to `angular.module`, `['ngRoute', 'phonecatControllers']`. This
|
||||
array lists the modules that `phonecatApp` depends on.
|
||||
* We flush the request queue in the browser by calling `$httpBackend.flush()`. This causes the
|
||||
promise returned by the `$http` service to be resolved with the trained response. See
|
||||
{@link ngMock.$httpBackend#flushing-http-requests Flushing HTTP requests} in the mock
|
||||
`$httpBackend` documentation for a full explanation of why this is necessary.
|
||||
|
||||
* We make the assertions, verifying that the `phones` property now exists on the controller.
|
||||
|
||||
Finally, we verify that the default value of `orderProp` is set correctly:
|
||||
|
||||
```js
|
||||
...
|
||||
|
||||
phonecatApp.config(['$routeProvider',
|
||||
function($routeProvider) {
|
||||
$routeProvider.
|
||||
when('/phones', {
|
||||
templateUrl: 'partials/phone-list.html',
|
||||
controller: 'PhoneListCtrl'
|
||||
}).
|
||||
when('/phones/:phoneId', {
|
||||
templateUrl: 'partials/phone-detail.html',
|
||||
controller: 'PhoneDetailCtrl'
|
||||
}).
|
||||
otherwise({
|
||||
redirectTo: '/phones'
|
||||
});
|
||||
}]);
|
||||
it('should set a default value for the `orderProp` property', function() {
|
||||
expect(ctrl.orderProp).toBe('age');
|
||||
});
|
||||
```
|
||||
|
||||
Using the `phonecatApp.config()` method, we request the `$routeProvider` to be injected into our
|
||||
config function and use the {@link ngRoute.$routeProvider#when `$routeProvider.when()`} method to
|
||||
define our routes.
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
Our application routes are defined as follows:
|
||||
|
||||
* `when('/phones')`: The phone list view will be shown when the URL hash fragment is `/phones`. To
|
||||
construct this view, Angular will use the `phone-list.html` template and the `PhoneListCtrl`
|
||||
controller.
|
||||
|
||||
* `when('/phones/:phoneId')`: The phone details view will be shown when the URL hash fragment
|
||||
matches '/phones/:phoneId', where `:phoneId` is a variable part of the URL. To construct the phone
|
||||
details view, Angular will use the `phone-detail.html` template and the `PhoneDetailCtrl`
|
||||
controller.
|
||||
|
||||
* `otherwise({redirectTo: '/phones'})`: triggers a redirection to `/phones` when the browser
|
||||
address doesn't match either of our routes.
|
||||
|
||||
|
||||
We reused the `PhoneListCtrl` controller that we constructed in previous steps and we added a new,
|
||||
empty `PhoneDetailCtrl` controller to the `app/js/controllers.js` file for the phone details view.
|
||||
|
||||
|
||||
Note the use of the `:phoneId` parameter in the second route declaration. The `$route` service uses
|
||||
the route declaration — `'/phones/:phoneId'` — as a template that is matched against the current
|
||||
URL. All variables defined with the `:` notation are extracted into the
|
||||
{@link ngRoute.$routeParams `$routeParams`} object.
|
||||
|
||||
|
||||
## Controllers
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
|
||||
```js
|
||||
var phonecatControllers = angular.module('phonecatControllers', []);
|
||||
|
||||
phonecatControllers.controller('PhoneListCtrl', ['$scope', '$http',
|
||||
function ($scope, $http) {
|
||||
$http.get('phones/phones.json').success(function(data) {
|
||||
$scope.phones = data;
|
||||
});
|
||||
|
||||
$scope.orderProp = 'age';
|
||||
}]);
|
||||
|
||||
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams',
|
||||
function($scope, $routeParams) {
|
||||
$scope.phoneId = $routeParams.phoneId;
|
||||
}]);
|
||||
```
|
||||
|
||||
Again, note that we created a new module called `phonecatControllers`. For small AngularJS
|
||||
applications, it's common to create just one module for all of your controllers if there are just a
|
||||
few. As your application grows, it is quite common to refactor your code into additional modules.
|
||||
For larger apps, you will probably want to create separate modules for each major feature of
|
||||
your app.
|
||||
|
||||
Because our example app is relatively small, we'll just add all of our controllers to the
|
||||
`phonecatControllers` module.
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
To automatically verify that everything is wired properly, we wrote end-to-end tests that navigate
|
||||
to various URLs and verify that the correct view was rendered.
|
||||
|
||||
```js
|
||||
...
|
||||
it('should redirect index.html to index.html#/phones', function() {
|
||||
browser.get('app/index.html');
|
||||
browser.getLocationAbsUrl().then(function(url) {
|
||||
expect(url).toEqual('/phones');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Phone list view', function() {
|
||||
beforeEach(function() {
|
||||
browser.get('app/index.html#/phones');
|
||||
});
|
||||
...
|
||||
|
||||
describe('Phone detail view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('app/index.html#/phones/nexus-s');
|
||||
});
|
||||
|
||||
|
||||
it('should display placeholder page with phoneId', function() {
|
||||
expect(element(by.binding('phoneId')).getText()).toBe('nexus-s');
|
||||
});
|
||||
});
|
||||
Chrome 49.0: Executed 2 of 2 SUCCESS (0.133 secs / 0.097 secs)
|
||||
```
|
||||
|
||||
|
||||
You can now rerun `npm run protractor` to see the tests run.
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Try to add an `{{orderProp}}` binding to `index.html`, and you'll see that nothing happens even
|
||||
when you are in the phone list view. This is because the `orderProp` model is visible only in the
|
||||
scope managed by `PhoneListCtrl`, which is associated with the `<div ng-view>` element. If you add
|
||||
the same binding into the `phone-list.html` template, the binding will work as expected.
|
||||
<div></div>
|
||||
|
||||
<div style="display: none">
|
||||
* In `PhoneCatCtrl`, create a new model called "`hero`" with `this.hero = 'Zoro'`. In
|
||||
`PhoneListCtrl`, let's shadow it with `this.hero = 'Batman'`. In `PhoneDetailCtrl`, we'll use
|
||||
`this.hero = "Captain Proton"`. Then add the `<p>hero = {{hero}}</p>` to all three of our templates
|
||||
(`index.html`, `phone-list.html`, and `phone-detail.html`). Open the app and you'll see scope
|
||||
inheritance and model property shadowing do some wonders.
|
||||
</div>
|
||||
* At the bottom of `phone-list.template.html`, add a
|
||||
`<pre>{{$ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp | json}}</pre>` binding to see
|
||||
the list of phones displayed in JSON format.
|
||||
|
||||
* In the `PhoneListController` controller, pre-process the HTTP response by limiting the number of
|
||||
phones to the first 5 in the list. Use the following code in the `$http` callback:
|
||||
|
||||
```js
|
||||
self.phones = response.data.slice(0, 5);
|
||||
```
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
With the routing set up and the phone list view implemented, we're ready to go to {@link step_08
|
||||
step 8} to implement the phone details view.
|
||||
Now that you have learned how easy it is to use Angular services (thanks to Angular's dependency
|
||||
injection), go to {@link step_08 step 8}, where you will add some thumbnail images of phones and
|
||||
some links.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="7"></ul>
|
||||
|
||||
|
||||
[bower]: http://bower.io
|
||||
[minification]: https://en.wikipedia.org/wiki/Minification_(programming)
|
||||
|
||||
@@ -1,197 +1,117 @@
|
||||
@ngdoc tutorial
|
||||
@name 8 - More Templating
|
||||
@name 8 - Templating Links & Images
|
||||
@step 8
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="8"></ul>
|
||||
|
||||
|
||||
In this step, you will implement the phone details view, which is displayed when a user clicks on a
|
||||
phone in the phone list.
|
||||
In this step, we will add thumbnail images for the phones in the phone list, and links that, for
|
||||
now, will go nowhere. In subsequent steps, we will use the links to display additional information
|
||||
about the phones in the catalog.
|
||||
|
||||
* When you click on a phone on the list, the phone details page with phone-specific information
|
||||
is displayed.
|
||||
* There are now links and images of the phones in the list.
|
||||
|
||||
To implement the phone details view we used {@link ng.$http $http} to fetch our data, and we
|
||||
fleshed out the `phone-detail.html` view template.
|
||||
|
||||
<div doc-tutorial-reset="8"></div>
|
||||
|
||||
|
||||
## Data
|
||||
|
||||
In addition to `phones.json`, the `app/phones/` directory also contains one JSON file for each
|
||||
phone:
|
||||
Note that the `phones.json` file contains unique IDs and image URLs for each of the phones. The
|
||||
URLs point to the `app/img/phones/` directory.
|
||||
|
||||
__`app/phones/nexus-s.json`:__ (sample snippet)
|
||||
<br />
|
||||
**`app/phones/phones.json`** (sample snippet):
|
||||
|
||||
```js
|
||||
{
|
||||
"additionalFeatures": "Contour Display, Near Field Communications (NFC),...",
|
||||
"android": {
|
||||
"os": "Android 2.3",
|
||||
"ui": "Android"
|
||||
```json
|
||||
[
|
||||
{
|
||||
...
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
...
|
||||
},
|
||||
...
|
||||
"images": [
|
||||
"img/phones/nexus-s.0.jpg",
|
||||
"img/phones/nexus-s.1.jpg",
|
||||
"img/phones/nexus-s.2.jpg",
|
||||
"img/phones/nexus-s.3.jpg"
|
||||
],
|
||||
"storage": {
|
||||
"flash": "16384MB",
|
||||
"ram": "512MB"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
Each of these files describes various properties of the phone using the same data structure. We'll
|
||||
show this data in the phone detail view.
|
||||
## Component Template
|
||||
|
||||
|
||||
## Controller
|
||||
|
||||
We'll expand the `PhoneDetailCtrl` by using the `$http` service to fetch the JSON files. This works
|
||||
the same way as the phone list controller.
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
|
||||
```js
|
||||
var phonecatControllers = angular.module('phonecatControllers',[]);
|
||||
|
||||
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', '$http',
|
||||
function($scope, $routeParams, $http) {
|
||||
$http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) {
|
||||
$scope.phone = data;
|
||||
});
|
||||
}]);
|
||||
```
|
||||
|
||||
To construct the URL for the HTTP request, we use `$routeParams.phoneId` extracted from the current
|
||||
route by the `$route` service.
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
The TBD placeholder line has been replaced with lists and bindings that comprise the phone details.
|
||||
Note where we use the Angular `{{expression}}` markup and `ngRepeat` to project phone data from
|
||||
our model into the view.
|
||||
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
<br />
|
||||
**`app/phone-list/phone-list.template.html`:**
|
||||
|
||||
```html
|
||||
<img ng-src="{{phone.images[0]}}" class="phone">
|
||||
|
||||
<h1>{{phone.name}}</h1>
|
||||
|
||||
<p>{{phone.description}}</p>
|
||||
|
||||
<ul class="phone-thumbs">
|
||||
<li ng-repeat="img in phone.images">
|
||||
<img ng-src="{{img}}">
|
||||
...
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp" class="thumbnail">
|
||||
<a href="#/phones/{{phone.id}}" class="thumb">
|
||||
<img ng-src="{{phone.imageUrl}}" alt="{{phone.name}}" />
|
||||
</a>
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
```
|
||||
|
||||
<ul class="specs">
|
||||
<li>
|
||||
<span>Availability and Networks</span>
|
||||
<dl>
|
||||
<dt>Availability</dt>
|
||||
<dd ng-repeat="availability in phone.availability">{{availability}}</dd>
|
||||
</dl>
|
||||
</li>
|
||||
To dynamically generate links that will in the future lead to phone detail pages, we used the
|
||||
now-familiar double-curly brace binding in the `href` attribute values. In step 2, we added the
|
||||
`{{phone.name}}` binding as the element content. In this step the `{{phone.id}}` binding is used in
|
||||
the element attribute.
|
||||
|
||||
We also added phone images next to each record using an image tag with the {@link ngSrc ngSrc}
|
||||
directive. That directive prevents the browser from treating the Angular `{{ expression }}` markup
|
||||
literally, and initiating a request to an invalid URL (`http://localhost:8000/{{phone.imageUrl}}`),
|
||||
which it would have done if we had only specified an attribute binding in a regular `src` attribute
|
||||
(`<img src="{{phone.imageUrl}}">`). Using the `ngSrc` directive, prevents the browser from making an
|
||||
HTTP request to an invalid location.
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
<br />
|
||||
**`e2e-tests/scenarios.js`**:
|
||||
|
||||
```js
|
||||
...
|
||||
<li>
|
||||
<span>Additional Features</span>
|
||||
<dd>{{phone.additionalFeatures}}</dd>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
<div style="display: none">
|
||||
TODO!
|
||||
<img class="diagram" src="img/tutorial/tutorial_08-09_final.png">
|
||||
</div>
|
||||
it('should render phone specific links', function() {
|
||||
var query = element(by.model('$ctrl.query'));
|
||||
query.sendKeys('nexus');
|
||||
|
||||
## Test
|
||||
|
||||
We wrote a new unit test that is similar to the one we wrote for the `PhoneListCtrl` controller in
|
||||
step 5.
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
|
||||
```js
|
||||
|
||||
beforeEach(module('phonecatApp'));
|
||||
|
||||
...
|
||||
|
||||
describe('PhoneDetailCtrl', function(){
|
||||
var scope, $httpBackend, ctrl;
|
||||
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/xyz.json').respond({name:'phone xyz'});
|
||||
|
||||
$routeParams.phoneId = 'xyz';
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller('PhoneDetailCtrl', {$scope: scope});
|
||||
}));
|
||||
|
||||
|
||||
it('should fetch phone detail', function() {
|
||||
expect(scope.phone).toBeUndefined();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(scope.phone).toEqual({name:'phone xyz'});
|
||||
});
|
||||
});
|
||||
...
|
||||
```
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
<pre>Chrome 22.0: Executed 3 of 3 SUCCESS (0.039 secs / 0.012 secs)</pre>
|
||||
|
||||
|
||||
We also added a new end-to-end test that navigates to the Nexus S detail page and verifies that the
|
||||
heading on the page is "Nexus S".
|
||||
|
||||
__`test/e2e/scenarios.js`:__
|
||||
|
||||
```js
|
||||
...
|
||||
describe('Phone detail view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('app/index.html#/phones/nexus-s');
|
||||
element.all(by.css('.phones li a')).first().click();
|
||||
expect(browser.getLocationAbsUrl()).toBe('/phones/nexus-s');
|
||||
});
|
||||
|
||||
|
||||
it('should display nexus-s page', function() {
|
||||
expect(element(by.binding('phone.name')).getText()).toBe('Nexus S');
|
||||
});
|
||||
});
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
We added a new E2E test to verify that the application is generating correct links to the phone
|
||||
views, that we will implement in the upcoming steps.
|
||||
|
||||
You can now rerun `npm run protractor` to see the tests run.
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Using the [Protractor API](http://angular.github.io/protractor/#/api),
|
||||
write a test that verifies that we display 4 thumbnail images on the Nexus S details page.
|
||||
<div></div>
|
||||
|
||||
* Replace the `ngSrc` directive with a plain old `src` attribute. Using tools such as your browser's
|
||||
developer tools or inspecting the web server access logs, confirm that the application is indeed
|
||||
making an extraneous request to `%7B%7Bphone.imageUrl%7D%7D` (or `{{phone.imageUrl}}`).
|
||||
|
||||
The issue here is that the browser will fire a request for that invalid image address as soon as
|
||||
it hits the `<img>` tag, which is before Angular has a chance to evaluate the expression and
|
||||
inject the valid address.
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now that the phone details view is in place, proceed to {@link step_09 step 9} to learn how to
|
||||
write your own custom display filter.
|
||||
Now that you have added phone images and links, go to {@link step_09 step 9} to learn about Angular
|
||||
layout templates and how Angular makes it easy to create applications that have multiple views.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="8"></ul>
|
||||
|
||||
@@ -1,144 +1,429 @@
|
||||
@ngdoc tutorial
|
||||
@name 9 - Filters
|
||||
@name 9 - Routing & Multiple Views
|
||||
@step 9
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="9"></ul>
|
||||
|
||||
In this step you will learn how to create your own custom display filter.
|
||||
|
||||
* In the previous step, the details page displayed either "true" or "false" to indicate whether
|
||||
certain phone features were present or not. We have used a custom filter to convert those text
|
||||
strings into glyphs: ✓ for "true", and ✘ for "false". Let's see what the filter code looks like.
|
||||
In this step, you will learn how to create a layout template and how to build an application that
|
||||
has multiple views by adding routing, using an Angular module called {@link ngRoute ngRoute}.
|
||||
|
||||
* When you now navigate to `/index.html`, you are redirected to `/index.html#!/phones` and the phone
|
||||
list appears in the browser.
|
||||
* When you click on a phone link, the URL changes to that specific phone and the stub of a phone
|
||||
detail page is displayed.
|
||||
|
||||
|
||||
<div doc-tutorial-reset="9"></div>
|
||||
|
||||
|
||||
## Custom Filter
|
||||
## Dependencies
|
||||
|
||||
In order to create a new filter, you are going to create a `phonecatFilters` module and register
|
||||
your custom filter with this module:
|
||||
The routing functionality added in this step is provided by Angular in the `ngRoute` module, which
|
||||
is distributed separately from the core Angular framework.
|
||||
|
||||
__`app/js/filters.js`:__
|
||||
Since we are using [Bower][bower] to install client-side dependencies, this step updates the
|
||||
`bower.json` configuration file to include the new dependency:
|
||||
|
||||
```js
|
||||
angular.module('phonecatFilters', []).filter('checkmark', function() {
|
||||
return function(input) {
|
||||
return input ? '\u2713' : '\u2718';
|
||||
};
|
||||
});
|
||||
<br />
|
||||
**`bower.json`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "angular-phonecat",
|
||||
"description": "A starter project for AngularJS",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/angular/angular-phonecat",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "1.5.x",
|
||||
"angular-mocks": "1.5.x",
|
||||
"angular-route": "1.5.x",
|
||||
"bootstrap": "3.3.x"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The name of our filter is "checkmark". The `input` evaluates to either `true` or `false`, and we
|
||||
return one of the two unicode characters we have chosen to represent true (`\u2713` -> ✓) or false (`\u2718` -> ✘).
|
||||
The new dependency `"angular-route": "1.5.x"` tells bower to install a version of the angular-route
|
||||
module that is compatible with version 1.5.x of Angular. We must tell bower to download and install
|
||||
this dependency.
|
||||
|
||||
Now that our filter is ready, we need to register the `phonecatFilters` module as a dependency for
|
||||
our main `phonecatApp` module.
|
||||
|
||||
__`app/js/app.js`:__
|
||||
|
||||
```js
|
||||
...
|
||||
angular.module('phonecatApp', ['ngRoute','phonecatControllers','phonecatFilters']);
|
||||
...
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** If you have bower installed globally, you can run `bower install`, but for this project
|
||||
we have preconfigured `npm install` to run bower for us.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then
|
||||
you may have a problem with the `bower install` due to a conflict between the versions of
|
||||
angular.js that need to be installed. If you run into this issue, simply delete your
|
||||
`app/bower_components` directory and then run `npm install`.
|
||||
</div>
|
||||
|
||||
|
||||
## Multiple Views, Routing and Layout Templates
|
||||
|
||||
Our app is slowly growing and becoming more complex. Prior to this step, the app provided our users
|
||||
with a single view (including the list of all phones), and all of the template code was located in
|
||||
the `phone-list.template.html` file. The next step in building the application is to add a view that
|
||||
will show detailed information about each of the devices in our list.
|
||||
|
||||
To add the detailed view, we are going to turn `index.html` into what we call a "layout template".
|
||||
This is a template that is common for all views in our application. Other "partial templates" are
|
||||
then included into this layout template depending on the current "route" — the view that is
|
||||
currently displayed to the user.
|
||||
|
||||
Application routes in Angular are declared via the {@link ngRoute.$routeProvider $routeProvider},
|
||||
which is the provider of the {@link ngRoute.$route $route} service. This service makes it easy to
|
||||
wire together controllers, view templates, and the current URL location in the browser. Using this
|
||||
feature, we can implement [deep linking][deep-linking], which lets us utilize the browser's history
|
||||
(back and forward navigation) and bookmarks.
|
||||
|
||||
<div class="alert alert-success">
|
||||
<p>
|
||||
`ngRoute` lets us associate a controller and a template with a specific URL (or URL
|
||||
pattern). This is pretty close to what we did with `ngController` and `index.html` back in
|
||||
{@link step_02 step 2}.
|
||||
</p>
|
||||
<p>
|
||||
Since we have already learned that components allow us to combine controllers with templates in
|
||||
a modular, testable way, we are going to use components for routing as well.
|
||||
Each route will be associated with a component and that component will be in charge of providing
|
||||
the view template and the controller.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
### A Note about DI, Injector and Providers
|
||||
|
||||
As you {@link step_07 noticed}, {@link guide/di dependency injection} (DI) is at the core of
|
||||
AngularJS, so it's important for you to understand a thing or two about how it works.
|
||||
|
||||
When the application bootstraps, Angular creates an injector that will be used to find and inject
|
||||
all of the services that are required by your application. The injector itself doesn't know anything
|
||||
about what the `$http` or `$route` services do. In fact, the injector doesn't even know about the
|
||||
existence of these services, unless it is configured with proper module definitions.
|
||||
|
||||
The injector only carries out the following steps:
|
||||
|
||||
* Load the module definition(s) that you specify in your application.
|
||||
* Register all Providers defined in these module definition(s).
|
||||
* When asked to do so, lazily instantiate services and their dependencies, via their Providers, as
|
||||
parameters to an injectable function.
|
||||
|
||||
Providers are objects that provide (create) instances of services and expose configuration APIs,
|
||||
that can be used to control the creation and runtime behavior of a service. In case of the `$route`
|
||||
service, the `$routeProvider` exposes APIs that allow you to define routes for your application.
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Note:** Providers can only be injected into `config` functions. Thus you could not inject
|
||||
`$routeProvider` into `PhoneListController` at runtime.
|
||||
</div>
|
||||
|
||||
Angular modules solve the problem of removing global variables from the application and provide a
|
||||
way of configuring the injector. As opposed to AMD or require.js modules, Angular modules don't try
|
||||
to solve the problem of script load ordering or lazy script fetching. These goals are totally
|
||||
independent and both module systems can live side-by-side and fulfill their goals.
|
||||
|
||||
To deepen your understanding on Angular's DI, see [Understanding Dependency Injection][wiki-di].
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
Since the filter code lives in the `app/js/filters.js` file, we need to include this file in our
|
||||
layout template.
|
||||
The `$route` service is usually used in conjunction with the {@link ngRoute.directive:ngView ngView}
|
||||
directive. The role of the `ngView` directive is to include the view template for the current route
|
||||
into the layout template. This makes it a perfect fit for our `index.html` template.
|
||||
|
||||
__`app/index.html`:__
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
...
|
||||
<script src="js/controllers.js"></script>
|
||||
<script src="js/filters.js"></script>
|
||||
...
|
||||
<head>
|
||||
...
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="bower_components/angular-route/angular-route.js"></script>
|
||||
<script src="app.module.js"></script>
|
||||
<script src="app.config.js"></script>
|
||||
...
|
||||
<script src="phone-detail/phone-detail.module.js"></script>
|
||||
<script src="phone-detail/phone-detail.component.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div ng-view></div>
|
||||
|
||||
</body>
|
||||
```
|
||||
|
||||
The syntax for using filters in Angular templates is as follows:
|
||||
We have added four new `<script>` tags in our `index.html` file to load some extra JavaScript files
|
||||
into our application:
|
||||
|
||||
{{ expression | filter }}
|
||||
* `angular-route.js`: Defines the Angular `ngRoute` module, which provides us with routing.
|
||||
* `app.config.js`: Configures the providers available to our main module (see
|
||||
[below](tutorial/step_09#configuring-a-module)).
|
||||
* `phone-detail.module.js`: Defines a new module containing a `phoneDetail` component.
|
||||
* `phone-detail.component.js`: Defines a dummy `phoneDetail` component (see
|
||||
[below](tutorial/step_09#the-phonedetail-component)).
|
||||
|
||||
Let's employ the filter in the phone details template:
|
||||
Note that we removed the `<phone-list></phone-list>` line from the `index.html` template and
|
||||
replaced it with a `<div>` with the `ng-view` attribute.
|
||||
|
||||
<img class="diagram" src="img/tutorial/tutorial_09.png">
|
||||
|
||||
|
||||
## Configuring a Module
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
A module's {@link ng.angular.Module#config .config()} method gives us access to the available
|
||||
providers for configuration. To make the providers, services and directives defined in `ngRoute`
|
||||
available to our application, we need to add `ngRoute` as a dependency of our `phonecatApp` module.
|
||||
|
||||
```html
|
||||
...
|
||||
<dl>
|
||||
<dt>Infrared</dt>
|
||||
<dd>{{phone.connectivity.infrared | checkmark}}</dd>
|
||||
<dt>GPS</dt>
|
||||
<dd>{{phone.connectivity.gps | checkmark}}</dd>
|
||||
</dl>
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
Filters, like any other component, should be tested and these tests are very easy to write.
|
||||
|
||||
__`test/unit/filtersSpec.js`:__
|
||||
<br />
|
||||
**`app/app.module.js`:**
|
||||
|
||||
```js
|
||||
describe('filter', function() {
|
||||
|
||||
beforeEach(module('phonecatFilters'));
|
||||
|
||||
describe('checkmark', function() {
|
||||
|
||||
it('should convert boolean values to unicode checkmark or cross',
|
||||
inject(function(checkmarkFilter) {
|
||||
expect(checkmarkFilter(true)).toBe('\u2713');
|
||||
expect(checkmarkFilter(false)).toBe('\u2718');
|
||||
}));
|
||||
});
|
||||
});
|
||||
angular.module('phonecatApp', [
|
||||
'ngRoute',
|
||||
...
|
||||
]);
|
||||
```
|
||||
|
||||
We must call `beforeEach(module('phonecatFilters'))` before any of
|
||||
our filter tests execute. This call loads our `phonecatFilters` module into the injector
|
||||
for this test run.
|
||||
Now, in addition to the core services and directives, we can also configure the `$route` service
|
||||
(using it's provider) for our application. In order to be able to quickly locate the configuration
|
||||
code, we put it into a separate file and used the `.config` suffix.
|
||||
|
||||
Note that we call the helper function, `inject(function(checkmarkFilter) { ... })`, to get
|
||||
access to the filter that we want to test. See {@link angular.mock.inject angular.mock.inject()}.
|
||||
<br />
|
||||
**`app/app.config.js`:**
|
||||
|
||||
Notice that the suffix 'Filter' is appended to your filter name when injected.
|
||||
See the {@link guide/filter#using-filters-in-controllers-services-and-directives Filter Guide}
|
||||
section where this is outlined.
|
||||
```js
|
||||
angular.
|
||||
module('phonecatApp').
|
||||
config(['$locationProvider', '$routeProvider',
|
||||
function config($locationProvider, $routeProvider) {
|
||||
$locationProvider.hashPrefix('!');
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
$routeProvider.
|
||||
when('/phones', {
|
||||
template: '<phone-list></phone-list>'
|
||||
}).
|
||||
when('/phones/:phoneId', {
|
||||
template: '<phone-detail></phone-detail>'
|
||||
}).
|
||||
otherwise('/phones');
|
||||
}
|
||||
]);
|
||||
```
|
||||
|
||||
<pre>Chrome 22.0: Executed 4 of 4 SUCCESS (0.034 secs / 0.012 secs)</pre>
|
||||
Using the `.config()` method, we request the necessary providers (for example the `$routeProvider`)
|
||||
to be injected into our configuration function and then use their methods to specify the behavior of
|
||||
the corresponding services. Here, we use the
|
||||
{@link ngRoute.$routeProvider#when $routeProvider.when()} and
|
||||
{@link ngRoute.$routeProvider#otherwise $routeProvider.otherwise()} methods to define our
|
||||
application routes.
|
||||
|
||||
<div class="alert alert-success">
|
||||
<p>
|
||||
We also used {@link $locationProvider#hashPrefix $locationProvider.hashPrefix()} to set the
|
||||
hash-prefix to `!`. This prefix will appear in the links to our client-side routes, right after
|
||||
the hash (`#`) symbol and before the actual path (e.g. `index.html#!/some/path`).
|
||||
</p>
|
||||
<p>
|
||||
Setting a prefix is not necessary, but it is considered a good practice (for reasons that are
|
||||
outside the scope of this tutorial). `!` is the most commonly used prefix.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Our routes are defined as follows:
|
||||
|
||||
* `when('/phones')`: Determines the view that will be shown, when the URL hash fragment is
|
||||
`/phones`. According to the specified template, Angular will create an instance of the `phoneList`
|
||||
component to manage the view. Note that this is the same markup that we used to have in the
|
||||
`index.html` file.
|
||||
|
||||
* `when('/phones/:phoneId')`: Determines the view that will be shown, when the URL hash fragment
|
||||
matches `/phones/<phoneId>`, where `<phoneId>` is a variable part of the URL. In charge of the
|
||||
view will be the `phoneDetail` component.
|
||||
|
||||
* `otherwise('/phones')`: Defines a fallback route to redirect to, when no route definition matches
|
||||
the current URL.(Here it will redirect to `/phones`.)
|
||||
|
||||
We reused the `phoneList` component that we have already built and a new "dummy" `phoneDetail`
|
||||
component. For now, the `phoneDetail` component will just display the selected phone's ID.
|
||||
(Not too impressive, but we will enhance it in the {@link step_10 next step}.)
|
||||
|
||||
Note the use of the `:phoneId` parameter in the second route declaration. The `$route` service uses
|
||||
the route declaration — `'/phones/:phoneId'` — as a template that is matched against the current
|
||||
URL. All variables defined with the `:` prefix are extracted into the (injectable)
|
||||
{@link ngRoute.$routeParams $routeParams} object.
|
||||
|
||||
|
||||
## The `phoneDetail` Component
|
||||
|
||||
We created a `phoneDetail` component to handle the phone details view. We followed the same
|
||||
conventions as with `phoneList`, using a separate directory and creating a `phoneDetail` module,
|
||||
which we added as a dependency of the `phonecatApp` module.
|
||||
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.module.js`:**
|
||||
|
||||
```js
|
||||
angular.module('phoneDetail', [
|
||||
'ngRoute'
|
||||
]);
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.component.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phoneDetail').
|
||||
component('phoneDetail', {
|
||||
template: 'TBD: Detail view for <span>{{$ctrl.phoneId}}</span>',
|
||||
controller: ['$routeParams',
|
||||
function PhoneDetailController($routeParams) {
|
||||
this.phoneId = $routeParams.phoneId;
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
<br />
|
||||
**`app/app.module.js`:**
|
||||
|
||||
```js
|
||||
angular.module('phonecatApp', [
|
||||
...
|
||||
'phoneDetail',
|
||||
...
|
||||
]);
|
||||
```
|
||||
|
||||
### A Note on Sub-module Dependencies
|
||||
|
||||
The `phoneDetail` module depends on the `ngRoute` module for providing the `$routeParams` object,
|
||||
which is used in the `phoneDetail` component's controller. Since `ngRoute` is also a dependency of
|
||||
the main `phonecatApp` module, its services and directives are already available everywhere in the
|
||||
application (including the `phoneDetail` component).
|
||||
|
||||
This means that our application would continue to work even if we didn't include `ngRoute` in the
|
||||
list of dependencies for the `phoneDetail` component. Although it might be tempting to omit
|
||||
dependencies of a sub-module that are already imported by the main module, it breaks our hard-earned
|
||||
modularity.
|
||||
|
||||
<div class="alert alert-warning">
|
||||
Imagine what would happen if we decided to copy the `phoneDetail` feature over to another project
|
||||
that does not declare a dependency on `ngRoute`. The injector would not be able to provide
|
||||
`$routeParams` and our application would break.
|
||||
</div>
|
||||
|
||||
The takeaway here is:
|
||||
|
||||
* Always be explicit about the dependecies of a sub-module. Do not rely on dependencies inherited
|
||||
from a parent module (because that parent module might not be there some day).
|
||||
|
||||
<div class="alert alert-success">
|
||||
Declaring the same dependency in multiple modules does not incur extra "cost", because Angular
|
||||
will still load each dependency once. For more info on modules and their dependencies take a look
|
||||
at the [Modules](guide/module) section of the Developer Guide.
|
||||
</div>
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
Since some of our modules depend on {@link ngRoute ngRoute} now, it is necessary to update the Karma
|
||||
configuration file with angular-route. Other than that, the unit tests should (still) pass without
|
||||
any modification.
|
||||
|
||||
<br />
|
||||
**`karma.conf.js`:**
|
||||
|
||||
```js
|
||||
files: [
|
||||
'bower_components/angular/angular.js',
|
||||
'bower_components/angular-route/angular-route.js',
|
||||
...
|
||||
],
|
||||
```
|
||||
|
||||
<br />
|
||||
To automatically verify that everything is wired properly, we wrote E2E tests for navigating to
|
||||
various URLs and verifying that the correct view was rendered.
|
||||
|
||||
<br />
|
||||
**`e2e-tests/scenarios.js`**
|
||||
|
||||
```js
|
||||
...
|
||||
|
||||
it('should redirect `index.html` to `index.html#!/phones', function() {
|
||||
browser.get('index.html');
|
||||
expect(browser.getLocationAbsUrl()).toBe('/phones');
|
||||
});
|
||||
|
||||
...
|
||||
|
||||
describe('View: Phone list', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('index.html#!/phones');
|
||||
});
|
||||
|
||||
...
|
||||
|
||||
});
|
||||
|
||||
...
|
||||
|
||||
describe('View: Phone details', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('index.html#!/phones/nexus-s');
|
||||
});
|
||||
|
||||
it('should display placeholder page with `phoneId`', function() {
|
||||
expect(element(by.binding('$ctrl.phoneId')).getText()).toBe('nexus-s');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
You can now rerun `npm run protractor` to see the tests run (and hopefully pass).
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Let's experiment with some of the {@link api/ng/filter built-in Angular filters} and add the
|
||||
following bindings to `index.html`:
|
||||
* `{{ "lower cap string" | uppercase }}`
|
||||
* `{{ {foo: "bar", baz: 23} | json }}`
|
||||
* `{{ 1304375948024 | date }}`
|
||||
* `{{ 1304375948024 | date:"MM/dd/yyyy @ h:mma" }}`
|
||||
<div></div>
|
||||
|
||||
* We can also create a model with an input element, and combine it with a filtered binding. Add
|
||||
the following to index.html:
|
||||
* Try to add a `{{$ctrl.phoneId}` binding in the template string for the phone details view:
|
||||
|
||||
```html
|
||||
<input ng-model="userInput"> Uppercased: {{ userInput | uppercase }}
|
||||
```js
|
||||
when('/phones/:phoneId', {
|
||||
template: '{{$ctrl.phoneId}} <phone-detail></phone-detail>'
|
||||
...
|
||||
```
|
||||
|
||||
You will see that nothing happens, even when you are in the phone details view. This is because
|
||||
the `phoneId` model is visible only in the context set by the `phoneDetail` component. Again,
|
||||
component isolation at work!
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now that you have learned how to write and test a custom filter, go to {@link step_10 step 10} to
|
||||
learn how we can use Angular to enhance the phone details page further.
|
||||
With the routing set up and the phone list view implemented, we are ready to go to
|
||||
{@link step_10 step 10} and implement a proper phone details view.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="9"></ul>
|
||||
|
||||
|
||||
[bower]: http://bower.io
|
||||
[deep-linking]: https://en.wikipedia.org/wiki/Deep_linking
|
||||
[wiki-di]: https://github.com/angular/angular.js/wiki/Understanding-Dependency-Injection
|
||||
|
||||
+153
-119
@@ -1,180 +1,214 @@
|
||||
@ngdoc tutorial
|
||||
@name 10 - Event Handlers
|
||||
@name 10 - More Templating
|
||||
@step 10
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="10"></ul>
|
||||
|
||||
|
||||
In this step, you will add a clickable phone image swapper to the phone details page.
|
||||
In this step, we will implement the phone details view, which is displayed when a user clicks on a
|
||||
phone in the phone list.
|
||||
|
||||
* When you click on a phone on the list, the phone details page with phone-specific information is
|
||||
displayed.
|
||||
|
||||
To implement the phone details view we are going to use {@link ng.$http $http} to fetch our data,
|
||||
and then flesh out the `phoneDetail` component's template.
|
||||
|
||||
* The phone details view displays one large image of the current phone and several smaller thumbnail
|
||||
images. It would be great if we could replace the large image with any of the thumbnails just by
|
||||
clicking on the desired thumbnail image. Let's have a look at how we can do this with Angular.
|
||||
|
||||
<div doc-tutorial-reset="10"></div>
|
||||
|
||||
## Controller
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
## Data
|
||||
|
||||
```js
|
||||
...
|
||||
var phonecatControllers = angular.module('phonecatControllers',[]);
|
||||
In addition to `phones.json`, the `app/phones/` directory also contains one JSON file for each
|
||||
phone:
|
||||
|
||||
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', '$http',
|
||||
function($scope, $routeParams, $http) {
|
||||
$http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) {
|
||||
$scope.phone = data;
|
||||
$scope.mainImageUrl = data.images[0];
|
||||
});
|
||||
<br />
|
||||
**`app/phones/nexus-s.json`:** (sample snippet)
|
||||
|
||||
$scope.setImage = function(imageUrl) {
|
||||
$scope.mainImageUrl = imageUrl;
|
||||
};
|
||||
}]);
|
||||
```json
|
||||
{
|
||||
"additionalFeatures": "Contour Display, Near Field Communications (NFC), ...",
|
||||
"android": {
|
||||
"os": "Android 2.3",
|
||||
"ui": "Android"
|
||||
},
|
||||
...
|
||||
"images": [
|
||||
"img/phones/nexus-s.0.jpg",
|
||||
"img/phones/nexus-s.1.jpg",
|
||||
"img/phones/nexus-s.2.jpg",
|
||||
"img/phones/nexus-s.3.jpg"
|
||||
],
|
||||
"storage": {
|
||||
"flash": "16384MB",
|
||||
"ram": "512MB"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the `PhoneDetailCtrl` controller, we created the `mainImageUrl` model property and set its
|
||||
default value to the first phone image URL.
|
||||
|
||||
We also created a `setImage` event handler function that will change the value of `mainImageUrl`.
|
||||
Each of these files describes various properties of the phone using the same data structure. We will
|
||||
show this data in the phone details view.
|
||||
|
||||
|
||||
## Template
|
||||
## Component Controller
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
We will expand the `phoneDetail` component's controller by using the `$http` service to fetch the
|
||||
appropriate JSON files. This works the same way as the `phoneList` component's controller.
|
||||
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.component.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('phoneDetail').
|
||||
component('phoneDetail', {
|
||||
templateUrl: 'phone-detail/phone-detail.template.html',
|
||||
controller: ['$http', '$routeParams',
|
||||
function PhoneDetailController($http, $routeParams) {
|
||||
var self = this;
|
||||
|
||||
$http.get('phones/' + $routeParams.phoneId + '.json').then(function(response) {
|
||||
self.phone = response.data;
|
||||
});
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
To construct the URL for the HTTP request, we use `$routeParams.phoneId`, which is extracted from
|
||||
the current route by the `$route` service.
|
||||
|
||||
|
||||
## Component Template
|
||||
|
||||
The inline, TBD placeholder template has been replaced with a full blown external template,
|
||||
including lists and bindings that comprise the phone details. Note how we use the Angular
|
||||
`{{expression}}` markup and `ngRepeat` to project phone data from our model into the view.
|
||||
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.template.html`:**
|
||||
|
||||
```html
|
||||
<img ng-src="{{mainImageUrl}}" class="phone">
|
||||
<img ng-src="{{$ctrl.phone.images[0]}}" class="phone" />
|
||||
|
||||
...
|
||||
<h1>{{$ctrl.phone.name}}</h1>
|
||||
|
||||
<p>{{$ctrl.phone.description}}</p>
|
||||
|
||||
<ul class="phone-thumbs">
|
||||
<li ng-repeat="img in phone.images">
|
||||
<img ng-src="{{img}}" ng-click="setImage(img)">
|
||||
<li ng-repeat="img in $ctrl.phone.images">
|
||||
<img ng-src="{{img}}" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="specs">
|
||||
<li>
|
||||
<span>Availability and Networks</span>
|
||||
<dl>
|
||||
<dt>Availability</dt>
|
||||
<dd ng-repeat="availability in $ctrl.phone.availability">{{availability}}</dd>
|
||||
</dl>
|
||||
</li>
|
||||
...
|
||||
<li>
|
||||
<span>Additional Features</span>
|
||||
<dd>{{$ctrl.phone.additionalFeatures}}</dd>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
```
|
||||
|
||||
We bound the `ngSrc` directive of the large image to the `mainImageUrl` property.
|
||||
<img class="diagram" src="img/tutorial/tutorial_10.png">
|
||||
|
||||
We also registered an {@link ng.directive:ngClick `ngClick`}
|
||||
handler with thumbnail images. When a user clicks on one of the thumbnail images, the handler will
|
||||
use the `setImage` event handler function to change the value of the `mainImageUrl` property to the
|
||||
URL of the thumbnail image.
|
||||
|
||||
<div style="display: none">
|
||||
TODO!
|
||||
<img class="diagram" src="img/tutorial/tutorial_10-11_final.png">
|
||||
</div>
|
||||
# Testing
|
||||
|
||||
## Test
|
||||
We wrote a new unit test that is similar to the one we wrote for the `phoneList` component's
|
||||
controller in {@link step_07#testing step 7}.
|
||||
|
||||
To verify this new feature, we added two end-to-end tests. One verifies that the main image is set
|
||||
to the first phone image by default. The second test clicks on several thumbnail images and
|
||||
verifies that the main image changed appropriately.
|
||||
|
||||
__`test/e2e/scenarios.js`:__
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.component.spec.js`:**
|
||||
|
||||
```js
|
||||
...
|
||||
describe('Phone detail view', function() {
|
||||
describe('phoneDetail', function() {
|
||||
|
||||
...
|
||||
// Load the module that contains the `phoneDetail` component before each test
|
||||
beforeEach(module('phoneDetail'));
|
||||
|
||||
it('should display the first phone image as the main phone image', function() {
|
||||
expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
|
||||
});
|
||||
// Test the controller
|
||||
describe('PhoneDetailController', function() {
|
||||
var $httpBackend, ctrl;
|
||||
|
||||
|
||||
it('should swap main image if a thumbnail image is clicked on', function() {
|
||||
element(by.css('.phone-thumbs li:nth-child(3) img')).click();
|
||||
expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.2.jpg/);
|
||||
|
||||
element(by.css('.phone-thumbs li:nth-child(1) img')).click();
|
||||
expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
You can now rerun `npm run protractor` to see the tests run.
|
||||
|
||||
|
||||
You also have to refactor one of your unit tests because of the addition of the `mainImageUrl`
|
||||
model property to the `PhoneDetailCtrl` controller. Below, we create the function `xyzPhoneData`
|
||||
which returns the appropriate json with the `images` attribute in order to get the test to pass.
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
|
||||
```js
|
||||
...
|
||||
beforeEach(module('phonecatApp'));
|
||||
|
||||
...
|
||||
|
||||
describe('PhoneDetailCtrl', function(){
|
||||
var scope, $httpBackend, ctrl,
|
||||
xyzPhoneData = function() {
|
||||
return {
|
||||
name: 'phone xyz',
|
||||
images: ['image/url1.png', 'image/url2.png']
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) {
|
||||
beforeEach(inject(function($componentController, _$httpBackend_, $routeParams) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData());
|
||||
$httpBackend.expectGET('phones/xyz.json').respond({name: 'phone xyz'});
|
||||
|
||||
$routeParams.phoneId = 'xyz';
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller('PhoneDetailCtrl', {$scope: scope});
|
||||
|
||||
ctrl = $componentController('phoneDetail');
|
||||
}));
|
||||
|
||||
it('should fetch the phone details', function() {
|
||||
expect(ctrl.phone).toBeUndefined();
|
||||
|
||||
it('should fetch phone detail', function() {
|
||||
expect(scope.phone).toBeUndefined();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(scope.phone).toEqual(xyzPhoneData());
|
||||
expect(ctrl.phone).toEqual({name: 'phone xyz'});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
Your unit tests should now be passing.
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
```
|
||||
Chrome 49.0: Executed 3 of 3 SUCCESS (0.159 secs / 0.136 secs)
|
||||
```
|
||||
|
||||
We also added a new E2E test that navigates to the 'Nexus S' details page and verifies that the
|
||||
heading on the page is "Nexus S".
|
||||
|
||||
<br />
|
||||
**`e2e-tests/scenarios.js`**
|
||||
|
||||
```js
|
||||
...
|
||||
|
||||
describe('View: Phone detail', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.get('index.html#!/phones/nexus-s');
|
||||
});
|
||||
|
||||
it('should display the `nexus-s` page', function() {
|
||||
expect(element(by.binding('$ctrl.phone.name')).getText()).toBe('Nexus S');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
You can run the tests with `npm run protractor`.
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
* Let's add a new controller method to `PhoneDetailCtrl`:
|
||||
<div></div>
|
||||
|
||||
$scope.hello = function(name) {
|
||||
alert('Hello ' + (name || 'world') + '!');
|
||||
}
|
||||
|
||||
and add:
|
||||
|
||||
<button ng-click="hello('Elmo')">Hello</button>
|
||||
|
||||
to the `phone-detail.html` template.
|
||||
|
||||
<div style="display: none">
|
||||
TODO!
|
||||
The controller methods are inherited between controllers/scopes, so you can use the same snippet
|
||||
in the `phone-list.html` template as well.
|
||||
|
||||
* Move the `hello` method from `PhoneCatCtrl` to `PhoneListCtrl` and you'll see that the button
|
||||
declared in `index.html` will stop working, while the one declared in the `phone-list.html`
|
||||
template remains operational.
|
||||
</div>
|
||||
* Using [Protractor's API][protractor-docs], write a test that verifies that we display 4 thumbnail
|
||||
images on the 'Nexus S' details page.
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
With the phone image swapper in place, we're ready for {@link step_11 step 11} to
|
||||
learn an even better way to fetch data.
|
||||
Now that the phone details view is in place, proceed to {@link step_11 step 11} to learn how to
|
||||
write your own custom display filter.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="10"></ul>
|
||||
|
||||
|
||||
[protractor-docs]: https://angular.github.io/protractor/#/api
|
||||
|
||||
+119
-235
@@ -1,292 +1,176 @@
|
||||
@ngdoc tutorial
|
||||
@name 11 - REST and Custom Services
|
||||
@name 11 - Custom Filters
|
||||
@step 11
|
||||
@description
|
||||
|
||||
<ul doc-tutorial-nav="11"></ul>
|
||||
|
||||
|
||||
In this step, you will change the way our app fetches data.
|
||||
In this step you will learn how to create your own custom display filter.
|
||||
|
||||
* We define a custom service that represents a [RESTful][restful] client. Using this client we
|
||||
can make requests to the server for data in an easier way, without having to deal with the
|
||||
lower-level {@link ng.$http $http} API, HTTP methods and URLs.
|
||||
* In the previous step, the details page displayed either "true" or "false" to indicate whether
|
||||
certain phone features were present or not. In this step, we are using a custom filter to convert
|
||||
those text strings into glyphs: ✓ for "true", and ✘ for "false".
|
||||
|
||||
Let's see what the filter code looks like.
|
||||
|
||||
|
||||
<div doc-tutorial-reset="11"></div>
|
||||
|
||||
## Dependencies
|
||||
|
||||
The RESTful functionality is provided by Angular in the `ngResource` module, which is distributed
|
||||
separately from the core Angular framework.
|
||||
## The `checkmark` Filter
|
||||
|
||||
We are using [Bower][bower] to install client side dependencies. This step updates the
|
||||
`bower.json` configuration file to include the new dependency:
|
||||
Since this filter is generic (i.e. it is not specific to any view or component), we are going to
|
||||
register it in a `core` module, which contains "application-wide" features.
|
||||
|
||||
```
|
||||
{
|
||||
"name": "angular-seed",
|
||||
"description": "A starter project for AngularJS",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/angular/angular-seed",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "~1.3.0",
|
||||
"angular-mocks": "~1.3.0",
|
||||
"bootstrap": "~3.1.1",
|
||||
"angular-route": "~1.3.0",
|
||||
"angular-resource": "~1.3.0"
|
||||
}
|
||||
}
|
||||
<br />
|
||||
**`app/core/core.module.js`:**
|
||||
|
||||
```js
|
||||
angular.module('core', []);
|
||||
```
|
||||
|
||||
The new dependency `"angular-resource": "~1.3.0"` tells bower to install a version of the
|
||||
angular-resource component that is compatible with version 1.3.x. We must ask bower to download
|
||||
and install this dependency. We can do this by running:
|
||||
<br />
|
||||
**`app/core/checkmark/checkmark.filter.js`:**
|
||||
|
||||
```js
|
||||
angular.
|
||||
module('core').
|
||||
filter('checkmark', function() {
|
||||
return function(input) {
|
||||
return input ? '\u2713' : '\u2718';
|
||||
};
|
||||
});
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then you may have a
|
||||
problem with the `bower install` due to a conflict between the versions of angular.js that need to
|
||||
be installed. If you get this then simply delete your `app/bower_components` folder before running
|
||||
`npm install`.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** If you have bower installed globally then you can run `bower install` but for this project we have
|
||||
preconfigured `npm install` to run bower for us.
|
||||
As you may have noticed, we (unsurprisingly) gave our file a `.filter` suffix.
|
||||
</div>
|
||||
|
||||
The name of our filter is "checkmark". The `input` evaluates to either `true` or `false`, and we
|
||||
return one of the two unicode characters we have chosen to represent true (`\u2713` -> ✓) and false
|
||||
(`\u2718` -> ✘).
|
||||
|
||||
## Template
|
||||
Now that our filter is ready, we need to register the `core` module as a dependency of our main
|
||||
`phonecatApp` module.
|
||||
|
||||
Our custom resource service will be defined in `app/js/services.js` so we need to include this file
|
||||
in our layout template. Additionally, we also need to load the `angular-resource.js` file, which
|
||||
contains the {@link module:ngResource ngResource} module:
|
||||
<br />
|
||||
**`app/app.module.js`:**
|
||||
|
||||
__`app/index.html`.__
|
||||
```js
|
||||
angular.module('phonecatApp', [
|
||||
...
|
||||
'core',
|
||||
...
|
||||
]);
|
||||
```
|
||||
|
||||
|
||||
## Templates
|
||||
|
||||
Since we have created two new files (**core.module.js**, **checkmark.filter.js**), we need to
|
||||
include them in our layout template.
|
||||
|
||||
<br />
|
||||
**`app/index.html`:**
|
||||
|
||||
```html
|
||||
...
|
||||
<script src="bower_components/angular-resource/angular-resource.js"></script>
|
||||
<script src="js/services.js"></script>
|
||||
...
|
||||
...
|
||||
<script src="core/core.module.js"></script>
|
||||
<script src="core/checkmark/checkmark.filter.js"></script>
|
||||
...
|
||||
```
|
||||
|
||||
## Service
|
||||
The syntax for using filters in Angular templates is as follows:
|
||||
|
||||
We create our own service to provide access to the phone data on the server:
|
||||
|
||||
__`app/js/services.js`.__
|
||||
|
||||
```js
|
||||
var phonecatServices = angular.module('phonecatServices', ['ngResource']);
|
||||
|
||||
phonecatServices.factory('Phone', ['$resource',
|
||||
function($resource){
|
||||
return $resource('phones/:phoneId.json', {}, {
|
||||
query: {method:'GET', params:{phoneId:'phones'}, isArray:true}
|
||||
});
|
||||
}]);
|
||||
```
|
||||
{{expression | filter}}
|
||||
```
|
||||
|
||||
We used the module API to register a custom service using a factory function. We passed in the name
|
||||
of the service - 'Phone' - and the factory function. The factory function is similar to a
|
||||
controller's constructor in that both can declare dependencies to be injected via function
|
||||
arguments. The Phone service declared a dependency on the `$resource` service.
|
||||
Let's employ the filter in the phone details template:
|
||||
|
||||
The {@link ngResource.$resource `$resource`} service makes it easy to create a
|
||||
[RESTful][restful] client with just a few lines of code. This client can then be used in our
|
||||
application, instead of the lower-level {@link ng.$http $http} service.
|
||||
<br />
|
||||
**`app/phone-detail/phone-detail.template.html`:**
|
||||
|
||||
__`app/js/app.js`.__
|
||||
|
||||
```js
|
||||
...
|
||||
angular.module('phonecatApp', ['ngRoute', 'phonecatControllers','phonecatFilters', 'phonecatServices']).
|
||||
...
|
||||
```html
|
||||
...
|
||||
<dl>
|
||||
<dt>Infrared</dt>
|
||||
<dd>{{$ctrl.phone.connectivity.infrared | checkmark}}</dd>
|
||||
<dt>GPS</dt>
|
||||
<dd>{{$ctrl.phone.connectivity.gps | checkmark}}</dd>
|
||||
</dl>
|
||||
...
|
||||
```
|
||||
|
||||
We need to add the 'phonecatServices' module dependency to 'phonecatApp' module's requires array.
|
||||
|
||||
# Testing
|
||||
|
||||
## Controller
|
||||
Filters, like any other code, should be tested. Luckily, these tests are very easy to write.
|
||||
|
||||
We simplified our sub-controllers (`PhoneListCtrl` and `PhoneDetailCtrl`) by factoring out the
|
||||
lower-level {@link ng.$http $http} service, replacing it with a new service called
|
||||
`Phone`. Angular's {@link ngResource.$resource `$resource`} service is easier to
|
||||
use than `$http` for interacting with data sources exposed as RESTful resources. It is also easier
|
||||
now to understand what the code in our controllers is doing.
|
||||
|
||||
__`app/js/controllers.js`.__
|
||||
<br />
|
||||
**`app/core/checkmark/checkmark.filter.spec.js`:**
|
||||
|
||||
```js
|
||||
var phonecatControllers = angular.module('phonecatControllers', []);
|
||||
describe('checkmark', function() {
|
||||
|
||||
...
|
||||
beforeEach(module('core'));
|
||||
|
||||
phonecatControllers.controller('PhoneListCtrl', ['$scope', 'Phone', function($scope, Phone) {
|
||||
$scope.phones = Phone.query();
|
||||
$scope.orderProp = 'age';
|
||||
}]);
|
||||
it('should convert boolean values to unicode checkmark or cross',
|
||||
inject(function(checkmarkFilter) {
|
||||
expect(checkmarkFilter(true)).toBe('\u2713');
|
||||
expect(checkmarkFilter(false)).toBe('\u2718');
|
||||
})
|
||||
);
|
||||
|
||||
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', 'Phone', function($scope, $routeParams, Phone) {
|
||||
$scope.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) {
|
||||
$scope.mainImageUrl = phone.images[0];
|
||||
});
|
||||
|
||||
$scope.setImage = function(imageUrl) {
|
||||
$scope.mainImageUrl = imageUrl;
|
||||
}
|
||||
}]);
|
||||
```
|
||||
|
||||
Notice how in `PhoneListCtrl` we replaced:
|
||||
|
||||
$http.get('phones/phones.json').success(function(data) {
|
||||
$scope.phones = data;
|
||||
});
|
||||
|
||||
with:
|
||||
|
||||
$scope.phones = Phone.query();
|
||||
|
||||
This is a simple statement that we want to query for all phones.
|
||||
|
||||
An important thing to notice in the code above is that we don't pass any callback functions when
|
||||
invoking methods of our Phone service. Although it looks as if the result were returned
|
||||
synchronously, that is not the case at all. What is returned synchronously is a "future" — an
|
||||
object, which will be filled with data when the XHR response returns. Because of the data-binding
|
||||
in Angular, we can use this future and bind it to our template. Then, when the data arrives, the
|
||||
view will automatically update.
|
||||
|
||||
Sometimes, relying on the future object and data-binding alone is not sufficient to do everything
|
||||
we require, so in these cases, we can add a callback to process the server response. The
|
||||
`PhoneDetailCtrl` controller illustrates this by setting the `mainImageUrl` in a callback.
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
Because we're now using the {@link ngResource ngResource} module, it's necessary to
|
||||
update the Karma config file with angular-resource so the new tests will pass.
|
||||
|
||||
__`test/karma.conf.js`:__
|
||||
|
||||
```js
|
||||
files : [
|
||||
'app/bower_components/angular/angular.js',
|
||||
'app/bower_components/angular-route/angular-route.js',
|
||||
'app/bower_components/angular-resource/angular-resource.js',
|
||||
'app/bower_components/angular-mocks/angular-mocks.js',
|
||||
'app/js/**/*.js',
|
||||
'test/unit/**/*.js'
|
||||
],
|
||||
```
|
||||
|
||||
We have modified our unit tests to verify that our new service is issuing HTTP requests and
|
||||
processing them as expected. The tests also check that our controllers are interacting with the
|
||||
service correctly.
|
||||
|
||||
The {@link ngResource.$resource $resource} service augments the response object
|
||||
with methods for updating and deleting the resource. If we were to use the standard `toEqual`
|
||||
matcher, our tests would fail because the test values would not match the responses exactly. To
|
||||
solve the problem, we use a newly-defined `toEqualData` [Jasmine matcher][jasmine-matchers]. When
|
||||
the `toEqualData` matcher compares two objects, it takes only object properties into account and
|
||||
ignores methods.
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
|
||||
|
||||
```js
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
beforeEach(function(){
|
||||
this.addMatchers({
|
||||
toEqualData: function(expected) {
|
||||
return angular.equals(this.actual, expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(module('phonecatApp'));
|
||||
beforeEach(module('phonecatServices'));
|
||||
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, ctrl, $httpBackend;
|
||||
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/phones.json').
|
||||
respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller('PhoneListCtrl', {$scope: scope});
|
||||
}));
|
||||
|
||||
|
||||
it('should create "phones" model with 2 phones fetched from xhr', function() {
|
||||
expect(scope.phones).toEqualData([]);
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(scope.phones).toEqualData(
|
||||
[{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
});
|
||||
|
||||
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(scope.orderProp).toBe('age');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('PhoneDetailCtrl', function(){
|
||||
var scope, $httpBackend, ctrl,
|
||||
xyzPhoneData = function() {
|
||||
return {
|
||||
name: 'phone xyz',
|
||||
images: ['image/url1.png', 'image/url2.png']
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData());
|
||||
|
||||
$routeParams.phoneId = 'xyz';
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller('PhoneDetailCtrl', {$scope: scope});
|
||||
}));
|
||||
|
||||
|
||||
it('should fetch phone detail', function() {
|
||||
expect(scope.phone).toEqualData({});
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(scope.phone).toEqualData(xyzPhoneData());
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
The call to `beforeEach(module('core'))` loads the `core` module (which contains the `checkmark`
|
||||
filter) into the injector, before every test.
|
||||
|
||||
Note that we call the helper function `inject(function(checkmarkFilter) {...})`, to get access to
|
||||
the filter that we want to test. See also {@link angular.mock.inject angular.mock.inject()}.
|
||||
|
||||
<div class="alert alert-info">
|
||||
When injecting a filter, we need to suffix the filter name with 'Filter'. For example, our
|
||||
`checkmark` filter is injected as `checkmarkFilter`.
|
||||
See the [Filters](guide/filter#using-filters-in-controllers-services-and-directives) section of
|
||||
the Developer Guide for more info.
|
||||
</div>
|
||||
|
||||
You should now see the following output in the Karma tab:
|
||||
|
||||
<pre>Chrome 22.0: Executed 5 of 5 SUCCESS (0.038 secs / 0.01 secs)</pre>
|
||||
```
|
||||
Chrome 49.0: Executed 4 of 4 SUCCESS (0.091 secs / 0.075 secs)
|
||||
```
|
||||
|
||||
|
||||
# Experiments
|
||||
|
||||
<div></div>
|
||||
|
||||
* Let's experiment with some of the {@link api/ng/filter built-in Angular filters}.
|
||||
Add the following bindings to `index.html`:
|
||||
|
||||
* `{{'lower cap string' | uppercase}}`
|
||||
* `{{{foo: 'bar', baz: 42} | json}}`
|
||||
* `{{1459461289000 | date}}`
|
||||
* `{{1459461289000 | date:'MM/dd/yyyy @ h:mma'}}`
|
||||
|
||||
|
||||
* We can also create a model with an input element, and combine it with a filtered binding.
|
||||
Add the following to `index.html`:
|
||||
|
||||
```html
|
||||
<input ng-model="userInput" /> Uppercased: {{userInput | uppercase}}
|
||||
```
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
Now that we've seen how to build a custom service as a RESTful client, we're ready for {@link step_12 step 12} (the last step!) to
|
||||
learn how to improve this application with animations.
|
||||
Now that we have learned how to write and test a custom filter, let's go to {@link step_12 step 12}
|
||||
to learn how we can use Angular to enhance the phone details page further.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="11"></ul>
|
||||
|
||||
[restful]: http://en.wikipedia.org/wiki/Representational_State_Transfer
|
||||
[jasmine-matchers]: http://jasmine.github.io/1.3/introduction.html#section-Matchers
|
||||
[bower]: http://bower.io/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user