Compare commits

...

114 Commits

Author SHA1 Message Date
Victor Berchet 7c3b95b4ab docs: add changelog for 5.2.6 2018-02-21 16:45:10 -08:00
Victor Berchet e4e8a68c06 release: cut the 5.2.6 release 2018-02-21 16:45:09 -08:00
Victor Berchet 6460ac0add Revert "feat(platform-browser): fix #19604, can config hammerOptions (#21979)"
This reverts commit fdbfd21bcd.
2018-02-21 16:45:09 -08:00
Trotyl ee91de9d5a feat(core): support metadata reflection for native class types (#22356)
closes #21731

PR Close #22356
2018-02-21 16:09:27 -08:00
Trotyl 5ec38f2f47 fix(core): properly handle function without prototype in reflector (#22284)
closes #19978

PR Close #22284
2018-02-21 14:52:05 -08:00
Marco Alvarado 612cfeca14 docs(aio): updates directive event hooks real capabilities (#16654)
Minor documentation update to include event hooks that were assumed to only work on components.

Closes angular/angular#10221

PR Close #16654
2018-02-21 14:51:05 -08:00
zhilich dfdade25ea docs(aio): Wrong code example. Form status field was added later in the guide. (#21275)
PR Close #21275
2018-02-21 11:06:48 -08:00
Ricardo M c2f78e1ca3 docs(http): fix a typo in code comment (#22327)
PR Close #22327
2018-02-21 11:06:06 -08:00
Victor Berchet 484802cd2a build: make git revert messages valid (#22339)
`git revert` default message is "Revert <original message>" (no semi-colon)

PR Close #22339
2018-02-21 11:05:35 -08:00
Kapunahele Wong ee535777bb docs: add ngStyle to cheat sheet (#22070)
PR Close #22070
2018-02-20 16:08:16 -08:00
Ro Savage 94756eb4bd docs(aio): fix incorrect quote mark usage (#22335)
PR Close #22335
2018-02-20 15:42:55 -08:00
Oussama Ben Brahim 23b0707707 docs(aio): fix the css of the heroes component's buttons (#22333)
Fixes #22222

PR Close #22333
2018-02-20 15:41:57 -08:00
Pascal Zwick a2cb0109f1 docs(aio): Fix name of component (#22332)
PR Close #22332
2018-02-20 15:41:35 -08:00
Stephen Fluin d20a08bc48 docs(aio): update installed mobile tool list (#22331)
PR Close #22331
2018-02-20 15:41:15 -08:00
Alex Eagle 5bdb3acace build: update tsickle dep from compiler-cli (#22295)
PR Close #22295
2018-02-20 15:40:45 -08:00
Alex Eagle c5418c7abe fix(compiler-cli): add missing entry point to package, update tsickle (#22295)
PR Close #22295
2018-02-20 15:40:45 -08:00
Jeff Cross 09b4612bdd docs(aio): add Nx and Angular Enterprise Playbook to resources (#22321)
PR Close #22321
2018-02-20 10:09:34 -08:00
Frederik Prijck a346d28df6 test(language-service): fix minor typos (#21372)
PR Close #21372
2018-02-20 10:08:55 -08:00
Victor Berchet bf07837d5d test(common): fix ngIf tests
The failing test was ported for the master branch which ignores whitespaces
2018-02-18 20:12:47 -08:00
Victor Berchet cdfedc1e49 fix(common): fix merge error in ng_if.ts 2018-02-18 19:51:15 -08:00
Victor Berchet af6a0563de fix(common): then and else template might be set to null (#22298)
PR Close #22298
2018-02-18 19:28:36 -08:00
Johannes Hoppe c726d1d6d3 docs(aio): add angular-buch to resources (#22163)
adds a link to the website of our book. second version of the text. thanks!

PR Close #22163
2018-02-18 15:12:14 -08:00
Johannes Hoppe 2030846df7 docs(aio): add angular-buch to resources (#22163)
adds a link to the website of our book. many thanks for reviewing this

PR Close #22163
2018-02-18 15:12:14 -08:00
Derk Snijders - i3anaan 9dae97c5d9 docs: correct grammar mistakes in CONTRIBUTING.md (#22285)
Various grammar mistakes were present in the contribution guidelines
This commit corrects some of them

PR Close #22285
2018-02-18 13:27:24 -08:00
George Kalpakas 228eb9feef fix(aio): improve announcement-bar layout with wide logos (#22272)
PR Close #22272
2018-02-18 13:16:30 -08:00
Joe Eames debf01d7a6 docs(aio): added ngconf announcement (#22272)
PR Close #22272
2018-02-18 13:16:30 -08:00
Victor Berchet 51abe69b60 fix: merge-pr script (#22290)
PR Close #22290
2018-02-18 13:13:29 -08:00
Victor Berchet 396bc0d9e9 build: use authenticated mode for the merge script (#22269)
`TOKEN` is the name with use for other GH scripts

PR Close #22269
2018-02-18 13:01:51 -08:00
Olivier Combe 861250b4e2 docs: fix changelog errors (#22228)
PR Close #22228
2018-02-16 18:03:04 -08:00
Fabian Wiles 81c1e0a3c3 docs: replace plnkr with StackBlitz (#20365)
PR Close #20365
2018-02-16 15:12:10 -08:00
Trotyl Yu ce5e8fad9e fix(common): correct mapping of Observable methods (#20518)
fixes #20516
PR Close #20518
2018-02-16 15:10:31 -08:00
Alex Eagle 185a6ab562 build: add esm5 build (#22258)
This is a partial cherry-pick of 370ab66c4f
which included this along with a new feature for ivy.

PR Close #22258
2018-02-16 14:49:24 -08:00
George Kalpakas 6b457843b9 test(aio): increase docs-test timeouts to prevent flakes on Travis (#22261)
PR Close #22261
2018-02-16 14:46:23 -08:00
Alex Eagle 5f52ea3d06 feat(bazel): ng_module produces bundle index (#22176)
It creates the bundle index .d.ts and .metadata.json files.
The names are based on the ng_module target.

PR Close #22176
2018-02-15 14:17:16 -08:00
Alex Eagle 6c1e7ac40e feat(bazel): introduce a binary stamping feature (#22176)
This grabs version control metadata and makes it available in the build, eg. to put in the version field for released artifacts

PR Close #22176
2018-02-15 14:08:54 -08:00
Alex Eagle 6597616aac refactor(bazel): convert most ts_library to ng_module (#22176)
This is necessary so we can produce ng metadata for our packages that are published as libraries

PR Close #22176
2018-02-15 14:08:54 -08:00
Victor Berchet 6a57264d38 Revert: "build: allow bazel build ... (#22168)"
This reverts commit 3237f1dbfc.
2018-02-15 14:07:41 -08:00
Stefanie Fluin 1e3e0fad49 fix(aio): improve printing styles (#19651)
printfix

PR Close #19651
2018-02-14 18:49:59 -05:00
Adrien Boullé 0c88d5dedd style: fix typos boostrap to bootstrap (#21917)
PR Close #21917
2018-02-14 18:21:52 -05:00
Matias Niemelä 0b8b06ee8b build: comment-out chromium version checking code temporarily (#22232)
Related #22231

PR Close #22232
2018-02-14 17:26:43 -05:00
Pat edd6cd4e29 docs: typo - components should be possessive (#22172)
PR Close #22172
2018-02-14 15:06:52 -05:00
JiaLi.Passion fdbfd21bcd feat(platform-browser): fix #19604, can config hammerOptions (#21979)
PR Close #21979
2018-02-14 15:02:59 -05:00
Kapunahele Wong 0a5283da1a docs(aio): fix extraneous divs (#22069)
PR Close #22069
2018-02-14 15:02:36 -05:00
Alex Eagle 3237f1dbfc build: allow bazel build ... (#22168)
Note, the reason this commit removes `firebase-tools` is:

1) firebase-tools has an optional dependency on
https://www.npmjs.com/package/@google-cloud/functions-emulator
2) yarn's `--ignore-optional` doesn't work for transitive deps, so
there's no way to yarn install without getting that functions-emulator
package
3) functions-emulator has a transitive dep on `grpc`
4) the version of `grpc` we get has `BUILD` files and no `WORKSPACE`
file so it always breaks `bazel build ...`

It could be solved by any of:
1) remove firebase-tools - this is what I did
2) fix yarn so you can omit optional deps of a transitive dep
3) make functions-emulator depend transitively on a more recent `grpc`
version
4) patch `grpc` after install by doing an `rm` command in our
postinstall or something

In its place we must install protobufjs. This is needed by the
ngc-wrapped test, which needs jasmine as well as bazel's worker mode
dependencies, and therefore cannot simply rely on
node_modules =
"@build_bazel_rules_typescript_tsc_wrapped_deps//:node_modules"

PR Close #22168
2018-02-14 15:01:42 -05:00
Misko Hevery 106b435297 docs: add changelog for 5.2.5 2018-02-13 21:07:53 -08:00
Misko Hevery ca69dfde31 release: cut the 5.2.5 release 2018-02-13 21:06:54 -08:00
Trotyl 1b8ea1005a fix(compiler): make unary plus operator consistent to JavaScript (#22154)
fixes #22089

PR Close #22154
2018-02-13 13:04:30 -08:00
adasilva da6ab91095 fix(platform-browser): support 0/false/null values in transfer_state (#22179)
Issue #22178

PR Close #22179
2018-02-13 11:28:22 -08:00
guillaumebarranco c4f841f031 fix(core): add stacktrace in log when error during cleanup component in TestBed (#22162)
PR Close #22162
2018-02-13 11:28:08 -08:00
Olivier Combe e576b69d10 ci: update ngbot config file (#22173)
Fixes #22053
PR Close #22173
2018-02-13 10:26:06 -08:00
Alex Eagle f938774748 ci: remove conditional clause for bazel install (#22170)
No longer needed since we don't have a bazel job
PR Close #22170
2018-02-13 10:25:51 -08:00
Alex Eagle c0b78067c4 ci: remove bazel job from Travis (#22170)
This saves us an executor on Travis.

Note that we still do a bazel build on travis when we run the integration tests under e2e_2.

We expect that CircleCI is the only place we'll ever consume bazel-built artifacts.

PR Close #22170
2018-02-13 10:25:51 -08:00
Alex Eagle 2931018b84 ci: enable bazel remote caching on CircleCI (#21784)
This should cause Bazel builds to be incremental, only re-building parts of Angular affected by changes since the last build.
It also fixes a potential version skew, where CI was running the Bazel linter binaries in the ngcontainer docker image, but developers built them using the versions in WORKSPACE

PR Close #21784
2018-02-13 10:14:22 -08:00
Oussama Ben Brahim e97afae67a docs(aio): fix typo in "preserveWhitespaces" example (#22182)
Fixes #22147

PR Close #22182
2018-02-12 15:57:42 -08:00
Chuck Jazdzewski c6bdc831c1 fix(common): weaken AsyncPipe transform signature (#22169)
The AsyncPipe type signature was changed to allow
deferred creation of promises and observalbes that
is supported by the implementation by allowing
`Promise<T>|null|undefined` and by allowing
`Observable<T>|null|undefined`.

PR Close #22169
2018-02-12 15:57:29 -08:00
mgmarlow 1a897e4f48 docs(aio): add angular-playground to resources (#22042)
PR Close #22042
2018-02-12 14:30:58 -08:00
Jason Aden 603e50d3bf refactor(router): move activation to private method (#22144)
PR Close #22144
2018-02-12 10:41:18 -08:00
Johannes Hoppe ca5b72461c docs(aio): add angular.schule to resources (#22164)
adds a link to our website. many thanks for reviewing this

PR Close #22164
2018-02-12 10:01:23 -08:00
Suguru Inatomi a616dd6c17 fix(aio): remove broken span closing tag (#22146)
PR Close #22146
2018-02-12 10:01:10 -08:00
yerkebulan 0ed64af5bc docs(aio): put structural directives back in the nav (#21856)
PR Close #21856
2018-02-12 10:00:14 -08:00
Alex Eagle d57fd0b84f fix(bazel): allow TS to read ambient typings (#21876)
Same fix as https://github.com/bazelbuild/rules_typescript/commit/e70d7a2a7c08d7031f4dd4a385a7a6aa27296948
This is because the CompilerOptions needs to have directoryExists undefined in order to get the google3 behavior,
so we have to set the property outside the constructor.

Fixes #21872

PR Close #21876
2018-02-09 17:16:25 -08:00
Miško Hevery 9b280eef63 build: merge-pr new checks that all requested changes have been addressed (#21817)
PR Close #21817
2018-02-09 17:14:17 -08:00
Pete Bacon Darwin d4a9db2af5 fix(aio): do not rewrite /styleguide URL in Service Worker (#22104)
This URL needs to be redirected via the server, so
we must exclude it from being rewitten.

Closes #22078

PR Close #22104
2018-02-09 13:10:48 -08:00
Pete Bacon Darwin 2740b69023 docs(aio): update docs changelog with links to ts-to-js guide (#21763) (#22104)
PR Close #21763

PR Close #22104
2018-02-09 13:10:48 -08:00
Pete Bacon Darwin 15ff7ba880 fix(aio): update Firebase redirects and SW routes (#21763) (#22104)
Closes #21377

PR Close #21763

PR Close #22104
2018-02-09 13:10:48 -08:00
Pete Bacon Darwin 615bb95138 build(aio): test Service Worker "routing" configuration (#21763) (#22104)
PR Close #21763

PR Close #22104
2018-02-09 13:10:48 -08:00
Pete Bacon Darwin 2b67400652 build(aio): test Firebase hosting redirection configuration (#21763) (#22104)
PR Close #21763

PR Close #22104
2018-02-09 13:10:48 -08:00
Mikhail Burshteyn ac815f7281 docs: fix typo in http.md (#22058)
PR Close #22058
2018-02-09 13:10:24 -08:00
Pete Bacon Darwin 854f0ff33a feat(aio): enable data driven homepage announcements (#22043)
PR Close #22043
2018-02-09 13:10:12 -08:00
Trotyl c55523466f docs(aio): several fix for ngmodule guides (#21517)
PR Close #21517
2018-02-09 13:03:47 -08:00
Miško Hevery f80b9da18a Revert "docs(common): add HttpParamsOptions to the public API (#20332)"
This reverts commit de561f36e1.
2018-02-08 14:38:13 -08:00
Alex Eagle c5ec8d952a fix(bazel): improve error message for missing assets (#22096)
fixes #22095

PR Close #22096
2018-02-08 10:01:27 -08:00
George Kalpakas de561f36e1 docs(common): add HttpParamsOptions to the public API (#20332)
Fixes #20276

PR Close #20332
2018-02-08 09:44:37 -08:00
Peter Bacon Darwin 47b71d98ae fix(core): use appropriate inert document strategy for Firefox & Safari (#17019)
Both Firefox and Safari are vulnerable to XSS if we use an inert document
created via `document.implementation.createHTMLDocument()`.

Now we check for those vulnerabilities and then use a DOMParser or XHR
strategy if needed.

Further the platform-server has its own library for parsing HTML, so we
sniff for that (by checking whether DOMParser exists) and fall back to
the standard strategy.

Thanks to @cure53 for the heads up on this issue.

PR Close #17019
2018-02-08 08:55:15 -08:00
Pete Bacon Darwin 22d548f4ed docs(aio): remove ngATL banner from homepage (#22060)
Closes #22029

PR Close #22060
2018-02-07 16:10:18 -08:00
Kapunahele Wong 799edd1e2f docs(aio): remove lifecycle hooks img (#21425)
PR Close #21425
2018-02-07 16:09:44 -08:00
Jesus Rodriguez ffcb5dd264 build(aio): update examples to CLI to 1.6.5 (#21222)
PR Close #21222
2018-02-07 16:09:27 -08:00
Pete Bacon Darwin 75897d4977 feat(aio): report logger.error calls to Google Analytics (#22011)
We have a number of observables that have `catch` handlers to recover
from errors without causing the stream to close, and breaking the app.
We also have some `try ... catch` blocks for synchronous code for a
similar reason.

In these cases we conventionally then call `logger.error` in the catch
handler. We are interested in these errors so we are going to capture them
by reporting them to Google Analytics via the new `ReportingErrorHandler`.

PR Close #22011
2018-02-07 12:09:38 -08:00
Pete Bacon Darwin eb0da530a7 feat(aio): report application errors to Google Analytics (#22011)
This is a basic implementation of error logging using the limited
facilities provided by Google Analytics.

Errors within the Angular app itself will be handled by a new
`ReportingErrorHandler` service, which overrides and extends the
built-in `ErrorHandler`.

Further, errors outside the app, which arrive at `window.onerror`
will also be reported to Google Analytics.

Closes #21943

PR Close #22011
2018-02-07 12:09:38 -08:00
Chuck Jazdzewski 0b23573573 fix(language-service): correct instructions to install the language service (#22000)
Fixes: #21956

PR Close #22000
2018-02-07 12:09:00 -08:00
Emilio 47b73fd153 fix(core): ensure initial value of QueryList length (#21980) (#21982)
Set initial value of `length` to `0`.

Fixes regression introduced by https://github.com/angular/angular/commit/e54474215629aa6a0e0497fe61bfc896cea532c9#diff-a85dbe0991a7577ea24b49374e9ae90b where the `length` property ceased to have initial value.

Closes #21980

PR Close #21982
2018-02-07 12:08:44 -08:00
Olivier Combe 07769e5caa test(common): disable deprecated date pipe tests on chrome mobile (#21933)
Closes #21907
PR Close #21933
2018-02-07 12:07:31 -08:00
Oussama Ben Brahim 9fdb804b63 test(forms): update test name with correct wording (#21833)
Use the term primitive value instead of standalone

Fixes #21831

PR Close #21833
2018-02-07 12:07:15 -08:00
George Kalpakas 9d02db3254 fix(aio): ignore .header-link when selecting the heading text (#21695)
Implemented @maxkorz's
[suggestion](https://github.com/angular/angular/issues/21515#issuecomment-357453634).

Fixes #21515

PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas 920b0df32a fix(aio): prevent heading misplacement while styles load (#21695)
During the initial load of the page (probably until the icon styles are
loaded and/or applied), the `.header-link` element is wider, pushing the
heading text slightly to the right (for a brief moment).

This commit prevents this slight shift by explicitly setting the width
for the `.header-link` element.

PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas b313976ac1 fix(aio): ensure header-links are visible at <600px (#21695)
PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas 2d19e7bbea refactor(aio): simplify .header-link styles (#21695)
PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas c3c92d7796 fix(aio): reduce flicker and reflows for initial rendering (#21695)
For the initial rendering, where there is no transition from a previous
visual state to a new one, animations make little sense. The page should
load with as few reflows as possible.
Similarly, while we typically want to defer updating the SideNav state
(e.g. opened/closed) until the "leaving" document is animated out of the
page, on the initial rendering (where there is no "leaving" document)
this leads to the SideNav flashing (from closed to open).

These worked as expected before, but several parts (mostly related to
documents with a SideNav) have been accidentally broken in recent
commits (e.g. when upgraded to latest material, or enabled animations
for DocViewer transitions, etc.).

This commit restores the previous behavior by ensuring that (on the
initial rendering) the SideNav state is updated as soon as possible and
that there will be no animations when:

1. The hamburger button appears.
2. The SideNav is opened.
3. The main section's width is adjusted to make room for the SideNav.

PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas a887c9339f refactor(aio): preserve HttpClient asynchronicity in tests (#21695)
Previously, the mocked `HttpClient` was synchronous in tests (despite
the actual `HttpClient` being asynchronous). Although we use observables
(which generally make the implementation sync/async-agnostic), the fact
that we have no control over when Angular updates/checks views and calls
lifecycle hooks resulted in different behavior (and errors) in tests
(with sync `HttpClient`) vs actual app (with async `HttpClient`).

This commit ensures that the behavior (and errors) are consistent
between the tests and the actual app by making the mocked `HttpClient`
asynchronous.

PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas 89051a0452 fix(aio): remove links from sub-menu toggles (#21695)
Navigating to a document while trying to expand or collapse a sub-menu
is undesirable and confusing. All sub-menu toggles should have no other
effect than expanding/collapsing the corresponding sub-menu.

PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas 97dafa8460 docs(animations): fix typo (disbled --> disabled) (#21695)
PR Close #21695
2018-02-07 12:06:45 -08:00
Kevin Fahy 56b9591746 fix(forms): prevent event emission on enable/disable when emitEvent is false (#12366) (#21018)
Previously, the emitEvent flag was only checked when emitting on the current control.
Thus, if  the control was part of a hierarchy, events were emitted on the parent and the childrens.
This fixes the issue by properly passing the emitEvent flag to both parent and childrens.

Fixes #12366

PR Close #21018
2018-02-07 12:05:26 -08:00
Pete Bacon Darwin b0ddb5ad0e build(aio): blacklist unwanted URLs from the generated sitemap.xml (#22061)
Closes #22017

PR Close #22061
2018-02-07 12:02:01 -08:00
jhenderson2099 103727aadf docs(aio): fix TOH inclusion of HeroesService. (#21228)
Change docs where the MessageService is referenced

Fixes #20398

PR Close #21228
2018-02-07 12:01:32 -08:00
Aric Thorn 44ea80b797 docs(forms): Custom Validator example selector name incorrect. (#20464)
Added bobby e2e test for template form.

Fixes: #20206

PR Close #20464
2018-02-07 12:01:12 -08:00
Aric Thorn 41046e4a90 docs(forms): Custom Validator example selector name incorrect. (#20464)
Name of selector in ForbiddenName example is not consistent with Validator class nor Html selector example. Added the selector name 'appForbiddenName' as an alias name for the input of the Validator class, and updated the view accordingly.

Fixes: #20206

PR Close #20464
2018-02-07 12:01:12 -08:00
Alex Rickabaugh 530b824faa docs: add changelog for 5.2.4 2018-02-07 10:19:39 -08:00
Alex Rickabaugh e22d3a605c release: cut the 5.2.4 release 2018-02-07 10:16:42 -08:00
Martin Sikora c6645e7a04 fix(core): fix proper propagation of subscriptions in EventEmitter (#22016)
Closes #21999

PR Close #22016
2018-02-06 07:56:34 -08:00
A7xSV f0396f1e54 docs(aio): fix swap value (#20905)
'http.get' has been swapped in for 'of'

PR Close #20905
2018-02-05 13:05:58 -08:00
Judy Bogart adb1d62967 docs: clarify npm/yarn commands, add blank lines to mix md/html in table (#21606)
PR Close #21606
2018-02-05 13:02:14 -08:00
Judy Bogart cfe83939a4 docs: update browser support (#21606)
PR Close #21606
2018-02-05 13:02:14 -08:00
Miško Hevery 973607fe9d ci: mark PRs with rejection as not green (#21922)
PR Close #21922
2018-02-05 13:01:12 -08:00
Pete Bacon Darwin 664f7fa477 build(aio): add API static members to search index (#21988)
Previously searching for `compose` did not include `Validators`
in the search results because we were not including all the
`static` members of API docs in the index.

PR Close #21988
2018-02-05 13:00:47 -08:00
Olivier Combe b155ae116b ci: add config for g3 status (#21996)
Ref #21642
PR Close #21996
2018-02-05 12:59:59 -08:00
JiaLi.Passion ce51ea93a1 fix(core): fix #20582, don't need to wrap zone in location change listener (#22007)
PR Close #22007
2018-02-05 12:59:05 -08:00
Pete Bacon Darwin d38e08812e feat(aio): dynamically, pre-emptively, add noindex (#21992)
These tags are removed when the doc is ready and valid, but this will
allow us to block indexing in the case that the Angular app fails to
bootstrap or load the document for some non-404 reason.

This should get around the problem with hardcoded tags. See
https://github.com/angular/angular/commit/c3fb820473d64036ef0dd3d4c004cc7fbc67be75

Closes #21941

PR Close #21992
2018-02-05 12:58:27 -08:00
JiaLi.Passion aa9ba7f9fe fix(core): should check Zone existance when scheduleMicroTask (#20656)
PR Close #20656
2018-02-02 07:53:55 -08:00
Jannis Lehmann 102d06b974 docs: consistency fix in describing a custom tag (#21747)
PR Close #21747
2018-02-02 07:53:18 -08:00
JiaLi.Passion 11ec80a053 docs: add docs for IE (#21824)
PR Close #21824
2018-02-02 07:51:47 -08:00
musicq 75eecdc351 docs: add missing underline (#21892)
PR Close #21892
2018-02-02 07:49:33 -08:00
Pete Bacon Darwin 965eecc587 build(aio): move zip and live-example generation to yarn predocs task (#21970)
This will prevent the confusing errors for first time users who
try to generate the docs with `yarn docs` and are told there are
dangling links.

Closes #21944

PR Close #21970
2018-02-02 07:48:42 -08:00
Alex Rickabaugh c4fb696189 fix(common): don't convert null to a string when flushing a mock request (#21417)
A bug in TestRequest caused null response bodies to be stringified. This
change causes null to be treated faithfully.

Fixes #20744

PR Close #21417
2018-02-01 08:32:44 -08:00
yerkebulan 72df747dd6 docs(aio): add missing closing <code-examle> tag (#21771)
PR Close #21771
2018-02-01 08:31:21 -08:00
215 changed files with 4032 additions and 940 deletions
+25
View File
@@ -0,0 +1,25 @@
# These options are enabled when running on CI
# We do this by copying this file to /etc/bazel.bazelrc at the start of the build.
# See remote cache documentation in /docs/BAZEL.md
# Don't be spammy in the logs
build --noshow_progress
# Don't run manual tests
test --test_tag_filters=-manual
# Enable experimental CircleCI bazel remote cache proxy
# See remote cache documentation in /docs/BAZEL.md
build --experimental_remote_spawn_cache --remote_rest_cache=http://localhost:7643
# Prevent unstable environment variables from tainting cache keys
build --experimental_strict_action_env
# Workaround https://github.com/bazelbuild/bazel/issues/3645
# Bazel doesn't calculate the memory ceiling correctly when running under Docker.
# Limit Bazel to consuming resources that fit in CircleCI "medium" class which is the default:
# https://circleci.com/docs/2.0/configuration-reference/#resource_class
build --local_resources=3072,2.0,1.0
# Retry in the event of flakes, eg. https://circleci.com/gh/angular/angular/31309
test --flaky_test_attempts=2
+22 -8
View File
@@ -15,6 +15,13 @@
var_1: &docker_image angular/ngcontainer:0.1.0
var_2: &cache_key angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.1.0
# See remote cache documentation in /docs/BAZEL.md
var_3: &setup-bazel-remote-cache
run:
name: Start up bazel remote cache proxy
command: ~/bazel-remote-proxy -backend circleci://
background: true
# Settings common to each job
anchor_1: &job_defaults
working_directory: ~/ng
@@ -34,14 +41,16 @@ jobs:
steps:
- checkout:
<<: *post_checkout
# Check BUILD.bazel formatting before we have a node_modules directory
# Then we don't need any exclude pattern to avoid checking those files
- run: 'buildifier -mode=check $(find . -type f \( -name BUILD.bazel -or -name BUILD \)) ||
(echo "BUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)'
# Run the skylark linter to check our Bazel rules
- run: 'find . -type f -name "*.bzl" |
xargs java -jar /usr/local/bin/Skylint_deploy.jar ||
# See remote cache documentation in /docs/BAZEL.md
- run: .circleci/setup_cache.sh
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
- *setup-bazel-remote-cache
- run: 'yarn buildifier -mode=check ||
(echo -e "\nBUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)'
- run: 'yarn skylint ||
(echo -e "\n.bzl files have lint errors. Please run ''yarn skylint''"; exit 1)'
- restore_cache:
key: *cache_key
@@ -54,6 +63,11 @@ jobs:
steps:
- checkout:
<<: *post_checkout
# See remote cache documentation in /docs/BAZEL.md
- run: .circleci/setup_cache.sh
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
- *setup-bazel-remote-cache
- restore_cache:
key: *cache_key
@@ -62,7 +76,7 @@ jobs:
# Use bazel query so that we explicitly ask for all buildable targets to be built as well
# This avoids waiting for a build command to finish before running the first test
# See https://github.com/bazelbuild/bazel/issues/4257
- run: bazel query --output=label '//modules/... union //packages/... union //tools/...' | xargs bazel test --config=ci
- run: bazel query --output=label '//modules/... union //packages/... union //tools/...' | xargs bazel test
- save_cache:
key: *cache_key
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
# Install bazel remote cache proxy
# This is temporary until the feature is no longer experimental on CircleCI.
# See remote cache documentation in /docs/BAZEL.md
set -u -e
readonly DOWNLOAD_URL="https://5-116431813-gh.circle-artifacts.com/0/pkg/bazel-remote-proxy-$(uname -s)_$(uname -m)"
curl --fail -o ~/bazel-remote-proxy "$DOWNLOAD_URL"
chmod +x ~/bazel-remote-proxy
+1 -1
View File
@@ -25,7 +25,7 @@ ISSUES MISSING IMPORTANT INFORMATION MAY BE CLOSED WITHOUT INVESTIGATION.
## Minimal reproduction of the problem with instructions
<!--
For bug reports please provide the *STEPS TO REPRODUCE* and if possible a *MINIMAL DEMO* of the problem via
https://plnkr.co or similar (you can use this template as a starting point: http://plnkr.co/edit/tpl:AvJOMERrnz94ekVua0u5).
https://stackblitz.com or similar (you can use this template as a starting point: https://stackblitz.com/fork/angular-gitter).
-->
## What is the motivation / use case for changing the behavior?
+37 -5
View File
@@ -13,6 +13,30 @@ merge:
# text to show when some checks are failing
failureText: "The following checks are failing:"
# the g3 status will be added to your pull requests if they include files that match the patterns
g3Status:
# set to true to disable
disabled: false
# the name of the status
context: "google3"
# text to show when the status is pending
pendingDesc: "Googler: test this change in google3 http://go/angular-g3sync"
# text to show when the status is success
successDesc: "Does not affect google3"
# list of patterns to check for the files changed by the PR
# this list must be manually kept in sync with google3/third_party/javascript/angular2/copy.bara.sky
include:
- "BUILD.bazel"
- "LICENSE"
- "WORKSPACE"
- "modules/**"
- "packages/**"
# list of patterns to ignore for the files changed by the PR
exclude:
- "packages/language-service/**"
- "**/.gitignore"
- "**/.gitkeep"
# comment that will be added to a PR when there is a conflict, leave empty or set to false to disable
mergeConflictComment: "Hi @{{PRAuthor}}! This PR has merge conflicts due to recent upstream merges.
\nPlease help to unblock it by resolving these conflicts. Thanks!"
@@ -26,13 +50,15 @@ merge:
noConflict: true
# list of labels that a PR needs to have, checked with a regexp (e.g. "PR target:" will work for the label "PR target: master")
requiredLabels:
- "PR target:"
- "PR target: *"
- "cla: yes"
# list of labels that a PR shouldn't have, checked after the required labels with a regexp
forbiddenLabels:
- "PR target: TBD"
- "PR action: cleanup"
- "PR action: review"
- "PR state: blocked"
- "cla: no"
# list of PR statuses that need to be successful
@@ -60,9 +86,15 @@ triage:
triagedLabels:
-
- "type: bug"
- "severity"
- "freq"
- "comp:"
- "severity*"
- "freq*"
- "comp: *"
-
- "type: feature"
- "comp:"
- "comp: *"
-
- "type: refactor"
- "comp: *"
-
- "type: RFC / Discussion / question"
- "comp: *"
+1
View File
@@ -44,6 +44,7 @@ groups:
all:
users: all
required: 1
rejection_value: -999
# In this group, your self-approval does not count
author_approval:
auto: false
-1
View File
@@ -56,7 +56,6 @@ env:
- CI_MODE=aio
- CI_MODE=aio_e2e AIO_SHARD=0
- CI_MODE=aio_e2e AIO_SHARD=1
- CI_MODE=bazel
matrix:
fast_finish: true
+1
View File
@@ -32,6 +32,7 @@ filegroup(
"reflect-metadata",
"source-map-support",
"minimist",
"tslib",
] for ext in [
"*.js",
"*.json",
+44
View File
@@ -1,3 +1,47 @@
<a name="5.2.6"></a>
## [5.2.6](https://github.com/angular/angular/compare/5.2.5...5.2.6) (2018-02-22)
### Bug Fixes
* **common:** correct mapping of Observable methods ([#20518](https://github.com/angular/angular/issues/20518)) ([ce5e8fa](https://github.com/angular/angular/commit/ce5e8fa)), closes [#20516](https://github.com/angular/angular/issues/20516)
* **common:** then and else template might be set to null ([#22298](https://github.com/angular/angular/issues/22298)) ([af6a056](https://github.com/angular/angular/commit/af6a056))
* **compiler-cli:** add missing entry point to package, update tsickle ([#22295](https://github.com/angular/angular/issues/22295)) ([c5418c7](https://github.com/angular/angular/commit/c5418c7))
* **core:** properly handle function without prototype in reflector ([#22284](https://github.com/angular/angular/issues/22284)) ([5ec38f2](https://github.com/angular/angular/commit/5ec38f2)), closes [#19978](https://github.com/angular/angular/issues/19978)
* **core:** support metadata reflection for native class types ([#22356](https://github.com/angular/angular/issues/22356)) ([ee91de9](https://github.com/angular/angular/commit/ee91de9)), closes [#21731](https://github.com/angular/angular/issues/21731)
<a name="5.2.5"></a>
## [5.2.5](https://github.com/angular/angular/compare/5.2.4...5.2.5) (2018-02-14)
### Bug Fixes
* **aio:** update Firebase redirects and SW routes ([#21763](https://github.com/angular/angular/pull/21763)) ([#22104](https://github.com/angular/angular/pull/22104)) ([15ff7ba](https://github.com/angular/angular/commit/15ff7ba)), closes [#21377](https://github.com/angular/angular/issues/21377)
* **bazel:** allow TS to read ambient typings ([#21876](https://github.com/angular/angular/issues/21876)) ([d57fd0b](https://github.com/angular/angular/commit/d57fd0b)), closes [#21872](https://github.com/angular/angular/issues/21872)
* **bazel:** improve error message for missing assets ([#22096](https://github.com/angular/angular/issues/22096)) ([c5ec8d9](https://github.com/angular/angular/commit/c5ec8d9)), closes [#22095](https://github.com/angular/angular/issues/22095)
* **common:** weaken AsyncPipe transform signature ([#22169](https://github.com/angular/angular/issues/22169)) ([c6bdc83](https://github.com/angular/angular/commit/c6bdc83))
* **compiler:** make unary plus operator consistent to JavaScript ([#22154](https://github.com/angular/angular/issues/22154)) ([1b8ea10](https://github.com/angular/angular/commit/1b8ea10)), closes [#22089](https://github.com/angular/angular/issues/22089)
* **core:** add stacktrace in log when error during cleanup component in TestBed ([#22162](https://github.com/angular/angular/issues/22162)) ([c4f841f](https://github.com/angular/angular/commit/c4f841f))
* **core:** ensure initial value of QueryList length ([#21980](https://github.com/angular/angular/issues/21980)) ([#21982](https://github.com/angular/angular/issues/21982)) ([47b73fd](https://github.com/angular/angular/commit/47b73fd)), closes [#21980](https://github.com/angular/angular/issues/21980)
* **core:** use appropriate inert document strategy for Firefox & Safari ([#17019](https://github.com/angular/angular/issues/17019)) ([47b71d9](https://github.com/angular/angular/commit/47b71d9))
* **forms:** prevent event emission on enable/disable when emitEvent is false ([#12366](https://github.com/angular/angular/issues/12366)) ([#21018](https://github.com/angular/angular/issues/21018)) ([56b9591](https://github.com/angular/angular/commit/56b9591))
* **language-service:** correct instructions to install the language service ([#22000](https://github.com/angular/angular/issues/22000)) ([0b23573](https://github.com/angular/angular/commit/0b23573))
* **platform-browser:** support 0/false/null values in transfer_state ([#22179](https://github.com/angular/angular/issues/22179)) ([da6ab91](https://github.com/angular/angular/commit/da6ab91))
<a name="5.2.4"></a>
## [5.2.4](https://github.com/angular/angular/compare/5.2.3...5.2.4) (2018-02-07)
### Bug Fixes
* **common:** don't convert null to a string when flushing a mock request ([#21417](https://github.com/angular/angular/issues/21417)) ([c4fb696](https://github.com/angular/angular/commit/c4fb696)), closes [#20744](https://github.com/angular/angular/issues/20744)
* **core:** fix [#20582](https://github.com/angular/angular/issues/20582), don't need to wrap zone in location change listener ([#22007](https://github.com/angular/angular/issues/22007)) ([ce51ea9](https://github.com/angular/angular/commit/ce51ea9))
* **core:** fix proper propagation of subscriptions in EventEmitter ([#22016](https://github.com/angular/angular/issues/22016)) ([c6645e7](https://github.com/angular/angular/commit/c6645e7)), closes [#21999](https://github.com/angular/angular/issues/21999)
* **core:** should check Zone existance when scheduleMicroTask ([#20656](https://github.com/angular/angular/issues/20656)) ([aa9ba7f](https://github.com/angular/angular/commit/aa9ba7f))
<a name="5.2.3"></a>
## [5.2.3](https://github.com/angular/angular/compare/5.2.2...5.2.3) (2018-01-31)
+8 -8
View File
@@ -51,7 +51,7 @@ and help you to craft the change so that it is successfully accepted into the pr
Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs we will systematically ask you to provide a minimal reproduction scenario using http://plnkr.co. Having a live, reproducible scenario gives us wealth of important information without going back & forth to you with additional questions like:
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs, we will systematically ask you to provide a minimal reproduction scenario using http://plnkr.co. Having a live, reproducible scenario gives us a wealth of important information without going back & forth to you with additional questions like:
- version of Angular used
- 3rd-party libraries and their versions
@@ -61,7 +61,7 @@ A minimal reproduce scenario using http://plnkr.co/ allows us to quickly confirm
We will be insisting on a minimal reproduce scenario in order to save maintainers time and ultimately be able to fix more bugs. Interestingly, from our experience users often find coding problems themselves while preparing a minimal plunk. We understand that sometimes it might be hard to extract essentials bits of code from a larger code-base but we really need to isolate the problem before we can fix it.
Unfortunately we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that don't have enough info to be reproduced.
Unfortunately, we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that doesn't have enough info to be reproduced.
You can file new issues by filling out our [new issue form](https://github.com/angular/angular/issues/new).
@@ -173,12 +173,12 @@ The **header** is mandatory and the **scope** of the header is optional.
Any line of the commit message cannot be longer 100 characters! This allows the message to be easier
to read on GitHub as well as in various git tools.
Footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
The footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
Samples: (even more [samples](https://github.com/angular/angular/commits/master))
```
docs(changelog): update change log to beta.5
docs(changelog): update changelog to beta.5
```
```
fix(release): need to depend on latest rxjs and zone.js
@@ -203,7 +203,7 @@ Must be one of the following:
* **test**: Adding missing tests or correcting existing tests
### Scope
The scope should be the name of the npm package affected (as perceived by person reading changelog generated from commit messages.
The scope should be the name of the npm package affected (as perceived by the person reading the changelog generated from commit messages.
The following is the list of supported scopes:
@@ -232,10 +232,10 @@ There are currently a few exceptions to the "use package name" rule:
* none/empty string: useful for `style`, `test` and `refactor` changes that are done across all packages (e.g. `style: add missing semicolons`)
### Subject
The subject contains succinct description of the change:
The subject contains a succinct description of the change:
* use the imperative, present tense: "change" not "changed" nor "changes"
* don't capitalize first letter
* don't capitalize the first letter
* no dot (.) at the end
### Body
@@ -266,7 +266,7 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise
* https://help.github.com/articles/setting-your-commit-email-address-in-git/
* https://stackoverflow.com/questions/37245303/what-does-usera-committed-with-userb-13-days-ago-on-github-mean
* https://help.github.com/articles/about-commit-email-addresses/
* https://help.github.com/articles/blocking-command-line-pushes-that-expose-your-personal-email-address/
* https://help.github.com/articles/blocking-command-line-pushes-that-expose-your-personal-email-address/
Note that if you have more than one Git identity, it is important to verify that you are logged in with the same ID with which you signed the CLA, before you commit changes. If not, your PR will fail the CLA check.
+13 -7
View File
@@ -1,11 +1,14 @@
workspace(name = "angular")
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
# Using a pre-release snapshot to pick up a commit that makes all nodejs_binary
# programs produce source-mapped stack traces.
RULES_NODEJS_VERSION = "926349cea4cd360afcd5647ccdd09d2d2fb471aa"
git_repository(
http_archive(
name = "build_bazel_rules_nodejs",
remote = "https://github.com/bazelbuild/rules_nodejs.git",
commit = "230d39a391226f51c03448f91eb61370e2e58c42",
url = "https://github.com/bazelbuild/rules_nodejs/archive/%s.zip" % RULES_NODEJS_VERSION,
strip_prefix = "rules_nodejs-%s" % RULES_NODEJS_VERSION,
sha256 = "5ba3c8c209078c2e3f0c6aa4abd01a1a561f92a5bfda04e25604af5f4734d69d",
)
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories")
@@ -13,10 +16,13 @@ load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_reposi
check_bazel_version("0.9.0")
node_repositories(package_json = ["//:package.json"])
git_repository(
RULES_TYPESCRIPT_VERSION = "0.10.1"
http_archive(
name = "build_bazel_rules_typescript",
remote = "https://github.com/bazelbuild/rules_typescript.git",
commit = "eb3244363e1cb265c84e723b347926f28c29aa35"
url = "https://github.com/bazelbuild/rules_typescript/archive/%s.zip" % RULES_TYPESCRIPT_VERSION,
strip_prefix = "rules_typescript-%s" % RULES_TYPESCRIPT_VERSION,
sha256 = "a2c81776a4a492ff9f878f9705639f5647bef345f7f3e1da09c9eeb8dec80485",
)
load("@build_bazel_rules_typescript//:defs.bzl", "ts_setup_workspace")
+1 -2
View File
@@ -105,8 +105,7 @@ The general setup is as follows:
* Open a terminal, ensure the dependencies are installed; run an initial doc generation; then start the doc-viewer:
```bash
yarn
yarn docs
yarn setup
yarn start
```
@@ -15,6 +15,7 @@ describe('Form Validation Tests', function () {
});
tests('Template-Driven Form');
bobTests();
});
describe('Reactive form', () => {
@@ -20,7 +20,7 @@ export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
// #enddocregion directive-providers
})
export class ForbiddenValidatorDirective implements Validator {
@Input() forbiddenName: string;
@Input('appForbiddenName') forbiddenName: string;
validate(control: AbstractControl): {[key: string]: any} {
return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)
@@ -12,7 +12,7 @@
<!-- #docregion name-with-error-msg -->
<!-- #docregion name-input -->
<input id="name" name="name" class="form-control"
required minlength="4" forbiddenName="bob"
required minlength="4" appForbiddenName="bob"
[(ngModel)]="hero.name" #name="ngModel" >
<!-- #enddocregion name-input -->
@@ -21,7 +21,14 @@ import { MessagesComponent } from './messages/messages.component';
FormsModule
],
// #docregion providers
providers: [ HeroService, MessageService ],
// #docregion providers-heroservice
providers: [
HeroService,
// #enddocregion providers-heroservice
MessageService
// #docregion providers-heroservice
],
// #enddocregion providers-heroservice
// #enddocregion providers
bootstrap: [ AppComponent ]
})
@@ -3,6 +3,6 @@
<h2>Messages</h2>
<button class="clear"
(click)="messageService.clear()">clear</button>
<div *ngFor='let message of messageService.messages'> {{message}} </div>
<div *ngFor="let message of messageService.messages"> {{message}} </div>
</div>
@@ -190,6 +190,29 @@ describe('Tutorial part 6', () => {
const maxId = heroesBefore[heroesBefore.length - 1].id;
expect(heroesAfter[numHeroes]).toEqual({id: maxId + 1, name: newHeroName});
});
it('displays correctly styled buttons', async () => {
element.all(by.buttonText('x')).then(buttons => {
for (const button of buttons) {
// Inherited styles from styles.css
expect(button.getCssValue('font-family')).toBe('Arial');
expect(button.getCssValue('border')).toContain('none');
expect(button.getCssValue('padding')).toBe('5px 10px');
expect(button.getCssValue('border-radius')).toBe('4px');
// Styles defined in heroes.component.css
expect(button.getCssValue('left')).toBe('194px');
expect(button.getCssValue('top')).toBe('-32px');
}
});
const addButton = element(by.buttonText('add'));
// Inherited styles from styles.css
expect(addButton.getCssValue('font-family')).toBe('Arial');
expect(addButton.getCssValue('border')).toContain('none');
expect(addButton.getCssValue('padding')).toBe('5px 10px');
expect(addButton.getCssValue('border-radius')).toBe('4px');
});
});
describe('Progressive hero search', () => {
@@ -51,7 +51,7 @@
}
/* #docregion additions */
.button {
button {
background-color: #eee;
border: none;
padding: 5px 10px;
+1 -1
View File
@@ -1254,7 +1254,7 @@ also encapsulate a style sheet within a specific component.
### Styles configuration
<code-example hideCopy path="ajs-quick-reference/.angular-cli.1.json" region="styles" linenums="false">
<code-example hideCopy path="ajs-quick-reference/.angular-cli.1.json" region="styles" linenums="false"></code-example>
With the Angular CLI, you can configure your global styles in the `.angular-cli.json` file.
You can rename the extension to `.scss` to use sass.
+1 -1
View File
@@ -92,7 +92,7 @@ You can control your app compilation by providing template compiler options in t
},
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"preserveWhiteSpaces": false,
"preserveWhitespaces": false,
...
}
}
+5 -5
View File
@@ -186,9 +186,9 @@ template for our `HeroListComponent`:
<code-example path="architecture/src/app/hero-list.component.html" title="src/app/hero-list.component.html"></code-example>
Although this template uses typical HTML elements like `<h2>` and `<p>`, it also has some differences. Code like `*ngFor`, `{{hero.name}}`, `(click)`, `[hero]`, and `<hero-detail>` uses Angular's [template syntax](guide/template-syntax).
Although this template uses typical HTML elements like `<h2>` and `<p>`, it also has some differences. Code like `*ngFor`, `{{hero.name}}`, `(click)`, `[hero]`, and `<app-hero-detail>` uses Angular's [template syntax](guide/template-syntax).
In the last line of the template, the `<hero-detail>` tag is a custom element that represents a new component, `HeroDetailComponent`.
In the last line of the template, the `<app-hero-detail>` tag is a custom element that represents a new component, `HeroDetailComponent`.
The `HeroDetailComponent` is a *different* component than the `HeroListComponent` you've been reviewing.
The `HeroDetailComponent` (code not shown) presents facts about a particular hero, the
@@ -197,7 +197,7 @@ The `HeroDetailComponent` is a **child** of the `HeroListComponent`.
<img src="generated/images/guide/architecture/component-tree.png" alt="Metadata" class="left">
Notice how `<hero-detail>` rests comfortably among native HTML elements. Custom components mix seamlessly with native HTML in the same layouts.
Notice how `<app-hero-detail>` rests comfortably among native HTML elements. Custom components mix seamlessly with native HTML in the same layouts.
<hr class="clear"/>
@@ -230,8 +230,8 @@ information Angular needs to create and present the component and its view.
Here are a few of the most useful `@Component` configuration options:
* `selector`: CSS selector that tells Angular to create and insert an instance of this component
where it finds a `<hero-list>` tag in *parent* HTML.
For example, if an app's HTML contains `<hero-list></hero-list>`, then
where it finds a `<app-hero-list>` tag in *parent* HTML.
For example, if an app's HTML contains `<app-hero-list></app-hero-list>`, then
Angular inserts an instance of the `HeroListComponent` view between those tags.
* `templateUrl`: module-relative address of this component's HTML template, shown [above](guide/architecture#templates).
+119 -179
View File
@@ -2,188 +2,96 @@
Angular supports most recent browsers. This includes the following specific versions:
<table>
<tr>
<th>
<th>
Browser
</th>
<th>
Supported versions
</th>
</tr>
<tr>
<td>
Chrome
</th>
</td>
<th>
<td>
latest
</td>
</tr>
<tr>
<td>
Firefox
</th>
</td>
<th>
<td>
latest
</td>
</tr>
<tr>
<td>
Edge
</th>
</td>
<th>
<td>
2 most recent major versions
</td>
</tr>
<tr>
<td>
IE
</th>
<th>
Safari
</th>
<th>
iOS
</th>
<th>
Android
</th>
<th>
</td>
<td>
11<br>10<br>9
</td>
</tr>
<tr>
<tr>
<td>
IE Mobile
</th>
</tr>
<tr>
<td>
latest
</td>
<td>
latest
</td>
<td>
14
</td>
<td>
11
</td>
<td>
10
</td>
<td>
10
</td>
<td>
Nougat (7.0)<br>Marshmallow (6.0)
</td>
<td>
11
</td>
</tr>
<tr>
<td>
Safari
</td>
<td>
2 most recent major versions
</td>
</tr>
<tr>
<td>
iOS
</td>
<td>
2 most recent major versions
</td>
<td>
13
</td>
<td>
10
</td>
<td>
9
</td>
<td>
9
</td>
<td>
Lollipop<br>(5.0, 5.1)
</td>
<td>
</td>
</tr>
</tr>
<tr>
<td>
Android
</td>
<td>
Nougat (7.0)<br>Marshmallow (6.0)<br>Lollipop (5.0, 5.1)<br>KitKat (4.4)
</td>
<td>
</td>
<td>
9
</td>
<td>
8
</td>
<td>
8
</td>
<td>
KitKat<br>(4.4)
</td>
<td>
</td>
</tr>
<tr>
<td>
</td>
<td>
</td>
<td>
</td>
<td>
</td>
<td>
7
</td>
<td>
7
</td>
<td>
Jelly Bean<br>(4.1, 4.2, 4.3)
</td>
<td>
</td>
</tr>
</tr>
</table>
@@ -219,21 +127,16 @@ the CLI created with your project.
This file incorporates the mandatory and many of the optional polyfills as JavaScript `import` statements.
The npm packages for the _mandatory_ polyfills (such as `zone.js`) were installed automatically for you when you created your project and
their corresponding `import` statements are ready to go.
You probably won't touch these.
The npm packages for the _mandatory_ polyfills (such as `zone.js`) were installed automatically for you when you created your project and their corresponding `import` statements are ready to go. You probably won't touch these.
But if you need an optional polyfill, you'll have to install its npm package with `npm` or `yarn`.
For example, [if you need the web animations polyfill](http://caniuse.com/#feat=web-animation),
you could install it with either of the following commands:
But if you need an optional polyfill, you'll have to install its npm package.
For example, [if you need the web animations polyfill](http://caniuse.com/#feat=web-animation), you could install it with `npm`, using the following command (or the `yarn` equivalent):
<code-example language="sh" class="code-shell">
npm install --save web-animations-js
yarn add web-animations-js
</code-example>
Then open the `polyfills.ts` file and un-comment the corresponding `import` statement
as in the following example:
Then open the `polyfills.ts` file and un-comment the corresponding `import` statement as in the following example:
<code-example title="src/polyfills.ts">
/**
@@ -243,7 +146,7 @@ as in the following example:
import 'web-animations-js'; // Run `npm install --save web-animations-js`.
</code-example>
If you can't find the polyfill you want in `polyfills.ts`,
If you can't find the polyfill you want in `polyfills.ts`,
add it yourself, following the same pattern:
1. install the npm package
@@ -283,6 +186,7 @@ These are the polyfills required to run an Angular application on each supported
<td>
[ES7/reflect](guide/browser-support#core-es7-reflect) (JIT only)
</td>
</tr>
@@ -295,8 +199,8 @@ These are the polyfills required to run an Angular application on each supported
<td>
[ES6](guide/browser-support#core-es6)
</td>
</tr>
@@ -309,7 +213,6 @@ These are the polyfills required to run an Angular application on each supported
<td>
[ES6<br>classList](guide/browser-support#classlist)
</td>
@@ -323,8 +226,7 @@ These are the polyfills required to run an Angular application on each supported
Some features of Angular may require additional polyfills.
For example, the animations library relies on the standard web animation API, which is only available in Chrome and Firefox today.
You'll need a polyfill to use animations in other browsers.
For example, the animations library relies on the standard web animation API, which is only available in Chrome and Firefox today. You'll need a polyfill to use animations in other browsers.
Here are the features which may require additional polyfills:
@@ -351,19 +253,20 @@ Here are the features which may require additional polyfills:
<td>
[JIT compilation](guide/aot-compiler).
[JIT compilation](guide/aot-compiler).
Required to reflect for metadata.
</td>
<td>
[ES7/reflect](guide/browser-support#core-es7-reflect)
</td>
<td>
All current browsers.
Enabled by default.
Can remove If you always use AOT and only use Angular decorators.
All current browsers. Enabled by default.
Can remove if you always use AOT and only use Angular decorators.
</td>
</tr>
@@ -373,12 +276,13 @@ Here are the features which may require additional polyfills:
<td>
[Animations](guide/animations)
</td>
<td>
[Web Animations](guide/browser-support#web-animations)
</td>
<td>
@@ -391,13 +295,23 @@ Here are the features which may require additional polyfills:
<td>
If you use the following deprecated i18n pipes: [date](api/common/DeprecatedDatePipe), [currency](api/common/DeprecatedCurrencyPipe), [decimal](api/common/DeprecatedDecimalPipe) and [percent](api/common/DeprecatedPercentPipe)
If you use the following deprecated i18n pipes:
[date](api/common/DeprecatedDatePipe),
[currency](api/common/DeprecatedCurrencyPipe),
[decimal](api/common/DeprecatedDecimalPipe),
[percent](api/common/DeprecatedPercentPipe)
</td>
<td>
[Intl API](guide/browser-support#intl)
</td>
<td>
@@ -410,13 +324,15 @@ Here are the features which may require additional polyfills:
<td>
[NgClass](api/common/NgClass) on SVG elements
[NgClass](api/common/NgClass)
on SVG elements
</td>
<td>
[classList](guide/browser-support#classlist)
</td>
<td>
@@ -429,14 +345,19 @@ Here are the features which may require additional polyfills:
<td>
[Http](guide/http) when sending and receiving binary data
[Http](guide/http)
when sending and receiving binary data
</td>
<td>
[Typed&nbsp;Array](guide/browser-support#typedarray)<br>
[Blob](guide/browser-support#blob)<br>
[FormData](guide/browser-support#formdata)
</td>
<td>
@@ -476,6 +397,7 @@ Below are the polyfills which are used to test the framework itself. They are a
<td>
<a id='core-es7-reflect' href="https://github.com/zloirock/core-js/blob/master/es7/reflect.js">ES7/reflect</a>
</td>
<td>
@@ -491,7 +413,9 @@ Below are the polyfills which are used to test the framework itself. They are a
<tr>
<td>
<a id='core-es6' href="https://github.com/zloirock/core-js">ES6</a>
</td>
<td>
@@ -507,7 +431,9 @@ Below are the polyfills which are used to test the framework itself. They are a
<tr>
<td>
<a id='classlist' href="https://github.com/eligrey/classList.js">classList</a>
</td>
<td>
@@ -523,7 +449,9 @@ Below are the polyfills which are used to test the framework itself. They are a
<tr>
<td>
<a id='intl' href="https://github.com/andyearnshaw/Intl.js">Intl</a>
</td>
<td>
@@ -539,7 +467,9 @@ Below are the polyfills which are used to test the framework itself. They are a
<tr>
<td>
<a id='web-animations' href="https://github.com/web-animations/web-animations-js">Web Animations</a>
</td>
<td>
@@ -555,7 +485,9 @@ Below are the polyfills which are used to test the framework itself. They are a
<tr>
<td>
<a id='typedarray' href="https://github.com/inexorabletash/polyfill/blob/master/typedarray.js">Typed Array</a>
</td>
<td>
@@ -571,7 +503,9 @@ Below are the polyfills which are used to test the framework itself. They are a
<tr>
<td>
<a id='blob' href="https://github.com/eligrey/Blob.js">Blob</a>
</td>
<td>
@@ -587,7 +521,9 @@ Below are the polyfills which are used to test the framework itself. They are a
<tr>
<td>
<a id='formdata' href="https://github.com/francois2metz/html5-formdata">FormData</a>
</td>
<td>
@@ -603,14 +539,13 @@ Below are the polyfills which are used to test the framework itself. They are a
</table>
\* Figures are for minified and gzipped code,
computed with the <a href="http://closure-compiler.appspot.com/home">closure compiler</a>.
{@a non-cli}
## Polyfills for non-CLI users
If you aren't using the CLI, you should add your polyfill scripts directly to the host web page (`index.html`), perhaps like this.
If you are not using the CLI, you should add your polyfill scripts directly to the host web page (`index.html`), perhaps like this.
<code-example title="src/index.html">
&lt;!-- pre-zone polyfills -->
@@ -625,8 +560,13 @@ If you aren't using the CLI, you should add your polyfill scripts directly to th
// __Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
// __Zone_disable_on_property = true; // disable patch onProperty such as onclick
// __zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
&lt;/script>
/*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*/
// __Zone_enable_cross_context_check = true;
&lt;/script>
&lt;!-- zone.js required by Angular -->
&lt;script src="node_modules/zone.js/dist/zone.js">&lt;/script>
+4 -2
View File
@@ -120,11 +120,13 @@ The documentation for the version prior to v.2.2.0 has been removed.
## ES6 described in "TypeScript to JavaScript" (2016-11-14)
The updated TypeScript to JavaScript guide (removed August 2017, PR #18694)
explains how to write apps in ES6/7
The updated TypeScript to JavaScript guide explains how to write apps in ES6/7
by translating the common idioms in the TypeScript documentation examples
(and elsewhere on the web) to ES6/7 and ES5.
This was [removed in August 2017](https://github.com/angular/angular/pull/18694) but can still be
viewed in the [v2 documentation](https://v2.angular.io/docs/ts/latest/cookbook/ts-to-js.html).
## Sync with Angular v.2.1.1 (2016-10-21)
Docs and code samples updated and tested with Angular v.2.1.1.
+5
View File
@@ -140,6 +140,11 @@ is available to <code>declarations</code> of this module.</p>
<td><p>Binds the presence of CSS classes on the element to the truthiness of the associated map values. The right-hand expression should return {class-name: true/false} map.</p>
</td>
</tr>
<tr>
<td><code>&lt;div <b>[ngStyle]</b>="{'property': 'value'}"&gt;</code><br><code>&lt;div <b>[ngStyle]</b>="dynamicStyles()"&gt;</code></td>
<td><p>Allows you to assign styles to an HTML element using CSS. You can use CSS directly, as in the first example, or you can call a method from the component.</p>
</td>
</tr>
</tbody></table>
<table class="is-full-width is-fixed-layout">
+1 -1
View File
@@ -101,7 +101,7 @@ Begin the title with the markdown `#` character. Alternatively, you can write th
**Only one title (`<h1>`) per document!**
Title text should be in "Title Case", which means that you use capital letters to start the first words and all _principal_ words. Use lower case letters for _secondary words such as "in", "of", and "the".
Title text should be in "Title Case", which means that you use capital letters to start the first words and all _principal_ words. Use lower case letters for _secondary_ words such as "in", "of", and "the".
```html
# The Meat of the Matter
+1 -1
View File
@@ -97,7 +97,7 @@ In fact, many libraries declare and export components you'll never use.
For example, a material design library will export all components because it doesn’t know which ones you will use. However, it is unlikely that you will use them all.
For the ones you don't reference, the tree shaker drops these components from the final code package.
If a component isn't an _entry component_ or isn't found in a template,
If a component isn't an _entry component_ and isn't found in a template,
the tree shaker will throw it away. So, it's best to add only the components that are truly entry components to help keep your app
as trim as possible.
+1 -1
View File
@@ -98,7 +98,7 @@ When the CLI generated the `CustomerDashboardComponent` for the feature module,
</code-example>
To see this HTML in the `AppComponent`, you first have to export the `CustomerDashboardComponent` in the `CustomerDashboardModule`. In `customer-dashboard.module.ts`, just beneath the declarations array, add an exports array containing `CustomerDashboardModule`:
To see this HTML in the `AppComponent`, you first have to export the `CustomerDashboardComponent` in the `CustomerDashboardModule`. In `customer-dashboard.module.ts`, just beneath the `declarations` array, add an `exports` array containing `CustomerDashboardModule`:
<code-example path="feature-modules/src/app/customer-dashboard/customer-dashboard.module.ts" region="component-exports" title="src/app/customer-dashboard/customer-dashboard.module.ts" linenums="false">
</code-example>
+1 -1
View File
@@ -171,7 +171,7 @@ comes together:
<code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="directive" title="shared/forbidden-name.directive.ts (directive)">
</code-example>
Once the `ForbiddenValidatorDirective` is ready, you can simply add its selector, `forbiddenName`, to any input element to activate it. For example:
Once the `ForbiddenValidatorDirective` is ready, you can simply 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" title="template/hero-form-template.component.html (forbidden-name-input)" linenums="false">
+1 -8
View File
@@ -512,10 +512,6 @@ You rarely access Angular feature modules directly. You usually import them from
## NgModule
<div class="l-sub-section">
Helps you organize an application into cohesive blocks of functionality.
An NgModule identifies the components, directives, and pipes that the application uses along with the list of external NgModules that the application needs, such as `FormsModule`.
@@ -526,7 +522,7 @@ For details and examples, see [NgModules](guide/ngmodules) and the
related files in that section.
</div>
{@a O}
@@ -631,13 +627,10 @@ For more information, see the [Routing & Navigation](guide/router) page.
## Router module
<div class="l-sub-section">
A separate [NgModule](guide/glossary#ngmodule) that provides the necessary service providers and directives for navigating through application views.
For more information, see the [Routing & Navigation](guide/router) page.
</div>
## Routing component
+1 -1
View File
@@ -358,7 +358,7 @@ subscribes without a callback.
The bare `.subscribe()` _seems_ pointless.
In fact, it is essential.
Merely calling `HeroService.addHero()` **does not initiate the DELETE request.**
Merely calling `HeroService.deleteHero()` **does not initiate the DELETE request.**
<code-example
path="http/src/app/heroes/heroes.component.ts"
+1 -1
View File
@@ -57,7 +57,7 @@ You can also use the VS Quick Open (⌘+P) to search for the extension. When you
enter the following command:
```sh
ext install ng-template
ext install Angular.ng-template
```
Then click the install button to install the Angular Language Service.
+6 -16
View File
@@ -1,7 +1,5 @@
# Lifecycle Hooks
<img src="generated/images/guide/lifecycle-hooks/hooks-in-sequence.png" alt="Us" class="left">
A component has a lifecycle managed by Angular.
Angular creates it, renders it, creates and renders its children,
@@ -10,7 +8,7 @@ checks it when its data-bound properties change, and destroys it before removing
Angular offers **lifecycle hooks**
that provide visibility into these key life moments and the ability to act when they occur.
A directive has the same set of lifecycle hooks, minus the hooks that are specific to component content and views.
A directive has the same set of lifecycle hooks.
{@a hooks-overview}
@@ -27,7 +25,7 @@ that Angular calls shortly after creating the component:
<code-example path="lifecycle-hooks/src/app/peek-a-boo.component.ts" region="ngOnInit" title="peek-a-boo.component.ts (excerpt)" linenums="false"></code-example>
No directive or component will implement all of the lifecycle hooks and some of the hooks only make sense for components.
No directive or component will implement all of the lifecycle hooks.
Angular only calls a directive/component hook method *if it is defined*.
{@a hooks-purpose-timing}
@@ -88,12 +86,10 @@ 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.
Respond after Angular projects external content into the component's view / the view that a directive is in.
Called _once_ after the first `ngDoCheck()`.
_A component-only hook_.
</td>
</tr>
<tr style='vertical-align:top'>
@@ -102,12 +98,10 @@ calls the lifecycle hook methods in the following sequence at specific moments:
</td>
<td>
Respond after Angular checks the content projected into the component.
Respond after Angular checks the content projected into the directive/component.
Called after the `ngAfterContentInit()` and every subsequent `ngDoCheck()`.
_A component-only hook_.
</td>
</tr>
<tr style='vertical-align:top'>
@@ -116,12 +110,10 @@ 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.
Respond after Angular initializes the component's views and child views / the view that a directive is in.
Called _once_ after the first `ngAfterContentChecked()`.
_A component-only hook_.
</td>
</tr>
<tr style='vertical-align:top'>
@@ -130,12 +122,10 @@ 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.
Respond after Angular checks the component's views and child views / the view that a directive is in.
Called after the `ngAfterViewInit` and every subsequent `ngAfterContentChecked()`.
_A component-only hook_.
</td>
</tr>
<tr style='vertical-align:top'>
+2 -2
View File
@@ -27,7 +27,7 @@ JavaScript modules help you namespace, preventing accidental global variables.
## NgModules
<!-- KW-- perMisko: let's discuss. This does not answer the question why it is different. Also, last sentence is confusing.-->
NgModules are classes decorated with `@NgModule`. The `@NgModule` decorator’s `imports` array tells Angular what other NgModules the current module needs. The modules in the imports array are different than JavaScript modules because they are NgModules rather than regular JavaScript modules. Classes with an `@NgModule` decorator are by convention kept in their own files, but what makes them an `NgModule` isn’t being in their own file, like JavaScript modules; it’s the presence of `@NgModule` and its metadata.
NgModules are classes decorated with `@NgModule`. The `@NgModule` decorator’s `imports` array tells Angular what other NgModules the current module needs. The modules in the `imports` array are different than JavaScript modules because they are NgModules rather than regular JavaScript modules. Classes with an `@NgModule` decorator are by convention kept in their own files, but what makes them an `NgModule` isn’t being in their own file, like JavaScript modules; it’s the presence of `@NgModule` and its metadata.
The `AppModule` generated from the Angular CLI demonstrates both kinds of modules in action:
@@ -53,7 +53,7 @@ export class AppModule { }
```
The NgModule classes differ from JavaScript module classes in the following key ways:
The NgModule classes differ from JavaScript module in the following key ways:
* An NgModule bounds [declarable classes](guide/ngmodule-faq#q-declarable) only.
Declarables are the only classes that matter to the [Angular compiler](guide/ngmodule-faq#q-angular-compiler).
+1 -1
View File
@@ -45,7 +45,7 @@ NgModule metadata does the following:
* Declares which components, directives, and pipes belong to the module.
* Makes some of those components, directives, and pipes public so that other module's component templates can use them.
* Imports other modules with the components, directives, and pipes that components in the current module need.
* Provides services at the other application components can use.
* Provides services that the other application components can use.
Every Angular app has at least one module, the root module.
You [bootstrap](guide/bootstrapping) that module to launch the application.
+1 -1
View File
@@ -496,7 +496,7 @@ Remember that impure pipes are called every few milliseconds.
If you're not careful, this pipe will punish the server with requests.
In the following code, the pipe only calls the server when the request URL changes and it caches the server response.
The code uses the [Angular http](guide/http) client to retrieve data</span>:
The code uses the [Angular http](guide/http) client to retrieve data:
<code-example path="pipes/src/app/fetch-json.pipe.ts" title="src/app/fetch-json.pipe.ts">
+3 -3
View File
@@ -10,7 +10,7 @@ see the <live-example></live-example>.
<hr>
## Create a service
You can provide services to your app by using the providers array in an NgModule.
You can provide services to your app by using the `providers` array in an NgModule.
Consider the default app generated by the CLI. In order to add a user service to it,
you can generate one by entering the following command in the terminal window:
@@ -20,7 +20,7 @@ ng generate service User
This creates a service called `UserService`. You now need to make the service available in your
app's injector. Update `app.module.ts` by importing it with your other import statements at the top
of the file and adding it to the providers array:
of the file and adding it to the `providers` array:
<code-example path="providers/src/app/app.module.ts" title="src/app/app.module.ts" linenums="false">
</code-example>
@@ -28,7 +28,7 @@ of the file and adding it to the providers array:
## Provider scope
When you add a service provider to the providers array of the root module, it’s available throughout the app. Additionally, when you import a module that has providers, those providers are also available to all the classes in the app as long they have the lookup token. For example, if you import the `HttpClientModule` into your `AppModule`, its providers are then available to the entire app and you can make HTTP requests from anywhere in your app.
When you add a service provider to the `providers` array of the root module, it’s available throughout the app. Additionally, when you import a module that has providers, those providers are also available to all the classes in the app as long they have the lookup token. For example, if you import the `HttpClientModule` into your `AppModule`, its providers are then available to the entire app and you can make HTTP requests from anywhere in your app.
## Limiting provider scope by lazy loading modules
+1 -1
View File
@@ -418,7 +418,7 @@ To see the form model, add the following line after the
closing `form` tag in the `hero-detail.component.html`:
<code-example path="reactive-forms/src/app/hero-detail/hero-detail-3.component.html" region="form-value-json" title="src/app/hero-detail/hero-detail.component.html" linenums="false">
<code-example path="reactive-forms/src/app/hero-detail/hero-detail-2.component.html" region="form-value-json" title="src/app/hero-detail/hero-detail.component.html" linenums="false">
</code-example>
+7
View File
@@ -350,3 +350,10 @@ It's the perfect place to reproduce a bug when you want to
<a href="https://github.com/angular/angular/issues/new" title="File an Angular issue">file an issue with Angular itself</a>.
For real development, we strongly recommend [developing locally](guide/setup#develop-locally).
## Appendix: develop locally with IE
If you develop angular locally with `ng serve`, there will be `websocket` connection being setup automatically between browser and local dev server, so when your code change, browser can automatically refresh.
In windows, by default one application can only have 6 websocket connections, <a href="https://msdn.microsoft.com/library/ee330736%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396#websocket_maxconn" title="MSDN WebSocket settings">MSDN WebSocket Settings</a>.
So if IE was refreshed manunally or automatically by `ng serve`, sometimes, the websocket will not close properly, when websocket connections exceed limitations, `SecurityError` will be thrown, this error will not affect the angular application, you can just restart IE to clear this error, or modify the windows registry to update the limitations.
@@ -135,3 +135,12 @@ QuickStart identifies two *typings*, or `d.ts`, files:
you can view an example in the [webpack](guide/webpack) page.
QuickStart doesn't require these typings but many of the samples do.
{@a target}
### *target*
By default, the target is `es5`, you can configure the target to `es6` if you only want to deploy the application to
es6 compatible browser. But if you configure the target to `es6` in some old browser such as `IE`, `Syntax Error` will be thrown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

+9
View File
@@ -0,0 +1,9 @@
[
{
"startDate": "2018-02-14",
"endDate": "2018-04-22",
"message": "Join us for ng-conf<br/>Apr 18th-20th, 2018",
"imageUrl": "generated/images/marketing/home/ng-conf.png",
"linkUrl": "http://ng-conf.org/"
}
]
+1 -1
View File
@@ -20,7 +20,7 @@
<div class="feature">
<div class="feature-title">Native</div>
<p class="text-body">Build native mobile apps with strategies from Ionic Framework, NativeScript, and React Native.</p>
<p class="text-body">Build native mobile apps with strategies from Cordova, Ionic, or NativeScript.</p>
</div>
<div class="feature">
+1 -8
View File
@@ -29,14 +29,7 @@
<div class="home-rows">
<!-- Announcement Bar -->
<div class="homepage-container">
<div class="announcement-bar">
<img src="generated/images/marketing/home/ng-atl.png">
<p>Join us in Atlanta for ngATL<br>Jan 30 - Feb 2, 2018</p>
<a class="button" href="http://ng-atl.org/">Learn More</a>
</div>
</div>
<aio-announcement-bar></aio-announcement-bar>
<!-- Group 1 -->
<div layout="row" layout-xs="column" class="home-row homepage-container">
+34
View File
@@ -241,6 +241,19 @@
"rev": true,
"title": "NinjaCodeGen - Angular CRUD Generator",
"url": "https://ninjaCodeGen.com"
},
"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"
}
}
},
@@ -438,6 +451,13 @@
"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/"
}
}
},
@@ -482,6 +502,13 @@
"title": "CodeSchool: Accelerating Through Angular",
"url": "https://www.codeschool.com/courses/accelerating-through-angular-2"
},
"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": "",
@@ -652,6 +679,13 @@
"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/"
}
}
}
+5 -2
View File
@@ -74,7 +74,6 @@
},
{
"url": "tutorial",
"title": "Tutorial",
"tooltip": "The Tour of Heroes tutorial takes you through the steps of creating an Angular application in TypeScript.",
"children": [
@@ -122,7 +121,6 @@
},
{
"url": "guide/architecture",
"title": "Fundamentals",
"tooltip": "The fundamentals of Angular",
"children": [
@@ -170,6 +168,11 @@
"title": "Attribute Directives",
"tooltip": "Attribute directives attach behavior to elements."
},
{
"url": "guide/structural-directives",
"title": "Structural Directives",
"tooltip": "Structural directives manipulate the layout of the page."
},
{
"url": "guide/pipes",
"title": "Pipes",
+1 -1
View File
@@ -29,7 +29,7 @@ and annotate the component class with `@Component`.
The CLI generated three metadata properties:
1. `selector`&mdash; the components CSS element selector
1. `selector`&mdash; the component's CSS element selector
1. `templateUrl`&mdash; the location of the component's template file.
1. `styleUrls`&mdash; the location of the component's private CSS styles.
+1 -1
View File
@@ -96,7 +96,7 @@ Bind the `HeroesComponent.selectedHero` to the element's `hero` property like th
`[hero]="selectedHero"` is an Angular [property binding](guide/template-syntax#property-binding).
It's a _one way_ data binding from
the `selectedHero` property of the `HeroComponent` to the `hero` property of the target element, which maps to the `hero` property of the `HeroDetailComponent`.
the `selectedHero` property of the `HeroesComponent` to the `hero` property of the target element, which maps to the `hero` property of the `HeroDetailComponent`.
Now when the user clicks a hero in the list, the `selectedHero` changes.
When the `selectedHero` changes, the _property binding_ updates `hero`
+11 -1
View File
@@ -97,7 +97,7 @@ Since you did not, you'll have to provide it yourself.
Open the `AppModule` class, import the `HeroService`, and add it to the `@NgModule.providers` array.
<code-example path="toh-pt4/src/app/app.module.ts" linenums="false" title="src/app/app.module.ts (providers)" region="providers">
<code-example path="toh-pt4/src/app/app.module.ts" linenums="false" title="src/app/app.module.ts (providers)" region="providers-heroservice">
</code-example>
The `providers` array tells Angular to create a single, shared instance of `HeroService`
@@ -105,6 +105,12 @@ and inject into any class that asks for it.
The `HeroService` is now ready to plug into the `HeroesComponent`.
<div class="alert is-important">
This is a interim code sample that will allow you to provide and use the `HeroService`. At this point, the code will differ from the `HeroService` in the ["final code review"](#final-code-review).
</div>
<div class="alert is-helpful">
Learn more about _providers_ in the [Providers](guide/providers) guide.
@@ -423,6 +429,10 @@ Here are the code files discussed on this page and your app should look like thi
path="toh-pt4/src/app/messages/messages.component.css">
</code-pane>
<code-pane title="src/app/app.module.ts"
path="toh-pt4/src/app/app.module.ts">
</code-pane>
<code-pane title="src/app/app.component.html"
path="toh-pt4/src/app/app.component.html">
</code-pane>
+1 -1
View File
@@ -135,7 +135,7 @@ Convert that method to use `HttpClient`
Refresh the browser. The hero data should successfully load from the
mock server.
You've swapped `http.get` for `of` and the app keeps working without any other changes
You've swapped `of` for `http.get` and the app keeps working without any other changes
because both functions return an `Observable<Hero[]>`.
### Http methods return one value
+57 -25
View File
@@ -1,4 +1,4 @@
import { browser, by, element } from 'protractor';
import { browser, by, element, ElementFinder } from 'protractor';
import { SitePage } from './app.po';
describe('site App', function() {
@@ -11,7 +11,7 @@ describe('site App', function() {
it('should show features text after clicking "Features"', () => {
page.navigateTo('');
page.getTopMenuLink('features').click();
page.click(page.getTopMenuLink('features'));
expect(page.getDocViewerText()).toMatch(/Progressive web apps/i);
});
@@ -19,28 +19,74 @@ describe('site App', function() {
page.navigateTo('');
expect(browser.getTitle()).toBe('Angular');
page.getTopMenuLink('features').click();
page.click(page.getTopMenuLink('features'));
expect(browser.getTitle()).toBe('Angular - FEATURES & BENEFITS');
page.homeLink.click();
page.click(page.homeLink);
expect(browser.getTitle()).toBe('Angular');
});
it('should not navigate when clicking on nav-item headings (sub-menu toggles)', () => {
// Show the sidenav.
page.navigateTo('docs');
expect(page.locationPath()).toBe('/docs');
// Get the top-level nav-item headings (sub-menu toggles).
const navItemHeadings = page.getNavItemHeadings(page.sidenav, 1);
// Test all headings (and sub-headings).
expect(navItemHeadings.count()).toBeGreaterThan(0);
navItemHeadings.each(heading => testNavItemHeading(heading!, 1));
// Helpers
function expectToBeCollapsed(element: ElementFinder) {
expect(element.getAttribute('class')).toMatch(/\bcollapsed\b/);
expect(element.getAttribute('class')).not.toMatch(/\bexpanded\b/);
}
function expectToBeExpanded(element: ElementFinder) {
expect(element.getAttribute('class')).not.toMatch(/\bcollapsed\b/);
expect(element.getAttribute('class')).toMatch(/\bexpanded\b/);
}
function testNavItemHeading(heading: ElementFinder, level: number) {
const children = page.getNavItemHeadingChildren(heading, level);
// Headings are initially collapsed.
expectToBeCollapsed(children);
// Ensure heading does not cause navigation when expanding.
page.click(heading);
expectToBeExpanded(children);
expect(page.locationPath()).toBe('/docs');
// Recursively test child-headings (while this heading is expanded).
const nextLevel = level + 1;
const childNavItemHeadings = page.getNavItemHeadings(children, nextLevel);
childNavItemHeadings.each(childHeading => testNavItemHeading(childHeading!, nextLevel));
// Ensure heading does not cause navigation when collapsing.
page.click(heading);
expectToBeCollapsed(children);
expect(page.locationPath()).toBe('/docs');
}
});
it('should show the tutorial index page at `/tutorial` after jitterbugging through features', () => {
// check that we can navigate directly to the tutorial page
page.navigateTo('tutorial');
expect(page.getDocViewerText()).toMatch(/Tutorial: Tour of Heroes/i);
// navigate to a different page
page.getTopMenuLink('features').click();
page.click(page.getTopMenuLink('features'));
expect(page.getDocViewerText()).toMatch(/Progressive web apps/i);
// Show the menu
page.docsMenuLink.click();
page.click(page.docsMenuLink);
// Tutorial folder should still be expanded because this test runs in wide mode
// Navigate to the tutorial introduction via a link in the sidenav
page.getNavItem(/introduction/i).click();
page.click(page.getNavItem(/introduction/i));
expect(page.getDocViewerText()).toMatch(/Tutorial: Tour of Heroes/i);
});
@@ -57,8 +103,7 @@ describe('site App', function() {
page.scrollToBottom();
expect(page.getScrollTop()).toBeGreaterThan(0);
page.getNavItem(/api/i).click();
browser.waitForAngular();
page.click(page.getNavItem(/api/i));
expect(page.locationPath()).toBe('/api');
expect(page.getScrollTop()).toBe(0);
});
@@ -69,8 +114,7 @@ describe('site App', function() {
page.scrollToBottom();
expect(page.getScrollTop()).toBeGreaterThan(0);
page.getNavItem(/security/i).click();
browser.waitForAngular();
page.click(page.getNavItem(/security/i));
expect(page.locationPath()).toBe('/guide/security');
expect(page.getScrollTop()).toBe(0);
});
@@ -102,7 +146,7 @@ describe('site App', function() {
it('should call ga with new URL on navigation', done => {
let path: string;
page.navigateTo('');
page.getTopMenuLink('features').click();
page.click(page.getTopMenuLink('features'));
page.locationPath()
.then(p => path = p)
.then(() => page.ga())
@@ -115,18 +159,6 @@ describe('site App', function() {
});
});
describe('search', () => {
it('should find pages when searching by a partial word in the title', () => {
page.navigateTo('');
page.enterSearch('ngCont');
expect(page.getSearchResults()).toContain('NgControl');
page.enterSearch('accessor');
expect(page.getSearchResults()).toContain('ControlValueAccessor');
});
});
describe('404 page', () => {
it('should add or remove the "noindex" meta tag depending upon the validity of the page', () => {
page.navigateTo('');
@@ -137,7 +169,7 @@ describe('site App', function() {
expect(element(by.css('meta[name="googlebot"][content="noindex"]')).isPresent()).toBeTruthy();
expect(element(by.css('meta[name="robots"][content="noindex"]')).isPresent()).toBeTruthy();
page.getTopMenuLink('features').click();
page.click(page.getTopMenuLink('features'));
expect(element(by.css('meta[name="googlebot"]')).isPresent()).toBeFalsy();
expect(element(by.css('meta[name="robots"]')).isPresent()).toBeFalsy();
});
+15
View File
@@ -7,6 +7,7 @@ export class SitePage {
links = element.all(by.css('md-toolbar a'));
homeLink = element(by.css('a.home'));
docsMenuLink = element(by.cssContainingText('aio-top-menu a', 'Docs'));
sidenav = element(by.css('mat-sidenav'));
docViewer = element(by.css('aio-doc-viewer'));
codeExample = element.all(by.css('aio-doc-viewer pre > code'));
ghLink = this.docViewer
@@ -24,7 +25,17 @@ export class SitePage {
.filter(element => element.getText().then(text => pattern.test(text)))
.first();
}
getNavItemHeadings(parent: ElementFinder, level: number) {
const targetSelector = `aio-nav-item .vertical-menu-item.heading.level-${level}`;
return parent.all(by.css(targetSelector));
}
getNavItemHeadingChildren(heading: ElementFinder, level: number) {
const targetSelector = `.heading-children.level-${level}`;
const script = `return arguments[0].parentNode.querySelector('${targetSelector}');`;
return element(() => browser.executeScript(script, heading));
}
getTopMenuLink(path) { return element(by.css(`aio-top-menu a[href="${path}"]`)); }
ga() { return browser.executeScript('return window["ga"].q') as promise.Promise<any[][]>; }
locationPath() { return browser.executeScript('return document.location.pathname') as promise.Promise<string>; }
@@ -53,6 +64,10 @@ export class SitePage {
return browser.executeScript('window.scrollTo(0, document.body.scrollHeight)');
}
click(element: ElementFinder) {
return element.click().then(() => browser.waitForAngular());
}
enterSearch(query: string) {
const input = element(by.css('.search-container input[type=search]'));
input.clear();
+28
View File
@@ -0,0 +1,28 @@
import { SitePage } from './app.po';
describe('site search', () => {
let page;
beforeEach(() => {
page = new SitePage();
page.navigateTo('');
});
it('should find pages when searching by a partial word in the title', () => {
page.enterSearch('ngCont');
expect(page.getSearchResults()).toContain('NgControl');
page.enterSearch('accessor');
expect(page.getSearchResults()).toContain('ControlValueAccessor');
});
it('should find API docs whose instance member name matches the search query', () => {
page.enterSearch('decode');
expect(page.getSearchResults()).toContain('HttpParameterCodec');
});
it('should find API docs whose static method name matches the search query', () => {
page.enterSearch('compose');
expect(page.getSearchResults()).toContain('Validators');
});
});
+88 -40
View File
@@ -12,50 +12,98 @@
// make sure the routing RegExp in `ngsw-manifest.json` is updated accordingly.
//////////////////////////////////////////////////////////////////////////////////////////////
// cli-quickstart.html, glossary.html, quickstart.html, server-communication.html, style-guide.html
{"type": 301, "source": "/docs/ts/latest/cli-quickstart.html", "destination": "/guide/quickstart"},
{"type": 301, "source": "/docs/ts/latest/glossary.html", "destination": "/guide/glossary"},
{"type": 301, "source": "/docs/ts/latest/quickstart.html", "destination": "/guide/quickstart"},
{"type": 301, "source": "/docs/ts/latest/guide/server-communication.html", "destination": "/guide/http"},
{"type": 301, "source": "/docs/ts/latest/guide/style-guide.html", "destination": "/guide/styleguide"},
// A random bad indexed page that used `api/api`
{"type": 301, "source": "/api/api/:rest*", "destination": "/api/:rest*"},
// guide/cli-quickstart, styleguide
// Guide renames
{"type": 301, "source": "/docs/*/latest/cli-quickstart.html", "destination": "/guide/quickstart"},
{"type": 301, "source": "/docs/*/latest/glossary.html", "destination": "/guide/glossary"},
{"type": 301, "source": "/docs/*/latest/quickstart.html", "destination": "/guide/quickstart"},
{"type": 301, "source": "/docs/*/latest/guide/server-communication.html", "destination": "/guide/http"},
{"type": 301, "source": "/docs/*/latest/guide/style-guide.html", "destination": "/guide/styleguide"},
{"type": 301, "source": "/guide/cli-quickstart", "destination": "/guide/quickstart"},
{"type": 301, "source": "/styleguide", "destination": "/guide/styleguide"},
// cookbook/a1-a2-quick-reference.html, cookbook/component-communication.html, cookbook/dependency-injection.html
{"type": 301, "source": "/docs/ts/latest/cookbook/a1-a2-quick-reference.html", "destination": "/guide/ajs-quick-reference"},
{"type": 301, "source": "/docs/ts/latest/cookbook/component-communication.html", "destination": "/guide/component-interaction"},
{"type": 301, "source": "/docs/ts/latest/cookbook/dependency-injection.html", "destination": "/guide/dependency-injection-in-action"},
// cookbook, cookbook/, cookbook/index.html
{"type": 301, "source": "/docs/ts/latest/cookbook", "destination": "/docs"},
{"type": 301, "source": "/docs/ts/latest/cookbook/", "destination": "/docs"},
{"type": 301, "source": "/docs/ts/latest/cookbook/index.html", "destination": "/docs"},
// cookbook/*.html
{"type": 301, "source": "/docs/ts/latest/cookbook/:cookbook.html", "destination": "/guide/:cookbook"},
// docs/ts/latest/api/<package>/index/*-<type>.html (+ special case for `NgFor` which has been renamed)
{"type": 301, "source": "/docs/ts/latest/api/common/index/NgFor-directive.html", "destination": "/api/common/NgForOf"},
{"type": 301, "source": "/docs/ts/latest/api/:package/index/:api-*.html", "destination": "/api/:package/:api"},
// docs/ts/latest
{"type": 301, "source": "/docs/ts/latest", "destination": "/docs"},
// guide/*, tutorial/*, **/*
{"type": 301, "source": "/docs/ts/latest/:any*", "destination": "/:any*"},
// aot-compiler.md and metadata.md combined into aot-compiler.md - issue #19510
{"type": 301, "source": "/guide/metadata", "destination": "/guide/aot-compiler"},
// ngmodule.md renamed to ngmodules.md
{"type": 301, "source": "/guide/ngmodule", "destination": "/guide/ngmodules"},
// service-worker-getstart.md, service-worker-comm.md, service-worker-configref.md
{"type": 301, "source": "/guide/service-worker-getstart", "destination": "/guide/service-worker-getting-started"},
{"type": 301, "source": "/guide/service-worker-comm", "destination": "/guide/service-worker-communications"},
{"type": 301, "source": "/guide/service-worker-configref", "destination": "/guide/service-worker-config"}
{"type": 301, "source": "/guide/service-worker-configref", "destination": "/guide/service-worker-config"},
// some top level guide pages on old site were moved below the guide folder
{"type": 301, "source": "/styleguide", "destination": "/guide/styleguide"},
{"type": 301, "source": "/docs/styleguide", "destination": "/guide/styleguide"},
// news is now blog
{"type": 301, "source": "/news*", "destination": "https://blog.angular.io/"},
// cookbook guides were moved (and sometime renamed or removed)
{"type": 301, "source": "/docs/*/latest/cookbook", "destination": "/docs"},
{"type": 301, "source": "/docs/*/latest/cookbook/", "destination": "/docs"},
{"type": 301, "source": "/docs/*/latest/cookbook/index.html", "destination": "/docs"},
{"type": 301, "source": "/**/cookbook/ts-to-js*", "destination": "https://github.com/angular/angular/blob/master/aio/content/guide/change-log.md#es6--described-in-typescript-to-javascript-2016-11-14"},
{"type": 301, "source": "/docs/*/latest/cookbook/a1-a2-quick-reference.html", "destination": "/guide/ajs-quick-reference"},
{"type": 301, "source": "/docs/*/latest/cookbook/component-communication.html", "destination": "/guide/component-interaction"},
{"type": 301, "source": "/docs/*/latest/cookbook/dependency-injection.html", "destination": "/guide/dependency-injection-in-action"},
{"type": 301, "source": "/docs/*/latest/cookbook/:cookbook.html", "destination": "/guide/:cookbook"},
// Forms related code was moved from the `common` to `forms` package (and NgFor was renamed to NgForOf)
{"type": 301, "source": "/**/NgFor-*", "destination": "/api/common/NgForOf"},
{"type": 301, "source": "/**/api/common/index/MaxLengthValidator-*", "destination": "/api/forms/MaxLengthValidator"},
{"type": 301, "source": "/**/api/common/ControlGroup*", "destination": "/api/forms/FormGroup"},
{"type": 301, "source": "/**/api/common/Control*", "destination": "/api/forms/FormControl"},
{"type": 301, "source": "/**/api/common/SelectControlValueAccessor-*", "destination": "/api/forms/SelectControlValueAccessor"},
{"type": 301, "source": "/**/api/common/NgModel", "destination": "/api/forms/NgModel"},
// Animations moves, renames and removals
{"type": 301, "source": "/api/animate/:rest*", "destination": "/api/animations/:rest*"},
// AnimationStateDeclarationMetadata was removed
{"type": 301, "source": "/**/AnimationStateDeclarationMetadata*", "destination": "/api/animations"},
// `AnimationDriver` was moved to the `animations/browser` package
{"type": 301, "source": "/api/platform-browser/AnimationDriver", "destination": "/api/animations/browser/AnimationDriver"},
// The `testing` package was renamed to `core/testing`
{"type": 301, "source": "/api/testing/:api-*", "destination": "/api/core/testing/:api"},
// CORE_DIRECTIVES & PLATFORM_PIPES were removed and are now in the CommonModule
{"type": 301, "source": "/**/CORE_DIRECTIVES*", "destination": "/api/common/CommonModule"},
{"type": 301, "source": "/**/PLATFORM_PIPES*", "destination": "/api/common/CommonModule"},
// DirectiveMetadata is now covered by the Directive decorator
{"type": 301, "source": "/**/DirectiveMetadata-*", "destination": "/api/core/Directive"},
// OptionalMetadata is now covered by the Optional decorator
{"type": 301, "source": "/**/OptionalMetadata-*", "destination": "/api/core/Optional"},
// HTTP_PROVIDERS was removed and is now provided in HttpModule
{"type": 301, "source": "/**/HTTP_PROVIDERS*", "destination": "/api/http/HttpModule"},
// URLs that use the old scheme of adding the type to the end (e.g. `SomeClass-class`)
{"type": 301, "source": "/api/:package/:api-*", "destination": "/api/:package/:api"},
{"type": 301, "source": "/api/:package/testing/index/:api-*", "destination": "/api/:package/testing/:api"},
{"type": 301, "source": "/api/:package/testing/:api-*", "destination": "/api/:package/testing/:api"},
{"type": 301, "source": "/api/upgrade/:package/index/:api-*", "destination": "/api/upgrade/:package/:api"},
{"type": 301, "source": "/api/upgrade/:package/:api-*", "destination": "/api/upgrade/:package/:api"},
// URLs that use the old scheme before we moved the docs to the angular/angular repo
{"type": 301, "source": "/docs/*/latest", "destination": "/docs"},
{"type": 301, "source": "/docs/*/latest/api/", "destination": "/api"},
{"type": 301, "source": "/docs/*/latest/api/:package", "destination": "/api/:package"},
{"type": 301, "source": "/docs/*/latest/api/testing/:api-*", "destination": "/api/core/testing/:api"},
{"type": 301, "source": "/docs/*/latest/api/:package/:api-*", "destination": "/api/:package/:api"},
{"type": 301, "source": "/docs/*/latest/api/:package/index/:api-*", "destination": "/api/:package/:api"},
{"type": 301, "source": "/docs/*/latest/api/:package/testing", "destination": "/api/:package/testing"},
{"type": 301, "source": "/docs/*/latest/api/:package/testing/index/:api-*", "destination": "/api/:package/testing/:api"},
{"type": 301, "source": "/docs/*/latest/api/platform-browser/animations/index/:api-*", "destination": "/api/platform-browser/animations/:api"},
{"type": 301, "source": "/docs/*/latest/api/upgrade/:package/:api-*", "destination": "/api/upgrade/:package/:api"},
{"type": 301, "source": "/docs/*/latest/api/upgrade/:package/index/:api-*", "destination": "/api/upgrade/:package/:api"},
{"type": 301, "source": "/docs/*/latest/glossary", "destination": "/guide/glossary"},
{"type": 301, "source": "/docs/*/latest/guide/", "destination": "/docs"},
{"type": 301, "source": "/docs/*/latest/guide/lifecycle-hooks", "destination": "/guide/lifecycle-hooks"},
{"type": 301, "source": "/docs/*/latest/:rest*", "destination": "/:rest*"},
{"type": 301, "source": "/docs/latest/:rest*", "destination": "/:rest*"},
{"type": 301, "source": "/docs/styleguide*", "destination": "/guide/styleguide"},
{"type": 301, "source": "/guide/metadata", "destination": "/guide/aot-compiler"},
{"type": 301, "source": "/guide/ngmodule", "destination": "/guide/ngmodules"},
{"type": 301, "source": "/guide/learning-angular*", "destination": "/guide/quickstart"},
{"type": 301, "source": "/testing", "destination": "/guide/testing"},
{"type": 301, "source": "/testing/**", "destination": "/guide/testing"}
],
"rewrites": [
{
+1 -1
View File
@@ -19,7 +19,7 @@
"routing": {
"index": "/index.html",
"routes": {
"^(?!/docs/.|(?:/guide/(?:cli-quickstart|metadata|ngmodule|service-worker-(?:getstart|comm|configref)|learning-angular)|/news/?)$|/testing|/api/(?:common/NgModel|platform-browser/AnimationDriver|testing|api)).*/(?!e?stackblitz|(?:NgFor|MaxLengthValidator)-|Control(?:Group)?|AnimationStateDeclarationMetadata|CORE_DIRECTIVES|PLATFORM_PIPES|DirectiveMetadata|HTTP_PROVIDERS)[^/.]*$": {
"^(?!/styleguide|/docs/.|(?:/guide/(?:cli-quickstart|metadata|ngmodule|service-worker-(?:getstart|comm|configref)|learning-angular)|/news)(?:\\.html|/)?$|/testing|/api/(?:.+/[^/]+-|platform-browser/AnimationDriver|testing|api/|(?:common/(?:NgModel|Control|MaxLengthValidator))|(?:[^/]+/)?(?:NgFor(?:$|-)|AnimationStateDeclarationMetadata|CORE_DIRECTIVES|PLATFORM_PIPES|DirectiveMetadata|HTTP_PROVIDERS))|.*/stackblitz(?:\\.html)?$|.*\\.[^\/.]+$)": {
"match": "regex"
}
}
+10 -3
View File
@@ -15,7 +15,7 @@
"build": "yarn ~~build",
"prebuild-local": "yarn setup-local",
"build-local": "yarn ~~build",
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint",
"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",
"e2e": "ng e2e --no-webdriver-update",
@@ -23,7 +23,7 @@
"preinstall": "node ../tools/yarn/check-yarn.js",
"presetup": "yarn install --frozen-lockfile && yarn ~~check-env && yarn boilerplate:remove",
"setup": "yarn aio-use-npm && yarn example-use-npm",
"postsetup": "yarn boilerplate:add && yarn build-ie-polyfills && yarn generate-stackblitz && yarn generate-zips && yarn docs",
"postsetup": "yarn boilerplate:add && yarn build-ie-polyfills && yarn docs",
"presetup-local": "yarn presetup",
"setup-local": "yarn aio-use-local && yarn example-use-local",
"postsetup-local": "yarn postsetup",
@@ -39,11 +39,15 @@
"check-env": "yarn ~~check-env",
"postcheck-env": "yarn aio-check-local",
"payload-size": "scripts/payload.sh",
"predocs": "yarn generate-stackblitz && yarn generate-zips",
"docs": "dgeni ./tools/transforms/angular.io-package",
"docs-watch": "node tools/transforms/authors-package/watchr.js",
"docs-lint": "eslint --ignore-path=\"tools/transforms/.eslintignore\" tools/transforms",
"docs-test": "node tools/transforms/test.js",
"tools-test": "./scripts/deploy-to-firebase.test.sh && yarn docs-test && yarn boilerplate:test && jasmine tools/ng-packages-installer/index.spec.js",
"deployment-config-test": "jasmine-ts tests/deployment/**/*.spec.ts",
"firebase-utils-test": "jasmine-ts tools/firebase-test-utils/*.spec.ts",
"tools-lint": "tslint -c \"tools/tslint.json\" \"tools/firebase-test-utils/**/*.ts\"",
"tools-test": "./scripts/deploy-to-firebase.test.sh && yarn docs-test && yarn boilerplate:test && jasmine tools/ng-packages-installer/index.spec.js && yarn firebase-utils-test",
"preserve-and-sync": "yarn docs",
"serve-and-sync": "concurrently --kill-others \"yarn docs-watch --watch-only\" \"yarn start\"",
"boilerplate:add": "node ./tools/examples/example-boilerplate add",
@@ -97,6 +101,7 @@
"archiver": "^1.3.0",
"canonical-path": "^0.0.2",
"chalk": "^2.1.0",
"cjson": "^0.5.0",
"codelyzer": "~2.0.0",
"concurrently": "^3.4.0",
"cross-spawn": "^5.1.0",
@@ -117,6 +122,7 @@
"image-size": "^0.5.1",
"jasmine-core": "^2.8.0",
"jasmine-spec-reporter": "^4.1.0",
"jasmine-ts": "^0.2.1",
"jsdom": "^9.12.0",
"karma": "^1.7.0",
"karma-chrome-launcher": "^2.1.1",
@@ -146,6 +152,7 @@
"unist-util-visit-parents": "^1.1.1",
"vrsource-tslint-rules": "^4.0.1",
"watchr": "^3.0.1",
"xregexp": "^4.0.0",
"yargs": "^7.0.2"
}
}
+5 -4
View File
@@ -4,7 +4,7 @@
<mat-progress-bar mode="indeterminate" color="warn"></mat-progress-bar>
</div>
<mat-toolbar color="primary" class="app-toolbar" [class.transitioning]="isTransitioning">
<mat-toolbar color="primary" class="app-toolbar no-print" [class.transitioning]="isTransitioning">
<mat-toolbar-row class="notification-container">
<aio-notification
icon="insert_comment"
@@ -18,7 +18,7 @@
</aio-notification>
</mat-toolbar-row>
<mat-toolbar-row>
<button mat-button class="hamburger" (click)="sidenav.toggle()" title="Docs menu">
<button mat-button class="hamburger" [class.starting]="isStarting" (click)="sidenav.toggle()" title="Docs menu">
<mat-icon svgIcon="menu"></mat-icon>
</button>
<a class="nav-link home" href="/" [ngSwitch]="isSideBySide">
@@ -29,6 +29,7 @@
<aio-search-box class="search-container" #searchBox (onSearch)="doSearch($event)" (onFocus)="doSearch($event)"></aio-search-box>
</mat-toolbar-row>
</mat-toolbar>
<aio-search-results #searchResultsView *ngIf="showSearchResults" [searchResults]="searchResults | async" (resultSelected)="hideSearchResults()"></aio-search-results>
<mat-sidenav-container class="sidenav-container" [class.starting]="isStarting" [class.has-floating-toc]="hasFloatingToc" role="main">
@@ -56,11 +57,11 @@
</mat-sidenav-container>
<div *ngIf="hasFloatingToc" class="toc-container" [style.max-height.px]="tocMaxHeight" (mousewheel)="restrainScrolling($event)">
<div *ngIf="hasFloatingToc" class="toc-container no-print" [style.max-height.px]="tocMaxHeight" (mousewheel)="restrainScrolling($event)">
<aio-toc></aio-toc>
</div>
<footer>
<footer class="no-print">
<aio-footer [nodes]="footerNodes" [versionInfo]="versionInfo" ></aio-footer>
</footer>
+170 -91
View File
@@ -8,9 +8,12 @@ import { By } from '@angular/platform-browser';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { timer } from 'rxjs/observable/timer';
import 'rxjs/add/operator/mapTo';
import { AppComponent } from './app.component';
import { AppModule } from './app.module';
import { DocumentService } from 'app/documents/document.service';
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
import { Deployment } from 'app/shared/deployment.service';
import { EmbedComponentsService } from 'app/embed-components/embed-components.service';
@@ -31,18 +34,30 @@ import { TocItem, TocService } from 'app/shared/toc.service';
const sideBySideBreakPoint = 992;
const hideToCBreakPoint = 800;
const startedDelay = 100;
describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let documentService: DocumentService;
let docViewer: HTMLElement;
let docViewerComponent: DocViewerComponent;
let hamburger: HTMLButtonElement;
let locationService: MockLocationService;
let sidenav: MatSidenav;
let tocService: TocService;
const initializeTest = () => {
async function awaitDocRendered() {
const newDocPromise = new Promise(resolve => documentService.currentDocument.subscribe(resolve));
const docRenderedPromise = new Promise(resolve => docViewerComponent.docRendered.subscribe(resolve));
await newDocPromise; // Wait for the new document to be fetched.
fixture.detectChanges(); // Propagate document change to the view (i.e to `DocViewer`).
await docRenderedPromise; // Wait for the `docRendered` event.
};
function initializeTest(waitForDoc = true) {
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
@@ -50,21 +65,27 @@ describe('AppComponent', () => {
component.onResize(sideBySideBreakPoint + 1); // wide by default
const de = fixture.debugElement;
docViewer = de.query(By.css('aio-doc-viewer')).nativeElement;
const docViewerDe = de.query(By.css('aio-doc-viewer'));
documentService = de.injector.get(DocumentService) as DocumentService;
docViewer = docViewerDe.nativeElement;
docViewerComponent = docViewerDe.componentInstance;
hamburger = de.query(By.css('.hamburger')).nativeElement;
locationService = de.injector.get(LocationService) as any as MockLocationService;
locationService = de.injector.get(LocationService) as any;
sidenav = de.query(By.directive(MatSidenav)).componentInstance;
tocService = de.injector.get(TocService);
return waitForDoc && awaitDocRendered();
};
describe('with proper DocViewer', () => {
beforeEach(() => {
beforeEach(async () => {
DocViewerComponent.animationsEnabled = false;
createTestingModule('a/b');
initializeTest();
await initializeTest();
});
afterEach(() => DocViewerComponent.animationsEnabled = true);
@@ -356,43 +377,43 @@ describe('AppComponent', () => {
let selectElement: DebugElement;
let selectComponent: SelectComponent;
function setupSelectorForTesting(mode?: string) {
async function setupSelectorForTesting(mode?: string) {
createTestingModule('a/b', mode);
initializeTest();
await initializeTest();
component.onResize(sideBySideBreakPoint + 1); // side-by-side
selectElement = fixture.debugElement.query(By.directive(SelectComponent));
selectComponent = selectElement.componentInstance;
}
it('should select the version that matches the deploy mode', () => {
setupSelectorForTesting();
it('should select the version that matches the deploy mode', async () => {
await setupSelectorForTesting();
expect(selectComponent.selected.title).toContain('stable');
setupSelectorForTesting('next');
await setupSelectorForTesting('next');
expect(selectComponent.selected.title).toContain('next');
setupSelectorForTesting('archive');
await setupSelectorForTesting('archive');
expect(selectComponent.selected.title).toContain('v4');
});
it('should add the current raw version string to the selected version', () => {
setupSelectorForTesting();
it('should add the current raw version string to the selected version', async () => {
await setupSelectorForTesting();
expect(selectComponent.selected.title).toContain(`(v${component.versionInfo.raw})`);
setupSelectorForTesting('next');
await setupSelectorForTesting('next');
expect(selectComponent.selected.title).toContain(`(v${component.versionInfo.raw})`);
setupSelectorForTesting('archive');
await setupSelectorForTesting('archive');
expect(selectComponent.selected.title).toContain(`(v${component.versionInfo.raw})`);
});
// Older docs versions have an href
it('should navigate when change to a version with a url', () => {
setupSelectorForTesting();
it('should navigate when change to a version with a url', async () => {
await setupSelectorForTesting();
const versionWithUrlIndex = component.docVersions.findIndex(v => !!v.url);
const versionWithUrl = component.docVersions[versionWithUrlIndex];
selectElement.triggerEventHandler('change', { option: versionWithUrl, index: versionWithUrlIndex});
expect(locationService.go).toHaveBeenCalledWith(versionWithUrl.url);
});
it('should not navigate when change to a version without a url', () => {
setupSelectorForTesting();
it('should not navigate when change to a version without a url', async () => {
await setupSelectorForTesting();
const versionWithoutUrlIndex = component.docVersions.length;
const versionWithoutUrl = component.docVersions[versionWithoutUrlIndex] = { title: 'foo' };
selectElement.triggerEventHandler('change', { option: versionWithoutUrl, index: versionWithoutUrlIndex });
@@ -401,37 +422,39 @@ describe('AppComponent', () => {
});
describe('currentDocument', () => {
it('should display a guide page (guide/pipes)', () => {
locationService.go('guide/pipes');
fixture.detectChanges();
const navigateTo = async (path: string) => {
locationService.go(path);
await awaitDocRendered();
};
it('should display a guide page (guide/pipes)', async () => {
await navigateTo('guide/pipes');
expect(docViewer.textContent).toMatch(/Pipes/i);
});
it('should display the api page', () => {
locationService.go('api');
fixture.detectChanges();
it('should display the api page', async () => {
await navigateTo('api');
expect(docViewer.textContent).toMatch(/API/i);
});
it('should display a marketing page', () => {
locationService.go('features');
fixture.detectChanges();
it('should display a marketing page', async () => {
await navigateTo('features');
expect(docViewer.textContent).toMatch(/Features/i);
});
it('should update the document title', () => {
it('should update the document title', async () => {
const titleService = TestBed.get(Title);
spyOn(titleService, 'setTitle');
locationService.go('guide/pipes');
fixture.detectChanges();
await navigateTo('guide/pipes');
expect(titleService.setTitle).toHaveBeenCalledWith('Angular - Pipes');
});
it('should update the document title, with a default value if the document has no title', () => {
it('should update the document title, with a default value if the document has no title', async () => {
const titleService = TestBed.get(Title);
spyOn(titleService, 'setTitle');
locationService.go('no-title');
fixture.detectChanges();
await navigateTo('no-title');
expect(titleService.setTitle).toHaveBeenCalledWith('Angular');
});
});
@@ -509,7 +532,9 @@ describe('AppComponent', () => {
expect(scrollToTopSpy).not.toHaveBeenCalled();
locationService.go('guide/pipes');
tick(1); // triggers the HTTP response for the document
fixture.detectChanges(); // triggers the event that calls `onDocInserted`
expect(scrollToTopSpy).toHaveBeenCalled();
expect(scrollSpy).not.toHaveBeenCalled();
@@ -658,18 +683,16 @@ describe('AppComponent', () => {
});
describe('deployment banner', () => {
it('should show a message if the deployment mode is "archive"', () => {
it('should show a message if the deployment mode is "archive"', async () => {
createTestingModule('a/b', 'archive');
initializeTest();
fixture.detectChanges();
await initializeTest();
const banner: HTMLElement = fixture.debugElement.query(By.css('aio-mode-banner')).nativeElement;
expect(banner.textContent).toContain('archived documentation for Angular v4');
});
it('should show no message if the deployment mode is not "archive"', () => {
it('should show no message if the deployment mode is not "archive"', async () => {
createTestingModule('a/b', 'stable');
initializeTest();
fixture.detectChanges();
await initializeTest();
const banner: HTMLElement = fixture.debugElement.query(By.css('aio-mode-banner')).nativeElement;
expect(banner.textContent!.trim()).toEqual('');
});
@@ -678,7 +701,6 @@ describe('AppComponent', () => {
describe('search', () => {
describe('initialization', () => {
it('should initialize the search worker', inject([SearchService], (searchService: SearchService) => {
fixture.detectChanges(); // triggers ngOnInit
expect(searchService.initWorker).toHaveBeenCalled();
}));
});
@@ -771,103 +793,103 @@ describe('AppComponent', () => {
describe('archive redirection', () => {
it('should redirect to `docs` if deployment mode is `archive` and not at a docs page', () => {
createTestingModule('', 'archive');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).toHaveBeenCalledWith('docs');
createTestingModule('resources', 'archive');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).toHaveBeenCalledWith('docs');
createTestingModule('guide/aot-compiler', 'archive');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('tutorial', 'archive');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('tutorial/toh-pt1', 'archive');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('docs', 'archive');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('api', 'archive');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('api/core/getPlatform', 'archive');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
});
it('should not redirect if deployment mode is `next`', () => {
createTestingModule('', 'next');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('resources', 'next');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('guide/aot-compiler', 'next');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('tutorial', 'next');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('tutorial/toh-pt1', 'next');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('docs', 'next');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('api', 'next');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('api/core/getPlatform', 'next');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
});
it('should not redirect to `docs` if deployment mode is `stable`', () => {
createTestingModule('', 'stable');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('resources', 'stable');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('guide/aot-compiler', 'stable');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('tutorial', 'stable');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('tutorial/toh-pt1', 'stable');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('docs', 'stable');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('api', 'stable');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
createTestingModule('api/core/getPlatform', 'stable');
initializeTest();
initializeTest(false);
expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled();
});
});
@@ -889,27 +911,68 @@ describe('AppComponent', () => {
});
describe('initial rendering', () => {
it('should initially add the starting class until a document is rendered', () => {
const getSidenavContainer = () => fixture.debugElement.query(By.css('mat-sidenav-container'));
beforeEach(jasmine.clock().install);
afterEach(jasmine.clock().uninstall);
initializeTest();
it('should initially disable Angular animations until a document is rendered', () => {
initializeTest(false);
jasmine.clock().tick(1); // triggers the HTTP response for the document
expect(component.isStarting).toBe(true);
expect(getSidenavContainer().classes['starting']).toBe(true);
expect(fixture.debugElement.properties['@.disabled']).toBe(true);
triggerDocViewerEvent('docInserted');
jasmine.clock().tick(startedDelay);
fixture.detectChanges();
expect(component.isStarting).toBe(true);
expect(fixture.debugElement.properties['@.disabled']).toBe(true);
triggerDocViewerEvent('docRendered');
jasmine.clock().tick(startedDelay);
fixture.detectChanges();
expect(component.isStarting).toBe(false);
expect(getSidenavContainer().classes['starting']).toBe(false);
expect(fixture.debugElement.properties['@.disabled']).toBe(false);
});
it('should initially add the starting class until a document is rendered', () => {
initializeTest(false);
jasmine.clock().tick(1); // triggers the HTTP response for the document
const sidenavContainer = fixture.debugElement.query(By.css('mat-sidenav-container')).nativeElement;
expect(component.isStarting).toBe(true);
expect(hamburger.classList.contains('starting')).toBe(true);
expect(sidenavContainer.classList.contains('starting')).toBe(true);
triggerDocViewerEvent('docInserted');
jasmine.clock().tick(startedDelay);
fixture.detectChanges();
expect(component.isStarting).toBe(true);
expect(hamburger.classList.contains('starting')).toBe(true);
expect(sidenavContainer.classList.contains('starting')).toBe(true);
triggerDocViewerEvent('docRendered');
jasmine.clock().tick(startedDelay);
fixture.detectChanges();
expect(component.isStarting).toBe(false);
expect(hamburger.classList.contains('starting')).toBe(false);
expect(sidenavContainer.classList.contains('starting')).toBe(false);
});
it('should initially disable animations on the DocViewer for the first rendering', () => {
initializeTest();
initializeTest(false);
jasmine.clock().tick(1); // triggers the HTTP response for the document
expect(component.isStarting).toBe(true);
expect(docViewer.classList.contains('no-animations')).toBe(true);
triggerDocViewerEvent('docInserted');
jasmine.clock().tick(startedDelay);
fixture.detectChanges();
expect(component.isStarting).toBe(true);
expect(docViewer.classList.contains('no-animations')).toBe(true);
triggerDocViewerEvent('docRendered');
jasmine.clock().tick(startedDelay);
fixture.detectChanges();
expect(component.isStarting).toBe(false);
expect(docViewer.classList.contains('no-animations')).toBe(false);
@@ -921,50 +984,63 @@ describe('AppComponent', () => {
afterEach(jasmine.clock().uninstall);
it('should set the transitioning class on `.app-toolbar` while a document is being rendered', () => {
const getToolbar = () => fixture.debugElement.query(By.css('.app-toolbar'));
initializeTest();
initializeTest(false);
jasmine.clock().tick(1); // triggers the HTTP response for the document
const toolbar = fixture.debugElement.query(By.css('.app-toolbar'));
// Initially, `isTransitoning` is true.
expect(component.isTransitioning).toBe(true);
expect(getToolbar().classes['transitioning']).toBe(true);
expect(toolbar.classes['transitioning']).toBe(true);
triggerDocViewerEvent('docRendered');
fixture.detectChanges();
expect(component.isTransitioning).toBe(false);
expect(getToolbar().classes['transitioning']).toBe(false);
expect(toolbar.classes['transitioning']).toBe(false);
// While a document is being rendered, `isTransitoning` is set to true.
triggerDocViewerEvent('docReady');
fixture.detectChanges();
expect(component.isTransitioning).toBe(true);
expect(getToolbar().classes['transitioning']).toBe(true);
expect(toolbar.classes['transitioning']).toBe(true);
triggerDocViewerEvent('docRendered');
fixture.detectChanges();
expect(component.isTransitioning).toBe(false);
expect(getToolbar().classes['transitioning']).toBe(false);
expect(toolbar.classes['transitioning']).toBe(false);
});
it('should update the sidenav state as soon as a new document is inserted', () => {
initializeTest();
it('should update the sidenav state as soon as a new document is inserted (but not before)', () => {
initializeTest(false);
jasmine.clock().tick(1); // triggers the HTTP response for the document
jasmine.clock().tick(0); // calls `updateSideNav()` for initial rendering
const updateSideNavSpy = spyOn(component, 'updateSideNav');
triggerDocViewerEvent('docReady');
jasmine.clock().tick(0);
expect(updateSideNavSpy).not.toHaveBeenCalled();
triggerDocViewerEvent('docInserted');
jasmine.clock().tick(0);
expect(updateSideNavSpy).toHaveBeenCalledTimes(1);
updateSideNavSpy.calls.reset();
triggerDocViewerEvent('docReady');
jasmine.clock().tick(0);
expect(updateSideNavSpy).not.toHaveBeenCalled();
triggerDocViewerEvent('docInserted');
jasmine.clock().tick(0);
expect(updateSideNavSpy).toHaveBeenCalledTimes(2);
expect(updateSideNavSpy).toHaveBeenCalledTimes(1);
});
});
describe('pageId', () => {
const navigateTo = (path: string) => {
locationService.go(path);
jasmine.clock().tick(1); // triggers the HTTP response for the document
triggerDocViewerEvent('docInserted');
jasmine.clock().tick(0);
jasmine.clock().tick(0); // triggers `updateHostClasses()`
fixture.detectChanges();
};
@@ -972,7 +1048,7 @@ describe('AppComponent', () => {
afterEach(jasmine.clock().uninstall);
it('should set the id of the doc viewer container based on the current doc', () => {
initializeTest();
initializeTest(false);
const container = fixture.debugElement.query(By.css('section.sidenav-content'));
navigateTo('guide/pipes');
@@ -989,7 +1065,7 @@ describe('AppComponent', () => {
});
it('should not be affected by changes to the query', () => {
initializeTest();
initializeTest(false);
const container = fixture.debugElement.query(By.css('section.sidenav-content'));
navigateTo('guide/pipes');
@@ -1002,8 +1078,9 @@ describe('AppComponent', () => {
describe('hostClasses', () => {
const triggerUpdateHostClasses = () => {
jasmine.clock().tick(1); // triggers the HTTP response for document
triggerDocViewerEvent('docInserted');
jasmine.clock().tick(0);
jasmine.clock().tick(0); // triggers `updateHostClasses()`
fixture.detectChanges();
};
const navigateTo = (path: string) => {
@@ -1015,7 +1092,7 @@ describe('AppComponent', () => {
afterEach(jasmine.clock().uninstall);
it('should set the css classes of the host container based on the current doc and navigation view', () => {
initializeTest();
initializeTest(false);
navigateTo('guide/pipes');
checkHostClass('page', 'guide-pipes');
@@ -1034,7 +1111,7 @@ describe('AppComponent', () => {
});
it('should set the css class of the host container based on the open/closed state of the side nav', async () => {
initializeTest();
initializeTest(false);
navigateTo('guide/pipes');
checkHostClass('sidenav', 'open');
@@ -1059,7 +1136,7 @@ describe('AppComponent', () => {
it('should set the css class of the host container based on the initial deployment mode', () => {
createTestingModule('a/b', 'archive');
initializeTest();
initializeTest(false);
triggerUpdateHostClasses();
checkHostClass('mode', 'archive');
@@ -1079,13 +1156,13 @@ describe('AppComponent', () => {
const HIDE_DELAY = 500;
const getProgressBar = () => fixture.debugElement.query(By.directive(MatProgressBar));
const initializeAndCompleteNavigation = () => {
initializeTest();
initializeTest(false);
triggerDocViewerEvent('docReady');
tick(HIDE_DELAY);
};
it('should initially be hidden', () => {
initializeTest();
initializeTest(false);
expect(getProgressBar()).toBeFalsy();
});
@@ -1300,6 +1377,8 @@ class TestHttpClient {
const contents = `${h1}<h2 id="#somewhere">Some heading</h2>`;
data = { id, contents };
}
return of(data);
// Preserve async nature of `HttpClient`.
return timer(1).mapTo(data);
}
}
+39 -28
View File
@@ -28,7 +28,7 @@ export class AppComponent implements OnInit {
currentDocument: DocumentContents;
currentDocVersion: NavigationNode;
currentNodes: CurrentNodes;
currentNodes: CurrentNodes = {};
currentPath: string;
docVersions: NavigationNode[];
dtOn = false;
@@ -57,9 +57,11 @@ export class AppComponent implements OnInit {
@HostBinding('class')
hostClasses = '';
isFetching = false;
// Disable all Angular animations for the initial render.
@HostBinding('@.disabled')
isStarting = true;
isTransitioning = true;
isFetching = false;
isSideBySide = false;
private isFetchingTimeout: any;
private isSideNavDoc = false;
@@ -118,12 +120,6 @@ export class AppComponent implements OnInit {
/* No need to unsubscribe because this root component never dies */
this.documentService.currentDocument.subscribe(doc => this.currentDocument = doc);
// Generally, we want to delay updating the host classes for the new document, until after the
// leaving document has been removed (to avoid having the styles for the new document applied
// prematurely).
// On the first document, though, (when we know there is no previous document), we want to
// ensure the styles are applied as soon as possible to avoid flicker.
this.documentService.currentDocument.first().subscribe(doc => this.updateHostClassesForDoc(doc));
this.locationService.currentPath.subscribe(path => {
// Redirect to docs if we are in archive mode and are not hitting a docs page
@@ -175,11 +171,22 @@ export class AppComponent implements OnInit {
this.topMenuNarrowNodes = views['TopBarNarrow'] || this.topMenuNodes;
});
this.navigationService.versionInfo.subscribe( vi => this.versionInfo = vi );
this.navigationService.versionInfo.subscribe(vi => this.versionInfo = vi);
const hasNonEmptyToc = this.tocService.tocList.map(tocList => tocList.length > 0);
combineLatest(hasNonEmptyToc, this.showFloatingToc)
.subscribe(([hasToc, showFloatingToc]) => this.hasFloatingToc = hasToc && showFloatingToc);
// Generally, we want to delay updating the shell (e.g. host classes, sidenav state) for the new
// document, until after the leaving document has been removed (to avoid having the styles for
// the new document applied prematurely).
// For the first document, though, (when we know there is no previous document), we want to
// ensure the styles are applied as soon as possible to avoid flicker.
combineLatest(
this.documentService.currentDocument, // ...needed to determine host classes
this.navigationService.currentNodes) // ...needed to determine `sidenav` state
.first()
.subscribe(() => this.updateShell());
}
// Scroll to the anchor in the hash fragment or top of doc.
@@ -205,14 +212,11 @@ export class AppComponent implements OnInit {
}
onDocInserted() {
// TODO: Find a better way to avoid `ExpressionChangedAfterItHasBeenChecked` error.
setTimeout(() => {
// Update the SideNav state (if necessary).
this.updateSideNav();
// Update the host classes to match the new document.
this.updateHostClassesForDoc(this.currentDocument);
});
// Update the shell (host classes, sidenav state) to match the new document.
// This may be called as a result of actions initiated by view updates.
// In order to avoid errors (e.g. `ExpressionChangedAfterItHasBeenChecked`), updating the view
// (e.g. sidenav, host classes) needs to happen asynchronously.
setTimeout(() => this.updateShell());
// Scroll 500ms after the new document has been inserted into the doc-viewer.
// The delay is to allow time for async layout to complete.
@@ -220,7 +224,14 @@ export class AppComponent implements OnInit {
}
onDocRendered() {
this.isStarting = false;
if (this.isStarting) {
// In order to ensure that the initial sidenav-content left margin
// adjustment happens without animation, we need to ensure that
// `isStarting` remains `true` until the margin change is triggered.
// (Apparently, this happens with a slight delay.)
setTimeout(() => this.isStarting = false, 100);
}
this.isTransitioning = false;
}
@@ -241,7 +252,7 @@ export class AppComponent implements OnInit {
// items in the top-bar, ensure the sidenav is closed.
// (This condition can only be met when the resize event changes the value of `isSideBySide`
// from `false` to `true` while on a non-sidenav doc.)
this.sideNavToggle(false);
this.sidenav.toggle(false);
}
}
@@ -272,10 +283,6 @@ export class AppComponent implements OnInit {
return true;
}
sideNavToggle(value?: boolean) {
this.sidenav.toggle(value);
}
setPageId(id: string) {
// Special case the home page
this.pageId = (id === 'index') ? 'home' : id.replace('/', '-');
@@ -300,7 +307,7 @@ export class AppComponent implements OnInit {
const sideNavOpen = `sidenav-${this.sidenav.opened ? 'open' : 'closed'}`;
const pageClass = `page-${this.pageId}`;
const folderClass = `folder-${this.folderId}`;
const viewClasses = Object.keys(this.currentNodes || {}).map(view => `view-${view}`).join(' ');
const viewClasses = Object.keys(this.currentNodes).map(view => `view-${view}`).join(' ');
const notificationClass = `aio-notification-${this.notification.showNotification}`;
const notificationAnimatingClass = this.notificationAnimating ? 'aio-notification-animating' : '';
@@ -315,9 +322,13 @@ export class AppComponent implements OnInit {
].join(' ');
}
updateHostClassesForDoc(doc: DocumentContents) {
this.setPageId(doc.id);
this.setFolderId(doc.id);
updateShell() {
// Update the SideNav state (if necessary).
this.updateSideNav();
// Update the host classes.
this.setPageId(this.currentDocument.id);
this.setFolderId(this.currentDocument.id);
this.updateHostClasses();
}
@@ -333,7 +344,7 @@ export class AppComponent implements OnInit {
}
// May be open or closed when wide; always closed when narrow.
this.sideNavToggle(this.isSideBySide && openSideNav);
this.sidenav.toggle(this.isSideBySide && openSideNav);
}
// Dynamically change height of table of contents container
+7 -5
View File
@@ -23,14 +23,16 @@ describe('AppModule', () => {
});
it('should provide a list of eagerly-loaded embedded components', () => {
const eagerSelector = Object.keys(componentsMap).find(selector => Array.isArray(componentsMap[selector]))!;
const selectorCount = eagerSelector.split(',').length;
expect(eagerSelector).not.toBeNull();
expect(selectorCount).toBe(componentsMap[eagerSelector].length);
const eagerConfig = Object.keys(componentsMap).filter(selector => Array.isArray(componentsMap[selector]));
expect(eagerConfig.length).toBeGreaterThan(0);
const eagerSelectors = eagerConfig.reduce<string[]>((selectors, config) => selectors.concat(config.split(',')), []);
expect(eagerSelectors.length).toBeGreaterThan(0);
// For example...
expect(eagerSelector).toContain('aio-toc');
expect(eagerSelectors).toContain('aio-toc');
expect(eagerSelectors).toContain('aio-announcement-bar');
});
it('should provide a list of lazy-loaded embedded components', () => {
+7 -2
View File
@@ -1,5 +1,5 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { ErrorHandler, NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@@ -14,6 +14,7 @@ import { MatToolbarModule } from '@angular/material/toolbar';
import { ROUTES } from '@angular/router';
import { AnnouncementBarComponent } from 'app/embedded/announcement-bar/announcement-bar.component';
import { AppComponent } from 'app/app.component';
import { EMBEDDED_COMPONENTS, EmbeddedComponentsMap } from 'app/embed-components/embed-components.service';
import { CustomIconRegistry, SVG_ICONS } from 'app/shared/custom-icon-registry';
@@ -31,6 +32,7 @@ import { TopMenuComponent } from 'app/layout/top-menu/top-menu.component';
import { FooterComponent } from 'app/layout/footer/footer.component';
import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component';
import { NavItemComponent } from 'app/layout/nav-item/nav-item.component';
import { ReportingErrorHandler } from 'app/shared/reporting-error-handler';
import { ScrollService } from 'app/shared/scroll.service';
import { ScrollSpyService } from 'app/shared/scroll-spy.service';
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
@@ -109,6 +111,7 @@ export const svgIconProviders = [
SharedModule
],
declarations: [
AnnouncementBarComponent,
AppComponent,
DocViewerComponent,
DtComponent,
@@ -124,6 +127,7 @@ export const svgIconProviders = [
providers: [
Deployment,
DocumentService,
{ provide: ErrorHandler, useClass: ReportingErrorHandler },
GaService,
Logger,
Location,
@@ -143,6 +147,7 @@ export const svgIconProviders = [
provide: EMBEDDED_COMPONENTS,
useValue: {
/* tslint:disable: max-line-length */
'aio-announcement-bar': [AnnouncementBarComponent],
'aio-toc': [TocComponent],
'aio-api-list, aio-contributor-list, aio-file-not-found-search, aio-resource-list, code-example, code-tabs, current-location, live-example': embeddedModulePath,
/* tslint:enable: max-line-length */
@@ -156,7 +161,7 @@ export const svgIconProviders = [
multi: true,
},
],
entryComponents: [ TocComponent ],
entryComponents: [ AnnouncementBarComponent, TocComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule {
@@ -0,0 +1,109 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Logger } from 'app/shared/logger.service';
import { MockLogger } from 'testing/logger.service';
import { AnnouncementBarComponent } from './announcement-bar.component';
const today = new Date();
const lastWeek = changeDays(today, -7);
const yesterday = changeDays(today, -1);
const tomorrow = changeDays(today, 1);
const nextWeek = changeDays(today, 7);
describe('AnnouncementBarComponent', () => {
let element: HTMLElement;
let fixture: ComponentFixture<AnnouncementBarComponent>;
let component: AnnouncementBarComponent;
let httpMock: HttpTestingController;
let mockLogger: MockLogger;
beforeEach(() => {
const injector = TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [AnnouncementBarComponent],
providers: [{ provide: Logger, useClass: MockLogger }]
});
httpMock = injector.get(HttpTestingController);
mockLogger = injector.get(Logger);
fixture = TestBed.createComponent(AnnouncementBarComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
});
it('should have no announcement when first created', () => {
expect(component.announcement).toBeUndefined();
});
describe('ngOnInit', () => {
it('should make a single request to the server', () => {
component.ngOnInit();
httpMock.expectOne('generated/announcements.json');
});
it('should set the announcement to the first "live" one in the list loaded from `announcements.json`', () => {
component.ngOnInit();
const request = httpMock.expectOne('generated/announcements.json');
request.flush([
{ startDate: lastWeek, endDate: yesterday, message: 'Test Announcement 0' },
{ startDate: tomorrow, endDate: nextWeek, message: 'Test Announcement 1' },
{ startDate: yesterday, endDate: tomorrow, message: 'Test Announcement 2' },
{ startDate: yesterday, endDate: tomorrow, message: 'Test Announcement 3' }
]);
expect(component.announcement.message).toEqual('Test Announcement 2');
});
it('should set the announcement to `undefined` if there are no announcements in `announcements.json`', () => {
component.ngOnInit();
const request = httpMock.expectOne('generated/announcements.json');
request.flush([]);
expect(component.announcement).toBeUndefined();
});
it('should handle invalid data in `announcements.json`', () => {
component.ngOnInit();
const request = httpMock.expectOne('generated/announcements.json');
request.flush('some random response');
expect(component.announcement).toBeUndefined();
expect(mockLogger.output.error[0][0]).toContain('generated/announcements.json contains invalid data:');
});
it('should handle a failed request for `announcements.json`', () => {
component.ngOnInit();
const request = httpMock.expectOne('generated/announcements.json');
request.error(new ErrorEvent('404'));
expect(component.announcement).toBeUndefined();
expect(mockLogger.output.error[0][0]).toContain('generated/announcements.json request failed:');
});
});
describe('rendering', () => {
beforeEach(() => {
component.announcement = {
imageUrl: 'link/to/image',
linkUrl: 'link/to/website',
message: 'this is an <b>important</b> message',
endDate: '2018-03-01',
startDate: '2018-02-01'
};
fixture.detectChanges();
});
it('should display the message as HTML', () => {
expect(element.innerHTML).toContain('this is an <b>important</b> message');
});
it('should display an image', () => {
expect(element.querySelector('img')!.src).toContain('link/to/image');
});
it('should display a link', () => {
expect(element.querySelector('a')!.href).toContain('link/to/website');
});
});
});
function changeDays(initial: Date, days: number) {
return (new Date(initial.valueOf()).setDate(initial.getDate() + days));
}
@@ -0,0 +1,82 @@
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Logger } from 'app/shared/logger.service';
import { CONTENT_URL_PREFIX } from 'app/documents/document.service';
const announcementsPath = CONTENT_URL_PREFIX + 'announcements.json';
export interface Announcement {
imageUrl: string;
message: string;
linkUrl: string;
startDate: string;
endDate: string;
}
/**
* Display the latest live announcement. This is used on the homepage.
*
* The data for the announcements is kept in `aio/content/marketing/announcements.json`.
*
* The format for that data file looks like:
*
* ```
* [
* {
* "startDate": "2018-02-01",
* "endDate": "2018-03-01",
* "message": "This is an <b>important</b> announcement",
* "imageUrl": "url/to/image",
* "linkUrl": "url/to/website"
* },
* ...
* ]
* ```
*
* Only one announcement will be shown at any time. This is determined as the first "live"
* announcement in the file, where "live" means that its start date is before today, and its
* end date is after today.
*
* **Security Note:**
* The `message` field can contain unsanitized HTML but this field should only updated by
* verified members of the Angular team.
*/
@Component({
selector: 'aio-announcement-bar',
template: `
<div class="homepage-container" *ngIf="announcement">
<div class="announcement-bar">
<img [src]="announcement.imageUrl">
<p [innerHTML]="announcement.message"></p>
<a class="button" [href]="announcement.linkUrl">Learn More</a>
</div>
</div>`
})
export class AnnouncementBarComponent implements OnInit {
announcement: Announcement;
constructor(private http: HttpClient, private logger: Logger) {}
ngOnInit() {
this.http.get<Announcement[]>(announcementsPath)
.catch(error => {
this.logger.error(`${announcementsPath} request failed: ${error.message}`);
return [];
})
.map(announcements => this.findCurrentAnnouncement(announcements))
.catch(error => {
this.logger.error(`${announcementsPath} contains invalid data: ${error.message}`);
return [];
})
.subscribe(announcement => this.announcement = announcement);
}
/**
* Get the first date in the list that is "live" now
*/
private findCurrentAnnouncement(announcements: Announcement[]) {
return announcements
.filter(announcement => new Date(announcement.startDate).valueOf() < Date.now())
.filter(announcement => new Date(announcement.endDate).valueOf() > Date.now())
[0];
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ const defaultLineNumsCount = 10; // by default, show linenums over this number
selector: 'aio-code',
template: `
<pre class="prettyprint lang-{{language}}">
<button *ngIf="!hideCopy" class="material-icons copy-button"
<button *ngIf="!hideCopy" class="material-icons copy-button no-print"
title="Copy code snippet"
[attr.aria-label]="ariaLabel"
(click)="doCopy()">
+1 -1
View File
@@ -1,4 +1,4 @@
<div *ngIf="type !== 'None'" class="toc-inner" [class.collapsed]="isCollapsed">
<div *ngIf="type !== 'None'" class="toc-inner no-print" [class.collapsed]="isCollapsed">
<div *ngIf="type === 'EmbeddedSimple'" class="toc-heading embedded">
Contents
+4 -1
View File
@@ -30,6 +30,9 @@ export class GaService {
}
ga(...args: any[]) {
(this.window as any)['ga'](...args);
const gaFn = (this.window as any)['ga'];
if (gaFn) {
gaFn(...args);
}
}
}
+46
View File
@@ -0,0 +1,46 @@
import { ErrorHandler, ReflectiveInjector } from '@angular/core';
import { Logger } from './logger.service';
describe('logger service', () => {
let logSpy: jasmine.Spy;
let warnSpy: jasmine.Spy;
let logger: Logger;
let errorHandler: ErrorHandler;
beforeEach(() => {
logSpy = spyOn(console, 'log');
warnSpy = spyOn(console, 'warn');
const injector = ReflectiveInjector.resolveAndCreate([
Logger,
{ provide: ErrorHandler, useClass: MockErrorHandler }
]);
logger = injector.get(Logger);
errorHandler = injector.get(ErrorHandler);
});
describe('log', () => {
it('should delegate to console.log', () => {
logger.log('param1', 'param2', 'param3');
expect(console.log).toHaveBeenCalledWith('param1', 'param2', 'param3');
});
});
describe('warn', () => {
it('should delegate to console.warn', () => {
logger.warn('param1', 'param2', 'param3');
expect(console.warn).toHaveBeenCalledWith('param1', 'param2', 'param3');
});
});
describe('error', () => {
it('should delegate to ErrorHandler', () => {
logger.error('param1', 'param2', 'param3');
expect(errorHandler.handleError).toHaveBeenCalledWith('param1 param2 param3');
});
});
});
class MockErrorHandler implements ErrorHandler {
handleError = jasmine.createSpy('handleError');
}
+5 -2
View File
@@ -1,10 +1,12 @@
import { Injectable } from '@angular/core';
import { ErrorHandler, Injectable } from '@angular/core';
import { environment } from '../../environments/environment';
@Injectable()
export class Logger {
constructor(private errorHandler: ErrorHandler) {}
log(value: any, ...rest: any[]) {
if (!environment.production) {
console.log(value, ...rest);
@@ -12,7 +14,8 @@ export class Logger {
}
error(value: any, ...rest: any[]) {
console.error(value, ...rest);
const message = [value, ...rest].join(' ');
this.errorHandler.handleError(message);
}
warn(value: any, ...rest: any[]) {
@@ -0,0 +1,64 @@
import { ErrorHandler, ReflectiveInjector } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { WindowToken } from 'app/shared/window';
import { AppModule } from 'app/app.module';
import { ReportingErrorHandler } from './reporting-error-handler';
describe('ReportingErrorHandler service', () => {
let handler: ReportingErrorHandler;
let superHandler: jasmine.Spy;
let onerrorSpy: jasmine.Spy;
beforeEach(() => {
onerrorSpy = jasmine.createSpy('onerror');
superHandler = spyOn(ErrorHandler.prototype, 'handleError');
const injector = ReflectiveInjector.resolveAndCreate([
{ provide: ErrorHandler, useClass: ReportingErrorHandler },
{ provide: WindowToken, useFactory: () => ({ onerror: onerrorSpy }) }
]);
handler = injector.get(ErrorHandler);
});
it('should be registered on the AppModule', () => {
handler = TestBed.configureTestingModule({ imports: [AppModule] }).get(ErrorHandler);
expect(handler).toEqual(jasmine.any(ReportingErrorHandler));
});
describe('handleError', () => {
it('should call the super class handleError', () => {
const error = new Error();
handler.handleError(error);
expect(superHandler).toHaveBeenCalledWith(error);
});
it('should cope with the super handler throwing an error', () => {
const error = new Error('initial error');
superHandler.and.throwError('super handler error');
handler.handleError(error);
expect(onerrorSpy).toHaveBeenCalledTimes(2);
// Error from super handler is reported first
expect(onerrorSpy.calls.argsFor(0)[0]).toEqual('super handler error');
expect(onerrorSpy.calls.argsFor(0)[4]).toEqual(jasmine.any(Error));
// Then error from initial exception
expect(onerrorSpy.calls.argsFor(1)[0]).toEqual('initial error');
expect(onerrorSpy.calls.argsFor(1)[4]).toEqual(error);
});
it('should send an error object to window.onerror', () => {
const error = new Error('this is an error message');
handler.handleError(error);
expect(onerrorSpy).toHaveBeenCalledWith(error.message, undefined, undefined, undefined, error);
});
it('should send an error string to window.onerror', () => {
const error = 'this is an error message';
handler.handleError(error);
expect(onerrorSpy).toHaveBeenCalledWith(error);
});
});
});
@@ -0,0 +1,37 @@
import { ErrorHandler, Inject, Injectable } from '@angular/core';
import { WindowToken } from './window';
/**
* Extend the default error handling to report errors to an external service - e.g Google Analytics.
*
* Errors outside the Angular application may also be handled by `window.onerror`.
*/
@Injectable()
export class ReportingErrorHandler extends ErrorHandler {
constructor(@Inject(WindowToken) private window: Window) {
super();
}
/**
* Send error info to Google Analytics, in addition to the default handling.
* @param error Information about the error.
*/
handleError(error: string | Error) {
try {
super.handleError(error);
} catch (e) {
this.reportError(e);
}
this.reportError(error);
}
private reportError(error: string | Error) {
if (typeof error === 'string') {
this.window.onerror(error);
} else {
this.window.onerror(error.message, undefined, undefined, undefined, error);
}
}
}
+40
View File
@@ -31,6 +31,14 @@
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="translucent">
<script>
// Dynamically, pre-emptively, add `noindex`, which will be removed when the doc is ready and valid
var tag = document.createElement('meta'); tag.name = 'googlebot'; tag.content = 'noindex';
document.head.appendChild(tag);
tag = document.createElement('meta'); tag.name = 'robots'; tag.content = 'noindex';
document.head.appendChild(tag);
</script>
<!-- Google Analytics -->
<script>
// Note this is a customised version of the GA tracking snippet
@@ -44,6 +52,38 @@
</script>
<!-- End Google Analytics -->
<script>
// Report fatal errors to Google Analytics
window.onerror = function() {
ga('send', 'exception', {exDescription: formatError.apply(null, arguments), exFatal: true});
function formatError(msg, url, line, col, e) {
var stack;
msg = msg.replace(/^Error: /, '');
if (e) {
stack = e.stack
// strip the leading "Error: " from the stack trace
.replace(/^Error: /, '')
// strip the message from the stack trace, if present
.replace(msg + '\n', '')
// strip leading spaces
.replace(/^ +/gm, '')
// strip all leading "at " for each frame
.replace(/^at /gm, '')
// replace long urls with just the last segment: `filename:line:column`
.replace(/(?: \(|@)http.+\/([^/)]+)\)?(?:\n|$)/gm, '@$1\n')
// replace "eval code" in Edge
.replace(/ *\(eval code(:\d+:\d+)\)(?:\n|$)/gm, '@???$1\n')
} else {
line = line || '?';
col = col || '?';
stack = url + ':' + line + ':' + col;
}
return (msg + '\n' + stack).substr(0, 150);
}
};
</script>
<script>
if (window.document.documentMode) {
// polyfill IE11 in a blocking way
@@ -12,3 +12,4 @@
@import 'sidenav';
@import 'table-of-contents';
@import 'top-menu';
@import 'print-layout';
@@ -125,6 +125,7 @@ section#intro {
.announcement-bar {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-around;
align-items: center;
max-width: 50vw;
+110
View File
@@ -0,0 +1,110 @@
@media print {
// General Adjustments
* {
box-shadow: none !important;
}
h1 {
height: 40px !important;
color: $darkgray !important;
}
h1, h2, h3, h4, h5, h6 {
page-break-after: avoid;
}
ul, ol, img, code-example, table, tr, .alert, .l-subsection, .feature {
page-break-inside: avoid;
}
table tbody tr:last-child td {
border-bottom: 1px solid $lightgray !important;
}
img {
max-width: 100% !important;
}
p {
widows: 4;
}
p > code, li > code, table code {
color: $blue !important;
}
// No Print Class
.no-print {
display: none !important;
}
// Custom No Print for Sidenav Menu
mat-sidenav.sidenav.mat-sidenav {
display: none !important;
}
// Custom No Print Element Adjustments
.mat-sidenav-content {
margin: 0 !important;
}
mat-sidenav-container.sidenav-container {
min-width: 100vw;
}
.sidenav-content {
overflow: visible;
}
.filetree {
max-width: 100%;
}
aio-code code {
border: none !important;
}
code-example {
pre.lang-bash code span {
color: $mediumgray !important;
}
pre.lang-sh code span {
color: $darkgray !important;
}
header {
border: 0.5px solid $lightgray;
color: $darkgray;
}
}
.content code {
border: 0.5px solid $lightgray;
}
.mat-tab-labels {
div.mat-tab-label {
&:not(.mat-tab-label-active) span {
font-style: italic;
}
&.mat-tab-label-active span {
font-weight: bold;
}
}
}
.api-header label {
color: $darkgray !important;
font-weight: bold !important;
margin: 2px !important;
padding: 0 !important;
display: block !important;
}
.feature-section img {
max-width: 70px !important;
}
}
+18 -18
View File
@@ -1,11 +1,6 @@
// Disable sidenav animations while starting the app
// See https://github.com/angular/material2/blob/master/src/lib/sidenav/sidenav-transitions.scss
.starting.mat-sidenav-transition {
.mat-sidenav,
.mat-sidenav-content,
.mat-sidenav-backdrop.mat-sidenav-shown {
transition: none;
}
// Disable sidenav animations for the initial render.
.starting.mat-drawer-transition .mat-drawer-content {
transition: none;
}
aio-nav-menu {
@@ -42,19 +37,19 @@ mat-sidenav.mat-sidenav.sidenav {
}
mat-sidenav-container.sidenav-container {
min-height: 100%;
height: auto !important;
max-width: 100%;
margin: 0;
transform: none;
min-height: 100%;
height: auto !important;
max-width: 100%;
margin: 0;
transform: none;
&.has-floating-toc {
max-width: 82%;
}
&.has-floating-toc {
max-width: 82%;
}
}
mat-sidenav-container div.mat-sidenav-content {
height: auto;
height: auto;
}
.vertical-menu-item {
@@ -128,6 +123,12 @@ button.vertical-menu-item {
transition-timing-function: ease-out;
}
.no-animations {
.heading-children.expanded, .heading-children.collapsed {
transition: none! important;
}
}
.level-1 {
font-family: $main-font;
font-size: 14px;
@@ -159,7 +160,6 @@ button.vertical-menu-item {
.level-1:not(.expanded) .mat-icon, .level-2:not(.expanded) .mat-icon {
@include rotate(0deg);
// margin: 4px;
}
aio-nav-menu.top-menu {
+6 -3
View File
@@ -68,9 +68,12 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
height: 100%;
margin: $hamburgerShownMargin;
padding: 0;
transition-duration: .4s;
transition-property: color, margin;
transition-timing-function: cubic-bezier(.25, .8, .25, 1);
&:not(.starting) {
transition-duration: .4s;
transition-property: color, margin;
transition-timing-function: cubic-bezier(.25, .8, .25, 1);
}
@media (min-width: 992px) {
// Hamburger hidden by default on large screens.
-1
View File
@@ -85,7 +85,6 @@ aio-code pre {
.copy-button {
display: inline-block;
position: absolute;
top: -8px;
right: -32px;
+15 -14
View File
@@ -5,25 +5,26 @@
display: none;
}
.mat-icon, .material-icons {
visibility: hidden;
display: inline-block;
.header-link {
box-sizing: border-box;
color: $mediumgray;
margin: 0 8px;
}
display: inline-block;
margin-left: -42px;
padding: 0 8px;
text-decoration: none;
user-select: none;
vertical-align: middle;
visibility: hidden;
width: 40px;
&:hover {
.mat-icon, .material-icons {
visibility: visible;
@media (max-width: 600px) {
float: right;
margin-left: 0;
}
}
a.header-link {
text-decoration: none;
padding-left: 8px;
margin-left: -50px;
display: inline-block;
vertical-align: middle;
&:hover .header-link {
visibility: visible;
}
}
+182
View File
@@ -0,0 +1,182 @@
/api/api/core/ElementRef /api/core/ElementRef
/api/common/Control-class /api/forms/FormControl
/api/common/CORE_DIRECTIVES /api/common/CommonModule
/api/common/DatePipe-class /api/common/DatePipe
/api/common/NgClass-directive.html /api/common/NgClass
/api/common/NgFor-directive.html /api/common/NgForOf
/api/common/NgModel-directive /api/forms/NgModel
/api/common/SelectControlValueAccessor-directive /api/forms/SelectControlValueAccessor
/api/common/SlicePipe-class.html /api/common/SlicePipe
/api/core/AnimationStateDeclarationMetadata /api/animations
/api/core/DoCheck-interface.html /api/core/DoCheck
/api/core/HostBinding-var /api/core/HostBinding
/api/core/OpaqueToken-class /api/core/OpaqueToken
/api/core/OptionalMetadata-class /api/core/Optional
/api/core/PLATFORM_PIPES /api/common/CommonModule
/api/core/Provider-class.html /api/core/Provider
/api/core/Renderer-class /api/core/Renderer
/api/core/testing/async-function /api/core/testing/async
/api/core/testing/index/async-function /api/core/testing/async
/api/core/testing/index/TestBed-class.html /api/core/testing/TestBed
/api/core/testing/inject-function /api/core/testing/inject
/api/core/testing/inject-function.html /api/core/testing/inject
/api/http/Headers-class /api/http/Headers
/api/http/Headers-class.html /api/http/Headers
/api/http/HTTP_PROVIDERS-let /api/http/HttpModule
/api/http/testing/index/MockBackend-class /api/http/testing/MockBackend
/api/http/testing/index/MockBackend-class.html /api/http/testing/MockBackend
/api/platform-browser-dynamic/testing/index/platformBrowserDynamicTesting-let.html /api/platform-browser-dynamic/testing/platformBrowserDynamicTesting
/api/platform-browser/AnimationDriver /api/animations/browser/AnimationDriver
/api/router/Route-class /api/router/Route
/api/router/RouterLink-directive /api/router/RouterLink
/api/testing/tick-function /api/core/testing/tick
/api/upgrade/static/downgradeComponent-function /api/upgrade/static/downgradeComponent
/api/upgrade/static/downgradeComponent-function.html /api/upgrade/static/downgradeComponent
/api/upgrade/static/index/downgradeComponent-function /api/upgrade/static/downgradeComponent
/api/upgrade/static/UpgradeModule-class /api/upgrade/static/UpgradeModule
/docs/js/latest/api/ /api
/docs/js/latest/api/animate/AnimationBuilder-class /api/animations/AnimationBuilder
/docs/js/latest/api/animate/CssAnimationBuilder-class.html /api/animations/CssAnimationBuilder
/docs/js/latest/api/animations/index/trigger-function /api/animations/trigger
/docs/js/latest/api/common/ControlGroup-class.html /api/forms/FormGroup
/docs/js/latest/api/common/index/DatePipe-pipe /api/common/DatePipe
/docs/js/latest/api/common/index/Location-class.html /api/common/Location
/docs/js/latest/api/common/index/MaxLengthValidator-directive.html /api/forms/MaxLengthValidator
/docs/js/latest/api/common/index/NgFor-directive /api/common/NgForOf
/docs/js/latest/api/common/index/NgFor-directive.html /api/common/NgForOf
/docs/js/latest/api/common/index/NgFor-type-alias /api/common/NgForOf
/docs/js/latest/api/common/index/NgFor-type-alias.html /api/common/NgForOf
/docs/js/latest/api/common/index/NgTemplateOutlet-directive /api/common/NgTemplateOutlet
/docs/js/latest/api/common/NgStyle-directive /api/common/NgStyle
/docs/js/latest/api/core/DynamicComponentLoader-class.html /api/core/DynamicComponentLoader
/docs/js/latest/api/core/HostListener-var /api/core/HostListener
/docs/js/latest/api/core/index/AfterViewChecked-class /api/core/AfterViewChecked
/docs/js/latest/api/core/index/AnimationStateTransitionMetadata-class.html /api/core/AnimationStateTransitionMetadata
/docs/js/latest/api/core/index/AnimationStateTransitionMetadata-type-alias /api/core/AnimationStateTransitionMetadata
/docs/js/latest/api/core/index/ApplicationModule-class /api/core/ApplicationModule
/docs/js/latest/api/core/index/ApplicationRef-class /api/core/ApplicationRef
/docs/js/latest/api/core/index/ChangeDetectorRef-class /api/core/ChangeDetectorRef
/docs/js/latest/api/core/index/ComponentFactory-class /api/core/ComponentFactory
/docs/js/latest/api/core/index/DebugNode-class /api/core/DebugNode
/docs/js/latest/api/core/index/destroyPlatform-function /api/core/destroyPlatform
/docs/js/latest/api/core/index/DirectiveMetadata-class /api/core/Directive
/docs/js/latest/api/core/index/ErrorHandler-class /api/core/ErrorHandler
/docs/js/latest/api/core/index/EventEmitter-class /api/core/EventEmitter
/docs/js/latest/api/core/index/EventEmitter-class.html /api/core/EventEmitter
/docs/js/latest/api/core/index/getPlatform-function /api/core/getPlatform
/docs/js/latest/api/core/index/NgModule-interface /api/core/NgModule
/docs/js/latest/api/core/index/NgModuleRef-class /api/core/NgModuleRef
/docs/js/latest/api/core/index/NgModuleRef-class.html /api/core/NgModuleRef
/docs/js/latest/api/core/index/OnInit-class /api/core/OnInit
/docs/js/latest/api/core/index/OnInit-class.html /api/core/OnInit
/docs/js/latest/api/core/index/OnInit-interface /api/core/OnInit
/docs/js/latest/api/core/index/PlatformRef-class /api/core/PlatformRef
/docs/js/latest/api/core/index/QueryList-class.html /api/core/QueryList
/docs/js/latest/api/core/index/RenderComponentType-class /api/core/RenderComponentType
/docs/js/latest/api/core/index/Renderer2-class.html /api/core/Renderer2
/docs/js/latest/api/core/index/state-function.html /api/core/state
/docs/js/latest/api/core/index/transition-function.html /api/core/transition
/docs/js/latest/api/core/index/trigger-function /api/core/trigger
/docs/js/latest/api/core/index/trigger-function.html /api/core/trigger
/docs/js/latest/api/core/index/ViewChild-decorator /api/core/ViewChild
/docs/js/latest/api/core/index/wtfLeave-let /api/core/wtfLeave
/docs/js/latest/api/core/Injector-class.html /api/core/Injector
/docs/js/latest/api/core/Query-var /api/core/Query
/docs/js/latest/api/core/ResolvedProvider-interface.html /api/core/ResolvedProvider
/docs/js/latest/api/core/testing/index/TestBed-class /api/core/testing/TestBed
/docs/js/latest/api/core/testing/index/tick-function /api/core/testing/tick
/docs/js/latest/api/core/testing/index/tick-function.html /api/core/testing/tick
/docs/js/latest/api/core/ViewContainerRef-class.html /api/core/ViewContainerRef
/docs/js/latest/api/forms/index/AbstractControl-class /api/forms/AbstractControl
/docs/js/latest/api/forms/index/AbstractControl-class.html /api/forms/AbstractControl
/docs/js/latest/api/forms/index/FormArray-class.html /api/forms/FormArray
/docs/js/latest/api/forms/index/FormBuilder-class.html /api/forms/FormBuilder
/docs/js/latest/api/forms/index/NG_VALIDATORS-let /api/forms/NG_VALIDATORS
/docs/js/latest/api/forms/index/Validator-interface.html /api/forms/Validator
/docs/js/latest/api/http/ConnectionBackend-class /api/http/ConnectionBackend
/docs/js/latest/api/http/index/Http-class.html /api/http/Http
/docs/js/latest/api/http/index/Jsonp-class.html /api/http/Jsonp
/docs/js/latest/api/http/index/ResponseOptions-class.html /api/http/ResponseOptions
/docs/js/latest/api/http/index/URLSearchParams-class /api/http/URLSearchParams
/docs/js/latest/api/http/index/XHRConnection-class /api/http/XHRConnection
/docs/js/latest/api/http/index/XHRConnection-class.html /api/http/XHRConnection
/docs/js/latest/api/http/testing/index/MockConnection-class.html /api/http/testing/MockConnection
/docs/js/latest/api/http/testing/MockBackend-class /api/http/testing/MockBackend
/docs/js/latest/api/platform-browser-dynamic/index/platformBrowserDynamic-let.html /api/platform-browser-dynamic/platformBrowserDynamic
/docs/js/latest/api/platform-browser-dynamic/testing/index/BrowserDynamicTestingModule-class.html /api/platform-browser-dynamic/testing/BrowserDynamicTestingModule
/docs/js/latest/api/platform-browser/animations/index/BrowserAnimationsModule-class /api/platform-browser/animations/BrowserAnimationsModule
/docs/js/latest/api/platform-browser/animations/index/BrowserAnimationsModule-class.html /api/platform-browser/animations/BrowserAnimationsModule
/docs/js/latest/api/platform-browser/animations/index/NoopAnimationsModule-class.html /api/platform-browser/animations/NoopAnimationsModule
/docs/js/latest/api/platform-browser/index/DomSanitizer-class /api/platform-browser/DomSanitizer
/docs/js/latest/api/platform-browser/index/Meta-class /api/platform-browser/Meta
/docs/js/latest/api/platform-browser/index/MetaDefinition-type-alias /api/platform-browser/MetaDefinition
/docs/js/latest/api/platform-browser/index/SafeUrl-interface /api/platform-browser/SafeUrl
/docs/js/latest/api/platform-browser/index/Title-class /api/platform-browser/Title
/docs/js/latest/api/platform-browser/index/Title-class.html /api/platform-browser/Title
/docs/js/latest/api/platform-server/index/PlatformState-class /api/platform-server/PlatformState
/docs/js/latest/api/platform-server/index/PlatformState-class.html /api/platform-server/PlatformState
/docs/js/latest/api/platform-webworker /api/platform-webworker
/docs/js/latest/api/platform-webworker/index/MessageBus-class /api/platform-webworker/MessageBus
/docs/js/latest/api/router/index/ActivatedRoute-interface /api/router/ActivatedRoute
/docs/js/latest/api/router/index/CanActivate-interface.html /api/router/CanActivate
/docs/js/latest/api/router/index/CanActivateChild-interface /api/router/CanActivateChild
/docs/js/latest/api/router/index/CanDeactivate-interface.html /api/router/CanDeactivate
/docs/js/latest/api/router/index/CanLoad-interface /api/router/CanLoad
/docs/js/latest/api/router/index/CanLoad-interface.html /api/router/CanLoad
/docs/js/latest/api/router/index/NavigationEnd-class /api/router/NavigationEnd
/docs/js/latest/api/router/index/provideRoutes-function /api/router/provideRoutes
/docs/js/latest/api/router/index/provideRoutes-function.html /api/router/provideRoutes
/docs/js/latest/api/router/index/Router-class /api/router/Router
/docs/js/latest/api/router/index/RouterLink-directive /api/router/RouterLink
/docs/js/latest/api/router/index/Routes-type-alias /api/router/Routes
/docs/js/latest/api/router/index/UrlSegment-class /api/router/UrlSegment
/docs/js/latest/api/router/index/UrlSerializer-class.html /api/router/UrlSerializer
/docs/js/latest/api/router/Instruction-class /api/router/Instruction
/docs/js/latest/api/router/Location-class /api/router/Location
/docs/js/latest/api/router/OnActivate-interface /api/router/OnActivate
/docs/js/latest/api/router/Redirect-class.html /api/router/Redirect
/docs/js/latest/api/router/RouteDefinition-interface.html /api/router/RouteDefinition
/docs/js/latest/api/router/RouteParams-class /api/router/RouteParams
/docs/js/latest/api/router/RouteParams-class.html /api/router/RouteParams
/docs/js/latest/api/router/Router-class.html /api/router/Router
/docs/js/latest/api/router/testing /api/router/testing
/docs/js/latest/api/upgrade/index/UpgradeAdapter-class.html /api/upgrade/UpgradeAdapter
/docs/js/latest/api/upgrade/static/UpgradeModule-class /api/upgrade/static/UpgradeModule
/docs/js/latest/api/upgrade/static/UpgradeModule-class.html /api/upgrade/static/UpgradeModule
/docs/js/latest/cookbook/ts-to-js.html https://github.com/angular/angular/blob/master/aio/content/guide/change-log.md#es6--described-in-typescript-to-javascript-2016-11-14
/docs/js/latest/glossary /guide/glossary
/docs/js/latest/guide/ /docs
/docs/js/latest/guide/lifecycle-hooks /guide/lifecycle-hooks
/docs/js/latest/guide/ngmodule /guide/ngmodules
/docs/js/latest/resources /resources
/docs/latest/tutorial /tutorial
/styleguide /guide/styleguide
/docs/styleguide /guide/styleguide
/docs/styleguide.html /guide/styleguide
/docs/ts/latest/api/core/HostBinding-var.html /api/core/HostBinding
/docs/ts/latest/api/core/index/BaseException-class.html /api/core/BaseException
/docs/ts/latest/api/core/index/PLATFORM_PIPES-let.html /api/common/CommonModule
/docs/ts/latest/api/core/OnInit-interface.html /api/core/OnInit
/docs/ts/latest/api/core/OpaqueToken-class.html /api/core/OpaqueToken
/docs/ts/latest/api/core/OptionalMetadata-class.html /api/core/Optional
/docs/ts/latest/api/core/testing/index/async-function.html /api/core/testing/async
/docs/ts/latest/api/core/testing/index/fakeAsync-function.html /api/core/testing/fakeAsync
/docs/ts/latest/api/core/testing/index/TestComponentRenderer-class.html /api/core/testing/TestComponentRenderer
/docs/ts/latest/api/core/testing/index/tick-function.html /api/core/testing/tick
/docs/ts/latest/api/http/Connection-class.html /api/http/Connection
/docs/ts/latest/api/http/testing/index/MockBackend-class.html /api/http/testing/MockBackend
/docs/ts/latest/api/http/testing/index/MockConnection-class.html /api/http/testing/MockConnection
/docs/ts/latest/api/platform-browser-dynamic/index/workerAppDynamicPlatform-let.html /api/platform-browser-dynamic/workerAppDynamicPlatform
/docs/ts/latest/api/testing/fakeAsync-function.html /api/core/testing/fakeAsync
/docs/ts/latest/cookbook/ts-to-js.html https://github.com/angular/angular/blob/master/aio/content/guide/change-log.md#es6--described-in-typescript-to-javascript-2016-11-14
/guide/cli-quickstart /guide/quickstart
/guide/learning-angular /guide/quickstart
/guide/learning-angular.html /guide/quickstart
/guide/metadata /guide/aot-compiler
/guide/service-worker-getstart /guide/service-worker-getting-started
/guide/service-worker-comm /guide/service-worker-communications
/guide/service-worker-configref /guide/service-worker-config
/news https://blog.angular.io/
/news.html https://blog.angular.io/
/testing /guide/testing
/testing/first-app-tests.html /guide/testing
+49
View File
@@ -0,0 +1,49 @@
const { readFileSync } = require('fs');
const path = require('canonical-path');
const cjson = require('cjson');
import { FirebaseRedirector, FirebaseRedirectConfig } from '../../tools/firebase-test-utils/FirebaseRedirector';
export function getRedirector() {
return new FirebaseRedirector(loadRedirects());
}
export function loadRedirects(): FirebaseRedirectConfig[] {
const pathToFirebaseJSON = path.resolve(__dirname, '../../firebase.json');
const contents = cjson.load(pathToFirebaseJSON);
return contents.hosting.redirects;
}
export function loadSitemapUrls() {
const pathToSiteMap = path.resolve(__dirname, '../../src/generated/sitemap.xml');
const xml = readFileSync(pathToSiteMap, 'utf8');
const urls: string[] = [];
xml.replace(/<loc>([^<]+)<\/loc>/g, (_, loc) => urls.push(loc.replace('%%DEPLOYMENT_HOST%%', '')));
return urls;
}
export function loadLegacyUrls() {
const pathToLegacyUrls = path.resolve(__dirname, 'URLS_TO_REDIRECT.txt');
const urls = readFileSync(pathToLegacyUrls, 'utf8').split('\n').map(line => line.split('\t'));
return urls;
}
export function loadSWRoutes() {
const pathToSWManifest = path.resolve(__dirname, '../../ngsw-manifest.json');
const contents = cjson.load(pathToSWManifest);
const routes = contents.routing.routes;
return Object.keys(routes).map(route => {
const routeConfig = routes[route];
switch (routeConfig.match) {
case 'exact':
return (url) => url === route;
case 'prefix':
return (url) => url.startsWith(route);
case 'regex':
const regex = new RegExp(route);
return (url) => regex.test(url);
default:
throw new Error(`unknown route config: ${route} - ${routeConfig.match}`);
}
});
}
@@ -0,0 +1,31 @@
import { getRedirector, loadLegacyUrls, loadRedirects, loadSitemapUrls } from './helpers';
describe('firebase.json redirect config', () => {
describe('with sitemap urls', () => {
loadSitemapUrls().forEach(url => {
it('should not redirect any urls in the sitemap', () => {
expect(getRedirector().redirect(url)).toEqual(url);
});
});
});
describe('with legacy urls', () => {
loadLegacyUrls().forEach(urlPair => {
it('should redirect the legacy urls', () => {
const redirector = getRedirector();
expect(redirector.redirect(urlPair[0])).not.toEqual(urlPair[0]);
if (urlPair[1]) {
expect(redirector.redirect(urlPair[0])).toEqual(urlPair[1]);
}
});
});
describe('destinations', () => {
loadRedirects().forEach(redirect => {
it('should match pattern "^(https?:/)?/.*"', () => {
expect(redirect.destination).toMatch(/^(https?:\/)?\/.*/);
});
});
});
});
});
@@ -0,0 +1,33 @@
import { loadLegacyUrls, loadSitemapUrls, loadSWRoutes } from './helpers';
describe('service-worker routes', () => {
loadSitemapUrls().forEach(url => {
it('should process URLs in the Sitemap', () => {
const routes = loadSWRoutes();
expect(routes.some(test => test(url))).toBeTruthy(url);
});
});
loadLegacyUrls().forEach(urlPair => {
const url = urlPair[0];
it('should ignore legacy URLs that will be redirected', () => {
const routes = loadSWRoutes();
expect(routes.some(test => test(url))).toBeFalsy(url);
});
});
it('should ignore stackblitz URLs', () => {
const routes = loadSWRoutes();
expect(routes.some(test => test('/generated/live-examples/toh-pt6/stackblitz.html'))).toBeFalsy();
expect(routes.some(test => test('/generated/live-examples/toh-pt6/stackblitz'))).toBeFalsy();
});
it('should ignore URLs to files with extensions', () => {
const routes = loadSWRoutes();
expect(routes.some(test => test('/generated/zips/animations/animations.zip'))).toBeFalsy();
expect(routes.some(test => test('/generated/images/guide/animations/animation_auto.gif'))).toBeFalsy();
expect(routes.some(test => test('/generated/ie-polyfills.min.js'))).toBeFalsy();
expect(routes.some(test => test('/generated/docs/guide/animations.json'))).toBeFalsy();
});
});
+203
View File
@@ -0,0 +1,203 @@
import { browser } from 'protractor';
import { SitePage } from './app.po';
/* tslint:disable:max-line-length */
describe('onerror handler', function() {
let page: SitePage;
beforeAll(() => {
page = new SitePage();
page.navigateTo('');
});
it('(called without an error object) should call ga with a payload based on the message, url, row and column arguments', async () => {
const message1 = await callOnError('Error: some error message', 'some-file.js', 12, 3, undefined);
expect(message1).toEqual('some error message\nsome-file.js:12:3');
const message2 = await callOnError('Error: some error message', undefined, undefined, undefined, undefined);
expect(message2).toEqual('some error message\nnull:?:?');
});
it('(called without an error object) should call ga with a payload that is no longer that 150 characters', async () => {
const message = await callOnError(
'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz' +
'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz',
'some-file.js', 12, 3, undefined);
expect(message).toEqual(
'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz' +
'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrst');
});
it('(called with a Firefox on android style error) should call ga with a payload based on the error object', async () => {
const message = await callOnError('Error: something terrible has happened. oh no. oh no.', undefined, undefined, undefined, {
stack: `AppComponent@https://example.com/app/app.component.ts:31:29
createClass@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:12200:20
createDirectiveInstance@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:12049:37
createViewNodes@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:13487:53
createRootView@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:13377:5
callWithDebugContext@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:14778:39
debugCreateRootView@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:14079:12
ComponentFactory_.prototype.create@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:10998:37
ComponentFactoryBoundToModule.prototype.create@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:3958:16
ApplicationRef.prototype.bootstrap@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:5769:40
PlatformRef.prototype._moduleDoBootstrap/<@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:5496:74
PlatformRef.prototype._moduleDoBootstrap@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:5496:13
PlatformRef.prototype.bootstrapModuleFactory/</</<@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:5417:21
ZoneDelegate.prototype.invoke@https://example.com/packages/zone.js@0.8.18/dist/zone.js:392:17
forkInnerZoneWithAngularBehavior/zone._inner<.onInvoke@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:4665:24
ZoneDelegate.prototype.invoke@https://example.com/packages/zone.js@0.8.18/dist/zone.js:391:17
Zone.prototype.run@https://example.com/packages/zone.js@0.8.18/dist/zone.js:142:24
scheduleResolveOrReject/<@https://example.com/packages/zone.js@0.8.18/dist/zone.js:873:52
ZoneDelegate.prototype.invokeTask@https://example.com/packages/zone.js@0.8.18/dist/zone.js:425:17
forkInnerZoneWithAngularBehavior/zone._inner<.onInvokeTask@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:4656:24
ZoneDelegate.prototype.invokeTask@https://example.com/packages/zone.js@0.8.18/dist/zone.js:424:17
Zone.prototype.runTask@https://example.com/packages/zone.js@0.8.18/dist/zone.js:192:28
drainMicroTaskQueue@https://example.com/packages/zone.js@0.8.18/dist/zone.js:602:25` });
expect(message).toEqual(`something terrible has happened. oh no. oh no.
AppComponent@app.component.ts:31:29
createClass@core.umd.js:12200:20
createDirectiveInstance@core.umd.j`);
});
it('(called with a Safari 11 style error) should call ga with a payload based on the error object', async () => {
const message = await callOnError('Error: something terrible has happened. oh no. oh no.', undefined, undefined, undefined, {
stack: `AppComponent
createClass
createDirectiveInstance
createViewNodes
createRootView
callWithDebugContext
create
bootstrap
forEach@[native code]
_moduleDoBootstrap
onInvoke
run
onInvokeTask
runTask
drainMicroTaskQueue
promiseReactionJob@[native code]` });
expect(message).toEqual(`something terrible has happened. oh no. oh no.
AppComponent
createClass
createDirectiveInstance
createViewNodes
createRootView
callWithDebugContext
cr`);
});
it('(called with a Opera 50 style error) should call ga with a payload based on the error object', async () => {
const message = await callOnError('Error: something terrible has happened. oh no. oh no.', undefined, undefined, undefined, {
stack: `Error: something terrible has happened. oh no. oh no.
at new AppComponent (https://example.com/app/app.component.ts:31:29)
at createClass (https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:12200:20)
at createDirectiveInstance (https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:12049:37)
at createViewNodes (https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:13487:53)
at createRootView (https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:13377:5)
at callWithDebugContext (https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:14778:42)
at Object.debugCreateRootView [as createRootView] (https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:14079:12)
at ComponentFactory_.create (https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:10998:46)
at ComponentFactoryBoundToModule.create (https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:3958:29)
at ApplicationRef.bootstrap (https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:5769:57)` });
expect(message).toEqual(`something terrible has happened. oh no. oh no.
new AppComponent@app.component.ts:31:29
createClass@core.umd.js:12200:20
createDirectiveInstance@core.u`);
});
it('(called with a Chrome 64 style error) should call ga with a payload based on the error object', async () => {
const message = await callOnError('Error: something terrible has happened. oh no. oh no.', undefined, undefined, undefined, {
stack: `Error: something terrible has happened. oh no. oh no.
at new AppComponent (https://example.com/app/app.component.ts:31:29)
at createClass (https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:12200:20)
at createDirectiveInstance (https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:12049:37)
at createViewNodes (https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:13487:53)
at createRootView (https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:13377:5)
at callWithDebugContext (https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:14778:42)
at Object.debugCreateRootView [as createRootView] (https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:14079:12)
at ComponentFactory_.create (https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:10998:46)
at ComponentFactoryBoundToModule.create (https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:3958:29)
at ApplicationRef.bootstrap (https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:5769:57)` });
expect(message).toEqual(`something terrible has happened. oh no. oh no.
new AppComponent@app.component.ts:31:29
createClass@core.umd.js:12200:20
createDirectiveInstance@core.u`);
});
it('(called with a Firefox 58 style error) should call ga with a payload based on the error object', async () => {
const message = await callOnError('Error: something terrible has happened. oh no. oh no.', undefined, undefined, undefined, {
stack: `AppComponent@https://example.com/app/app.component.ts:31:29
createClass@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:12200:20
createDirectiveInstance@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:12049:37
createViewNodes@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:13487:53
createRootView@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:13377:5
callWithDebugContext@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:14778:39
debugCreateRootView@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:14079:12
ComponentFactory_.prototype.create@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:10998:37
ComponentFactoryBoundToModule.prototype.create@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:3958:16
ApplicationRef.prototype.bootstrap@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:5769:40
PlatformRef.prototype._moduleDoBootstrap/<@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:5496:74
PlatformRef.prototype._moduleDoBootstrap@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:5496:13
PlatformRef.prototype.bootstrapModuleFactory/</</<@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:5417:21
ZoneDelegate.prototype.invoke@https://example.com/packages/zone.js@0.8.18/dist/zone.js:392:17
onInvoke@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:4665:24
ZoneDelegate.prototype.invoke@https://example.com/packages/zone.js@0.8.18/dist/zone.js:391:17
Zone.prototype.run@https://example.com/packages/zone.js@0.8.18/dist/zone.js:142:24
scheduleResolveOrReject/<@https://example.com/packages/zone.js@0.8.18/dist/zone.js:873:52
ZoneDelegate.prototype.invokeTask@https://example.com/packages/zone.js@0.8.18/dist/zone.js:425:17
onInvokeTask@https://example.com/packages/@angular/core@5.0.0/bundles/core.umd.js:4656:24
ZoneDelegate.prototype.invokeTask@https://example.com/packages/zone.js@0.8.18/dist/zone.js:424:17
Zone.prototype.runTask@https://example.com/packages/zone.js@0.8.18/dist/zone.js:192:28
drainMicroTaskQueue@https://example.com/packages/zone.js@0.8.18/dist/zone.js:602:25` });
expect(message).toEqual(`something terrible has happened. oh no. oh no.
AppComponent@app.component.ts:31:29
createClass@core.umd.js:12200:20
createDirectiveInstance@core.umd.j`);
});
it('(called with a Edge 16 style error) should call ga with a payload based on the error object', async () => {
const message = await callOnError('Error: something terrible has happened. oh no. oh no.', undefined, undefined, undefined, {
stack: `Error: something terrible has happened. oh no. oh no.
at AppComponent (eval code:31:21)
at createClass (eval code:12200:13)
at createDirectiveInstance (eval code:12049:5)
at createViewNodes (eval code:13487:21)
at createRootView (eval code:13377:5)
at callWithDebugContext (eval code:14778:9)
at debugCreateRootView (eval code:14079:5)
at ComponentFactory_.prototype.create (eval code:10998:9)
at ComponentFactoryBoundToModule.prototype.create (eval code:3958:9)
at ApplicationRef.prototype.bootstrap (eval code:5769:9)` });
expect(message).toEqual(`something terrible has happened. oh no. oh no.
AppComponent@???:31:21
createClass@???:12200:13
createDirectiveInstance@???:12049:5
createViewNodes@???`);
});
async function callOnError(message, url, line, column, error) {
await browser.executeScript(function() {
// reset the ga queue
(window as any).ga.q.length = 0;
// post the error to the handler
window.onerror(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4]);
}, message, url, line, column, error);
const gaCalls = await page.ga();
const exceptionCall = gaCalls.find(call => call[0] === 'send' && call[1] === 'exception');
if (exceptionCall) {
const payload = exceptionCall[2];
expect(payload.exFatal).toBe(true);
return payload.exDescription;
}
}
});
@@ -1,7 +1,7 @@
{
"scripts": [
{ "name": "ng", "command": "ng" },
{ "name": "build", "command": "ng build" },
{ "name": "build", "command": "ng build --prod" },
{ "name": "start", "command": "ng serve" },
{ "name": "test", "command": "ng test" },
{ "name": "lint", "command": "ng lint" },
+28 -44
View File
@@ -1,74 +1,57 @@
# Overview
Many of the documentation pages contain snippets of code examples. We extract these snippets from
Many of the documentation pages contain snippets of code examples. Extract these snippets from
real working example applications, which are stored in subfolders of the `/aio/content/examples`
folder. Each example can be built and run independently. Each example also provides e2e specs, which
are run as part of our Travis build tasks, to verify that the examples continue to work as expected,
as changes are made to the core Angular libraries.
In order to build, run and test these examples independently we need to install dependencies into
In order to build, run and test these examples independently you need to install dependencies into
their sub-folder. Also there are a number of common boilerplate files that are needed to configure
each example's project. We maintain these common boilerplate files centrally to reduce the amount
each example's project. Maintain these common boilerplate files centrally to reduce the amount
of effort if one of them needs to change.
## Boilerplate overview
As mentioned, many of the documentation pages contain snippets extracted from real example applications.
To achieve that, all those applications needs to contain a basic boilerplate. E.g. a `node_modules`
folder, `package.json` with scripts, `system.js` configuration, etc.
folder, `package.json` with scripts, etc.
No one wants to maintain the boilerplate on each example, so the goal of this tool is to provide a
generic boilerplate that works in all the examples.
set of boilerplates that works in all the examples.
### Boilerplate files
Inside `/aio/tools/examples/shared/boilerplate` you will see all the common boilerplate you can find
in any Angular application using System.js. This is the boilerplate that will be carried to each example.
Inside `/aio/tools/examples/shared/boilerplate` you will find a set of folders representing each
boilerplate.
Among these files, there are a few special ones:
Currently you will find the next boilerplates:
* **src/systemjs.config.js** - This is the configuration of System.js used to run the example locally.
* **src/systemjs.config.web.js** - This configuration replaces the previous one on Stackblitz.
* **src/systemjs.config.web.build.js** - Same as the previous one but for using angular's `-builds`
versions.
* **src/systemjs-angular-loader.js** - It is a System.js plugin that removes the need of `moduleId`.
* **package.json** - This package.json only contains scripts, no dependencies. It contains the
different tasks needed to run any example. Doesn't matter if CLI, System.js or Webpack.
* **stackblitz.json** - This file is used by the Stackblitz tool to generate a stackblitz for an example. This
concrete file is just a placeholder. Authors needs to tweak it for each guide. More at the
[stackblitz docs](../stackblitz-builder/README.md).
* **example-config.json** - This file serves as a flag to indicate that the current folder is an
example. This concrete file is just a placeholder. More on this later in this readme.
* CLI - For CLI based examples. This is the default one, to be used in the majority of the examples.
* systemjs - Currently in deprecation, only used in a a few examples.
* i18n - Based on the CLI one, features a few scripts for i18n.
* universal - Based on the cli with a extra server for universal.
There is also a `common` folder that contains files used in all different examples.
### The example-config.json
So what is this **example-config.json** again? If an author wants to create a new example, say
`/aio/content/examples/awesome-example`. The author needs to create an empty `example-config.json`
in that folder. That serves as a flag so this tool will say "Hey, that is an example, let's copy
all the boilerplate there".
So when the tool runs, it finds **all** the folders with an `example-config.json` file, and puts
a copy of the boilerplate in those folders.
Normally the file is empty, but we can add information in it, for example:
Each example is identified by an **example-config.json** configuration file in its root folder.
This configuration file indicates what type of boilerplate this example needs. E.g.
```json
{
"build": "build:cli",
"run": "serve:cli"
}
{ projectType: 'universal' }
```
In this case, this would indicate that this is a CLI example. Won't make any difference on the
boilerplate, but will be useful for e2e tests (more on this later). Also works as a hint for
the example to know how is executed.
If the file is empty then the default type of cli is assumed.
When the boilerplate tooling runs, it will copy into the example folder all of the appropriate boilerplate files.
### A node_modules to share
With all the boilerplate files in place, the only missing piece are the installed packages. For
that we have a `/aio/tools/examples/shared/package.json` which contains **all** the packages
needed to run all the examples.
that you have a `/aio/tools/examples/shared/package.json` which contains **all** the packages
needed to run all the examples through all different boilerplates.
After installing these dependencies, a `node_modules` will be created at
`/aio/tools/examples/shared/node_modules`. This folder will be **symlinked** into each example.
@@ -77,12 +60,13 @@ may require admin rights.
### End to end tests
Each example contains an `e2e-spec.ts` file. We can find all the related configuration files for
e2e in the `/aio/tools/examples/shared` folder.
End to end changes between boilerplates.
This tool expects all the examples to be build with `npm run build`. If an example is not built
with that script, the author would need to specify the new build command in the `example-config.json`
as shown earlier.
For CLI applications, create a `app.e2e-spec.ts` inside the `e2e` folder. The tooling will run
`ng e2e` for each CLI based examples.
For SystemJS, each example contains an `e2e-spec.ts` file. You can find all the related configuration files
in the `/aio/tools/examples/shared` folder.
### example-boilerplate.js
@@ -0,0 +1,53 @@
# How to update the CLI boilerplate
The boilerplate is updated by hand so you normally update it every minor version unless there is a major bug to fix.
## Getting a new boilerplate
The first thing would be updating the CLI globally
```
npm i -g @angular/cli
```
Then create a new dummy project in a temporary folder outside angular
```
ng new dummy
```
Now you have a fresh application to get our new boilerplate files.
## Updating files
From `dummy` you can replace the following files into `aio/tools/examples/shared/boilerplate/cli`:
* tslint.json
* tsconfig.json
* package.json
* protractor.conf.js
* karma.conf.js
* .editorconfig
* angular-cli.json
* src/tsconfig.spec.json
* src/test.ts
* src/polyfills.js
* src/typings.d.ts
* src/environments/environment.prod.ts
* src/environments/environment.ts
### .angular-cli.json
Update the `project > name` to `angular.io-example`.
### package.json
Update the `name` to `angular.io-example`.
### src/polyfills.ts
Uncomment the `import 'web-animations-js';` line to enable `web-animations-js` package.
### src/tsconfig.app.json
This file is small enough and there are a few new excludes, update by hand.
@@ -5,46 +5,45 @@
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"build": "ng build --prod",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "^5.0.0",
"@angular/common": "^5.0.0",
"@angular/compiler": "^5.0.0",
"@angular/core": "^5.0.0",
"@angular/forms": "^5.0.0",
"@angular/http": "^5.0.0",
"@angular/platform-browser": "^5.0.0",
"@angular/platform-browser-dynamic": "^5.0.0",
"@angular/router": "^5.0.0",
"@angular/animations": "^5.2.0",
"@angular/common": "^5.2.0",
"@angular/compiler": "^5.2.0",
"@angular/core": "^5.2.0",
"@angular/forms": "^5.2.0",
"@angular/http": "^5.2.0",
"@angular/platform-browser": "^5.2.0",
"@angular/platform-browser-dynamic": "^5.2.0",
"@angular/router": "^5.2.0",
"angular-in-memory-web-api": "~0.5.0",
"core-js": "^2.4.1",
"rxjs": "^5.5.2",
"zone.js": "^0.8.14"
"rxjs": "^5.5.6",
"zone.js": "^0.8.19"
},
"devDependencies": {
"@angular/cli": "1.5.4",
"@angular/compiler-cli": "^5.0.0",
"@angular/language-service": "^5.0.0",
"@types/jasmine": "~2.8.0",
"@angular/cli": "1.6.5",
"@angular/compiler-cli": "^5.2.0",
"@angular/language-service": "^5.2.0",
"@types/jasmine": "~2.8.3",
"@types/jasminewd2": "~2.0.2",
"@types/node": "~6.0.60",
"codelyzer": "^4.0.1",
"jasmine-core": "~2.8.0",
"jasmine-spec-reporter": "~4.1.0",
"karma": "~1.7.0",
"karma-chrome-launcher": "~2.1.1",
"karma-cli": "~1.0.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~2.0.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "^1.2.1",
"karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.1.2",
"ts-node": "~3.2.0",
"tslint": "~5.7.0",
"typescript": "~2.4.2"
"ts-node": "~4.1.0",
"tslint": "~5.9.1",
"typescript": "~2.5.3"
}
}
@@ -55,7 +55,7 @@ import 'core-js/es7/reflect';
/***************************************************************************************************
* Zone JS is required by Angular itself.
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
@@ -64,13 +64,3 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
/**
* Date, currency, decimal and percent pipes.
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
*/
// import 'intl'; // Run `npm install --save intl`.
/**
* Need to import at least one locale-data with intl.
*/
// import 'intl/locale-data/jsonp/en';
@@ -11,6 +11,9 @@
"check-space"
],
"curly": true,
"deprecation": {
"severity": "warn"
},
"eofline": true,
"forin": true,
"import-blacklist": [
@@ -127,6 +130,7 @@
"app",
"kebab-case"
],
"no-output-on-prefix": true,
"use-input-property-decorator": true,
"use-output-property-decorator": true,
"use-host-property-decorator": true,
@@ -135,7 +139,6 @@
"use-life-cycle-interface": true,
"use-pipe-transform-interface": true,
"component-class-suffix": true,
"directive-class-suffix": true,
"invoke-injectable": true
"directive-class-suffix": true
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

@@ -0,0 +1,189 @@
import { FirebaseGlob } from './FirebaseGlob';
describe('FirebaseGlob', () => {
describe('test', () => {
it('should match * parts', () => {
testGlob('asdf/*.jpg',
['asdf/.jpg', 'asdf/asdf.jpg', 'asdf/asdf_asdf.jpg'],
['asdf/asdf/asdf.jpg', 'xxxasdf/asdf.jpgxxx']);
});
it('should match ** parts', () => {
testGlob('asdf/**.jpg', // treated like two consecutive single `*`s
['asdf/.jpg', 'asdf/asdf.jpg', 'asdf/asdf_asdf.jpg'],
['asdf/a/.jpg', 'asdf/a/b.jpg', '/asdf/asdf.jpg', 'asdff/asdf.jpg', 'xxxasdf/asdf.jpg', 'asdf/asdf.jpgxxx']);
});
it('should match **/ and /**/', () => {
testGlob('**/*.js',
['asdf.js', 'asdf/asdf.js', 'asdf/asdf/asdfasdf_asdf.js', '/asdf/asdf.js', '/asdf/aasdf-asdf.2.1.4.js'],
['asdf/asdf.jpg', '/asdf/asdf.jpg']);
testGlob('aaa/**/bbb',
['aaa/xxx/bbb', 'aaa/xxx/yyy/bbb', 'aaa/bbb'],
['/aaa/xxx/bbb', 'aaa/x/bbb/', 'aaa/bbb/ccc']);
});
it('should match choice groups', () => {
testGlob('aaa/*.@(bbb|ccc)',
['aaa/aaa.bbb', 'aaa/aaa_aaa.ccc'],
['/aaa/aaa.bbb', 'aaaf/aaa.bbb', 'aaa/aaa.ddd']);
testGlob('aaa/*(bbb|ccc)',
['aaa/', 'aaa/bbb', 'aaa/ccc', 'aaa/bbbbbb', 'aaa/bbbccc', 'aaa/cccbbb', 'aaa/bbbcccbbb'],
['aaa/aaa', 'aaa/bbbb']);
testGlob('aaa/+(bbb|ccc)',
['aaa/bbb', 'aaa/ccc', 'aaa/bbbbbb', 'aaa/bbbccc', 'aaa/cccbbb', 'aaa/bbbcccbbb'],
['aaa/', 'aaa/aaa', 'aaa/bbbb']);
testGlob('aaa/?(bbb|ccc)',
['aaa/', 'aaa/bbb', 'aaa/ccc'],
['aaa/aaa', 'aaa/bbbb', 'aaa/bbbbbb', 'aaa/bbbccc', 'aaa/cccbbb', 'aaa/bbbcccbbb']);
});
it('should error on non-supported choice groups', () => {
expect(() => new FirebaseGlob('/!(a|b)/c'))
.toThrowError('Error in FirebaseGlob: "/!(a|b)/c" - "not" expansions are not supported: "!(a|b)"');
expect(() => new FirebaseGlob('/(a|b)/c'))
.toThrowError('Error in FirebaseGlob: "/(a|b)/c" - unknown expansion type: "/" in "/(a|b)"');
expect(() => new FirebaseGlob('/&(a|b)/c'))
.toThrowError('Error in FirebaseGlob: "/&(a|b)/c" - unknown expansion type: "&" in "&(a|b)"');
});
// Globs that contain params tested via the match tests below
});
describe('match', () => {
it('should match patterns with no parameters', () => {
testMatch('/abc/def/*', {
}, {
'/abc/def/': {},
'/abc/def/ghi': {},
'/': undefined,
'/abc': undefined,
'/abc/def/ghi/jk;': undefined,
});
});
it('should capture a simple named param', () => {
testMatch('/:abc', {
named: ['abc']
}, {
'/a': {abc: 'a'},
'/abc': {abc: 'abc'},
'/': undefined,
'/a/': undefined,
'/a/b/': undefined,
'/a/a/b': undefined,
'/a/a/b/': undefined,
});
testMatch('/a/:b', {
named: ['b']
}, {
'/a/b': {b: 'b'},
'/a/bcd': {b: 'bcd'},
'/a/': undefined,
'/a/b/': undefined,
'/a': undefined,
'/a//': undefined,
'/a/a/b': undefined,
'/a/a/b/': undefined,
});
});
it('should capture a named param followed by non-word chars', () => {
testMatch('/a/:x-', {
named: ['x']
}, {
'/a/b-': {x: 'b'},
'/a/bcd-': {x: 'bcd'},
'/a/--': {x: '-'},
'/a': undefined,
'/a/-': undefined,
'/a/-/': undefined,
'/a/': undefined,
'/a/b/-': undefined,
'/a/b-c': undefined,
});
});
it('should capture multiple named params', () => {
testMatch('/a/:b/:c', {
named: ['b', 'c']
}, {
'/a/b/c': {b: 'b', c: 'c'},
'/a/bcd/efg': {b: 'bcd', c: 'efg'},
'/a/b/c-': {b: 'b', c: 'c-'},
'/a/': undefined,
'/a/b/': undefined,
'/a/b/c/': undefined,
});
testMatch('/:a/b/:c', {
named: ['a', 'c']
}, {
'/a/b/c': {a: 'a', c: 'c'},
'/abc/b/efg': {a: 'abc', c: 'efg'},
'/a/b/c-': {a: 'a', c: 'c-'},
'/a/': undefined,
'/a/b/': undefined,
'/a/b/c/': undefined,
});
});
it('should capture a simple rest param', () => {
testMatch('/:abc*', {
rest: ['abc']
}, {
'/a': {abc: 'a'},
'/a/b': {abc: 'a/b'},
'/a/bcd': {abc: 'a/bcd'},
'/a/': {abc: 'a/'},
'/a/b/': {abc: 'a/b/'},
'/a//': {abc: 'a//'},
'/a/b/c': {abc: 'a/b/c'},
'/a/b/c/': {abc: 'a/b/c/'},
});
testMatch('/a/:b*', {
rest: ['b']
}, {
'/a/b': {b: 'b'},
'/a/bcd': {b: 'bcd'},
'/a/': {b: ''},
'/a/b/': {b: 'b/'},
'/a': {b: undefined},
'/a//': {b: '/'},
'/a/a/b': {b: 'a/b'},
'/a/a/b/': {b: 'a/b/'},
});
});
it('should capture a rest param mixed with a named param', () => {
testMatch('/:abc/:rest*', {
named: ['abc'],
rest: ['rest']
}, {
'/a': {abc: 'a', rest: undefined},
'/a/b': {abc: 'a', rest: 'b'},
'/a/bcd': {abc: 'a', rest: 'bcd'},
'/a/': {abc: 'a', rest: ''},
'/a/b/': {abc: 'a', rest: 'b/'},
'/a//': {abc: 'a', rest: '/'},
'/a/b/c': {abc: 'a', rest: 'b/c'},
'/a/b/c/': {abc: 'a', rest: 'b/c/'},
});
});
});
});
function testGlob(pattern: string, matches: string[], nonMatches: string[]) {
const glob = new FirebaseGlob(pattern);
matches.forEach(url => expect(glob.test(url)).toBe(true, url));
nonMatches.forEach(url => expect(glob.test(url)).toBe(false, url));
}
function testMatch(pattern: string, captures: { named?: string[], rest?: string[] }, matches: { [url: string]: object|undefined }) {
const glob = new FirebaseGlob(pattern);
expect(Object.keys(glob.namedParams)).toEqual(captures.named || []);
expect(Object.keys(glob.restParams)).toEqual(captures.rest || []);
Object.keys(matches).forEach(url => expect(glob.match(url)).toEqual(matches[url]));
}
@@ -0,0 +1,74 @@
import * as XRegExp from 'xregexp';
const dot = /\./g;
const star = /\*/g;
const doubleStar = /(^|\/)\*\*($|\/)/g; // e.g. a/**/b or **/b or a/** but not a**b
const modifiedPatterns = /(.)\(([^)]+)\)/g; // e.g. `@(a|b)
const restParam = /\/:([A-Za-z]+)\*/g; // e.g. `:rest*`
const namedParam = /\/:([A-Za-z]+)/g; // e.g. `:api`
const possiblyEmptyInitialSegments = /^\.🐷\//g; // e.g. `**/a` can also match `a`
const possiblyEmptySegments = /\/\.🐷\//g; // e.g. `a/**/b` can also match `a/b`
const willBeStar = /🐷/g; // e.g. `a**b` not matched by previous rule
export class FirebaseGlob {
pattern: string;
regex: XRegExp;
namedParams: { [key: string]: boolean } = {};
restParams: { [key: string]: boolean } = {};
constructor(glob: string) {
try {
const pattern = glob
.replace(dot, '\\.')
.replace(modifiedPatterns, replaceModifiedPattern)
.replace(restParam, (_, param) => {
// capture the rest of the string
this.restParams[param] = true;
return `(?:/(?<${param}>.🐷))?`;
})
.replace(namedParam, (_, param) => {
// capture the named parameter
this.namedParams[param] = true;
return `/(?<${param}>[^/]+)`;
})
.replace(doubleStar, '$1.🐷$2') // use the pig to avoid replacing ** in next rule
.replace(star, '[^/]*') // match a single segment
.replace(possiblyEmptyInitialSegments, '(?:.*)')// deal with **/ special cases
.replace(possiblyEmptySegments, '(?:/|/.*/)') // deal with /**/ special cases
.replace(willBeStar, '*'); // other ** matches
this.pattern = `^${pattern}$`;
this.regex = XRegExp(this.pattern);
} catch (e) {
throw new Error(`Error in FirebaseGlob: "${glob}" - ${e.message}`);
}
}
test(url: string) {
return XRegExp.test(url, this.regex);
}
match(url: string) {
const match = XRegExp.exec(url, this.regex);
if (match) {
const result = {};
const names = (this.regex as any).xregexp.captureNames || [];
names.forEach(name => result[name] = match[name]);
return result;
}
}
}
function replaceModifiedPattern(_, modifier, pattern) {
switch (modifier) {
case '!':
throw new Error(`"not" expansions are not supported: "${_}"`);
case '?':
case '+':
return `(${pattern})${modifier}`;
case '*':
return `(${pattern})🐷`; // it will become a star
case '@':
return `(${pattern})`;
default:
throw new Error(`unknown expansion type: "${modifier}" in "${_}"`);
}
}
@@ -0,0 +1,30 @@
import { FirebaseRedirect } from './FirebaseRedirect';
describe('FirebaseRedirect', () => {
describe('replace', () => {
it('should return undefined if the redirect does not match the url', () => {
const redirect = new FirebaseRedirect('/a/b/c', '/x/y/z');
expect(redirect.replace('/1/2/3')).toBe(undefined);
});
it('should return the destination if there is a match', () => {
const redirect = new FirebaseRedirect('/a/b/c', '/x/y/z');
expect(redirect.replace('/a/b/c')).toBe('/x/y/z');
});
it('should inject name params into the destination', () => {
const redirect = new FirebaseRedirect('/api/:package/:api-*', '<:package><:api>');
expect(redirect.replace('/api/common/NgClass-directive')).toEqual('<common><NgClass>');
});
it('should inject rest params into the destination', () => {
const redirect = new FirebaseRedirect('/a/:rest*', '/x/:rest*/y');
expect(redirect.replace('/a/b/c')).toEqual('/x/b/c/y');
});
it('should inject both named and rest parameters into the destination', () => {
const redirect = new FirebaseRedirect('/:a/:rest*', '/x/:a/y/:rest*/z');
expect(redirect.replace('/a/b/c')).toEqual('/x/a/y/b/c/z');
});
});
});
@@ -0,0 +1,16 @@
import * as XRegExp from 'xregexp';
import { FirebaseGlob } from './FirebaseGlob';
export class FirebaseRedirect {
glob = new FirebaseGlob(this.source);
constructor(public source: string, public destination: string) {}
replace(url: string) {
const match = this.glob.match(url);
if (match) {
const paramReplacers = Object.keys(this.glob.namedParams).map(name => [ XRegExp(`:${name}`, 'g'), match[name] ]);
const restReplacers = Object.keys(this.glob.restParams).map(name => [ XRegExp(`:${name}\\*`, 'g'), match[name] ]);
return XRegExp.replaceEach(this.destination, [...paramReplacers, ...restReplacers]);
}
}
}
@@ -0,0 +1,42 @@
import { FirebaseRedirector } from './FirebaseRedirector';
describe('FirebaseRedirector', () => {
it('should replace with the first matching redirect', () => {
const redirector = new FirebaseRedirector([
{ source: '/a/b/c', destination: '/X/Y/Z' },
{ source: '/a/:foo/c', destination: '/X/:foo/Z' },
{ source: '/**/:foo/c', destination: '/A/:foo/zzz' },
]);
expect(redirector.redirect('/a/b/c')).toEqual('/X/Y/Z');
expect(redirector.redirect('/a/moo/c')).toEqual('/X/moo/Z');
expect(redirector.redirect('/x/y/a/b/c')).toEqual('/A/b/zzz');
expect(redirector.redirect('/x/y/c')).toEqual('/A/y/zzz');
});
it('should return the original url if no redirect matches', () => {
const redirector = new FirebaseRedirector([
{ source: 'x', destination: 'X' },
{ source: 'y', destination: 'Y' },
{ source: 'z', destination: 'Z' },
]);
expect(redirector.redirect('a')).toEqual('a');
});
it('should recursively redirect', () => {
const redirector = new FirebaseRedirector([
{ source: 'a', destination: 'b' },
{ source: 'b', destination: 'c' },
{ source: 'c', destination: 'd' },
]);
expect(redirector.redirect('a')).toEqual('d');
});
it('should throw if stuck in an infinite loop', () => {
const redirector = new FirebaseRedirector([
{ source: 'a', destination: 'b' },
{ source: 'b', destination: 'c' },
{ source: 'c', destination: 'a' },
]);
expect(() => redirector.redirect('a')).toThrowError('infinite redirect loop');
});
});
@@ -0,0 +1,36 @@
import { FirebaseRedirect } from './FirebaseRedirect';
export interface FirebaseRedirectConfig {
source: string;
destination: string;
}
export class FirebaseRedirector {
private redirects: FirebaseRedirect[];
constructor(redirects: FirebaseRedirectConfig[]) {
this.redirects = redirects.map(redirect => new FirebaseRedirect(redirect.source, redirect.destination));
}
redirect(url: string) {
let ttl = 50;
while (ttl > 0) {
const newUrl = this.doRedirect(url);
if (newUrl === url) {
return url;
} else {
url = newUrl;
ttl--;
}
}
throw new Error('infinite redirect loop');
}
private doRedirect(url: string) {
for (let i = 0; i < this.redirects.length; i++) {
const newUrl = this.redirects[i].replace(url);
if (newUrl !== undefined) {
return newUrl;
}
}
return url;
}
}
@@ -1,5 +1,15 @@
module.exports = function createSitemap() {
return {
blacklistedDocTypes: [
'navigation-json',
'contributors-json',
'resources-json',
],
blacklistedPaths: [
'test',
'file-not-found',
'overview-dump'
],
$runAfter: ['paths-computed'],
$runBefore: ['rendering-docs'],
$process(docs) {
@@ -8,7 +18,16 @@ module.exports = function createSitemap() {
path: 'sitemap.xml',
outputPath: '../sitemap.xml',
template: 'sitemap.template.xml',
urls: docs.filter(doc => doc.outputPath).map(doc => doc.path)
urls: docs
// Filter out docs that are not outputted
.filter(doc => doc.outputPath)
// Filter out unwanted docs
.filter(doc => this.blacklistedDocTypes.indexOf(doc.docType) === -1)
.filter(doc => this.blacklistedPaths.indexOf(doc.path) === -1)
// Capture the path of each doc
.map(doc => doc.path)
// Convert the homepage: `index` to `/`
.map(path => path === 'index' ? '' : path)
});
}
};
@@ -46,6 +46,38 @@ describe('createSitemap processor', () => {
processor.$process(docs);
expect(docs.pop().urls).toEqual(['abc', 'fgh']);
});
it('ignoring blacklisted doc types', () => {
const docs = [
{ path: 'abc', outputPath: 'abc', docType: 'good' },
{ path: 'cde', outputPath: 'cde', docType: 'bad' },
{ path: 'fgh', outputPath: 'fgh', docType: 'good' },
];
processor.blacklistedDocTypes = ['bad'];
processor.$process(docs);
expect(docs.pop().urls).toEqual(['abc', 'fgh']);
});
it('ignoring blacklisted paths', () => {
const docs = [
{ path: 'abc', outputPath: 'abc' },
{ path: 'cde', outputPath: 'cde' },
{ path: 'fgh', outputPath: 'fgh' },
];
processor.blacklistedPaths = ['cde'];
processor.$process(docs);
expect(docs.pop().urls).toEqual(['abc', 'fgh']);
});
it('mapping the home page\'s path to `/`', () => {
const docs = [
{ path: 'abc', outputPath: 'abc' },
{ path: 'index', outputPath: 'index.json' },
{ path: 'fgh', outputPath: 'fgh' },
];
processor.$process(docs);
expect(docs.pop().urls).toEqual(['abc', '', 'fgh']);
});
});
});
});
@@ -97,7 +97,7 @@ module.exports = function generateKeywordsProcessor(log, readFilesProcessor) {
// Special case properties that contain content relating to "members"
// of a doc that represents, say, a class or interface
if (key === 'methods' || key === 'properties' || key === 'events') {
if (key === 'members' || key === 'statics') {
value.forEach(function(member) { extractWords(member.name, members, membersMap); });
}
});

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