Compare commits

...

139 Commits

Author SHA1 Message Date
1f0c1f3ff2 release: cut the v9.1.12 release 2020-07-08 12:36:30 -07:00
a86a41210f fix(docs-infra): fix deploy-to-firebase.sh for master and v10.0.x branches (#37762)
The deployment to aio is currently failing because #37721 introduced
"project" entry into the firebase.json which means that we now need to
select the deployment target before deploying to firebase.

This change fixes the issue and refactors the file to be easier to read.

I also added extra echo statements so that the CI logs are easier to
read in case we need to troubleshoot future issues.

PR Close #37762
2020-06-25 17:58:21 -07:00
20c44a6240 fix(docs-infra): fix typo in the deploy-to-firebase.sh script
This typo caused the script to fail on Linux (interestingly it works fine on Mac).

This is a painful reminder that we should not write any more Bash scripts EVER. shelljs FTW! :-)
2020-06-25 15:03:12 -07:00
0feb560136 fixup! feat(docs-infra): update deploy-to-firebase.sh script to support v9 multisite setup 2020-06-25 13:13:58 -07:00
f4d06445a3 feat(docs-infra): update deploy-to-firebase.sh script to support v9 multisite setup
v9.angular.io was used to pilot the firebase hosting multisites setup for angular.io.

The deployments so far have been done manually to control the deployment process.

This change, automates the deployment for v9.angular.io so that future deployments can be made from
the CI.

See https://angular-team.atlassian.net/browse/DEV-125 for more info.

In the process of updating the scripts I rediscovered a bug in the deploy-to-firebase.sh script that
incorrect compared two numbers as strings. This previously worked correctly because we were comparing
single digit numbers. With the release of v10, we now compare 9 > 10 which behaves differently for
strings and numbers. The bug was fixed by switching to an arithmetic comparison of the two variables.

This bug has been fixed on the master branch but not on the 9.1.x branch. I realized this during the
rebase, but found my version to be a bit cleaner, so I kept it.
2020-06-25 13:10:26 -07:00
6c1ab479ee fix(core): infinite loop if injectable using inheritance has a custom decorator
If we detect that an injectable class is inheriting from another injectable, we generate code that looks something like this:

```
const baseFactory = ɵɵgetInheritedFactory(Child);

@Injectable()
class Parent {}

@Injectable()
class Child extends Parent {
  static ɵfac = (t) => baseFactory(t || Child)
}
```

This usually works fine, because the `ɵɵgetInheritedFactory` resolves to the factory of `Parent`, but the logic can break down if the `Child` class has a custom decorator. Custom decorators can return a new class that extends the original once, which means that the `ɵɵgetInheritedFactory` call will now resolve to the factory of the `Child`, causing an infinite loop.

These changes fix the issue by changing the inherited factory resolution logic so that it walks up the prototype chain class-by-class, while skipping classes that have the same factory as the class that was passed in.

Fixes #35733.
2020-06-12 22:01:18 +02:00
f7997256fc release: cut the v9.1.11 release 2020-06-10 11:18:17 -07:00
dc9da17f37 Revert "fix(elements): fire custom element output events during component initialization"
This reverts commit 454e073918. This
commit was found to cause some tests inside Google to fail.
2020-06-10 17:31:04 +00:00
99c41d6db0 release: cut the v9.1.10 release 2020-06-09 15:05:14 -07:00
1aae94a421 perf(ngcc): cache parsed tsconfig between runs
This commit will store a cached copy of the parsed tsconfig
that can be reused if the tsconfig path is the same.

This will improve the ngcc "noop" case, where there is no processing
to do, when the entry-points have already been processed.
Previously we were parsing this config every time we checked for
entry-points to process, which can take up to seconds in some
cases.

Cherry-picked from #37417 (6e7bd939f6).

Resolves #36882
2020-06-08 10:54:30 +01:00
f0f319b1d6 ci: temporarily disable Android 10 browser unit tests on Saucelabs (#37399)
Disabling Android 10 browser unit tests on Saucelabs due to errors.

After remediation from Saucelabs to correct the discovered failures, this change can be reverted to renable the tests on Android 10.

Example of failures seen:

```
02 06 2020 14:03:05.048:INFO [SaucelabsLauncher]: Chrome 10.0 (Android) session at https://saucelabs.com/tests/54f5fb181db644a3b4779187c2309000

02 06 2020 14:03:06.869:INFO [Chrome Mobile 74.0.3729 (Android 0.0.0)]: Disconnected browser returned on socket E-bi0p0NKtghk-HcAAAO with id 85563367.

Chrome Mobile 74.0.3729 (Android 0.0.0) ERROR: Error: XHR error loading http://angular-ci.local:9876/base/node_modules/rxjs/internal/operators/zip.js

	Error loading http://angular-ci.local:9876/base/node_modules/rxjs/internal/operators/zip.js as "../internal/operators/zip" from http://angular-ci.local:9876/base/node_modules/rxjs/operators/index.js

Error: XHR error loading http://angular-ci.local:9876/base/node_modules/rxjs/internal/operators/zip.js

    at error (http://angular-ci.local:9876/base/node_modules/systemjs/dist/system.src.js?1c6a6c12fec50a8db7aeebe8e06e2b70135c0615:1028:16)

    at XMLHttpRequest.xhr.onreadystatechange [as __zone_symbol__ON_PROPERTYreadystatechange] (http://angular-ci.local:9876/base/node_modules/systemjs/dist/system.src.js?1c6a6c12fec50a8db7aeebe8e06e2b70135c0615:1036:13)

    at XMLHttpRequest.wrapFn (http://angular-ci.local:9876/base/dist/bin/packages/zone.js/npm_package/dist/zone.js?942d01da94828e1c75e8527fa8d06f363d6379ce:809:43)

    at ZoneDelegate.invokeTask (http://angular-ci.local:9876/base/dist/bin/packages/zone.js/npm_package/dist/zone.js?942d01da94828e1c75e8527fa8d06f363d6379ce:432:35)

    at Zone.runTask (http://angular-ci.local:9876/base/dist/bin/packages/zone.js/npm_package/dist/zone.js?942d01da94828e1c75e8527fa8d06f363d6379ce:201:55)

    at ZoneTask.invokeTask [as invoke] (http://angular-ci.local:9876/base/dist/bin/packages/zone.js/npm_package/dist/zone.js?942d01da94828e1c75e8527fa8d06f363d6379ce:514:38)

    at invokeTask (http://angular-ci.local:9876/base/dist/bin/packages/zone.js/npm_package/dist/zone.js?942d01da94828e1c75e8527fa8d06f363d6379ce:1722:18)

    at XMLHttpRequest.globalZoneAwareCallback (http://angular-ci.local:9876/base/dist/bin/packages/zone.js/npm_package/dist/zone.js?942d01da94828e1c75e8527fa8d06f363d6379ce:1748:21)
```

PR Close #37399
2020-06-05 22:45:49 +03:00
454e073918 fix(elements): fire custom element output events during component initialization
Previously, event listeners for component output events attached on an
Angular custom element before inserting it into the DOM (i.e. before
instantiating the underlying component) didn't fire for events emitted
during initialization lifecycle hooks, such as `ngAfterContentInit`,
`ngAfterViewInit`, `ngOnChanges` (initial call) and `ngOnInit`.
The reason was that that `NgElementImpl` [subscribed to events][1]
_after_ calling [ngElementStrategy#connect()][2], which is where the
[initial change detection][3] takes place (running the initialization
lifecycle hooks).

This commit fixes this by:
1. Ensuring `ComponentNgElementStrategy#events` is defined and available
   for subscribing to, even before instantiating the component.
2. Ensuring `NgElementImpl` subscribes to `NgElementStrategy#events`
   before calling `NgElementStrategy#connect()` (which initializes the
   component instance).

Jira issue: [FW-2010](https://angular-team.atlassian.net/browse/FW-2010)

[1]: c0143cb2ab/packages/elements/src/create-custom-element.ts (L167-L170)
[2]: c0143cb2ab/packages/elements/src/create-custom-element.ts (L164)
[3]: c0143cb2ab/packages/elements/src/component-factory-strategy.ts (L158)

Fixes #36141
2020-06-05 20:16:36 +03:00
76d64331bd refactor(elements): remove unnecessary non-null assertions and as any type-casts
This commit removes some unnecessary non-null assertions (`!`) and
`as any` type-casts from the `elements` package.
2020-06-05 20:16:35 +03:00
6855396449 docs: update aio in support for #BlackLivesMatter
Update angular.io in support for #BlackLivesMatter. The PR updates the
styles of the landing page and changes the current survey notification.
2020-06-02 17:34:57 -07:00
0b96908225 feat(dev-infra): migrate release tool to use new logging system (#37232)
Migrate the release tool in ng-dev to use new logging system rather
than directly calling console.* to create a better experience
for users.

PR Close #37232
2020-05-21 17:40:36 -04:00
65f52f3761 feat(dev-infra): migrate ts-circular-dependencies tool to use new logging system (#37232)
Migrate the ts-circular-dependencies tool in ng-dev to use new logging system rather
than directly calling console.* to create a better experience
for users.

PR Close #37232
2020-05-21 17:40:36 -04:00
9ce55d08f8 feat(dev-infra): migrate merge tool to use new logging system (#37232)
Migrate the merge tool in ng-dev to use new logging system rather
than directly calling console.* to create a better experience
for users.

PR Close #37232
2020-05-21 17:40:36 -04:00
765c1c5b17 feat(dev-infra): migrate ng-dev utils to use new logging system (#37232)
Migrate the ng-dev utils to use new logging system rather
than directly calling console.* to create a better experience
for users.

PR Close #37232
2020-05-21 17:40:36 -04:00
ccfa3426f7 feat(dev-infra): migrate pullapprove tool to use new logging system (#37232)
Migrate the pullapprove tool in ng-dev to use new logging system rather
than directly calling console.* to create a better experience
for users.

PR Close #37232
2020-05-21 17:40:36 -04:00
83379fa0bb feat(dev-infra): migrate rebase tool to use new logging system (#37232)
Migrate the rebase tool in ng-dev to use new logging system rather
than directly calling console.*  to create a better experience
for users.

PR Close #37232
2020-05-21 17:40:36 -04:00
2ffd44ad96 feat(dev-infra): migrate discover-new-conflicts tool to use new logging system (#37232)
Migrate the discover-new-conflicts tool in ng-dev to use new logging system
rather than directly calling console.* to create a better experience
for users.

PR Close #37232
2020-05-21 17:40:36 -04:00
4a84842bbb feat(dev-infra): migrate commit-message tool to use new logging system (#37232)
Migrate the commit-message tool in ng-dev to use new logging system rather
than directly calling console.* to create a better experience
for users.

PR Close #37232
2020-05-21 17:40:36 -04:00
daa715a1ca feat(dev-infra): migrate format tool to use new logging system (#37232)
Migrate the formatting tool in ng-dev to use new logging system rather
than directly calling console.* to create a better experience
for users.

PR Close #37232
2020-05-21 17:40:36 -04:00
31bce80771 feat(dev-infra): add group functions to logging system and remove color param (#37232)
Adds .group and .groupEnd functions to each of the logging functions
to allow creating groups in the logged output.  Additionally removes
the color parameter from logging functions, in favor of the color
being applied to the string at the call site.

PR Close #37232
2020-05-21 17:40:35 -04:00
711e4d4cc2 build: use static patch value for targetting branches in merge config (#37245)
Due to the desired patch branch (10.0.x) being on a semver version
that is unreleased as stable (there is no 10.0.0 on latest, it is on
next) our logic for determining target patch branches does not work.

This change is a workaround to unblock merging in the repo while a
longer term answer is discovered.

PR Close #37245
2020-05-21 17:07:26 -04:00
fdc5941fe7 feat(dev-infra): expose script for determining merge branches (#37217)
The components repo and framework repository follow the same patch
branch concept. We should be able to share a script for determining
these merge branches.

Additonally the logic has been improved compared to the old merge script because
we no longer consult `git ls-remote` unless really needed. Currently,
`git ls-remote` is always consulted, even though not necessarily needed.

This can slow down the merge script and the caretaker process when a
couple of PRs are merged (personally saw around ~4 seconds per merge).

Additionally, the new logic is more strict and will ensure (in most
cases) that no wrong patch/minor branch is determined. Previously,
the script just used the lexicographically greatest patch branch.
This _could_ be wrong when a new patch branch has been created too
early, or by accident.

PR Close #37217
2020-05-21 10:35:24 -07:00
ae10a541bf fix(dev-infra): ensure ts-node is registered with commonjs as module (#37217)
We recently added support for automatic registration of `ts-node`
when the dev-infra configuration is loaded.

In addition to registering ts-node, we should also ensure that the
`commonjs` module is set up. By default, `ts-node` would use ES module
imports that are not supported by default in NodeJS.

PR Close #37217
2020-05-21 10:35:24 -07:00
b797913d10 release: cut the v9.1.9 release 2020-05-20 15:26:59 -07:00
1465372a0e fix(elements): do not break when the constructor of an Angular Element is not called (#36114) (#37226)
Previously, the correct behavior of Angular custom elements relied on
the constructor being called (and thus the `injector` property being
initialized). However, some polyfills (e.g. `document-register-element`)
do not call the constructor of custom elements, which resulted in the
`injector` property being undefined and the `NgElementStrategy` failing
to be instantiated.

This commit fixes it by being tolerant to the `injector` property being
undefined and falling back to the injector passed to the
`createCustomElement()` config.

NOTE:
We don't have proper tests exercising the situation where the
constructor is not called. For now this is tested using a Google
internal test suite (which is how this issue was caught).
This commit also adds a rudimentary unit test to emulate this situation.

PR Close #36114

PR Close #37226
2020-05-20 14:49:21 -07:00
a33cb2d39b fix(elements): capture input properties set before upgrading the element (#36114) (#37226)
Previously, if an element started out as a regular `HTMLElement` (not a
Custom Element) and was later upgraded to a Custom Element, any
properties corresponding to component inputs that were set on the
element before upgrading it would not be captured correctly and thus not
reflected on the instantiated component.

This commit fixes it by ensuring that such properties are captured
correctly.

Jira issue: [FW-2006](https://angular-team.atlassian.net/browse/FW-2006)

Fixes #30848
Closes #31416

PR Close #36114

PR Close #37226
2020-05-20 14:49:21 -07:00
1202d17b4c test(elements): clean up TestStrategy between tests (#36114) (#37226)
Previously, the `TestStrategy` `NgElementStrategy` used in
`createCustomElement()` tests was created once and re-used in each test
(due to complications related to how `customElements.register()` works).
As a result, the `TestStrategy` instance's state (e.g. inputs) could be
polluted from previous tests and affect subsequent ones.

This commit ensures the strategy instance is reset before each test.

PR Close #36114

PR Close #37226
2020-05-20 14:49:21 -07:00
d837828317 test(elements): only declare helpers if needed (#36114) (#37226)
Previously, helper modules/components classes were declared even if the
tests were not run (because the environment did not support Custom
Elements for example).

This commit moves the declaration of the helpers inside the `describe()`
block, so they are not declared unnecessarily. This is in preparation of
adding more helpers that need to access variables declared inside the
`describe()` block.

PR Close #36114

PR Close #37226
2020-05-20 14:49:21 -07:00
dc210bc75b refactor(elements): remove unused imports and properties (#36114) (#37226)
- Remove imports that are not used.
- Remove private class properties that are not used outside of the
  constructor.

PR Close #36114

PR Close #37226
2020-05-20 14:49:21 -07:00
75ead94ed5 refactor(elements): simplify accessing NgElementStrategy (#36114) (#37226)
Previously, we had to check whether `NgElementStrategy` had been
instantiated before accessing it. This was tedious and error prone,
since it was easy to forget to add the check in new call sites.

This commit switches to using a getter, so that the check has to be
performed in one place and is transparent to call sites (including any
future ones).

PR Close #36114

PR Close #37226
2020-05-20 14:49:21 -07:00
6ac0042aef fix(elements): correctly handle getting/setting properties before connecting the element (#36114) (#37226)
`createCustomElements()` creates some getters/setters for properties
corresponding to component inputs that delegate to the
`NgElementStrategy`. However, it is not guaranteed that the element's
`NgElementStrategy` will have been created when these getters/setters
are called, because some polyfills (e.g. `document-register-element`) do
not call the constructor.

Previously, trying to get/set input properties before connecting the
element to the DOM (via `connectedCallback()`) would fail due to
`NgElementStrategy` not being created.

This commit ensures that the `NgElementStrategy` is always created
before used inside the input property getters/setters (similar to how it
is done for other methods of `NgElement`).

Mentioned in https://github.com/angular/angular/pull/31416/files#r300326698.

PR Close #36114

PR Close #37226
2020-05-20 14:49:21 -07:00
3eee53603c docs: update docs for microbenchmarks (#37140)
Update docs in the micro benchmarks to include:
* How to run with no turbo inlining
* Where to find the profiles in the DevTools
* Best way to debug benchmarks (using the profile_in_browser rather than --inspect-brk)

PR Close #37140
2020-05-20 13:37:28 -07:00
5c94fa97c1 build: complete removal of bazel format yarn commands (#37148)
Remove bazel yarn format deprecation message to complete the removal of
formatting bazel related files via yarn command command.

PR Close #37148
2020-05-20 13:32:41 -07:00
468f4a3f9e feat(dev-infra): create logging utils with support for defined logging levels (#37192)
Creates common logging functions at different levels. Allows for providing
logging statements which are actually printed to the console based on the
LOG_LEVEL environment variable.

PR Close #37192
2020-05-20 13:32:06 -07:00
a2393bef02 fix(dev-infra): ignore dev-infra scope while checking for features in patch branch (#37210)
This commit updates the script that checks master and patch branches to ignore features with `dev-infra` scope
 while verifying that there are no feature commits in patch branch. It's ok and in fact desirable for dev-infra features to be on the patch branch.

PR Close #37210
2020-05-20 13:31:38 -07:00
5bd1c7bbf7 ci: testing folders in compiler-cli should not require fw-testingapproval (#37220)
The `fw-testing` PullApprove group is really designed to
capture the top level public testing API groups in packages
like `common` and `router`.

The compiler-cli also has some folders that contain the path
segment `testing` but these should not require `fw-testing`
PullApprove approval.

This commit excludes the whole of `compiler-cli` package from
the `fw-testing` group.

PR Close #37220
2020-05-20 13:31:07 -07:00
19313f7dad Revert "fix(elements): correctly handle getting/setting properties before connecting the element (#36114)"
This reverts commit 363f14c893
because it is causing the side effects test to break on the
9.1.x branch.
2020-05-20 11:07:39 -07:00
4e2500278c Revert "refactor(elements): simplify accessing NgElementStrategy (#36114)"
This reverts commit fa073bafbd
because it is causing the side effects test to break on the
9.1.x branch.
2020-05-20 11:07:17 -07:00
2c4cfa6f2c Revert "refactor(elements): remove unused imports and properties (#36114)"
This reverts commit 2ed66acdd9
because it is causing the side effects test to break on the
9.1.x branch.
2020-05-20 11:06:52 -07:00
c8f0c3b637 Revert "test(elements): only declare helpers if needed (#36114)"
This reverts commit 7e56222afb
because it is causing the side effects test to break on the
9.1.x branch.
2020-05-20 11:06:29 -07:00
cf4883240b Revert "test(elements): clean up TestStrategy between tests (#36114)"
This reverts commit d98d0dbf17
because it is causing the side effects test to break on the
9.1.x branch.
2020-05-20 11:06:08 -07:00
6f3157fe6d Revert "fix(elements): capture input properties set before upgrading the element (#36114)"
This reverts commit 1c8f1799d4
because it is causing the side effects test to break on the
9.1.x branch.
2020-05-20 11:05:46 -07:00
12d8af50dd Revert "fix(elements): do not break when the constructor of an Angular Element is not called (#36114)"
This reverts commit 87b9f08d3b
because it is causing the side effects test to break on the
9.1.x branch.
2020-05-20 11:04:55 -07:00
94fc8463cc release: cut the v9.1.8 release 2020-05-20 10:03:36 -07:00
87b9f08d3b fix(elements): do not break when the constructor of an Angular Element is not called (#36114)
Previously, the correct behavior of Angular custom elements relied on
the constructor being called (and thus the `injector` property being
initialized). However, some polyfills (e.g. `document-register-element`)
do not call the constructor of custom elements, which resulted in the
`injector` property being undefined and the `NgElementStrategy` failing
to be instantiated.

This commit fixes it by being tolerant to the `injector` property being
undefined and falling back to the injector passed to the
`createCustomElement()` config.

NOTE:
We don't have proper tests exercising the situation where the
constructor is not called. For now this is tested using a Google
internal test suite (which is how this issue was caught).
This commit also adds a rudimentary unit test to emulate this situation.

PR Close #36114
2020-05-20 09:43:38 -07:00
1c8f1799d4 fix(elements): capture input properties set before upgrading the element (#36114)
Previously, if an element started out as a regular `HTMLElement` (not a
Custom Element) and was later upgraded to a Custom Element, any
properties corresponding to component inputs that were set on the
element before upgrading it would not be captured correctly and thus not
reflected on the instantiated component.

This commit fixes it by ensuring that such properties are captured
correctly.

Jira issue: [FW-2006](https://angular-team.atlassian.net/browse/FW-2006)

Fixes #30848
Closes #31416

PR Close #36114
2020-05-20 09:43:38 -07:00
d98d0dbf17 test(elements): clean up TestStrategy between tests (#36114)
Previously, the `TestStrategy` `NgElementStrategy` used in
`createCustomElement()` tests was created once and re-used in each test
(due to complications related to how `customElements.register()` works).
As a result, the `TestStrategy` instance's state (e.g. inputs) could be
polluted from previous tests and affect subsequent ones.

This commit ensures the strategy instance is reset before each test.

PR Close #36114
2020-05-20 09:43:38 -07:00
7e56222afb test(elements): only declare helpers if needed (#36114)
Previously, helper modules/components classes were declared even if the
tests were not run (because the environment did not support Custom
Elements for example).

This commit moves the declaration of the helpers inside the `describe()`
block, so they are not declared unnecessarily. This is in preparation of
adding more helpers that need to access variables declared inside the
`describe()` block.

PR Close #36114
2020-05-20 09:43:37 -07:00
2ed66acdd9 refactor(elements): remove unused imports and properties (#36114)
- Remove imports that are not used.
- Remove private class properties that are not used outside of the
  constructor.

PR Close #36114
2020-05-20 09:43:37 -07:00
fa073bafbd refactor(elements): simplify accessing NgElementStrategy (#36114)
Previously, we had to check whether `NgElementStrategy` had been
instantiated before accessing it. This was tedious and error prone,
since it was easy to forget to add the check in new call sites.

This commit switches to using a getter, so that the check has to be
performed in one place and is transparent to call sites (including any
future ones).

PR Close #36114
2020-05-20 09:43:37 -07:00
363f14c893 fix(elements): correctly handle getting/setting properties before connecting the element (#36114)
`createCustomElements()` creates some getters/setters for properties
corresponding to component inputs that delegate to the
`NgElementStrategy`. However, it is not guaranteed that the element's
`NgElementStrategy` will have been created when these getters/setters
are called, because some polyfills (e.g. `document-register-element`) do
not call the constructor.

Previously, trying to get/set input properties before connecting the
element to the DOM (via `connectedCallback()`) would fail due to
`NgElementStrategy` not being created.

This commit ensures that the `NgElementStrategy` is always created
before used inside the input property getters/setters (similar to how it
is done for other methods of `NgElement`).

Mentioned in https://github.com/angular/angular/pull/31416/files#r300326698.

PR Close #36114
2020-05-20 09:43:37 -07:00
a9edbc3a4e docs(dev-infra): update .circleci/README.md (#37212)
The info about the pw storage is out of date.

We should really just point the reader to a go link, something like go/angular/passwords and keep
the info about secrets there.

PR Close #37212
2020-05-20 09:40:52 -07:00
e62fa67440 docs: remove Vikram and Alex from aio (#37212)
Vikram and Alex are now Angular alumni. They made a mark on Angular and I hope we'll
get to collaborate with them in the future.

PR Close #37212
2020-05-20 09:40:52 -07:00
57236f1e11 docs: fix broken link in DEVELOPER.md (#37212)
The TOC at the top refers to an anchor that does not exist.

This change adds the missing anchor while preserving the old one in case someone depends on it.

PR Close #37212
2020-05-20 09:40:52 -07:00
14ffecdf09 docs(dev-infra): update ngcontainer docs (#37185)
Update developer instructions by clarify and adding missing login details for the hub.docker.com image server.

PR Close #37185
2020-05-20 09:38:45 -07:00
9ae3e1199d fix(dev-infra): make commit-message-filter script executable for merge (#37209)
Marks the commit-mesage-filter.js file as executable, allowing it
to be run by 'git filter-branch' during the merge script process.

PR Close #37209
2020-05-19 15:35:28 -07:00
947f542185 build: fix deprecation notice in scripts/github/merge-pr script (#37208)
The deprecation notice for the merge-pr script errantly referenced
'ng dev' rather than 'ng-dev', this PR corrects this.

PR Close #37208
2020-05-19 15:30:20 -07:00
353195c9c5 feat(dev-infra): register ts-node when reading configuration (#37196)
`ts-node` is now an optional peer dependency of the shared dev-infra
package. Whenever a `ng-dev` command runs, and a TypeScript-based
configuration file exists, `ts-node` is set up if available.

That allows consumers of the package (as the components repo) to more
conveniently use a TypeScript-based configuration for dev-infra.

Currently, commands would need to be proxied through `ts-node`
which rather complicates the setup:

```
NG_DEV_COMMAND="ts-node ./node_modules/@angular/dev-infra-private/cli.js"
```

I'm thinking that it should be best-practice to use TypeScript for
writing the configuration files. Given that the tool is used primarily
in Angular projects (for which most sources are TypeScript), this should
be acceptable.

PR Close #37196
2020-05-19 15:21:58 -07:00
72505dfd6f build: Add deprecation notice to merge-pr script to nudge to new tooling (#37204)
Adds a deprecation notice to the old merge-pr script informing the
user the script will be removed in favor of the ng-dev merge tooling.
This currently serves as a warning, and does not fail to perform the
merge.

PR Close #37204
2020-05-19 15:04:29 -07:00
a63f634592 docs: remove https://angular.io from internal links (#37157)
PR #36601 itroduces icons on all links if the link contains https:// or http:// but there were some internal links left which contained https://angular.io. Removed https://angular.io from all these links.

PR Close #37157
2020-05-19 10:11:42 -07:00
18d08a3857 refactor(docs-infra): remove rev property from Resource interface and data (#37181)
The `rev` property has been in the initial commit that introduced
`resources.json` (196203f6d7) and has been
added to all new entries since (always with the value `true`). This
field is a remnant from back when this data was stored in a Firebase
Database and the `rev` field indicated whether the entry has been
reviewed/approved by a DevRel lead or something. Now that the data is
kept in the repository and the reviewing is done as part of the
corresponding PR, this field is no longer necessary.

PR Close #37181
2020-05-19 10:10:57 -07:00
c4234577db docs(router): corrected minor typo (#37166)
This commit corrects a typo and rephrases the first sentence of the `redirectTo`
description to be more understandable.

PR Close #37166
2020-05-18 16:23:32 -07:00
4830d664da build(docs-infra): upgrade cli command docs sources to 7653f8616 (#37155)
Updating [angular#9.1.x](https://github.com/angular/angular/tree/9.1.x) from [cli-builds#9.1.x](https://github.com/angular/cli-builds/tree/9.1.x).

##
Relevant changes in [commit range](98de22823...7653f8616):

**Modified**
- help/e2e.json

PR Close #37155
2020-05-18 16:22:18 -07:00
e26879ad1d feat(dev-infra): create rebase-pr tool in ng-dev (#37055)
Creates a tool in ng-dev which rebases a PR automatically and pushes
the rebase commits back to the PR.  This is meant to be a replacement
to the local merge script currently in the repo and currently has
feature parity.

PR Close #37055
2020-05-18 16:18:33 -07:00
c5c3113f00 build: remove local rebase-pr script (#37055)
Remove the local rebase-pr script with a deprecation message
informing the user to instead perform rebases via ng-dev

PR Close #37055
2020-05-18 16:18:33 -07:00
178a78f9ed docs(router): add docs for RouterLink inputs (#37018)
The RouterLink and RouterLinkWithHref inputs do not have any docs. This comment adds jsdoc comments to the inputs.

PR Close #37018
2020-05-18 14:55:42 -07:00
7de7871dd9 fix(router): update type for routerLink to include null and undefined (#37018)
PR #13380 added support for `null` and `undefined` but the type on the parameter was not updated.
This would result in a compilation error if `fullTemplateTypeCheck` is enabled.
Fixes #36544

PR Close #37018
2020-05-18 14:55:42 -07:00
784cf46a93 ci: update components repo unit tests job commit (#37183)
Updates the commit the `components-repo-unit-tests` job runs
against. The goal is that we run against a revision that at
least contains: https://github.com/angular/components/pull/19273.

The new commit contains fixes for a flaky test in the datepicker that we
saw failing in the components-repo-unit-tests job too:
https://app.circleci.com/pipelines/github/angular/angular/15359/workflows/27ffae7c-a7b8-46a3-9c9e-6dd22ca4734d/jobs/712643.

PR Close #37183
2020-05-18 14:50:15 -07:00
ba9e63a278 fix(dev-infra): incorrect base revision for merge script autosquash (#37184)
As mentioned in the previous commit, the autosquash strategy has
not been used in the components repo, so we could easily regress.

After thorough manual testing of the autosquash strategy again,
now that the merge script will be moved to framework, it came
to mind that there is a bug with the base revision in the
autosquash merge strategy. The problem is that the base revision
of a given PR is relying on the amount of commits in a PR.

This is prone to error because the amount of commits could easily
change in the autosquash merge strategy, because fixup or squash
commits will be collapsed. Basically invalidating the base revision.

To fix this, we fixate the base revision by determining the actual
SHA. This one is guaranteed to not change after the autosquash rebase.

The current merge script in framework fixates the revision by creating
a separate branch, but there is no benefit in that, compared to just
using an explicit SHA that doesn't need to be cleaned up..

PR Close #37184
2020-05-18 11:55:32 -07:00
83231f2174 fix(dev-infra): merge script autosquash should be interactive (#37184)
The components repo does not use the autosquash merge strategy, so
recent changes to that seem to broke the autosquash strategy.

Since we don't run the rebase in interactive mode, the `--autosquash`
flag has no effect. This is by design in Git. We can make it work by
setting the git sequence editor to `true` so that the rebase seems
like an interactive one to Git, while it isn't one for the user.

This matches conceptually with the merge script currently used in
framework. The only difference is that we allow a real interactive
rebase if the `commit message fixup` label is applied. This allows
commit message modifications (and others) if needed.

PR Close #37184
2020-05-18 11:55:32 -07:00
62e3196849 build: configure dev-infra merge script (#37184)
Sets up the dev-infa merge script in the framework ng-dev configuration
file. This allow us to use the script in the future.

PR Close #37184
2020-05-18 11:55:31 -07:00
127a06c031 feat(dev-infra): integrate merge script into ng-dev cli (#37184)
Integrates the merge script into the `ng-dev` CLI. The goal is that
caretakers can run the same command across repositories to merge a pull
request. The command is as followed: `yarn ng-dev pr merge <number>`.

PR Close #37184
2020-05-18 11:55:31 -07:00
d4e00ee5a1 feat(dev-infra): move merge script over from components repo (#37184)
Moves the merge script from the components repository over
to the shared dev-infra package. The merge script has been
orginally built for all Angular repositories, but we just
kept it in the components repo temporarily to test it.

Since everything went well on the components side, we now
move the script over and integrate it into the dev-infra package.

PR Close #37184
2020-05-18 11:55:31 -07:00
6db8e7eb8f ci: fix a typo in payload size limit config (#37151)
This commit fixes a small typo in the payload size limit config introduced in 639fab2c14.

PR Close #37151
2020-05-18 11:30:42 -07:00
ee1ef8512f refactor(docs-infra): refactors createOverviewDump (#37141)
This commit removes the dependency on the `lodash` module and refactors
the `createOverviewDump` method.

PR Close #37141
2020-05-18 10:31:07 -07:00
efef04487c build: complete removal of gulp format (#37147)
Remove gulp format deprecation message to complete the removal of
formatting Typescript and Javascript files via gulp command.

PR Close #37147
2020-05-18 10:27:02 -07:00
74d8b2308b fix(dev-infra): misc fixes for the compare master and patch script (#37150)
This commit includes a couple minor fixes for the script that compares master and patch branch:
- take only relevant release commit into account while generating the diff
- fix the initial version display (avoid '+' sign from being added)
- removes obsolete parameter that was needed for v9.0.x branch only

PR Close #37150
2020-05-18 10:26:22 -07:00
7543e5925e refactor(docs-infra): refactors extractDecoratedClasses (#37135)
This commit removes the dependency on the `lodash` module and refactors
the `extractDecoratedClasses` method.

PR Close #37135
2020-05-18 10:25:31 -07:00
5ca8ba3be9 docs: supply correct manifest.webmanifest filename (#37146)
Previously, the docs incorrectly identified the generated manifest file as "manifest.json" when it is actually "manifest.webmanifest".
PR Close #37146
2020-05-18 10:24:55 -07:00
3f70e3df3b docs(docs-infra): improve docs on creating/updating the preview server Docker image (#37015)
This commit includes the following improvements:

- Document that the `create-image.sh` script (and by extention the
  `update-preview-server.sh` script) need to have access to a `yarn`
  executable.

- Add a note on cron jobs running in non-interactive, non-login shells
  (which affects their execution context and have different behavior vs
  running the same commands in an interactive, login shell).

- Change the Node.js and `yarn` installation instructions to ensure the
  `yarn` executable will be available on the `PATH` and not require an
  interactive, login shell (as happens, for example, when installing it
  via [nvm](https://github.com/nvm-sh/nvm)). This makes it easier to set
  up a cron job that runs the `update-preview-server.sh` script.

NOTE: The equivalent updates have been made on the GCE VM that hosts the
      PR preview server.

PR Close #37015
2020-05-18 10:24:21 -07:00
0b68189051 fix(docs-infra): better detect failed attempts to update the preview server Docker container (#37015)
In order to avoid unnecessary operations, the `update-preview-server.sh`
script, that is used to update the PR preview server Docker container,
will only try to update the Docker container if either any files in the
`aio/aio-builds-setup/` directory have changed since the last update or
if a previous update failed. A failed previous update is detected by
checking whether the temporary `aio-builds:provisional` Docker image
still exists. This temporary image is created during the update
operation and is renamed to `aio-builds:latest` if the update goes well.

Previously, the update script was not able to detect a previous failed
attempt if the operation failed before creating the
`aio-builds:provisional` Docker image, such as if the `create-image.sh`
script failed. This could lead to a situation where the preview server
Docker container would not be updated after a failed attempt.

This commit improves the logic for detecting failed attempts to account
for this type of failures. It does this by not removing an older
`aio-builds:provisional` Docker image until a new one is successfully
created.

NOTE: While this is not full-proof, it is an improvement as it
      eliminates a certain kind of failures.

PR Close #37015
2020-05-18 10:24:21 -07:00
b811212013 docs: grammatical error in ngcc (#37156)
The glossary.md file had a gramatical mistake. Added a that in ngcc definition to make the grammatical mistake right

PR Close #37156
2020-05-18 10:23:52 -07:00
4d0b831d09 build: add kara to public-api approvers (#37143)
This commit adds Kara (me) to the public-api group
so that we can start to divide the public-api code
reviews among more people.

PR Close #37143
2020-05-15 16:59:32 -07:00
2cf6b809ae build: move CHANGELOG to public-api approval group (#37143)
Previously, the CHANGELOG file was caught under the general
"*" glob, which meant that it fell under the dev-infra
approval group. This does not really make sense since it
has more to do with public-facing changes. As such, this
commit moves the CHANGELOG file specifically to the
public-api approval group.

PR Close #37143
2020-05-15 16:59:31 -07:00
639fab2c14 refactor(core): remove _tViewNode field from ViewRef (#36814)
The _tViewNode field (that was marked as internal) on the ViewRef is not
necessery as a reference to a relevant TView is available as a local
variable.

PR Close #36814
2020-05-15 15:43:02 -07:00
0912be47ff feat(dev-infra): create tool to determine conflicts created by a PR (#37051)
Creates a tool in ng-dev to determine the PRs which become conflicted
by merging a specified PR.  Often the question is brought up of how
many PRs require a rebase as a result of a change.  This script allows
to determine this impact.

PR Close #37051
2020-05-15 11:29:36 -07:00
9548c7cb94 release: cut the v9.1.7 release (#37116)
Note: This commit was applied to the 9.1.x branch
out of order because local changes were not pushed
directly after the 9.1.7 release. You can see what
is actually included in 9.1.7 using its tag here:
https://github.com/angular/angular/commits/9.1.7

PR Close #37116
2020-05-15 10:45:27 -07:00
de5449380f docs(core): fix typo in advanced di doc (#36634) (#37116)
This PR fixes a typo in the advanced DI doc, specifically
in the function name `createComponentInjector`.

PR Close #36634

PR Close #37116
2020-05-15 10:45:27 -07:00
5ffa4f12e5 refactor(docs-infra): refactors checkUnbalancedBackTicks (#37065)
This commit removes the dependency on the `lodash` module and refactors
the `checkUnbalancedBackTicks` method.

PR Close #37065
2020-05-15 10:13:20 -07:00
9022091dc6 build: remove skipLibCheck from AIO examples (#37128)
`skipLibCheck` had to be added to a few AIO examples, because of a breaking change in master. These changes update to a newer version of `angular-in-memory-web-api` that accounts for the breaking change.

PR Close #37128
2020-05-15 10:05:27 -07:00
74878984b2 build(docs-infra): remove {@searchKeywords} from content when rendering (#37132)
Previously this inline-tag-def was returning the `doc` which is rendered
to the output as `[Object Object]` - obviously not what is intended.

Now it returns `''` which effectively strips the tag handler from the
rendered output.

PR Close #37132
2020-05-15 10:04:46 -07:00
6a6fbc3890 build: add github config for ng-dev configuration (#37097)
Adds the gitub configuration to the ng-dev configuration. This github
configuration provides information needed for making API requests to
github.  Upcoming tooling related PRs will require these API requests
being possible.

PR Close #37097
2020-05-15 10:02:42 -07:00
cb888afd0c feat(dev-infra): add github to common config for ng-dev configuration typings (#37097)
Adds a gitub object to the common configuration for ng-dev. This github
configuration provides information needed for making API requests to
github.

PR Close #37097
2020-05-15 10:02:41 -07:00
48cf61cf45 docs: move lazy loading and preloading tasks from router to lazy loading doc (#36748)
After rewriting much of the router doc, it became apparent that the lazy loading/preloading information should be in the lazy loading doc rather than in the router doc. There is now instead a short section that touches on lazy loading but links to the lazy learning document instead of covering it in detail in the router doc.

PR Close #36748
2020-05-15 10:01:53 -07:00
d808333047 ci: increase AIO payload size limit (#37124)
This commit updates payload size limit that is triggering errors after merging cda2530. That commit seems to contribute to the payload size increase, but all checks were "green" for the original PR (#35889), so it looks like it's an accumulated payload size increase from multiple changes. The goal of this commit is to bring the patch branch back to "green" state.

PR Close #37124
2020-05-14 19:19:56 -07:00
f872b69d3b fix(core): Host classes should not be fed back into @Input (#35889)
Previously all static styling information (including the ones from component/directive host bindings) would get merged into a single value before it would be written into the `@Input('class'/'style')`. The new behavior specifically excludes host values from the `@Input` bindings.

Fix #35383

PR Close #35889
2020-05-14 15:54:13 -07:00
9cca82ed70 refactor(core): rename refreshDynamicEmbeddedViews to refreshEmbeddedViews (#37117)
Dynamic embedded views were conceptually different from inline embedded views, but we have since
removed the inline embedded views so we now only have "embedded views".
See related refactoring work to remove inline embedded views in #34715
and #37073.

PR Close #37117
2020-05-14 15:50:53 -07:00
e3d33958d2 fix(core): inheritance delegate ctor regex updated to work on minified code (#36962)
If one component Parent inherit another component Base like the following:

@Component(...)
class Base {
  constructor(@Inject(InjectionToken) injector: Injector) { }
}

@Component(...)
class Parent extends Base {
    // no constructor
}

When creating Component Parent, the dependency injection should work on delegating ctors like above.

The code Parent code above will be compiled into something like:

class Parent extends Base {
  constructor() {
    super(...arguments);
  }
}

The angular core isDelegateCtor function will identify the delegation ctor to the base class.

But when the code above is minified (using terser), the minified code will compress the spaces, resulting in something like:
class Parent extends Base{constructor(){super(...arguments)}}

The regex will stop working, since it wasn't aware of this case. So this fix will allow this to work in minified code cases.

PR Close #36962
2020-05-14 15:49:47 -07:00
4de546eef5 test(core): verify that Ivy i18n works correctly with HTML namespaces (#36943)
This commit adds several tests to verify that i18n logic in Ivy handles elements with HTML namespaces correctly.

Related to #36941.

PR Close #36943
2020-05-14 15:20:43 -07:00
719ca5283b fix(dev-infra): do not require a commit body for release commits (#37110)
Release commits do not require a commit body as the context, usually
provided in commit body, is already available in the process of
releasing.  No additional value is gained from adding a body message
on these commits.

PR Close #37110
2020-05-14 13:03:35 -07:00
3813f54bec docs(router): filter the event to subscribe (#37027)
The current code will not work as the `e` will be an event,
If we try to access e.id and e.url it will throw an exception, the correct way is to use map or filter down to specific events

PR Close #37027
2020-05-14 12:01:47 -07:00
92590693e6 docs: clarifies the service limitation (#36349)
Closes #36332

PR Close #36349
2020-05-14 11:59:45 -07:00
ef9da2f619 refactor(dev-infra): add atscott for fw-core approvers (#37020)
Add atscott (Andrew Scott) to the list of approvers in the fw-core section of the .pullapprove.yml file.

PR Close #37020
2020-05-14 11:00:21 -07:00
23f380d6b9 refactor(docs-infra): refactors addImageDimensionsImpl (#37046)
This commit changes the `addImageDimensionsImpl` method to reduce
nesting. It will exit early, if the conditions are not matched.

PR Close #37046
2020-05-14 10:48:02 -07:00
92b97f7d60 docs(common): English grammar (#36908)
In English grammar, the comma followed by a conjunction is considered incorrect. This kind of mistakes called comma splice.
See: https://www.grammarly.com/blog/comma-splice/
PR Close #36908
2020-05-13 16:03:25 -07:00
d12ef0c0a7 docs: correct typos in doc for template type guards (#37090)
correct some small typos in new feature recommendation in stuctural-directives.md and reference from aot-compiler.md

PR Close #37090
2020-05-13 16:01:21 -07:00
4827dbe308 docs: add Cindy to contributors (#37076)
Cindy has been working with us for a while now
and should be listed on AIO as a part of our team.
This commit adds her image / bio to the Angular
contributors page.

PR Close #37076
2020-05-13 16:01:01 -07:00
04cd4187ce refactor(core): remove ActiveIndexFlag from LContainer (#37073)
The ActiveIndexFlag is no longer needed because we no longer have "inline embedded views".
There is only one type of embedded view so we do not need complex tracking for
inline embedded views.

HAS_TRANSPLANTED_VIEWS now takes the place of the ACTIVE_INDEX slot as a
simple boolean rather than being a shifted flag inside the ACTIVE_INDEX bits.

PR Close #37073
2020-05-13 16:00:41 -07:00
bb5e455ab2 docs: Remove duplicate and (#37067)
This commit removes the duplicate `and` found in the
`Support for the development cycle` section part of the
getting started guide.

Fixes #37060

PR Close #37067
2020-05-13 15:59:42 -07:00
96787f2e40 build: fix assumed aliases in rebase-pr script (#37050)
The local rebase-pr script assumes the existence of specific git
aliases.  Instead this script should rely on the full written out
command instead.

PR Close #37050
2020-05-13 15:59:20 -07:00
4f1b7a4a6a feat(dev-infra): introduce validators for ng-dev config loading (#37049)
Introduces infrastructure to validate configuration of the ng-dev
command at run time.  Allowing for errors to be returned to the
user running the command.

PR Close #37049
2020-05-13 15:58:47 -07:00
d113bfa14f refactor(compiler-cli): expose loadTestDirectory() test helper (#36843)
This helper can be useful in other packages to load files from the
real disk into a mock file system.

PR Close #36843
2020-05-13 15:52:49 -07:00
a8aa2caeb6 refactor(compiler-cli): support Buffer files in FileSystem (#36843)
Adding `readFileBuffer()` method and allowing `writeFile()` to accept a
Buffer object will be useful when reading and writing non-text files,
such as is done in the `@angular/localize` package.

PR Close #36843
2020-05-13 15:52:49 -07:00
f20c725406 docs: replaces on with of (#36773)
Closes #36770
PR Close #36773
2020-05-13 15:52:08 -07:00
caafd7e0a3 docs: minor typos (#36772)
Closes #36771
PR Close #36772
2020-05-13 15:51:47 -07:00
f59f5a2ab3 docs: refactor lifecycle hooks doc topic for task orientation (#36353)
rework lifecycle-hooks.md to meet current documentation standards and conventions
add "content projection" to glossary

PR Close #36353
2020-05-13 15:51:22 -07:00
b91b3d7462 docs(common): fix CLDR website link in FormStyle doc (#37069)
The `FormStyle` enum offers two options, and the explanation of the difference between the two can be found on the CLDR official website. Sadly, the link changed and the one currently referenced is a dead-end. This commit fixes the link.

PR Close #37069
2020-05-12 14:41:21 -07:00
a235daee71 docs(platform-webworker): remove mention of version 10 (#37052)
The deprecation notice for platform-webworker
APIs is too prescriptive and notes that we will
remove the package in version 10. Since we are
not planning to do this for version 10, this
commit updates the notice to read "a future
version of Angular".

PR Close #37052
2020-05-12 14:40:57 -07:00
77218d6974 docs: fixes ngOnit routing variable (#36905)
This commit fixes the routing variable in the `ngOnit` life-cyle event from
`activatedRoute` to `route`.

Closes #36885

PR Close #36905
2020-05-12 14:40:25 -07:00
928dc8f263 docs: add doc for template type guards to stuctural-directives.md (#34549)
add new feature recommendation to existing content
include references from aot-compiler.md and template-typecheck.md

PR Close #34549
2020-05-12 14:40:02 -07:00
468aeb454a docs: minor typo fix in the bazel documentation (#37043)
This commit fixes a minor typo issue within the bazel documentation and
removes unnecessary spacing.

PR Close #37043
2020-05-12 10:51:28 -07:00
b9dd2b45af build(dev-infra): cleanup package dependencies for shared package (#36980)
As per our discussion in the dev-infra sync meeting, we don't want
to have all dependencies show up as peer dependencies. Instead, we
only want to have larger dependencies such as `typescript` or buildifier
as peer dependencies. Tslib is also included for the sake of it being
generally a peer dependency of all Angular framework packages.

The rationale is that Yarn is smart enough to collapse packages
if all satisfy a given range. This means that we don't necessarily
need to have all dependencies as peer dependencies. The initial
idea was to keep all dependencies as peer dependencies so that
we have control over duplication of packages as downloading multiple
packages w/ different versions impacts local dev, CI and caches.

At the same time though, we don't want to bother with setting
up peer dependencies all the time. Not every consumer of the
shared dev-infra package would like to manually specify `yaml`
or `multimatch` etc. in the project `package.json`. Hence we
decided to go with a hybrid approach where only more impactful
dependencies are peer dependencies, and other smaller ones can
be standard depdencies that are usually collapsed by Yarn anyway.

Also this commit removes tslib from build targets that don't
rely on it.

PR Close #36980
2020-05-12 10:51:06 -07:00
2ab32c870f docs(forms): remove rxjs of alias import in form async validator example (#36856)
In the code example of the AsyncValidator example there was an aliased
import for the rxjs operator `of`. To align with the RxJS docs it should
just use a plain import of `of`

PR Close #36856
2020-05-12 10:50:40 -07:00
53897d4a84 docs: refactor dynamic forms topic as tutorial (#36465)
rework content to meet current documentation standards and conventions, structure as tutorial document

PR Close #36465
2020-05-12 10:49:24 -07:00
efe1713371 fix: add aikidave as reviewer for DOCS: Marketing (#37014)
It makes sense that the lead technical writer should be allowed
to approve changes to DOCS: Marketing. The inspriration for this
change came to the current lead technical writer in a vision, or
perhaps it was a fugue state caused by too little caffeine. No
one knows. It's still a good idea though.

PR Close #37014
2020-05-11 11:08:25 -07:00
24b9e12383 refactor(language-service): move TS utils to separate file (#36984)
This commit refactors TS-only utility functions to a separate file so
that they could be shared with Ivy language service.
A separate ts_library rule is created so that there is no dependency on
`compiler` and `compiler-cli` to make the compilation fast and
light-weight.
The method `getPropertyAssignmentFromValue` is modified slightly to
improve the ergonomics of the function.

PR Close #36984
2020-05-11 11:07:51 -07:00
ad3fbe1ea0 refactor(ngcc): change async locker timeout to 250 secs (#36979)
Previously the `AsyncLocker` was configured to only wait
50x500ms before timing out. This is 25secs, which is often
less than a normal run of ngcc, so the chance of a timeout
flake was quite high.

The timeout is now 500x500ms, which is 250secs.

PR Close #36979
2020-05-11 11:00:52 -07:00
f0ce3c2ce5 docs: refactor form validation doc to be task-oriented (#36374)
rework content to meet current documentation standards and conventions, structure as task-oriented document

PR Close #36374
2020-05-11 10:59:50 -07:00
ae989b8cb7 refactor(core): rename instructions/container.ts -> instructions/template.ts (#34715)
Prior to this change, the `template` instruction logic was located in the `instructions/container.ts` file alongside embedded view instructions. Since unused embedded view instructions are removed in a previous commit, this commit renames `container.ts` -> `template.ts`, since only template-related instructions were retained.

PR Close #34715
2020-05-11 10:52:29 -07:00
edcc8b8acf refactor(core): remove unused embedded view instructions (#34715)
This commit performs a cleanup and removes unused embedded view instructions and corresponding tests.

PR Close #34715
2020-05-11 10:52:29 -07:00
790af88288 docs: add migration guides to sidenav in updating to Version 9 (#34979)
All migration guides did not have a direct link to access them so added them to the side nav in the section updating to version 9 for direct access, it also helps to add right side nav to these migration guides

Fixes #33582

PR Close #34979
2020-05-08 14:45:17 -07:00
6eef7dbdbe fix(docs-infra): add visual signal for external links (#36601)
Clicking on a link may take us to an extenal source, which may lead to user leaving angular.io unintentionally.

Added visual cues on external links so that user knows which links are external and which are intenal to angular.io.

Fixes #17620

PR Close #36601
2020-05-08 14:43:20 -07:00
ba1dfd0cf3 feat(compiler): add name spans for property reads and method calls (#36826)
ASTs for property read and method calls contain information about
the entire span of the expression, including its receiver. Use cases
like a language service and compile error messages may be more
interested in the span of the direct identifier for which the
expression is constructed (i.e. an accessed property). To support this,
this commit adds a `nameSpan` property on

- `PropertyRead`s
- `SafePropertyRead`s
- `PropertyWrite`s
- `MethodCall`s
- `SafeMethodCall`s

The `nameSpan` property already existed for `BindingPipe`s.

This commit also updates usages of these expressions' `sourceSpan`s in
Ngtsc and the langauge service to use `nameSpan`s where appropriate.

PR Close #36826
2020-05-08 14:42:43 -07:00
b37071c22f build: migrate to typescript file for ng-dev config (#37017)
Migrate to use a typescript file rather than a javascript file for
defining the ng-dev config file.  This will allow long term for the
config to rely on the types while writing the config, avoiding
errors.

PR Close #37017
2020-05-08 14:40:12 -07:00
b6cba54821 feat(dev-infra): support typescript config file (#37017)
Previously ng-dev loaded the config through a javascript file, this
change allows for the loaded file to be either javascript or
typescript.  This enables configurations to be written with type
safety.

PR Close #37017
2020-05-08 14:40:12 -07:00
259 changed files with 6113 additions and 5537 deletions

View File

@ -12,8 +12,8 @@ We use this as a symmetric AES encryption key to encrypt tokens like
a GitHub token that enables publishing snapshots.
To create the github_token file, we take this approach:
- Find the angular-builds:token in http://valentine
- Find the angular-builds:token in the internal pw database
- Go inside the CircleCI default docker image so you use the same version of openssl as we will at runtime: `docker run --rm -it circleci/node:10.12`
- echo "https://[token]:@github.com" > credentials
- openssl aes-256-cbc -e -in credentials -out .circleci/github_token -k $KEY
- If needed, base64-encode the result so you can copy-paste it out of docker: `base64 github_token`
- If needed, base64-encode the result so you can copy-paste it out of docker: `base64 github_token`

View File

@ -32,8 +32,8 @@ var_4_win: &cache_key_win_fallback v6-angular-win-node-12-{{ checksum ".bazelver
# Cache key for the `components-repo-unit-tests` job. **Note** when updating the SHA in the
# cache keys also update the SHA for the "COMPONENTS_REPO_COMMIT" environment variable.
var_5: &components_repo_unit_tests_cache_key v6-angular-components-598db096e668aa7e9debd56eedfd127b7a55e371
var_6: &components_repo_unit_tests_cache_key_fallback v6-angular-components-
var_5: &components_repo_unit_tests_cache_key v7-angular-components-caad0b54ed41949f0ee529c152508749875bc9af
var_6: &components_repo_unit_tests_cache_key_fallback v7-angular-components-
# Workspace initially persisted by the `setup` job, and then enhanced by `build-npm-packages` and
# `build-ivy-npm-packages`.

View File

@ -74,7 +74,7 @@ setPublicVar COMPONENTS_REPO_TMP_DIR "/tmp/angular-components-repo"
setPublicVar COMPONENTS_REPO_URL "https://github.com/angular/components.git"
setPublicVar COMPONENTS_REPO_BRANCH "master"
# **NOTE**: When updating the commit SHA, also update the cache key in the CircleCI `config.yml`.
setPublicVar COMPONENTS_REPO_COMMIT "598db096e668aa7e9debd56eedfd127b7a55e371"
setPublicVar COMPONENTS_REPO_COMMIT "caad0b54ed41949f0ee529c152508749875bc9af"
####################################################################################################

View File

@ -1,3 +1,5 @@
import {MergeConfig} from './dev-infra/pr/merge/config';
// The configuration for `ng-dev commit-message` commands.
const commitMessage = {
'maxLength': 120,
@ -72,8 +74,50 @@ const format = {
'buildifier': true
};
/** Github metadata information for `ng-dev` commands. */
const github = {
owner: 'angular',
name: 'angular',
};
// Configuration for the `ng-dev pr merge` command. The command can be used
// for merging upstream pull requests into branches based on a PR target label.
const merge = () => {
// TODO: resume dynamically determining patch branch
const patch = '10.0.x';
const config: MergeConfig = {
githubApiMerge: false,
claSignedLabel: 'cla: yes',
mergeReadyLabel: /^PR action: merge(-assistance)?/,
commitMessageFixupLabel: 'commit message fixup',
labels: [
{
pattern: 'PR target: master-only',
branches: ['master'],
},
{
pattern: 'PR target: patch-only',
branches: [patch],
},
{
pattern: 'PR target: master & patch',
branches: ['master', patch],
},
],
requiredBaseCommits: {
// PRs that target either `master` or the patch branch, need to be rebased
// on top of the latest commit message validation fix.
'master': '4341743b4a6d7e23c6f944aa9e34166b701369a1',
[patch]: '2a53f471592f424538802907aca1f60f1177a86d'
},
};
return config;
};
// Export function to build ng-dev configuration object.
module.exports = {
commitMessage,
format,
github,
merge,
};

View File

@ -351,6 +351,7 @@ groups:
users:
- alxhub
- AndrewKushnir
- atscott
- kara
- mhevery
- pkozlowski-opensource
@ -567,7 +568,7 @@ groups:
- *can-be-global-approved
- *can-be-global-docs-approved
- >
contains_any_globs(files, [
contains_any_globs(files.exclude('packages/compiler-cli/**'), [
'**/testing/**',
'aio/content/guide/testing.md',
'aio/content/examples/testing/**',
@ -776,10 +777,10 @@ groups:
])
reviewers:
users:
- aikidave
- IgorMinar
- StephenFluin
# =========================================================
# Docs: Observables
# =========================================================
@ -946,7 +947,7 @@ groups:
conditions:
- *can-be-global-approved
- >
contains_any_globs(files, [
contains_any_globs(files.exclude("CHANGELOG.md"), [
'*',
'.circleci/**',
'.devcontainer/**',
@ -1018,6 +1019,7 @@ groups:
- >
contains_any_globs(files, [
'goldens/public-api/**',
'CHANGELOG.md',
'docs/NAMING.md',
'aio/content/guide/glossary.md',
'aio/content/guide/styleguide.md',
@ -1027,6 +1029,7 @@ groups:
reviewers:
users:
- IgorMinar
- kara
# ================================================

View File

@ -1,3 +1,69 @@
<a name="9.1.12"></a>
## [9.1.12](https://github.com/angular/angular/compare/9.1.11...9.1.12) (2020-07-08)
### Bug Fixes
* **core:** infinite loop if injectable using inheritance has a custom decorator ([6c1ab47](https://github.com/angular/angular/commit/6c1ab47)), closes [#35733](https://github.com/angular/angular/issues/35733)
<a name="9.1.11"></a>
## [9.1.11](https://github.com/angular/angular/compare/9.1.10...9.1.11) (2020-06-10)
### Reverts
* **elements:** fire custom element output events during component initialization ([dc9da17](https://github.com/angular/angular/commit/dc9da17))
<a name="9.1.10"></a>
## [9.1.10](https://github.com/angular/angular/compare/9.1.9...9.1.10) (2020-06-09)
### Bug Fixes
* **elements:** fire custom element output events during component initialization ([454e073](https://github.com/angular/angular/commit/454e073)), closes [/github.com/angular/angular/blob/c0143cb2abdd172de1b95fd1d2c4cfc738640e28/packages/elements/src/create-custom-element.ts#L167-L170](https://github.com//github.com/angular/angular/blob/c0143cb2abdd172de1b95fd1d2c4cfc738640e28/packages/elements/src/create-custom-element.ts/issues/L167-L170) [/github.com/angular/angular/blob/c0143cb2abdd172de1b95fd1d2c4cfc738640e28/packages/elements/src/create-custom-element.ts#L164](https://github.com//github.com/angular/angular/blob/c0143cb2abdd172de1b95fd1d2c4cfc738640e28/packages/elements/src/create-custom-element.ts/issues/L164) [/github.com/angular/angular/blob/c0143cb2abdd172de1b95fd1d2c4cfc738640e28/packages/elements/src/component-factory-strategy.ts#L158](https://github.com//github.com/angular/angular/blob/c0143cb2abdd172de1b95fd1d2c4cfc738640e28/packages/elements/src/component-factory-strategy.ts/issues/L158) [#36141](https://github.com/angular/angular/issues/36141)
### Performance Improvements
* **ngcc:** cache parsed tsconfig between runs ([1aae94a](https://github.com/angular/angular/commit/1aae94a)), closes [#37417](https://github.com/angular/angular/issues/37417) [#36882](https://github.com/angular/angular/issues/36882)
<a name="9.1.9"></a>
## [9.1.9](https://github.com/angular/angular/compare/9.1.8...9.1.9) (2020-05-20)
This release contains a re-submit of the following 3 commits with fixes for TS 3.8.
### Bug Fixes
* **elements:** capture input properties set before upgrading the element ([#36114](https://github.com/angular/angular/issues/36114)) ([#37226](https://github.com/angular/angular/issues/37226)) ([a33cb2d](https://github.com/angular/angular/commit/a33cb2d)), closes [#30848](https://github.com/angular/angular/issues/30848) [#31416](https://github.com/angular/angular/issues/31416)
* **elements:** correctly handle getting/setting properties before connecting the element ([#36114](https://github.com/angular/angular/issues/36114)) ([#37226](https://github.com/angular/angular/issues/37226)) ([6ac0042](https://github.com/angular/angular/commit/6ac0042)), closes [/github.com/angular/angular/pull/31416/files#r300326698](https://github.com//github.com/angular/angular/pull/31416/files/issues/r300326698)
* **elements:** do not break when the constructor of an Angular Element is not called ([#36114](https://github.com/angular/angular/issues/36114)) ([#37226](https://github.com/angular/angular/issues/37226)) ([1465372](https://github.com/angular/angular/commit/1465372))
<a name="9.1.8"></a>
## [9.1.8](https://github.com/angular/angular/compare/9.1.6...9.1.8) (2020-05-20)
### Bug Fixes
* **core:** Host classes should not be fed back into `@Input` ([#35889](https://github.com/angular/angular/issues/35889)) ([f872b69](https://github.com/angular/angular/commit/f872b69)), closes [#35383](https://github.com/angular/angular/issues/35383)
* **core:** inheritance delegate ctor regex updated to work on minified code ([#36962](https://github.com/angular/angular/issues/36962)) ([e3d3395](https://github.com/angular/angular/commit/e3d3395))
* **elements:** capture input properties set before upgrading the element ([#36114](https://github.com/angular/angular/issues/36114)) ([1c8f179](https://github.com/angular/angular/commit/1c8f179)), closes [#30848](https://github.com/angular/angular/issues/30848) [#31416](https://github.com/angular/angular/issues/31416)
* **elements:** correctly handle getting/setting properties before connecting the element ([#36114](https://github.com/angular/angular/issues/36114)) ([363f14c](https://github.com/angular/angular/commit/363f14c)), closes [/github.com/angular/angular/pull/31416/files#r300326698](https://github.com//github.com/angular/angular/pull/31416/files/issues/r300326698)
* **elements:** do not break when the constructor of an Angular Element is not called ([#36114](https://github.com/angular/angular/issues/36114)) ([87b9f08](https://github.com/angular/angular/commit/87b9f08))
* **router:** update type for routerLink to include null and undefined ([#37018](https://github.com/angular/angular/issues/37018)) ([7de7871](https://github.com/angular/angular/commit/7de7871)), closes [#13380](https://github.com/angular/angular/issues/13380) [#36544](https://github.com/angular/angular/issues/36544)
<a name="9.1.7"></a>
## [9.1.7](https://github.com/angular/angular/compare/9.1.6...9.1.7) (2020-05-13)
This release contains various API docs improvements.
<a name="9.1.6"></a>
## [9.1.6](https://github.com/angular/angular/compare/9.1.5...9.1.6) (2020-05-08)

View File

@ -4,9 +4,8 @@
## Install git, Node.js and yarn
- `sudo apt-get update`
- `sudo apt-get install -y git`
- Install [nvm](https://github.com/nvm-sh/nvm#installing-and-updating).
- Install Node.js: `nvm install 12`
- Install yarn: `npm install --global yarn`
- Install the latest stable version of [Node.js](https://nodejs.org/en/download).
- Install the latest stable version of [yarn](https://classic.yarnpkg.com/en/docs/install).
## Checkout repository
@ -18,7 +17,11 @@
- You can overwrite the default environment variables inside the image, by passing new values using
`--build-arg`.
**Note:** The script has to execute docker commands with `sudo`.
**Note 1:** The script has to execute docker commands with `sudo`.
**Note 2:**
The script has to execute `yarn` commands, so make sure `yarn` is on the `PATH` when invoking the
script.
## Example

View File

@ -8,7 +8,7 @@ VM host to update the preview server based on changes in the source code.
The script will pull the latest changes from the origin's master branch and examine if there have
been any changes in files inside the preview server source code directory (see below). If there are,
it will create a new image and verify that is works as expected. Finally, it will stop and remove
it will create a new image and verify that it works as expected. Finally, it will stop and remove
the old docker container and image, create a new container based on the new image and start it.
The script assumes that the preview server source code is in the repository's
@ -25,7 +25,11 @@ used for.
**Note 1:** The script has to execute docker commands with `sudo`.
**Note 2:** Make sure the user that executes the script has access to update the repository
**Note 2:**
The script has to execute `yarn` commands, so make sure `yarn` is on the `PATH` when invoking the
script.
**Note 3:** Make sure the user that executes the script has access to update the repository.
## Run the script manually
@ -50,3 +54,9 @@ log its output to `update-preview-server.log` (assuming the user has the necessa
# Periodically check for changes and update the preview server (if necessary)
*/30 * * * * /path/to/update-preview-server.sh /path/to/repo /path/to/secrets /path/to/builds /path/to/localcerts /path/to/logs >> /path/to/update-preview-server.log 2>&1
```
**Note:**
Keep in mind that cron jobs run in non-interactive, non-login shells. This means that the execution
context might be different compared to when running the same commands from an interactive, login
shell. For example, `.bashrc` files are normally _not_ sourced automatically in cron jobs. See
[here](http://www.gnu.org/software/bash/manual/html_node/Bash-Startup-Files.html) for more info.

View File

@ -13,7 +13,8 @@ readonly HOST_LOCALCERTS_DIR=$4
readonly HOST_LOGS_DIR=$5
# Constants
readonly PROVISIONAL_IMAGE_NAME=aio-builds:provisional
readonly PROVISIONAL_TAG=provisional
readonly PROVISIONAL_IMAGE_NAME=aio-builds:$PROVISIONAL_TAG
readonly LATEST_IMAGE_NAME=aio-builds:latest
readonly CONTAINER_NAME=aio
@ -30,7 +31,7 @@ readonly CONTAINER_NAME=aio
# Do not update the server unless files inside `aio-builds-setup/` have changed
# or the last attempt failed (identified by the provisional image still being around).
readonly relevantChangedFilesCount=$(git diff --name-only $lastDeployedCommit...HEAD | grep -P "^aio/aio-builds-setup/" | wc -l)
readonly lastAttemptFailed=$(sudo docker rmi "$PROVISIONAL_IMAGE_NAME" >> /dev/fd/3 && echo "true" || echo "false")
readonly lastAttemptFailed=$(sudo docker image ls | grep "$PROVISIONAL_TAG" >> /dev/fd/3 && echo "true" || echo "false")
if [[ $relevantChangedFilesCount -eq 0 ]] && [[ "$lastAttemptFailed" != "true" ]]; then
echo "Skipping update because no relevant files have been touched."
exit 0

View File

@ -23,7 +23,7 @@ to provide semantic meaning where it might otherwise be missing.
Use [attribute binding](guide/template-syntax#attribute-binding) template syntax to control the values of accessibility-related attributes.
When binding to ARIA attributes in Angular, you must use the `attr.` prefix, as the ARIA
specification depends specifically on HTML attributes rather than properties on DOM elements.
specification depends specifically on HTML attributes rather than properties of DOM elements.
```html
<!-- Use attr. when binding to an ARIA attribute -->
@ -44,7 +44,7 @@ NOTE:
By convention, HTML attributes use lowercase names (`tabindex`), while properties use camelCase names (`tabIndex`).
See the [Template Syntax](https://angular.io/guide/template-syntax#html-attribute-vs-dom-property) guide for more background on the difference between attributes and properties.
See the [Template Syntax](guide/template-syntax#html-attribute-vs-dom-property) guide for more background on the difference between attributes and properties.
</div>

View File

@ -542,6 +542,7 @@ It does not, however, rewrite the `.d.ts` file, so TypeScript doesn't recognize
{@a binding-expression-validation}
## Phase 3: Template type checking
One of the Angular compiler's most helpful features is the ability to type-check expressions within templates, and catch any errors before they cause crashes at runtime.
@ -559,7 +560,7 @@ As a result, templates that previously compiled under View Engine can fail type
This stricter type checking is not enabled by default in version 9, but can be enabled by setting the `strictTemplates` configuration option.
We do expect to make strict type checking the default in the future.
<!-- For more information about type-checking options, and about improvements to template type checking in version 9 and above, see [Template type checking](guide/template-type-checking). -->
For more information about type-checking options, and about improvements to template type checking in version 9 and above, see [Template type checking](guide/template-typecheck).
</div>
@ -618,16 +619,7 @@ For example, to avoid `Object is possibly 'undefined'` error in the template abo
Using `*ngIf` allows the TypeScript compiler to infer that the `person` used in the binding expression will never be `undefined`.
#### Custom `ngIf` like directives
Directives that behave like `*ngIf` can declare that they want the same treatment by including a static member marker that is a signal to the template compiler to treat them like `*ngIf`. This static member for `*ngIf` is:
```typescript
public static ngIfUseIfTypeGuard: void;
```
This declares that the input property `ngIf` of the `NgIf` directive should be treated as a guard to the use of its template, implying that the template will only be instantiated if the `ngIf` input property is true.
For more information about input type narrowing, see [Input setter coercion](guide/template-typecheck#input-setter-coercion) and [Improving template type checking for custom directives](guide/structural-directives#directive-type-checks).
### Non-null type assertion operator

View File

@ -52,7 +52,7 @@ For some platforms and applications, you might also want to use the PWA (Progres
## Support for the development cycle
The **Development Workflow** section describes the tools and processes you use to compile, test, and and deploy Angular applications.
The **Development Workflow** section describes the tools and processes you use to compile, test, and deploy Angular applications.
* [CLI Command Reference](cli): The Angular CLI is a command-line tool that you use to create projects, generate application and library code, and perform a variety of ongoing development tasks such as testing, bundling, and deployment.

View File

@ -55,7 +55,7 @@ This method is for development and testing only, and is not a supported or secur
### Automatic deployment with the CLI
The Angular CLI command `ng deploy` (introduced in version 8.3.0) executes the `deploy` [CLI builder](https://angular.io/guide/cli-builder) associated with your project. A number of third-party builders implement deployment capabilities to different platforms. You can add any of them to your project by running `ng add [package name]`.
The Angular CLI command `ng deploy` (introduced in version 8.3.0) executes the `deploy` [CLI builder](guide/cli-builder) associated with your project. A number of third-party builders implement deployment capabilities to different platforms. You can add any of them to your project by running `ng add [package name]`.
When you add a package with deployment capability, it'll automatically update your workspace configuration (`angular.json` file) with a `deploy` section for the selected project. You can then use the `ng deploy` command to deploy that project.

View File

@ -1,42 +1,53 @@
# Dynamic forms
# Building dynamic forms
{@a top}
Many forms, such as questionaires, can be very similar to one another in format and intent.
To make it faster and easier to generate different versions of such a form,
you can create a *dynamic form template* based on metadata that describes the business object model.
You can then use the template to generate new forms automatically, according to changes in the data model.
Building handcrafted forms can be costly and time-consuming,
especially if you need a great number of them, they're similar to each other, and they change frequently
to meet rapidly changing business and regulatory requirements.
The technique is particularly useful when you have a type of form whose content must
change frequently to meet rapidly changing business and regulatory requirements.
A typical use case is a questionaire. You might need to get input from users in different contexts.
The format and style of the forms a user sees should remain constant, while the actual questions you need to ask vary with the context.
It may be more economical to create the forms dynamically, based on
metadata that describes the business object model.
In this tutorial you will build a dynamic form that presents a basic questionaire.
You will build an online application for heroes seeking employment.
The agency is constantly tinkering with the application process, but by using the dynamic form
you can create the new forms on the fly without changing the application code.
This cookbook shows you how to use `formGroup` to dynamically
render a simple form with different control types and validation.
It's a primitive start.
It might evolve to support a much richer variety of questions, more graceful rendering, and superior user experience.
All such greatness has humble beginnings.
The tutorial walks you through the following steps.
The example in this cookbook is a dynamic form to build an
online application experience for heroes seeking employment.
The agency is constantly tinkering with the application process.
You can create the forms on the fly *without changing the application code*.
{@a toc}
1. Enable reactive forms for a project.
2. Establish a data model to represent form controls.
3. Populate the model with sample data.
4. Develop a component to create form controls dynamically.
The form you create uses input validation and styling to improve the user experience.
It has a Submit button that is only enabled when all user input is valid, and flags invalid input with color coding and error messages.
The basic version can evolve to support a richer variety of questions, more graceful rendering, and superior user experience.
<div class="alert is-helpful">
See the <live-example name="dynamic-form"></live-example>.
{@a bootstrap}
</div>
## Bootstrap
## Prerequisites
Start by creating an `NgModule` called `AppModule`.
Before doing this tutorial, you should have a basic understanding to the following.
This cookbook uses [reactive forms](guide/reactive-forms).
* [TypeScript](https://www.typescriptlang.org/docs/home.html "The TypeScript language") and HTML5 programming.
Reactive forms belongs to a different `NgModule` called `ReactiveFormsModule`,
so in order to access any reactive forms directives, you have to import
`ReactiveFormsModule` from the `@angular/forms` library.
* Fundamental concepts of [Angular app design](guide/architecture "Introduction to Angular app-design concepts").
Bootstrap the `AppModule` in `main.ts`.
* Basic knowledge of [reactive forms](guide/reactive-forms "Reactive forms guide").
## Enable reactive forms for your project
Dynamic forms are based on reactive forms. To give the application access reactive forms directives, the [root module](guide/bootstrapping "Learn about bootstrapping an app from the root module.") imports `ReactiveFormsModule` from the `@angular/forms` library.
The following code from the example shows the setup in the root module.
<code-tabs>
@ -50,79 +61,56 @@ Bootstrap the `AppModule` in `main.ts`.
</code-tabs>
{@a object-model}
## Question model
## Create a form object model
The next step is to define an object model that can describe all scenarios needed by the form functionality.
The hero application process involves a form with a lot of questions.
The _question_ is the most fundamental object in the model.
A dynamic form requires an object model that can describe all scenarios needed by the form functionality.
The example hero-application form is a set of questions&mdash;that is, each control in the form must ask a question and accept an answer.
The following `QuestionBase` is a fundamental question class.
The data model for this type of form must represent a question.
The example includes the `DynamicFormQuestionComponent`, which defines a question as the fundamental object in the model.
The following `QuestionBase` is a base class for a set of controls that can represent the question and its answer in the form.
<code-example path="dynamic-form/src/app/question-base.ts" header="src/app/question-base.ts">
</code-example>
### Define control classes
From this base, the example derives two new classes, `TextboxQuestion` and `DropdownQuestion`,
that represent different control types.
When you create the form template in the next step, you will instantiate these specific question types in order to render the appropriate controls dynamically.
From this base you can derive two new classes in `TextboxQuestion` and `DropdownQuestion`
that represent textbox and dropdown questions.
The idea is that the form will be bound to specific question types and render the
appropriate controls dynamically.
* The `TextboxQuestion` control type presents a question and allows users to enter input.
`TextboxQuestion` supports multiple HTML5 types such as text, email, and url
via the `type` property.
<code-example path="dynamic-form/src/app/question-textbox.ts" header="src/app/question-textbox.ts"></code-example>
The `TextboxQuestion` control type will be represented in a form template using an `<input>` element.
The `type` attribute of the element will be defined based on the `type` field specified in the `options` argument (for example `text`, `email`, `url`).
<code-example path="dynamic-form/src/app/question-textbox.ts" header="src/app/question-textbox.ts"></code-example>
* The `DropdownQuestion` control presents a list of choices in a select box.
<code-example path="dynamic-form/src/app/question-dropdown.ts" header="src/app/question-dropdown.ts"></code-example>
### Compose form groups
`DropdownQuestion` presents a list of choices in a select box.
<code-example path="dynamic-form/src/app/question-dropdown.ts" header="src/app/question-dropdown.ts"></code-example>
Next is `QuestionControlService`, a simple service for transforming the questions to a `FormGroup`.
In a nutshell, the form group consumes the metadata from the question model and
allows you to specify default values and validation rules.
A dynamic form uses a service to create grouped sets of input controls, based on the form model.
The following `QuestionControlService` collects a set of `FormGroup` instances that consume the metadata from the question model. You can specify default values and validation rules.
<code-example path="dynamic-form/src/app/question-control.service.ts" header="src/app/question-control.service.ts"></code-example>
{@a form-component}
## Question form components
Now that you have defined the complete model you are ready
to create components to represent the dynamic form.
## Compose dynamic form contents
The dynamic form itself will be represented by a container component, which you will add in a later step.
Each question is represented in the form component's template by an `<app-question>` tag, which matches an instance of `DynamicFormQuestionComponent`.
`DynamicFormComponent` is the entry point and the main container for the form.
<code-tabs>
<code-pane header="dynamic-form.component.html" path="dynamic-form/src/app/dynamic-form.component.html">
</code-pane>
<code-pane header="dynamic-form.component.ts" path="dynamic-form/src/app/dynamic-form.component.ts">
</code-pane>
</code-tabs>
It presents a list of questions, each bound to a `<app-question>` component element.
The `<app-question>` tag matches the `DynamicFormQuestionComponent`,
the component responsible for rendering the details of each _individual_
question based on values in the data-bound question object.
The `DynamicFormQuestionComponent` is responsible for rendering the details of an individual question based on values in the data-bound question object.
The form relies on a [`[formGroup]` directive](api/forms/FormGroupDirective "API reference") to connect the template HTML to the underlying control objects.
The `DynamicFormQuestionComponent` creates form groups and populates them with controls defined in the question model, specifying display and validation rules.
<code-tabs>
@ -136,70 +124,88 @@ question based on values in the data-bound question object.
</code-tabs>
Notice this component can present any type of question in your model.
The goal of the `DynamicFormQuestionComponent` is to present question types defined in your model.
You only have two types of questions at this point but you can imagine many more.
The `ngSwitch` determines which type of question to display.
The `ngSwitch` statement in the template determines which type of question to display.
The switch uses directives with the [`formControlName`](api/forms/FormControlName "FormControlName directive API reference") and [`formGroup`](api/forms/FormGroupDirective "FormGroupDirective API reference") selectors. Both directives are defined in `ReactiveFormsModule`.
In both components you're relying on Angular's **formGroup** to connect the template HTML to the
underlying control objects, populated from the question model with display and validation rules.
`formControlName` and `formGroup` are directives defined in
`ReactiveFormsModule`. The templates can access these directives
directly since you imported `ReactiveFormsModule` from `AppModule`.
{@a questionnaire-data}
## Questionnaire data
### Supply data
`DynamicFormComponent` expects the list of questions in the form of an array bound to `@Input() questions`.
Another service is needed to supply a specific set of questions from which to build an individual form.
For this exercise you will create the `QuestionService` to supply this array of questions from the hard-coded sample data.
In a real-world app, the service might fetch data from a backend system.
The key point, however, is that you control the hero job-application questions entirely through the objects returned from `QuestionService`.
To maintain the questionnaire as requirements change, you only need to add, update, and remove objects from the `questions` array.
The set of questions you've defined for the job application is returned from the `QuestionService`.
In a real app you'd retrieve these questions from storage.
The key point is that you control the hero job application questions
entirely through the objects returned from `QuestionService`.
Questionnaire maintenance is a simple matter of adding, updating,
and removing objects from the `questions` array.
The `QuestionService` supplies a set of questions in the form of an array bound to `@Input()` questions.
<code-example path="dynamic-form/src/app/question.service.ts" header="src/app/question.service.ts">
</code-example>
{@a dynamic-template}
Finally, display an instance of the form in the `AppComponent` shell.
## Create a dynamic form template
The `DynamicFormComponent` component is the entry point and the main container for the form, which is represented using the `<app-dynamic-form>` in a template.
The `DynamicFormComponent` component presents a list of questions by binding each one to an `<app-question>` element that matches the `DynamicFormQuestionComponent`.
<code-tabs>
<code-pane header="dynamic-form.component.html" path="dynamic-form/src/app/dynamic-form.component.html">
</code-pane>
<code-pane header="dynamic-form.component.ts" path="dynamic-form/src/app/dynamic-form.component.ts">
</code-pane>
</code-tabs>
### Display the form
To display an instance of the dynamic form, the `AppComponent` shell template passes the `questions` array returned by the `QuestionService` to the form container component, `<app-dynamic-form>`.
<code-example path="dynamic-form/src/app/app.component.ts" header="app.component.ts">
</code-example>
{@a dynamic-template}
## Dynamic Template
Although in this example you're modelling a job application for heroes, there are
no references to any specific hero question
outside the objects returned by `QuestionService`.
This is very important since it allows you to repurpose the components for any type of survey
The example provides a model for a job application for heroes, but there are
no references to any specific hero question other than the objects returned by `QuestionService`.
This separation of model and data allows you to repurpose the components for any type of survey
as long as it's compatible with the *question* object model.
The key is the dynamic data binding of metadata used to render the form
### Ensuring valid data
The form template uses dynamic data binding of metadata to render the form
without making any hardcoded assumptions about specific questions.
In addition to control metadata, you are also adding validation dynamically.
It adds both control metadata and validation criteria dynamically.
The *Save* button is disabled until the form is in a valid state.
To ensure valid input, the *Save* button is disabled until the form is in a valid state.
When the form is valid, you can click *Save* and the app renders the current form values as JSON.
This proves that any user input is bound back to the data model.
Saving and retrieving the data is an exercise for another time.
The final form looks like this:
The following figure shows the final form.
<div class="lightbox">
<img src="generated/images/guide/dynamic-form/dynamic-form.png" alt="Dynamic-Form">
</div>
## Next steps
[Back to top](guide/dynamic-form#top)
* **Different types of forms and control collection**
This tutorial shows how to build a a questionaire, which is just one kind of dynamic form.
The example uses `FormGroup` to collect a set of controls.
For an example of a different type of dynamic form, see the section [Creating dynamic forms](guide/reactive-forms#creating-dynamic-forms "Create dynamic forms with arrays") in the Reactive Forms guide.
That example also shows how to use `FormArray` instead of `FormGroup` to collect a set of controls.
* **Validating user input**
The section [Validating form input](guide/reactive-forms#validating-form-input "Basic input validation") introduces the basics of how input validation works in reactive forms.
The [Form validation guide](guide/form-validation "Form validation guide") covers the topic in more depth.

View File

@ -72,7 +72,7 @@ Files at the top level of `src/` support testing and running your application. S
| `environments/` | Contains build configuration options for particular target environments. By default there is an unnamed standard development environment and a production ("prod") environment. You can define additional target environment configurations. |
| `favicon.ico` | An icon to use for this application in the bookmark bar. |
| `index.html` | The main HTML page that is served when someone visits your site. The CLI automatically adds all JavaScript and CSS files when building your app, so you typically don't need to add any `<script>` or` <link>` tags here manually. |
| `main.ts` | The main entry point for your application. Compiles the application with the [JIT compiler](https://angular.io/guide/glossary#jit) and bootstraps the application's root module (AppModule) to run in the browser. You can also use the [AOT compiler](https://angular.io/guide/aot-compiler) without changing any code by appending the `--aot` flag to the CLI `build` and `serve` commands. |
| `main.ts` | The main entry point for your application. Compiles the application with the [JIT compiler](guide/glossary#jit) and bootstraps the application's root module (AppModule) to run in the browser. You can also use the [AOT compiler](guide/aot-compiler) without changing any code by appending the `--aot` flag to the CLI `build` and `serve` commands. |
| `polyfills.ts` | Provides polyfill scripts for browser support. |
| `styles.sass` | Lists CSS files that supply styles for a project. The extension reflects the style preprocessor you have configured for the project. |
| `test.ts` | The main entry point for your unit tests, with some Angular-specific configuration. You don't typically need to edit this file. |
@ -165,7 +165,7 @@ my-workspace/
## Library project files
When you generate a library using the CLI (with a command such as `ng generate library my-lib`), the generated files go into the projects/ folder of the workspace. For more information about creating your own libraries, see [Creating Libraries](https://angular.io/guide/creating-libraries).
When you generate a library using the CLI (with a command such as `ng generate library my-lib`), the generated files go into the projects/ folder of the workspace. For more information about creating your own libraries, see [Creating Libraries](guide/creating-libraries).
Libraries (unlike applications and their associated e2e projects) have their own `package.json` configuration files.

View File

@ -1,132 +1,131 @@
# Form validation
# Validating form input
Improve overall data quality by validating user input for accuracy and completeness.
You can improve overall data quality by validating user input for accuracy and completeness.
This page shows how to validate user input from the UI and display useful validation messages,
in both reactive and template-driven forms.
This page shows how to validate user input in the UI and display useful validation messages
using both reactive and template-driven forms. It assumes some basic knowledge of the two
forms modules.
**Prerequisites**
Before reading about form validation, you should have a basic understanding of the following.
* [TypeScript](https://www.typescriptlang.org/docs/home.html "The TypeScript language") and HTML5 programming.
* Fundamental concepts of [Angular app design](guide/architecture "Introduction to Angular app-design concepts").
* The [two types of forms that Angular supports](guide/forms-overview "Introduction to Angular forms").
* Basics of either [Template-driven Forms](guide/forms "Template-driven forms guide") or [Reactive Forms](guide/reactive-forms "Reactive forms guide").
<div class="alert is-helpful">
For the sample app that this page describes, see the <live-example></live-example>.
Get the complete example code for the reactive and template-driven forms used here to illustrate form validation.
Run the <live-example></live-example>.
</div>
<div class="alert is-helpful">
{@a template-driven-validation}
If you're new to forms, start by reviewing the [Forms](guide/forms) and
[Reactive Forms](guide/reactive-forms) guides.
</div>
## Template-driven validation
## Validating input in template-driven forms
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.
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.
either a list of validation errors that results in an INVALID status, or null, which results in a VALID status.
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`:
<code-example path="form-validation/src/app/template/hero-form-template.component.html" region="name-with-error-msg" header="template/hero-form-template.component.html (name)"></code-example>
Note the following:
Notice the following features illustrated by the example.
* 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.
information, see the [Custom validators](#custom-validators) section.
* `#name="ngModel"` exports `NgModel` into a local variable called `name`. `NgModel` mirrors many of the properties of its underlying
`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 `*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 the `name` is invalid and the control is either `dirty` or `touched`.
* Each nested `<div>` can present a custom message for one of the possible validation errors.
* Each nested `<div>` can present a custom message for one of the possible validation errors.
There are messages for `required`, `minlength`, and `forbiddenName`.
{@a dirty-or-touched}
<div class="alert is-helpful">
To prevent the validator from displaying errors before the user has a chance to edit the form, you should check for either the `dirty` or `touched` states in a control.
#### Why check _dirty_ and _touched_?
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 errors from showing until the user
does one of two things: changes the value,
turning the control dirty; or blurs the form control element, setting the control to touched.
* When the user changes the value in the watched field, the control is marked as "dirty".
* When the user blurs the form control element, the control is marked as "touched".
</div>
## Reactive form validation
{@a 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.
## Validating input in reactive forms
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.
### Validator functions
There are two types of validator functions: sync validators and async validators.
Validator functions can be either synchronous or asynchronous.
* **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`.
* **Sync validators**: Synchronous 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`.
* **Async validators**: functions that take a control instance and return a Promise
* **Async validators**: Asynchronous 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.
For performance reasons, Angular only runs async validators if all sync validators pass. Each must complete before errors are set.
### Built-in validators
### Built-in validator functions
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.
You can choose to [write your own validator functions](#custom-validators), or you can use some of Angular's built-in validators.
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.
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:
built-in validators&mdash;this time, in function form, as in the following example.
{@a reactive-component-class}
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.1.ts" region="form-group" header="reactive/hero-form-reactive.component.ts (validator functions)"></code-example>
Note that:
In this example, 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 [custom validators](#custom-validators) below.)
* 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.
All of these validators are synchronous, so they are passed as the second argument. Notice that you can support multiple validators by passing the functions in as an array.
This example also 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 shorthand for the template.
If you look at the template for the name input again, it is fairly similar to the template-driven example.
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" header="reactive/hero-form-reactive.component.html (name with error msg)"></code-example>
Key takeaways:
This form differs from the template-driven version in that it no longer exports any directives. Instead, it uses the `name` getter defined in the component class.
* 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.
Notice that the `required` attribute is still present in the template. Although it's not necessary for validation, it should be retained to for accessibility purposes.
{@a custom-validators}
## Custom validators
## Defining custom validators
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.
The built-in validators don't always match the exact use case of your application, so you sometimes need to create a custom validator.
Consider the `forbiddenNameValidator` function from previous
[examples](guide/form-validation#reactive-component-class) in
this guide. Here's what the definition of that function looks like:
Consider the `forbiddenNameValidator` function from previous [reactive-form examples](#reactive-component-class).
Here's what the definition of that function looks like.
<code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="custom-validator" header="shared/forbidden-name.directive.ts (forbiddenNameValidator)"></code-example>
The function is actually a factory that takes a regular expression to detect a _specific_ forbidden name and returns a validator function.
The function is 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".
Elsewhere it could reject "alice" or any name that the configuring regular expression matches.
@ -137,55 +136,57 @@ null if the control value is valid _or_ a validation error object.
The validation error object typically has a property whose name is the validation key, `'forbiddenName'`,
and whose value is an arbitrary dictionary of values that you could insert into an error message, `{name}`.
Custom async validators are similar to sync validators, but they must instead return a Promise or Observable
that later emits null or a validation error object. In the case of an Observable, the Observable must complete,
at which point the form uses the last value emitted for validation.
Custom async validators are similar to sync validators, but they must instead return a Promise or observable that later emits null or a validation error object.
In the case of an observable, the observable must complete, at which point the form uses the last value emitted for validation.
### Adding to reactive forms
{@a 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`.
### Adding custom validators to reactive forms
In reactive forms, add a custom validator by passing the function directly to the `FormControl`.
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.1.ts" region="custom-validator" header="reactive/hero-form-reactive.component.ts (validator functions)"></code-example>
### Adding to template-driven forms
{@a 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.
### Adding custom validators to template-driven forms
The corresponding `ForbiddenValidatorDirective` serves as a wrapper around the `forbiddenNameValidator`.
In template-driven forms, add a directive to the template, where the directive wraps the validator function.
For example, the corresponding `ForbiddenValidatorDirective` serves as a wrapper around the `forbiddenNameValidator`.
Angular 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 validators.
Angular recognizes the directive's role in the validation process because the directive registers itself with the `NG_VALIDATORS` provider, as shown in the following example.
`NG_VALIDATORS` is a predefined provider with an extensible collection of validators.
<code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="directive-providers" header="shared/forbidden-name.directive.ts (providers)"></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
comes together:
with Angular forms.
Here is the rest of the directive to help you get an idea of how it all
comes together.
<code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="directive" header="shared/forbidden-name.directive.ts (directive)">
</code-example>
Once the `ForbiddenValidatorDirective` is ready, you can simply add its selector, `appForbiddenName`, to any input element to activate it. For example:
Once the `ForbiddenValidatorDirective` is ready, you can add its selector, `appForbiddenName`, 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" header="template/hero-form-template.component.html (forbidden-name-input)"></code-example>
<div class="alert is-helpful">
You may have noticed that the custom validation directive is instantiated with `useExisting`
rather than `useClass`. The registered validator must be _this instance_ of
Notice that the custom validation directive is instantiated with `useExisting` rather than `useClass`. The registered validator must be _this instance_ of
the `ForbiddenValidatorDirective`&mdash;the instance in the form with
its `forbiddenName` property bound to “bob". If you were to replace
`useExisting` with `useClass`, then youd be registering a new class instance, one that
doesnt have a `forbiddenName`.
its `forbiddenName` property bound to “bob".
If you were to replace `useExisting` with `useClass`, then youd be registering a new class instance, one that doesnt have a `forbiddenName`.
</div>
## Control status CSS classes
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:
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.
* `.ng-valid`
* `.ng-invalid`
@ -195,25 +196,27 @@ Like in AngularJS, Angular automatically mirrors many control properties onto th
* `.ng-untouched`
* `.ng-touched`
The hero form uses the `.ng-valid` and `.ng-invalid` classes to
In the following example, 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/assets/forms.css" header="forms.css (status classes)">
</code-example>
## Cross field validation
This section shows how to perform cross field validation. It assumes some basic knowledge of creating custom validators.
## Cross-field validation
<div class="alert is-helpful">
A cross-field validator is a [custom validator](#custom-validators "Read about custom validators") that compares the values of different fields in a form and accepts or rejects them in combination.
For example, you might have a form that offers mutually incompatible options, so that if the user can choose A or B, but not both.
Some field values might also depend on others; a user might be allowed to choose B only if A is also chosen.
If you haven't created custom validators before, start by reviewing the [custom validators section](guide/form-validation#custom-validators).
The following cross validation examples show how to do the following:
</div>
* Validate reactive or template-based form input based on the values of two sibling controls,
* Show a descriptive error message after the user interacted with the form and the validation failed.
In the following section, we will make sure that our heroes do not reveal their true identities by filling out the Hero Form. We will do that by validating that the hero names and alter egos do not match.
The examples use cross-validation to ensure that heroes do not reveal their true identities by filling out the Hero Form. The validators do this by checking that the hero names and alter egos do not match.
### Adding to reactive forms
### Adding cross-validation to reactive forms
The form has the following structure:
@ -225,7 +228,9 @@ const heroForm = new FormGroup({
});
```
Notice that the name and alterEgo are sibling controls. To evaluate both controls in a single custom validator, we should perform the validation in a common ancestor control: the `FormGroup`. That way, we can query the `FormGroup` for the child controls which will allow us to compare their values.
Notice that the `name` and `alterEgo` are sibling controls.
To evaluate both controls in a single custom validator, you must perform the validation in a common ancestor control: the `FormGroup`.
You query the `FormGroup` for its child controls so that you can compare their values.
To add a validator to the `FormGroup`, pass the new validator in as the second argument on creation.
@ -237,74 +242,73 @@ const heroForm = new FormGroup({
}, { validators: identityRevealedValidator });
```
The validator code is as follows:
The validator code is as follows.
<code-example path="form-validation/src/app/shared/identity-revealed.directive.ts" region="cross-validation-validator" header="shared/identity-revealed.directive.ts"></code-example>
The identity validator implements the `ValidatorFn` interface. It takes an Angular control object as an argument and returns either null if the form is valid, or `ValidationErrors` otherwise.
The `identity` validator implements the `ValidatorFn` interface. It takes an Angular control object as an argument and returns either null if the form is valid, or `ValidationErrors` otherwise.
First we retrieve the child controls by calling the `FormGroup`'s [get](api/forms/AbstractControl#get) method. Then we simply compare the values of the `name` and `alterEgo` controls.
The validator retrieves the child controls by calling the `FormGroup`'s [get](api/forms/AbstractControl#get) method, then compares the values of the `name` and `alterEgo` controls.
If the values do not match, the hero's identity remains secret, and we can safely return null. Otherwise, the hero's identity is revealed and we must mark the form as invalid by returning an error object.
If the values do not match, the hero's identity remains secret, both are valid, and the validator returns null.
If they do match, the hero's identity is revealed and the validator must mark the form as invalid by returning an error object.
To provide better user experience, the template shows an appropriate error message when the form is invalid.
Next, to provide better user experience, we show an appropriate error message when the form is invalid.
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.html" region="cross-validation-error-message" header="reactive/hero-form-template.component.html"></code-example>
Note that we check if:
- the `FormGroup` has the cross validation error returned by the `identityRevealed` validator,
- the user is yet to [interact](guide/form-validation#why-check-dirty-and-touched) with the form.
This `*ngIf` displays the error if the `FormGroup` has the cross validation error returned by the `identityRevealed` validator, but only if the user has finished [interacting with the form](#dirty-or-touched).
### Adding to template driven forms
First we must create a directive that will wrap the validator function. We provide it as the validator using the `NG_VALIDATORS` token. If you are not sure why, or you do not fully understand the syntax, revisit the previous [section](guide/form-validation#adding-to-template-driven-forms).
### Adding cross-validation to template-driven forms
For a template-driven form, you must create a directive to wrap the validator function.
You provide that directive as the validator using the [`NG_VALIDATORS` token](#adding-to-template-driven-forms "Read about providing validators"), as shown in the following example.
<code-example path="form-validation/src/app/shared/identity-revealed.directive.ts" region="cross-validation-directive" header="shared/identity-revealed.directive.ts"></code-example>
Next, we have to add the directive to the html template. Since the validator must be registered at the highest level in the form, we put the directive on the `form` tag.
You must add the new directive to the HTML template.
Because the validator must be registered at the highest level in the form, the following template puts the directive on the `form` tag.
<code-example path="form-validation/src/app/template/hero-form-template.component.html" region="cross-validation-register-validator" header="template/hero-form-template.component.html"></code-example>
To provide better user experience, we show an appropriate error message when the form is invalid.
<code-example path="form-validation/src/app/template/hero-form-template.component.html" region="cross-validation-error-message" header="template/hero-form-template.component.html"></code-example>
Note that we check if:
- the form has the cross validation error returned by the `identityRevealed` validator,
- the user is yet to [interact](guide/form-validation#why-check-dirty-and-touched) with the form.
This is the same in both template-driven and reactive forms.
This completes the cross validation example. We managed to:
- validate the form based on the values of two sibling controls,
- show a descriptive error message after the user interacted with the form and the validation failed.
## Creating asynchronous validators
## Async Validation
This section shows how to create asynchronous validators. It assumes some basic knowledge of creating [custom validators](guide/form-validation#custom-validators).
Asynchronous validators implement the `AsyncValidatorFn` and `AsyncValidator` interfaces.
These are very similar to their synchronous counterparts, with the following differences.
### The Basics
Just like synchronous validators have the `ValidatorFn` and `Validator` interfaces, asynchronous validators have their own counterparts: `AsyncValidatorFn` and `AsyncValidator`.
* The `validate()` functions must return a Promise or an observable,
* The observable returned must be finite, meaning it must complete at some point.
To convert an infinite observable into a finite one, pipe the observable through a filtering operator such as `first`, `last`, `take`, or `takeUntil`.
They are very similar with the only difference being:
Asynchronous validation happens after the synchronous validation, and is performed only if the synchronous validation is successful.
This check allows forms to avoid potentially expensive async validation processes (such as an HTTP request) if the more basic validation methods have already found invalid input.
* They must return a Promise or an Observable,
* The observable returned must be finite, meaning it must complete at some point. To convert an infinite observable into a finite one, pipe the observable through a filtering operator such as `first`, `last`, `take`, or `takeUntil`.
After asynchronous validation begins, the form control enters a `pending` state. You can inspect the control's `pending` property and use it to give visual feedback about the ongoing validation operation.
It is important to note that the asynchronous validation happens after the synchronous validation, and is performed only if the synchronous validation is successful. This check allows forms to avoid potentially expensive async validation processes such as an HTTP request if more basic validation methods fail.
After asynchronous validation begins, the form control enters a `pending` state. You can inspect the control's `pending` property and use it to give visual feedback about the ongoing validation.
A common UI pattern is to show a spinner while the async validation is being performed. The following example presents how to achieve this with template-driven forms:
A common UI pattern is to show a spinner while the async validation is being performed. The following example shows how to achieve this in a template-driven form.
```html
<input [(ngModel)]="name" #model="ngModel" appSomeAsyncValidator>
<app-spinner *ngIf="model.pending"></app-spinner>
```
### Implementing Custom Async Validator
In the following section, validation is performed asynchronously to ensure that our heroes pick an alter ego that is not already taken. New heroes are constantly enlisting and old heroes are leaving the service. That means that we do not have the list of available alter egos ahead of time.
### Implementing a custom async validator
To validate the potential alter ego, we need to consult a central database of all currently enlisted heroes. The process is asynchronous, so we need a special validator for that.
In the following example, an async validator ensures that heroes pick an alter ego that is not already taken.
New heroes are constantly enlisting and old heroes are leaving the service, so the list of available alter egos cannot be retrieved ahead of time.
To validate the potential alter ego entry, the validator must initiate an asynchronous operation to consult a central database of all currently enlisted heroes.
Let's start by creating the validator class.
The following code create the validator class, `UniqueAlterEgoValidator`, which implements the `AsyncValidator` interface.
<code-example path="form-validation/src/app/shared/alter-ego.directive.ts" region="async-validator"></code-example>
As you can see, the `UniqueAlterEgoValidator` class implements the `AsyncValidator` interface. In the constructor, we inject the `HeroesService` that has the following interface:
The constructor injects the `HeroesService`, which defines the following interface.
```typescript
interface HeroesService {
@ -312,29 +316,37 @@ interface HeroesService {
}
```
In a real world application, the `HeroesService` is responsible for making an HTTP request to the hero database to check if the alter ego is available. From the validator's point of view, the actual implementation of the service is not important, so we can just code against the `HeroesService` interface.
In a real world application, the `HeroesService` would be responsible for making an HTTP request to the hero database to check if the alter ego is available.
From the validator's point of view, the actual implementation of the service is not important, so the example can just code against the `HeroesService` interface.
As the validation begins, the `UniqueAlterEgoValidator` delegates to the `HeroesService` `isAlterEgoTaken()` method with the current control value. At this point the control is marked as `pending` and remains in this state until the observable chain returned from the `validate()` method completes.
As the validation begins, the `UniqueAlterEgoValidator` delegates to the `HeroesService` `isAlterEgoTaken()` method with the current control value.
At this point the control is marked as `pending` and remains in this state until the observable chain returned from the `validate()` method completes.
The `isAlterEgoTaken()` method dispatches an HTTP request that checks if the alter ego is available, and returns `Observable<boolean>` as the result. We pipe the response through the `map` operator and transform it into a validation result. As always, we return `null` if the form is valid, and `ValidationErrors` if it is not. We make sure to handle any potential errors with the `catchError` operator.
The `isAlterEgoTaken()` method dispatches an HTTP request that checks if the alter ego is available, and returns `Observable<boolean>` as the result.
The `validate()` method pipes the response through the `map` operator and transforms it into a validation result.
Here we decided that `isAlterEgoTaken()` error is treated as a successful validation, because failure to make a validation request does not necessarily mean that the alter ego is invalid. You could handle the error differently and return the `ValidationError` object instead.
The method then, like any validator, returns `null` if the form is valid, and `ValidationErrors` if it is not.
This validator handles any potential errors with the `catchError` operator.
In this case, the validator treats the `isAlterEgoTaken()` error as a successful validation, because failure to make a validation request does not necessarily mean that the alter ego is invalid.
You could handle the error differently and return the `ValidationError` object instead.
After some time passes, the observable chain completes and the async validation is done. The `pending` flag is set to `false`, and the form validity is updated.
After some time passes, the observable chain completes and the asynchronous validation is done.
The `pending` flag is set to `false`, and the form validity is updated.
### Note on performance
### Optimizing performance of async validators
By default, all validators are run after every form value change. With synchronous validators, this will not likely have a noticeable impact on application performance. However, it's common for async validators to perform some kind of HTTP request to validate the control. Dispatching an HTTP request after every keystroke could put a strain on the backend API, and should be avoided if possible.
By default, all validators run after every form value change. With synchronous validators, this does not normally have a noticeable impact on application performance.
Async validators, however, commonly perform some kind of HTTP request to validate the control. Dispatching an HTTP request after every keystroke could put a strain on the backend API, and should be avoided if possible.
We can delay updating the form validity by changing the `updateOn` property from `change` (default) to `submit` or `blur`.
You can delay updating the form validity by changing the `updateOn` property from `change` (default) to `submit` or `blur`.
With template-driven forms:
With template-driven forms, set the property in the template.
```html
<input [(ngModel)]="name" [ngModelOptions]="{updateOn: 'blur'}">
```
With reactive forms:
With reactive forms, set the property in the `FormControl` instance.
```typescript
new FormControl('', {updateOn: 'blur'});

View File

@ -214,6 +214,13 @@ Read more about component classes, templates, and views in [Introduction to Angu
See [workspace configuration](#cli-config)
{@a content-projection}
## content projection
A way to insert DOM content from outside a component into the component's view in a designated spot.
For more information, see [Responding to changes in content](guide/lifecycle-hooks#content-projection).
{@a custom-element}
@ -594,7 +601,7 @@ Compare to [NgModule](#ngmodule).
## ngcc
Angular compatibility compiler.
If you build your app using [Ivy](#ivy), but it depends on libraries have not been compiled with Ivy, the CLI uses `ngcc` to automatically update the dependent libraries to use Ivy.
If you build your app using [Ivy](#ivy), but it depends on libraries that have not been compiled with Ivy, the CLI uses `ngcc` to automatically update the dependent libraries to use Ivy.
{@a ngmodule}
@ -946,6 +953,19 @@ Read more about TypeScript at [typescriptlang.org](http://www.typescriptlang.org
{@a U}
{@a unidirectional-data-flow}
## unidirectional data flow
A data flow model where the component tree is always checked for changes in one direction (parent to child), which prevents cycles in the change detection graph.
In practice, this means that data in Angular flows downward during change detection.
A parent component can easily change values in its child components because the parent is checked first.
A failure could occur, however, if a child component tries to change a value in its parent during change detection (inverting the expected data flow), because the parent component has already been rendered.
In development mode, Angular throws the `ExpressionChangedAfterItHasBeenCheckedError` error if your app attempts to do this, rather than silently failing to render the new value.
To avoid this error, a [lifecycle hook](guide/lifecycle-hooks) method that seeks to make such a change should trigger a new change detection run. The new run follows the same direction as before, but succeeds in picking up the new value.
{@a universal}
## Universal
@ -968,7 +988,7 @@ Angular renders a view under the control of one or more [directives](#directive)
A [component](#component) class and its associated [template](#template) define a view.
A view is specifically represented by a `ViewRef` instance associated with a component.
A view that belongs immediately to a component is called a *host view*.
Views are typically collected into [view hierarchies](#view-tree).
Views are typically collected into [view hierarchies](#view-tree).
Properties of elements in a view can change dynamically, in response to user actions;
the structure (number and order) of elements in a view can't.

View File

@ -1,19 +1,61 @@
# Lazy-loading feature modules
## High level view
By default, NgModules are eagerly loaded, which means that as soon as the app loads, so do all the NgModules, whether or not they are immediately necessary. For large apps with lots of routes, consider lazy loading&mdash;a design pattern that loads NgModules as needed. Lazy loading helps keep initial
bundle sizes smaller, which in turn helps decrease load times.
<div class="alert is-helpful">
For the final sample app with two lazy-loaded modules that this page describes, see the
<live-example></live-example>.
</div>
{@a lazy-loading}
## Lazy loading basics
This section introduces the basic procedure for configuring a lazy-loaded route.
For a step-by-step example, see the [step-by-step setup](#step-by-step) section on this page.
To lazy load Angular modules, use `loadchildren` (instead of `component`) in your `AppRoutingModule` `routes` configuration as follows.
<code-example header="AppRoutingModule (excerpt)">
const routes: Routes = [
{
path: 'items',
loadChildren: () => import('./items/items.module').then(m => m.ItemsModule)
}
];
</code-example>
In the lazy-loaded module's routing module, add a route for the component.
<code-example header="Routing module for lazy loaded module (excerpt)">
const routes: Routes = [
{
path: '',
component: ItemsComponent
}
];
</code-example>
Also be sure to remove the `ItemsModule` from the `AppModule`.
For step-by-step instructions on lazy loading modules, continue with the following sections of this page.
{@a step-by-step}
## Step-by-step setup
There are two main steps to setting up a lazy-loaded feature module:
1. Create the feature module with the CLI, using the `--route` flag.
1. Configure the routes.
## Set up an app
### Set up an app
If you dont already have an app, you can follow the steps below to
create one with the CLI. If you already have an app, skip to
@ -36,7 +78,7 @@ See [Keeping Up to Date](guide/updating).
</div>
## Create a feature module with routing
### Create a feature module with routing
Next, youll need a feature module with a component to route to.
To make one, enter the following command in the terminal, where `customers` is the name of the feature module. The path for loading the `customers` feature modules is also `customers` because it is specified with the `--route` option:
@ -59,7 +101,7 @@ Instead, it adds the declared route, `customers` to the `routes` array declared
Notice that the lazy-loading syntax uses `loadChildren` followed by a function that uses the browser's built-in `import('...')` syntax for dynamic imports.
The import path is the relative path to the module.
### Add another feature module
#### Add another feature module
Use the same command to create a second lazy-loaded feature module with routing, along with its stub component.
@ -76,16 +118,14 @@ The `orders` route, specified with the `--route` option, is added to the `routes
region="routes-customers-orders">
</code-example>
## Set up the UI
### Set up the UI
Though you can type the URL into the address bar, a navigation UI is easier for the user and more common.
Replace the default placeholder markup in `app.component.html` with a custom nav
so you can easily navigate to your modules in the browser:
<code-example path="lazy-loading-ngmodules/src/app/app.component.html" header="app.component.html" region="app-component-template" header="src/app/app.component.html"></code-example>
To see your app in the browser so far, enter the following command in the terminal window:
<code-example language="bash">
@ -102,7 +142,7 @@ These buttons work, because the CLI automatically added the routes to the featur
{@a config-routes}
## Imports and route configuration
### Imports and route configuration
The CLI automatically added each feature module to the routes map at the application level.
Finish this off by adding the default route. In the `app-routing.module.ts` file, update the `routes` array with the following:
@ -134,7 +174,7 @@ The other feature module's routing module is configured similarly.
<code-example path="lazy-loading-ngmodules/src/app/orders/orders-routing.module.ts" id="orders-routing.module.ts" region="orders-routing-module-detail" header="src/app/orders/orders-routing.module.ts (excerpt)"></code-example>
## Confirm its working
### Verify lazy loading
You can check to see that a module is indeed being lazy loaded with the Chrome developer tools. In Chrome, open the dev tools by pressing `Cmd+Option+i` on a Mac or `Ctrl+Shift+j` on a PC and go to the Network Tab.
@ -175,6 +215,105 @@ The `forRoot()` method takes care of the *global* injector configuration for the
The `forChild()` method has no injector configuration. It uses directives such as `RouterOutlet` and `RouterLink`.
For more information, see the [`forRoot()` pattern](guide/singleton-services#forRoot) section of the [Singleton Services](guide/singleton-services) guide.
{@a preloading}
## Preloading
Preloading improves UX by loading parts of your app in the background.
You can preload modules or component data.
### Preloading modules
Preloading modules improves UX by loading parts of your app in the background so users don't have to wait for the elements to download when they activate a route.
To enable preloading of all lazy loaded modules, import the `PreloadAllModules` token from the Angular `router`.
<code-example header="AppRoutingModule (excerpt)">
import { PreloadAllModules } from '@angular/router';
</code-example>
Still in the `AppRoutingModule`, specify your preloading strategy in `forRoot()`.
<code-example header="AppRoutingModule (excerpt)">
RouterModule.forRoot(
appRoutes,
{
preloadingStrategy: PreloadAllModules
}
)
</code-example>
### Preloading component data
To preload component data, you can use a `resolver`.
Resolvers improve UX by blocking the page load until all necessary data is available to fully display the page.
#### Resolvers
Create a resolver service.
With the CLI, the command to generate a service is as follows:
<code-example language="none" class="code-shell">
ng generate service <service-name>
</code-example>
In your service, import the following router members, implement `Resolve`, and inject the `Router` service:
<code-example header="Resolver service (excerpt)">
import { Resolve } from '@angular/router';
...
export class CrisisDetailResolverService implements Resolve<> {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<> {
// your logic goes here
}
}
</code-example>
Import this resolver into your module's routing module.
<code-example header="Feature module's routing module (excerpt)">
import { YourResolverService } from './your-resolver.service';
</code-example>
Add a `resolve` object to the component's `route` configuration.
<code-example header="Feature module's routing module (excerpt)">
{
path: '/your-path',
component: YourComponent,
resolve: {
crisis: YourResolverService
}
}
</code-example>
In the component, use an `Observable` to get the data from the `ActivatedRoute`.
<code-example header="Component (excerpt)">
ngOnInit() {
this.route.data
.subscribe((your-parameters) => {
// your data-specific code goes here
});
}
</code-example>
For more information with a working example, see the [routing tutorial section on preloading](guide/router#preloading-background-loading-of-feature-areas).
<hr>
## More on NgModules and routing

View File

@ -1,45 +1,49 @@
# Lifecycle hooks
# Hooking into the component lifecycle
A component has a lifecycle managed by Angular.
A component instance has a lifecycle that starts when Angular instantiates the component class and renders the component view along with its child views.
The lifecycle continues with change detection, as Angular checks to see when data-bound properties change, and updates both the view and the component instance as needed.
The lifecycle ends when Angular destroys the component instance and removes its rendered template from the DOM.
Directives have a similar lifecycle, as Angular creates, updates, and destroys instances in the course of execution.
Angular creates and renders components along with their children, checks when their data-bound properties change, and destroys them before removing them from the DOM.
Your application can use [lifecycle hook methods](guide/glossary#lifecycle-hook "Definition of lifecycle hook") to tap into key events in the lifecycle of a component or directive in order to initialize new instances, initiate change detection when needed, respond to updates during change detection, and clean up before deletion of instances.
Angular offers **lifecycle hooks**
that provide visibility into these key life moments and the ability to act when they occur.
## Prerequisites
A directive has the same set of lifecycle hooks.
Before working with lifecycle hooks, you should have a basic understanding of the following:
* [TypeScript programming](https://www.typescriptlang.org/).
* Angular app-design fundamentals, as described in [Angular Concepts](guide/architecture "Introduction to fundamental app-design concepts").
{@a hooks-overview}
## Component lifecycle hooks overview
## Responding to lifecycle events
Directive and component instances have a lifecycle
as Angular creates, updates, and destroys them.
Developers can tap into key moments in that lifecycle by implementing
one or more of the *lifecycle hook* interfaces in the Angular `core` library.
You can respond to events in the lifecycle of a component or directive by implementing one or more of the *lifecycle hook* interfaces in the Angular `core` library.
The hooks give you the opportunity to act on a component or directive instance at the appropriate moment, as Angular creates, updates, or destroys that instance.
Each interface has a single hook method whose name is the interface name prefixed with `ng`.
For example, the `OnInit` interface has a hook method named `ngOnInit()`
that Angular calls shortly after creating the component:
Each interface defines the prototype for a single hook method, whose name is the interface name prefixed with `ng`.
For example, the `OnInit` interface has a hook method named `ngOnInit()`. If you implement this method in your component or directive class, Angular calls it shortly after checking the input properties for that component or directive for the first time.
<code-example path="lifecycle-hooks/src/app/peek-a-boo.component.ts" region="ngOnInit" header="peek-a-boo.component.ts (excerpt)"></code-example>
No directive or component will implement all of the lifecycle hooks.
Angular only calls a directive/component hook method *if it is defined*.
You don't have to implement all (or any) of the lifecycle hooks, just the ones you need.
{@a hooks-purpose-timing}
## Lifecycle sequence
### Lifecycle event sequence
*After* creating a component/directive by calling its constructor, Angular
calls the lifecycle hook methods in the following sequence at specific moments:
After your application instantiates a component or directive by calling its constructor, Angular calls the hook methods you have implemented at the appropriate point in the lifecycle of that instance.
Angular executes hook methods in the following sequence. You can use them to perform the following kinds of operations.
<table width="100%">
<col width="20%"></col>
<col width="80%"></col>
<col width="60%"></col>
<col width="20%"></col>
<tr>
<th>Hook</th>
<th>Purpose and Timing</th>
<th>Hook method</th>
<th>Purpose</th>
<th>Timing</th>
</tr>
<tr style='vertical-align:top'>
<td>
@ -47,9 +51,15 @@ calls the lifecycle hook methods in the following sequence at specific moments:
</td>
<td>
Respond when Angular (re)sets data-bound input properties.
Respond when Angular sets or resets data-bound input properties.
The method receives a `SimpleChanges` object of current and previous property values.
Note that this happens very frequently, so any operation you perform here impacts performance significantly.
See details in [Using change detection hooks](#onchanges) in this document.
</td>
<td>
Called before `ngOnInit()` and whenever one or more data-bound input properties change.
</td>
@ -60,10 +70,14 @@ calls the lifecycle hook methods in the following sequence at specific moments:
</td>
<td>
Initialize the directive/component after Angular first displays the data-bound properties
and sets the directive/component's input properties.
Initialize the directive or component after Angular first displays the data-bound properties
and sets the directive or component's input properties.
See details in [Initializing a component or directive](#oninit) in this document.
Called _once_, after the _first_ `ngOnChanges()`.
</td>
<td>
Called once, after the first `ngOnChanges()`.
</td>
</tr>
@ -74,8 +88,12 @@ calls the lifecycle hook methods in the following sequence at specific moments:
<td>
Detect and act upon changes that Angular can't or won't detect on its own.
See details and example in [Defining custom change detection](#docheck) in this document.
Called during every change detection run, immediately after `ngOnChanges()` and `ngOnInit()`.
</td>
<td>
Called immediately after `ngOnChanges()` on every change detection run, and immediately after `ngOnInit()` on the first run.
</td>
</tr>
@ -85,7 +103,13 @@ calls the lifecycle hook methods in the following sequence at specific moments:
</td>
<td>
Respond after Angular projects external content into the component's view / the view that a directive is in.
Respond after Angular projects external content into the component's view, or into the view that a directive is in.
See details and example in [Responding to changes in content](#aftercontent) in this document.
</td>
<td>
Called _once_ after the first `ngDoCheck()`.
@ -97,9 +121,15 @@ calls the lifecycle hook methods in the following sequence at specific moments:
</td>
<td>
Respond after Angular checks the content projected into the directive/component.
Respond after Angular checks the content projected into the directive or component.
Called after the `ngAfterContentInit()` and every subsequent `ngDoCheck()`.
See details and example in [Responding to projected content changes](#aftercontent) in this document.
</td>
<td>
Called after `ngAfterContentInit()` and every subsequent `ngDoCheck()`.
</td>
</tr>
@ -109,10 +139,15 @@ calls the lifecycle hook methods in the following sequence at specific moments:
</td>
<td>
Respond after Angular initializes the component's views and child views / the view that a directive is in.
Respond after Angular initializes the component's views and child views, or the view that contains the directive.
See details and example in [Responding to view changes](#afterview) in this document.
</td>
<td>
Called _once_ after the first `ngAfterContentChecked()`.
</td>
</tr>
<tr style='vertical-align:top'>
@ -121,7 +156,11 @@ calls the lifecycle hook methods in the following sequence at specific moments:
</td>
<td>
Respond after Angular checks the component's views and child views / the view that a directive is in.
Respond after Angular checks the component's views and child views, or the view that contains the directive.
</td>
<td>
Called after the `ngAfterViewInit()` and every subsequent `ngAfterContentChecked()`.
@ -133,53 +172,32 @@ calls the lifecycle hook methods in the following sequence at specific moments:
</td>
<td>
Cleanup just before Angular destroys the directive/component.
Cleanup just before Angular destroys the directive or component.
Unsubscribe Observables and detach event handlers to avoid memory leaks.
See details in [Cleaning up on instance destruction](#ondestroy) in this document.
Called _just before_ Angular destroys the directive/component.
</td>
<td>
Called immediately before Angular destroys the directive or component.
</td>
</tr>
</table>
{@a interface-optional}
## Interfaces are optional (technically)
The interfaces are optional for JavaScript and Typescript developers from a purely technical perspective.
The JavaScript language doesn't have interfaces.
Angular can't see TypeScript interfaces at runtime because they disappear from the transpiled JavaScript.
Fortunately, they aren't necessary.
You don't have to add the lifecycle hook interfaces to directives and components to benefit from the hooks themselves.
Angular instead inspects directive and component classes and calls the hook methods *if they are defined*.
Angular finds and calls methods like `ngOnInit()`, with or without the interfaces.
Nonetheless, it's good practice to add interfaces to TypeScript directive classes
in order to benefit from strong typing and editor tooling.
{@a other-lifecycle-hooks}
## Other Angular lifecycle hooks
Other Angular sub-systems may have their own lifecycle hooks apart from these component hooks.
3rd party libraries might implement their hooks as well in order to give developers more
control over how these libraries are used.
{@a the-sample}
## Lifecycle examples
### Lifecycle example set
The <live-example></live-example>
demonstrates the lifecycle hooks in action through a series of exercises
demonstrates the use of lifecycle hooks through a series of exercises
presented as components under the control of the root `AppComponent`.
They follow a common pattern: a *parent* component serves as a test rig for
In each case a *parent* component serves as a test rig for
a *child* component that illustrates one or more of the lifecycle hook methods.
Here's a brief description of each exercise:
The following table lists the exercises with brief descriptions.
The sample code is also used to illustrate specific tasks in the following sections.
<table width="100%">
<col width="20%"></col>
@ -205,12 +223,9 @@ Here's a brief description of each exercise:
</td>
<td>
Directives have lifecycle hooks too.
A `SpyDirective` can log when the element it spies upon is
created or destroyed using the `ngOnInit` and `ngOnDestroy` hooks.
This example applies the `SpyDirective` to a `<div>` in an `ngFor` *hero* repeater
managed by the parent `SpyComponent`.
Shows how you can use lifecycle hooks with a custom directive.
The `SpyDirective` implements the `ngOnInit()` and `ngOnDestroy()` hooks,
and uses them to watch and report when an element goes in or out of the current view.
</td>
</tr>
@ -220,9 +235,9 @@ Here's a brief description of each exercise:
</td>
<td>
See how Angular calls the `ngOnChanges()` hook with a `changes` object
every time one of the component input properties changes.
Shows how to interpret the `changes` object.
Demonstrates how Angular calls the `ngOnChanges()` hook
every time one of the component input properties changes,
and shows how to interpret the `changes` object passed to the hook method.
</td>
</tr>
@ -232,8 +247,8 @@ Here's a brief description of each exercise:
</td>
<td>
Implements an `ngDoCheck()` method with custom change detection.
See how often Angular calls this hook and watch it post changes to a log.
Implements the `ngDoCheck()` method with custom change detection.
Watch the hook post changes to a log to see how often Angular calls this hook.
</td>
</tr>
@ -243,8 +258,8 @@ Here's a brief description of each exercise:
</td>
<td>
Shows what Angular means by a *view*.
Demonstrates the `ngAfterViewInit` and `ngAfterViewChecked` hooks.
Shows what Angular means by a [view](guide/glossary#view "Definition of view.").
Demonstrates the `ngAfterViewInit()` and `ngAfterViewChecked()` hooks.
</td>
</tr>
@ -256,40 +271,87 @@ Here's a brief description of each exercise:
Shows how to project external content into a component and
how to distinguish projected content from a component's view children.
Demonstrates the `ngAfterContentInit` and `ngAfterContentChecked` hooks.
Demonstrates the `ngAfterContentInit()` and `ngAfterContentChecked()` hooks.
</td>
</tr>
<tr style='vertical-align:top'>
<td>
Counter
<a href="#counter">Counter</a>
</td>
<td>
Demonstrates a combination of a component and a directive
each with its own hooks.
In this example, a `CounterComponent` logs a change (via `ngOnChanges`)
every time the parent component increments its input counter property.
Meanwhile, the `SpyDirective` from the previous example is applied
to the `CounterComponent` log where it watches log entries being created and destroyed.
Demonstrates a combination of a component and a directive, each with its own hooks.
</td>
</tr>
</table>
The remainder of this page discusses selected exercises in further detail.
{@a oninit}
## Initializing a component or directive
Use the `ngOnInit()` method to perform the following initialization tasks.
* Perform complex initializations outside of the constructor.
Components should be cheap and safe to construct.
You should not, for example, fetch data in a component constructor.
You shouldn't worry that a new component will try to contact a remote server when
created under test or before you decide to display it.
An `ngOnInit()` is a good place for a component to fetch its initial data.
For an example, see the [Tour of Heroes tutorial](tutorial/toh-pt4#oninit).
<div class="alert is-helpful">
In [Flaw: Constructor does Real Work](http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/), Misko Hevery, Angular team lead, explains why you should avoid complex constructor logic.
</div>
* Set up the component after Angular sets the input properties.
Constructors should do no more than set the initial local variables to simple values.
Keep in mind that a directive's data-bound input properties are not set until _after construction_.
If you need to initialize the directive based on those properties, set them when `ngOnInit()` runs.
<div class="alert is-helpful">
The `ngOnChanges()` method is your first opportunity to access those properties.
Angular calls `ngOnChanges()` before `ngOnInit()`, but also many times after that.
It only calls `ngOnInit()` once.
</div>
{@a ondestroy}
## Cleaning up on instance destruction
Put cleanup logic in `ngOnDestroy()`, the logic that must run before Angular destroys the directive.
This is the place to free resources that won't be garbage-collected automatically.
You risk memory leaks if you neglect to do so.
* Unsubscribe from Observables and DOM events.
* Stop interval timers.
* Unregister all callbacks that the directive registered with global or application services.
The `ngOnDestroy()` method is also the time to notify another part of the application that the component is going away.
## General examples
The following examples demonstrate the call sequence and relative frequency of the various lifecycle events, and how the hooks can be used separately or together for components and directives.
{@a peek-a-boo}
## Peek-a-boo: all hooks
### Sequence and frequency of all lifecycle events
The `PeekABooComponent` demonstrates all of the hooks in one component.
To show how Angular calls the hooks in the expected order, the `PeekABooComponent` demonstrates all of the hooks in one component.
You would rarely, if ever, implement all of the interfaces like this.
The peek-a-boo exists to show how Angular calls the hooks in the expected order.
In practice you would rarely, if ever, implement all of the interfaces the way this demo does.
This snapshot reflects the state of the log after the user clicked the *Create...* button and then the *Destroy...* button.
The following snapshot reflects the state of the log after the user clicked the *Create...* button and then the *Destroy...* button.
<div class="lightbox">
<img src="generated/images/guide/lifecycle-hooks/peek-a-boo.png" alt="Peek-a-boo">
@ -301,52 +363,42 @@ The sequence of log messages follows the prescribed hook calling order:
<div class="alert is-helpful">
The constructor isn't an Angular hook *per se*.
The log confirms that input properties (the `name` property in this case) have no assigned values at construction.
Notice that the log confirms that input properties (the `name` property in this case) have no assigned values at construction.
The input properties are available to the `onInit()` method for further initialization.
</div>
Had the user clicked the *Update Hero* button, the log would show another `OnChanges` and two more triplets of
`DoCheck`, `AfterContentChecked` and `AfterViewChecked`.
Clearly these three hooks fire *often*. Keep the logic in these hooks as lean as possible!
The next examples focus on hook details.
Had the user clicked the *Update Hero* button, the log would show another `OnChanges` and two more triplets of `DoCheck`, `AfterContentChecked` and `AfterViewChecked`.
Notice that these three hooks fire *often*, so it is important to keep their logic as lean as possible.
{@a spy}
## Spying *OnInit* and *OnDestroy*
### Use directives to watch the DOM
Go undercover with these two spy hooks to discover when an element is initialized or destroyed.
The `Spy` example demonstrates how you can use hook method for directives as well as components.
The `SpyDirective` implements two hooks, `ngOnInit()` and `ngOnDestroy()`, in order to discover when a watched element is in the current view.
This is the perfect infiltration job for a directive.
The heroes will never know they're being watched.
This template applies the `SpyDirective` to a `<div>` in the `ngFor` *hero* repeater managed by the parent `SpyComponent`.
<div class="alert is-helpful">
The example does not perform any initialization or clean-up.
It just tracks the appearance and disappearance of an element in the view by recording when the directive itself is instantiated and destroyed.
Kidding aside, pay attention to two key points:
A spy directive like this can provide insight into a DOM object that you cannot change directly.
You can't touch the implementation of a native `<div>`, or modify a third party component.
You can, however watch these elements with a directive.
1. Angular calls hook methods for *directives* as well as components.<br><br>
2. A spy directive can provide insight into a DOM object that you cannot change directly.
Obviously you can't touch the implementation of a native `<div>`.
You can't modify a third party component either.
But you can watch both with a directive.
</div>
The sneaky spy directive is simple, consisting almost entirely of `ngOnInit()` and `ngOnDestroy()` hooks
The directive defines `ngOnInit()` and `ngOnDestroy()` hooks
that log messages to the parent via an injected `LoggerService`.
<code-example path="lifecycle-hooks/src/app/spy.directive.ts" region="spy-directive" header="src/app/spy.directive.ts"></code-example>
You can apply the spy to any native or component element and it'll be initialized and destroyed
You can apply the spy to any native or component element, and see that it is initialized and destroyed
at the same time as that element.
Here it is attached to the repeated hero `<div>`:
<code-example path="lifecycle-hooks/src/app/spy.component.html" region="template" header="src/app/spy.component.html"></code-example>
Each spy's birth and death marks the birth and death of the attached hero `<div>`
Each spy's creation and destruction marks the appearance and disappearance of the attached hero `<div>`
with an entry in the *Hook Log* as seen here:
<div class="lightbox">
@ -359,70 +411,20 @@ The *Reset* button clears the `heroes` list.
Angular removes all hero `<div>` elements from the DOM and destroys their spy directives at the same time.
The spy's `ngOnDestroy()` method reports its last moments.
The `ngOnInit()` and `ngOnDestroy()` methods have more vital roles to play in real applications.
{@a counter}
{@a oninit}
### Use component and directive hooks together
### _OnInit()_
In this example, a `CounterComponent` uses the `ngOnChanges()` method to log a change every time the parent component increments its input `counter` property.
Use `ngOnInit()` for two main reasons:
1. To perform complex initializations shortly after construction.
1. To set up the component after Angular sets the input properties.
Experienced developers agree that components should be cheap and safe to construct.
<div class="alert is-helpful">
Misko Hevery, Angular team lead,
[explains why](http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/)
you should avoid complex constructor logic.
</div>
Don't fetch data in a component constructor.
You shouldn't worry that a new component will try to contact a remote server when
created under test or before you decide to display it.
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
[Tour of Heroes Tutorial](tutorial/toh-pt4#oninit) guide shows how.
Remember also that a directive's data-bound input properties are not set until _after construction_.
That's a problem if you need to initialize the directive based on those properties.
They'll have been set when `ngOnInit()` runs.
<div class="alert is-helpful">
The `ngOnChanges()` method is your first opportunity to access those properties.
Angular calls `ngOnChanges()` before `ngOnInit()` and many times after that.
It only calls `ngOnInit()` once.
</div>
You can count on Angular to call the `ngOnInit()` method _soon_ after creating the component.
That's where the heavy initialization logic belongs.
{@a ondestroy}
### _OnDestroy()_
Put cleanup logic in `ngOnDestroy()`, the logic that *must* run before Angular destroys the directive.
This is the time to notify another part of the application that the component is going away.
This is the place to free resources that won't be garbage collected automatically.
Unsubscribe from Observables and DOM events. Stop interval timers.
Unregister all callbacks that this directive registered with global or application services.
You risk memory leaks if you neglect to do so.
This example applies the `SpyDirective` from the previous example to the `CounterComponent` log, in order to watch the creation and destruction of log entries.
{@a onchanges}
## _OnChanges()_
## Using change detection hooks
Angular calls its `ngOnChanges()` method whenever it detects changes to ***input properties*** of the component (or directive).
This example monitors the `OnChanges` hook.
Angular calls the `ngOnChanges()` method of a component or directive whenever it detects changes to the ***input properties***.
The *onChanges* example demonstrates this by monitoring the `OnChanges()` hook.
<code-example path="lifecycle-hooks/src/app/on-changes.component.ts" region="ng-on-changes" header="on-changes.component.ts (excerpt)"></code-example>
@ -434,7 +436,7 @@ The example component, `OnChangesComponent`, has two input properties: `hero` an
<code-example path="lifecycle-hooks/src/app/on-changes.component.ts" region="inputs" header="src/app/on-changes.component.ts"></code-example>
The host `OnChangesParentComponent` binds to them like this:
The host `OnChangesParentComponent` binds to them as follows.
<code-example path="lifecycle-hooks/src/app/on-changes-parent.component.html" region="on-changes" header="src/app/on-changes-parent.component.html"></code-example>
@ -445,51 +447,20 @@ Here's the sample in action as the user makes changes.
</div>
The log entries appear as the string value of the *power* property changes.
But the `ngOnChanges` does not catch changes to `hero.name`
That's surprising at first.
Notice, however, that the `ngOnChanges()` method does not catch changes to `hero.name`.
This is because Angular calls the hook only when the value of the input property changes.
In this case, `hero` is the input property, and the value of the `hero` property is the *reference to the hero object*.
The object reference did not change when the value of its own `name` property changed.
Angular only calls the hook when the value of the input property changes.
The value of the `hero` property is the *reference to the hero object*.
Angular doesn't care that the hero's own `name` property changed.
The hero object *reference* didn't change so, from Angular's perspective, there is no change to report!
{@a docheck}
## _DoCheck()_
Use the `DoCheck` hook to detect and act upon changes that Angular doesn't catch on its own.
<div class="alert is-helpful">
Use this method to detect a change that Angular overlooked.
</div>
The *DoCheck* sample extends the *OnChanges* sample with the following `ngDoCheck()` hook:
<code-example path="lifecycle-hooks/src/app/do-check.component.ts" region="ng-do-check" header="DoCheckComponent (ngDoCheck)"></code-example>
This code inspects certain _values of interest_, capturing and comparing their current state against previous values.
It writes a special message to the log when there are no substantive changes to the `hero` or the `power`
so you can see how often `DoCheck` is called. The results are illuminating:
<div class="lightbox">
<img src='generated/images/guide/lifecycle-hooks/do-check-anim.gif' alt="DoCheck">
</div>
While the `ngDoCheck()` hook can detect when the hero's `name` has changed, it has a frightful cost.
This hook is called with enormous frequency&mdash;after _every_
change detection cycle no matter where the change occurred.
It's called over twenty times in this example before the user can do anything.
Most of these initial checks are triggered by Angular's first rendering of *unrelated data elsewhere on the page*.
Mere mousing into another `<input>` triggers a call.
Relatively few calls reveal actual changes to pertinent data.
Clearly our implementation must be very lightweight or the user experience suffers.
{@a afterview}
## AfterView
### Responding to view changes
As Angular traverses the [view hierarchy](guide/glossary#view-hierarchy "Definition of view hierarchy definition") during change detection, it needs to be sure that a change in a child does not attempt to cause a change in its own parent. Such a change would not be rendered properly, because of how [unidirectional data flow](guide/glossary#unidirectional-data-flow "Definition") works.
If you need to make a change that inverts the expected data flow, you must trigger a new change detection cycle to allow that change to be rendered.
The examples illustrate how to make such changes safely.
The *AfterView* sample explores the `AfterViewInit()` and `AfterViewChecked()` hooks that Angular calls
*after* it creates a component's child views.
@ -506,47 +477,46 @@ The following hooks take action based on changing values *within the child view*
which can only be reached by querying for the child view via the property decorated with
[@ViewChild](api/core/ViewChild).
<code-example path="lifecycle-hooks/src/app/after-view.component.ts" region="hooks" header="AfterViewComponent (class excerpts)"></code-example>
{@a wait-a-tick}
### Abide by the unidirectional data flow rule
The `doSomething()` method updates the screen when the hero name exceeds 10 characters.
#### Wait before updating the view
In this example, the `doSomething()` method updates the screen when the hero name exceeds 10 characters, but waits a tick before updating `comment`.
<code-example path="lifecycle-hooks/src/app/after-view.component.ts" region="do-something" header="AfterViewComponent (doSomething)"></code-example>
Why does the `doSomething()` method wait a tick before updating `comment`?
Both the `AfterViewInit()` and `AfterViewChecked()` hooks fire after the component's view has been composed.
If you modify the code so that the hook updates the component's data-bound `comment` property immediately, you can see that Angular throws an error.
Angular's unidirectional data flow rule forbids updates to the view *after* it has been composed.
Both of these hooks fire _after_ the component's view has been composed.
The `LoggerService.tick_then()` statement postpones the log update
for one turn of the browser's JavaScript cycle, which triggers a new change-detection cycle.
Angular throws an error if the hook updates the component's data-bound `comment` property immediately (try it!).
The `LoggerService.tick_then()` postpones the log update
for one turn of the browser's JavaScript cycle and that's just long enough.
#### Write lean hook methods to avoid performance problems
Here's *AfterView* in action:
When you run the *AfterView* sample, notice how frequently Angular calls `AfterViewChecked()`$emdash;often when there are no changes of interest.
Be very careful about how much logic or computation you put into one of these methods.
<div class="lightbox">
<img src='generated/images/guide/lifecycle-hooks/after-view-anim.gif' alt="AfterView">
</div>
Notice that Angular frequently calls `AfterViewChecked()`, often when there are no changes of interest.
Write lean hook methods to avoid performance problems.
{@a aftercontent}
## AfterContent
The *AfterContent* sample explores the `AfterContentInit()` and `AfterContentChecked()` hooks that Angular calls
*after* Angular projects external content into the component.
{@a aftercontent-hooks}
{@a content-projection}
### Content projection
### Responding to projected content changes
*Content projection* is a way to import HTML content from outside the component and insert that content
into the component's template in a designated spot.
You can identify content projection in a template by looking for the following constructs.
* HTML between component element tags.
* The presence of `<ng-content>` tags in the component's template.
<div class="alert is-helpful">
@ -554,9 +524,12 @@ into the component's template in a designated spot.
</div>
Consider this variation on the [previous _AfterView_](guide/lifecycle-hooks#afterview) example.
The *AfterContent* sample explores the `AfterContentInit()` and `AfterContentChecked()` hooks that Angular calls *after* Angular projects external content into the component.
Consider this variation on the [previous _AfterView_](#afterview) example.
This time, instead of including the child view within the template, it imports the content from
the `AfterContentComponent`'s parent. Here's the parent's template:
the `AfterContentComponent`'s parent.
The following is the parent's template.
<code-example path="lifecycle-hooks/src/app/after-content.component.ts" region="parent-template" header="AfterContentParentComponent (template excerpt)"></code-example>
@ -564,7 +537,7 @@ Notice that the `<app-child>` tag is tucked between the `<after-content>` tags.
Never put content between a component's element tags *unless you intend to project that content
into the component*.
Now look at the component's template:
Now look at the component's template.
<code-example path="lifecycle-hooks/src/app/after-content.component.ts" region="template" header="AfterContentComponent (template)"></code-example>
@ -576,18 +549,8 @@ In this case, the projected content is the `<app-child>` from the parent.
<img src='generated/images/guide/lifecycle-hooks/projected-child-view.png' alt="Projected Content">
</div>
<div class="alert is-helpful">
The telltale signs of *content projection* are twofold:
* HTML between component element tags.
* The presence of `<ng-content>` tags in the component's template.
</div>
{@a aftercontent-hooks}
### AfterContent hooks
#### Using AfterContent hooks
*AfterContent* hooks are similar to the *AfterView* hooks.
The key difference is in the child component.
@ -606,11 +569,44 @@ which can only be reached by querying for them via the property decorated with
{@a no-unidirectional-flow-worries}
### No unidirectional flow worries with _AfterContent_
<div class="alert is-helpful>
This component's `doSomething()` method update's the component's data-bound `comment` property immediately.
There's no [need to wait](guide/lifecycle-hooks#wait-a-tick).
<header>No need to wait for content updates</header>
Recall that Angular calls both *AfterContent* hooks before calling either of the *AfterView* hooks.
This component's `doSomething()` method updates the component's data-bound `comment` property immediately.
There's no need to [delay the update to ensure proper rendering](#wait-a-tick "Delaying updates").
Angular calls both *AfterContent* hooks before calling either of the *AfterView* hooks.
Angular completes composition of the projected content *before* finishing the composition of this component's view.
There is a small window between the `AfterContent...` and `AfterView...` hooks to modify the host view.
There is a small window between the `AfterContent...` and `AfterView...` hooks that allows you to modify the host view.
</div>
{@a docheck}
## Defining custom change detection
To monitor changes that occur where `ngOnChanges()` won't catch them, you can implement your own change check, as shown in the *DoCheck* example.
This example shows how you can use the `ngDoCheck()` hook to detect and act upon changes that Angular doesn't catch on its own.
The *DoCheck* sample extends the *OnChanges* sample with the following `ngDoCheck()` hook:
<code-example path="lifecycle-hooks/src/app/do-check.component.ts" region="ng-do-check" header="DoCheckComponent (ngDoCheck)"></code-example>
This code inspects certain _values of interest_, capturing and comparing their current state against previous values.
It writes a special message to the log when there are no substantive changes to the `hero` or the `power` so you can see how often `DoCheck()` is called.
The results are illuminating.
<div class="lightbox">
<img src='generated/images/guide/lifecycle-hooks/do-check-anim.gif' alt="DoCheck">
</div>
While the `ngDoCheck()` hook can detect when the hero's `name` has changed, it is very expensive.
This hook is called with enormous frequency&mdash;after _every_
change detection cycle no matter where the change occurred.
It's called over twenty times in this example before the user can do anything.
Most of these initial checks are triggered by Angular's first rendering of *unrelated data elsewhere on the page*.
Just moving the cursor into another `<input>` triggers a call.
Relatively few calls reveal actual changes to pertinent data.
If you use this hook, your implementation must be extremely lightweight or the user experience suffers.

View File

@ -53,11 +53,11 @@ With dynamic queries (`static: false`), the query resolves after either `ngAfter
The result will be updated for changes to your view, such as changes to `ngIf` and `ngFor` blocks.
For more information, see the following entries in the
[Static Query Migration Guide](https://angular.io/guide/static-query-migration):
[Static Query Migration Guide](guide/static-query-migration):
* [How do I choose which `static` flag value to use: `true` or `false`?](https://angular.io/guide/static-query-migration#how-do-i-choose-which-static-flag-value-to-use-true-or-false)
* [How do I choose which `static` flag value to use: `true` or `false`?](guide/static-query-migration#how-do-i-choose-which-static-flag-value-to-use-true-or-false)
* [Is there a case where I should use `{static: true}`?](https://angular.io/guide/static-query-migration#is-there-a-case-where-i-should-use-static-true)
* [Is there a case where I should use `{static: true}`?](guide/static-query-migration#is-there-a-case-where-i-should-use-static-true)
</div>

View File

@ -207,7 +207,7 @@ The following table summarizes the `@NgModule` metadata properties.
Angular automatically adds components in the module's `bootstrap` and route definitions into the `entryComponents` list.
That leaves only components bootstrapped using one of the imperative techniques, such as [`ViewComponentRef.createComponent()`](https://angular.io/api/core/ViewContainerRef#createComponent) as undiscoverable.
That leaves only components bootstrapped using one of the imperative techniques, such as [`ViewComponentRef.createComponent()`](api/core/ViewContainerRef#createComponent) as undiscoverable.
Dynamic component loading is not common in most apps beyond the router. If you need to dynamically load components, you must add these components to the `entryComponents` list yourself.

View File

@ -12,7 +12,7 @@ Angular provides an `EventEmitter` class that is used when publishing values fro
`EventEmitter` extends [RxJS `Subject`](https://rxjs.dev/api/index/class/Subject), adding an `emit()` method so it can send arbitrary values.
When you call `emit()`, it passes the emitted value to the `next()` method of any subscribed observer.
A good example of usage can be found in the [EventEmitter](https://angular.io/api/core/EventEmitter) documentation. Here is the example component that listens for open and close events:
A good example of usage can be found in the [EventEmitter](api/core/EventEmitter) documentation. Here is the example component that listens for open and close events:
`<zippy (open)="onOpen($event)" (close)="onClose($event)"></zippy>`
@ -30,7 +30,7 @@ Angulars `HttpClient` returns observables from HTTP method calls. For instanc
## Async pipe
The [AsyncPipe](https://angular.io/api/common/AsyncPipe) subscribes to an observable or promise and returns the latest value it has emitted. When a new value is emitted, the pipe marks the component to be checked for changes.
The [AsyncPipe](api/common/AsyncPipe) subscribes to an observable or promise and returns the latest value it has emitted. When a new value is emitted, the pipe marks the component to be checked for changes.
The following example binds the `time` observable to the component's view. The observable continuously updates the view with the current time.
@ -38,16 +38,16 @@ The following example binds the `time` observable to the component's view. The o
## Router
[`Router.events`](https://angular.io/api/router/Router#events) provides events as observables. You can use the `filter()` operator from RxJS to look for events of interest, and subscribe to them in order to make decisions based on the sequence of events in the navigation process. Here's an example:
[`Router.events`](api/router/Router#events) provides events as observables. You can use the `filter()` operator from RxJS to look for events of interest, and subscribe to them in order to make decisions based on the sequence of events in the navigation process. Here's an example:
<code-example path="observables-in-angular/src/main.ts" header="Router events" region="router"></code-example>
The [ActivatedRoute](https://angular.io/api/router/ActivatedRoute) is an injected router service that makes use of observables to get information about a route path and parameters. For example, `ActivatedRoute.url` contains an observable that reports the route path or paths. Here's an example:
The [ActivatedRoute](api/router/ActivatedRoute) is an injected router service that makes use of observables to get information about a route path and parameters. For example, `ActivatedRoute.url` contains an observable that reports the route path or paths. Here's an example:
<code-example path="observables-in-angular/src/main.ts" header="ActivatedRoute" region="activated_route"></code-example>
## Reactive forms
Reactive forms have properties that use observables to monitor form control values. The [`FormControl`](https://angular.io/api/forms/FormControl) properties `valueChanges` and `statusChanges` contain observables that raise change events. Subscribing to an observable form-control property is a way of triggering application logic within the component class. For example:
Reactive forms have properties that use observables to monitor form control values. The [`FormControl`](api/forms/FormControl) properties `valueChanges` and `statusChanges` contain observables that raise change events. Subscribing to an observable form-control property is a way of triggering application logic within the component class. For example:
<code-example path="observables-in-angular/src/main.ts" header="Reactive forms" region="forms"></code-example>

View File

@ -58,8 +58,8 @@ Though you can provide services by lazy loading modules, not all services can be
Another way to limit provider scope is by adding the service you want to limit to the components
`providers` array. Component providers and NgModule providers are independent of each other. This
method is helpful when you want to eagerly load a module that needs a service all to itself.
Providing a service in the component limits the service only to that component (other components in
the same module cant access it).
Providing a service in the component limits the service only to that component and its descendants.
Other components in the same module cant access it.
<code-example path="providers/src/app/app.component.ts" region="component-providers" header="src/app/app.component.ts"></code-example>

View File

@ -9,11 +9,11 @@ A basic understanding of the following concepts:
<hr>
The [AnimationOptions](https://angular.io/api/animations/AnimationOptions) interface in Angular animations enables you to create animations that you can reuse across different components.
The [AnimationOptions](api/animations/AnimationOptions) interface in Angular animations enables you to create animations that you can reuse across different components.
## Creating reusable animations
To create a reusable animation, use the [`animation()`](https://angular.io/api/animations/animation) method to define an animation in a separate `.ts` file and declare this animation definition as a `const` export variable. You can then import and reuse this animation in any of your app components using the [`useAnimation()`](https://angular.io/api/animations/useAnimation) API.
To create a reusable animation, use the [`animation()`](api/animations/animation) method to define an animation in a separate `.ts` file and declare this animation definition as a `const` export variable. You can then import and reuse this animation in any of your app components using the [`useAnimation()`](api/animations/useAnimation) API.
<code-example path="animations/src/app/animations.ts" header="src/app/animations.ts" region="reusable" language="typescript"></code-example>

View File

@ -169,7 +169,7 @@ To get information from a route:
<code-example header="In the component (excerpt)">
ngOnInit() {
this.activatedRoute.queryParams.subscribe(params => {
this.route.queryParams.subscribe(params => {
this.name = params['name'];
});
}
@ -357,130 +357,12 @@ Inject `ActivatedRoute` and `Router` in the constructor of the component class s
{@a lazy-loading}
## Lazy loading modules
## Lazy loading
To lazy load Angular modules, use `loadchildren` (instead of `component`) in your `AppRoutingModule` `routes` configuration as follows:
<code-example header="AppRoutingModule (excerpt)">
const routes: Routes = [
{
path: 'items',
loadChildren: () => import('./items/items.module').then(m => m.ItemsModule)
}
];
</code-example>
In the lazy loaded module's routing module, add a route for the component.
<code-example header="Routing module for lazy loaded module (excerpt)">
const routes: Routes = [
{
path: '',
component: ItemsComponent
}
];
</code-example>
Also be sure to remove the `ItemsModule` from the `AppModule`. For more information on lazy loading modules see [Lazy-loading feature modules](guide/lazy-loading-ngmodules).
{@a preloading}
## Preloading
Preloading improves UX by loading parts of your app in the background. You can preload modules or component data.
### Preloading modules
To enable preloading of all lazy loaded modules, import the `PreloadAllModules` token from the Angular `router`.
<code-example header="AppRoutingModule (excerpt)">
import { PreloadAllModules } from '@angular/router';
</code-example>
Still in the `AppRoutingModule`, specify your preloading strategy in `forRoot()`.
<code-example header="AppRoutingModule (excerpt)">
RouterModule.forRoot(
appRoutes,
{
preloadingStrategy: PreloadAllModules
}
)
</code-example>
### Preloading component data
You can preload component data so that all elements and data on a page render at the same time when the user activates a route. To preload component data, you can use a `resolver`.
#### Resolvers
Create a resolver service. With the CLI, the command to generate a service is as follows:
<code-example language="none" class="code-shell">
ng generate service <service-name>
</code-example>
In your service, import the following router members, implement `Resolve`, and inject the `Router` service:
<code-example header="Resolver service (excerpt)">
import { Resolve } from '@angular/router';
...
export class CrisisDetailResolverService implements Resolve<> {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<> {
// your logic goes here
}
}
</code-example>
Import this resolver into your module's routing module.
<code-example header="Feature module's routing module (excerpt)">
import { YourResolverService } from './your-resolver.service';
</code-example>
Add a `resolve` object to the component's `route` configuration.
<code-example header="Feature module's routing module (excerpt)">
{
path: '/your-path',
component: YourComponent,
resolve: {
crisis: YourResolverService
}
}
</code-example>
In the component, use an observable to get the data from the `ActivatedRoute`.
<code-example header="Component (excerpt)">
ngOnInit() {
this.route.data
.subscribe((your-parameters) => {
// your data-specific code goes here
});
}
</code-example>
For more information with a working example, see the [routing tutorial section on preloading](guide/router#preloading-background-loading-of-feature-areas).
You can configure your routes to lazy load modules, which means that Angular only loads modules as needed, rather than loading all modules when the app launches.
Additionally, you can preload parts of your app in the background to improve the user experience.
For more information on lazy loading and preloading see the dedicated guide [Lazy loading NgModules](guide/lazy-loading-ngmodules).
## Preventing unauthorized access

View File

@ -23,7 +23,7 @@ The above command completes the following actions:
2. Enables service worker build support in the CLI.
3. Imports and registers the service worker in the app module.
4. Updates the `index.html` file:
* Includes a link to add the `manifest.json` file.
* Includes a link to add the `manifest.webmanifest` file.
* Adds meta tags for `theme-color`.
5. Installs icon files to support the installed Progressive Web App (PWA).
6. Creates the service worker configuration file called [`ngsw-config.json`](/guide/service-worker-config), which specifies the caching behaviors and other settings.

View File

@ -44,7 +44,7 @@ For more information on how to choose, see the [next question](#how-do-i-choose)
{@a how-do-i-choose}
### How do I choose which `static` flag value to use: `true` or `false`?
In the official API docs, we have always recommended retrieving query results in [`ngAfterViewInit` for view queries](https://angular.io/api/core/ViewChild#description) and [`ngAfterContentInit` for content queries](https://angular.io/api/core/ContentChild#description).
In the official API docs, we have always recommended retrieving query results in [`ngAfterViewInit` for view queries](api/core/ViewChild#description) and [`ngAfterContentInit` for content queries](api/core/ContentChild#description).
This is because by the time those lifecycle hooks run, change detection has completed for the relevant nodes and we can guarantee that we have collected all the possible query results.
Most applications will want to use `{static: false}` for the same reason. This setting will ensure query matches that are dependent on binding resolution (e.g. results inside `*ngIf`s or `*ngFor`s) will be found by the query.

View File

@ -832,6 +832,89 @@ When the `condition` is truthy, the top (A) paragraph is removed and the bottom
<img src='generated/images/guide/structural-directives/unless-anim.gif' alt="UnlessDirective in action">
</div>
{@a directive-type-checks}
## Improving template type checking for custom directives
You can improve template type checking for custom directives by adding template guard properties to your directive definition.
These properties help the Angular template type checker find mistakes in the template at compile time, which can avoid runtime errors those mistakes can cause.
Use the type-guard properties to inform the template type checker of an expected type, thus improving compile-time type-checking for that template.
* A property `ngTemplateGuard_(someInputProperty)` lets you specify a more accurate type for an input expression within the template.
* The `ngTemplateContextGuard` static property declares the type of the template context.
This section provides example of both kinds of type-guard property.
<div class="alert is-helpful">
For more information, see [Template type checking guide](guide/template-typecheck "Template type-checking guide").
</div>
{@a narrowing-input-types}
### Make in-template type requirements more specific with template guards
A structural directive in a template controls whether that template is rendered at run time, based on its input expression.
To help the compiler catch template type errors, you should specify as closely as possible the required type of a directive's input expression when it occurs inside the template.
A type guard function *narrows* the expected type of an input expression to a subset of types that might be passed to the directive within the template at run time.
You can provide such a function to help the type-checker infer the proper type for the expression at compile time.
For example, the `NgIf` implementation uses type-narrowing to ensure that the
template is only instantiated if the input expression to `*ngIf` is truthy.
To provide the specific type requirement, the `NgIf` directive defines a [static property `ngTemplateGuard_ngIf: 'binding'`](api/common/NgIf#static-properties).
The `binding` value is a special case for a common kind of type-narrowing where the input expression is evaluated in order to satisfy the type requirement.
To provide a more specific type for an input expression to a directive within the template, add a `ngTemplateGuard_xx` property to the directive, where the suffix to the static property name is the `@Input` field name.
The value of the property can be either a general type-narrowing function based on its return type, or the string `"binding"` as in the case of `NgIf`.
For example, consider the following structural directive that takes the result of a template expression as an input.
<code-example language="ts" header="IfLoadedDirective">
export type Loaded<T> = { type: 'loaded', data: T };
export type Loading = { type: 'loading' };
export type LoadingState<T> = Loaded<T> | Loading;
export class IfLoadedDirective<T> {
@Input('ifLoaded') set state(state: LoadingState<T>) {}
static ngTemplateGuard_state<T>(dir: IfLoadedDirective<T>, expr: LoadingState<T>): expr is Loaded<T> { return true; };
export interface Person {
name: string;
}
@Component({
template: `<div *ifLoaded="state">{{ state.data }}</div>`,
})
export class AppComponent {
state: LoadingState<Person>;
}
</code-example>
In this example, the `LoadingState<T>` type permits either of two states, `Loaded<T>` or `Loading`. The expression used as the directives `state` input is of the umbrella type `LoadingState`, as its unknown what the loading state is at that point.
The `IfLoadedDirective` definition declares the static field `ngTemplateGuard_state`, which expresses the narrowing behavior.
Within the `AppComponent` template, the `*ifLoaded` structural directive should render this template only when `state` is actually `Loaded<Person>`.
The type guard allows the type checker to infer that the acceptable type of `state` within the template is a `Loaded<T>`, and further infer that `T` must be an instance of `Person`.
{@a narrowing-context-type}
### Typing the directive's context
If your structural directive provides a context to the instantiated template, you can properly type it inside the template by providing a static `ngTemplateContextGuard` function.
The following snippet shows an example of such a function.
<code-example language="ts" header="myDirective.ts">
@Directive({…})
export class ExampleDirective {
// Make sure the template checker knows the type of the context with which the
// template of this directive will be rendered
static ngTemplateContextGuard(dir: ExampleDirective, ctx: unknown): ctx is ExampleContext { return true; };
// …
}
</code-example>
{@a summary}
@ -879,7 +962,7 @@ Here is the source from the `src/app/` folder.
You learned
You learned:
* that structural directives manipulate HTML layout.
* to use [`<ng-container>`](guide/structural-directives#ngcontainer) as a grouping element when there is no suitable host element.

View File

@ -432,7 +432,7 @@ Attributes can be changed by `setAttribute()`, which re-initializes correspondin
</div>
For more information, see the [MDN Interfaces documentation](https://developer.mozilla.org/en-US/docs/Web/API#Interfaces) which has API docs for all the standard DOM elements and their properties.
Comparing the [`<td>` attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td) attributes to the [`<td>` properties](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement) provides a helpful example for differentiation.
Comparing the [`<td>` attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td) to the [`<td>` properties](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement) provides a helpful example for differentiation.
In particular, you can navigate from the attributes page to the properties via "DOM interface" link, and navigate the inheritance hierarchy up to `HTMLTableCellElement`.
@ -473,7 +473,7 @@ To control the state of the button, set the `disabled` *property*,
<div class="alert is-helpful">
Though you could technically set the `[attr.disabled]` attribute binding, the values are different in that the property binding requires to a boolean value, while its corresponding attribute binding relies on whether the value is `null` or not. Consider the following:
Though you could technically set the `[attr.disabled]` attribute binding, the values are different in that the property binding requires to be a boolean value, while its corresponding attribute binding relies on whether the value is `null` or not. Consider the following:
```html
<input [disabled]="condition ? true : false">
@ -1455,7 +1455,7 @@ Angular provides *value accessors* for all of the basic HTML form elements and t
You can't apply `[(ngModel)]` to a non-form native element or a
third-party custom component until you write a suitable value accessor. For more information, see
the API documentation on [DefaultValueAccessor](https://angular.io/api/forms/DefaultValueAccessor).
the API documentation on [DefaultValueAccessor](api/forms/DefaultValueAccessor).
You don't need a value accessor for an Angular component that
you write because you can name the value and event properties

View File

@ -167,6 +167,8 @@ Here, during type checking of the template for `AppComponent`, the `[user]="sele
Therefore, Angular assigns the `selectedUser` property to `UserDetailComponent.user`, which would result in an error if their types were incompatible.
TypeScript checks the assignment according to its type system, obeying flags such as `strictNullChecks` as they are configured in the application.
You can avoid run-time type errors by providing more specific in-template type requirements to the template type checker. Make the input type requirements for your own directives as specific as possible by providing template-guard functions in the directive definition. See [Improving template type checking for custom directives](guide/structural-directives#directive-type-checks), and [Input setter coercion](#input-setter-coercion) in this guide.
### Strict null checks
@ -201,7 +203,7 @@ There are two potential workarounds to the above issues:
As a library author, you can take several measures to provide an optimal experience for your users.
First, enabling `strictNullChecks` and including `null` in an input's type, as appropriate, communicates to your consumers whether they can provide a nullable value or not.
Additionally, it is possible to provide type hints that are specific to the template type checker, see the [Input setter coercion](guide/template-typecheck#input-setter-coercion) section of this guide.
Additionally, it is possible to provide type hints that are specific to the template type checker. See [Improving template type checking for custom directives](guide/structural-directives#directive-type-checks), and [Input setter coercion](#input-setter-coercion) below.
{@a input-setter-coercion}

View File

@ -153,7 +153,7 @@ You can define more than one animation trigger for a component. You can attach a
### Parent-child animations
Each time an animation is triggered in Angular, the parent animation always get priority and child animations are blocked. In order for a child animation to run, the parent animation must query each of the elements containing child animations and then allow the animations to run using the [`animateChild()`](https://angular.io/api/animations/animateChild) function.
Each time an animation is triggered in Angular, the parent animation always get priority and child animations are blocked. In order for a child animation to run, the parent animation must query each of the elements containing child animations and then allow the animations to run using the [`animateChild()`](api/animations/animateChild) function.
#### Disabling an animation on an HTML element
@ -178,7 +178,7 @@ You can't selectively disable multiple animations on a single element.
However, selective child animations can still be run on a disabled parent in one of the following ways:
* A parent animation can use the [`query()`](https://angular.io/api/animations/query) function to collect inner elements located in disabled areas of the HTML template.
* A parent animation can use the [`query()`](api/animations/query) function to collect inner elements located in disabled areas of the HTML template.
Those elements can still animate.
* A subanimation can be queried by a parent and then later animated with the `animateChild()` function.

View File

@ -1,7 +1,7 @@
# Using published libraries
When building Angular applications you can take advantage of sophisticated first-party libraries, such as [Angular Material](https://material.angular.io/), as well as rich ecosystem of third-party libraries.
See the [Angular Resources](https://angular.io/resources) page for links to the most popular ones.
See the [Angular Resources](resources) page for links to the most popular ones.
## Installing libraries

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -44,15 +44,6 @@
"groups": ["Angular"],
"lead": "juleskremer"
},
"alexeagle": {
"name": "Alex Eagle",
"picture": "alex-eagle.jpg",
"twitter": "jakeherringbone",
"website": "http://google.com/+alexeagle",
"bio": "Alex works on language tooling for JavaScript and TypeScript. Previously Alex spent five years in Google's developer testing tools. He has developed systems including Google's continuous integration service, capturing build&test failures, and explaining them to developers. Before Google, Alex worked at startups including Opower, and consulted for large government IT. In his 20% time, he created the Error-Prone static analysis tool, which detects common Java programming mistakes and reports them as compile errors.",
"groups": ["Collaborators"],
"lead": "igorminar"
},
"aikidave": {
"name": "Dave Shevitz",
"picture": "daveshevitz.jpg",
@ -63,7 +54,7 @@
"kyliau": {
"name": "Keen Yee Liau",
"groups": ["Angular"],
"lead": "alexeagle",
"lead": "igorminar",
"picture": "kyliau.jpg"
},
"clydin": {
@ -113,14 +104,6 @@
"groups": ["Angular"],
"lead": "igorminar"
},
"vikerman": {
"name": "Vikram Subramanian",
"picture": "vikram.jpg",
"twitter": "vikerman",
"bio": "Vikram is a Software Engineer on the Angular team focused on Engineering Productivity. That means he makes sure people on the team can move fast and not break things. Vikram enjoys doing Yoga and going on walks with his daughter.",
"groups": ["Angular"],
"lead": "alexeagle"
},
"pkozlowski-opensource": {
"name": "Pawel Kozlowski",
"picture": "pawel.jpg",
@ -314,7 +297,7 @@
"website": "http://blog.mgechev.com",
"bio": "Software engineer who enjoys theoretical computer science and its practical applications. Speaker, author of the book 'Switching to Angular', codelyzer, Guess.js, and the Go linter revive. Working for faster and more reliable software.",
"groups": ["Angular"],
"lead": "alexeagle"
"lead": "stephenfluin"
},
"urish": {
"name": "Uri Shaked",
@ -650,7 +633,7 @@
"picture": "denny.jpg",
"bio": "Denny is founder of Expert Support, a professional services firm specializing in technical communication, and leads the Angular technical writing team. His lifelong passion has been to reduce the time and effort required to understand complex technical information. Early on, he was Associate Chairman of the Computer Science Department at Stanford, where he taught introductory courses in programming. He also plays old-timers baseball in local leagues and national tournaments.",
"groups": ["Angular"],
"lead": "juleskremer"
"lead": "aikidave"
},
"jbogarthyde": {
"name": "Judy Bogart",
@ -771,7 +754,7 @@
"website": "https://github.com/dgp1130",
"bio": "Doug is an overly-opinionated software developer with a passion for making awesome developer tools. He is motivated primarily by his selfish desire to make his own life easier, but loves to share his tools and workflows with others to make everyone's lives easier.",
"groups": ["Angular"],
"lead": "igorminar"
"lead": "kyliau"
},
"martinakraus": {
"name": "Martina Kraus",
@ -840,5 +823,13 @@
"bio": "Annie is an engineering resident on the Angular Components team at Google. She is passionate about the intersection between design and technology and enjoys drawing in her free time.",
"groups": ["Angular"],
"lead": "jelbourn"
},
"cindygk": {
"name": "Cindy Greene-Kaplan",
"picture": "cindygreenekaplan.jpg",
"twitter": "CindyGK2019",
"bio": "Cindy is a Program Manager on the Angular team at Google. She is passionate about improving team processes and overall execution. She enjoys dance fitness, movies and travel.",
"groups": ["Angular"],
"lead": "juleskremer"
}
}

View File

@ -8,6 +8,6 @@ While we can't accept all contributions, qualifying contributions can be submitt
1. Your contribution must be valid, and contain a link to a page talking specifically about using Angular
1. Your contribution should have a clear and concise title and description
1. Your resource should follow our brand guidelines (see our [Presskit](https://angular.io/presskit))
1. Your resource should follow our brand guidelines (see our [Presskit](presskit))
1. Your resource should have significant benefit to Angular developers
1. Your resource should already have traction and praise from Angular developers

View File

@ -7,38 +7,32 @@
"resources": {
"awesome-angular-components": {
"desc": "A community index of components and libraries maintained on GitHub",
"rev": true,
"title": "Catalog of Angular Components & Libraries",
"url": "https://github.com/brillout/awesome-angular-components"
},
"angular-ru": {
"desc": "Angular-RU Community on GitHub is a single entry point for all resources, chats, podcasts and meetups for Angular in Russia.",
"rev": true,
"title": "Angular Conferences and Angular Camps in Moscow, Russia.",
"url": "https://angular-ru.github.io/"
},
"made-with-angular": {
"desc": "A showcase of web apps built with Angular.",
"rev": true,
"title": "Made with Angular",
"url": "https://www.madewithangular.com/"
},
"angular-subreddit": {
"desc": "An Angular-dedicated subreddit.",
"rev": true,
"title": "Angular Subreddit",
"url": "https://www.reddit.com/r/Angular2/"
},
"angular-devto" : {
"angular-devto": {
"desc": "Read and share content and chat about Angular on DEV Community.",
"url": "https://dev.to/t/angular",
"rev": true,
"title": "DEV Community"
},
"angular-in-depth" : {
"angular-in-depth": {
"desc": "The place where advanced Angular concepts are explained",
"url": "https://blog.angularindepth.com",
"rev": true,
"title": "Angular In Depth"
}
}
@ -49,28 +43,24 @@
"sdfjkdkfj": {
"desc": "Adventures in Angular is a weekly podcast dedicated to the Angular platform and related technologies, tools, languages, and practices.",
"logo": "",
"rev": true,
"title": "Adventures in Angular",
"url": "https://devchat.tv/adv-in-angular/"
},
"sdlkfjsldfkj": {
"desc": "Weekly video podcast hosted by Jeff Whelpley with all the latest and greatest happenings in the wild world of Angular.",
"logo": "",
"rev": true,
"title": "AngularAir",
"url": "https://angularair.com/"
},
"sdlkfjsldfkz": {
"desc": "A weekly German podcast for Angular on the go",
"logo": "",
"rev": true,
"title": "Happy Angular Podcast",
"url": "https://happy-angular.de/"
},
"ngruair": {
"desc": "Russian language video podcast about Angular.",
"logo": "",
"rev": true,
"title": "NgRuAir",
"url": "https://github.com/ngRuAir/ngruair"
}
@ -87,21 +77,18 @@
"a3b": {
"desc": "Ionic offers a library of mobile-optimized HTML, CSS and JS components and tools for building highly interactive native and progressive web apps.",
"logo": "http://ionicframework.com/img/ionic-logo-white.svg",
"rev": true,
"title": "Ionic",
"url": "https://ionicframework.com/docs"
},
"a4b": {
"desc": "Electron Platform for Angular.",
"logo": "",
"rev": true,
"title": "Electron",
"url": "https://github.com/maximegris/angular-electron"
},
"ab": {
"desc": "NativeScript is how you build cross-platform, native iOS and Android apps with Angular and TypeScript. Get 100% access to native APIs via JavaScript and reuse of packages from NPM, CocoaPods and Gradle. Open source and backed by Telerik.",
"logo": "",
"rev": true,
"title": "NativeScript",
"url": "https://docs.nativescript.org/angular/start/introduction"
}
@ -110,55 +97,47 @@
"Data Libraries": {
"order": 3,
"resources": {
"rx-web":{
"desc":"RxWeb Reactive Form Validators provides all types of complex, conditional, cross field, and dynamic validation on validator-based reactive forms, model-based reactive forms, and template driven forms.",
"rev": true,
"rx-web": {
"desc": "RxWeb Reactive Form Validators provides all types of complex, conditional, cross field, and dynamic validation on validator-based reactive forms, model-based reactive forms, and template driven forms.",
"title": "RxWeb Reactive Form Validators",
"url": "https://www.rxweb.io"
},
"-KLIzHDRfiB3d7W7vk-e": {
"desc": "Reactive Extensions for Angular",
"rev": true,
"title": "ngrx",
"url": "https://ngrx.io/"
},
"ngxs": {
"desc": "NGXS is a state management pattern + library for Angular. NGXS is modeled after the CQRS pattern popularly implemented in libraries like Redux and NgRx but reduces boilerplate by using modern TypeScript features such as classes and decorators.",
"rev": true,
"title": "NGXS",
"url": "https://ngxs.io/"
},
"akita": {
"desc": "Akita is a state management pattern, built on top of RxJS, which takes the idea of multiple data stores from Flux and the immutable updates from Redux, along with the concept of streaming data, to create the Observable Data Store model.",
"rev": true,
"title": "Akita",
"url": "https://netbasal.gitbook.io/akita/"
},
"ab": {
"desc": "The official library for Firebase and Angular",
"logo": "",
"rev": true,
"title": "Angular Fire",
"url": "https://github.com/angular/angularfire2"
},
"ab2": {
"desc": "Use Angular and Meteor to build full-stack JavaScript apps for Mobile and Desktop.",
"logo": "http://www.angular-meteor.com/images/logo.png",
"rev": true,
"title": "Meteor",
"url": "https://github.com/urigo/angular-meteor"
},
"ab3": {
"desc": "Apollo is a data stack for modern apps, built with GraphQL.",
"logo": "http://docs.apollostack.com/logo/large.png",
"rev": true,
"title": "Apollo",
"url": "https://www.apollographql.com/docs/angular/"
},
"ngx-api-utils": {
"desc": "ngx-api-utils is a lean library of utilities and helpers to quickly integrate any HTTP API (REST, Ajax, and any other) with Angular.",
"logo": "",
"rev": true,
"title": "ngx-api-utils",
"url": "https://github.com/ngx-api-utils/ngx-api-utils"
}
@ -170,33 +149,28 @@
"ab": {
"desc": "VS Code is a Free, Lightweight Tool for Editing and Debugging Web Apps.",
"logo": "",
"rev": true,
"title": "Visual Studio Code",
"url": "http://code.visualstudio.com/"
},
"ab2": {
"desc": "Lightweight yet powerful IDE, perfectly equipped for complex client-side development and server-side development with Node.js",
"logo": "",
"rev": true,
"title": "WebStorm",
"url": "https://www.jetbrains.com/webstorm/"
},
"ab3": {
"desc": "Capable and Ergonomic Java * IDE",
"logo": "",
"rev": true,
"title": "IntelliJ IDEA",
"url": "https://www.jetbrains.com/idea/"
},
"angular-ide": {
"desc": "Built first and foremost for Angular. Turnkey setup for beginners; powerful for experts.",
"rev": true,
"title": "Angular IDE by Webclipse",
"url": "https://www.genuitec.com/products/angular-ide"
},
"amexio-canvas": {
"desc": "Amexio Canvas is Drag and Drop Environment to create Fully Responsive Web and Smart Device HTML5/Angular Apps. Code will be auto generated and hot deployed by the Canvas for live testing. Out of the box 50+ Material Design Theme support. Commit your code to GitHub public or private repository.",
"rev": true,
"title": "Amexio Canvas Web Based Drag and Drop IDE by MetaMagic",
"url": "https://amexio.tech/"
}
@ -208,53 +182,45 @@
"a1": {
"desc": "A Google Chrome Dev Tools extension for debugging Angular applications.",
"logo": "https://augury.angular.io/images/augury-logo.svg",
"rev": true,
"title": "Augury",
"url": "http://augury.angular.io/"
},
"b1": {
"desc": "Server-side Rendering for Angular apps.",
"logo": "https://cloud.githubusercontent.com/assets/1016365/10639063/138338bc-7806-11e5-8057-d34c75f3cafc.png",
"rev": true,
"title": "Angular Universal",
"url": "https://angular.io/guide/universal"
},
"c1": {
"desc": "Lightweight development only Node.js® server",
"logo": "",
"rev": true,
"title": "Lite-server",
"url": "https://github.com/johnpapa/lite-server"
},
"cli": {
"desc": "The official Angular CLI makes it easy to create and develop applications from initial commit to production deployment. It already follows our best practices right out of the box!",
"rev": true,
"title": "Angular CLI",
"url": "https://cli.angular.io"
},
"d1": {
"desc": "Static analysis for Angular projects.",
"logo": "",
"rev": true,
"title": "Codelyzer",
"url": "https://github.com/mgechev/codelyzer"
},
"f1": {
"desc": "This tool generates dedicated documentation for Angular applications.",
"logo": "",
"rev": true,
"title": "Compodoc",
"url": "https://github.com/compodoc/compodoc"
},
"angular-playground": {
"desc": "UI development environment for building, testing, and documenting Angular applications.",
"rev": true,
"title": "Angular Playground",
"url": "http://www.angularplayground.it/"
},
"nx": {
"desc": "Nx (Nrwl Extensions for Angular) is an open source toolkit built on top of Angular CLI to help enterprise teams develop Angular at scale.",
"rev": true,
"title": "Nx",
"logo": "https://nrwl.io/assets/nx-logo.png",
"url": "https://nrwl.io/nx"
@ -262,14 +228,12 @@
"uijar": {
"desc": "A drop in module to automatically create a living style guide based on the test you write for your components.",
"logo": "",
"rev": true,
"title": "UI-jar - Test Driven Style Guide Development",
"url": "https://github.com/ui-jar/ui-jar"
},
"protactor": {
"desc": "The official end to end testing framework for Angular apps",
"logo": "",
"rev": true,
"title": "Protractor",
"url": "https://protractor.angular.io/"
}
@ -280,154 +244,130 @@
"resources": {
"AngularUIToolkit": {
"desc": "Angular UI Toolkit: 115 professionally maintained UI components ranging from a robust grid to charts and more. Try for free & build Angular apps faster.",
"rev": true,
"title": "Angular UI Toolkit",
"url": "https://www.angular-ui-tools.com"
},
"SenchaforAngular": {
"desc": "Build modern web apps faster with 115+ pre-built UI components. Try for free and download today.",
"rev": true,
"title": "Sencha for Angular",
"url": "https://www.sencha.com/products/extangular/"
},
"IgniteUIforAngular": {
"desc": "Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps.",
"rev": true,
"title": "Ignite UI for Angular",
"url": "https://www.infragistics.com/products/ignite-ui-angular?utm_source=angular.io&utm_medium=Referral&utm_campaign=Angular"
},
"DevExtreme": {
"desc": "50+ UI components including data grid, pivot grid, scheduler, charts, editors, maps and other multi-purpose controls for creating highly responsive web applications for touch devices and traditional desktops.",
"rev": true,
"title": "DevExtreme",
"url": "https://js.devexpress.com/Overview/Angular/"
},
"234237": {
"desc": "UX guidelines, HTML/CSS framework, and Angular components working together to craft exceptional experiences",
"rev": true,
"title": "Clarity Design System",
"url": "https://vmware.github.io/clarity/"
},
"-KMVB8P4TDfht8c0L1AE": {
"desc": "The Angular version of the Angular UI Bootstrap library. This library is being built from scratch in Typescript using the Bootstrap 4 CSS framework.",
"rev": true,
"title": "ng-bootstrap",
"url": "https://ng-bootstrap.github.io/"
},
"4ab": {
"desc": "Native Angular components & directives for Lightning Design System",
"logo": "http://ng-lightning.github.io/ng-lightning/img/shield.svg",
"rev": true,
"title": "ng-lightning",
"url": "http://ng-lightning.github.io/ng-lightning/"
},
"7ab": {
"desc": "UI components for hybrid mobile apps with bindings for both Angular & AngularJS.",
"rev": true,
"title": "Onsen UI",
"url": "https://onsen.io/v2/"
},
"a2b": {
"desc": "PrimeNG is a collection of rich UI components for Angular",
"logo": "http://www.primefaces.org/primeng/showcase/resources/images/primeng.svg",
"rev": true,
"title": "Prime Faces",
"url": "http://www.primefaces.org/primeng/"
},
"a3b": {
"desc": "A professional grade library of Angular UI components written in TypeScript that includes our Data Grid, TreeView, Charts, Editors, DropDowns, DatePickers, and many more. Features include support for AOT compilation, Tree Shaking for high-performance, localization, and accessibility.",
"logo": "",
"rev": true,
"title": "Kendo UI",
"url": "http://www.telerik.com/kendo-angular-ui/"
},
"a5b": {
"desc": "High-performance UI controls with the most complete Angular support available. Wijmos controls are all written in TypeScript and have zero dependencies. FlexGrid control includes full declarative markup, including cell templates.",
"logo": "http://wijmocdn.azureedge.net/wijmositeblob/wijmo-theme/logos/wijmo-55.png",
"rev": true,
"title": "Wijmo",
"url": "http://wijmo.com/products/wijmo-5/"
},
"a6b": {
"desc": "Material design inspired UI components for building great web apps. For mobile and desktop.",
"logo": "",
"rev": true,
"title": "Vaadin",
"url": "https://vaadin.com/elements"
},
"a7b": {
"desc": "Native Angular directives for Bootstrap",
"logo": "",
"rev": true,
"title": "ngx-bootstrap",
"url": "http://valor-software.com/ngx-bootstrap/#/"
},
"ab": {
"desc": "Material Design components for Angular",
"logo": "",
"rev": true,
"title": "Angular Material",
"url": "https://material.angular.io/"
},
"mcc": {
"desc": "Material components made by the community",
"logo": "",
"rev": true,
"title": "Material Community Components",
"url": "https://github.com/tiaguinho/material-community-components"
},
"mosaic": {
"desc": "Positive Technologies UI components based on Angular",
"logo": "https://i.ibb.co/fQNPgv6/logo-png-200.png",
"rev": true,
"title": "Mosaic - Angular UI Components",
"url": "https://github.com/positive-js/mosaic"
},
"ngzorro": {
"desc": "A set of enterprise-class UI components based on Ant Design and Angular",
"rev": true,
"title": "Ant Design of Angular (ng-zorro-antd)",
"url": "https://ng.ant.design/docs/introduce/en"
},
"ngzorromobile": {
"desc": "A set of enterprise-class mobile UI components based on Ant Design Mobile and Angular",
"rev": true,
"title": "Ant Design Mobile of Angular (ng-zorro-antd-mobile)",
"url": "http://ng.mobile.ant.design/#/docs/introduce/en"
},
"aggrid": {
"desc": "A datagrid for Angular with enterprise style features such as sorting, filtering, custom rendering, editing, grouping, aggregation and pivoting.",
"rev": true,
"title": "ag-Grid",
"url": "https://www.ag-grid.com/best-angular-2-data-grid/"
},
"angular-slickgrid": {
"desc": "Angular-SlickGrid is a wrapper of the lightning fast & customizable SlickGrid datagrid library with Bootstrap 3,4 themes",
"rev": true,
"title": "Angular-Slickgrid",
"url": "https://github.com/ghiscoding/Angular-Slickgrid"
},
"fancygrid": {
"desc": "Angular grid library with charts integration and server communication for Enterprise.",
"rev": true,
"title": "FancyGrid",
"url": "https://fancygrid.com/docs/getting-started/angular"
},
"ngx-smart-modal": {
"desc": "Angular smart, light and fast modal handler to manage modals and data everywhere.",
"rev": true,
"title": "ngx-smart-modal",
"url": "https://biig-io.github.io/ngx-smart-modal"
},
"jqwidgets": {
"desc": "Angular UI Components including data grid, tree grid, pivot grid, scheduler, charts, editors and other multi-purpose components",
"rev": true,
"title": "jQWidgets",
"url": "https://www.jqwidgets.com/angular/"
},
"amexio": {
"desc": "Amexio is a rich set of Angular components powered by HTML5 & CSS3 for Responsive Web Design and 80+ built-in Material Design Themes. Amexio has 3 Editions, Standard, Enterprise and Creative. Std Edition consists of basic UI Components which include Grid, Tabs, Form Inputs and so on. While Enterprise Edition consists of components like Calendar, Tree Tabs, Social Media Logins (Facebook, GitHub, Twitter and so on) and Creative Edition is focused building elegant and beautiful websites. With more than 200+ components/features. All the editions are open-sourced and free, based on Apache 2 License.",
"rev": true,
"title": "Amexio - Angular Extensions",
"url": "http://www.amexio.tech/",
"logo": "http://www.amexio.org/amexio-logo.png"
@ -435,56 +375,47 @@
"bm": {
"desc": "A lightweight Material Design library for Angular, based upon Google's Material Components for the Web",
"logo": "https://blox.src.zone/assets/bloxmaterial.03ecfe4fa0147a781487749dc1cc4580.svg",
"rev": true,
"title": "Blox Material",
"url": "https://github.com/src-zone/material"
},
"essentialjs2": {
"desc": "Essential JS 2 for Angular is a collection modern TypeScript based true Angular Components. It has support for Ahead Of Time (AOT) compilation and Tree-Shaking. All the components are developed from the ground up to be lightweight, responsive, modular and touch friendly.",
"rev": true,
"title": "Essential JS 2",
"url": "https://www.syncfusion.com/products/angular-js2"
},
"trulyui": {
"desc": "TrulyUI is an Angular UI Framework especially developed for Desktop Applications based on Web Components using the greatest technologies of the world.",
"rev": true,
"title": "Truly UI",
"url": "http://truly-ui.com"
},
"ngsqui": {
"desc": "Simple Quality UI (SQ-UI) is a flexible and easily customizable UI-kit, aiming to provide maximum efficiency with as little overhead as possible. Driven by the idea that it should be strictly \"for developers by developers\", every new feature release includes functionalities demanded by the developers who are using it.",
"logo": "https://sq-ui.github.io/ng-sq-ui/_media/sq-ui-logo.png",
"rev": true,
"title": "Simple Quality UI",
"url": "https://sq-ui.github.io/ng-sq-ui/#/"
},
"smart": {
"desc": "Web Components for Angular. Dependency-free Angular components for building modern and mobile-friendly web apps",
"rev": true,
"title": "Smart Web Components",
"url": "https://www.htmlelements.com/angular/"
},
"AlyleUI": {
"desc": "Minimal Design, a set of components for Angular.",
"rev": true,
"title": "Alyle UI",
"url": "https://alyle-ui.firebaseapp.com/"
},
"nebular": {
"desc": "Theme System, UI Components, Auth and Security for your next Angular application.",
"rev": true,
"title": "Nebular",
"url": "https://akveo.github.io/nebular/"
},
"carbondesignsystem": {
"desc": "An Angular implementation of the Carbon Design System for IBM.",
"rev": true,
"title": "Carbon Components Angular",
"url": "https://angular.carbondesignsystem.com/"
},
"jigsaw": {
"desc": "Jigsaw provides a set of web components based on Angular. It is supporting the development of all applications of Big Data Product of ZTE (https://www.zte.com.cn).",
"rev": true,
"title": "Awade Jigsaw (Chinese)",
"url": "https://jigsaw-zte.gitee.io"
}
@ -500,76 +431,64 @@
"resources": {
"-KLIzGEp8Mh5W-FkiQnL": {
"desc": "Your quick, no-nonsense guide to building real-world apps with Angular",
"rev": true,
"title": "Learning Angular - Second Edition",
"url": "https://www.packtpub.com/web-development/learning-angular-second-edition"
},
"3ab": {
"desc": "More than 15 books from O'Reilly about Angular",
"rev": true,
"title": "O'Reilly Media",
"url": "https://ssearch.oreilly.com/?q=angular"
},
"a5b": {
"desc": "The in-depth, complete, and up-to-date book on Angular. Become an Angular expert today.",
"rev": true,
"title": "ng-book",
"url": "https://www.ng-book.com/2/"
},
"a7b": {
"desc": "This ebook will help you getting the philosophy of the framework: what comes from 1.x, what has been introduced and why",
"rev": true,
"title": "Becoming a Ninja with Angular",
"url": "https://books.ninja-squad.com/angular"
},
"ab": {
"desc": "More than 10 books from Packt Publishing about Angular",
"rev": true,
"title": "Packt Publishing",
"url": "https://www.packtpub.com/catalogsearch/result/?q=angular"
},
"cnoring-rxjs-fundamentals": {
"desc": "A free book that covers all facets of working with Rxjs from your first Observable to how to make your code run at optimal speed with Schedulers.",
"rev": true,
"title": "RxJS Ultimate",
"url": "https://chrisnoring.gitbooks.io/rxjs-5-ultimate/content/"
},
"vsavkin-angular-router": {
"desc": "This book is a comprehensive guide to the Angular router written by its designer. The book explores the library in depth, including the mental model, design constraints, subtleties of the API.",
"rev": true,
"title": "Angular Router",
"url": "https://leanpub.com/router"
},
"vsavkin-essential-angular": {
"desc": "The book is a short, but at the same time, fairly complete overview of the key aspects of Angular written by its core contributors Victor Savkin and Jeff Cross. The book will give you a strong foundation. It will help you put all the concepts into right places. So you will get a good understanding of why the framework is the way it is.",
"rev": true,
"title": "Essential Angular",
"url": "https://gumroad.com/l/essential_angular"
},
"angular-buch": {
"desc": "The first German book about Angular. It gives you a detailed practical overview of the key concepts of the platform. In each chapter a sample application is built upon with a new Angular topic. All sources are available on GitHub.",
"logo": "https://angular-buch.com/assets/img/brand.svg",
"rev": true,
"title": "Angular-Buch (German)",
"url": "https://angular-buch.com/"
},
"wishtack-guide-angular": {
"desc": "The free, open-source and up-to-date Angular guide. This pragmatic guide is focused on best practices and will drive you from scratch to cloud.",
"logo": "https://raw.githubusercontent.com/wishtack/gitbook-guide-angular/master/.gitbook/assets/wishtack-logo-with-text.png",
"rev": true,
"title": "The Angular Guide by Wishtack (Français)",
"url": "https://guide-angular.wishtack.io/"
},
"ab5": {
"desc": "How to build Angular applications using NGRX",
"logo": "",
"rev": true,
"title": "Architecting Angular Applications with NGRX",
"url": "https://www.packtpub.com/web-development/architecting-angular-applications-redux"
},
"dwa": {
"desc": "Practical journey with Angular framework, ES6, TypeScript, webpack and Angular CLI.",
"rev": true,
"title": "Developing with Angular",
"url": "https://leanpub.com/developing-with-angular"
}
@ -580,32 +499,27 @@
"resources": {
"angular-dyma": {
"desc": "Learn Angular and all its ecosystem (Material, Flex-layout, Ngrx and more) from scratch.",
"rev": true,
"title": "Dyma (French)",
"url": "https://dyma.fr/angular"
},
"-KLIBoTWXMiBcvG0dAM6": {
"desc": "This course introduces you to the essentials of this \"superheroic\" framework, including declarative templates, two-way data binding, and dependency injection.",
"rev": true,
"title": "Angular: Essential Training",
"url": "https://www.lynda.com/AngularJS-tutorials/Angular-2-Essential-Training/540347-2.html"
},
"-KLIzGq3CiFeoZUemVyE": {
"desc": "Learn the core concepts, play with the code, become a competent Angular developer",
"rev": true,
"title": "Angular Concepts, Code and Collective Wisdom",
"url": "https://www.udemy.com/angular-2-concepts-code-and-collective-wisdom/"
},
"-KLIzHwg-glQLXni1hvL": {
"desc": "Spanish language Angular articles and information",
"rev": true,
"title": "Academia Binaria (español)",
"url": "http://academia-binaria.com/"
},
"-KN3uNQvxifu26D6WKJW": {
"category": "Education",
"desc": "Create the future of web applications by taking Angular for a test drive.",
"rev": true,
"subcategory": "Online Training",
"title": "CodeSchool: Accelerating Through Angular",
"url": "https://www.codeschool.com/courses/accelerating-through-angular-2"
@ -613,96 +527,81 @@
"angular-playbook": {
"desc": "Learn advanced Angular best practices for enterprise teams, created by Nrwl.io.",
"logo": "https://nrwl.io/assets/logo_footer_2x.png",
"rev": true,
"title": "Angular Enterprise Playbook",
"url": "https://angularplaybook.com"
},
"a2b": {
"desc": "Hundreds of Angular courses for all skill levels",
"logo": "",
"rev": true,
"title": "Pluralsight",
"url": "https://www.pluralsight.com/paths/angular"
},
"ab3": {
"desc": "Angular courses hosted by Udemy",
"logo": "",
"rev": true,
"title": "Udemy",
"url": "https://www.udemy.com/courses/search/?q=angular"
},
"ab4": {
"desc": "Angular Fundamentals and advanced topics focused on Redux Style Angular Applications",
"logo": "",
"rev": true,
"title": "Egghead.io",
"url": "https://egghead.io/browse/frameworks/angular"
},
"ab5": {
"desc": "Build Web Apps with Angular - recorded video content",
"logo": "",
"rev": true,
"title": "Frontend Masters",
"url": "https://frontendmasters.com/courses/angular-core/"
},
"angular-love": {
"desc": "Polish language Angular articles and information",
"rev": true,
"title": "angular.love (Polski)",
"url": "http://www.angular.love/"
},
"learn-angular-fr": {
"desc": "French language Angular content.",
"rev": true,
"title": "Learn Angular (francais)",
"url": "http://www.learn-angular.fr/"
},
"upgrading-ajs": {
"desc": "The world's most comprehensive, step-by-step course on using best practices and avoiding pitfalls while migrating from AngularJS to Angular.",
"rev": true,
"title": "Upgrading AngularJS",
"url": "https://www.upgradingangularjs.com"
},
"toddmotto-ultimateangular": {
"desc": "Online courses providing in-depth coverage of the Angular ecosystem, AngularJS, Angular and TypeScript, with functional code samples and a full-featured seed environment. Get a deep understanding of Angular and TypeScript from foundation to functional application, then move on to advanced topics with Todd Motto and collaborators.",
"rev": true,
"title": "Ultimate Angular",
"url": "https://ultimateangular.com/"
},
"willh-angular-zero": {
"desc": "Online video course in Chinese for newbies who need to learning from the scratch in Chinese. It's covering Angular, Angular CLI, TypeScript, VSCode, and some must known knowledge of Angular development.",
"rev": true,
"title": "Angular in Action: Start From Scratch (正體中文)",
"url": "https://www.udemy.com/angular-zero/?couponCode=ANGULAR.IO"
},
"angular-firebase": {
"desc": "Video lessons covering progressive web apps with Angular, Firebase, RxJS, and related APIs.",
"rev": true,
"title": "AngularFirebase.com",
"url": "https://angularfirebase.com/"
},
"loiane-angulartraining": {
"desc": "Free Angular course in Portuguese.",
"rev": true,
"title": "Loiane Training (Português)",
"url": "https://loiane.training/course/angular/"
},
"web-dev-angular": {
"desc": "Build performant and progressive Angular applications.",
"rev": true,
"title": "web.dev/angular",
"url": "https://web.dev/angular"
},
"mdb-angular-boilerplate": {
"desc": "Angular CRUD application starter with NgRx state management, Firebase backend and installation guide.",
"rev": true,
"title": "MDB Angular Boilerplate",
"url": "https://github.com/mdbootstrap/Angular-Bootstrap-Boilerplate"
},
"dotnettricks": {
"desc": "Online videos and training for Angular.",
"logo": "",
"rev": true,
"title": "DotNetTricks",
"url": "https://www.dotnettricks.com/courses/angular"
}
@ -713,127 +612,107 @@
"resources": {
"webucator": {
"desc": "Customized in-person instructor-led Angular training for private groups and public online instructor-led Angular classes.",
"rev": true,
"title": "Webucator",
"url": "https://www.webucator.com/webdev-training/angular-training"
},
"-acceleb": {
"desc": "Customized, Instructor-Led Angular Training",
"rev": true,
"title": "Accelebrate",
"url": "https://www.accelebrate.com/angular-training"
},
"-KLIBoFWStce29UCwkvY": {
"desc": "Private Angular Training and Mentoring",
"rev": true,
"title": "Chariot Solutions",
"url": "http://chariotsolutions.com/course/angular2-workshop-fundamentals-architecture/"
},
"-KLIBoN0p9be3kwC6-ga": {
"desc": "Angular Academy is a two day hands-on public course given in-person across Canada!",
"rev": true,
"title": "Angular Academy (Canada)",
"url": "http://www.angularacademy.ca"
},
"at": {
"desc": "Angular Training teaches Angular on-site all over the world. Also provides consulting and mentoring.",
"rev": true,
"title": "Angular Training",
"url": "http://www.angulartraining.com"
},
"-KLIBo_lm-WrK1Sjtt-2": {
"desc": "Basic and Advanced training across Europe in German",
"rev": true,
"title": "TheCodeCampus (German)",
"url": "https://www.thecodecampus.de/schulungen/angular"
},
"-KLIzFhfGKi1xttqJ7Uh": {
"desc": "4 day in-depth Angular training in Israel",
"rev": true,
"title": "ng-course (Israel)",
"url": "http://ng-course.org/"
},
"-KLIzIcRoDq3TzCJWnYc": {
"desc": "Virtual and in-person training in Canada and the US",
"rev": true,
"title": "Web Age Solutions",
"url": "http://www.webagesolutions.com/courses/WA2533-angular-2-programming"
},
"500tech": {
"desc": "Learn from 500Tech, an Angular consultancy in Israel. This course was built by an expert developer, who lives and breathes Angular, and has practical experience with real world large scale Angular apps.",
"rev": true,
"title": "Angular Hands-on Course (Israel)",
"url": "http://angular2.courses.500tech.com/"
},
"9ab": {
"desc": "OnSite Training From the Authors of \"Become A Ninja with Angular\"",
"rev": true,
"title": "Ninja Squad",
"url": "http://ninja-squad.com/formations/formation-angular2"
},
"a2b": {
"desc": "Angular Boot Camp covers introductory through advanced Angular topics. It includes extensive workshop sessions, with hands-on help from our experienced developer-trainers. We take developers or teams from the beginnings of Angular understanding through a working knowledge of all essential Angular features.",
"logo": "https://angularbootcamp.com/images/angular-boot-camp-logo.svg",
"rev": true,
"title": "Angular Boot Camp",
"url": "https://angularbootcamp.com"
},
"ab3": {
"desc": "Trainings & Code Reviews. We help people to get a deep understanding of different technologies through trainings and code reviews. Our services can be arranged online, making it possible to join in from anywhere in the world, or on-site to get the best experience possible.",
"logo": "",
"rev": true,
"title": "Thoughtram",
"url": "http://thoughtram.io/"
},
"jsru": {
"desc": "Complete Angular online course. Constantly updating. Real-time webinars with immediate feedback from the teacher.",
"logo": "https://learn.javascript.ru/img/sitetoolbar__logo_ru.svg",
"rev": true,
"title": "Learn Javascript (Russian)",
"url": "https://learn.javascript.ru/courses/angular"
},
"zenika-angular": {
"desc": "Angular trainings delivered by Zenika (FRANCE)",
"rev": true,
"title": "Angular Trainings (French)",
"url": "https://training.zenika.com/fr/training/angular/description"
},
"formationjs": {
"desc": "Angular onsite training in Paris (France). Monthly Angular workshops and custom onsite classes. We are focused on Angular, so we are always up to date.",
"rev": true,
"title": "Formation JavaScript (French)",
"url": "https://formationjavascript.com/formation-angular/"
},
"humancoders-angular": {
"desc": "Angular trainings delivered by Human Coders (France)",
"rev": true,
"title": "Formation Angular (French)",
"url": "https://www.humancoders.com/formations/angular"
},
"wao": {
"desc": "Onsite Angular Training delivered by We Are One Sàrl in Switzerland",
"logo": "https://weareone.ch/wordpress/wao-content/uploads/2014/12/logo_200_2x.png",
"rev": true,
"title": "We Are One Sàrl",
"url": "https://weareone.ch/courses/angular/"
},
"angular-schule": {
"desc": "Angular onsite training and public workshops in Germany from the authors of the German Angular book. We also regularly post articles and videos on our blog (in English and German language).",
"logo": "https://angular.schule/assets/img/brand.svg",
"rev": true,
"title": "Angular.Schule (German)",
"url": "https://angular.schule/"
},
"strbrw": {
"desc": "Angular and RxJS trainings, Code Reviews and consultancy. We help software engineers all over the world to create better web-applications...",
"rev": true,
"title": "StrongBrew",
"url": "https://strongbrew.io/"
},
"angular.de": {
"desc": "Onsite Angular Training delivered by the greatest community in the german speaking area in Germany, Austria and Switzerland. We also regularly post articles and tutorials on our blog.",
"logo": "https://angular.de/assets/img/angular-de-logo.svg",
"rev": true,
"title": "Angular.de (German)",
"url": "https://angular.de/"
}

View File

@ -215,7 +215,7 @@
},
{
"url": "guide/lifecycle-hooks",
"title": "Lifecycle Hooks",
"title": "Hook into the Component Lifecycle",
"tooltip": "Angular calls lifecycle hook methods on directives and components as it creates, changes, and destroys them."
},
{
@ -261,13 +261,13 @@
},
{
"url": "guide/form-validation",
"title": "Form Validation",
"title": "Validate form input",
"tooltip": "Validate user's form entries."
},
{
"url": "guide/dynamic-form",
"title": "Dynamic Forms",
"tooltip": "Render dynamic forms with FormGroup."
"title": "Building Dynamic Forms",
"tooltip": "Create dynamic form templates using FormGroup."
}
]
},
@ -761,9 +761,56 @@
"tooltip": "Angular versioning, release, support, and deprecation policies and practices."
},
{
"url": "guide/updating-to-version-9",
"title": "Updating to Version 9",
"tooltip": "Support for updating your application from version 8 to 9."
"tooltip": "Support for updating your application from version 8 to 9.",
"children": [
{
"url": "guide/updating-to-version-9",
"title": "Overview",
"tooltip": "Everything you need to know for updating your application from version 8 to 9."
},
{
"url": "guide/ivy-compatibility",
"title": "Ivy Compatibility Guide",
"tooltip": "Details to help you make sure your application is compatible with Ivy."
},
{
"title": "Optional Migrations",
"tooltip": "Optional migration details regarding updating to version 9.",
"children": [
{
"url": "guide/migration-renderer",
"title": "Renderer to Renderer2",
"tooltip": "Migration from the deprecated Renderer API to the newer Renderer2 API."
},
{
"url": "guide/migration-dynamic-flag",
"title": "Dynamic Queries Flag",
"tooltip": "Migration to remove unnecessary `static: false` flag from @ViewChild and @ContentChild queries."
},
{
"url": "guide/migration-injectable",
"title": "Missing @Injectable() Decorators",
"tooltip": "Migration to add missing @Injectable() decorators and incomplete provider definitions."
},
{
"url": "guide/migration-localize",
"title": "$localize Global Import",
"tooltip": "Migration to add an import statement for @angular/localize to polyfills.ts."
},
{
"url": "guide/migration-module-with-providers",
"title": "Missing ModuleWithProviders Generic",
"tooltip": "Migration to add a generic type to any ModuleWithProviders usages that are missing the generic."
},
{
"url": "guide/migration-undecorated-classes",
"title": "Missing @Directive() Decorators",
"tooltip": "Migration to add missing @Directive()/@Component() decorators."
}
]
}
]
},
{
"url": "guide/deprecations",

View File

@ -86,6 +86,6 @@ Angular offers many more capabilities, and you now have a foundation that empowe
* Angular provides advanced capabilities for mobile apps, animation, internationalization, server-side rendering, and more.
* [Angular Material](https://material.angular.io/ "Angular Material web site") offers an extensive library of Material Design components.
* [Angular Protractor](https://protractor.angular.io/ "Angular Protractor web site") offers an end-to-end testing framework for Angular apps.
* Angular also has an extensive [network of 3rd-party tools and libraries](https://angular.io/resources "Angular resources list").
* Angular also has an extensive [network of 3rd-party tools and libraries](resources "Angular resources list").
Keep current by following the [Angular blog](https://blog.angular.io/ "Angular blog").

View File

@ -1,5 +1,6 @@
{
"hosting": {
"target": "aio",
"public": "dist",
"cleanUrls": true,
"redirects": [

View File

@ -23,7 +23,7 @@
"build-local-with-viewengine": "yarn ~~build",
"prebuild-local-with-viewengine-ci": "node scripts/switch-to-viewengine && yarn setup-local-ci",
"build-local-with-viewengine-ci": "yarn ~~build --progress=false",
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js 98de22823",
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js 7653f8616",
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint",
"test": "yarn check-env && ng test",
"pree2e": "yarn check-env && yarn update-webdriver",

View File

@ -33,7 +33,7 @@ else
readonly majorVersionStable=${CI_STABLE_BRANCH%%.*}
# Do not deploy if the major version is not less than the stable branch major version
if [[ !( "$majorVersion" < "$majorVersionStable" ) ]]; then
if (( $majorVersion >= $majorVersionStable )); then
echo "Skipping deploy of branch \"$CI_BRANCH\" to firebase."
echo "We only deploy archive branches with the major version less than the stable branch: \"$CI_STABLE_BRANCH\""
exit 0
@ -64,16 +64,27 @@ fi
case $deployEnv in
next)
readonly projectId=aio-staging
readonly siteId=$projectId
readonly deployedUrl=https://next.angular.io/
readonly firebaseToken=$CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN
;;
stable)
readonly projectId=angular-io
readonly siteId=$projectId
readonly deployedUrl=https://angular.io/
readonly firebaseToken=$CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN
;;
archive)
readonly projectId=v${majorVersion}-angular-io
# Special case v9-angular-io because its piloting the firebase hosting "multisites" setup
# See https://angular-team.atlassian.net/browse/DEV-125 for more info.
if [[ "$majorVersion" == "9" ]]; then
readonly projectId=aio-staging
readonly siteId=v9-angular-io
else
readonly projectId=v${majorVersion}-angular-io
readonly siteId=$projectId
fi
readonly deployedUrl=https://v${majorVersion}.angular.io/
readonly firebaseToken=$CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN
;;
@ -82,6 +93,7 @@ esac
echo "Git branch : $CI_BRANCH"
echo "Build/deploy mode : $deployEnv"
echo "Firebase project : $projectId"
echo "Firebase site : $siteId"
echo "Deployment URL : $deployedUrl"
if [[ ${1:-} == "--dry-run" ]]; then
@ -92,23 +104,29 @@ fi
(
cd "`dirname $0`/.."
# Build the app
echo "\n\n\n==== Build the aio app ====\n"
yarn build --configuration=$deployEnv --progress=false
# Include any mode-specific files
echo "\n\n\n==== Add any mode-specific files into the aio distribution ====\n"
cp -rf src/extra-files/$deployEnv/. dist/
# Set deployedUrl as parameter in the opensearch description
echo "\n\n\n==== Update opensearch descriptor for aio with the deployedUrl ====\n"
# deployedUrl must end with /
yarn set-opensearch-url $deployedUrl
# Check payload size
echo "\n\n\n==== Check payload size and upload the numbers to firebase db ====\n"
yarn payload-size
# Deploy to Firebase
yarn firebase use "$projectId" --token "$firebaseToken"
yarn firebase deploy --message "Commit: $CI_COMMIT" --non-interactive --token "$firebaseToken"
# Run PWA-score tests
echo "\n\n\n==== Deploy aio to firebase hosting ====\n"
yarn firebase use "${projectId}" --token "$firebaseToken"
yarn firebase target:apply hosting aio $siteId --token "$firebaseToken"
yarn firebase deploy --only hosting:aio --message "Commit: $CI_COMMIT" --non-interactive --token "$firebaseToken"
echo "\n\n\n==== Run PWA-score tests ====\n"
yarn test-pwa-score "$deployedUrl" "$CI_AIO_MIN_PWA_SCORE"
)

View File

@ -68,6 +68,7 @@ function check {
expected="Git branch : master
Build/deploy mode : next
Firebase project : aio-staging
Firebase site : aio-staging
Deployment URL : https://next.angular.io/"
check "$actual" "$expected"
)
@ -103,6 +104,7 @@ Deployment URL : https://next.angular.io/"
expected="Git branch : 4.3.x
Build/deploy mode : stable
Firebase project : angular-io
Firebase site : angular-io
Deployment URL : https://angular.io/"
check "$actual" "$expected"
)
@ -139,10 +141,37 @@ Deployment URL : https://angular.io/"
expected="Git branch : 2.4.x
Build/deploy mode : archive
Firebase project : v2-angular-io
Firebase site : v2-angular-io
Deployment URL : https://v2.angular.io/"
check "$actual" "$expected"
)
(
echo ===== archive - v9-angular-io multisite special case - deploy success
actual=$(
export BASH_ENV=/dev/null
export CI_REPO_OWNER=angular
export CI_REPO_NAME=angular
export CI_PULL_REQUEST=false
export CI_BRANCH=9.1.x
export CI_STABLE_BRANCH=10.0.x
export CI_COMMIT=$(git ls-remote origin 9.1.x | cut -c1-40)
export CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN=XXXXX
$deployToFirebaseDryRun
)
expected="Git branch : 9.1.x
Build/deploy mode : archive
Firebase project : aio-staging
Firebase site : v9-angular-io
Deployment URL : https://v9.angular.io/"
# TODO: This test incorrectly expects the Firebase project to be v9-angular-io.
# v9-angular-io is a "multisites" project currently within the aio-staging project
# This setup is temporary and was created in order to deploy v9.angular.io without
# disruptions.
# See https://angular-team.atlassian.net/browse/DEV-125 for more info.
check "$actual" "$expected"
)
(
echo ===== archive - skip deploy - commit not HEAD
actual=$(

View File

@ -5,13 +5,9 @@
</div>
<mat-toolbar color="primary" class="app-toolbar no-print" [class.transitioning]="isTransitioning">
<mat-toolbar-row class="notification-container">
<aio-notification notificationId="survey-march-2020" expirationDate="2020-04-15" [dismissOnContentClick]="true" (dismissed)="notificationDismissed()">
<a href="https://goo.gle/angular-survey-2020">
<mat-icon class="icon" svgIcon="insert_comment" aria-label="Announcement"></mat-icon>
<span class="message">Help Angular by taking a <b>1 minute survey</b>!</span>
<span class="action-button">Go to survey</span>
</a>
<mat-toolbar-row class="notification-container blm-message">
<aio-notification notificationId="blm-2020" expirationDate="2022-04-15" [dismissOnContentClick]="true" (dismissed)="notificationDismissed()">
#BlackLivesMatter
</aio-notification>
</mat-toolbar-row>
<mat-toolbar-row>

View File

@ -13,7 +13,7 @@
<h3 class="subcategory-title">{{subCategory.title}}</h3>
<div *ngFor="let resource of subCategory.resources">
<div class="c-resource" *ngIf="resource.rev">
<div class="c-resource">
<a class="l-flex--column resource-row-link" rel="noopener" target="_blank" [href]="resource.url">
<div>
<h4>{{resource.title}}</h4>

View File

@ -17,7 +17,6 @@ export interface Resource {
subCategory: string; // "Books"
id: string; // "-KLI8vJ0ZkvWhqPembZ7"
desc: string; // "This books shows all the steps necessary for the development of SPA"
rev: boolean; // true (always true in the original)
title: string; // "Practical Angular 2",
url: string; // "https://leanpub.com/practical-angular-2"
}

View File

@ -96,13 +96,11 @@ function getTestResources() {
"resources": {
"Cat3 SubCat1 Res1": {
"desc": "Meetup in Barcelona, Spain. ",
"rev": true,
"title": "Angular Beers",
"url": "http://www.meetup.com/AngularJS-Beers/"
},
"Cat3 SubCat1 Res2": {
"desc": "Angular Camps in Barcelona, Spain.",
"rev": true,
"title": "Angular Camp",
"url": "http://angularcamp.org/"
}
@ -113,7 +111,6 @@ function getTestResources() {
"resources": {
"Cat3 SubCat2 Res1": {
"desc": "A community index of components and libraries",
"rev": true,
"title": "Catalog of Angular Components & Libraries",
"url": "https://a/b/c"
}
@ -129,19 +126,16 @@ function getTestResources() {
"resources": {
"S S S": {
"desc": "SSS",
"rev": true,
"title": "Sssss",
"url": "http://s/s/s"
},
"A A A": {
"desc": "AAA",
"rev": true,
"title": "Aaaa",
"url": "http://a/a/a"
},
"Z Z Z": {
"desc": "ZZZ",
"rev": true,
"title": "Zzzzz",
"url": "http://z/z/z"
}

View File

@ -192,3 +192,32 @@ code {
color: $mediumgray;
}
}
.folder-api,
.folder-cli,
.folder-docs,
.folder-guide,
.folder-start,
.folder-tutorial {
aio-doc-viewer{
a {
&[href^="http:"]::after,
&[href^="https:"]::after {
font-family: "Material Icons";
content: "open_in_new";
margin-left: 2px;
position: relative;
@include line-height(24);
vertical-align: bottom;
}
}
.github-links a {
&[href^="http:"]::after,
&[href^="https:"]::after {
display: none;
}
}
}
}

View File

@ -183,8 +183,8 @@ section#intro {
// ANGULAR LINE
.background-sky {
background-color: $blue;
background: $bluegradient;
background-color: $black;
background: $black;
color: $white;
}

View File

@ -177,6 +177,16 @@ button.vertical-menu-item {
margin: 0;
padding-left: 32px;
text-transform: none;
&.expanded .mat-icon,
.level-3.expanded .mat-icon {
@include rotate(90deg);
}
&:not(.expanded) .mat-icon,
.level-3:not(.expanded) .mat-icon {
@include rotate(0deg);
}
}
.level-3 {
@ -185,6 +195,16 @@ button.vertical-menu-item {
@include font-size(14);
margin: 0;
padding-left: 40px;
text-transform: none;
}
.level-4 {
color: $mediumgray;
font-family: $main-font;
@include font-size(14);
margin: 0;
padding-left: 48px;
text-transform: none;
}
aio-nav-menu.top-menu {

View File

@ -20,9 +20,16 @@ mat-toolbar.mat-toolbar {
}
}
.blm-message {
text-align: center;
justify-content: center;
background: #2d2d2d;
font-size: 0.75em;
}
// HOME PAGE OVERRIDE: TOPNAV TOOLBAR
aio-shell.page-home mat-toolbar.mat-toolbar {
background-color: $blue;
background-color: $black;
@media (min-width: 481px) {
&:not(.transitioning) {

View File

@ -21,7 +21,7 @@
"@angular/platform-browser-dynamic": "~9.1.4",
"@angular/router": "~9.1.4",
"angular": "1.7.9",
"angular-in-memory-web-api": "~0.9.0",
"angular-in-memory-web-api": "~0.11.0",
"angular-route": "1.7.9",
"rxjs": "~6.5.4",
"tslib": "^1.10.0",

View File

@ -20,7 +20,7 @@
"@angular/platform-browser": "~9.1.4",
"@angular/platform-browser-dynamic": "~9.1.4",
"@angular/router": "~9.1.4",
"angular-in-memory-web-api": "~0.9.0",
"angular-in-memory-web-api": "~0.11.0",
"rxjs": "~6.5.4",
"tslib": "^1.10.0",
"zone.js": "~0.10.3"

View File

@ -22,7 +22,7 @@
"@angular/platform-browser-dynamic": "~9.1.4",
"@angular/router": "~9.1.4",
"@webcomponents/custom-elements": "^1.4.1",
"angular-in-memory-web-api": "~0.9.0",
"angular-in-memory-web-api": "~0.11.0",
"rxjs": "~6.5.4",
"tslib": "^1.10.0",
"zone.js": "~0.10.3"

View File

@ -23,7 +23,7 @@
"@angular/platform-browser": "~9.1.4",
"@angular/platform-browser-dynamic": "~9.1.4",
"@angular/router": "~9.1.4",
"angular-in-memory-web-api": "~0.9.0",
"angular-in-memory-web-api": "~0.11.0",
"rxjs": "~6.5.4",
"tslib": "^1.10.0",
"zone.js": "~0.10.3"

View File

@ -21,7 +21,7 @@
"@angular/platform-browser-dynamic": "~9.1.4",
"@angular/router": "~9.1.4",
"@angular/service-worker": "~9.1.4",
"angular-in-memory-web-api": "~0.9.0",
"angular-in-memory-web-api": "~0.11.0",
"rxjs": "~6.5.4",
"tslib": "^1.10.0",
"zone.js": "~0.10.3"

View File

@ -26,7 +26,7 @@
"@angular/platform-server": "~9.1.4",
"@angular/router": "~9.1.4",
"@nguniversal/express-engine": "~9.0.1",
"angular-in-memory-web-api": "~0.9.0",
"angular-in-memory-web-api": "~0.11.0",
"express": "^4.15.2",
"rxjs": "~6.5.4",
"tslib": "^1.10.0",

View File

@ -35,7 +35,7 @@
"@nguniversal/express-engine": "~9.0.1",
"@webcomponents/custom-elements": "^1.4.1",
"angular": "1.7.9",
"angular-in-memory-web-api": "~0.9.0",
"angular-in-memory-web-api": "~0.11.0",
"angular-route": "1.7.9",
"core-js": "^2.5.4",
"express": "^4.15.2",

View File

@ -1899,10 +1899,10 @@ alphanum-sort@^1.0.0:
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
angular-in-memory-web-api@~0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/angular-in-memory-web-api/-/angular-in-memory-web-api-0.9.0.tgz#6c98d9494fadc6b98f54e68376a1998ccfff04bc"
integrity sha512-//PiJ5qb1+Yf/N7270ioQqR2laf4/Irjavg+M+WEn8y4At9LUoYgbQ5HVwvM5xUTlVlL0XkbJRLxREcGGNdIEw==
angular-in-memory-web-api@~0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/angular-in-memory-web-api/-/angular-in-memory-web-api-0.11.0.tgz#46e4ad896b36d669f36801fc8cafa7db8278d078"
integrity sha512-QV1qYHm+Zd+wrvlcPLnAcqqGpOmCN1EUj4rRuYHpek8+QqFFdxBNuPZOJCKvU7I97z5QSKHsdc6PNKlpUQr3UA==
angular-route@1.7.9:
version "1.7.9"

View File

@ -1,5 +1,3 @@
var _ = require('lodash');
module.exports = function extractDecoratedClassesProcessor(EXPORT_DOC_TYPES) {
// Add the "directive" docType into those that can be exported from a module
@ -10,12 +8,9 @@ module.exports = function extractDecoratedClassesProcessor(EXPORT_DOC_TYPES) {
$runBefore: ['docs-processed'],
decoratorTypes: ['Directive', 'Component', 'Pipe', 'NgModule'],
$process: function(docs) {
var decoratorTypes = this.decoratorTypes;
_.forEach(docs, function(doc) {
_.forEach(doc.decorators, function(decorator) {
const decoratorTypes = this.decoratorTypes;
docs.forEach(doc => {
(doc.decorators || []).forEach(decorator => {
if (decoratorTypes.indexOf(decorator.name) !== -1) {
doc.docType = decorator.name.toLowerCase();
doc[doc.docType + 'Options'] = decorator.argumentInfo[0];

View File

@ -1,9 +1,9 @@
module.exports = {
name: 'searchKeywords',
description: 'A shorthand for creating elements with search terms. Usage: `{@searchKeywords term1 term2 termN }`',
description:
'A shorthand for creating elements with search terms. Usage: `{@searchKeywords term1 term2 termN }`',
handler: function(doc, tagName, tagDescription) {
doc.searchKeywords = tagDescription;
return doc;
return '';
}
};

View File

@ -15,25 +15,28 @@ module.exports = function addImageDimensions(getImageDimensions) {
return (ast, file) => {
visit(ast, node => {
if (is(node, 'img')) {
const props = node.properties;
const src = props.src;
if (!src) {
file.message('Missing src in image tag `' + source(node, file) + '`');
if (!is(node, 'img')) {
return;
}
const props = node.properties;
const src = props.src;
if (!src) {
file.message('Missing src in image tag `' + source(node, file) + '`');
return;
}
try {
const dimensions = getImageDimensions(addImageDimensionsImpl.basePath, src);
if (props.width === undefined && props.height === undefined) {
props.width = '' + dimensions.width;
props.height = '' + dimensions.height;
}
} catch(e) {
if (e.code === 'ENOENT') {
file.fail('Unable to load src in image tag `' + source(node, file) + '`');
} else {
try {
const dimensions = getImageDimensions(addImageDimensionsImpl.basePath, src);
if (props.width === undefined && props.height === undefined) {
props.width = '' + dimensions.width;
props.height = '' + dimensions.height;
}
} catch(e) {
if (e.code === 'ENOENT') {
file.fail('Unable to load src in image tag `' + source(node, file) + '`');
} else {
file.fail(e.message);
}
}
file.fail(e.message);
}
}
});

View File

@ -1,5 +1,3 @@
var _ = require('lodash');
/**
* @dgProcessor checkUnbalancedBackTicks
* @description
@ -9,25 +7,28 @@ var _ = require('lodash');
*/
module.exports = function checkUnbalancedBackTicks(log, createDocMessage) {
var BACKTICK_REGEX = /^ *```/gm;
const BACKTICK_REGEX = /^ *```/gm;
const UNBALANCED_BACKTICK_WARNING = 'checkUnbalancedBackTicks processor: unbalanced backticks found in rendered content';
return {
// $runAfter: ['checkAnchorLinksProcessor'],
$runAfter: ['inlineTagProcessor'],
$runBefore: ['writeFilesProcessor'],
$process: function(docs) {
_.forEach(docs, function(doc) {
if (doc.renderedContent) {
var matches = doc.renderedContent.match(BACKTICK_REGEX);
if (matches && matches.length % 2 !== 0) {
doc.unbalancedBackTicks = true;
log.warn(createDocMessage(
'checkUnbalancedBackTicks processor: unbalanced backticks found in rendered content',
doc));
log.warn(doc.renderedContent);
}
}
});
docs
.forEach(doc => setUnbalancedBackTicks(doc));
}
};
function setUnbalancedBackTicks(doc) {
if (!doc.renderedContent) {
return;
}
const matches = doc.renderedContent.match(BACKTICK_REGEX);
if (matches && matches.length % 2 !== 0) {
doc.unbalancedBackTicks = true;
log.warn(createDocMessage(UNBALANCED_BACKTICK_WARNING, doc));
log.warn(doc.renderedContent);
}
}
};

View File

@ -1,23 +1,23 @@
var _ = require('lodash');
module.exports = function createOverviewDump() {
return {
$runAfter: ['processing-docs'],
$runBefore: ['docs-processed'],
$process: function(docs) {
var overviewDoc = {
const overviewDoc = {
id: 'overview-dump',
aliases: ['overview-dump'],
path: 'overview-dump',
outputPath: 'overview-dump.html',
modules: []
};
_.forEach(docs, function(doc) {
docs.forEach(doc => {
if (doc.docType === 'package') {
overviewDoc.modules.push(doc);
}
});
docs.push(overviewDoc);
}
};

View File

@ -30,7 +30,16 @@ var CIconfiguration = {
'Android7': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
'Android8': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
'Android9': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
'Android10': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
// Disable Android 10 tests due to infrastructure failure.
// ex:
// Chrome Mobile 74.0.3729 (Android 0.0.0) ERROR:
// Error: XHR error loading
// http://angular-ci.local:9876/base/node_modules/rxjs/internal/operators/zip.js
//
// Error loading http://angular-ci.local:9876/base/node_modules/rxjs/internal/operators/zip.js as
// "../internal/operators/zip" from
// http://angular-ci.local:9876/base/node_modules/rxjs/operators/index.js
'Android10': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
'Safari12': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
'Safari13': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
'iOS10': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},

View File

@ -10,6 +10,7 @@ ts_library(
deps = [
"//dev-infra/commit-message",
"//dev-infra/format",
"//dev-infra/pr",
"//dev-infra/pullapprove",
"//dev-infra/release",
"//dev-infra/ts-circular-dependencies",

View File

@ -12,12 +12,14 @@ import {buildPullapproveParser} from './pullapprove/cli';
import {buildCommitMessageParser} from './commit-message/cli';
import {buildFormatParser} from './format/cli';
import {buildReleaseParser} from './release/cli';
import {buildPrParser} from './pr/cli';
yargs.scriptName('ng-dev')
.demandCommand()
.recommendCommands()
.command('commit-message <command>', '', buildCommitMessageParser)
.command('format <command>', '', buildFormatParser)
.command('pr <command>', '', buildPrParser)
.command('pullapprove <command>', '', buildPullapproveParser)
.command('release <command>', '', buildReleaseParser)
.command('ts-circular-deps <command>', '', tsCircularDependenciesBuilder)

View File

@ -18,7 +18,6 @@ ts_library(
"@npm//@types/shelljs",
"@npm//@types/yargs",
"@npm//shelljs",
"@npm//tslib",
"@npm//yargs",
],
)
@ -33,6 +32,7 @@ ts_library(
"@npm//@types/events",
"@npm//@types/jasmine",
"@npm//@types/node",
"@npm//inquirer",
],
)

View File

@ -6,6 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as yargs from 'yargs';
import {info} from '../utils/console';
import {validateFile} from './validate-file';
import {validateCommitRange} from './validate-range';
@ -51,10 +54,10 @@ export function buildCommitMessageParser(localYargs: yargs.Argv) {
// If on CI, and not pull request number is provided, assume the branch
// being run on is an upstream branch.
if (process.env['CI'] && process.env['CI_PULL_REQUEST'] === 'false') {
console.info(
`Since valid commit messages are enforced by PR linting on CI, we do not\n` +
`need to validate commit messages on CI runs on upstream branches.\n\n` +
`Skipping check of provided commit range`);
info(`Since valid commit messages are enforced by PR linting on CI, we do not`);
info(`need to validate commit messages on CI runs on upstream branches.`);
info();
info(`Skipping check of provided commit range`);
return;
}
validateCommitRange(argv.range);

View File

@ -5,9 +5,27 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
export interface CommitMessageConfig {
maxLineLength: number;
minBodyLength: number;
types: string[];
scopes: string[];
}
/** Retrieve and validate the config as `CommitMessageConfig`. */
export function getCommitMessageConfig() {
// List of errors encountered validating the config.
const errors: string[] = [];
// The unvalidated config object.
const config: Partial<NgDevConfig<{commitMessage: CommitMessageConfig}>> = getConfig();
if (config.commitMessage === undefined) {
errors.push(`No configuration defined for "commitMessage"`);
}
assertNoErrors(errors);
return config as Required<typeof config>;
}

View File

@ -9,6 +9,7 @@ import {readFileSync} from 'fs';
import {resolve} from 'path';
import {getRepoBaseDir} from '../utils/config';
import {info} from '../utils/console';
import {validateCommitMessage} from './validate';
@ -16,7 +17,7 @@ import {validateCommitMessage} from './validate';
export function validateFile(filePath: string) {
const commitMessage = readFileSync(resolve(getRepoBaseDir(), filePath), 'utf8');
if (validateCommitMessage(commitMessage)) {
console.info('√ Valid commit message');
info('√ Valid commit message');
return;
}
// If the validation did not return true, exit as a failure.

View File

@ -6,6 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {exec} from 'shelljs';
import {info} from '../utils/console';
import {parseCommitMessage, validateCommitMessage, ValidateCommitMessageOptions} from './validate';
// Whether the provided commit is a fixup commit.
@ -31,7 +34,7 @@ export function validateCommitRange(range: string) {
// Separate the commits from a single string into individual commits
const commits = result.split(randomValueSeparator).map(l => l.trim()).filter(line => !!line);
console.info(`Examining ${commits.length} commit(s) in the provided range: ${range}`);
info(`Examining ${commits.length} commit(s) in the provided range: ${range}`);
// Check each commit in the commit range. Commits are allowed to be fixup commits for other
// commits in the provided commit range.
@ -46,7 +49,7 @@ export function validateCommitRange(range: string) {
});
if (allCommitsInRangeValid) {
console.info('√ All commit messages in range valid.');
info('√ All commit messages in range valid.');
} else {
// Exit with a non-zero exit code if invalid commit messages have
// been discovered.

View File

@ -7,11 +7,9 @@
*/
// Imports
import * as utilConfig from '../utils/config';
import * as validateConfig from './config';
import {validateCommitMessage} from './validate';
// Constants
const config = {
'commitMessage': {
@ -46,7 +44,7 @@ describe('validate-commit-message.js', () => {
lastError = '';
spyOn(console, 'error').and.callFake((msg: string) => lastError = msg);
spyOn(utilConfig, 'getAngularDevConfig').and.returnValue(config);
spyOn(validateConfig, 'getCommitMessageConfig').and.returnValue(config);
});
describe('validateMessage()', () => {

View File

@ -5,8 +5,9 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {getAngularDevConfig} from '../utils/config';
import {CommitMessageConfig} from './config';
import {error} from '../utils/console';
import {getCommitMessageConfig} from './config';
/** Options for commit message validation. */
export interface ValidateCommitMessageOptions {
@ -63,8 +64,8 @@ export function parseCommitMessage(commitMsg: string) {
/** Validate a commit message against using the local repo's config. */
export function validateCommitMessage(
commitMsg: string, options: ValidateCommitMessageOptions = {}) {
function error(errorMessage: string) {
console.error(
function printError(errorMessage: string) {
error(
`INVALID COMMIT MSG: \n` +
`${'─'.repeat(40)}\n` +
`${commitMsg}\n` +
@ -76,7 +77,7 @@ export function validateCommitMessage(
`<type>(<scope>): <subject>\n\n<body>`);
}
const config = getAngularDevConfig<'commitMessage', CommitMessageConfig>().commitMessage;
const config = getCommitMessageConfig().commitMessage;
const commit = parseCommitMessage(commitMsg);
////////////////////////////////////
@ -92,7 +93,7 @@ export function validateCommitMessage(
// the git history anyway, unless the options provided to not allow squash commits.
if (commit.isSquash) {
if (options.disallowSquash) {
error('The commit must be manually squashed into the target commit');
printError('The commit must be manually squashed into the target commit');
return false;
}
return true;
@ -105,7 +106,7 @@ export function validateCommitMessage(
// check.
if (commit.isFixup) {
if (options.nonFixupCommitHeaders && !options.nonFixupCommitHeaders.includes(commit.header)) {
error(
printError(
'Unable to find match for fixup commit among prior commits: ' +
(options.nonFixupCommitHeaders.map(x => `\n ${x}`).join('') || '-'));
return false;
@ -118,38 +119,44 @@ export function validateCommitMessage(
// Checking commit header //
////////////////////////////
if (commit.header.length > config.maxLineLength) {
error(`The commit message header is longer than ${config.maxLineLength} characters`);
printError(`The commit message header is longer than ${config.maxLineLength} characters`);
return false;
}
if (!commit.type) {
error(`The commit message header does not match the expected format.`);
printError(`The commit message header does not match the expected format.`);
return false;
}
if (!config.types.includes(commit.type)) {
error(`'${commit.type}' is not an allowed type.\n => TYPES: ${config.types.join(', ')}`);
printError(`'${commit.type}' is not an allowed type.\n => TYPES: ${config.types.join(', ')}`);
return false;
}
if (commit.scope && !config.scopes.includes(commit.scope)) {
error(`'${commit.scope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`);
printError(
`'${commit.scope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`);
return false;
}
// Commits with the type of `release` do not require a commit body.
if (commit.type === 'release') {
return true;
}
//////////////////////////
// Checking commit body //
//////////////////////////
if (commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
error(`The commit message body does not meet the minimum length of ${
printError(`The commit message body does not meet the minimum length of ${
config.minBodyLength} characters`);
return false;
}
const bodyByLine = commit.body.split('\n');
if (bodyByLine.some(line => line.length > config.maxLineLength)) {
error(
printError(
`The commit messsage body contains lines greater than ${config.maxLineLength} characters`);
return false;
}

View File

@ -18,7 +18,6 @@ ts_library(
"@npm//inquirer",
"@npm//multimatch",
"@npm//shelljs",
"@npm//tslib",
"@npm//yargs",
],
)

View File

@ -34,35 +34,9 @@ export function buildFormatParser(localYargs: yargs.Argv) {
const executionCmd = check ? checkFiles : formatFiles;
executionCmd(allChangedFilesSince(sha));
})
.command(
'files <files..>', 'Run the formatter on provided files', {},
({check, files}) => {
const executionCmd = check ? checkFiles : formatFiles;
executionCmd(files);
})
// TODO(josephperrott): remove this hidden command after deprecation period.
.command('deprecation-warning [originalCommand]', false, {}, ({originalCommand}) => {
console.warn(`\`yarn ${
originalCommand}\` is deprecated in favor of running the formatter via ng-dev`);
console.warn();
console.warn(`As a replacement of \`yarn ${originalCommand}\`, run:`);
switch (originalCommand) {
case 'bazel:format':
case 'bazel:lint-fix':
console.warn(` yarn ng-dev format all`);
break;
case 'bazel:lint':
console.warn(` yarn ng-dev format all --check`);
break;
default:
console.warn(`Error: Unrecognized previous command.`);
}
console.warn();
console.warn(`You can find more usage information by running:`);
console.warn(` yarn ng-dev format --help`);
console.warn();
console.warn(`For more on the rationale and effects of this deprecation visit:`);
console.warn(` https://github.com/angular/angular/pull/36842#issue-410321447`);
.command('files <files..>', 'Run the formatter on provided files', {}, ({check, files}) => {
const executionCmd = check ? checkFiles : formatFiles;
executionCmd(files);
});
}

View File

@ -6,8 +6,46 @@
* found in the LICENSE file at https://angular.io/license
*/
export interface FormatConfig {
[keyof: string]: boolean|{
matchers: string[];
};
import {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
interface Formatter {
matchers: string[];
}
export interface FormatConfig {
[keyof: string]: boolean|Formatter;
}
/** Retrieve and validate the config as `FormatConfig`. */
export function getFormatConfig() {
// List of errors encountered validating the config.
const errors: string[] = [];
// The unvalidated config object.
const config: Partial<NgDevConfig<{format: FormatConfig}>> = getConfig();
if (config.format === undefined) {
errors.push(`No configuration defined for "format"`);
}
for (const [key, value] of Object.entries(config.format!)) {
switch (typeof value) {
case 'boolean':
break;
case 'object':
checkFormatterConfig(key, value, errors);
break;
default:
errors.push(`"format.${key}" is not a boolean or Formatter object`);
}
}
assertNoErrors(errors);
return config as Required<typeof config>;
}
/** Validate an individual Formatter config. */
function checkFormatterConfig(key: string, config: Partial<Formatter>, errors: string[]) {
if (config.matchers === undefined) {
errors.push(`Missing "format.${key}.matchers" value`);
}
}

View File

@ -7,6 +7,9 @@
*/
import {prompt} from 'inquirer';
import {error, info} from '../utils/console';
import {runFormatterInParallel} from './run-commands-parallel';
/**
@ -17,16 +20,16 @@ export async function formatFiles(files: string[]) {
let failures = await runFormatterInParallel(files, 'format');
if (failures === false) {
console.info('No files matched for formatting.');
info('No files matched for formatting.');
process.exit(0);
}
// The process should exit as a failure if any of the files failed to format.
if (failures.length !== 0) {
console.error(`Formatting failed, see errors above for more information.`);
error(`Formatting failed, see errors above for more information.`);
process.exit(1);
}
console.info(`√ Formatting complete.`);
info(`√ Formatting complete.`);
process.exit(0);
}
@ -38,18 +41,18 @@ export async function checkFiles(files: string[]) {
const failures = await runFormatterInParallel(files, 'check');
if (failures === false) {
console.info('No files matched for formatting check.');
info('No files matched for formatting check.');
process.exit(0);
}
if (failures.length) {
// Provide output expressing which files are failing formatting.
console.group('\nThe following files are out of format:');
info.group('\nThe following files are out of format:');
for (const file of failures) {
console.info(` - ${file}`);
info(` - ${file}`);
}
console.groupEnd();
console.info();
info.groupEnd();
info();
// If the command is run in a non-CI environment, prompt to format the files immediately.
let runFormatter = false;
@ -67,13 +70,13 @@ export async function checkFiles(files: string[]) {
process.exit(0);
} else {
// Inform user how to format files in the future.
console.info();
console.info(`To format the failing file run the following command:`);
console.info(` yarn ng-dev format files ${failures.join(' ')}`);
info();
info(`To format the failing file run the following command:`);
info(` yarn ng-dev format files ${failures.join(' ')}`);
process.exit(1);
}
} else {
console.info('√ All files correctly formatted.');
info('√ All files correctly formatted.');
process.exit(0);
}
}

View File

@ -9,6 +9,7 @@
import {join} from 'path';
import {getRepoBaseDir} from '../../utils/config';
import {error} from '../../utils/console';
import {Formatter} from './base-formatter';
@ -35,9 +36,9 @@ export class Buildifier extends Formatter {
callback:
(file: string, code: number, _: string, stderr: string) => {
if (code !== 0) {
console.error(`Error running buildifier on: ${file}`);
console.error(stderr);
console.error();
error(`Error running buildifier on: ${file}`);
error(stderr);
error();
return true;
}
return false;

View File

@ -9,6 +9,7 @@
import {join} from 'path';
import {getRepoBaseDir} from '../../utils/config';
import {error} from '../../utils/console';
import {Formatter} from './base-formatter';
@ -35,9 +36,9 @@ export class ClangFormat extends Formatter {
callback:
(file: string, code: number, _: string, stderr: string) => {
if (code !== 0) {
console.error(`Error running clang-format on: ${file}`);
console.error(stderr);
console.error();
error(`Error running clang-format on: ${file}`);
error(stderr);
error();
return true;
}
return false;

View File

@ -6,8 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {getAngularDevConfig} from '../../utils/config';
import {FormatConfig} from '../config';
import {getFormatConfig} from '../config';
import {Buildifier} from './buildifier';
import {ClangFormat} from './clang-format';
@ -16,11 +15,7 @@ import {ClangFormat} from './clang-format';
* Get all defined formatters which are active based on the current loaded config.
*/
export function getActiveFormatters() {
let config = {};
try {
config = getAngularDevConfig<'format', FormatConfig>().format || {};
} catch {
}
const config = getFormatConfig().format;
return [new Buildifier(config), new ClangFormat(config)].filter(
formatter => formatter.isEnabled());
}

View File

@ -11,6 +11,8 @@ import * as multimatch from 'multimatch';
import {cpus} from 'os';
import {exec} from 'shelljs';
import {info} from '../utils/console';
import {Formatter, FormatterAction, getActiveFormatters} from './formatters';
const AVAILABLE_THREADS = Math.max(cpus().length - 1, 1);
@ -47,10 +49,10 @@ export function runFormatterInParallel(allFiles: string[], action: FormatterActi
switch (action) {
case 'format':
console.info(`Formatting ${pendingCommands.length} file(s)`);
info(`Formatting ${pendingCommands.length} file(s)`);
break;
case 'check':
console.info(`Checking format of ${pendingCommands.length} file(s)`);
info(`Checking format of ${pendingCommands.length} file(s)`);
break;
default:
throw Error(`Invalid format action "${action}": allowed actions are "format" and "check"`);

14
dev-infra/pr/BUILD.bazel Normal file
View File

@ -0,0 +1,14 @@
load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "pr",
srcs = ["cli.ts"],
module_name = "@angular/dev-infra-private/pr",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/pr/discover-new-conflicts",
"//dev-infra/pr/merge",
"//dev-infra/pr/rebase",
"@npm//@types/yargs",
],
)

32
dev-infra/pr/cli.ts Normal file
View File

@ -0,0 +1,32 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as yargs from 'yargs';
import {buildDiscoverNewConflictsCommand, handleDiscoverNewConflictsCommand} from './discover-new-conflicts/cli';
import {buildMergeCommand, handleMergeCommand} from './merge/cli';
import {buildRebaseCommand, handleRebaseCommand} from './rebase/cli';
/** Build the parser for pull request commands. */
export function buildPrParser(localYargs: yargs.Argv) {
return localYargs.help()
.strict()
.demandCommand()
.command('merge <pr-number>', 'Merge pull requests', buildMergeCommand, handleMergeCommand)
.command(
'discover-new-conflicts <pr-number>',
'Check if a pending PR causes new conflicts for other pending PRs',
buildDiscoverNewConflictsCommand, handleDiscoverNewConflictsCommand)
.command(
'rebase <pr-number>', 'Rebase a pending PR and push the rebased commits back to Github',
buildRebaseCommand, handleRebaseCommand);
}
if (require.main === module) {
buildPrParser(yargs).parse();
}

View File

@ -0,0 +1,19 @@
load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "discover-new-conflicts",
srcs = [
"cli.ts",
"index.ts",
],
module_name = "@angular/dev-infra-private/pr/discover-new-conflicts",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/utils",
"@npm//@types/cli-progress",
"@npm//@types/node",
"@npm//@types/shelljs",
"@npm//@types/yargs",
"@npm//typed-graphqlify",
],
)

View File

@ -0,0 +1,35 @@
import {Arguments, Argv} from 'yargs';
import {error} from '../../utils/console';
import {discoverNewConflictsForPr} from './index';
/** Builds the discover-new-conflicts pull request command. */
export function buildDiscoverNewConflictsCommand(yargs: Argv) {
return yargs.option('date', {
description: 'Only consider PRs updated since provided date',
defaultDescription: '30 days ago',
coerce: Date.parse,
default: getThirtyDaysAgoDate,
});
}
/** Handles the discover-new-conflicts pull request command. */
export async function handleDiscoverNewConflictsCommand({prNumber, date}: Arguments) {
// If a provided date is not able to be parsed, yargs provides it as NaN.
if (isNaN(date)) {
error('Unable to parse the value provided via --date flag');
process.exit(1);
}
await discoverNewConflictsForPr(prNumber, date);
}
/** Gets a date object 30 days ago from today. */
function getThirtyDaysAgoDate(): Date {
const date = new Date();
// Set the hours, minutes and seconds to 0 to only consider date.
date.setHours(0, 0, 0, 0);
// Set the date to 30 days in the past.
date.setDate(date.getDate() - 30);
return date;
}

View File

@ -0,0 +1,159 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Bar} from 'cli-progress';
import {types as graphQLTypes} from 'typed-graphqlify';
import {getConfig, NgDevConfig} from '../../utils/config';
import {error, info} from '../../utils/console';
import {getCurrentBranch, hasLocalChanges} from '../../utils/git';
import {getPendingPrs} from '../../utils/github';
import {exec} from '../../utils/shelljs';
/* GraphQL schema for the response body for each pending PR. */
const PR_SCHEMA = {
headRef: {
name: graphQLTypes.string,
repository: {
url: graphQLTypes.string,
nameWithOwner: graphQLTypes.string,
},
},
baseRef: {
name: graphQLTypes.string,
repository: {
url: graphQLTypes.string,
nameWithOwner: graphQLTypes.string,
},
},
updatedAt: graphQLTypes.string,
number: graphQLTypes.number,
mergeable: graphQLTypes.string,
title: graphQLTypes.string,
};
/* Pull Request response from Github GraphQL query */
type RawPullRequest = typeof PR_SCHEMA;
/** Convert raw Pull Request response from Github to usable Pull Request object. */
function processPr(pr: RawPullRequest) {
return {...pr, updatedAt: (new Date(pr.updatedAt)).getTime()};
}
/* Pull Request object after processing, derived from the return type of the processPr function. */
type PullRequest = ReturnType<typeof processPr>;
/** Name of a temporary local branch that is used for checking conflicts. **/
const tempWorkingBranch = '__NgDevRepoBaseAfterChange__';
/** Checks if the provided PR will cause new conflicts in other pending PRs. */
export async function discoverNewConflictsForPr(
newPrNumber: number, updatedAfter: number, config: Pick<NgDevConfig, 'github'> = getConfig()) {
// If there are any local changes in the current repository state, the
// check cannot run as it needs to move between branches.
if (hasLocalChanges()) {
error('Cannot run with local changes. Please make sure there are no local changes.');
process.exit(1);
}
/** The active github branch when the run began. */
const originalBranch = getCurrentBranch();
/* Progress bar to indicate progress. */
const progressBar = new Bar({format: `[{bar}] ETA: {eta}s | {value}/{total}`});
/* PRs which were found to be conflicting. */
const conflicts: Array<PullRequest> = [];
info(`Requesting pending PRs from Github`);
/** List of PRs from github currently known as mergable. */
const allPendingPRs = (await getPendingPrs(PR_SCHEMA, config.github)).map(processPr);
/** The PR which is being checked against. */
const requestedPr = allPendingPRs.find(pr => pr.number === newPrNumber);
if (requestedPr === undefined) {
error(
`The request PR, #${newPrNumber} was not found as a pending PR on github, please confirm`);
error(`the PR number is correct and is an open PR`);
process.exit(1);
}
const pendingPrs = allPendingPRs.filter(pr => {
return (
// PRs being merged into the same target branch as the requested PR
pr.baseRef.name === requestedPr.baseRef.name &&
// PRs which either have not been processed or are determined as mergable by Github
pr.mergeable !== 'CONFLICTING' &&
// PRs updated after the provided date
pr.updatedAt >= updatedAfter);
});
info(`Retrieved ${allPendingPRs.length} total pending PRs`);
info(`Checking ${pendingPrs.length} PRs for conflicts after a merge of #${newPrNumber}`);
// Fetch and checkout the PR being checked.
exec(`git fetch ${requestedPr.headRef.repository.url} ${requestedPr.headRef.name}`);
exec(`git checkout -B ${tempWorkingBranch} FETCH_HEAD`);
// Rebase the PR against the PRs target branch.
exec(`git fetch ${requestedPr.baseRef.repository.url} ${requestedPr.baseRef.name}`);
const result = exec(`git rebase FETCH_HEAD`);
if (result.code) {
error('The requested PR currently has conflicts');
cleanUpGitState(originalBranch);
process.exit(1);
}
// Start the progress bar
progressBar.start(pendingPrs.length, 0);
// Check each PR to determine if it can merge cleanly into the repo after the target PR.
for (const pr of pendingPrs) {
// Fetch and checkout the next PR
exec(`git fetch ${pr.headRef.repository.url} ${pr.headRef.name}`);
exec(`git checkout --detach FETCH_HEAD`);
// Check if the PR cleanly rebases into the repo after the target PR.
const result = exec(`git rebase ${tempWorkingBranch}`);
if (result.code !== 0) {
conflicts.push(pr);
}
// Abort any outstanding rebase attempt.
exec(`git rebase --abort`);
progressBar.increment(1);
}
// End the progress bar as all PRs have been processed.
progressBar.stop();
info();
info(`Result:`);
cleanUpGitState(originalBranch);
// If no conflicts are found, exit successfully.
if (conflicts.length === 0) {
info(`No new conflicting PRs found after #${newPrNumber} merging`);
process.exit(0);
}
// Inform about discovered conflicts, exit with failure.
error.group(`${conflicts.length} PR(s) which conflict(s) after #${newPrNumber} merges:`);
for (const pr of conflicts) {
error(` - ${pr.number}: ${pr.title}`);
}
error.groupEnd();
process.exit(1);
}
/** Reset git back to the provided branch. */
export function cleanUpGitState(branch: string) {
// Ensure that any outstanding rebases are aborted.
exec(`git rebase --abort`);
// Ensure that any changes in the current repo state are cleared.
exec(`git reset --hard`);
// Checkout the original branch from before the run began.
exec(`git checkout ${branch}`);
// Delete the generated branch.
exec(`git branch -D ${tempWorkingBranch}`);
}

View File

@ -0,0 +1,18 @@
load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "merge",
srcs = glob(["**/*.ts"]),
module_name = "@angular/dev-infra-private/pr/merge",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/commit-message",
"//dev-infra/utils",
"@npm//@octokit/rest",
"@npm//@types/inquirer",
"@npm//@types/node",
"@npm//@types/semver",
"@npm//@types/yargs",
"@npm//chalk",
],
)

34
dev-infra/pr/merge/cli.ts Normal file
View File

@ -0,0 +1,34 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Arguments, Argv} from 'yargs';
import {error, red, yellow} from '../../utils/console';
import {GITHUB_TOKEN_GENERATE_URL, mergePullRequest} from './index';
/** Builds the options for the merge command. */
export function buildMergeCommand(yargs: Argv) {
return yargs.help().strict().option('github-token', {
type: 'string',
description: 'Github token. If not set, token is retrieved from the environment variables.'
})
}
/** Handles the merge command. i.e. performs the merge of a specified pull request. */
export async function handleMergeCommand(args: Arguments) {
const githubToken = args.githubToken || process.env.GITHUB_TOKEN || process.env.TOKEN;
if (!githubToken) {
error(red('No Github token set. Please set the `GITHUB_TOKEN` environment variable.'));
error(red('Alternatively, pass the `--github-token` command line flag.'));
error(yellow(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`));
process.exit(1);
}
await mergePullRequest(args.prNumber, githubToken);
}

View File

@ -0,0 +1,137 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {getConfig, NgDevConfig} from '../../utils/config';
import {GithubApiMergeStrategyConfig} from './strategies/api-merge';
/**
* Possible merge methods supported by the Github API.
* https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-button.
*/
export type GithubApiMergeMethod = 'merge'|'squash'|'rebase';
/**
* Target labels represent Github pull requests labels. These labels instruct the merge
* script into which branches a given pull request should be merged to.
*/
export interface TargetLabel {
/** Pattern that matches the given target label. */
pattern: RegExp|string;
/**
* List of branches a pull request with this target label should be merged into.
* Can also be wrapped in a function that accepts the target branch specified in the
* Github Web UI. This is useful for supporting labels like `target: development-branch`.
*/
branches: string[]|((githubTargetBranch: string) => string[]);
}
/** Describes the remote used for merging pull requests. */
export interface MergeRemote {
/** Owner name of the repository. */
owner: string;
/** Name of the repository. */
name: string;
/** Whether SSH should be used for merging pull requests. */
useSsh?: boolean
}
/**
* Configuration for the merge script with all remote options specified. The
* default `MergeConfig` has does not require any of these options as defaults
* are provided by the common dev-infra github configuration.
*/
export type MergeConfigWithRemote = MergeConfig&{remote: MergeRemote};
/** Configuration for the merge script. */
export interface MergeConfig {
/**
* Configuration for the upstream remote. All of these options are optional as
* defaults are provided by the common dev-infra github configuration.
*/
remote?: Partial<MergeRemote>;
/** List of target labels. */
labels: TargetLabel[];
/** Required base commits for given branches. */
requiredBaseCommits?: {[branchName: string]: string};
/** Pattern that matches labels which imply a signed CLA. */
claSignedLabel: string|RegExp;
/** Pattern that matches labels which imply a merge ready pull request. */
mergeReadyLabel: string|RegExp;
/** Label which can be applied to fixup commit messages in the merge script. */
commitMessageFixupLabel: string|RegExp;
/**
* Whether pull requests should be merged using the Github API. This can be enabled
* if projects want to have their pull requests show up as `Merged` in the Github UI.
* The downside is that fixup or squash commits no longer work as the Github API does
* not support this.
*/
githubApiMerge: false|GithubApiMergeStrategyConfig;
}
/**
* Configuration of the merge script in the dev-infra configuration. Note that the
* merge configuration is retrieved lazily as usually these configurations rely
* on branch name computations. We don't want to run these immediately whenever
* the dev-infra configuration is loaded as that could slow-down other commands.
*/
export type DevInfraMergeConfig = NgDevConfig<{'merge': () => MergeConfig}>;
/** Loads and validates the merge configuration. */
export function loadAndValidateConfig(): {config?: MergeConfigWithRemote, errors?: string[]} {
const config: Partial<DevInfraMergeConfig> = getConfig();
if (config.merge === undefined) {
return {
errors: ['No merge configuration found. Set the `merge` configuration.']
}
}
if (typeof config.merge !== 'function') {
return {
errors: ['Expected merge configuration to be defined lazily through a function.']
}
}
const mergeConfig = config.merge();
const errors = validateMergeConfig(mergeConfig);
if (errors.length) {
return {errors};
}
if (mergeConfig.remote) {
mergeConfig.remote = {...config.github, ...mergeConfig.remote};
} else {
mergeConfig.remote = config.github;
}
// We always set the `remote` option, so we can safely cast the
// config to `MergeConfigWithRemote`.
return {config: mergeConfig as MergeConfigWithRemote};
}
/** Validates the specified configuration. Returns a list of failure messages. */
function validateMergeConfig(config: Partial<MergeConfig>): string[] {
const errors: string[] = [];
if (!config.labels) {
errors.push('No label configuration.');
} else if (!Array.isArray(config.labels)) {
errors.push('Label configuration needs to be an array.');
}
if (!config.claSignedLabel) {
errors.push('No CLA signed label configured.');
}
if (!config.mergeReadyLabel) {
errors.push('No merge ready label configured.');
}
if (config.githubApiMerge === undefined) {
errors.push('No explicit choice of merge strategy. Please set `githubApiMerge`.');
}
return errors;
}

View File

@ -0,0 +1,68 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as semver from 'semver';
import {exec} from '../../utils/shelljs';
/**
* Helper function that can be used to determine merge branches based on a given
* project version. The function determines merge branches primarily through the
* specified version, but falls back to consulting the NPM registry when needed.
*
* Consulting the NPM registry for determining the patch branch may slow down merging,
* so whenever possible, the branches are determined statically based on the current
* version. In some cases, consulting the NPM registry is inevitable because for major
* pre-releases, we cannot determine the latest stable minor version from the current
* pre-release version.
*/
export function determineMergeBranches(
currentVersion: string, npmPackageName: string): {minor: string, patch: string} {
const projectVersion = semver.parse(currentVersion);
if (projectVersion === null) {
throw Error('Cannot parse version set in project "package.json" file.');
}
const {major, minor, patch, prerelease} = projectVersion;
const isMajor = minor === 0 && patch === 0;
const isMinor = minor !== 0 && patch === 0;
// If there is no prerelease, then we compute patch and minor branches based
// on the current version major and minor.
if (prerelease.length === 0) {
return {minor: `${major}.x`, patch: `${major}.${minor}.x`};
}
// If current version is set to a minor prerelease, we can compute the merge branches
// statically. e.g. if we are set to `9.3.0-next.0`, then our merge branches should
// be set to `9.x` and `9.2.x`.
if (isMinor) {
return {minor: `${major}.x`, patch: `${major}.${minor - 1}.x`};
} else if (!isMajor) {
throw Error('Unexpected version. Cannot have prerelease for patch version.');
}
// If we are set to a major prerelease, we cannot statically determine the stable patch
// branch (as the latest minor segment is unknown). We determine it by looking in the NPM
// registry for the latest stable release that will tell us about the current minor segment.
// e.g. if the current major is `v10.0.0-next.0`, then we need to look for the latest release.
// Let's say this is `v9.2.6`. Our patch branch will then be called `9.2.x`.
const latestVersion = exec(`yarn -s info ${npmPackageName} dist-tags.latest`).trim();
if (!latestVersion) {
throw Error('Could not determine version of latest release.');
}
const expectedMajor = major - 1;
const parsedLatestVersion = semver.parse(latestVersion);
if (parsedLatestVersion === null) {
throw Error(`Could not parse latest version from NPM registry: ${latestVersion}`);
} else if (parsedLatestVersion.major !== expectedMajor) {
throw Error(
`Expected latest release to have major version: v${expectedMajor}, ` +
`but got: v${latestVersion}`);
}
return {patch: `${expectedMajor}.${parsedLatestVersion.minor}.x`, minor: `${expectedMajor}.x`};
}

View File

@ -0,0 +1,79 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* Class that can be used to describe pull request failures. A failure
* is described through a human-readable message and a flag indicating
* whether it is non-fatal or not.
*/
export class PullRequestFailure {
constructor(
/** Human-readable message for the failure */
public message: string,
/** Whether the failure is non-fatal and can be forcibly ignored. */
public nonFatal = false) {}
static claUnsigned() {
return new this(`CLA has not been signed. Please make sure the PR author has signed the CLA.`);
}
static failingCiJobs() {
return new this(`Failing CI jobs.`, true);
}
static pendingCiJobs() {
return new this(`Pending CI jobs.`, true);
}
static notMergeReady() {
return new this(`Not marked as merge ready.`);
}
static noTargetLabel() {
return new this(`No target branch could be determined. Please ensure a target label is set.`);
}
static mismatchingTargetBranch(allowedBranches: string[]) {
return new this(
`Pull request is set to wrong base branch. Please update the PR in the Github UI ` +
`to one of the following branches: ${allowedBranches.join(', ')}.`);
}
static unsatisfiedBaseSha() {
return new this(
`Pull request has not been rebased recently and could be bypassing CI checks. ` +
`Please rebase the PR.`);
}
static mergeConflicts(failedBranches: string[]) {
return new this(
`Could not merge pull request into the following branches due to merge ` +
`conflicts: ${
failedBranches.join(', ')}. Please rebase the PR or update the target label.`);
}
static unknownMergeError() {
return new this(`Unknown merge error occurred. Please see console output above for debugging.`);
}
static unableToFixupCommitMessageSquashOnly() {
return new this(
`Unable to fixup commit message of pull request. Commit message can only be ` +
`modified if the PR is merged using squash.`);
}
static notFound() {
return new this(`Pull request could not be found upstream.`);
}
static insufficientPermissionsToMerge() {
return new this(
`Insufficient Github API permissions to merge pull request. Please ` +
`ensure that your auth token has write access.`);
}
}

120
dev-infra/pr/merge/git.ts Normal file
View File

@ -0,0 +1,120 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as Octokit from '@octokit/rest';
import {spawnSync, SpawnSyncOptions, SpawnSyncReturns} from 'child_process';
import {info} from '../../utils/console';
import {MergeConfigWithRemote} from './config';
/** Error for failed Github API requests. */
export class GithubApiRequestError extends Error {
constructor(public status: number, message: string) {
super(message);
}
}
/** Error for failed Git commands. */
export class GitCommandError extends Error {
constructor(client: GitClient, public args: string[]) {
// Errors are not guaranteed to be caught. To ensure that we don't
// accidentally leak the Github token that might be used in a command,
// we sanitize the command that will be part of the error message.
super(`Command failed: git ${client.omitGithubTokenFromMessage(args.join(' '))}`);
}
}
export class GitClient {
/** Short-hand for accessing the remote configuration. */
remoteConfig = this._config.remote;
/** Octokit request parameters object for targeting the configured remote. */
remoteParams = {owner: this.remoteConfig.owner, repo: this.remoteConfig.name};
/** URL that resolves to the configured repository. */
repoGitUrl = this.remoteConfig.useSsh ?
`git@github.com:${this.remoteConfig.owner}/${this.remoteConfig.name}.git` :
`https://${this._githubToken}@github.com/${this.remoteConfig.owner}/${
this.remoteConfig.name}.git`;
/** Instance of the authenticated Github octokit API. */
api: Octokit;
/** Regular expression that matches the provided Github token. */
private _tokenRegex = new RegExp(this._githubToken, 'g');
constructor(
private _projectRoot: string, private _githubToken: string,
private _config: MergeConfigWithRemote) {
this.api = new Octokit({auth: _githubToken});
this.api.hook.error('request', error => {
// Wrap API errors in a known error class. This allows us to
// expect Github API errors better and in a non-ambiguous way.
throw new GithubApiRequestError(error.status, error.message);
});
}
/** Executes the given git command. Throws if the command fails. */
run(args: string[], options?: SpawnSyncOptions): Omit<SpawnSyncReturns<string>, 'status'> {
const result = this.runGraceful(args, options);
if (result.status !== 0) {
throw new GitCommandError(this, args);
}
// Omit `status` from the type so that it's obvious that the status is never
// non-zero as explained in the method description.
return result as Omit<SpawnSyncReturns<string>, 'status'>;
}
/**
* Spawns a given Git command process. Does not throw if the command fails. Additionally,
* if there is any stderr output, the output will be printed. This makes it easier to
* debug failed commands.
*/
runGraceful(args: string[], options: SpawnSyncOptions = {}): SpawnSyncReturns<string> {
// To improve the debugging experience in case something fails, we print all executed
// Git commands. Note that we do not want to print the token if is contained in the
// command. It's common to share errors with others if the tool failed.
info('Executing: git', this.omitGithubTokenFromMessage(args.join(' ')));
const result = spawnSync('git', args, {
cwd: this._projectRoot,
stdio: 'pipe',
...options,
// Encoding is always `utf8` and not overridable. This ensures that this method
// always returns `string` as output instead of buffers.
encoding: 'utf8',
});
if (result.stderr !== null) {
// Git sometimes prints the command if it failed. This means that it could
// potentially leak the Github token used for accessing the remote. To avoid
// printing a token, we sanitize the string before printing the stderr output.
process.stderr.write(this.omitGithubTokenFromMessage(result.stderr));
}
return result;
}
/** Whether the given branch contains the specified SHA. */
hasCommit(branchName: string, sha: string): boolean {
return this.run(['branch', branchName, '--contains', sha]).stdout !== '';
}
/** Gets the currently checked out branch. */
getCurrentBranch(): string {
return this.run(['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim();
}
/** Gets whether the current Git repository has uncommitted changes. */
hasUncommittedChanges(): boolean {
return this.runGraceful(['diff-index', '--quiet', 'HEAD']).status !== 0;
}
/** Sanitizes a given message by omitting the provided Github token if present. */
omitGithubTokenFromMessage(value: string): string {
return value.replace(this._tokenRegex, '<TOKEN>');
}
}

127
dev-infra/pr/merge/index.ts Normal file
View File

@ -0,0 +1,127 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {getRepoBaseDir} from '../../utils/config';
import {error, green, info, promptConfirm, red, yellow} from '../../utils/console';
import {loadAndValidateConfig, MergeConfigWithRemote} from './config';
import {GithubApiRequestError} from './git';
import {MergeResult, MergeStatus, PullRequestMergeTask} from './task';
/** URL to the Github page where personal access tokens can be generated. */
export const GITHUB_TOKEN_GENERATE_URL = `https://github.com/settings/tokens`;
/**
* Merges a given pull request based on labels configured in the given merge configuration.
* Pull requests can be merged with different strategies such as the Github API merge
* strategy, or the local autosquash strategy. Either strategy has benefits and downsides.
* More information on these strategies can be found in their dedicated strategy classes.
*
* See {@link GithubApiMergeStrategy} and {@link AutosquashMergeStrategy}
*
* @param prNumber Number of the pull request that should be merged.
* @param githubToken Github token used for merging (i.e. fetching and pushing)
* @param projectRoot Path to the local Git project that is used for merging.
* @param config Configuration for merging pull requests.
*/
export async function mergePullRequest(
prNumber: number, githubToken: string, projectRoot: string = getRepoBaseDir(),
config?: MergeConfigWithRemote) {
// If no explicit configuration has been specified, we load and validate
// the configuration from the shared dev-infra configuration.
if (config === undefined) {
const {config: _config, errors} = loadAndValidateConfig();
if (errors) {
error(red('Invalid configuration:'));
errors.forEach(desc => error(yellow(` - ${desc}`)));
process.exit(1);
}
config = _config!;
}
const api = new PullRequestMergeTask(projectRoot, config, githubToken);
// Perform the merge. Force mode can be activated through a command line flag.
// Alternatively, if the merge fails with non-fatal failures, the script
// will prompt whether it should rerun in force mode.
if (!await performMerge(false)) {
process.exit(1);
}
/** Performs the merge and returns whether it was successful or not. */
async function performMerge(ignoreFatalErrors: boolean): Promise<boolean> {
try {
const result = await api.merge(prNumber, ignoreFatalErrors);
return await handleMergeResult(result, ignoreFatalErrors);
} catch (e) {
// Catch errors to the Github API for invalid requests. We want to
// exit the script with a better explanation of the error.
if (e instanceof GithubApiRequestError && e.status === 401) {
error(red('Github API request failed. ' + e.message));
error(yellow('Please ensure that your provided token is valid.'));
error(yellow(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`));
process.exit(1);
}
throw e;
}
}
/**
* Prompts whether the specified pull request should be forcibly merged. If so, merges
* the specified pull request forcibly (ignoring non-critical failures).
* @returns Whether the specified pull request has been forcibly merged.
*/
async function promptAndPerformForceMerge(): Promise<boolean> {
if (await promptConfirm('Do you want to forcibly proceed with merging?')) {
// Perform the merge in force mode. This means that non-fatal failures
// are ignored and the merge continues.
return performMerge(true);
}
return false;
}
/**
* Handles the merge result by printing console messages, exiting the process
* based on the result, or by restarting the merge if force mode has been enabled.
* @returns Whether the merge was successful or not.
*/
async function handleMergeResult(result: MergeResult, disableForceMergePrompt = false) {
const {failure, status} = result;
const canForciblyMerge = failure && failure.nonFatal;
switch (status) {
case MergeStatus.SUCCESS:
info(green(`Successfully merged the pull request: ${prNumber}`));
return true;
case MergeStatus.DIRTY_WORKING_DIR:
error(
red(`Local working repository not clean. Please make sure there are ` +
`no uncommitted changes.`));
return false;
case MergeStatus.UNKNOWN_GIT_ERROR:
error(
red('An unknown Git error has been thrown. Please check the output ' +
'above for details.'));
return false;
case MergeStatus.FAILED:
error(yellow(`Could not merge the specified pull request.`));
error(red(failure!.message));
if (canForciblyMerge && !disableForceMergePrompt) {
info();
info(yellow('The pull request above failed due to non-critical errors.'));
info(yellow(`This error can be forcibly ignored if desired.`));
return await promptAndPerformForceMerge();
}
return false;
default:
throw Error(`Unexpected merge result: ${status}`);
}
}
}

View File

@ -0,0 +1,111 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as Octokit from '@octokit/rest';
import {PullRequestFailure} from './failures';
import {GitClient} from './git';
import {matchesPattern} from './string-pattern';
import {getBranchesFromTargetLabel, getTargetLabelFromPullRequest} from './target-label';
import {PullRequestMergeTask} from './task';
/** Interface that describes a pull request. */
export interface PullRequest {
/** Number of the pull request. */
prNumber: number;
/** Title of the pull request. */
title: string;
/** Labels applied to the pull request. */
labels: string[];
/** List of branches this PR should be merged into. */
targetBranches: string[];
/** Branch that the PR targets in the Github UI. */
githubTargetBranch: string;
/** Count of commits in this pull request. */
commitCount: number;
/** Optional SHA that this pull request needs to be based on. */
requiredBaseSha?: string;
/** Whether the pull request commit message fixup. */
needsCommitMessageFixup: boolean;
}
/**
* Loads and validates the specified pull request against the given configuration.
* If the pull requests fails, a pull request failure is returned.
*/
export async function loadAndValidatePullRequest(
{git, config}: PullRequestMergeTask, prNumber: number,
ignoreNonFatalFailures = false): Promise<PullRequest|PullRequestFailure> {
const prData = await fetchPullRequestFromGithub(git, prNumber);
if (prData === null) {
return PullRequestFailure.notFound();
}
const labels = prData.labels.map(l => l.name);
if (!labels.some(name => matchesPattern(name, config.mergeReadyLabel))) {
return PullRequestFailure.notMergeReady();
}
if (!labels.some(name => matchesPattern(name, config.claSignedLabel))) {
return PullRequestFailure.claUnsigned();
}
const targetLabel = getTargetLabelFromPullRequest(config, labels);
if (targetLabel === null) {
return PullRequestFailure.noTargetLabel();
}
const {data: {state}} =
await git.api.repos.getCombinedStatusForRef({...git.remoteParams, ref: prData.head.sha});
if (state === 'failure' && !ignoreNonFatalFailures) {
return PullRequestFailure.failingCiJobs();
}
if (state === 'pending' && !ignoreNonFatalFailures) {
return PullRequestFailure.pendingCiJobs();
}
const githubTargetBranch = prData.base.ref;
const requiredBaseSha =
config.requiredBaseCommits && config.requiredBaseCommits[githubTargetBranch];
const needsCommitMessageFixup = !!config.commitMessageFixupLabel &&
labels.some(name => matchesPattern(name, config.commitMessageFixupLabel));
return {
prNumber,
labels,
requiredBaseSha,
githubTargetBranch,
needsCommitMessageFixup,
title: prData.title,
targetBranches: getBranchesFromTargetLabel(targetLabel, githubTargetBranch),
commitCount: prData.commits,
};
}
/** Fetches a pull request from Github. Returns null if an error occurred. */
async function fetchPullRequestFromGithub(
git: GitClient, prNumber: number): Promise<Octokit.PullsGetResponse|null> {
try {
const result = await git.api.pulls.get({...git.remoteParams, pull_number: prNumber});
return result.data;
} catch (e) {
// If the pull request could not be found, we want to return `null` so
// that the error can be handled gracefully.
if (e.status === 404) {
return null;
}
throw e;
}
}
/** Whether the specified value resolves to a pull request. */
export function isPullRequest(v: PullRequestFailure|PullRequest): v is PullRequest {
return (v as PullRequest).targetBranches !== undefined;
}

View File

@ -0,0 +1,225 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {PullsListCommitsResponse, PullsMergeParams} from '@octokit/rest';
import {prompt} from 'inquirer';
import {parseCommitMessage} from '../../../commit-message/validate';
import {GithubApiMergeMethod} from '../config';
import {PullRequestFailure} from '../failures';
import {GitClient} from '../git';
import {PullRequest} from '../pull-request';
import {matchesPattern} from '../string-pattern';
import {MergeStrategy, TEMP_PR_HEAD_BRANCH} from './strategy';
/** Configuration for the Github API merge strategy. */
export interface GithubApiMergeStrategyConfig {
/** Default method used for merging pull requests */
default: GithubApiMergeMethod;
/** Labels which specify a different merge method than the default. */
labels?: {pattern: string, method: GithubApiMergeMethod}[];
}
/** Separator between commit message header and body. */
const COMMIT_HEADER_SEPARATOR = '\n\n';
/**
* Merge strategy that primarily leverages the Github API. The strategy merges a given
* pull request into a target branch using the API. This ensures that Github displays
* the pull request as merged. The merged commits are then cherry-picked into the remaining
* target branches using the local Git instance. The benefit is that the Github merged state
* is properly set, but a notable downside is that PRs cannot use fixup or squash commits.
*/
export class GithubApiMergeStrategy extends MergeStrategy {
constructor(git: GitClient, private _config: GithubApiMergeStrategyConfig) {
super(git);
}
async merge(pullRequest: PullRequest): Promise<PullRequestFailure|null> {
const {githubTargetBranch, prNumber, targetBranches, requiredBaseSha, needsCommitMessageFixup} =
pullRequest;
// If the pull request does not have its base branch set to any determined target
// branch, we cannot merge using the API.
if (targetBranches.every(t => t !== githubTargetBranch)) {
return PullRequestFailure.mismatchingTargetBranch(targetBranches);
}
// In cases where a required base commit is specified for this pull request, check if
// the pull request contains the given commit. If not, return a pull request failure.
// This check is useful for enforcing that PRs are rebased on top of a given commit.
// e.g. a commit that changes the code ownership validation. PRs which are not rebased
// could bypass new codeowner ship rules.
if (requiredBaseSha && !this.git.hasCommit(TEMP_PR_HEAD_BRANCH, requiredBaseSha)) {
return PullRequestFailure.unsatisfiedBaseSha();
}
const method = this._getMergeActionFromPullRequest(pullRequest);
const cherryPickTargetBranches = targetBranches.filter(b => b !== githubTargetBranch);
// First cherry-pick the PR into all local target branches in dry-run mode. This is
// purely for testing so that we can figure out whether the PR can be cherry-picked
// into the other target branches. We don't want to merge the PR through the API, and
// then run into cherry-pick conflicts after the initial merge already completed.
const failure = await this._checkMergability(pullRequest, cherryPickTargetBranches);
// If the PR could not be cherry-picked into all target branches locally, we know it can't
// be done through the Github API either. We abort merging and pass-through the failure.
if (failure !== null) {
return failure;
}
const mergeOptions: PullsMergeParams = {
pull_number: prNumber,
merge_method: method,
...this.git.remoteParams,
};
if (needsCommitMessageFixup) {
// Commit message fixup does not work with other merge methods as the Github API only
// allows commit message modifications for squash merging.
if (method !== 'squash') {
return PullRequestFailure.unableToFixupCommitMessageSquashOnly();
}
await this._promptCommitMessageEdit(pullRequest, mergeOptions);
}
let mergeStatusCode: number;
let targetSha: string;
try {
// Merge the pull request using the Github API into the selected base branch.
const result = await this.git.api.pulls.merge(mergeOptions);
mergeStatusCode = result.status;
targetSha = result.data.sha;
} catch (e) {
// Note: Github usually returns `404` as status code if the API request uses a
// token with insufficient permissions. Github does this because it doesn't want
// to leak whether a repository exists or not. In our case we expect a certain
// repository to exist, so we always treat this as a permission failure.
if (e.status === 403 || e.status === 404) {
return PullRequestFailure.insufficientPermissionsToMerge();
}
throw e;
}
// https://developer.github.com/v3/pulls/#response-if-merge-cannot-be-performed
// Pull request cannot be merged due to merge conflicts.
if (mergeStatusCode === 405) {
return PullRequestFailure.mergeConflicts([githubTargetBranch]);
}
if (mergeStatusCode !== 200) {
return PullRequestFailure.unknownMergeError();
}
// If the PR does not need to be merged into any other target branches,
// we exit here as we already completed the merge.
if (!cherryPickTargetBranches.length) {
return null;
}
// Refresh the target branch the PR has been merged into through the API. We need
// to re-fetch as otherwise we cannot cherry-pick the new commits into the remaining
// target branches.
this.fetchTargetBranches([githubTargetBranch]);
// Number of commits that have landed in the target branch. This could vary from
// the count of commits in the PR due to squashing.
const targetCommitsCount = method === 'squash' ? 1 : pullRequest.commitCount;
// Cherry pick the merged commits into the remaining target branches.
const failedBranches = await this.cherryPickIntoTargetBranches(
`${targetSha}~${targetCommitsCount}..${targetSha}`, cherryPickTargetBranches);
// We already checked whether the PR can be cherry-picked into the target branches,
// but in case the cherry-pick somehow fails, we still handle the conflicts here. The
// commits created through the Github API could be different (i.e. through squash).
if (failedBranches.length) {
return PullRequestFailure.mergeConflicts(failedBranches);
}
this.pushTargetBranchesUpstream(cherryPickTargetBranches);
return null;
}
/**
* Prompts the user for the commit message changes. Unlike as in the autosquash merge
* strategy, we cannot start an interactive rebase because we merge using the Github API.
* The Github API only allows modifications to PR title and body for squash merges.
*/
async _promptCommitMessageEdit(pullRequest: PullRequest, mergeOptions: PullsMergeParams) {
const commitMessage = await this._getDefaultSquashCommitMessage(pullRequest);
const {result} = await prompt<{result: string}>({
type: 'editor',
name: 'result',
message: 'Please update the commit message',
default: commitMessage,
});
// Split the new message into title and message. This is necessary because the
// Github API expects title and message to be passed separately.
const [newTitle, ...newMessage] = result.split(COMMIT_HEADER_SEPARATOR);
// Update the merge options so that the changes are reflected in there.
mergeOptions.commit_title = `${newTitle} (#${pullRequest.prNumber})`;
mergeOptions.commit_message = newMessage.join(COMMIT_HEADER_SEPARATOR);
}
/**
* Gets a commit message for the given pull request. Github by default concatenates
* multiple commit messages if a PR is merged in squash mode. We try to replicate this
* behavior here so that we have a default commit message that can be fixed up.
*/
private async _getDefaultSquashCommitMessage(pullRequest: PullRequest): Promise<string> {
const commits = (await this._getPullRequestCommitMessages(pullRequest))
.map(message => ({message, parsed: parseCommitMessage(message)}));
const messageBase = `${pullRequest.title}${COMMIT_HEADER_SEPARATOR}`;
if (commits.length <= 1) {
return `${messageBase}${commits[0].parsed.body}`;
}
const joinedMessages = commits.map(c => `* ${c.message}`).join(COMMIT_HEADER_SEPARATOR);
return `${messageBase}${joinedMessages}`;
}
/** Gets all commit messages of commits in the pull request. */
private async _getPullRequestCommitMessages({prNumber}: PullRequest) {
const request = this.git.api.pulls.listCommits.endpoint.merge(
{...this.git.remoteParams, pull_number: prNumber});
const allCommits: PullsListCommitsResponse = await this.git.api.paginate(request);
return allCommits.map(({commit}) => commit.message);
}
/**
* Checks if given pull request could be merged into its target branches.
* @returns A pull request failure if it the PR could not be merged.
*/
private async _checkMergability(pullRequest: PullRequest, targetBranches: string[]):
Promise<null|PullRequestFailure> {
const revisionRange = this.getPullRequestRevisionRange(pullRequest);
const failedBranches =
this.cherryPickIntoTargetBranches(revisionRange, targetBranches, {dryRun: true});
if (failedBranches.length) {
return PullRequestFailure.mergeConflicts(failedBranches);
}
return null;
}
/** Determines the merge action from the given pull request. */
private _getMergeActionFromPullRequest({labels}: PullRequest): GithubApiMergeMethod {
if (this._config.labels) {
const matchingLabel =
this._config.labels.find(({pattern}) => labels.some(l => matchesPattern(l, pattern)));
if (matchingLabel !== undefined) {
return matchingLabel.method;
}
}
return this._config.default;
}
}

View File

@ -0,0 +1,80 @@
import {join} from 'path';
import {PullRequestFailure} from '../failures';
import {PullRequest} from '../pull-request';
import {MergeStrategy, TEMP_PR_HEAD_BRANCH} from './strategy';
/** Path to the commit message filter script. Git expects this paths to use forward slashes. */
const MSG_FILTER_SCRIPT = join(__dirname, './commit-message-filter.js').replace(/\\/g, '/');
/**
* Merge strategy that does not use the Github API for merging. Instead, it fetches
* all target branches and the PR locally. The PR is then cherry-picked with autosquash
* enabled into the target branches. The benefit is the support for fixup and squash commits.
* A notable downside though is that Github does not show the PR as `Merged` due to non
* fast-forward merges
*/
export class AutosquashMergeStrategy extends MergeStrategy {
/**
* Merges the specified pull request into the target branches and pushes the target
* branches upstream. This method requires the temporary target branches to be fetched
* already as we don't want to fetch the target branches per pull request merge. This
* would causes unnecessary multiple fetch requests when multiple PRs are merged.
* @throws {GitCommandError} An unknown Git command error occurred that is not
* specific to the pull request merge.
* @returns A pull request failure or null in case of success.
*/
async merge(pullRequest: PullRequest): Promise<PullRequestFailure|null> {
const {prNumber, targetBranches, requiredBaseSha, needsCommitMessageFixup} = pullRequest;
// In case a required base is specified for this pull request, check if the pull
// request contains the given commit. If not, return a pull request failure. This
// check is useful for enforcing that PRs are rebased on top of a given commit. e.g.
// a commit that changes the codeowner ship validation. PRs which are not rebased
// could bypass new codeowner ship rules.
if (requiredBaseSha && !this.git.hasCommit(TEMP_PR_HEAD_BRANCH, requiredBaseSha)) {
return PullRequestFailure.unsatisfiedBaseSha();
}
// SHA for the first commit the pull request is based on. Usually we would able
// to just rely on the base revision provided by `getPullRequestBaseRevision`, but
// the revision would rely on the amount of commits in a pull request. This is not
// reliable as we rebase the PR with autosquash where the amount of commits could
// change. We work around this by parsing the base revision so that we have a fixated
// SHA before the autosquash rebase is performed.
const baseSha =
this.git.run(['rev-parse', this.getPullRequestBaseRevision(pullRequest)]).stdout.trim();
// Git revision range that matches the pull request commits.
const revisionRange = `${baseSha}..${TEMP_PR_HEAD_BRANCH}`;
// We always rebase the pull request so that fixup or squash commits are automatically
// collapsed. Git's autosquash functionality does only work in interactive rebases, so
// our rebase is always interactive. In reality though, unless a commit message fixup
// is desired, we set the `GIT_SEQUENCE_EDITOR` environment variable to `true` so that
// the rebase seems interactive to Git, while it's not interactive to the user.
// See: https://github.com/git/git/commit/891d4a0313edc03f7e2ecb96edec5d30dc182294.
const branchBeforeRebase = this.git.getCurrentBranch();
const rebaseEnv =
needsCommitMessageFixup ? undefined : {...process.env, GIT_SEQUENCE_EDITOR: 'true'};
this.git.run(
['rebase', '--interactive', '--autosquash', baseSha, TEMP_PR_HEAD_BRANCH],
{stdio: 'inherit', env: rebaseEnv});
// Update pull requests commits to reference the pull request. This matches what
// Github does when pull requests are merged through the Web UI. The motivation is
// that it should be easy to determine which pull request contained a given commit.
// **Note**: The filter-branch command relies on the working tree, so we want to make
// sure that we are on the initial branch where the merge script has been run.
this.git.run(['checkout', '-f', branchBeforeRebase]);
this.git.run(
['filter-branch', '-f', '--msg-filter', `${MSG_FILTER_SCRIPT} ${prNumber}`, revisionRange]);
// Cherry-pick the pull request into all determined target branches.
const failedBranches = this.cherryPickIntoTargetBranches(revisionRange, targetBranches);
if (failedBranches.length) {
return PullRequestFailure.mergeConflicts(failedBranches);
}
this.pushTargetBranchesUpstream(targetBranches);
return null;
}
}

View File

@ -0,0 +1,38 @@
#!/usr/bin/env node
/**
* Script that can be passed as commit message filter to `git filter-branch --msg-filter`.
* The script rewrites commit messages to contain a Github instruction to close the
* corresponding pull request. For more details. See: https://git.io/Jv64r.
*/
if (require.main === module) {
const [prNumber] = process.argv.slice(2);
if (!prNumber) {
console.error('No pull request number specified.');
process.exit(1);
}
let commitMessage = '';
process.stdin.setEncoding('utf8');
process.stdin.on('readable', () => {
const chunk = process.stdin.read();
if (chunk !== null) {
commitMessage += chunk;
}
});
process.stdin.on('end', () => {
console.info(rewriteCommitMessage(commitMessage, prNumber));
});
}
function rewriteCommitMessage(message, prNumber) {
const lines = message.split(/\n/);
// Add the pull request number to the commit message title. This matches what
// Github does when PRs are merged on the web through the `Squash and Merge` button.
lines[0] += ` (#${prNumber})`;
// Push a new line that instructs Github to close the specified pull request.
lines.push(`PR Close #${prNumber}`);
return lines.join('\n');
}

View File

@ -0,0 +1,131 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {PullRequestFailure} from '../failures';
import {GitClient} from '../git';
import {PullRequest} from '../pull-request';
/**
* Name of a temporary branch that contains the head of a currently-processed PR. Note
* that a branch name should be used that most likely does not conflict with other local
* development branches.
*/
export const TEMP_PR_HEAD_BRANCH = 'merge_pr_head';
/**
* Base class for merge strategies. A merge strategy accepts a pull request and
* merges it into the determined target branches.
*/
export abstract class MergeStrategy {
constructor(protected git: GitClient) {}
/**
* Prepares a merge of the given pull request. The strategy by default will
* fetch all target branches and the pull request into local temporary branches.
*/
async prepare(pullRequest: PullRequest) {
this.fetchTargetBranches(
pullRequest.targetBranches, `pull/${pullRequest.prNumber}/head:${TEMP_PR_HEAD_BRANCH}`);
}
/**
* Performs the merge of the given pull request. This needs to be implemented
* by individual merge strategies.
*/
abstract merge(pullRequest: PullRequest): Promise<null|PullRequestFailure>;
/** Cleans up the pull request merge. e.g. deleting temporary local branches. */
async cleanup(pullRequest: PullRequest) {
// Delete all temporary target branches.
pullRequest.targetBranches.forEach(
branchName => this.git.run(['branch', '-D', this.getLocalTargetBranchName(branchName)]));
// Delete temporary branch for the pull request head.
this.git.run(['branch', '-D', TEMP_PR_HEAD_BRANCH]);
}
/** Gets the revision range for all commits in the given pull request. */
protected getPullRequestRevisionRange(pullRequest: PullRequest): string {
return `${this.getPullRequestBaseRevision(pullRequest)}..${TEMP_PR_HEAD_BRANCH}`;
}
/** Gets the base revision of a pull request. i.e. the commit the PR is based on. */
protected getPullRequestBaseRevision(pullRequest: PullRequest): string {
return `${TEMP_PR_HEAD_BRANCH}~${pullRequest.commitCount}`;
}
/** Gets a deterministic local branch name for a given branch. */
protected getLocalTargetBranchName(targetBranch: string): string {
return `merge_pr_target_${targetBranch.replace(/\//g, '_')}`;
}
/**
* Cherry-picks the given revision range into the specified target branches.
* @returns A list of branches for which the revisions could not be cherry-picked into.
*/
protected cherryPickIntoTargetBranches(revisionRange: string, targetBranches: string[], options: {
dryRun?: boolean
} = {}) {
const cherryPickArgs = [revisionRange];
const failedBranches: string[] = [];
if (options.dryRun) {
// https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt---no-commit
// This causes `git cherry-pick` to not generate any commits. Instead, the changes are
// applied directly in the working tree. This allow us to easily discard the changes
// for dry-run purposes.
cherryPickArgs.push('--no-commit');
}
// Cherry-pick the refspec into all determined target branches.
for (const branchName of targetBranches) {
const localTargetBranch = this.getLocalTargetBranchName(branchName);
// Checkout the local target branch.
this.git.run(['checkout', localTargetBranch]);
// Cherry-pick the refspec into the target branch.
if (this.git.runGraceful(['cherry-pick', ...cherryPickArgs]).status !== 0) {
// Abort the failed cherry-pick. We do this because Git persists the failed
// cherry-pick state globally in the repository. This could prevent future
// pull request merges as a Git thinks a cherry-pick is still in progress.
this.git.runGraceful(['cherry-pick', '--abort']);
failedBranches.push(branchName);
}
// If we run with dry run mode, we reset the local target branch so that all dry-run
// cherry-pick changes are discard. Changes are applied to the working tree and index.
if (options.dryRun) {
this.git.run(['reset', '--hard', 'HEAD']);
}
}
return failedBranches;
}
/**
* Fetches the given target branches. Also accepts a list of additional refspecs that
* should be fetched. This is helpful as multiple slow fetches could be avoided.
*/
protected fetchTargetBranches(names: string[], ...extraRefspecs: string[]) {
const fetchRefspecs = names.map(targetBranch => {
const localTargetBranch = this.getLocalTargetBranchName(targetBranch);
return `refs/heads/${targetBranch}:${localTargetBranch}`;
});
// Fetch all target branches with a single command. We don't want to fetch them
// individually as that could cause an unnecessary slow-down.
this.git.run(['fetch', '-f', this.git.repoGitUrl, ...fetchRefspecs, ...extraRefspecs]);
}
/** Pushes the given target branches upstream. */
protected pushTargetBranchesUpstream(names: string[]) {
const pushRefspecs = names.map(targetBranch => {
const localTargetBranch = this.getLocalTargetBranchName(targetBranch);
return `${localTargetBranch}:refs/heads/${targetBranch}`;
});
// Push all target branches with a single command if we don't run in dry-run mode.
// We don't want to push them individually as that could cause an unnecessary slow-down.
this.git.run(['push', this.git.repoGitUrl, ...pushRefspecs]);
}
}

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