Compare commits
486 Commits
4.2.6
...
5.0.0-beta
Author | SHA1 | Date | |
---|---|---|---|
cea02414b0 | |||
f0ec31e47f | |||
ff5c58be6b | |||
dca50deae4 | |||
7f2037f0b6 | |||
fd6ae571b8 | |||
b14250bef9 | |||
6fb5250185 | |||
2f9d8ff46d | |||
e54bd59f22 | |||
1e1833198d | |||
6f2038cc85 | |||
f0a55016af | |||
fcadbf4bf6 | |||
d9d00bd9b5 | |||
f69561b2de | |||
685cc26ab2 | |||
5b7432b6ea | |||
04b18a9f46 | |||
05472cb21b | |||
c0c03dc4ba | |||
f9f8924c49 | |||
10897d6473 | |||
340837aa46 | |||
42ef1be75c | |||
a5801b6020 | |||
70b62949de | |||
36161d99f6 | |||
0714139e37 | |||
bcb36d9b6d | |||
ca695e0632 | |||
9b015a95eb | |||
939dc44391 | |||
5651e4ac72 | |||
1dca575701 | |||
333a708bb6 | |||
3a227a1f6f | |||
81cb5bc3a7 | |||
1640d2aa0b | |||
5f501c722b | |||
ea07856cc5 | |||
7c47b62a96 | |||
e25b3dd163 | |||
6a88659c9a | |||
44ae6e94e3 | |||
1635a06bda | |||
3923c30df0 | |||
99017bf3ff | |||
4d117faf1a | |||
5cc9913ded | |||
1d09838622 | |||
9adf40aa77 | |||
89c616199f | |||
1e1af7ffcb | |||
a84b2bc945 | |||
7abcb99d57 | |||
49cd8513e4 | |||
82b067fc40 | |||
9479a106bb | |||
e64b54b67b | |||
cc2a4c41f9 | |||
a11542a375 | |||
b6c4af6495 | |||
67dff7bd5d | |||
381471d338 | |||
ebef5e697a | |||
d71ae278ef | |||
46207538ef | |||
71eb7437b6 | |||
b5ffbe342b | |||
0f79223008 | |||
a085223331 | |||
c383048259 | |||
b18eb04b46 | |||
c8c2ab012a | |||
ecff8e6c93 | |||
51f1da1b85 | |||
a5e18c4cdf | |||
cf6284656f | |||
3182ddaf3e | |||
416ed691e5 | |||
0fb7484d51 | |||
6a3454e81e | |||
c3fbe87012 | |||
24117d7a49 | |||
5808153359 | |||
9030c8a03e | |||
b14fc06fa2 | |||
a7f2468184 | |||
fae47d86b3 | |||
d20ac14fe2 | |||
cae3e6dca0 | |||
086f4aa72c | |||
82923a381d | |||
5152abb037 | |||
67f7032321 | |||
205abe8140 | |||
b582e2b311 | |||
91ab39cc55 | |||
38ec05f533 | |||
b3085e96c2 | |||
4cea2bd612 | |||
ce47546188 | |||
6279e50d78 | |||
8bcb268140 | |||
6fc5940959 | |||
0317c4c478 | |||
b7a6f52d59 | |||
7ae7573bc8 | |||
abee785821 | |||
619e625ee2 | |||
a6c635e69e | |||
e0a9625e46 | |||
fd0cc01eed | |||
1bfc77bf8c | |||
a094769bca | |||
b4c98305da | |||
a3a54299af | |||
15a3e2d307 | |||
54e0244954 | |||
43c33d5663 | |||
6d7799fce9 | |||
d31dc7b2b3 | |||
4cd4f7a208 | |||
72fe45db2b | |||
8d2819121b | |||
073e8ba2f2 | |||
5d1864fe68 | |||
eaa843b55f | |||
c6cf678a07 | |||
31cb418370 | |||
8de44cf5e3 | |||
c67bad4f43 | |||
410f21c75c | |||
54ea5b6ffd | |||
0af03beaed | |||
d71fa734f5 | |||
6f45519d6f | |||
65c9e13105 | |||
9208f0beea | |||
5344be5182 | |||
5db6f38b73 | |||
d22f8f54db | |||
23146c9201 | |||
a5205c686e | |||
807648251f | |||
5c62e300e1 | |||
256bc8acdd | |||
59c23c7bd7 | |||
e03adb9edd | |||
b399cb26d9 | |||
3b588fe2b0 | |||
95635c18c7 | |||
e20cfe1bbc | |||
eb6fb5f87e | |||
ad3029e786 | |||
2a2fe11e8d | |||
7d0f2cd51e | |||
36faba1aab | |||
92179bcc64 | |||
cdb069ab0e | |||
c453b7bcfa | |||
9d97163c64 | |||
f054c8360b | |||
758848961e | |||
99b666614d | |||
3f331b53b2 | |||
375d598a9f | |||
cd67fced1c | |||
a77cf7ee37 | |||
2150b45954 | |||
9f99f4fae2 | |||
c6ad212a98 | |||
47b3ecd9a3 | |||
8c81c62d46 | |||
7e72317059 | |||
0bb8423df9 | |||
95698d93ad | |||
c649da9f0a | |||
0bf0c35bca | |||
97e6901ded | |||
30e76fcd80 | |||
44b50427d9 | |||
a0b06befb6 | |||
4fbb5b29ea | |||
e0fa727594 | |||
3ecc5e5398 | |||
f7686d4124 | |||
01a2688848 | |||
8e56c3cb30 | |||
7955cacec4 | |||
dd04f09483 | |||
3d85f72652 | |||
9f28e838d3 | |||
ddb766e456 | |||
72143e80da | |||
bc1ea8c54b | |||
45ffe54ae4 | |||
1bf7ba87a0 | |||
db96c963a8 | |||
18559897a0 | |||
ce0f4f0d7c | |||
4f1e4ffa4e | |||
f0beb4d750 | |||
bc3b2ac251 | |||
ffda3e41e0 | |||
a301dba68f | |||
04f3a4a7a5 | |||
f06ce9adc8 | |||
660eec4a23 | |||
be3352a084 | |||
998049ec9b | |||
a7ea0086ee | |||
edb8375a5f | |||
26b9492315 | |||
e110a80caf | |||
20127c1456 | |||
a50d935a48 | |||
7c479f073e | |||
bbf2133fa9 | |||
4300439ab2 | |||
ec14679668 | |||
df06e8b7a4 | |||
1b1a6ba0bb | |||
876ca9ee3a | |||
d9b03be08f | |||
b6aad07634 | |||
3d0406c247 | |||
db3bcc939e | |||
4d45fe6fb5 | |||
076ea2281f | |||
aec39c28d8 | |||
b9525ece77 | |||
719101338a | |||
e131f6bbe8 | |||
a9757ec674 | |||
9003770f02 | |||
e8bbf86e66 | |||
3a0886dc12 | |||
062a7aa2cf | |||
e28f097fc2 | |||
b30c5fc874 | |||
d52ab8e2c9 | |||
df7b875f6c | |||
0e71836cd5 | |||
470a7c6bcd | |||
3abf208235 | |||
92c18d167e | |||
99b38f52cb | |||
633ec30291 | |||
57cfcb0830 | |||
433d479a1e | |||
7c4ac68e66 | |||
20556346a3 | |||
5a417b8514 | |||
8cfc2e2ec0 | |||
11647e4c78 | |||
9e1b61326c | |||
cb7609109d | |||
b3d90365b6 | |||
cc611c93b6 | |||
8928a58796 | |||
05a33d5035 | |||
09f1609f81 | |||
c723d42d0a | |||
9dd550fa1e | |||
abbac4bc69 | |||
671a175dfb | |||
c1474f33be | |||
0ede642cb9 | |||
9c1f6fd06f | |||
798947efa4 | |||
7ae8ad6aab | |||
9c3386b1b7 | |||
2ba3ada27f | |||
8e28382e4a | |||
3203639d7d | |||
f85b543cc1 | |||
c81ad9d19d | |||
37797e2b4e | |||
2a7ebbe982 | |||
72747e5213 | |||
504500de50 | |||
e1174f3774 | |||
6bae73c076 | |||
11db3bd85e | |||
0193be7c9b | |||
2ea73513ea | |||
67e9c62013 | |||
227dbbcfba | |||
cb16e9c747 | |||
3b2d2c467a | |||
ae27af7399 | |||
c69fff15c9 | |||
dd7c1134e3 | |||
b116901400 | |||
70981c601e | |||
f2f61c9cf0 | |||
1bb2476804 | |||
ec58246a1b | |||
6fc5174a13 | |||
105e920b69 | |||
858dea98e5 | |||
71ee0c5b03 | |||
578bdeb522 | |||
6282a86135 | |||
e9b67243ed | |||
fa1c187abc | |||
b51697c197 | |||
c6b75b0823 | |||
0d7e1a9b4e | |||
9d15d85391 | |||
92d7ecf627 | |||
9263da570f | |||
dc88e0a881 | |||
fa34ed8bf3 | |||
f54a901b8d | |||
8a1a989a1c | |||
b479ed9407 | |||
d5dc53ead8 | |||
8e00161601 | |||
01d4eae984 | |||
154154dde2 | |||
4459e0c1c8 | |||
b052ef5f1e | |||
40921bb927 | |||
dfcca66fdc | |||
1ac9085b0a | |||
1cfe67dac4 | |||
8d01db4638 | |||
4268c82898 | |||
3c4eef99be | |||
96b17034e1 | |||
e47a77f941 | |||
af14b1e384 | |||
40f77cb563 | |||
6c1a8daafc | |||
d699c354db | |||
34f3832af9 | |||
f1626574dd | |||
68fc65dbcb | |||
693f79e88a | |||
448d9f9f46 | |||
8786ba95fb | |||
97bb374218 | |||
233044e337 | |||
f365a0f45c | |||
263c1a1d7e | |||
3097083277 | |||
43c187b624 | |||
3165fd3dc9 | |||
e80851d98b | |||
b754e600e3 | |||
81734cf7b6 | |||
30f4fe26e0 | |||
d6265dfcbe | |||
d51f86291f | |||
97ace57d39 | |||
86949e0c20 | |||
6924780ae9 | |||
1b0b69eeec | |||
fa85389f62 | |||
2e55857c82 | |||
ca970f5ee5 | |||
204a2cf942 | |||
0440251919 | |||
08ecfd891d | |||
7395a64668 | |||
979bfd07e1 | |||
b6ce814279 | |||
66088fef1a | |||
808bd4af41 | |||
f90b35a85e | |||
8ae0eec230 | |||
0fe685102f | |||
a98440bb85 | |||
3112311134 | |||
1b13bdea4b | |||
3ce9d51a9c | |||
14d2de13bb | |||
5713e7c9b6 | |||
87206e1986 | |||
414c7e956b | |||
6191d53a78 | |||
7d30ccc4a9 | |||
494a0d064a | |||
849200b576 | |||
60273a941f | |||
7ba720a62a | |||
eacc36bbd5 | |||
8b4acbbcbf | |||
b1fe63d081 | |||
f2ee1dcdb7 | |||
21018af2bf | |||
67ffbae6f9 | |||
5dd5bfde72 | |||
400486ced7 | |||
1a947e4b75 | |||
92bcfefc35 | |||
133b5e6e36 | |||
5c576d3b9d | |||
68b64a261a | |||
68f939ea8c | |||
8c129d73b8 | |||
1fc0d05565 | |||
20a04f9076 | |||
02a38d3ea5 | |||
37cdc4f759 | |||
b1dab181e0 | |||
fd6c4e371b | |||
4352dd27c4 | |||
34cc3f2982 | |||
97fd2480e7 | |||
1d93cf2e85 | |||
3fb98fe4ea | |||
bb804dd3e9 | |||
0034bb28e5 | |||
ca51e020cb | |||
d16852898f | |||
46ddf501a9 | |||
8c89cc4fc5 | |||
00874c27f4 | |||
c59c390cdc | |||
009651e14f | |||
f194f18dbd | |||
4e6be15069 | |||
3e685f98c6 | |||
6c8e7dd63e | |||
2447bd1bac | |||
1c6a252596 | |||
d3c92a307a | |||
8f5836cb14 | |||
319ce182db | |||
aa92f3a721 | |||
afbb6bb797 | |||
4f37f86433 | |||
3093c55e9e | |||
5ac3919259 | |||
f58211d9d8 | |||
fe126cb737 | |||
adc1b129e4 | |||
9315ab88d7 | |||
1ddbddb0db | |||
232bd9395d | |||
956a7e95d7 | |||
ce00fa3627 | |||
3515860b15 | |||
77e717e872 | |||
b46cc744b3 | |||
2cc931ed2a | |||
e096a85874 | |||
2c3e948e61 | |||
53f57d74b8 | |||
2b1de07f02 | |||
fb877696bf | |||
01173b9441 | |||
0564dd25e2 | |||
ba0f6decc3 | |||
06a0cf2e31 | |||
a8afa65a54 | |||
f73a4c229c | |||
d378a29565 | |||
2bdf2feea7 | |||
607fb1fff8 | |||
38fc2a0055 | |||
0c07f8c099 | |||
d8d21c77d5 | |||
b4cd20cbbc | |||
f840afb983 | |||
93d1b4ed9d | |||
fa81c8eeb3 | |||
98308cd79c | |||
052331fabc | |||
541c9a94bf | |||
12452d4ab4 | |||
709a3f6de7 | |||
7caa0a8aa4 | |||
0f56296c24 | |||
0a846a2fce | |||
bffccf4622 | |||
fc774a1871 | |||
d647db222c | |||
0c7eb93889 | |||
0658e1da7f | |||
d2d8e5d40f | |||
5e794492c1 |
@ -1,18 +1,31 @@
|
|||||||
defaults: &defaults
|
# Configuration file for https://circleci.com/gh/angular/angular
|
||||||
|
|
||||||
|
# Note: YAML anchors allow an object to be re-used, reducing duplication.
|
||||||
|
# The ampersand declares an alias for an object, then later the `<<: *name`
|
||||||
|
# syntax dereferences it.
|
||||||
|
# See http://blog.daemonl.com/2016/02/yaml.html
|
||||||
|
# To validate changes, use an online parser, eg.
|
||||||
|
# http://yaml-online-parser.appspot.com/
|
||||||
|
|
||||||
|
# Settings common to each job
|
||||||
|
anchor_1: &job_defaults
|
||||||
working_directory: ~/ng
|
working_directory: ~/ng
|
||||||
docker:
|
docker:
|
||||||
- image: angular/ngcontainer
|
- image: angular/ngcontainer
|
||||||
|
|
||||||
|
# After checkout, rebase on top of master.
|
||||||
|
# Similar to travis behavior, but not quite the same.
|
||||||
|
# See https://discuss.circleci.com/t/1662
|
||||||
|
anchor_2: &post_checkout
|
||||||
|
post: git pull --ff-only origin "refs/pull/${CI_PULL_REQUEST//*pull\//}/merge"
|
||||||
|
|
||||||
version: 2
|
version: 2
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
<<: *defaults
|
<<: *job_defaults
|
||||||
steps:
|
steps:
|
||||||
- checkout:
|
- checkout:
|
||||||
# After checkout, rebase on top of master.
|
<<: *post_checkout
|
||||||
# Similar to travis behavior, but not quite the same.
|
|
||||||
# See https://discuss.circleci.com/t/1662
|
|
||||||
post: git pull --ff-only origin "refs/pull/${CI_PULL_REQUEST//*pull\//}/merge"
|
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
|
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
|
||||||
|
|
||||||
@ -21,14 +34,15 @@ jobs:
|
|||||||
- run: ./node_modules/.bin/gulp lint
|
- run: ./node_modules/.bin/gulp lint
|
||||||
|
|
||||||
build:
|
build:
|
||||||
<<: *defaults
|
<<: *job_defaults
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout:
|
||||||
|
<<: *post_checkout
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
|
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
|
||||||
|
|
||||||
- run: bazel run @io_bazel_rules_typescript_node//:bin/npm install
|
- run: bazel run @build_bazel_rules_typescript_node//:bin/npm install
|
||||||
- run: bazel build ...
|
- run: bazel build packages/...
|
||||||
- save_cache:
|
- save_cache:
|
||||||
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
|
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
|
||||||
paths:
|
paths:
|
||||||
|
14
.github/ISSUE_TEMPLATE.md
vendored
@ -1,14 +1,14 @@
|
|||||||
<!--
|
<!--
|
||||||
PLEASE HELP US PROCESS GITHUB ISSUES FASTER BY PROVIDING THE FOLLOWING INFORMATION.
|
PLEASE HELP US PROCESS GITHUB ISSUES FASTER BY PROVIDING THE FOLLOWING INFORMATION.
|
||||||
|
|
||||||
ISSUES MISSING IMPORTANT INFORMATION MIGHT BE CLOSED WITHOUT INVESTIGATION.
|
ISSUES MISSING IMPORTANT INFORMATION MAY BE CLOSED WITHOUT INVESTIGATION.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## I'm submitting a ...
|
## I'm submitting a...
|
||||||
<!-- Check one of the following options with "x" -->
|
<!-- Check one of the following options with "x" -->
|
||||||
<pre><code>
|
<pre><code>
|
||||||
[ ] Regression (behavior that used to work and stopped working in a new release)
|
[ ] Regression (a behavior that used to work and stopped working in a new release)
|
||||||
[ ] Bug report <!-- Please search github for a similar issue or PR before submitting -->
|
[ ] Bug report <!-- Please search GitHub for a similar issue or PR before submitting -->
|
||||||
[ ] Feature request
|
[ ] Feature request
|
||||||
[ ] Documentation issue or request
|
[ ] Documentation issue or request
|
||||||
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
|
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
|
||||||
@ -32,7 +32,7 @@ https://plnkr.co or similar (you can use this template as a starting point: http
|
|||||||
<!-- Describe the motivation or the concrete use case. -->
|
<!-- Describe the motivation or the concrete use case. -->
|
||||||
|
|
||||||
|
|
||||||
## Please tell us about your environment
|
## Environment
|
||||||
|
|
||||||
<pre><code>
|
<pre><code>
|
||||||
Angular version: X.Y.Z
|
Angular version: X.Y.Z
|
||||||
@ -49,8 +49,8 @@ Browser:
|
|||||||
- [ ] Edge version XX
|
- [ ] Edge version XX
|
||||||
|
|
||||||
For Tooling issues:
|
For Tooling issues:
|
||||||
- Node version: XX <!-- use `node --version` -->
|
- Node version: XX <!-- run `node --version` -->
|
||||||
- Platform: <!-- Mac, Linux, Windows -->
|
- Platform: <!-- Mac, Linux, Windows -->
|
||||||
|
|
||||||
Others:
|
Others:
|
||||||
<!-- Anything else relevant? Operating system version, IDE, package manager, HTTP server, ... -->
|
<!-- Anything else relevant? Operating system version, IDE, package manager, HTTP server, ... -->
|
||||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,5 +1,5 @@
|
|||||||
## PR Checklist
|
## PR Checklist
|
||||||
Does please check if your PR fulfills the following requirements:
|
Please check if your PR fulfills the following requirements:
|
||||||
|
|
||||||
- [ ] The commit message follows our guidelines: https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit
|
- [ ] The commit message follows our guidelines: https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit
|
||||||
- [ ] Tests for the changes have been added (for bug fixes / features)
|
- [ ] Tests for the changes have been added (for bug fixes / features)
|
||||||
|
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
/dist/
|
/dist/
|
||||||
bazel-*
|
bazel-*
|
||||||
|
e2e_test.*
|
||||||
node_modules
|
node_modules
|
||||||
bower_components
|
bower_components
|
||||||
|
|
||||||
|
@ -69,8 +69,8 @@ groups:
|
|||||||
- "*.lock"
|
- "*.lock"
|
||||||
- "tools/*"
|
- "tools/*"
|
||||||
exclude:
|
exclude:
|
||||||
- "tools/@angular/tsc-wrapped/*"
|
|
||||||
- "tools/public_api_guard/*"
|
- "tools/public_api_guard/*"
|
||||||
|
- "tools/ngc-wrapped/*"
|
||||||
- "aio/*"
|
- "aio/*"
|
||||||
users:
|
users:
|
||||||
- IgorMinar #primary
|
- IgorMinar #primary
|
||||||
@ -136,8 +136,9 @@ groups:
|
|||||||
compiler-cli:
|
compiler-cli:
|
||||||
conditions:
|
conditions:
|
||||||
files:
|
files:
|
||||||
- "tools/@angular/tsc-wrapped/*"
|
- "packages/tsc-wrapped/*"
|
||||||
- "packages/compiler-cli/*"
|
- "packages/compiler-cli/*"
|
||||||
|
- "tools/ngc-wrapped/*"
|
||||||
users:
|
users:
|
||||||
- alexeagle
|
- alexeagle
|
||||||
- chuckjaz
|
- chuckjaz
|
||||||
@ -268,16 +269,18 @@ groups:
|
|||||||
conditions:
|
conditions:
|
||||||
files:
|
files:
|
||||||
include:
|
include:
|
||||||
- "aio/content/examples/*"
|
- "aio/content/*"
|
||||||
- "aio/content/guide/*"
|
exclude:
|
||||||
- "aio/content/images/*"
|
- "aio/content/marketing/*"
|
||||||
- "aio/content/tutorial/*"
|
- "aio/content/navigation.json"
|
||||||
- "aio/content/file-not-found.md"
|
- "aio/content/license.md"
|
||||||
users:
|
users:
|
||||||
- juleskremer #primary
|
- juleskremer #primary
|
||||||
- Foxandxss
|
- Foxandxss
|
||||||
- stephenfluin
|
- stephenfluin
|
||||||
- wardbell
|
- wardbell
|
||||||
|
- petebacondarwin
|
||||||
|
- gkalpak
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
|
|
||||||
@ -285,15 +288,13 @@ groups:
|
|||||||
conditions:
|
conditions:
|
||||||
files:
|
files:
|
||||||
include:
|
include:
|
||||||
- "aio/content/*"
|
- "aio/content/marketing/*"
|
||||||
exclude:
|
- "aio/content/navigation.json"
|
||||||
- "aio/content/examples/*"
|
- "aio/content/license.md"
|
||||||
- "aio/content/guide/*"
|
|
||||||
- "aio/content/images/*"
|
|
||||||
- "aio/content/tutorial/*"
|
|
||||||
- "aio/content/file-not-found.md"
|
|
||||||
users:
|
users:
|
||||||
- juleskremer #primary
|
- juleskremer #primary
|
||||||
- stephenfluin
|
- stephenfluin
|
||||||
|
- petebacondarwin
|
||||||
|
- gkalpak
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
|
10
.travis.yml
@ -1,9 +1,12 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
sudo: false
|
sudo: false
|
||||||
|
# force trusty as Google Chrome addon is not supported on Precise
|
||||||
|
dist: trusty
|
||||||
node_js:
|
node_js:
|
||||||
- '6.9.5'
|
- '6.9.5'
|
||||||
|
|
||||||
addons:
|
addons:
|
||||||
|
chrome: stable
|
||||||
# firefox: "38.0"
|
# firefox: "38.0"
|
||||||
apt:
|
apt:
|
||||||
sources:
|
sources:
|
||||||
@ -50,16 +53,17 @@ env:
|
|||||||
- CI_MODE=browserstack_required
|
- CI_MODE=browserstack_required
|
||||||
- CI_MODE=saucelabs_optional
|
- CI_MODE=saucelabs_optional
|
||||||
- CI_MODE=browserstack_optional
|
- CI_MODE=browserstack_optional
|
||||||
- CI_MODE=docs_test
|
- CI_MODE=aio_tools_test
|
||||||
- CI_MODE=aio
|
- CI_MODE=aio
|
||||||
- CI_MODE=aio_e2e
|
- CI_MODE=aio_e2e AIO_SHARD=0
|
||||||
|
- CI_MODE=aio_e2e AIO_SHARD=1
|
||||||
|
- CI_MODE=bazel
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- env: "CI_MODE=saucelabs_optional"
|
- env: "CI_MODE=saucelabs_optional"
|
||||||
- env: "CI_MODE=browserstack_optional"
|
- env: "CI_MODE=browserstack_optional"
|
||||||
- env: "CI_MODE=aio_e2e"
|
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
# source the env.sh script so that the exported variables are available to other scripts later on
|
# source the env.sh script so that the exported variables are available to other scripts later on
|
||||||
|
@ -11,8 +11,15 @@ filegroup(
|
|||||||
# This won't scale in the general case.
|
# This won't scale in the general case.
|
||||||
# TODO(alexeagle): figure out what to do
|
# TODO(alexeagle): figure out what to do
|
||||||
"node_modules/typescript/**",
|
"node_modules/typescript/**",
|
||||||
"node_modules/zone.js/**/*.d.ts",
|
"node_modules/zone.js/**",
|
||||||
"node_modules/rxjs/**/*.d.ts",
|
"node_modules/rxjs/**/*.d.ts",
|
||||||
|
"node_modules/rxjs/**/*.js",
|
||||||
"node_modules/@types/**/*.d.ts",
|
"node_modules/@types/**/*.d.ts",
|
||||||
|
"node_modules/tsickle/**",
|
||||||
|
"node_modules/hammerjs/**/*.d.ts",
|
||||||
|
"node_modules/protobufjs/**",
|
||||||
|
"node_modules/bytebuffer/**",
|
||||||
|
"node_modules/reflect-metadata/**",
|
||||||
|
"node_modules/minimist/**/*.js",
|
||||||
]),
|
]),
|
||||||
)
|
)
|
4360
CHANGELOG.md
@ -17,7 +17,7 @@ Help us keep Angular open and inclusive. Please read and follow our [Code of Con
|
|||||||
|
|
||||||
## <a name="question"></a> Got a Question or Problem?
|
## <a name="question"></a> Got a Question or Problem?
|
||||||
|
|
||||||
Please, do not open issues for the general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [Stack Overflow](https://stackoverflow.com/questions/tagged/angular) where the questions should be tagged with tag `angular`.
|
Do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [Stack Overflow](https://stackoverflow.com/questions/tagged/angular) where the questions should be tagged with tag `angular`.
|
||||||
|
|
||||||
Stack Overflow is a much better place to ask questions since:
|
Stack Overflow is a much better place to ask questions since:
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ Stack Overflow is a much better place to ask questions since:
|
|||||||
- questions and answers stay available for public viewing so your question / answer might help someone else
|
- questions and answers stay available for public viewing so your question / answer might help someone else
|
||||||
- Stack Overflow's voting system assures that the best answers are prominently visible.
|
- Stack Overflow's voting system assures that the best answers are prominently visible.
|
||||||
|
|
||||||
To save your and our time we will be systematically closing all the issues that are requests for general support and redirecting people to Stack Overflow.
|
To save your and our time, we will systematically close all issues that are requests for general support and redirect people to Stack Overflow.
|
||||||
|
|
||||||
If you would like to chat about the question in real-time, you can reach out via [our gitter channel][gitter].
|
If you would like to chat about the question in real-time, you can reach out via [our gitter channel][gitter].
|
||||||
|
|
||||||
|
10
README.md
@ -4,22 +4,20 @@
|
|||||||
[](http://issuestats.com/github/angular/angular)
|
[](http://issuestats.com/github/angular/angular)
|
||||||
[](http://issuestats.com/github/angular/angular)
|
[](http://issuestats.com/github/angular/angular)
|
||||||
[](https://www.npmjs.com/@angular/core)
|
[](https://www.npmjs.com/@angular/core)
|
||||||
)
|
|
||||||
|
|
||||||
[](https://saucelabs.com/u/angular2-ci)
|
[](https://saucelabs.com/u/angular2-ci)
|
||||||
|
|
||||||
*Safari (7+), iOS (7+), Edge (14) and IE mobile (11) are tested on [BrowserStack][browserstack].*
|
*Safari (7+), iOS (7+), Edge (14) and IE mobile (11) are tested on [BrowserStack][browserstack].*
|
||||||
|
|
||||||
Angular
|
# Angular
|
||||||
=========
|
|
||||||
|
|
||||||
Angular is a development platform for building mobile and desktop web applications using Typescript/JavaScript (JS) and other languages.
|
|
||||||
|
|
||||||
|
Angular is a development platform for building mobile and desktop web applications using Typescript/JavaScript and other languages.
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
[Get started in 5 minutes][quickstart].
|
[Get started in 5 minutes][quickstart].
|
||||||
|
|
||||||
|
|
||||||
## Want to help?
|
## Want to help?
|
||||||
|
|
||||||
Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on our
|
Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on our
|
||||||
|
12
WORKSPACE
@ -1,11 +1,17 @@
|
|||||||
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
|
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
|
||||||
|
|
||||||
git_repository(
|
git_repository(
|
||||||
name = "io_bazel_rules_typescript",
|
name = "build_bazel_rules_typescript",
|
||||||
remote = "https://github.com/bazelbuild/rules_typescript.git",
|
remote = "https://github.com/bazelbuild/rules_typescript.git",
|
||||||
commit = "3a8404d",
|
tag = "0.0.5",
|
||||||
)
|
)
|
||||||
|
|
||||||
load("@io_bazel_rules_typescript//:defs.bzl", "node_repositories")
|
load("@build_bazel_rules_typescript//:defs.bzl", "node_repositories")
|
||||||
|
|
||||||
node_repositories(package_json = "//:package.json")
|
node_repositories(package_json = "//:package.json")
|
||||||
|
|
||||||
|
git_repository(
|
||||||
|
name = "build_bazel_rules_angular",
|
||||||
|
remote = "https://github.com/bazelbuild/rules_angular.git",
|
||||||
|
tag = "0.0.1",
|
||||||
|
)
|
@ -31,8 +31,9 @@
|
|||||||
"environmentSource": "environments/environment.ts",
|
"environmentSource": "environments/environment.ts",
|
||||||
"environments": {
|
"environments": {
|
||||||
"dev": "environments/environment.ts",
|
"dev": "environments/environment.ts",
|
||||||
"stage": "environments/environment.stage.ts",
|
"next": "environments/environment.next.ts",
|
||||||
"prod": "environments/environment.prod.ts"
|
"stable": "environments/environment.stable.ts",
|
||||||
|
"archive": "environments/environment.archive.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -88,6 +88,21 @@ server {
|
|||||||
resolver 127.0.0.1;
|
resolver 127.0.0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Notify about PR changes
|
||||||
|
location "~^/pr-updated/?$" {
|
||||||
|
if ($request_method != "POST") {
|
||||||
|
add_header Allow "POST";
|
||||||
|
return 405;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_pass_request_headers on;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_method POST;
|
||||||
|
proxy_pass http://{{$AIO_UPLOAD_HOSTNAME}}:{{$AIO_UPLOAD_PORT}}$request_uri;
|
||||||
|
|
||||||
|
resolver 127.0.0.1;
|
||||||
|
}
|
||||||
|
|
||||||
# Everything else
|
# Everything else
|
||||||
location / {
|
location / {
|
||||||
return 404;
|
return 404;
|
||||||
|
@ -18,45 +18,17 @@ export class BuildCreator extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Methods - Public
|
// Methods - Public
|
||||||
public changePrVisibility(pr: string, makePublic: boolean): Promise<void> {
|
|
||||||
const {oldPrDir, newPrDir} = this.getCandidatePrDirs(pr, makePublic);
|
|
||||||
|
|
||||||
return Promise.
|
|
||||||
all([this.exists(oldPrDir), this.exists(newPrDir)]).
|
|
||||||
then(([oldPrDirExisted, newPrDirExisted]) => {
|
|
||||||
if (!oldPrDirExisted) {
|
|
||||||
throw new UploadError(404, `Request to move non-existing directory '${oldPrDir}' to '${newPrDir}'.`);
|
|
||||||
} else if (newPrDirExisted) {
|
|
||||||
throw new UploadError(409, `Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve().
|
|
||||||
then(() => shell.mv(oldPrDir, newPrDir)).
|
|
||||||
then(() => this.listShasByDate(newPrDir)).
|
|
||||||
then(shas => this.emit(ChangedPrVisibilityEvent.type, new ChangedPrVisibilityEvent(+pr, shas, makePublic))).
|
|
||||||
then(() => undefined);
|
|
||||||
}).
|
|
||||||
catch(err => {
|
|
||||||
if (!(err instanceof UploadError)) {
|
|
||||||
err = new UploadError(500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\n${err}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public create(pr: string, sha: string, archivePath: string, isPublic: boolean): Promise<void> {
|
public create(pr: string, sha: string, archivePath: string, isPublic: boolean): Promise<void> {
|
||||||
// Use only part of the SHA for more readable URLs.
|
// Use only part of the SHA for more readable URLs.
|
||||||
sha = sha.substr(0, SHORT_SHA_LEN);
|
sha = sha.substr(0, SHORT_SHA_LEN);
|
||||||
|
|
||||||
const {oldPrDir: otherVisPrDir, newPrDir: prDir} = this.getCandidatePrDirs(pr, isPublic);
|
const {newPrDir: prDir} = this.getCandidatePrDirs(pr, isPublic);
|
||||||
const shaDir = path.join(prDir, sha);
|
const shaDir = path.join(prDir, sha);
|
||||||
let dirToRemoveOnError: string;
|
let dirToRemoveOnError: string;
|
||||||
|
|
||||||
return Promise.resolve().
|
return Promise.resolve().
|
||||||
then(() => this.exists(otherVisPrDir)).
|
|
||||||
// If the same PR exists with different visibility, update the visibility first.
|
// If the same PR exists with different visibility, update the visibility first.
|
||||||
then(otherVisPrDirExisted => (otherVisPrDirExisted && this.changePrVisibility(pr, isPublic)) as any).
|
then(() => this.updatePrVisibility(pr, isPublic)).
|
||||||
then(() => Promise.all([this.exists(prDir), this.exists(shaDir)])).
|
then(() => Promise.all([this.exists(prDir), this.exists(shaDir)])).
|
||||||
then(([prDirExisted, shaDirExisted]) => {
|
then(([prDirExisted, shaDirExisted]) => {
|
||||||
if (shaDirExisted) {
|
if (shaDirExisted) {
|
||||||
@ -84,6 +56,36 @@ export class BuildCreator extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public updatePrVisibility(pr: string, makePublic: boolean): Promise<boolean> {
|
||||||
|
const {oldPrDir: otherVisPrDir, newPrDir: targetVisPrDir} = this.getCandidatePrDirs(pr, makePublic);
|
||||||
|
|
||||||
|
return Promise.
|
||||||
|
all([this.exists(otherVisPrDir), this.exists(targetVisPrDir)]).
|
||||||
|
then(([otherVisPrDirExisted, targetVisPrDirExisted]) => {
|
||||||
|
if (!otherVisPrDirExisted) {
|
||||||
|
// No visibility change: Either the visibility is up-to-date or the PR does not exist.
|
||||||
|
return false;
|
||||||
|
} else if (targetVisPrDirExisted) {
|
||||||
|
// Error: Directories for both visibilities exist.
|
||||||
|
throw new UploadError(409, `Request to move '${otherVisPrDir}' to existing directory '${targetVisPrDir}'.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visibility change: Moving `otherVisPrDir` to `targetVisPrDir`.
|
||||||
|
return Promise.resolve().
|
||||||
|
then(() => shell.mv(otherVisPrDir, targetVisPrDir)).
|
||||||
|
then(() => this.listShasByDate(targetVisPrDir)).
|
||||||
|
then(shas => this.emit(ChangedPrVisibilityEvent.type, new ChangedPrVisibilityEvent(+pr, shas, makePublic))).
|
||||||
|
then(() => true);
|
||||||
|
}).
|
||||||
|
catch(err => {
|
||||||
|
if (!(err instanceof UploadError)) {
|
||||||
|
err = new UploadError(500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\n${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Methods - Protected
|
// Methods - Protected
|
||||||
protected exists(fileOrDir: string): Promise<boolean> {
|
protected exists(fileOrDir: string): Promise<boolean> {
|
||||||
return new Promise(resolve => fs.access(fileOrDir, err => resolve(!err)));
|
return new Promise(resolve => fs.access(fileOrDir, err => resolve(!err)));
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
// Imports
|
// Imports
|
||||||
|
import * as bodyParser from 'body-parser';
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||||
@ -84,6 +85,7 @@ class UploadServerFactory {
|
|||||||
|
|
||||||
protected createMiddleware(buildVerifier: BuildVerifier, buildCreator: BuildCreator): express.Express {
|
protected createMiddleware(buildVerifier: BuildVerifier, buildCreator: BuildCreator): express.Express {
|
||||||
const middleware = express();
|
const middleware = express();
|
||||||
|
const jsonParser = bodyParser.json();
|
||||||
|
|
||||||
middleware.get(/^\/create-build\/([1-9][0-9]*)\/([0-9a-f]{40})\/?$/, (req, res) => {
|
middleware.get(/^\/create-build\/([1-9][0-9]*)\/([0-9a-f]{40})\/?$/, (req, res) => {
|
||||||
const pr = req.params[0];
|
const pr = req.params[0];
|
||||||
@ -96,8 +98,8 @@ class UploadServerFactory {
|
|||||||
} else if (!archive) {
|
} else if (!archive) {
|
||||||
this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req);
|
this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req);
|
||||||
} else {
|
} else {
|
||||||
buildVerifier.
|
Promise.resolve().
|
||||||
verify(+pr, authHeader).
|
then(() => buildVerifier.verify(+pr, authHeader)).
|
||||||
then(verStatus => verStatus === BUILD_VERIFICATION_STATUS.verifiedAndTrusted).
|
then(verStatus => verStatus === BUILD_VERIFICATION_STATUS.verifiedAndTrusted).
|
||||||
then(isPublic => buildCreator.create(pr, sha, archive, isPublic).
|
then(isPublic => buildCreator.create(pr, sha, archive, isPublic).
|
||||||
then(() => res.sendStatus(isPublic ? 201 : 202))).
|
then(() => res.sendStatus(isPublic ? 201 : 202))).
|
||||||
@ -105,8 +107,23 @@ class UploadServerFactory {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
|
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
|
||||||
middleware.get('*', req => this.throwRequestError(404, 'Unknown resource', req));
|
middleware.post(/^\/pr-updated\/?$/, jsonParser, (req, res) => {
|
||||||
middleware.all('*', req => this.throwRequestError(405, 'Unsupported method', req));
|
const {action, number: prNo}: {action?: string, number?: number} = req.body;
|
||||||
|
const visMayHaveChanged = !action || (action === 'labeled') || (action === 'unlabeled');
|
||||||
|
|
||||||
|
if (!visMayHaveChanged) {
|
||||||
|
res.sendStatus(200);
|
||||||
|
} else if (!prNo) {
|
||||||
|
this.throwRequestError(400, `Missing or empty 'number' field`, req);
|
||||||
|
} else {
|
||||||
|
Promise.resolve().
|
||||||
|
then(() => buildVerifier.getPrIsTrusted(prNo)).
|
||||||
|
then(isPublic => buildCreator.updatePrVisibility(String(prNo), isPublic)).
|
||||||
|
then(() => res.sendStatus(200)).
|
||||||
|
catch(err => this.respondWithError(res, err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
middleware.all('*', req => this.throwRequestError(404, 'Unknown resource', req));
|
||||||
middleware.use((err: any, _req: any, res: express.Response, _next: any) => this.respondWithError(res, err));
|
middleware.use((err: any, _req: any, res: express.Response, _next: any) => this.respondWithError(res, err));
|
||||||
|
|
||||||
return middleware;
|
return middleware;
|
||||||
@ -125,7 +142,10 @@ class UploadServerFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected throwRequestError(status: number, error: string, req: express.Request) {
|
protected throwRequestError(status: number, error: string, req: express.Request) {
|
||||||
throw new UploadError(status, `${error} in request: ${req.method} ${req.originalUrl}`);
|
const message = `${error} in request: ${req.method} ${req.originalUrl}` +
|
||||||
|
(!req.body ? '' : ` ${JSON.stringify(req.body)}`);
|
||||||
|
|
||||||
|
throw new UploadError(status, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
// Using the values below, we can fake the response of the corresponding methods in tests. This is
|
||||||
|
// necessary, because the test upload-server will be running as a separate node process, so we will
|
||||||
|
// not have direct access to the code (e.g. for mocking).
|
||||||
|
// (See also 'lib/verify-setup/start-test-upload-server.ts'.)
|
||||||
|
|
||||||
|
/* tslint:disable: variable-name */
|
||||||
|
|
||||||
|
// Special values to be used as `authHeader` in `BuildVerifier#verify()`.
|
||||||
|
export const BV_verify_error = 'FAKE_VERIFICATION_ERROR';
|
||||||
|
export const BV_verify_verifiedNotTrusted = 'FAKE_VERIFIED_NOT_TRUSTED';
|
||||||
|
|
||||||
|
// Special values to be used as `pr` in `BuildVerifier#getPrIsTrusted()`.
|
||||||
|
export const BV_getPrIsTrusted_error = 32203;
|
||||||
|
export const BV_getPrIsTrusted_notTrusted = 72457;
|
||||||
|
|
||||||
|
/* tslint:enable: variable-name */
|
@ -317,6 +317,51 @@ describe(`nginx`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe(`${host}/pr-updated`, () => {
|
||||||
|
const url = `${scheme}://${host}/pr-updated`;
|
||||||
|
|
||||||
|
|
||||||
|
it('should disallow non-POST requests', done => {
|
||||||
|
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 pass requests through to the upload server', done => {
|
||||||
|
const cmdPrefix = `curl -iLX POST --header "Content-Type: application/json"`;
|
||||||
|
|
||||||
|
const cmd1 = `${cmdPrefix} ${url}`;
|
||||||
|
const cmd2 = `${cmdPrefix} --data '{"number":${pr}}' ${url}`;
|
||||||
|
const cmd3 = `${cmdPrefix} --data '{"number":${pr},"action":"foo"}' ${url}`;
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(cmd1).then(h.verifyResponse(400, /Missing or empty 'number' field/)),
|
||||||
|
h.runCmd(cmd2).then(h.verifyResponse(200)),
|
||||||
|
h.runCmd(cmd3).then(h.verifyResponse(200)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 for unknown paths', done => {
|
||||||
|
const cmdPrefix = `curl -iLX POST ${scheme}://${host}`;
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(`${cmdPrefix}/foo/pr-updated`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/foo-pr-updated`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/foonpr-updated`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/pr-updated/foo`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/pr-updated-foo`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/pr-updatednfoo`).then(h.verifyResponse(404)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe(`${host}/*`, () => {
|
describe(`${host}/*`, () => {
|
||||||
|
|
||||||
it('should respond with 404 for unknown URLs (even if the resource exists)', done => {
|
it('should respond with 404 for unknown URLs (even if the resource exists)', done => {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// Imports
|
// Imports
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import * as c from './constants';
|
||||||
import {helper as h} from './helper';
|
import {helper as h} from './helper';
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
@ -14,12 +15,14 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
|||||||
const getFile = (pr: string, sha: string, file: string) =>
|
const getFile = (pr: string, sha: string, file: string) =>
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${h.getShordSha(sha)}.${host}/${file}`);
|
h.runCmd(`curl -iL ${scheme}://pr${pr}-${h.getShordSha(sha)}.${host}/${file}`);
|
||||||
const uploadBuild = (pr: string, sha: string, archive: string, authHeader = 'Token FOO') => {
|
const uploadBuild = (pr: string, sha: string, archive: string, authHeader = 'Token FOO') => {
|
||||||
// Using `FAKE_VERIFICATION_ERROR` or `FAKE_VERIFIED_NOT_TRUSTED` as `authHeader`,
|
|
||||||
// we can fake the response of the overwritten `BuildVerifier.verify()` method.
|
|
||||||
// (See 'lib/upload-server/index-test.ts'.)
|
|
||||||
const curlPost = `curl -iLX POST --header "Authorization: ${authHeader}"`;
|
const curlPost = `curl -iLX POST --header "Authorization: ${authHeader}"`;
|
||||||
return h.runCmd(`${curlPost} --data-binary "@${archive}" ${scheme}://${host}/create-build/${pr}/${sha}`);
|
return h.runCmd(`${curlPost} --data-binary "@${archive}" ${scheme}://${host}/create-build/${pr}/${sha}`);
|
||||||
};
|
};
|
||||||
|
const prUpdated = (pr: number, action?: string) => {
|
||||||
|
const url = `${scheme}://${host}/pr-updated`;
|
||||||
|
const payloadStr = JSON.stringify({number: pr, action});
|
||||||
|
return h.runCmd(`curl -iLX POST --header "Content-Type: application/json" --data '${payloadStr}' ${url}`);
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
|
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -29,7 +32,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('for a new PR', () => {
|
describe('for a new/non-existing PR', () => {
|
||||||
|
|
||||||
it('should be able to upload and serve a public build', done => {
|
it('should be able to upload and serve a public build', done => {
|
||||||
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
||||||
@ -54,7 +57,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
|||||||
|
|
||||||
h.createDummyArchive(pr9, sha9, archivePath);
|
h.createDummyArchive(pr9, sha9, archivePath);
|
||||||
|
|
||||||
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFIED_NOT_TRUSTED').
|
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
|
||||||
then(() => Promise.all([
|
then(() => Promise.all([
|
||||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(404)),
|
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(404)),
|
||||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(404)),
|
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(404)),
|
||||||
@ -74,7 +77,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
|||||||
|
|
||||||
h.createDummyArchive(pr9, sha9, archivePath);
|
h.createDummyArchive(pr9, sha9, archivePath);
|
||||||
|
|
||||||
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFICATION_ERROR').
|
uploadBuild(pr9, sha9, archivePath, c.BV_verify_error).
|
||||||
then(h.verifyResponse(403, errorRegex9)).
|
then(h.verifyResponse(403, errorRegex9)).
|
||||||
then(() => {
|
then(() => {
|
||||||
expect(h.buildExists(pr9)).toBe(false);
|
expect(h.buildExists(pr9)).toBe(false);
|
||||||
@ -83,6 +86,18 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
|||||||
then(done);
|
then(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should be able to notify that a PR has been updated (and do nothing)', done => {
|
||||||
|
prUpdated(+pr9).
|
||||||
|
then(h.verifyResponse(200)).
|
||||||
|
then(() => {
|
||||||
|
// The PR should still not exist.
|
||||||
|
expect(h.buildExists(pr9, '', false)).toBe(false);
|
||||||
|
expect(h.buildExists(pr9, '', true)).toBe(false);
|
||||||
|
}).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -123,7 +138,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
|||||||
h.createDummyBuild(pr9, sha0, false);
|
h.createDummyBuild(pr9, sha0, false);
|
||||||
h.createDummyArchive(pr9, sha9, archivePath);
|
h.createDummyArchive(pr9, sha9, archivePath);
|
||||||
|
|
||||||
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFIED_NOT_TRUSTED').
|
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
|
||||||
then(() => Promise.all([
|
then(() => Promise.all([
|
||||||
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(404)),
|
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(404)),
|
||||||
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(404)),
|
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(404)),
|
||||||
@ -148,7 +163,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
|||||||
h.createDummyBuild(pr9, sha0);
|
h.createDummyBuild(pr9, sha0);
|
||||||
h.createDummyArchive(pr9, sha9, archivePath);
|
h.createDummyArchive(pr9, sha9, archivePath);
|
||||||
|
|
||||||
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFICATION_ERROR').
|
uploadBuild(pr9, sha9, archivePath, c.BV_verify_error).
|
||||||
then(h.verifyResponse(403, errorRegex9)).
|
then(h.verifyResponse(403, errorRegex9)).
|
||||||
then(() => {
|
then(() => {
|
||||||
expect(h.buildExists(pr9)).toBe(true);
|
expect(h.buildExists(pr9)).toBe(true);
|
||||||
@ -186,7 +201,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
|||||||
h.createDummyBuild(pr9, sha9, false);
|
h.createDummyBuild(pr9, sha9, false);
|
||||||
h.createDummyArchive(pr9, sha9, archivePath);
|
h.createDummyArchive(pr9, sha9, archivePath);
|
||||||
|
|
||||||
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFIED_NOT_TRUSTED').
|
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
|
||||||
then(h.verifyResponse(409)).
|
then(h.verifyResponse(409)).
|
||||||
then(() => {
|
then(() => {
|
||||||
expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9);
|
expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9);
|
||||||
@ -195,6 +210,110 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
|||||||
then(done);
|
then(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should be able to request re-checking visibility (if outdated)', done => {
|
||||||
|
const publicPr = pr9;
|
||||||
|
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
||||||
|
|
||||||
|
h.createDummyBuild(publicPr, sha9, false);
|
||||||
|
h.createDummyBuild(hiddenPr, sha9, true);
|
||||||
|
|
||||||
|
// PR visibilities are outdated (i.e. the opposte of what the should).
|
||||||
|
expect(h.buildExists(publicPr, '', false)).toBe(true);
|
||||||
|
expect(h.buildExists(publicPr, '', true)).toBe(false);
|
||||||
|
expect(h.buildExists(hiddenPr, '', false)).toBe(false);
|
||||||
|
expect(h.buildExists(hiddenPr, '', true)).toBe(true);
|
||||||
|
|
||||||
|
Promise.
|
||||||
|
all([
|
||||||
|
prUpdated(+publicPr).then(h.verifyResponse(200)),
|
||||||
|
prUpdated(+hiddenPr).then(h.verifyResponse(200)),
|
||||||
|
]).
|
||||||
|
then(() => {
|
||||||
|
// PR visibilities should have been updated.
|
||||||
|
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
||||||
|
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
||||||
|
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
||||||
|
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
||||||
|
}).
|
||||||
|
then(() => {
|
||||||
|
h.deletePrDir(publicPr, true);
|
||||||
|
h.deletePrDir(hiddenPr, false);
|
||||||
|
}).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should be able to request re-checking visibility (if up-to-date)', done => {
|
||||||
|
const publicPr = pr9;
|
||||||
|
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
||||||
|
|
||||||
|
h.createDummyBuild(publicPr, sha9, true);
|
||||||
|
h.createDummyBuild(hiddenPr, sha9, false);
|
||||||
|
|
||||||
|
// PR visibilities are already up-to-date.
|
||||||
|
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
||||||
|
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
||||||
|
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
||||||
|
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
||||||
|
|
||||||
|
Promise.
|
||||||
|
all([
|
||||||
|
prUpdated(+publicPr).then(h.verifyResponse(200)),
|
||||||
|
prUpdated(+hiddenPr).then(h.verifyResponse(200)),
|
||||||
|
]).
|
||||||
|
then(() => {
|
||||||
|
// PR visibilities are still up-to-date.
|
||||||
|
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
||||||
|
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
||||||
|
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
||||||
|
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
||||||
|
}).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should reject a request if re-checking visibility fails', done => {
|
||||||
|
const errorPr = String(c.BV_getPrIsTrusted_error);
|
||||||
|
|
||||||
|
h.createDummyBuild(errorPr, sha9, true);
|
||||||
|
|
||||||
|
expect(h.buildExists(errorPr, '', false)).toBe(false);
|
||||||
|
expect(h.buildExists(errorPr, '', true)).toBe(true);
|
||||||
|
|
||||||
|
prUpdated(+errorPr).
|
||||||
|
then(h.verifyResponse(500, /Test/)).
|
||||||
|
then(() => {
|
||||||
|
// PR visibility should not have been updated.
|
||||||
|
expect(h.buildExists(errorPr, '', false)).toBe(false);
|
||||||
|
expect(h.buildExists(errorPr, '', true)).toBe(true);
|
||||||
|
}).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should reject a request if updating visibility fails', done => {
|
||||||
|
// One way to cause an error is to have both a public and a hidden directory for the same PR.
|
||||||
|
h.createDummyBuild(pr9, sha9, false);
|
||||||
|
h.createDummyBuild(pr9, sha9, true);
|
||||||
|
|
||||||
|
const hiddenPrDir = h.getPrDir(pr9, false);
|
||||||
|
const publicPrDir = h.getPrDir(pr9, true);
|
||||||
|
const bodyRegex = new RegExp(`Request to move '${hiddenPrDir}' to existing directory '${publicPrDir}'`);
|
||||||
|
|
||||||
|
expect(h.buildExists(pr9, '', false)).toBe(true);
|
||||||
|
expect(h.buildExists(pr9, '', true)).toBe(true);
|
||||||
|
|
||||||
|
prUpdated(+pr9).
|
||||||
|
then(h.verifyResponse(409, bodyRegex)).
|
||||||
|
then(() => {
|
||||||
|
// PR visibility should not have been updated.
|
||||||
|
expect(h.buildExists(pr9, '', false)).toBe(true);
|
||||||
|
expect(h.buildExists(pr9, '', true)).toBe(true);
|
||||||
|
}).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}));
|
}));
|
||||||
|
@ -1,17 +1,31 @@
|
|||||||
// Imports
|
// Imports
|
||||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||||
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from './build-verifier';
|
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../upload-server/build-verifier';
|
||||||
import {UploadError} from './upload-error';
|
import {UploadError} from '../upload-server/upload-error';
|
||||||
|
import * as c from './constants';
|
||||||
|
|
||||||
// Run
|
// Run
|
||||||
// TODO(gkalpak): Add e2e tests to cover these interactions as well.
|
// TODO(gkalpak): Add e2e tests to cover these interactions as well.
|
||||||
GithubPullRequests.prototype.addComment = () => Promise.resolve();
|
GithubPullRequests.prototype.addComment = () => Promise.resolve();
|
||||||
|
BuildVerifier.prototype.getPrIsTrusted = (pr: number) => {
|
||||||
|
switch (pr) {
|
||||||
|
case c.BV_getPrIsTrusted_error:
|
||||||
|
// For e2e tests, fake an error.
|
||||||
|
return Promise.reject('Test');
|
||||||
|
case c.BV_getPrIsTrusted_notTrusted:
|
||||||
|
// For e2e tests, fake an untrusted PR (`false`).
|
||||||
|
return Promise.resolve(false);
|
||||||
|
default:
|
||||||
|
// For e2e tests, default to trusted PRs (`true`).
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
BuildVerifier.prototype.verify = (expectedPr: number, authHeader: string) => {
|
BuildVerifier.prototype.verify = (expectedPr: number, authHeader: string) => {
|
||||||
switch (authHeader) {
|
switch (authHeader) {
|
||||||
case 'FAKE_VERIFICATION_ERROR':
|
case c.BV_verify_error:
|
||||||
// For e2e tests, fake a verification error.
|
// For e2e tests, fake a verification error.
|
||||||
return Promise.reject(new UploadError(403, `Error while verifying upload for PR ${expectedPr}: Test`));
|
return Promise.reject(new UploadError(403, `Error while verifying upload for PR ${expectedPr}: Test`));
|
||||||
case 'FAKE_VERIFIED_NOT_TRUSTED':
|
case c.BV_verify_verifiedNotTrusted:
|
||||||
// For e2e tests, fake a `verifiedNotTrusted` verification status.
|
// For e2e tests, fake a `verifiedNotTrusted` verification status.
|
||||||
return Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted);
|
return Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted);
|
||||||
default:
|
default:
|
||||||
@ -21,4 +35,4 @@ BuildVerifier.prototype.verify = (expectedPr: number, authHeader: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// tslint:disable-next-line: no-var-requires
|
// tslint:disable-next-line: no-var-requires
|
||||||
require('./index');
|
require('../upload-server/index');
|
@ -1,6 +1,7 @@
|
|||||||
// Imports
|
// Imports
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import * as c from './constants';
|
||||||
import {CmdResult, helper as h} from './helper';
|
import {CmdResult, helper as h} from './helper';
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
@ -25,13 +26,13 @@ describe('upload-server (on HTTP)', () => {
|
|||||||
|
|
||||||
it('should disallow non-GET requests', done => {
|
it('should disallow non-GET requests', done => {
|
||||||
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
||||||
const bodyRegex = /^Unsupported method/;
|
const bodyRegex = /^Unknown resource/;
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
h.runCmd(`curl -iLX POST ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
h.runCmd(`curl -iLX POST ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
]).then(done);
|
]).then(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -63,7 +64,7 @@ describe('upload-server (on HTTP)', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should reject requests for which the PR verification fails', done => {
|
it('should reject requests for which the PR verification fails', done => {
|
||||||
const headers = `--header "Authorization: FAKE_VERIFICATION_ERROR" ${xFileHeader}`;
|
const headers = `--header "Authorization: ${c.BV_verify_error}" ${xFileHeader}`;
|
||||||
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
||||||
const bodyRegex = new RegExp(`Error while verifying upload for PR ${pr}: Test`);
|
const bodyRegex = new RegExp(`Error while verifying upload for PR ${pr}: Test`);
|
||||||
|
|
||||||
@ -107,7 +108,7 @@ describe('upload-server (on HTTP)', () => {
|
|||||||
|
|
||||||
[true, false].forEach(isPublic => describe(`(for ${isPublic ? 'public' : 'hidden'} builds)`, () => {
|
[true, false].forEach(isPublic => describe(`(for ${isPublic ? 'public' : 'hidden'} builds)`, () => {
|
||||||
const authorizationHeader2 = isPublic ?
|
const authorizationHeader2 = isPublic ?
|
||||||
authorizationHeader : '--header "Authorization: FAKE_VERIFIED_NOT_TRUSTED"';
|
authorizationHeader : `--header "Authorization: ${c.BV_verify_verifiedNotTrusted}"`;
|
||||||
const cmdPrefix = curl('', `${authorizationHeader2} ${xFileHeader}`);
|
const cmdPrefix = curl('', `${authorizationHeader2} ${xFileHeader}`);
|
||||||
|
|
||||||
|
|
||||||
@ -373,27 +374,194 @@ describe('upload-server (on HTTP)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe(`${host}/pr-updated`, () => {
|
||||||
|
const url = `http://${host}/pr-updated`;
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
const curl = (payload?: {number: number, action?: string}) => {
|
||||||
|
const payloadStr = payload && JSON.stringify(payload) || '';
|
||||||
|
return `curl -iLX POST --header "Content-Type: application/json" --data '${payloadStr}' ${url}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
it('should disallow non-POST requests', done => {
|
||||||
|
const bodyRegex = /^Unknown resource in request/;
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 400 for requests without a payload', done => {
|
||||||
|
const bodyRegex = /^Missing or empty 'number' field in request/;
|
||||||
|
|
||||||
|
h.runCmd(curl()).
|
||||||
|
then(h.verifyResponse(400, bodyRegex)).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 400 for requests without a \'number\' field', done => {
|
||||||
|
const bodyRegex = /^Missing or empty 'number' field in request/;
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(curl({} as any)).then(h.verifyResponse(400, bodyRegex)),
|
||||||
|
h.runCmd(curl({number: null} as any)).then(h.verifyResponse(400, bodyRegex)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should reject requests for which checking the PR visibility fails', done => {
|
||||||
|
h.runCmd(curl({number: c.BV_getPrIsTrusted_error})).
|
||||||
|
then(h.verifyResponse(500, /Test/)).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 for unknown paths', done => {
|
||||||
|
const mockPayload = JSON.stringify({number: +pr});
|
||||||
|
const cmdPrefix = `curl -iLX POST --data "${mockPayload}" http://${host}`;
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(`${cmdPrefix}/foo/pr-updated`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/foo-pr-updated`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/foonpr-updated`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/pr-updated/foo`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/pr-updated-foo`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/pr-updatednfoo`).then(h.verifyResponse(404)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should do nothing if PR\'s visibility is already up-to-date', done => {
|
||||||
|
const publicPr = pr;
|
||||||
|
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
||||||
|
const checkVisibilities = () => {
|
||||||
|
// Public build is already public.
|
||||||
|
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
||||||
|
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
||||||
|
// Hidden build is already hidden.
|
||||||
|
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
||||||
|
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
h.createDummyBuild(publicPr, sha9, true);
|
||||||
|
h.createDummyBuild(hiddenPr, sha9, false);
|
||||||
|
checkVisibilities();
|
||||||
|
|
||||||
|
Promise.
|
||||||
|
all([
|
||||||
|
h.runCmd(curl({number: +publicPr, action: 'foo'})).then(h.verifyResponse(200)),
|
||||||
|
h.runCmd(curl({number: +hiddenPr, action: 'foo'})).then(h.verifyResponse(200)),
|
||||||
|
]).
|
||||||
|
// Visibilities should not have changed, because the specified action could not have triggered a change.
|
||||||
|
then(checkVisibilities).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should do nothing if \'action\' implies no visibility change', done => {
|
||||||
|
const publicPr = pr;
|
||||||
|
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
||||||
|
const checkVisibilities = () => {
|
||||||
|
// Public build is hidden atm.
|
||||||
|
expect(h.buildExists(publicPr, '', false)).toBe(true);
|
||||||
|
expect(h.buildExists(publicPr, '', true)).toBe(false);
|
||||||
|
// Hidden build is public atm.
|
||||||
|
expect(h.buildExists(hiddenPr, '', false)).toBe(false);
|
||||||
|
expect(h.buildExists(hiddenPr, '', true)).toBe(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
h.createDummyBuild(publicPr, sha9, false);
|
||||||
|
h.createDummyBuild(hiddenPr, sha9, true);
|
||||||
|
checkVisibilities();
|
||||||
|
|
||||||
|
Promise.
|
||||||
|
all([
|
||||||
|
h.runCmd(curl({number: +publicPr, action: 'foo'})).then(h.verifyResponse(200)),
|
||||||
|
h.runCmd(curl({number: +hiddenPr, action: 'foo'})).then(h.verifyResponse(200)),
|
||||||
|
]).
|
||||||
|
// Visibilities should not have changed, because the specified action could not have triggered a change.
|
||||||
|
then(checkVisibilities).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('when the visiblity has changed', () => {
|
||||||
|
const publicPr = pr;
|
||||||
|
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create initial PR builds with opposite visibilities as the ones that will be reported:
|
||||||
|
// - The now public PR was previously hidden.
|
||||||
|
// - The now hidden PR was previously public.
|
||||||
|
h.createDummyBuild(publicPr, sha9, false);
|
||||||
|
h.createDummyBuild(hiddenPr, sha9, true);
|
||||||
|
|
||||||
|
expect(h.buildExists(publicPr, '', false)).toBe(true);
|
||||||
|
expect(h.buildExists(publicPr, '', true)).toBe(false);
|
||||||
|
expect(h.buildExists(hiddenPr, '', false)).toBe(false);
|
||||||
|
expect(h.buildExists(hiddenPr, '', true)).toBe(true);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
// Expect PRs' visibility to have been updated:
|
||||||
|
// - The public PR should be actually public (previously it was hidden).
|
||||||
|
// - The hidden PR should be actually hidden (previously it was public).
|
||||||
|
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
||||||
|
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
||||||
|
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
||||||
|
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
||||||
|
|
||||||
|
h.deletePrDir(publicPr, true);
|
||||||
|
h.deletePrDir(hiddenPr, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should update the PR\'s visibility (action: undefined)', done => {
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(curl({number: +publicPr})).then(h.verifyResponse(200)),
|
||||||
|
h.runCmd(curl({number: +hiddenPr})).then(h.verifyResponse(200)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should update the PR\'s visibility (action: labeled)', done => {
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(curl({number: +publicPr, action: 'labeled'})).then(h.verifyResponse(200)),
|
||||||
|
h.runCmd(curl({number: +hiddenPr, action: 'labeled'})).then(h.verifyResponse(200)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should update the PR\'s visibility (action: unlabeled)', done => {
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(curl({number: +publicPr, action: 'unlabeled'})).then(h.verifyResponse(200)),
|
||||||
|
h.runCmd(curl({number: +hiddenPr, action: 'unlabeled'})).then(h.verifyResponse(200)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe(`${host}/*`, () => {
|
describe(`${host}/*`, () => {
|
||||||
|
|
||||||
it('should respond with 404 for GET requests to unknown URLs', done => {
|
it('should respond with 404 for requests to unknown URLs', done => {
|
||||||
const bodyRegex = /^Unknown resource/;
|
const bodyRegex = /^Unknown resource/;
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
h.runCmd(`curl -iL http://${host}/index.html`).then(h.verifyResponse(404, bodyRegex)),
|
h.runCmd(`curl -iL http://${host}/index.html`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
h.runCmd(`curl -iL http://${host}/`).then(h.verifyResponse(404, bodyRegex)),
|
h.runCmd(`curl -iL http://${host}/`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
h.runCmd(`curl -iL http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
h.runCmd(`curl -iL http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
]).then(done);
|
h.runCmd(`curl -iLX PUT http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
});
|
h.runCmd(`curl -iLX POST http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
h.runCmd(`curl -iLX PATCH http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
h.runCmd(`curl -iLX DELETE http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
it('should respond with 405 for non-GET requests to any URL', done => {
|
|
||||||
const bodyRegex = /^Unsupported method/;
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(`curl -iLX PUT http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
|
||||||
h.runCmd(`curl -iLX POST http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
|
||||||
h.runCmd(`curl -iLX PATCH http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
|
||||||
h.runCmd(`curl -iLX DELETE http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
|
||||||
]).then(done);
|
]).then(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -20,12 +20,14 @@
|
|||||||
"test-watch": "nodemon --exec \"yarn ~~test-only\" --watch dist"
|
"test-watch": "nodemon --exec \"yarn ~~test-only\" --watch dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"body-parser": "^1.17.2",
|
||||||
"express": "^4.14.1",
|
"express": "^4.14.1",
|
||||||
"jasmine": "^2.5.3",
|
"jasmine": "^2.5.3",
|
||||||
"jsonwebtoken": "^7.3.0",
|
"jsonwebtoken": "^7.3.0",
|
||||||
"shelljs": "^0.7.6"
|
"shelljs": "^0.7.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/body-parser": "^1.16.4",
|
||||||
"@types/express": "^4.0.35",
|
"@types/express": "^4.0.35",
|
||||||
"@types/jasmine": "^2.5.43",
|
"@types/jasmine": "^2.5.43",
|
||||||
"@types/jsonwebtoken": "^7.2.0",
|
"@types/jsonwebtoken": "^7.2.0",
|
||||||
|
@ -43,178 +43,25 @@ describe('BuildCreator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('changePrVisibility()', () => {
|
|
||||||
let bcEmitSpy: jasmine.Spy;
|
|
||||||
let bcExistsSpy: jasmine.Spy;
|
|
||||||
let bcListShasByDate: jasmine.Spy;
|
|
||||||
let shellMvSpy: jasmine.Spy;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
bcEmitSpy = spyOn(bc, 'emit');
|
|
||||||
bcExistsSpy = spyOn(bc as any, 'exists');
|
|
||||||
bcListShasByDate = spyOn(bc as any, 'listShasByDate');
|
|
||||||
shellMvSpy = spyOn(shell, 'mv');
|
|
||||||
|
|
||||||
bcExistsSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
|
||||||
bcListShasByDate.and.returnValue([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should return a promise', done => {
|
|
||||||
const promise = bc.changePrVisibility(pr, true);
|
|
||||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
|
||||||
// to avoid running the actual `extractArchive()`.
|
|
||||||
|
|
||||||
expect(promise).toEqual(jasmine.any(Promise));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
[true, false].forEach(makePublic => {
|
|
||||||
const oldPrDir = makePublic ? hiddenPrDir : publicPrDir;
|
|
||||||
const newPrDir = makePublic ? publicPrDir : hiddenPrDir;
|
|
||||||
|
|
||||||
|
|
||||||
it('should rename the directory', done => {
|
|
||||||
bc.changePrVisibility(pr, makePublic).
|
|
||||||
then(() => expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir)).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should emit a ChangedPrVisibilityEvent on success', done => {
|
|
||||||
let emitted = false;
|
|
||||||
|
|
||||||
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
|
|
||||||
expect(type).toBe(ChangedPrVisibilityEvent.type);
|
|
||||||
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
|
|
||||||
expect(evt.pr).toBe(+pr);
|
|
||||||
expect(evt.shas).toEqual(jasmine.any(Array));
|
|
||||||
expect(evt.isPublic).toBe(makePublic);
|
|
||||||
|
|
||||||
emitted = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
bc.changePrVisibility(pr, makePublic).
|
|
||||||
then(() => expect(emitted).toBe(true)).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should include all shas in the emitted event', done => {
|
|
||||||
const shas = ['foo', 'bar', 'baz'];
|
|
||||||
let emitted = false;
|
|
||||||
|
|
||||||
bcListShasByDate.and.returnValue(Promise.resolve(shas));
|
|
||||||
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
|
|
||||||
expect(bcListShasByDate).toHaveBeenCalledWith(newPrDir);
|
|
||||||
|
|
||||||
expect(type).toBe(ChangedPrVisibilityEvent.type);
|
|
||||||
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
|
|
||||||
expect(evt.pr).toBe(+pr);
|
|
||||||
expect(evt.shas).toBe(shas);
|
|
||||||
expect(evt.isPublic).toBe(makePublic);
|
|
||||||
|
|
||||||
emitted = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
bc.changePrVisibility(pr, makePublic).
|
|
||||||
then(() => expect(emitted).toBe(true)).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('on error', () => {
|
|
||||||
|
|
||||||
it('should abort and skip further operations if the old directory does not exist', done => {
|
|
||||||
bcExistsSpy.and.callFake((dir: string) => dir !== oldPrDir);
|
|
||||||
bc.changePrVisibility(pr, makePublic).catch(err => {
|
|
||||||
expectToBeUploadError(err, 404, `Request to move non-existing directory '${oldPrDir}' to '${newPrDir}'.`);
|
|
||||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
|
||||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
|
||||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should abort and skip further operations if the new directory does already exist', done => {
|
|
||||||
bcExistsSpy.and.returnValue(true);
|
|
||||||
bc.changePrVisibility(pr, makePublic).catch(err => {
|
|
||||||
expectToBeUploadError(err, 409, `Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
|
|
||||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
|
||||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
|
||||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should abort and skip further operations if it fails to rename the directory', done => {
|
|
||||||
shellMvSpy.and.throwError('');
|
|
||||||
bc.changePrVisibility(pr, makePublic).catch(() => {
|
|
||||||
expect(shellMvSpy).toHaveBeenCalled();
|
|
||||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
|
||||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should abort and skip further operations if it fails to list the SHAs', done => {
|
|
||||||
bcListShasByDate.and.throwError('');
|
|
||||||
bc.changePrVisibility(pr, makePublic).catch(() => {
|
|
||||||
expect(shellMvSpy).toHaveBeenCalled();
|
|
||||||
expect(bcListShasByDate).toHaveBeenCalled();
|
|
||||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should reject with an UploadError', done => {
|
|
||||||
shellMvSpy.and.callFake(() => { throw 'Test'; });
|
|
||||||
bc.changePrVisibility(pr, makePublic).catch(err => {
|
|
||||||
expectToBeUploadError(err, 500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\nTest`);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should pass UploadError instances unmodified', done => {
|
|
||||||
shellMvSpy.and.callFake(() => { throw new UploadError(543, 'Test'); });
|
|
||||||
bc.changePrVisibility(pr, makePublic).catch(err => {
|
|
||||||
expectToBeUploadError(err, 543, 'Test');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('create()', () => {
|
describe('create()', () => {
|
||||||
let bcChangePrVisibilitySpy: jasmine.Spy;
|
|
||||||
let bcEmitSpy: jasmine.Spy;
|
let bcEmitSpy: jasmine.Spy;
|
||||||
let bcExistsSpy: jasmine.Spy;
|
let bcExistsSpy: jasmine.Spy;
|
||||||
let bcExtractArchiveSpy: jasmine.Spy;
|
let bcExtractArchiveSpy: jasmine.Spy;
|
||||||
|
let bcUpdatePrVisibilitySpy: jasmine.Spy;
|
||||||
let shellMkdirSpy: jasmine.Spy;
|
let shellMkdirSpy: jasmine.Spy;
|
||||||
let shellRmSpy: jasmine.Spy;
|
let shellRmSpy: jasmine.Spy;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
bcChangePrVisibilitySpy = spyOn(bc, 'changePrVisibility');
|
|
||||||
bcEmitSpy = spyOn(bc, 'emit');
|
bcEmitSpy = spyOn(bc, 'emit');
|
||||||
bcExistsSpy = spyOn(bc as any, 'exists');
|
bcExistsSpy = spyOn(bc as any, 'exists');
|
||||||
bcExtractArchiveSpy = spyOn(bc as any, 'extractArchive');
|
bcExtractArchiveSpy = spyOn(bc as any, 'extractArchive');
|
||||||
|
bcUpdatePrVisibilitySpy = spyOn(bc, 'updatePrVisibility');
|
||||||
shellMkdirSpy = spyOn(shell, 'mkdir');
|
shellMkdirSpy = spyOn(shell, 'mkdir');
|
||||||
shellRmSpy = spyOn(shell, 'rm');
|
shellRmSpy = spyOn(shell, 'rm');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
[true, false].forEach(isPublic => {
|
[true, false].forEach(isPublic => {
|
||||||
const otherVisPrDir = isPublic ? hiddenPrDir : publicPrDir;
|
|
||||||
const prDir = isPublic ? publicPrDir : hiddenPrDir;
|
const prDir = isPublic ? publicPrDir : hiddenPrDir;
|
||||||
const shaDir = isPublic ? publicShaDir : hiddenShaDir;
|
const shaDir = isPublic ? publicShaDir : hiddenShaDir;
|
||||||
|
|
||||||
@ -228,20 +75,12 @@ describe('BuildCreator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should not update the PR\'s visibility first if not necessary', done => {
|
|
||||||
bc.create(pr, sha, archive, isPublic).
|
|
||||||
then(() => expect(bcChangePrVisibilitySpy).not.toHaveBeenCalled()).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should update the PR\'s visibility first if necessary', done => {
|
it('should update the PR\'s visibility first if necessary', done => {
|
||||||
bcChangePrVisibilitySpy.and.callFake(() => expect(shellMkdirSpy).not.toHaveBeenCalled());
|
bcUpdatePrVisibilitySpy.and.callFake(() => expect(shellMkdirSpy).not.toHaveBeenCalled());
|
||||||
bcExistsSpy.and.callFake((dir: string) => dir === otherVisPrDir);
|
|
||||||
|
|
||||||
bc.create(pr, sha, archive, isPublic).
|
bc.create(pr, sha, archive, isPublic).
|
||||||
then(() => {
|
then(() => {
|
||||||
expect(bcChangePrVisibilitySpy).toHaveBeenCalledWith(pr, isPublic);
|
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(pr, isPublic);
|
||||||
expect(shellMkdirSpy).toHaveBeenCalled();
|
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||||
}).
|
}).
|
||||||
then(done);
|
then(done);
|
||||||
@ -286,7 +125,6 @@ describe('BuildCreator', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
existsValues = {
|
existsValues = {
|
||||||
[otherVisPrDir]: false,
|
|
||||||
[prDir]: false,
|
[prDir]: false,
|
||||||
[shaDir]: false,
|
[shaDir]: false,
|
||||||
};
|
};
|
||||||
@ -297,14 +135,12 @@ describe('BuildCreator', () => {
|
|||||||
|
|
||||||
it('should abort and skip further operations if changing the PR\'s visibility fails', done => {
|
it('should abort and skip further operations if changing the PR\'s visibility fails', done => {
|
||||||
const mockError = new UploadError(543, 'Test');
|
const mockError = new UploadError(543, 'Test');
|
||||||
|
bcUpdatePrVisibilitySpy.and.returnValue(Promise.reject(mockError));
|
||||||
existsValues[otherVisPrDir] = true;
|
|
||||||
bcChangePrVisibilitySpy.and.returnValue(Promise.reject(mockError));
|
|
||||||
|
|
||||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||||
expect(err).toBe(mockError);
|
expect(err).toBe(mockError);
|
||||||
|
|
||||||
expect(bcExistsSpy).toHaveBeenCalledTimes(1);
|
expect(bcExistsSpy).not.toHaveBeenCalled();
|
||||||
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||||
@ -327,8 +163,10 @@ describe('BuildCreator', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should detect existing build directory after visibility change', done => {
|
it('should detect existing build directory after visibility change', done => {
|
||||||
existsValues[otherVisPrDir] = true;
|
bcUpdatePrVisibilitySpy.and.callFake(() => existsValues[prDir] = existsValues[shaDir] = true);
|
||||||
bcChangePrVisibilitySpy.and.callFake(() => existsValues[prDir] = existsValues[shaDir] = true);
|
|
||||||
|
expect(bcExistsSpy(prDir)).toBe(false);
|
||||||
|
expect(bcExistsSpy(shaDir)).toBe(false);
|
||||||
|
|
||||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||||
expectToBeUploadError(err, 409, `Request to overwrite existing directory: ${shaDir}`);
|
expectToBeUploadError(err, 409, `Request to overwrite existing directory: ${shaDir}`);
|
||||||
@ -406,6 +244,190 @@ describe('BuildCreator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('updatePrVisibility()', () => {
|
||||||
|
let bcEmitSpy: jasmine.Spy;
|
||||||
|
let bcExistsSpy: jasmine.Spy;
|
||||||
|
let bcListShasByDate: jasmine.Spy;
|
||||||
|
let shellMvSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
bcEmitSpy = spyOn(bc, 'emit');
|
||||||
|
bcExistsSpy = spyOn(bc as any, 'exists');
|
||||||
|
bcListShasByDate = spyOn(bc as any, 'listShasByDate');
|
||||||
|
shellMvSpy = spyOn(shell, 'mv');
|
||||||
|
|
||||||
|
bcExistsSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||||
|
bcListShasByDate.and.returnValue([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should return a promise', done => {
|
||||||
|
const promise = bc.updatePrVisibility(pr, true);
|
||||||
|
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||||
|
// to avoid running the actual `extractArchive()`.
|
||||||
|
|
||||||
|
expect(promise).toEqual(jasmine.any(Promise));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
[true, false].forEach(makePublic => {
|
||||||
|
const oldPrDir = makePublic ? hiddenPrDir : publicPrDir;
|
||||||
|
const newPrDir = makePublic ? publicPrDir : hiddenPrDir;
|
||||||
|
|
||||||
|
|
||||||
|
it('should rename the directory', done => {
|
||||||
|
bc.updatePrVisibility(pr, makePublic).
|
||||||
|
then(() => expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir)).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('when the visibility is updated', () => {
|
||||||
|
|
||||||
|
it('should resolve to true', done => {
|
||||||
|
bc.updatePrVisibility(pr, makePublic).
|
||||||
|
then(result => expect(result).toBe(true)).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should rename the directory', done => {
|
||||||
|
bc.updatePrVisibility(pr, makePublic).
|
||||||
|
then(() => expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir)).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should emit a ChangedPrVisibilityEvent on success', done => {
|
||||||
|
let emitted = false;
|
||||||
|
|
||||||
|
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
|
||||||
|
expect(type).toBe(ChangedPrVisibilityEvent.type);
|
||||||
|
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
|
||||||
|
expect(evt.pr).toBe(+pr);
|
||||||
|
expect(evt.shas).toEqual(jasmine.any(Array));
|
||||||
|
expect(evt.isPublic).toBe(makePublic);
|
||||||
|
|
||||||
|
emitted = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
bc.updatePrVisibility(pr, makePublic).
|
||||||
|
then(() => expect(emitted).toBe(true)).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should include all shas in the emitted event', done => {
|
||||||
|
const shas = ['foo', 'bar', 'baz'];
|
||||||
|
let emitted = false;
|
||||||
|
|
||||||
|
bcListShasByDate.and.returnValue(Promise.resolve(shas));
|
||||||
|
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
|
||||||
|
expect(bcListShasByDate).toHaveBeenCalledWith(newPrDir);
|
||||||
|
|
||||||
|
expect(type).toBe(ChangedPrVisibilityEvent.type);
|
||||||
|
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
|
||||||
|
expect(evt.pr).toBe(+pr);
|
||||||
|
expect(evt.shas).toBe(shas);
|
||||||
|
expect(evt.isPublic).toBe(makePublic);
|
||||||
|
|
||||||
|
emitted = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
bc.updatePrVisibility(pr, makePublic).
|
||||||
|
then(() => expect(emitted).toBe(true)).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should do nothing if the visibility is already up-to-date', done => {
|
||||||
|
bcExistsSpy.and.callFake((dir: string) => dir === newPrDir);
|
||||||
|
bc.updatePrVisibility(pr, makePublic).
|
||||||
|
then(result => {
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||||
|
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||||
|
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||||
|
}).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should do nothing if the PR directory does not exist', done => {
|
||||||
|
bcExistsSpy.and.returnValue(false);
|
||||||
|
bc.updatePrVisibility(pr, makePublic).
|
||||||
|
then(result => {
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||||
|
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||||
|
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||||
|
}).
|
||||||
|
then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('on error', () => {
|
||||||
|
|
||||||
|
it('should abort and skip further operations if both directories exist', done => {
|
||||||
|
bcExistsSpy.and.returnValue(true);
|
||||||
|
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
||||||
|
expectToBeUploadError(err, 409, `Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
|
||||||
|
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||||
|
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||||
|
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should abort and skip further operations if it fails to rename the directory', done => {
|
||||||
|
shellMvSpy.and.throwError('');
|
||||||
|
bc.updatePrVisibility(pr, makePublic).catch(() => {
|
||||||
|
expect(shellMvSpy).toHaveBeenCalled();
|
||||||
|
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||||
|
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should abort and skip further operations if it fails to list the SHAs', done => {
|
||||||
|
bcListShasByDate.and.throwError('');
|
||||||
|
bc.updatePrVisibility(pr, makePublic).catch(() => {
|
||||||
|
expect(shellMvSpy).toHaveBeenCalled();
|
||||||
|
expect(bcListShasByDate).toHaveBeenCalled();
|
||||||
|
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should reject with an UploadError', done => {
|
||||||
|
shellMvSpy.and.callFake(() => { throw 'Test'; });
|
||||||
|
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
||||||
|
expectToBeUploadError(err, 500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\nTest`);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should pass UploadError instances unmodified', done => {
|
||||||
|
shellMvSpy.and.callFake(() => { throw new UploadError(543, 'Test'); });
|
||||||
|
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
||||||
|
expectToBeUploadError(err, 543, 'Test');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Protected methods
|
// Protected methods
|
||||||
|
|
||||||
describe('exists()', () => {
|
describe('exists()', () => {
|
||||||
|
@ -258,12 +258,12 @@ describe('uploadServerFactory', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 405 for non-GET requests', done => {
|
it('should respond with 404 for non-GET requests', done => {
|
||||||
verifyRequests([
|
verifyRequests([
|
||||||
agent.put(`/create-build/${pr}/${sha}`).expect(405),
|
agent.put(`/create-build/${pr}/${sha}`).expect(404),
|
||||||
agent.post(`/create-build/${pr}/${sha}`).expect(405),
|
agent.post(`/create-build/${pr}/${sha}`).expect(404),
|
||||||
agent.patch(`/create-build/${pr}/${sha}`).expect(405),
|
agent.patch(`/create-build/${pr}/${sha}`).expect(404),
|
||||||
agent.delete(`/create-build/${pr}/${sha}`).expect(405),
|
agent.delete(`/create-build/${pr}/${sha}`).expect(404),
|
||||||
], done);
|
], done);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -418,12 +418,12 @@ describe('uploadServerFactory', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 405 for non-GET requests', done => {
|
it('should respond with 404 for non-GET requests', done => {
|
||||||
verifyRequests([
|
verifyRequests([
|
||||||
agent.put('/health-check').expect(405),
|
agent.put('/health-check').expect(404),
|
||||||
agent.post('/health-check').expect(405),
|
agent.post('/health-check').expect(404),
|
||||||
agent.patch('/health-check').expect(405),
|
agent.patch('/health-check').expect(404),
|
||||||
agent.delete('/health-check').expect(405),
|
agent.delete('/health-check').expect(404),
|
||||||
], done);
|
], done);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -442,11 +442,141 @@ describe('uploadServerFactory', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('GET *', () => {
|
describe('POST /pr-updated', () => {
|
||||||
|
const pr = '9';
|
||||||
|
const url = '/pr-updated';
|
||||||
|
let bvGetPrIsTrustedSpy: jasmine.Spy;
|
||||||
|
let bcUpdatePrVisibilitySpy: jasmine.Spy;
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
const createRequest = (num: number, action?: string) =>
|
||||||
|
agent.post(url).send({number: num, action});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
bvGetPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted');
|
||||||
|
bcUpdatePrVisibilitySpy = spyOn(buildCreator, 'updatePrVisibility');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 for non-POST requests', done => {
|
||||||
|
verifyRequests([
|
||||||
|
agent.get(url).expect(404),
|
||||||
|
agent.put(url).expect(404),
|
||||||
|
agent.patch(url).expect(404),
|
||||||
|
agent.delete(url).expect(404),
|
||||||
|
], done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 400 for requests without a payload', done => {
|
||||||
|
const responseBody = `Missing or empty 'number' field in request: POST ${url} {}`;
|
||||||
|
|
||||||
|
const request1 = agent.post(url);
|
||||||
|
const request2 = agent.post(url).send();
|
||||||
|
|
||||||
|
verifyRequests([
|
||||||
|
request1.expect(400, responseBody),
|
||||||
|
request2.expect(400, responseBody),
|
||||||
|
], done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 400 for requests without a \'number\' field', done => {
|
||||||
|
const responseBodyPrefix = `Missing or empty 'number' field in request: POST ${url}`;
|
||||||
|
|
||||||
|
const request1 = agent.post(url).send({});
|
||||||
|
const request2 = agent.post(url).send({number: null});
|
||||||
|
|
||||||
|
verifyRequests([
|
||||||
|
request1.expect(400, `${responseBodyPrefix} {}`),
|
||||||
|
request2.expect(400, `${responseBodyPrefix} {"number":null}`),
|
||||||
|
], done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should call \'BuildVerifier#gtPrIsTrusted()\' with the correct arguments', done => {
|
||||||
|
const req = createRequest(+pr);
|
||||||
|
|
||||||
|
promisifyRequest(req).
|
||||||
|
then(() => expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9)).
|
||||||
|
then(done, done.fail);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should propagate errors from BuildVerifier', done => {
|
||||||
|
bvGetPrIsTrustedSpy.and.callFake(() => Promise.reject('Test'));
|
||||||
|
|
||||||
|
const req = createRequest(+pr).expect(500, 'Test');
|
||||||
|
|
||||||
|
promisifyRequest(req).
|
||||||
|
then(() => {
|
||||||
|
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
|
||||||
|
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
|
||||||
|
}).
|
||||||
|
then(done, done.fail);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should call \'BuildCreator#updatePrVisibility()\' with the correct arguments', done => {
|
||||||
|
bvGetPrIsTrustedSpy.and.callFake((pr2: number) => Promise.resolve(pr2 === 42));
|
||||||
|
|
||||||
|
const req1 = createRequest(24);
|
||||||
|
const req2 = createRequest(42);
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
promisifyRequest(req1).then(() => expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith('24', false)),
|
||||||
|
promisifyRequest(req2).then(() => expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith('42', true)),
|
||||||
|
]).then(done, done.fail);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should propagate errors from BuildCreator', done => {
|
||||||
|
bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject('Test'));
|
||||||
|
|
||||||
|
const req = createRequest(+pr).expect(500, 'Test');
|
||||||
|
verifyRequests([req], done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('on success', () => {
|
||||||
|
|
||||||
|
it('should respond with 200 (action: undefined)', done => {
|
||||||
|
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||||
|
|
||||||
|
const reqs = [4, 2].map(num => createRequest(num).expect(200, http.STATUS_CODES[200]));
|
||||||
|
verifyRequests(reqs, done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 200 (action: labeled)', done => {
|
||||||
|
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||||
|
|
||||||
|
const reqs = [4, 2].map(num => createRequest(num, 'labeled').expect(200, http.STATUS_CODES[200]));
|
||||||
|
verifyRequests(reqs, done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 200 (action: unlabeled)', done => {
|
||||||
|
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||||
|
|
||||||
|
const reqs = [4, 2].map(num => createRequest(num, 'unlabeled').expect(200, http.STATUS_CODES[200]));
|
||||||
|
verifyRequests(reqs, done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 200 (and do nothing) if \'action\' implies no visibility change', done => {
|
||||||
|
const promises = ['foo', 'notlabeled'].
|
||||||
|
map(action => createRequest(+pr, action).expect(200, http.STATUS_CODES[200])).
|
||||||
|
map(promisifyRequest);
|
||||||
|
|
||||||
|
Promise.all(promises).
|
||||||
|
then(() => {
|
||||||
|
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||||
|
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
|
||||||
|
}).
|
||||||
|
then(done, done.fail);
|
||||||
|
});
|
||||||
|
|
||||||
it('should respond with 404', done => {
|
|
||||||
const responseBody = 'Unknown resource in request: GET /some/url';
|
|
||||||
verifyRequests([agent.get('/some/url').expect(404, responseBody)], done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -454,14 +584,15 @@ describe('uploadServerFactory', () => {
|
|||||||
|
|
||||||
describe('ALL *', () => {
|
describe('ALL *', () => {
|
||||||
|
|
||||||
it('should respond with 405', done => {
|
it('should respond with 404', done => {
|
||||||
const responseFor = (method: string) => `Unsupported method in request: ${method.toUpperCase()} /some/url`;
|
const responseFor = (method: string) => `Unknown resource in request: ${method.toUpperCase()} /some/url`;
|
||||||
|
|
||||||
verifyRequests([
|
verifyRequests([
|
||||||
agent.put('/some/url').expect(405, responseFor('put')),
|
agent.get('/some/url').expect(404, responseFor('get')),
|
||||||
agent.post('/some/url').expect(405, responseFor('post')),
|
agent.put('/some/url').expect(404, responseFor('put')),
|
||||||
agent.patch('/some/url').expect(405, responseFor('patch')),
|
agent.post('/some/url').expect(404, responseFor('post')),
|
||||||
agent.delete('/some/url').expect(405, responseFor('delete')),
|
agent.patch('/some/url').expect(404, responseFor('patch')),
|
||||||
|
agent.delete('/some/url').expect(404, responseFor('delete')),
|
||||||
], done);
|
], done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,13 +2,20 @@
|
|||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@types/body-parser@^1.16.4":
|
||||||
|
version "1.16.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.16.4.tgz#96f3660e6f88a677fee7250f5a5e6d6bda3c76bb"
|
||||||
|
dependencies:
|
||||||
|
"@types/express" "*"
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/express-serve-static-core@*":
|
"@types/express-serve-static-core@*":
|
||||||
version "4.0.48"
|
version "4.0.48"
|
||||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.0.48.tgz#b4fa06b0fce282e582b4535ff7fac85cc90173e9"
|
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.0.48.tgz#b4fa06b0fce282e582b4535ff7fac85cc90173e9"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/express@^4.0.35":
|
"@types/express@*", "@types/express@^4.0.35":
|
||||||
version "4.0.36"
|
version "4.0.36"
|
||||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.0.36.tgz#14eb47de7ecb10319f0a2fb1cf971aa8680758c2"
|
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.0.36.tgz#14eb47de7ecb10319f0a2fb1cf971aa8680758c2"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -236,6 +243,21 @@ block-stream@*:
|
|||||||
dependencies:
|
dependencies:
|
||||||
inherits "~2.0.0"
|
inherits "~2.0.0"
|
||||||
|
|
||||||
|
body-parser@^1.17.2:
|
||||||
|
version "1.17.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.2.tgz#f8892abc8f9e627d42aedafbca66bf5ab99104ee"
|
||||||
|
dependencies:
|
||||||
|
bytes "2.4.0"
|
||||||
|
content-type "~1.0.2"
|
||||||
|
debug "2.6.7"
|
||||||
|
depd "~1.1.0"
|
||||||
|
http-errors "~1.6.1"
|
||||||
|
iconv-lite "0.4.15"
|
||||||
|
on-finished "~2.3.0"
|
||||||
|
qs "6.4.0"
|
||||||
|
raw-body "~2.2.0"
|
||||||
|
type-is "~1.6.15"
|
||||||
|
|
||||||
boom@2.x.x:
|
boom@2.x.x:
|
||||||
version "2.10.1"
|
version "2.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
|
resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
|
||||||
@ -273,6 +295,10 @@ buffer-equal-constant-time@1.0.1:
|
|||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
|
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
|
||||||
|
|
||||||
|
bytes@2.4.0:
|
||||||
|
version "2.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339"
|
||||||
|
|
||||||
caller-path@^0.1.0:
|
caller-path@^0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
|
resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
|
||||||
@ -1158,6 +1184,10 @@ http-signature@~1.1.0:
|
|||||||
jsprim "^1.2.2"
|
jsprim "^1.2.2"
|
||||||
sshpk "^1.7.0"
|
sshpk "^1.7.0"
|
||||||
|
|
||||||
|
iconv-lite@0.4.15:
|
||||||
|
version "0.4.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
|
||||||
|
|
||||||
ignore-by-default@^1.0.0:
|
ignore-by-default@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
|
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
|
||||||
@ -1958,6 +1988,14 @@ range-parser@~1.2.0:
|
|||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
|
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
|
||||||
|
|
||||||
|
raw-body@~2.2.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96"
|
||||||
|
dependencies:
|
||||||
|
bytes "2.4.0"
|
||||||
|
iconv-lite "0.4.15"
|
||||||
|
unpipe "1.0.0"
|
||||||
|
|
||||||
rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
|
rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
|
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
|
||||||
@ -2477,7 +2515,7 @@ unique-string@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
crypto-random-string "^1.0.0"
|
crypto-random-string "^1.0.0"
|
||||||
|
|
||||||
unpipe@~1.0.0:
|
unpipe@1.0.0, unpipe@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ appName=aio-upload-server-test
|
|||||||
if [[ "$1" == "stop" ]]; then
|
if [[ "$1" == "stop" ]]; then
|
||||||
pm2 delete $appName
|
pm2 delete $appName
|
||||||
else
|
else
|
||||||
pm2 start $AIO_SCRIPTS_JS_DIR/dist/lib/upload-server/index-test.js \
|
pm2 start $AIO_SCRIPTS_JS_DIR/dist/lib/verify-setup/start-test-upload-server.js \
|
||||||
--log /var/log/aio/upload-server-test.log \
|
--log /var/log/aio/upload-server-test.log \
|
||||||
--name $appName \
|
--name $appName \
|
||||||
--no-autorestart \
|
--no-autorestart \
|
||||||
|
@ -3,10 +3,9 @@
|
|||||||
|
|
||||||
TODO (gkalpak): Add docs. Mention:
|
TODO (gkalpak): Add docs. Mention:
|
||||||
- Travis' JWT addon (+ limitations).
|
- Travis' JWT addon (+ limitations).
|
||||||
Relevant files: `.travis.yml`
|
Relevant files: `.travis.yml`, `scripts/ci/env.sh`
|
||||||
- Testing on CI.
|
- Testing on CI.
|
||||||
Relevant files: `ci/test-aio.sh`, `aio/aio-builds-setup/scripts/test.sh`
|
Relevant files: `scripts/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.
|
- Deploying from CI.
|
||||||
Relevant files: `ci/deploy.sh`, `aio/scripts/deploy-preview.sh`
|
Relevant files: `scripts/ci/deploy.sh`, `aio/scripts/deploy-preview.sh`,
|
||||||
|
`aio/scripts/deploy-to-firebase.sh`
|
||||||
|
@ -80,13 +80,31 @@ More info on the possible HTTP status codes and their meaning can be found
|
|||||||
[here](overview--http-status-codes.md).
|
[here](overview--http-status-codes.md).
|
||||||
|
|
||||||
|
|
||||||
|
### Updating PR visibility
|
||||||
|
- nginx receives a natification that a PR has been updated and passes it through to the
|
||||||
|
upload-server. This could, for example, be sent by a GitHub webhook every time a PR's labels
|
||||||
|
change.
|
||||||
|
E.g.: `ngbuilds.io/pr-updated` (payload: `{"number":<PR>,"action":"labeled"}`)
|
||||||
|
- The request contains the PR number (as `number`) and optionally the action that triggered the
|
||||||
|
request (as `action`) in the payload.
|
||||||
|
- The upload-server verifies the payload and determines whether the `action` (if specified) could
|
||||||
|
have led to PR visibility changes. Only requests that omit the `action` field altogether or
|
||||||
|
specify an action that can affect visibility are further processed.
|
||||||
|
(Currently, the only actions that are considered capable of affecting visibility are `labeled` and
|
||||||
|
`unlabeled`.)
|
||||||
|
- The upload-server re-checks and if necessary updates the PR's visibility.
|
||||||
|
|
||||||
|
More info on the possible HTTP status codes and their meaning can be found
|
||||||
|
[here](overview--http-status-codes.md).
|
||||||
|
|
||||||
|
|
||||||
### Serving build artifacts
|
### Serving build artifacts
|
||||||
- nginx receives a request for an uploaded resource on a subdomain corresponding to the PR and SHA.
|
- 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`
|
E.g.: `pr<PR>-<SHA>.ngbuilds.io/path/to/resource`
|
||||||
- nginx maps the subdomain to the correct sub-directory and serves the resource.
|
- nginx maps the subdomain to the correct sub-directory and serves the resource.
|
||||||
E.g.: `/<PR>/<SHA>/path/to/resource`
|
E.g.: `/<PR>/<SHA>/path/to/resource`
|
||||||
|
|
||||||
Again, more info on the possible HTTP status codes and their meaning can be found
|
More info on the possible HTTP status codes and their meaning can be found
|
||||||
[here](overview--http-status-codes.md).
|
[here](overview--http-status-codes.md).
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,10 +42,6 @@ with a bried explanation of what they mean:
|
|||||||
- **403 (Forbidden)**:
|
- **403 (Forbidden)**:
|
||||||
Unable to verify build (e.g. invalid JWT token, or unable to talk to 3rd-party APIs, etc).
|
Unable to verify build (e.g. invalid JWT token, or unable to talk to 3rd-party APIs, etc).
|
||||||
|
|
||||||
- **404 (Not Found)**:
|
|
||||||
Tried to change PR visibility but the source directory did not exist.
|
|
||||||
(Currently, this can only happen as a rare race condition during build deployment.)
|
|
||||||
|
|
||||||
- **405 (Method Not Allowed)**:
|
- **405 (Method Not Allowed)**:
|
||||||
Request method other than POST.
|
Request method other than POST.
|
||||||
|
|
||||||
@ -57,6 +53,28 @@ with a bried explanation of what they mean:
|
|||||||
Payload larger than size specified in `AIO_UPLOAD_MAX_SIZE`.
|
Payload larger than size specified in `AIO_UPLOAD_MAX_SIZE`.
|
||||||
|
|
||||||
|
|
||||||
|
## `https://ngbuilds.io/health-check`
|
||||||
|
|
||||||
|
- **200 (OK)**:
|
||||||
|
The server is healthy (i.e. up and running and processing requests).
|
||||||
|
|
||||||
|
|
||||||
|
## `https://ngbuilds.io/pr-updated`
|
||||||
|
|
||||||
|
- **200 (OK)**:
|
||||||
|
Request processed successfully. Processing may or may not have resulted in further actions.
|
||||||
|
|
||||||
|
- **400 (Bad Request)**:
|
||||||
|
No payload or no `number` field in payload.
|
||||||
|
|
||||||
|
- **405 (Method Not Allowed)**:
|
||||||
|
Request method other than POST.
|
||||||
|
|
||||||
|
- **409 (Conflict)**:
|
||||||
|
Request to overwrite existing directory (i.e. directories for both visibilities exist).
|
||||||
|
(Normally, this should not happen.)
|
||||||
|
|
||||||
|
|
||||||
## `https://*.ngbuilds.io/*`
|
## `https://*.ngbuilds.io/*`
|
||||||
|
|
||||||
- **404 (Not Found)**:
|
- **404 (Not Found)**:
|
||||||
|
@ -16,13 +16,6 @@ available:
|
|||||||
Can be used for running the tests for `<aio-builds-setup-dir>/dockerbuild/scripts-js/`. This is
|
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.
|
useful for CI integration. See [here](misc--integrate-with-ci.md) for more info.
|
||||||
|
|
||||||
- `travis-preverify-pr.sh`:
|
|
||||||
Can be used for "pre-verifying" a PR before uploading the artifacts to the server. It checks
|
|
||||||
whether the author of the PR is a member of one of the specified GitHub teams (therefore allowed
|
|
||||||
to upload build artifacts) or the PR has the specified "trusted PR" label (meaning it has been
|
|
||||||
manually verified by a trusted member). This is useful for CI integration.
|
|
||||||
See [here](misc--integrate-with-ci.md) for more info.
|
|
||||||
|
|
||||||
- `update-preview-server.sh`:
|
- `update-preview-server.sh`:
|
||||||
Can be used for updating the docker container (and image) based on the latest changes checked out
|
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.
|
from a git repository. See [here](vm-setup--update-docker-container.md) for more info.
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -eux -o pipefail
|
|
||||||
|
|
||||||
# Set up env
|
|
||||||
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-contributors" \
|
|
||||||
AIO_GITHUB_TOKEN=$(echo ${GITHUB_TEAM_MEMBERSHIP_CHECK_KEY} | rev) \
|
|
||||||
AIO_REPO_SLUG=$TRAVIS_REPO_SLUG \
|
|
||||||
AIO_TRUSTED_PR_LABEL="aio: preview" \
|
|
||||||
AIO_PREVERIFY_PR=$TRAVIS_PULL_REQUEST \
|
|
||||||
node "$SCRIPTS_JS_DIR/dist/lib/upload-server/index-preverify-pr"
|
|
||||||
|
|
||||||
# Exit codes:
|
|
||||||
# - 0: The PR can be automatically trusted (i.e. author belongs to trusted team or PR has the "trusted PR" label).
|
|
||||||
# - 1: An error occurred.
|
|
||||||
# - 2: The PR cannot be automatically trusted.
|
|
10
aio/content/examples/.gitignore
vendored
@ -43,13 +43,9 @@ dist/
|
|||||||
**/app/**/*.ajs.js
|
**/app/**/*.ajs.js
|
||||||
|
|
||||||
# aot
|
# aot
|
||||||
**/*.ngfactory.ts
|
*/aot/**/*
|
||||||
**/*.ngsummary.json
|
!*/aot/bs-config.json
|
||||||
**/*.ngsummary.ts
|
!*/aot/index.html
|
||||||
**/*.shim.ngstyle.ts
|
|
||||||
**/*.metadata.json
|
|
||||||
!aot/bs-config.json
|
|
||||||
!aot/index.html
|
|
||||||
!rollup-config.js
|
!rollup-config.js
|
||||||
|
|
||||||
# i18n
|
# i18n
|
||||||
|
@ -9,30 +9,20 @@ describe('Form Validation Tests', function () {
|
|||||||
browser.get('');
|
browser.get('');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Hero Form 1', () => {
|
describe('Template-driven form', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
getPage('hero-form-template1');
|
getPage('hero-form-template');
|
||||||
});
|
});
|
||||||
|
|
||||||
tests();
|
tests('Template-Driven Form');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Hero Form 2', () => {
|
describe('Reactive form', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
getPage('hero-form-template2');
|
getPage('hero-form-reactive');
|
||||||
});
|
});
|
||||||
|
|
||||||
tests();
|
tests('Reactive Form');
|
||||||
bobTests();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Hero Form 3 (Reactive)', () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
getPage('hero-form-reactive3');
|
|
||||||
makeNameTooLong();
|
|
||||||
});
|
|
||||||
|
|
||||||
tests();
|
|
||||||
bobTests();
|
bobTests();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -48,6 +38,7 @@ let page: {
|
|||||||
nameInput: ElementFinder,
|
nameInput: ElementFinder,
|
||||||
alterEgoInput: ElementFinder,
|
alterEgoInput: ElementFinder,
|
||||||
powerSelect: ElementFinder,
|
powerSelect: ElementFinder,
|
||||||
|
powerOption: ElementFinder,
|
||||||
errorMessages: ElementArrayFinder,
|
errorMessages: ElementArrayFinder,
|
||||||
heroFormButtons: ElementArrayFinder,
|
heroFormButtons: ElementArrayFinder,
|
||||||
heroSubmitted: ElementFinder
|
heroSubmitted: ElementFinder
|
||||||
@ -64,19 +55,21 @@ function getPage(sectionTag: string) {
|
|||||||
nameInput: section.element(by.css('#name')),
|
nameInput: section.element(by.css('#name')),
|
||||||
alterEgoInput: section.element(by.css('#alterEgo')),
|
alterEgoInput: section.element(by.css('#alterEgo')),
|
||||||
powerSelect: section.element(by.css('#power')),
|
powerSelect: section.element(by.css('#power')),
|
||||||
|
powerOption: section.element(by.css('#power option')),
|
||||||
errorMessages: section.all(by.css('div.alert')),
|
errorMessages: section.all(by.css('div.alert')),
|
||||||
heroFormButtons: buttons,
|
heroFormButtons: buttons,
|
||||||
heroSubmitted: section.element(by.css('hero-submitted > div'))
|
heroSubmitted: section.element(by.css('.submitted-message'))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function tests() {
|
function tests(title: string) {
|
||||||
|
|
||||||
it('should display correct title', function () {
|
it('should display correct title', function () {
|
||||||
expect(page.title.getText()).toContain('Hero Form');
|
expect(page.title.getText()).toContain(title);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not display submitted message before submit', function () {
|
it('should not display submitted message before submit', function () {
|
||||||
expect(page.heroSubmitted.isElementPresent(by.css('h2'))).toBe(false);
|
expect(page.heroSubmitted.isElementPresent(by.css('p'))).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have form buttons', function () {
|
it('should have form buttons', function () {
|
||||||
@ -130,11 +123,11 @@ function tests() {
|
|||||||
|
|
||||||
it('should hide form after submit', function () {
|
it('should hide form after submit', function () {
|
||||||
page.heroFormButtons.get(0).click();
|
page.heroFormButtons.get(0).click();
|
||||||
expect(page.title.isDisplayed()).toBe(false);
|
expect(page.heroFormButtons.get(0).isDisplayed()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('submitted form should be displayed', function () {
|
it('submitted form should be displayed', function () {
|
||||||
expect(page.heroSubmitted.isElementPresent(by.css('h2'))).toBe(true);
|
expect(page.heroSubmitted.isElementPresent(by.css('p'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('submitted form should have new hero name', function () {
|
it('submitted form should have new hero name', function () {
|
||||||
@ -142,9 +135,9 @@ function tests() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('clicking edit button should reveal form again', function () {
|
it('clicking edit button should reveal form again', function () {
|
||||||
const editBtn = page.heroSubmitted.element(by.css('button'));
|
const newFormBtn = page.heroSubmitted.element(by.css('button'));
|
||||||
editBtn.click();
|
newFormBtn.click();
|
||||||
expect(page.heroSubmitted.isElementPresent(by.css('h2')))
|
expect(page.heroSubmitted.isElementPresent(by.css('p')))
|
||||||
.toBe(false, 'submitted hidden again');
|
.toBe(false, 'submitted hidden again');
|
||||||
expect(page.title.isDisplayed()).toBe(true, 'can see form title');
|
expect(page.title.isDisplayed()).toBe(true, 'can see form title');
|
||||||
});
|
});
|
||||||
@ -159,9 +152,13 @@ function expectFormIsInvalid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bobTests() {
|
function bobTests() {
|
||||||
const emsg = 'Someone named "Bob" cannot be a hero.';
|
const emsg = 'Name cannot be Bob.';
|
||||||
|
|
||||||
it('should produce "no bob" error after setting name to "Bobby"', function () {
|
it('should produce "no bob" error after setting name to "Bobby"', function () {
|
||||||
|
// Re-populate select element
|
||||||
|
page.powerSelect.click();
|
||||||
|
page.powerOption.click();
|
||||||
|
|
||||||
page.nameInput.clear();
|
page.nameInput.clear();
|
||||||
page.nameInput.sendKeys('Bobby');
|
page.nameInput.sendKeys('Bobby');
|
||||||
expectFormIsInvalid();
|
expectFormIsInvalid();
|
||||||
@ -174,8 +171,3 @@ function bobTests() {
|
|||||||
expectFormIsValid();
|
expectFormIsValid();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeNameTooLong() {
|
|
||||||
// make the first name invalid
|
|
||||||
page.nameInput.sendKeys('ThisHeroNameHasWayWayTooManyLetters');
|
|
||||||
}
|
|
||||||
|
@ -3,10 +3,8 @@ import { Component } from '@angular/core';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-app',
|
selector: 'my-app',
|
||||||
template: `<hero-form-template1></hero-form-template1>
|
template: `<hero-form-template></hero-form-template>
|
||||||
<hr>
|
<hr>
|
||||||
<hero-form-template2></hero-form-template2>
|
<hero-form-reactive></hero-form-reactive>`
|
||||||
<hr>
|
|
||||||
<hero-form-reactive3></hero-form-reactive3>`
|
|
||||||
})
|
})
|
||||||
export class AppComponent { }
|
export class AppComponent { }
|
||||||
|
@ -1,18 +1,26 @@
|
|||||||
// #docregion
|
// #docregion
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { HeroFormTemplateModule } from './template/hero-form-template.module';
|
import { HeroFormTemplateComponent } from './template/hero-form-template.component';
|
||||||
import { HeroFormReactiveModule } from './reactive/hero-form-reactive.module';
|
import { HeroFormReactiveComponent } from './reactive/hero-form-reactive.component';
|
||||||
|
import { ForbiddenValidatorDirective } from './shared/forbidden-name.directive';
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
HeroFormTemplateModule,
|
FormsModule,
|
||||||
HeroFormReactiveModule
|
ReactiveFormsModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AppComponent,
|
||||||
|
HeroFormTemplateComponent,
|
||||||
|
HeroFormReactiveComponent,
|
||||||
|
ForbiddenValidatorDirective
|
||||||
],
|
],
|
||||||
declarations: [ AppComponent ],
|
|
||||||
bootstrap: [ AppComponent ]
|
bootstrap: [ AppComponent ]
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule { }
|
||||||
|
@ -1,26 +1,38 @@
|
|||||||
<!-- #docregion -->
|
<!-- #docregion -->
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div [hidden]="submitted">
|
|
||||||
<h1>Hero Form 3 (Reactive)</h1>
|
|
||||||
<!-- #docregion form-tag-->
|
|
||||||
<form [formGroup]="heroForm" *ngIf="active" (ngSubmit)="onSubmit()">
|
|
||||||
<!-- #enddocregion form-tag-->
|
|
||||||
<div class="form-group">
|
|
||||||
<!-- #docregion name-with-error-msg -->
|
|
||||||
<label for="name">Name</label>
|
|
||||||
|
|
||||||
<input type="text" id="name" class="form-control"
|
<h1>Reactive Form</h1>
|
||||||
|
|
||||||
|
<form [formGroup]="heroForm" #formDir="ngForm">
|
||||||
|
|
||||||
|
<div [hidden]="formDir.submitted">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<!-- #docregion name-with-error-msg -->
|
||||||
|
<input id="name" class="form-control"
|
||||||
formControlName="name" required >
|
formControlName="name" required >
|
||||||
|
|
||||||
<div *ngIf="formErrors.name" class="alert alert-danger">
|
<div *ngIf="name.invalid && (name.dirty || name.touched)"
|
||||||
{{ formErrors.name }}
|
class="alert alert-danger">
|
||||||
|
|
||||||
|
<div *ngIf="name.errors.required">
|
||||||
|
Name is required.
|
||||||
|
</div>
|
||||||
|
<div *ngIf="name.errors.minlength">
|
||||||
|
Name must be at least 4 characters long.
|
||||||
|
</div>
|
||||||
|
<div *ngIf="name.errors.forbiddenName">
|
||||||
|
Name cannot be Bob.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- #enddocregion name-with-error-msg -->
|
<!-- #enddocregion name-with-error-msg -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="alterEgo">Alter Ego</label>
|
<label for="alterEgo">Alter Ego</label>
|
||||||
<input type="text" id="alterEgo" class="form-control"
|
<input id="alterEgo" class="form-control"
|
||||||
formControlName="alterEgo" >
|
formControlName="alterEgo" >
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -31,17 +43,20 @@
|
|||||||
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
|
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div *ngIf="formErrors.power" class="alert alert-danger">
|
<div *ngIf="power.invalid && power.touched" class="alert alert-danger">
|
||||||
{{ formErrors.power }}
|
<div *ngIf="power.errors.required">Power is required.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-default"
|
<button type="submit" class="btn btn-default"
|
||||||
[disabled]="!heroForm.valid">Submit</button>
|
[disabled]="heroForm.invalid">Submit</button>
|
||||||
<button type="button" class="btn btn-default"
|
<button type="button" class="btn btn-default"
|
||||||
(click)="addHero()">New Hero</button>
|
(click)="formDir.resetForm({})">Reset</button>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
|
|
||||||
<hero-submitted [hero]="hero" [(submitted)]="submitted"></hero-submitted>
|
<div class="submitted-message" *ngIf="formDir.submitted">
|
||||||
|
<p>You've submitted your hero, {{ heroForm.value.name }}!</p>
|
||||||
|
<button (click)="formDir.resetForm({})">Add new hero</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,115 +2,39 @@
|
|||||||
// #docplaster
|
// #docplaster
|
||||||
// #docregion
|
// #docregion
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||||
|
|
||||||
import { Hero } from '../shared/hero';
|
|
||||||
import { forbiddenNameValidator } from '../shared/forbidden-name.directive';
|
import { forbiddenNameValidator } from '../shared/forbidden-name.directive';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'hero-form-reactive3',
|
selector: 'hero-form-reactive',
|
||||||
templateUrl: './hero-form-reactive.component.html'
|
templateUrl: './hero-form-reactive.component.html'
|
||||||
})
|
})
|
||||||
export class HeroFormReactiveComponent implements OnInit {
|
export class HeroFormReactiveComponent implements OnInit {
|
||||||
|
|
||||||
powers = ['Really Smart', 'Super Flexible', 'Weather Changer'];
|
powers = ['Really Smart', 'Super Flexible', 'Weather Changer'];
|
||||||
|
|
||||||
hero = new Hero(18, 'Dr. WhatIsHisName', this.powers[0], 'Dr. What');
|
hero = {name: 'Dr.', alterEgo: 'Dr. What', power: this.powers[0]};
|
||||||
|
|
||||||
submitted = false;
|
|
||||||
|
|
||||||
// #docregion on-submit
|
|
||||||
onSubmit() {
|
|
||||||
this.submitted = true;
|
|
||||||
this.hero = this.heroForm.value;
|
|
||||||
}
|
|
||||||
// #enddocregion on-submit
|
|
||||||
// #enddocregion
|
|
||||||
|
|
||||||
// Reset the form with a new hero AND restore 'pristine' class state
|
|
||||||
// by toggling 'active' flag which causes the form
|
|
||||||
// to be removed/re-added in a tick via NgIf
|
|
||||||
// TODO: Workaround until NgForm has a reset method (#6822)
|
|
||||||
active = true;
|
|
||||||
// #docregion class
|
|
||||||
// #docregion add-hero
|
|
||||||
addHero() {
|
|
||||||
this.hero = new Hero(42, '', '');
|
|
||||||
this.buildForm();
|
|
||||||
// #enddocregion add-hero
|
|
||||||
// #enddocregion class
|
|
||||||
|
|
||||||
this.active = false;
|
|
||||||
setTimeout(() => this.active = true, 0);
|
|
||||||
// #docregion
|
|
||||||
// #docregion add-hero
|
|
||||||
}
|
|
||||||
// #enddocregion add-hero
|
|
||||||
|
|
||||||
// #docregion form-builder
|
|
||||||
heroForm: FormGroup;
|
heroForm: FormGroup;
|
||||||
constructor(private fb: FormBuilder) { }
|
|
||||||
|
|
||||||
|
// #docregion form-group
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.buildForm();
|
// #docregion custom-validator
|
||||||
}
|
this.heroForm = new FormGroup({
|
||||||
|
'name': new FormControl(this.hero.name, [
|
||||||
buildForm(): void {
|
Validators.required,
|
||||||
this.heroForm = this.fb.group({
|
Validators.minLength(4),
|
||||||
// #docregion name-validators
|
forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator.
|
||||||
'name': [this.hero.name, [
|
]),
|
||||||
Validators.required,
|
'alterEgo': new FormControl(this.hero.alterEgo),
|
||||||
Validators.minLength(4),
|
'power': new FormControl(this.hero.power, Validators.required)
|
||||||
Validators.maxLength(24),
|
|
||||||
forbiddenNameValidator(/bob/i)
|
|
||||||
]
|
|
||||||
],
|
|
||||||
// #enddocregion name-validators
|
|
||||||
'alterEgo': [this.hero.alterEgo],
|
|
||||||
'power': [this.hero.power, Validators.required]
|
|
||||||
});
|
});
|
||||||
|
// #enddocregion custom-validator
|
||||||
this.heroForm.valueChanges
|
|
||||||
.subscribe(data => this.onValueChanged(data));
|
|
||||||
|
|
||||||
this.onValueChanged(); // (re)set validation messages now
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// #enddocregion form-builder
|
get name() { return this.heroForm.get('name'); }
|
||||||
|
|
||||||
onValueChanged(data?: any) {
|
get power() { return this.heroForm.get('power'); }
|
||||||
if (!this.heroForm) { return; }
|
// #enddocregion form-group
|
||||||
const form = this.heroForm;
|
|
||||||
|
|
||||||
for (const field in this.formErrors) {
|
|
||||||
// clear previous error message (if any)
|
|
||||||
this.formErrors[field] = '';
|
|
||||||
const control = form.get(field);
|
|
||||||
|
|
||||||
if (control && control.dirty && !control.valid) {
|
|
||||||
const messages = this.validationMessages[field];
|
|
||||||
for (const key in control.errors) {
|
|
||||||
this.formErrors[field] += messages[key] + ' ';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formErrors = {
|
|
||||||
'name': '',
|
|
||||||
'power': ''
|
|
||||||
};
|
|
||||||
|
|
||||||
validationMessages = {
|
|
||||||
'name': {
|
|
||||||
'required': 'Name is required.',
|
|
||||||
'minlength': 'Name must be at least 4 characters long.',
|
|
||||||
'maxlength': 'Name cannot be more than 24 characters long.',
|
|
||||||
'forbiddenName': 'Someone named "Bob" cannot be a hero.'
|
|
||||||
},
|
|
||||||
'power': {
|
|
||||||
'required': 'Power is required.'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
// #enddocregion
|
// #enddocregion
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
// #docregion
|
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
|
||||||
|
|
||||||
import { SharedModule } from '../shared/shared.module';
|
|
||||||
import { HeroFormReactiveComponent } from './hero-form-reactive.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [ SharedModule, ReactiveFormsModule ],
|
|
||||||
declarations: [ HeroFormReactiveComponent ],
|
|
||||||
exports: [ HeroFormReactiveComponent ]
|
|
||||||
})
|
|
||||||
export class HeroFormReactiveModule { }
|
|
@ -6,9 +6,8 @@ import { AbstractControl, NG_VALIDATORS, Validator, ValidatorFn, Validators } fr
|
|||||||
/** A hero's name can't match the given regular expression */
|
/** A hero's name can't match the given regular expression */
|
||||||
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
|
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
|
||||||
return (control: AbstractControl): {[key: string]: any} => {
|
return (control: AbstractControl): {[key: string]: any} => {
|
||||||
const name = control.value;
|
const forbidden = nameRe.test(control.value);
|
||||||
const no = nameRe.test(name);
|
return forbidden ? {'forbiddenName': {value: control.value}} : null;
|
||||||
return no ? {'forbiddenName': {name}} : null;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// #enddocregion custom-validator
|
// #enddocregion custom-validator
|
||||||
@ -20,23 +19,12 @@ export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
|
|||||||
providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
|
providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
|
||||||
// #enddocregion directive-providers
|
// #enddocregion directive-providers
|
||||||
})
|
})
|
||||||
export class ForbiddenValidatorDirective implements Validator, OnChanges {
|
export class ForbiddenValidatorDirective implements Validator {
|
||||||
@Input() forbiddenName: string;
|
@Input() forbiddenName: string;
|
||||||
private valFn = Validators.nullValidator;
|
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
|
||||||
const change = changes['forbiddenName'];
|
|
||||||
if (change) {
|
|
||||||
const val: string | RegExp = change.currentValue;
|
|
||||||
const re = val instanceof RegExp ? val : new RegExp(val, 'i');
|
|
||||||
this.valFn = forbiddenNameValidator(re);
|
|
||||||
} else {
|
|
||||||
this.valFn = Validators.nullValidator;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validate(control: AbstractControl): {[key: string]: any} {
|
validate(control: AbstractControl): {[key: string]: any} {
|
||||||
return this.valFn(control);
|
return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// #enddocregion directive
|
// #enddocregion directive
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
// #docregion
|
|
||||||
export class Hero {
|
|
||||||
constructor(
|
|
||||||
public id: number,
|
|
||||||
public name: string,
|
|
||||||
public power: string,
|
|
||||||
public alterEgo?: string
|
|
||||||
) { }
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
// #docregion
|
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
import { ForbiddenValidatorDirective } from './forbidden-name.directive';
|
|
||||||
import { SubmittedComponent } from './submitted.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [ CommonModule],
|
|
||||||
declarations: [ ForbiddenValidatorDirective, SubmittedComponent ],
|
|
||||||
exports: [ ForbiddenValidatorDirective, SubmittedComponent,
|
|
||||||
CommonModule ]
|
|
||||||
})
|
|
||||||
export class SharedModule { }
|
|
@ -1,32 +0,0 @@
|
|||||||
// #docregion
|
|
||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
|
||||||
|
|
||||||
import { Hero } from './hero';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'hero-submitted',
|
|
||||||
template: `
|
|
||||||
<div *ngIf="submitted">
|
|
||||||
<h2>You submitted the following:</h2>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-3">Name</div>
|
|
||||||
<div class="col-xs-9 pull-left">{{ hero.name }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-3">Alter Ego</div>
|
|
||||||
<div class="col-xs-9 pull-left">{{ hero.alterEgo }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-3">Power</div>
|
|
||||||
<div class="col-xs-9 pull-left">{{ hero.power }}</div>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<button class="btn btn-default" (click)="onClick()">Edit</button>
|
|
||||||
</div>`
|
|
||||||
})
|
|
||||||
export class SubmittedComponent {
|
|
||||||
@Input() hero: Hero;
|
|
||||||
@Input() submitted = false;
|
|
||||||
@Output() submittedChange = new EventEmitter<boolean>();
|
|
||||||
onClick() { this.submittedChange.emit(false); }
|
|
||||||
}
|
|
@ -0,0 +1,66 @@
|
|||||||
|
<!-- #docregion -->
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<h1>Template-Driven Form</h1>
|
||||||
|
<!-- #docregion form-tag-->
|
||||||
|
<form #heroForm="ngForm">
|
||||||
|
<!-- #enddocregion form-tag-->
|
||||||
|
<div [hidden]="heroForm.submitted">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<!-- #docregion name-with-error-msg -->
|
||||||
|
<!-- #docregion name-input -->
|
||||||
|
<input id="name" name="name" class="form-control"
|
||||||
|
required minlength="4" forbiddenName="bob"
|
||||||
|
[(ngModel)]="hero.name" #name="ngModel" >
|
||||||
|
<!-- #enddocregion name-input -->
|
||||||
|
|
||||||
|
<div *ngIf="name.invalid && (name.dirty || name.touched)"
|
||||||
|
class="alert alert-danger">
|
||||||
|
|
||||||
|
<div *ngIf="name.errors.required">
|
||||||
|
Name is required.
|
||||||
|
</div>
|
||||||
|
<div *ngIf="name.errors.minlength">
|
||||||
|
Name must be at least 4 characters long.
|
||||||
|
</div>
|
||||||
|
<div *ngIf="name.errors.forbiddenName">
|
||||||
|
Name cannot be Bob.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- #enddocregion name-with-error-msg -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="alterEgo">Alter Ego</label>
|
||||||
|
<input id="alterEgo" class="form-control"
|
||||||
|
name="alterEgo" [(ngModel)]="hero.alterEgo" >
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="power">Hero Power</label>
|
||||||
|
<select id="power" name="power" class="form-control"
|
||||||
|
required [(ngModel)]="hero.power" #power="ngModel" >
|
||||||
|
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div *ngIf="power.errors && power.touched" class="alert alert-danger">
|
||||||
|
<div *ngIf="power.errors.required">Power is required.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-default"
|
||||||
|
[disabled]="heroForm.invalid">Submit</button>
|
||||||
|
<button type="button" class="btn btn-default"
|
||||||
|
(click)="heroForm.resetForm({})">Reset</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="submitted-message" *ngIf="heroForm.submitted">
|
||||||
|
<p>You've submitted your hero, {{ heroForm.value.name }}!</p>
|
||||||
|
<button (click)="heroForm.resetForm({})">Add new hero</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
@ -0,0 +1,16 @@
|
|||||||
|
/* tslint:disable: member-ordering */
|
||||||
|
// #docplaster
|
||||||
|
// #docregion
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'hero-form-template',
|
||||||
|
templateUrl: './hero-form-template.component.html'
|
||||||
|
})
|
||||||
|
export class HeroFormTemplateComponent {
|
||||||
|
|
||||||
|
powers = ['Really Smart', 'Super Flexible', 'Weather Changer'];
|
||||||
|
|
||||||
|
hero = {name: 'Dr.', alterEgo: 'Dr. What', power: this.powers[0]};
|
||||||
|
|
||||||
|
}
|
@ -1,14 +0,0 @@
|
|||||||
// #docregion
|
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
|
|
||||||
import { SharedModule } from '../shared/shared.module';
|
|
||||||
import { HeroFormTemplate1Component } from './hero-form-template1.component';
|
|
||||||
import { HeroFormTemplate2Component } from './hero-form-template2.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [ SharedModule, FormsModule ],
|
|
||||||
declarations: [ HeroFormTemplate1Component, HeroFormTemplate2Component ],
|
|
||||||
exports: [ HeroFormTemplate1Component, HeroFormTemplate2Component ]
|
|
||||||
})
|
|
||||||
export class HeroFormTemplateModule { }
|
|
@ -1,61 +0,0 @@
|
|||||||
<!-- #docregion -->
|
|
||||||
<div class="container">
|
|
||||||
<div [hidden]="submitted">
|
|
||||||
<h1>Hero Form 1 (Template)</h1>
|
|
||||||
<!-- #docregion form-tag-->
|
|
||||||
<form #heroForm="ngForm" *ngIf="active" (ngSubmit)="onSubmit()">
|
|
||||||
<!-- #enddocregion form-tag-->
|
|
||||||
<div class="form-group">
|
|
||||||
<!-- #docregion name-with-error-msg -->
|
|
||||||
<label for="name">Name</label>
|
|
||||||
|
|
||||||
<input type="text" id="name" class="form-control"
|
|
||||||
required minlength="4" maxlength="24"
|
|
||||||
name="name" [(ngModel)]="hero.name"
|
|
||||||
#name="ngModel" >
|
|
||||||
|
|
||||||
<div *ngIf="name.errors && (name.dirty || name.touched)"
|
|
||||||
class="alert alert-danger">
|
|
||||||
<div [hidden]="!name.errors.required">
|
|
||||||
Name is required
|
|
||||||
</div>
|
|
||||||
<div [hidden]="!name.errors.minlength">
|
|
||||||
Name must be at least 4 characters long.
|
|
||||||
</div>
|
|
||||||
<div [hidden]="!name.errors.maxlength">
|
|
||||||
Name cannot be more than 24 characters long.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- #enddocregion name-with-error-msg -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="alterEgo">Alter Ego</label>
|
|
||||||
<input type="text" id="alterEgo" class="form-control"
|
|
||||||
name="alterEgo"
|
|
||||||
[(ngModel)]="hero.alterEgo" >
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="power">Hero Power</label>
|
|
||||||
<select id="power" class="form-control"
|
|
||||||
name="power"
|
|
||||||
[(ngModel)]="hero.power" required
|
|
||||||
#power="ngModel" >
|
|
||||||
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div *ngIf="power.errors && power.touched" class="alert alert-danger">
|
|
||||||
<div [hidden]="!power.errors.required">Power is required</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-default"
|
|
||||||
[disabled]="!heroForm.form.valid">Submit</button>
|
|
||||||
<button type="button" class="btn btn-default"
|
|
||||||
(click)="addHero()">New Hero</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hero-submitted [hero]="hero" [(submitted)]="submitted"></hero-submitted>
|
|
||||||
</div>
|
|
@ -1,47 +0,0 @@
|
|||||||
/* tslint:disable: member-ordering */
|
|
||||||
// #docplaster
|
|
||||||
// #docregion
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
|
|
||||||
import { Hero } from '../shared/hero';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'hero-form-template1',
|
|
||||||
templateUrl: './hero-form-template1.component.html'
|
|
||||||
})
|
|
||||||
// #docregion class
|
|
||||||
export class HeroFormTemplate1Component {
|
|
||||||
|
|
||||||
powers = ['Really Smart', 'Super Flexible', 'Weather Changer'];
|
|
||||||
|
|
||||||
hero = new Hero(18, 'Dr. WhatIsHisWayTooLongName', this.powers[0], 'Dr. What');
|
|
||||||
|
|
||||||
submitted = false;
|
|
||||||
|
|
||||||
onSubmit() {
|
|
||||||
this.submitted = true;
|
|
||||||
}
|
|
||||||
// #enddocregion class
|
|
||||||
// #enddocregion
|
|
||||||
// Reset the form with a new hero AND restore 'pristine' class state
|
|
||||||
// by toggling 'active' flag which causes the form
|
|
||||||
// to be removed/re-added in a tick via NgIf
|
|
||||||
// TODO: Workaround until NgForm has a reset method (#6822)
|
|
||||||
active = true;
|
|
||||||
// #docregion
|
|
||||||
// #docregion class
|
|
||||||
|
|
||||||
addHero() {
|
|
||||||
this.hero = new Hero(42, '', '');
|
|
||||||
// #enddocregion class
|
|
||||||
// #enddocregion
|
|
||||||
|
|
||||||
this.active = false;
|
|
||||||
setTimeout(() => this.active = true, 0);
|
|
||||||
// #docregion
|
|
||||||
// #docregion class
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// #enddocregion class
|
|
||||||
// #enddocregion
|
|
@ -1,52 +0,0 @@
|
|||||||
<!-- #docregion -->
|
|
||||||
<div class="container">
|
|
||||||
<div [hidden]="submitted">
|
|
||||||
<h1>Hero Form 2 (Template & Messages)</h1>
|
|
||||||
<!-- #docregion form-tag-->
|
|
||||||
<form #heroForm="ngForm" *ngIf="active" (ngSubmit)="onSubmit()">
|
|
||||||
<!-- #enddocregion form-tag-->
|
|
||||||
<div class="form-group">
|
|
||||||
<!-- #docregion name-with-error-msg -->
|
|
||||||
<label for="name">Name</label>
|
|
||||||
|
|
||||||
<!-- #docregion name-input -->
|
|
||||||
<input type="text" id="name" class="form-control"
|
|
||||||
required minlength="4" maxlength="24" forbiddenName="bob"
|
|
||||||
name="name" [(ngModel)]="hero.name" >
|
|
||||||
<!-- #enddocregion name-input -->
|
|
||||||
|
|
||||||
<div *ngIf="formErrors.name" class="alert alert-danger">
|
|
||||||
{{ formErrors.name }}
|
|
||||||
</div>
|
|
||||||
<!-- #enddocregion name-with-error-msg -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="alterEgo">Alter Ego</label>
|
|
||||||
<input type="text" id="alterEgo" class="form-control"
|
|
||||||
name="alterEgo"
|
|
||||||
[(ngModel)]="hero.alterEgo" >
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="power">Hero Power</label>
|
|
||||||
<select id="power" class="form-control"
|
|
||||||
name="power"
|
|
||||||
[(ngModel)]="hero.power" required >
|
|
||||||
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div *ngIf="formErrors.power" class="alert alert-danger">
|
|
||||||
{{ formErrors.power }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-default"
|
|
||||||
[disabled]="!heroForm.form.valid">Submit</button>
|
|
||||||
<button type="button" class="btn btn-default"
|
|
||||||
(click)="addHero()">New Hero</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hero-submitted [hero]="hero" [(submitted)]="submitted"></hero-submitted>
|
|
||||||
</div>
|
|
@ -1,99 +0,0 @@
|
|||||||
/* tslint:disable: member-ordering forin */
|
|
||||||
// #docplaster
|
|
||||||
// #docregion
|
|
||||||
import { Component, AfterViewChecked, ViewChild } from '@angular/core';
|
|
||||||
import { NgForm } from '@angular/forms';
|
|
||||||
|
|
||||||
import { Hero } from '../shared/hero';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'hero-form-template2',
|
|
||||||
templateUrl: './hero-form-template2.component.html'
|
|
||||||
})
|
|
||||||
export class HeroFormTemplate2Component implements AfterViewChecked {
|
|
||||||
|
|
||||||
powers = ['Really Smart', 'Super Flexible', 'Weather Changer'];
|
|
||||||
|
|
||||||
hero = new Hero(18, 'Dr. WhatIsHisWayTooLongName', this.powers[0], 'Dr. What');
|
|
||||||
|
|
||||||
submitted = false;
|
|
||||||
|
|
||||||
onSubmit() {
|
|
||||||
this.submitted = true;
|
|
||||||
}
|
|
||||||
// #enddocregion
|
|
||||||
|
|
||||||
// Reset the form with a new hero AND restore 'pristine' class state
|
|
||||||
// by toggling 'active' flag which causes the form
|
|
||||||
// to be removed/re-added in a tick via NgIf
|
|
||||||
// TODO: Workaround until NgForm has a reset method (#6822)
|
|
||||||
active = true;
|
|
||||||
// #docregion
|
|
||||||
|
|
||||||
addHero() {
|
|
||||||
this.hero = new Hero(42, '', '');
|
|
||||||
// #enddocregion
|
|
||||||
|
|
||||||
this.active = false;
|
|
||||||
setTimeout(() => this.active = true, 0);
|
|
||||||
// #docregion
|
|
||||||
}
|
|
||||||
|
|
||||||
// #docregion view-child
|
|
||||||
heroForm: NgForm;
|
|
||||||
@ViewChild('heroForm') currentForm: NgForm;
|
|
||||||
|
|
||||||
ngAfterViewChecked() {
|
|
||||||
this.formChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
formChanged() {
|
|
||||||
if (this.currentForm === this.heroForm) { return; }
|
|
||||||
this.heroForm = this.currentForm;
|
|
||||||
if (this.heroForm) {
|
|
||||||
this.heroForm.valueChanges
|
|
||||||
.subscribe(data => this.onValueChanged(data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// #enddocregion view-child
|
|
||||||
|
|
||||||
// #docregion handler
|
|
||||||
onValueChanged(data?: any) {
|
|
||||||
if (!this.heroForm) { return; }
|
|
||||||
const form = this.heroForm.form;
|
|
||||||
|
|
||||||
for (const field in this.formErrors) {
|
|
||||||
// clear previous error message (if any)
|
|
||||||
this.formErrors[field] = '';
|
|
||||||
const control = form.get(field);
|
|
||||||
|
|
||||||
if (control && control.dirty && !control.valid) {
|
|
||||||
const messages = this.validationMessages[field];
|
|
||||||
for (const key in control.errors) {
|
|
||||||
this.formErrors[field] += messages[key] + ' ';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formErrors = {
|
|
||||||
'name': '',
|
|
||||||
'power': ''
|
|
||||||
};
|
|
||||||
// #enddocregion handler
|
|
||||||
|
|
||||||
// #docregion messages
|
|
||||||
validationMessages = {
|
|
||||||
'name': {
|
|
||||||
'required': 'Name is required.',
|
|
||||||
'minlength': 'Name must be at least 4 characters long.',
|
|
||||||
'maxlength': 'Name cannot be more than 24 characters long.',
|
|
||||||
'forbiddenName': 'Someone named "Bob" cannot be a hero.'
|
|
||||||
},
|
|
||||||
'power': {
|
|
||||||
'required': 'Power is required.'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// #enddocregion messages
|
|
||||||
}
|
|
||||||
// #enddocregion
|
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
.ng-valid[required], .ng-valid.required {
|
.ng-valid[required], .ng-valid.required {
|
||||||
border-left: 5px solid #42A948; /* green */
|
border-left: 5px solid #42A948; /* green */
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ export class AppComponent {
|
|||||||
wolves = 0;
|
wolves = 0;
|
||||||
gender = 'f';
|
gender = 'f';
|
||||||
fly = true;
|
fly = true;
|
||||||
logo = 'https://angular.io/resources/images/logos/angular/angular.png';
|
logo = 'https://angular.io/assets/images/logos/angular/angular.png';
|
||||||
count = 3;
|
count = 3;
|
||||||
heroes: string[] = ['Magneta', 'Celeritas', 'Dynama'];
|
heroes: string[] = ['Magneta', 'Celeritas', 'Dynama'];
|
||||||
inc(i: number) {
|
inc(i: number) {
|
||||||
|
@ -12,7 +12,7 @@ import { UserService } from './user.service';
|
|||||||
})
|
})
|
||||||
export class TitleComponent {
|
export class TitleComponent {
|
||||||
@Input() subtitle = '';
|
@Input() subtitle = '';
|
||||||
title = 'Angular Modules';
|
title = 'NgModules';
|
||||||
// #enddocregion v1
|
// #enddocregion v1
|
||||||
user = '';
|
user = '';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { Pipe, PipeTransform } from '@angular/core';
|
|||||||
* Usage:
|
* Usage:
|
||||||
* value | exponentialStrength:exponent
|
* value | exponentialStrength:exponent
|
||||||
* Example:
|
* Example:
|
||||||
* {{ 2 | exponentialStrength:10}}
|
* {{ 2 | exponentialStrength:10 }}
|
||||||
* formats to: 1024
|
* formats to: 1024
|
||||||
*/
|
*/
|
||||||
@Pipe({name: 'exponentialStrength'})
|
@Pipe({name: 'exponentialStrength'})
|
||||||
|
@ -16,7 +16,7 @@ import { HeroService } from './hero.service'; // <-- #1 import service
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
ReactiveFormsModule // <-- #2 add to Angular module imports
|
ReactiveFormsModule // <-- #2 add to @NgModule imports
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
|
@ -4,7 +4,7 @@ import { Directive } from '@angular/core';
|
|||||||
@Directive({
|
@Directive({
|
||||||
selector: '[tohValidator2]',
|
selector: '[tohValidator2]',
|
||||||
host: {
|
host: {
|
||||||
'attr.role': 'button',
|
'[attr.role]': 'role',
|
||||||
'(mouseenter)': 'onMouseEnter()'
|
'(mouseenter)': 'onMouseEnter()'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,116 +0,0 @@
|
|||||||
/* #docregion , quickstart, toh */
|
|
||||||
/* Master Styles */
|
|
||||||
h1 {
|
|
||||||
color: #369;
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
font-size: 250%;
|
|
||||||
}
|
|
||||||
h2, h3 {
|
|
||||||
color: #444;
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
font-weight: lighter;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
margin: 2em;
|
|
||||||
}
|
|
||||||
/* #enddocregion quickstart */
|
|
||||||
body, input[text], button {
|
|
||||||
color: #888;
|
|
||||||
font-family: Cambria, Georgia;
|
|
||||||
}
|
|
||||||
/* #enddocregion toh */
|
|
||||||
a {
|
|
||||||
cursor: pointer;
|
|
||||||
cursor: hand;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
font-family: Arial;
|
|
||||||
background-color: #eee;
|
|
||||||
border: none;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
cursor: hand;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
background-color: #cfd8dc;
|
|
||||||
}
|
|
||||||
button:disabled {
|
|
||||||
background-color: #eee;
|
|
||||||
color: #aaa;
|
|
||||||
cursor: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation link styles */
|
|
||||||
nav a {
|
|
||||||
padding: 5px 10px;
|
|
||||||
text-decoration: none;
|
|
||||||
margin-right: 10px;
|
|
||||||
margin-top: 10px;
|
|
||||||
display: inline-block;
|
|
||||||
background-color: #eee;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
nav a:visited, a:link {
|
|
||||||
color: #607D8B;
|
|
||||||
}
|
|
||||||
nav a:hover {
|
|
||||||
color: #039be5;
|
|
||||||
background-color: #CFD8DC;
|
|
||||||
}
|
|
||||||
nav a.active {
|
|
||||||
color: #039be5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* items class */
|
|
||||||
.items {
|
|
||||||
margin: 0 0 2em 0;
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 0;
|
|
||||||
width: 24em;
|
|
||||||
}
|
|
||||||
.items li {
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
left: 0;
|
|
||||||
background-color: #EEE;
|
|
||||||
margin: .5em;
|
|
||||||
padding: .3em 0;
|
|
||||||
height: 1.6em;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.items li:hover {
|
|
||||||
color: #607D8B;
|
|
||||||
background-color: #DDD;
|
|
||||||
left: .1em;
|
|
||||||
}
|
|
||||||
.items li.selected {
|
|
||||||
background-color: #CFD8DC;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.items li.selected:hover {
|
|
||||||
background-color: #BBD8DC;
|
|
||||||
}
|
|
||||||
.items .text {
|
|
||||||
position: relative;
|
|
||||||
top: -3px;
|
|
||||||
}
|
|
||||||
.items .badge {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: small;
|
|
||||||
color: white;
|
|
||||||
padding: 0.8em 0.7em 0 0.7em;
|
|
||||||
background-color: #607D8B;
|
|
||||||
line-height: 1em;
|
|
||||||
position: relative;
|
|
||||||
left: -1px;
|
|
||||||
top: -4px;
|
|
||||||
height: 1.8em;
|
|
||||||
margin-right: .8em;
|
|
||||||
border-radius: 4px 0 0 4px;
|
|
||||||
}
|
|
||||||
/* #docregion toh */
|
|
||||||
/* everywhere else */
|
|
||||||
* {
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
}
|
|
@ -9,7 +9,8 @@ describe('PhoneCat Application', function() {
|
|||||||
|
|
||||||
it('should redirect `index.html` to `index.html#!/phones', function() {
|
it('should redirect `index.html` to `index.html#!/phones', function() {
|
||||||
browser.get('index.html');
|
browser.get('index.html');
|
||||||
expect(browser.getLocationAbsUrl()).toBe('/phones');
|
browser.sleep(1000); // Not sure why this is needed but it is. The route change works fine.
|
||||||
|
expect(browser.getCurrentUrl()).toMatch(/\/phones$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('View: Phone list', function() {
|
describe('View: Phone list', function() {
|
||||||
@ -65,7 +66,7 @@ describe('PhoneCat Application', function() {
|
|||||||
|
|
||||||
element.all(by.css('.phones li a')).first().click();
|
element.all(by.css('.phones li a')).first().click();
|
||||||
browser.sleep(1000); // Not sure why this is needed but it is. The route change works fine.
|
browser.sleep(1000); // Not sure why this is needed but it is. The route change works fine.
|
||||||
expect(browser.getLocationAbsUrl()).toBe('/phones/nexus-s');
|
expect(browser.getCurrentUrl()).toMatch(/\/phones\/nexus-s$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1000,7 +1000,7 @@ For more information on pipes, see [Pipes](guide/pipes).
|
|||||||
|
|
||||||
|
|
||||||
## Modules/controllers/components
|
## Modules/controllers/components
|
||||||
In both AngularJS and Angular, Angular modules help you organize your application into cohesive blocks of functionality.
|
In both AngularJS and Angular, modules help you organize your application into cohesive blocks of functionality.
|
||||||
|
|
||||||
In AngularJS, you write the code that provides the model and the methods for the view in a **controller**.
|
In AngularJS, you write the code that provides the model and the methods for the view in a **controller**.
|
||||||
In Angular, you build a **component**.
|
In Angular, you build a **component**.
|
||||||
@ -1080,18 +1080,18 @@ The Angular code is shown using TypeScript.
|
|||||||
<td>
|
<td>
|
||||||
|
|
||||||
|
|
||||||
### Angular modules
|
### NgModules
|
||||||
<code-example hideCopy path="ajs-quick-reference/src/app/app.module.1.ts" linenums="false">
|
<code-example hideCopy path="ajs-quick-reference/src/app/app.module.1.ts" linenums="false">
|
||||||
|
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
|
|
||||||
Angular modules, defined with the `NgModule` decorator, serve the same purpose:
|
NgModules, defined with the `NgModule` decorator, serve the same purpose:
|
||||||
|
|
||||||
* `imports`: specifies the list of other modules that this module depends upon
|
* `imports`: specifies the list of other modules that this module depends upon
|
||||||
* `declaration`: keeps track of your components, pipes, and directives.
|
* `declaration`: keeps track of your components, pipes, and directives.
|
||||||
|
|
||||||
For more information on modules, see [Angular Modules (NgModule)](guide/ngmodule).
|
For more information on modules, see [NgModules](guide/ngmodule).
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -475,7 +475,7 @@ You'll need separate TypeScript configuration files such as these:
|
|||||||
<div class="callout is-helpful">
|
<div class="callout is-helpful">
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
@Types and node modules
|
`@types` and node modules
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
In the file structure of _this particular sample project_,
|
In the file structure of _this particular sample project_,
|
||||||
@ -528,6 +528,11 @@ Compiling with AOT presupposes certain supporting files, most of them discussed
|
|||||||
|
|
||||||
Extend the `scripts` section of the `package.json` with these npm scripts:
|
Extend the `scripts` section of the `package.json` with these npm scripts:
|
||||||
|
|
||||||
|
<code-example language="json">
|
||||||
|
"build:aot": "ngc -p tsconfig-aot.json && rollup -c rollup-config.js",
|
||||||
|
"serve:aot": "lite-server -c bs-config.aot.json",
|
||||||
|
</code-example>
|
||||||
|
|
||||||
Copy the AOT distribution files into the `/aot` folder with the node script:
|
Copy the AOT distribution files into the `/aot` folder with the node script:
|
||||||
|
|
||||||
<code-example language="none" class="code-shell">
|
<code-example language="none" class="code-shell">
|
||||||
@ -568,7 +573,7 @@ Run the following command to generate the map.
|
|||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
The `source-map-explorer` analyzes the source map generated with the bundle and draws a map of all dependencies,
|
The `source-map-explorer` analyzes the source map generated with the bundle and draws a map of all dependencies,
|
||||||
showing exactly which application and Angular modules and classes are included in the bundle.
|
showing exactly which application and NgModules and classes are included in the bundle.
|
||||||
|
|
||||||
Here's the map for _Tour of Heroes_.
|
Here's the map for _Tour of Heroes_.
|
||||||
|
|
||||||
|
@ -31,21 +31,21 @@ You'll learn the details in the pages that follow. For now, focus on the big pic
|
|||||||
<img src="generated/images/guide/architecture/module.png" alt="Component" class="left">
|
<img src="generated/images/guide/architecture/module.png" alt="Component" class="left">
|
||||||
|
|
||||||
|
|
||||||
Angular apps are modular and Angular has its own modularity system called _Angular modules_ or _NgModules_.
|
Angular apps are modular and Angular has its own modularity system called _NgModules_.
|
||||||
|
|
||||||
_Angular modules_ are a big deal.
|
NgModules are a big deal.
|
||||||
This page introduces modules; the [Angular modules](guide/ngmodule) page covers them in depth.
|
This page introduces modules; the [NgModules](guide/ngmodule) page covers them in depth.
|
||||||
|
|
||||||
<br class="clear">
|
<br class="clear">
|
||||||
|
|
||||||
Every Angular app has at least one Angular module class, [the _root module_](guide/bootstrapping "AppModule: the root module"),
|
Every Angular app has at least one NgModule class, [the _root module_](guide/bootstrapping "Bootstrapping"),
|
||||||
conventionally named `AppModule`.
|
conventionally named `AppModule`.
|
||||||
|
|
||||||
While the _root module_ may be the only module in a small application, most apps have many more
|
While the _root module_ may be the only module in a small application, most apps have many more
|
||||||
_feature modules_, each a cohesive block of code dedicated to an application domain,
|
_feature modules_, each a cohesive block of code dedicated to an application domain,
|
||||||
a workflow, or a closely related set of capabilities.
|
a workflow, or a closely related set of capabilities.
|
||||||
|
|
||||||
An Angular module, whether a _root_ or _feature_, is a class with an `@NgModule` decorator.
|
An NgModule, whether a _root_ or _feature_, is a class with an `@NgModule` decorator.
|
||||||
|
|
||||||
<div class="l-sub-section">
|
<div class="l-sub-section">
|
||||||
|
|
||||||
@ -87,12 +87,12 @@ During development you're likely to bootstrap the `AppModule` in a `main.ts` fil
|
|||||||
|
|
||||||
<code-example path="architecture/src/main.ts" title="src/main.ts" linenums="false"></code-example>
|
<code-example path="architecture/src/main.ts" title="src/main.ts" linenums="false"></code-example>
|
||||||
|
|
||||||
### Angular modules vs. JavaScript modules
|
### NgModules vs. JavaScript modules
|
||||||
|
|
||||||
The Angular module — a class decorated with `@NgModule` — is a fundamental feature of Angular.
|
The NgModule — a class decorated with `@NgModule` — is a fundamental feature of Angular.
|
||||||
|
|
||||||
JavaScript also has its own module system for managing collections of JavaScript objects.
|
JavaScript also has its own module system for managing collections of JavaScript objects.
|
||||||
It's completely different and unrelated to the Angular module system.
|
It's completely different and unrelated to the NgModule system.
|
||||||
|
|
||||||
In JavaScript each _file_ is a module and all objects defined in the file belong to that module.
|
In JavaScript each _file_ is a module and all objects defined in the file belong to that module.
|
||||||
The module declares some objects to be public by marking them with the `export` key word.
|
The module declares some objects to be public by marking them with the `export` key word.
|
||||||
@ -124,7 +124,7 @@ For example, import Angular's `Component` decorator from the `@angular/core` lib
|
|||||||
|
|
||||||
<code-example path="architecture/src/app/app.component.ts" region="import" linenums="false"></code-example>
|
<code-example path="architecture/src/app/app.component.ts" region="import" linenums="false"></code-example>
|
||||||
|
|
||||||
You also import Angular _modules_ from Angular _libraries_ using JavaScript import statements:
|
You also import NgModules from Angular _libraries_ using JavaScript import statements:
|
||||||
|
|
||||||
<code-example path="architecture/src/app/mini-app.ts" region="import-browser-module" linenums="false"></code-example>
|
<code-example path="architecture/src/app/mini-app.ts" region="import-browser-module" linenums="false"></code-example>
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ Hang in there. The confusion yields to clarity with time and experience.
|
|||||||
|
|
||||||
<div class="l-sub-section">
|
<div class="l-sub-section">
|
||||||
|
|
||||||
Learn more from the [Angular modules](guide/ngmodule) page.
|
Learn more from the [NgModules](guide/ngmodule) page.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# Bootstrapping
|
# Bootstrapping
|
||||||
|
|
||||||
An Angular module class describes how the application parts fit together.
|
An NgModule class describes how the application parts fit together.
|
||||||
Every application has at least one Angular module, the _root_ module
|
Every application has at least one NgModule, the _root_ module
|
||||||
that you [bootstrap](guide/bootstrapping#main) to launch the application.
|
that you [bootstrap](#main) to launch the application.
|
||||||
You can call it anything you want. The conventional name is `AppModule`.
|
You can call it anything you want. The conventional name is `AppModule`.
|
||||||
|
|
||||||
The [setup](guide/setup) instructions produce a new project with the following minimal `AppModule`.
|
The [setup](guide/setup) instructions produce a new project with the following minimal `AppModule`.
|
||||||
@ -17,14 +17,14 @@ You'll evolve this module as your application grows.
|
|||||||
After the `import` statements, you come to a class adorned with the
|
After the `import` statements, you come to a class adorned with the
|
||||||
**`@NgModule`** [_decorator_](guide/glossary#decorator '"Decorator" explained').
|
**`@NgModule`** [_decorator_](guide/glossary#decorator '"Decorator" explained').
|
||||||
|
|
||||||
The `@NgModule` decorator identifies `AppModule` as an Angular module class (also called an `NgModule` class).
|
The `@NgModule` decorator identifies `AppModule` as an `NgModule` class.
|
||||||
`@NgModule` takes a _metadata_ object that tells Angular how to compile and launch the application.
|
`@NgModule` takes a _metadata_ object that tells Angular how to compile and launch the application.
|
||||||
|
|
||||||
* **_imports_** — the `BrowserModule` that this and every application needs to run in a browser.
|
* **_imports_** — the `BrowserModule` that this and every application needs to run in a browser.
|
||||||
* **_declarations_** — the application's lone component, which is also ...
|
* **_declarations_** — the application's lone component, which is also ...
|
||||||
* **_bootstrap_** — the _root_ component that Angular creates and inserts into the `index.html` host web page.
|
* **_bootstrap_** — the _root_ component that Angular creates and inserts into the `index.html` host web page.
|
||||||
|
|
||||||
The [Angular Modules (NgModule)](guide/ngmodule) guide dives deeply into the details of Angular modules.
|
The [NgModules](guide/ngmodule) guide dives deeply into the details of NgModules.
|
||||||
All you need to know at the moment is a few basics about these three properties.
|
All you need to know at the moment is a few basics about these three properties.
|
||||||
|
|
||||||
|
|
||||||
@ -33,8 +33,8 @@ All you need to know at the moment is a few basics about these three properties.
|
|||||||
|
|
||||||
### The _imports_ array
|
### The _imports_ array
|
||||||
|
|
||||||
Angular modules are a way to consolidate features that belong together into discrete units.
|
NgModules are a way to consolidate features that belong together into discrete units.
|
||||||
Many features of Angular itself are organized as Angular modules.
|
Many features of Angular itself are organized as NgModules.
|
||||||
HTTP services are in the `HttpModule`. The router is in the `RouterModule`.
|
HTTP services are in the `HttpModule`. The router is in the `RouterModule`.
|
||||||
Eventually you may create a feature module.
|
Eventually you may create a feature module.
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ Other guide and cookbook pages will tell you when you need to add additional mod
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
The `import` statements at the top of the file and the Angular module's `imports` array
|
The `import` statements at the top of the file and the NgModule's `imports` array
|
||||||
are unrelated and have completely different jobs.
|
are unrelated and have completely different jobs.
|
||||||
|
|
||||||
The _JavaScript_ `import` statements give you access to symbols _exported_ by other files
|
The _JavaScript_ `import` statements give you access to symbols _exported_ by other files
|
||||||
@ -70,8 +70,8 @@ You add `import` statements to almost every application file.
|
|||||||
They have nothing to do with Angular and Angular knows nothing about them.
|
They have nothing to do with Angular and Angular knows nothing about them.
|
||||||
|
|
||||||
The _module's_ `imports` array appears _exclusively_ in the `@NgModule` metadata object.
|
The _module's_ `imports` array appears _exclusively_ in the `@NgModule` metadata object.
|
||||||
It tells Angular about specific _other_ Angular modules — all of them classes decorated with `@NgModule` —
|
It tells Angular about specific _other_ NgModules—all of them classes decorated
|
||||||
that the application needs to function properly.
|
with `@NgModule`—that the application needs to function properly.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -110,7 +110,7 @@ Do not put any other kind of class in `declarations`; _not_ `NgModule` classes,
|
|||||||
|
|
||||||
### The _bootstrap_ array
|
### The _bootstrap_ array
|
||||||
|
|
||||||
You launch the application by [_bootstrapping_](guide/bootstrapping#main) the root `AppModule`.
|
You launch the application by [_bootstrapping_](#main) the root `AppModule`.
|
||||||
Among other things, the _bootstrapping_ process creates the component(s) listed in the `bootstrap` array
|
Among other things, the _bootstrapping_ process creates the component(s) listed in the `bootstrap` array
|
||||||
and inserts each one into the browser DOM.
|
and inserts each one into the browser DOM.
|
||||||
|
|
||||||
@ -127,13 +127,6 @@ Which brings us to the _bootstrapping_ process itself.
|
|||||||
|
|
||||||
{@a main}
|
{@a main}
|
||||||
|
|
||||||
|
|
||||||
<l-main-section>
|
|
||||||
|
|
||||||
</l-main-section>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Bootstrap in _main.ts_
|
## Bootstrap in _main.ts_
|
||||||
|
|
||||||
There are many ways to bootstrap an application.
|
There are many ways to bootstrap an application.
|
||||||
@ -178,11 +171,11 @@ This file is very stable. Once you've set it up, you may never change it again.
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
## More about Angular Modules
|
## More about NgModules
|
||||||
|
|
||||||
Your initial app has only a single module, the _root_ module.
|
Your initial app has only a single module, the _root_ module.
|
||||||
As your app grows, you'll consider subdividing it into multiple "feature" modules,
|
As your app grows, you'll consider subdividing it into multiple "feature" modules,
|
||||||
some of which can be loaded later ("lazy loaded") if and when the user chooses
|
some of which can be loaded later ("lazy loaded") if and when the user chooses
|
||||||
to visit those features.
|
to visit those features.
|
||||||
|
|
||||||
When you're ready to explore these possibilities, visit the [Angular Modules (NgModule)](guide/ngmodule) guide.
|
When you're ready to explore these possibilities, visit the [NgModules](guide/ngmodule) guide.
|
||||||
|
@ -193,8 +193,8 @@ Angular supports most recent browsers. This includes the following specific vers
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
Angular's continuous integration process runs unit tests of the framework on all of these browsers for every pull request,
|
Angular's continuous integration process runs unit tests of the framework on all of these browsers for every pull request,
|
||||||
using <a href="https://saucelabs.com/">SauceLabs</a> and
|
using <a href="https://saucelabs.com/">SauceLabs</a> and
|
||||||
<a href="https://www.browserstack.com">Browserstack</a>.
|
<a href="https://www.browserstack.com">Browserstack</a>.
|
||||||
|
|
||||||
|
|
||||||
@ -215,7 +215,7 @@ that implement missing features in JavaScript.
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
A particular browser may require at least one polyfill to run _any_ Angular application.
|
A particular browser may require at least one polyfill to run _any_ Angular application.
|
||||||
You may need additional polyfills for specific features.
|
You may need additional polyfills for specific features.
|
||||||
|
|
||||||
The tables below can help you determine which polyfills to load, depending on the browsers you target and the features you use.
|
The tables below can help you determine which polyfills to load, depending on the browsers you target and the features you use.
|
||||||
@ -241,7 +241,7 @@ These are the polyfills required to run an Angular application on each supported
|
|||||||
<table>
|
<table>
|
||||||
|
|
||||||
<tr style="vertical-align: top">
|
<tr style="vertical-align: top">
|
||||||
|
|
||||||
<th>
|
<th>
|
||||||
Browsers (Desktop & Mobile)
|
Browsers (Desktop & Mobile)
|
||||||
</th>
|
</th>
|
||||||
@ -253,7 +253,7 @@ These are the polyfills required to run an Angular application on each supported
|
|||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr style="vertical-align: top">
|
<tr style="vertical-align: top">
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
Chrome, Firefox, Edge, Safari 9+
|
Chrome, Firefox, Edge, Safari 9+
|
||||||
</td>
|
</td>
|
||||||
@ -265,7 +265,7 @@ These are the polyfills required to run an Angular application on each supported
|
|||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr style="vertical-align: top">
|
<tr style="vertical-align: top">
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
Safari 7 & 8, IE10 & 11, Android 4.1+
|
Safari 7 & 8, IE10 & 11, Android 4.1+
|
||||||
</td>
|
</td>
|
||||||
@ -279,7 +279,7 @@ These are the polyfills required to run an Angular application on each supported
|
|||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr style="vertical-align: top">
|
<tr style="vertical-align: top">
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
IE9
|
IE9
|
||||||
</td>
|
</td>
|
||||||
@ -309,7 +309,7 @@ Here are the features which may require additional polyfills:
|
|||||||
<table>
|
<table>
|
||||||
|
|
||||||
<tr style="vertical-align: top">
|
<tr style="vertical-align: top">
|
||||||
|
|
||||||
<th>
|
<th>
|
||||||
Feature
|
Feature
|
||||||
</th>
|
</th>
|
||||||
@ -325,7 +325,7 @@ Here are the features which may require additional polyfills:
|
|||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr style="vertical-align: top">
|
<tr style="vertical-align: top">
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
[Animations](guide/animations)
|
[Animations](guide/animations)
|
||||||
@ -363,14 +363,14 @@ Here are the features which may require additional polyfills:
|
|||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr style="vertical-align: top">
|
<tr style="vertical-align: top">
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
[NgClass](api/common/NgClass) on SVG elements
|
[NgClass](api/common/NgClass) on SVG elements
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
|
|
||||||
[classList](guide/browser-support#classlist)
|
[classList](guide/browser-support#classlist)
|
||||||
</td>
|
</td>
|
||||||
@ -382,16 +382,17 @@ Here are the features which may require additional polyfills:
|
|||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr style="vertical-align: top">
|
<tr style="vertical-align: top">
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
[Http](guide/http) when sending and receiving binary data
|
[Http](guide/http) when sending and receiving binary data
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
|
|
||||||
[Typed Array](guide/browser-support#typedarray) <br>[Blob](guide/browser-support#blob)<br>[FormData](guide/browser-support#formdata)
|
[Typed Array](guide/browser-support#typedarray)<br>
|
||||||
|
[Blob](guide/browser-support#blob)<br>
|
||||||
|
[FormData](guide/browser-support#formdata)
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
@ -405,7 +406,7 @@ Here are the features which may require additional polyfills:
|
|||||||
|
|
||||||
|
|
||||||
### Suggested polyfills ##
|
### Suggested polyfills ##
|
||||||
Below are the polyfills which are used to test the framework itself. They are a good starting point for an application.
|
Below are the polyfills which are used to test the framework itself. They are a good starting point for an application.
|
||||||
|
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
@ -542,5 +543,5 @@ Below are the polyfills which are used to test the framework itself. They are a
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
\* Figures are for minified and gzipped code,
|
\* Figures are for minified and gzipped code,
|
||||||
computed with the <a href="http://closure-compiler.appspot.com/home">closure compiler</a>.
|
computed with the <a href="http://closure-compiler.appspot.com/home">closure compiler</a>.
|
||||||
|
@ -27,12 +27,12 @@ Revised samples are more clear and cover all topics discussed.
|
|||||||
## NEW: Samples re-structured with `src/` folder (2017-02-02)
|
## NEW: Samples re-structured with `src/` folder (2017-02-02)
|
||||||
All documentation samples have been realigned with the default folder structure of the Angular CLI.
|
All documentation samples have been realigned with the default folder structure of the Angular CLI.
|
||||||
That's a step along the road to basing the sample in the Angular CLI.
|
That's a step along the road to basing the sample in the Angular CLI.
|
||||||
But it's also good in its own right.
|
But it's also good in its own right.
|
||||||
It helps clearly separate app code from setup and configuration files.
|
It helps clearly separate app code from setup and configuration files.
|
||||||
|
|
||||||
All samples now have a `src/` folder at the project root.
|
All samples now have a `src/` folder at the project root.
|
||||||
The former `app/` folder moves under `src/`.
|
The former `app/` folder moves under `src/`.
|
||||||
Read about moving your existing project to this structure in
|
Read about moving your existing project to this structure in
|
||||||
<a href="https://github.com/angular/quickstart#updating-to-a-newer-version-of-the-quickstart-repo" target="Migrating samples/quickstart app to the src folder">
|
<a href="https://github.com/angular/quickstart#updating-to-a-newer-version-of-the-quickstart-repo" target="Migrating samples/quickstart app to the src folder">
|
||||||
the QuickStart repo update instructions</a>.
|
the QuickStart repo update instructions</a>.
|
||||||
|
|
||||||
@ -48,8 +48,8 @@ Notably:
|
|||||||
The new [**Reactive Forms**](guide/reactive-forms) guide explains how and why to build a "reactive form".
|
The new [**Reactive Forms**](guide/reactive-forms) guide explains how and why to build a "reactive form".
|
||||||
"Reactive Forms" are the code-based counterpart to the declarative "Template Driven" forms approach
|
"Reactive Forms" are the code-based counterpart to the declarative "Template Driven" forms approach
|
||||||
introduced in the [Forms](guide/forms) guide.
|
introduced in the [Forms](guide/forms) guide.
|
||||||
Check it out before you decide how to add forms to your app.
|
Check it out before you decide how to add forms to your app.
|
||||||
Remember also that you can use both techniques in the same app,
|
Remember also that you can use both techniques in the same app,
|
||||||
choosing the approach that best fits each scenario.
|
choosing the approach that best fits each scenario.
|
||||||
|
|
||||||
## NEW: Deployment guide (2017-01-30)
|
## NEW: Deployment guide (2017-01-30)
|
||||||
@ -65,25 +65,25 @@ Revised samples are clearer and cover all topics discussed.
|
|||||||
|
|
||||||
## Miscellaneous (2017-01-05)
|
## Miscellaneous (2017-01-05)
|
||||||
|
|
||||||
* [Setup](guide/setup) guide:
|
* [Setup](guide/setup) guide:
|
||||||
added (optional) instructions on how to remove _non-essential_ files.
|
added (optional) instructions on how to remove _non-essential_ files.
|
||||||
* No longer consolidate RxJS operator imports in `rxjs-extensions` file; each file should import what it needs.
|
* No longer consolidate RxJS operator imports in `rxjs-extensions` file; each file should import what it needs.
|
||||||
* All samples prepend template/style URLs with `./` as a best practice.
|
* All samples prepend template/style URLs with `./` as a best practice.
|
||||||
* [Style Guide](guide/styleguide): copy edits and revised rules.
|
* [Style Guide](guide/styleguide): copy edits and revised rules.
|
||||||
|
|
||||||
## Router: more detail (2016-12-21)
|
## Router: more detail (2016-12-21)
|
||||||
|
|
||||||
Added more information to the [Router](guide/router) guide
|
Added more information to the [Router](guide/router) guide
|
||||||
including sections named outlets, wildcard routes, and preload strategies.
|
including sections named outlets, wildcard routes, and preload strategies.
|
||||||
|
|
||||||
## HTTP: how to set default request headers (and other request options) (2016-12-14)
|
## HTTP: how to set default request headers (and other request options) (2016-12-14)
|
||||||
|
|
||||||
Added section on how to set default request headers (and other request options) to
|
Added section on how to set default request headers (and other request options) to
|
||||||
[HTTP](guide/http#override-default-request-options) guide.
|
HTTP guide.
|
||||||
|
|
||||||
## Testing: added component test plunkers (2016-12-02)
|
## Testing: added component test plunkers (2016-12-02)
|
||||||
|
|
||||||
Added two plunkers that each test _one simple component_ so you can write a component test plunker of your own: <live-example name="setup" plnkr="quickstart-specs">one</live-example> for the QuickStart seed's `AppComponent` and <live-example name="testing" plnkr="banner-specs">another</live-example> for the Testing guide's `BannerComponent`.
|
Added two plunkers that each test _one simple component_ so you can write a component test plunker of your own: <live-example name="setup" plnkr="quickstart-specs">one</live-example> for the QuickStart seed's `AppComponent` and <live-example name="testing" plnkr="banner-specs">another</live-example> for the Testing guide's `BannerComponent`.
|
||||||
Linked to these plunkers in [Testing](guide/testing#live-examples) and [Setup anatomy](guide/setup-systemjs-anatomy) guides.
|
Linked to these plunkers in [Testing](guide/testing#live-examples) and [Setup anatomy](guide/setup-systemjs-anatomy) guides.
|
||||||
|
|
||||||
## Internationalization: pluralization and _select_ (2016-11-30)
|
## Internationalization: pluralization and _select_ (2016-11-30)
|
||||||
@ -112,15 +112,15 @@ Docs and code samples updated and tested with Angular v.2.2.0.
|
|||||||
|
|
||||||
## UPDATE: NgUpgrade Guide for the AOT friendly _upgrade/static_ module (2016-11-14)
|
## UPDATE: NgUpgrade Guide for the AOT friendly _upgrade/static_ module (2016-11-14)
|
||||||
|
|
||||||
The updated [NgUpgrade Guide](guide/upgrade) guide covers the
|
The updated [NgUpgrade Guide](guide/upgrade) guide covers the
|
||||||
new AOT friendly `upgrade/static` module
|
new AOT friendly `upgrade/static` module
|
||||||
released in v.2.2.0, which is the recommended
|
released in v.2.2.0, which is the recommended
|
||||||
facility for migrating from AngularJS to Angular.
|
facility for migrating from AngularJS to Angular.
|
||||||
The documentation for the version prior to v.2.2.0 has been removed.
|
The documentation for the version prior to v.2.2.0 has been removed.
|
||||||
|
|
||||||
## ES6 described in "TypeScript to JavaScript" (2016-11-14)
|
## ES6 described in "TypeScript to JavaScript" (2016-11-14)
|
||||||
|
|
||||||
The updated [TypeScript to JavaScript](guide/ts-to-js) guide
|
The updated [TypeScript to JavaScript](guide/ts-to-js) guide
|
||||||
now explains how to write apps in ES6/7
|
now explains how to write apps in ES6/7
|
||||||
by translating the common idioms in the TypeScript documentation examples
|
by translating the common idioms in the TypeScript documentation examples
|
||||||
(and elsewhere on the web) to ES6/7 and ES5.
|
(and elsewhere on the web) to ES6/7 and ES5.
|
||||||
@ -156,7 +156,7 @@ in the `in-memory-web-api` repo.
|
|||||||
The router can lazily _preload_ modules _after_ the app starts and
|
The router can lazily _preload_ modules _after_ the app starts and
|
||||||
_before_ the user navigates to them for improved perceived performance.
|
_before_ the user navigates to them for improved perceived performance.
|
||||||
|
|
||||||
New `:enter` and `:leave` aliases make animation more natural.
|
New `:enter` and `:leave` aliases make animation more natural.
|
||||||
|
|
||||||
## Sync with Angular v.2.1.0 (2016-10-12)
|
## Sync with Angular v.2.1.0 (2016-10-12)
|
||||||
|
|
||||||
@ -176,11 +176,11 @@ Docs and code samples updated and tested with Angular v.2.0.2.
|
|||||||
## "Routing and Navigation" guide with the _Router Module_ (2016-10-5)
|
## "Routing and Navigation" guide with the _Router Module_ (2016-10-5)
|
||||||
|
|
||||||
The [Routing and Navigation](guide/router) guide now locates route configuration
|
The [Routing and Navigation](guide/router) guide now locates route configuration
|
||||||
in a _Routing Module_.
|
in a _Routing Module_.
|
||||||
The _Routing Module_ replaces the previous _routing object_ involving the `ModuleWithProviders`.
|
The _Routing Module_ replaces the previous _routing object_ involving the `ModuleWithProviders`.
|
||||||
|
|
||||||
All guided samples with routing use the _Routing Module_ and prose content has been updated,
|
All guided samples with routing use the _Routing Module_ and prose content has been updated,
|
||||||
most conspicuously in the
|
most conspicuously in the
|
||||||
[NgModule](guide/ngmodule) guide and [NgModule FAQ](guide/ngmodule-faq) guide.
|
[NgModule](guide/ngmodule) guide and [NgModule FAQ](guide/ngmodule-faq) guide.
|
||||||
|
|
||||||
## New "Internationalization" guide (2016-09-30)
|
## New "Internationalization" guide (2016-09-30)
|
||||||
@ -194,12 +194,12 @@ Many samples use the `angular-in-memory-web-api` to simulate a remote server.
|
|||||||
This library is also useful to you during early development before you have a server to talk to.
|
This library is also useful to you during early development before you have a server to talk to.
|
||||||
|
|
||||||
The package name was changed from "angular2-in-memory-web-api" which is still frozen-in-time on npm.
|
The package name was changed from "angular2-in-memory-web-api" which is still frozen-in-time on npm.
|
||||||
The new "angular-in-memory-web-api" has new features.
|
The new "angular-in-memory-web-api" has new features.
|
||||||
<a href="https://github.com/angular/in-memory-web-api/blob/master/README.md">Read about them on github</a>.
|
<a href="https://github.com/angular/in-memory-web-api/blob/master/README.md">Read about them on github</a>.
|
||||||
|
|
||||||
## "Style Guide" with _NgModules_ (2016-09-27)
|
## "Style Guide" with _NgModules_ (2016-09-27)
|
||||||
|
|
||||||
[StyleGuide](guide/styleguide) explains recommended conventions for Angular modules (NgModule).
|
[StyleGuide](guide/styleguide) explains recommended conventions for NgModules.
|
||||||
Barrels now are far less useful and have been removed from the style guide;
|
Barrels now are far less useful and have been removed from the style guide;
|
||||||
they remain valuable but are not a matter of Angular style.
|
they remain valuable but are not a matter of Angular style.
|
||||||
Also relaxed the rule that discouraged use of the `@Component.host` property.
|
Also relaxed the rule that discouraged use of the `@Component.host` property.
|
||||||
@ -215,5 +215,5 @@ modules with SystemJS as the samples currently do.
|
|||||||
|
|
||||||
## "Lifecycle Hooks" guide simplified (2016-09-24)
|
## "Lifecycle Hooks" guide simplified (2016-09-24)
|
||||||
|
|
||||||
The [Lifecycle Hooks](guide/lifecycle-hooks) guide is shorter, simpler, and
|
The [Lifecycle Hooks](guide/lifecycle-hooks) guide is shorter, simpler, and
|
||||||
draws more attention to the order in which Angular calls the hooks.
|
draws more attention to the order in which Angular calls the hooks.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Cheat Sheet
|
<h1 class="no-toc">Cheat Sheet</h1>
|
||||||
|
|
||||||
<div id="cheatsheet">
|
<div id="cheatsheet">
|
||||||
<table class="is-full-width is-fixed-layout">
|
<table class="is-full-width is-fixed-layout">
|
||||||
@ -23,28 +23,28 @@
|
|||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>@<b>NgModule</b>({ declarations: ..., imports: ...,<br> exports: ..., providers: ..., bootstrap: ...})<br>class MyModule {}</code></td>
|
<td><code>@<b>NgModule</b>({ declarations: ..., imports: ...,<br> exports: ..., providers: ..., bootstrap: ...})<br>class MyModule {}</code></td>
|
||||||
<td><p>Defines a module that contains components, directives, pipes, and providers.</p>
|
<td><p>Defines a module that contains components, directives, pipes, and providers.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>declarations:</b> [MyRedComponent, MyBlueComponent, MyDatePipe]</code></td>
|
<td><code><b>declarations:</b> [MyRedComponent, MyBlueComponent, MyDatePipe]</code></td>
|
||||||
<td><p>List of components, directives, and pipes that belong to this module.</p>
|
<td><p>List of components, directives, and pipes that belong to this module.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>imports:</b> [BrowserModule, SomeOtherModule]</code></td>
|
<td><code><b>imports:</b> [BrowserModule, SomeOtherModule]</code></td>
|
||||||
<td><p>List of modules to import into this module. Everything from the imported modules
|
<td><p>List of modules to import into this module. Everything from the imported modules
|
||||||
is available to <code>declarations</code> of this module.</p>
|
is available to <code>declarations</code> of this module.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>exports:</b> [MyRedComponent, MyDatePipe]</code></td>
|
<td><code><b>exports:</b> [MyRedComponent, MyDatePipe]</code></td>
|
||||||
<td><p>List of components, directives, and pipes visible to modules that import this module.</p>
|
<td><p>List of components, directives, and pipes visible to modules that import this module.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>providers:</b> [MyService, { provide: ... }]</code></td>
|
<td><code><b>providers:</b> [MyService, { provide: ... }]</code></td>
|
||||||
<td><p>List of dependency injection providers visible both to the contents of this module and to importers of this module.</p>
|
<td><p>List of dependency injection providers visible both to the contents of this module and to importers of this module.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>bootstrap:</b> [MyAppComponent]</code></td>
|
<td><code><b>bootstrap:</b> [MyAppComponent]</code></td>
|
||||||
<td><p>List of components to bootstrap when this module is bootstrapped.</p>
|
<td><p>List of components to bootstrap when this module is bootstrapped.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -56,61 +56,61 @@ is available to <code>declarations</code> of this module.</p>
|
|||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code><input <b>[value]</b>="firstName"></code></td>
|
<td><code><input <b>[value]</b>="firstName"></code></td>
|
||||||
<td><p>Binds property <code>value</code> to the result of expression <code>firstName</code>.</p>
|
<td><p>Binds property <code>value</code> to the result of expression <code>firstName</code>.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><div <b>[attr.role]</b>="myAriaRole"></code></td>
|
<td><code><div <b>[attr.role]</b>="myAriaRole"></code></td>
|
||||||
<td><p>Binds attribute <code>role</code> to the result of expression <code>myAriaRole</code>.</p>
|
<td><p>Binds attribute <code>role</code> to the result of expression <code>myAriaRole</code>.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><div <b>[class.extra-sparkle]</b>="isDelightful"></code></td>
|
<td><code><div <b>[class.extra-sparkle]</b>="isDelightful"></code></td>
|
||||||
<td><p>Binds the presence of the CSS class <code>extra-sparkle</code> on the element to the truthiness of the expression <code>isDelightful</code>.</p>
|
<td><p>Binds the presence of the CSS class <code>extra-sparkle</code> on the element to the truthiness of the expression <code>isDelightful</code>.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><div <b>[style.width.px]</b>="mySize"></code></td>
|
<td><code><div <b>[style.width.px]</b>="mySize"></code></td>
|
||||||
<td><p>Binds style property <code>width</code> to the result of expression <code>mySize</code> in pixels. Units are optional.</p>
|
<td><p>Binds style property <code>width</code> to the result of expression <code>mySize</code> in pixels. Units are optional.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><button <b>(click)</b>="readRainbow($event)"></code></td>
|
<td><code><button <b>(click)</b>="readRainbow($event)"></code></td>
|
||||||
<td><p>Calls method <code>readRainbow</code> when a click event is triggered on this button element (or its children) and passes in the event object.</p>
|
<td><p>Calls method <code>readRainbow</code> when a click event is triggered on this button element (or its children) and passes in the event object.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><div title="Hello <b>{{ponyName}}</b>"></code></td>
|
<td><code><div title="Hello <b>{{ponyName}}</b>"></code></td>
|
||||||
<td><p>Binds a property to an interpolated string, for example, "Hello Seabiscuit". Equivalent to:
|
<td><p>Binds a property to an interpolated string, for example, "Hello Seabiscuit". Equivalent to:
|
||||||
<code><div [title]="'Hello ' + ponyName"></code></p>
|
<code><div [title]="'Hello ' + ponyName"></code></p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><p>Hello <b>{{ponyName}}</b></p></code></td>
|
<td><code><p>Hello <b>{{ponyName}}</b></p></code></td>
|
||||||
<td><p>Binds text content to an interpolated string, for example, "Hello Seabiscuit".</p>
|
<td><p>Binds text content to an interpolated string, for example, "Hello Seabiscuit".</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><my-cmp <b>[(title)]</b>="name"></code></td>
|
<td><code><my-cmp <b>[(title)]</b>="name"></code></td>
|
||||||
<td><p>Sets up two-way data binding. Equivalent to: <code><my-cmp [title]="name" (titleChange)="name=$event"></code></p>
|
<td><p>Sets up two-way data binding. Equivalent to: <code><my-cmp [title]="name" (titleChange)="name=$event"></code></p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><video <b>#movieplayer</b> ...><br> <button <b>(click)</b>="movieplayer.play()"><br></video></code></td>
|
<td><code><video <b>#movieplayer</b> ...><br> <button <b>(click)</b>="movieplayer.play()"><br></video></code></td>
|
||||||
<td><p>Creates a local variable <code>movieplayer</code> that provides access to the <code>video</code> element instance in data-binding and event-binding expressions in the current template.</p>
|
<td><p>Creates a local variable <code>movieplayer</code> that provides access to the <code>video</code> element instance in data-binding and event-binding expressions in the current template.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><p <b>*myUnless</b>="myExpression">...</p></code></td>
|
<td><code><p <b>*myUnless</b>="myExpression">...</p></code></td>
|
||||||
<td><p>The <code>*</code> symbol turns the current element into an embedded template. Equivalent to:
|
<td><p>The <code>*</code> symbol turns the current element into an embedded template. Equivalent to:
|
||||||
<code><ng-template [myUnless]="myExpression"><p>...</p></ng-template></code></p>
|
<code><ng-template [myUnless]="myExpression"><p>...</p></ng-template></code></p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><p>Card No.: <b>{{cardNumber | myCardNumberFormatter}}</b></p></code></td>
|
<td><code><p>Card No.: <b>{{cardNumber | myCardNumberFormatter}}</b></p></code></td>
|
||||||
<td><p>Transforms the current value of expression <code>cardNumber</code> via the pipe called <code>myCardNumberFormatter</code>.</p>
|
<td><p>Transforms the current value of expression <code>cardNumber</code> via the pipe called <code>myCardNumberFormatter</code>.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><p>Employer: <b>{{employer?.companyName}}</b></p></code></td>
|
<td><code><p>Employer: <b>{{employer?.companyName}}</b></p></code></td>
|
||||||
<td><p>The safe navigation operator (<code>?</code>) means that the <code>employer</code> field is optional and if <code>undefined</code>, the rest of the expression should be ignored.</p>
|
<td><p>The safe navigation operator (<code>?</code>) means that the <code>employer</code> field is optional and if <code>undefined</code>, the rest of the expression should be ignored.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><<b>svg:</b>rect x="0" y="0" width="100" height="100"/></code></td>
|
<td><code><<b>svg:</b>rect x="0" y="0" width="100" height="100"/></code></td>
|
||||||
<td><p>An SVG snippet template needs an <code>svg:</code> prefix on its root element to disambiguate the SVG element from an HTML component.</p>
|
<td><p>An SVG snippet template needs an <code>svg:</code> prefix on its root element to disambiguate the SVG element from an HTML component.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><<b>svg</b>><br> <rect x="0" y="0" width="100" height="100"/><br></<b>svg</b>></code></td>
|
<td><code><<b>svg</b>><br> <rect x="0" y="0" width="100" height="100"/><br></<b>svg</b>></code></td>
|
||||||
<td><p>An <code><svg></code> root element is detected as an SVG element automatically, without the prefix.</p>
|
<td><p>An <code><svg></code> root element is detected as an SVG element automatically, without the prefix.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -124,19 +124,19 @@ is available to <code>declarations</code> of this module.</p>
|
|||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code><section <b>*ngIf</b>="showSection"></code></td>
|
<td><code><section <b>*ngIf</b>="showSection"></code></td>
|
||||||
<td><p>Removes or recreates a portion of the DOM tree based on the <code>showSection</code> expression.</p>
|
<td><p>Removes or recreates a portion of the DOM tree based on the <code>showSection</code> expression.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><li <b>*ngFor</b>="let item of list"></code></td>
|
<td><code><li <b>*ngFor</b>="let item of list"></code></td>
|
||||||
<td><p>Turns the li element and its contents into a template, and uses that to instantiate a view for each item in list.</p>
|
<td><p>Turns the li element and its contents into a template, and uses that to instantiate a view for each item in list.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><div <b>[ngSwitch]</b>="conditionExpression"><br> <ng-template <b>[<b>ngSwitchCase</b>]</b>="case1Exp">...</ng-template><br> <ng-template <b>ngSwitchCase</b>="case2LiteralString">...</ng-template><br> <ng-template <b>ngSwitchDefault</b>>...</ng-template><br></div></code></td>
|
<td><code><div <b>[ngSwitch]</b>="conditionExpression"><br> <ng-template <b>[<b>ngSwitchCase</b>]</b>="case1Exp">...</ng-template><br> <ng-template <b>ngSwitchCase</b>="case2LiteralString">...</ng-template><br> <ng-template <b>ngSwitchDefault</b>>...</ng-template><br></div></code></td>
|
||||||
<td><p>Conditionally swaps the contents of the div by selecting one of the embedded templates based on the current value of <code>conditionExpression</code>.</p>
|
<td><p>Conditionally swaps the contents of the div by selecting one of the embedded templates based on the current value of <code>conditionExpression</code>.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><div <b>[ngClass]</b>="{'active': isActive, 'disabled': isDisabled}"></code></td>
|
<td><code><div <b>[ngClass]</b>="{'active': isActive, 'disabled': isDisabled}"></code></td>
|
||||||
<td><p>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.</p>
|
<td><p>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.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -150,7 +150,7 @@ is available to <code>declarations</code> of this module.</p>
|
|||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code><input <b>[(ngModel)]</b>="userName"></code></td>
|
<td><code><input <b>[(ngModel)]</b>="userName"></code></td>
|
||||||
<td><p>Provides two-way data-binding, parsing, and validation for form controls.</p>
|
<td><p>Provides two-way data-binding, parsing, and validation for form controls.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -164,19 +164,19 @@ is available to <code>declarations</code> of this module.</p>
|
|||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code><b>@Component({...})</b><br>class MyComponent() {}</code></td>
|
<td><code><b>@Component({...})</b><br>class MyComponent() {}</code></td>
|
||||||
<td><p>Declares that a class is a component and provides metadata about the component.</p>
|
<td><p>Declares that a class is a component and provides metadata about the component.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>@Directive({...})</b><br>class MyDirective() {}</code></td>
|
<td><code><b>@Directive({...})</b><br>class MyDirective() {}</code></td>
|
||||||
<td><p>Declares that a class is a directive and provides metadata about the directive.</p>
|
<td><p>Declares that a class is a directive and provides metadata about the directive.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>@Pipe({...})</b><br>class MyPipe() {}</code></td>
|
<td><code><b>@Pipe({...})</b><br>class MyPipe() {}</code></td>
|
||||||
<td><p>Declares that a class is a pipe and provides metadata about the pipe.</p>
|
<td><p>Declares that a class is a pipe and provides metadata about the pipe.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>@Injectable()</b><br>class MyService() {}</code></td>
|
<td><code><b>@Injectable()</b><br>class MyService() {}</code></td>
|
||||||
<td><p>Declares that a class has dependencies that should be injected into the constructor when the dependency injector is creating an instance of this class.
|
<td><p>Declares that a class has dependencies that should be injected into the constructor when the dependency injector is creating an instance of this class.
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
@ -191,13 +191,13 @@ is available to <code>declarations</code> of this module.</p>
|
|||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code><b>selector:</b> '.cool-button:not(a)'</code></td>
|
<td><code><b>selector:</b> '.cool-button:not(a)'</code></td>
|
||||||
<td><p>Specifies a CSS selector that identifies this directive within a template. Supported selectors include <code>element</code>,
|
<td><p>Specifies a CSS selector that identifies this directive within a template. Supported selectors include <code>element</code>,
|
||||||
<code>[attribute]</code>, <code>.class</code>, and <code>:not()</code>.</p>
|
<code>[attribute]</code>, <code>.class</code>, and <code>:not()</code>.</p>
|
||||||
<p>Does not support parent-child relationship selectors.</p>
|
<p>Does not support parent-child relationship selectors.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>providers:</b> [MyService, { provide: ... }]</code></td>
|
<td><code><b>providers:</b> [MyService, { provide: ... }]</code></td>
|
||||||
<td><p>List of dependency injection providers for this directive and its children.</p>
|
<td><p>List of dependency injection providers for this directive and its children.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -212,19 +212,19 @@ so the <code>@Directive</code> configuration applies to components as well</p>
|
|||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code><b>moduleId:</b> module.id</code></td>
|
<td><code><b>moduleId:</b> module.id</code></td>
|
||||||
<td><p>If set, the <code>templateUrl</code> and <code>styleUrl</code> are resolved relative to the component.</p>
|
<td><p>If set, the <code>templateUrl</code> and <code>styleUrl</code> are resolved relative to the component.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>viewProviders:</b> [MyService, { provide: ... }]</code></td>
|
<td><code><b>viewProviders:</b> [MyService, { provide: ... }]</code></td>
|
||||||
<td><p>List of dependency injection providers scoped to this component's view.</p>
|
<td><p>List of dependency injection providers scoped to this component's view.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>template:</b> 'Hello {{name}}'<br><b>templateUrl:</b> 'my-component.html'</code></td>
|
<td><code><b>template:</b> 'Hello {{name}}'<br><b>templateUrl:</b> 'my-component.html'</code></td>
|
||||||
<td><p>Inline template or external template URL of the component's view.</p>
|
<td><p>Inline template or external template URL of the component's view.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>styles:</b> ['.primary {color: red}']<br><b>styleUrls:</b> ['my-component.css']</code></td>
|
<td><code><b>styles:</b> ['.primary {color: red}']<br><b>styleUrls:</b> ['my-component.css']</code></td>
|
||||||
<td><p>List of inline CSS styles or external stylesheet URLs for styling the component’s view.</p>
|
<td><p>List of inline CSS styles or external stylesheet URLs for styling the component’s view.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -238,36 +238,36 @@ so the <code>@Directive</code> configuration applies to components as well</p>
|
|||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code><b>@Input()</b> myProperty;</code></td>
|
<td><code><b>@Input()</b> myProperty;</code></td>
|
||||||
<td><p>Declares an input property that you can update via property binding (example:
|
<td><p>Declares an input property that you can update via property binding (example:
|
||||||
<code><my-cmp [myProperty]="someExpression"></code>).</p>
|
<code><my-cmp [myProperty]="someExpression"></code>).</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>@Output()</b> myEvent = new EventEmitter();</code></td>
|
<td><code><b>@Output()</b> myEvent = new EventEmitter();</code></td>
|
||||||
<td><p>Declares an output property that fires events that you can subscribe to with an event binding (example: <code><my-cmp (myEvent)="doSomething()"></code>).</p>
|
<td><p>Declares an output property that fires events that you can subscribe to with an event binding (example: <code><my-cmp (myEvent)="doSomething()"></code>).</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>@HostBinding('class.valid')</b> isValid;</code></td>
|
<td><code><b>@HostBinding('class.valid')</b> isValid;</code></td>
|
||||||
<td><p>Binds a host element property (here, the CSS class <code>valid</code>) to a directive/component property (<code>isValid</code>).</p>
|
<td><p>Binds a host element property (here, the CSS class <code>valid</code>) to a directive/component property (<code>isValid</code>).</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>@HostListener('click', ['$event'])</b> onClick(e) {...}</code></td>
|
<td><code><b>@HostListener('click', ['$event'])</b> onClick(e) {...}</code></td>
|
||||||
<td><p>Subscribes to a host element event (<code>click</code>) with a directive/component method (<code>onClick</code>), optionally passing an argument (<code>$event</code>).</p>
|
<td><p>Subscribes to a host element event (<code>click</code>) with a directive/component method (<code>onClick</code>), optionally passing an argument (<code>$event</code>).</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>@ContentChild(myPredicate)</b> myChildComponent;</code></td>
|
<td><code><b>@ContentChild(myPredicate)</b> myChildComponent;</code></td>
|
||||||
<td><p>Binds the first result of the component content query (<code>myPredicate</code>) to a property (<code>myChildComponent</code>) of the class.</p>
|
<td><p>Binds the first result of the component content query (<code>myPredicate</code>) to a property (<code>myChildComponent</code>) of the class.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>@ContentChildren(myPredicate)</b> myChildComponents;</code></td>
|
<td><code><b>@ContentChildren(myPredicate)</b> myChildComponents;</code></td>
|
||||||
<td><p>Binds the results of the component content query (<code>myPredicate</code>) to a property (<code>myChildComponents</code>) of the class.</p>
|
<td><p>Binds the results of the component content query (<code>myPredicate</code>) to a property (<code>myChildComponents</code>) of the class.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>@ViewChild(myPredicate)</b> myChildComponent;</code></td>
|
<td><code><b>@ViewChild(myPredicate)</b> myChildComponent;</code></td>
|
||||||
<td><p>Binds the first result of the component view query (<code>myPredicate</code>) to a property (<code>myChildComponent</code>) of the class. Not available for directives.</p>
|
<td><p>Binds the first result of the component view query (<code>myPredicate</code>) to a property (<code>myChildComponent</code>) of the class. Not available for directives.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>@ViewChildren(myPredicate)</b> myChildComponents;</code></td>
|
<td><code><b>@ViewChildren(myPredicate)</b> myChildComponents;</code></td>
|
||||||
<td><p>Binds the results of the component view query (<code>myPredicate</code>) to a property (<code>myChildComponents</code>) of the class. Not available for directives.</p>
|
<td><p>Binds the results of the component view query (<code>myPredicate</code>) to a property (<code>myChildComponents</code>) of the class. Not available for directives.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -281,39 +281,39 @@ so the <code>@Directive</code> configuration applies to components as well</p>
|
|||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code><b>constructor(myService: MyService, ...)</b> { ... }</code></td>
|
<td><code><b>constructor(myService: MyService, ...)</b> { ... }</code></td>
|
||||||
<td><p>Called before any other lifecycle hook. Use it to inject dependencies, but avoid any serious work here.</p>
|
<td><p>Called before any other lifecycle hook. Use it to inject dependencies, but avoid any serious work here.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>ngOnChanges(changeRecord)</b> { ... }</code></td>
|
<td><code><b>ngOnChanges(changeRecord)</b> { ... }</code></td>
|
||||||
<td><p>Called after every change to input properties and before processing content or child views.</p>
|
<td><p>Called after every change to input properties and before processing content or child views.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>ngOnInit()</b> { ... }</code></td>
|
<td><code><b>ngOnInit()</b> { ... }</code></td>
|
||||||
<td><p>Called after the constructor, initializing input properties, and the first call to <code>ngOnChanges</code>.</p>
|
<td><p>Called after the constructor, initializing input properties, and the first call to <code>ngOnChanges</code>.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>ngDoCheck()</b> { ... }</code></td>
|
<td><code><b>ngDoCheck()</b> { ... }</code></td>
|
||||||
<td><p>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.</p>
|
<td><p>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.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>ngAfterContentInit()</b> { ... }</code></td>
|
<td><code><b>ngAfterContentInit()</b> { ... }</code></td>
|
||||||
<td><p>Called after <code>ngOnInit</code> when the component's or directive's content has been initialized.</p>
|
<td><p>Called after <code>ngOnInit</code> when the component's or directive's content has been initialized.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>ngAfterContentChecked()</b> { ... }</code></td>
|
<td><code><b>ngAfterContentChecked()</b> { ... }</code></td>
|
||||||
<td><p>Called after every check of the component's or directive's content.</p>
|
<td><p>Called after every check of the component's or directive's content.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>ngAfterViewInit()</b> { ... }</code></td>
|
<td><code><b>ngAfterViewInit()</b> { ... }</code></td>
|
||||||
<td><p>Called after <code>ngAfterContentInit</code> when the component's view has been initialized. Applies to components only.</p>
|
<td><p>Called after <code>ngAfterContentInit</code> when the component's view has been initialized. Applies to components only.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>ngAfterViewChecked()</b> { ... }</code></td>
|
<td><code><b>ngAfterViewChecked()</b> { ... }</code></td>
|
||||||
<td><p>Called after every check of the component's view. Applies to components only.</p>
|
<td><p>Called after every check of the component's view. Applies to components only.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><b>ngOnDestroy()</b> { ... }</code></td>
|
<td><code><b>ngOnDestroy()</b> { ... }</code></td>
|
||||||
<td><p>Called once, before the instance is destroyed.</p>
|
<td><p>Called once, before the instance is destroyed.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -325,15 +325,15 @@ so the <code>@Directive</code> configuration applies to components as well</p>
|
|||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>{ <b>provide</b>: MyService, <b>useClass</b>: MyMockService }</code></td>
|
<td><code>{ <b>provide</b>: MyService, <b>useClass</b>: MyMockService }</code></td>
|
||||||
<td><p>Sets or overrides the provider for <code>MyService</code> to the <code>MyMockService</code> class.</p>
|
<td><p>Sets or overrides the provider for <code>MyService</code> to the <code>MyMockService</code> class.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code>{ <b>provide</b>: MyService, <b>useFactory</b>: myFactory }</code></td>
|
<td><code>{ <b>provide</b>: MyService, <b>useFactory</b>: myFactory }</code></td>
|
||||||
<td><p>Sets or overrides the provider for <code>MyService</code> to the <code>myFactory</code> factory function.</p>
|
<td><p>Sets or overrides the provider for <code>MyService</code> to the <code>myFactory</code> factory function.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code>{ <b>provide</b>: MyValue, <b>useValue</b>: 41 }</code></td>
|
<td><code>{ <b>provide</b>: MyValue, <b>useValue</b>: 41 }</code></td>
|
||||||
<td><p>Sets or overrides the provider for <code>MyValue</code> to the value <code>41</code>.</p>
|
<td><p>Sets or overrides the provider for <code>MyValue</code> to the value <code>41</code>.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -347,39 +347,39 @@ so the <code>@Directive</code> configuration applies to components as well</p>
|
|||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>const routes: <b>Routes</b> = [<br> { path: '', component: HomeComponent },<br> { path: 'path/:routeParam', component: MyComponent },<br> { path: 'staticPath', component: ... },<br> { path: '**', component: ... },<br> { path: 'oldPath', redirectTo: '/staticPath' },<br> { path: ..., component: ..., data: { message: 'Custom' } }<br>]);<br><br>const routing = RouterModule.forRoot(routes);</code></td>
|
<td><code>const routes: <b>Routes</b> = [<br> { path: '', component: HomeComponent },<br> { path: 'path/:routeParam', component: MyComponent },<br> { path: 'staticPath', component: ... },<br> { path: '**', component: ... },<br> { path: 'oldPath', redirectTo: '/staticPath' },<br> { path: ..., component: ..., data: { message: 'Custom' } }<br>]);<br><br>const routing = RouterModule.forRoot(routes);</code></td>
|
||||||
<td><p>Configures routes for the application. Supports static, parameterized, redirect, and wildcard routes. Also supports custom route data and resolve.</p>
|
<td><p>Configures routes for the application. Supports static, parameterized, redirect, and wildcard routes. Also supports custom route data and resolve.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><br><<b>router-outlet</b>></<b>router-outlet</b>><br><<b>router-outlet</b> name="aux"></<b>router-outlet</b>><br></code></td>
|
<td><code><br><<b>router-outlet</b>></<b>router-outlet</b>><br><<b>router-outlet</b> name="aux"></<b>router-outlet</b>><br></code></td>
|
||||||
<td><p>Marks the location to load the component of the active route.</p>
|
<td><p>Marks the location to load the component of the active route.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><br><a routerLink="/path"><br><a <b>[routerLink]</b>="[ '/path', routeParam ]"><br><a <b>[routerLink]</b>="[ '/path', { matrixParam: 'value' } ]"><br><a <b>[routerLink]</b>="[ '/path' ]" [queryParams]="{ page: 1 }"><br><a <b>[routerLink]</b>="[ '/path' ]" fragment="anchor"><br></code></td>
|
<td><code><br><a routerLink="/path"><br><a <b>[routerLink]</b>="[ '/path', routeParam ]"><br><a <b>[routerLink]</b>="[ '/path', { matrixParam: 'value' } ]"><br><a <b>[routerLink]</b>="[ '/path' ]" [queryParams]="{ page: 1 }"><br><a <b>[routerLink]</b>="[ '/path' ]" fragment="anchor"><br></code></td>
|
||||||
<td><p>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 <code>/</code> prefix; for a child route, use the <code>./</code>prefix; for a sibling or parent, use the <code>../</code> prefix.</p>
|
<td><p>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 <code>/</code> prefix; for a child route, use the <code>./</code>prefix; for a sibling or parent, use the <code>../</code> prefix.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code><a [routerLink]="[ '/path' ]" routerLinkActive="active"></code></td>
|
<td><code><a [routerLink]="[ '/path' ]" routerLinkActive="active"></code></td>
|
||||||
<td><p>The provided classes are added to the element when the <code>routerLink</code> becomes the current active route.</p>
|
<td><p>The provided classes are added to the element when the <code>routerLink</code> becomes the current active route.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code>class <b>CanActivate</b>Guard implements <b>CanActivate</b> {<br> canActivate(<br> route: ActivatedRouteSnapshot,<br> state: RouterStateSnapshot<br> ): Observable<boolean>|Promise<boolean>|boolean { ... }<br>}<br><br>{ path: ..., canActivate: [<b>CanActivate</b>Guard] }</code></td>
|
<td><code>class <b>CanActivate</b>Guard implements <b>CanActivate</b> {<br> canActivate(<br> route: ActivatedRouteSnapshot,<br> state: RouterStateSnapshot<br> ): Observable<boolean>|Promise<boolean>|boolean { ... }<br>}<br><br>{ path: ..., canActivate: [<b>CanActivate</b>Guard] }</code></td>
|
||||||
<td><p>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.</p>
|
<td><p>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.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code>class <b>CanDeactivate</b>Guard implements <b>CanDeactivate</b><T> {<br> canDeactivate(<br> component: T,<br> route: ActivatedRouteSnapshot,<br> state: RouterStateSnapshot<br> ): Observable<boolean>|Promise<boolean>|boolean { ... }<br>}<br><br>{ path: ..., canDeactivate: [<b>CanDeactivate</b>Guard] }</code></td>
|
<td><code>class <b>CanDeactivate</b>Guard implements <b>CanDeactivate</b><T> {<br> canDeactivate(<br> component: T,<br> route: ActivatedRouteSnapshot,<br> state: RouterStateSnapshot<br> ): Observable<boolean>|Promise<boolean>|boolean { ... }<br>}<br><br>{ path: ..., canDeactivate: [<b>CanDeactivate</b>Guard] }</code></td>
|
||||||
<td><p>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.</p>
|
<td><p>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.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code>class <b>CanActivateChild</b>Guard implements <b>CanActivateChild</b> {<br> canActivateChild(<br> route: ActivatedRouteSnapshot,<br> state: RouterStateSnapshot<br> ): Observable<boolean>|Promise<boolean>|boolean { ... }<br>}<br><br>{ path: ..., canActivateChild: [CanActivateGuard],<br> children: ... }</code></td>
|
<td><code>class <b>CanActivateChild</b>Guard implements <b>CanActivateChild</b> {<br> canActivateChild(<br> route: ActivatedRouteSnapshot,<br> state: RouterStateSnapshot<br> ): Observable<boolean>|Promise<boolean>|boolean { ... }<br>}<br><br>{ path: ..., canActivateChild: [CanActivateGuard],<br> children: ... }</code></td>
|
||||||
<td><p>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.</p>
|
<td><p>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.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code>class <b>Resolve</b>Guard implements <b>Resolve</b><T> {<br> resolve(<br> route: ActivatedRouteSnapshot,<br> state: RouterStateSnapshot<br> ): Observable<any>|Promise<any>|any { ... }<br>}<br><br>{ path: ..., resolve: [<b>Resolve</b>Guard] }</code></td>
|
<td><code>class <b>Resolve</b>Guard implements <b>Resolve</b><T> {<br> resolve(<br> route: ActivatedRouteSnapshot,<br> state: RouterStateSnapshot<br> ): Observable<any>|Promise<any>|any { ... }<br>}<br><br>{ path: ..., resolve: [<b>Resolve</b>Guard] }</code></td>
|
||||||
<td><p>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.</p>
|
<td><p>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.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><code>class <b>CanLoad</b>Guard implements <b>CanLoad</b> {<br> canLoad(<br> route: Route<br> ): Observable<boolean>|Promise<boolean>|boolean { ... }<br>}<br><br>{ path: ..., canLoad: [<b>CanLoad</b>Guard], loadChildren: ... }</code></td>
|
<td><code>class <b>CanLoad</b>Guard implements <b>CanLoad</b> {<br> canLoad(<br> route: Route<br> ): Observable<boolean>|Promise<boolean>|boolean { ... }<br>}<br><br>{ path: ..., canLoad: [<b>CanLoad</b>Guard], loadChildren: ... }</code></td>
|
||||||
<td><p>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.</p>
|
<td><p>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.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -119,12 +119,13 @@ if some ancestor element has the CSS class `theme-light`.
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
### /deep/
|
### (deprecated) `/deep/`, `>>>`, and `::ng-deep`
|
||||||
|
|
||||||
Component styles normally apply only to the HTML in the component's own template.
|
Component styles normally apply only to the HTML in the component's own template.
|
||||||
|
|
||||||
Use the `/deep/` selector to force a style down through the child component tree into all the child component views.
|
Use the `/deep/` shadow-piercing descendant combinator to force a style down through the child
|
||||||
The `/deep/` selector works to any depth of nested components, and it applies to both the view
|
component tree into all the child component views.
|
||||||
|
The `/deep/` combinator works to any depth of nested components, and it applies to both the view
|
||||||
children and content children of the component.
|
children and content children of the component.
|
||||||
|
|
||||||
The following example targets all `<h3>` elements, from the host element down
|
The following example targets all `<h3>` elements, from the host element down
|
||||||
@ -134,17 +135,24 @@ through this component to all of its child elements in the DOM.
|
|||||||
|
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
The `/deep/` selector also has the alias `>>>`. You can use either interchangeably.
|
The `/deep/` combinator also has the aliases `>>>`, and `::ng-deep`.
|
||||||
|
|
||||||
|
|
||||||
<div class="alert is-important">
|
<div class="alert is-important">
|
||||||
|
|
||||||
Use the `/deep/` and `>>>` selectors only with *emulated* view encapsulation.
|
Use `/deep/`, `>>>` and `::ng-deep` only with *emulated* view encapsulation.
|
||||||
Emulated is the default and most commonly used view encapsulation. For more information, see the
|
Emulated is the default and most commonly used view encapsulation. For more information, see the
|
||||||
[Controlling view encapsulation](guide/component-styles#view-encapsulation) section.
|
[Controlling view encapsulation](guide/component-styles#view-encapsulation) section.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="alert is-important">
|
||||||
|
|
||||||
|
The shadow-piercing descendant combinator is deprecated and [support is being removed from major browsers](https://www.chromestatus.com/features/6750456638341120) and tools.
|
||||||
|
As such we plan to drop support in Angular (for all 3 of `/deep/`, `>>>` and `::ng-deep`).
|
||||||
|
Until then `::ng-deep` should be preferred for a broader compatibility with the tools.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
{@a loading-styles}
|
{@a loading-styles}
|
||||||
|
|
||||||
## Loading component styles
|
## Loading component styles
|
||||||
|
@ -6,75 +6,50 @@
|
|||||||
Improve overall data quality by validating user input for accuracy and completeness.
|
Improve overall data quality by validating user input for accuracy and completeness.
|
||||||
|
|
||||||
This page shows how to validate user input in the UI and display useful validation messages
|
This page shows 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.
|
using both reactive and template-driven forms. It assumes some basic knowledge of the two
|
||||||
|
forms modules.
|
||||||
|
|
||||||
<div class="l-sub-section">
|
<div class="l-sub-section">
|
||||||
|
|
||||||
Read more about these choices in the [Forms](guide/forms)
|
If you're new to forms, start by reviewing the [Forms](guide/forms) and
|
||||||
and the [Reactive Forms](guide/reactive-forms) guides.
|
[Reactive Forms](guide/reactive-forms) guides.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{@a live-example}
|
## Template-driven validation
|
||||||
|
|
||||||
|
To add validation to a template-driven form, you add the same validation attributes as you
|
||||||
|
would with [native HTML form validation](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation).
|
||||||
|
Angular uses directives to match these attributes with validator functions in the framework.
|
||||||
|
|
||||||
**Try the live example to see and download the full cookbook source code.**
|
Every time the value of a form control changes, Angular runs validation and generates
|
||||||
|
either a list of validation errors, which results in an INVALID status, or null, which results in a VALID status.
|
||||||
|
|
||||||
<live-example name="form-validation" embedded=true img="guide/form-validation/plunker.png">
|
You can then inspect the control's state by exporting `ngModel` to a local template variable.
|
||||||
|
The following example exports `NgModel` into a variable called `name`:
|
||||||
|
|
||||||
</live-example>
|
<code-example path="form-validation/src/app/template/hero-form-template.component.html" region="name-with-error-msg" title="template/hero-form-template.component.html (name)" linenums="false">
|
||||||
|
|
||||||
## 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.
|
|
||||||
In Template Driven forms, 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 this first template validation example,
|
|
||||||
notice the HTML that reads the control state and updates the display appropriately.
|
|
||||||
Here's an excerpt from the template HTML for a single input control bound to the hero name:
|
|
||||||
|
|
||||||
<code-example path="form-validation/src/app/template/hero-form-template1.component.html" region="name-with-error-msg" title="template/hero-form-template1.component.html (Hero name)" linenums="false">
|
|
||||||
|
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Note the following:
|
Note the following:
|
||||||
|
|
||||||
* The `<input>` element carries the HTML validation attributes: `required`, `minlength`, and `maxlength`.
|
* The `<input>` element carries the HTML validation attributes: `required` and `minlength`. It
|
||||||
|
also carries a custom validator directive, `forbiddenName`. For more
|
||||||
|
information, see [Custom validators](guide/form-validation#custom-validators) section.
|
||||||
|
|
||||||
* The `name` attribute of the input is set to `"name"` so Angular can track this input element and associate it
|
* `#name="ngModel"` exports `NgModel` into a local variable callled `name`. `NgModel` mirrors many of the properties of its underlying
|
||||||
with an Angular form control called `name` in its internal control model.
|
`FormControl` instance, so you can use this in the template to check for control states such as `valid` and `dirty`. For a full list of control properties, see the [AbstractControl](api/forms/AbstractControl)
|
||||||
|
API reference.
|
||||||
|
|
||||||
* The `[(ngModel)]` directive allows two-way data binding between the input box to the `hero.name` property.
|
* The `*ngIf` on the `<div>` element reveals a set of nested message `divs`
|
||||||
|
but only if the `name` is invalid and the control is either `dirty` or `touched`.
|
||||||
* The template variable (`#name`) has the value `"ngModel"` (always `ngModel`).
|
|
||||||
This gives you a reference to the Angular `NgModel` directive
|
|
||||||
associated with this control that you can use _in the template_
|
|
||||||
to check for control states such as `valid` and `dirty`.
|
|
||||||
|
|
||||||
* The `*ngIf` on the `<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.
|
* Each nested `<div>` can present a custom message for one of the possible validation errors.
|
||||||
There are messages for `required`, `minlength`, and `maxlength`.
|
There are messages for `required`, `minlength`, and `forbiddenName`.
|
||||||
|
|
||||||
The full template repeats this kind of layout for each data entry control on the form.
|
|
||||||
|
|
||||||
{@a why-check}
|
|
||||||
|
|
||||||
|
|
||||||
<div class="l-sub-section">
|
<div class="l-sub-section">
|
||||||
|
|
||||||
@ -82,567 +57,152 @@ The full template repeats this kind of layout for each data entry control on the
|
|||||||
|
|
||||||
#### Why check _dirty_ and _touched_?
|
#### Why check _dirty_ and _touched_?
|
||||||
|
|
||||||
The app shouldn't show errors for a new hero before the user has had a chance to edit the value.
|
You may not want your application to display errors before the user has a chance to edit the form.
|
||||||
The checks for `dirty` and `touched` prevent premature display of errors.
|
The checks for `dirty` and `touched` prevent errors from showing until the user
|
||||||
|
does one of two things: changes the value,
|
||||||
Learn about `dirty` and `touched` in the [Forms](guide/forms) guide.
|
turning the control dirty; or blurs the form control element, setting the control to touched.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## Reactive form validation
|
||||||
|
|
||||||
|
In a reactive form, the source of truth is the component class. Instead of adding validators through attributes in the template, you add validator functions directly to the form control model in the component class. Angular then calls these functions whenever the value of the control changes.
|
||||||
|
|
||||||
The component class manages the hero model used in the data binding
|
### Validator functions
|
||||||
as well as other code to support the view.
|
|
||||||
|
|
||||||
|
There are two types of validator functions: sync validators and async validators.
|
||||||
|
|
||||||
<code-example path="form-validation/src/app/template/hero-form-template1.component.ts" region="class" title="template/hero-form-template1.component.ts (class)">
|
* **Sync validators**: functions that take a control instance and immediately return either a set of validation errors or `null`. You can pass these in as the second argument when you instantiate a `FormControl`.
|
||||||
|
|
||||||
</code-example>
|
* **Async validators**: functions that take a control instance and return a Promise
|
||||||
|
or Observable that later emits a set of validation errors or `null`. You can
|
||||||
|
pass these in as the third argument when you instantiate a `FormControl`.
|
||||||
|
|
||||||
|
Note: for performance reasons, Angular only runs async validators if all sync validators pass. Each must complete before errors are set.
|
||||||
|
|
||||||
|
### Built-in validators
|
||||||
|
|
||||||
Use this Template Driven validation technique when working with static forms with simple, standard validation rules.
|
You can choose to [write your own validator functions](guide/form-validation#custom-validators), or you can use some of
|
||||||
|
Angular's built-in validators.
|
||||||
Here are the complete files for the first version of `HeroFormTemplateCompononent` in the Template Driven approach:
|
|
||||||
|
|
||||||
|
|
||||||
<code-tabs>
|
|
||||||
|
|
||||||
<code-pane title="template/hero-form-template1.component.html" path="form-validation/src/app/template/hero-form-template1.component.html">
|
|
||||||
|
|
||||||
</code-pane>
|
|
||||||
|
|
||||||
<code-pane title="template/hero-form-template1.component.ts" path="form-validation/src/app/template/hero-form-template1.component.ts">
|
|
||||||
|
|
||||||
</code-pane>
|
|
||||||
|
|
||||||
</code-tabs>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Template Driven Forms with validation messages in code
|
|
||||||
|
|
||||||
While the layout is straightforward,
|
|
||||||
there are obvious shortcomings with the way it's handling 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.
|
|
||||||
|
|
||||||
* There's a lot of JavaScript logic in the HTML.
|
|
||||||
|
|
||||||
* The messages are static strings, hard-coded into the template.
|
|
||||||
It's easier to maintain _dynamic_ messages in the component class.
|
|
||||||
|
|
||||||
In this example, you 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:
|
|
||||||
|
|
||||||
<code-tabs>
|
|
||||||
|
|
||||||
<code-pane title="hero-form-template2.component.html (name #2)" path="form-validation/src/app/template/hero-form-template2.component.html" region="name-with-error-msg">
|
|
||||||
|
|
||||||
</code-pane>
|
|
||||||
|
|
||||||
<code-pane title="hero-form-template1.component.html (name #1)" path="form-validation/src/app/template/hero-form-template1.component.html" region="name-with-error-msg">
|
|
||||||
|
|
||||||
</code-pane>
|
|
||||||
|
|
||||||
</code-tabs>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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" in the name `<input>`([try it](guide/form-validation#live-example)).
|
|
||||||
See the [custom validation](guide/form-validation#custom-validation) section later in this page for more information
|
|
||||||
on custom validation directives.
|
|
||||||
|
|
||||||
* The `#name` template variable is gone because the app no longer refers to the Angular control for this element.
|
|
||||||
|
|
||||||
* Binding to the new `formErrors.name` property is sufficient to display all name validation error messages.
|
|
||||||
|
|
||||||
{@a component-class}
|
|
||||||
|
|
||||||
### Component class
|
|
||||||
The original component code for Template 1 stayed the same; however,
|
|
||||||
Template 2 requires some changes in the component. This section covers the code
|
|
||||||
necessary in Template 2's component class 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 at the
|
|
||||||
`#heroForm` template variable in the `<form>` element:
|
|
||||||
|
|
||||||
<code-example path="form-validation/src/app/template/hero-form-template1.component.html" region="form-tag" title="template/hero-form-template1.component.html (form tag)" linenums="false">
|
|
||||||
|
|
||||||
</code-example>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The `heroForm` variable is a reference to the control model that Angular derived from the template.
|
|
||||||
Tell Angular to inject that model into the component class's `currentForm` property using a `@ViewChild` query:
|
|
||||||
|
|
||||||
<code-example path="form-validation/src/app/template/hero-form-template2.component.ts" region="view-child" title="template/hero-form-template2.component.ts (heroForm)" linenums="false">
|
|
||||||
|
|
||||||
</code-example>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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 you add a new hero.
|
|
||||||
Periodically inspecting it reveals these changes.
|
|
||||||
|
|
||||||
* Angular calls the `ngAfterViewChecked()` [lifecycle hook method](guide/lifecycle-hooks#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, `formChanged()` subscribes to its `valueChanges` _Observable_ property.
|
|
||||||
The `onValueChanged` handler looks for validation errors after every keystroke.
|
|
||||||
|
|
||||||
<code-example path="form-validation/src/app/template/hero-form-template2.component.ts" region="handler" title="template/hero-form-template2.component.ts (handler)" linenums="false">
|
|
||||||
|
|
||||||
</code-example>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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 `onValueChanged` handler does the following:
|
|
||||||
* Clears the prior error message, if any.
|
|
||||||
* Acquires the field's corresponding Angular form control.
|
|
||||||
* If such a control exists _and_ it's been changed ("dirty")
|
|
||||||
_and_ it's invalid, the handler composes a consolidated error message for all of the control's errors.
|
|
||||||
|
|
||||||
Next, the component needs some error messages—a set for each validated property with
|
|
||||||
one message per validation rule:
|
|
||||||
|
|
||||||
<code-example path="form-validation/src/app/template/hero-form-template2.component.ts" region="messages" title="template/hero-form-template2.component.ts (messages)" linenums="false">
|
|
||||||
|
|
||||||
</code-example>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Now every time the user makes a change, the `onValueChanged` handler checks for validation errors and produces messages accordingly.
|
|
||||||
|
|
||||||
|
|
||||||
{@a improvement}
|
|
||||||
|
|
||||||
|
|
||||||
### The benefits of messages in code
|
|
||||||
|
|
||||||
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 the number of validated
|
|
||||||
fields and rules increases.
|
|
||||||
In general, HTML is harder to read and maintain than code.
|
|
||||||
The initial template was already large and threatening to get rapidly worse
|
|
||||||
with the addition of more validation message `<div>` elements.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Now that the messages are in code, you have more flexibility and can compose messages more efficiently.
|
|
||||||
You 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.
|
|
||||||
|
|
||||||
|
|
||||||
{@a formmodule}
|
|
||||||
|
|
||||||
|
|
||||||
### _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.
|
|
||||||
|
|
||||||
You've been reviewing the Template Driven approach which requires the `FormsModule`.
|
|
||||||
Here's how you imported it in the `HeroFormTemplateModule`.
|
|
||||||
|
|
||||||
|
|
||||||
<code-example path="form-validation/src/app/template/hero-form-template.module.ts" title="template/hero-form-template.module.ts" linenums="false">
|
|
||||||
|
|
||||||
</code-example>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="l-sub-section">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
This guide hasn'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](guide/form-validation#live-example) if you're interested.
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{@a reactive}
|
|
||||||
|
|
||||||
|
|
||||||
## Reactive Forms with validation in code
|
|
||||||
|
|
||||||
In the Template Driven approach, you mark up 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 allows you to do the following:
|
|
||||||
|
|
||||||
* Add, change, and remove validation functions on the fly.
|
|
||||||
* Manipulate the control model dynamically from within the component.
|
|
||||||
* [Test](guide/form-validation#testing) validation and control logic with isolated unit tests.
|
|
||||||
|
|
||||||
The following sample re-writes the hero form in Reactive Forms style.
|
|
||||||
|
|
||||||
|
|
||||||
{@a reactive-forms-module}
|
|
||||||
|
|
||||||
|
|
||||||
### 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:
|
|
||||||
|
|
||||||
<code-example path="form-validation/src/app/reactive/hero-form-reactive.module.ts" title="src/app/reactive/hero-form-reactive.module.ts" linenums="false">
|
|
||||||
|
|
||||||
</code-example>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The Reactive Forms feature module and component are in the `src/app/reactive` folder.
|
|
||||||
Focus on the `HeroFormReactiveComponent` there, starting with its template.
|
|
||||||
|
|
||||||
|
|
||||||
{@a reactive-component-template}
|
|
||||||
|
|
||||||
|
|
||||||
### Component template
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.html" region="form-tag" title="form-validation/src/app/reactive/hero-form-reactive.component.html" linenums="false">
|
|
||||||
|
|
||||||
</code-example>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Next, 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:
|
|
||||||
|
|
||||||
<code-tabs>
|
|
||||||
|
|
||||||
<code-pane title="hero-form-reactive.component.html (name #3)" path="form-validation/src/app/reactive/hero-form-reactive.component.html" region="name-with-error-msg">
|
|
||||||
|
|
||||||
</code-pane>
|
|
||||||
|
|
||||||
<code-pane title="hero-form-template1.component.html (name #2)" path="form-validation/src/app/template/hero-form-template2.component.html" region="name-with-error-msg">
|
|
||||||
|
|
||||||
</code-pane>
|
|
||||||
|
|
||||||
</code-tabs>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Key changes are:
|
|
||||||
* The validation attributes are gone (except `required`) because
|
|
||||||
validating happens in code.
|
|
||||||
|
|
||||||
* `required` remains, not for validation purposes (that's in the code),
|
|
||||||
but rather for css styling and accessibility.
|
|
||||||
|
|
||||||
|
|
||||||
<div class="l-sub-section">
|
|
||||||
|
|
||||||
Currently, Reactive Forms doesn't add the `required` or `aria-required`
|
|
||||||
HTML validation attribute to the DOM element
|
|
||||||
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 you'll see below.
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
* The `formControlName` replaces the `name` attribute; it serves the same
|
|
||||||
purpose of correlating the input 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.
|
|
||||||
That's all in code.
|
|
||||||
|
|
||||||
|
The same built-in validators that are available as attributes in template-driven forms, such as `required` and `minlength`, are all available to use as functions from the `Validators` class. For a full list of built-in validators, see the [Validators](api/forms/Validators) API reference.
|
||||||
|
|
||||||
|
To update the hero form to be a reactive form, you can use some of the same
|
||||||
|
built-in validators—this time, in function form. See below:
|
||||||
|
|
||||||
{@a reactive-component-class}
|
{@a reactive-component-class}
|
||||||
|
|
||||||
|
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.ts" region="form-group" title="reactive/hero-form-reactive.component.ts (validator functions)" linenums="false">
|
||||||
### 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 you can no longer query for it.
|
|
||||||
You can create the Angular form control model explicitly with
|
|
||||||
the help of the `FormBuilder` class.
|
|
||||||
|
|
||||||
Here's the section of code devoted to that process, paired with the Template Driven code it replaces:
|
|
||||||
|
|
||||||
<code-tabs>
|
|
||||||
|
|
||||||
<code-pane title="reactive/hero-form-reactive.component.ts (FormBuilder)" path="form-validation/src/app/reactive/hero-form-reactive.component.ts" region="form-builder">
|
|
||||||
|
|
||||||
</code-pane>
|
|
||||||
|
|
||||||
<code-pane title="template/hero-form-template2.component.ts (ViewChild)" path="form-validation/src/app/template/hero-form-template2.component.ts" region="view-child">
|
|
||||||
|
|
||||||
</code-pane>
|
|
||||||
|
|
||||||
</code-tabs>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
* Inject `FormBuilder` in a constructor.
|
|
||||||
|
|
||||||
* Call a `buildForm` method in the `ngOnInit` [lifecycle hook method](guide/lifecycle-hooks#hooks-overview)
|
|
||||||
because that's when you'll have the hero data. Call it again in the `addHero` method.
|
|
||||||
|
|
||||||
<div class="l-sub-section">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
A real app would retrieve the hero asynchronously from a data service, a task best performed in the `ngOnInit` hook.
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
* 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 `valueChanges` event and calls it immediately
|
|
||||||
to set error messages for the new control model.
|
|
||||||
|
|
||||||
## Built-in validators
|
|
||||||
|
|
||||||
Angular forms include a number of built-in validator functions, which are functions
|
|
||||||
that help you check common user input in forms. In addition to the built-in
|
|
||||||
validators covered here of `minlength`, `maxlength`,
|
|
||||||
and `required`, there are others such as `email` and `pattern`
|
|
||||||
for Reactive Forms.
|
|
||||||
For a full list of built-in validators,
|
|
||||||
see the [Validators](api/forms/Validators) API reference.
|
|
||||||
|
|
||||||
|
|
||||||
#### _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 `forbiddenName` validator on the `"name"` control is a custom validator,
|
|
||||||
discussed in a separate [section below](guide/form-validation#custom-validation).
|
|
||||||
|
|
||||||
<div class="l-sub-section">
|
|
||||||
|
|
||||||
Learn more about `FormBuilder` in the [Introduction to FormBuilder](guide/reactive-forms#formbuilder) section of Reactive Forms guide.
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
#### Committing hero value changes
|
|
||||||
|
|
||||||
In two-way data binding, the user's changes flow automatically from the controls back to the data model properties.
|
|
||||||
A Reactive Forms component should not use data binding to
|
|
||||||
automatically 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 adds a new hero.
|
|
||||||
|
|
||||||
The `onSubmit()` method simply replaces the `hero` object with the combined values of the form:
|
|
||||||
|
|
||||||
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.ts" region="on-submit" title="form-validation/src/app/reactive/hero-form-reactive.component.ts" linenums="false">
|
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
|
Note that:
|
||||||
|
|
||||||
The `addHero()` method discards pending changes and creates a brand new `hero` model object.
|
* The name control sets up two built-in validators—`Validators.required` and `Validators.minLength(4)`—and one custom validator, `forbiddenNameValidator`. For more details see the [Custom validators](guide/form-validation#custom-validators) section in this guide.
|
||||||
|
* As these validators are all sync validators, you pass them in as the second argument.
|
||||||
|
* Support multiple validators by passing the functions in as an array.
|
||||||
|
* This example adds a few getter methods. In a reactive form, you can always access any form control through the `get` method on its parent group, but sometimes it's useful to define getters as shorthands
|
||||||
|
for the template.
|
||||||
|
|
||||||
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.ts" region="add-hero" title="form-validation/src/app/reactive/hero-form-reactive.component.ts" linenums="false">
|
|
||||||
|
If you look at the template for the name input again, it is fairly similar to the template-driven example.
|
||||||
|
|
||||||
|
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.html" region="name-with-error-msg" title="reactive/hero-form-reactive.component.html (name with error msg)" linenums="false">
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
|
Key takeaways:
|
||||||
|
|
||||||
|
* The form no longer exports any directives, and instead uses the `name` getter defined in
|
||||||
|
the component class.
|
||||||
|
* The `required` attribute is still present. While it's not necessary for validation purposes,
|
||||||
|
you may want to keep it in your template for CSS styling or accessibility reasons.
|
||||||
|
|
||||||
|
|
||||||
Then it calls `buildForm()` again which replaces the previous `heroForm` control model with a new one.
|
## Custom validators
|
||||||
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.
|
Since the built-in validators won't always match the exact use case of your application, sometimes you'll want to create a custom validator.
|
||||||
|
|
||||||
<code-tabs>
|
Consider the `forbiddenNameValidator` function from previous
|
||||||
|
[examples](guide/form-validation#reactive-component-class) in
|
||||||
<code-pane title="reactive/hero-form-reactive.component.ts (#3)" path="form-validation/src/app/reactive/hero-form-reactive.component.ts">
|
this guide. Here's what the definition of that function looks like:
|
||||||
|
|
||||||
</code-pane>
|
|
||||||
|
|
||||||
<code-pane title="template/hero-form-template2.component.ts (#2)" path="form-validation/src/app/template/hero-form-template2.component.ts">
|
|
||||||
|
|
||||||
</code-pane>
|
|
||||||
|
|
||||||
<code-pane title="template/hero-form-template1.component.ts (#1)" path="form-validation/src/app/template/hero-form-template1.component.ts">
|
|
||||||
|
|
||||||
</code-pane>
|
|
||||||
|
|
||||||
</code-tabs>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="l-sub-section">
|
|
||||||
|
|
||||||
Run the [live example](guide/form-validation#live-example) to see how the reactive form behaves,
|
|
||||||
and to compare all of the files in this sample.
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## 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:
|
|
||||||
|
|
||||||
<code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="custom-validator" title="shared/forbidden-name.directive.ts (forbiddenNameValidator)" linenums="false">
|
<code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="custom-validator" title="shared/forbidden-name.directive.ts (forbiddenNameValidator)" linenums="false">
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
|
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", so the validator will reject any hero name containing "bob".
|
||||||
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.
|
Elsewhere it could reject "alice" or any name that the configuring regular expression matches.
|
||||||
|
|
||||||
The `forbiddenNameValidator` factory returns the configured validator function.
|
The `forbiddenNameValidator` factory returns the configured validator function.
|
||||||
That function takes an Angular control object and returns _either_
|
That function takes an Angular control object and returns _either_
|
||||||
null if the control value is valid _or_ a validation error object.
|
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'`,
|
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 you could insert into an error message (`{name}`).
|
and whose value is an arbitrary dictionary of values that you could insert into an error message, `{name}`.
|
||||||
|
|
||||||
|
### Adding to reactive forms
|
||||||
|
|
||||||
|
In reactive forms, custom validators are fairly simple to add. All you have to do is pass the function directly
|
||||||
|
to the `FormControl`.
|
||||||
|
|
||||||
### Custom validation directive
|
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.ts" region="custom-validator" title="reactive/hero-form-reactive.component.ts (validator functions)" linenums="false">
|
||||||
In the Reactive Forms component, the `'name'` control's validator function list
|
|
||||||
has a `forbiddenNameValidator` at the bottom.
|
|
||||||
|
|
||||||
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.ts" region="name-validators" title="reactive/hero-form-reactive.component.ts (name validators)" linenums="false">
|
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
|
### Adding to template-driven forms
|
||||||
|
|
||||||
|
In template-driven forms, you don't have direct access to the `FormControl` instance, so you can't pass the
|
||||||
|
validator in like you can for reactive forms. Instead, you need to add a directive to the template.
|
||||||
|
|
||||||
In the Template Driven example, the `<input>` has the selector (`forbiddenName`)
|
The corresponding `ForbiddenValidatorDirective` serves as a wrapper around the `forbiddenNameValidator`.
|
||||||
of a custom _attribute directive_, which rejects "bob".
|
|
||||||
|
|
||||||
<code-example path="form-validation/src/app/template/hero-form-template2.component.html" region="name-input" title="template/hero-form-template2.component.html (name input)" linenums="false">
|
Angular recognizes the directive's role in the validation process because the directive registers itself
|
||||||
</code-example>
|
with the `NG_VALIDATORS` provider, a provider with an extensible collection of validators.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
<code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="directive-providers" title="shared/forbidden-name.directive.ts (providers)" linenums="false">
|
<code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="directive-providers" title="shared/forbidden-name.directive.ts (providers)" linenums="false">
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
|
The directive class then implements the `Validator` interface, so that it can easily integrate
|
||||||
|
with Angular forms. Here is the rest of the directive to help you get an idea of how it all
|
||||||
Here is the rest of the directive to help you get an idea of how it all comes together:
|
comes together:
|
||||||
|
|
||||||
<code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="directive" title="shared/forbidden-name.directive.ts (directive)">
|
<code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="directive" title="shared/forbidden-name.directive.ts (directive)">
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
|
Once the `ForbiddenValidatorDirective` is ready, you can simply add its selector, `forbiddenName`, to any input element to activate it. For example:
|
||||||
|
|
||||||
|
<code-example path="form-validation/src/app/template/hero-form-template.component.html" region="name-input" title="template/hero-form-template.component.html (forbidden-name-input)" linenums="false">
|
||||||
|
|
||||||
|
</code-example>
|
||||||
|
|
||||||
|
|
||||||
<div class="l-sub-section">
|
<div class="l-sub-section">
|
||||||
|
|
||||||
If you are familiar with Angular validations, you may have noticed
|
You may have noticed that the custom validation directive is instantiated with `useExisting`
|
||||||
that the custom validation directive is instantiated with `useExisting`
|
|
||||||
rather than `useClass`. The registered validator must be _this instance_ of
|
rather than `useClass`. The registered validator must be _this instance_ of
|
||||||
the `ForbiddenValidatorDirective`—the instance in the form with
|
the `ForbiddenValidatorDirective`—the instance in the form with
|
||||||
its `forbiddenName` property bound to “bob". If you were to replace
|
its `forbiddenName` property bound to “bob". If you were to replace
|
||||||
`useExisting` with `useClass`, then you’d be registering a new class instance, one that
|
`useExisting` with `useClass`, then you’d be registering a new class instance, one that
|
||||||
doesn’t have a `forbiddenName`.
|
doesn’t have a `forbiddenName`.
|
||||||
|
|
||||||
To see this in action, run the example and then type “bob” in the name of Hero Form 2.
|
|
||||||
Notice that you get a validation error. Now change from `useExisting` to `useClass` and try again.
|
|
||||||
This time, when you type “bob”, there's no "bob" error message.
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## Control status CSS classes
|
||||||
|
|
||||||
<div class="l-sub-section">
|
Like in AngularJS, Angular automatically mirrors many control properties onto the form control element as CSS classes. You can use these classes to style form control elements according to the state of the form. The following classes are currently supported:
|
||||||
|
|
||||||
For more information on attaching behavior to elements,
|
* `.ng-valid`
|
||||||
see [Attribute Directives](guide/attribute-directives).
|
* `.ng-invalid`
|
||||||
|
* `.ng-pending`
|
||||||
|
* `.ng-pristine`
|
||||||
|
* `.ng-dirty`
|
||||||
|
* `.ng-untouched`
|
||||||
|
* `.ng-touched`
|
||||||
|
|
||||||
</div>
|
The hero form uses the `.ng-valid` and `.ng-invalid` classes to
|
||||||
|
set the color of each form control's border.
|
||||||
|
|
||||||
|
<code-example path="form-validation/src/forms.css" title="forms.css (status classes)">
|
||||||
|
|
||||||
|
</code-example>
|
||||||
|
|
||||||
|
|
||||||
|
**You can run the <live-example></live-example> to see the complete reactive and template-driven example code.**
|
||||||
## Testing Considerations
|
|
||||||
|
|
||||||
You 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.
|
|
||||||
|
@ -214,10 +214,10 @@ There are three changes:
|
|||||||
|
|
||||||
1. You import `FormsModule` and the new `HeroFormComponent`.
|
1. You import `FormsModule` and the new `HeroFormComponent`.
|
||||||
|
|
||||||
1. You add the `FormsModule` to the list of `imports` defined in the `ngModule` decorator. This gives the application
|
1. You add the `FormsModule` to the list of `imports` defined in the `@NgModule` decorator. This gives the application
|
||||||
access to all of the template-driven forms features, including `ngModel`.
|
access to all of the template-driven forms features, including `ngModel`.
|
||||||
|
|
||||||
1. You add the `HeroFormComponent` to the list of `declarations` defined in the `ngModule` decorator. This makes
|
1. You add the `HeroFormComponent` to the list of `declarations` defined in the `@NgModule` decorator. This makes
|
||||||
the `HeroFormComponent` component visible throughout this module.
|
the `HeroFormComponent` component visible throughout this module.
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,15 +25,8 @@ to a module factory, meaning you don't need to include the Angular compiler in y
|
|||||||
Ahead-of-time compiled applications also benefit from decreased load time and increased performance.
|
Ahead-of-time compiled applications also benefit from decreased load time and increased performance.
|
||||||
|
|
||||||
|
|
||||||
## Angular module
|
|
||||||
|
|
||||||
Helps you organize an application into cohesive blocks of functionality.
|
</div>
|
||||||
An Angular module identifies the components, directives, and pipes that the application uses along with the list of external Angular modules that the application needs, such as `FormsModule`.
|
|
||||||
|
|
||||||
Every Angular application has an application root-module class. By convention, the class is
|
|
||||||
called `AppModule` and resides in a file named `app.module.ts`.
|
|
||||||
|
|
||||||
For details and examples, see the [Angular Modules (NgModule)](guide/ngmodule) page.
|
|
||||||
|
|
||||||
|
|
||||||
## Annotation
|
## Annotation
|
||||||
@ -115,7 +108,7 @@ The Angular [scoped packages](guide/glossary#scoped-package) each have a barrel
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
You can often achieve the same result using [Angular modules](guide/glossary#angular-module) instead.
|
You can often achieve the same result using [NgModules](guide/glossary#ngmodule) instead.
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -132,7 +125,11 @@ between a "token"—also referred to as a "key"—and a dependency [prov
|
|||||||
|
|
||||||
## Bootstrap
|
## Bootstrap
|
||||||
|
|
||||||
You launch an Angular application by "bootstrapping" it using the application root Angular module (`AppModule`).
|
|
||||||
|
<div class="l-sub-section">
|
||||||
|
|
||||||
|
You launch an Angular application by "bootstrapping" it using the application root NgModule (`AppModule`).
|
||||||
|
|
||||||
Bootstrapping identifies an application's top level "root" [component](guide/glossary#component),
|
Bootstrapping identifies an application's top level "root" [component](guide/glossary#component),
|
||||||
which is the first component that is loaded for the application.
|
which is the first component that is loaded for the application.
|
||||||
For more information, see the [Setup](guide/setup) page.
|
For more information, see the [Setup](guide/setup) page.
|
||||||
@ -346,13 +343,13 @@ elements and their children.
|
|||||||
The [official JavaScript language specification](https://en.wikipedia.org/wiki/ECMAScript).
|
The [official JavaScript language specification](https://en.wikipedia.org/wiki/ECMAScript).
|
||||||
|
|
||||||
The latest approved version of JavaScript is
|
The latest approved version of JavaScript is
|
||||||
[ECMAScript 2016](http://www.ecma-international.org/ecma-262/7.0/)
|
[ECMAScript 2017](http://www.ecma-international.org/ecma-262/8.0/)
|
||||||
(also known as "ES2016" or "ES7"). Many Angular developers write their applications
|
(also known as "ES2017" or "ES8"). Many Angular developers write their applications
|
||||||
in ES7 or a dialect that strives to be
|
in ES8 or a dialect that strives to be
|
||||||
compatible with it, such as [TypeScript](guide/glossary#typescript).
|
compatible with it, such as [TypeScript](guide/glossary#typescript).
|
||||||
|
|
||||||
Most modern browsers only support the much older "ECMAScript 5" (also known as "ES5") standard.
|
Most modern browsers only support the much older "ECMAScript 5" (also known as "ES5") standard.
|
||||||
Applications written in ES2016, ES2015, or one of their dialects must be [transpiled](guide/glossary#transpile)
|
Applications written in ES2017, ES2016, ES2015, or one of their dialects must be [transpiled](guide/glossary#transpile)
|
||||||
to ES5 JavaScript.
|
to ES5 JavaScript.
|
||||||
|
|
||||||
Angular developers can write in ES5 directly.
|
Angular developers can write in ES5 directly.
|
||||||
@ -475,8 +472,8 @@ Read more in the [Lifecycle Hooks](guide/lifecycle-hooks) page.
|
|||||||
|
|
||||||
Angular has the following types of modules:
|
Angular has the following types of modules:
|
||||||
|
|
||||||
* [Angular modules](guide/glossary#angular-module).
|
* [NgModules](guide/glossary#ngmodule).
|
||||||
For details and examples, see the [Angular Modules](guide/ngmodule) page.
|
For details and examples, see the [NgModules](guide/ngmodule) page.
|
||||||
* ES2015 modules, as described in this section.
|
* ES2015 modules, as described in this section.
|
||||||
|
|
||||||
|
|
||||||
@ -493,7 +490,7 @@ In general, you assemble an application from many modules, both the ones you wri
|
|||||||
A module *exports* something of value in that code, typically one thing such as a class;
|
A module *exports* something of value in that code, typically one thing such as a class;
|
||||||
a module that needs that class *imports* it.
|
a module that needs that class *imports* it.
|
||||||
|
|
||||||
The structure of Angular modules and the import/export syntax
|
The structure of NgModules and the import/export syntax
|
||||||
is based on the [ES2015 module standard](http://www.2ality.com/2014/09/es6-modules-final.html).
|
is based on the [ES2015 module standard](http://www.2ality.com/2014/09/es6-modules-final.html).
|
||||||
|
|
||||||
An application that adheres to this standard requires a module loader to
|
An application that adheres to this standard requires a module loader to
|
||||||
@ -511,6 +508,24 @@ You rarely access Angular feature modules directly. You usually import them from
|
|||||||
|
|
||||||
{@a N}
|
{@a N}
|
||||||
|
|
||||||
|
|
||||||
|
## NgModule
|
||||||
|
|
||||||
|
<div class="l-sub-section">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Helps you organize an application into cohesive blocks of functionality.
|
||||||
|
An NgModule identifies the components, directives, and pipes that the application uses along with the list of external NgModules that the application needs, such as `FormsModule`.
|
||||||
|
|
||||||
|
Every Angular application has an application root-module class. By convention, the class is
|
||||||
|
called `AppModule` and resides in a file named `app.module.ts`.
|
||||||
|
|
||||||
|
For details and examples, see [NgModules](guide/ngmodule).
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
{@a O}
|
{@a O}
|
||||||
|
|
||||||
## Observable
|
## Observable
|
||||||
@ -614,7 +629,9 @@ For more information, see the [Routing & Navigation](guide/router) page.
|
|||||||
|
|
||||||
## Router module
|
## Router module
|
||||||
|
|
||||||
A separate [Angular module](guide/glossary#angular-module) that provides the necessary service providers and directives for navigating through application views.
|
<div class="l-sub-section">
|
||||||
|
|
||||||
|
A separate [NgModule](guide/glossary#ngmodule) that provides the necessary service providers and directives for navigating through application views.
|
||||||
|
|
||||||
For more information, see the [Routing & Navigation](guide/router) page.
|
For more information, see the [Routing & Navigation](guide/router) page.
|
||||||
|
|
||||||
@ -633,7 +650,7 @@ For more information, see the [Routing & Navigation](guide/router) page.
|
|||||||
A way to group related *npm* packages.
|
A way to group related *npm* packages.
|
||||||
Read more at the [npm-scope](https://docs.npmjs.com/misc/scope) page.
|
Read more at the [npm-scope](https://docs.npmjs.com/misc/scope) page.
|
||||||
|
|
||||||
Angular modules are delivered within *scoped packages* such as `@angular/core`,
|
NgModules are delivered within *scoped packages* such as `@angular/core`,
|
||||||
`@angular/common`, `@angular/platform-browser-dynamic`, `@angular/http`, and `@angular/router`.
|
`@angular/common`, `@angular/platform-browser-dynamic`, `@angular/http`, and `@angular/router`.
|
||||||
|
|
||||||
Import a scoped package the same way that you import a normal package.
|
Import a scoped package the same way that you import a normal package.
|
||||||
@ -795,4 +812,4 @@ asynchronous events by checking for data changes and updating
|
|||||||
the information it displays via [data bindings](guide/glossary#data-binding).
|
the information it displays via [data bindings](guide/glossary#data-binding).
|
||||||
|
|
||||||
Learn more about zones in this
|
Learn more about zones in this
|
||||||
[Brian Ford video](https://www.youtube.com/watch?v=3IqtmUscE_U).
|
[Brian Ford video](https://www.youtube.com/watch?v=3IqtmUscE_U).
|
||||||
|
@ -408,7 +408,7 @@ This XML element represents the translation of the `<h1>` greeting tag you marke
|
|||||||
|
|
||||||
<div class="l-sub-section">
|
<div class="l-sub-section">
|
||||||
|
|
||||||
Note that the translation unit `id=introductionHeader` is derived from the _custom_ `id`](#custom-id "Set a custom id") that you set earlier, but **without the `@@` prefix** required in the source HTML.
|
Note that the translation unit `id=introductionHeader` is derived from the [_custom_ `id`](#custom-id "Set a custom id") that you set earlier, but **without the `@@` prefix** required in the source HTML.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
176
aio/content/guide/language-service.md
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
# Angular Language Service
|
||||||
|
|
||||||
|
The Angular Language Service is a way to get completions, errors,
|
||||||
|
hints, and navigation inside your Angular templates whether they
|
||||||
|
are external in an HTML file or embedded in annotations/decorators
|
||||||
|
in a string. The Angular Language Service autodetects that you are
|
||||||
|
opening an Angular file, reads your `tsconfig.json` file, finds all the
|
||||||
|
templates you have in your application, and then provides language
|
||||||
|
services for any templates that you open.
|
||||||
|
|
||||||
|
|
||||||
|
## Autocompletion
|
||||||
|
|
||||||
|
Autocompletion can speed up your development time by providing you with
|
||||||
|
contextual possibilities and hints as you type. This example shows
|
||||||
|
autocomplete in an interpolation. As you type it out,
|
||||||
|
you can hit tab to complete.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="generated/images/guide/language-service/language-completion.gif" alt="autocompletion">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
There are also completions within
|
||||||
|
elements. Any elements you have as a component selector will
|
||||||
|
show up in the completion list.
|
||||||
|
|
||||||
|
## Error checking
|
||||||
|
|
||||||
|
The Angular Language Service can also forewarn you of mistakes in your code.
|
||||||
|
In this example, Angular doesn't know what `orders` is or where it comes from.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="generated/images/guide/language-service/language-error.gif" alt="error checking">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
Navigation allows you to hover to
|
||||||
|
see where a component, directive, module, etc. is from and then
|
||||||
|
click and press F12 to go directly to its definition.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="generated/images/guide/language-service/language-navigation.gif" alt="navigation">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
|
||||||
|
## Angular Language Service in your editor
|
||||||
|
|
||||||
|
Angular Language Service is currently available for [Visual Studio Code](https://code.visualstudio.com/) and
|
||||||
|
[WebStorm](https://www.jetbrains.com/webstorm).
|
||||||
|
|
||||||
|
### Visual Studio Code
|
||||||
|
|
||||||
|
In Visual Studio Code, install Angular Language Service from the store,
|
||||||
|
which is accessible from the bottom icon on the left menu pane.
|
||||||
|
You can also use the VS Quick Open (⌘+P) to search for the extension. When you've opened it,
|
||||||
|
enter the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ext install ng-template
|
||||||
|
```
|
||||||
|
|
||||||
|
Then click the install button to install the Angular Language Service.
|
||||||
|
|
||||||
|
|
||||||
|
### WebStorm
|
||||||
|
|
||||||
|
In webstorm, you have to install the language service as a dev dependency.
|
||||||
|
When Angular sees this dev dependency, it provides the
|
||||||
|
language service inside of WebStorm. Webstorm then gives you
|
||||||
|
colorization inside the template and autocomplete in addition to the Angular Language Service.
|
||||||
|
|
||||||
|
Here's the dev dependency
|
||||||
|
you need to have in `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
|
||||||
|
devDependencies {
|
||||||
|
"@angular/language-service": "^4.0.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in the terminal window at the root of your project,
|
||||||
|
install the `devDependencies` with `npm` or `yarn`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
*OR*
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
*OR*
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Sublime Text
|
||||||
|
|
||||||
|
In [Sublime Text](https://www.sublimetext.com/), you first need an extension to allow Typescript.
|
||||||
|
Install the latest version of typescript in a local `node_modules` directory:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install --save-dev typescript
|
||||||
|
```
|
||||||
|
|
||||||
|
Then install the Angular Language Service in the same location:
|
||||||
|
```sh
|
||||||
|
npm install --save-dev @angular/language-service
|
||||||
|
```
|
||||||
|
|
||||||
|
Starting with TypeScript 2.3, TypeScript has a language service plugin model that the language service can use.
|
||||||
|
|
||||||
|
Next, in your user preferences (`Cmd+,` or `Ctrl+,`), add:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"typescript-tsdk": "<path to your folder>/node_modules/typescript/lib"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Installing in your project
|
||||||
|
|
||||||
|
You can also install Angular Language Service in your project with the
|
||||||
|
following `npm` command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install --save-dev @angular/language-service
|
||||||
|
```
|
||||||
|
Additionally, add the following to the `"compilerOptions"` section of
|
||||||
|
your project's `tsconfig.json`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"plugins": [
|
||||||
|
{"name": "@angular/language-service"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
Note that this only provides diagnostics and completions in `.ts`
|
||||||
|
files. You need a custom sublime plugin (or modifications to the current plugin)
|
||||||
|
for completions in HTML files.
|
||||||
|
|
||||||
|
|
||||||
|
## How the Language Service works
|
||||||
|
|
||||||
|
When you use an editor with a language service, there's an
|
||||||
|
editor process which starts a separate language process/service
|
||||||
|
to which it speaks through an [RPC](https://en.wikipedia.org/wiki/Remote_procedure_call).
|
||||||
|
Any time you type inside of the editor, it sends information to the other process to
|
||||||
|
track the state of your project. When you trigger a completion list within a template, the editor process first parses the template into an HTML AST, or [abstract syntax tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree). Then the Angular compiler interprets
|
||||||
|
what module the template is part of, the scope you're in, and the component selector. Then it figures out where in the template AST your cursor is. When it determines the
|
||||||
|
context, it can then determine what the children can be.
|
||||||
|
|
||||||
|
It's a little more involved if you are in an interpolation. If you have an interpolation of `{{data.---}}` inside a `div` and need the completion list after `data.---`, the compiler can't use the HTML AST to find the answer. The HTML AST can only tell the compiler that there is some text with the characters "`{{data.---}}`". That's when the template parser produces an expression AST, which resides within the template AST. The Angular Language Services then looks at `data.---` within its context and asks the TypeScript Language Service what the members of data are. TypeScript then returns the list of possibilities.
|
||||||
|
|
||||||
|
|
||||||
|
For more in-depth information, see the
|
||||||
|
[Angular Language Service API](https://github.com/angular/angular/blob/master/packages/language-service/src/types.ts)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
## More on Information
|
||||||
|
|
||||||
|
For more information, see [Chuck Jazdzewski's presentation](https://www.youtube.com/watch?v=ez3R0Gi4z5A&t=368s) on the Angular Language
|
||||||
|
Service from [ng-conf](https://www.ng-conf.org/) 2017.
|
||||||
|
|
||||||
|
|
@ -397,8 +397,7 @@ created under test or before you decide to display it.
|
|||||||
Constructors should do no more than set the initial local variables to simple values.
|
Constructors should do no more than set the initial local variables to simple values.
|
||||||
|
|
||||||
An `ngOnInit()` is a good place for a component to fetch its initial data. The
|
An `ngOnInit()` is a good place for a component to fetch its initial data. The
|
||||||
[Tour of Heroes Tutorial](tutorial/toh-pt4#oninit) and [HTTP Client](guide/http#oninit)
|
[Tour of Heroes Tutorial](tutorial/toh-pt4#oninit) guide shows how.
|
||||||
guides show how.
|
|
||||||
|
|
||||||
|
|
||||||
Remember also that a directive's data-bound input properties are not set until _after construction_.
|
Remember also that a directive's data-bound input properties are not set until _after construction_.
|
||||||
|
@ -615,7 +615,7 @@ Once the application begins, the app root injector is closed to new providers.
|
|||||||
|
|
||||||
Time passes and application logic triggers lazy loading of a module.
|
Time passes and application logic triggers lazy loading of a module.
|
||||||
Angular must add the lazy-loaded module's providers to an injector somewhere.
|
Angular must add the lazy-loaded module's providers to an injector somewhere.
|
||||||
It can't added them to the app root injector because that injector is closed to new providers.
|
It can't add them to the app root injector because that injector is closed to new providers.
|
||||||
So Angular creates a new child injector for the lazy-loaded module context.
|
So Angular creates a new child injector for the lazy-loaded module context.
|
||||||
|
|
||||||
|
|
||||||
@ -1327,7 +1327,7 @@ Here's an _NgModule_ class with imports, exports, and declarations.
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
Of course you use _JavaScript_ modules to write _Angular_ modules as seen in the complete `contact.module.ts` file:
|
Of course you use _JavaScript_ modules to write NgModules as seen in the complete `contact.module.ts` file:
|
||||||
|
|
||||||
<code-example path="ngmodule/src/app/contact/contact.module.2.ts" title="src/app/contact/contact.module.ts" linenums="false">
|
<code-example path="ngmodule/src/app/contact/contact.module.2.ts" title="src/app/contact/contact.module.ts" linenums="false">
|
||||||
|
|
||||||
|
@ -163,7 +163,6 @@ without waiting for Angular updates.
|
|||||||
***angular-in-memory-web-api***: An Angular-supported library that simulates a remote server's web api
|
***angular-in-memory-web-api***: An Angular-supported library that simulates a remote server's web api
|
||||||
without requiring an actual server or real HTTP calls.
|
without requiring an actual server or real HTTP calls.
|
||||||
Good for demos, samples, and early stage development (before you even have a server).
|
Good for demos, samples, and early stage development (before you even have a server).
|
||||||
Read about it in the [HTTP Client](guide/http#in-mem-web-api) page.
|
|
||||||
|
|
||||||
***bootstrap***: [Bootstrap](http://getbootstrap.com/) is a popular HTML and CSS framework for designing responsive web apps.
|
***bootstrap***: [Bootstrap](http://getbootstrap.com/) is a popular HTML and CSS framework for designing responsive web apps.
|
||||||
Some of the samples improve their appearance with *bootstrap*.
|
Some of the samples improve their appearance with *bootstrap*.
|
||||||
|
@ -464,7 +464,7 @@ These files go in the root folder next to `src/`.
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
Inside `e2e/` live the End-to-End tests.
|
Inside `e2e/` live the end-to-end tests.
|
||||||
They shouldn't be inside `src/` because e2e tests are really a separate app that
|
They shouldn't be inside `src/` because e2e tests are really a separate app that
|
||||||
just so happens to test your main app.
|
just so happens to test your main app.
|
||||||
That's also why they have their own `tsconfig.e2e.json`.
|
That's also why they have their own `tsconfig.e2e.json`.
|
||||||
@ -493,7 +493,7 @@ These files go in the root folder next to `src/`.
|
|||||||
|
|
||||||
Configuration for Angular CLI.
|
Configuration for Angular CLI.
|
||||||
In this file you can set several defaults and also configure what files are included
|
In this file you can set several defaults and also configure what files are included
|
||||||
when your project is build.
|
when your project is built.
|
||||||
Check out the official documentation if you want to know more.
|
Check out the official documentation if you want to know more.
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
|
@ -1080,8 +1080,11 @@ To get access to the `FormArray` class, import it into `hero-detail.component.ts
|
|||||||
|
|
||||||
|
|
||||||
To _work_ with a `FormArray` you do the following:
|
To _work_ with a `FormArray` you do the following:
|
||||||
|
|
||||||
1. Define the items (`FormControls` or `FormGroups`) in the array.
|
1. Define the items (`FormControls` or `FormGroups`) in the array.
|
||||||
|
|
||||||
1. Initialize the array with items created from data in the _data model_.
|
1. Initialize the array with items created from data in the _data model_.
|
||||||
|
|
||||||
1. Add and remove items as the user requires.
|
1. Add and remove items as the user requires.
|
||||||
|
|
||||||
In this guide, you define a `FormArray` for `Hero.addresses` and
|
In this guide, you define a `FormArray` for `Hero.addresses` and
|
||||||
|
@ -255,11 +255,11 @@ During each navigation, the `Router` emits navigation events through the `Router
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<code>RouteConfigLoadStart</code>
|
<code>RouteConfigLoadEnd</code>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
An [event](api/router/RouteConfigLoadStart) triggered after a route has been lazy loaded.
|
An [event](api/router/RouteConfigLoadEnd) triggered after a route has been lazy loaded.
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -347,7 +347,7 @@ Here are the key `Router` terms and their meanings:
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
A separate Angular module that provides the necessary service providers
|
A separate NgModule that provides the necessary service providers
|
||||||
and directives for navigating through application views.
|
and directives for navigating through application views.
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@ -1830,7 +1830,7 @@ Finally, you activate the observable with `subscribe` method and (re)set the com
|
|||||||
|
|
||||||
#### _ParamMap_ API
|
#### _ParamMap_ API
|
||||||
|
|
||||||
The `ParamMap` API is inspired by the [URLSearchParams interface](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParamsOPut). It provides methods
|
The `ParamMap` API is inspired by the [URLSearchParams interface](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams). It provides methods
|
||||||
to handle parameter access for both route parameters (`paramMap`) and query parameters (`queryParamMap`).
|
to handle parameter access for both route parameters (`paramMap`) and query parameters (`queryParamMap`).
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
@ -3794,7 +3794,7 @@ Take the final step and detach the admin feature set from the main application.
|
|||||||
The root `AppModule` must neither load nor reference the `AdminModule` or its files.
|
The root `AppModule` must neither load nor reference the `AdminModule` or its files.
|
||||||
|
|
||||||
In `app.module.ts`, remove the `AdminModule` import statement from the top of the file
|
In `app.module.ts`, remove the `AdminModule` import statement from the top of the file
|
||||||
and remove the `AdminModule` from the Angular module's `imports` array.
|
and remove the `AdminModule` from the NgModule's `imports` array.
|
||||||
|
|
||||||
|
|
||||||
{@a can-load-guard}
|
{@a can-load-guard}
|
||||||
|
@ -282,36 +282,7 @@ This technique is effective because all browsers implement the _same origin poli
|
|||||||
on which cookies are set can read the cookies from that site and set custom headers on requests to that site.
|
on which cookies are set can read the cookies from that site and set custom headers on requests to that site.
|
||||||
That means only your application can read this cookie token and set the custom header. The malicious code on `evil.com` can't.
|
That means only your application can read this cookie token and set the custom header. The malicious code on `evil.com` can't.
|
||||||
|
|
||||||
Angular's `http` has built-in support for the client-side half of this technique in its `XSRFStrategy`.
|
Angular's `HttpClient` has built-in support for the client-side half of this technique. Read about it more in the [HttpClient guide](/guide/http).
|
||||||
The default `CookieXSRFStrategy` is turned on automatically.
|
|
||||||
Before sending an HTTP request, the `CookieXSRFStrategy` looks for a cookie called `XSRF-TOKEN` and
|
|
||||||
sets a header named `X-XSRF-TOKEN` with the value of that cookie.
|
|
||||||
|
|
||||||
The server must do its part by setting the
|
|
||||||
initial `XSRF-TOKEN` cookie and confirming that each subsequent state-modifying request
|
|
||||||
includes a matching `XSRF-TOKEN` cookie and `X-XSRF-TOKEN` header.
|
|
||||||
|
|
||||||
XSRF/CSRF tokens should be unique per user and session, have a large random value generated by a
|
|
||||||
cryptographically secure random number generator, and expire in a day or two.
|
|
||||||
|
|
||||||
Your server may use a different cookie or header name for this purpose.
|
|
||||||
An Angular application can customize cookie and header names by providing its own `CookieXSRFStrategy` values.
|
|
||||||
|
|
||||||
<code-example language="typescript">
|
|
||||||
{ provide: XSRFStrategy, useValue: new CookieXSRFStrategy('myCookieName', 'My-Header-Name') }
|
|
||||||
</code-example>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Or you can implement and provide an entirely custom `XSRFStrategy`:
|
|
||||||
|
|
||||||
|
|
||||||
<code-example language="typescript">
|
|
||||||
{ provide: XSRFStrategy, useClass: MyXSRFStrategy }
|
|
||||||
|
|
||||||
</code-example>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
For information about CSRF at the Open Web Application Security Project (OWASP), see
|
For information about CSRF at the Open Web Application Security Project (OWASP), see
|
||||||
<a href="https://www.owasp.org/index.php/Cross-Site_Request_Forgery_%28CSRF%29">Cross-Site Request Forgery (CSRF)</a> and
|
<a href="https://www.owasp.org/index.php/Cross-Site_Request_Forgery_%28CSRF%29">Cross-Site Request Forgery (CSRF)</a> and
|
||||||
@ -337,7 +308,7 @@ This attack is only successful if the returned JSON is executable as JavaScript.
|
|||||||
prevent an attack by prefixing all JSON responses to make them non-executable, by convention, using the
|
prevent an attack by prefixing all JSON responses to make them non-executable, by convention, using the
|
||||||
well-known string `")]}',\n"`.
|
well-known string `")]}',\n"`.
|
||||||
|
|
||||||
Angular's `Http` library recognizes this convention and automatically strips the string
|
Angular's `HttpClient` library recognizes this convention and automatically strips the string
|
||||||
`")]}',\n"` from all responses before further parsing.
|
`")]}',\n"` from all responses before further parsing.
|
||||||
|
|
||||||
For more information, see the XSSI section of this [Google web security blog
|
For more information, see the XSSI section of this [Google web security blog
|
||||||
|
@ -2129,12 +2129,12 @@ discourage the `I` prefix.
|
|||||||
<a href="#toc">Back to top</a>
|
<a href="#toc">Back to top</a>
|
||||||
|
|
||||||
|
|
||||||
## Application structure and Angular modules
|
## Application structure and NgModules
|
||||||
|
|
||||||
Have a near-term view of implementation and a long-term vision. Start small but keep in mind where the app is heading down the road.
|
Have a near-term view of implementation and a long-term vision. Start small but keep in mind where the app is heading down the road.
|
||||||
|
|
||||||
All of the app's code goes in a folder named `src`.
|
All of the app's code goes in a folder named `src`.
|
||||||
All feature areas are in their own folder, with their own Angular module.
|
All feature areas are in their own folder, with their own NgModule.
|
||||||
|
|
||||||
All content is one asset per file. Each component, service, and pipe is in its own file.
|
All content is one asset per file. Each component, service, and pipe is in its own file.
|
||||||
All third party vendor scripts are stored in another folder and not in the `src` folder.
|
All third party vendor scripts are stored in another folder and not in the `src` folder.
|
||||||
@ -2779,7 +2779,7 @@ and more difficult in a flat structure.
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Do** create an Angular module for each feature area.
|
**Do** create an NgModule for each feature area.
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -2790,7 +2790,7 @@ and more difficult in a flat structure.
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Why?** Angular modules make it easy to lazy load routable features.
|
**Why?** NgModules make it easy to lazy load routable features.
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -2801,7 +2801,7 @@ and more difficult in a flat structure.
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Why?** Angular modules make it easier to isolate, test, and re-use features.
|
**Why?** NgModules make it easier to isolate, test, and re-use features.
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -2827,7 +2827,7 @@ and more difficult in a flat structure.
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Do** create an Angular module in the app's root folder,
|
**Do** create an NgModule in the app's root folder,
|
||||||
for example, in `/src/app`.
|
for example, in `/src/app`.
|
||||||
|
|
||||||
|
|
||||||
@ -2839,7 +2839,7 @@ for example, in `/src/app`.
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Why?** Every app requires at least one root Angular module.
|
**Why?** Every app requires at least one root NgModule.
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -2888,7 +2888,7 @@ for example, in `/src/app`.
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Do** create an Angular module for all distinct features in an application;
|
**Do** create an NgModule for all distinct features in an application;
|
||||||
for example, a `Heroes` feature.
|
for example, a `Heroes` feature.
|
||||||
|
|
||||||
|
|
||||||
|
@ -12,8 +12,8 @@ component class instance (the *component*) and its user-facing template.
|
|||||||
You may be familiar with the component/template duality from your experience with model-view-controller (MVC) or model-view-viewmodel (MVVM).
|
You may be familiar with the component/template duality from your experience with model-view-controller (MVC) or model-view-viewmodel (MVVM).
|
||||||
In Angular, the component plays the part of the controller/viewmodel, and the template represents the view.
|
In Angular, the component plays the part of the controller/viewmodel, and the template represents the view.
|
||||||
|
|
||||||
This page is a comprehensive technical reference to the Angular template language.
|
This page is a comprehensive technical reference to the Angular template language.
|
||||||
It explains basic principles of the template language and describes most of the syntax that you'll encounter elsewhere in the documentation.
|
It explains basic principles of the template language and describes most of the syntax that you'll encounter elsewhere in the documentation.
|
||||||
|
|
||||||
Many code snippets illustrate the points and concepts, all of them available
|
Many code snippets illustrate the points and concepts, all of them available
|
||||||
in the <live-example title="Template Syntax Live Code"></live-example>.
|
in the <live-example title="Template Syntax Live Code"></live-example>.
|
||||||
@ -1155,7 +1155,7 @@ other HTML elements, attributes, properties, and components.
|
|||||||
They are usually applied to elements as if they were HTML attributes, hence the name.
|
They are usually applied to elements as if they were HTML attributes, hence the name.
|
||||||
|
|
||||||
Many details are covered in the [_Attribute Directives_](guide/attribute-directives) guide.
|
Many details are covered in the [_Attribute Directives_](guide/attribute-directives) guide.
|
||||||
Many Angular modules such as the [`RouterModule`](guide/router "Routing and Navigation")
|
Many NgMdules such as the [`RouterModule`](guide/router "Routing and Navigation")
|
||||||
and the [`FormsModule`](guide/forms "Forms") define their own attribute directives.
|
and the [`FormsModule`](guide/forms "Forms") define their own attribute directives.
|
||||||
This section is an introduction to the most commonly used attribute directives:
|
This section is an introduction to the most commonly used attribute directives:
|
||||||
|
|
||||||
@ -1260,7 +1260,7 @@ Two-way data binding with the `NgModel` directive makes that easy. Here's an exa
|
|||||||
#### _FormsModule_ is required to use _ngModel_
|
#### _FormsModule_ is required to use _ngModel_
|
||||||
|
|
||||||
Before using the `ngModel` directive in a two-way data binding,
|
Before using the `ngModel` directive in a two-way data binding,
|
||||||
you must import the `FormsModule` and add it to the Angular module's `imports` list.
|
you must import the `FormsModule` and add it to the NgModule's `imports` list.
|
||||||
Learn more about the `FormsModule` and `ngModel` in the
|
Learn more about the `FormsModule` and `ngModel` in the
|
||||||
[Forms](guide/forms#ngModel) guide.
|
[Forms](guide/forms#ngModel) guide.
|
||||||
|
|
||||||
@ -1947,12 +1947,12 @@ As of Typescript 2.0, you can enforce [strict null checking](http://www.typescri
|
|||||||
|
|
||||||
In this mode, typed variables disallow null and undefined by default. The type checker throws an error if you leave a variable unassigned or try to assign null or undefined to a variable whose type disallows null and undefined.
|
In this mode, typed variables disallow null and undefined by default. The type checker throws an error if you leave a variable unassigned or try to assign null or undefined to a variable whose type disallows null and undefined.
|
||||||
|
|
||||||
The type checker also throws an error if it can't determine whether a variable will be null or undefined at runtime.
|
The type checker also throws an error if it can't determine whether a variable will be null or undefined at runtime.
|
||||||
You may know that can't happen but the type checker doesn't know.
|
You may know that can't happen but the type checker doesn't know.
|
||||||
You tell the type checker that it can't happen by applying the post-fix
|
You tell the type checker that it can't happen by applying the post-fix
|
||||||
[_non-null assertion operator (!)_]((http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator "Non-null assertion operator").
|
[_non-null assertion operator (!)_](http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator "Non-null assertion operator").
|
||||||
|
|
||||||
The _Angular_ **non-null assertion operator (`!`)** serves the same purpose in an Angular template.
|
The _Angular_ **non-null assertion operator (`!`)** serves the same purpose in an Angular template.
|
||||||
|
|
||||||
For example, after you use [*ngIf](guide/template-syntax#ngIf) to check that `hero` is defined, you can assert that
|
For example, after you use [*ngIf](guide/template-syntax#ngIf) to check that `hero` is defined, you can assert that
|
||||||
`hero` properties are also defined.
|
`hero` properties are also defined.
|
||||||
|
@ -178,7 +178,7 @@ For a discussion of the unit testing setup files, [see below](guide/testing#setu
|
|||||||
{@a isolated-v-testing-utilities}
|
{@a isolated-v-testing-utilities}
|
||||||
|
|
||||||
|
|
||||||
### Isolated unit tests vs. the Angular testing utilites
|
### Isolated unit tests vs. the Angular testing utilities
|
||||||
|
|
||||||
[Isolated unit tests](guide/testing#isolated-unit-tests "Unit testing without the Angular testing utilities")
|
[Isolated unit tests](guide/testing#isolated-unit-tests "Unit testing without the Angular testing utilities")
|
||||||
examine an instance of a class all by itself without any dependence on Angular or any injected values.
|
examine an instance of a class all by itself without any dependence on Angular or any injected values.
|
||||||
@ -427,7 +427,7 @@ and re-attach it to a dynamically-constructed Angular test module
|
|||||||
tailored specifically for this battery of tests.
|
tailored specifically for this battery of tests.
|
||||||
|
|
||||||
The `configureTestingModule` method takes an `@NgModule`-like metadata object.
|
The `configureTestingModule` method takes an `@NgModule`-like metadata object.
|
||||||
The metadata object can have most of the properties of a normal [Angular module](guide/ngmodule).
|
The metadata object can have most of the properties of a normal [NgModule](guide/ngmodule).
|
||||||
|
|
||||||
_This metadata object_ simply declares the component to test, `BannerComponent`.
|
_This metadata object_ simply declares the component to test, `BannerComponent`.
|
||||||
The metadata lack `imports` because (a) the default testing module configuration already has what `BannerComponent` needs
|
The metadata lack `imports` because (a) the default testing module configuration already has what `BannerComponent` needs
|
||||||
|
@ -247,12 +247,10 @@ next to the original _ES5_ version for comparison:
|
|||||||
</code-pane>
|
</code-pane>
|
||||||
</code-tabs>
|
</code-tabs>
|
||||||
|
|
||||||
|
{@a name-constructor}
|
||||||
|
|
||||||
<div class="callout is-helpful">
|
<div class="callout is-helpful">
|
||||||
|
|
||||||
{@a name-constructor}
|
|
||||||
|
|
||||||
### Name the constructor
|
|
||||||
|
|
||||||
A **named** constructor displays clearly in the console log
|
A **named** constructor displays clearly in the console log
|
||||||
if the component throws a runtime error.
|
if the component throws a runtime error.
|
||||||
An **unnamed** constructor displays as an anonymous function, for example, `class0`,
|
An **unnamed** constructor displays as an anonymous function, for example, `class0`,
|
||||||
|
@ -62,7 +62,7 @@ There are a few rules in particular that will make it much easier to do
|
|||||||
* The [Folders-by-Feature Structure](https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md#folders-by-feature-structure)
|
* The [Folders-by-Feature Structure](https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md#folders-by-feature-structure)
|
||||||
and [Modularity](https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md#modularity)
|
and [Modularity](https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md#modularity)
|
||||||
rules define similar principles on a higher level of abstraction: Different parts of the
|
rules define similar principles on a higher level of abstraction: Different parts of the
|
||||||
application should reside in different directories and Angular modules.
|
application should reside in different directories and NgModules.
|
||||||
|
|
||||||
When an application is laid out feature per feature in this way, it can also be
|
When an application is laid out feature per feature in this way, it can also be
|
||||||
migrated one feature at a time. For applications that don't already look like
|
migrated one feature at a time. For applications that don't already look like
|
||||||
@ -382,12 +382,12 @@ that describes Angular assets in metadata. The differences blossom from there.
|
|||||||
|
|
||||||
In a hybrid application you run both versions of Angular at the same time.
|
In a hybrid application you run both versions of Angular at the same time.
|
||||||
That means that you need at least one module each from both AngularJS and Angular.
|
That means that you need at least one module each from both AngularJS and Angular.
|
||||||
You will import `UpgradeModule` inside the Angular module, and then use it for
|
You will import `UpgradeModule` inside the NgModule, and then use it for
|
||||||
bootstrapping the AngularJS module.
|
bootstrapping the AngularJS module.
|
||||||
|
|
||||||
<div class="l-sub-section">
|
<div class="l-sub-section">
|
||||||
|
|
||||||
Learn more about Angular modules at the [NgModule guide](guide/ngmodule).
|
Read more about [NgModules](guide/ngmodule).
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -485,7 +485,7 @@ Because `HeroDetailComponent` is an Angular component, you must also add it to t
|
|||||||
|
|
||||||
And because this component is being used from the AngularJS module, and is an entry point into
|
And because this component is being used from the AngularJS module, and is an entry point into
|
||||||
the Angular application, you must add it to the `entryComponents` for the
|
the Angular application, you must add it to the `entryComponents` for the
|
||||||
Angular module.
|
NgModule.
|
||||||
|
|
||||||
<code-example path="upgrade-module/src/app/downgrade-static/app.module.ts" region="ngmodule" title="app.module.ts">
|
<code-example path="upgrade-module/src/app/downgrade-static/app.module.ts" region="ngmodule" title="app.module.ts">
|
||||||
</code-example>
|
</code-example>
|
||||||
|
BIN
aio/content/images/bios/gerardsans.jpg
Normal file
After Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 101 KiB |
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 320 KiB After Width: | Height: | Size: 200 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 83 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.3 KiB |