Compare commits
941 Commits
4.0.0-rc.3
...
4.2.0
Author | SHA1 | Date | |
---|---|---|---|
1c04b83ea3 | |||
429bc9d3cd | |||
41a765d715 | |||
f370fd36e0 | |||
a222c3e609 | |||
5a71df0cb3 | |||
6a46cabb10 | |||
74673545c0 | |||
75a311f250 | |||
391ed6334d | |||
0940e6d6ed | |||
4d2ee51bb0 | |||
680128bc09 | |||
5364b51979 | |||
67ef0f0c8f | |||
e9886d701d | |||
022835bab2 | |||
8be2e4c325 | |||
33ba3e31ed | |||
cba2b3c72d | |||
a4a2901294 | |||
bb46f54ad7 | |||
c9b930dd82 | |||
a39f7d63bb | |||
e317f7d51c | |||
3ddd28d37d | |||
fe6b39d585 | |||
d837bfc2d7 | |||
4759975be6 | |||
078a4b00a7 | |||
b00b80a45b | |||
269bbe0e7d | |||
bb2fc6b8da | |||
d4e196035c | |||
b8979c8701 | |||
bfdd3398f6 | |||
3a121a621f | |||
c06f4fc702 | |||
95f1ea2f12 | |||
784347f61f | |||
42176a7ac4 | |||
76b4b80a23 | |||
7c78282ce8 | |||
47c2a2e411 | |||
5faf520067 | |||
02d74cafba | |||
734f30d14c | |||
f2d810febc | |||
819514aeba | |||
b55adee982 | |||
4c32cb952f | |||
35f714e438 | |||
d5ce086089 | |||
82ec02daf8 | |||
8e7a3f031b | |||
7822187b17 | |||
068133ec85 | |||
598fdad089 | |||
ad6a57e0a3 | |||
230255f887 | |||
1338995ee9 | |||
47e4fca7fd | |||
665e7071fa | |||
c20f60b144 | |||
1408357198 | |||
1f9a3dd1e6 | |||
11505fa0d8 | |||
9326e062d8 | |||
cb2cb7c3bd | |||
92d99ba279 | |||
dfbbbb5e3e | |||
89f317915d | |||
4d5fa5c855 | |||
2f35392cd8 | |||
b056adc032 | |||
0d894a18fc | |||
e5138081ec | |||
8ffa483bb6 | |||
ea8a43def0 | |||
535d9da6b6 | |||
18bf77204e | |||
160221c815 | |||
6220b49463 | |||
aa683a765d | |||
b8b91d3418 | |||
0018acfede | |||
51d7a65a2b | |||
a1724f7816 | |||
d108479d84 | |||
cd5bc64658 | |||
5f723cb92c | |||
7ffb75f476 | |||
c0981b8e13 | |||
80e506563c | |||
694951096c | |||
9e7e178585 | |||
db3113ba16 | |||
3117f565c1 | |||
8443d199b2 | |||
8e2f72c644 | |||
0c1d1e2396 | |||
740450287d | |||
0c691af1d2 | |||
5aa53d70aa | |||
3ab86bd661 | |||
2538094e13 | |||
b37a0484d4 | |||
4c5e28e53a | |||
e10d763446 | |||
573b8611bc | |||
966eb2fbd0 | |||
b0c5018c70 | |||
19a509a92c | |||
9e17a147ec | |||
eba59aaf87 | |||
fa809ec8cf | |||
1651a8f189 | |||
27761b4500 | |||
a80ac0a8d3 | |||
5af143e8e4 | |||
255d7226d1 | |||
50abca4583 | |||
de8d7c65f2 | |||
6123b9c0c6 | |||
3b28c75d1f | |||
368169dc15 | |||
f5b2ce0206 | |||
78e3be12a4 | |||
e7d9fd8056 | |||
08dfe91b95 | |||
85d4c4b82e | |||
6e41add867 | |||
11c10b2ab8 | |||
98849de127 | |||
afd703d08c | |||
ce18fdb399 | |||
670771da33 | |||
c8b08f3a59 | |||
3d382dc750 | |||
9081f84543 | |||
f1a9e3c1bb | |||
b10029c18b | |||
5d4f5434fd | |||
81ca51a8f0 | |||
6cb93c1fac | |||
eed67ddafb | |||
be9e8b99ff | |||
8a0e5659c0 | |||
2842b0cc3d | |||
a42322da0c | |||
4ccb2269a5 | |||
b836aca999 | |||
7d9f96abf0 | |||
86b7bd9c8e | |||
a0a6029915 | |||
6531806996 | |||
3a604ba0bf | |||
39f2977fa8 | |||
faacbe4dac | |||
47c72e4627 | |||
59136fdbe4 | |||
35d1922006 | |||
b40aae54b7 | |||
baa3fbab46 | |||
3361a7b834 | |||
0e13a5956c | |||
9466908c22 | |||
93d27d283a | |||
6f59a4a5b2 | |||
71c3103312 | |||
198edb3109 | |||
44d48d9d7a | |||
712630ca65 | |||
6416e79933 | |||
238c5238a5 | |||
593fe5ed25 | |||
54a6e4ff9e | |||
8a6eb1ac78 | |||
16c8167886 | |||
d761059e4d | |||
c805082648 | |||
9a7f5d580f | |||
af99cf2a41 | |||
221f85af3f | |||
a68ad6d58d | |||
7ae0440cca | |||
2ee7662737 | |||
44195858e6 | |||
39b92f7e54 | |||
1eba623d12 | |||
ea2b24fb4e | |||
a9d9aa18a0 | |||
ecc356ecea | |||
98e31290a7 | |||
7a759df49e | |||
38c524d655 | |||
06264645fd | |||
21d213dfc7 | |||
3065fc6cca | |||
bcefc61da4 | |||
569b1e0eb7 | |||
b83fe6f2a4 | |||
b016984c04 | |||
90bc5b221b | |||
37f1fcbfc8 | |||
4a599eec45 | |||
fdfeaaf1f3 | |||
decb4cc4a3 | |||
44c7ac0fe9 | |||
abb36e3cba | |||
84c30be164 | |||
5afaa39e68 | |||
ce1d7c4a6e | |||
b9521b568f | |||
2eca6e67e1 | |||
c757e5794f | |||
67dc970ce4 | |||
56ccd5e6a1 | |||
bcbee13e26 | |||
d28a3f7878 | |||
162dffb7e8 | |||
0f0b9896b7 | |||
e242e20ca3 | |||
f148ebe99e | |||
f795f649cf | |||
063db641f8 | |||
b44f5c69e1 | |||
56833a6171 | |||
efa2928547 | |||
f5335d17ec | |||
dc7d24267d | |||
412ab3f20c | |||
954c08e97c | |||
a2dcb7b476 | |||
bb0902c592 | |||
9e661e58d1 | |||
d0e72a8f8f | |||
b44eb328e0 | |||
7b4a8d53a7 | |||
73505e2ff0 | |||
e11f5294ab | |||
0190df9cf3 | |||
1af42aedfa | |||
215611aa9e | |||
98d83b2e2d | |||
cf7689ea7b | |||
2848f0499f | |||
5d4b36f80f | |||
415a0f8047 | |||
e0a8376237 | |||
f0f65443c6 | |||
fcc91d862f | |||
3887d8a429 | |||
041f57cb7d | |||
c5ce0408a1 | |||
98dd609f89 | |||
6dc67772b0 | |||
5cf64266f8 | |||
03513e9d70 | |||
b9ed97c0d9 | |||
799be9c98a | |||
566dab1140 | |||
42dc2c19ee | |||
518eb540aa | |||
309ada5df5 | |||
9da63408b0 | |||
7ae7a8440c | |||
aef524506b | |||
547c363473 | |||
2714644528 | |||
d27588b5fb | |||
a8379a46cf | |||
b7caa3e024 | |||
3e33482386 | |||
5057e16874 | |||
9449eff6fd | |||
03610526e5 | |||
cd28df627c | |||
59d62bc5aa | |||
8f46db3701 | |||
109229246c | |||
5856298798 | |||
7f9c589ba3 | |||
8931e71c5c | |||
5bc435eba3 | |||
4dabec6a48 | |||
978376a46e | |||
895f47a9c2 | |||
aec65dee71 | |||
2f66932bd1 | |||
919ff12377 | |||
6748aeabb6 | |||
b3e63c09ab | |||
77b8a76f2e | |||
55b8de9fdd | |||
eb56ab38dc | |||
f29c6bbc6f | |||
464701a899 | |||
5b96fb9320 | |||
1cfb263ee3 | |||
9ca2b4c967 | |||
673d8ae583 | |||
8760bf7be4 | |||
ea02073c84 | |||
b051d7ff48 | |||
cade722e48 | |||
58817f55e1 | |||
3f1d7f7a76 | |||
c8dc116951 | |||
9684d78cae | |||
71f5b73296 | |||
5ba8c14893 | |||
ea9d8a6bc7 | |||
9650ff0824 | |||
00dce1698a | |||
c946a929b7 | |||
21c96a5af1 | |||
a0b9c23100 | |||
cb5bc76766 | |||
1c8772a879 | |||
79ed0e7121 | |||
0c69903123 | |||
04dc24820d | |||
e263e19a2a | |||
ac220fc2bb | |||
3b80472bad | |||
ca17d4f639 | |||
64335d3521 | |||
9945ce2259 | |||
6d9da73090 | |||
c889fb1ef5 | |||
d32bc5df3e | |||
efaf502e95 | |||
ff32c53099 | |||
235eb17cca | |||
aaa562898f | |||
c8fd904c32 | |||
65a8f7f6c9 | |||
f32bfcdbe5 | |||
24b9067047 | |||
de8de84cea | |||
8d300ffbfc | |||
f74dafcb07 | |||
065b76d14c | |||
253345c0c0 | |||
270d694ed2 | |||
f4b771a0c6 | |||
a4de214e2b | |||
aa8bba4865 | |||
392d584572 | |||
900a88b15d | |||
cb384e8ed3 | |||
a2b2afb21c | |||
b70b9606eb | |||
938406122e | |||
951a575db3 | |||
8c50457385 | |||
8c09d10ba9 | |||
c04e51cb15 | |||
7b94f493b9 | |||
f1f04fa782 | |||
4be1966a21 | |||
de25cfc0cb | |||
de36a9b718 | |||
883a3250e4 | |||
1ceb2f9c79 | |||
fbde2a8010 | |||
81925fa66d | |||
6e2abcd5fc | |||
3f46645f5f | |||
2a7f63650c | |||
cd5b1f306a | |||
749bcf3d9c | |||
a9027a2570 | |||
72e32b1f17 | |||
96648ba98a | |||
6561d46349 | |||
2e5e37ac58 | |||
6bac4e65ce | |||
a28f5eeed3 | |||
c0c50133e3 | |||
b9723f9765 | |||
cb35c26d70 | |||
bb52e22ecf | |||
35a2dfc177 | |||
d8e2829e5b | |||
ac5e6baced | |||
710b4a3503 | |||
5331fc1aab | |||
9b7f2ce6c3 | |||
11b2f62ed2 | |||
e7c37d77a8 | |||
1762047bc0 | |||
9a4c8d543d | |||
f5aaa55f21 | |||
23e6502ef3 | |||
ff0f53c915 | |||
64ef69fb34 | |||
249cd8c2ec | |||
d1fb066d07 | |||
2f977312be | |||
feda017c50 | |||
2991221551 | |||
d7719aa0f5 | |||
54e587a46d | |||
4ac5096232 | |||
3d06b18fee | |||
446153675f | |||
bb1850a912 | |||
93516ea8a1 | |||
c3fa8803d3 | |||
3cad5da5a4 | |||
7a8bd99ab1 | |||
7520ddcf40 | |||
9c1318d731 | |||
062fc4afee | |||
87ec8d13ce | |||
cf034f759a | |||
b0c5d21f31 | |||
cbe5f78c1a | |||
4598af23c0 | |||
d855e90524 | |||
897fc87620 | |||
b37f5fc59b | |||
9730351048 | |||
ce687550ae | |||
563b909279 | |||
c390b06da2 | |||
db4e9ea04a | |||
5e6a3ff6a7 | |||
d0dcabd700 | |||
6ed812ff75 | |||
5461182e25 | |||
cbe95f3d25 | |||
ce57b6fb45 | |||
134f542b36 | |||
c3727f330b | |||
d313aad671 | |||
a54fe634ee | |||
ccb6c45466 | |||
800b1b060e | |||
ab03852234 | |||
f90258162a | |||
745731e1a3 | |||
11b0213d20 | |||
cd29d68f3c | |||
da668848c9 | |||
bfd5f27525 | |||
9f66c9c1d1 | |||
1259fe75c9 | |||
e9db74f937 | |||
f99cb96533 | |||
1e848d696b | |||
1bfa7c6f14 | |||
05d1008a08 | |||
9a98fc0f05 | |||
2ca6258a0f | |||
24c34385ee | |||
e07b7ea114 | |||
73c7503ae0 | |||
79db6c3d19 | |||
b6ddacdccf | |||
c9983e6440 | |||
51098c4f86 | |||
4624406ce8 | |||
76269f4a1f | |||
50174d9fb5 | |||
648de7dd86 | |||
9d40ab9e20 | |||
691e86c9bf | |||
8fa4453aec | |||
93d8f8cd63 | |||
c2fca3d9e0 | |||
8b31178647 | |||
e951612af2 | |||
b668c2c781 | |||
ecd0348d96 | |||
69b86925b3 | |||
5293794316 | |||
728c9d0632 | |||
01d93f3af8 | |||
a0d124bd91 | |||
57bc245cb4 | |||
bc431888f3 | |||
ec028b8109 | |||
014594fe8f | |||
3c8a61e40c | |||
bccfaa46ec | |||
2f442062d2 | |||
840cb3d69e | |||
7dfa0b9da9 | |||
f5d0fac800 | |||
baecb553a6 | |||
6e89f0bf8d | |||
f21ff904c2 | |||
38a7e0d1c7 | |||
90814e4449 | |||
e927aeae86 | |||
a77b126d72 | |||
82417b3ca5 | |||
5b141fbf27 | |||
268884296a | |||
ea8ffc9841 | |||
6d930d2fc5 | |||
2ddf3bcdd1 | |||
0a3a9afe58 | |||
5a88d2f68b | |||
7165eb15bd | |||
e20a72280b | |||
abecf3eb6d | |||
7c418a7e06 | |||
58f080a325 | |||
be719e4817 | |||
11b54f69e5 | |||
4a3c66fe22 | |||
9a5084412d | |||
24670667f1 | |||
3f307ff061 | |||
7433da546f | |||
eda2a7b2dc | |||
196203f6d7 | |||
46b0c7a18c | |||
b6a0098aa3 | |||
7d986ae5dc | |||
a2bdcc9ba8 | |||
624b5a5f83 | |||
b57c9605ce | |||
49dfc9fe2d | |||
8f9ba62dc3 | |||
16673fa38b | |||
49d97e1216 | |||
a73050de48 | |||
2535769a65 | |||
b1a3c47766 | |||
8479e9e6d7 | |||
c0b1bbea3e | |||
092f0df7a6 | |||
dfc81c3dab | |||
6649743a2d | |||
56c46d70f7 | |||
c36ec9bf60 | |||
86396a43e9 | |||
590e68c251 | |||
bd704c90dd | |||
ea4afebeb9 | |||
886cca028f | |||
4054055d0d | |||
09c4cb2540 | |||
8ad464d90e | |||
65af9641c2 | |||
cb5a7efa91 | |||
ce47d33cd9 | |||
2e47a0d19f | |||
6a2e08d0a8 | |||
fdb3f26448 | |||
14b7dfa007 | |||
9394835db4 | |||
70b1d6dd9d | |||
d263595c63 | |||
9cb5964b4d | |||
3ad0cc5736 | |||
f09fd6ec16 | |||
8cfa58715c | |||
909264feb5 | |||
cc1ed77dd8 | |||
7d69a91bfe | |||
a6545ddd4d | |||
498bd64d9c | |||
ad9a3a2d3b | |||
2da0f1639f | |||
3ec58ecea0 | |||
061475402c | |||
8f1359d25f | |||
8659bd2c4b | |||
4a052cd343 | |||
28c2a71cfc | |||
69b37fff26 | |||
df619adc76 | |||
374bf1ed98 | |||
540581da3e | |||
6f5fccfeb7 | |||
14669f20bf | |||
075f3f8c9f | |||
2798c1bbec | |||
38d75d410e | |||
04fb29b589 | |||
9037593ab8 | |||
3ced940b5a | |||
f5a98d98c8 | |||
0799f184dc | |||
c530c5d317 | |||
91b2e394d5 | |||
a487563768 | |||
0ab04bd62c | |||
46ce3317c3 | |||
83527fd4fb | |||
6d12aa978d | |||
96aa2365ae | |||
258d5392d5 | |||
09d9f5fe54 | |||
d8b73e4223 | |||
4c566dbfbb | |||
bde9771991 | |||
fe0d02fc47 | |||
7b005aadc1 | |||
7764c5c697 | |||
c6917d9d4f | |||
25132bff86 | |||
0e38bf9de0 | |||
309bae0a0b | |||
ae70293df3 | |||
1e623a3710 | |||
928e2543cd | |||
92e14a36f9 | |||
34a1990c57 | |||
e4c3882a81 | |||
62f9738a9a | |||
a562c64ed6 | |||
8c726ea87e | |||
c8be3960a0 | |||
759af8b56b | |||
97deb01b1f | |||
ad639d783f | |||
e1c2e50d92 | |||
dd4e3f8704 | |||
08941aa0c7 | |||
03fb766428 | |||
2454a3b641 | |||
3e793f079d | |||
5fbb0d050c | |||
71a8627c5d | |||
5c38012980 | |||
7c2ce296d3 | |||
9d37d86223 | |||
21ef5a1961 | |||
46f8a6dd85 | |||
0aa90c6be4 | |||
c05a8cf7bb | |||
4c21114087 | |||
65bb2373ae | |||
8c925bca71 | |||
330a8c90e4 | |||
d8c35c893b | |||
0fe308c0ef | |||
2d14c3b17a | |||
8ef621ad2a | |||
28bf222a6a | |||
9e883f5873 | |||
43a9619e57 | |||
f518e3a75f | |||
146b30796e | |||
5942b2b27c | |||
330b1090f4 | |||
3ccbe28d9f | |||
67719f2185 | |||
6388768d73 | |||
1dda05a965 | |||
14fd78fd85 | |||
a9321b1387 | |||
19cb503531 | |||
9f2acf54bc | |||
9c77a7cdaf | |||
d58a242fe7 | |||
910c0d9ee7 | |||
331b9f6425 | |||
49162784a8 | |||
0c36f2353d | |||
a580f8c61f | |||
f368381d12 | |||
7c2f795ea6 | |||
93d48f1d89 | |||
861953c95c | |||
aa16ccda79 | |||
6269d28bb0 | |||
d438b88f19 | |||
8e03f65645 | |||
606b8fafb0 | |||
b7fa5dec21 | |||
a5c972aa8b | |||
d05aa70c6b | |||
eac99c1b16 | |||
b7a89cec59 | |||
9e0d7be014 | |||
99951911d5 | |||
fd72fad8fd | |||
ff82756415 | |||
d28243d5fc | |||
447e534350 | |||
cda887896a | |||
09b548db75 | |||
920bf373fe | |||
a9ae4daab2 | |||
15662efec4 | |||
5846c46b76 | |||
816b389759 | |||
6605dd1c7c | |||
837ed788f4 | |||
2e4fe7fd2e | |||
52ea193638 | |||
5e3ef775d5 | |||
228238e602 | |||
a6fd22c399 | |||
1616cae5cf | |||
d5cf684d99 | |||
c9e51d9911 | |||
b2830384f5 | |||
487a0e1b5d | |||
8fe42e58b3 | |||
afb17a5b6e | |||
b8f3533d53 | |||
4918cd241f | |||
45983301d5 | |||
acf57def13 | |||
800591db00 | |||
159c98c202 | |||
c837b3dfb9 | |||
53b89ec312 | |||
c17b912eb9 | |||
8785b2bf6d | |||
fb1be83a1b | |||
ea848f74af | |||
b8d5f87f96 | |||
95afaf495b | |||
d92930e975 | |||
c2892dada3 | |||
b2b1195534 | |||
e1b09e3bcc | |||
c65b75443e | |||
db0dca3fc1 | |||
7a715b2403 | |||
1ba296644d | |||
b800a0c824 | |||
0dda01e37c | |||
c8ab5cb0c5 | |||
92084f2b6a | |||
08f2f08d74 | |||
376088da70 | |||
73808dd38b | |||
ee03418b10 | |||
da700d1842 | |||
de87c47dd9 | |||
1060805a1f | |||
08d86751b9 | |||
9319b5f329 | |||
26f6bd4d3b | |||
edb2571a59 | |||
98cb974796 | |||
8b414222aa | |||
ea49a95bd9 | |||
a50d79df47 | |||
64285a2171 | |||
941f194a83 | |||
8b4edcc7ad | |||
c58499786c | |||
1bcbcfd56f | |||
90d2518d9a | |||
7354949763 | |||
5efc86069f | |||
97149f9424 | |||
bac265fdc2 | |||
e59e5e24b9 | |||
9e5d4781cb | |||
fc1f6efe0d | |||
c9710d4fb5 | |||
cf16f3b0dd | |||
a9e91115bf | |||
90f699fdcf | |||
fd7b855cfc | |||
20aab64c65 | |||
2eb027a793 | |||
b0a7bc77ee | |||
4e10faf1eb | |||
a0c6d44e18 | |||
9bc998c7a1 | |||
1a0c6d89b1 | |||
8c12374c4c | |||
45e2126273 | |||
41497b052d | |||
068cad1c1b | |||
31ef92fc36 | |||
b4081e3713 | |||
c8b4a33a7f | |||
a13ddf2e8a | |||
64beae9527 | |||
15a082c74e | |||
fbccd5cd38 | |||
1e8b132ade | |||
431eb309f3 | |||
8e6995c91e | |||
0759911431 | |||
1d7693c1e1 | |||
16e0423085 | |||
c2ffb6bfcd | |||
2489e4ba1b | |||
94da80148e | |||
764e90f9bb | |||
9bf2fb4a74 | |||
de3d2eeeba | |||
a805d00256 | |||
61135bc842 | |||
fa36ffda14 | |||
d3eda7a5b5 | |||
a755b715ed | |||
a9d5de0e56 | |||
f634c62cb3 | |||
f8c075ae27 | |||
aeb99645bb | |||
0d3e314df0 | |||
28ce68a13d | |||
80075afe8a | |||
bcc29ffdd1 | |||
0c43535ccc | |||
49829b4a4d | |||
5c5c2ae405 | |||
6e9264a79c | |||
a6fb78ee3c | |||
d0bc83ca27 | |||
b5b2fed54d | |||
4cef5dddc6 | |||
f92591054b | |||
2a0e55ffb5 | |||
9429032da1 | |||
791534f2f4 | |||
604546c287 | |||
c66437fc13 | |||
8415910375 | |||
5486e5417b | |||
2d78c8cc05 | |||
52bed7f9b3 | |||
7fb45283df | |||
b7212f5afe | |||
4e25601c4d | |||
add7829cb7 | |||
a52184bdda | |||
410aa33005 | |||
0ab49d4cec | |||
994089d36b | |||
d2fbbb44ae | |||
77fd91d615 | |||
492153a986 | |||
c0e05e6f03 | |||
73a46205a0 | |||
992aa17361 | |||
a4076c70cc | |||
52bbc9baf4 | |||
26d4ce29e8 | |||
41f61b0b5b | |||
f92d644c95 | |||
923d0c56e7 | |||
dd36d413ba | |||
013d806b79 | |||
6e98757665 | |||
313158132d | |||
6772c913c7 | |||
b11d0119ac | |||
637a489996 | |||
c2bd357825 | |||
4870f910d6 | |||
c95a3048ce | |||
0e6eb6d719 | |||
09574fc285 | |||
4347cb2119 | |||
f600d4e9e4 | |||
eaa04354d5 | |||
3f7cfde476 | |||
62d5543b01 | |||
f1b33ab7b1 | |||
029d0f25e5 | |||
3f38c6fdcd | |||
80112a9ea1 | |||
795638e18b | |||
322bf7a0d5 | |||
cd981499f9 | |||
c439742a54 | |||
a3e32fb7e1 | |||
1171f91a80 | |||
8e2c8b3e4d | |||
b00fe20afd | |||
36ce0afff6 | |||
5c0ea20bd0 | |||
49764a5bff | |||
4f7d62adac | |||
c10c060d20 | |||
5fe2d8fd80 | |||
5c34066058 | |||
50ab06e29d | |||
06fc42bc44 | |||
959a03a61f | |||
3b1956bbf2 | |||
3c15916e17 | |||
ff60c041f6 | |||
13686bb518 | |||
f093501501 | |||
80649ea03c | |||
778f7d6f33 | |||
2c5a671341 | |||
0aad270267 | |||
221899a930 | |||
060a2d11e5 | |||
abbbb4d52c | |||
ccd38dd54d | |||
cdc882bd36 | |||
1c1085b140 | |||
914797a8ff | |||
ab40fcb068 | |||
1847550ad1 | |||
6497633529 | |||
8850098ea4 | |||
eedca09d73 | |||
7b6dbf0952 | |||
e6c81d2a42 | |||
498a95148b | |||
21a18d6ceb | |||
195b863ea4 | |||
75147ff008 | |||
018e5c979b | |||
e7dab7e6c1 | |||
26efa3a25c | |||
893652a813 | |||
6559425b07 | |||
df914ef4bf | |||
4e1cf5b41a | |||
0c5f893f6e | |||
17f5f3b32c | |||
3bb59902f7 | |||
b804a488c5 | |||
cbde75e77b | |||
413e11fac2 | |||
0e2dd76c3b | |||
ff71eff157 | |||
fa1920a02b | |||
71cd2957f7 | |||
6c8638cf01 | |||
8b5c6b2732 | |||
601494734c | |||
1aebea52e1 | |||
920b3d259d | |||
fce55d87d2 | |||
53d62fa7d0 | |||
a69afeb614 | |||
5f9fb911f7 |
6
.bazelrc
Normal file
6
.bazelrc
Normal file
@ -0,0 +1,6 @@
|
||||
# Disable sandboxing because it's too slow.
|
||||
# https://github.com/bazelbuild/bazel/issues/2424
|
||||
build --spawn_strategy=standalone
|
||||
|
||||
# Performance: avoid stat'ing input files
|
||||
build --watchfs
|
20
.circleci/config.yml
Normal file
20
.circleci/config.yml
Normal file
@ -0,0 +1,20 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
working_directory: ~/ng
|
||||
docker:
|
||||
- image: alexeagle/ngcontainer
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
|
||||
- run: npm install
|
||||
- run: npm run postinstall
|
||||
- run: ./node_modules/.bin/gulp lint
|
||||
# Build twice, workaround for
|
||||
# https://github.com/bazelbuild/bazel/issues/3114
|
||||
- run: bazel build ... || bazel build ...
|
||||
- save_cache:
|
||||
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
|
||||
paths:
|
||||
- "node_modules"
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
.DS_STORE
|
||||
|
||||
/dist/
|
||||
bazel-*
|
||||
node_modules
|
||||
bower_components
|
||||
|
||||
@ -26,7 +27,3 @@ yarn-error.log
|
||||
|
||||
# rollup-test output
|
||||
/modules/rollup-test/dist/
|
||||
|
||||
# angular.io
|
||||
/aio/src/content/docs
|
||||
/aio/dist
|
||||
|
@ -20,6 +20,8 @@
|
||||
# tbosch - Tobias Bosch
|
||||
# vicb - Victor Berchet
|
||||
# vikerman - Vikram Subramanian
|
||||
# wardbell - Ward Bell
|
||||
# tinayuangao - Tina Gao
|
||||
|
||||
version: 2
|
||||
|
||||
@ -40,6 +42,7 @@ groups:
|
||||
- "aio/*"
|
||||
- "integration/*"
|
||||
- "modules/*"
|
||||
- "packages/*"
|
||||
- "tools/*"
|
||||
users:
|
||||
- IgorMinar
|
||||
@ -94,15 +97,15 @@ groups:
|
||||
- vicb
|
||||
- IgorMinar #fallback
|
||||
|
||||
compiler/animations:
|
||||
animations:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/compiler/src/animation/*"
|
||||
- "packages/animation/*"
|
||||
- "packages/platform-browser/animations/*"
|
||||
users:
|
||||
- matsko #primary
|
||||
- tbosch
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
- IgorMinar #fallback
|
||||
|
||||
compiler/i18n:
|
||||
conditions:
|
||||
@ -133,6 +136,7 @@ groups:
|
||||
users:
|
||||
- alexeagle
|
||||
- chuckjaz
|
||||
- vicb
|
||||
- tbosch
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
@ -153,7 +157,7 @@ groups:
|
||||
- "packages/forms/*"
|
||||
users:
|
||||
- kara #primary
|
||||
# needs secondary
|
||||
- tinayuangao #secondary
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
@ -173,7 +177,8 @@ groups:
|
||||
- "packages/language-service/*"
|
||||
users:
|
||||
- chuckjaz #primary
|
||||
# needs secondary
|
||||
- tbosch #secondary
|
||||
- vicb
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
@ -182,8 +187,8 @@ groups:
|
||||
files:
|
||||
- "packages/router/*"
|
||||
users:
|
||||
- vicb #primary
|
||||
# needs secondary
|
||||
- jasonaden
|
||||
- vicb
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
@ -246,7 +251,8 @@ groups:
|
||||
files:
|
||||
- "aio/*"
|
||||
users:
|
||||
- IgorMinar
|
||||
- robwormald
|
||||
- petebacondarwin
|
||||
- IgorMinar #primary
|
||||
- petebacondarwin #secondary
|
||||
- gkalpak
|
||||
- wardbell
|
||||
- mhevery #fallback
|
||||
|
15
.travis.yml
15
.travis.yml
@ -19,7 +19,7 @@ addons:
|
||||
- secure: "L7nrZwkAtFtYrP2DykPXgZvEKjkv0J/TwQ/r2QGxFTaBq4VZn+2Dw0YS7uCxoMqYzDwH0aAOqxoutibVpk8Z/16nE3tNmU5RzltMd6Xmt3qU2f/JDQLMo6PSlBodnjOUsDHJgmtrcbjhqrx/znA237BkNUu6UZRT7mxhXIZpn0U="
|
||||
branches:
|
||||
except:
|
||||
- g3_v2_0
|
||||
- g3
|
||||
|
||||
cache:
|
||||
yarn: true
|
||||
@ -30,16 +30,17 @@ cache:
|
||||
|
||||
env:
|
||||
global:
|
||||
# GITHUB_TOKEN_ANGULAR
|
||||
# GITHUB_TOKEN_ANGULAR=<github token, a personal access token of the angular-builds account, account access in valentine>
|
||||
# This is needed for the e2e Travis matrix task to publish packages to github for continuous packages delivery.
|
||||
- secure: "fq/U7VDMWO8O8SnAQkdbkoSe2X92PVqg4d044HmRYVmcf6YbO48+xeGJ8yOk0pCBwl3ISO4Q2ot0x546kxfiYBuHkZetlngZxZCtQiFT9kyId8ZKcYdXaIW9OVdw3Gh3tQyUwDucfkVhqcs52D6NZjyE2aWZ4/d1V4kWRO/LMgo="
|
||||
- secure: "aCdHveZuY8AT4Jr1JoJB4LxZsnGWRe/KseZh1YXYe5UtufFCtTVHvUcLn0j2aLBF0KpdyS+hWf0i4np9jthKu2xPKriefoPgCMpisYeC0MFkwbmv+XlgkUbgkgVZMGiVyX7DCYXVahxIoOUjVMEDCbNiHTIrfEuyq24U3ok2tHc="
|
||||
# FIREBASE_TOKEN
|
||||
# This is needed for publishing builds to the "aio-staging" firebase site.
|
||||
# TODO(i): the token was generated using the iminar@google account, we should switch to a shared/role-base account.
|
||||
- secure: "MPx3UM77o5IlhT75PKHL0FXoB5tSXDc3vnCXCd1sRy4XUTZ9vjcV6nNuyqEf+SOw659bGbC1FI4mACGx1Q+z7MQDR85b1mcA9uSgHDkh+IR82CnCVdaX9d1RXafdJIArahxfmorbiiPPLyPIKggo7ituRm+2c+iraoCkE/pXxYg="
|
||||
# This is needed for publishing builds to the "aio-staging" and "angular-io" firebase projects.
|
||||
# This token was generated using the aio-deploy@angular.io account using `firebase login:ci` and password from valentine
|
||||
- secure: "L5CyQmpwWtoR4Qi4xlWQh/cL1M6ZeJL4W4QAr4HdKFMgYt9h+Whqkymyh2NxwmCbPvWa7yUd+OiLQUDCY7L2VIg16hTwoe2CgYDyQA0BEwLzxtRrJXl93TfwMlrUx5JSIzAccD6D4sjtz8kSFMomK2Nls33xOXOukwyhVMjd0Cg="
|
||||
matrix:
|
||||
# Order: a slower build first, so that we don't occupy an idle travis worker waiting for others to complete.
|
||||
- CI_MODE=e2e
|
||||
- CI_MODE=e2e_2
|
||||
- CI_MODE=js
|
||||
- CI_MODE=saucelabs_required
|
||||
- CI_MODE=browserstack_required
|
||||
@ -47,12 +48,14 @@ env:
|
||||
- CI_MODE=browserstack_optional
|
||||
- CI_MODE=docs_test
|
||||
- CI_MODE=aio
|
||||
- CI_MODE=aio_e2e
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- env: "CI_MODE=saucelabs_optional"
|
||||
- env: "CI_MODE=browserstack_optional"
|
||||
- env: "CI_MODE=aio_e2e"
|
||||
|
||||
before_install:
|
||||
# source the env.sh script so that the exported variables are available to other scripts later on
|
||||
|
18
BUILD
Normal file
18
BUILD
Normal file
@ -0,0 +1,18 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
exports_files(["tsconfig.json"])
|
||||
|
||||
# This rule belongs in node_modules/BUILD
|
||||
# It's here as a workaround for
|
||||
# https://github.com/bazelbuild/bazel/issues/374#issuecomment-296217940
|
||||
filegroup(
|
||||
name = "node_modules",
|
||||
srcs = glob([
|
||||
# Performance workaround: list individual files
|
||||
# This won't scale in the general case.
|
||||
# TODO(alexeagle): figure out what to do
|
||||
"node_modules/typescript/lib/**",
|
||||
"node_modules/zone.js/**/*.d.ts",
|
||||
"node_modules/rxjs/**/*.d.ts",
|
||||
"node_modules/@types/**/*.d.ts",
|
||||
]),
|
||||
)
|
540
CHANGELOG.md
540
CHANGELOG.md
@ -1,3 +1,542 @@
|
||||
<a name="4.2.0"></a>
|
||||
# [4.2.0](https://github.com/angular/angular/compare/4.2.0-rc.2...4.2.0) salubrious-stratagem (2017-06-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** ensure web-animations understands a numeric CSS perspective value ([819514a](https://github.com/angular/angular/commit/819514a)), closes [#14007](https://github.com/angular/angular/issues/14007)
|
||||
* **animations:** evaluate substitutions on option param values ([e9886d7](https://github.com/angular/angular/commit/e9886d7))
|
||||
* **forms:** fix min and max validator behavior on non-numbers ([a222c3e](https://github.com/angular/angular/commit/a222c3e))
|
||||
* **router:** opening links in new window ([4c32cb9](https://github.com/angular/angular/commit/4c32cb9))
|
||||
* **upgrade:** call setInterval outside the Angular zone ([269bbe0](https://github.com/angular/angular/commit/269bbe0))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler-cli:** introduce synchronous codegen API ([b00b80a](https://github.com/angular/angular/commit/b00b80a))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **animations:** do not create a closure each time a node is removed ([fe6b39d](https://github.com/angular/angular/commit/fe6b39d))
|
||||
* **animations:** only apply `:leave` flags if animations are set to run ([b55adee](https://github.com/angular/angular/commit/b55adee))
|
||||
|
||||
|
||||
|
||||
<a name="4.2.0-rc.2"></a>
|
||||
# [4.2.0-rc.2](https://github.com/angular/angular/compare/4.2.0-rc.1...4.2.0-rc.2) (2017-06-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** always change to desired animation state even if no transition fires ([#17025](https://github.com/angular/angular/issues/17025)) ([665e707](https://github.com/angular/angular/commit/665e707)), closes [#16947](https://github.com/angular/angular/issues/16947)
|
||||
* **animations:** do not retain deleted nodes during an non-removal animation ([#17153](https://github.com/angular/angular/issues/17153)) ([068133e](https://github.com/angular/angular/commit/068133e)), closes [#17086](https://github.com/angular/angular/issues/17086)
|
||||
* **common:** always use 'other' case for locales with no plural rules ([#16990](https://github.com/angular/angular/issues/16990)) ([535d9da](https://github.com/angular/angular/commit/535d9da))
|
||||
* **compiler:** enableLegacyTemplate should not be ignored ([#17051](https://github.com/angular/angular/issues/17051)) ([8ffa483](https://github.com/angular/angular/commit/8ffa483)), closes [#15555](https://github.com/angular/angular/issues/15555)
|
||||
* **router:** make remove trailing slash consistent with URL params ([c20f60b](https://github.com/angular/angular/commit/c20f60b)), closes [#16069](https://github.com/angular/angular/issues/16069)
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler:** emit typescript nodes from an output ast ([#16823](https://github.com/angular/angular/issues/16823)) ([18bf772](https://github.com/angular/angular/commit/18bf772))
|
||||
* **compiler-cli:** produce template diagnostics error messages ([#17125](https://github.com/angular/angular/issues/17125)) ([230255f](https://github.com/angular/angular/commit/230255f))
|
||||
* **tsc-wrapped:** always convert shorthand imports ([#16898](https://github.com/angular/angular/issues/16898)) ([ea8a43d](https://github.com/angular/angular/commit/ea8a43d))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **animations:** do not place enterId values on elements for querying purposes ([#17150](https://github.com/angular/angular/issues/17150)) ([ad6a57e](https://github.com/angular/angular/commit/ad6a57e))
|
||||
|
||||
<a name="4.2.0-rc.1"></a>
|
||||
# [4.2.0-rc.1](https://github.com/angular/angular/compare/4.2.0-rc.0...4.2.0-rc.1) (2017-05-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** repair flicker issues with WA polyfill ([#16937](https://github.com/angular/angular/issues/16937)) ([e7d9fd8](https://github.com/angular/angular/commit/e7d9fd8)), closes [#16919](https://github.com/angular/angular/issues/16919) [#16918](https://github.com/angular/angular/issues/16918)
|
||||
* **animations:** use a lightweight renderer for non-animation components ([#17003](https://github.com/angular/angular/issues/17003)) ([3ab86bd](https://github.com/angular/angular/commit/3ab86bd))
|
||||
* **compiler:** compile `.ngfactory.ts` files even if nobody references them. ([#16899](https://github.com/angular/angular/issues/16899)) ([573b861](https://github.com/angular/angular/commit/573b861)), closes [#16741](https://github.com/angular/angular/issues/16741)
|
||||
* **compiler:** do not report type errors for arguments with `@Inject` ([#16222](https://github.com/angular/angular/issues/16222)) ([27761b4](https://github.com/angular/angular/commit/27761b4)), closes [#15424](https://github.com/angular/angular/issues/15424)
|
||||
* **core:** make decorators closure safe ([#16905](https://github.com/angular/angular/issues/16905)) ([a80ac0a](https://github.com/angular/angular/commit/a80ac0a)), closes [#16889](https://github.com/angular/angular/issues/16889)
|
||||
* **tsc-wrapped:** ignore `|null` and `|undefined` when collecting types ([#16222](https://github.com/angular/angular/issues/16222)) ([1651a8f](https://github.com/angular/angular/commit/1651a8f))
|
||||
* **tsc-wrapped:** resolve short-hand literal values to locals ([#16873](https://github.com/angular/angular/issues/16873)) ([11c10b2](https://github.com/angular/angular/commit/11c10b2))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler:** add location note to extracted xliff2 files ([#16791](https://github.com/angular/angular/issues/16791)) ([08dfe91](https://github.com/angular/angular/commit/08dfe91)), closes [#16531](https://github.com/angular/angular/issues/16531)
|
||||
* **core:** update zone.js to 0.8.10 and expose the flush method ([#16860](https://github.com/angular/angular/issues/16860)) ([85d4c4b](https://github.com/angular/angular/commit/85d4c4b))
|
||||
* **tsc-wrapped:** support template literals in metadata collection ([#16880](https://github.com/angular/angular/issues/16880)) ([6e41add](https://github.com/angular/angular/commit/6e41add))
|
||||
|
||||
|
||||
|
||||
<a name="4.2.0-rc.0"></a>
|
||||
# [4.2.0-rc.0](https://github.com/angular/angular/compare/4.2.0-beta.0...4.2.0-rc.0) (2017-05-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** make sure reuseable animation subtitutions work without default params ([#16875](https://github.com/angular/angular/issues/16875)) ([7d9f96a](https://github.com/angular/angular/commit/7d9f96a))
|
||||
* **animations:** only require one flushMicrotasks call when testing animations ([6cb93c1](https://github.com/angular/angular/commit/6cb93c1))
|
||||
* **compiler:** avoid a `...null` spread in extraction ([#16547](https://github.com/angular/angular/issues/16547)) ([e0a8376](https://github.com/angular/angular/commit/e0a8376))
|
||||
* **compiler-cli:** allow '==' to compare nullable types ([#16731](https://github.com/angular/angular/issues/16731)) ([d761059](https://github.com/angular/angular/commit/d761059))
|
||||
* **core:** detach projected views when a parent view is destroyed ([#16592](https://github.com/angular/angular/issues/16592)) ([f0f6544](https://github.com/angular/angular/commit/f0f6544)), closes [#15578](https://github.com/angular/angular/issues/15578)
|
||||
* **core:** projected views should be dirty checked when the declaring component is dirty checked. ([#16592](https://github.com/angular/angular/issues/16592)) ([fcc91d8](https://github.com/angular/angular/commit/fcc91d8)), closes [#14321](https://github.com/angular/angular/issues/14321)
|
||||
* **http:** flatten metadata for [@angular](https://github.com/angular)/http/testing ([9da6340](https://github.com/angular/angular/commit/9da6340)), closes [#15521](https://github.com/angular/angular/issues/15521)
|
||||
* **http:** honor RequestArgs.search and RequestArgs.params map type ([aef5245](https://github.com/angular/angular/commit/aef5245)), closes [#15761](https://github.com/angular/angular/issues/15761) [#16392](https://github.com/angular/angular/issues/16392)
|
||||
* **http:** introduce encodingHint for text() for better ArrayBuffer support ([7ae7a84](https://github.com/angular/angular/commit/7ae7a84)), closes [#15932](https://github.com/angular/angular/issues/15932) [#16420](https://github.com/angular/angular/issues/16420)
|
||||
* **router:** fix redirect to a URL with a param having multiple values ([#16376](https://github.com/angular/angular/issues/16376)) ([5d4b36f](https://github.com/angular/angular/commit/5d4b36f)), closes [#16310](https://github.com/angular/angular/issues/16310)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **animations:** introduce a wave of new animation features ([16c8167](https://github.com/angular/angular/commit/16c8167))
|
||||
* **animations:** introduce routeable animation support ([f1a9e3c](https://github.com/angular/angular/commit/f1a9e3c))
|
||||
* add .ngsummary.ts files to support AOT unit tests ([547c363](https://github.com/angular/angular/commit/547c363))
|
||||
* introduce `TestBed.overrideProvider` ([#16725](https://github.com/angular/angular/issues/16725)) ([39b92f7](https://github.com/angular/angular/commit/39b92f7))
|
||||
* **compiler:** support a non-null postfix assert ([#16672](https://github.com/angular/angular/issues/16672)) ([b9521b5](https://github.com/angular/angular/commit/b9521b5))
|
||||
* **core:** introduce fixture.whenRenderingDone for testing ([#16732](https://github.com/angular/angular/issues/16732)) ([38c524d](https://github.com/angular/angular/commit/38c524d))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **animations:** reduce size of animations bundle ([712630c](https://github.com/angular/angular/commit/712630c))
|
||||
|
||||
|
||||
|
||||
<a name="4.1.3"></a>
|
||||
## [4.1.3](https://github.com/angular/angular/compare/4.1.2...4.1.3) (2017-05-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add typescript 2.3.2 typings test ([#16738](https://github.com/angular/angular/issues/16738)) ([a5bdbed](https://github.com/angular/angular/commit/a5bdbed)), closes [#16663](https://github.com/angular/angular/issues/16663)
|
||||
* **compiler-cli:** import routing module with forRoot ([#16438](https://github.com/angular/angular/issues/16438)) ([b7f8581](https://github.com/angular/angular/commit/b7f8581))
|
||||
* **platform-server:** wait for async app initializers to complete before removing server side styles ([#16712](https://github.com/angular/angular/issues/16712)) ([0a82f7d](https://github.com/angular/angular/commit/0a82f7d)), closes [#15716](https://github.com/angular/angular/issues/15716)
|
||||
* **router:** Wrap Promise-like instances in native Promises ([#16759](https://github.com/angular/angular/issues/16759)) ([883ca28](https://github.com/angular/angular/commit/883ca28))
|
||||
* **upgrade:** Prevent renaming of $inject property ([#16706](https://github.com/angular/angular/issues/16706)) ([afb7540](https://github.com/angular/angular/commit/afb7540))
|
||||
* **upgrade:** use quote to prevent ClossureCompiler obfuscating $event. ([#16724](https://github.com/angular/angular/issues/16724)) ([47df3d6](https://github.com/angular/angular/commit/47df3d6))
|
||||
|
||||
|
||||
|
||||
<a name="4.2.0-beta.1"></a>
|
||||
# [4.2.0-beta.1](https://github.com/angular/angular/compare/4.2.0-beta.0...4.2.0-beta.1) (2017-05-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add .ngsummary.ts files to support AOT unit tests ([547c363](https://github.com/angular/angular/commit/547c363))
|
||||
|
||||
|
||||
|
||||
<a name="4.1.2"></a>
|
||||
## [4.1.2](https://github.com/angular/angular/compare/4.1.1...4.1.2) (2017-05-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** avoid a `...null` spread in extraction ([#16547](https://github.com/angular/angular/issues/16547)) ([d0e1688](https://github.com/angular/angular/commit/d0e1688))
|
||||
* **core:** detach projected views when a parent view is destroyed ([#16592](https://github.com/angular/angular/issues/16592)) ([ee6705a](https://github.com/angular/angular/commit/ee6705a)), closes [#15578](https://github.com/angular/angular/issues/15578)
|
||||
* **core:** projected views should be dirty checked when the declaring component is dirty checked. ([#16592](https://github.com/angular/angular/issues/16592)) ([9218812](https://github.com/angular/angular/commit/9218812)), closes [#14321](https://github.com/angular/angular/issues/14321)
|
||||
* **http:** flatten metadata for [@angular](https://github.com/angular)/http/testing ([9c70a3c](https://github.com/angular/angular/commit/9c70a3c)), closes [#15521](https://github.com/angular/angular/issues/15521)
|
||||
* **http:** honor RequestArgs.search and RequestArgs.params map type ([63066f7](https://github.com/angular/angular/commit/63066f7)), closes [#15761](https://github.com/angular/angular/issues/15761) [#16392](https://github.com/angular/angular/issues/16392)
|
||||
* **http:** introduce encodingHint for text() for better ArrayBuffer support ([ec3b6e9](https://github.com/angular/angular/commit/ec3b6e9)), closes [#15932](https://github.com/angular/angular/issues/15932) [#16420](https://github.com/angular/angular/issues/16420)
|
||||
* **router:** fix redirect to a URL with a param having multiple values ([#16376](https://github.com/angular/angular/issues/16376)) ([915eae5](https://github.com/angular/angular/commit/915eae5)), closes [#16310](https://github.com/angular/angular/issues/16310)
|
||||
|
||||
|
||||
|
||||
<a name="4.2.0-beta.0"></a>
|
||||
# [4.2.0-beta.0](https://github.com/angular/angular/compare/4.1.0...4.2.0-beta.0) (2017-05-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core**: strictNullCheck support. ([#16389](https://github.com/angular/angular/issues/16389)) ([#16389](https://github.com/angular/angular/issues/16389)) ([8c09d10](https://github.com/angular/angular/commit/8c09d10)), closes [#16357](https://github.com/angular/angular/issues/16357)
|
||||
* **core:** allow directives to inject the component’s `ChangeDetectorRef`. ([#16394](https://github.com/angular/angular/issues/16394)) ([392d584](https://github.com/angular/angular/commit/392d584)), closes [#12816](https://github.com/angular/angular/issues/12816)
|
||||
* **core:** allow to detach `OnPush` components ([#16394](https://github.com/angular/angular/issues/16394)) ([aa8bba4](https://github.com/angular/angular/commit/aa8bba4)), closes [#9720](https://github.com/angular/angular/issues/9720)
|
||||
* **core:** don’t set `ng-version` for dynamically created components ([#16394](https://github.com/angular/angular/issues/16394)) ([a4de214](https://github.com/angular/angular/commit/a4de214)), closes [#15880](https://github.com/angular/angular/issues/15880)
|
||||
* **core:** don’t stop change detection because of errors ([e263e19](https://github.com/angular/angular/commit/e263e19)), closes [#9531](https://github.com/angular/angular/issues/9531) [#2413](https://github.com/angular/angular/issues/2413) [#15925](https://github.com/angular/angular/issues/15925)
|
||||
* **language-service:** remove asserts for non-null expressions ([#16422](https://github.com/angular/angular/issues/16422)) ([253345c](https://github.com/angular/angular/commit/253345c))
|
||||
* **upgrade:** initialize all inputs in time for `ngOnChanges()` ([b3e63c0](https://github.com/angular/angular/commit/b3e63c0)), closes [#16212](https://github.com/angular/angular/issues/16212)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler-cli:** add param to set MissingTranslationStrategy on ngc ([#15987](https://github.com/angular/angular/issues/15987)) ([6e2abcd](https://github.com/angular/angular/commit/6e2abcd)), closes [#15808](https://github.com/angular/angular/issues/15808)
|
||||
* **core:** add `begin` and `end` renderer methods to track change detection ([7f9c589](https://github.com/angular/angular/commit/7f9c589))
|
||||
* **core:** allow custom selector when bootstrapping components ([#15668](https://github.com/angular/angular/issues/15668)) ([900a88b](https://github.com/angular/angular/commit/900a88b)), closes [#7136](https://github.com/angular/angular/issues/7136)
|
||||
* **core:** upgrade dep on zone.js to 0.8.9 ([#16401](https://github.com/angular/angular/issues/16401)) ([065b76d](https://github.com/angular/angular/commit/065b76d))
|
||||
* **forms:** introduce min and max validators ([#15813](https://github.com/angular/angular/issues/15813)) ([81925fa](https://github.com/angular/angular/commit/81925fa))
|
||||
* **language-service:** provide external file list to TypeScript ([#16417](https://github.com/angular/angular/issues/16417)) ([f4b771a](https://github.com/angular/angular/commit/f4b771a))
|
||||
|
||||
|
||||
|
||||
<a name="4.1.1"></a>
|
||||
## [4.1.1](https://github.com/angular/angular/compare/4.1.0...4.1.1) (2017-05-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core**: strictNullCheck support. ([#16389](https://github.com/angular/angular/issues/16389)) ([#16389](https://github.com/angular/angular/issues/16389)) ([427d63a](https://github.com/angular/angular/commit/427d63a)), closes [#16357](https://github.com/angular/angular/issues/16357)
|
||||
* **core:** allow directives to inject the component’s `ChangeDetectorRef`. ([#16394](https://github.com/angular/angular/issues/16394)) ([f66e59e](https://github.com/angular/angular/commit/f66e59e)), closes [#12816](https://github.com/angular/angular/issues/12816)
|
||||
* **core:** allow to detach `OnPush` components ([#16394](https://github.com/angular/angular/issues/16394)) ([acf83b9](https://github.com/angular/angular/commit/acf83b9)), closes [#9720](https://github.com/angular/angular/issues/9720)
|
||||
* **core:** don’t set `ng-version` for dynamically created components ([#16394](https://github.com/angular/angular/issues/16394)) ([85a1b54](https://github.com/angular/angular/commit/85a1b54)), closes [#15880](https://github.com/angular/angular/issues/15880)
|
||||
* **core:** don’t stop change detection because of errors ([07cef36](https://github.com/angular/angular/commit/07cef36)), closes [#9531](https://github.com/angular/angular/issues/9531) [#2413](https://github.com/angular/angular/issues/2413) [#15925](https://github.com/angular/angular/issues/15925)
|
||||
* **language-service:** remove asserts for non-null expressions ([#16422](https://github.com/angular/angular/issues/16422)) ([c060110](https://github.com/angular/angular/commit/c060110))
|
||||
* **upgrade:** initialize all inputs in time for `ngOnChanges()` ([dd4e501](https://github.com/angular/angular/commit/dd4e501)), closes [#16212](https://github.com/angular/angular/issues/16212)
|
||||
|
||||
|
||||
|
||||
<a name="4.1.0"></a>
|
||||
# [4.1.0](https://github.com/angular/angular/compare/4.1.0-rc.0...4.1.0) (2017-04-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **router:** forward the query parameters in the ng1 -> ng2 url sync ([#16249](https://github.com/angular/angular/issues/16249)) ([2f97731](https://github.com/angular/angular/commit/2f97731)), closes [#16067](https://github.com/angular/angular/issues/16067)
|
||||
* **upgrade:** use correct attribute name for upgraded component's bindings ([#16128](https://github.com/angular/angular/issues/16128)) ([d1fb066](https://github.com/angular/angular/commit/d1fb066)), closes [#8856](https://github.com/angular/angular/issues/8856)
|
||||
|
||||
|
||||
<a name="4.1.0-rc.0"></a>
|
||||
# [4.1.0-rc.0](https://github.com/angular/angular/compare/4.1.0-beta.0...4.1.0-rc.0) (2017-04-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **benchpress:** chrome - prevent trace buffer overflow ([2f44206](https://github.com/angular/angular/commit/2f44206))
|
||||
* **benchpress:** Update types for TypeScript nullability support ([14669f2](https://github.com/angular/angular/commit/14669f2))
|
||||
* **common:** Update types for TypeScript nullability support ([d8b73e4](https://github.com/angular/angular/commit/d8b73e4))
|
||||
* **compiler:** fix build error in xliff2 ([bd704c9](https://github.com/angular/angular/commit/bd704c9))
|
||||
* **compiler:** fix inheritance for AOT with summaries ([#15583](https://github.com/angular/angular/issues/15583)) ([8ef621a](https://github.com/angular/angular/commit/8ef621a))
|
||||
* **compiler:** ignore calls to unresolved symbols in metadata ([38a7e0d](https://github.com/angular/angular/commit/38a7e0d)), closes [#15969](https://github.com/angular/angular/issues/15969)
|
||||
* **compiler:** ignore calls to unresolved symbols in metadata ([#15970](https://github.com/angular/angular/issues/15970)) ([ce47d33](https://github.com/angular/angular/commit/ce47d33)), closes [#15969](https://github.com/angular/angular/issues/15969)
|
||||
* **compiler:** Inform user where Quoted error was thrown ([a77b126](https://github.com/angular/angular/commit/a77b126))
|
||||
* **compiler:** make I18NHtmlParser provider AoT-compliant ([#15980](https://github.com/angular/angular/issues/15980)) ([745731e](https://github.com/angular/angular/commit/745731e))
|
||||
* **compiler:** support `<ng-container>` whatever the namespace ([5b141fb](https://github.com/angular/angular/commit/5b141fb)), closes [#14257](https://github.com/angular/angular/issues/14257)
|
||||
* **compiler:** suppress another closure warning ([#16137](https://github.com/angular/angular/issues/16137)) ([11b0213](https://github.com/angular/angular/commit/11b0213))
|
||||
* **compiler:** Update types for TypeScript nullability support ([09d9f5f](https://github.com/angular/angular/commit/09d9f5f))
|
||||
* **core:** benchmarks - enable ng1 benchmark again ([bccfaa4](https://github.com/angular/angular/commit/bccfaa4))
|
||||
* **core:** distribute externs for testability API ([#16179](https://github.com/angular/angular/issues/16179)) ([da66884](https://github.com/angular/angular/commit/da66884))
|
||||
* **core:** key-value differ changes iteration ([#15968](https://github.com/angular/angular/issues/15968)) ([cb5a7ef](https://github.com/angular/angular/commit/cb5a7ef)), closes [#14997](https://github.com/angular/angular/issues/14997)
|
||||
* **forms:** Update types for TypeScript nullability support ([6649743](https://github.com/angular/angular/commit/6649743))
|
||||
* **forms:** Update types for TypeScript nullability support ([57bc245](https://github.com/angular/angular/commit/57bc245))
|
||||
* **forms:** Update types for TypeScript nullability support ([#15859](https://github.com/angular/angular/issues/15859)) ([6a2e08d](https://github.com/angular/angular/commit/6a2e08d))
|
||||
* **http:** Update types for TypeScript nullability support ([c36ec9b](https://github.com/angular/angular/commit/c36ec9b))
|
||||
* **http:** Update types for TypeScript nullability support ([ec028b8](https://github.com/angular/angular/commit/ec028b8))
|
||||
* **language-service:** avoid throwing exceptions when reporting metadata errors ([7764c5c](https://github.com/angular/angular/commit/7764c5c))
|
||||
* **language-service:** detect when there isn't a tsconfig.json ([258d539](https://github.com/angular/angular/commit/258d539)), closes [#15874](https://github.com/angular/angular/issues/15874)
|
||||
* **language-service:** improve resilience to incomplete information ([71a8627](https://github.com/angular/angular/commit/71a8627))
|
||||
* **language-service:** infer correct type of `?.` expressions ([0a3a9af](https://github.com/angular/angular/commit/0a3a9af)), closes [#15885](https://github.com/angular/angular/issues/15885)
|
||||
* **language-service:** initialize static reflector correctly ([fe0d02f](https://github.com/angular/angular/commit/fe0d02f)), closes [#15768](https://github.com/angular/angular/issues/15768)
|
||||
* **language-service:** look for type constructors on canonical symbol ([2ddf3bc](https://github.com/angular/angular/commit/2ddf3bc))
|
||||
* **language-service:** only use canonical symbols ([5a88d2f](https://github.com/angular/angular/commit/5a88d2f))
|
||||
* **language-service:** parse extended i18n forms ([bde9771](https://github.com/angular/angular/commit/bde9771))
|
||||
* **language-service:** resolve any parameter types to any result ([5fbb0d0](https://github.com/angular/angular/commit/5fbb0d0))
|
||||
* **language-service:** respect baseUrl compiler option ([f21ff90](https://github.com/angular/angular/commit/f21ff90)), closes [#15974](https://github.com/angular/angular/issues/15974)
|
||||
* **language-service:** Update types for TypeScript nullability support ([540581d](https://github.com/angular/angular/commit/540581d))
|
||||
* **packaging:** increased buffer size ([#15840](https://github.com/angular/angular/issues/15840)) ([65af964](https://github.com/angular/angular/commit/65af964))
|
||||
* **platform-browser:** Update types for TypeScript nullability support ([728c9d0](https://github.com/angular/angular/commit/728c9d0)), closes [#15898](https://github.com/angular/angular/issues/15898)
|
||||
* **platform-server:** handle innerText ([#15818](https://github.com/angular/angular/issues/15818)) ([9394835](https://github.com/angular/angular/commit/9394835))
|
||||
* **router:** fix query param parsing ([a487563](https://github.com/angular/angular/commit/a487563))
|
||||
* **router:** prevent `RouterLinkActive` from causing an infinite CD loop ([82417b3](https://github.com/angular/angular/commit/82417b3)), closes [#15825](https://github.com/angular/angular/issues/15825)
|
||||
* **router:** relax nullability requirements ([a0d124b](https://github.com/angular/angular/commit/a0d124b))
|
||||
* turn on nullability in the code base. ([5293794](https://github.com/angular/angular/commit/5293794))
|
||||
* Update types for TypeScript nullability support in examples ([6f5fccf](https://github.com/angular/angular/commit/6f5fccf))
|
||||
* **router:** the preloader use the module from the loaded config ([6d12aa9](https://github.com/angular/angular/commit/6d12aa9))
|
||||
* **router:** Update types for TypeScript nullability support ([56c46d7](https://github.com/angular/angular/commit/56c46d7))
|
||||
* **router:** Update types for TypeScript nullability support ([bc43188](https://github.com/angular/angular/commit/bc43188))
|
||||
* **tsc-wrapped:** collect new expressions with no arguments ([#15908](https://github.com/angular/angular/issues/15908)) ([70b1d6d](https://github.com/angular/angular/commit/70b1d6d)), closes [#15906](https://github.com/angular/angular/issues/15906)
|
||||
* **tsc-wrapped:** ensure valid path separators in metadata ([96aa236](https://github.com/angular/angular/commit/96aa236))
|
||||
* **upgrade:** Update types for TypeScript nullability support ([01d93f3](https://github.com/angular/angular/commit/01d93f3)), closes [#15897](https://github.com/angular/angular/issues/15897)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **animations:** Update types for TypeScript nullability support ([38d75d4](https://github.com/angular/angular/commit/38d75d4)), closes [#15870](https://github.com/angular/angular/issues/15870)
|
||||
* **compiler:** add source files to xmb/xliff translations ([#14705](https://github.com/angular/angular/issues/14705)) ([4054055](https://github.com/angular/angular/commit/4054055)), closes [#14190](https://github.com/angular/angular/issues/14190)
|
||||
* **compiler:** Implement i18n XLIFF 2.0 serializer ([#14185](https://github.com/angular/angular/issues/14185)) ([09c4cb2](https://github.com/angular/angular/commit/09c4cb2)), closes [#11735](https://github.com/angular/angular/issues/11735)
|
||||
* **upgrade:** allow setting the angularjs lib at runtime ([#15168](https://github.com/angular/angular/issues/15168)) ([e927aea](https://github.com/angular/angular/commit/e927aea))
|
||||
* **upgrade:** allow setting the angularjs lib at runtime ([#15168](https://github.com/angular/angular/issues/15168)) ([8ad464d](https://github.com/angular/angular/commit/8ad464d))
|
||||
* **upgrade:** fixes for allow setting the angularjs lib at runtime ([90814e4](https://github.com/angular/angular/commit/90814e4))
|
||||
* add support for TS 2.2 ([3c8a61e](https://github.com/angular/angular/commit/3c8a61e))
|
||||
* add support for TS 2.3 ([014594f](https://github.com/angular/angular/commit/014594f))
|
||||
|
||||
|
||||
|
||||
<a name="4.0.3"></a>
|
||||
## [4.0.3](https://github.com/angular/angular/compare/4.0.2...4.0.3) (2017-04-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **benchpress:** chrome - prevent trace buffer overflow ([d216f94](https://github.com/angular/angular/commit/d216f94))
|
||||
* **compiler:** fix build error in xliff2 ([1870347](https://github.com/angular/angular/commit/1870347))
|
||||
* **compiler:** ignore calls to unresolved symbols in metadata ([d4038ab](https://github.com/angular/angular/commit/d4038ab)), closes [#15969](https://github.com/angular/angular/issues/15969)
|
||||
* **compiler:** ignore calls to unresolved symbols in metadata ([#15970](https://github.com/angular/angular/issues/15970)) ([db25f08](https://github.com/angular/angular/commit/db25f08)), closes [#15969](https://github.com/angular/angular/issues/15969)
|
||||
* **compiler:** Inform user where Quoted error was thrown ([3184cc5](https://github.com/angular/angular/commit/3184cc5))
|
||||
* **compiler:** suppress another closure warning ([#16137](https://github.com/angular/angular/issues/16137)) ([72e240a](https://github.com/angular/angular/commit/72e240a))
|
||||
* **core:** benchmarks - enable ng1 benchmark again ([ccac4c6](https://github.com/angular/angular/commit/ccac4c6))
|
||||
* **core:** distribute externs for testability API ([#16179](https://github.com/angular/angular/issues/16179)) ([e377d9d](https://github.com/angular/angular/commit/e377d9d))
|
||||
* **core:** key-value differ changes iteration ([#15968](https://github.com/angular/angular/issues/15968)) ([a8600dc](https://github.com/angular/angular/commit/a8600dc)), closes [#14997](https://github.com/angular/angular/issues/14997)
|
||||
* **language-service:** only use canonical symbols ([786093a](https://github.com/angular/angular/commit/786093a))
|
||||
* **packaging:** increased buffer size ([#15840](https://github.com/angular/angular/issues/15840)) ([88ad490](https://github.com/angular/angular/commit/88ad490))
|
||||
* **platform-server:** handle innerText ([#15818](https://github.com/angular/angular/issues/15818)) ([7de340d](https://github.com/angular/angular/commit/7de340d))
|
||||
* **router:** prevent `RouterLinkActive` from causing an infinite CD loop ([4479c42](https://github.com/angular/angular/commit/4479c42)), closes [#15825](https://github.com/angular/angular/issues/15825)
|
||||
* **tsc-wrapped:** collect new expressions with no arguments ([#15908](https://github.com/angular/angular/issues/15908)) ([41cac9e](https://github.com/angular/angular/commit/41cac9e)), closes [#15906](https://github.com/angular/angular/issues/15906)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler:** Implement i18n XLIFF 2.0 serializer ([#14185](https://github.com/angular/angular/issues/14185)) ([a7d8edd](https://github.com/angular/angular/commit/a7d8edd)), closes [#11735](https://github.com/angular/angular/issues/11735)
|
||||
* **upgrade:** allow setting the angularjs lib at runtime ([#15168](https://github.com/angular/angular/issues/15168)) ([a75d056](https://github.com/angular/angular/commit/a75d056))
|
||||
* **upgrade:** allow setting the angularjs lib at runtime ([#15168](https://github.com/angular/angular/issues/15168)) ([4f172b0](https://github.com/angular/angular/commit/4f172b0))
|
||||
* **upgrade:** fixes for allow setting the angularjs lib at runtime ([bb6932d](https://github.com/angular/angular/commit/bb6932d))
|
||||
* add support for TS 2.3 ([5cf101f](https://github.com/angular/angular/commit/5cf101f))
|
||||
|
||||
|
||||
|
||||
<a name="4.1.0-beta.1"></a>
|
||||
# [4.1.0-beta.1](https://github.com/angular/angular/compare/4.1.0-beta.0...4.1.0-beta.1) (2017-04-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** fix inheritance for AOT with summaries ([#15583](https://github.com/angular/angular/issues/15583)) ([8ef621a](https://github.com/angular/angular/commit/8ef621a))
|
||||
* **language-service:** avoid throwing exceptions when reporting metadata errors ([7764c5c](https://github.com/angular/angular/commit/7764c5c))
|
||||
* **language-service:** detect when there isn't a tsconfig.json ([258d539](https://github.com/angular/angular/commit/258d539)), closes [#15874](https://github.com/angular/angular/issues/15874)
|
||||
* **language-service:** improve resilience to incomplete information ([71a8627](https://github.com/angular/angular/commit/71a8627))
|
||||
* **language-service:** initialize static reflector correctly ([fe0d02f](https://github.com/angular/angular/commit/fe0d02f)), closes [#15768](https://github.com/angular/angular/issues/15768)
|
||||
* **language-service:** parse extended i18n forms ([bde9771](https://github.com/angular/angular/commit/bde9771))
|
||||
* **language-service:** resolve any parameter types to any result ([5fbb0d0](https://github.com/angular/angular/commit/5fbb0d0))
|
||||
* **router:** fix query param parsing ([a487563](https://github.com/angular/angular/commit/a487563))
|
||||
* **router:** the preloader use the module from the loaded config ([6d12aa9](https://github.com/angular/angular/commit/6d12aa9))
|
||||
* **tsc-wrapped:** ensure valid path separators in metadata ([96aa236](https://github.com/angular/angular/commit/96aa236))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **animations:** Update types for TypeScript nullability support ([38d75d4](https://github.com/angular/angular/commit/38d75d4)), closes [#15870](https://github.com/angular/angular/issues/15870)
|
||||
* **benchpress:** Update types for TypeScript nullability support ([14669f2](https://github.com/angular/angular/commit/14669f2))
|
||||
* **common:** Update types for TypeScript nullability support ([d8b73e4](https://github.com/angular/angular/commit/d8b73e4))
|
||||
* **compiler:** Update types for TypeScript nullability support ([09d9f5f](https://github.com/angular/angular/commit/09d9f5f))
|
||||
* **language-service:** Update types for TypeScript nullability support ([540581d](https://github.com/angular/angular/commit/540581d))
|
||||
|
||||
|
||||
|
||||
<a name="4.0.2"></a>
|
||||
## [4.0.2](https://github.com/angular/angular/compare/4.0.1...4.0.2) (2017-04-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** fix inheritance for AOT with summaries ([#15583](https://github.com/angular/angular/issues/15583)) ([1864ccb](https://github.com/angular/angular/commit/1864ccb))
|
||||
* **language-service:** avoid throwing exceptions when reporting metadata errors ([0861fda](https://github.com/angular/angular/commit/0861fda))
|
||||
* **language-service:** detect when there isn't a tsconfig.json ([168a2eb](https://github.com/angular/angular/commit/168a2eb)), closes [#15874](https://github.com/angular/angular/issues/15874)
|
||||
* **language-service:** improve resilience to incomplete information ([e4277a0](https://github.com/angular/angular/commit/e4277a0))
|
||||
* **language-service:** initialize static reflector correctly ([5b99533](https://github.com/angular/angular/commit/5b99533)), closes [#15768](https://github.com/angular/angular/issues/15768)
|
||||
* **language-service:** parse extended i18n forms ([c9c7acd](https://github.com/angular/angular/commit/c9c7acd))
|
||||
* **language-service:** resolve any parameter types to any result ([feae7b6](https://github.com/angular/angular/commit/feae7b6))
|
||||
* **router:** fix query param parsing ([2f41b52](https://github.com/angular/angular/commit/2f41b52))
|
||||
* **router:** the preloader use the module from the loaded config ([978f809](https://github.com/angular/angular/commit/978f809))
|
||||
* **tsc-wrapped:** ensure valid path separators in metadata ([c10e50c](https://github.com/angular/angular/commit/c10e50c))
|
||||
|
||||
|
||||
|
||||
<a name="4.1.0-beta.0"></a>
|
||||
# [4.1.0-beta.0](https://github.com/angular/angular/compare/4.0.0...4.1.0-beta.0) (2017-03-29)
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler:** support ICU messages in XLIFF ([b8d5f87](https://github.com/angular/angular/commit/b8d5f87)), closes [#12636](https://github.com/angular/angular/issues/12636) [#15068](https://github.com/angular/angular/issues/15068)
|
||||
|
||||
Note: 4.1.0-beta.0 release also contains all the changes present in the 4.0.1 release.
|
||||
|
||||
|
||||
<a name="4.0.1"></a>
|
||||
## [4.0.1](https://github.com/angular/angular/compare/4.0.0...4.0.1) (2017-03-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** make sure style calculations are not computed too early ([#15540](https://github.com/angular/angular/issues/15540)) ([c828511](https://github.com/angular/angular/commit/c828511)), closes [#15507](https://github.com/angular/angular/issues/15507)
|
||||
* **compiler:** allow single quotes into named interpolations ([#15461](https://github.com/angular/angular/issues/15461)) ([a654875](https://github.com/angular/angular/commit/a654875)), closes [#15318](https://github.com/angular/angular/issues/15318)
|
||||
* **compiler:** ignore errors when evaluating base classes ([#15560](https://github.com/angular/angular/issues/15560)) ([a88413f](https://github.com/angular/angular/commit/a88413f)), closes [#15536](https://github.com/angular/angular/issues/15536)
|
||||
* **compiler:** throw when a component defines both template and templateUrl ([#15572](https://github.com/angular/angular/issues/15572)) ([902bb2f](https://github.com/angular/angular/commit/902bb2f)), closes [#15566](https://github.com/angular/angular/issues/15566)
|
||||
* **core:** check for undefined on normalizeDebugBindingValue ([#15503](https://github.com/angular/angular/issues/15503)) ([b8c0a97](https://github.com/angular/angular/commit/b8c0a97)), closes [#15494](https://github.com/angular/angular/issues/15494)
|
||||
* **core:** fix inheritance in JIT mode for TS 2.1 ([#15599](https://github.com/angular/angular/issues/15599)) ([ca66530](https://github.com/angular/angular/commit/ca66530)), closes [#15502](https://github.com/angular/angular/issues/15502)
|
||||
* **core:** fix the key/value differ ([#15539](https://github.com/angular/angular/issues/15539)) ([e72124c](https://github.com/angular/angular/commit/e72124c)), closes [#15457](https://github.com/angular/angular/issues/15457)
|
||||
* **core:** improve error msg for invalid KeyValueDiffer.diff arg ([#15489](https://github.com/angular/angular/issues/15489)) ([d74e4d0](https://github.com/angular/angular/commit/d74e4d0)), closes [#15402](https://github.com/angular/angular/issues/15402)
|
||||
* **core:** Update types for TypeScript nullability support ([#15472](https://github.com/angular/angular/issues/15472)) ([8c4b963](https://github.com/angular/angular/commit/8c4b963))
|
||||
* **language-service:** be resilient to invalidate ordering ([#15470](https://github.com/angular/angular/issues/15470)) ([a2c2b87](https://github.com/angular/angular/commit/a2c2b87)), closes [#15466](https://github.com/angular/angular/issues/15466)
|
||||
* **language-service:** correctly determine base members of types ([#15600](https://github.com/angular/angular/issues/15600)) ([0fe4985](https://github.com/angular/angular/commit/0fe4985)), closes [#15460](https://github.com/angular/angular/issues/15460)
|
||||
* **language-service:** don't require `reflect-metadata` module to be provided ([#15569](https://github.com/angular/angular/issues/15569)) ([bfa4f70](https://github.com/angular/angular/commit/bfa4f70)), closes [#15568](https://github.com/angular/angular/issues/15568)
|
||||
* **language-service:** guard access to `Symbol.members` ([#15529](https://github.com/angular/angular/issues/15529)) ([bf25e94](https://github.com/angular/angular/commit/bf25e94)), closes [#15528](https://github.com/angular/angular/issues/15528)
|
||||
* **language-service:** improve performance of `updateModuleAnalysis()` ([#15543](https://github.com/angular/angular/issues/15543)) ([5597fd3](https://github.com/angular/angular/commit/5597fd3))
|
||||
* **router:** should run CanActivate after CanDeactivate guards ([75478b2](https://github.com/angular/angular/commit/75478b2)), closes [#14059](https://github.com/angular/angular/issues/14059) [#15467](https://github.com/angular/angular/issues/15467)
|
||||
* **router:** shouldn't execute CanLoad when a route has been loaded ([2360676](https://github.com/angular/angular/commit/2360676)), closes [#14475](https://github.com/angular/angular/issues/14475) [#15438](https://github.com/angular/angular/issues/15438)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **router:** don't create new serializer every time UrlTree.toString is called ([#15565](https://github.com/angular/angular/issues/15565)) ([fd61145](https://github.com/angular/angular/commit/fd61145))
|
||||
|
||||
<a name="4.0.0"></a>
|
||||
# [4.0.0](https://github.com/angular/angular/compare/4.0.0-rc.6...4.0.0) invisible-makeover (2017-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** assume queries with no matches as static ([#15429](https://github.com/angular/angular/issues/15429)) ([c8ab5cb](https://github.com/angular/angular/commit/c8ab5cb)), closes [#15417](https://github.com/angular/angular/issues/15417)
|
||||
* **compiler:** correctly handle when `toString` is exported ([#15430](https://github.com/angular/angular/issues/15430)) ([0dda01e](https://github.com/angular/angular/commit/0dda01e)), closes [#15420](https://github.com/angular/angular/issues/15420)
|
||||
* **platform-browser:** setAttribute should work with xmlns namespace ([#14874](https://github.com/angular/angular/issues/14874)) ([92084f2](https://github.com/angular/angular/commit/92084f2)), closes [#14865](https://github.com/angular/angular/issues/14865)
|
||||
* **router:** should pass new data to Observable when query params change ([#15387](https://github.com/angular/angular/issues/15387)) ([08f2f08](https://github.com/angular/angular/commit/08f2f08)), closes [#15290](https://github.com/angular/angular/issues/15290)
|
||||
* prevent strictNullChecks support until [#15432](https://github.com/angular/angular/issues/15432) is fixed ([#15434](https://github.com/angular/angular/issues/15434)) ([b800a0c](https://github.com/angular/angular/commit/b800a0c))
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
From 4.0.0 @angular/core uses a [`WeakMap`](https://github.com/angular/angular/commit/52b21275f4c2c26c46627f5648b41a33bb5c8283), a polyfill needs to be included for [browsers that do not support it natively](http://kangax.github.io/compat-table/es6/#test-WeakMap).
|
||||
|
||||
<a name="4.0.0-rc.6"></a>
|
||||
# [4.0.0-rc.6](https://github.com/angular/angular/compare/4.0.0-rc.5...4.0.0-rc.6) (2017-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** correct the main entry path in package.json ([#15300](https://github.com/angular/angular/issues/15300)) ([2489e4b](https://github.com/angular/angular/commit/2489e4b))
|
||||
* **animations:** ensure empty animate() steps work at the end of a sequence ([#15328](https://github.com/angular/angular/issues/15328)) ([fbccd5c](https://github.com/angular/angular/commit/fbccd5c)), closes [#15310](https://github.com/angular/angular/issues/15310)
|
||||
* **animations:** ensure enter/leave cancellations work ([#15323](https://github.com/angular/angular/issues/15323)) ([9bf2fb4](https://github.com/angular/angular/commit/9bf2fb4)), closes [#15315](https://github.com/angular/angular/issues/15315)
|
||||
* **animations:** make sure easing values work with web-animations ([#15195](https://github.com/angular/angular/issues/15195)) ([f925910](https://github.com/angular/angular/commit/f925910)), closes [#15115](https://github.com/angular/angular/issues/15115)
|
||||
* **animations:** make sure non-transitioned leave operations cancel existing animations ([#15254](https://github.com/angular/angular/issues/15254)) ([a6fb78e](https://github.com/angular/angular/commit/a6fb78e)), closes [#15213](https://github.com/angular/angular/issues/15213)
|
||||
* **animations:** only process element nodes through the animation engine ([#15268](https://github.com/angular/angular/issues/15268)) ([80075af](https://github.com/angular/angular/commit/80075af)), closes [#15267](https://github.com/angular/angular/issues/15267)
|
||||
* **animations:** only treat view removals as `void` state transitions ([#15245](https://github.com/angular/angular/issues/15245)) ([c66437f](https://github.com/angular/angular/commit/c66437f)), closes [#15223](https://github.com/angular/angular/issues/15223)
|
||||
* **animations:** stringify boolean values as `1` and `0` ([#15311](https://github.com/angular/angular/issues/15311)) ([94da801](https://github.com/angular/angular/commit/94da801)), closes [#15247](https://github.com/angular/angular/issues/15247)
|
||||
* **compiler:** add an empty content for source file of non mapped code. ([#15246](https://github.com/angular/angular/issues/15246)) ([8415910](https://github.com/angular/angular/commit/8415910))
|
||||
* **compiler:** don’t call `check` if we don’t need to ([#15322](https://github.com/angular/angular/issues/15322)) ([764e90f](https://github.com/angular/angular/commit/764e90f))
|
||||
* **compiler:** look for flat module resources using declaration module path ([#15367](https://github.com/angular/angular/issues/15367)) ([90d2518](https://github.com/angular/angular/commit/90d2518)), closes [#15221](https://github.com/angular/angular/issues/15221)
|
||||
* **compiler:** only log template deprecation warning once ([#15364](https://github.com/angular/angular/issues/15364)) ([08d8675](https://github.com/angular/angular/commit/08d8675))
|
||||
* **compiler:** use attribute id to merge translations ([#15302](https://github.com/angular/angular/issues/15302)) ([1d7693c](https://github.com/angular/angular/commit/1d7693c)), closes [#15234](https://github.com/angular/angular/issues/15234)
|
||||
* **compiler-cli:** adding missing format xliff for the extractor ([#15386](https://github.com/angular/angular/issues/15386)) ([a50d79d](https://github.com/angular/angular/commit/a50d79d))
|
||||
* **core:** allow tree shaking of component factories and styles ([#15214](https://github.com/angular/angular/issues/15214)) ([2a0e55f](https://github.com/angular/angular/commit/2a0e55f)), closes [#15181](https://github.com/angular/angular/issues/15181)
|
||||
* **core:** don’t create a comment for components with empty template. ([#15260](https://github.com/angular/angular/issues/15260)) ([f8c075a](https://github.com/angular/angular/commit/f8c075a)), closes [#15143](https://github.com/angular/angular/issues/15143)
|
||||
* **core:** mark components for check when host events trigger. ([#15359](https://github.com/angular/angular/issues/15359)) ([64beae9](https://github.com/angular/angular/commit/64beae9)), closes [#15352](https://github.com/angular/angular/issues/15352)
|
||||
* **core:** only apply `WrappedValue` to the binding of the pipe ([#15257](https://github.com/angular/angular/issues/15257)) ([0c43535](https://github.com/angular/angular/commit/0c43535)), closes [#15116](https://github.com/angular/angular/issues/15116)
|
||||
* **core:** provide `NgModuleRef` in `ViewContainerRef.createComponent`. ([#15350](https://github.com/angular/angular/issues/15350)) ([431eb30](https://github.com/angular/angular/commit/431eb30)), closes [#15241](https://github.com/angular/angular/issues/15241)
|
||||
* **core:** stringify shouldn't throw when toString returns null/undefined ([#14975](https://github.com/angular/angular/issues/14975)) ([8e6995c](https://github.com/angular/angular/commit/8e6995c)), closes [#14948](https://github.com/angular/angular/issues/14948)
|
||||
* **core:** trigger host animations for elements that are removed. ([#15251](https://github.com/angular/angular/issues/15251)) ([0d3e314](https://github.com/angular/angular/commit/0d3e314)), closes [#14813](https://github.com/angular/angular/issues/14813) [#15193](https://github.com/angular/angular/issues/15193)
|
||||
* **core:** update peer dep on zone.js to ^0.8.5 ([#15365](https://github.com/angular/angular/issues/15365)) ([97149f9](https://github.com/angular/angular/commit/97149f9)), closes [#15185](https://github.com/angular/angular/issues/15185)
|
||||
* **forms:** make composition event buffering configurable ([#15256](https://github.com/angular/angular/issues/15256)) ([5efc860](https://github.com/angular/angular/commit/5efc860)), closes [#15079](https://github.com/angular/angular/issues/15079)
|
||||
* **platform-server:** interpret Native view encapsulation as Emulated on the server ([#15155](https://github.com/angular/angular/issues/15155)) ([de3d2ee](https://github.com/angular/angular/commit/de3d2ee))
|
||||
* **platform-server:** setup NoopAnimationsModule in ServerModule by default ([#15131](https://github.com/angular/angular/issues/15131)) ([5c5c2ae](https://github.com/angular/angular/commit/5c5c2ae)), closes [#15098](https://github.com/angular/angular/issues/15098) [#14784](https://github.com/angular/angular/issues/14784)
|
||||
* **platform-server:** throw a better error message for relative URLs ([#15357](https://github.com/angular/angular/issues/15357)) ([15a082c](https://github.com/angular/angular/commit/15a082c)), closes [#15349](https://github.com/angular/angular/issues/15349)
|
||||
* **tsc-wrapped:** emit flat module format correctly on Windows ([#15215](https://github.com/angular/angular/issues/15215)) ([6e9264a](https://github.com/angular/angular/commit/6e9264a)), closes [#15192](https://github.com/angular/angular/issues/15192)
|
||||
* **tsc-wrapped:** use windows friendly path normalization in bundler ([#15374](https://github.com/angular/angular/issues/15374)) ([c584997](https://github.com/angular/angular/commit/c584997)), closes [#15289](https://github.com/angular/angular/issues/15289)
|
||||
* **upgrade:** component injectors should not link the module injector tree ([#15385](https://github.com/angular/angular/issues/15385)) ([ea49a95](https://github.com/angular/angular/commit/ea49a95))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **core:** expose `inputs`, `outputs` and `ngContentSelectors` on `ComponentFactory`. ([#15214](https://github.com/angular/angular/issues/15214)) ([791534f](https://github.com/angular/angular/commit/791534f))
|
||||
* **router:** add `ParamMap.keys` to get a list of parameters ([d3eda7a](https://github.com/angular/angular/commit/d3eda7a))
|
||||
* **router:** introduce `ParamMap` to access parameters ([a755b71](https://github.com/angular/angular/commit/a755b71))
|
||||
* **tsc-wrapped:** record original location of flattened symbols ([#15367](https://github.com/angular/angular/issues/15367)) ([7354949](https://github.com/angular/angular/commit/7354949))
|
||||
* **upgrade:** use `ComponentFactory.inputs/outputs/ngContentSelectors` ([#15214](https://github.com/angular/angular/issues/15214)) ([9429032](https://github.com/angular/angular/commit/9429032))
|
||||
|
||||
|
||||
|
||||
<a name="4.0.0-rc.5"></a>
|
||||
# [4.0.0-rc.5](https://github.com/angular/angular/compare/4.0.0-rc.4...4.0.0-rc.5) (2017-03-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler-cli:** update the tsc-wrapped dependency version ([#15226](https://github.com/angular/angular/issues/15226)) ([7fb4528](https://github.com/angular/angular/commit/7fb4528))
|
||||
|
||||
|
||||
|
||||
<a name="4.0.0-rc.4"></a>
|
||||
# [4.0.0-rc.4](https://github.com/angular/angular/compare/4.0.0-rc.3...4.0.0-rc.4) (2017-03-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** always fire callbacks even for noop animations ([#15170](https://github.com/angular/angular/issues/15170)) ([3f38c6f](https://github.com/angular/angular/commit/3f38c6f))
|
||||
* **animations:** make sure easing values are applied to an empty animate() step ([#15174](https://github.com/angular/angular/issues/15174)) ([62d5543](https://github.com/angular/angular/commit/62d5543)), closes [#15115](https://github.com/angular/angular/issues/15115)
|
||||
* **animations:** support multiple state names per state() call ([#15147](https://github.com/angular/angular/issues/15147)) ([36ce0af](https://github.com/angular/angular/commit/36ce0af)), closes [#14732](https://github.com/angular/angular/issues/14732)
|
||||
* **compiler:** always use `ng://` prefix for sourcemap urls ([#15218](https://github.com/angular/angular/issues/15218)) ([994089d](https://github.com/angular/angular/commit/994089d))
|
||||
* **compiler:** fix utf8encode, move to sharted utils, add tests ([#15076](https://github.com/angular/angular/issues/15076)) ([959a03a](https://github.com/angular/angular/commit/959a03a))
|
||||
* **compiler:** generated code should pass `noUnusedLocals` check ([50ab06e](https://github.com/angular/angular/commit/50ab06e)), closes [#14797](https://github.com/angular/angular/issues/14797)
|
||||
* **compiler:** Improve error message for missing annotations ([#14724](https://github.com/angular/angular/issues/14724)) ([3c15916](https://github.com/angular/angular/commit/3c15916))
|
||||
* **compiler:** improve error msg for unexpected closing tags ([#14747](https://github.com/angular/angular/issues/14747)) ([5f9fb91](https://github.com/angular/angular/commit/5f9fb91)), closes [#6652](https://github.com/angular/angular/issues/6652)
|
||||
* **compiler:** make sourcemaps work in AOT mode ([492153a](https://github.com/angular/angular/commit/492153a))
|
||||
* **compiler:** only warn for `[@Injectable](https://github.com/Injectable)` classes with invalid args. ([5c34066](https://github.com/angular/angular/commit/5c34066)), closes [#15003](https://github.com/angular/angular/issues/15003)
|
||||
* **compiler:** shouldn't throw when Symbol is used as DI token ([#13701](https://github.com/angular/angular/issues/13701)) ([8b5c6b2](https://github.com/angular/angular/commit/8b5c6b2)), closes [#13314](https://github.com/angular/angular/issues/13314)
|
||||
* **compiler:** support interface types in injectable constuctors ([#14894](https://github.com/angular/angular/issues/14894)) ([b00fe20](https://github.com/angular/angular/commit/b00fe20)), closes [#12631](https://github.com/angular/angular/issues/12631)
|
||||
* **compiler:** warning prints "WARNING" instead of "ERROR" ([#15125](https://github.com/angular/angular/issues/15125)) ([3b1956b](https://github.com/angular/angular/commit/3b1956b))
|
||||
* **core:** don’t recreate `TemplateRef` when used as a reference. ([#15066](https://github.com/angular/angular/issues/15066)) ([df914ef](https://github.com/angular/angular/commit/df914ef)), closes [#14873](https://github.com/angular/angular/issues/14873)
|
||||
* **core:** don’t throw if queries change during change detection. ([06fc42b](https://github.com/angular/angular/commit/06fc42b)), closes [#14925](https://github.com/angular/angular/issues/14925)
|
||||
* **core:** ErrorHandler should not rethrow an error by default ([#15077](https://github.com/angular/angular/issues/15077)) ([#15208](https://github.com/angular/angular/issues/15208)) ([77fd91d](https://github.com/angular/angular/commit/77fd91d)), closes [#14949](https://github.com/angular/angular/issues/14949) [#15182](https://github.com/angular/angular/issues/15182) [#14316](https://github.com/angular/angular/issues/14316)
|
||||
* **core:** update peer dep on zone.js to ^0.8.4 ([#15209](https://github.com/angular/angular/issues/15209)) ([d2fbbb4](https://github.com/angular/angular/commit/d2fbbb4)), closes [#15180](https://github.com/angular/angular/issues/15180) [#15185](https://github.com/angular/angular/issues/15185)
|
||||
* **core:** use presence of .subscribe to detect observables rather then Symbol.observable ([#15171](https://github.com/angular/angular/issues/15171)) ([6e98757](https://github.com/angular/angular/commit/6e98757)), closes [#14298](https://github.com/angular/angular/issues/14298) [#14473](https://github.com/angular/angular/issues/14473) [#14926](https://github.com/angular/angular/issues/14926)
|
||||
* **forms:** ensure observable validators are properly canceled ([#15132](https://github.com/angular/angular/issues/15132)) ([26d4ce2](https://github.com/angular/angular/commit/26d4ce2))
|
||||
* **forms:** remove equalsTo validator ([#15050](https://github.com/angular/angular/issues/15050)) ([778f7d6](https://github.com/angular/angular/commit/778f7d6))
|
||||
* element injector vs module injector ([#15044](https://github.com/angular/angular/issues/15044)) ([13686bb](https://github.com/angular/angular/commit/13686bb)), closes [#12869](https://github.com/angular/angular/issues/12869) [#12889](https://github.com/angular/angular/issues/12889) [#13885](https://github.com/angular/angular/issues/13885) [#13870](https://github.com/angular/angular/issues/13870)
|
||||
* **http:** Make ResponseOptionsArgs an interface ([#14607](https://github.com/angular/angular/issues/14607)) ([#14623](https://github.com/angular/angular/issues/14623)) ([f1b33ab](https://github.com/angular/angular/commit/f1b33ab)), closes [#13708](https://github.com/angular/angular/issues/13708)
|
||||
* **platform-browser:** prevent clobbered elements from freezing the browser ([a4076c7](https://github.com/angular/angular/commit/a4076c7))
|
||||
* **platform-server:** correctly implement get href in parse5 adapter ([#15022](https://github.com/angular/angular/issues/15022)) ([80649ea](https://github.com/angular/angular/commit/80649ea))
|
||||
* **platform-server:** fix an exception when HostListener('window:scroll') is used on the server ([#15019](https://github.com/angular/angular/issues/15019)) ([4f7d62a](https://github.com/angular/angular/commit/4f7d62a))
|
||||
* correct UMD resolutions for platform-browser_animations ([#15190](https://github.com/angular/angular/issues/15190)) ([923d0c5](https://github.com/angular/angular/commit/923d0c5)), closes [#15114](https://github.com/angular/angular/issues/15114)
|
||||
* don't instantiate providers with ngOnDestroy eagerly. ([#15070](https://github.com/angular/angular/issues/15070)) ([2c5a671](https://github.com/angular/angular/commit/2c5a671)), closes [#14552](https://github.com/angular/angular/issues/14552)
|
||||
* fix path locally to empty.js ([#15073](https://github.com/angular/angular/issues/15073)) ([80112a9](https://github.com/angular/angular/commit/80112a9))
|
||||
* **platform-server:** fix get/set title in parse5 adapter ([#14965](https://github.com/angular/angular/issues/14965)) ([018e5c9](https://github.com/angular/angular/commit/018e5c9))
|
||||
* **platform-server:** handle styles with extra ':'s correctly ([#15189](https://github.com/angular/angular/issues/15189)) ([013d806](https://github.com/angular/angular/commit/013d806))
|
||||
* **platform-server:** support svg elements with namespaced attributes ([#15101](https://github.com/angular/angular/issues/15101)) ([f093501](https://github.com/angular/angular/commit/f093501))
|
||||
* **router:** fix query parameters with multiple values ([#15129](https://github.com/angular/angular/issues/15129)) ([029d0f2](https://github.com/angular/angular/commit/029d0f2)), closes [#14796](https://github.com/angular/angular/issues/14796)
|
||||
* **tsc-wrapped:** emit js files in all cases ([c0e05e6](https://github.com/angular/angular/commit/c0e05e6))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **core:** use flags in `Renderer2.setStyle` instead of booleans ([#15045](https://github.com/angular/angular/issues/15045)) ([ff71eff](https://github.com/angular/angular/commit/ff71eff))
|
||||
|
||||
### Features
|
||||
|
||||
* **common:** support `as` syntax in template/* bindings ([#15025](https://github.com/angular/angular/issues/15025)) ([c10c060](https://github.com/angular/angular/commit/c10c060)), closes [#15020](https://github.com/angular/angular/issues/15020)
|
||||
* **compiler-cli:** support metadata file aliases ([0ab49d4](https://github.com/angular/angular/commit/0ab49d4))
|
||||
* **core:** allow to provide multiple default testing modules ([#15054](https://github.com/angular/angular/issues/15054)) ([6c8638c](https://github.com/angular/angular/commit/6c8638c))
|
||||
* **core:** expose `inputs`, `outputs` and `ngContentSelectors` on `ComponentFactory`. ([1171f91](https://github.com/angular/angular/commit/1171f91))
|
||||
* **upgrade:** support multi-slot projection in upgrade/static ([#14282](https://github.com/angular/angular/issues/14282)) ([914797a](https://github.com/angular/angular/commit/914797a)), closes [#14261](https://github.com/angular/angular/issues/14261)
|
||||
* **upgrade:** use `ComponentFactory.inputs/outputs/ngContentSelectors` ([a3e32fb](https://github.com/angular/angular/commit/a3e32fb))
|
||||
* introduce source maps for templates ([#15011](https://github.com/angular/angular/issues/15011)) ([cdc882b](https://github.com/angular/angular/commit/cdc882b))
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Perviously, any provider that had an ngOnDestroy lifecycle hook would be created eagerly.
|
||||
|
||||
Now, only classes that are annotated with @Component, @Directive, @Pipe, @NgModule are eager. Providers only become eager if they are either directly or transitively injected into one of the above.
|
||||
|
||||
This also makes all `useValue` providers eager, which
|
||||
should have no observable impact other than code size.
|
||||
|
||||
**EXPECTED IMPACT**:
|
||||
Making providers eager was an incorrect behavior and never documented.
|
||||
Also, providers that are used by a directive / pipe / ngModule stay eager.
|
||||
So the impact should be rather small.
|
||||
|
||||
* DebugNode.source no longer returns the source location of a node.
|
||||
|
||||
Closes 14013
|
||||
|
||||
* core: (since v4 rc.1)
|
||||
- `Renderer2.setStyle` no longer takes booleans but rather a
|
||||
bit mask of flags.
|
||||
|
||||
|
||||
|
||||
<a name="2.4.10"></a>
|
||||
## [2.4.10](https://github.com/angular/angular/compare/2.4.9...2.4.10) (2017-03-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** fix decoding surrogate pairs ([#15154](https://github.com/angular/angular/issues/15154)) ([e5c9bbc](https://github.com/angular/angular/commit/e5c9bbc))
|
||||
* **router:** do not finish bootstrap until all the routes are resolved ([#15121](https://github.com/angular/angular/issues/15121)) ([34403cd](https://github.com/angular/angular/commit/34403cd))
|
||||
|
||||
|
||||
<a name="4.0.0-rc.3"></a>
|
||||
# [4.0.0-rc.3](https://github.com/angular/angular/compare/4.0.0-rc.2...4.0.0-rc.3) (2017-03-10)
|
||||
|
||||
@ -19,7 +558,6 @@
|
||||
- rename `RendererTypeV2` to `RendererType2`
|
||||
- rename `RendererFactoryV2` to `RendererFactory2`
|
||||
|
||||
|
||||
<a name="2.4.9"></a>
|
||||
## [2.4.9](https://github.com/angular/angular/compare/2.4.8...2.4.9) (2017-03-02)
|
||||
|
||||
|
@ -147,7 +147,7 @@ To ensure consistency throughout the source code, keep these rules in mind as yo
|
||||
* All public API methods **must be documented**. (Details TBC).
|
||||
* We follow [Google's JavaScript Style Guide][js-style-guide], but wrap all code at
|
||||
**100 characters**. An automated formatter is available, see
|
||||
[DEVELOPER.md](DEVELOPER.md#clang-format).
|
||||
[DEVELOPER.md](docs/DEVELOPER.md#clang-format).
|
||||
|
||||
## <a name="commit"></a> Commit Message Guidelines
|
||||
|
||||
@ -263,7 +263,7 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise
|
||||
[coc]: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md
|
||||
[commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#
|
||||
[corporate-cla]: http://code.google.com/legal/corporate-cla-v1.0.html
|
||||
[dev-doc]: https://github.com/angular/angular/blob/master/DEVELOPER.md
|
||||
[dev-doc]: https://github.com/angular/angular/blob/master/docs/DEVELOPER.md
|
||||
[github]: https://github.com/angular/angular
|
||||
[gitter]: https://gitter.im/angular/angular
|
||||
[individual-cla]: http://code.google.com/legal/individual-cla-v1.0.html
|
||||
|
12
WORKSPACE
Normal file
12
WORKSPACE
Normal file
@ -0,0 +1,12 @@
|
||||
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
|
||||
|
||||
git_repository(
|
||||
name = "io_bazel_rules_typescript",
|
||||
remote = "https://github.com/bazelbuild/rules_typescript.git",
|
||||
tag = "0.0.3",
|
||||
)
|
||||
|
||||
load("@io_bazel_rules_typescript//:defs.bzl", "node_repositories", "yarn_install")
|
||||
|
||||
node_repositories()
|
||||
yarn_install(package_json = "//:package.json")
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"project": {
|
||||
"version": "1.0.0-beta.32.3",
|
||||
"name": "site"
|
||||
},
|
||||
"apps": [
|
||||
@ -9,21 +9,24 @@
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"assets",
|
||||
"content",
|
||||
"generated",
|
||||
"app/search/search-worker.js",
|
||||
"favicon.ico"
|
||||
"favicon.ico",
|
||||
"pwa-manifest.json",
|
||||
"google385281288605d160.html"
|
||||
],
|
||||
"index": "index.html",
|
||||
"main": "main.ts",
|
||||
"polyfills": "polyfills.ts",
|
||||
"test": "test.ts",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"tsconfig": "tsconfig.app.json",
|
||||
"testTsconfig": "tsconfig.spec.json",
|
||||
"prefix": "aio",
|
||||
"serviceWorker": false,
|
||||
"styles": [
|
||||
"styles.scss"
|
||||
],
|
||||
"scripts": [
|
||||
|
||||
],
|
||||
"environmentSource": "environments/environment.ts",
|
||||
"environments": {
|
||||
@ -39,12 +42,13 @@
|
||||
},
|
||||
"lint": [
|
||||
{
|
||||
"files": "src/**/*.ts",
|
||||
"project": "src/tsconfig.json"
|
||||
"project": "src/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"files": "e2e/**/*.ts",
|
||||
"project": "e2e/tsconfig.json"
|
||||
"project": "src/tsconfig.spec.json"
|
||||
},
|
||||
{
|
||||
"project": "e2e/tsconfig.e2e.json"
|
||||
}
|
||||
],
|
||||
"test": {
|
||||
@ -54,19 +58,8 @@
|
||||
},
|
||||
"defaults": {
|
||||
"styleExt": "scss",
|
||||
"component": {},
|
||||
"prefixInterfaces": false,
|
||||
"inline": {
|
||||
"style": false,
|
||||
"template": false
|
||||
},
|
||||
"spec": {
|
||||
"class": false,
|
||||
"component": true,
|
||||
"directive": true,
|
||||
"module": false,
|
||||
"pipe": true,
|
||||
"service": true
|
||||
"component": {
|
||||
"inlineStyle": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
45
aio/.gitignore
vendored
Normal file
45
aio/.gitignore
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/out-tsc
|
||||
/src/generated
|
||||
/tmp
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
testem.log
|
||||
/typings
|
||||
yarn-error.log
|
||||
|
||||
# e2e
|
||||
/e2e/*.js
|
||||
/e2e/*.map
|
||||
protractor-results*.txt
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
108
aio/README.md
108
aio/README.md
@ -1,31 +1,105 @@
|
||||
# Site
|
||||
# Angular documentation project (https://angular.io)
|
||||
|
||||
This project was generated with [angular-cli](https://github.com/angular/angular-cli) version 1.0.0-beta.26.
|
||||
Everything in this folder is part of the documentation project. This includes
|
||||
|
||||
## Development server
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
* the web site for displaying the documentation
|
||||
* the dgeni configuration for converting source files to rendered files that can be viewed in the web site.
|
||||
* the tooling for setting up examples for development; and generating plunkers and zip files from the examples.
|
||||
|
||||
## Code scaffolding
|
||||
## Developer tasks
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class/module`.
|
||||
We use `yarn` to manage the dependencies and to run build tasks.
|
||||
You should run all these tasks from the `angular/aio` folder.
|
||||
Here are the most important tasks you might need to use:
|
||||
|
||||
## Build
|
||||
* `yarn` - install all the dependencies.
|
||||
* `yarn setup` - Install all the dependencies, boilerplate, plunkers, zips and runs dgeni on the docs.
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||
* `yarn start` - run a development web server that watches the files; then builds the doc-viewer and reloads the page, as necessary.
|
||||
* `yarn lint` - check that the doc-viewer code follows our style rules.
|
||||
* `yarn test` - watch all the source files, for the doc-viewer, and run all the unit tests when any change.
|
||||
* `yarn e2e` - run all the e2e tests for the doc-viewer.
|
||||
|
||||
## Running unit tests
|
||||
* `yarn docs` - generate all the docs from the source files.
|
||||
* `yarn docs-watch` - watch the Angular source and the docs files and run a short-circuited doc-gen for the docs that changed.
|
||||
* `yarn docs-lint` - check that the doc gen code follows our style rules.
|
||||
* `yarn docs-test` - run the unit tests for the doc generation code.
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
* `yarn boilerplate:add` - generate all the boilerplate code for the examples, so that they can be run locally.
|
||||
* `yarn boilerplate:remove` - remove all the boilerplate code that was added via `yarn boilerplate:add`.
|
||||
* `yarn generate-plunkers` - generate the plunker files that are used by the `live-example` tags in the docs.
|
||||
* `yarn generate-zips` - generate the zip files from the examples. Zip available via the `live-example` tags in the docs.
|
||||
|
||||
## Running end-to-end tests
|
||||
* `yarn build-ie-polyfills` - generates a js file of polyfills that can be loaded in Internet Explorer.
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
Before running the tests make sure you are serving the app via `ng serve`.
|
||||
## Using ServiceWorker locally
|
||||
|
||||
## Deploying to GitHub Pages
|
||||
Since abb36e3cb, running `yarn start -- --prod` will no longer set up the ServiceWorker, which
|
||||
would require manually running `yarn sw-manifest` and `yarn sw-copy` (something that is not possible
|
||||
with webpack serving the files from memory).
|
||||
|
||||
Run `ng github-pages:deploy` to deploy to GitHub Pages.
|
||||
If you want to test ServiceWorker locally, you can use `yarn build` and serve the files in `dist/`
|
||||
with `yarn http-server -- dist -p 4200`.
|
||||
|
||||
## Further help
|
||||
For more details see #16745.
|
||||
|
||||
To get more help on the `angular-cli` use `ng help` or go check out the [Angular-CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
||||
|
||||
## Guide to authoring
|
||||
|
||||
There are two types of content in the documentatation:
|
||||
|
||||
* **API docs**: descriptions of the modules, classes, interfaces, decorators, etc that make up the Angular platform.
|
||||
API docs are generated directly from the source code.
|
||||
The source code is contained in TypeScript files, located in the `angular/packages` folder.
|
||||
Each API item may have a preceding comment, which contains JSDoc style tags and content.
|
||||
The content is written in markdown.
|
||||
|
||||
* **Other content**: guides, tutorials, and other marketing material.
|
||||
All other content is written using markdown in text files, located in the `angular/aio/content` folder.
|
||||
More specifically, there are sub-folders that contain particular types of content: guides, tutorial and marketing.
|
||||
|
||||
We use the [dgeni](https://github.com/angular/dgeni) tool to convert these files into docs that can be viewed in the doc-viewer.
|
||||
|
||||
### Generating the complete docs
|
||||
|
||||
The main task for generating the docs is `yarn docs`. This will process all the source files (API and other),
|
||||
extracting the documentation and generating JSON files that can be consumed by the doc-viewer.
|
||||
|
||||
### Partial doc generation for editors
|
||||
|
||||
Full doc generation can take up to one minute. That's too slow for efficient document creation and editing.
|
||||
|
||||
You can make small changes in a smart editor that displays formatted markdown:
|
||||
>In VS Code, _Cmd-K, V_ opens markdown preview in side pane; _Cmd-B_ toggles left sidebar
|
||||
|
||||
You also want to see those changes displayed properly in the doc viewer
|
||||
with a quick, edit/view cycle time.
|
||||
|
||||
For this purpose, use the `yarn docs-watch` task, which watches for changes to source files and only
|
||||
re-processes the the files necessary to generate the docs that are related to the file that has changed.
|
||||
Since this task takes shortcuts, it is much faster (often less than 1 second) but it won't produce full
|
||||
fidelity content. For example, links to other docs and code examples may not render correctly. This is
|
||||
most particularly noticed in links to other docs and in the embedded examples, which may not always render
|
||||
correctly.
|
||||
|
||||
The general setup is as follows:
|
||||
|
||||
* Open a terminal, ensure the dependencies are installed; run an initial doc generation; then start the doc-viewer:
|
||||
|
||||
```bash
|
||||
yarn
|
||||
yarn docs
|
||||
yarn start
|
||||
```
|
||||
|
||||
* Open a second terminal and start watching the docs
|
||||
|
||||
```bash
|
||||
yarn docs-watch
|
||||
```
|
||||
|
||||
* Open a browser at https://localhost:4200/ and navigate to the document on which you want to work.
|
||||
You can automatically open the browser by using `yarn start -- -o` in the first terminal.
|
||||
|
||||
* Make changes to the page's associated doc or example files. Every time a file is saved, the doc will
|
||||
be regenerated, the app will rebuild and the page will reload.
|
||||
|
@ -19,8 +19,8 @@ ARG AIO_DOMAIN_NAME=ngbuilds.io
|
||||
ARG TEST_AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME.localhost
|
||||
ARG AIO_GITHUB_ORGANIZATION=angular
|
||||
ARG TEST_AIO_GITHUB_ORGANIZATION=angular
|
||||
ARG AIO_GITHUB_TEAM_SLUGS=angular-core
|
||||
ARG TEST_AIO_GITHUB_TEAM_SLUGS=angular-core
|
||||
ARG AIO_GITHUB_TEAM_SLUGS=angular-core,aio-contributors
|
||||
ARG TEST_AIO_GITHUB_TEAM_SLUGS=angular-core,aio-contributors
|
||||
ARG AIO_NGINX_HOSTNAME=$AIO_DOMAIN_NAME
|
||||
ARG TEST_AIO_NGINX_HOSTNAME=$TEST_AIO_DOMAIN_NAME
|
||||
ARG AIO_NGINX_PORT_HTTP=80
|
||||
@ -42,6 +42,7 @@ ENV AIO_BUILDS_DIR=$AIO_BUILDS_DIR TEST_AIO_BUILDS_DIR=$TEST
|
||||
AIO_GITHUB_TEAM_SLUGS=$AIO_GITHUB_TEAM_SLUGS TEST_AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS \
|
||||
AIO_LOCALCERTS_DIR=/etc/ssl/localcerts TEST_AIO_LOCALCERTS_DIR=/etc/ssl/localcerts-test \
|
||||
AIO_NGINX_HOSTNAME=$AIO_NGINX_HOSTNAME TEST_AIO_NGINX_HOSTNAME=$TEST_AIO_NGINX_HOSTNAME \
|
||||
AIO_NGINX_LOGS_DIR=/var/log/aio/nginx TEST_AIO_NGINX_LOGS_DIR=/var/log/aio/nginx-test \
|
||||
AIO_NGINX_PORT_HTTP=$AIO_NGINX_PORT_HTTP TEST_AIO_NGINX_PORT_HTTP=$TEST_AIO_NGINX_PORT_HTTP \
|
||||
AIO_NGINX_PORT_HTTPS=$AIO_NGINX_PORT_HTTPS TEST_AIO_NGINX_PORT_HTTPS=$TEST_AIO_NGINX_PORT_HTTPS \
|
||||
AIO_REPO_SLUG=$AIO_REPO_SLUG TEST_AIO_REPO_SLUG=$TEST_AIO_REPO_SLUG \
|
||||
@ -50,6 +51,7 @@ ENV AIO_BUILDS_DIR=$AIO_BUILDS_DIR TEST_AIO_BUILDS_DIR=$TEST
|
||||
AIO_UPLOAD_HOSTNAME=$AIO_UPLOAD_HOSTNAME TEST_AIO_UPLOAD_HOSTNAME=$TEST_AIO_UPLOAD_HOSTNAME \
|
||||
AIO_UPLOAD_MAX_SIZE=$AIO_UPLOAD_MAX_SIZE TEST_AIO_UPLOAD_MAX_SIZE=$TEST_AIO_UPLOAD_MAX_SIZE \
|
||||
AIO_UPLOAD_PORT=$AIO_UPLOAD_PORT TEST_AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT \
|
||||
AIO_WWW_USER=www-data \
|
||||
NODE_ENV=production
|
||||
|
||||
|
||||
@ -62,6 +64,7 @@ RUN apt-get update -y && apt-get install -y curl
|
||||
RUN curl --silent --show-error --location https://deb.nodesource.com/setup_6.x | bash -
|
||||
RUN curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
RUN echo "deb http://ftp.debian.org/debian jessie-backports main" | tee /etc/apt/sources.list.d/backports.list
|
||||
|
||||
|
||||
# Install packages
|
||||
@ -70,26 +73,32 @@ RUN apt-get update -y && apt-get install -y \
|
||||
cron \
|
||||
dnsmasq \
|
||||
nano \
|
||||
nginx \
|
||||
nodejs \
|
||||
openssl \
|
||||
rsyslog \
|
||||
yarn
|
||||
RUN apt-get install -t jessie-backports -y nginx
|
||||
RUN yarn global add pm2@2
|
||||
|
||||
|
||||
# Set up log rotation
|
||||
COPY logrotate/* /etc/logrotate.d/
|
||||
RUN chmod 0644 /etc/logrotate.d/*
|
||||
|
||||
|
||||
# Set up cronjobs
|
||||
COPY cronjobs/aio-builds-cleanup /etc/cron.d/
|
||||
RUN chmod 0744 /etc/cron.d/aio-builds-cleanup
|
||||
RUN crontab /etc/cron.d/aio-builds-cleanup
|
||||
RUN printenv | grep AIO_ >> /etc/environment
|
||||
|
||||
|
||||
# Set up dnsmasq
|
||||
COPY dnsmasq/dnsmasq.conf /etc/
|
||||
RUN sed -i "s|{{\$AIO_NGINX_HOSTNAME}}|$AIO_NGINX_HOSTNAME|" /etc/dnsmasq.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|" /etc/dnsmasq.conf
|
||||
RUN sed -i "s|{{\$TEST_AIO_NGINX_HOSTNAME}}|$TEST_AIO_NGINX_HOSTNAME|" /etc/dnsmasq.conf
|
||||
RUN sed -i "s|{{\$TEST_AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|" /etc/dnsmasq.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_HOSTNAME}}|$AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|g" /etc/dnsmasq.conf
|
||||
RUN sed -i "s|{{\$TEST_AIO_NGINX_HOSTNAME}}|$TEST_AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf
|
||||
RUN sed -i "s|{{\$TEST_AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|g" /etc/dnsmasq.conf
|
||||
|
||||
|
||||
# Set up SSL/TLS certificates
|
||||
@ -102,29 +111,31 @@ RUN update-ca-certificates
|
||||
|
||||
|
||||
# Set up nginx (for production and testing)
|
||||
RUN rm /etc/nginx/sites-enabled/*
|
||||
RUN sed -i -E "s|^user\s+\S+;|user $AIO_WWW_USER;|" /etc/nginx/nginx.conf
|
||||
RUN rm -f /etc/nginx/conf.d/*
|
||||
RUN rm -f /etc/nginx/sites-enabled/*
|
||||
|
||||
COPY nginx/aio-builds.conf /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$AIO_BUILDS_DIR|" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$AIO_DOMAIN_NAME|" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$AIO_LOCALCERTS_DIR|" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$AIO_NGINX_PORT_HTTP|" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$AIO_NGINX_PORT_HTTPS|" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$AIO_UPLOAD_MAX_SIZE|" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$AIO_UPLOAD_PORT|" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN ln -s /etc/nginx/sites-available/aio-builds-prod.conf /etc/nginx/sites-enabled/aio-builds-prod.conf
|
||||
COPY nginx/aio-builds.conf /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$AIO_BUILDS_DIR|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$AIO_DOMAIN_NAME|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$AIO_LOCALCERTS_DIR|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$AIO_NGINX_LOGS_DIR|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$AIO_NGINX_PORT_HTTP|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$AIO_NGINX_PORT_HTTPS|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$AIO_UPLOAD_MAX_SIZE|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$AIO_UPLOAD_PORT|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
|
||||
COPY nginx/aio-builds.conf /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$TEST_AIO_BUILDS_DIR|" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$TEST_AIO_DOMAIN_NAME|" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$TEST_AIO_LOCALCERTS_DIR|" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$TEST_AIO_NGINX_PORT_HTTP|" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$TEST_AIO_NGINX_PORT_HTTPS|" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$TEST_AIO_UPLOAD_MAX_SIZE|" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$TEST_AIO_UPLOAD_PORT|" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN ln -s /etc/nginx/sites-available/aio-builds-test.conf /etc/nginx/sites-enabled/aio-builds-test.conf
|
||||
COPY nginx/aio-builds.conf /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$TEST_AIO_BUILDS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$TEST_AIO_DOMAIN_NAME|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$TEST_AIO_LOCALCERTS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$TEST_AIO_NGINX_LOGS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$TEST_AIO_NGINX_PORT_HTTP|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$TEST_AIO_NGINX_PORT_HTTPS|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$TEST_AIO_UPLOAD_MAX_SIZE|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$TEST_AIO_UPLOAD_PORT|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
|
||||
|
||||
# Set up pm2
|
||||
|
@ -1,2 +1,2 @@
|
||||
# Periodically clean up builds that do not correspond to currently open PRs
|
||||
0 4 * * * root /usr/local/bin/aio-clean-up >> /var/log/cron.log 2>&1
|
||||
0 12 * * * root /usr/local/bin/aio-clean-up >> /var/log/cron.log 2>&1
|
||||
|
9
aio/aio-builds-setup/dockerbuild/logrotate/aio-misc
Normal file
9
aio/aio-builds-setup/dockerbuild/logrotate/aio-misc
Normal file
@ -0,0 +1,9 @@
|
||||
/var/log/aio/clean-up.log /var/log/aio/init.log /var/log/aio/verify-setup.log {
|
||||
compress
|
||||
create
|
||||
delaycompress
|
||||
missingok
|
||||
monthly
|
||||
notifempty
|
||||
rotate 6
|
||||
}
|
13
aio/aio-builds-setup/dockerbuild/logrotate/aio-nginx
Normal file
13
aio/aio-builds-setup/dockerbuild/logrotate/aio-nginx
Normal file
@ -0,0 +1,13 @@
|
||||
/var/log/aio/nginx/*.log /var/log/aio/nginx-test/*.log {
|
||||
compress
|
||||
create
|
||||
delaycompress
|
||||
missingok
|
||||
monthly
|
||||
notifempty
|
||||
rotate 6
|
||||
sharedscripts
|
||||
postrotate
|
||||
service nginx rotate >/dev/null 2>&1
|
||||
endscript
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
/var/log/aio/upload-server-*.log {
|
||||
compress
|
||||
copytruncate
|
||||
delaycompress
|
||||
missingok
|
||||
monthly
|
||||
notifempty
|
||||
rotate 6
|
||||
}
|
@ -1,44 +1,73 @@
|
||||
# Redirect all HTTP traffic to HTTPS
|
||||
server {
|
||||
server_name _;
|
||||
|
||||
listen {{$AIO_NGINX_PORT_HTTP}} default_server;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTP}};
|
||||
|
||||
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
|
||||
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
|
||||
|
||||
# Ideally we want 308 (permanent + keep original method),
|
||||
# but it is relatively new and not supported by some clients (e.g. cURL).
|
||||
return 307 https://$host:{{$AIO_NGINX_PORT_HTTPS}}$request_uri;
|
||||
}
|
||||
|
||||
# Serve PR-preview requests
|
||||
server {
|
||||
server_name "~^pr(?<pr>[1-9][0-9]*)-(?<sha>[0-9a-f]{40})\.";
|
||||
|
||||
listen {{$AIO_NGINX_PORT_HTTP}};
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTP}};
|
||||
listen {{$AIO_NGINX_PORT_HTTPS}} ssl;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl;
|
||||
listen {{$AIO_NGINX_PORT_HTTPS}} ssl http2;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl http2;
|
||||
|
||||
ssl_certificate {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.crt;
|
||||
ssl_certificate_key {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.key;
|
||||
ssl_certificate {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.crt;
|
||||
ssl_certificate_key {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.key;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
|
||||
|
||||
root {{$AIO_BUILDS_DIR}}/$pr/$sha;
|
||||
disable_symlinks on from=$document_root;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
gzip on;
|
||||
gzip_comp_level 7;
|
||||
gzip_types *;
|
||||
|
||||
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
|
||||
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
|
||||
|
||||
location "~/[^/]+\.[^/]+$" {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html =404;
|
||||
}
|
||||
}
|
||||
|
||||
# Handle all other requests
|
||||
server {
|
||||
server_name _;
|
||||
|
||||
listen {{$AIO_NGINX_PORT_HTTP}} default_server;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTP}};
|
||||
listen {{$AIO_NGINX_PORT_HTTPS}} ssl default_server;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl;
|
||||
listen {{$AIO_NGINX_PORT_HTTPS}} ssl http2 default_server;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl http2;
|
||||
|
||||
ssl_certificate {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.crt;
|
||||
ssl_certificate_key {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.key;
|
||||
ssl_certificate {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.crt;
|
||||
ssl_certificate_key {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.key;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
|
||||
|
||||
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
|
||||
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
|
||||
|
||||
# Health check
|
||||
location "~^\/health-check\/?$" {
|
||||
location "~^/health-check/?$" {
|
||||
add_header Content-Type text/plain;
|
||||
return 200 '';
|
||||
}
|
||||
|
||||
# Upload builds
|
||||
location "~^\/create-build\/(?<pr>[1-9][0-9]*)\/(?<sha>[0-9a-f]{40})\/?$" {
|
||||
location "~^/create-build/(?<pr>[1-9][0-9]*)/(?<sha>[0-9a-f]{40})/?$" {
|
||||
if ($request_method != "POST") {
|
||||
add_header Allow "POST";
|
||||
return 405;
|
||||
|
@ -1 +1,2 @@
|
||||
/dist/
|
||||
/dist
|
||||
/node_modules
|
||||
|
@ -12,6 +12,8 @@ _main();
|
||||
|
||||
// Functions
|
||||
function _main() {
|
||||
console.log(`[${new Date()}] - Cleaning up builds...`);
|
||||
|
||||
const buildCleaner = new BuildCleaner(AIO_BUILDS_DIR, AIO_REPO_SLUG, AIO_GITHUB_TOKEN);
|
||||
|
||||
buildCleaner.cleanUp().catch(err => {
|
||||
|
@ -26,7 +26,7 @@ export class BuildCreator extends EventEmitter {
|
||||
all([this.exists(prDir), this.exists(shaDir)]).
|
||||
then(([prDirExisted, shaDirExisted]) => {
|
||||
if (shaDirExisted) {
|
||||
throw new UploadError(403, `Request to overwrite existing directory: ${shaDir}`);
|
||||
throw new UploadError(409, `Request to overwrite existing directory: ${shaDir}`);
|
||||
}
|
||||
|
||||
dirToRemoveOnError = prDirExisted ? shaDir : prDir;
|
||||
|
@ -18,8 +18,8 @@ function _main() {
|
||||
|
||||
// Exit codes:
|
||||
// - 0: The PR author is a member.
|
||||
// - 1: The PR author is not a member.
|
||||
// - 2: An error occurred.
|
||||
// - 1: An error occurred.
|
||||
// - 2: The PR author is not a member.
|
||||
buildVerifier.getPrAuthorTeamMembership(pr).
|
||||
then(({author, isMember}) => {
|
||||
if (isMember) {
|
||||
@ -27,10 +27,10 @@ function _main() {
|
||||
} else {
|
||||
const errorMessage = `User '${author}' is not an active member of any of the following teams: ` +
|
||||
`${allowedTeamSlugs.join(', ')}`;
|
||||
onError(errorMessage, 1);
|
||||
onError(errorMessage, 2);
|
||||
}
|
||||
}).
|
||||
catch(err => onError(err, 2));
|
||||
catch(err => onError(err, 1));
|
||||
}
|
||||
|
||||
function onError(err: string, exitCode: number) {
|
||||
|
@ -1,6 +1,3 @@
|
||||
// TODO(gkalpak): Find more suitable way to run as `www-data`.
|
||||
process.setuid('www-data');
|
||||
|
||||
// Imports
|
||||
import {getEnvVar} from '../common/utils';
|
||||
import {uploadServerFactory} from './upload-server-factory';
|
||||
@ -15,8 +12,10 @@ const AIO_PREVIEW_DEPLOYMENT_TOKEN = getEnvVar('AIO_PREVIEW_DEPLOYMENT_TOKEN');
|
||||
const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG');
|
||||
const AIO_UPLOAD_HOSTNAME = getEnvVar('AIO_UPLOAD_HOSTNAME');
|
||||
const AIO_UPLOAD_PORT = +getEnvVar('AIO_UPLOAD_PORT');
|
||||
const AIO_WWW_USER = getEnvVar('AIO_WWW_USER');
|
||||
|
||||
// Run
|
||||
process.setuid(AIO_WWW_USER); // TODO(gkalpak): Find more suitable way to run as `www-data`.
|
||||
_main();
|
||||
|
||||
// Functions
|
||||
|
@ -58,7 +58,7 @@ class UploadServerFactory {
|
||||
const githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
|
||||
|
||||
buildCreator.on(CreatedBuildEvent.type, ({pr, sha}: CreatedBuildEvent) => {
|
||||
const body = `The angular.io preview for ${sha.slice(0, 7)} is available [here][1].\n\n` +
|
||||
const body = `The angular.io preview for ${sha} is available [here][1].\n\n` +
|
||||
`[1]: https://pr${pr}-${sha}.${domainName}/`;
|
||||
|
||||
githubPullRequests.addComment(pr, body);
|
||||
|
@ -7,7 +7,6 @@ import * as shell from 'shelljs';
|
||||
import {getEnvVar} from '../common/utils';
|
||||
|
||||
// Constans
|
||||
const SERVER_USER = 'www-data';
|
||||
const TEST_AIO_BUILDS_DIR = getEnvVar('TEST_AIO_BUILDS_DIR');
|
||||
const TEST_AIO_NGINX_HOSTNAME = getEnvVar('TEST_AIO_NGINX_HOSTNAME');
|
||||
const TEST_AIO_NGINX_PORT_HTTP = +getEnvVar('TEST_AIO_NGINX_PORT_HTTP');
|
||||
@ -15,6 +14,7 @@ const TEST_AIO_NGINX_PORT_HTTPS = +getEnvVar('TEST_AIO_NGINX_PORT_HTTPS');
|
||||
const TEST_AIO_UPLOAD_HOSTNAME = getEnvVar('TEST_AIO_UPLOAD_HOSTNAME');
|
||||
const TEST_AIO_UPLOAD_MAX_SIZE = +getEnvVar('TEST_AIO_UPLOAD_MAX_SIZE');
|
||||
const TEST_AIO_UPLOAD_PORT = +getEnvVar('TEST_AIO_UPLOAD_PORT');
|
||||
const WWW_USER = getEnvVar('AIO_WWW_USER');
|
||||
|
||||
// Interfaces - Types
|
||||
export interface CmdResult { success: boolean; err: Error; stdout: string; stderr: string; }
|
||||
@ -31,7 +31,7 @@ class Helper {
|
||||
public get nginxHostname() { return TEST_AIO_NGINX_HOSTNAME; }
|
||||
public get nginxPortHttp() { return TEST_AIO_NGINX_PORT_HTTP; }
|
||||
public get nginxPortHttps() { return TEST_AIO_NGINX_PORT_HTTPS; }
|
||||
public get serverUser() { return SERVER_USER; }
|
||||
public get wwwUser() { return WWW_USER; }
|
||||
public get uploadHostname() { return TEST_AIO_UPLOAD_HOSTNAME; }
|
||||
public get uploadPort() { return TEST_AIO_UPLOAD_PORT; }
|
||||
public get uploadMaxSize() { return TEST_AIO_UPLOAD_MAX_SIZE; }
|
||||
@ -46,7 +46,7 @@ class Helper {
|
||||
// Constructor
|
||||
constructor() {
|
||||
shell.mkdir('-p', this.buildsDir);
|
||||
shell.exec(`chown -R ${this.serverUser} ${this.buildsDir}`);
|
||||
shell.exec(`chown -R ${this.wwwUser} ${this.buildsDir}`);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
@ -64,7 +64,7 @@ class Helper {
|
||||
public createDummyArchive(pr: string, sha: string, archivePath: string): CleanUpFn {
|
||||
const inputDir = path.join(this.buildsDir, 'uploaded', pr, sha);
|
||||
const cmd1 = `tar --create --gzip --directory "${inputDir}" --file "${archivePath}" .`;
|
||||
const cmd2 = `chown ${this.serverUser} ${archivePath}`;
|
||||
const cmd2 = `chown ${this.wwwUser} ${archivePath}`;
|
||||
|
||||
const cleanUpTemp = this.createDummyBuild(`uploaded/${pr}`, sha, true);
|
||||
shell.exec(cmd1);
|
||||
@ -82,7 +82,7 @@ class Helper {
|
||||
|
||||
this.writeFile(idxPath, {content: `PR: ${pr} | SHA: ${sha} | File: /index.html`}, force);
|
||||
this.writeFile(barPath, {content: `PR: ${pr} | SHA: ${sha} | File: /foo/bar.js`}, force);
|
||||
shell.exec(`chown -R ${this.serverUser} ${prDir}`);
|
||||
shell.exec(`chown -R ${this.wwwUser} ${prDir}`);
|
||||
|
||||
return this.createCleanUpFn(() => shell.rm('-rf', prDir));
|
||||
}
|
||||
@ -166,7 +166,7 @@ class Helper {
|
||||
// Create a file with the specified content.
|
||||
fs.writeFileSync(filePath, content || '');
|
||||
}
|
||||
shell.exec(`chown ${this.serverUser} ${filePath}`);
|
||||
shell.exec(`chown ${this.wwwUser} ${filePath}`);
|
||||
|
||||
return this.createCleanUpFn(() => shell.rm('-rf', cleanUpTarget));
|
||||
}
|
||||
|
@ -3,230 +3,269 @@ import * as path from 'path';
|
||||
import {helper as h} from './helper';
|
||||
|
||||
// Tests
|
||||
h.runForAllSupportedSchemes((scheme, port) => describe(`nginx (on ${scheme.toUpperCase()})`, () => {
|
||||
const hostname = h.nginxHostname;
|
||||
const host = `${hostname}:${port}`;
|
||||
const pr = '9';
|
||||
const sha9 = '9'.repeat(40);
|
||||
const sha0 = '0'.repeat(40);
|
||||
describe(`nginx`, () => {
|
||||
|
||||
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
|
||||
afterEach(() => h.cleanUp());
|
||||
|
||||
|
||||
describe(`pr<pr>-<sha>.${host}/*`, () => {
|
||||
it('should redirect HTTP to HTTPS', done => {
|
||||
const httpHost = `${h.nginxHostname}:${h.nginxPortHttp}`;
|
||||
const httpsHost = `${h.nginxHostname}:${h.nginxPortHttps}`;
|
||||
const urlMap = {
|
||||
[`http://${httpHost}/`]: `https://${httpsHost}/`,
|
||||
[`http://${httpHost}/foo`]: `https://${httpsHost}/foo`,
|
||||
[`http://foo.${httpHost}/`]: `https://foo.${httpsHost}/`,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
h.createDummyBuild(pr, sha9);
|
||||
h.createDummyBuild(pr, sha0);
|
||||
});
|
||||
|
||||
|
||||
it('should return /index.html', done => {
|
||||
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
||||
const bodyegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyegex)),
|
||||
h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyegex)),
|
||||
h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should return /foo/bar.js', done => {
|
||||
const bodyegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
|
||||
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/bar.js`).
|
||||
then(h.verifyResponse(200, bodyegex)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 403 for directories', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/`).then(h.verifyResponse(403)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo`).then(h.verifyResponse(403)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', done => {
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz.css`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown PRs/SHAs', done => {
|
||||
const otherPr = 54321;
|
||||
const otherSha = '8'.repeat(40);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}9-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${otherPr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}9.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${otherSha}.${host}`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the subdomain format is wrong', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://xpr${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://prx${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://xx${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://p${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://r${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}_${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject PRs with leading zeros', done => {
|
||||
h.runCmd(`curl -iL ${scheme}://pr0${pr}-${sha9}.${host}`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should accept SHAs with leading zeros (but not ignore them)', done => {
|
||||
const bodyegex = new RegExp(`^PR: ${pr} | SHA: ${sha0} | File: /index\\.html$`);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-0${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha0}.${host}`).then(h.verifyResponse(200, bodyegex)),
|
||||
]).then(done);
|
||||
const verifyRedirection = (httpUrl: string) => h.runCmd(`curl -i ${httpUrl}`).then(result => {
|
||||
h.verifyResponse(307)(result);
|
||||
|
||||
const headers = result.stdout.split(/(?:\r?\n){2,}/)[0];
|
||||
expect(headers).toContain(`Location: ${urlMap[httpUrl]}`);
|
||||
});
|
||||
|
||||
Promise.
|
||||
all(Object.keys(urlMap).map(verifyRedirection)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/health-check`, () => {
|
||||
|
||||
it('should respond with 200', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check`).then(h.verifyResponse(200)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check/`).then(h.verifyResponse(200)),
|
||||
]).then(done);
|
||||
});
|
||||
h.runForAllSupportedSchemes((scheme, port) => describe(`nginx (on ${scheme.toUpperCase()})`, () => {
|
||||
const hostname = h.nginxHostname;
|
||||
const host = `${hostname}:${port}`;
|
||||
const pr = '9';
|
||||
const sha9 = '9'.repeat(40);
|
||||
const sha0 = '0'.repeat(40);
|
||||
|
||||
|
||||
it('should respond with 404 if the path does not match exactly', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check/foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check-foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-checknfoo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foo/health-check`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foo-health-check`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foonhealth-check`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
describe(`pr<pr>-<sha>.${host}/*`, () => {
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/create-build/<pr>/<sha>`, () => {
|
||||
|
||||
it('should disallow non-POST requests', done => {
|
||||
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it(`should reject files larger than ${h.uploadMaxSize}B (according to header)`, done => {
|
||||
const headers = `--header "Content-Length: ${1.5 * h.uploadMaxSize}"`;
|
||||
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
|
||||
|
||||
h.runCmd(`curl -iLX POST ${headers} ${url}`).
|
||||
then(h.verifyResponse([413, 'Request Entity Too Large'])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it(`should reject files larger than ${h.uploadMaxSize}B (without header)`, done => {
|
||||
const filePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
||||
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
|
||||
|
||||
h.writeFile(filePath, {size: 1.5 * h.uploadMaxSize});
|
||||
|
||||
h.runCmd(`curl -iLX POST --data-binary "@${filePath}" ${url}`).
|
||||
then(h.verifyResponse([413, 'Request Entity Too Large'])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should pass requests through to the upload server', done => {
|
||||
h.runCmd(`curl -iLX POST ${scheme}://${host}/create-build/${pr}/${sha9}`).
|
||||
then(h.verifyResponse(401, /Missing or empty 'AUTHORIZATION' header/)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', done => {
|
||||
const cmdPrefix = `curl -iLX POST ${scheme}://${host}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/foo/create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foo-create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/fooncreate-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build/foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build-foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-buildnfoo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build/pr${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build/${pr}/${sha9}42`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject PRs with leading zeros', done => {
|
||||
h.runCmd(`curl -iLX POST ${scheme}://${host}/create-build/0${pr}/${sha9}`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should accept SHAs with leading zeros (but not ignore them)', done => {
|
||||
const cmdPrefix = `curl -iLX POST ${scheme}://${host}/create-build/${pr}`;
|
||||
const bodyRegex = /Missing or empty 'AUTHORIZATION' header/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/0${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/${sha0}`).then(h.verifyResponse(401, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/*`, () => {
|
||||
|
||||
it('should respond with 404 for unkown URLs (even if the resource exists)', done => {
|
||||
['index.html', 'foo.js', 'foo/index.html'].forEach(relFilePath => {
|
||||
const absFilePath = path.join(h.buildsDir, relFilePath);
|
||||
h.writeFile(absFilePath, {content: `File: /${relFilePath}`});
|
||||
beforeEach(() => {
|
||||
h.createDummyBuild(pr, sha9);
|
||||
h.createDummyBuild(pr, sha0);
|
||||
});
|
||||
|
||||
|
||||
it('should return /index.html', done => {
|
||||
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should return /foo/bar.js', done => {
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
|
||||
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/bar.js`).
|
||||
then(h.verifyResponse(200, bodyRegex)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 403 for directories', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/`).then(h.verifyResponse(403)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo`).then(h.verifyResponse(403)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths to files', done => {
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz.css`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should rewrite to \'index.html\' for unknown paths that don\'t look like files', done => {
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz`).then(h.verifyResponse(200, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz/`).then(h.verifyResponse(200, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown PRs/SHAs', done => {
|
||||
const otherPr = 54321;
|
||||
const otherSha = '8'.repeat(40);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}9-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${otherPr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}9.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${otherSha}.${host}`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the subdomain format is wrong', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://xpr${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://prx${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://xx${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://p${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://r${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}_${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject PRs with leading zeros', done => {
|
||||
h.runCmd(`curl -iL ${scheme}://pr0${pr}-${sha9}.${host}`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
||||
const bodyRegex9 = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
const bodyRegex0 = new RegExp(`^PR: ${pr} | SHA: ${sha0} | File: /index\\.html$`);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-0${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}`).then(h.verifyResponse(200, bodyRegex9)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha0}.${host}`).then(h.verifyResponse(200, bodyRegex0)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/index.html`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://foo.${host}/index.html`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://foo.${host}/`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://foo.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foo.js`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foo/index.html`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}));
|
||||
describe(`${host}/health-check`, () => {
|
||||
|
||||
it('should respond with 200', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check`).then(h.verifyResponse(200)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check/`).then(h.verifyResponse(200)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the path does not match exactly', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check/foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check-foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-checknfoo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foo/health-check`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foo-health-check`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foonhealth-check`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/create-build/<pr>/<sha>`, () => {
|
||||
|
||||
it('should disallow non-POST requests', done => {
|
||||
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it(`should reject files larger than ${h.uploadMaxSize}B (according to header)`, done => {
|
||||
const headers = `--header "Content-Length: ${1.5 * h.uploadMaxSize}"`;
|
||||
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
|
||||
|
||||
h.runCmd(`curl -iLX POST ${headers} ${url}`).
|
||||
then(h.verifyResponse([413, 'Request Entity Too Large'])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it(`should reject files larger than ${h.uploadMaxSize}B (without header)`, done => {
|
||||
const filePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
||||
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
|
||||
|
||||
h.writeFile(filePath, {size: 1.5 * h.uploadMaxSize});
|
||||
|
||||
h.runCmd(`curl -iLX POST --data-binary "@${filePath}" ${url}`).
|
||||
then(h.verifyResponse([413, 'Request Entity Too Large'])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should pass requests through to the upload server', done => {
|
||||
h.runCmd(`curl -iLX POST ${scheme}://${host}/create-build/${pr}/${sha9}`).
|
||||
then(h.verifyResponse(401, /Missing or empty 'AUTHORIZATION' header/)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', done => {
|
||||
const cmdPrefix = `curl -iLX POST ${scheme}://${host}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/foo/create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foo-create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/fooncreate-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build/foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build-foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-buildnfoo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build/pr${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build/${pr}/${sha9}42`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject PRs with leading zeros', done => {
|
||||
h.runCmd(`curl -iLX POST ${scheme}://${host}/create-build/0${pr}/${sha9}`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
||||
const cmdPrefix = `curl -iLX POST ${scheme}://${host}/create-build/${pr}`;
|
||||
const bodyRegex = /Missing or empty 'AUTHORIZATION' header/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/0${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/${sha0}`).then(h.verifyResponse(401, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/*`, () => {
|
||||
|
||||
it('should respond with 404 for unkown URLs (even if the resource exists)', done => {
|
||||
['index.html', 'foo.js', 'foo/index.html'].forEach(relFilePath => {
|
||||
const absFilePath = path.join(h.buildsDir, relFilePath);
|
||||
h.writeFile(absFilePath, {content: `File: /${relFilePath}`});
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/index.html`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://foo.${host}/index.html`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://foo.${host}/`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://foo.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foo.js`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foo/index.html`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
});
|
||||
|
@ -73,7 +73,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath).
|
||||
then(h.verifyResponse(403)).
|
||||
then(h.verifyResponse(409)).
|
||||
then(() => Promise.all([
|
||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
|
||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
|
||||
|
@ -84,9 +84,10 @@ describe('upload-server (on HTTP)', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should accept SHAs with leading zeros (but not ignore them)', done => {
|
||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/0${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).then(h.verifyResponse(500)),
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha0}`).then(h.verifyResponse(500)),
|
||||
]).then(done);
|
||||
});
|
||||
@ -100,7 +101,7 @@ describe('upload-server (on HTTP)', () => {
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html')).toBe('My content');
|
||||
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).
|
||||
then(h.verifyResponse(403, /^Request to overwrite existing directory/)).
|
||||
then(h.verifyResponse(409, /^Request to overwrite existing directory/)).
|
||||
then(() => expect(h.readBuildFile(pr, sha9, 'index.html')).toBe('My content')).
|
||||
then(done);
|
||||
});
|
||||
@ -158,7 +159,7 @@ describe('upload-server (on HTTP)', () => {
|
||||
});
|
||||
|
||||
|
||||
it(`should create files/directories owned by '${h.serverUser}'`, done => {
|
||||
it(`should create files/directories owned by '${h.wwwUser}'`, done => {
|
||||
const shaDir = path.join(h.buildsDir, pr, sha9);
|
||||
const idxPath = path.join(shaDir, 'index.html');
|
||||
const barPath = path.join(shaDir, 'foo', 'bar.js');
|
||||
@ -166,7 +167,7 @@ describe('upload-server (on HTTP)', () => {
|
||||
uploadPromise.
|
||||
then(() => Promise.all([
|
||||
h.runCmd(`find ${shaDir}`),
|
||||
h.runCmd(`find ${shaDir} -user ${h.serverUser}`),
|
||||
h.runCmd(`find ${shaDir} -user ${h.wwwUser}`),
|
||||
])).
|
||||
then(([{stdout: allFiles}, {stdout: userFiles}]) => {
|
||||
expect(userFiles).toBe(allFiles);
|
||||
|
@ -6,18 +6,18 @@
|
||||
"author": "Angular",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prebuild": "yarn run clean",
|
||||
"prebuild": "yarn clean-dist",
|
||||
"build": "tsc",
|
||||
"build-watch": "yarn run tsc -- --watch",
|
||||
"clean": "node --eval \"require('shelljs').rm('-rf', 'dist')\"",
|
||||
"dev": "concurrently --kill-others --raw --success first \"yarn run build-watch\" \"yarn run test-watch\"",
|
||||
"build-watch": "yarn tsc -- --watch",
|
||||
"clean-dist": "node --eval \"require('shelljs').rm('-rf', 'dist')\"",
|
||||
"dev": "concurrently --kill-others --raw --success first \"yarn build-watch\" \"yarn test-watch\"",
|
||||
"lint": "tslint --project tsconfig.json",
|
||||
"pre~~test-only": "yarn run lint",
|
||||
"pre~~test-only": "yarn lint",
|
||||
"~~test-only": "node dist/test",
|
||||
"pretest": "yarn run build",
|
||||
"test": "yarn run ~~test-only",
|
||||
"pretest-watch": "yarn run build",
|
||||
"test-watch": "nodemon --exec \"yarn run ~~test-only\" --watch dist"
|
||||
"pretest": "yarn build",
|
||||
"test": "yarn ~~test-only",
|
||||
"pretest-watch": "yarn build",
|
||||
"test-watch": "nodemon --exec \"yarn ~~test-only\" --watch dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.14.1",
|
||||
|
@ -66,7 +66,7 @@ describe('BuildCreator', () => {
|
||||
it('should throw if the build does already exist', done => {
|
||||
bcExistsSpy.and.returnValue(true);
|
||||
bc.create(pr, sha, archive).catch(err => {
|
||||
expectToBeUploadError(err, 403, `Request to overwrite existing directory: ${shaDir}`);
|
||||
expectToBeUploadError(err, 409, `Request to overwrite existing directory: ${shaDir}`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -65,8 +65,12 @@ describe('BuildVerifier', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(bv.verify(pr, createAuthHeader())).toEqual(jasmine.any(Promise));
|
||||
it('should return a promise', done => {
|
||||
const promise = bv.verify(pr, createAuthHeader());
|
||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||
// to avoid running the actual `bvGetPrAuthorTeamMembership()`.
|
||||
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
@ -194,8 +198,12 @@ describe('BuildVerifier', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(bv.getPrAuthorTeamMembership(pr)).toEqual(jasmine.any(Promise));
|
||||
it('should return a promise', done => {
|
||||
const promise = bv.getPrAuthorTeamMembership(pr);
|
||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||
// to avoid running the actual `GithubTeams#isMemberBySlug()`.
|
||||
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
|
@ -143,7 +143,7 @@ describe('uploadServerFactory', () => {
|
||||
|
||||
it('should post a comment on GitHub on \'build.created\'', () => {
|
||||
const prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment');
|
||||
const commentBody = 'The angular.io preview for 1234567 is available [here][1].\n\n' +
|
||||
const commentBody = 'The angular.io preview for 1234567890 is available [here][1].\n\n' +
|
||||
'[1]: https://pr42-1234567890.domain.name/';
|
||||
|
||||
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890'});
|
||||
@ -323,18 +323,16 @@ describe('uploadServerFactory', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should accept SHAs with leading zeros (but not ignore them)', done => {
|
||||
const sha41 = '0'.repeat(41);
|
||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
||||
const sha40 = '0'.repeat(40);
|
||||
const sha41 = `0${sha40}`;
|
||||
|
||||
const request41 = agent.get(`/create-build/${pr}/${sha41}`);
|
||||
const request40 = agent.get(`/create-build/${pr}/${sha40}`).
|
||||
set('AUTHORIZATION', 'foo').
|
||||
set('X-FILE', 'bar');
|
||||
const request40 = agent.get(`/create-build/${pr}/${sha40}`).set('AUTHORIZATION', 'foo').set('X-FILE', 'bar');
|
||||
const request41 = agent.get(`/create-build/${pr}/${sha41}`).set('AUTHORIZATION', 'baz').set('X-FILE', 'qux');
|
||||
|
||||
Promise.all([
|
||||
promisifyRequest(request41.expect(404)),
|
||||
promisifyRequest(request40.expect(201)),
|
||||
promisifyRequest(request41.expect(404)),
|
||||
]).then(done, done.fail);
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -e -o pipefail
|
||||
|
||||
# Set up env variables
|
||||
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
|
||||
|
||||
# Run the clean-up
|
||||
node $AIO_SCRIPTS_JS_DIR/dist/lib/clean-up >> /var/log/aio/clean-up.log 2>&1
|
||||
|
@ -6,6 +6,9 @@ exec 2>&1
|
||||
|
||||
# Start the services
|
||||
echo [`date`] - Starting services...
|
||||
mkdir -p $AIO_NGINX_LOGS_DIR
|
||||
mkdir -p $TEST_AIO_NGINX_LOGS_DIR
|
||||
|
||||
service rsyslog start
|
||||
service cron start
|
||||
service dnsmasq start
|
||||
|
@ -1,31 +0,0 @@
|
||||
# VM Setup Instructions
|
||||
|
||||
- Set up secrets (access tokens, passwords, etc)
|
||||
- Set up docker
|
||||
- Attach persistent disk
|
||||
- Build docker image (+ checkout repo)
|
||||
- Run image (+ setup for run on boot)
|
||||
|
||||
|
||||
## Build image
|
||||
- `<aio-builds-setup-dir>/build.sh [<name>[:<tag>] [--build-arg <NAME>=<value> ...]]`
|
||||
|
||||
|
||||
## Run image
|
||||
- `sudo docker run \
|
||||
-d \
|
||||
--dns 127.0.0.1 \
|
||||
--name <instance-name> \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
--restart unless-stopped \
|
||||
[-v <host-cert-dir>:/etc/ssl/localcerts:ro] \
|
||||
-v <host-secrets-dir>:/aio-secrets:ro \
|
||||
-v <host-builds-dir>:/var/www/aio-builds \
|
||||
<name>[:<tag>]
|
||||
`
|
||||
|
||||
|
||||
## Questions
|
||||
- Do we care to keep logs (e.g. cron, nginx, aio-upload-server, aio-clean-up, pm2) outside of the container?
|
||||
- Instead of creating new comments for each commit, update the original comment?
|
32
aio/aio-builds-setup/docs/_TOC.md
Normal file
32
aio/aio-builds-setup/docs/_TOC.md
Normal file
@ -0,0 +1,32 @@
|
||||
# VM Setup Instructions
|
||||
|
||||
|
||||
## Overview
|
||||
- [General overview](overview--general.md)
|
||||
- [Security model](overview--security-model.md)
|
||||
- [Available Commands](overview--scripts-and-commands.md)
|
||||
|
||||
|
||||
## Setting up the VM
|
||||
- [Set up secrets](vm-setup--set-up-secrets.md)
|
||||
- [Set up docker](vm-setup--set-up-docker.md)
|
||||
- [Attach persistent disk](vm-setup--attach-persistent-disk.md)
|
||||
- [Create host directories and files](vm-setup--create-host-dirs-and-files.md)
|
||||
- [Create docker image](vm-setup--create-docker-image.md)
|
||||
|
||||
|
||||
## Configuring the docker image
|
||||
- [Available environment variables](image-config--environment-variables.md)
|
||||
|
||||
|
||||
## Starting the docker container
|
||||
- [Start docker container](vm-setup--start-docker-container.md)
|
||||
|
||||
|
||||
## Updating the docker container
|
||||
- [Update docker container](vm-setup--update-docker-container.md)
|
||||
|
||||
|
||||
## Miscellaneous
|
||||
- [Debug docker container](misc--debug-docker-container.md)
|
||||
- [Integrate with CI](misc--integrate-with-ci.md)
|
@ -0,0 +1,52 @@
|
||||
# Image config - Environment variables
|
||||
|
||||
|
||||
Below is a list of environment variables that can be configured when creating the docker image (as
|
||||
described [here](vm-setup--create-docker-image.md)). An up-to-date list of the configurable
|
||||
environment variables and their default values can be found in the
|
||||
[Dockerfile](../dockerbuild/Dockerfile).
|
||||
|
||||
**Note:**
|
||||
Each variable has a `TEST_` prefixed counterpart, which is used for testing purposes. In most cases
|
||||
you don't need to specify values for those.
|
||||
|
||||
- `AIO_BUILDS_DIR`:
|
||||
The directory (inside the container) where the uploaded build artifacts are kept.
|
||||
|
||||
- `AIO_DOMAIN_NAME`:
|
||||
The domain name of the server.
|
||||
|
||||
- `AIO_GITHUB_ORGANIZATION`:
|
||||
The GitHub organization whose teams arew whitelisted for accepting uploads.
|
||||
See also `AIO_GITHUB_TEAM_SLUGS`.
|
||||
|
||||
- `AIO_GITHUB_TEAM_SLUGS`:
|
||||
A comma-separated list of teams, whose authors are allowed to upload PRs.
|
||||
See also `AIO_GITHUB_ORGANIZATION`.
|
||||
|
||||
- `AIO_NGINX_HOSTNAME`:
|
||||
The internal hostname for accessing the nginx server. This is mostly used for performing a
|
||||
periodic health-check.
|
||||
|
||||
- `AIO_NGINX_PORT_HTTP`:
|
||||
The port number on which nginx listens for HTTP connections. This should be mapped to the
|
||||
corresponding port on the host VM (as described [here](vm-setup--start-docker-container.md)).
|
||||
|
||||
- `AIO_NGINX_PORT_HTTPS`:
|
||||
The port number on which nginx listens for HTTPS connections. This should be mapped to the
|
||||
corresponding port on the host VM (as described [here](vm-setup--start-docker-container.md)).
|
||||
|
||||
- `AIO_REPO_SLUG`:
|
||||
The repository slug (in the form `<user>/<repo>`) for which PRs will be uploaded.
|
||||
|
||||
- `AIO_UPLOAD_HOSTNAME`:
|
||||
The internal hostname for accessing the Node.js upload-server. This is used by nginx for
|
||||
delegating upload requests and also for performing a periodic health-check.
|
||||
|
||||
- `AIO_UPLOAD_MAX_SIZE`:
|
||||
The maximum allowed size for the uploaded gzip archive containing the build artifacts. Files
|
||||
larger than this will be rejected.
|
||||
|
||||
- `AIO_UPLOAD_PORT`:
|
||||
The port number on which the Node.js upload-server listens for HTTP connections. This is used by
|
||||
nginx for delegating upload requests and also for performing a periodic health-check.
|
12
aio/aio-builds-setup/docs/misc--debug-docker-container.md
Normal file
12
aio/aio-builds-setup/docs/misc--debug-docker-container.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Miscellaneous - Debug docker container
|
||||
|
||||
|
||||
TODO (gkalpak): Add docs. Mention:
|
||||
- `aio-health-check`
|
||||
- `aio-verify-setup`
|
||||
- Test nginx accessible at:
|
||||
- `http://$TEST_AIO_NGINX_HOTNAME:$TEST_AIO_NGINX_PORT_HTTP`
|
||||
- `https://$TEST_AIO_NGINX_HOTNAME:$TEST_AIO_NGINX_PORT_HTTPS`
|
||||
- Test upload-server accessible at:
|
||||
- `http://$TEST_AIO_UPLOAD_HOTNAME:$TEST_AIO_UPLOAD_PORT`
|
||||
- Local DNS (via dnsmasq) maps the above hostnames to 127.0.0.1
|
12
aio/aio-builds-setup/docs/misc--integrate-with-ci.md
Normal file
12
aio/aio-builds-setup/docs/misc--integrate-with-ci.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Miscellaneous - Integrate with CI
|
||||
|
||||
|
||||
TODO (gkalpak): Add docs. Mention:
|
||||
- Travis' JWT addon (+ limitations).
|
||||
Relevant files: `.travis.yml`
|
||||
- Testing on CI.
|
||||
Relevant files: `ci/test-aio.sh`, `aio/aio-builds-setup/scripts/test.sh`
|
||||
- Preverifying on CI.
|
||||
Relevant files: `ci/deploy.sh`, `aio/aio-builds-setup/scripts/travis-preverify-pr.sh`
|
||||
- Deploying from CI.
|
||||
Relevant files: `ci/deploy.sh`, `aio/scripts/deploy-preview.sh`
|
84
aio/aio-builds-setup/docs/overview--general.md
Normal file
84
aio/aio-builds-setup/docs/overview--general.md
Normal file
@ -0,0 +1,84 @@
|
||||
# Overview - General
|
||||
|
||||
|
||||
## Objective
|
||||
Whenever a PR job is run on Travis, we want to build `angular.io` and upload the build artifacts to
|
||||
a publicly accessible server so that collaborators (developers, designers, authors, etc) can preview
|
||||
the changes without having to checkout and build the app locally.
|
||||
|
||||
|
||||
## Source code
|
||||
In order to make it easier to administer the server and version-control the setup, we are using
|
||||
[docker](https://www.docker.com) to run a container on a VM. The Dockerfile and all other files
|
||||
necessary for creating the docker container are stored (and versioned) along with the angular.io
|
||||
project's source code (currently part of the angular/angular repo) in the `aio-builds-setup/`
|
||||
directory.
|
||||
|
||||
|
||||
## Setup
|
||||
The VM is hosted on [Google Compute Engine](https://cloud.google.com/compute/). The host OS is
|
||||
debian:jessie. For more info how to set up the host VM take a look at the "Setting up the VM"
|
||||
section in [TOC](_TOC.md).
|
||||
|
||||
|
||||
## Security model
|
||||
Since we are managing a public server, it is important to take appropriate measures in order to
|
||||
prevent abuse. For more details on the challenges and the chosen approach take a look at the
|
||||
[security model](overview--security-model.md).
|
||||
|
||||
|
||||
## The 10000 feet view
|
||||
This section gives a brief summary of the several operations performed on CI and by the docker
|
||||
container:
|
||||
|
||||
|
||||
### On CI (Travis)
|
||||
- Build job completes successfully (i.e. build succeeds and tests pass).
|
||||
- The CI script checks whether the build job was initiated by a PR against the angular/angular
|
||||
master branch.
|
||||
- The CI script checks whether the PR has touched any files inside the angular.io project directory
|
||||
(currently `aio/`).
|
||||
- The CI script checks whether the author of the PR is a member of one of the whitelisted GitHub
|
||||
teams (and therefore allowed to upload).
|
||||
**Note:**
|
||||
For security reasons, the same checks will be performed on the server as well. This is an optional
|
||||
step with the purpose of:
|
||||
1. Avoiding the wasted overhead associated with uploads that are going to be rejected (e.g.
|
||||
building the artifacts, sending them to the server, running checks on the server, etc).
|
||||
2. Avoiding failing the build (due to an error response from the server) or requiring additional
|
||||
logic for detecting the reasons of the failure.
|
||||
- The CI script gzip and upload the build artifacts to the server.
|
||||
|
||||
More info on how to set things up on CI can be found [here](misc--integrate-with-ci.md).
|
||||
|
||||
|
||||
### Uploading build artifacts
|
||||
- nginx receives upload request.
|
||||
- nginx checks that the uploaded gzip archive does not exceed the specified max file size, stores it
|
||||
in a temporary location and passes the filepath to the Node.js upload-server.
|
||||
- The upload-server verifies that the uploaded file is not trying to overwrite an existing build,
|
||||
and runs several checks to determine whether the request should be accepted (more details can be
|
||||
found [here](overview--security-model.md)).
|
||||
- The upload-server deploys the artifacts to a sub-directory named after the PR number and SHA:
|
||||
`<PR>/<SHA>/`
|
||||
- The upload-server posts a comment on the corresponding PR on GitHub mentioning the SHA and the
|
||||
the link where the preview can be found.
|
||||
|
||||
|
||||
### Serving build artifacts
|
||||
- nginx receives a request for an uploaded resource on a subdomain corresponding to the PR and SHA.
|
||||
E.g.: `pr<PR>-<SHA>.ngbuilds.io/path/to/resource`
|
||||
- nginx maps the subdomain to the correct sub-directory and serves the resource.
|
||||
E.g.: `/<PR>/<SHA>/path/to/resource`
|
||||
|
||||
|
||||
### Removing obsolete artifacts
|
||||
In order to avoid flooding the disk with unnecessary build artifacts, there is a cronjob that runs a
|
||||
clean-up tasks once a day. The task retrieves all open PRs from GitHub and removes all directories
|
||||
that do not correspond with an open PR.
|
||||
|
||||
|
||||
### Health-check
|
||||
The docker service runs a periodic health-check that verifies the running conditions of the
|
||||
container. This includes verifying the status of specific system services, the responsiveness of
|
||||
nginx and the upload-server and internet connectivity.
|
58
aio/aio-builds-setup/docs/overview--scripts-and-commands.md
Normal file
58
aio/aio-builds-setup/docs/overview--scripts-and-commands.md
Normal file
@ -0,0 +1,58 @@
|
||||
# Overview - Scripts and Commands
|
||||
|
||||
|
||||
This is an overview of the available scripts and commands.
|
||||
|
||||
|
||||
## Scripts
|
||||
The scripts are located inside `<aio-builds-setup-dir>/scripts/`. The following scripts are
|
||||
available:
|
||||
|
||||
- `create-image.sh`:
|
||||
Can be used for creating a preconfigured docker image.
|
||||
See [here](vm-setup--create-docker-image.md) for more info.
|
||||
|
||||
- `test.sh`
|
||||
Can be used for running the tests for `<aio-builds-setup-dir>/dockerbuild/scripts-js/`. This is
|
||||
useful for CI integration. See [here](misc--integrate-with-ci.md) for more info.
|
||||
|
||||
- `travis-preverify-pr.sh`
|
||||
Can be used for "preverifying" a PR before uploading the artifacts to the server. It checks that
|
||||
the author of the PR is a member of one of the specified GitHub teams and therefore allowed to
|
||||
upload build artifacts. This is useful for CI integration. See [here](misc--integrate-with-ci.md)
|
||||
for more info.
|
||||
|
||||
- `update-preview-server.sh`
|
||||
Can be used for updating the docker container (and image) based on the latest changes checked out
|
||||
from a git repository. See [here](vm-setup--update-docker-container.md) for more info.
|
||||
|
||||
## Commands
|
||||
The following commands are available globally from inside the docker container. They are either used
|
||||
by the container to perform its various operations or can be used ad-hoc, mainly for testing
|
||||
purposes. Each command is backed by a corresponding script inside
|
||||
`<aio-builds-setup-dir>/dockerbuild/scripts-sh/`.
|
||||
|
||||
- `aio-clean-up`:
|
||||
Cleans up the builds directory by removing the artifacts that do not correspond to an open PR.
|
||||
_It is run as a daily cronjob._
|
||||
|
||||
- `aio-health-check`:
|
||||
Runs a basic health-check, verifying that the necessary services are running, the servers are
|
||||
responding and there is a working internet connection.
|
||||
_It is used periodically by docker for determining the container's health status._
|
||||
|
||||
- `aio-init`:
|
||||
Initializes the container (mainly by starting the necessary services).
|
||||
_It is run (by default) when starting the container._
|
||||
|
||||
- `aio-upload-server-prod`:
|
||||
Spins up a Node.js upload-server instance.
|
||||
_It is used in `aio-init` (see above) during initialization._
|
||||
|
||||
- `aio-upload-server-test`:
|
||||
Spins up a Node.js upload-server instance for tests.
|
||||
_It is used in `aio-verify-setup` (see below) for running tests._
|
||||
|
||||
- `aio-verify-setup`:
|
||||
Runs a suite of e2e-like tests, mainly verifying the correct (inter)operation of nginx and the
|
||||
Node.js upload-server.
|
116
aio/aio-builds-setup/docs/overview--security-model.md
Normal file
116
aio/aio-builds-setup/docs/overview--security-model.md
Normal file
@ -0,0 +1,116 @@
|
||||
# Overview - Security model
|
||||
|
||||
|
||||
Whenever a PR job is run on Travis, we want to build `angular.io` and upload the build artifacts to
|
||||
a publicly accessible server so that collaborators (developers, designers, authors, etc) can preview
|
||||
the changes without having to checkout and build the app locally.
|
||||
|
||||
This document discusses the security considerations associated with uploading build artifacts as
|
||||
part of the CI setup and serving them publicly.
|
||||
|
||||
|
||||
## Security objectives
|
||||
|
||||
- **Prevent uploading arbitrary content to our servers.**
|
||||
Since there is no restriction on who can submit a PR, we cannot allow any PR's build artifacts to
|
||||
be uploaded.
|
||||
|
||||
- **Prevent overwriting other peoples uploaded content.**
|
||||
There needs to be a mechanism in place to ensure that the uploaded content does indeed correspond
|
||||
to the PR indicated by its URL.
|
||||
|
||||
- **Prevent arbitrary access on the server.**
|
||||
Since the PR author has full access over the build artifacts that would be uploaded, we must
|
||||
ensure that the uploaded files will not enable arbitrary access to the server or expose sensitive
|
||||
info.
|
||||
|
||||
|
||||
## Issues / Caveats
|
||||
|
||||
- Because the PR author can change the scripts run on CI, any security mechanisms must be immune to
|
||||
such changes.
|
||||
|
||||
- For security reasons, encrypted Travis variables are not available to PRs, so we can't rely on
|
||||
them to implement security.
|
||||
|
||||
|
||||
## Implemented approach
|
||||
|
||||
|
||||
### In a nutshell
|
||||
The implemented approach can be broken up to the following sub-tasks:
|
||||
|
||||
1. Verify which PR the uploaded artifacts correspond to.
|
||||
2. Determine the author of the PR.
|
||||
3. Check whether the PR author is a member of some whitelisted GitHub team.
|
||||
4. Deploy the artifacts to the corresponding PR's directory.
|
||||
5. Prevent overwriting previously deployed artifacts (which ensures that the guarantees established
|
||||
during deployment will remain valid until the artifacts are removed).
|
||||
6. Prevent uploaded files from accessing anything outside their directory.
|
||||
|
||||
|
||||
### Implementation details
|
||||
This section describes how each of the aforementioned sub-tasks is accomplished:
|
||||
|
||||
1. **Verify which PR the uploaded artifacts correspond to.**
|
||||
|
||||
We are taking advantage of Travis' [JWT addon](https://docs.travis-ci.com/user/jwt). By sharing
|
||||
a secret between Travis (which keeps it private but uses it to sign a JWT) and the server (which
|
||||
uses it to verify the authenticity of the JWT), we can accomplish the following:
|
||||
a. Verify that the upload request comes from Travis.
|
||||
b. Determine the PR that these artifacts correspond to (since Travis puts that information into
|
||||
the JWT, without the PR author being able to modify it).
|
||||
|
||||
_Note:_
|
||||
_There are currently certain limitation in the implementation of the JWT addon._
|
||||
_See the next section for more details._
|
||||
|
||||
2. **Determine the author of the PR.**
|
||||
|
||||
Once we have securely associated the uploaded artifaacts to a PR, we retrieve the PR's metadata -
|
||||
including the author's username - using the [GitHub API](https://developer.github.com/v3/).
|
||||
To avoid rate-limit restrictions, we use a Personal Access Token (issued by
|
||||
[@mary-poppins](https://github.com/mary-poppins)).
|
||||
|
||||
3. **Check whether the PR author is a member of some whitelisted GitHub team.**
|
||||
|
||||
Again using the GitHub API, we can verify the author's membership in one of the
|
||||
whitelisted/trusted GitHub teams. For this operation, we need a PErsonal Access Token with the
|
||||
`read:org` scope issued by a user that can "see" the specified GitHub organization.
|
||||
Here too, we use token by @mary-poppins.
|
||||
|
||||
4. **Deploy the artifacts to the corresponding PR's directory.**
|
||||
|
||||
With the preceeding steps, we have verified that the uploaded artifacts have been uploaded by
|
||||
Travis and correspond to a PR whose author is a member of a trusted team. Essentially, as long as
|
||||
sub-tasks 1, 2 and 3 can be securely accomplished, it is possible to "project" the trust we have
|
||||
in a team's members through the PR and Travis to the build artifacts.
|
||||
|
||||
5. **Prevent overwriting previously deployed artifacts**.
|
||||
|
||||
In order to enforce this restriction (and ensure that the deployed artifacts validity is
|
||||
preserved throughout their "lifetime"), the server that handles the upload (currently a Node.js
|
||||
Express server) rejects uploads that target an existing directory.
|
||||
_Note: A PR can contain multiple uploads; one for each SHA that was built on Travis._
|
||||
|
||||
6. **Prevent uploaded files from accessing anything outside their directory.**
|
||||
|
||||
Nginx (which is used to serve the uploaded artifacts) has been configured to not follow symlinks
|
||||
outside of the directory where the build artifacts are stored.
|
||||
|
||||
|
||||
## Assumptions / Things to keep in mind
|
||||
|
||||
- Each trusted PR author has full control over the content that is uploaded for their PRs. Part of
|
||||
the security model relies on the trustworthiness of these authors.
|
||||
|
||||
- If anyone gets access to the `PREVIEW_DEPLOYMENT_TOKEN` (a.k.a. `NGBUILDS_IO_KEY` on
|
||||
angular/angular) variable generated for each Travis job, they will be able to impersonate the
|
||||
corresponding PR's author on the preview server for as long as the token is valid (currently 90
|
||||
mins). Because of this, the value of the `PREVIEW_DEPLOYMENT_TOKEN` should not be made publicly
|
||||
accessible (e.g. by printing it on the Travis job log).
|
||||
|
||||
- Travis does only allow specific whitelisted property names to be used with the JWT addon. The only
|
||||
known such property at the time is `SAUCE_ACCESS_KEY` (used for integration with SauceLabs). In
|
||||
order to be able to actually use the JWT addon we had to name the encrypted variable
|
||||
`SAUCE_ACCESS_KEY` (which we later re-assign to `NGBUILDS_IO_KEY`).
|
@ -13,5 +13,8 @@
|
||||
|
||||
|
||||
## Mount disk on boot
|
||||
- ``echo UUID=`sudo blkid -s UUID -o value /dev/disk/by-id/google-aio-builds` \
|
||||
/mnt/disks/aio-builds ext4 discard,defaults,nofail 0 2 | sudo tee -a /etc/fstab``
|
||||
- Run:
|
||||
```
|
||||
echo UUID=`sudo blkid -s UUID -o value /dev/disk/by-id/google-aio-builds` \
|
||||
/mnt/disks/aio-builds ext4 discard,defaults,nofail 0 2 | sudo tee -a /etc/fstab
|
||||
```
|
32
aio/aio-builds-setup/docs/vm-setup--create-docker-image.md
Normal file
32
aio/aio-builds-setup/docs/vm-setup--create-docker-image.md
Normal file
@ -0,0 +1,32 @@
|
||||
# VM setup - Create docker image
|
||||
|
||||
|
||||
## Checkout repository
|
||||
- `git clone <repo-url>`
|
||||
|
||||
|
||||
## Build docker image
|
||||
- `<aio-builds-setup-dir>/scripts/create-image.sh [<name>[:<tag>] [--build-arg <NAME>=<value> ...]]`
|
||||
- You can overwrite the default environment variables inside the image, by passing new values using
|
||||
`--build-arg`.
|
||||
|
||||
**Note:** The script has to execute docker commands with `sudo`.
|
||||
|
||||
|
||||
## Example
|
||||
The following commands would create a docker image from GitHub repo `foo/bar` to be deployed on the
|
||||
`foobar-builds.io` domain and accepting PR deployments from authors that are members of the
|
||||
`bar-core` and `bar-docs-authors` teams of organization `foo`:
|
||||
|
||||
- `git clone https://github.com/foo/bar.git foobar`
|
||||
- Run:
|
||||
```
|
||||
./foobar/aio-builds-setup/scripts/build.sh foobar-builds \
|
||||
--build-arg AIO_REPO_SLUG=foo/bar \
|
||||
--build-arg AIO_DOMAIN_NAME=foobar-builds.io \
|
||||
--build-arg AIO_GITHUB_ORGANIZATION=foo \
|
||||
--build-arg AIO_GITHUB_TEMA_SLUGS=bar-core,bar-docs-authors
|
||||
```
|
||||
|
||||
A full list of the available environment variables can be found
|
||||
[here](image-config--environment-variables.md).
|
@ -0,0 +1,75 @@
|
||||
# VM setup - Create host directories and files
|
||||
|
||||
|
||||
## Create directory with secrets
|
||||
For security reasons, sensitive info (such as tokens and passwords) are not hardcoded into the
|
||||
docker image, nor passed as environment variables at runtime. They are passed to the docker
|
||||
container from the host VM as files inside a directory. Each file's name is the name of the variable
|
||||
and the file content is the value. These are read from inside the running container when necessary.
|
||||
|
||||
More info on how to create `secrets` directory and files can be found
|
||||
[here](vm-setup--set-up-secrets.md).
|
||||
|
||||
|
||||
## Create directory for build artifacts
|
||||
The uploaded build artifacts should be kept on a directory outside the docker container, so it is
|
||||
easier to replace the container without losing the uploaded builds. For portability across VMs a
|
||||
persistent disk can be used (as described [here](vm-setup--attach-persistent-disk.md)).
|
||||
|
||||
**Note:** The directories created inside that directory will be owned by user `www-data`.
|
||||
|
||||
|
||||
## Create SSL certificates (Optional for dev)
|
||||
The host VM can attach a directory containing the SSL certificate and key to be used by the nginx
|
||||
server for serving the uploaded build artifacts. More info on how to attach the directory when
|
||||
starting the container can be found [here](vm-setup--start-docker-container.md).
|
||||
|
||||
In order for the container to be able to find the certificate and key, they should be named
|
||||
`<DOMAIN_NAME>.crt` and `<DOMAIN_NAME>.key` respectively. For example, for a domain name
|
||||
`ngbuild.io`, nginx will look for files `ngbuilds.io.crt` and `ngbuilds.io.key`. More info on how to
|
||||
specify the domain name see [here](vm-setup--create-docker-image.md).
|
||||
|
||||
If no directory is attached, nginx will use an internal self-signed certificate. This is convenient
|
||||
during development, but is not suitable for production.
|
||||
|
||||
**Note:**
|
||||
Since nginx needs to be able to serve requests for both the main domain as well as any subdomain
|
||||
(e.g. `ngbuilds.io/` and `foo-bar.ngbuilds.io/`), the provided certificate needs to be a wildcard
|
||||
certificate covering both the domain and subdomains.
|
||||
|
||||
|
||||
## Create directory for logs (Optional)
|
||||
Optionally, a logs directory can pe passed to the docker container for storing non-system-related
|
||||
logs. If not provided, the logs are kept locally on the container and will be lost whenever the
|
||||
container is replaced (e.g. when updating to use a newer version of the docker image). Log files are
|
||||
rotated and retained for 6 months.
|
||||
|
||||
The following log files are kept in this directory:
|
||||
|
||||
- `clean-up.log`:
|
||||
Output of the `aio-clean-up` command, run as a cronjob for cleaning up the build artifacts of
|
||||
closed PRs.
|
||||
|
||||
- `init.log`:
|
||||
Output of the `aio-init` command, run (by default) when starting the container.
|
||||
|
||||
- `nginx/{access,error}.log`:
|
||||
The access and error logs produced by the nginx server while serving "production" files.
|
||||
|
||||
- `nginx-test/{access,error}.log`:
|
||||
The access and error logs produced by the nginx server while serving "test" files. This is only
|
||||
used when running tests locally from inside the container, e.g. with the `aio-verify-setup`
|
||||
command. (See [here](overview--scripts-and-commands.md) for more info.)
|
||||
|
||||
- `upload-server-{prod,test,verify-setup}-*.log`:
|
||||
The logs produced by the Node.js upload-server while serving either:
|
||||
- `-prod`: "Production" files (g.g during normal operation).
|
||||
- `-test`: "Test" files (e.g. when a test instance is started with the `aio-upload-server-test`
|
||||
command).
|
||||
- `-verify-setup`: "Test" files, but while running `aio-verify-setup`.
|
||||
|
||||
(See [here](overview--scripts-and-commands.md) for more info the commands mentioned above.)
|
||||
|
||||
- `verify-setup.log`:
|
||||
The output of the `aio-verify-setup` command (e.g. Jasmine output), except for upload-server
|
||||
output which is logged to `upload-server-verify-setup-*.log` (see above).
|
@ -0,0 +1,92 @@
|
||||
# VM setup - Start docker container
|
||||
|
||||
|
||||
## The `docker run` command
|
||||
Once everything has been setup and configured, a docker container can be started with the following
|
||||
command:
|
||||
|
||||
```
|
||||
sudo docker run \
|
||||
--detach \
|
||||
--dns 127.0.0.1 \
|
||||
--name <instance-name> \
|
||||
--publish 80:80 \
|
||||
--publish 443:443 \
|
||||
--restart unless-stopped \
|
||||
[--volume <host-cert-dir>:/etc/ssl/localcerts:ro] \
|
||||
--volume <host-secrets-dir>:/aio-secrets:ro \
|
||||
--volume <host-builds-dir>:/var/www/aio-builds \
|
||||
[--volume <host-logs-dir>:/var/log/aio] \
|
||||
<name>[:<tag>]
|
||||
```
|
||||
|
||||
Below is the same command with inline comments explaining each option. The aPI docs for `docker run`
|
||||
can be found [here](https://docs.docker.com/engine/reference/run/).
|
||||
|
||||
```
|
||||
sudo docker run \
|
||||
|
||||
# Start as a daemon.
|
||||
--detach \
|
||||
|
||||
# Use the local DNS server.
|
||||
# (This is necessary for mapping internal URLs, e.g. for the Node.js upload-server.)
|
||||
--dns 127.0.0.1 \
|
||||
|
||||
# USe `<instance-name>` as an alias for the container.
|
||||
# Useful for running `docker` commands, e.g.: `docker stop <instance-name>`
|
||||
--name <instance-name> \
|
||||
|
||||
# Map ports of the host VM (left) to ports of the docker container (right)
|
||||
--publish 80:80 \
|
||||
--publish 443:443 \
|
||||
|
||||
# Automatically restart the container (unless it was explicitly stopped by the user).
|
||||
# (This ensures that the container will be automatically started on boot.)
|
||||
--restart unless-stopped \
|
||||
|
||||
# The directory the contains the SSL certificates.
|
||||
# (See [here](vm-setup--create-host-dirs-and-files.md) for more info.)
|
||||
# If not provided, the container will use self-signed certificates.
|
||||
[--volume <host-cert-dir>:/etc/ssl/localcerts:ro] \
|
||||
|
||||
# The directory the contains the secrets (e.g. GitHub token, JWT secret, etc).
|
||||
# (See [here](vm-setup--set-up-secrets.md) for more info.)
|
||||
--volume <host-secrets-dir>:/aio-secrets:ro \
|
||||
|
||||
# The uploaded build artifacts will stored to and served from this directory.
|
||||
# (If you are using a persistent disk - as described [here](vm-setup--attach-persistent-disk.md) -
|
||||
# this will be a directory inside the disk.)
|
||||
--volume <host-builds-dir>:/var/www/aio-builds \
|
||||
|
||||
# The directory where the logs are being kept.
|
||||
# (See [here](vm-setup--create-host-dirs-and-files.md) for more info.)
|
||||
# If not provided, the logs will be kept inside the container, which means they will be lost
|
||||
# whenever a new container is created.
|
||||
[--volume <host-logs-dir>:/var/log/aio] \
|
||||
|
||||
# The name of the docker image to use (and an optional tag; defaults to `latest`).
|
||||
# (See [here](vm-setup--create-docker-image.md) for instructions on how to create the iamge.)
|
||||
<name>[:<tag>]
|
||||
```
|
||||
|
||||
|
||||
## Example
|
||||
The following command would start a docker container based on the previously created `foobar-builds`
|
||||
docker image, alias it as 'foobar-builds-1' and map predefined directories on the host VM to be used
|
||||
by the container for accesing secrets and SSL certificates and keeping the build artifacts and logs.
|
||||
|
||||
```
|
||||
sudo docker run \
|
||||
--detach \
|
||||
--dns 127.0.0.1 \
|
||||
--name foobar-builds-1 \
|
||||
--publish 80:80 \
|
||||
--publish 443:443 \
|
||||
--restart unless-stopped \
|
||||
--volume /etc/ssl/localcerts:/etc/ssl/localcerts:ro \
|
||||
--volume /foobar-secrets:/aio-secrets:ro \
|
||||
--volume /mnt/disks/foobar-builds:/var/www/aio-builds \
|
||||
--volume /foobar-logs:/var/log/aio \
|
||||
foobar-builds
|
||||
```
|
@ -0,0 +1,52 @@
|
||||
# VM setup - Update docker container
|
||||
|
||||
|
||||
## Overview
|
||||
Assuming you have cloned the repository containing the preview server code (as described
|
||||
[here](vm-setup--create-docker-image.md)), you can use the `update-preview-server.sh` script on the
|
||||
VM host to update the preview server based on changes in the source code.
|
||||
|
||||
The script will pull the latest changes from the origin's master branch and examine if there have
|
||||
been any changes in files inside the preview server source code directory (see below). If there are,
|
||||
it will create a new image and verify that is works as expected. Finally, it will stop and remove
|
||||
the old docker container and image, create and new container based on the new image and start it.
|
||||
|
||||
The script assumes that the preview server source code is in the repository's
|
||||
`aio/aio-builds-setup/` directory and expects the following inputs:
|
||||
|
||||
- **$1**: `HOST_REPO_DIR`
|
||||
- **$2**: `HOST_LOCALCERTS_DIR`
|
||||
- **$3**: `HOST_SECRETS_DIR`
|
||||
- **$4**: `HOST_BUILDS_DIR`
|
||||
- **$5**: `HOST_LOGS_DIR`
|
||||
|
||||
See [here](vm-setup--create-host-dirs-and-files.md) for more info on what each input directory is
|
||||
used for.
|
||||
|
||||
**Note 1:** The script has to execute docker commands with `sudo`.
|
||||
|
||||
**Note 2:** Make sure the user that executes the script has access to update the repository
|
||||
|
||||
|
||||
## Run the script manually
|
||||
You may choose to manually run the script, when necessary. Example:
|
||||
|
||||
```
|
||||
update-preview-server.sh \
|
||||
/path/to/repo \
|
||||
/path/to/localcerts \
|
||||
/path/to/secrets \
|
||||
/path/to/builds \
|
||||
/path/to/logs
|
||||
```
|
||||
|
||||
|
||||
## Run the script automatically
|
||||
You may choose to automatically trigger the script, e.g. using a cronjob. For example, the following
|
||||
cronjob entry would run the script every hour and update the preview server (assuming the user has
|
||||
the necessary permissions):
|
||||
|
||||
```
|
||||
# Periodically check for changes and update the preview server (if necessary)
|
||||
*/30 * * * * /path/to/update-preview-server.sh /path/to/repo /path/to/localcerts /path/to/secrets /path/to/builds /path/to/logs
|
||||
```
|
@ -2,15 +2,9 @@
|
||||
set -eux -o pipefail
|
||||
|
||||
# Set up env
|
||||
source "`dirname $0`/env.sh"
|
||||
source "`dirname $0`/_env.sh"
|
||||
readonly defaultImageNameAndTag="aio-builds:latest"
|
||||
|
||||
# Build `scripts-js/`
|
||||
cd "$SCRIPTS_JS_DIR"
|
||||
yarn install
|
||||
yarn run build
|
||||
cd -
|
||||
|
||||
# Create docker image
|
||||
readonly nameAndOptionalTag=${1:-$defaultImageNameAndTag}
|
||||
sudo docker build --tag $nameAndOptionalTag ${@:2} $DOCKERBUILD_DIR
|
@ -2,10 +2,11 @@
|
||||
set -eux -o pipefail
|
||||
|
||||
# Set up env
|
||||
source "`dirname $0`/env.sh"
|
||||
source "`dirname $0`/_env.sh"
|
||||
|
||||
# Test `scripts-js/`
|
||||
cd "$SCRIPTS_JS_DIR"
|
||||
yarn install
|
||||
yarn test
|
||||
cd -
|
||||
(
|
||||
cd "$SCRIPTS_JS_DIR"
|
||||
yarn install
|
||||
yarn test
|
||||
)
|
||||
|
@ -2,11 +2,18 @@
|
||||
set -eux -o pipefail
|
||||
|
||||
# Set up env
|
||||
source "`dirname $0`/env.sh"
|
||||
source "`dirname $0`/_env.sh"
|
||||
|
||||
# Build `scripts-js/`
|
||||
(
|
||||
cd "$SCRIPTS_JS_DIR"
|
||||
yarn install
|
||||
yarn build
|
||||
)
|
||||
|
||||
# Preverify PR
|
||||
AIO_GITHUB_ORGANIZATION="angular" \
|
||||
AIO_GITHUB_TEAM_SLUGS="angular-core" \
|
||||
AIO_GITHUB_TEAM_SLUGS="angular-core,aio-contributors" \
|
||||
AIO_GITHUB_TOKEN=$(echo ${GITHUB_TEAM_MEMBERSHIP_CHECK_KEY} | rev) \
|
||||
AIO_REPO_SLUG=$TRAVIS_REPO_SLUG \
|
||||
AIO_PREVERIFY_PR=$TRAVIS_PULL_REQUEST \
|
||||
|
70
aio/aio-builds-setup/scripts/update-preview-server.sh
Executable file
70
aio/aio-builds-setup/scripts/update-preview-server.sh
Executable file
@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eux -o pipefail
|
||||
exec 3>&1
|
||||
|
||||
echo "[`date`] - Updating the preview server..."
|
||||
|
||||
# Input
|
||||
readonly HOST_REPO_DIR=$1
|
||||
readonly HOST_LOCALCERTS_DIR=$2
|
||||
readonly HOST_SECRETS_DIR=$3
|
||||
readonly HOST_BUILDS_DIR=$4
|
||||
readonly HOST_LOGS_DIR=$5
|
||||
|
||||
# Constants
|
||||
readonly PROVISIONAL_IMAGE_NAME=aio-builds:provisional
|
||||
readonly LATEST_IMAGE_NAME=aio-builds:latest
|
||||
readonly CONTAINER_NAME=aio
|
||||
|
||||
# Run
|
||||
(
|
||||
cd "$HOST_REPO_DIR"
|
||||
|
||||
readonly lastDeployedCommit=$(git rev-parse HEAD)
|
||||
echo "Currently at commit $lastDeployedCommit."
|
||||
|
||||
# Pull latest master from origin.
|
||||
git pull origin master
|
||||
|
||||
# Do not update the server unless files inside `aio-builds-setup/` have changed
|
||||
# or the last attempt failed (identified by the provisional image still being around).
|
||||
readonly relevantChangedFilesCount=$(git diff --name-only $lastDeployedCommit...HEAD | grep -P "^aio/aio-builds-setup/" | wc -l)
|
||||
readonly lastAttemptFailed=$(sudo docker rmi "$PROVISIONAL_IMAGE_NAME" >> /dev/fd/3 && echo "true" || echo "false")
|
||||
if [[ $relevantChangedFilesCount -eq 0 ]] && [[ "$lastAttemptFailed" != "true" ]]; then
|
||||
echo "Skipping update because no relevant files have been touched."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create and verify a new docker image.
|
||||
aio/aio-builds-setup/scripts/create-image.sh "$PROVISIONAL_IMAGE_NAME"
|
||||
readonly imageVerified=$(sudo docker run --dns 127.0.0.1 --rm --volume $HOST_SECRETS_DIR:/aio-secrets:ro "$PROVISIONAL_IMAGE_NAME" /bin/bash -c "aio-init && aio-health-check && aio-verify-setup" >> /dev/fd/3 && echo "true" || echo "false")
|
||||
|
||||
if [[ "$imageVerified" != "true" ]]; then
|
||||
echo "Failed to verify new docker image. Aborting update!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove the old container and replace the docker image.
|
||||
sudo docker stop "$CONTAINER_NAME" || true
|
||||
sudo docker rm "$CONTAINER_NAME" || true
|
||||
sudo docker rmi "$LATEST_IMAGE_NAME" || true
|
||||
sudo docker tag "$PROVISIONAL_IMAGE_NAME" "$LATEST_IMAGE_NAME"
|
||||
sudo docker rmi "$PROVISIONAL_IMAGE_NAME"
|
||||
|
||||
# Create and start a docker container based on the new image.
|
||||
sudo docker run \
|
||||
--detach \
|
||||
--dns 127.0.0.1 \
|
||||
--name "$CONTAINER_NAME" \
|
||||
--publish 80:80 \
|
||||
--publish 443:443 \
|
||||
--restart unless-stopped \
|
||||
--volume $HOST_LOCALCERTS_DIR:/etc/ssl/localcerts:ro \
|
||||
--volume $HOST_SECRETS_DIR:/aio-secrets:ro \
|
||||
--volume $HOST_BUILDS_DIR:/var/www/aio-builds \
|
||||
--volume $HOST_LOGS_DIR:/var/log/aio \
|
||||
"$LATEST_IMAGE_NAME"
|
||||
|
||||
echo "The new docker image has been successfully deployed."
|
||||
)
|
@ -1,18 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Bootstrapping
|
||||
@cheatsheetIndex 0
|
||||
@description
|
||||
{@target ts}`import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';`{@endtarget}
|
||||
{@target js}Available from the `ng.platformBrowserDynamic` namespace{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`platformBrowserDynamic().bootstrapModule(AppModule);`|`platformBrowserDynamic().bootstrapModule`
|
||||
syntax(js):
|
||||
`document.addEventListener('DOMContentLoaded', function() {
|
||||
ng.platformBrowserDynamic
|
||||
.platformBrowserDynamic()
|
||||
.bootstrapModule(app.AppModule);
|
||||
});`|`platformBrowserDynamic().bootstrapModule`
|
||||
description:
|
||||
Bootstraps the app, using the root component from the specified `NgModule`. {@target js}Must be wrapped in the event listener to fire when the page loads.{@endtarget}
|
@ -1,34 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Built-in directives
|
||||
@cheatsheetIndex 3
|
||||
@description
|
||||
{@target ts}`import { CommonModule } from '@angular/common';`{@endtarget}
|
||||
{@target js}Available using the `ng.common.CommonModule` module{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<section *ngIf="showSection">`|`*ngIf`
|
||||
description:
|
||||
Removes or recreates a portion of the DOM tree based on the `showSection` expression.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<li *ngFor="let item of list">`|`*ngFor`
|
||||
description:
|
||||
Turns the li element and its contents into a template, and uses that to instantiate a view for each item in list.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<div [ngSwitch]="conditionExpression">
|
||||
<ng-template [ngSwitchCase]="case1Exp">...</ng-template>
|
||||
<ng-template ngSwitchCase="case2LiteralString">...</ng-template>
|
||||
<ng-template ngSwitchDefault>...</ng-template>
|
||||
</div>`|`[ngSwitch]`|`[ngSwitchCase]`|`ngSwitchCase`|`ngSwitchDefault`
|
||||
description:
|
||||
Conditionally swaps the contents of the div by selecting one of the embedded templates based on the current value of `conditionExpression`.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<div [ngClass]="{active: isActive, disabled: isDisabled}">`|`[ngClass]`
|
||||
description:
|
||||
Binds the presence of CSS classes on the element to the truthiness of the associated map values. The right-hand expression should return {class-name: true/false} map.
|
@ -1,49 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Class decorators
|
||||
@cheatsheetIndex 5
|
||||
@description
|
||||
{@target ts}`import { Directive, ... } from '@angular/core';`{@endtarget}
|
||||
{@target js}Available from the `ng.core` namespace{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@Component({...})
|
||||
class MyComponent() {}`|`@Component({...})`
|
||||
syntax(js):
|
||||
`var MyComponent = ng.core.Component({...}).Class({...})`|`ng.core.Component({...})`
|
||||
description:
|
||||
Declares that a class is a component and provides metadata about the component.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@Directive({...})
|
||||
class MyDirective() {}`|`@Directive({...})`
|
||||
syntax(js):
|
||||
`var MyDirective = ng.core.Directive({...}).Class({...})`|`ng.core.Directive({...})`
|
||||
description:
|
||||
Declares that a class is a directive and provides metadata about the directive.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@Pipe({...})
|
||||
class MyPipe() {}`|`@Pipe({...})`
|
||||
syntax(js):
|
||||
`var MyPipe = ng.core.Pipe({...}).Class({...})`|`ng.core.Pipe({...})`
|
||||
description:
|
||||
Declares that a class is a pipe and provides metadata about the pipe.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@Injectable()
|
||||
class MyService() {}`|`@Injectable()`
|
||||
syntax(js):
|
||||
`var OtherService = ng.core.Class(
|
||||
{constructor: function() { }});
|
||||
var MyService = ng.core.Class(
|
||||
{constructor: [OtherService, function(otherService) { }]});`|`var MyService = ng.core.Class({constructor: [OtherService, function(otherService) { }]});`
|
||||
description:
|
||||
{@target ts}Declares that a class has dependencies that should be injected into the constructor when the dependency injector is creating an instance of this class.
|
||||
{@endtarget}
|
||||
{@target js}
|
||||
Declares a service to inject into a class by providing an array with the services, with the final item being the function to receive the injected services.
|
||||
{@endtarget}
|
@ -1,38 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Component configuration
|
||||
@cheatsheetIndex 7
|
||||
@description
|
||||
{@target js}`ng.core.Component` extends `ng.core.Directive`,
|
||||
so the `ng.core.Directive` configuration applies to components as well{@endtarget}
|
||||
{@target ts}`@Component` extends `@Directive`,
|
||||
so the `@Directive` configuration applies to components as well{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`moduleId: module.id`|`moduleId:`
|
||||
description:
|
||||
If set, the `templateUrl` and `styleUrl` are resolved relative to the component.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`viewProviders: [MyService, { provide: ... }]`|`viewProviders:`
|
||||
syntax(js):
|
||||
`viewProviders: [MyService, { provide: ... }]`|`viewProviders:`
|
||||
description:
|
||||
List of dependency injection providers scoped to this component's view.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`template: 'Hello {{name}}'
|
||||
templateUrl: 'my-component.html'`|`template:`|`templateUrl:`
|
||||
description:
|
||||
Inline template or external template URL of the component's view.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`styles: ['.primary {color: red}']
|
||||
styleUrls: ['my-component.css']`|`styles:`|`styleUrls:`
|
||||
description:
|
||||
List of inline CSS styles or external stylesheet URLs for styling the component’s view.
|
@ -1,30 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Dependency injection configuration
|
||||
@cheatsheetIndex 10
|
||||
@description
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`{ provide: MyService, useClass: MyMockService }`|`provide`|`useClass`
|
||||
syntax(js):
|
||||
`{ provide: MyService, useClass: MyMockService }`|`provide`|`useClass`
|
||||
description:
|
||||
Sets or overrides the provider for `MyService` to the `MyMockService` class.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`{ provide: MyService, useFactory: myFactory }`|`provide`|`useFactory`
|
||||
syntax(js):
|
||||
`{ provide: MyService, useFactory: myFactory }`|`provide`|`useFactory`
|
||||
description:
|
||||
Sets or overrides the provider for `MyService` to the `myFactory` factory function.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`{ provide: MyValue, useValue: 41 }`|`provide`|`useValue`
|
||||
syntax(js):
|
||||
`{ provide: MyValue, useValue: 41 }`|`provide`|`useValue`
|
||||
description:
|
||||
Sets or overrides the provider for `MyValue` to the value `41`.
|
@ -1,86 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Class field decorators for directives and components
|
||||
@cheatsheetIndex 8
|
||||
@description
|
||||
{@target ts}`import { Input, ... } from '@angular/core';`{@endtarget}
|
||||
{@target js}Available from the `ng.core` namespace{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@Input() myProperty;`|`@Input()`
|
||||
syntax(js):
|
||||
`ng.core.Input(myProperty, myComponent);`|`ng.core.Input(`|`);`
|
||||
description:
|
||||
Declares an input property that you can update via property binding (example:
|
||||
`<my-cmp [myProperty]="someExpression">`).
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@Output() myEvent = new EventEmitter();`|`@Output()`
|
||||
syntax(js):
|
||||
`myEvent = new ng.core.EventEmitter();
|
||||
ng.core.Output(myEvent, myComponent);`|`ng.core.Output(`|`);`
|
||||
description:
|
||||
Declares an output property that fires events that you can subscribe to with an event binding (example: `<my-cmp (myEvent)="doSomething()">`).
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@HostBinding('class.valid') isValid;`|`@HostBinding('class.valid')`
|
||||
syntax(js):
|
||||
`ng.core.HostBinding('class.valid',
|
||||
'isValid', myComponent);`|`ng.core.HostBinding('class.valid', 'isValid'`|`);`
|
||||
description:
|
||||
Binds a host element property (here, the CSS class `valid`) to a directive/component property (`isValid`).
|
||||
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@HostListener('click', ['$event']) onClick(e) {...}`|`@HostListener('click', ['$event'])`
|
||||
syntax(js):
|
||||
`ng.core.HostListener('click',
|
||||
['$event'], onClick(e) {...}, myComponent);`|`ng.core.HostListener('click', ['$event'], onClick(e)`|`);`
|
||||
description:
|
||||
Subscribes to a host element event (`click`) with a directive/component method (`onClick`), optionally passing an argument (`$event`).
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@ContentChild(myPredicate) myChildComponent;`|`@ContentChild(myPredicate)`
|
||||
syntax(js):
|
||||
`ng.core.ContentChild(myPredicate,
|
||||
'myChildComponent', myComponent);`|`ng.core.ContentChild(myPredicate,`|`);`
|
||||
description:
|
||||
Binds the first result of the component content query (`myPredicate`) to a property (`myChildComponent`) of the class.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@ContentChildren(myPredicate) myChildComponents;`|`@ContentChildren(myPredicate)`
|
||||
syntax(js):
|
||||
`ng.core.ContentChildren(myPredicate,
|
||||
'myChildComponents', myComponent);`|`ng.core.ContentChildren(myPredicate,`|`);`
|
||||
description:
|
||||
Binds the results of the component content query (`myPredicate`) to a property (`myChildComponents`) of the class.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@ViewChild(myPredicate) myChildComponent;`|`@ViewChild(myPredicate)`
|
||||
syntax(js):
|
||||
`ng.core.ViewChild(myPredicate,
|
||||
'myChildComponent', myComponent);`|`ng.core.ViewChild(myPredicate,`|`);`
|
||||
description:
|
||||
Binds the first result of the component view query (`myPredicate`) to a property (`myChildComponent`) of the class. Not available for directives.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@ViewChildren(myPredicate) myChildComponents;`|`@ViewChildren(myPredicate)`
|
||||
syntax(js):
|
||||
`ng.core.ViewChildren(myPredicate,
|
||||
'myChildComponents', myComponent);`|`ng.core.ViewChildren(myPredicate,`|`);`
|
||||
description:
|
||||
Binds the results of the component view query (`myPredicate`) to a property (`myChildComponents`) of the class. Not available for directives.
|
@ -1,23 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Directive configuration
|
||||
@cheatsheetIndex 6
|
||||
@description
|
||||
{@target ts}`@Directive({ property1: value1, ... })`{@endtarget}
|
||||
{@target js}`ng.core.Directive({ property1: value1, ... }).Class({...})`{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`selector: '.cool-button:not(a)'`|`selector:`
|
||||
description:
|
||||
Specifies a CSS selector that identifies this directive within a template. Supported selectors include `element`,
|
||||
`[attribute]`, `.class`, and `:not()`.
|
||||
|
||||
Does not support parent-child relationship selectors.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`providers: [MyService, { provide: ... }]`|`providers:`
|
||||
syntax(js):
|
||||
`providers: [MyService, { provide: ... }]`|`providers:`
|
||||
description:
|
||||
List of dependency injection providers for this directive and its children.
|
@ -1,12 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Forms
|
||||
@cheatsheetIndex 4
|
||||
@description
|
||||
{@target ts}`import { FormsModule } from '@angular/forms';`{@endtarget}
|
||||
{@target js}Available using the `ng.forms.FormsModule` module{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<input [(ngModel)]="userName">`|`[(ngModel)]`
|
||||
description:
|
||||
Provides two-way data-binding, parsing, and validation for form controls.
|
@ -1,86 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Directive and component change detection and lifecycle hooks
|
||||
@cheatsheetIndex 9
|
||||
@description
|
||||
{@target ts}(implemented as class methods){@endtarget}
|
||||
{@target js}(implemented as component properties){@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`constructor(myService: MyService, ...) { ... }`|`constructor(myService: MyService, ...)`
|
||||
syntax(js):
|
||||
`constructor: function(MyService, ...) { ... }`|`constructor: function(MyService, ...)`
|
||||
description:
|
||||
Called before any other lifecycle hook. Use it to inject dependencies, but avoid any serious work here.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngOnChanges(changeRecord) { ... }`|`ngOnChanges(changeRecord)`
|
||||
syntax(js):
|
||||
`ngOnChanges: function(changeRecord) { ... }`|`ngOnChanges: function(changeRecord)`
|
||||
description:
|
||||
Called after every change to input properties and before processing content or child views.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngOnInit() { ... }`|`ngOnInit()`
|
||||
syntax(js):
|
||||
`ngOnInit: function() { ... }`|`ngOnInit: function()`
|
||||
description:
|
||||
Called after the constructor, initializing input properties, and the first call to `ngOnChanges`.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngDoCheck() { ... }`|`ngDoCheck()`
|
||||
syntax(js):
|
||||
`ngDoCheck: function() { ... }`|`ngDoCheck: function()`
|
||||
description:
|
||||
Called every time that the input properties of a component or a directive are checked. Use it to extend change detection by performing a custom check.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngAfterContentInit() { ... }`|`ngAfterContentInit()`
|
||||
syntax(js):
|
||||
`ngAfterContentInit: function() { ... }`|`ngAfterContentInit: function()`
|
||||
description:
|
||||
Called after `ngOnInit` when the component's or directive's content has been initialized.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngAfterContentChecked() { ... }`|`ngAfterContentChecked()`
|
||||
syntax(js):
|
||||
`ngAfterContentChecked: function() { ... }`|`ngAfterContentChecked: function()`
|
||||
description:
|
||||
Called after every check of the component's or directive's content.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngAfterViewInit() { ... }`|`ngAfterViewInit()`
|
||||
syntax(js):
|
||||
`ngAfterViewInit: function() { ... }`|`ngAfterViewInit: function()`
|
||||
description:
|
||||
Called after `ngAfterContentInit` when the component's view has been initialized. Applies to components only.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngAfterViewChecked() { ... }`|`ngAfterViewChecked()`
|
||||
syntax(js):
|
||||
`ngAfterViewChecked: function() { ... }`|`ngAfterViewChecked: function()`
|
||||
description:
|
||||
Called after every check of the component's view. Applies to components only.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngOnDestroy() { ... }`|`ngOnDestroy()`
|
||||
syntax(js):
|
||||
`ngOnDestroy: function() { ... }`|`ngOnDestroy: function()`
|
||||
description:
|
||||
Called once, before the instance is destroyed.
|
@ -1,58 +0,0 @@
|
||||
@cheatsheetSection
|
||||
NgModules
|
||||
@cheatsheetIndex 1
|
||||
@description
|
||||
{@target ts}`import { NgModule } from '@angular/core';`{@endtarget}
|
||||
{@target js}Available from the `ng.core` namespace{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@NgModule({ declarations: ..., imports: ...,
|
||||
exports: ..., providers: ..., bootstrap: ...})
|
||||
class MyModule {}`|`NgModule`
|
||||
description:
|
||||
Defines a module that contains components, directives, pipes, and providers.
|
||||
|
||||
syntax(js):
|
||||
`ng.core.NgModule({declarations: ..., imports: ...,
|
||||
exports: ..., providers: ..., bootstrap: ...}).
|
||||
Class({ constructor: function() {}})`
|
||||
description:
|
||||
Defines a module that contains components, directives, pipes, and providers.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`declarations: [MyRedComponent, MyBlueComponent, MyDatePipe]`|`declarations:`
|
||||
description:
|
||||
List of components, directives, and pipes that belong to this module.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`imports: [BrowserModule, SomeOtherModule]`|`imports:`
|
||||
description:
|
||||
List of modules to import into this module. Everything from the imported modules
|
||||
is available to `declarations` of this module.
|
||||
|
||||
syntax(js):
|
||||
`imports: [ng.platformBrowser.BrowserModule, SomeOtherModule]`|`imports:`
|
||||
description:
|
||||
List of modules to import into this module. Everything from the imported modules
|
||||
is available to `declarations` of this module.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`exports: [MyRedComponent, MyDatePipe]`|`exports:`
|
||||
description:
|
||||
List of components, directives, and pipes visible to modules that import this module.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`providers: [MyService, { provide: ... }]`|`providers:`
|
||||
description:
|
||||
List of dependency injection providers visible both to the contents of this module and to importers of this module.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`bootstrap: [MyAppComponent]`|`bootstrap:`
|
||||
description:
|
||||
List of components to bootstrap when this module is bootstrapped.
|
@ -1,170 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Routing and navigation
|
||||
@cheatsheetIndex 11
|
||||
@description
|
||||
{@target ts}`import { Routes, RouterModule, ... } from '@angular/router';`{@endtarget}
|
||||
{@target js}Available from the `ng.router` namespace{@endtarget}
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`const routes: Routes = [
|
||||
{ path: '', component: HomeComponent },
|
||||
{ path: 'path/:routeParam', component: MyComponent },
|
||||
{ path: 'staticPath', component: ... },
|
||||
{ path: '**', component: ... },
|
||||
{ path: 'oldPath', redirectTo: '/staticPath' },
|
||||
{ path: ..., component: ..., data: { message: 'Custom' } }
|
||||
]);
|
||||
|
||||
const routing = RouterModule.forRoot(routes);`|`Routes`
|
||||
syntax(js):
|
||||
`var routes = [
|
||||
{ path: '', component: HomeComponent },
|
||||
{ path: ':routeParam', component: MyComponent },
|
||||
{ path: 'staticPath', component: ... },
|
||||
{ path: '**', component: ... },
|
||||
{ path: 'oldPath', redirectTo: '/staticPath' },
|
||||
{ path: ..., component: ..., data: { message: 'Custom' } }
|
||||
]);
|
||||
|
||||
var routing = ng.router.RouterModule.forRoot(routes);`|`ng.router.Routes`
|
||||
description:
|
||||
Configures routes for the application. Supports static, parameterized, redirect, and wildcard routes. Also supports custom route data and resolve.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet name="aux"></router-outlet>
|
||||
`|`router-outlet`
|
||||
description:
|
||||
Marks the location to load the component of the active route.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`
|
||||
<a routerLink="/path">
|
||||
<a [routerLink]="[ '/path', routeParam ]">
|
||||
<a [routerLink]="[ '/path', { matrixParam: 'value' } ]">
|
||||
<a [routerLink]="[ '/path' ]" [queryParams]="{ page: 1 }">
|
||||
<a [routerLink]="[ '/path' ]" fragment="anchor">
|
||||
`|`[routerLink]`
|
||||
description:
|
||||
Creates a link to a different view based on a route instruction consisting of a route path, required and optional parameters, query parameters, and a fragment. To navigate to a root route, use the `/` prefix; for a child route, use the `./`prefix; for a sibling or parent, use the `../` prefix.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<a [routerLink]="[ '/path' ]" routerLinkActive="active">`
|
||||
description:
|
||||
The provided classes are added to the element when the `routerLink` becomes the current active route.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`class CanActivateGuard implements CanActivate {
|
||||
canActivate(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Observable<boolean>|Promise<boolean>|boolean { ... }
|
||||
}
|
||||
|
||||
{ path: ..., canActivate: [CanActivateGuard] }`|`CanActivate`
|
||||
syntax(js):
|
||||
`var CanActivateGuard = ng.core.Class({
|
||||
canActivate: function(route, state) {
|
||||
// return Observable/Promise boolean or boolean
|
||||
}
|
||||
});
|
||||
|
||||
{ path: ..., canActivate: [CanActivateGuard] }`|`CanActivate`
|
||||
description:
|
||||
An interface for defining a class that the router should call first to determine if it should activate this component. Should return a boolean or an Observable/Promise that resolves to a boolean.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`class CanDeactivateGuard implements CanDeactivate<T> {
|
||||
canDeactivate(
|
||||
component: T,
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Observable<boolean>|Promise<boolean>|boolean { ... }
|
||||
}
|
||||
|
||||
{ path: ..., canDeactivate: [CanDeactivateGuard] }`|`CanDeactivate`
|
||||
syntax(js):
|
||||
`var CanDeactivateGuard = ng.core.Class({
|
||||
canDeactivate: function(component, route, state) {
|
||||
// return Observable/Promise boolean or boolean
|
||||
}
|
||||
});
|
||||
|
||||
{ path: ..., canDeactivate: [CanDeactivateGuard] }`|`CanDeactivate`
|
||||
description:
|
||||
An interface for defining a class that the router should call first to determine if it should deactivate this component after a navigation. Should return a boolean or an Observable/Promise that resolves to a boolean.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`class CanActivateChildGuard implements CanActivateChild {
|
||||
canActivateChild(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Observable<boolean>|Promise<boolean>|boolean { ... }
|
||||
}
|
||||
|
||||
{ path: ..., canActivateChild: [CanActivateGuard],
|
||||
children: ... }`|`CanActivateChild`
|
||||
syntax(js):
|
||||
`var CanActivateChildGuard = ng.core.Class({
|
||||
canActivateChild: function(route, state) {
|
||||
// return Observable/Promise boolean or boolean
|
||||
}
|
||||
});
|
||||
|
||||
{ path: ..., canActivateChild: [CanActivateChildGuard],
|
||||
children: ... }`|`CanActivateChild`
|
||||
description:
|
||||
An interface for defining a class that the router should call first to determine if it should activate the child route. Should return a boolean or an Observable/Promise that resolves to a boolean.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`class ResolveGuard implements Resolve<T> {
|
||||
resolve(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Observable<any>|Promise<any>|any { ... }
|
||||
}
|
||||
|
||||
{ path: ..., resolve: [ResolveGuard] }`|`Resolve`
|
||||
syntax(js):
|
||||
`var ResolveGuard = ng.core.Class({
|
||||
resolve: function(route, state) {
|
||||
// return Observable/Promise value or value
|
||||
}
|
||||
});
|
||||
|
||||
{ path: ..., resolve: [ResolveGuard] }`|`Resolve`
|
||||
description:
|
||||
An interface for defining a class that the router should call first to resolve route data before rendering the route. Should return a value or an Observable/Promise that resolves to a value.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`class CanLoadGuard implements CanLoad {
|
||||
canLoad(
|
||||
route: Route
|
||||
): Observable<boolean>|Promise<boolean>|boolean { ... }
|
||||
}
|
||||
|
||||
{ path: ..., canLoad: [CanLoadGuard], loadChildren: ... }`|`CanLoad`
|
||||
syntax(js):
|
||||
`var CanLoadGuard = ng.core.Class({
|
||||
canLoad: function(route) {
|
||||
// return Observable/Promise boolean or boolean
|
||||
}
|
||||
});
|
||||
|
||||
{ path: ..., canLoad: [CanLoadGuard], loadChildren: ... }`|`CanLoad`
|
||||
description:
|
||||
An interface for defining a class that the router should call first to check if the lazy loaded module should be loaded. Should return a boolean or an Observable/Promise that resolves to a boolean.
|
||||
|
@ -1,94 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Template syntax
|
||||
@cheatsheetIndex 2
|
||||
@description
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<input [value]="firstName">`|`[value]`
|
||||
description:
|
||||
Binds property `value` to the result of expression `firstName`.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<div [attr.role]="myAriaRole">`|`[attr.role]`
|
||||
description:
|
||||
Binds attribute `role` to the result of expression `myAriaRole`.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<div [class.extra-sparkle]="isDelightful">`|`[class.extra-sparkle]`
|
||||
description:
|
||||
Binds the presence of the CSS class `extra-sparkle` on the element to the truthiness of the expression `isDelightful`.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<div [style.width.px]="mySize">`|`[style.width.px]`
|
||||
description:
|
||||
Binds style property `width` to the result of expression `mySize` in pixels. Units are optional.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<button (click)="readRainbow($event)">`|`(click)`
|
||||
description:
|
||||
Calls method `readRainbow` when a click event is triggered on this button element (or its children) and passes in the event object.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<div title="Hello {{ponyName}}">`|`{{ponyName}}`
|
||||
description:
|
||||
Binds a property to an interpolated string, for example, "Hello Seabiscuit". Equivalent to:
|
||||
`<div [title]="'Hello ' + ponyName">`
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<p>Hello {{ponyName}}</p>`|`{{ponyName}}`
|
||||
description:
|
||||
Binds text content to an interpolated string, for example, "Hello Seabiscuit".
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<my-cmp [(title)]="name">`|`[(title)]`
|
||||
description:
|
||||
Sets up two-way data binding. Equivalent to: `<my-cmp [title]="name" (titleChange)="name=$event">`
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<video #movieplayer ...>
|
||||
<button (click)="movieplayer.play()">
|
||||
</video>`|`#movieplayer`|`(click)`
|
||||
description:
|
||||
Creates a local variable `movieplayer` that provides access to the `video` element instance in data-binding and event-binding expressions in the current template.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<p *myUnless="myExpression">...</p>`|`*myUnless`
|
||||
description:
|
||||
The `*` symbol turns the current element into an embedded template. Equivalent to:
|
||||
`<ng-template [myUnless]="myExpression"><p>...</p></ng-template>`
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<p>Card No.: {{cardNumber | myCardNumberFormatter}}</p>`|`{{cardNumber | myCardNumberFormatter}}`
|
||||
description:
|
||||
Transforms the current value of expression `cardNumber` via the pipe called `myCardNumberFormatter`.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<p>Employer: {{employer?.companyName}}</p>`|`{{employer?.companyName}}`
|
||||
description:
|
||||
The safe navigation operator (`?`) means that the `employer` field is optional and if `undefined`, the rest of the expression should be ignored.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<svg:rect x="0" y="0" width="100" height="100"/>`|`svg:`
|
||||
description:
|
||||
An SVG snippet template needs an `svg:` prefix on its root element to disambiguate the SVG element from an HTML component.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<svg>
|
||||
<rect x="0" y="0" width="100" height="100"/>
|
||||
</svg>`|`svg`
|
||||
description:
|
||||
An `<svg>` root element is detected as an SVG element automatically, without the prefix.
|
File diff suppressed because it is too large
Load Diff
@ -1,301 +0,0 @@
|
||||
@title
|
||||
Component Interaction
|
||||
|
||||
@intro
|
||||
Share information between different directives and components
|
||||
|
||||
@description
|
||||
<a id="top"></a>This cookbook contains recipes for common component communication scenarios
|
||||
in which two or more components share information.
|
||||
<a id="toc"></a>## Table of contents
|
||||
|
||||
[Pass data from parent to child with input binding](#parent-to-child)
|
||||
|
||||
[Intercept input property changes with a setter](#parent-to-child-setter)
|
||||
|
||||
[Intercept input property changes with *ngOnChanges*](#parent-to-child-on-changes)
|
||||
|
||||
[Parent listens for child event](#child-to-parent)
|
||||
|
||||
[Parent interacts with child via a *local variable*](#parent-to-child-local-var)
|
||||
|
||||
[Parent calls a *ViewChild*](#parent-to-view-child)
|
||||
|
||||
[Parent and children communicate via a service](#bidirectional-service)
|
||||
**See the <live-example name="cb-component-communication"></live-example>**.
|
||||
|
||||
<a id="parent-to-child"></a>## Pass data from parent to child with input binding
|
||||
|
||||
`HeroChildComponent` has two ***input properties***,
|
||||
typically adorned with [@Input decorations](../guide/template-syntax.html#inputs-outputs).
|
||||
|
||||
|
||||
{@example 'cb-component-communication/ts/src/app/hero-child.component.ts'}
|
||||
|
||||
The second `@Input` aliases the child component property name `masterName` as `'master'`.
|
||||
|
||||
The `HeroParentComponent` nests the child `HeroChildComponent` inside an `*ngFor` repeater,
|
||||
binding its `master` string property to the child's `master` alias
|
||||
and each iteration's `hero` instance to the child's `hero` property.
|
||||
|
||||
|
||||
{@example 'cb-component-communication/ts/src/app/hero-parent.component.ts'}
|
||||
|
||||
The running application displays three heroes:
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/component-communication/parent-to-child.png" alt="Parent-to-child"> </img>
|
||||
</figure>
|
||||
|
||||
### Test it
|
||||
|
||||
E2E test that all children were instantiated and displayed as expected:
|
||||
|
||||
|
||||
{@example 'cb-component-communication/e2e-spec.ts' region='parent-to-child'}
|
||||
|
||||
[Back to top](#top)
|
||||
|
||||
<a id="parent-to-child-setter"></a>## Intercept input property changes with a setter
|
||||
|
||||
Use an input property setter to intercept and act upon a value from the parent.
|
||||
|
||||
The setter of the `name` input property in the child `NameChildComponent`
|
||||
trims the whitespace from a name and replaces an empty value with default text.
|
||||
|
||||
|
||||
{@example 'cb-component-communication/ts/src/app/name-child.component.ts'}
|
||||
|
||||
Here's the `NameParentComponent` demonstrating name variations including a name with all spaces:
|
||||
|
||||
|
||||
{@example 'cb-component-communication/ts/src/app/name-parent.component.ts'}
|
||||
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/component-communication/setter.png" alt="Parent-to-child-setter"> </img>
|
||||
</figure>
|
||||
|
||||
### Test it
|
||||
|
||||
E2E tests of input property setter with empty and non-empty names:
|
||||
|
||||
|
||||
{@example 'cb-component-communication/e2e-spec.ts' region='parent-to-child-setter'}
|
||||
|
||||
[Back to top](#top)
|
||||
|
||||
<a id="parent-to-child-on-changes"></a>## Intercept input property changes with *ngOnChanges*
|
||||
|
||||
Detect and act upon changes to input property values with the `ngOnChanges` method of the `OnChanges` lifecycle hook interface.
|
||||
May prefer this approach to the property setter when watching multiple, interacting input properties.
|
||||
|
||||
Learn about `ngOnChanges` in the [LifeCycle Hooks](../guide/lifecycle-hooks.html) chapter.This `VersionChildComponent` detects changes to the `major` and `minor` input properties and composes a log message reporting these changes:
|
||||
|
||||
|
||||
{@example 'cb-component-communication/ts/src/app/version-child.component.ts'}
|
||||
|
||||
The `VersionParentComponent` supplies the `minor` and `major` values and binds buttons to methods that change them.
|
||||
|
||||
|
||||
{@example 'cb-component-communication/ts/src/app/version-parent.component.ts'}
|
||||
|
||||
Here's the output of a button-pushing sequence:
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/component-communication/parent-to-child-on-changes.gif" alt="Parent-to-child-onchanges"> </img>
|
||||
</figure>
|
||||
|
||||
### Test it
|
||||
|
||||
Test that ***both*** input properties are set initially and that button clicks trigger
|
||||
the expected `ngOnChanges` calls and values:
|
||||
|
||||
|
||||
{@example 'cb-component-communication/e2e-spec.ts' region='parent-to-child-onchanges'}
|
||||
|
||||
[Back to top](#top)
|
||||
|
||||
<a id="child-to-parent"></a>## Parent listens for child event
|
||||
|
||||
The child component exposes an `EventEmitter` property with which it `emits`events when something happens.
|
||||
The parent binds to that event property and reacts to those events.
|
||||
|
||||
The child's `EventEmitter` property is an ***output property***,
|
||||
typically adorned with an [@Output decoration](../guide/template-syntax.html#inputs-outputs)
|
||||
as seen in this `VoterComponent`:
|
||||
|
||||
|
||||
{@example 'cb-component-communication/ts/src/app/voter.component.ts'}
|
||||
|
||||
Clicking a button triggers emission of a `true` or `false` (the boolean *payload*).
|
||||
|
||||
The parent `VoteTakerComponent` binds an event handler (`onVoted`) that responds to the child event
|
||||
payload (`$event`) and updates a counter.
|
||||
|
||||
|
||||
{@example 'cb-component-communication/ts/src/app/votetaker.component.ts'}
|
||||
|
||||
The framework passes the event argument — represented by `$event` — to the handler method,
|
||||
and the method processes it:
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/component-communication/child-to-parent.gif" alt="Child-to-parent"> </img>
|
||||
</figure>
|
||||
|
||||
### Test it
|
||||
|
||||
Test that clicking the *Agree* and *Disagree* buttons update the appropriate counters:
|
||||
|
||||
|
||||
{@example 'cb-component-communication/e2e-spec.ts' region='child-to-parent'}
|
||||
|
||||
[Back to top](#top)
|
||||
|
||||
## Parent interacts with child via *local variable*
|
||||
|
||||
A parent component cannot use data binding to read child properties
|
||||
or invoke child methods. We can do both
|
||||
by creating a template reference variable for the child element
|
||||
and then reference that variable *within the parent template*
|
||||
as seen in the following example.
|
||||
|
||||
<a id="countdown-timer-example"></a>
|
||||
We have a child `CountdownTimerComponent` that repeatedly counts down to zero and launches a rocket.
|
||||
It has `start` and `stop` methods that control the clock and it displays a
|
||||
countdown status message in its own template.
|
||||
|
||||
{@example 'cb-component-communication/ts/src/app/countdown-timer.component.ts'}
|
||||
|
||||
Let's see the `CountdownLocalVarParentComponent` that hosts the timer component.
|
||||
|
||||
|
||||
{@example 'cb-component-communication/ts/src/app/countdown-parent.component.ts' region='lv'}
|
||||
|
||||
The parent component cannot data bind to the child's
|
||||
`start` and `stop` methods nor to its `seconds` property.
|
||||
|
||||
We can place a local variable (`#timer`) on the tag (`<countdown-timer>`) representing the child component.
|
||||
That gives us a reference to the child component itself and the ability to access
|
||||
*any of its properties or methods* from within the parent template.
|
||||
|
||||
In this example, we wire parent buttons to the child's `start` and `stop` and
|
||||
use interpolation to display the child's `seconds` property.
|
||||
|
||||
Here we see the parent and child working together.
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/component-communication/countdown-timer-anim.gif" alt="countdown timer"> </img>
|
||||
</figure>
|
||||
|
||||
|
||||
|
||||
{@a countdown-tests}
|
||||
### Test it
|
||||
|
||||
Test that the seconds displayed in the parent template
|
||||
match the seconds displayed in the child's status message.
|
||||
Test also that clicking the *Stop* button pauses the countdown timer:
|
||||
|
||||
|
||||
{@example 'cb-component-communication/e2e-spec.ts' region='countdown-timer-tests'}
|
||||
|
||||
[Back to top](#top)
|
||||
|
||||
<a id="parent-to-view-child"></a>## Parent calls a *ViewChild*
|
||||
|
||||
The *local variable* approach is simple and easy. But it is limited because
|
||||
the parent-child wiring must be done entirely within the parent template.
|
||||
The parent component *itself* has no access to the child.
|
||||
|
||||
We can't use the *local variable* technique if an instance of the parent component *class*
|
||||
must read or write child component values or must call child component methods.
|
||||
|
||||
When the parent component *class* requires that kind of access,
|
||||
we ***inject*** the child component into the parent as a *ViewChild*.
|
||||
|
||||
We'll illustrate this technique with the same [Countdown Timer](#countdown-timer-example) example.
|
||||
We won't change its appearance or behavior.
|
||||
The child [CountdownTimerComponent](#countdown-timer-example) is the same as well.
|
||||
We are switching from the *local variable* to the *ViewChild* technique
|
||||
solely for the purpose of demonstration.Here is the parent, `CountdownViewChildParentComponent`:
|
||||
|
||||
{@example 'cb-component-communication/ts/src/app/countdown-parent.component.ts' region='vc'}
|
||||
|
||||
It takes a bit more work to get the child view into the parent component *class*.
|
||||
|
||||
We import references to the `ViewChild` decorator and the `AfterViewInit` lifecycle hook.
|
||||
|
||||
We inject the child `CountdownTimerComponent` into the private `timerComponent` property
|
||||
via the `@ViewChild` property decoration.
|
||||
|
||||
The `#timer` local variable is gone from the component metadata.
|
||||
Instead we bind the buttons to the parent component's own `start` and `stop` methods and
|
||||
present the ticking seconds in an interpolation around the parent component's `seconds` method.
|
||||
|
||||
These methods access the injected timer component directly.
|
||||
|
||||
The `ngAfterViewInit` lifecycle hook is an important wrinkle.
|
||||
The timer component isn't available until *after* Angular displays the parent view.
|
||||
So we display `0` seconds initially.
|
||||
|
||||
Then Angular calls the `ngAfterViewInit` lifecycle hook at which time it is *too late*
|
||||
to update the parent view's display of the countdown seconds.
|
||||
Angular's unidirectional data flow rule prevents us from updating the parent view's
|
||||
in the same cycle. We have to *wait one turn* before we can display the seconds.
|
||||
|
||||
We use `setTimeout` to wait one tick and then revise the `seconds` method so
|
||||
that it takes future values from the timer component.
|
||||
|
||||
### Test it
|
||||
Use [the same countdown timer tests](#countdown-tests) as before.[Back to top](#top)
|
||||
|
||||
<a id="bidirectional-service"></a>## Parent and children communicate via a service
|
||||
|
||||
A parent component and its children share a service whose interface enables bi-directional communication
|
||||
*within the family*.
|
||||
|
||||
The scope of the service instance is the parent component and its children.
|
||||
Components outside this component subtree have no access to the service or their communications.
|
||||
|
||||
This `MissionService` connects the `MissionControlComponent` to multiple `AstronautComponent` children.
|
||||
|
||||
|
||||
{@example 'cb-component-communication/ts/src/app/mission.service.ts'}
|
||||
|
||||
The `MissionControlComponent` both provides the instance of the service that it shares with its children
|
||||
(through the `providers` metadata array) and injects that instance into itself through its constructor:
|
||||
|
||||
|
||||
{@example 'cb-component-communication/ts/src/app/missioncontrol.component.ts'}
|
||||
|
||||
The `AstronautComponent` also injects the service in its constructor.
|
||||
Each `AstronautComponent` is a child of the `MissionControlComponent` and therefore receives its parent's service instance:
|
||||
|
||||
|
||||
{@example 'cb-component-communication/ts/src/app/astronaut.component.ts'}
|
||||
|
||||
|
||||
Notice that we capture the `subscription` and unsubscribe when the `AstronautComponent` is destroyed.
|
||||
This is a memory-leak guard step. There is no actual risk in this app because the
|
||||
lifetime of a `AstronautComponent` is the same as the lifetime of the app itself.
|
||||
That *would not* always be true in a more complex application.
|
||||
|
||||
We do not add this guard to the `MissionControlComponent` because, as the parent,
|
||||
it controls the lifetime of the `MissionService`.The *History* log demonstrates that messages travel in both directions between
|
||||
the parent `MissionControlComponent` and the `AstronautComponent` children,
|
||||
facilitated by the service:
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/component-communication/bidirectional-service.gif" alt="bidirectional-service"> </img>
|
||||
</figure>
|
||||
|
||||
### Test it
|
||||
|
||||
Tests click buttons of both the parent `MissionControlComponent` and the `AstronautComponent` children
|
||||
and verify that the *History* meets expectations:
|
||||
|
||||
|
||||
{@example 'cb-component-communication/e2e-spec.ts' region='bidirectional-service'}
|
||||
|
||||
[Back to top](#top)
|
@ -1,194 +0,0 @@
|
||||
@title
|
||||
Component-relative Paths
|
||||
|
||||
@intro
|
||||
Use relative URLs for component templates and styles.
|
||||
|
||||
@description
|
||||
## Write *Component-Relative* URLs to component templates and style files
|
||||
|
||||
Our components often refer to external template and style files.
|
||||
We identify those files with a URL in the `templateUrl` and `styleUrls` properties of the `@Component` metadata
|
||||
as seen here:
|
||||
|
||||
|
||||
{@example 'cb-component-relative-paths/ts/src/app/some.component.ts' region='absolute-config'}
|
||||
|
||||
By default, we *must* specify the full path back to the application root.
|
||||
We call this an ***absolute path*** because it is *absolute* with respect to the application root.
|
||||
|
||||
There are two problems with an *absolute path*:
|
||||
|
||||
1. We have to remember the full path back to the application root.
|
||||
|
||||
1. We have to update the URL when we move the component around in the application files structure.
|
||||
|
||||
It would be much easier to write and maintain our application components if we could specify template and style locations
|
||||
*relative* to their component class file.
|
||||
|
||||
*We can!*
|
||||
|
||||
|
||||
~~~ {.alert.is-important}
|
||||
|
||||
We can if we build our application as `commonjs` modules and load those modules
|
||||
with a suitable package loader such as `systemjs` or `webpack`.
|
||||
Learn why [below](#why-default).
|
||||
|
||||
The Angular CLI uses these technologies and defaults to the
|
||||
*component-relative path* approach described here.
|
||||
CLI users can skip this chapter or read on to understand
|
||||
how it works.
|
||||
|
||||
|
||||
~~~
|
||||
|
||||
|
||||
## _Component-Relative_ Paths
|
||||
|
||||
Our goal is to specify template and style URLs *relative* to their component class files,
|
||||
hence the term ***component-relative path***.
|
||||
|
||||
The key to success is following a convention that puts related component files in well-known locations.
|
||||
|
||||
We recommend keeping component template and component-specific style files as *siblings* of their
|
||||
companion component class files.
|
||||
Here we see the three files for `SomeComponent` sitting next to each other in the `app` folder.
|
||||
|
||||
<aio-filetree>
|
||||
|
||||
<aio-folder>
|
||||
app
|
||||
<aio-file>
|
||||
some.component.css
|
||||
</aio-file>
|
||||
|
||||
|
||||
<aio-file>
|
||||
some.component.html
|
||||
</aio-file>
|
||||
|
||||
|
||||
<aio-file>
|
||||
some.component.ts
|
||||
</aio-file>
|
||||
|
||||
|
||||
</aio-folder>
|
||||
|
||||
|
||||
</aio-filetree>
|
||||
|
||||
We'll have more files and folders — and greater folder depth — as our application grows.
|
||||
We'll be fine as long as the component files travel together as the inseparable siblings they are.
|
||||
|
||||
### Set the *moduleId*
|
||||
|
||||
Having adopted this file structure convention, we can specify locations of the template and style files
|
||||
relative to the component class file simply by setting the `moduleId` property of the `@Component` metadata like this
|
||||
|
||||
{@example 'cb-component-relative-paths/ts/src/app/some.component.ts' region='module-id'}
|
||||
|
||||
We strip the `src/app/` base path from the `templateUrl` and `styleUrls` and replace it with `./`.
|
||||
The result looks like this:
|
||||
|
||||
{@example 'cb-component-relative-paths/ts/src/app/some.component.ts' region='relative-config'}
|
||||
|
||||
|
||||
|
||||
~~~ {.alert.is-helpful}
|
||||
|
||||
Webpack users may prefer [an alternative approach](#webpack).
|
||||
|
||||
|
||||
~~~
|
||||
|
||||
|
||||
## Source
|
||||
|
||||
**We can see the <live-example name="cb-component-relative-paths"></live-example>**
|
||||
and download the source code from there
|
||||
or simply read the pertinent source here.
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="src/app/some.component.ts">
|
||||
{@example 'cb-component-relative-paths/ts/src/app/some.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="src/app/some.component.html">
|
||||
{@example 'cb-component-relative-paths/ts/src/app/some.component.html'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="src/app/some.component.css">
|
||||
{@example 'cb-component-relative-paths/ts/src/app/some.component.css'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="src/app/app.component.ts">
|
||||
{@example 'cb-component-relative-paths/ts/src/app/app.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
|
||||
|
||||
{@a why-default}
|
||||
|
||||
## Appendix: why *component-relative* is not the default
|
||||
|
||||
A *component-relative* path is obviously superior to an *absolute* path.
|
||||
Why did Angular default to the *absolute* path?
|
||||
Why do *we* have to set the `moduleId`? Why can't Angular set it?
|
||||
|
||||
First, let's look at what happens if we use a relative path and omit the `moduleId`.
|
||||
|
||||
`EXCEPTION: Failed to load some.component.html`
|
||||
|
||||
Angular can't find the file so it throws an error.
|
||||
|
||||
Why can't Angular calculate the template and style URLs from the component file's location?
|
||||
|
||||
Because the location of the component can't be determined without the developer's help.
|
||||
Angular apps can be loaded in many ways: from individual files, from SystemJS packages, or
|
||||
from CommonJS packages, to name a few.
|
||||
We might generate modules in any of several formats.
|
||||
We might not be writing modular code at all!
|
||||
|
||||
With this diversity of packaging and module load strategies,
|
||||
it's not possible for Angular to know with certainty where these files reside at runtime.
|
||||
|
||||
The only location Angular can be sure of is the URL of the `index.html` home page, the application root.
|
||||
So by default it resolves template and style paths relative to the URL of `index.html`.
|
||||
That's why we previously wrote our file URLs with an `app/` base path prefix.
|
||||
|
||||
But *if* we follow the recommended guidelines and we write modules in `commonjs` format
|
||||
and we use a module loader that *plays nice*,
|
||||
*then* we — the developers of the application —
|
||||
know that the semi-global `module.id` variable is available and contains
|
||||
the absolute URL of the component class module file.
|
||||
|
||||
That knowledge enables us to tell Angular where the *component* file is
|
||||
by setting the `moduleId`:
|
||||
|
||||
{@example 'cb-component-relative-paths/ts/src/app/some.component.ts' region='module-id'}
|
||||
|
||||
|
||||
|
||||
{@a webpack}
|
||||
|
||||
## Webpack: load templates and styles
|
||||
Webpack developers have an alternative to `moduleId`.
|
||||
|
||||
They can load templates and styles at runtime by adding `./` at the beginning of the `template` and `styles` / `styleUrls`
|
||||
properties that reference *component-relative URLS.
|
||||
|
||||
|
||||
{@example 'webpack/ts/src/app/app.component.ts'}
|
||||
|
||||
|
||||
Webpack will do a `require` behind the scenes to load the templates and styles. Read more [here](../guide/webpack.html#highlights).
|
||||
|
||||
See the [Introduction to Webpack](../guide/webpack.html).
|
@ -1,921 +0,0 @@
|
||||
@title
|
||||
Dependency Injection
|
||||
|
||||
@intro
|
||||
Techniques for Dependency Injection
|
||||
|
||||
@description
|
||||
Dependency Injection is a powerful pattern for managing code dependencies.
|
||||
In this cookbook we will explore many of the features of Dependency Injection (DI) in Angular.
|
||||
<a id="toc"></a>## Table of contents
|
||||
|
||||
[Application-wide dependencies](#app-wide-dependencies)
|
||||
|
||||
[External module configuration](#external-module-configuration)
|
||||
|
||||
[*@Injectable* and nested service dependencies](#nested-dependencies)
|
||||
|
||||
[Limit service scope to a component subtree](#service-scope)
|
||||
|
||||
[Multiple service instances (sandboxing)](#multiple-service-instances)
|
||||
|
||||
[Qualify dependency lookup with *@Optional* and *@Host*](#qualify-dependency-lookup)
|
||||
|
||||
[Inject the component's DOM element](#component-element)
|
||||
|
||||
[Define dependencies with providers](#providers)
|
||||
* [The *provide* object literal](#provide)
|
||||
* [useValue - the *value provider*](#usevalue)
|
||||
* [useClass - the *class provider*](#useclass)
|
||||
* [useExisting - the *alias provider*](#useexisting)
|
||||
* [useFactory - the *factory provider*](#usefactory)
|
||||
|
||||
[Provider token alternatives](#tokens)
|
||||
* [class-interface](#class-interface)
|
||||
* [OpaqueToken](#opaque-token)
|
||||
|
||||
[Inject into a derived class](#di-inheritance)
|
||||
|
||||
[Find a parent component by injection](#find-parent)
|
||||
* [Find parent with a known component type](#known-parent)
|
||||
* [Cannot find a parent by its base class](#base-parent)
|
||||
* [Find a parent by its class-interface](#class-interface-parent)
|
||||
* [Find a parent in a tree of parents (*@SkipSelf*)](#parent-tree)
|
||||
* [A *provideParent* helper function](#provideparent)
|
||||
|
||||
[Break circularities with a forward class reference (*forwardRef*)](#forwardref)
|
||||
**See the <live-example name="cb-dependency-injection"></live-example>**
|
||||
of the code supporting this cookbook.
|
||||
|
||||
<a id="app-wide-dependencies"></a>## Application-wide dependencies
|
||||
Register providers for dependencies used throughout the application in the root application component, `AppComponent`.
|
||||
|
||||
In the following example, we import and register several services
|
||||
(the `LoggerService`, `UserContext`, and the `UserService`)
|
||||
in the `@Component` metadata `providers` array.
|
||||
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/app.component.ts' region='import-services'}
|
||||
|
||||
All of these services are implemented as classes.
|
||||
Service classes can act as their own providers which is why listing them in the `providers` array
|
||||
is all the registration we need.
|
||||
A *provider* is something that can create or deliver a service.
|
||||
Angular creates a service instance from a class provider by "new-ing" it.
|
||||
Learn more about providers [below](#providers).Now that we've registered these services,
|
||||
Angular can inject them into the constructor of *any* component or service, *anywhere* in the application.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-bios.component.ts' region='ctor'}
|
||||
|
||||
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/user-context.service.ts' region='ctor'}
|
||||
|
||||
<a id="external-module-configuration"></a>
|
||||
## External module configuration
|
||||
We often register providers in the `NgModule` rather than in the root application component.
|
||||
|
||||
We do this when (a) we expect the service to be injectable everywhere
|
||||
or (b) we must configure another application global service _before it starts_.
|
||||
|
||||
We see an example of the second case here, where we configure the Component Router with a non-default
|
||||
[location strategy](../guide/router.html#location-strategy) by listing its provider
|
||||
in the `providers` list of the `AppModule`.
|
||||
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/app.module.ts' region='providers'}
|
||||
|
||||
|
||||
|
||||
{@a injectable}
|
||||
|
||||
|
||||
{@a nested-dependencies}
|
||||
|
||||
## *@Injectable* and nested service dependencies
|
||||
The consumer of an injected service does not know how to create that service.
|
||||
It shouldn't care.
|
||||
It's the dependency injection's job to create and cache that service.
|
||||
|
||||
Sometimes a service depends on other services ... which may depend on yet other services.
|
||||
Resolving these nested dependencies in the correct order is also the framework's job.
|
||||
At each step, the consumer of dependencies simply declares what it requires in its constructor and the framework takes over.
|
||||
|
||||
For example, we inject both the `LoggerService` and the `UserContext` in the `AppComponent`.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/app.component.ts' region='ctor'}
|
||||
|
||||
The `UserContext` in turn has dependencies on both the `LoggerService` (again) and
|
||||
a `UserService` that gathers information about a particular user.
|
||||
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/user-context.service.ts' region='injectables'}
|
||||
|
||||
When Angular creates an`AppComponent`, the dependency injection framework creates an instance of the `LoggerService` and
|
||||
starts to create the `UserContextService`.
|
||||
The `UserContextService` needs the `LoggerService`, which the framework already has, and the `UserService`, which it has yet to create.
|
||||
The `UserService` has no dependencies so the dependency injection framework can just `new` one into existence.
|
||||
|
||||
The beauty of dependency injection is that the author of `AppComponent` didn't care about any of this.
|
||||
The author simply declared what was needed in the constructor (`LoggerService` and `UserContextService`) and the framework did the rest.
|
||||
|
||||
Once all the dependencies are in place, the `AppComponent` displays the user information:
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/dependency-injection/logged-in-user.png" alt="Logged In User"> </img>
|
||||
</figure>
|
||||
|
||||
### *@Injectable()*
|
||||
Notice the `@Injectable()`decorator on the `UserContextService` class.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/user-context.service.ts' region='injectable'}
|
||||
|
||||
That decorator makes it possible for Angular to identify the types of its two dependencies, `LoggerService` and `UserService`.
|
||||
|
||||
Technically, the `@Injectable()`decorator is only _required_ for a service class that has _its own dependencies_.
|
||||
The `LoggerService` doesn't depend on anything. The logger would work if we omitted `@Injectable()`
|
||||
and the generated code would be slightly smaller.
|
||||
|
||||
But the service would break the moment we gave it a dependency and we'd have to go back and
|
||||
and add `@Injectable()` to fix it. We add `@Injectable()` from the start for the sake of consistency and to avoid future pain.
|
||||
|
||||
|
||||
~~~ {.alert.is-helpful}
|
||||
|
||||
Although we recommend applying `@Injectable` to all service classes, do not feel bound by it.
|
||||
Some developers prefer to add it only where needed and that's a reasonable policy too.
|
||||
|
||||
|
||||
~~~
|
||||
|
||||
|
||||
The `AppComponent` class had two dependencies as well but no `@Injectable()`.
|
||||
It didn't need `@Injectable()` because that component class has the `@Component` decorator.
|
||||
In Angular with TypeScript, a *single* decorator — *any* decorator — is sufficient to identify dependency types.
|
||||
|
||||
<a id="service-scope"></a>
|
||||
## Limit service scope to a component subtree
|
||||
|
||||
All injected service dependencies are singletons meaning that,
|
||||
for a given dependency injector ("injector"), there is only one instance of service.
|
||||
|
||||
But an Angular application has multiple dependency injectors, arranged in a tree hierarchy that parallels the component tree.
|
||||
So a particular service can be *provided* (and created) at any component level and multiple times
|
||||
if provided in multiple components.
|
||||
|
||||
By default, a service dependency provided in one component is visible to all of its child components and
|
||||
Angular injects the same service instance into all child components that ask for that service.
|
||||
|
||||
Accordingly, dependencies provided in the root `AppComponent` can be injected into *any* component *anywhere* in the application.
|
||||
|
||||
That isn't always desirable.
|
||||
Sometimes we want to restrict service availability to a particular region of the application.
|
||||
|
||||
We can limit the scope of an injected service to a *branch* of the application hierarchy
|
||||
by providing that service *at the sub-root component for that branch*.
|
||||
Here we provide the `HeroService` to the `HeroesBaseComponent` by listing it in the `providers` array:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/sorted-heroes.component.ts' region='injection'}
|
||||
|
||||
When Angular creates the `HeroesBaseComponent`, it also creates a new instance of `HeroService`
|
||||
that is visible only to the component and its children (if any).
|
||||
|
||||
We could also provide the `HeroService` to a *different* component elsewhere in the application.
|
||||
That would result in a *different* instance of the service, living in a *different* injector.
|
||||
We examples of such scoped `HeroService` singletons appear throughout the accompanying sample code,
|
||||
including the `HeroBiosComponent`, `HeroOfTheMonthComponent`, and `HeroesBaseComponent`.
|
||||
Each of these components has its own `HeroService` instance managing its own independent collection of heroes.
|
||||
|
||||
|
||||
|
||||
~~~ {.alert.is-helpful}
|
||||
|
||||
### Take a break!
|
||||
This much Dependency Injection knowledge may be all that many Angular developers
|
||||
ever need to build their applications. It doesn't always have to be more complicated.
|
||||
|
||||
|
||||
~~~
|
||||
|
||||
<a id="multiple-service-instances"></a>
|
||||
## Multiple service instances (sandboxing)
|
||||
|
||||
Sometimes we want multiple instances of a service at *the same level of the component hierarchy*.
|
||||
|
||||
A good example is a service that holds state for its companion component instance.
|
||||
We need a separate instance of the service for each component.
|
||||
Each service has its own work-state, isolated from the service-and-state of a different component.
|
||||
We call this *sandboxing* because each service and component instance has its own sandbox to play in.
|
||||
|
||||
<a id="hero-bios-component"></a>
|
||||
Imagine a `HeroBiosComponent` that presents three instances of the `HeroBioComponent`.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-bios.component.ts' region='simple'}
|
||||
|
||||
Each `HeroBioComponent` can edit a single hero's biography.
|
||||
A `HeroBioComponent` relies on a `HeroCacheService` to fetch, cache, and perform other persistence operations on that hero.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-cache.service.ts' region='service'}
|
||||
|
||||
Clearly the three instances of the `HeroBioComponent` can't share the same `HeroCacheService`.
|
||||
They'd be competing with each other to determine which hero to cache.
|
||||
|
||||
Each `HeroBioComponent` gets its *own* `HeroCacheService` instance
|
||||
by listing the `HeroCacheService` in its metadata `providers` array.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-bio.component.ts' region='component'}
|
||||
|
||||
The parent `HeroBiosComponent` binds a value to the `heroId`.
|
||||
The `ngOnInit` pass that `id` to the service which fetches and caches the hero.
|
||||
The getter for the `hero` property pulls the cached hero from the service.
|
||||
And the template displays this data-bound property.
|
||||
|
||||
Find this example in <live-example name="cb-dependency-injection">live code</live-example>
|
||||
and confirm that the three `HeroBioComponent` instances have their own cached hero data.
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/dependency-injection/hero-bios.png" alt="Bios"> </img>
|
||||
</figure>
|
||||
|
||||
|
||||
|
||||
{@a optional}
|
||||
|
||||
|
||||
{@a qualify-dependency-lookup}
|
||||
|
||||
## Qualify dependency lookup with *@Optional* and *@Host*
|
||||
We learned that dependencies can be registered at any level in the component hierarchy.
|
||||
|
||||
When a component requests a dependency, Angular starts with that component's injector and walks up the injector tree
|
||||
until it finds the first suitable provider. Angular throws an error if it can't find the dependency during that walk.
|
||||
|
||||
We *want* this behavior most of the time.
|
||||
But sometimes we need to limit the search and/or accommodate a missing dependency.
|
||||
We can modify Angular's search behavior with the `@Host` and `@Optional` qualifying decorators,
|
||||
used individually or together.
|
||||
|
||||
The `@Optional` decorator tells Angular to continue when it can't find the dependency.
|
||||
Angular sets the injection parameter to `null` instead.
|
||||
|
||||
The `@Host` decorator stops the upward search at the *host component*.
|
||||
|
||||
The host component is typically the component requesting the dependency.
|
||||
But when this component is projected into a *parent* component, that parent component becomes the host.
|
||||
We look at this second, more interesting case in our next example.
|
||||
|
||||
### Demonstration
|
||||
The `HeroBiosAndContactsComponent` is a revision of the `HeroBiosComponent` that we looked at [above](#hero-bios-component).
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-bios.component.ts' region='hero-bios-and-contacts'}
|
||||
|
||||
Focus on the template:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-bios.component.ts' region='template'}
|
||||
|
||||
We've inserted a `<hero-contact>` element between the `<hero-bio>` tags.
|
||||
Angular *projects* (*transcludes*) the corresponding `HeroContactComponent` into the `HeroBioComponent` view,
|
||||
placing it in the `<ng-content>` slot of the `HeroBioComponent` template:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-bio.component.ts' region='template'}
|
||||
|
||||
It looks like this, with the hero's telephone number from `HeroContactComponent` projected above the hero description:
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/dependency-injection/hero-bio-and-content.png" alt="bio and contact"> </img>
|
||||
</figure>
|
||||
|
||||
Here's the `HeroContactComponent` which demonstrates the qualifying decorators that we're talking about in this section:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-contact.component.ts' region='component'}
|
||||
|
||||
Focus on the constructor parameters
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-contact.component.ts' region='ctor-params'}
|
||||
|
||||
The `@Host()` function decorating the `heroCache` property ensures that
|
||||
we get a reference to the cache service from the parent `HeroBioComponent`.
|
||||
Angular throws if the parent lacks that service, even if a component higher in the component tree happens to have that service.
|
||||
|
||||
A second `@Host()` function decorates the `loggerService` property.
|
||||
We know the only `LoggerService` instance in the app is provided at the `AppComponent` level.
|
||||
The host `HeroBioComponent` doesn't have its own `LoggerService` provider.
|
||||
|
||||
Angular would throw an error if we hadn't also decorated the property with the `@Optional()` function.
|
||||
Thanks to `@Optional()`, Angular sets the `loggerService` to null and the rest of the component adapts.
|
||||
|
||||
We'll come back to the `elementRef` property shortly.Here's the `HeroBiosAndContactsComponent` in action.
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/dependency-injection/hero-bios-and-contacts.png" alt="Bios with contact into"> </img>
|
||||
</figure>
|
||||
|
||||
If we comment out the `@Host()` decorator, Angular now walks up the injector ancestor tree
|
||||
until it finds the logger at the `AppComponent` level. The logger logic kicks in and the hero display updates
|
||||
with the gratuitous "!!!", indicating that the logger was found.
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/dependency-injection/hero-bio-contact-no-host.png" alt="Without @Host"> </img>
|
||||
</figure>
|
||||
|
||||
On the other hand, if we restore the `@Host()` decorator and comment out `@Optional`,
|
||||
the application fails for lack of the required logger at the host component level.
|
||||
<br>
|
||||
`EXCEPTION: No provider for LoggerService! (HeroContactComponent -> LoggerService)`
|
||||
<a id="component-element"></a>## Inject the component's element
|
||||
|
||||
On occasion we might need to access a component's corresponding DOM element.
|
||||
Although we strive to avoid it, many visual effects and 3rd party tools (such as jQuery)
|
||||
require DOM access.
|
||||
|
||||
To illustrate, we've written a simplified version of the `HighlightDirective` from
|
||||
the [Attribute Directives](../guide/attribute-directives.html) chapter.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/highlight.directive.ts'}
|
||||
|
||||
The directive sets the background to a highlight color when the user mouses over the
|
||||
DOM element to which it is applied.
|
||||
|
||||
Angular set the constructor's `el` parameter to the injected `ElementRef` which is
|
||||
a wrapper around that DOM element.
|
||||
Its `nativeElement` property exposes the DOM element for the directive to manipulate.
|
||||
|
||||
The sample code applies the directive's `myHighlight` attribute to two `<div>` tags,
|
||||
first without a value (yielding the default color) and then with an assigned color value.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/app.component.html' region='highlight'}
|
||||
|
||||
The following image shows the effect of mousing over the `<hero-bios-and-contacts>` tag.
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/dependency-injection/highlight.png" alt="Highlighted bios"> </img>
|
||||
</figure>
|
||||
|
||||
<a id="providers"></a>
|
||||
## Define dependencies with providers
|
||||
|
||||
In this section we learn to write providers that deliver dependent services.
|
||||
|
||||
### Background
|
||||
We get a service from a dependency injector by giving it a ***token***.
|
||||
|
||||
We usually let Angular handle this transaction for us by specifying a constructor parameter and its type.
|
||||
The parameter type serves as the injector lookup *token*.
|
||||
Angular passes this token to the injector and assigns the result to the parameter.
|
||||
Here's a typical example:
|
||||
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-bios.component.ts' region='ctor'}
|
||||
|
||||
Angular asks the injector for the service associated with the `LoggerService` and
|
||||
and assigns the returned value to the `logger` parameter.
|
||||
|
||||
Where did the injector get that value?
|
||||
It may already have that value in its internal container.
|
||||
If it doesn't, it may be able to make one with the help of a ***provider***.
|
||||
A *provider* is a recipe for delivering a service associated with a *token*.
|
||||
If the injector doesn't have a provider for the requested *token*, it delegates the request
|
||||
to its parent injector, where the process repeats until there are no more injectors.
|
||||
If the search is futile, the injector throws an error ... unless the request was [optional](#optional).
|
||||
|
||||
Let's return our attention to providers themselves.A new injector has no providers.
|
||||
Angular initializes the injectors it creates with some providers it cares about.
|
||||
We have to register our _own_ application providers manually,
|
||||
usually in the `providers` array of the `Component` or `Directive` metadata:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/app.component.ts' region='providers'}
|
||||
|
||||
### Defining providers
|
||||
|
||||
The simple class provider is the most typical by far.
|
||||
We mention the class in the `providers` array and we're done.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-bios.component.ts' region='class-provider'}
|
||||
|
||||
It's that simple because the most common injected service is an instance of a class.
|
||||
But not every dependency can be satisfied by creating a new instance of a class.
|
||||
We need other ways to deliver dependency values and that means we need other ways to specify a provider.
|
||||
|
||||
The `HeroOfTheMonthComponent` example demonstrates many of the alternatives and why we need them.
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/dependency-injection/hero-of-month.png" alt="Hero of the month" width="300px"> </img>
|
||||
</figure>
|
||||
|
||||
It's visually simple: a few properties and the output of a logger. The code behind it gives us plenty to talk about.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts' region='hero-of-the-month'}
|
||||
|
||||
|
||||
|
||||
|
||||
{@a provide}
|
||||
#### The *provide* object literal
|
||||
|
||||
The `provide` object literal takes a *token* and a *definition object*.
|
||||
The *token* is usually a class but [it doesn't have to be](#tokens).
|
||||
|
||||
The *definition* object has one main property, (e.g. `useValue`) that indicates how the provider
|
||||
should create or return the provided value.
|
||||
|
||||
|
||||
|
||||
{@a usevalue}
|
||||
#### useValue - the *value provider*
|
||||
|
||||
Set the `useValue` property to a ***fixed value*** that the provider can return as the dependency object.
|
||||
|
||||
Use this technique to provide *runtime configuration constants* such as web-site base addresses and feature flags.
|
||||
We often use a *value provider* in a unit test to replace a production service with a fake or mock.
|
||||
|
||||
The `HeroOfTheMonthComponent` example has two *value providers*.
|
||||
The first provides an instance of the `Hero` class;
|
||||
the second specifies a literal string resource:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts' region='use-value'}
|
||||
|
||||
The `Hero` provider token is a class which makes sense because the value is a `Hero`
|
||||
and the consumer of the injected hero would want the type information.
|
||||
|
||||
The `TITLE` provider token is *not a class*.
|
||||
It's a special kind of provider lookup key called an [OpaqueToken](#opaquetoken).
|
||||
We often use an `OpaqueToken` when the dependency is a simple value like a string, a number, or a function.
|
||||
|
||||
The value of a *value provider* must be defined *now*. We can't create the value later.
|
||||
Obviously the title string literal is immediately available.
|
||||
The `someHero` variable in this example was set earlier in the file:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts' region='some-hero'}
|
||||
|
||||
The other providers create their values *lazily* when they're needed for injection.
|
||||
|
||||
|
||||
|
||||
{@a useclass}
|
||||
#### useClass - the *class provider*
|
||||
|
||||
The `useClass` provider creates and returns new instance of the specified class.
|
||||
|
||||
Use this technique to ***substitute an alternative implementation*** for a common or default class.
|
||||
The alternative could implement a different strategy, extend the default class,
|
||||
or fake the behavior of the real class in a test case.
|
||||
|
||||
We see two examples in the `HeroOfTheMonthComponent`:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts' region='use-class'}
|
||||
|
||||
The first provider is the *de-sugared*, expanded form of the most typical case in which the
|
||||
class to be created (`HeroService`) is also the provider's injection token.
|
||||
We wrote it in this long form to de-mystify the preferred short form.
|
||||
|
||||
The second provider substitutes the `DateLoggerService` for the `LoggerService`.
|
||||
The `LoggerService` is already registered at the `AppComponent` level.
|
||||
When _this component_ requests the `LoggerService`, it receives the `DateLoggerService` instead.
|
||||
This component and its tree of child components receive the `DateLoggerService` instance.
|
||||
Components outside the tree continue to receive the original `LoggerService` instance.The `DateLoggerService` inherits from `LoggerService`; it appends the current date/time to each message:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/date-logger.service.ts' region='date-logger-service'}
|
||||
|
||||
|
||||
|
||||
|
||||
{@a useexisting}
|
||||
#### useExisting - the *alias provider*
|
||||
|
||||
The `useExisting` provider maps one token to another.
|
||||
In effect, the first token is an ***alias*** for the service associated with second token,
|
||||
creating ***two ways to access the same service object***.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts' region='use-existing'}
|
||||
|
||||
Narrowing an API through an aliasing interface is _one_ important use case for this technique.
|
||||
We're aliasing for that very purpose here.
|
||||
Imagine that the `LoggerService` had a large API (it's actually only three methods and a property).
|
||||
We want to shrink that API surface to just the two members exposed by the `MinimalLogger` [*class-interface*](#class-interface):
|
||||
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/date-logger.service.ts' region='minimal-logger'}
|
||||
|
||||
The constructor's `logger` parameter is typed as `MinimalLogger` so only its two members are visible in TypeScript:
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/dependency-injection/minimal-logger-intellisense.png" alt="MinimalLogger restricted API"> </img>
|
||||
</figure>
|
||||
|
||||
Angular actually sets the `logger` parameter to the injector's full version of the `LoggerService`
|
||||
which happens to be the `DateLoggerService` thanks to the override provider registered previously via `useClass`.
|
||||
The following image, which displays the logging date, confirms the point:
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/dependency-injection/date-logger-entry.png" alt="DateLoggerService entry" width="300px"> </img>
|
||||
</figure>
|
||||
|
||||
|
||||
|
||||
|
||||
{@a usefactory}
|
||||
#### useFactory - the *factory provider*
|
||||
|
||||
The `useFactory` provider creates a dependency object by calling a factory function
|
||||
as seen in this example.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts' region='use-factory'}
|
||||
|
||||
Use this technique to ***create a dependency object***
|
||||
with a factory function whose inputs are some ***combination of injected services and local state***.
|
||||
|
||||
The *dependency object* doesn't have to be a class instance. It could be anything.
|
||||
In this example, the *dependency object* is a string of the names of the runners-up
|
||||
to the "Hero of the Month" contest.
|
||||
|
||||
The local state is the number `2`, the number of runners-up this component should show.
|
||||
We execute `runnersUpFactory` immediately with `2`.
|
||||
|
||||
The `runnersUpFactory` itself isn't the provider factory function.
|
||||
The true provider factory function is the function that `runnersUpFactory` returns.
|
||||
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/runners-up.ts' region='factory-synopsis'}
|
||||
|
||||
That returned function takes a winning `Hero` and a `HeroService` as arguments.
|
||||
|
||||
Angular supplies these arguments from injected values identified by
|
||||
the two *tokens* in the `deps` array.
|
||||
The two `deps` values are *tokens* that the injector uses
|
||||
to provide these factory function dependencies.
|
||||
|
||||
After some undisclosed work, the function returns the string of names
|
||||
and Angular injects it into the `runnersUp` parameter of the `HeroOfTheMonthComponent`.
|
||||
|
||||
The function retrieves candidate heroes from the `HeroService`,
|
||||
takes `2` of them to be the runners-up, and returns their concatenated names.
|
||||
Look at the <live-example name="cb-dependency-injection"></live-example>
|
||||
for the full source code.
|
||||
|
||||
|
||||
{@a tokens}
|
||||
|
||||
## Provider token alternatives: the *class-interface* and *OpaqueToken*
|
||||
|
||||
Angular dependency injection is easiest when the provider *token* is a class
|
||||
that is also the type of the returned dependency object (what we usually call the *service*).
|
||||
|
||||
But the token doesn't have to be a class and even when it is a class,
|
||||
it doesn't have to be the same type as the returned object.
|
||||
That's the subject of our next section.
|
||||
|
||||
<a id="class-interface"></a>
|
||||
### class-interface
|
||||
In the previous *Hero of the Month* example, we used the `MinimalLogger` class
|
||||
as the token for a provider of a `LoggerService`.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts' region='use-existing'}
|
||||
|
||||
The `MinimalLogger` is an abstract class.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/date-logger.service.ts' region='minimal-logger'}
|
||||
|
||||
We usually inherit from an abstract class.
|
||||
But `LoggerService` doesn't inherit from `MinimalLogger`. *No class* inherits from it.
|
||||
Instead, we use it like an interface.
|
||||
|
||||
Look again at the declaration for `DateLoggerService`
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/date-logger.service.ts' region='date-logger-service-signature'}
|
||||
|
||||
`DateLoggerService` inherits (extends) from `LoggerService`, not `MinimalLogger`.
|
||||
The `DateLoggerService` *implements* `MinimalLogger` as if `MinimalLogger` were an *interface*.
|
||||
|
||||
We call a class used in this way a ***class-interface***.
|
||||
The key benefit of a *class-interface* is that we can get the strong-typing of an interface
|
||||
and we can ***use it as a provider token*** in the same manner as a normal class.
|
||||
|
||||
A ***class-interface*** should define *only* the members that its consumers are allowed to call.
|
||||
Such a narrowing interface helps decouple the concrete class from its consumers.
|
||||
The `MinimalLogger` defines just two of the `LoggerClass` members.
|
||||
|
||||
#### Why *MinimalLogger* is a class and not an interface
|
||||
We can't use an interface as a provider token because
|
||||
interfaces are not JavaScript objects.
|
||||
They exist only in the TypeScript design space.
|
||||
They disappear after the code is transpiled to JavaScript.
|
||||
|
||||
A provider token must be a real JavaScript object of some kind:
|
||||
a function, an object, a string ... a class.
|
||||
|
||||
Using a class as an interface gives us the characteristics of an interface in a JavaScript object.
|
||||
|
||||
The minimize memory cost, the class should have *no implementation*.
|
||||
The `MinimalLogger` transpiles to this unoptimized, pre-minified JavaScript:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/date-logger.service.ts' region='minimal-logger-transpiled'}
|
||||
|
||||
It never grows larger no matter how many members we add *as long as they are typed but not implemented*.
|
||||
|
||||
|
||||
{@a opaque-token}
|
||||
### OpaqueToken
|
||||
|
||||
Dependency objects can be simple values like dates, numbers and strings or
|
||||
shapeless objects like arrays and functions.
|
||||
|
||||
Such objects don't have application interfaces and therefore aren't well represented by a class.
|
||||
They're better represented by a token that is both unique and symbolic,
|
||||
a JavaScript object that has a friendly name but won't conflict with
|
||||
another token that happens to have the same name.
|
||||
|
||||
The `OpaqueToken` has these characteristics.
|
||||
We encountered them twice in the *Hero of the Month* example,
|
||||
in the *title* value provider and in the *runnersUp* factory provider.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts' region='provide-opaque-token'}
|
||||
|
||||
We created the `TITLE` token like this:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts' region='opaque-token'}
|
||||
|
||||
|
||||
|
||||
{@a di-inheritance}
|
||||
|
||||
## Inject into a derived class
|
||||
We must take care when writing a component that inherits from another component.
|
||||
If the base component has injected dependencies,
|
||||
we must re-provide and re-inject them in the derived class
|
||||
and then pass them down to the base class through the constructor.
|
||||
|
||||
In this contrived example, `SortedHeroesComponent` inherits from `HeroesBaseComponent`
|
||||
to display a *sorted* list of heroes.
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/dependency-injection/sorted-heroes.png" alt="Sorted Heroes"> </img>
|
||||
</figure>
|
||||
|
||||
The `HeroesBaseComponent` could stand on its own.
|
||||
It demands its own instance of the `HeroService` to get heroes
|
||||
and displays them in the order they arrive from the database.
|
||||
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/sorted-heroes.component.ts' region='heroes-base'}
|
||||
|
||||
|
||||
We strongly prefer simple constructors. They should do little more than initialize variables.
|
||||
This rule makes the component safe to construct under test without fear that it will do something dramatic like talk to the server.
|
||||
That's why we call the `HeroService` from within the `ngOnInit` rather than the constructor.
|
||||
|
||||
We explain the mysterious `afterGetHeroes` below.Users want to see the heroes in alphabetical order.
|
||||
Rather than modify the original component, we sub-class it and create a
|
||||
`SortedHeroesComponent` that sorts the heroes before presenting them.
|
||||
The `SortedHeroesComponent` lets the base class fetch the heroes.
|
||||
(we said it was contrived).
|
||||
|
||||
Unfortunately, Angular cannot inject the `HeroService` directly into the base class.
|
||||
We must provide the `HeroService` again for *this* component,
|
||||
then pass it down to the base class inside the constructor.
|
||||
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/sorted-heroes.component.ts' region='sorted-heroes'}
|
||||
|
||||
Now take note of the `afterGetHeroes` method.
|
||||
Our first instinct was to create an `ngOnInit` method in `SortedHeroesComponent` and do the sorting there.
|
||||
But Angular calls the *derived* class's `ngOnInit` *before* calling the base class's `ngOnInit`
|
||||
so we'd be sorting the heroes array *before they arrived*. That produces a nasty error.
|
||||
|
||||
Overriding the base class's `afterGetHeroes` method solves the problem
|
||||
|
||||
These complications argue for *avoiding component inheritance*.
|
||||
|
||||
|
||||
{@a find-parent}
|
||||
|
||||
## Find a parent component by injection
|
||||
|
||||
Application components often need to share information.
|
||||
We prefer the more loosely coupled techniques such as data binding and service sharing.
|
||||
But sometimes it makes sense for one component to have a direct reference to another component
|
||||
perhaps to access values or call methods on that component.
|
||||
|
||||
Obtaining a component reference is a bit tricky in Angular.
|
||||
Although an Angular application is a tree of components,
|
||||
there is no public API for inspecting and traversing that tree.
|
||||
|
||||
There is an API for acquiring a child reference
|
||||
(checkout `Query`, `QueryList`, `ViewChildren`, and `ContentChildren`).
|
||||
|
||||
There is no public API for acquiring a parent reference.
|
||||
But because every component instance is added to an injector's container,
|
||||
we can use Angular dependency injection to reach a parent component.
|
||||
|
||||
This section describes some techniques for doing that.
|
||||
|
||||
<a id="known-parent"></a>
|
||||
### Find a parent component of known type
|
||||
|
||||
We use standard class injection to acquire a parent component whose type we know.
|
||||
|
||||
In the following example, the parent `AlexComponent` has several children including a `CathyComponent`:
|
||||
|
||||
{@a alex}
|
||||
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alex-1'}
|
||||
|
||||
*Cathy* reports whether or not she has access to *Alex*
|
||||
after injecting an `AlexComponent` into her constructor:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='cathy'}
|
||||
|
||||
We added the [@Optional](#optional) qualifier for safety but
|
||||
the <live-example name="cb-dependency-injection"></live-example>
|
||||
confirms that the `alex` parameter is set.
|
||||
|
||||
<a id="base-parent"></a>
|
||||
### Cannot find a parent by its base class
|
||||
|
||||
What if we do *not* know the concrete parent component class?
|
||||
|
||||
A re-usable component might be a child of multiple components.
|
||||
Imagine a component for rendering breaking news about a financial instrument.
|
||||
For sound (cough) business reasons, this news component makes frequent calls
|
||||
directly into its parent instrument as changing market data stream by.
|
||||
|
||||
The app probably defines more than a dozen financial instrument components.
|
||||
If we're lucky, they all implement the same base class
|
||||
whose API our `NewsComponent` understands.
|
||||
|
||||
Looking for components that implement an interface would be better.
|
||||
That's not possible because TypeScript interfaces disappear from the transpiled JavaScript
|
||||
which doesn't support interfaces. There's no artifact we could look for.We're not claiming this is good design.
|
||||
We are asking *can a component inject its parent via the parent's base class*?
|
||||
|
||||
The sample's `CraigComponent` explores this question. [Looking back](#alex)
|
||||
we see that the `Alex` component *extends* (*inherits*) from a class named `Base`.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alex-class-signature'}
|
||||
|
||||
The `CraigComponent` tries to inject `Base` into its `alex` constructor parameter and reports if it succeeded.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='craig'}
|
||||
|
||||
Unfortunately, this does not work.
|
||||
The <live-example name="cb-dependency-injection"></live-example>
|
||||
confirms that the `alex` parameter is null.
|
||||
*We cannot inject a parent by its base class.*
|
||||
|
||||
<a id="class-interface-parent"></a>
|
||||
### Find a parent by its class-interface
|
||||
|
||||
We can find a parent component with a [class-interface](#class-interface).
|
||||
|
||||
The parent must cooperate by providing an *alias* to itself in the name of a *class-interface* token.
|
||||
|
||||
Recall that Angular always adds a component instance to its own injector;
|
||||
that's why we could inject *Alex* into *Cathy* [earlier](#known-parent).
|
||||
|
||||
We write an [*alias provider*](#useexisting) — a `provide` object literal with a `useExisting` definition —
|
||||
that creates an *alternative* way to inject the same component instance
|
||||
and add that provider to the `providers` array of the `@Component` metadata for the `AlexComponent`:
|
||||
|
||||
{@a alex-providers}
|
||||
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alex-providers'}
|
||||
|
||||
[Parent](#parent-token) is the provider's *class-interface* token.
|
||||
The [*forwardRef*](#forwardref) breaks the circular reference we just created by having the `AlexComponent` refer to itself.
|
||||
|
||||
*Carol*, the third of *Alex*'s child components, injects the parent into its `parent` parameter, the same way we've done it before:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='carol-class'}
|
||||
|
||||
Here's *Alex* and family in action:
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/dependency-injection/alex.png" alt="Alex in action"> </img>
|
||||
</figure>
|
||||
|
||||
|
||||
|
||||
{@a parent-tree}
|
||||
### Find the parent in a tree of parents
|
||||
|
||||
Imagine one branch of a component hierarchy: *Alice* -> *Barry* -> *Carol*.
|
||||
Both *Alice* and *Barry* implement the `Parent` *class-interface*.
|
||||
|
||||
*Barry* is the problem. He needs to reach his parent, *Alice*, and also be a parent to *Carol*.
|
||||
That means he must both *inject* the `Parent` *class-interface* to get *Alice* and
|
||||
*provide* a `Parent` to satisfy *Carol*.
|
||||
|
||||
Here's *Barry*:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='barry'}
|
||||
|
||||
*Barry*'s `providers` array looks just like [*Alex*'s](#alex-providers).
|
||||
If we're going to keep writing [*alias providers*](#useexisting) like this we should create a [helper function](#provideparent).
|
||||
|
||||
For now, focus on *Barry*'s constructor:
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="Barry's constructor">
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='barry-ctor'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="Carol's constructor">
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='carol-ctor'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
It's identical to *Carol*'s constructor except for the additional `@SkipSelf` decorator.
|
||||
|
||||
`@SkipSelf` is essential for two reasons:
|
||||
|
||||
1. It tells the injector to start its search for a `Parent` dependency in a component *above* itself,
|
||||
which *is* what parent means.
|
||||
|
||||
2. Angular throws a cyclic dependency error if we omit the `@SkipSelf` decorator.
|
||||
|
||||
`Cannot instantiate cyclic dependency! (BethComponent -> Parent -> BethComponent)`
|
||||
|
||||
Here's *Alice*, *Barry* and family in action:
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/dependency-injection/alice.png" alt="Alice in action"> </img>
|
||||
</figure>
|
||||
|
||||
|
||||
|
||||
{@a parent-token}
|
||||
### The *Parent* class-interface
|
||||
We [learned earlier](#class-interface) that a *class-interface* is an abstract class used as an interface rather than as a base class.
|
||||
|
||||
Our example defines a `Parent` *class-interface* .
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='parent'}
|
||||
|
||||
The `Parent` *class-interface* defines a `name` property with a type declaration but *no implementation*.,
|
||||
The `name` property is the only member of a parent component that a child component can call.
|
||||
Such a narrowing interface helps decouple the child component class from its parent components.
|
||||
|
||||
A component that could serve as a parent *should* implement the *class-interface* as the `AliceComponent` does:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alice-class-signature'}
|
||||
|
||||
Doing so adds clarity to the code. But it's not technically necessary.
|
||||
Although the `AlexComponent` has a `name` property (as required by its `Base` class)
|
||||
its class signature doesn't mention `Parent`:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alex-class-signature'}
|
||||
|
||||
|
||||
The `AlexComponent` *should* implement `Parent` as a matter of proper style.
|
||||
It doesn't in this example *only* to demonstrate that the code will compile and run without the interface
|
||||
|
||||
|
||||
{@a provideparent}
|
||||
### A *provideParent* helper function
|
||||
|
||||
Writing variations of the same parent *alias provider* gets old quickly,
|
||||
especially this awful mouthful with a [*forwardRef*](#forwardref):
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alex-providers'}
|
||||
|
||||
We can extract that logic into a helper function like this:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='provide-the-parent'}
|
||||
|
||||
Now we can add a simpler, more meaningful parent provider to our components:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alice-providers'}
|
||||
|
||||
We can do better. The current version of the helper function can only alias the `Parent` *class-interface*.
|
||||
Our application might have a variety of parent types, each with its own *class-interface* token.
|
||||
|
||||
Here's a revised version that defaults to `parent` but also accepts an optional second parameter for a different parent *class-interface*.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='provide-parent'}
|
||||
|
||||
And here's how we could use it with a different parent type:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='beth-providers'}
|
||||
|
||||
|
||||
|
||||
{@a forwardref}
|
||||
|
||||
## Break circularities with a forward class reference (*forwardRef*)
|
||||
|
||||
The order of class declaration matters in TypeScript.
|
||||
We can't refer directly to a class until it's been defined.
|
||||
|
||||
This isn't usually a problem, especially if we adhere to the recommended *one class per file* rule.
|
||||
But sometimes circular references are unavoidable.
|
||||
We're in a bind when class 'A refers to class 'B' and 'B' refers to 'A'.
|
||||
One of them has to be defined first.
|
||||
|
||||
The Angular `forwardRef` function creates an *indirect* reference that Angular can resolve later.
|
||||
|
||||
The *Parent Finder* sample is full of circular class references that are impossible to break.
|
||||
We face this dilemma when a class makes *a reference to itself*
|
||||
as does the `AlexComponent` in its `providers` array.
|
||||
The `providers` array is a property of the `@Component` decorator function which must
|
||||
appear *above* the class definition.
|
||||
|
||||
We break the circularity with `forwardRef`:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alex-providers'}
|
||||
|
@ -1,138 +0,0 @@
|
||||
@title
|
||||
Dynamic Component Loader
|
||||
|
||||
@intro
|
||||
Load components dynamically
|
||||
|
||||
@description
|
||||
Component templates are not always fixed. An application may need to load new components at runtime.
|
||||
|
||||
In this cookbook we show how to use `ComponentFactoryResolver` to add components dynamically.
|
||||
|
||||
<a id="toc"></a>## Table of contents
|
||||
|
||||
[Dynamic Component Loading](#dynamic-loading)
|
||||
|
||||
[Where to load the component](#where-to-load)
|
||||
|
||||
[Loading components](#loading-components)
|
||||
|
||||
<a id="dynamic-loading"></a>## Dynamic Component Loading
|
||||
|
||||
The following example shows how to build a dynamic ad banner.
|
||||
|
||||
The hero agency is planning an ad campaign with several different ads cycling through the banner.
|
||||
|
||||
New ad components are added frequently by several different teams. This makes it impractical to use a template with a static component structure.
|
||||
|
||||
Instead we need a way to load a new component without a fixed reference to the component in the ad banner's template.
|
||||
|
||||
Angular comes with its own API for loading components dynamically. In the following sections you will learn how to use it.
|
||||
|
||||
|
||||
<a id="where-to-load"></a>## Where to load the component
|
||||
|
||||
Before components can be added we have to define an anchor point to mark where components can be inserted dynamically.
|
||||
|
||||
The ad banner uses a helper directive called `AdDirective` to mark valid insertion points in the template.
|
||||
|
||||
|
||||
{@example 'cb-dynamic-component-loader/ts/src/app/ad.directive.ts'}
|
||||
|
||||
`AdDirective` injects `ViewContainerRef` to gain access to the view container of the element that will become the host of the dynamically added component.
|
||||
|
||||
<a id="loading-components"></a>## Loading components
|
||||
|
||||
The next step is to implement the ad banner. Most of the implementation is in `AdBannerComponent`.
|
||||
|
||||
We start by adding a `template` element with the `AdDirective` directive applied.
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="ad-banner.component.ts">
|
||||
{@example 'cb-dynamic-component-loader/ts/src/app/ad-banner.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ad.service.ts">
|
||||
{@example 'cb-dynamic-component-loader/ts/src/app/ad.service.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ad-item.ts">
|
||||
{@example 'cb-dynamic-component-loader/ts/src/app/ad-item.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="app.module.ts">
|
||||
{@example 'cb-dynamic-component-loader/ts/src/app/app.module.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="app.component">
|
||||
{@example 'cb-dynamic-component-loader/ts/src/app/app.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
The `template` element decorated with the `ad-host` directive marks where dynamically loaded components will be added.
|
||||
|
||||
Using a `template` element is recommended since it doesn't render any additional output.
|
||||
|
||||
|
||||
{@example 'cb-dynamic-component-loader/ts/src/app/ad-banner.component.ts' region='ad-host'}
|
||||
|
||||
### Resolving Components
|
||||
|
||||
`AdBanner` takes an array of `AdItem` objects as input. `AdItem` objects specify the type of component to load and any data to bind to the component.
|
||||
|
||||
The ad components making up the ad campaign are returned from `AdService`.
|
||||
|
||||
Passing an array of components to `AdBannerComponent` allows for a dynamic list of ads without static elements in the template.
|
||||
|
||||
`AdBannerComponent` cycles through the array of `AdItems` and loads the corresponding components on an interval. Every 3 seconds a new component is loaded.
|
||||
|
||||
`ComponentFactoryResolver` is used to resolve a `ComponentFactory` for each specific component. The component factory is need to create an instance of the component.
|
||||
|
||||
`ComponentFactories` are generated by the Angular compiler.
|
||||
|
||||
Generally the compiler will generate a component factory for any component referenced in a template.
|
||||
|
||||
With dynamically loaded components there are no selector references in the templates since components are loaded at runtime. In order to ensure that the compiler will still generate a factory, dynamically loaded components have to be added to their `NgModule`'s `entryComponents` array.
|
||||
|
||||
|
||||
{@example 'cb-dynamic-component-loader/ts/src/app/app.module.ts' region='entry-components'}
|
||||
|
||||
Components are added to the template by calling `createComponent` on the `ViewContainerRef` reference.
|
||||
|
||||
`createComponent` returns a reference to the loaded component. The component reference can be used to pass input data or call methods to interact with the component.
|
||||
|
||||
In the Ad banner, all components implement a common `AdComponent` interface to standardize the api for passing data to the components.
|
||||
|
||||
Two sample components and the `AdComponent` interface are shown below:
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="hero-job-ad.component.ts">
|
||||
{@example 'cb-dynamic-component-loader/ts/src/app/hero-job-ad.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="hero-profile.component.ts">
|
||||
{@example 'cb-dynamic-component-loader/ts/src/app/hero-profile.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ad.component.ts">
|
||||
{@example 'cb-dynamic-component-loader/ts/src/app/ad.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
The final ad banner looks like this:
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/dynamic-component-loader/ads.gif" alt="Ads"> </img>
|
||||
</figure>
|
||||
|
@ -1,168 +0,0 @@
|
||||
@title
|
||||
Dynamic Forms
|
||||
|
||||
@intro
|
||||
Render dynamic forms with FormGroup
|
||||
|
||||
@description
|
||||
We can't always justify the cost and time to build handcrafted forms,
|
||||
especially if we'll need a great number of them, they're similar to each other, and they change frequently
|
||||
to meet rapidly changing business and regulatory requirements.
|
||||
|
||||
It may be more economical to create the forms dynamically, based on metadata that describe the business object model.
|
||||
|
||||
In this cookbook we show how to use `formGroup` to dynamically render a simple form with different control types and validation.
|
||||
It's a primitive start.
|
||||
It might evolve to support a much richer variety of questions, more graceful rendering, and superior user experience.
|
||||
All such greatness has humble beginnings.
|
||||
|
||||
In our example we use a dynamic form to build an online application experience for heroes seeking employment.
|
||||
The agency is constantly tinkering with the application process.
|
||||
We can create the forms on the fly *without changing our application code*.
|
||||
<a id="toc"></a>## Table of contents
|
||||
|
||||
[Bootstrap](#bootstrap)
|
||||
|
||||
[Question Model](#object-model)
|
||||
|
||||
[Form Component](#form-component)
|
||||
|
||||
[Questionnaire Metadata](#questionnaire-metadata)
|
||||
|
||||
[Dynamic Template](#dynamic-template)
|
||||
**See the <live-example name="cb-dynamic-form"></live-example>**.
|
||||
|
||||
<a id="bootstrap"></a>## Bootstrap
|
||||
|
||||
We start by creating an `NgModule` called `AppModule`.
|
||||
|
||||
In our example we will be using Reactive Forms.
|
||||
|
||||
Reactive Forms belongs to a different `NgModule` called `ReactiveFormsModule`, so in order to access any Reactive Forms directives, we have to import `ReactiveFormsModule` from the `@angular/forms` library.
|
||||
|
||||
We bootstrap our `AppModule` in main.ts.
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="app.module.ts">
|
||||
{@example 'cb-dynamic-form/ts/src/app/app.module.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="main.ts">
|
||||
{@example 'cb-dynamic-form/ts/src/main.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
|
||||
<a id="object-model"></a>## Question Model
|
||||
|
||||
The next step is to define an object model that can describe all scenarios needed by the form functionality.
|
||||
The hero application process involves a form with a lot of questions.
|
||||
The "question" is the most fundamental object in the model.
|
||||
|
||||
We have created `QuestionBase` as the most fundamental question class.
|
||||
|
||||
|
||||
{@example 'cb-dynamic-form/ts/src/app/question-base.ts'}
|
||||
|
||||
From this base we derived two new classes in `TextboxQuestion` and `DropdownQuestion` that represent Textbox and Dropdown questions.
|
||||
The idea is that the form will be bound to specific question types and render the appropriate controls dynamically.
|
||||
|
||||
`TextboxQuestion` supports multiple html5 types like text, email, url etc via the `type` property.
|
||||
|
||||
|
||||
{@example 'cb-dynamic-form/ts/src/app/question-textbox.ts'}
|
||||
|
||||
`DropdownQuestion` presents a list of choices in a select box.
|
||||
|
||||
|
||||
{@example 'cb-dynamic-form/ts/src/app/question-dropdown.ts'}
|
||||
|
||||
Next we have defined `QuestionControlService`, a simple service for transforming our questions to a `FormGroup`.
|
||||
In a nutshell, the form group consumes the metadata from the question model and allows us to specify default values and validation rules.
|
||||
|
||||
|
||||
{@example 'cb-dynamic-form/ts/src/app/question-control.service.ts'}
|
||||
|
||||
<a id="form-component"></a>## Question form components
|
||||
Now that we have defined the complete model we are ready to create components to represent the dynamic form.
|
||||
`DynamicFormComponent` is the entry point and the main container for the form.
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="dynamic-form.component.html">
|
||||
{@example 'cb-dynamic-form/ts/src/app/dynamic-form.component.html'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="dynamic-form.component.ts">
|
||||
{@example 'cb-dynamic-form/ts/src/app/dynamic-form.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
It presents a list of questions, each question bound to a `<df-question>` component element.
|
||||
The `<df-question>` tag matches the `DynamicFormQuestionComponent`,
|
||||
the component responsible for rendering the details of each _individual_ question based on values in the data-bound question object.
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="dynamic-form-question.component.html">
|
||||
{@example 'cb-dynamic-form/ts/src/app/dynamic-form-question.component.html'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="dynamic-form-question.component.ts">
|
||||
{@example 'cb-dynamic-form/ts/src/app/dynamic-form-question.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
Notice this component can present any type of question in our model.
|
||||
We only have two types of questions at this point but we can imagine many more.
|
||||
The `ngSwitch` determines which type of question to display.
|
||||
|
||||
In both components we're relying on Angular's **formGroup** to connect the template HTML to the
|
||||
underlying control objects, populated from the question model with display and validation rules.
|
||||
|
||||
`formControlName` and `formGroup` are directives defined in `ReactiveFormsModule`. Our templates can can access these directives directly since we imported `ReactiveFormsModule` from `AppModule`.
|
||||
<a id="questionnaire-metadata"></a>## Questionnaire data`DynamicFormComponent` expects the list of questions in the form of an array bound to `@Input() questions`.
|
||||
|
||||
The set of questions we have defined for the job application is returned from the `QuestionService`.
|
||||
In a real app we'd retrieve these questions from storage.
|
||||
|
||||
The key point is that we control the hero job application questions entirely through the objects returned from `QuestionService`.
|
||||
Questionnaire maintenance is a simple matter of adding, updating, and removing objects from the `questions` array.
|
||||
|
||||
|
||||
{@example 'cb-dynamic-form/ts/src/app/question.service.ts'}
|
||||
|
||||
Finally, we display an instance of the form in the `AppComponent` shell.
|
||||
|
||||
|
||||
{@example 'cb-dynamic-form/ts/src/app/app.component.ts'}
|
||||
|
||||
<a id="dynamic-template"></a>## Dynamic Template
|
||||
Although in this example we're modelling a job application for heroes, there are no references to any specific hero question
|
||||
outside the objects returned by `QuestionService`.
|
||||
|
||||
This is very important since it allows us to repurpose the components for any type of survey
|
||||
as long as it's compatible with our *question* object model.
|
||||
The key is the dynamic data binding of metadata used to render the form
|
||||
without making any hardcoded assumptions about specific questions.
|
||||
In addition to control metadata, we are also adding validation dynamically.
|
||||
|
||||
The *Save* button is disabled until the form is in a valid state.
|
||||
When the form is valid, we can click *Save* and the app renders the current form values as JSON.
|
||||
This proves that any user input is bound back to the data model.
|
||||
Saving and retrieving the data is an exercise for another time.
|
||||
The final form looks like this:
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/dynamic-form/dynamic-form.png" alt="Dynamic-Form"> </img>
|
||||
</figure>
|
||||
|
||||
[Back to top](#top)
|
@ -1,486 +0,0 @@
|
||||
@title
|
||||
Form Validation
|
||||
|
||||
@intro
|
||||
Validate user's form entries
|
||||
|
||||
@description
|
||||
|
||||
|
||||
{@a top}
|
||||
We can improve overall data quality by validating user input for accuracy and completeness.
|
||||
|
||||
In this cookbook we show how to validate user input in the UI and display useful validation messages
|
||||
using first the template-driven forms and then the reactive forms approach.
|
||||
Learn more about these choices in the [Forms chapter.](../guide/forms.html)
|
||||
|
||||
|
||||
{@a toc}
|
||||
## Table of Contents
|
||||
|
||||
[Simple Template-Driven Forms](#template1)
|
||||
|
||||
[Template-Driven Forms with validation messages in code](#template2)
|
||||
|
||||
[Reactive Forms with validation in code](#reactive)
|
||||
|
||||
[Custom validation](#custom-validation)
|
||||
|
||||
[Testing](#testing)
|
||||
|
||||
|
||||
{@a live-example}
|
||||
**Try the live example to see and download the full cookbook source code**
|
||||
<live-example name="cb-form-validation" embedded=true img="cookbooks/form-validation/plunker.png">
|
||||
|
||||
</live-example>
|
||||
|
||||
|
||||
|
||||
|
||||
{@a template1}
|
||||
## Simple Template-Driven Forms
|
||||
|
||||
In the template-driven approach, you arrange
|
||||
[form elements](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Forms_in_HTML) in the component's template.
|
||||
|
||||
You add Angular form directives (mostly directives beginning `ng...`) to help
|
||||
Angular construct a corresponding internal control model that implements form functionality.
|
||||
We say that the control model is _implicit_ in the template.
|
||||
|
||||
To validate user input, you add [HTML validation attributes](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation)
|
||||
to the elements. Angular interprets those as well, adding validator functions to the control model.
|
||||
|
||||
Angular exposes information about the state of the controls including
|
||||
whether the user has "touched" the control or made changes and if the control values are valid.
|
||||
|
||||
In the first template validation example,
|
||||
we add more HTML to read that control state and update the display appropriately.
|
||||
Here's an excerpt from the template html for a single input box control bound to the hero name:
|
||||
|
||||
{@example 'cb-form-validation/ts/src/app/template/hero-form-template1.component.html' region='name-with-error-msg'}
|
||||
|
||||
Note the following:
|
||||
- The `<input>` element carries the HTML validation attributes: `required`, `minlength`, and `maxlength`.
|
||||
|
||||
- We set the `name` attribute of the input box to `"name"` so Angular can track this input element and associate it
|
||||
with an Angular form control called `name` in its internal control model.
|
||||
|
||||
- We use the `[(ngModel)]` directive to two-way data bind the input box to the `hero.name` property.
|
||||
|
||||
- We set a template variable (`#name`) to the value `"ngModel"` (always `ngModel`).
|
||||
This gives us a reference to the Angular `NgModel` directive
|
||||
associated with this control that we can use _in the template_
|
||||
to check for control states such as `valid` and `dirty`.
|
||||
|
||||
- The `*ngIf` on `<div>` element reveals a set of nested message `divs` but only if there are "name" errors and
|
||||
the control is either `dirty` or `touched`.
|
||||
|
||||
- Each nested `<div>` can present a custom message for one of the possible validation errors.
|
||||
We've prepared messages for `required`, `minlength`, and `maxlength`.
|
||||
|
||||
The full template repeats this kind of layout for each data entry control on the form.
|
||||
#### Why check _dirty_ and _touched_?
|
||||
|
||||
We shouldn't show errors for a new hero before the user has had a chance to edit the value.
|
||||
The checks for `dirty` and `touched` prevent premature display of errors.
|
||||
|
||||
Learn about `dirty` and `touched` in the [Forms](../guide/forms.html) chapter.The component class manages the hero model used in the data binding
|
||||
as well as other code to support the view.
|
||||
|
||||
|
||||
{@example 'cb-form-validation/ts/src/app/template/hero-form-template1.component.ts' region='class'}
|
||||
|
||||
Use this template-driven validation technique when working with static forms with simple, standard validation rules.
|
||||
|
||||
Here are the complete files for the first version of `HeroFormTemplateCompononent` in the template-driven approach:
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="template/hero-form-template1.component.html">
|
||||
{@example 'cb-form-validation/ts/src/app/template/hero-form-template1.component.html'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="template/hero-form-template1.component.ts">
|
||||
{@example 'cb-form-validation/ts/src/app/template/hero-form-template1.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
|
||||
|
||||
|
||||
{@a template2}
|
||||
## Template-Driven Forms with validation messages in code
|
||||
|
||||
While the layout is straightforward,
|
||||
there are obvious shortcomings with the way we handle validation messages:
|
||||
|
||||
* It takes a lot of HTML to represent all possible error conditions.
|
||||
This gets out of hand when there are many controls and many validation rules.
|
||||
|
||||
* We're not fond of so much JavaScript logic in HTML.
|
||||
|
||||
* The messages are static strings, hard-coded into the template.
|
||||
We often require dynamic messages that we should shape in code.
|
||||
|
||||
We can move the logic and the messages into the component with a few changes to
|
||||
the template and component.
|
||||
|
||||
Here's the hero name again, excerpted from the revised template ("Template 2"), next to the original version:
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="hero-form-template2.component.html (name #2)">
|
||||
{@example 'cb-form-validation/ts/src/app/template/hero-form-template2.component.html' region='name-with-error-msg'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="hero-form-template1.component.html (name #1)">
|
||||
{@example 'cb-form-validation/ts/src/app/template/hero-form-template1.component.html' region='name-with-error-msg'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
The `<input>` element HTML is almost the same. There are noteworthy differences:
|
||||
- The hard-code error message `<divs>` are gone.
|
||||
|
||||
- There's a new attribute, `forbiddenName`, that is actually a custom validation directive.
|
||||
It invalidates the control if the user enters "bob" anywhere in the name ([try it](#live-example)).
|
||||
We discuss [custom validation directives](#custom-validation) later in this cookbook.
|
||||
|
||||
- The `#name` template variable is gone because we no longer refer to the Angular control for this element.
|
||||
|
||||
- Binding to the new `formErrors.name` property is sufficent to display all name validation error messages.
|
||||
|
||||
#### Component class
|
||||
The original component code stays the same.
|
||||
We _added_ new code to acquire the Angular form control and compose error messages.
|
||||
|
||||
The first step is to acquire the form control that Angular created from the template by querying for it.
|
||||
|
||||
Look back at the top of the component template where we set the
|
||||
`#heroForm` template variable in the `<form>` element:
|
||||
|
||||
{@example 'cb-form-validation/ts/src/app/template/hero-form-template1.component.html' region='form-tag'}
|
||||
|
||||
The `heroForm` variable is a reference to the control model that Angular derived from the template.
|
||||
We tell Angular to inject that model into the component class's `currentForm` property using a `@ViewChild` query:
|
||||
|
||||
{@example 'cb-form-validation/ts/src/app/template/hero-form-template2.component.ts' region='view-child'}
|
||||
|
||||
Some observations:
|
||||
|
||||
- Angular `@ViewChild` queries for a template variable when you pass it
|
||||
the name of that variable as a string (`'heroForm'` in this case).
|
||||
|
||||
- The `heroForm` object changes several times during the life of the component, most notably when we add a new hero.
|
||||
We'll have to re-inspect it periodically.
|
||||
|
||||
- Angular calls the `ngAfterViewChecked` [lifecycle hook method](../guide/lifecycle-hooks.html#afterview)
|
||||
when anything changes in the view.
|
||||
That's the right time to see if there's a new `heroForm` object.
|
||||
|
||||
- When there _is_ a new `heroForm` model, we subscribe to its `valueChanged` _Observable_ property.
|
||||
The `onValueChanged` handler looks for validation errors after every user keystroke.
|
||||
|
||||
{@example 'cb-form-validation/ts/src/app/template/hero-form-template2.component.ts' region='handler'}
|
||||
|
||||
The `onValueChanged` handler interprets user data entry.
|
||||
The `data` object passed into the handler contains the current element values.
|
||||
The handler ignores them. Instead, it iterates over the fields of the component's `formErrors` object.
|
||||
|
||||
The `formErrors` is a dictionary of the hero fields that have validation rules and their current error messages.
|
||||
Only two hero properties have validation rules, `name` and `power`.
|
||||
The messages are empty strings when the hero data are valid.
|
||||
|
||||
For each field, the handler
|
||||
- clears the prior error message if any
|
||||
- acquires the field's corresponding Angular form control
|
||||
- if such a control exists _and_ its been changed ("dirty") _and_ its invalid ...
|
||||
- the handler composes a consolidated error message for all of the control's errors.
|
||||
|
||||
We'll need some error messages of course, a set for each validated property, one message per validation rule:
|
||||
|
||||
{@example 'cb-form-validation/ts/src/app/template/hero-form-template2.component.ts' region='messages'}
|
||||
|
||||
Now every time the user makes a change, the `onValueChanged` handler checks for validation errors and produces messages accordingly.
|
||||
|
||||
### Is this an improvement?
|
||||
|
||||
Clearly the template got substantially smaller while the component code got substantially larger.
|
||||
It's not easy to see the benefit when there are just three fields and only two of them have validation rules.
|
||||
|
||||
Consider what happens as we increase the number of validated fields and rules.
|
||||
In general, HTML is harder to read and maintain than code.
|
||||
The initial template was already large and threatening to get rapidly worse as we add more validation message `<divs>`.
|
||||
|
||||
After moving the validation messaging to the component,
|
||||
the template grows more slowly and proportionally.
|
||||
Each field has approximately the same number of lines no matter its number of validation rules.
|
||||
The component also grows proportionally, at the rate of one line per validated field
|
||||
and one line per validation message.
|
||||
|
||||
Both trends are manageable.
|
||||
|
||||
Now that the messages are in code, we have more flexibility. We can compose messages more intelligently.
|
||||
We can refactor the messages out of the component, perhaps to a service class that retrieves them from the server.
|
||||
In short, there are more opportunities to improve message handling now that text and logic have moved from template to code.
|
||||
|
||||
### _FormModule_ and template-driven forms
|
||||
|
||||
Angular has two different forms modules — `FormsModule` and `ReactiveFormsModule` —
|
||||
that correspond with the two approaches to form development.
|
||||
Both modules come from the same `@angular/forms` library package.
|
||||
|
||||
We've been reviewing the "Template-driven" approach which requires the `FormsModule`
|
||||
Here's how we imported it in the `HeroFormTemplateModule`.
|
||||
|
||||
|
||||
{@example 'cb-form-validation/ts/src/app/template/hero-form-template.module.ts'}
|
||||
|
||||
|
||||
We haven't talked about the `SharedModule` or its `SubmittedComponent` which appears at the bottom of every
|
||||
form template in this cookbook.
|
||||
|
||||
They're not germane to the validation story. Look at the [live example](#live-example) if you're interested.
|
||||
|
||||
|
||||
|
||||
{@a reactive}
|
||||
## Reactive Forms
|
||||
|
||||
In the template-driven approach, you markup the template with form elements, validation attributes,
|
||||
and `ng...` directives from the Angular `FormsModule`.
|
||||
At runtime, Angular interprets the template and derives its _form control model_.
|
||||
|
||||
**Reactive Forms** takes a different approach.
|
||||
You create the form control model in code. You write the template with form elements
|
||||
and`form...` directives from the Angular `ReactiveFormsModule`.
|
||||
At runtime, Angular binds the template elements to your control model based on your instructions.
|
||||
|
||||
This approach requires a bit more effort. *You have to write the control model and manage it*.
|
||||
|
||||
In return, you can
|
||||
* add, change, and remove validation functions on the fly
|
||||
* manipulate the control model dynamically from within the component
|
||||
* [test](#testing) validation and control logic with isolated unit tests.
|
||||
|
||||
The third cookbook sample re-writes the hero form in _reactive forms_ style.
|
||||
|
||||
### Switch to the _ReactiveFormsModule_
|
||||
The reactive forms classes and directives come from the Angular `ReactiveFormsModule`, not the `FormsModule`.
|
||||
The application module for the "Reactive Forms" feature in this sample looks like this:
|
||||
|
||||
{@example 'cb-form-validation/ts/src/app/reactive/hero-form-reactive.module.ts'}
|
||||
|
||||
The "Reactive Forms" feature module and component are in the `src/app/reactive` folder.
|
||||
Let's focus on the `HeroFormReactiveComponent` there, starting with its template.
|
||||
|
||||
### Component template
|
||||
|
||||
We begin by changing the `<form>` tag so that it binds the Angular `formGroup` directive in the template
|
||||
to the `heroForm` property in the component class.
|
||||
The `heroForm` is the control model that the component class builds and maintains.
|
||||
|
||||
|
||||
{@example 'cb-form-validation/ts/src/app/reactive/hero-form-reactive.component.html' region='form-tag'}
|
||||
|
||||
Then we modify the template HTML elements to match the _reactive forms_ style.
|
||||
Here is the "name" portion of the template again, revised for reactive forms and compared with the template-driven version:
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="hero-form-reactive.component.html (name #3)">
|
||||
{@example 'cb-form-validation/ts/src/app/reactive/hero-form-reactive.component.html' region='name-with-error-msg'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="hero-form-template1.component.html (name #2)">
|
||||
{@example 'cb-form-validation/ts/src/app/template/hero-form-template2.component.html' region='name-with-error-msg'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
Key changes:
|
||||
- the validation attributes are gone (except `required`) because we'll be validating in code.
|
||||
|
||||
- `required` remains, not for validation purposes (we'll cover that in the code),
|
||||
but rather for css styling and accessibility.
|
||||
|
||||
A future version of reactive forms will add the `required` HTML validation attribute to the DOM element
|
||||
(and perhaps the `aria-required` attribute) when the control has the `required` validator function.
|
||||
|
||||
Until then, apply the `required` attribute _and_ add the `Validator.required` function
|
||||
to the control model, as we'll do below.
|
||||
- the `formControlName` replaces the `name` attribute; it serves the same
|
||||
purpose of correlating the input box with the Angular form control.
|
||||
|
||||
- the two-way `[(ngModel)]` binding is gone.
|
||||
The reactive approach does not use data binding to move data into and out of the form controls.
|
||||
We do that in code.
|
||||
|
||||
The retreat from data binding is a principle of the reactive paradigm rather than a technical limitation.### Component class
|
||||
|
||||
The component class is now responsible for defining and managing the form control model.
|
||||
|
||||
Angular no longer derives the control model from the template so we can no longer query for it.
|
||||
We create the Angular form control model explicitly with the help of the `FormBuilder`.
|
||||
|
||||
Here's the section of code devoted to that process, paired with the template-driven code it replaces:
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="reactive/hero-form-reactive.component.ts (FormBuilder)">
|
||||
{@example 'cb-form-validation/ts/src/app/reactive/hero-form-reactive.component.ts' region='form-builder'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="template/hero-form-template2.component.ts (ViewChild)">
|
||||
{@example 'cb-form-validation/ts/src/app/template/hero-form-template2.component.ts' region='view-child'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
- we inject the `FormBuilder` in a constructor.
|
||||
|
||||
- we call a `buildForm` method in the `ngOnInit` [lifecycle hook method](../guide/lifecycle-hooks.html#hooks-overview)
|
||||
because that's when we'll have the hero data. We'll call it again in the `addHero` method.
|
||||
A real app would retrieve the hero asynchronously from a data service, a task best performed in the `ngOnInit` hook.- the `buildForm` method uses the `FormBuilder` (`fb`) to declare the form control model.
|
||||
Then it attaches the same `onValueChanged` handler (there's a one line difference)
|
||||
to the form's `valueChanged` event and calls it immediately
|
||||
to set error messages for the new control model.
|
||||
#### _FormBuilder_ declaration
|
||||
The `FormBuilder` declaration object specifies the three controls of the sample's hero form.
|
||||
|
||||
Each control spec is a control name with an array value.
|
||||
The first array element is the current value of the corresponding hero field.
|
||||
The (optional) second value is a validator function or an array of validator functions.
|
||||
|
||||
Most of the validator functions are stock validators provided by Angular as static methods of the `Validators` class.
|
||||
Angular has stock validators that correspond to the standard HTML validation attributes.
|
||||
|
||||
The `forbiddenNames` validator on the `"name"` control is a custom validator,
|
||||
discussed in a separate [section below](#custom-validation).
|
||||
|
||||
Learn more about `FormBuilder` in a _forthcoming_ chapter on reactive forms.
|
||||
#### Committing hero value changes
|
||||
|
||||
In two-way data binding, the user's changes flow automatically from the controls back to the data model properties.
|
||||
Reactive forms do not use data binding to update data model properties.
|
||||
The developer decides _when and how_ to update the data model from control values.
|
||||
|
||||
This sample updates the model twice:
|
||||
1. when the user submits the form
|
||||
1. when the user chooses to add a new hero
|
||||
|
||||
The `onSubmit` method simply replaces the `hero` object with the combined values of the form:
|
||||
|
||||
{@example 'cb-form-validation/ts/src/app/reactive/hero-form-reactive.component.ts' region='on-submit'}
|
||||
|
||||
|
||||
This example is "lucky" in that the `heroForm.value` properties _just happen_ to
|
||||
correspond _exactly_ to the hero data object properties.The `addHero` method discards pending changes and creates a brand new `hero` model object.
|
||||
|
||||
{@example 'cb-form-validation/ts/src/app/reactive/hero-form-reactive.component.ts' region='add-hero'}
|
||||
|
||||
Then it calls `buildForm` again which replaces the previous `heroForm` control model with a new one.
|
||||
The `<form>` tag's `[formGroup]` binding refreshes the page with the new control model.
|
||||
|
||||
Here's the complete reactive component file, compared to the two template-driven component files.
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="reactive/hero-form-reactive.component.ts (#3)">
|
||||
{@example 'cb-form-validation/ts/src/app/reactive/hero-form-reactive.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="template/hero-form-template2.component.ts (#2)">
|
||||
{@example 'cb-form-validation/ts/src/app/template/hero-form-template2.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="template/hero-form-template1.component.ts (#1)">
|
||||
{@example 'cb-form-validation/ts/src/app/template/hero-form-template1.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
|
||||
Run the [live example](#live-example) to see how the reactive form behaves
|
||||
and to compare all of the files in this cookbook sample.
|
||||
|
||||
|
||||
|
||||
{@a custom-validation}
|
||||
## Custom validation
|
||||
This cookbook sample has a custom `forbiddenNamevalidator` function that's applied to both the
|
||||
template-driven and the reactive form controls. It's in the `src/app/shared` folder
|
||||
and declared in the `SharedModule`.
|
||||
|
||||
Here's the `forbiddenNamevalidator` function itself:
|
||||
|
||||
{@example 'cb-form-validation/ts/src/app/shared/forbidden-name.directive.ts' region='custom-validator'}
|
||||
|
||||
The function is actually a factory that takes a regular expression to detect a _specific_ forbidden name
|
||||
and returns a validator function.
|
||||
|
||||
In this sample, the forbidden name is "bob";
|
||||
the validator rejects any hero name containing "bob".
|
||||
Elsewhere it could reject "alice" or any name that the configuring regular expression matches.
|
||||
|
||||
The `forbiddenNamevalidator` factory returns the configured validator function.
|
||||
That function takes an Angular control object and returns _either_
|
||||
null if the control value is valid _or_ a validation error object.
|
||||
The validation error object typically has a property whose name is the validation key ('forbiddenName')
|
||||
and whose value is an arbitrary dictionary of values that we could insert into an error message (`{name}`).
|
||||
|
||||
Learn more about validator functions in a _forthcoming_ chapter on custom form validation.#### Custom validation directive
|
||||
In the reactive forms component we added a configured `forbiddenNamevalidator`
|
||||
to the bottom of the `'name'` control's validator function list.
|
||||
|
||||
{@example 'cb-form-validation/ts/src/app/reactive/hero-form-reactive.component.ts' region='name-validators'}
|
||||
|
||||
In the template-driven component template, we add the selector (`forbiddenName`) of a custom _attribute directive_ to the name's input box
|
||||
and configured it to reject "bob".
|
||||
|
||||
{@example 'cb-form-validation/ts/src/app/template/hero-form-template2.component.html' region='name-input'}
|
||||
|
||||
The corresponding `ForbiddenValidatorDirective` is a wrapper around the `forbiddenNamevalidator`.
|
||||
|
||||
Angular forms recognizes the directive's role in the validation process because the directive registers itself
|
||||
with the `NG_VALIDATORS` provider, a provider with an extensible collection of validation directives.
|
||||
|
||||
{@example 'cb-form-validation/ts/src/app/shared/forbidden-name.directive.ts' region='directive-providers'}
|
||||
|
||||
The rest of the directive is unremarkable and we present it here without further comment.
|
||||
|
||||
{@example 'cb-form-validation/ts/src/app/shared/forbidden-name.directive.ts' region='directive'}
|
||||
|
||||
|
||||
See the [Attribute Directives](../guide/attribute-directives.html) chapter.
|
||||
|
||||
|
||||
|
||||
{@a testing}
|
||||
## Testing Considerations
|
||||
|
||||
We can write _isolated unit tests_ of validation and control logic in _Reactive Forms_.
|
||||
|
||||
_Isolated unit tests_ probe the component class directly, independent of its
|
||||
interactions with its template, the DOM, other dependencies, or Angular itself.
|
||||
|
||||
Such tests have minimal setup, are quick to write, and easy to maintain.
|
||||
They do not require the `Angular TestBed` or asynchronous testing practices.
|
||||
|
||||
That's not possible with _Template-driven_ forms.
|
||||
The template-driven approach relies on Angular to produce the control model and
|
||||
to derive validation rules from the HTML validation attributes.
|
||||
You must use the `Angular TestBed` to create component test instances,
|
||||
write asynchronous tests, and interact with the DOM.
|
||||
|
||||
While not difficult, this takes more time, work and skill —
|
||||
factors that tend to diminish test code coverage and quality.
|
@ -1,28 +0,0 @@
|
||||
@title
|
||||
Cookbook
|
||||
|
||||
@intro
|
||||
A collection of recipes for common Angular application scenarios
|
||||
|
||||
@description
|
||||
The *Cookbook* offers answers to common implementation questions.
|
||||
|
||||
Each cookbook chapter is a collection of recipes focused on a particular Angular feature or application challenge
|
||||
such as data binding, cross-component interaction, and communicating with a remote server via HTTP.
|
||||
|
||||
The cookbook is just getting started. Many more recipes are on the way.
|
||||
Each cookbook chapter links to a live sample with every recipe included.
|
||||
|
||||
Recipes are deliberately brief and code-centric.
|
||||
Each recipe links to a chapter of the Developer Guide or the API Guide
|
||||
where you can learn more about the purpose, context, and design choices behind the code snippets.
|
||||
|
||||
## Feedback
|
||||
|
||||
The cookbook is a perpetual *work-in-progress*.
|
||||
We welcome feedback! Leave a comment by clicking the icon in upper right corner of the banner.
|
||||
|
||||
Post *documentation* issues and pull requests on the
|
||||
[angular.io](https://github.com/angular/angular.io) github repository.
|
||||
|
||||
Post issues with *Angular itself* to the [angular](https://github.com/angular/angular) github repository.
|
@ -1,103 +0,0 @@
|
||||
@title
|
||||
Set the Document Title
|
||||
|
||||
@intro
|
||||
Setting the document or window title using the Title service.
|
||||
|
||||
@description
|
||||
|
||||
|
||||
{@a top}
|
||||
Our app should be able to make the browser title bar say whatever we want it to say.
|
||||
This cookbook explains how to do it.**See the <live-example name="cb-set-document-title"></live-example>**.
|
||||
|
||||
<table>
|
||||
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
To see the browser title bar change in the live example,
|
||||
open it again in the Plunker editor by clicking the icon in the upper right,
|
||||
then pop out the preview window by clicking the blue 'X' button in the upper right corner.
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<img src='assets/images/devguide/plunker-switch-to-editor-button.png' width="200px" height="70px" alt="pop out the window" align="right"> </img> <br> </br> <img src='assets/images/devguide/plunker-separate-window-button.png' width="200px" height="47px" alt="pop out the window" align="right"> </img>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
|
||||
## The problem with *<title>*
|
||||
|
||||
The obvious approach is to bind a property of the component to the HTML `<title>` like this:
|
||||
<code-example format=''>
|
||||
<title>{{This_Does_Not_Work}}</title>
|
||||
</code-example>
|
||||
|
||||
Sorry but that won't work.
|
||||
The root component of our application is an element contained within the `<body>` tag.
|
||||
The HTML `<title>` is in the document `<head>`, outside the body, making it inaccessible to Angular data binding.
|
||||
|
||||
We could grab the browser `document` object and set the title manually.
|
||||
That's dirty and undermines our chances of running the app outside of a browser someday.
|
||||
Running your app outside a browser means that you can take advantage of server-side
|
||||
pre-rendering for near-instant first app render times and for SEO. It means you could run from
|
||||
inside a Web Worker to improve your app's responsiveness by using multiple threads. And it
|
||||
means that you could run your app inside Electron.js or Windows Universal to deliver it to the desktop.
|
||||
## Use the *Title* service
|
||||
Fortunately, Angular bridges the gap by providing a `Title` service as part of the *Browser platform*.
|
||||
The [Title](../api/platform-browser/index/Title-class.html) service is a simple class that provides an API
|
||||
for getting and setting the current HTML document title:
|
||||
|
||||
* `getTitle() : string` — Gets the title of the current HTML document.
|
||||
* `setTitle( newTitle : string )` — Sets the title of the current HTML document.
|
||||
|
||||
Let's inject the `Title` service into the root `AppComponent` and expose a bindable `setTitle` method that calls it:
|
||||
|
||||
|
||||
{@example 'cb-set-document-title/ts/src/app/app.component.ts' region='class'}
|
||||
|
||||
We bind that method to three anchor tags and, voilà!
|
||||
<figure class='image-display'>
|
||||
<img src="assets/images/cookbooks/set-document-title/set-title-anim.gif" alt="Set title"> </img>
|
||||
</figure>
|
||||
|
||||
Here's the complete solution
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="src/main.ts">
|
||||
{@example 'cb-set-document-title/ts/src/main.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="src/app/app.module.ts">
|
||||
{@example 'cb-set-document-title/ts/src/app/app.module.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="src/app/app.component.ts">
|
||||
{@example 'cb-set-document-title/ts/src/app/app.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
|
||||
## Why we provide the *Title* service in *bootstrap*
|
||||
|
||||
We generally recommended providing application-wide services in the root application component, `AppComponent`.
|
||||
|
||||
Here we recommend registering the title service during bootstrapping,
|
||||
a location we reserve for configuring the runtime Angular environment.
|
||||
|
||||
That's exactly what we're doing.
|
||||
The `Title` service is part of the Angular *browser platform*.
|
||||
If we bootstrap our application into a different platform,
|
||||
we'll have to provide a different `Title` service that understands the concept of a "document title" for that specific platform.
|
||||
Ideally the application itself neither knows nor cares about the runtime environment.[Back to top](#top)
|
@ -1,868 +0,0 @@
|
||||
@title
|
||||
TypeScript to JavaScript
|
||||
|
||||
@intro
|
||||
Convert Angular TypeScript examples into ES6 and ES5 JavaScript
|
||||
|
||||
@description
|
||||
Anything you can do with Angular in _TypeScript_, you can also do
|
||||
in JavaScript. Translating from one language to the other is mostly a
|
||||
matter of changing the way you organize your code and access Angular APIs.
|
||||
|
||||
_TypeScript_ is a popular language option for Angular development.
|
||||
Most code examples on the Internet as well as on this site are written in _TypeScript_.
|
||||
This cookbook contains recipes for translating _TypeScript_
|
||||
code examples to _ES6_ and to _ES5_ so that JavaScript developers
|
||||
can read and write Angular apps in their preferred dialect.
|
||||
|
||||
|
||||
{@a toc}
|
||||
## Table of contents
|
||||
|
||||
[_TypeScript_ to _ES6_ to _ES5_](#from-ts)<br>
|
||||
[Modularity: imports and exports](#modularity)<br>
|
||||
[Classes and Class Metadata](#class-metadata)<br>
|
||||
[_ES5_ DSL](#dsl)<br>
|
||||
[Interfaces](#interfaces)<br>
|
||||
[Input and Output Metadata](#io-decorators)<br>
|
||||
[Dependency Injection](#dependency-injection)<br>
|
||||
[Host Binding](#host-binding)<br>
|
||||
[View and Child Decorators](#view-child-decorators)<br>
|
||||
[AOT compilation in _TypeScript_ Only](#aot)<br>
|
||||
|
||||
**Run and compare the live <live-example name="cb-ts-to-js">_TypeScript_</live-example> and <live-example name="cb-ts-to-js" lang="js">JavaScript</live-example>
|
||||
code shown in this cookbook.**
|
||||
|
||||
|
||||
{@a from-ts}
|
||||
|
||||
## _TypeScript_ to _ES6_ to _ES5_
|
||||
|
||||
_TypeScript_
|
||||
<a href="https://www.typescriptlang.org" target="_blank" title='"TypeScript is a typed, superset of JavaScript"'>is a typed superset of _ES6 JavaScript_</a>.
|
||||
_ES6 JavaScript_ is a superset of _ES5 JavaScript_. _ES5_ is the kind of JavaScript that runs natively in all modern browsers.
|
||||
The transformation of _TypeScript_ code all the way down to _ES5_ code can be seen as "shedding" features.
|
||||
|
||||
The downgrade progression is
|
||||
* _TypeScript_ to _ES6-with-decorators_
|
||||
* _ES6-with-decorators_ to _ES6-without-decorators_ ("_plain ES6_")
|
||||
* _ES6-without-decorators_ to _ES5_
|
||||
|
||||
When translating from _TypeScript_ to _ES6-with-decorators_, remove
|
||||
[class property access modifiers](http://www.typescriptlang.org/docs/handbook/classes.html#public-private-and-protected-modifiers)
|
||||
such as `public` and `private`.
|
||||
Remove most of the
|
||||
[type declarations](https://www.typescriptlang.org/docs/handbook/basic-types.html),
|
||||
such as `:string` and `:boolean`
|
||||
but **keep the constructor parameter types which are used for dependency injection**.
|
||||
|
||||
|
||||
From _ES6-with-decorators_ to _plain ES6_, remove all
|
||||
[decorators](https://www.typescriptlang.org/docs/handbook/decorators.html)
|
||||
and the remaining types.
|
||||
You must declare properties in the class constructor (`this.title = '...'`) rather than in the body of the class.
|
||||
|
||||
Finally, from _plain ES6_ to _ES5_, the main missing features are `import`
|
||||
statements and `class` declarations.
|
||||
|
||||
For _plain ES6_ transpilation you can _start_ with a setup similar to the
|
||||
[_TypeScript_ quickstart](https://github.com/angular/quickstart) and adjust the application code accordingly.
|
||||
Transpile with [Babel](https://babeljs.io/) using the `es2015` preset.
|
||||
To use decorators and annotations with Babel, install the
|
||||
[`angular2`](https://github.com/shuhei/babel-plugin-angular2-annotations) preset as well.
|
||||
|
||||
|
||||
|
||||
{@a modularity}
|
||||
|
||||
## Importing and Exporting
|
||||
|
||||
### Importing Angular Code
|
||||
|
||||
In both _TypeScript_ and _ES6_, you import Angular classes, functions, and other members with _ES6_ `import` statements.
|
||||
|
||||
In _ES5_, you access the Angular entities of the [the Angular packages](../glossary.html#scoped-package)
|
||||
through the global `ng` object.
|
||||
Anything you can import from `@angular` is a nested member of this `ng` object:
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="TypeScript">
|
||||
{@example 'cb-ts-to-js/ts/src/app/app.module.ts' region='ng2import'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript with decorators">
|
||||
{@example 'cb-ts-to-js/js-es6-decorators/src/app/app.module.es6' region='ng2import'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript">
|
||||
{@example 'cb-ts-to-js/js-es6/src/app/app.module.es6' region='ng2import'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript">
|
||||
{@example 'cb-ts-to-js/js/src/app/app.module.js' region='ng2import'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
### Exporting Application Code
|
||||
|
||||
Each file in a _TypeScript_ or _ES6_ Angular application constitutes an _ES6_ module.
|
||||
When you want to make something available to other modules, you `export` it.
|
||||
|
||||
_ES5_ lacks native support for modules.
|
||||
In an Angular _ES5_ application, you load each file manually by adding a `<script>` tag to `index.html`.
|
||||
|
||||
~~~ {.alert.is-important}
|
||||
|
||||
The order of `<script>` tags is often significant.
|
||||
You must load a file that defines a public JavaScript entity before a file that references that entity.
|
||||
|
||||
~~~
|
||||
|
||||
The best practice in _ES5_ is to create a form of modularity that avoids polluting the global scope.
|
||||
Add one application namespace object such as `app` to the global `document`.
|
||||
Then each code file "exports" public entities by attaching them to that namespace object, e.g., `app.HeroComponent`.
|
||||
You could factor a large application into several sub-namespaces
|
||||
which leads to "exports" along the lines of `app.heroQueries.HeroComponent`.
|
||||
|
||||
Every _ES5_ file should wrap code in an
|
||||
[Immediately Invoked Function Expression (IIFE)](https://en.wikipedia.org/wiki/Immediately-invoked_function_expression)
|
||||
to limit unintentional leaking of private symbols into the global scope.
|
||||
|
||||
Here is a `HeroComponent` as it might be defined and "exported" in each of the four language variants.
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="TypeScript">
|
||||
{@example 'cb-ts-to-js/ts/src/app/hero.component.ts' region='appexport'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript with decorators">
|
||||
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero.component.es6' region='appexport'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript">
|
||||
{@example 'cb-ts-to-js/js-es6/src/app/hero.component.es6' region='appexport'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript">
|
||||
{@example 'cb-ts-to-js/js/src/app/hero.component.js' region='appexport'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
### Importing Application Code
|
||||
|
||||
In _TypeScript_ and _ES6_ apps, you `import` things that have been exported from other modules.
|
||||
|
||||
In _ES5_ you use the shared namespace object to access "exported" entities from other files.
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="TypeScript">
|
||||
{@example 'cb-ts-to-js/ts/src/app/app.module.ts' region='appimport'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript with decorators">
|
||||
{@example 'cb-ts-to-js/js-es6-decorators/src/app/app.module.es6' region='appimport'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript">
|
||||
{@example 'cb-ts-to-js/js-es6/src/app/app.module.es6' region='appimport'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript">
|
||||
{@example 'cb-ts-to-js/js/src/app/app.module.js' region='appimport'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
|
||||
|
||||
~~~ {.alert.is-helpful}
|
||||
|
||||
Alternatively, you can use a module loader such as Webpack or
|
||||
Browserify in an Angular JavaScript project. In such a project, you would
|
||||
use _CommonJS_ modules and the `require` function to load Angular framework code.
|
||||
Then use `module.exports` and `require` to export and import application code.
|
||||
|
||||
|
||||
|
||||
~~~
|
||||
|
||||
|
||||
|
||||
{@a class-metadata}
|
||||
|
||||
## Classes and Class Metadata
|
||||
|
||||
### Classes
|
||||
|
||||
Most Angular _TypeScript_ and _ES6_ code is written as classes.
|
||||
|
||||
Properties and method parameters of _TypeScript_ classes may be marked with the access modifiers
|
||||
`private`, `internal`, and `public`.
|
||||
Remove these modifiers when translating to JavaScript.
|
||||
|
||||
Most type declarations (e.g, `:string` and `:boolean`) should be removed when translating to JavaScript.
|
||||
When translating to _ES6-with-decorators_, ***do not remove types from constructor parameters!***
|
||||
|
||||
Look for types in _TypeScript_ property declarations.
|
||||
In general it is better to initialize such properties with default values because
|
||||
many browser JavaScript engines can generate more performant code.
|
||||
When _TypeScript_ code follows this same advice, it can infer the property types
|
||||
and there is nothing to remove during translation.
|
||||
|
||||
In _ES6-without-decorators_, properties of classes must be assigned inside the constructor.
|
||||
|
||||
_ES5_ JavaScript has no classes.
|
||||
Use the constructor function pattern instead, adding methods to the prototype.
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="TypeScript">
|
||||
{@example 'cb-ts-to-js/ts/src/app/hero.component.ts' region='class'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript with decorators">
|
||||
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero.component.es6' region='class'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript">
|
||||
{@example 'cb-ts-to-js/js-es6/src/app/hero.component.es6' region='class'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript">
|
||||
{@example 'cb-ts-to-js/js/src/app/hero.component.js' region='constructorproto'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
### Metadata
|
||||
|
||||
When writing in _TypeScript_ or _ES6-with-decorators_,
|
||||
provide configuration and metadata by adorning a class with one or more *decorators*.
|
||||
For example, you supply metadata to a component class by preceding its definition with a
|
||||
[`@Component`](../api/core/index/Component-decorator.html) decorator function whose
|
||||
argument is an object literal with metadata properties.
|
||||
|
||||
In _plain ES6_, you provide metadata by attaching an `annotations` array to the _class_.
|
||||
Each item in the array is a new instance of a metadata decorator created with a similar metadata object literal.
|
||||
|
||||
In _ES5_, you also provide an `annotations` array but you attach it to the _constructor function_ rather than to a class.
|
||||
|
||||
See these variations side-by-side:
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="TypeScript">
|
||||
{@example 'cb-ts-to-js/ts/src/app/hero.component.ts' region='metadata'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript with decorators">
|
||||
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero.component.es6' region='metadata'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript">
|
||||
{@example 'cb-ts-to-js/js-es6/src/app/hero.component.es6' region='metadata'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript">
|
||||
{@example 'cb-ts-to-js/js/src/app/hero.component.js' region='metadata'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
***External Template file***
|
||||
|
||||
A large component template is often kept in a separate template file.
|
||||
|
||||
{@example 'cb-ts-to-js/ts/src/app/hero-title.component.html'}
|
||||
|
||||
The component (`HeroTitleComponent` in this case) then references the template file in its metadata `templateUrl` property:
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="TypeScript">
|
||||
{@example 'cb-ts-to-js/ts/src/app/hero-title.component.ts' region='templateUrl'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript with decorators">
|
||||
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero-title.component.es6' region='templateUrl'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript">
|
||||
{@example 'cb-ts-to-js/js-es6/src/app/hero-title.component.es6' region='templateUrl'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript">
|
||||
{@example 'cb-ts-to-js/js/src/app/hero-title.component.js' region='templateUrl'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
Note that both the _TypeScript_ and _ES6_ `templateUrl` properties identify the location of the template file _relative to the component module_.
|
||||
All three metadata configurations specify the `moduleId` property
|
||||
so that Angular can calculate the proper module address.
|
||||
|
||||
The _ES5_ approach shown here does not support modules and therefore there is no way to calculate a _module-relative URL_.
|
||||
The `templateUrl` for the _ES5_ code must specify the _path from the project root_ and
|
||||
omits the irrelevant `moduleId` property.
|
||||
|
||||
With the right tooling, the `moduleId` may not be needed in the other JavaScript dialects either.
|
||||
But it's safest to provide it anyway.
|
||||
|
||||
|
||||
{@a dsl}
|
||||
|
||||
## _ES5_ DSL
|
||||
|
||||
This _ES5_ pattern of creating a constructor and annotating it with metadata is so common that Angular
|
||||
provides a convenience API to make it a little more compact and locates the metadata above the constructor,
|
||||
as you would if you wrote in _TypeScript_ or _ES6-with-decorators_.
|
||||
|
||||
This _API_ (_Application Programming Interface_) is commonly known as the _ES5 DSL_ (_Domain Specific Language_).
|
||||
|
||||
Set an application namespace property (e.g., `app.HeroDslComponent`) to the result of an `ng.core.Component` function call.
|
||||
Pass the same metadata object to `ng.core.Component` as you did before.
|
||||
Then chain a call to the `Class` method which takes an object defining the class constructor and instance methods.
|
||||
|
||||
Here is an example of the `HeroComponent`, re-written with the DSL,
|
||||
next to the original _ES5_ version for comparison:
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="ES5 JavaScript with DSL">
|
||||
{@example 'cb-ts-to-js/js/src/app/hero.component.js' region='dsl'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript">
|
||||
{@example 'cb-ts-to-js/js/src/app/hero.component.js'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
|
||||
|
||||
~~~ {.callout.is-helpful}
|
||||
|
||||
|
||||
<header>
|
||||
Name the constructor
|
||||
</header>
|
||||
|
||||
A **named** constructor displays clearly in the console log
|
||||
if the component throws a runtime error.
|
||||
An **unnamed** constructor displays as an anonymous function (e.g., `class0`)
|
||||
which is impossible to find in the source code.
|
||||
|
||||
|
||||
~~~
|
||||
|
||||
### Properties with getters and setters
|
||||
|
||||
_TypeScript_ and _ES6_ support with getters and setters.
|
||||
Here's an example of a read-only _TypeScript_ property with a getter
|
||||
that prepares a toggle-button label for the next clicked state:
|
||||
|
||||
{@example 'cb-ts-to-js/ts/src/app/hero-queries.component.ts' region='defined-property'}
|
||||
|
||||
This _TypeScript_ "getter" property is transpiled to an _ES5_
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty"
|
||||
target="_blank" title="Defined Properties">defined property</a>.
|
||||
The _ES5 DSL_ does not support _defined properties_ directly
|
||||
but you can still create them by extracting the "class" prototype and
|
||||
adding the _defined property_ in raw JavaScript like this:
|
||||
|
||||
{@example 'cb-ts-to-js/js/src/app/hero-queries.component.js' region='defined-property'}
|
||||
|
||||
### DSL for other classes
|
||||
There are similar DSLs for other decorated classes.
|
||||
You can define a directive with `ng.core.Directive`:
|
||||
|
||||
<code-example>
|
||||
app.MyDirective = ng.core.Directive({
|
||||
selector: '[myDirective]'
|
||||
}).Class({
|
||||
...
|
||||
});
|
||||
</code-example>
|
||||
|
||||
and a pipe with `ng.core.Pipe`:
|
||||
<code-example>
|
||||
app.MyPipe = ng.core.Pipe({
|
||||
name: 'myPipe'
|
||||
}).Class({
|
||||
...
|
||||
});
|
||||
|
||||
</code-example>
|
||||
|
||||
|
||||
|
||||
{@a interfaces}
|
||||
|
||||
## Interfaces
|
||||
|
||||
A _TypeScript_ interface helps ensure that a class implements the interface's members correctly.
|
||||
We strongly recommend Angular interfaces where appropriate.
|
||||
For example, the component class that implements the `ngOnInit` lifecycle hook method
|
||||
should implement the `OnInit` interface.
|
||||
|
||||
_TypeScript_ interfaces exist for developer convenience and are not used by Angular at runtime.
|
||||
They have no physical manifestation in the generated JavaScript code.
|
||||
Just implement the methods and ignore interfaces when translating code samples from _TypeScript_ to JavaScript.
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="TypeScript">
|
||||
{@example 'cb-ts-to-js/ts/src/app/hero-lifecycle.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript with decorators">
|
||||
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero-lifecycle.component.es6'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript">
|
||||
{@example 'cb-ts-to-js/js-es6/src/app/hero-lifecycle.component.es6'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript">
|
||||
{@example 'cb-ts-to-js/js/src/app/hero-lifecycle.component.js'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript with DSL">
|
||||
{@example 'cb-ts-to-js/js/src/app/hero-lifecycle.component.js' region='dsl'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
|
||||
|
||||
{@a io-decorators}
|
||||
|
||||
## Input and Output Metadata
|
||||
|
||||
### Input and Output Decorators
|
||||
|
||||
In _TypeScript_ and _ES6-with-decorators_, you often add metadata to class _properties_ with _property decorators_.
|
||||
For example, you apply [`@Input` and `@Output` property decorators](../guide/template-syntax.html#inputs-outputs)
|
||||
to public class properties that will be the target of data binding expressions in parent components.
|
||||
|
||||
There is no equivalent of a property decorator in _ES5_ or _plain ES6_.
|
||||
Fortunately, every property decorator has an equivalent representation in a class decorator metadata property.
|
||||
A _TypeScript_ `@Input` property decorator can be represented by an item in the `Component` metadata's `inputs` array.
|
||||
|
||||
You already know how to add `Component` or `Directive` class metadata in _any_ JavaScript dialect so
|
||||
there's nothing fundamentally new about adding another property.
|
||||
But note that what would have been _separate_ `@Input` and `@Output` property decorators for each class property are
|
||||
combined in the metadata `inputs` and `outputs` _arrays_.
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="TypeScript">
|
||||
{@example 'cb-ts-to-js/ts/src/app/confirm.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript with decorators">
|
||||
{@example 'cb-ts-to-js/js-es6-decorators/src/app/confirm.component.es6'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript">
|
||||
{@example 'cb-ts-to-js/js-es6/src/app/confirm.component.es6'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript">
|
||||
{@example 'cb-ts-to-js/js/src/app/confirm.component.js'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript with DSL">
|
||||
{@example 'cb-ts-to-js/js/src/app/confirm.component.js' region='dsl'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
In the previous example, one of the public-facing binding names (`cancelMsg`)
|
||||
differs from the corresponding class property name (`notOkMsg`).
|
||||
That's OK but you must tell Angular about it so that it can map an external binding of `cancelMsg`
|
||||
to the component's `notOkMsg` property.
|
||||
|
||||
In _TypeScript_ and _ES6-with-decorators_,
|
||||
you specify the special binding name in the argument to the property decorator.
|
||||
|
||||
In _ES5_ and _plain ES6_ code, convey this pairing with the `propertyName: bindingName` syntax in the class metadata.
|
||||
|
||||
## Dependency Injection
|
||||
Angular relies heavily on [Dependency Injection](../guide/dependency-injection.html) to provide services to the objects it creates.
|
||||
When Angular creates a new component, directive, pipe or another service,
|
||||
it sets the class constructor parameters to instances of services provided by an _Injector_.
|
||||
|
||||
The developer must tell Angular what to inject into each parameter.
|
||||
|
||||
### Injection by Class Type
|
||||
|
||||
The easiest and most popular technique in _TypeScript_ and _ES6-with-decorators_ is to set the constructor parameter type
|
||||
to the class associated with the service to inject.
|
||||
|
||||
The _TypeScript_ transpiler writes parameter type information into the generated JavaScript.
|
||||
Angular reads that information at runtime and locates the corresponding service in the appropriate _Injector_..
|
||||
The _ES6-with-decorators_ transpiler does essentially the same thing using the same parameter-typing syntax.
|
||||
|
||||
_ES5_ and _plain ES6_ lack types so you must identify "injectables" by attaching a **`parameters`** array to the constructor function.
|
||||
Each item in the array specifies the service's injection token.
|
||||
|
||||
As with _TypeScript_ the most popular token is a class,
|
||||
or rather a _constructor function_ that represents a class in _ES5_ and _plain ES6_.
|
||||
The format of the `parameters` array varies:
|
||||
|
||||
* _plain ES6_ — nest each constructor function in a sub-array.
|
||||
|
||||
* _ES5_ — simply list the constructor functions.
|
||||
|
||||
When writing with _ES5 DSL_, set the `Class.constructor` property to
|
||||
an array whose first parameters are the injectable constructor functions and whose
|
||||
last parameter is the class constructor itself.
|
||||
This format should be familiar to AngularJS developers.
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="TypeScript">
|
||||
{@example 'cb-ts-to-js/ts/src/app/hero-di.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript with decorators">
|
||||
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero-di.component.es6'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript">
|
||||
{@example 'cb-ts-to-js/js-es6/src/app/hero-di.component.es6'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript">
|
||||
{@example 'cb-ts-to-js/js/src/app/hero-di.component.js'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript with DSL">
|
||||
{@example 'cb-ts-to-js/js/src/app/hero-di.component.js' region='dsl'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
### Injection with the @Inject decorator
|
||||
|
||||
Sometimes the dependency injection token isn't a class or constructor function.
|
||||
|
||||
In _TypeScript_ and _ES6-with-decorators_, you precede the class constructor parameter
|
||||
by calling the `@Inject()` decorator with the injection token.
|
||||
In the following example, the token is the string `'heroName'`.
|
||||
|
||||
The other JavaScript dialects add a `parameters` array to the class contructor function.
|
||||
Each item constains a new instance of `Inject`:
|
||||
|
||||
* _plain ES6_ — each item is a new instance of `Inject(token)` in a sub-array.
|
||||
|
||||
* _ES5_ — simply list the string tokens.
|
||||
|
||||
When writing with _ES5 DSL_, set the `Class.constructor` property to a function definition
|
||||
array as before. Create a new instance of `ng.core.Inject(token)` for each parameter.
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="TypeScript">
|
||||
{@example 'cb-ts-to-js/ts/src/app/hero-di-inject.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript with decorators">
|
||||
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero-di-inject.component.es6'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript">
|
||||
{@example 'cb-ts-to-js/js-es6/src/app/hero-di-inject.component.es6'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript">
|
||||
{@example 'cb-ts-to-js/js/src/app/hero-di-inject.component.js'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript with DSL">
|
||||
{@example 'cb-ts-to-js/js/src/app/hero-di-inject.component.js' region='dsl'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
### Additional Injection Decorators
|
||||
|
||||
You can qualify injection behavior with injection decorators from `@angular/core`.
|
||||
|
||||
In _TypeScript_ and _ES6-with-decorators_,
|
||||
you precede the constructor parameters with injection qualifiers such as:
|
||||
* [`@Optional`](../api/core/index/Optional-decorator.html) sets the parameter to `null` if the service is missing
|
||||
* [`@Attribute`](../api/core/index/Attribute-interface.html) to inject a host element attribute value
|
||||
* [`@ContentChild`](../api/core/index/ContentChild-decorator.html) to inject a content child
|
||||
* [`@ViewChild`](../api/core/index/ViewChild-decorator.html) to inject a view child
|
||||
* [`@Host`](../api/core/index/Host-decorator.html) to inject a service in this component or its host
|
||||
* [`@SkipSelf`](../api/core/index/SkipSelf-decorator.html) to inject a service provided in an ancestor of this component
|
||||
|
||||
In _plain ES6_ and _ES5_, create an instance of the equivalent injection qualifier in a nested array within the `parameters` array.
|
||||
For example, you'd write `new Optional()` in _plain ES6_ and `new ng.core.Optional()` in _ES5_.
|
||||
|
||||
|
||||
When writing with _ES5 DSL_, set the `Class.constructor` property to a function definition
|
||||
array as before. Use a nested array to define a parameter's complete injection specification.
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="TypeScript">
|
||||
{@example 'cb-ts-to-js/ts/src/app/hero-title.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript with decorators">
|
||||
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero-title.component.es6'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript">
|
||||
{@example 'cb-ts-to-js/js-es6/src/app/hero-title.component.es6'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript">
|
||||
{@example 'cb-ts-to-js/js/src/app/hero-title.component.js'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript with DSL">
|
||||
{@example 'cb-ts-to-js/js/src/app/hero-title.component.js' region='dsl'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
|
||||
In the example above, there is no provider for the `'titlePrefix'` token.
|
||||
Without `Optional`, Angular would raise an error.
|
||||
With `Optional`, Angular sets the constructor parameter to `null`
|
||||
and the component displays the title without a prefix.
|
||||
|
||||
|
||||
{@a host-binding}
|
||||
|
||||
## Host Binding
|
||||
Angular supports bindings to properties and events of the _host element_ which is the
|
||||
element whose tag matches the component selector.
|
||||
|
||||
### Host Decorators
|
||||
|
||||
In _TypeScript_ and _ES6-with-decorators_, you can use host property decorators to bind a host
|
||||
element to a component or directive.
|
||||
The [`@HostBinding`](../api/core/index/HostBinding-interface.html) decorator
|
||||
binds host element properties to component data properties.
|
||||
The [`@HostListener`](../api/core/index/HostListener-interface.html) decorator binds
|
||||
host element events to component event handlers.
|
||||
|
||||
In _plain ES6_ or _ES5_, add a `host` attribute to the component metadata to achieve the
|
||||
same effect as `@HostBinding` and `@HostListener`.
|
||||
|
||||
The `host` value is an object whose properties are host property and listener bindings:
|
||||
|
||||
* Each key follows regular Angular binding syntax: `[property]` for host bindings
|
||||
or `(event)` for host listeners.
|
||||
* Each value identifies the corresponding component property or method.
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="TypeScript">
|
||||
{@example 'cb-ts-to-js/ts/src/app/hero-host.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript with decorators">
|
||||
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero-host.component.es6'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript">
|
||||
{@example 'cb-ts-to-js/js-es6/src/app/hero-host.component.es6'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript">
|
||||
{@example 'cb-ts-to-js/js/src/app/hero-host.component.js'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript with DSL">
|
||||
{@example 'cb-ts-to-js/js/src/app/hero-host.component.js' region='dsl'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
### Host Metadata
|
||||
Some developers prefer to specify host properties and listeners
|
||||
in the component metadata.
|
||||
They'd _rather_ do it the way you _must_ do it _ES5_ and _plain ES6_.
|
||||
|
||||
The following re-implementation of the `HeroComponent` reminds us that _any property metadata decorator_
|
||||
can be expressed as component or directive metadata in both _TypeScript_ and _ES6-with-decorators_.
|
||||
These particular _TypeScript_ and _ES6_ code snippets happen to be identical.
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="TypeScript">
|
||||
{@example 'cb-ts-to-js/ts/src/app/hero-host-meta.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript with decorators">
|
||||
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero-host-meta.component.es6'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
|
||||
|
||||
{@a view-child-decorators}
|
||||
|
||||
### View and Child Decorators
|
||||
|
||||
Several _property_ decorators query a component's nested view and content components.
|
||||
|
||||
_View_ children are associated with element tags that appear _within_ the component's template.
|
||||
|
||||
_Content_ children are associated with elements that appear _between_ the component's element tags;
|
||||
they are projected into an `<ng-content>` slot in the component's template. The [`@ViewChild`](../api/core/index/ViewChild-decorator.html) and
|
||||
[`@ViewChildren`](../api/core/index/ViewChildren-decorator.html) property decorators
|
||||
allow a component to query instances of other components that are used in
|
||||
its view.
|
||||
|
||||
In _ES5_ and _ES6_, you access a component's view children by adding a `queries` property to the component metadata.
|
||||
The `queries` property value is a hash map.
|
||||
|
||||
* each _key_ is the name of a component property that will hold the view child or children.
|
||||
* each _value_ is a new instance of either `ViewChild` or `ViewChildren`.
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="TypeScript">
|
||||
{@example 'cb-ts-to-js/ts/src/app/hero-queries.component.ts' region='view'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript with decorators">
|
||||
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero-queries.component.es6' region='view'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript">
|
||||
{@example 'cb-ts-to-js/js-es6/src/app/hero-queries.component.es6' region='view'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript with DSL">
|
||||
{@example 'cb-ts-to-js/js/src/app/hero-queries.component.js' region='view'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
The [`@ContentChild`](../api/core/index/ContentChild-decorator.html) and
|
||||
[`@ContentChildren`](../api/core/index/ContentChildren-decorator.html) property decorators
|
||||
allow a component to query instances of other components that have been projected
|
||||
into its view from elsewhere.
|
||||
|
||||
They can be added in the same way as [`@ViewChild`](../api/core/index/ViewChild-decorator.html) and
|
||||
[`@ViewChildren`](../api/core/index/ViewChildren-decorator.html).
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="TypeScript">
|
||||
{@example 'cb-ts-to-js/ts/src/app/hero-queries.component.ts' region='content'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript with decorators">
|
||||
{@example 'cb-ts-to-js/js-es6-decorators/src/app/hero-queries.component.es6' region='content'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES6 JavaScript">
|
||||
{@example 'cb-ts-to-js/js-es6/src/app/hero-queries.component.es6' region='content'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="ES5 JavaScript with DSL">
|
||||
{@example 'cb-ts-to-js/js/src/app/hero-queries.component.js' region='content'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
|
||||
|
||||
~~~ {.alert.is-helpful}
|
||||
|
||||
In _TypeScript_ and _ES6-with-decorators_ you can also use the `queries` metadata
|
||||
instead of the `@ViewChild` and `@ContentChild` property decorators.
|
||||
|
||||
|
||||
~~~
|
||||
|
||||
|
||||
|
||||
{@a aot}
|
||||
|
||||
## AOT Compilation in _TypeScript_ only
|
||||
|
||||
Angular offers two modes of template compilation, JIT (_Just-in-Time_) and
|
||||
[AOT (_Ahead-of-Time_)](aot-compiler.html).
|
||||
Currently the AOT compiler only works with _TypeScript_ applications because, in part, it generates
|
||||
_TypeScript_ files as an intermediate result.
|
||||
**AOT is not an option for pure JavaScript applications** at this time.
|
@ -1,171 +0,0 @@
|
||||
@title
|
||||
Visual Studio 2015 QuickStart
|
||||
|
||||
@intro
|
||||
Use Visual Studio 2015 with the QuickStart files
|
||||
|
||||
@description
|
||||
<a id="top"></a>Some developers prefer Visual Studio as their Integrated Development Environment (IDE).
|
||||
|
||||
This cookbook describes the steps required to set up and use the
|
||||
Angular QuickStart files in **Visual Studio 2015 within an ASP.NET 4.x project**.
|
||||
There is no *live example* for this cookbook because it describes Visual Studio, not the application.
|
||||
|
||||
<a id="asp-net-4"></a>## ASP.NET 4.x Project
|
||||
|
||||
This cookbook explains how to set up the QuickStart files with an **ASP.NET 4.x project** in
|
||||
Visual Studio 2015.
|
||||
If you prefer a `File | New Project` experience and are using **ASP.NET Core**,
|
||||
then consider the _experimental_
|
||||
<a href="http://blog.stevensanderson.com/2016/10/04/angular2-template-for-visual-studio/" target="_blank">ASP.NET Core + Angular template for Visual Studio 2015</a>.
|
||||
Note that the resulting code does not map to the docs. Adjust accordingly.
|
||||
The steps are as follows:
|
||||
|
||||
- [Prerequisite](#prereq1): Install Node.js
|
||||
- [Prerequisite](#prereq2): Install Visual Studio 2015 Update 3
|
||||
- [Prerequisite](#prereq3): Configure External Web tools
|
||||
- [Prerequisite](#prereq4): Install TypeScript 2 for Visual Studio 2015
|
||||
- [Step 1](#download): Download the QuickStart files
|
||||
- [Step 2](#create-project): Create the Visual Studio ASP.NET project
|
||||
- [Step 3](#copy): Copy the QuickStart files into the ASP.NET project folder
|
||||
- [Step 4](#restore): Restore required packages
|
||||
- [Step 5](#build-and-run): Build and run the app
|
||||
|
||||
|
||||
<h2 id='prereq1'>
|
||||
Prerequisite: Node.js
|
||||
</h2>
|
||||
|
||||
Install **[Node.js® and npm](https://nodejs.org/en/download/)**
|
||||
if they are not already on your machine.
|
||||
**Verify that you are running node version `4.6.x` or greater, and npm `3.x.x` or greater**
|
||||
by running `node -v` and `npm -v` in a terminal/console window.
|
||||
Older versions produce errors.
|
||||
|
||||
|
||||
<h2 id='prereq2'>
|
||||
Prerequisite: Visual Studio 2015 Update 3
|
||||
</h2>
|
||||
|
||||
The minimum requirement for developing Angular applications with Visual Studio is Update 3.
|
||||
Earlier versions do not follow the best practices for developing applications with TypeScript.
|
||||
To view your version of Visual Studio 2015, go to `Help | About Visual Studio`.
|
||||
|
||||
If you don't have it, install **[Visual Studio 2015 Update 3](https://www.visualstudio.com/en-us/news/releasenotes/vs2015-update3-vs)**.
|
||||
Or use `Tools | Extensions and Updates` to update to Update 3 directly from Visual Studio 2015.
|
||||
|
||||
|
||||
<h2 id='prereq3'>
|
||||
Prerequisite: Configure External Web tools
|
||||
</h2>
|
||||
|
||||
Configure Visual Studio to use the global external web tools instead of the tools that ship with Visual Studio:
|
||||
|
||||
* Open the **Options** dialog with `Tools` | `Options`
|
||||
* In the tree on the left, select `Projects and Solutions` | `External Web Tools`.
|
||||
* On the right, move the `$(PATH)` entry above the `$(DevEnvDir`) entries. This tells Visual Studio to
|
||||
use the external tools (such as npm) found in the global path before using its own version of the external tools.
|
||||
* Click OK to close the dialog.
|
||||
* Restart Visual Studio for this change to take effect.
|
||||
|
||||
Visual Studio will now look first for external tools in the current workspace and
|
||||
if not found then look in the global path and if it is not found there, Visual Studio
|
||||
will use its own versions of the tools.
|
||||
|
||||
|
||||
<h2 id='prereq4'>
|
||||
Prerequisite: Install TypeScript 2 for Visual Studio 2015
|
||||
</h2>
|
||||
|
||||
While Visual Studio Update 3 ships with TypeScript support out of the box, it currently doesn’t ship with TypeScript 2,
|
||||
which you need to develop Angular applications.
|
||||
|
||||
To install TypeScript 2:
|
||||
* Download and install **[TypeScript 2.0 for Visual Studio 2015](http://download.microsoft.com/download/6/D/8/6D8381B0-03C1-4BD2-AE65-30FF0A4C62DA/TS2.0.3-TS-release20-nightly-20160921.1/TypeScript_Dev14Full.exe)**
|
||||
* OR install it with npm: `npm install -g typescript@2.0`.
|
||||
|
||||
You can find out more about TypeScript 2 support in Visual studio **[here](https://blogs.msdn.microsoft.com/typescript/2016/09/22/announcing-typescript-2-0/)**
|
||||
|
||||
At this point, Visual Studio is ready. It’s a good idea to close Visual Studio and
|
||||
restart it to make sure everything is clean.
|
||||
|
||||
|
||||
<h2 id='download'>
|
||||
Step 1: Download the QuickStart files
|
||||
</h2>
|
||||
|
||||
[Download the QuickStart source](https://github.com/angular/quickstart)
|
||||
from github. If you downloaded as a zip file, extract the files.
|
||||
|
||||
|
||||
<h2 id='create-project'>
|
||||
Step 2: Create the Visual Studio ASP.NET project
|
||||
</h2>
|
||||
|
||||
Create the ASP.NET 4.x project in the usual way as follows:
|
||||
|
||||
* In Visual Studio, select `File` | `New` | `Project` from the menu.
|
||||
* In the template tree, select `Templates` | `Visual C#` (or `Visual Basic`) | `Web`.
|
||||
* Select the `ASP.NET Web Application` template, give the project a name, and click OK.
|
||||
* Select the desired ASP.NET 4.5.2 template and click OK.
|
||||
|
||||
In this cookbook we'll select the `Empty` template with no added folders,
|
||||
no authentication and no hosting. Pick the template and options appropriate for your project.
|
||||
|
||||
|
||||
<h2 id='copy'>
|
||||
Step 3: Copy the QuickStart files into the ASP.NET project folder
|
||||
</h2>
|
||||
|
||||
Copy the QuickStart files we downloaded from github into the folder containing the `.csproj` file.
|
||||
Include the files in the Visual Studio project as follows:
|
||||
|
||||
* Click the `Show All Files` button in Solution Explorer to reveal all of the hidden files in the project.
|
||||
* Right-click on each folder/file to be included in the project and select `Include in Project`.
|
||||
Minimally, include the following folder/files:
|
||||
* app folder (answer *No* if asked to search for TypeScript Typings)
|
||||
* styles.css
|
||||
* index.html
|
||||
* package.json
|
||||
* tsconfig.json
|
||||
|
||||
|
||||
<h2 id='restore'>
|
||||
Step 4: Restore the required packages
|
||||
</h2>
|
||||
|
||||
Restore the packages required for an Angular application as follows:
|
||||
|
||||
* Right-click on the `package.json` file in Solution Explorer and select `Restore Packages`.
|
||||
<br>This uses `npm` to install all of the packages defined in the `package.json` file.
|
||||
It may take some time.
|
||||
* If desired, open the Output window (`View` | `Output`) to watch the npm commands execute.
|
||||
* Ignore the warnings.
|
||||
* When the restore is finished, a message should say: `npm command completed with exit code 0`.
|
||||
* Click the `Refresh` icon in Solution Explorer.
|
||||
* **Do not** include the `node_modules` folder in the project. Let it be a hidden project folder.
|
||||
|
||||
|
||||
<h2 id='build-and-run'>
|
||||
Step 5: Build and run the app
|
||||
</h2>
|
||||
|
||||
First, ensure that `index.html` is set as the start page.
|
||||
Right-click `index.html` in Solution Explorer and select option `Set As Start Page`.
|
||||
|
||||
Build and launch the app with debugger by clicking the **Run** button or press `F5`.
|
||||
It's faster to run without the debugger by pressing `Ctrl-F5`.The default browser opens and displays the QuickStart sample application.
|
||||
|
||||
Try editing any of the project files. *Save* and refresh the browser to
|
||||
see the changes.
|
||||
|
||||
|
||||
<h2 id='routing'>
|
||||
Note on Routing Applications
|
||||
</h2>
|
||||
|
||||
If this application used the Angular router, a browser refresh could return a *404 - Page Not Found*.
|
||||
Look at the address bar. Does it contain a navigation url (a "deep link") ... any path other than `/` or `/index.html`?
|
||||
|
||||
You'll have to configure the server to return `index.html` for these requests.
|
||||
Until you do, remove the navigation path and refresh again.
|
76
aio/content/examples/.gitignore
vendored
76
aio/content/examples/.gitignore
vendored
@ -1,17 +1,71 @@
|
||||
# _boilerplate files
|
||||
!_boilerplate/*
|
||||
*/*/src/styles.css
|
||||
*/*/src/systemjs.config.js
|
||||
*/*/src/tsconfig.json
|
||||
*/*/bs-config.e2e.json
|
||||
*/*/bs-config.json
|
||||
*/*/package.json
|
||||
*/*/tslint.json
|
||||
# boilerplate files
|
||||
**/src/styles.css
|
||||
**/src/systemjs-angular-loader.js
|
||||
**/src/systemjs.config.js
|
||||
**/src/tsconfig.json
|
||||
**/bs-config.e2e.json
|
||||
**/bs-config.json
|
||||
**/package.json
|
||||
**/tslint.json
|
||||
**/karma.conf.js
|
||||
**/karma-test-shim.js
|
||||
**/browser-test-shim.js
|
||||
**/node_modules
|
||||
|
||||
# example files
|
||||
# built files
|
||||
*.map
|
||||
_test-output
|
||||
protractor-helpers.js
|
||||
*/e2e-spec.js
|
||||
**/*.js
|
||||
**/ts/**/*.js
|
||||
**/js-es6*/**/*.js
|
||||
**/ts-snippets/**/*.js
|
||||
dist/
|
||||
|
||||
|
||||
# special
|
||||
!/*
|
||||
!*.1.*
|
||||
!*.2.*
|
||||
!*.3.*
|
||||
*.1.js
|
||||
*.2.js
|
||||
*.3.js
|
||||
*.1.js.map
|
||||
*.2.js.map
|
||||
*.3.js.map
|
||||
!systemjs.config.*.js
|
||||
!karma-test-shim.*.js
|
||||
!copy-dist-files.js
|
||||
|
||||
# AngularJS files
|
||||
!**/*.ajs.js
|
||||
**/app/**/*.ajs.js
|
||||
|
||||
# aot
|
||||
**/*.ngfactory.ts
|
||||
**/*.ngsummary.json
|
||||
**/*.ngsummary.ts
|
||||
**/*.shim.ngstyle.ts
|
||||
**/*.metadata.json
|
||||
!aot/bs-config.json
|
||||
!aot/index.html
|
||||
!rollup-config.js
|
||||
|
||||
# testing
|
||||
!testing/src/browser-test-shim.js
|
||||
!testing/karma*.js
|
||||
|
||||
# TS to JS
|
||||
!ts-to-js/js*/**/*.js
|
||||
ts-to-js/js*/**/system*.js
|
||||
|
||||
# webpack
|
||||
!webpack/**/config/*.js
|
||||
!webpack/**/*webpack*.js
|
||||
|
||||
# styleguide
|
||||
!styleguide/src/systemjs.custom.js
|
||||
|
||||
# plunkers
|
||||
*plnkr.no-link.html
|
||||
|
@ -4,7 +4,6 @@ import { MovieService } from './movie.service';
|
||||
import { IMovie } from './movie';
|
||||
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'my-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: [ './app.component.css' ],
|
||||
@ -20,7 +19,7 @@ export class AppComponent {
|
||||
movie: IMovie = null;
|
||||
movies: IMovie[] = [];
|
||||
showImage = true;
|
||||
title: string = 'AngularJS to Angular Quick Ref Cookbook';
|
||||
title = 'AngularJS to Angular Quick Ref Cookbook';
|
||||
toggleImage(event: UIEvent) {
|
||||
this.showImage = !this.showImage;
|
||||
this.eventType = (event && event.type) || 'not provided';
|
@ -8,7 +8,6 @@ import { MovieService } from './movie.service';
|
||||
|
||||
// #docregion component
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'movie-list',
|
||||
templateUrl: './movie-list.component.html',
|
||||
// #docregion style-url
|
||||
@ -20,7 +19,7 @@ import { MovieService } from './movie.service';
|
||||
export class MovieListComponent {
|
||||
// #enddocregion class
|
||||
favoriteHero: string;
|
||||
showImage: boolean = false;
|
||||
showImage = false;
|
||||
movies: IMovie[];
|
||||
|
||||
// #docregion di
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user