Compare commits

..

486 Commits

Author SHA1 Message Date
cea02414b0 docs: add changelog for 5.0.0-beta.3 2017-08-09 16:07:37 -07:00
f0ec31e47f release: cut the 5.0.0-beta.3 release 2017-08-09 16:04:57 -07:00
ff5c58be6b feat(forms): add default updateOn values for groups and arrays (#18536)
This commit adds support for setting default `updateOn` values
in `FormGroups` and `FormArrays`. If you set `updateOn` to
’blur’` at the group level, all child controls will default to `’blur’`,
unless the child has explicitly specified a different `updateOn` value.

```
const c = new FormGroup({
   one: new FormControl()
}, {updateOn: blur});
```

 It's worth noting that parent groups will always update their value and
validity immediately upon value/validity updates from children. In other
words, if a group is set to update on blur and its children are individually
set to update on change, the group will still update on change with its
children; its default value will simply not be used.
2017-08-09 15:41:53 -07:00
dca50deae4 docs(core): deprecate ReflectiveInjector
closes #18598
2017-08-09 14:44:49 -07:00
7f2037f0b6 test(aio): fix running docs examples against local builds (#18520)
This commit also updates the version of `@angular/cli` used for docs examples.
The previous (transient) dependency `@ngtools/webpack` was not compatible with
`@angular/compiler-cli@>=5` and was breaking when running against the local
builds (currently at 5.0.0-beta.2). The version of `@ngtools/webpack` used by
the latest `@angular/cli` version is compatible with `@angular/compiler-cli@5`.
2017-08-09 14:21:10 -07:00
fd6ae571b8 fix(aio): add missing code snippet (#18547)
The snippet got lost some time during the migration from the old version (it is
[present in v2][1]).

[1]: https://v2.angular.io/docs/ts/latest/cookbook/aot-compiler.html#!#running-the-application

Fixes #18544
2017-08-09 14:20:25 -07:00
b14250bef9 test(aio): fix the deploy-to-firebase tests
This commit also ensures that if the tests fail, the script exits with an error.
2017-08-09 14:18:33 -07:00
6fb5250185 ci(aio): fix deploying the stable branch to Firebase
The `deploy-to-firebase.sh` always expects there to be a
`src/extra-files/<mode>` directory and breaks if it doesn't exist.
2017-08-09 14:18:33 -07:00
2f9d8ff46d test(animations): disable buggy test in Chrome 39 (#18483)
Fixes #15793
2017-08-09 14:15:40 -07:00
e54bd59f22 fix(core): forbid destroyed views to be inserted or moved in VC (#18568)
Fixes #17780
2017-08-09 14:11:51 -07:00
1e1833198d ci(aio): fix deploying to firebase (#18590) 2017-08-08 13:59:25 -07:00
6f2038cc85 fix(compiler-cli): fix and re-enble expression lowering (#18570)
Fixes issue uncovered by #18388 and re-enables expression
lowering disabled by #18513.
2017-08-08 12:40:08 -07:00
f0a55016af fix(core): fix platform-browser-dynamic (#18576)
follow-up for #18496
2017-08-08 11:41:12 -07:00
fcadbf4bf6 perf: switch angular to use StaticInjector instead of ReflectiveInjector
This change allows ReflectiveInjector to be tree shaken resulting
in not needed Reflect polyfil and smaller bundles.

Code savings for HelloWorld using Closure:

Reflective: bundle.js:  105,864(34,190 gzip)
    Static: bundle.js:  154,889(33,555 gzip)
                            645( 2%)

BREAKING CHANGE:

`platformXXXX()` no longer accepts providers which depend on reflection.
Specifically the method signature when from `Provider[]` to
`StaticProvider[]`.

Example:
Before:
```
[
  MyClass,
  {provide: ClassA, useClass: SubClassA}
]

```

After:
```
[
  {provide: MyClass, deps: [Dep1,...]},
  {provide: ClassA, useClass: SubClassA, deps: [Dep1,...]}
]
```

NOTE: This only applies to platform creation and providers for the JIT
compiler. It does not apply to `@Compotent` or `@NgModule` provides
declarations.

Benchpress note: Previously Benchpress also supported reflective
provides, which now require static providers.

DEPRECATION:

- `ReflectiveInjector` is now deprecated as it will be remove. Use
  `Injector.create` as a replacement.

closes #18496
2017-08-07 15:42:34 -07:00
d9d00bd9b5 feat(core): Create StaticInjector which does not depend on Reflect polyfill. 2017-08-07 15:40:15 -07:00
f69561b2de feat(forms): add updateOn submit option to FormControls (#18514) 2017-08-07 15:39:25 -07:00
685cc26ab2 fix(common): don't recreate view when context shape doesn't change (#18277)
Problem description: when using ngTemplateOutlet with context as
an object literal in a template and binding to the context's property
the embedded view would get re-created even if context object remains
essentially the same (the same shape, just update to one properties).
This happens since currently change detection will re-create object
references when an object literal is used and one of its properties
gets updated through a binding.

Solution: this commit changes ngTemplateOutlet logic so we take
context object shape into account before deciding if we should
re-create view or just update existing context.

Fixes #13407
2017-08-07 14:31:26 -07:00
5b7432b6ea fix(compiler-cli): remove minimist dependency of compiler-cli/index (#18532)
Indirectly removes the minimist dependency in the language service
package was added with the addition of `ngc.ts`.
2017-08-07 14:30:35 -07:00
04b18a9f46 docs(common): fix the DatePipe API docs (#18548) 2017-08-07 11:47:08 -07:00
05472cb21b fix(animations): support persisting dynamic styles within animation states (#18468)
Closes #18423
Closes #17505
2017-08-07 11:40:04 -07:00
c0c03dc4ba fix(animations): revert container/queried animations accordingly during cancel (#18516) 2017-08-07 11:38:30 -07:00
f9f8924c49 docs(aio): typo & update the bio (#18559) 2017-08-07 10:20:11 -07:00
10897d6473 ci(aio): compute AIO deployment mode
There are now 3 modes for deployment: next, stable, archive.
We compute which mode (and other deployment properties)
from the `TRAVIS_BRANCH` and the `STABLE_BRANCH`.

If the TRAVIS_BRANCH is master we deploy as "next".
If the `TRAVIS_BRANCH` matches the `STABLE_BRANCH` we deploy as "stable".

Otherwise if the branch has a major version lower than the stable version
and its minor version is highest of similar branches we deploy as "archive".

For "archive" deployments we compute the firebase project and deployment
url based on the major version of the `TRAVIS_BRANCH`.

As well as choosing where to deploy the build, we also use this
to select the environment file for the AIO Angular app.
This will enable the app to change its rendering and behaviour
based on its mode.

See #18287
Closes #18297
2017-08-04 09:14:18 -07:00
340837aa46 feat(aio): add "archive" and "next" color themes 2017-08-04 09:13:34 -07:00
42ef1be75c feat(aio): redirect marketing pages to docs if deploy mode is archive
See #18287
2017-08-04 09:13:34 -07:00
a5801b6020 feat(aio): add deploy mode to version picker
See #18287
2017-08-04 09:13:34 -07:00
70b62949de feat(aio): enable deployment mode to be set via URL query
The deployment mode set from the environment provided at build time;
or overridden by the `mode` query parameter: e.g. `...?mode=archive`

See #18287
2017-08-04 09:13:34 -07:00
36161d99f6 feat(aio): update UI based on deployment mode
* Add a banner if the mode is "archive"
* Add a `mode-...` class to the `aio-shell` element to enable
mode based theming.

See #18287
2017-08-04 09:13:34 -07:00
0714139e37 ci(aio): include extra files in AIO deployment based on mode
Any files that are inside the `extra-files/{mode}` folder
will be copied over to the `dist` folder before deployment
to Firebase.

See #18287
2017-08-04 09:13:34 -07:00
bcb36d9b6d ci(aio): compute AIO deployment mode
There are now 3 modes for deployment: next, stable, archive.
We compute which mode (and other deployment properties)
from the `TRAVIS_BRANCH` and the `STABLE_BRANCH`.

If the TRAVIS_BRANCH is master we deploy as "next".
Otherwise if the branch is the highest of its minor versions
we deploy as "stable" if the `TRAVIS_BRANCH` matches the `STABLE_BRANCH` or
else "archive".

For "archive" deployments we compute the firebase project and deployment
url based on the major version of the `TRAVIS_BRANCH`.

As well as choosing where to deploy the build, we also use this
to select the environment file for the AIO Angular app.
This will enable the app to change its rendering and behaviour
based on its mode.

See #18287
2017-08-04 09:13:34 -07:00
ca695e0632 fix(compiler-cli): disable buggy expression lowering (#18513) 2017-08-03 14:31:23 -07:00
9b015a95eb docs(aio): tech edits to form validation
closes #18495
2017-08-03 13:57:31 -07:00
939dc44391 docs(forms): update and re-organize validation guide 2017-08-03 13:56:53 -07:00
5651e4ac72 fix(compiler-cli): modified ngc to throw all errors, not just syntax (#18388) 2017-08-03 11:10:47 -07:00
1dca575701 fix(compiler): ignore @import in multi-line css (#18452)
Fixes #18038
2017-08-03 11:00:38 -07:00
333a708bb6 feat(forms): add updateOn blur option to FormControls (#18408)
By default, the value and validation status of a `FormControl` updates
whenever its value changes. If an application has heavy validation
requirements, updating on every text change can sometimes be too expensive.

This commit introduces a new option that improves performance by delaying
form control updates until the "blur" event.  To use it, set the `updateOn`
option to `blur` when instantiating the `FormControl`.

```ts
// example without validators
const c = new FormControl(, { updateOn: blur });

// example with validators
const c= new FormControl(, {
   validators: Validators.required,
   updateOn: blur
});
```

Like in AngularJS, setting `updateOn` to `blur` will delay the update of
the value as well as the validation status. Updating value and validity
together keeps the system easy to reason about, as the two will always be
in sync. It's  also worth noting that the value/validation pipeline does
still run when the form is initialized (in order to support initial values).

Closes #7113
2017-08-02 18:10:10 -07:00
3a227a1f6f refactor(router): compile router cleanly with TypeScript 2.4 (#18465) 2017-08-02 17:32:02 -07:00
81cb5bc3a7 docs(router): fix typo (#18479) 2017-08-02 17:31:09 -07:00
1640d2aa0b refactor(platform-browser): compiler platform-browser packages cleanly (#18464) 2017-08-02 16:30:50 -07:00
5f501c722b refactor(forms): compile forms cleanly with TypeScript 2.4 (#18462) 2017-08-02 16:29:31 -07:00
ea07856cc5 refactor(upgrade): compile upgrade cleanly with TypeScript 2.4 (#18461) 2017-08-02 16:28:04 -07:00
7c47b62a96 fix(compiler): cleanly compile with TypeScript 2.4 (#18456) 2017-08-02 16:26:42 -07:00
e25b3dd163 fix(benchpress): compile cleanly with TS 2.4 (#18455) 2017-08-02 16:24:00 -07:00
6a88659c9a test(upgrade): fix an IE9 timer issue in downgrade module tests (#18482) 2017-08-02 16:09:38 -07:00
44ae6e94e3 fix(aio): fix layout of the webpack guide (#18493)
This is possibly a temporary fix for the layout, until we decide whether we want
to remove the guide or properly add it to the SideNav menu.

Fixes #17912
2017-08-02 16:00:36 -07:00
1635a06bda ci(aio): Add commit message to payload data (#18137) 2017-08-02 15:59:20 -07:00
3923c30df0 docs(aio): fix missing anchor-open in i18n documentation (#18476)
The Internationalisation documentation, "Translate text nodes" section, has an incomplete
markdown anchor, and leaks markdown into the page. Fix the anchor by adding the opening bracket.
2017-08-02 15:56:01 -07:00
99017bf3ff fix(aio): correctly redirect cookbook/a1-a2-quick-reference.html (#18418)
Fixes #18415
2017-08-02 15:54:22 -07:00
4d117faf1a test(common): skip some DatePipe tests in old Chrome where Intl is buggy (#15784) 2017-08-02 15:51:59 -07:00
5cc9913ded docs(aio): replace old blog link in footer (#18448)
Fixes #18233
2017-08-02 15:45:54 -07:00
1d09838622 ci: remove chromium fold reference (#18445) 2017-08-02 15:43:26 -07:00
9adf40aa77 build(aio): use cli 1.3.0-rc (#18290) 2017-08-02 15:37:03 -07:00
89c616199f docs: improve github labels by introducing "PR target" labels (#18436)
I also renamed all "pr_*" lables to "PR *" lables, removed obsolete
"chore" label, and added docs label.
2017-08-02 15:30:36 -07:00
1e1af7ffcb docs: changelog for 5.0.0-beta.2 release 2017-08-02 13:23:27 -07:00
a84b2bc945 release: cut the 5.0.0-beta.2 release 2017-08-02 13:21:07 -07:00
7abcb99d57 docs: add changelog for 4.3.3 2017-08-02 13:19:00 -07:00
49cd8513e4 feat(router): add events tracking activation of individual routes
* Adds `ChildActivationStart` and `ChildActivationEnd`
* Adds test to verify the PreActivation phase of routing
2017-08-01 10:44:00 -07:00
82b067fc40 build: ignore node_modules for tslint 2017-08-01 10:13:44 -07:00
9479a106bb build: enable TSLint on the packages folder 2017-07-31 15:47:57 -07:00
e64b54b67b fix(compiler): do not consider arguments when determining recursion
The static reflectory check for macro function recursion was too
agressive and disallowed calling a function with argument that also
calls the same function. For example, it disallowed nested animation
groups.

Fixes: #17467
2017-07-31 13:42:31 -07:00
cc2a4c41f9 build(aio): fix warning about missing <h1>
Fixes #17549
2017-07-31 13:40:07 -07:00
a11542a375 docs(aio): fixed list format in FormArray section 2017-07-31 11:31:05 -07:00
b6c4af6495 feat(compiler-cli): automatically lower lambda expressions in metadata 2017-07-31 11:30:44 -07:00
67dff7bd5d feat(tsc-wrapped): allow values to be substituted by collector clients
Also reenabled tests that were unintentionally disabled when they were
moved from tools/@angular.
2017-07-31 11:30:44 -07:00
381471d338 fix(compiler): fix for element needing implicit parent placed in top-level ng-container
fixes #18314
2017-07-31 11:30:19 -07:00
ebef5e697a feat(forms): add options arg to abstract controls
FormControls, FormGroups, and FormArrays now optionally accept an options
object as their second argument. Validators and async validators can be
passed in as part of this options object (though they can still be passed
in as the second and third arg as before).

```ts
const c = new FormControl(, {
   validators: [Validators.required],
   asyncValidators: [myAsyncValidator]
});
```

This commit also adds support for passing arrays of validators and async
validators to FormGroups and FormArrays, which formerly only accepted
individual functions.

```ts
const g = new FormGroup({
   one: new FormControl()
}, [myPasswordValidator, myOtherValidator]);
```

This change paves the way for adding more options to AbstractControls,
such as more fine-grained control of validation timing.
2017-07-31 11:29:32 -07:00
d71ae278ef fix(aio): fix links to source for paths with symlinks
Fixes #18353
2017-07-28 15:28:59 -07:00
46207538ef ci: short-circuit npm install for aio builds that use yarn only 2017-07-28 15:28:28 -07:00
71eb7437b6 docs(aio): delay ngUpgrade e2e test to avoid flakes 2017-07-28 15:28:28 -07:00
b5ffbe342b build: short-circuit build for AIO tasks 2017-07-28 15:28:28 -07:00
0f79223008 docs(aio): fix deprecated protractor API usage
`browser.getLocationAbsUrl()` is deprecated.
We should use `browser.getCurrentUrl()` instead.
2017-07-28 15:28:28 -07:00
a085223331 ci(aio): test the example e2e files using local build of Angular 2017-07-28 15:28:28 -07:00
c383048259 build(aio): ignore generated aot files
Assets such as images and data which are generated
by the aot build were not being ignored.
2017-07-28 15:28:28 -07:00
b18eb04b46 docs(aio): remove generated styles.css file
This file should have been ignored as it is created
during the build of the example
2017-07-28 15:28:28 -07:00
c8c2ab012a build(aio): support overriding the Angular packages in examples with locally built ones 2017-07-28 15:28:28 -07:00
ecff8e6c93 build(aio): refactor and test the example-boilerplate tool 2017-07-28 15:28:28 -07:00
51f1da1b85 ci: shard the aio example e2e tests 2017-07-28 15:28:28 -07:00
a5e18c4cdf ci(aio): support sharding of example e2e tests 2017-07-28 15:28:28 -07:00
cf6284656f build(aio): upgrade @angular/material to 2.0.0-meta.8 2017-07-28 15:26:45 -07:00
3182ddaf3e build(aio): upgrade @angular/* to 4.3.1 2017-07-28 15:26:45 -07:00
416ed691e5 docs(aio): fix URLSearchParams interface link to MDN
Fixes #18367
2017-07-28 15:26:04 -07:00
0fb7484d51 refactor(aio): move content-specific images to content/images/
Fixes #17053
2017-07-28 15:06:49 -07:00
6a3454e81e refactor(aio): rename unused directories to _unused 2017-07-28 15:06:49 -07:00
c3fbe87012 fix(aio): fix link to logo in example 2017-07-28 15:06:49 -07:00
24117d7a49 refactor(aio): move unused images to unused directories
This prevents the ServiceWorker from prefetching unnecessary files.
2017-07-28 15:06:49 -07:00
5808153359 docs: add changelog for 5.0.0.-beta.1 2017-07-27 14:59:24 -07:00
9030c8a03e release: cut the 5.0.0-beta.1 release 2017-07-27 14:57:38 -07:00
b14fc06fa2 docs: add changelog for 4.3.2 2017-07-27 14:52:35 -07:00
a7f2468184 Revert "fix(router): should throw when lazy loaded module doesn't define any routes (#15001)"
This reverts commit 82923a381d.
2017-07-27 10:53:01 -07:00
fae47d86b3 refactor(forms): move value accessor tests into own spec (#18356)
PR Close #18356
2017-07-26 17:55:37 -05:00
d20ac14fe2 refactor(compiler-cli): allow custom error checking function in ngc (#18355)
PR Close #18355
2017-07-26 17:55:31 -05:00
cae3e6dca0 ci: give ownership of ngc-wrapped to compiler-cli maintainers (#18354)
PR Close #18354
2017-07-26 17:55:24 -05:00
086f4aa72c fix(router): child CanActivate guard should wait for parent to complete (#18110)
Closes #15670

PR Close #18110
2017-07-26 17:11:22 -05:00
82923a381d fix(router): should throw when lazy loaded module doesn't define any routes (#15001)
Closes #14596

PR Close #15001
2017-07-26 17:11:07 -05:00
5152abb037 docs(aio): add my details as a contributor (#18315)
PR Close #18315
2017-07-26 17:11:07 -05:00
67f7032321 fix(aio): correctly process markdown link in "Browser Support" (#18349)
The markdown processor expects an empty line between an opening tag and the
markdown content. (If there is no empty line, the content is interpreted as
plain HTML.)
Previously, the line between the opening `<td>` and the content contained
whitespace, which caused the content to be interpreted as HTML and not markdown.

Fixes #18312

PR Close #18349
2017-07-26 16:07:26 -05:00
205abe8140 build: fix broken bazel build (#18335) 2017-07-26 09:40:33 -07:00
b582e2b311 docs(aio): update examples to 4.3 2017-07-25 15:32:38 -07:00
91ab39cc55 docs(aio) - Fixed link to the glossary dash-case term (#18311)
PR Close #18311
2017-07-25 15:59:28 -05:00
38ec05f533 fix(compiler): add equiv & disp attributes to Xliff2 ICU placeholders (#18283)
Fixes #17344

PR Close #18283
2017-07-25 15:58:53 -05:00
b3085e96c2 feat(compiler): add representation of placeholders to xliff & xmb
Closes #17345
2017-07-25 15:58:53 -05:00
4cea2bd612 docs(platform-server): inline PlatformOptions and add doc strings (#18264)
Fix documentation for the options passed into renderModule and
renderModuleFactory.

PR Close #18264
2017-07-25 15:58:13 -05:00
ce47546188 refactor(compiler-cli): add support for browser compiler bundle (#17979)
PR Close #17979
2017-07-25 15:51:46 -05:00
6279e50d78 perf(core): use native addEventListener for faster rendering. (#18107)
Angular can make many assumptions about its event handlers. As a result
the bookkeeping for native addEventListener is significantly cheaper
than Zone's addEventLister which can't make such assumptions.

This change bypasses the Zone's addEventListener if present and always
uses the native addEventHandler. As a result registering event listeners
is about 3 times faster.

PR Close #18107
2017-07-25 15:35:44 -05:00
8bcb268140 ci: use chrome stable (#18307) 2017-07-25 11:18:24 -07:00
6fc5940959 build: Bazel builds ngfactories for packages/core (#18289)
PR Close #18289
2017-07-21 18:09:47 -05:00
0317c4c478 build: update bazel rules to latest (#18289) 2017-07-21 18:09:44 -05:00
b7a6f52d59 perf: latest tsickle to tree shake: abstract class methods & interfaces (#18236)
In previous version of tsickle abstract class methods were materialized.
The change resulted in 6Kb savings in angular.io bundle.

This change also required the removal of `@private` and `@return` type
annotation as it is explicitly dissalowed by tsickle.

NOTE: removed casts in front of `makeDecorator` due to:
https://github.com/angular/devkit/issues/45

```
 14938 Jul 19 13:16 0.b19e913fbdd6507d346b.chunk.js
  1535 Jul 19 13:16 inline.d8e019ea3cfdd86c2bd0.bundle.js
589178 Jul 19 13:16 main.54c97bcb6f254776b678.bundle.js
 34333 Jul 19 13:16 polyfills.4a3c9ca9481d53803157.bundle.js

 14938 Jul 18 16:55 0.b19e913fbdd6507d346b.chunk.js
  1535 Jul 18 16:55 inline.0c83abb44fad9a2768a7.bundle.js
582786 Jul 18 16:55 main.ea290db71b051813e156.bundle.js
 34333 Jul 18 16:55 polyfills.4a3c9ca9481d53803157.bundle.js

main savings: 589178 - 582786 = 6,392
```

PR Close #18236
2017-07-21 16:35:37 -05:00
7ae7573bc8 fix(core): invoke error handler outside of the Angular Zone (#18269)
In Node.JS console.log/error/warn functions actually resuls in a socket
write which in turn is considered by Zone.js as an async task.

This means that if there is any exception during change detection in a platform-server
application the error handler will make the Angular Zone unstable which
in turn will cause change detection to run on next tick and cause an
infinite loop.

It is also better to run the error handler outside of the Angular Zone
in general on all platforms so that an error in the error handler itself doesn't cause an
infinite loop.

Fixes #17073, #7774.

PR Close #18269
2017-07-21 16:35:23 -05:00
abee785821 refactor(tsc-wrapped): update tsc-wrapped to pass strictNullCheck (#18160)
PR Close #18160
2017-07-21 12:26:20 -05:00
619e625ee2 refactor(tsc-wrapped): move tsc-wrapped to the packages directory (#18160) 2017-07-21 12:26:16 -05:00
a6c635e69e ci: force precise on Travis (#18282)
PR Close #18282
2017-07-21 12:20:27 -05:00
e0a9625e46 test: fix bad merge (#18267)
PR Close #18267
2017-07-21 11:53:34 -05:00
fd0cc01eed fix(animations): export BrowserModule as apart of BrowserAnimationsModule (#18263)
PR Close #18263
2017-07-20 17:47:49 -05:00
1bfc77bf8c docs(aio): pngcrush all pngs (#18243)
PR Close #18243
2017-07-20 17:47:06 -05:00
a094769bca fix(platform-server): don't clobber parse5 properties when setting (#18237)
element properties.

Fixes #17050.

We now store all element properties in a separate 'properties' bag.

PR Close #18237
2017-07-20 17:46:37 -05:00
b4c98305da refactor(common): CleanUp HttpClient's imports (#18120)
PR Close #18120
2017-07-20 17:43:23 -05:00
a3a54299af fix(compiler): allow numbers for ICU message cases in lexer (#18095)
Closes #18095
Fixes #17799
2017-07-20 17:41:54 -05:00
15a3e2d307 ci(aio): fix aio payload script 2017-07-20 15:32:32 -07:00
54e0244954 Revert "docs: Remove unneeded file (#18106)"
This reverts commit 72fe45db2b.
2017-07-20 16:46:47 -05:00
43c33d5663 fix(upgrade): ensure downgraded components are created in the Angular zone (#18209)
PR Close #18209
2017-07-20 16:25:11 -05:00
6d7799fce9 test(upgrade): fail tests when there are AngularJS errors (#18209) 2017-07-20 16:25:07 -05:00
d31dc7b2b3 fix(upgrade): throw error if trying to get injector before setting (#18209)
Previously, `undefined` would be returned.
This change makes it easier to identify incorrect uses/bugs.
(Discussed in https://github.com/angular/angular/pull/18213#issuecomment-316191308.)
2017-07-20 16:24:45 -05:00
4cd4f7a208 aio: debounce search and delay index building (#18134)
* feat(aio): debounce search requests

* feat(aio): delay loading search worker and index
2017-07-20 09:51:40 -07:00
72fe45db2b docs: Remove unneeded file (#18106) 2017-07-20 09:50:17 -07:00
8d2819121b fix(aio): invalid formatting in architecure.md (#18159)
Introduced in e110a80caf (diff-9ac9c6a9277eea9856d75249a7c0a40aL127)
2017-07-20 09:48:02 -07:00
073e8ba2f2 docs(aio): replace old blog link (#18252)
Fixes #18233
* Docs(aio): Replaced old blog link Now with the Link to the new Angular.io Blog
* Removed double braces
2017-07-20 09:45:14 -07:00
5d1864fe68 docs: fixing invisible tag in README (#18245) 2017-07-20 09:43:42 -07:00
eaa843b55f ci: test bazel builds on travis (#18240) 2017-07-20 09:40:40 -07:00
c6cf678a07 docs(aio): Fixed typo with closing div 2017-07-20 09:38:28 -07:00
31cb418370 docs(aio): Fix http guide 2017-07-20 09:35:27 -07:00
8de44cf5e3 docs: fix wrong link in CONTRIBUTING.md (#18228)
The link to the CONTRIBUTING.md file was not correct
2017-07-20 09:33:15 -07:00
c67bad4f43 docs(router): minor typo (#18226)
Fix a minor typo in the description of a router spec.
2017-07-20 09:32:34 -07:00
410f21c75c docs(aio): fix typo in NgModule FAQs (#18211)
Fix #18133. Fix typo 'added' to 'add' in the 'Why does lazy loading create a child injector' section.
2017-07-20 09:07:30 -07:00
54ea5b6ffd docs: add changelog for 5.0.0-beta.0 2017-07-19 13:16:43 -07:00
0af03beaed release: cut the 5.0.0-beta.0 release 2017-07-19 13:12:50 -07:00
d71fa734f5 docs: add changelog for 4.3.1 2017-07-19 12:55:08 -07:00
6f45519d6f feat(animations): support :increment and :decrement transition aliases 2017-07-19 11:24:00 -07:00
65c9e13105 fix(compiler-cli): don't generate empty <target/> when extracting xliff
Fixes #15754
2017-07-19 09:45:52 -07:00
9208f0beea docs(aio): fix typo in Router documentation
Fix title and link to RouteConfigLoadEnd documentation
2017-07-19 15:01:50 +01:00
5344be5182 fix(animations): make sure @.disabled works in non-animation components
Note 4.3 only!

Prior to this fix when [@.disabled] was used in a component that
contained zero animation code it wouldn't register properly because the
renderer associated with that component was not an animation renderer.
This patch ensures that it gets registered even when there are no
animations set.
2017-07-18 16:37:04 -07:00
5db6f38b73 fix(animations): do not crash animations if a nested component fires CD during CD
Closes #18193
2017-07-18 15:22:43 -07:00
d22f8f54db fix(animations): always camelcase style property names that contain auto styles
Closes #17938
2017-07-18 15:22:30 -07:00
23146c9201 fix(animations): capture cancelled animation styles within grouped animations
Closes #17170
2017-07-18 15:22:10 -07:00
a5205c686e fix(upgrade): allow accessing AngularJS injector from downgraded module 2017-07-18 14:00:19 -07:00
807648251f fix(platform-server): provide XhrFactory for HttpClient 2017-07-18 13:59:26 -07:00
5c62e300e1 fix(common): send flushed body as error instead of null
fix #18181
2017-07-18 10:57:51 -07:00
256bc8acdd docs(http): Make name of injected HttpTestingController consistent 2017-07-18 10:35:56 -07:00
59c23c7bd7 feat(upgrade): propagate touched state of NgModelController 2017-07-18 10:35:35 -07:00
e03adb9edd docs(platform-server): add doc string for PlatformOptions 2017-07-18 10:34:57 -07:00
b399cb26d9 fix(router): terminal route in custom matcher 2017-07-18 10:25:18 -07:00
3b588fe2b0 docs: fix typo 2017-07-18 10:11:55 -07:00
95635c18c7 fix(compiler): ensure jit external id arguments names are unique
Fixes: #17558, #17378, #8676
2017-07-18 10:11:32 -07:00
e20cfe1bbc fix(router): canDeactivate guards should run from bottom to top
Closes #15657.
2017-07-18 10:04:39 -07:00
eb6fb5f87e fix(router): should navigate to the same url when config changes
Closes #15535
2017-07-18 10:04:11 -07:00
ad3029e786 fix(router): should run resolvers for the same route concurrently
Fixes #14279
2017-07-18 10:03:33 -07:00
2a2fe11e8d docs(aio): fix HttpClient setting new header sample 2017-07-18 10:02:27 -07:00
7d0f2cd51e fix(aio): remove title attribute from CodeExampleComponent
This was causing browser to add an unwanted tooltip that appeared
when the user hovers over the code.

See #17524
2017-07-18 17:55:28 +01:00
36faba1aab fix(aio): add quote to module 2017-07-18 17:48:04 +01:00
92179bcc64 fix(aio): do not wrap <code-tabs> tab labels
Fixes #17751
2017-07-18 17:43:59 +01:00
cdb069ab0e docs(aio): fix cheatsheet layout for narrow screens
* Tell the app that this will have no Table of Contents, since we have no
h2 headings anyway.
* Remove all the `nbsp;` from the code since that doesn't help with layout
* Remove side padding from sidenav-content when screen is narrow
* Restyle the cheatsheet table when the screen is narrow
2017-07-18 17:32:43 +01:00
c453b7bcfa build(aio): fail doc-gen if referenced images are missing 2017-07-18 11:45:05 +01:00
9d97163c64 docs(aio): fix broken image sources 2017-07-18 11:45:05 +01:00
f054c8360b docs(aio): fix up broken links 2017-07-18 11:45:05 +01:00
758848961e build(aio): abort doc-gen on dangling links 2017-07-18 11:45:05 +01:00
99b666614d build(aio): abort doc-gen if an example is missing
Closes #16936
2017-07-18 11:45:05 +01:00
3f331b53b2 docs(aio): update jelbourn photo 2017-07-17 14:51:11 -07:00
375d598a9f docs(aio): add George's fixes 2017-07-17 14:49:19 -07:00
cd67fced1c docs(aio): add npm install to Sublime instructions 2017-07-17 14:49:19 -07:00
a77cf7ee37 docs(aio): add Chuck's comments 2017-07-17 14:49:19 -07:00
2150b45954 docs(aio): add language service doc 2017-07-17 14:49:19 -07:00
9f99f4fae2 docs: fix HttpClient sample 2017-07-17 14:03:32 -07:00
c6ad212a98 docs(aio): fix HttpClient's interceptor sample 2017-07-17 14:03:16 -07:00
47b3ecd9a3 docs(http): fix "Expecting and answering requests" example mistake
Possibly overlooked testing documentation mistake fixed:
- `http.get('/data')` probably ought to be paired with `httpMock.expectOne('/data')` instead of `httpMock.expectOne('/data')`.
2017-07-17 14:01:44 -07:00
8c81c62d46 docs: fix HttpClient logging's sample 2017-07-17 14:01:24 -07:00
7e72317059 docs(http): fixed syntax error in AuthInterceptor example 2017-07-17 14:00:59 -07:00
0bb8423df9 ci: add GK and PBD to aio content and marketing groups 2017-07-17 10:52:34 -07:00
95698d93ad fix(aio): remove title from callout 2017-07-15 15:35:15 +01:00
c649da9f0a fix(aio): remove unused news.html file
Although outdated and not used, the file would be picked up and showed in search
results.
2017-07-15 15:29:20 +01:00
0bf0c35bca build(aio): render type parameters of API function exports
Fixes #18123
2017-07-15 08:52:35 +01:00
97e6901ded Revert "revert: revert: ci(aio): exclude changes in aio/content folder"
This reverts commit 3d85f72652.

Still causing repeated flakes on master.
2017-07-14 14:54:31 -07:00
30e76fcd80 feat(upgrade): support lazy-loading Angular module into AngularJS app 2017-07-14 14:10:30 -07:00
44b50427d9 refactor(upgrade): clean up some types 2017-07-14 14:10:30 -07:00
a0b06befb6 docs: add changelog for 4.3.0 2017-07-14 13:12:37 -07:00
4fbb5b29ea release: cut the 4.3.0 release 2017-07-14 12:50:40 -07:00
e0fa727594 fix(aio): document XSRF protection in HttpClient 2017-07-14 12:42:37 -07:00
3ecc5e5398 fix(common): rename HttpXsrfModule to HttpClientXsrfModule 2017-07-14 12:40:52 -07:00
f7686d4124 Revert "fix(animations): make sure @.disabled works in non-animation components"
This reverts commit 01a2688848.
2017-07-14 11:00:29 -07:00
01a2688848 fix(animations): make sure @.disabled works in non-animation components
Note 4.3 only!

Prior to this fix when [@.disabled] was used in a component that
contained zero animation code it wouldn't register properly because the
renderer associated with that component was not an animation renderer.
This patch ensures that it gets registered even when there are no
animations set.
2017-07-14 10:34:58 -07:00
8e56c3cb30 docs: fix type error in HttpClient sample 2017-07-14 09:35:25 -07:00
7955cacec4 build(aio): turn on debugging of the payload.sh script to debug ci failures 2017-07-13 22:50:05 -07:00
dd04f09483 feat(common): on-by-default XSRF support in HttpClient (#18108)
Fixes #18100
2017-07-13 17:22:02 -07:00
3d85f72652 revert: revert: ci(aio): exclude changes in aio/content folder
This reverts commit 72143e80da.

it seems that the previous build failure was a travis flake.

reverting the revert...
2017-07-13 16:22:57 -07:00
9f28e838d3 fix(common): expose reportProgress option on HttpClient API (#18083) 2017-07-13 16:19:52 -07:00
ddb766e456 feat(compiler): do not evaluate metadata expressions that can use references (#18001) 2017-07-13 16:16:56 -07:00
72143e80da revert: ci(aio): exclude changes in aio/content folder
This reverts commit 1bf7ba87a0.

The commit caused master builds to fail with:

```
$ scripts/payload.sh
error Command failed with exit code 1.
error Command failed with exit code 1.
```
https://travis-ci.org/angular/angular/jobs/253405260#L1024
2017-07-13 15:32:44 -07:00
bc1ea8c54b docs: rewrite 'Server Communication' doc guide to use HttpClient (#18079) 2017-07-13 15:22:07 -07:00
45ffe54ae4 fix(tsc-wrapped): support as and class expressions (#16904) 2017-07-13 15:17:16 -07:00
1bf7ba87a0 ci(aio): exclude changes in aio/content folder 2017-07-13 15:13:37 -07:00
db96c963a8 fix(aio): add <hr> in between overloads 2017-07-13 14:58:33 -07:00
18559897a0 fix(common): document HttpClient, fixing a few other issues 2017-07-13 14:58:33 -07:00
ce0f4f0d7c fix(tsc-wrapped): report errors for invalid ast forms (#17994)
Fixes: #17993
2017-07-13 14:55:59 -07:00
4f1e4ffa4e fix(common): don't guess Content-Type for FormData bodies (#18104)
Fixes #18096
2017-07-13 12:28:20 -07:00
f0beb4d750 docs(aio): add Dart as a language option (#18080) 2017-07-13 12:21:28 -07:00
bc3b2ac251 docs(aio): Updated resources to UI Components (#18067)
With https:// the site gives console errors, because it loads fonts with http://
2017-07-13 12:19:31 -07:00
ffda3e41e0 build(aio): simplify the primary overload's anchor
The primary link to a member should simply be its name, and not require
the parameter list.
2017-07-13 12:16:20 -07:00
a301dba68f build(aio): improve API docs layout and styling 2017-07-13 12:16:20 -07:00
04f3a4a7a5 build(aio): fix githubLinks line numbers
The API docs have changed. The line numbers are now found in
`startingLine` and `endingLine` properties rather than the `location`
property, which moved into the `fileInfo` property anyway.
2017-07-13 12:16:20 -07:00
f06ce9adc8 build(aio): truncate API overview parameters at one line 2017-07-13 12:16:20 -07:00
660eec4a23 build(aio): upgrade to dgeni-packages 0.20.0-rc.5
This fixes unwanted extra overloads in interfaces and abstract classes
This provides the overload grouping support and fixes API doc line numbers
2017-07-13 12:16:20 -07:00
be3352a084 build(aio): render grouped overloads 2017-07-13 12:16:20 -07:00
998049ec9b build(aio): truncate API overview parameters at one line 2017-07-13 12:16:20 -07:00
a7ea0086ee ci: fast-forward to master/HEAD on all circle jobs (#18101)
This problem was introduced by a bad merge.
2017-07-13 12:16:02 -07:00
edb8375a5f docs: adjust the release schedule with the 4.3.0-rc.0 release date 2017-07-11 15:52:06 -07:00
26b9492315 docs(aio): Added resource link to amexio-ng-extensions [duplicate of #17785] (#17790) 2017-07-11 15:06:14 -07:00
e110a80caf docs(aio): change Angular Module to NgModule (#16964) 2017-07-11 15:05:20 -07:00
20127c1456 docs: fix typos and grammar (#17818) 2017-07-11 13:00:53 -07:00
a50d935a48 docs(benchpress): fix android broken links (#17568) 2017-07-11 12:54:34 -07:00
7c479f073e docs: document using Bazel (#17701) 2017-07-11 12:48:50 -07:00
bbf2133fa9 docs: update ECMAScript info (#17801) 2017-07-11 12:44:56 -07:00
4300439ab2 docs: improve grammar and wording on CONTRIBUTING.md (#17855) 2017-07-11 12:40:30 -07:00
ec14679668 docs: add @angular/animations to list (#17488)
@angular/animations was missing from the SemVer supported API surface list, now added.
2017-07-11 12:30:01 -07:00
df06e8b7a4 fix(router): fix outdated homepage url in NPM package (#17899) 2017-07-11 12:00:56 -07:00
1b1a6ba0bb docs: fix 404 link to SFEIR website (#17913)
Also, I moved SFEIR training description to the "onsite" section since SFEIR doesn't deliver online training.
2017-07-11 12:00:27 -07:00
876ca9ee3a docs(aio): fix host usage in styleguide (#17932) 2017-07-11 11:58:26 -07:00
d9b03be08f test: git ignore e2e test folders (#17984) 2017-07-11 11:55:24 -07:00
b6aad07634 build: remove duplicate dependency on TypeScript
TypeScript was added as a dependency in 9a7f5d580. I assume the intention was to
upgrade the existing devDependency instead.
2017-07-11 11:53:43 -07:00
3d0406c247 docs: update required Node/npm versions in DEVELOPER.md 2017-07-11 11:53:43 -07:00
db3bcc939e docs: fix CHANGELOG typo (#18026) 2017-07-11 11:51:46 -07:00
4d45fe6fb5 fix(aio): fix tab animations on CodeTabsComponent
In our attempt to remove the material ripple effect from tab labels, we were
killing all `transform`-based animations on other `md-tab-group` elements, such
as animating the content when entering/leaving. (This wasn't an issue on Chrome,
because it didn't respect our `!important` flag.)
This commit fixes it by properly hiding the ripple effect (using a feature
introduced in angular/material2@e4789c7b8) and allowing other animations to
execute normally.

Fixes #17998
2017-07-11 11:49:22 -07:00
076ea2281f test(aio): add tests for CodeTabsComponent 2017-07-11 11:49:22 -07:00
aec39c28d8 refactor(aio): remove unnecessary comments/imports, improve indentation 2017-07-11 11:49:22 -07:00
b9525ece77 docs: Unbreak README Formatting (#18043)
- Makes headers consistent
 - Removes extra `)`
 - Makes browserstack line on a separate line
2017-07-11 11:48:37 -07:00
719101338a docs: does please -> does (#18044)
Fixes grammar issue.
2017-07-11 11:46:57 -07:00
e131f6bbe8 docs: various minor formatting changes (#18046) 2017-07-11 11:46:34 -07:00
a9757ec674 docs: Fix code example in the ChangeDetectorRef docs (#18051)
Minor example fix: mark the 'ref' constructor parameter as a private property
2017-07-11 11:45:32 -07:00
9003770f02 docs: fix style in helper text (#18055) 2017-07-11 11:43:52 -07:00
e8bbf86e66 build(aio): add terms from heading to the search index 2017-07-11 15:04:37 +01:00
3a0886dc12 build(aio): capture all the headings from a doc in the vFile.headings property 2017-07-11 15:04:37 +01:00
062a7aa2cf build(aio): revert to general purpose search algorithm
Now that we have upgraded to the latest lunr search engine, the results
from the standard `search` method are more appropriate.
So we do not need to create our own special queries to get good results.
2017-07-11 15:04:37 +01:00
e28f097fc2 ci: add aio/content to aio group
This commit also simplifies the rules for the aio content and marketing groups
2017-07-11 11:55:09 +01:00
b30c5fc874 docs(aio): fix dangling links 2017-07-11 11:44:05 +01:00
d52ab8e2c9 build(aio): fix matchUpDirectiveDecorators processor 2017-07-11 11:44:05 +01:00
df7b875f6c build(aio): fix mergeDecoratorDocs processor 2017-07-11 11:44:05 +01:00
0e71836cd5 docs(aio): fix potential invalid jsdoc tag 2017-07-11 11:44:05 +01:00
470a7c6bcd build(aio): use dedicated overview for decorator API docs 2017-07-11 11:44:05 +01:00
3abf208235 build(aio): update doc-gen templates 2017-07-11 11:44:05 +01:00
92c18d167e build(aio): fix mergeDecoratorDocs processor 2017-07-11 11:44:05 +01:00
99b38f52cb build(aio): fix matchUpDirectiveDecorators processor 2017-07-11 11:44:05 +01:00
633ec30291 build(aio): fix test description 2017-07-11 11:44:05 +01:00
57cfcb0830 build(aio): refactor filterMemberDocs to be more general
Now it also filters function-overloads, hence the name change.
2017-07-11 11:44:05 +01:00
433d479a1e build(aio): upgrade to dgeni-packages v0.20.0-rc.1 2017-07-11 11:44:05 +01:00
7c4ac68e66 build(aio): remove unused script and GitHub token
Since 808bd4af4, we are no longer pre-verifying PRs before uploading the build
artifacts to the preview server, thus we no longer need the
`travis-preverify-pr.sh` script or the `GITHUB_TEAM_MEMBERSHIP_CHECK_KEY`
variable.
2017-07-11 11:03:29 +01:00
20556346a3 feat(aio): add API endpoint for notifying about PR updates
This commit adds an API endpoint for notifying the preview server about PR
updates (`/pr-updated`). According to the update, the preview server can take
several actions. Currently, it will only check and (if necessary) update the
PR's preview visibility (but more actions could be supported in the future).
The API can be used with an automatic trigger (e.g. a GitHub webhook) to
instantly update a PR's preview visibility when it changes.

Fixes #16526
2017-07-10 12:30:45 +01:00
5a417b8514 feat(aio): implement a way to check and update a PR\'s preview visibility
Previously, `BuildCreator#changePrVisibility()` would throw an error if the PR's
visibility was already up-to-date or if the PR directory did not exist (e.g. was
removed). This method was only used from inside `BuildCreator#create()`, which
had already checked for the existence of the directories.

This commit renames `changePrVisibility()` to `updatePrVisibility()` and makes
it more "forgiving" (i.e. it will only throw if both public and non-public
directories exist). This allows it to be used on events that may or may not have
caused the PR's visibility to change (e.g. a GitHub webhook triggered whenever a
PR's labels change).
2017-07-10 12:30:45 +01:00
8cfc2e2ec0 refactor(aio): unify error messages for invalid requests to upload-server
Previously, there was a distinction between GET requests to invalid URLs and all
other requests. This was mainly because the upload-server only accepts GET
requests, but that is not a hard limitation and may change in the future.

Thus, it makes sense to return a 404 response for requests to invalid URLs
regardless of the method used.
2017-07-10 12:30:45 +01:00
11647e4c78 refactor(aio): use dedicated constants.ts file for e2e-specific constants 2017-07-10 12:30:45 +01:00
9e1b61326c refactor(aio): move script to a more relevant directory 2017-07-10 12:30:45 +01:00
cb7609109d fix(aio): fix text blurring when hovering over item in resources
Fixes #17907
2017-07-10 12:24:10 +01:00
b3d90365b6 fix(aio): add new common/http package into API list 2017-07-10 12:23:47 +01:00
cc611c93b6 docs: add changelog for 4.3.0-rc.0 2017-07-07 22:10:17 -07:00
8928a58796 release: cut the 4.3.0-rc.0 release 2017-07-07 22:07:13 -07:00
05a33d5035 docs: add changelog for 4.2.6 2017-07-07 22:03:53 -07:00
09f1609f81 style(http): fix linting error in http/testing (#18002) 2017-07-07 17:02:07 -07:00
c723d42d0a refactor: fix typos (#18000) 2017-07-07 16:55:17 -07:00
9dd550fa1e ci(aio): only deploy latest commits to staging/production (#17988)
Fixes #17941
2017-07-07 16:35:07 -07:00
abbac4bc69 feat(aio): use new ngo (#17977) 2017-07-07 16:34:47 -07:00
671a175dfb fix(compiler-cli): fix relative source paths on windows for extracted msg (#17915)
Fixes #16639
2017-07-07 16:33:40 -07:00
c1474f33be feat(compiler-cli): add parameters to ngc main needed by bazel rules (#17885) 2017-07-07 16:29:39 -07:00
0ede642cb9 docs(aio): Update NgStyle and NgClass to use quoted keys 2017-07-07 16:17:33 -07:00
9c1f6fd06f fix(compiler): emits quoted keys only iff they are quoted in the original template
fixes #14292
2017-07-07 16:17:33 -07:00
798947efa4 feat(compiler): adds support for quoted object keys in the parser 2017-07-07 16:17:33 -07:00
7ae8ad6aab fix(compiler): fix types 2017-07-07 16:17:33 -07:00
9c3386b1b7 fix(compiler): remove i18n markup even if no translations (#17999)
Fixes #11042
2017-07-07 16:16:49 -07:00
2ba3ada27f revert: "refactor(compiler-cli): remove the dependency on fs in codegen.ts (#17738)"
This reverts commit b116901400.
2017-07-07 16:16:25 -07:00
8e28382e4a feat(animations): support disabling animations for sub elements
Closes #16483
2017-07-07 14:58:40 -07:00
3203639d7d fix(animations): ensure :animating queries collect previous animation elements properly 2017-07-07 14:58:40 -07:00
f85b543cc1 fix(animations): properly detect state transition changes for object literals 2017-07-07 14:58:40 -07:00
c81ad9d19d feat(common): two missing features in HttpClient (#17996)
- Add params to HttpRequest API
- Add optional description to testing APIs
2017-07-07 14:56:36 -07:00
37797e2b4e feat(common): new HttpClient API
HttpClient is an evolution of the existing Angular HTTP API, which exists
alongside of it in a separate package, @angular/common/http. This structure
ensures that existing codebases can slowly migrate to the new API.

The new API improves significantly on the ergonomics and features of the legacy
API. A partial list of new features includes:

* Typed, synchronous response body access, including support for JSON body types
* JSON is an assumed default and no longer needs to be explicitly parsed
* Interceptors allow middleware logic to be inserted into the pipeline
* Immutable request/response objects
* Progress events for both request upload and response download
* Post-request verification & flush based testing framework
2017-07-07 12:09:32 -07:00
2a7ebbe982 refactor: move DOCUMENT from platform/browser to common 2017-07-07 12:09:32 -07:00
72747e5213 ci: disable sauce-connect logging for realz (#17995)
follow up on #17947
2017-07-07 11:59:40 -07:00
504500de50 fix(aio): activate ServiceWorker updates asap (#17699)
Previouly, whenever a new ServiceWorker update was detected the user was
prompted to update (with a notification). This turned out to be more distracting
than helpful. Also, one would get notifications on all open browser tabs/windows
and had to manually reload each one in order for the whole content (including
the app) to be updated.

This commit changes the update strategy as follows:
- Whenever a new update is detected, it is immediately activated (and all
  tabs/windows will be notified).
- Once an update is activated (regardless of whether the activation was
  initiated by the current tab/window or not), a flag will be set to do a
  "full page navigation" the next time the user navigates to a document.

Benefits:
- All tabs/windows are updated asap.
- The updates are applied authomatically, without the user's needing to do
  anything.
- The updates are applied in a way that:
  a. Ensures that the app and content versions are always compatible.
  b. Does not distract the user from their usual workflow.

NOTE:
The "full page navigation" may cause a flash (while the page is loading from
scratch), but this is expected to be minimal, since at that point almost all
necessary resources are cached by and served from the ServiceWorker.

Fixes #17539
2017-07-07 11:17:19 -07:00
e1174f3774 fix(aio): home & marketing styles cleanup (#17926) 2017-07-07 11:16:51 -07:00
6bae73c076 refactor(common): replace Object.assign with the spread operator (#17982)
`Object.assign` is not available in all supported browsers and one had to
provide a polyfill. This commit replaces `Object.assign` with the spread
operator (`...`), which TypeScript will transpile to ES5-compatible code.
2017-07-07 08:55:29 -07:00
11db3bd85e fix(upgrade): bring the dynamic version closer to the static one
(#17971)

This commit changes the dynamic version of ngUpgrade to use `UpgradeHelper`,
thus bringing its behavior (wrt upgraded components) much closer to
`upgrade/static`. Fixes/features include:

- Fix template compilation: Now takes place in the correct DOM context, instead
  of in a detached node (thus has access to required ancestors etc).
- Fix support for the `$onInit()` lifecycle hook.
- Fix single-slot transclusion (including optional transclusion and fallback
  content).
- Add support for multi-slot transclusion (inclusing optional slots and fallback
  content).
- Add support for binding required controllers to the directive's controller
  (and make the `require` behavior more consistent with AngularJS).
- Add support for pre-/post-linking functions.

(This also ports the fixes from #16627 to the dynamic version.)

Fixes #11044
2017-07-07 08:54:39 -07:00
0193be7c9b feat(upgrade): fix support for directive.link in upgraded components
(#17971)

Although, pre- and post-linking functions are correctly called during directive
linking, directives with `link.post` would throw an error. Interestingly, having
`link.pre` only or defining `link: fn` (which is an alias for `link.post: fn`)
would not throw.

This commit removes this check and allows directives with pre- and/or
post-linking functions to work.
2017-07-07 08:54:09 -07:00
2ea73513ea refactor(upgrade): move shareable functionality to UpgradeHelper class (#17971)
This functionality can be potentionally re-used by the dynamic version.
2017-07-07 08:53:16 -07:00
67e9c62013 fix(upgrade): fix transclusion on upgraded components (#17971)
Previously, only simple, single-slot transclusion worked on upgraded components.
This commit fixes/adds support for the following:

- Multi-slot transclusion.
- Using fallback content when no transclusion content is provided.
- Destroy unused scope (when using fallback content).

Fixes #13271
2017-07-07 08:52:46 -07:00
227dbbcfba fix(language-service): ignore hover of symbols not in the TypeScript program (#17969)
Fixes: #17965
2017-07-07 08:47:28 -07:00
cb16e9c747 fix(tsc-wrapped): emit exports metadata in flat modules (#17893)
Fixes: #17888
2017-07-07 08:46:42 -07:00
3b2d2c467a fix(language-service): do not crash when hovering over a label definitions (#17974)
Fixes: #17972
2017-07-07 08:46:18 -07:00
ae27af7399 fix(router): encode URLs the same way AngularJS did (closer to spec) (#17890)
fixes #16067
2017-07-06 17:10:25 -07:00
c69fff15c9 fix(core): fix re-insertions in the iterable differ (#17891)
fixes #17852
2017-07-06 12:11:47 -07:00
dd7c1134e3 feat(compiler): update the schema by extracting from latest chrome (#17858)
adds:
- the slot element,
- the slot attribute on [Element],
- a few other attributes.
2017-07-06 12:11:34 -07:00
b116901400 refactor(compiler-cli): remove the dependency on fs in codegen.ts (#17738) 2017-07-06 12:11:21 -07:00
70981c601e feat(aio): serve-and-sync command (#17850) 2017-07-06 11:51:58 -07:00
f2f61c9cf0 ci: allow chuck to approve animations (#17940) 2017-07-06 11:49:44 -07:00
1bb2476804 docs(aio): add instructions for example e2e tests to README (#17819) 2017-07-06 11:15:31 -07:00
ec58246a1b docs(aio): add DevExtreme to the UI Components section of resources.json (#17939) 2017-07-06 11:15:15 -07:00
6fc5174a13 build: disable sauce connect logging (#17947)
this change is expected to mitigate the flakes on CI that occur when
we cat the log in the print-logs.sh
2017-07-06 11:00:00 -07:00
105e920b69 fix(animations): properly handle cancelled animation style application 2017-07-06 10:18:12 -07:00
858dea98e5 fix(animations): properly cleanup query artificats when animation construction fails 2017-07-06 10:18:12 -07:00
71ee0c5b03 ci(aio): raise polyfill payload limits 2017-07-06 14:46:53 +01:00
578bdeb522 fix(router): export missing UrlMatcher and UrlMatchResult types
Fixes #15140
2017-07-05 15:26:27 -07:00
6282a86135 ci: add angular.io content related groups to .pullapprove.yaml 2017-07-05 15:26:27 -07:00
e9b67243ed build(aio): upgrade to angular@4.2.4 and zone.js 2017-07-05 15:26:27 -07:00
fa1c187abc build: upgrade yarn to 0.24.6 2017-07-05 15:26:27 -07:00
b51697c197 ci: add Chuck to the owners of core (#17892) 2017-07-05 15:10:11 -07:00
c6b75b0823 docs: add animations package to CONTRIBUTING.md (#15413) 2017-07-05 14:34:13 -07:00
Joe
0d7e1a9b4e docs(changelog): Correct typos (#16966) 2017-07-05 14:33:47 -07:00
9d15d85391 docs: README - use correctly encoded link for npm badge (#17707)
closes #14990, an old PR that was approved but to old/difficult to merge
2017-07-05 14:33:25 -07:00
92d7ecf627 docs: shefali edits to Authors Style Guide (#17853) 2017-07-05 14:33:06 -07:00
9263da570f ci(aio): Fix the payload script only check for changes in aio/scripts 2017-07-03 10:40:19 +01:00
dc88e0a881 docs(aio): update quickstart to latest cli 2017-07-03 08:25:19 +01:00
fa34ed8bf3 ci(aio): use valid database path for Firebase payload size upload
Firebase does not allow `.` in the path, so when trying to upload payload size
data for branches like `4.2.x`, the following error is thrown:

```
HTTP Error: 400, Invalid path: Invalid token in path
```

This commit fixes it by replacing `.` with `_` in branch names.
2017-07-03 08:19:59 +01:00
f54a901b8d docs(aio): fix visual-studio-2015 typo
This fixes #17283
2017-07-03 08:12:28 +01:00
8a1a989a1c feat(router): add router-level events for GuardsCheck and Resolve (#17601) 2017-07-01 10:30:17 -07:00
b479ed9407 perf(core): refactor NgZone, decrease size by 1.2Kb (#17773)
- Remove getters
- Hide private methods for better property renaming

```
497893 May 31 11:26 core.umd.js
718073 May 31 11:26 core.umd.js.map
217108 May 31 11:26 core.umd.min.js
575092 May 31 11:26 core.umd.min.js.map
```

```
495594 May 31 11:28 core.umd.js
716943 May 31 11:28 core.umd.js.map
215826 May 31 11:28 core.umd.min.js
574401 May 31 11:28 core.umd.min.js.map
```
diff: 1,282
2017-07-01 10:29:56 -07:00
d5dc53ead8 ci(aio): avoid printing too large PWA report (and restore print-logs.sh)
There have been some issues lately with Travis jobs failing due to
`print-log.sh`. This is likely due to trying to print the Lighthouse PWA report,
which is too large.
This commit stops printing that report (since it was rarely used and is pretty
easy to acquire when needed) and restores the `print-logs.sh` script (that was
temporarily removed with dfcca66fd).
2017-07-01 14:40:36 +01:00
8e00161601 docs(aio): Add resources contributor info 2017-07-01 14:38:26 +01:00
01d4eae984 ci(aio): use correct URL for preview in PWA score test
In 4268c8289, the preview URLs were changed to not use the whole SHA, but just
the 7 first characters.
2017-06-30 13:02:28 +01:00
154154dde2 docs(aio): fix quickstart-cli bad HTML escape chars 2017-06-30 12:23:11 +01:00
4459e0c1c8 fix(aio): restore visible sidenav keyboard focus; shift TOC up
closes #17665
Restores keyboard focus that was removed by commit b8b91d3.
Raises the right-TOC by 20px (96px->76px) because was too far down.

To prevent keyboard focus on hidden child nodes,
also collapses inner expanded nodes when parent node is collapsed.
The implicit parent node of top nodes is always expanded.
2017-06-30 11:58:10 +01:00
b052ef5f1e fix(aio): do not include hidden content in window title
The window title is derived based on the current document's `<h1>` heading. Such
headings may contain hidden/non-visible content (e.g. textual name of font
ligatures: `<i class="material-icons">link</i>`) that should not be included in
the title.

This commit fixes this by using `innerText` (instead of `textContent`) to
extract the visible text from the `<h1>` heading. It will still fall back to
`textContent` on browsers that do not support `innerText` (e.g. Firefox 44).

Fixes #17732
2017-06-30 11:54:01 +01:00
40921bb927 docs: add changelog for 4.2.5 2017-06-29 17:19:57 -07:00
dfcca66fdc ci: remove print-logs.sh to try to fix failing builds on Travis 2017-06-29 14:54:34 -07:00
1ac9085b0a docs: add changelog for 4.3.0-beta.1 2017-06-29 14:41:40 -07:00
1cfe67dac4 release: cut the 4.3.0-beta.1 release 2017-06-29 14:38:52 -07:00
8d01db4638 docs: Updated router guide content and examples for paramMap
and queryParamMap, tracing, and incidental improvements.
closes #16991 and #16259 which it also fixes.
2017-06-29 09:35:39 -07:00
4268c82898 feat(aio): use shorter URLs for previews
Use the 7 first characters of the 40-chars long SHAs for shorter/cleaner URLs.
The collision probability is extremely low (since all SHAs are further
"namespaced" under the corresponding PR). In case of a collision, the second PR
will not be deployed, in order to avoid overwriting the original build.

(This is a design decision to keep the implementation simple. It can be changed
later if necessary.)
2017-06-29 09:35:22 -07:00
3c4eef99be fix(aio): clean up non-public previews
The previous clean-up code for PR directories on the preview server assumed that
all directories were named after the PR number. With the changes introduced
in #17640 it is possible to have PR directories that do not follow that naming
convention (e.g. "non-public" directories).

This PR ensures that both public and non-public directories are removed when
cleaning up.
2017-06-29 09:35:22 -07:00
96b17034e1 feat(aio): cross reference readme and docs styleguide 2017-06-29 09:35:04 -07:00
e47a77f941 docs(aio): update punctuation mark added in TOOLS guide 2017-06-29 09:33:12 -07:00
af14b1e384 test(animations): add proper tests to check multiple combinations of :enter/:leave animation queries 2017-06-28 18:18:10 -07:00
40f77cb563 fix(animations): properly collect :enter nodes that exist within multi-level DOM trees
Closes #17632
2017-06-28 18:18:10 -07:00
6c1a8daafc docs: correct grammar in CONTRIBUTING.md 2017-06-27 16:23:57 -07:00
d699c354db fix(animations): do not remove container nodes when children are queried by a parent animation
Closes #17746
2017-06-27 11:00:17 -07:00
34f3832af9 fix(animations): do not delay style() values before a stagger() runs
Closes #17412
2017-06-27 11:00:06 -07:00
f1626574dd fix(aio): build scripts-js before creating a new docker image for the preview server
When creating a new docker image for the preview server, the TypeScript source
code in `scripts-js/` is not copied over. Instead only the generated JavaScript
core in `scripts-js/dist/` are. Because of that, it is necessary to have run
`yarn build` before running `docker build`, so that the new docker image
contains the latest changes in `scripts-js/`.

This was previously part of the `create-image.sh` script, but was accidentally
removed in 21d213dfc.
2017-06-27 10:11:06 -07:00
68fc65dbcb ci: add npm postinstall back to the lint step so node_modules doesn't get out of date 2017-06-26 17:12:06 -07:00
693f79e88a docs: fix spelling of case variants in naming.md
unify variant forms of spelling letter cases (upper-case, uppercase, lower case)
2017-06-26 13:43:37 -07:00
448d9f9f46 test(compiler): add a test for components not part of any NgModule 2017-06-26 13:24:36 -07:00
8786ba95fb build(aio): boilerplate wont be removed by default now 2017-06-26 11:05:34 -07:00
97bb374218 fix(aio): Typo in Setup Anatomy documentation page
Solves #17076
2017-06-26 11:04:46 -07:00
233044e337 docs(aio): fix typo 2017-06-26 11:04:32 -07:00
f365a0f45c docs(aio): animations typos fixed 2017-06-26 11:04:13 -07:00
263c1a1d7e refactor: remove unused imports of the deprecated Renderer 2017-06-23 17:24:40 -07:00
3097083277 feat(compiler-cli): new compiler api and command-line using TypeScript transformers 2017-06-23 16:18:44 -07:00
43c187b624 refactor(forms): remove usage of deprecated Renderer 2017-06-23 16:12:52 -07:00
3165fd3dc9 refactor(router): remove usage of deprecated Renderer 2017-06-23 16:12:43 -07:00
e80851d98b fix(core): add needed closure compiler warning suppression 2017-06-23 14:44:00 -07:00
b754e600e3 feat(compiler): add support ::ng-deep
- /deep/ is deprecated and being removed from Chrome
- >>> is semantically invalid in a stylesheet
- sass will no longer support either in any version of sass

-> use ::ng-deep in emulated shadow DOM mode

Because the deep combinator is deprecated in the CSS spec,
`/deep/`, `>>>` and `::ng-deep` are also deprecated in emulated shadow DOM mode
and will be removed in the future.

see https://www.chromestatus.com/features/6750456638341120
2017-06-23 14:43:42 -07:00
81734cf7b6 refactor(compiler): add tracking comment for self-import detection
The underlying cause of #17386 needs to be investigated and as a
simple string compare should have been valid.
2017-06-23 14:43:24 -07:00
30f4fe26e0 docs(aio): cleanup rollup-config script 2017-06-23 11:55:55 -07:00
d6265dfcbe ci(aio): Change the firebase token 2017-06-23 11:55:20 -07:00
d51f86291f ci(aio): address comments 2017-06-23 11:55:20 -07:00
97ace57d39 ci(aio): fix test 2017-06-23 11:55:20 -07:00
86949e0c20 ci(aio): remove umd 2017-06-23 11:55:20 -07:00
6924780ae9 ci(aio): debug 2017-06-23 11:55:20 -07:00
1b0b69eeec ci(aio): add back deploy-preview 2017-06-23 11:55:20 -07:00
fa85389f62 ci(aio): updated limits 2017-06-23 11:55:20 -07:00
2e55857c82 ci(aio): rename limits file and address comments 2017-06-23 11:55:20 -07:00
ca970f5ee5 ci(aio): Also track umd.min.js file size 2017-06-23 11:55:20 -07:00
204a2cf942 ci(aio): Add payload size limit file 2017-06-23 11:55:20 -07:00
0440251919 ci(aio): upload aio payload size to firebase
ci(aio): Add timestamp and change data
2017-06-23 11:55:20 -07:00
08ecfd891d build: No longer need to bazel build twice 2017-06-23 11:54:51 -07:00
7395a64668 build: circleci workflows to run lint in parallel 2017-06-23 11:54:51 -07:00
979bfd07e1 refactor(aio): provide fallback values for secrets (useful during dev) 2017-06-23 11:54:20 -07:00
b6ce814279 refactor(aio): enable -u flag on preview server scripts 2017-06-23 11:54:20 -07:00
66088fef1a docs(aio): document preview server HTTP status codes 2017-06-23 11:54:20 -07:00
808bd4af41 ci(aio): deploy previews for all PRs
PRs that could not be automatically verified will not be publicly accessible,
until manually verified.
2017-06-23 11:54:20 -07:00
f90b35a85e test(aio): add e2e tests for non-public previews 2017-06-23 11:54:20 -07:00
8ae0eec230 feat(aio): enable previews for any PR
This commit introduces the ability to show previews for PRs by any author. It works as follows:

- The build artifacts of all PRs are uploaded to the preview server.
- Automatically verified PRs (i.e. from trusted authors or having a specific label) are deployed and
  publicly accessible as usual.
- PRs that could not be automatically verified are stored for later use (after re-verification).
- A PR can be marked as "trusted" and make its preview publicly accessible by adding the GitHub
  label specified in the `AIO_TRUSTED_PR_LABEL` env var of the preview server.

At the moment, there is no automatic mechanism for notifying the preview server about changes to the
PR's verification status. The PR's "visibility" will be checked and updated every time a new build
is uploaded.
2017-06-23 11:54:20 -07:00
0fe685102f refactor(aio): simplify preview server build events 2017-06-23 11:54:20 -07:00
a98440bb85 test(aio): add missing unit test for preview server 2017-06-23 11:54:20 -07:00
3112311134 test(aio): fix preview server tests on Windows 2017-06-23 11:54:20 -07:00
1b13bdea4b build(aio): upgrade preview server dependencies 2017-06-23 11:54:20 -07:00
3ce9d51a9c fix(aio): prefix location.assign with window.
No practical effect but clears the TS compiler warning.
2017-06-23 11:53:53 -07:00
14d2de13bb fix(aio): fix links on /about in Firefox
Fixes #17661
2017-06-23 11:53:22 -07:00
5713e7c9b6 refactor(aio): clean up aio-contributor template and styles 2017-06-23 11:53:22 -07:00
87206e1986 fix(aio): preserve newlines when copying code
Before 4f37f8643, we were using `innerText` to retrieved the code content for
copying. This preserved the text layout (including newlines), but suffered from
other issues (browser support, performance). With 4f37f8643 we switched to
`textContent`, which works well except in the following case:
When `prettify` formats the code to have line numbers, it removes the newlines
and uses `<li>` elements instead. This affects `textContent`.

This commit fixes this by keeping a reference of the code as text and using that
for copying.

Fixes #17659
2017-06-23 11:53:01 -07:00
414c7e956b ci: test merge commits on circle
We expect this behavior because it's what Travis does. Also it's better because we want
to test what happens if we merge the PR, not the status of the PR branch.
2017-06-23 11:52:41 -07:00
6191d53a78 docs(aio): Update resources.json - fixed language error 2017-06-23 11:52:22 -07:00
7d30ccc4a9 docs(aio): update resources - Add Compodoc - documentation tool for Angular app 2017-06-23 11:52:22 -07:00
494a0d064a fix(aio): fix topbar nav-item focus style
Fixing it requires upgrading `@angular/material` to v2.0.0-beta.7.

Fixes #17216
2017-06-23 11:51:17 -07:00
849200b576 docs: remove unnecessary newline 2017-06-23 11:50:48 -07:00
60273a941f docs: fix “under to hood” typo in changelog.md
closes #14856
2017-06-23 09:04:40 -07:00
7ba720a62a docs(aio): resources.json - replace “Angular 2” in titles/descriptions
closes #16965
2017-06-23 09:04:21 -07:00
eacc36bbd5 docs(aio): update Bash for Windows info
As of the Creator's Update, Bash on Ubuntu on Windows supports all npm commands. 
https://blogs.msdn.microsoft.com/commandline/2017/04/11/windows-10-creators-update-whats-new-in-bashwsl-windows-console/
Update setup documentation accordingly.
2017-06-23 09:04:04 -07:00
8b4acbbcbf docs(aio): minor fixes for the upgrade guide
Fixes #17093
2017-06-23 09:03:28 -07:00
b1fe63d081 fix(aio): add missing WeakMap polyfill 2017-06-23 09:03:07 -07:00
f2ee1dcdb7 fix(animations): do not validate style overlap errors in different transitions 2017-06-23 09:02:37 -07:00
21018af2bf docs(aio): replace “Angular 1” in upgrade phonecat script 2017-06-22 14:57:02 -07:00
67ffbae6f9 docs(aio): fix promises link in toh-pt4
closes #16050
2017-06-22 14:56:41 -07:00
5dd5bfde72 docs(aio): remove "_" from private property name
follow best practices from the documentation: https://angular.io/guide/styleguide#properties-and-methods
2017-06-22 14:55:42 -07:00
400486ced7 docs(aio): correct typos in Tour of Heroes HeroSearchService 2017-06-22 14:55:17 -07:00
1a947e4b75 docs(aio): Added resources to UI Components (#17635)
docs(aio): Added resources to UI Components
2017-06-22 14:52:58 -07:00
92bcfefc35 docs(aio): remove min, max & move built-in validators paragraph 2017-06-22 14:51:05 -07:00
133b5e6e36 docs(aio): added link to Russian Angular Courses 2017-06-22 14:46:24 -07:00
5c576d3b9d docs: add documentation for LTS versions 2017-06-22 14:45:36 -07:00
68b64a261a docs: add missing colon in the Constants section of NAMING.md 2017-06-22 14:40:33 -07:00
68f939ea8c docs: Fix Stack Overflow being repeatedly misspelt as a single word
(To see that it is two words, see e.g. the page title at https://stackoverflow.com/ or the
spelling at http://stackoverflow.com/tour)
2017-06-22 14:39:10 -07:00
8c129d73b8 docs: Fix some non-code being formatted as code in the router docs 2017-06-22 14:38:55 -07:00
1fc0d05565 docs: add changelog for 4.3.0-beta.0 2017-06-21 17:21:39 -07:00
20a04f9076 release: cut the 4.3.0-beta.0 release 2017-06-21 17:16:04 -07:00
02a38d3ea5 docs: add changelog for 4.2.4 2017-06-21 17:11:19 -07:00
37cdc4f759 docs(aio): ward's changes 2017-06-21 16:21:20 -07:00
b1dab181e0 docs(aio): change to overall prose 2017-06-21 16:21:20 -07:00
fd6c4e371b docs(aio): create author style guide 2017-06-21 16:21:20 -07:00
4352dd27c4 fix(compiler): avoid emitting self importing factories
Fixes: #17389
2017-06-21 16:19:43 -07:00
34cc3f2982 feat(aio): display “Searching ..." while building search index
closes #15923
2017-06-21 14:31:32 -07:00
97fd2480e7 fix(aio): fix patch script on windows
Running the patch script on Windows (with `patch` available) yields an invalid syntax warning, and does not apply patches.

```
kamik@T460p MINGW64 /d/work/angular/aio (master)
$ yarn postinstall
yarn postinstall v0.24.6
$ node tools/cli-patches/patch.js && uglifyjs node_modules/lunr/lunr.js -c -m -o src/assets/js/lunr.min.js --source-map
The syntax of the command is incorrect.
Done in 1.52s.
```
2017-06-21 13:51:35 -07:00
1d93cf2e85 build(aio): ensure all doc tests are run
It is not possible to run all the docs tests directly via the  jasmine CLI.
Instead we now have a small script that will run jasmine via its library.
2017-06-21 13:51:15 -07:00
3fb98fe4ea build(aio): do not recurse node_modules when loading examples in doc-gen 2017-06-21 13:51:15 -07:00
bb804dd3e9 feat(aio): select contributor group with URL “about?group=gde”
closes #17656
also adds test for ContributorListComponent.
2017-06-21 13:50:57 -07:00
0034bb28e5 fix(aio): restore component-styles/exclude hidden children 2017-06-21 11:33:19 -07:00
ca51e020cb fix(aio): redirect "cli-quickstart" to "quickstart" 2017-06-21 11:33:19 -07:00
d16852898f docs(aio): fix numbered list in testing.md
This list renders OK in the github UI but not at https://angular.io/guide/testing#setup
2017-06-20 16:43:26 -07:00
46ddf501a9 fix(tsc-wrapped): skip collecting metadata for default functions
Fixes: #17518
2017-06-20 14:22:49 -07:00
8c89cc4fc5 fix(compiler-cli): find lazy routes in nested module import arrays
Fixes: #17531
2017-06-20 14:21:14 -07:00
00874c27f4 docs(aio): update resource listing 2017-06-20 12:58:02 -07:00
c59c390cdc fix: argument destructuring sometimes breaks strictNullChecks
Destructuring of the form:

function foo({a, b}: {a?, b?} = {})

breaks strictNullChecks, due to the TypeScript bug https://github.com/microsoft/typescript/issues/10078.
This change eliminates usage of destructuring in function argument lists in cases where it would leak
into the public API .d.ts.
2017-06-20 12:56:08 -07:00
009651e14f refactor(core): remove toString() method from DefaultKeyValueDiffer
toString() from DefaultKeyValueDiffer is only used in tests and should not
be part of the production code. toString() methods from differs add
~ 0.3KB (min+gzip) to the production bundle size.
2017-06-20 12:55:20 -07:00
f194f18dbd fix(language-service): infer any ngForOf of type any
Fixes: #17611
2017-06-20 12:04:51 -07:00
4e6be15069 fix(language-service): rollup tslib into the language service package
Fixes: #17614
2017-06-20 11:50:20 -07:00
3e685f98c6 fix(forms): roll back breaking change with min/max directives
With 4.2, we introduced the min and max validator directives. This was actually a breaking change because their selectors could include custom value accessors using the min/max properties for their own purposes.

For now, we are rolling back the change by removing the exports. At the least, we should wait to add them until a major version. In the meantime, we will have further discussion about what the best solution is going forward for all validator directives.

Closes #17491.

----

PR #17551 tried to roll this back, but did not remove the dead code. This failed internal tests that were checking that all declared directives were used.
This PR rolls back the original PR and commit the same as #17551 while also removing the dead code.
2017-06-20 09:04:50 -07:00
6c8e7dd63e revert: fix(forms): temp roll back breaking change with min/max directives
This reverts commit 232bd9395d.
2017-06-20 09:04:50 -07:00
2447bd1bac fix(aio): leave results panel open when opening search result on new page
Fixes #17580
2017-06-19 15:12:46 -07:00
1c6a252596 build(aio): upgrade jasmine to v2.6.4
This version fixes the DISCONNECTED errors (described in #17543) and removes the
need to the workaround (8af203c).
The relevant jasmine commit is jasmine/jasmine@c60d66994.
2017-06-19 15:12:25 -07:00
d3c92a307a fix(router): update the version placeholder so that it gets replaced during the build
Fixes #17403
2017-06-19 15:11:06 -07:00
8f5836cb14 docs(TRIAGE_AND_LABELS): update labels to reflect the current state 2017-06-19 14:53:55 -07:00
319ce182db docs(RELEASE_SCHEDULE): fix version numbers for August/September releases 2017-06-19 14:46:23 -07:00
aa92f3a721 docs(RELEASE_SCHEDULE): update the release schedule w/ recent regression patch releases 2017-06-19 11:33:44 -07:00
afbb6bb797 docs: remove aio from changelog 2017-06-19 11:14:35 -07:00
4f37f86433 fix(aio): switch from innerText to textContent to support older browsers
`innerText` is not supported in Firefox prior to v45. In most cases (at least
the ones we are interested in), `innerText` and `textContent` work equally well,
but `textContent` is more performant (as it doesn't require a reflow).

From [MDN][1] on the differences of `innerText` vs `textContent`:

> - [...]
> - `innerText` is aware of style and will not return the text of hidden
>   elements, whereas `textContent` will.
> - As `innerText` is aware of CSS styling, it will trigger a reflow, whereas
>   `textContent` will not.
> - [...]

[1]: https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#Differences_from_innerText

Fixes #17585
2017-06-19 10:32:36 -07:00
3093c55e9e build(aio): update @angular/service-worker to 1.0.0-beta.16
This version includes a fix for potential cache corruption and invalid redirect behavior in rare cases.
2017-06-16 13:33:37 -07:00
5ac3919259 feat(core): update zone.js to 0.8.12 2017-06-16 12:13:14 -07:00
f58211d9d8 fix(aio): improve no-javascript screen 2017-06-16 12:04:45 -07:00
fe126cb737 fix(aio): remove unused icon reference and file
These icons are not there (and never were afaict).

Fixes #17561
2017-06-16 12:03:43 -07:00
adc1b129e4 fix(aio): correctly redirect /docs/ts/latest and /styleguide
Previously, we had redirect rules for Firebase for `/docs/ts/latest` and
`/styleguide`, but once the ServiceWorker was activated, it would take over
routing and rewrite these requests to `/index.html`.
This commit fixes it by excluding them from ServiceWorker routing.

Fixes #17542
2017-06-16 12:02:52 -07:00
9315ab88d7 test(compiler): fix typo "mamespace" 2017-06-16 11:17:15 -07:00
1ddbddb0db docs: add changelog for 4.2.3 2017-06-16 09:42:31 -07:00
232bd9395d fix(forms): temp roll back breaking change with min/max directives
With 4.2, we introduced the min and max validator directives. This was actually a breaking change because
their selectors could include custom value accessors using the min/max properties for their own purposes.

For now, we are rolling back the change by removing the exports.

Closes #17491.
2017-06-16 09:20:04 -07:00
956a7e95d7 docs(aio): re-add biography entry for devversion
With SHA 2c3e948e61 the biography of Paul Gschwendtner has been accidentally removed.

This re-adds the biography entry (picture still present) as requested on Slack.
2017-06-16 07:56:15 +01:00
ce00fa3627 build(aio): do not fail if check-env for the main angular project fails
Fixes #17434
2017-06-16 07:51:18 +01:00
3515860b15 build: fix link to DEVELOPER.md in check-environment.js 2017-06-16 07:51:18 +01:00
77e717e872 build(aio): remove dependency on build artifacts from parent folder 2017-06-16 07:51:18 +01:00
b46cc744b3 build: remove redundant line
The same value is set a few lines below.
2017-06-16 07:51:18 +01:00
2cc931ed2a fix(aio): add missing redirect rule for /styleguide
Fixes #17542
2017-06-16 07:49:51 +01:00
e096a85874 fix(animations): remove duplicate license header 2017-06-15 14:41:59 -07:00
2c3e948e61 docs(aio): update about page 2017-06-15 07:49:41 +01:00
53f57d74b8 feat(aio): add iphone pwa features 2017-06-15 07:47:49 +01:00
2b1de07f02 docs(aio): incorporate Ward's comments 2017-06-15 07:46:44 +01:00
fb877696bf docs(aio): add short section on built-in validators, copy edits 2017-06-15 07:46:44 +01:00
01173b9441 fix(aio): do not log messages in production
In dev mode, all messages passed to `Logger` will be logged.
In production mode, only warnings and errors will be logged.

Fixes #17453
2017-06-15 07:46:03 +01:00
0564dd25e2 refactor(aio): remove unused Logger dependencies 2017-06-15 07:46:03 +01:00
ba0f6decc3 build(aio): extra redirect rule 2017-06-15 07:45:47 +01:00
06a0cf2e31 docs(aio): http guide shows how to import toPromise operator from rxjs
Solves #17454
2017-06-15 07:45:12 +01:00
a8afa65a54 fix(aio): specify large image for PWA splash-screen 2017-06-14 09:36:54 -07:00
f73a4c229c build(aio): upgrade lighthouse to v2.1 2017-06-14 09:36:54 -07:00
d378a29565 ci(aio): fail the build if the PWA score is too low
Previously, there was an issue with testing the PWA score on staging and failing
the build was temporarily disabled. It works now, so we need to enable failing
the build is the score drops below some threshold.
2017-06-14 09:36:37 -07:00
2bdf2feea7 test(aio): add async beforeEach to prevent Chrome disconnects
Related to 3d5f520ff0 from #17405
2017-06-14 15:29:35 +01:00
607fb1fff8 fix(aio): always cover the whole footer with its background
Fixes #17465
2017-06-14 15:01:06 +01:00
38fc2a0055 fix(aio): fix trackBy demo in template-syntax article 2017-06-14 12:04:59 +01:00
0c07f8c099 test(platform-server): fix and re-enable integration tests 2017-06-13 15:50:23 -07:00
d8d21c77d5 ci: update github templates (#17466) 2017-06-13 14:49:13 -07:00
b4cd20cbbc fix(aio): make the footer links clickable on all browsers
The footer background (implemented via `footer:after`) had a higher `z-index`
than other footer elements and was obscuring the footer links on certain
browsers (Firefox, Edge, IE), which made them unclickable.
This commit lowers the index of `footer:after`, so that links are clickable on
these browsers.

Fixes #17460
2017-06-13 14:05:37 -07:00
f840afb983 ci: disable platform-server integration test as it is currently broken 2017-06-13 14:01:22 -07:00
93d1b4ed9d refactor(compiler): remove duplicate code 2017-06-13 13:37:33 -07:00
fa81c8eeb3 ci: use npm_install for bazel
using yarn_install polluted our node_modules cache because it disregards the npm_shrinkwrap.json
2017-06-13 13:35:01 -07:00
98308cd79c docs(aio): update typescript for examples/webpack to same as cli 2017-06-13 10:28:10 -07:00
052331fabc build(aio): add staging environment
You can now specify what environment you are building
by add it to the `yarn build` command. For example:

```
yarn build -- --env=stage
```

Moreover the `deploy-to-firebase.sh` script will automatically apply the
appropriate environment.
2017-06-13 13:43:36 +01:00
541c9a94bf docs(aio): i18n guide - updates for v4 2017-06-13 12:38:38 +01:00
12452d4ab4 build(aio): increase docs integration test timeouts
The API docs tests have very variable run times, depending
upon the build environment.
This change doubles their test timeout values to prevent
false-negative failures.
2017-06-13 11:56:45 +01:00
709a3f6de7 fix(aio): fix scrolling to elements near the bottom of the page
Previously, we always assumed that elements would be scrolled to the top of the
page, when calling `element.scrollIntoView()`. This is not true for elements
that cannot be scrolled to the top, e.g. when the viewport height is larger than
the height of the content after the element (common for small sections near the
end of the page).
In such cases, we would unnecessarily scroll up to account for the static
toolbar, which was unnecessary (since the element was not behind the toolbar
anyway) and caused ScrollSpy to fail to identify the scrolled-to section as
active.

This commit fixes it by ensuring that we do not scroll more than necessary in
order to align the top of the element with the bottom of the toolbar.

Fixes #17452
2017-06-13 11:54:05 +01:00
7caa0a8aa4 fix(aio): show search results when search box gets focus
Due to a previous commit, the search was only triggered
if the query changed, and not when the search box regained
focus.
2017-06-13 11:26:35 +01:00
0f56296c24 fix(aio): use locally hosted lunr library
The library is downloaded from npm but then
copied into the assets folder (and ignored by git)
as part of the postinstall step.
2017-06-13 11:26:35 +01:00
0a846a2fce fix(aio): make search results better
* update to latest version of lunr search
* add trailing wildcard to search terms to increase matches
* fix unwanted error when escape was pressed

Closes #17417
2017-06-13 11:26:35 +01:00
bffccf4622 fix(aio): fix buttons in "Home" and "Features"
Using `<a>` inside a `<button>` is not syntactically valid HTML and breaks on
some browsers (e.g. Firefox). Furthermore, clicking the button doesn't do
anything unless you click on the link (e.g. clicking on the padding around the
link does nothing), which is inconvenient and confusing.

Fixes #17448
2017-06-13 11:24:36 +01:00
fc774a1871 docs: clarify when non-null-assertion-operator is needed in template-syntax 2017-06-13 08:02:30 +01:00
d647db222c fix(aio): ensure that API filter page can display 3 columns in wide view
Fixes #17251
2017-06-13 07:41:10 +01:00
0c7eb93889 fix(aio): tidy up layout of api filter page
* Remove the "info-banner" styling from the filters.
* Fix alignment of the search box on a narrow screen (closes #17395)
* Remove unnecessary whitespace before section headers
2017-06-13 07:41:10 +01:00
0658e1da7f docs(aio): rename Upgrade docs to cheatsheet 2017-06-13 07:15:05 +01:00
d2d8e5d40f fix(aio): remove outline from search input on focus
Closes #17396
2017-06-13 06:56:21 +01:00
5e794492c1 build(aio): make deploy-to-firebase.sh executable 2017-06-12 16:14:17 -07:00
736 changed files with 27653 additions and 12203 deletions

View File

@ -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:

View File

@ -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,7 +49,7 @@ 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:

View File

@ -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
View File

@ -2,6 +2,7 @@
/dist/ /dist/
bazel-* bazel-*
e2e_test.*
node_modules node_modules
bower_components bower_components

View File

@ -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

View File

@ -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

View File

@ -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",
]), ]),
) )

File diff suppressed because it is too large Load Diff

View File

@ -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].

View File

@ -4,22 +4,20 @@
[![Issue Stats](http://issuestats.com/github/angular/angular/badge/pr?style=flat)](http://issuestats.com/github/angular/angular) [![Issue Stats](http://issuestats.com/github/angular/angular/badge/pr?style=flat)](http://issuestats.com/github/angular/angular)
[![Issue Stats](http://issuestats.com/github/angular/angular/badge/issue?style=flat)](http://issuestats.com/github/angular/angular) [![Issue Stats](http://issuestats.com/github/angular/angular/badge/issue?style=flat)](http://issuestats.com/github/angular/angular)
[![npm version](https://badge.fury.io/js/%40angular%2Fcore.svg)](https://www.npmjs.com/@angular/core) [![npm version](https://badge.fury.io/js/%40angular%2Fcore.svg)](https://www.npmjs.com/@angular/core)
)
[![Sauce Test Status](https://saucelabs.com/browser-matrix/angular2-ci.svg)](https://saucelabs.com/u/angular2-ci) [![Sauce Test Status](https://saucelabs.com/browser-matrix/angular2-ci.svg)](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

View File

@ -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",
)

View File

@ -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"
} }
} }
], ],

View File

@ -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;

View File

@ -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)));

View File

@ -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);
} }
} }

View File

@ -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 */

View File

@ -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 => {

View File

@ -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);
});
}); });
})); }));

View File

@ -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');

View File

@ -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);
}); });

View File

@ -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",

View File

@ -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()', () => {

View File

@ -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);
}); });

View File

@ -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"

View File

@ -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 \

View File

@ -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`

View File

@ -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).

View File

@ -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)**:

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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');
}

View File

@ -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 { }

View File

@ -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 { }

View File

@ -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>

View File

@ -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 {
this.heroForm = this.fb.group({
// #docregion name-validators
'name': [this.hero.name, [
Validators.required, Validators.required,
Validators.minLength(4), Validators.minLength(4),
Validators.maxLength(24), forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator.
forbiddenNameValidator(/bob/i) ]),
] 'alterEgo': new FormControl(this.hero.alterEgo),
], 'power': new FormControl(this.hero.power, Validators.required)
// #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

View File

@ -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 { }

View File

@ -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

View File

@ -1,9 +0,0 @@
// #docregion
export class Hero {
constructor(
public id: number,
public name: string,
public power: string,
public alterEgo?: string
) { }
}

View File

@ -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 { }

View File

@ -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); }
}

View File

@ -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>

View File

@ -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]};
}

View File

@ -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 { }

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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 */
} }

View File

@ -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) {

View File

@ -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 = '';

View File

@ -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'})

View File

@ -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,

View File

@ -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()'
} }
}) })

View File

@ -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;
}

View File

@ -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$/);
}); });
}); });

View File

@ -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>

View File

@ -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_.

View File

@ -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 &mdash; a class decorated with `@NgModule` &mdash; is a fundamental feature of Angular. The NgModule &mdash; a class decorated with `@NgModule` &mdash; 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>

View File

@ -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_** &mdash; the `BrowserModule` that this and every application needs to run in a browser. * **_imports_** &mdash; the `BrowserModule` that this and every application needs to run in a browser.
* **_declarations_** &mdash; the application's lone component, which is also ... * **_declarations_** &mdash; the application's lone component, which is also ...
* **_bootstrap_** &mdash; the _root_ component that Angular creates and inserts into the `index.html` host web page. * **_bootstrap_** &mdash; 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 &mdash; all of them classes decorated with `@NgModule` &mdash; It tells Angular about specific _other_ NgModules&mdash;all of them classes decorated
that the application needs to function properly. with `@NgModule`&mdash;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.

View File

@ -390,8 +390,9 @@ Here are the features which may require additional polyfills:
<td> <td>
[Typed&nbsp;Array](guide/browser-support#typedarray)<br>
[Typed&nbsp;Array](guide/browser-support#typedarray) <br>[Blob](guide/browser-support#blob)<br>[FormData](guide/browser-support#formdata) [Blob](guide/browser-support#blob)<br>
[FormData](guide/browser-support#formdata)
</td> </td>
<td> <td>

View File

@ -79,7 +79,7 @@ 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)
@ -199,7 +199,7 @@ The new "angular-in-memory-web-api" has new features.
## "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.

View File

@ -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>({&nbsp;declarations:&nbsp;...,&nbsp;imports:&nbsp;...,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;exports:&nbsp;...,&nbsp;providers:&nbsp;...,&nbsp;bootstrap:&nbsp;...})<br>class&nbsp;MyModule&nbsp;{}</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>&nbsp;[MyRedComponent,&nbsp;MyBlueComponent,&nbsp;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>&nbsp;[BrowserModule,&nbsp;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>&nbsp;[MyRedComponent,&nbsp;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>&nbsp;[MyService,&nbsp;{&nbsp;provide:&nbsp;...&nbsp;}]</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>&nbsp;[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>&lt;input&nbsp;<b>[value]</b>="firstName"&gt;</code></td> <td><code>&lt;input <b>[value]</b>="firstName"&gt;</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>&lt;div&nbsp;<b>[attr.role]</b>="myAriaRole"&gt;</code></td> <td><code>&lt;div <b>[attr.role]</b>="myAriaRole"&gt;</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>&lt;div&nbsp;<b>[class.extra-sparkle]</b>="isDelightful"&gt;</code></td> <td><code>&lt;div <b>[class.extra-sparkle]</b>="isDelightful"&gt;</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>&lt;div&nbsp;<b>[style.width.px]</b>="mySize"&gt;</code></td> <td><code>&lt;div <b>[style.width.px]</b>="mySize"&gt;</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>&lt;button&nbsp;<b>(click)</b>="readRainbow($event)"&gt;</code></td> <td><code>&lt;button <b>(click)</b>="readRainbow($event)"&gt;</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>&lt;div&nbsp;title="Hello&nbsp;<b>{{ponyName}}</b>"&gt;</code></td> <td><code>&lt;div title="Hello <b>{{ponyName}}</b>"&gt;</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>&lt;div [title]="'Hello ' + ponyName"&gt;</code></p> <code>&lt;div [title]="'Hello ' + ponyName"&gt;</code></p>
</td> </td>
</tr><tr> </tr><tr>
<td><code>&lt;p&gt;Hello&nbsp;<b>{{ponyName}}</b>&lt;/p&gt;</code></td> <td><code>&lt;p&gt;Hello <b>{{ponyName}}</b>&lt;/p&gt;</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>&lt;my-cmp&nbsp;<b>[(title)]</b>="name"&gt;</code></td> <td><code>&lt;my-cmp <b>[(title)]</b>="name"&gt;</code></td>
<td><p>Sets up two-way data binding. Equivalent to: <code>&lt;my-cmp [title]="name" (titleChange)="name=$event"&gt;</code></p> <td><p>Sets up two-way data binding. Equivalent to: <code>&lt;my-cmp [title]="name" (titleChange)="name=$event"&gt;</code></p>
</td> </td>
</tr><tr> </tr><tr>
<td><code>&lt;video&nbsp;<b>#movieplayer</b>&nbsp;...&gt;<br>&nbsp;&nbsp;&lt;button&nbsp;<b>(click)</b>="movieplayer.play()"&gt;<br>&lt;/video&gt;</code></td> <td><code>&lt;video <b>#movieplayer</b> ...&gt;<br> &lt;button <b>(click)</b>="movieplayer.play()"&gt;<br>&lt;/video&gt;</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>&lt;p&nbsp;<b>*myUnless</b>="myExpression"&gt;...&lt;/p&gt;</code></td> <td><code>&lt;p <b>*myUnless</b>="myExpression"&gt;...&lt;/p&gt;</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>&lt;ng-template [myUnless]="myExpression"&gt;&lt;p&gt;...&lt;/p&gt;&lt;/ng-template&gt;</code></p> <code>&lt;ng-template [myUnless]="myExpression"&gt;&lt;p&gt;...&lt;/p&gt;&lt;/ng-template&gt;</code></p>
</td> </td>
</tr><tr> </tr><tr>
<td><code>&lt;p&gt;Card&nbsp;No.:&nbsp;<b>{{cardNumber&nbsp;|&nbsp;myCardNumberFormatter}}</b>&lt;/p&gt;</code></td> <td><code>&lt;p&gt;Card No.: <b>{{cardNumber | myCardNumberFormatter}}</b>&lt;/p&gt;</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>&lt;p&gt;Employer:&nbsp;<b>{{employer?.companyName}}</b>&lt;/p&gt;</code></td> <td><code>&lt;p&gt;Employer: <b>{{employer?.companyName}}</b>&lt;/p&gt;</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>&lt;<b>svg:</b>rect&nbsp;x="0"&nbsp;y="0"&nbsp;width="100"&nbsp;height="100"/&gt;</code></td> <td><code>&lt;<b>svg:</b>rect x="0" y="0" width="100" height="100"/&gt;</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>&lt;<b>svg</b>&gt;<br>&nbsp;&nbsp;&lt;rect&nbsp;x="0"&nbsp;y="0"&nbsp;width="100"&nbsp;height="100"/&gt;<br>&lt;/<b>svg</b>&gt;</code></td> <td><code>&lt;<b>svg</b>&gt;<br> &lt;rect x="0" y="0" width="100" height="100"/&gt;<br>&lt;/<b>svg</b>&gt;</code></td>
<td><p>An <code>&lt;svg&gt;</code> root element is detected as an SVG element automatically, without the prefix.</p> <td><p>An <code>&lt;svg&gt;</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>&lt;section&nbsp;<b>*ngIf</b>="showSection"&gt;</code></td> <td><code>&lt;section <b>*ngIf</b>="showSection"&gt;</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>&lt;li&nbsp;<b>*ngFor</b>="let&nbsp;item&nbsp;of&nbsp;list"&gt;</code></td> <td><code>&lt;li <b>*ngFor</b>="let item of list"&gt;</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>&lt;div&nbsp;<b>[ngSwitch]</b>="conditionExpression"&gt;<br>&nbsp;&nbsp;&lt;ng-template&nbsp;<b>[<b>ngSwitchCase</b>]</b>="case1Exp"&gt;...&lt;/ng-template&gt;<br>&nbsp;&nbsp;&lt;ng-template&nbsp;<b>ngSwitchCase</b>="case2LiteralString"&gt;...&lt;/ng-template&gt;<br>&nbsp;&nbsp;&lt;ng-template&nbsp;<b>ngSwitchDefault</b>&gt;...&lt;/ng-template&gt;<br>&lt;/div&gt;</code></td> <td><code>&lt;div <b>[ngSwitch]</b>="conditionExpression"&gt;<br> &lt;ng-template <b>[<b>ngSwitchCase</b>]</b>="case1Exp"&gt;...&lt;/ng-template&gt;<br> &lt;ng-template <b>ngSwitchCase</b>="case2LiteralString"&gt;...&lt;/ng-template&gt;<br> &lt;ng-template <b>ngSwitchDefault</b>&gt;...&lt;/ng-template&gt;<br>&lt;/div&gt;</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>&lt;div&nbsp;<b>[ngClass]</b>="{'active':&nbsp;isActive,&nbsp;'disabled':&nbsp;isDisabled}"&gt;</code></td> <td><code>&lt;div <b>[ngClass]</b>="{'active': isActive, 'disabled': isDisabled}"&gt;</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>&lt;input&nbsp;<b>[(ngModel)]</b>="userName"&gt;</code></td> <td><code>&lt;input <b>[(ngModel)]</b>="userName"&gt;</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&nbsp;MyComponent()&nbsp;{}</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&nbsp;MyDirective()&nbsp;{}</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&nbsp;MyPipe()&nbsp;{}</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&nbsp;MyService()&nbsp;{}</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>&nbsp;'.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>&nbsp;[MyService,&nbsp;{&nbsp;provide:&nbsp;...&nbsp;}]</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>&nbsp;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>&nbsp;[MyService,&nbsp;{&nbsp;provide:&nbsp;...&nbsp;}]</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>&nbsp;'Hello&nbsp;{{name}}'<br><b>templateUrl:</b>&nbsp;'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>&nbsp;['.primary&nbsp;{color:&nbsp;red}']<br><b>styleUrls:</b>&nbsp;['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 components view.</p> <td><p>List of inline CSS styles or external stylesheet URLs for styling the components 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>&nbsp;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>&lt;my-cmp [myProperty]="someExpression"&gt;</code>).</p> <code>&lt;my-cmp [myProperty]="someExpression"&gt;</code>).</p>
</td> </td>
</tr><tr> </tr><tr>
<td><code><b>@Output()</b>&nbsp;myEvent&nbsp;=&nbsp;new&nbsp;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>&lt;my-cmp (myEvent)="doSomething()"&gt;</code>).</p> <td><p>Declares an output property that fires events that you can subscribe to with an event binding (example: <code>&lt;my-cmp (myEvent)="doSomething()"&gt;</code>).</p>
</td> </td>
</tr><tr> </tr><tr>
<td><code><b>@HostBinding('class.valid')</b>&nbsp;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',&nbsp;['$event'])</b>&nbsp;onClick(e)&nbsp;{...}</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>&nbsp;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>&nbsp;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>&nbsp;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>&nbsp;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:&nbsp;MyService,&nbsp;...)</b>&nbsp;{&nbsp;...&nbsp;}</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>&nbsp;{&nbsp;...&nbsp;}</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>&nbsp;{&nbsp;...&nbsp;}</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>&nbsp;{&nbsp;...&nbsp;}</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>&nbsp;{&nbsp;...&nbsp;}</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>&nbsp;{&nbsp;...&nbsp;}</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>&nbsp;{&nbsp;...&nbsp;}</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>&nbsp;{&nbsp;...&nbsp;}</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>&nbsp;{&nbsp;...&nbsp;}</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>{&nbsp;<b>provide</b>:&nbsp;MyService,&nbsp;<b>useClass</b>:&nbsp;MyMockService&nbsp;}</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>{&nbsp;<b>provide</b>:&nbsp;MyService,&nbsp;<b>useFactory</b>:&nbsp;myFactory&nbsp;}</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>{&nbsp;<b>provide</b>:&nbsp;MyValue,&nbsp;<b>useValue</b>:&nbsp;41&nbsp;}</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&nbsp;routes:&nbsp;<b>Routes</b>&nbsp;=&nbsp;[<br>&nbsp;&nbsp;{&nbsp;path:&nbsp;'',&nbsp;component:&nbsp;HomeComponent&nbsp;},<br>&nbsp;&nbsp;{&nbsp;path:&nbsp;'path/:routeParam',&nbsp;component:&nbsp;MyComponent&nbsp;},<br>&nbsp;&nbsp;{&nbsp;path:&nbsp;'staticPath',&nbsp;component:&nbsp;...&nbsp;},<br>&nbsp;&nbsp;{&nbsp;path:&nbsp;'**',&nbsp;component:&nbsp;...&nbsp;},<br>&nbsp;&nbsp;{&nbsp;path:&nbsp;'oldPath',&nbsp;redirectTo:&nbsp;'/staticPath'&nbsp;},<br>&nbsp;&nbsp;{&nbsp;path:&nbsp;...,&nbsp;component:&nbsp;...,&nbsp;data:&nbsp;{&nbsp;message:&nbsp;'Custom'&nbsp;}&nbsp;}<br>]);<br><br>const&nbsp;routing&nbsp;=&nbsp;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>&lt;<b>router-outlet</b>&gt;&lt;/<b>router-outlet</b>&gt;<br>&lt;<b>router-outlet</b>&nbsp;name="aux"&gt;&lt;/<b>router-outlet</b>&gt;<br></code></td> <td><code><br>&lt;<b>router-outlet</b>&gt;&lt;/<b>router-outlet</b>&gt;<br>&lt;<b>router-outlet</b> name="aux"&gt;&lt;/<b>router-outlet</b>&gt;<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>&lt;a&nbsp;routerLink="/path"&gt;<br>&lt;a&nbsp;<b>[routerLink]</b>="[&nbsp;'/path',&nbsp;routeParam&nbsp;]"&gt;<br>&lt;a&nbsp;<b>[routerLink]</b>="[&nbsp;'/path',&nbsp;{&nbsp;matrixParam:&nbsp;'value'&nbsp;}&nbsp;]"&gt;<br>&lt;a&nbsp;<b>[routerLink]</b>="[&nbsp;'/path'&nbsp;]"&nbsp;[queryParams]="{&nbsp;page:&nbsp;1&nbsp;}"&gt;<br>&lt;a&nbsp;<b>[routerLink]</b>="[&nbsp;'/path'&nbsp;]"&nbsp;fragment="anchor"&gt;<br></code></td> <td><code><br>&lt;a routerLink="/path"&gt;<br>&lt;a <b>[routerLink]</b>="[ '/path', routeParam ]"&gt;<br>&lt;a <b>[routerLink]</b>="[ '/path', { matrixParam: 'value' } ]"&gt;<br>&lt;a <b>[routerLink]</b>="[ '/path' ]" [queryParams]="{ page: 1 }"&gt;<br>&lt;a <b>[routerLink]</b>="[ '/path' ]" fragment="anchor"&gt;<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>&lt;a&nbsp;[routerLink]="[&nbsp;'/path'&nbsp;]"&nbsp;routerLinkActive="active"&gt;</code></td> <td><code>&lt;a [routerLink]="[ '/path' ]" routerLinkActive="active"&gt;</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&nbsp;<b>CanActivate</b>Guard&nbsp;implements&nbsp;<b>CanActivate</b>&nbsp;{<br>&nbsp;&nbsp;&nbsp;&nbsp;canActivate(<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;route:&nbsp;ActivatedRouteSnapshot,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;state:&nbsp;RouterStateSnapshot<br>&nbsp;&nbsp;&nbsp;&nbsp;):&nbsp;Observable&lt;boolean&gt;|Promise&lt;boolean&gt;|boolean&nbsp;{&nbsp;...&nbsp;}<br>}<br><br>{&nbsp;path:&nbsp;...,&nbsp;canActivate:&nbsp;[<b>CanActivate</b>Guard]&nbsp;}</code></td> <td><code>class <b>CanActivate</b>Guard implements <b>CanActivate</b> {<br> canActivate(<br> route: ActivatedRouteSnapshot,<br> state: RouterStateSnapshot<br> ): Observable&lt;boolean&gt;|Promise&lt;boolean&gt;|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&nbsp;<b>CanDeactivate</b>Guard&nbsp;implements&nbsp;<b>CanDeactivate</b>&lt;T&gt;&nbsp;{<br>&nbsp;&nbsp;&nbsp;&nbsp;canDeactivate(<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;component:&nbsp;T,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;route:&nbsp;ActivatedRouteSnapshot,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;state:&nbsp;RouterStateSnapshot<br>&nbsp;&nbsp;&nbsp;&nbsp;):&nbsp;Observable&lt;boolean&gt;|Promise&lt;boolean&gt;|boolean&nbsp;{&nbsp;...&nbsp;}<br>}<br><br>{&nbsp;path:&nbsp;...,&nbsp;canDeactivate:&nbsp;[<b>CanDeactivate</b>Guard]&nbsp;}</code></td> <td><code>class <b>CanDeactivate</b>Guard implements <b>CanDeactivate</b>&lt;T&gt; {<br> canDeactivate(<br> component: T,<br> route: ActivatedRouteSnapshot,<br> state: RouterStateSnapshot<br> ): Observable&lt;boolean&gt;|Promise&lt;boolean&gt;|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&nbsp;<b>CanActivateChild</b>Guard&nbsp;implements&nbsp;<b>CanActivateChild</b>&nbsp;{<br>&nbsp;&nbsp;&nbsp;&nbsp;canActivateChild(<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;route:&nbsp;ActivatedRouteSnapshot,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;state:&nbsp;RouterStateSnapshot<br>&nbsp;&nbsp;&nbsp;&nbsp;):&nbsp;Observable&lt;boolean&gt;|Promise&lt;boolean&gt;|boolean&nbsp;{&nbsp;...&nbsp;}<br>}<br><br>{&nbsp;path:&nbsp;...,&nbsp;canActivateChild:&nbsp;[CanActivateGuard],<br>&nbsp;&nbsp;&nbsp;&nbsp;children:&nbsp;...&nbsp;}</code></td> <td><code>class <b>CanActivateChild</b>Guard implements <b>CanActivateChild</b> {<br> canActivateChild(<br> route: ActivatedRouteSnapshot,<br> state: RouterStateSnapshot<br> ): Observable&lt;boolean&gt;|Promise&lt;boolean&gt;|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&nbsp;<b>Resolve</b>Guard&nbsp;implements&nbsp;<b>Resolve</b>&lt;T&gt;&nbsp;{<br>&nbsp;&nbsp;&nbsp;&nbsp;resolve(<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;route:&nbsp;ActivatedRouteSnapshot,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;state:&nbsp;RouterStateSnapshot<br>&nbsp;&nbsp;&nbsp;&nbsp;):&nbsp;Observable&lt;any&gt;|Promise&lt;any&gt;|any&nbsp;{&nbsp;...&nbsp;}<br>}<br><br>{&nbsp;path:&nbsp;...,&nbsp;resolve:&nbsp;[<b>Resolve</b>Guard]&nbsp;}</code></td> <td><code>class <b>Resolve</b>Guard implements <b>Resolve</b>&lt;T&gt; {<br> resolve(<br> route: ActivatedRouteSnapshot,<br> state: RouterStateSnapshot<br> ): Observable&lt;any&gt;|Promise&lt;any&gt;|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&nbsp;<b>CanLoad</b>Guard&nbsp;implements&nbsp;<b>CanLoad</b>&nbsp;{<br>&nbsp;&nbsp;&nbsp;&nbsp;canLoad(<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;route:&nbsp;Route<br>&nbsp;&nbsp;&nbsp;&nbsp;):&nbsp;Observable&lt;boolean&gt;|Promise&lt;boolean&gt;|boolean&nbsp;{&nbsp;...&nbsp;}<br>}<br><br>{&nbsp;path:&nbsp;...,&nbsp;canLoad:&nbsp;[<b>CanLoad</b>Guard],&nbsp;loadChildren:&nbsp;...&nbsp;}</code></td> <td><code>class <b>CanLoad</b>Guard implements <b>CanLoad</b> {<br> canLoad(<br> route: Route<br> ): Observable&lt;boolean&gt;|Promise&lt;boolean&gt;|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>

View File

@ -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

View File

@ -6,74 +6,49 @@
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 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` * The `*ngIf` on the `<div>` element reveals a set of nested message `divs`
but only if there are `name` errors and but only if the `name` is invalid and the control is either `dirty` or `touched`.
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&mdash;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&mdash;`FormsModule` and
`ReactiveFormsModule`&mdash;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&mdash;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&mdash;`Validators.required` and `Validators.minLength(4)`&mdash;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`&mdash;the instance in the form with the `ForbiddenValidatorDirective`&mdash;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 youd be registering a new class instance, one that `useExisting` with `useClass`, then youd be registering a new class instance, one that
doesnt have a `forbiddenName`. doesnt 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&mdash;factors that tend to diminish test code
coverage and quality.

View File

@ -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.

View File

@ -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"&mdash;also referred to as a "key"&mdash;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.

File diff suppressed because it is too large Load Diff

View File

@ -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>

View 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.

View File

@ -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_.

View File

@ -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">

View File

@ -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*.

View File

@ -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>

View File

@ -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

View File

@ -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}

View File

@ -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

View File

@ -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.

View File

@ -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.
@ -1950,7 +1950,7 @@ In this mode, typed variables disallow null and undefined by default. The type c
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.

View File

@ -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

View File

@ -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`,

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Some files were not shown because too many files have changed in this diff Show More