Compare commits

..

50 Commits
5.2x ... 5.2.1

Author SHA1 Message Date
af4eb00c91 docs: add changelog for 5.2.1 2018-01-17 09:39:46 -08:00
d3e7ebb3b4 release: cut the 5.2.1 release 2018-01-17 09:37:50 -08:00
420f5c4275 build(bazel): remove spurious file (#21455)
PR Close #21455
2018-01-17 08:24:34 -08:00
b773a4ab98 docs(aio): change df-question to app-question (#21438)
closes: #21404
PR Close #21438
2018-01-17 07:20:52 -08:00
55f15c54d9 docs(aio): add description and docs links for code samples (#21561)
PR Close #21561
2018-01-17 07:19:37 -08:00
4556532c26 feat(core): add binding name to content changed error (#20352)
Adding the binding name to the error message recieved by the user gives
extra context on what exactly changed. The tests are also updated to
reflect the new error message.

PR Close #20352
2018-01-17 07:17:00 -08:00
54e75766ad fix(core): fix chained http call (#20924)
Fixes an issue where chained http calls would prematurely call
testability whenStable callbacks after the first http call.

Fixes #20921

PR Close #20924
2018-01-17 07:14:55 -08:00
d3333f04ba refactor(core): refactor WrappedValue (#20997)
- Improve `WrappedValue` by adding `unwrap` symetrical to `wrap`.
- remove dead code - `ValueUnwrapper`

The property `wrapped` is an implementation details and should never be accessed
directly - use `unwrap(wrappedValue)`. Will change to protected in Angular 7.

PR Close #20997
2018-01-16 07:13:24 -08:00
75f8522b8d feat(forms): handle string with and without line boundary on pattern validator (#19256)
PR Close #19256
2018-01-16 07:12:14 -08:00
a771ee5d90 build(aio): move file cleaning to later in the doc gen (#21540)
Previously the generated files were cleaned out before
doc-gen began (via a yarn pre-script). This can cause a
race condition in the CLI server, which prevents the new
generated files from being picked up.

Now we delay the cleaning until the last minute to ensure
that they ar still picked up by the webpack server.

PR Close #21540
2018-01-16 07:11:01 -08:00
a4cbe3542a ci: disable integration/cli-hello-world test (#21492)
it is non-hermetic and breaks often due to unpinned dependencies.

PR Close #21492
2018-01-12 14:57:45 -08:00
cc9419d1ca fix(language-service): Clear caches when program changes (#21337)
This commit fixes a bug whereby the caches are not cleared when the
program changes. This subsequently produces the incorrect error of
'Component ... is not included in a module ...'.

PR Close #19405

PR Close #21337
2018-01-12 14:43:34 -08:00
d5393c7f91 docs(compiler): document the $any type cast function (#20968)
Closes #20966

PR Close #20968
2018-01-12 14:38:42 -08:00
71dd92bbb8 style(aio): enforce strict TypeScript checks (#21342)
Closes #20646

PR Close #21342
2018-01-12 14:37:02 -08:00
977978edb5 test(common): make date pipe tests work in more timezones (#21379)
Fixes #21112
PR Close #21379
2018-01-12 13:50:06 -08:00
eb70966065 fix(aio): correctly handle redirects (#21416)
- Fixes handling of some redirects by the ServiceWorker.
- Fixes redirect for old `NgFor` to new `NgForOf` URL.

Fixes #21318

PR Close #21416
2018-01-12 13:49:10 -08:00
cf4bea587d ci(aio): run e2e tests in production mode (#21470)
This will enable catching errors introduced by build optimizations that
do not appear in `development` mode.

Fixes #21446

PR Close #21470
2018-01-12 13:46:00 -08:00
8e9cd57951 fix(common): set correct timezone for ISO8601 dates in Safari (#21506)
Fixes #21491
PR Close #21506
2018-01-12 13:21:11 -08:00
f23896f519 docs: fix release schedule date (#21474)
PR Close #21474
2018-01-12 12:10:26 -08:00
0e59d18fc2 ci: add angular robot config file (#21489)
PR Close #21489
2018-01-12 10:12:28 -08:00
54c8a321a9 ci: update pullapprove rules now that we allow selfapprovals (#21494)
PR Close #21494
2018-01-12 10:11:37 -08:00
9005a6f3cd feat(aio): implement survey notification link (#21371)
Closes #21094

PR Close #21371
2018-01-12 10:06:08 -08:00
3c6a5063f7 fix(benchpress): should still support selenium_webdriver < 3.6.0 (#21477)
PR Close #21477
2018-01-11 10:54:11 -08:00
b49d54e606 docs(forms): clarify note in reactive forms doc (#21134)
PR Close #21134
2018-01-11 07:04:15 -08:00
55fd82e587 docs: fix release schedule date (#21469)
PR Close #21469
2018-01-11 06:45:49 -08:00
7f3d0bbf97 build: update polyfill size (#21461)
PR Close #21461
2018-01-10 17:07:32 -08:00
3db02d244a docs: fix lazy-loading example (#20306)
PR Close #20306
2018-01-10 16:26:28 -08:00
50b605686e docs: fix yarn.lock (#20306)
PR Close #20306
2018-01-10 16:26:28 -08:00
64d4aafbc7 docs(aio): add NgModule docs (#20306)
PR Close #20306
2018-01-10 16:26:28 -08:00
a931a419fa fix(compiler): make .ngsummary.json files idempotent (#21448)
Fixes: #21432

PR Close #21448
2018-01-10 16:21:32 -08:00
6a97b5b722 docs(ivy): add Ivy as a recognized label (#21428)
PR Close #21428
2018-01-10 15:32:59 -08:00
aad1126446 docs: update the release schedule with v6 info (#21435)
Fixes #20649

PR Close #21435
2018-01-10 15:20:27 -08:00
7b463df52b fix(aio): preserve static Observable methods (#21351)
PR Close #21351
2018-01-10 14:30:42 -08:00
a2432c9f10 build(aio): turn on namedChunks option to make debugging, profiling and tracking easier (#21351)
PR Close #21351
2018-01-10 14:30:42 -08:00
e500484ccc build(aio): upgrade to angular@5.2.0 (#21351)
-rw-r--r--  1 iminar  eng   72498 Jan  9 22:20 dist/0.0d2802b63e9f4f4615cd.chunk.js
-rw-r--r--  1 iminar  eng   14872 Jan  9 22:20 dist/4.c719ac5645940382cdce.chunk.js
-rw-r--r--  1 iminar  eng    1558 Jan  9 22:20 dist/inline.cb4b7a639193fc489d3b.bundle.js
-rw-r--r--  1 iminar  eng  454654 Jan  9 22:20 dist/main.b65f32ffc92e075e64e0.bundle.js
-rw-r--r--  1 iminar  eng   40264 Jan  9 22:20 dist/polyfills.87edf5d695f14a29bf91.bundle.js
-rw-r--r--  1 iminar  eng   54001 Jan  9 22:20 dist/worker-basic.min.js

PR Close #21351
2018-01-10 14:30:42 -08:00
a0dcb0b828 ci(aio): track sizes of all js files (#21351)
PR Close #21351
2018-01-10 14:30:42 -08:00
c4b71920d1 build(aio): upgrade to angular@5.2.0-rc.0 (#21351)
Because of c2b3792 one of the test assertions had to be adjusted (more info: https://github.com/angular/angular/pull/21351#issuecomment-356083940).

PR Close #21351
2018-01-10 14:30:42 -08:00
650f5fb5c7 build(bazel): fix merge conflict between c4f02e2 and ef956a2 (#21453)
PR Close #21453
2018-01-10 12:57:38 -08:00
c32e83334b fix(compiler): cache external reference resolution (#21359)
Cache reference resolution for external references as finding
the declaration of a symbol is expensive and does not change
for a program once created.

This resolves a signficant performance regression in the langauge
service.

PR Close #21359
2018-01-10 12:34:07 -08:00
7bdd9aecbd build: upgrade yarn to 1.3.2 (#21406)
Fixes #20566
PR Close #21406
2018-01-10 12:32:47 -08:00
5ede67c345 build: move repeated tsconfig attributes to a macro (#20964)
This helps ensure we use the same tsconfig.json file for all compilations.
Next steps are to make it the same tsconfig.json file used by the editor

PR Close #20964
2018-01-10 12:30:44 -08:00
31b671ab54 fix(aio): support multibyte character in heading (#21414)
PR Close #21414
2018-01-10 12:25:45 -08:00
3804d20b6d docs(bazel): document an installation issue (#21401)
observed by Igor on a Mac

PR Close #21401
2018-01-10 12:24:03 -08:00
0a5a87887e build(aio): upgrade to uglify-es@3.3.5 (#21350)
This should fix the size regression spotted via the previous commit.

-rw-r--r--  1 iminar  eng   72498 Jan  8 00:03 dist/0.86a7a08f7866e6cdc36f.chunk.js
-rw-r--r--  1 iminar  eng   14872 Jan  8 00:03 dist/4.c719ac5645940382cdce.chunk.js
-rw-r--r--  1 iminar  eng    1558 Jan  8 00:03 dist/inline.7722895d8c844f710bcd.bundle.js
-rw-r--r--  1 iminar  eng  453905 Jan  8 00:03 dist/main.faff0e2da95443f759f5.bundle.js
-rw-r--r--  1 iminar  eng   40264 Jan  8 00:03 dist/polyfills.87edf5d695f14a29bf91.bundle.js
-rw-r--r--  1 iminar  eng   54001 Jan  8 00:03 dist/worker-basic.min.js

PR Close #21350
2018-01-10 12:22:12 -08:00
c46afce0f5 build(aio): upgrade to @angular/cli@1.6.3 (#21350)
This change introduces a size regression of 9kb for main.js :-(

I filed an issue for this: https://github.com/angular/angular-cli/issues/9108

-rw-r--r--  1 iminar  eng   72546 Jan  5 19:27 dist.cli-1.6.3/0.86a7a08f7866e6cdc36f.chunk.js
-rw-r--r--  1 iminar  eng   14893 Jan  5 19:27 dist.cli-1.6.3/4.c719ac5645940382cdce.chunk.js
-rw-r--r--  1 iminar  eng    1558 Jan  5 19:27 dist.cli-1.6.3/inline.7722895d8c844f710bcd.bundle.js
-rw-r--r--  1 iminar  eng  466484 Jan  5 19:27 dist.cli-1.6.3/main.faff0e2da95443f759f5.bundle.js
-rw-r--r--  1 iminar  eng   40363 Jan  5 19:27 dist.cli-1.6.3/polyfills.87edf5d695f14a29bf91.bundle.js
-rw-r--r--  1 iminar  eng   54001 Jan  5 19:27 dist.cli-1.6.3/worker-basic.min.js

PR Close #21350
2018-01-10 12:22:12 -08:00
76c781fd37 docs(router): remove extra brackets (#21294)
PR Close #21294
2018-01-10 12:20:48 -08:00
51eb3d418e fix(service-worker): properly handle invalid hashes in all scenarios (#21288)
When the SW fetches URLs listed in a manifest with hashes, it checks
the content hash against the manifest to make sure it has the correct
version of the URL. In the event of a mismatch, the SW is supposed to
consider the manifest invalid, and avoid using it. There are 3 cases
to consider by which this can happen.

Case 1: during the initial SW installation, a manifest is activated
without waiting for every URL to be fully loaded. In the background,
every prefetch URL listed by the manifest is requested and cached.
One such prefetch request could fail the hash test, and cause the
manifest to be treated as invalid. In such a case, the SW should
enter a state of EXISTING_CLIENTS_ONLY, as the latest manifest is
invalid.

This case works today.

Case 2: during the initial SW installation, as in Case 1, a manifest
is activated without waiting for each URL to fully load. However,
it's possible that the application could request a URL with a bad
hash before background initialization tries to load that URL. This
happens if, for example, the application has a broken index.html.

In this case, the SW should enter a state of EXISTING_CLIENTS_ONLY,
and serve the request from the network instead.

What happens today is that the internal error escapes the SW and
is returned as a rejected Promise to respondWith(), causing a
browser-level error that the site cannot be loaded, breaking the
site.

This change allows the SW to detect the error and enter the correct
state, falling back on the network if needed.

Case 3: during checkForUpdate(), the SW will try to fully cache the
new update before making it the latest version. Failure here is
complicated - if the page fails to load due to transient network
conditions (timeouts, 500s, etc), then it makes sense to continue
serving the existing cached version, and attempt to activate the
update on the next cycle.

If the page fails due to non-transient conditions though (400 error,
hash mismatch, etc), then the SW should consider the updated
manifest invalid, and enter a state of EXISTING_CLIENTS_ONLY.

Currently, all errors are treated as transient.

This change causes the SW to treat all errors during updates as
non-transient, which can cause the SW to unnecessarily enter a
safe mode. A future change can allow the SW to remain in normal mode
if the error is provably transient.

PR Close #21288
2018-01-10 12:18:55 -08:00
48c18985cc fix(animations): fix increment/decrement aliases example (#18323)
PR Close #18323
2018-01-10 12:07:24 -08:00
167cbed266 build: add bazel rulse for benchmarks (#21436)
PR Close #21436
2018-01-10 12:01:17 -08:00
70e8802540 docs(core): provide note for unit test changes with the ng-star-inserted className (#21450)
PR Close #21450
2018-01-10 11:14:28 -08:00
1503 changed files with 15391 additions and 73288 deletions

View File

@ -1,25 +0,0 @@
# 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

View File

@ -15,13 +15,6 @@
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
@ -41,16 +34,14 @@ 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
- run: 'yarn buildifier -mode=check ||
(echo -e "\nBUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)'
- run: 'yarn skylint ||
# 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 ||
(echo -e "\n.bzl files have lint errors. Please run ''yarn skylint''"; exit 1)'
- restore_cache:
key: *cache_key
@ -63,11 +54,6 @@ 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
@ -76,16 +62,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
# CircleCI will allow us to go back and view/download these artifacts from past builds.
# Also we can use a service like https://buildsize.org/ to automatically track binary size of these artifacts.
- store_artifacts:
path: dist/bin/packages/core/test/bundling/hello_world/bundle.min.js
destination: packages/core/test/bundling/hello_world/bundle.min.js
- store_artifacts:
path: dist/bin/packages/core/test/bundling/hello_world/bundle.min.js.brotli
destination: packages/core/test/bundling/hello_world/bundle.min.js.brotli
- run: bazel query --output=label '//modules/... union //packages/... union //tools/... union @angular//...' | xargs bazel test --config=ci
- save_cache:
key: *cache_key

View File

@ -1,11 +0,0 @@
#!/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

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://stackblitz.com or similar (you can use this template as a starting point: https://stackblitz.com/fork/angular-gitter).
https://plnkr.co or similar (you can use this template as a starting point: http://plnkr.co/edit/tpl:AvJOMERrnz94ekVua0u5).
-->
## What is the motivation / use case for changing the behavior?

View File

@ -13,33 +13,9 @@ 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!"
mergeConflictComment: "Hello? Don't want to hassle you. Sure you're busy. But this PR has some merge conflicts that you probably ought to resolve.
\nThat is... if you want it to be merged someday..."
# label to monitor
mergeLabel: "PR action: merge"
@ -50,15 +26,12 @@ 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
@ -71,30 +44,9 @@ merge:
# the comment that will be added when the merge label is added despite failing checks, leave empty or set to false to disable
# {{MERGE_LABEL}} will be replaced by the value of the mergeLabel option
# {{PLACEHOLDER}} will be replaced by the list of failing checks
mergeRemovedComment: "I see that you just added the `{{MERGE_LABEL}}` label, but the following checks are still failing:
\n{{PLACEHOLDER}}
\n
\n**If you want your PR to be merged, it has to pass all the CI checks.**
\n
\nIf you can't get the PR to a green state due to flakes or broken master, please try rebasing to master and/or restarting the CI job. If that fails and you believe that the issue is not due to your change, please contact the caretaker and ask for help."
# options for the triage plugin
triage:
# number of the milestone to apply when the issue is triaged
defaultMilestone: 82,
# arrays of labels that determine if an issue is triaged
triagedLabels:
-
- "type: bug"
- "severity*"
- "freq*"
- "comp: *"
-
- "type: feature"
- "comp: *"
-
- "type: refactor"
- "comp: *"
-
- "type: RFC / Discussion / question"
- "comp: *"
mergeRemovedComment: "I see that you just added the `{{MERGE_LABEL}}` label. It won't do anything good though, because the following checks are still failing:
\n{{PLACEHOLDER}}
\n
\n**If you want your PR to be merged, it has to pass all the CI checks.**
\n
\nIf you can't get the PR to a green state due to flakes or broken master, please try rebasing to master and/or restarting the CI job. If that fails and you believe that the issue is not due to your change, please contact the caretaker and ask for help."

View File

@ -44,7 +44,6 @@ groups:
all:
users: all
required: 1
rejection_value: -999
# In this group, your self-approval does not count
author_approval:
auto: false
@ -93,7 +92,6 @@ groups:
- alexeagle #primary
- chuckjaz
- IgorMinar #fallback
- mhevery
- vikerman #fallback
build-and-ci:
@ -207,12 +205,6 @@ groups:
conditions:
files:
- "packages/forms/*"
- "aio/content/guide/forms.md"
- "aio/content/guide/form-validation.md"
- "aio/content/guide/reactive-forms.md"
- "aio/content/examples/forms/*"
- "aio/content/examples/form-validation/*"
- "aio/content/examples/reactive-forms/*"
users:
- kara #primary
- tinayuangao #secondary

View File

@ -1,5 +1,7 @@
language: node_js
sudo: false
# Work-around for https://github.com/travis-ci/travis-ci/issues/8836#issuecomment-356362524.
# (Restore `sudo: false` once that is resolved.)
sudo: required
dist: trusty
node_js:
- '8.9.1'
@ -56,6 +58,7 @@ 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

View File

@ -24,7 +24,9 @@ filegroup(
"typescript",
"zone.js",
"tsutils",
"@types",
"@types/jasmine",
"@types/node",
"@types/source-map",
"tsickle",
"hammerjs",
"protobufjs",
@ -32,7 +34,6 @@ filegroup(
"reflect-metadata",
"source-map-support",
"minimist",
"tslib",
] for ext in [
"*.js",
"*.json",

View File

@ -1,202 +1,3 @@
<a name="6.0.0-beta.4"></a>
# [6.0.0-beta.4](https://github.com/angular/angular/compare/6.0.0-beta.3...6.0.0-beta.4) (2018-02-14)
### Bug Fixes
* **bazel:** allow TS to read ambient typings ([#21876](https://github.com/angular/angular/issues/21876)) ([b081dfe](https://github.com/angular/angular/commit/b081dfe)), closes [#21872](https://github.com/angular/angular/issues/21872)
* **bazel:** improve error message for missing assets ([#22096](https://github.com/angular/angular/issues/22096)) ([dcf64a0](https://github.com/angular/angular/commit/dcf64a0)), closes [#22095](https://github.com/angular/angular/issues/22095)
* **common:** add locale currency values ([#21783](https://github.com/angular/angular/issues/21783)) ([420cc7a](https://github.com/angular/angular/commit/420cc7a)), closes [#20385](https://github.com/angular/angular/issues/20385)
* **common:** round currencies based on decimal digits in `CurrencyPipe` ([#21783](https://github.com/angular/angular/issues/21783)) ([44154e7](https://github.com/angular/angular/commit/44154e7)), closes [#10189](https://github.com/angular/angular/issues/10189)
* **common:** weaken AsyncPipe transform signature ([#22169](https://github.com/angular/angular/issues/22169)) ([be59c3a](https://github.com/angular/angular/commit/be59c3a))
* **compiler:** make unary plus operator consistent to JavaScript ([#22154](https://github.com/angular/angular/issues/22154)) ([72f8abd](https://github.com/angular/angular/commit/72f8abd)), 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)) ([16d1700](https://github.com/angular/angular/commit/16d1700))
* **core:** ensure initial value of QueryList length ([#21980](https://github.com/angular/angular/issues/21980)) ([#21982](https://github.com/angular/angular/issues/21982)) ([e56de10](https://github.com/angular/angular/commit/e56de10)), 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)) ([a751649](https://github.com/angular/angular/commit/a751649))
* **forms:** make Validators.email support optional controls ([#20869](https://github.com/angular/angular/issues/20869)) ([140e7c0](https://github.com/angular/angular/commit/140e7c0))
* **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)) ([0bcfae7](https://github.com/angular/angular/commit/0bcfae7))
* **forms:** set state before emitting a value from ngModelChange ([#21514](https://github.com/angular/angular/issues/21514)) ([9744a1c](https://github.com/angular/angular/commit/9744a1c)), closes [#21513](https://github.com/angular/angular/issues/21513)
* **language-service:** correct instructions to install the language service ([#22000](https://github.com/angular/angular/issues/22000)) ([b37cee3](https://github.com/angular/angular/commit/b37cee3))
* **platform-browser:** add @Injectable where it was missing ([#22005](https://github.com/angular/angular/issues/22005)) ([0a1a397](https://github.com/angular/angular/commit/0a1a397))
* **platform-browser:** support 0/false/null values in transfer_state ([#22179](https://github.com/angular/angular/issues/22179)) ([6435ecd](https://github.com/angular/angular/commit/6435ecd))
### Features
* **bazel:** allow explicit specification of factories ([#22003](https://github.com/angular/angular/issues/22003)) ([e442881](https://github.com/angular/angular/commit/e442881))
* **compiler:** mark @NgModules in provider lists for identification at runtime ([#22005](https://github.com/angular/angular/issues/22005)) ([2d5e7d1](https://github.com/angular/angular/commit/2d5e7d1))
* **forms:** multiple validators for array method ([#20766](https://github.com/angular/angular/issues/20766)) ([941e88f](https://github.com/angular/angular/commit/941e88f)), closes [#20665](https://github.com/angular/angular/issues/20665)
* change @Injectable() to support tree-shakeable tokens ([#22005](https://github.com/angular/angular/issues/22005)) ([235a235](https://github.com/angular/angular/commit/235a235))
<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="6.0.0-beta.3"></a>
# [6.0.0-beta.3](https://github.com/angular/angular/compare/6.0.0-beta.2...6.0.0-beta.3) (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)) ([8b14488](https://github.com/angular/angular/commit/8b14488)), 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 ([#20640](https://github.com/angular/angular/issues/20640)) ([f791e9f](https://github.com/angular/angular/commit/f791e9f))
* **core:** fix proper propagation of subscriptions in EventEmitter ([#22016](https://github.com/angular/angular/issues/22016)) ([e81606c](https://github.com/angular/angular/commit/e81606c)), closes [#21999](https://github.com/angular/angular/issues/21999)
* **core:** should check Zone existance when scheduleMicroTask ([#20656](https://github.com/angular/angular/issues/20656)) ([3a86940](https://github.com/angular/angular/commit/3a86940))
* **forms:** publish missing types ([#19941](https://github.com/angular/angular/issues/19941)) ([2707012](https://github.com/angular/angular/commit/2707012))
* **ivy:** generate correct interpolations ([#21946](https://github.com/angular/angular/issues/21946)) ([3cc1d76](https://github.com/angular/angular/commit/3cc1d76))
* **ivy:** generate lifecycle pattern ([#21865](https://github.com/angular/angular/issues/21865)) ([f816666](https://github.com/angular/angular/commit/f816666))
* **ivy:** improve `bindV` perf and memory usage ([#21881](https://github.com/angular/angular/issues/21881)) ([0846784](https://github.com/angular/angular/commit/0846784))
* **ivy:** remove unnecessary parameter of NgOnChangesFeature ([#21879](https://github.com/angular/angular/issues/21879)) ([65cf1ad](https://github.com/angular/angular/commit/65cf1ad))
### Features
* **compiler-cli:** reflect static methods added to classes in metadata ([#21926](https://github.com/angular/angular/issues/21926)) ([eb8ddd2](https://github.com/angular/angular/commit/eb8ddd2))
* **ivy:** add canonical example of a pipe. ([#21834](https://github.com/angular/angular/issues/21834)) ([743d8bc](https://github.com/angular/angular/commit/743d8bc))
* **ivy:** add support for attributes on ng-content nodes ([#21935](https://github.com/angular/angular/issues/21935)) ([1aa2947](https://github.com/angular/angular/commit/1aa2947))
* **ivy:** memoize array literals in render3 ([#21973](https://github.com/angular/angular/issues/21973)) ([4d62be6](https://github.com/angular/angular/commit/4d62be6))
### Performance Improvements
* **ivy:** improve Uglify configuration in hello world integration test ([#21985](https://github.com/angular/angular/issues/21985)) ([7e51e52](https://github.com/angular/angular/commit/7e51e52))
<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="6.0.0-beta.2"></a>
# [6.0.0-beta.2](https://github.com/angular/angular/compare/6.0.0-beta.1...6.0.0-beta.2) (2018-01-31)
### Features
* **router:** add navigationSource and restoredState to NavigationStart event ([#21728](https://github.com/angular/angular/issues/21728)) ([c40ae7f](https://github.com/angular/angular/commit/c40ae7f))
* **service-worker:** add helper script which will uninstall SW ([#21863](https://github.com/angular/angular/issues/21863)) ([b10540a](https://github.com/angular/angular/commit/b10540a))
<a name="5.2.3"></a>
## [5.2.3](https://github.com/angular/angular/compare/5.2.2...5.2.3) (2018-01-31)
### Bug Fixes
* **common:** allow HttpInterceptors to inject HttpClient ([#19809](https://github.com/angular/angular/issues/19809)) ([ed2b717](https://github.com/angular/angular/commit/ed2b717)), closes [#18224](https://github.com/angular/angular/issues/18224)
* **common:** generate closure-locale data file with exported plural functions ([#21873](https://github.com/angular/angular/issues/21873)) ([c2f5ed5](https://github.com/angular/angular/commit/c2f5ed5)), closes [#21870](https://github.com/angular/angular/issues/21870)
* **core:** fix retrieving the binding name when an expression changes ([#21814](https://github.com/angular/angular/issues/21814)) ([81d64d6](https://github.com/angular/angular/commit/81d64d6)), closes [#21735](https://github.com/angular/angular/issues/21735) [#21788](https://github.com/angular/angular/issues/21788)
* **forms:** allow FormBuilder to create controls with any formState type ([#20917](https://github.com/angular/angular/issues/20917)) ([56f3e18](https://github.com/angular/angular/commit/56f3e18)), closes [#20368](https://github.com/angular/angular/issues/20368)
* **forms:** inserting and removing controls should work in re-bound form arrays ([#21822](https://github.com/angular/angular/issues/21822)) ([fad99cc](https://github.com/angular/angular/commit/fad99cc)), closes [#21501](https://github.com/angular/angular/issues/21501)
* **language-service:** ensure correct paths are passed to TypeScript ([#21812](https://github.com/angular/angular/issues/21812)) ([250c8da](https://github.com/angular/angular/commit/250c8da))
* **language-service:** spell diagnostics correctly ([#21812](https://github.com/angular/angular/issues/21812)) ([778e6e7](https://github.com/angular/angular/commit/778e6e7))
* **router:** remove [@internal](https://github.com/internal) tag on ParamInheritanceType ([#21773](https://github.com/angular/angular/issues/21773)) ([35a0721](https://github.com/angular/angular/commit/35a0721)), closes [#21456](https://github.com/angular/angular/issues/21456)
<a name="6.0.0-beta.1"></a>
# [6.0.0-beta.1](https://github.com/angular/angular/compare/6.0.0-beta.0...6.0.0-beta.1) (2018-01-25)
### Bug Fixes
* **common:** A null value should remove the style on IE ([#21679](https://github.com/angular/angular/issues/21679)) ([7d49443](https://github.com/angular/angular/commit/7d49443)), closes [#21064](https://github.com/angular/angular/issues/21064)
* avoid triggering a cli bug ([#21611](https://github.com/angular/angular/issues/21611)) ([0eabd07](https://github.com/angular/angular/commit/0eabd07))
* **common:** don't remove special characters when extracting CLDR data ([#21626](https://github.com/angular/angular/issues/21626)) ([135a282](https://github.com/angular/angular/commit/135a282))
* **common:** extract plural function from i18n locale data files for TS 2.6 ([#21626](https://github.com/angular/angular/issues/21626)) ([97b18b2](https://github.com/angular/angular/commit/97b18b2)), closes [#21608](https://github.com/angular/angular/issues/21608)
* **common:** fallback to last defined value for named date and time formats ([#21299](https://github.com/angular/angular/issues/21299)) ([879756d](https://github.com/angular/angular/commit/879756d)), closes [#21282](https://github.com/angular/angular/issues/21282)
* **compiler:** add support for marker tags in xliff serializers ([#21250](https://github.com/angular/angular/issues/21250)) ([f74130c](https://github.com/angular/angular/commit/f74130c)), closes [#21078](https://github.com/angular/angular/issues/21078)
* **compiler:** Don't strip `/*# sourceURL ... */` ([#16088](https://github.com/angular/angular/issues/16088)) ([5f681f9](https://github.com/angular/angular/commit/5f681f9))
* **compiler:** fix ICU select messages to use male/female/other ([#21713](https://github.com/angular/angular/issues/21713)) ([cb5090c](https://github.com/angular/angular/commit/cb5090c))
* **compiler-cli:** do not fold errors past calls in the collector ([#21708](https://github.com/angular/angular/issues/21708)) ([dd86790](https://github.com/angular/angular/commit/dd86790))
* **compiler-cli:** do not lower expressions in non-modules ([#21649](https://github.com/angular/angular/issues/21649)) ([7f93aad](https://github.com/angular/angular/commit/7f93aad))
* **router:** don't use ParamsInheritanceStrategy in declarations ([#21574](https://github.com/angular/angular/issues/21574)) ([925e654](https://github.com/angular/angular/commit/925e654)), closes [#21456](https://github.com/angular/angular/issues/21456)
### Features
* **compiler:** implement "enableIvy" compiler option ([#21427](https://github.com/angular/angular/issues/21427)) ([64d16de](https://github.com/angular/angular/commit/64d16de))
* **core:** optional generic type for ElementRef ([#20765](https://github.com/angular/angular/issues/20765)) ([d3d9aac](https://github.com/angular/angular/commit/d3d9aac)), closes [#13139](https://github.com/angular/angular/issues/13139)
<a name="5.2.2"></a>
## [5.2.2](https://github.com/angular/angular/compare/5.2.1...5.2.2) (2018-01-25)
### Bug Fixes
* **common:** A null value should remove the style on IE ([#21679](https://github.com/angular/angular/issues/21679)) ([c12ea3a](https://github.com/angular/angular/commit/c12ea3a)), closes [#21064](https://github.com/angular/angular/issues/21064)
* **common:** don't remove special characters when extracting CLDR data ([#21626](https://github.com/angular/angular/issues/21626)) ([a62c186](https://github.com/angular/angular/commit/a62c186))
* **common:** extract plural function from i18n locale data files for TS 2.6 ([#21626](https://github.com/angular/angular/issues/21626)) ([71f9eaa](https://github.com/angular/angular/commit/71f9eaa)), closes [#21608](https://github.com/angular/angular/issues/21608)
* **common:** fallback to last defined value for named date and time formats ([#21299](https://github.com/angular/angular/issues/21299)) ([982eb7b](https://github.com/angular/angular/commit/982eb7b)), closes [#21282](https://github.com/angular/angular/issues/21282)
* **compiler:** add support for marker tags in xliff serializers ([#21250](https://github.com/angular/angular/issues/21250)) ([02352bc](https://github.com/angular/angular/commit/02352bc)), closes [#21078](https://github.com/angular/angular/issues/21078)
* **compiler:** Don't strip `/*# sourceURL ... */` ([#16088](https://github.com/angular/angular/issues/16088)) ([de6c644](https://github.com/angular/angular/commit/de6c644))
* **compiler:** fix ICU select messages to use male/female/other ([#21713](https://github.com/angular/angular/issues/21713)) ([8e44577](https://github.com/angular/angular/commit/8e44577))
* **compiler-cli:** do not fold errors past calls in the collector ([#21708](https://github.com/angular/angular/issues/21708)) ([52970c0](https://github.com/angular/angular/commit/52970c0))
* **compiler-cli:** do not lower expressions in non-modules ([#21649](https://github.com/angular/angular/issues/21649)) ([ba4ea82](https://github.com/angular/angular/commit/ba4ea82))
* **router:** don't use ParamsInheritanceStrategy in declarations ([#21574](https://github.com/angular/angular/issues/21574)) ([8b3fbb5](https://github.com/angular/angular/commit/8b3fbb5)), closes [#21456](https://github.com/angular/angular/issues/21456)
<a name="6.0.0-beta.0"></a>
# [6.0.0-beta.0](https://github.com/angular/angular/compare/5.2.0...6.0.0-beta.0) (2018-01-17)
### Bug Fixes
* **animations:** fix increment/decrement aliases example ([#18323](https://github.com/angular/angular/issues/18323)) ([d2aa8ac](https://github.com/angular/angular/commit/d2aa8ac))
* **benchpress:** should still support selenium_webdriver < 3.6.0 ([#21477](https://github.com/angular/angular/issues/21477)) ([9b84a32](https://github.com/angular/angular/commit/9b84a32))
* **common:** set correct timezone for ISO8601 dates in Safari ([#21506](https://github.com/angular/angular/issues/21506)) ([05208b8](https://github.com/angular/angular/commit/05208b8)), closes [#21491](https://github.com/angular/angular/issues/21491)
* **compiler:** cache external reference resolution ([#21359](https://github.com/angular/angular/issues/21359)) ([e3e2fc0](https://github.com/angular/angular/commit/e3e2fc0))
* **compiler:** make `.ngsummary.json` files idempotent ([#21448](https://github.com/angular/angular/issues/21448)) ([e64b1e9](https://github.com/angular/angular/commit/e64b1e9))
* **core:** fix chained http call ([#20924](https://github.com/angular/angular/issues/20924)) ([7e3f9a4](https://github.com/angular/angular/commit/7e3f9a4)), closes [#20921](https://github.com/angular/angular/issues/20921)
* **ivy:** Add workaround for AJD in google3 ([#21488](https://github.com/angular/angular/issues/21488)) ([6af3672](https://github.com/angular/angular/commit/6af3672))
* **language-service:** Clear caches when program changes ([#21337](https://github.com/angular/angular/issues/21337)) ([43e1520](https://github.com/angular/angular/commit/43e1520)), closes [#19405](https://github.com/angular/angular/issues/19405)
* **service-worker:** properly handle invalid hashes in all scenarios ([#21288](https://github.com/angular/angular/issues/21288)) ([3951098](https://github.com/angular/angular/commit/3951098))
### Features
* **bazel:** allow ng_module rules to control whether type checking is enabled ([#21460](https://github.com/angular/angular/issues/21460)) ([cffa0fe](https://github.com/angular/angular/commit/cffa0fe))
* **core:** add binding name to content changed error ([#20352](https://github.com/angular/angular/issues/20352)) ([d3bf54b](https://github.com/angular/angular/commit/d3bf54b))
* **forms:** handle string with and without line boundary on pattern validator ([#19256](https://github.com/angular/angular/issues/19256)) ([54bf179](https://github.com/angular/angular/commit/54bf179))
### Performance Improvements
* **ivy:** add missing dom element in render3_function tree benchmark ([#21476](https://github.com/angular/angular/issues/21476)) ([9b5a485](https://github.com/angular/angular/commit/9b5a485))
<a name="5.2.1"></a>
## [5.2.1](https://github.com/angular/angular/compare/5.2.0...5.2.1) (2018-01-17)

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 a 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 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 doesn'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 don'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).
@ -72,7 +72,7 @@ Before you submit your Pull Request (PR) consider the following guidelines:
1. Search [GitHub](https://github.com/angular/angular/pulls) for an open or closed PR
that relates to your submission. You don't want to duplicate effort.
1. Please sign our [Contributor License Agreement (CLA)](#cla) before sending PRs.
We cannot accept code without this. Make sure you sign with the primary email address of the Git identity that has been granted access to the Angular repository.
We cannot accept code without this.
1. Fork the angular/angular repo.
1. Make your changes in a new git branch:
@ -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.
The footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
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 changelog to beta.5
docs(changelog): update change log 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 the person reading the changelog generated from commit messages.
The scope should be the name of the npm package affected (as perceived by person reading 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 a succinct description of the change:
The subject contains succinct description of the change:
* use the imperative, present tense: "change" not "changed" nor "changes"
* don't capitalize the first letter
* don't capitalize first letter
* no dot (.) at the end
### Body
@ -259,19 +259,6 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise
* For corporations we'll need you to
[print, sign and one of scan+email, fax or mail the form][corporate-cla].
<hr>
If you have more than one Git identity, you must make sure that you sign the CLA using the primary email address associated with the ID that has been granted access to the Angular repository. Git identities can be associated with more than one email address, and only one is primary. Here are some links to help you sort out multiple Git identities and email addresses:
* 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/
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.
<hr>
[angular-group]: https://groups.google.com/forum/#!forum/angular
[coc]: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md

View File

@ -1,14 +1,11 @@
workspace(name = "angular")
workspace(name = "angular_src")
# 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"
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
http_archive(
git_repository(
name = "build_bazel_rules_nodejs",
url = "https://github.com/bazelbuild/rules_nodejs/archive/%s.zip" % RULES_NODEJS_VERSION,
strip_prefix = "rules_nodejs-%s" % RULES_NODEJS_VERSION,
sha256 = "5ba3c8c209078c2e3f0c6aa4abd01a1a561f92a5bfda04e25604af5f4734d69d",
remote = "https://github.com/bazelbuild/rules_nodejs.git",
tag = "0.3.1",
)
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories")
@ -16,19 +13,22 @@ load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_reposi
check_bazel_version("0.9.0")
node_repositories(package_json = ["//:package.json"])
RULES_TYPESCRIPT_VERSION = "0.10.1"
http_archive(
git_repository(
name = "build_bazel_rules_typescript",
url = "https://github.com/bazelbuild/rules_typescript/archive/%s.zip" % RULES_TYPESCRIPT_VERSION,
strip_prefix = "rules_typescript-%s" % RULES_TYPESCRIPT_VERSION,
sha256 = "a2c81776a4a492ff9f878f9705639f5647bef345f7f3e1da09c9eeb8dec80485",
remote = "https://github.com/bazelbuild/rules_typescript.git",
# tag = "0.7.1+",
commit = "89d2c75066bea3d9c942f29dd1d2ea543c58d6d5"
)
load("@build_bazel_rules_typescript//:defs.bzl", "ts_setup_workspace")
load("@build_bazel_rules_typescript//:setup.bzl", "ts_setup_workspace")
ts_setup_workspace()
local_repository(
name = "angular",
path = "packages/bazel",
)
local_repository(
name = "rxjs",
path = "node_modules/rxjs/src",
@ -62,23 +62,3 @@ http_archive(
strip_prefix = "bazel-9755c72b48866ed034bd28aa033e9abd27431b1e",
sha256 = "5b8443fc3481b5fcd9e7f348e1dd93c1397f78b223623c39eb56494c55f41962",
)
# We have a source dependency on the Devkit repository, because it's built with
# Bazel.
# This allows us to edit sources and have the effect appear immediately without
# re-packaging or "npm link"ing.
# Even better, things like aspects will visit the entire graph including
# ts_library rules in the devkit repository.
http_archive(
name = "angular_devkit",
url = "https://github.com/angular/devkit/archive/v0.3.1.zip",
strip_prefix = "devkit-0.3.1",
sha256 = "31d4b597fe9336650acf13df053c1c84dcbe9c29c6a833bcac3819cd3fd8cad3",
)
http_archive(
name = "org_brotli",
url = "https://github.com/google/brotli/archive/v1.0.2.zip",
strip_prefix = "brotli-1.0.2",
sha256 = "b43d5d6bc40f2fa6c785b738d86c6bbe022732fe25196ebbe43b9653a025920d",
)

View File

@ -39,7 +39,7 @@
],
"e2e": {
"protractor": {
"config": "tests/e2e/protractor.conf.js"
"config": "./protractor.conf.js"
}
},
"lint": [
@ -50,12 +50,12 @@
"project": "src/tsconfig.spec.json"
},
{
"project": "tests/e2e/tsconfig.e2e.json"
"project": "e2e/tsconfig.e2e.json"
}
],
"test": {
"karma": {
"config": "src/karma.conf.js"
"config": "./karma.conf.js"
}
},
"defaults": {

View File

@ -4,7 +4,7 @@ Everything in this folder is part of the documentation project. This includes
* the web site for displaying the documentation
* the dgeni configuration for converting source files to rendered files that can be viewed in the web site.
* the tooling for setting up examples for development; and generating live-example and zip files from the examples.
* the tooling for setting up examples for development; and generating plunkers and zip files from the examples.
## Developer tasks
@ -13,7 +13,7 @@ You should run all these tasks from the `angular/aio` folder.
Here are the most important tasks you might need to use:
* `yarn` - install all the dependencies.
* `yarn setup` - install all the dependencies, boilerplate, stackblitz, zips and run dgeni on the docs.
* `yarn setup` - install all the dependencies, boilerplate, plunkers, zips and run dgeni on the docs.
* `yarn setup-local` - same as `setup`, but use the locally built Angular packages for aio and docs examples boilerplate.
* `yarn build` - create a production build of the application (after installing dependencies, boilerplate, etc).
@ -32,7 +32,7 @@ Here are the most important tasks you might need to use:
* `yarn boilerplate:add` - generate all the boilerplate code for the examples, so that they can be run locally. Add the option `--local` to use your local version of Angular contained in the "dist" folder.
* `yarn boilerplate:remove` - remove all the boilerplate code that was added via `yarn boilerplate:add`.
* `yarn generate-stackblitz` - generate the stackblitz files that are used by the `live-example` tags in the docs.
* `yarn generate-plunkers` - generate the plunker files that are used by the `live-example` tags in the docs.
* `yarn generate-zips` - generate the zip files from the examples. Zip available via the `live-example` tags in the docs.
* `yarn example-e2e` - run all e2e tests for examples
@ -105,7 +105,8 @@ 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 setup
yarn
yarn docs
yarn start
```

Binary file not shown.

View File

@ -76,8 +76,8 @@ aot-compiler/**/*.factory.d.ts
# universal
!universal/webpack.server.config.js
# stackblitz
*stackblitz.no-link.html
# plunkers
*plnkr.no-link.html
# ngUpgrade testing
!upgrade-phonecat-*/**/karma.conf.js

View File

@ -1,5 +1,6 @@
{
"description": "AngularJS to Angular Quick Reference",
"basePath": "src/",
"files":[
"!**/*.d.ts",
"!**/*.js",

View File

@ -1,5 +1,6 @@
{
"description": "Angular Animations",
"basePath": "src/",
"files":[
"!**/*.d.ts",
"!**/*.js"

View File

@ -1,9 +1,9 @@
{
"description": "Intro to Angular",
"basePath": "src/",
"files":[
"!**/*.d.ts",
"!**/*.js",
"!**/*.[1].*"
],
"file": "src/app/app.module.ts"
"!app/hero-list.component.1.*"
]
}

View File

@ -1,9 +1,10 @@
{
"description": "Attribute Directive",
"basePath": "src/",
"files":[
"!**/*.d.ts",
"!**/*.js",
"!**/*.[1,2,3].*"
"!app/*.[0,1,2,3].*"
],
"tags": ["attribute", "directive"]
}

View File

@ -1,4 +1,4 @@
// Not used. Keep away from stackblitz
// Not used. Keep away from plunker
// Keeps ATLS from complaining about undeclared directives.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

View File

@ -1,10 +1,11 @@
{
"description": "Bootstrapping",
"basePath": "src/",
"files": [
"!**/*.d.ts",
"!**/*.js",
"!**/*.[1,2].*"
],
"file": "src/app/app.component.ts",
"open": "app/app.component.ts",
"tags": ["ngmodules"]
}

View File

@ -1,5 +1,6 @@
{
"description": "Component Communication Cookbook samples",
"basePath": "src/",
"files":[
"!**/*.d.ts",
"!**/*.js"

View File

@ -1,5 +1,6 @@
{
"description": "Component Styles",
"basePath": "src/",
"files": [
"!**/*.d.ts",
"!**/*.js",

View File

@ -1,6 +1,5 @@
/* #docregion import */
/* The AOT compiler needs the `./` to show that this is local */
@import './hero-details-box.css';
@import 'hero-details-box.css';
/* #enddocregion import */
/* #docregion host */

View File

@ -5,8 +5,7 @@ import { Hero } from './hero';
@Component({
selector: 'app-hero-team',
template: `
<!-- We must use a relative URL so that the AOT compiler can find the stylesheet -->
<link rel="stylesheet" href="../assets/hero-team.component.css">
<link rel="stylesheet" href="assets/hero-team.component.css">
<h3>Team</h3>
<ul>
<li *ngFor="let member of hero.team">

View File

@ -1,5 +1,6 @@
{
"description": "Dependency Injection",
"basePath": "src/",
"files":[
"!**/*.d.ts",
"!**/*.js",

View File

@ -1,5 +1,6 @@
{
"description": "Dependency Injection",
"basePath": "src/",
"files":[
"!**/*.d.ts",
"!**/*.js",

View File

@ -1,5 +1,6 @@
{
"description": "Displaying Data",
"basePath": "src/",
"files": [
"!**/*.d.ts",
"!**/*.js",

View File

@ -1,5 +1,6 @@
{
"description": "Authors style guide",
"basePath": "src/",
"files": [
"!**/*.d.ts",
"!**/*.js",

View File

@ -0,0 +1,9 @@
{
"description": "Second authors style guide plunker (non-executing)",
"basePath": "src/",
"files": [
"index.2.html"
],
"main": "index.2.html",
"tags": ["author", "style guide"]
}

View File

@ -1,8 +0,0 @@
{
"description": "Second authors style guide stackblitz (non-executing)",
"files": [
"src/index.2.html"
],
"main": "src/index.2.html",
"tags": ["author", "style guide"]
}

View File

@ -1,5 +1,6 @@
{
"description": "Dynamic Component Loader",
"basePath": "src/",
"files":[
"!**/*.d.ts",
"!**/*.js"

View File

@ -1,5 +1,6 @@
{
"description": "Dynamic Form",
"basePath": "src/",
"files":[
"!**/*.d.ts",
"!**/*.js",

View File

@ -1,10 +1,11 @@
{
"description": "Feature Modules",
"basePath": "src/",
"files": [
"!**/*.d.ts",
"!**/*.js",
"!**/*.[1,2].*"
],
"file": "src/app/app.component.ts",
"open": "app/app.component.ts",
"tags": ["feature modules"]
}

View File

@ -15,7 +15,6 @@ describe('Form Validation Tests', function () {
});
tests('Template-Driven Form');
bobTests();
});
describe('Reactive form', () => {

View File

@ -1,5 +1,6 @@
{
"description": "Validation",
"basePath": "src/",
"files":[
"!**/*.d.ts",
"!**/*.js"

View File

@ -20,7 +20,7 @@ export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
// #enddocregion directive-providers
})
export class ForbiddenValidatorDirective implements Validator {
@Input('appForbiddenName') forbiddenName: string;
@Input() forbiddenName: string;
validate(control: AbstractControl): {[key: string]: any} {
return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)

View File

@ -12,7 +12,7 @@
<!-- #docregion name-with-error-msg -->
<!-- #docregion name-input -->
<input id="name" name="name" class="form-control"
required minlength="4" appForbiddenName="bob"
required minlength="4" forbiddenName="bob"
[(ngModel)]="hero.name" #name="ngModel" >
<!-- #enddocregion name-input -->

View File

@ -1,5 +1,6 @@
{
"description": "Forms",
"basePath": "src/",
"files":[
"!**/*.d.ts",
"!**/*.js"

View File

@ -1,5 +1,6 @@
{
"description": "Hierarchical Dependency Injection",
"basePath": "src/",
"files":[
"!**/*.d.ts",
"!**/*.js"

View File

@ -0,0 +1,138 @@
'use strict'; // necessary for es6 output in node
import { browser, element, by } from 'protractor';
describe('Server Communication', function () {
beforeAll(function () {
browser.get('');
});
describe('Tour of Heroes (Observable)', function () {
let initialHeroCount = 4;
let newHeroName = 'Mr. IQ';
let heroCountAfterAdd = 5;
let heroListComp = element(by.tagName('hero-list'));
let addButton = heroListComp.element(by.tagName('button'));
let heroTags = heroListComp.all(by.tagName('li'));
let heroNameInput = heroListComp.element(by.tagName('input'));
it('should exist', function() {
expect(heroListComp).toBeDefined('<hero-list> must exist');
});
it('should display ' + initialHeroCount + ' heroes after init', function () {
expect(heroTags.count()).toBe(initialHeroCount);
});
it('should not add hero with empty name', function () {
expect(addButton).toBeDefined('"Add Hero" button must be defined');
addButton.click().then(function() {
expect(heroTags.count()).toBe(initialHeroCount, 'No new hero should be added');
});
});
it('should add a new hero to the list', function () {
expect(heroNameInput).toBeDefined('<input> for hero name must exist');
expect(addButton).toBeDefined('"Add Hero" button must be defined');
heroNameInput.sendKeys(newHeroName);
addButton.click().then(function() {
expect(heroTags.count()).toBe(heroCountAfterAdd, 'A new hero should be added');
let newHeroInList = heroTags.get(heroCountAfterAdd - 1).getText();
expect(newHeroInList).toBe(newHeroName, 'The hero should be added to the end of the list');
});
});
});
describe('Wikipedia Demo', function () {
it('should initialize the demo with empty result list', function () {
let myWikiComp = element(by.tagName('my-wiki'));
expect(myWikiComp).toBeDefined('<my-wiki> must exist');
let resultList = myWikiComp.all(by.tagName('li'));
expect(resultList.count()).toBe(0, 'result list must be empty');
});
describe('Fetches after each keystroke', function () {
it('should fetch results after "B"', function(done: any) {
testForRefreshedResult('B', done);
});
it('should fetch results after "Ba"', function(done: any) {
testForRefreshedResult('a', done);
});
it('should fetch results after "Bas"', function(done: any) {
testForRefreshedResult('s', done);
});
it('should fetch results after "Basic"', function(done: any) {
testForRefreshedResult('ic', done);
});
});
function testForRefreshedResult(keyPressed: string, done: () => void) {
testForResult('my-wiki', keyPressed, false, done);
}
});
describe('Smarter Wikipedia Demo', function () {
it('should initialize the demo with empty result list', function () {
let myWikiSmartComp = element(by.tagName('my-wiki-smart'));
expect(myWikiSmartComp).toBeDefined('<my-wiki-smart> must exist');
let resultList = myWikiSmartComp.all(by.tagName('li'));
expect(resultList.count()).toBe(0, 'result list must be empty');
});
it('should fetch results after "Java"', function(done: any) {
testForNewResult('Java', done);
});
it('should fetch results after "JavaS"', function(done: any) {
testForStaleResult('S', done);
});
it('should fetch results after "JavaSc"', function(done: any) {
testForStaleResult('c', done);
});
it('should fetch results after "JavaScript"', function(done: any) {
testForStaleResult('ript', done);
});
function testForNewResult(keyPressed: string, done: () => void) {
testForResult('my-wiki-smart', keyPressed, false, done);
}
function testForStaleResult(keyPressed: string, done: () => void) {
testForResult('my-wiki-smart', keyPressed, true, done);
}
});
function testForResult(componentTagName: string, keyPressed: string, hasListBeforeSearch: boolean, done: () => void) {
let searchWait = 1000; // Wait for wikipedia but not so long that tests timeout
let wikiComponent = element(by.tagName(componentTagName));
expect(wikiComponent).toBeDefined('<' + componentTagName + '> must exist');
let searchBox = wikiComponent.element(by.tagName('input'));
expect(searchBox).toBeDefined('<input> for search must exist');
searchBox.sendKeys(keyPressed).then(function () {
let resultList = wikiComponent.all(by.tagName('li'));
if (hasListBeforeSearch) {
expect(resultList.count()).toBeGreaterThan(0, 'result list should not be empty before search');
}
setTimeout(function() {
expect(resultList.count()).toBeGreaterThan(0, 'result list should not be empty after search');
done();
}, searchWait);
});
}
});

View File

@ -1,139 +0,0 @@
import { browser, element, by, ElementFinder } from 'protractor';
import { resolve } from 'path';
const page = {
configClearButton: element.all(by.css('app-config > div button')).get(2),
configErrorButton: element.all(by.css('app-config > div button')).get(3),
configErrorMessage: element(by.css('app-config p')),
configGetButton: element.all(by.css('app-config > div button')).get(0),
configGetResponseButton: element.all(by.css('app-config > div button')).get(1),
configSpan: element(by.css('app-config span')),
downloadButton: element.all(by.css('app-downloader button')).get(0),
downloadClearButton: element.all(by.css('app-downloader button')).get(1),
downloadMessage: element(by.css('app-downloader p')),
heroesListAddButton: element.all(by.css('app-heroes > div button')).get(0),
heroesListInput: element(by.css('app-heroes > div input')),
heroesListSearchButton: element.all(by.css('app-heroes > div button')).get(1),
heroesListItems: element.all(by.css('app-heroes ul li')),
logClearButton: element(by.css('app-messages button')),
logList: element(by.css('app-messages ol')),
logListItems: element.all(by.css('app-messages ol li')),
searchInput: element(by.css('app-package-search input#name')),
searchListItems: element.all(by.css('app-package-search li')),
uploadInput: element(by.css('app-uploader input')),
uploadMessage: element(by.css('app-uploader p'))
};
let checkLogForMessage = (message: string) => {
expect(page.logList.getText()).toContain(message);
};
describe('Http Tests', function() {
beforeEach(() => {
browser.get('');
});
describe('Heroes', () => {
it('retrieves the list of heroes at startup', () => {
expect(page.heroesListItems.count()).toBe(4);
expect(page.heroesListItems.get(0).getText()).toContain('Mr. Nice');
checkLogForMessage('GET "api/heroes"');
});
it('makes a POST to add a new hero', () => {
page.heroesListInput.sendKeys('Magneta');
page.heroesListAddButton.click();
expect(page.heroesListItems.count()).toBe(5);
checkLogForMessage('POST "api/heroes"');
});
it('makes a GET to search for a hero', () => {
page.heroesListInput.sendKeys('Celeritas');
page.heroesListSearchButton.click();
checkLogForMessage('GET "api/heroes?name=Celeritas"');
});
});
describe('Messages', () => {
it('can clear the logs', () => {
expect(page.logListItems.count()).toBe(1);
page.logClearButton.click();
expect(page.logListItems.count()).toBe(0);
});
});
describe('Configuration', () => {
it('can fetch the configuration JSON file', () => {
page.configGetButton.click();
checkLogForMessage('GET "assets/config.json"');
expect(page.configSpan.getText()).toContain('Heroes API URL is "api/heroes"');
expect(page.configSpan.getText()).toContain('Textfile URL is "assets/textfile.txt"');
});
it('can fetch the configuration JSON file with headers', () => {
page.configGetResponseButton.click();
checkLogForMessage('GET "assets/config.json"');
expect(page.configSpan.getText()).toContain('Response headers:');
expect(page.configSpan.getText()).toContain('content-type: application/json; charset=UTF-8');
});
it('can clear the configuration log', () => {
page.configGetResponseButton.click();
expect(page.configSpan.getText()).toContain('Response headers:');
page.configClearButton.click();
expect(page.configSpan.isPresent()).toBeFalsy();
});
it('throws an error for a non valid url', () => {
page.configErrorButton.click();
checkLogForMessage('GET "not/a/real/url"');
expect(page.configErrorMessage.getText()).toContain('"Something bad happened; please try again later."');
});
});
describe('Download', () => {
it('can download a txt file and show it', () => {
page.downloadButton.click();
checkLogForMessage('DownloaderService downloaded "assets/textfile.txt"');
checkLogForMessage('GET "assets/textfile.txt"');
expect(page.downloadMessage.getText()).toContain('Contents: "This is the downloaded text file "');
});
it('can clear the log of the download', () => {
page.downloadButton.click();
expect(page.downloadMessage.getText()).toContain('Contents: "This is the downloaded text file "');
page.downloadClearButton.click();
expect(page.downloadMessage.isPresent()).toBeFalsy();
});
});
describe('Upload', () => {
it('can upload a file', () => {
const filename = 'app.po.ts';
const url = resolve(__dirname, filename);
page.uploadInput.sendKeys(url);
checkLogForMessage('POST "/upload/file" succeeded in');
expect(page.uploadMessage.getText()).toContain(
`File "${filename}" was completely uploaded!`);
});
});
describe('PackageSearch', () => {
it('can search for npm package and find in cache', () => {
const packageName = 'angular';
page.searchInput.sendKeys(packageName);
checkLogForMessage(
'Caching response from "https://npmsearch.com/query?q=angular"');
expect(page.searchListItems.count()).toBeGreaterThan(1, 'angular items');
page.searchInput.clear();
page.searchInput.sendKeys(' ');
expect(page.searchListItems.count()).toBe(0, 'search empty');
page.searchInput.clear();
page.searchInput.sendKeys(packageName);
checkLogForMessage(
'Found cached response for "https://npmsearch.com/query?q=angular"');
});
});
});

View File

@ -1,3 +0,0 @@
{
"projectType": "testing"
}

View File

@ -1,11 +1,10 @@
{
"description": "Http",
"basePath": "src/",
"files":[
"!**/*.d.ts",
"!**/*.js",
"!src/testing/*.*",
"!src/index-specs.html"
"!**/*.[1].*"
],
"tags": ["http"]
"tags": ["http", "jsonp"]
}

View File

@ -1,18 +0,0 @@
{
"description": "Http Guide Testing",
"files":[
"src/app/heroes/heroes.service.ts",
"src/app/heroes/heroes.service.spec.ts",
"src/app/http-error-handler.service.ts",
"src/app/message.service.ts",
"src/testing/*.ts",
"src/styles.css",
"src/test.css",
"src/main-specs.ts",
"src/index-specs.html"
],
"main": "src/index-specs.html",
"tags": ["http", "testing"]
}

View File

@ -1,24 +0,0 @@
<h1>HTTP Sample</h1>
<div>
<input type="checkbox" id="heroes" [checked]="toggleHeroes" (click)="toggleHeroes()">
<label for="heroes">Heroes</label>
<input type="checkbox" id="config" [checked]="showConfig" (click)="toggleConfig()">
<label for="config">Config</label>
<input type="checkbox" id="downloader" [checked]="showDownloader" (click)="toggleDownloader()">
<label for="downloader">Downloader</label>
<input type="checkbox" id="uploader" [checked]="showUploader" (click)="toggleUploader()">
<label for="uploader">Uploader</label>
<input type="checkbox" id="search" [checked]="showSearch" (click)="toggleSearch()">
<label for="search">Search</label>
</div>
<app-heroes *ngIf="showHeroes"></app-heroes>
<app-messages></app-messages>
<app-config *ngIf="showConfig"></app-config>
<app-downloader *ngIf="showDownloader"></app-downloader>
<app-uploader *ngIf="showUploader"></app-uploader>
<app-package-search *ngIf="showSearch"></app-package-search>

View File

@ -1,19 +1,13 @@
import { Component } from '@angular/core';
// #docregion
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
selector: 'my-app',
template: `
<hero-list></hero-list>
<hero-list-promise></hero-list-promise>
<my-wiki></my-wiki>
<my-wiki-smart></my-wiki-smart>
`
})
export class AppComponent {
showHeroes = true;
showConfig = true;
showDownloader = true;
showUploader = true;
showSearch = true;
toggleHeroes() { this.showHeroes = !this.showHeroes; }
toggleConfig() { this.showConfig = !this.showConfig; }
toggleDownloader() { this.showDownloader = !this.showDownloader; }
toggleUploader() { this.showUploader = !this.showUploader; }
toggleSearch() { this.showSearch = !this.showSearch; }
}
export class AppComponent { }

View File

@ -0,0 +1,23 @@
// #docregion
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule, JsonpModule } from '@angular/http';
import { AppComponent } from './app.component';
@NgModule({
imports: [
BrowserModule,
FormsModule,
HttpModule,
JsonpModule
],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule {
}

View File

@ -1,89 +1,46 @@
// #docplaster
// #docregion sketch
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
// #enddocregion sketch
import { FormsModule } from '@angular/forms';
// #docregion sketch
import { HttpClientModule } from '@angular/common/http';
// #enddocregion sketch
import { HttpClientXsrfModule } from '@angular/common/http';
// #docregion
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule, JsonpModule } from '@angular/http';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
import { RequestCache, RequestCacheWithMap } from './request-cache.service';
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { HeroData } from './hero-data';
import { requestOptionsProvider } from './default-request-options.service';
import { AppComponent } from './app.component';
import { AuthService } from './auth.service';
import { ConfigComponent } from './config/config.component';
import { DownloaderComponent } from './downloader/downloader.component';
import { HeroesComponent } from './heroes/heroes.component';
import { HttpErrorHandler } from './http-error-handler.service';
import { MessageService } from './message.service';
import { MessagesComponent } from './messages/messages.component';
import { PackageSearchComponent } from './package-search/package-search.component';
import { UploaderComponent } from './uploader/uploader.component';
import { AppComponent } from './app.component';
import { httpInterceptorProviders } from './http-interceptors/index';
// #docregion sketch
import { HeroListComponent } from './toh/hero-list.component';
import { HeroListPromiseComponent } from './toh/hero-list.component.promise';
import { WikiComponent } from './wiki/wiki.component';
import { WikiSmartComponent } from './wiki/wiki-smart.component';
@NgModule({
// #docregion xsrf
imports: [
// #enddocregion xsrf
BrowserModule,
// #enddocregion sketch
FormsModule,
// #docregion sketch
// import HttpClientModule after BrowserModule.
// #docregion xsrf
HttpClientModule,
// #enddocregion sketch
HttpClientXsrfModule.withOptions({
cookieName: 'My-Xsrf-Cookie',
headerName: 'My-Xsrf-Header',
}),
// #enddocregion xsrf
// The HttpClientInMemoryWebApiModule module intercepts HTTP requests
// and returns simulated server responses.
// Remove it when a real server is ready to receive requests.
HttpClientInMemoryWebApiModule.forRoot(
InMemoryDataService, {
dataEncapsulation: false,
passThruUnknownUrl: true,
put204: false // return entity after PUT/update
}
)
// #docregion sketch, xsrf
HttpModule,
JsonpModule,
// #docregion in-mem-web-api
InMemoryWebApiModule.forRoot(HeroData)
// #enddocregion in-mem-web-api
],
// #enddocregion xsrf
declarations: [
AppComponent,
// #enddocregion sketch
ConfigComponent,
DownloaderComponent,
HeroesComponent,
MessagesComponent,
UploaderComponent,
PackageSearchComponent,
// #docregion sketch
HeroListComponent,
HeroListPromiseComponent,
WikiComponent,
WikiSmartComponent
],
// #enddocregion sketch
// #docregion interceptor-providers
providers: [
// #enddocregion interceptor-providers
AuthService,
HttpErrorHandler,
MessageService,
{ provide: RequestCache, useClass: RequestCacheWithMap },
// #docregion interceptor-providers
httpInterceptorProviders
],
// #enddocregion interceptor-providers
// #docregion sketch
// #docregion provide-default-request-options
providers: [ requestOptionsProvider ],
// #enddocregion provide-default-request-options
bootstrap: [ AppComponent ]
})
export class AppModule {}
// #enddocregion sketch

View File

@ -1,9 +0,0 @@
import { Injectable } from '@angular/core';
/** Mock client-side authentication/authorization service */
@Injectable()
export class AuthService {
getAuthorizationToken() {
return 'some-auth-token';
}
}

View File

@ -1,18 +0,0 @@
<h3>Get configuration from JSON file</h3>
<div>
<button (click)="clear(); showConfig()">get</button>
<button (click)="clear(); showConfigResponse()">getResponse</button>
<button (click)="clear()">clear</button>
<button (click)="clear(); makeError()">error</button>
<span *ngIf="config">
<p>Heroes API URL is "{{config.heroesUrl}}"</p>
<p>Textfile URL is "{{config.textfile}}"</p>
<div *ngIf="headers">
Response headers:
<ul>
<li *ngFor="let header of headers">{{header}}</li>
</ul>
</div>
</span>
</div>
<p *ngIf="error" class="error">{{error | json}}</p>

View File

@ -1,78 +0,0 @@
// #docplaster
// #docregion
import { Component } from '@angular/core';
import { Config, ConfigService } from './config.service';
import { MessageService } from '../message.service';
@Component({
selector: 'app-config',
templateUrl: './config.component.html',
providers: [ ConfigService ],
styles: ['.error {color: red;}']
})
export class ConfigComponent {
error: any;
headers: string[];
// #docregion v2
config: Config;
// #enddocregion v2
constructor(private configService: ConfigService) {}
clear() {
this.config = undefined;
this.error = undefined;
this.headers = undefined;
}
// #docregion v1, v2, v3
showConfig() {
this.configService.getConfig()
// #enddocregion v1, v2
.subscribe(
data => this.config = { ...data }, // success path
error => this.error = error // error path
);
}
// #enddocregion v3
showConfig_v1() {
this.configService.getConfig_1()
// #docregion v1, v1_callback
.subscribe(data => this.config = {
heroesUrl: data['heroesUrl'],
textfile: data['textfile']
});
// #enddocregion v1_callback
}
// #enddocregion v1
showConfig_v2() {
this.configService.getConfig()
// #docregion v2, v2_callback
// clone the data object, using its known Config shape
.subscribe(data => this.config = { ...data });
// #enddocregion v2_callback
}
// #enddocregion v2
// #docregion showConfigResponse
showConfigResponse() {
this.configService.getConfigResponse()
// resp is of type `HttpResponse<Config>`
.subscribe(resp => {
// display its headers
const keys = resp.headers.keys();
this.headers = keys.map(key =>
`${key}: ${resp.headers.get(key)}`);
// access the body directly, which is typed as `Config`.
this.config = { ... resp.body };
});
}
// #enddocregion showConfigResponse
makeError() {
this.configService.makeIntentionalError().subscribe(null, error => this.error = error );
}
}
// #enddocregion

View File

@ -1,100 +0,0 @@
// #docplaster
// #docregion , proto
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
// #enddocregion proto
import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
// #docregion rxjs-imports
import { Observable } from 'rxjs/Observable';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { catchError, retry } from 'rxjs/operators';
// #enddocregion rxjs-imports
// #docregion config-interface
export interface Config {
heroesUrl: string;
textfile: string;
}
// #enddocregion config-interface
// #docregion proto
@Injectable()
export class ConfigService {
// #enddocregion proto
// #docregion getConfig_1
configUrl = 'assets/config.json';
// #enddocregion getConfig_1
// #docregion proto
constructor(private http: HttpClient) { }
// #enddocregion proto
// #docregion getConfig, getConfig_1, getConfig_2, getConfig_3
getConfig() {
// #enddocregion getConfig_1, getConfig_2, getConfig_3
return this.http.get<Config>(this.configUrl)
.pipe(
retry(3), // retry a failed request up to 3 times
catchError(this.handleError) // then handle the error
);
}
// #enddocregion getConfig
getConfig_1() {
// #docregion getConfig_1
return this.http.get(this.configUrl);
}
// #enddocregion getConfig_1
getConfig_2() {
// #docregion getConfig_2
// now returns an Observable of Config
return this.http.get<Config>(this.configUrl);
}
// #enddocregion getConfig_2
getConfig_3() {
// #docregion getConfig_3
return this.http.get<Config>(this.configUrl)
.pipe(
catchError(this.handleError)
);
}
// #enddocregion getConfig_3
// #docregion getConfigResponse
getConfigResponse(): Observable<HttpResponse<Config>> {
return this.http.get<Config>(
this.configUrl, { observe: 'response' });
}
// #enddocregion getConfigResponse
// #docregion handleError
private handleError(error: HttpErrorResponse) {
if (error.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly.
console.error('An error occurred:', error.error.message);
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong,
console.error(
`Backend returned code ${error.status}, ` +
`body was: ${error.error}`);
}
// return an ErrorObservable with a user-facing error message
return new ErrorObservable(
'Something bad happened; please try again later.');
};
// #enddocregion handleError
makeIntentionalError() {
return this.http.get('not/a/real/url')
.pipe(
catchError(this.handleError)
);
}
// #docregion proto
}
// #enddocregion proto

View File

@ -0,0 +1,16 @@
// #docregion
import { Injectable } from '@angular/core';
import { BaseRequestOptions, RequestOptions } from '@angular/http';
@Injectable()
export class DefaultRequestOptions extends BaseRequestOptions {
constructor() {
super();
// Set the default 'Content-Type' header
this.headers.set('Content-Type', 'application/json');
}
}
export const requestOptionsProvider = { provide: RequestOptions, useClass: DefaultRequestOptions };

View File

@ -1,4 +0,0 @@
<h3>Download the textfile</h3>
<button (click)="download()">Download</button>
<button (click)="clear()">clear</button>
<p *ngIf="contents">Contents: "{{contents}}"</p>

View File

@ -1,23 +0,0 @@
import { Component } from '@angular/core';
import { DownloaderService } from './downloader.service';
@Component({
selector: 'app-downloader',
templateUrl: './downloader.component.html',
providers: [ DownloaderService ]
})
export class DownloaderComponent {
contents: string;
constructor(private downloaderService: DownloaderService) {}
clear() {
this.contents = undefined;
}
// #docregion download
download() {
this.downloaderService.getTextFile('assets/textfile.txt')
.subscribe(results => this.contents = results);
}
// #enddocregion download
}

View File

@ -1,39 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { tap } from 'rxjs/operators';
import { MessageService } from '../message.service';
@Injectable()
export class DownloaderService {
constructor(
private http: HttpClient,
private messageService: MessageService) { }
// #docregion getTextFile
getTextFile(filename: string) {
// The Observable returned by get() is of type Observable<string>
// because a text response was specified.
// There's no need to pass a <string> type parameter to get().
return this.http.get(filename, {responseType: 'text'})
.pipe(
tap( // Log the result or error
data => this.log(filename, data),
error => this.logError(filename, error)
)
);
}
// #enddocregion getTextFile
private log(filename: string, data: string) {
const message = `DownloaderService downloaded "${filename}" and got "${data}".`;
this.messageService.add(message);
}
private logError(filename: string, error: any) {
const message = `DownloaderService failed to download "${filename}"; got error "${error.message}".`;
console.error(message);
this.messageService.add(message);
}
}

View File

@ -0,0 +1,13 @@
// #docregion
import { InMemoryDbService } from 'angular-in-memory-web-api';
export class HeroData implements InMemoryDbService {
createDb() {
let heroes = [
{ id: 1, name: 'Windstorm' },
{ id: 2, name: 'Bombasto' },
{ id: 3, name: 'Magneta' },
{ id: 4, name: 'Tornado' }
];
return {heroes};
}
}

View File

@ -0,0 +1,8 @@
{
"data": [
{ "id": 1, "name": "Windstorm" },
{ "id": 2, "name": "Bombasto" },
{ "id": 3, "name": "Magneta" },
{ "id": 4, "name": "Tornado" }
]
}

View File

@ -1,89 +0,0 @@
/* HeroesComponent's private CSS styles */
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
.heroes li {
position: relative;
cursor: pointer;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
width: 19em;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes a {
color: #888;
text-decoration: none;
position: relative;
display: block;
width: 250px;
}
.heroes a:hover {
color:#607D8B;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
min-width: 16px;
text-align: right;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
.button {
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
font-family: Arial;
}
button:hover {
background-color: #cfd8dc;
}
button.delete {
position: relative;
left: 24em;
top: -32px;
background-color: gray !important;
color: white;
display: inherit;
padding: 5px 8px;
width: 2em;
}
input {
font-size: 100%;
margin-bottom: 2px;
width: 11em;
}
.heroes input {
position: relative;
top: -3px;
width: 12em;
}

View File

@ -1,32 +0,0 @@
<h3>Heroes</h3>
<!-- #docregion add -->
<div>
<label>Hero name:
<input #heroName />
</label>
<!-- (click) passes input value to add() and then clears the input -->
<button (click)="add(heroName.value); heroName.value=''">
add
</button>
<button (click)="search(heroName.value)">
search
</button>
</div>
<!-- #enddocregion add -->
<!-- #docregion list -->
<ul class="heroes">
<li *ngFor="let hero of heroes">
<a (click)="edit(hero)">
<span class="badge">{{ hero.id || -1 }}</span>
<span *ngIf="hero!==editHero">{{hero.name}}</span>
<input *ngIf="hero===editHero" [(ngModel)]="hero.name"
(blur)="update()" (keyup.enter)="update()">
</a>
<!-- #docregion delete -->
<button class="delete" title="delete hero"
(click)="delete(hero)">x</button>
<!-- #enddocregion delete -->
</li>
</ul>
<!-- #enddocregion list -->

View File

@ -1,76 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Hero } from './hero';
import { HeroesService } from './heroes.service';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
providers: [ HeroesService ],
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
heroes: Hero[];
editHero: Hero; // the hero currently being edited
constructor(private heroesService: HeroesService) { }
ngOnInit() {
this.getHeroes();
}
getHeroes(): void {
this.heroesService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
add(name: string): void {
this.editHero = undefined;
name = name.trim();
if (!name) { return; }
// The server will generate the id for this new hero
const newHero: Hero = { name } as Hero;
// #docregion add-hero-subscribe
this.heroesService.addHero(newHero)
.subscribe(hero => this.heroes.push(hero));
// #enddocregion add-hero-subscribe
}
delete(hero: Hero): void {
this.heroes = this.heroes.filter(h => h !== hero);
// #docregion delete-hero-subscribe
this.heroesService.deleteHero(hero.id).subscribe();
// #enddocregion delete-hero-subscribe
/*
// #docregion delete-hero-no-subscribe
// oops ... subscribe() is missing so nothing happens
this.heroesService.deleteHero(hero.id);
// #enddocregion delete-hero-no-subscribe
*/
}
edit(hero) {
this.editHero = hero;
}
search(searchTerm: string) {
this.editHero = undefined;
if (searchTerm) {
this.heroesService.searchHeroes(searchTerm)
.subscribe(heroes => this.heroes = heroes);
}
}
update() {
if (this.editHero) {
this.heroesService.updateHero(this.editHero)
.subscribe(hero => {
// replace the hero in the heroes list with update from server
const ix = hero ? this.heroes.findIndex(h => h.id === hero.id) : -1;
if (ix > -1) { this.heroes[ix] = hero; }
});
this.editHero = undefined;
}
}
}

View File

@ -1,156 +0,0 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
// Other imports
import { TestBed } from '@angular/core/testing';
import { HttpClient, HttpResponse } from '@angular/common/http';
import { Hero } from './hero';
import { HeroesService } from './heroes.service';
import { HttpErrorHandler } from '../http-error-handler.service';
import { MessageService } from '../message.service';
describe('HeroesService', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
let heroService: HeroesService;
beforeEach(() => {
TestBed.configureTestingModule({
// Import the HttpClient mocking services
imports: [ HttpClientTestingModule ],
// Provide the service-under-test and its dependencies
providers: [
HeroesService,
HttpErrorHandler,
MessageService
]
});
// Inject the http, test controller, and service-under-test
// as they will be referenced by each test.
httpClient = TestBed.get(HttpClient);
httpTestingController = TestBed.get(HttpTestingController);
heroService = TestBed.get(HeroesService);
});
afterEach(() => {
// After every test, assert that there are no more pending requests.
httpTestingController.verify();
});
/// HeroService method tests begin ///
describe('#getHeroes', () => {
let expectedHeroes: Hero[];
beforeEach(() => {
heroService = TestBed.get(HeroesService);
expectedHeroes = [
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
] as Hero[];
});
it('should return expected heroes (called once)', () => {
heroService.getHeroes().subscribe(
heroes => expect(heroes).toEqual(expectedHeroes, 'should return expected heroes'),
fail
);
// HeroService should have made one request to GET heroes from expected URL
const req = httpTestingController.expectOne(heroService.heroesUrl);
expect(req.request.method).toEqual('GET');
// Respond with the mock heroes
req.flush(expectedHeroes);
});
it('should be OK returning no heroes', () => {
heroService.getHeroes().subscribe(
heroes => expect(heroes.length).toEqual(0, 'should have empty heroes array'),
fail
);
const req = httpTestingController.expectOne(heroService.heroesUrl);
req.flush([]); // Respond with no heroes
});
// This service reports the error but finds a way to let the app keep going.
it('should turn 404 into an empty heroes result', () => {
heroService.getHeroes().subscribe(
heroes => expect(heroes.length).toEqual(0, 'should return empty heroes array'),
fail
);
const req = httpTestingController.expectOne(heroService.heroesUrl);
// respond with a 404 and the error message in the body
const msg = 'deliberate 404 error';
req.flush(msg, {status: 404, statusText: 'Not Found'});
});
it('should return expected heroes (called multiple times)', () => {
heroService.getHeroes().subscribe();
heroService.getHeroes().subscribe();
heroService.getHeroes().subscribe(
heroes => expect(heroes).toEqual(expectedHeroes, 'should return expected heroes'),
fail
);
const requests = httpTestingController.match(heroService.heroesUrl);
expect(requests.length).toEqual(3, 'calls to getHeroes()');
// Respond to each request with different mock hero results
requests[0].flush([]);
requests[1].flush([{id: 1, name: 'bob'}]);
requests[2].flush(expectedHeroes);
});
});
describe('#updateHero', () => {
// Expecting the query form of URL so should not 404 when id not found
const makeUrl = (id: number) => `${heroService.heroesUrl}/?id=${id}`;
it('should update a hero and return it', () => {
const updateHero: Hero = { id: 1, name: 'A' };
heroService.updateHero(updateHero).subscribe(
data => expect(data).toEqual(updateHero, 'should return the hero'),
fail
);
// HeroService should have made one request to PUT hero
const req = httpTestingController.expectOne(heroService.heroesUrl);
expect(req.request.method).toEqual('PUT');
expect(req.request.body).toEqual(updateHero);
// Expect server to return the hero after PUT
const expectedResponse = new HttpResponse(
{ status: 200, statusText: 'OK', body: updateHero });
req.event(expectedResponse);
});
// This service reports the error but finds a way to let the app keep going.
it('should turn 404 error into return of the update hero', () => {
const updateHero: Hero = { id: 1, name: 'A' };
heroService.updateHero(updateHero).subscribe(
data => expect(data).toEqual(updateHero, 'should return the update hero'),
fail
);
const req = httpTestingController.expectOne(heroService.heroesUrl);
// respond with a 404 and the error message in the body
const msg = 'deliberate 404 error';
req.flush(msg, {status: 404, statusText: 'Not Found'});
});
});
// TODO: test other HeroService methods
});

View File

@ -1,99 +0,0 @@
// #docplaster
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
// #docregion http-options
import { HttpHeaders } from '@angular/common/http';
// #enddocregion http-options
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { catchError } from 'rxjs/operators';
import { Hero } from './hero';
import { HttpErrorHandler, HandleError } from '../http-error-handler.service';
// #docregion http-options
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'my-auth-token'
})
};
// #enddocregion http-options
@Injectable()
export class HeroesService {
heroesUrl = 'api/heroes'; // URL to web api
private handleError: HandleError;
constructor(
private http: HttpClient,
httpErrorHandler: HttpErrorHandler) {
this.handleError = httpErrorHandler.createHandleError('HeroesService');
}
/** GET heroes from the server */
getHeroes (): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
catchError(this.handleError('getHeroes', []))
);
}
// #docregion searchHeroes
/* GET heroes whose name contains search term */
searchHeroes(term: string): Observable<Hero[]> {
term = term.trim();
// Add safe, URL encoded search parameter if there is a search term
const options = term ?
{ params: new HttpParams().set('name', term) } : {};
return this.http.get<Hero[]>(this.heroesUrl, options)
.pipe(
catchError(this.handleError<Hero[]>('searchHeroes', []))
);
}
// #enddocregion searchHeroes
//////// Save methods //////////
// #docregion addHero
/** POST: add a new hero to the database */
addHero (hero: Hero): Observable<Hero> {
return this.http.post<Hero>(this.heroesUrl, hero, httpOptions)
.pipe(
catchError(this.handleError('addHero', hero))
);
}
// #enddocregion addHero
// #docregion deleteHero
/** DELETE: delete the hero from the server */
deleteHero (id: number): Observable<{}> {
const url = `${this.heroesUrl}/${id}`; // DELETE api/heroes/42
return this.http.delete(url, httpOptions)
.pipe(
catchError(this.handleError('deleteHero'))
);
}
// #enddocregion deleteHero
// #docregion updateHero
/** PUT: update the hero on the server. Returns the updated hero upon success. */
updateHero (hero: Hero): Observable<Hero> {
// #enddocregion updateHero
// #docregion update-headers
httpOptions.headers =
httpOptions.headers.set('Authorization', 'my-new-auth-token');
// #enddocregion update-headers
// #docregion updateHero
return this.http.put<Hero>(this.heroesUrl, hero, httpOptions)
.pipe(
catchError(this.handleError('updateHero', hero))
);
}
// #enddocregion updateHero
}

View File

@ -1,47 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { MessageService } from './message.service';
/** Type of the handleError function returned by HttpErrorHandler.createHandleError */
export type HandleError =
<T> (operation?: string, result?: T) => (error: HttpErrorResponse) => Observable<T>;
/** Handles HttpClient errors */
@Injectable()
export class HttpErrorHandler {
constructor(private messageService: MessageService) { }
/** Create curried handleError function that already knows the service name */
createHandleError = (serviceName = '') => <T>
(operation = 'operation', result = {} as T) => this.handleError(serviceName, operation, result);
/**
* Returns a function that handles Http operation failures.
* This error handler lets the app continue to run as if no error occurred.
* @param serviceName = name of the data service that attempted the operation
* @param operation - name of the operation that failed
* @param result - optional value to return as the observable result
*/
handleError<T> (serviceName = '', operation = 'operation', result = {} as T) {
return (error: HttpErrorResponse): Observable<T> => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
const message = (error.error instanceof ErrorEvent) ?
error.error.message :
`server returned code ${error.status} with body "${error.error}"`;
// TODO: better job of transforming error for user consumption
this.messageService.add(`${serviceName}: ${operation} failed: ${message}`);
// Let the app keep running by returning a safe result.
return of( result );
};
}
}

View File

@ -1,42 +0,0 @@
// #docplaster
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
// #docregion
import { AuthService } from '../auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
// Get the auth token from the service.
const authToken = this.auth.getAuthorizationToken();
// #enddocregion
/*
* The verbose way:
// #docregion
// Clone the request and replace the original headers with
// cloned headers, updated with the authorization.
const authReq = req.clone({
headers: req.headers.set('Authorization', authToken)
});
// #enddocregion
*/
// #docregion set-header-shortcut
// Clone the request and set the new header in one step.
const authReq = req.clone({ setHeaders: { Authorization: authToken } });
// #enddocregion set-header-shortcut
// #docregion
// send cloned request with header to the next handler.
return next.handle(authReq);
}
}
// #enddocregion

View File

@ -1,86 +0,0 @@
// #docplaster
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpHeaders, HttpRequest, HttpResponse,
HttpInterceptor, HttpHandler
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { startWith, tap } from 'rxjs/operators';
import { RequestCache } from '../request-cache.service';
import { searchUrl } from '../package-search/package-search.service';
/**
* If request is cachable (e.g., package search) and
* response is in cache return the cached response as observable.
* If has 'x-refresh' header that is true,
* then also re-run the package search, using response from next(),
* returning an observable that emits the cached response first.
*
* If not in cache or not cachable,
* pass request through to next()
*/
// #docregion v1
@Injectable()
export class CachingInterceptor implements HttpInterceptor {
constructor(private cache: RequestCache) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
// continue if not cachable.
if (!isCachable(req)) { return next.handle(req); }
const cachedResponse = this.cache.get(req);
// #enddocregion v1
// #docregion intercept-refresh
// cache-then-refresh
if (req.headers.get('x-refresh')) {
const results$ = sendRequest(req, next, this.cache);
return cachedResponse ?
results$.pipe( startWith(cachedResponse) ) :
results$;
}
// cache-or-fetch
// #docregion v1
return cachedResponse ?
of(cachedResponse) : sendRequest(req, next, this.cache);
// #enddocregion intercept-refresh
}
}
// #enddocregion v1
/** Is this request cachable? */
function isCachable(req: HttpRequest<any>) {
// Only GET requests are cachable
return req.method === 'GET' &&
// Only npm package search is cachable in this app
-1 < req.url.indexOf(searchUrl);
}
// #docregion send-request
/**
* Get server response observable by sending request to `next()`.
* Will add the response to the cache on the way out.
*/
function sendRequest(
req: HttpRequest<any>,
next: HttpHandler,
cache: RequestCache): Observable<HttpEvent<any>> {
// No headers allowed in npm search request
const noHeaderReq = req.clone({ headers: new HttpHeaders() });
return next.handle(noHeaderReq).pipe(
tap(event => {
// There may be other events besides the response.
if (event instanceof HttpResponse) {
cache.put(req, event); // Update the cache.
}
})
);
}
// #enddocregion send-request

View File

@ -1,20 +0,0 @@
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class EnsureHttpsInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// #docregion excerpt
// clone request and replace 'http://' with 'https://' at the same time
const secureReq = req.clone({
url: req.url.replace('http://', 'https://')
});
// send the cloned, "secure" request to the next handler.
return next.handle(secureReq);
// #enddocregion excerpt
}
}

View File

@ -1,34 +0,0 @@
// #docplaster
// #docregion interceptor-providers
/* "Barrel" of Http Interceptors */
import { HTTP_INTERCEPTORS } from '@angular/common/http';
// #enddocregion interceptor-providers
import { AuthInterceptor } from './auth-interceptor';
import { CachingInterceptor } from './caching-interceptor';
import { EnsureHttpsInterceptor } from './ensure-https-interceptor';
import { LoggingInterceptor } from './logging-interceptor';
// #docregion interceptor-providers
import { NoopInterceptor } from './noop-interceptor';
// #enddocregion interceptor-providers
import { TrimNameInterceptor } from './trim-name-interceptor';
import { UploadInterceptor } from './upload-interceptor';
// #docregion interceptor-providers
/** Http interceptor providers in outside-in order */
export const httpInterceptorProviders = [
// #docregion noop-provider
{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },
// #enddocregion noop-provider, interceptor-providers
{ provide: HTTP_INTERCEPTORS, useClass: EnsureHttpsInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: TrimNameInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: UploadInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true },
// #docregion interceptor-providers
];
// #enddocregion interceptor-providers

View File

@ -1,39 +0,0 @@
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler,
HttpRequest, HttpResponse
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
// #docregion excerpt
import { finalize, tap } from 'rxjs/operators';
import { MessageService } from '../message.service';
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
constructor(private messenger: MessageService) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
const started = Date.now();
let ok: string;
// extend server response observable with logging
return next.handle(req)
.pipe(
tap(
// Succeeds when there is a response; ignore other events
event => ok = event instanceof HttpResponse ? 'succeeded' : '',
// Operation failed; error is an HttpErrorResponse
error => ok = 'failed'
),
// Log when response observable either completes or errors
finalize(() => {
const elapsed = Date.now() - started;
const msg = `${req.method} "${req.urlWithParams}"
${ok} in ${elapsed} ms.`;
this.messenger.add(msg);
})
);
}
}
// #enddocregion excerpt

View File

@ -1,16 +0,0 @@
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
/** Pass untouched request through to the next request handler. */
@Injectable()
export class NoopInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler):
Observable<HttpEvent<any>> {
return next.handle(req);
}
}

View File

@ -1,24 +0,0 @@
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class TrimNameInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const body = req.body;
if (!body || !body.name ) {
return next.handle(req);
}
// #docregion excerpt
// copy the body and trim whitespace from the name property
const newBody = { ...body, name: body.name.trim() };
// clone request and set its body
const newReq = req.clone({ body: newBody });
// send the cloned request to the next handler.
return next.handle(newReq);
// #enddocregion excerpt
}
}

View File

@ -1,62 +0,0 @@
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler,
HttpRequest, HttpResponse,
HttpEventType, HttpProgressEvent
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
/** Simulate server replying to file upload request */
@Injectable()
export class UploadInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.indexOf('/upload/file') === -1) {
return next.handle(req);
}
const delay = 300; // Todo: inject delay?
return createUploadEvents(delay);
}
}
/** Create simulation of upload event stream */
function createUploadEvents(delay: number) {
// Simulate XHR behavior which would provide this information in a ProgressEvent
const chunks = 5;
const total = 12345678;
const chunkSize = Math.ceil(total / chunks);
return new Observable<HttpEvent<any>>(observer => {
// notify the event stream that the request was sent.
observer.next({type: HttpEventType.Sent});
uploadLoop(0);
function uploadLoop(loaded: number) {
// N.B.: Cannot use setInterval or rxjs delay (which uses setInterval)
// because e2e test won't complete. A zone thing?
// Use setTimeout and tail recursion instead.
setTimeout(() => {
loaded += chunkSize;
if (loaded >= total) {
const doneResponse = new HttpResponse({
status: 201, // OK but no body;
});
observer.next(doneResponse);
observer.complete();
return;
}
const progressEvent: HttpProgressEvent = {
type: HttpEventType.UploadProgress,
loaded,
total
};
observer.next(progressEvent);
uploadLoop(loaded);
}, delay);
}
});
}

View File

@ -1,13 +0,0 @@
import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const heroes = [
{ id: 11, name: 'Mr. Nice' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
];
return {heroes};
}
}

View File

@ -1,14 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable()
export class MessageService {
messages: string[] = [];
add(message: string) {
this.messages.push(message);
}
clear() {
this.messages = [];
}
}

View File

@ -1,8 +0,0 @@
<div *ngIf="messageService.messages.length">
<h3>Messages</h3>
<button class="clear" (click)="messageService.clear()">clear</button>
<br>
<ol>
<li *ngFor='let message of messageService.messages'> {{message}} </li>
</ol>
</div>

View File

@ -1,10 +0,0 @@
import { Component } from '@angular/core';
import { MessageService } from '../message.service';
@Component({
selector: 'app-messages',
templateUrl: './messages.component.html'
})
export class MessagesComponent {
constructor(public messageService: MessageService) {}
}

View File

@ -1,17 +0,0 @@
<!-- #docplaster -->
<h3>Search Npm Packages</h3>
<p><i>Searches when typing stops. Caches for 30 seconds.</i></p>
<!-- #docregion search -->
<input (keyup)="search($event.target.value)" id="name" placeholder="Search"/>
<!-- #enddocregion search -->
<input type="checkbox" id="refresh" [checked]="withRefresh" (click)="toggleRefresh()">
<label for="refresh">with refresh</label>
<!-- #docregion search -->
<ul>
<li *ngFor="let package of packages$ | async">
<b>{{package.name}} v.{{package.version}}</b> -
<i>{{package.description}}</i>
</li>
</ul>
<!-- #enddocregion search -->

View File

@ -1,39 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { NpmPackageInfo, PackageSearchService } from './package-search.service';
@Component({
selector: 'app-package-search',
templateUrl: './package-search.component.html',
providers: [ PackageSearchService ]
})
export class PackageSearchComponent implements OnInit {
// #docregion debounce
withRefresh = false;
packages$: Observable<NpmPackageInfo[]>;
private searchText$ = new Subject<string>();
search(packageName: string) {
this.searchText$.next(packageName);
}
ngOnInit() {
this.packages$ = this.searchText$.pipe(
debounceTime(500),
distinctUntilChanged(),
switchMap(packageName =>
this.searchService.search(packageName, this.withRefresh))
);
}
constructor(private searchService: PackageSearchService) { }
// #enddocregion debounce
toggleRefresh() { this.withRefresh = ! this.withRefresh; }
}

View File

@ -1,62 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { catchError, map } from 'rxjs/operators';
import { HttpErrorHandler, HandleError } from '../http-error-handler.service';
export interface NpmPackageInfo {
name: string;
version: string;
description: string;
}
export const searchUrl = 'https://npmsearch.com/query';
const httpOptions = {
headers: new HttpHeaders({
'x-refresh': 'true'
})
};
function createHttpOptions(packageName: string, refresh = false) {
// npm package name search api
// e.g., http://npmsearch.com/query?q=dom'
const params = new HttpParams({ fromObject: { q: packageName } });
const headerMap = refresh ? {'x-refresh': 'true'} : {};
const headers = new HttpHeaders(headerMap) ;
return { headers, params };
}
@Injectable()
export class PackageSearchService {
private handleError: HandleError;
constructor(
private http: HttpClient,
httpErrorHandler: HttpErrorHandler) {
this.handleError = httpErrorHandler.createHandleError('HeroesService');
}
search (packageName: string, refresh = false): Observable<NpmPackageInfo[]> {
// clear if no pkg name
if (!packageName.trim()) { return of([]); }
const options = createHttpOptions(packageName, refresh);
// TODO: Add error handling
return this.http.get(searchUrl, options).pipe(
map((data: any) => {
return data.results.map(entry => ({
name: entry.name[0],
version: entry.version[0],
description: entry.description[0]
} as NpmPackageInfo )
);
}),
catchError(this.handleError('search', []))
);
}
}

View File

@ -1,60 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse } from '@angular/common/http';
import { MessageService } from './message.service';
export interface RequestCacheEntry {
url: string;
response: HttpResponse<any>;
lastRead: number;
}
// #docregion request-cache
export abstract class RequestCache {
abstract get(req: HttpRequest<any>): HttpResponse<any> | undefined;
abstract put(req: HttpRequest<any>, response: HttpResponse<any>): void
}
// #enddocregion request-cache
const maxAge = 30000; // maximum cache age (ms)
@Injectable()
export class RequestCacheWithMap implements RequestCache {
cache = new Map<string, RequestCacheEntry>();
constructor(private messenger: MessageService) { }
get(req: HttpRequest<any>): HttpResponse<any> | undefined {
const url = req.urlWithParams;
const cached = this.cache.get(url);
if (!cached) {
return undefined;
}
const isExpired = cached.lastRead < (Date.now() - maxAge);
const expired = isExpired ? 'expired ' : '';
this.messenger.add(
`Found ${expired}cached response for "${url}".`);
return isExpired ? undefined : cached.response;
}
put(req: HttpRequest<any>, response: HttpResponse<any>): void {
const url = req.urlWithParams;
this.messenger.add(`Caching response from "${url}".`);
const entry = { url, response, lastRead: Date.now() };
this.cache.set(url, entry);
// remove expired cache entries
const expired = Date.now() - maxAge;
this.cache.forEach(entry => {
if (entry.lastRead < expired) {
this.cache.delete(entry.url);
}
});
this.messenger.add(`Request cache size: ${this.cache.size}.`);
}
}

View File

@ -0,0 +1,11 @@
<!-- #docregion -->
<h1>Tour of Heroes ({{mode}})</h1>
<h3>Heroes:</h3>
<ul>
<li *ngFor="let hero of heroes">{{hero.name}}</li>
</ul>
<label>New hero name: <input #newHeroName /></label>
<button (click)="addHero(newHeroName.value); newHeroName.value=''">Add Hero</button>
<p class="error" *ngIf="errorMessage">{{errorMessage}}</p>

View File

@ -0,0 +1,40 @@
// #docregion
// Promise Version
import { Component, OnInit } from '@angular/core';
import { Hero } from './hero';
import { HeroService } from './hero.service.promise';
@Component({
selector: 'hero-list-promise',
templateUrl: './hero-list.component.html',
providers: [ HeroService ],
styles: ['.error {color:red;}']
})
// #docregion component
export class HeroListPromiseComponent implements OnInit {
errorMessage: string;
heroes: Hero[];
mode = 'Promise';
constructor (private heroService: HeroService) {}
ngOnInit() { this.getHeroes(); }
// #docregion methods
getHeroes() {
this.heroService.getHeroes()
.then(
heroes => this.heroes = heroes,
error => this.errorMessage = <any>error);
}
addHero (name: string) {
if (!name) { return; }
this.heroService.addHero(name)
.then(
hero => this.heroes.push(hero),
error => this.errorMessage = <any>error);
}
// #enddocregion methods
}
// #enddocregion component

View File

@ -0,0 +1,44 @@
// #docregion
// Observable Version
import { Component, OnInit } from '@angular/core';
import { Hero } from './hero';
import { HeroService } from './hero.service';
@Component({
selector: 'hero-list',
templateUrl: './hero-list.component.html',
providers: [ HeroService ],
styles: ['.error {color:red;}']
})
// #docregion component
export class HeroListComponent implements OnInit {
errorMessage: string;
heroes: Hero[];
mode = 'Observable';
constructor (private heroService: HeroService) {}
ngOnInit() { this.getHeroes(); }
// #docregion methods
// #docregion getHeroes
getHeroes() {
this.heroService.getHeroes()
.subscribe(
heroes => this.heroes = heroes,
error => this.errorMessage = <any>error);
}
// #enddocregion getHeroes
// #docregion addHero
addHero(name: string) {
if (!name) { return; }
this.heroService.create(name)
.subscribe(
hero => this.heroes.push(hero),
error => this.errorMessage = <any>error);
}
// #enddocregion addHero
// #enddocregion methods
}
// #enddocregion component

View File

@ -0,0 +1,60 @@
// #docplaster
// #docregion
// Promise Version
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Headers, RequestOptions } from '@angular/http';
// #docregion rxjs-imports
import 'rxjs/add/operator/toPromise';
// #enddocregion rxjs-imports
import { Hero } from './hero';
@Injectable()
export class HeroService {
// URL to web api
private heroesUrl = 'app/heroes';
constructor (private http: Http) {}
// #docregion methods
getHeroes (): Promise<Hero[]> {
return this.http.get(this.heroesUrl)
.toPromise()
.then(this.extractData)
.catch(this.handleError);
}
addHero (name: string): Promise<Hero> {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this.heroesUrl, { name }, options)
.toPromise()
.then(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
return body.data || { };
}
private handleError (error: Response | any) {
// In a real world app, we might use a remote logging infrastructure
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Promise.reject(errMsg);
}
// #enddocregion methods
}
// #enddocregion

View File

@ -0,0 +1,80 @@
// #docplaster
// #docregion
// Observable Version
// #docregion v1
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
// #enddocregion v1
// #docregion import-request-options
import { Headers, RequestOptions } from '@angular/http';
// #enddocregion import-request-options
// #docregion v1
// #docregion rxjs-imports
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
// #enddocregion rxjs-imports
import { Hero } from './hero';
@Injectable()
export class HeroService {
// #docregion endpoint
private heroesUrl = 'api/heroes'; // URL to web API
// #enddocregion endpoint
// #docregion ctor
constructor (private http: Http) {}
// #enddocregion ctor
// #docregion methods, error-handling, http-get
getHeroes(): Observable<Hero[]> {
return this.http.get(this.heroesUrl)
.map(this.extractData)
.catch(this.handleError);
}
// #enddocregion error-handling, http-get, v1
// #docregion create, create-sig
create(name: string): Observable<Hero> {
// #enddocregion create-sig
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this.heroesUrl, { name }, options)
.map(this.extractData)
.catch(this.handleError);
}
// #enddocregion create
// #docregion v1, extract-data
private extractData(res: Response) {
let body = res.json();
return body.data || { };
}
// #enddocregion extract-data
// #docregion error-handling
private handleError (error: Response | any) {
// In a real world app, you might use a remote logging infrastructure
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
// #enddocregion error-handling, methods
}
// #enddocregion
/*
// #docregion endpoint-json
private heroesUrl = 'app/heroes.json'; // URL to JSON file
// #enddocregion endpoint-json
*/

View File

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

View File

@ -1,12 +0,0 @@
<h3>Upload file</h3>
<form enctype="multipart/form-data" method="post">
<div>
<label for="picked">Choose file to upload</label>
<div>
<input type="file" id="picked" #picked
(click)="message=''"
(change)="onPicked(picked)">
</div>
</div>
<p *ngIf="message">{{message}}</p>
</form>

View File

@ -1,25 +0,0 @@
import { Component } from '@angular/core';
import { UploaderService } from './uploader.service';
@Component({
selector: 'app-uploader',
templateUrl: './uploader.component.html',
providers: [ UploaderService ]
})
export class UploaderComponent {
message: string;
constructor(private uploaderService: UploaderService) {}
onPicked(input: HTMLInputElement) {
const file = input.files[0];
if (file) {
this.uploaderService.upload(file).subscribe(
msg => {
input.value = null;
this.message = msg;
}
);
}
}
}

View File

@ -1,105 +0,0 @@
import { Injectable } from '@angular/core';
import {
HttpClient, HttpEvent, HttpEventType, HttpProgressEvent,
HttpRequest, HttpResponse, HttpErrorResponse
} from '@angular/common/http';
import { of } from 'rxjs/observable/of';
import { catchError, last, map, tap } from 'rxjs/operators';
import { MessageService } from '../message.service';
@Injectable()
export class UploaderService {
constructor(
private http: HttpClient,
private messenger: MessageService) {}
// If uploading multiple files, change to:
// upload(files: FileList) {
// const formData = new FormData();
// files.forEach(f => formData.append(f.name, f));
// new HttpRequest('POST', '/upload/file', formData, {reportProgress: true});
// ...
// }
upload(file: File) {
if (!file) { return; }
// COULD HAVE WRITTEN:
// return this.http.post('/upload/file', file, {
// reportProgress: true,
// observe: 'events'
// }).pipe(
// Create the request object that POSTs the file to an upload endpoint.
// The `reportProgress` option tells HttpClient to listen and return
// XHR progress events.
// #docregion upload-request
const req = new HttpRequest('POST', '/upload/file', file, {
reportProgress: true
});
// #enddocregion upload-request
// #docregion upload-body
// The `HttpClient.request` API produces a raw event stream
// which includes start (sent), progress, and response events.
return this.http.request(req).pipe(
map(event => this.getEventMessage(event, file)),
tap(message => this.showProgress(message)),
last(), // return last (completed) message to caller
catchError(this.handleError(file))
);
// #enddocregion upload-body
}
// #docregion getEventMessage
/** Return distinct message for sent, upload progress, & response events */
private getEventMessage(event: HttpEvent<any>, file: File) {
switch (event.type) {
case HttpEventType.Sent:
return `Uploading file "${file.name}" of size ${file.size}.`;
case HttpEventType.UploadProgress:
// Compute and show the % done:
const percentDone = Math.round(100 * event.loaded / event.total);
return `File "${file.name}" is ${percentDone}% uploaded.`;
case HttpEventType.Response:
return `File "${file.name}" was completely uploaded!`;
default:
return `File "${file.name}" surprising upload event: ${event.type}.`;
}
}
// #enddocregion getEventMessage
/**
* Returns a function that handles Http upload failures.
* @param file - File object for file being uploaded
*
* When no `UploadInterceptor` and no server,
* you'll end up here in the error handler.
*/
private handleError(file: File) {
const userMessage = `${file.name} upload failed.`;
return (error: HttpErrorResponse) => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
const message = (error.error instanceof Error) ?
error.error.message :
`server returned code ${error.status} with body "${error.error}"`;
this.messenger.add(`${userMessage} ${message}`);
// Let app keep running but indicate failure.
return of(userMessage);
};
}
private showProgress(message: string) {
this.messenger.add(message);
}
}

View File

@ -0,0 +1,47 @@
/* tslint:disable: member-ordering forin */
// #docplaster
// #docregion
import { Component, OnInit } from '@angular/core';
// #docregion rxjs-imports
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/switchMap';
// #docregion import-subject
import { Subject } from 'rxjs/Subject';
// #enddocregion import-subject
import { WikipediaService } from './wikipedia.service';
@Component({
selector: 'my-wiki-smart',
template: `
<h1>Smarter Wikipedia Demo</h1>
<p>Search when typing stops</p>
<input #term (keyup)="search(term.value)"/>
<ul>
<li *ngFor="let item of items | async">{{item}}</li>
</ul>`,
providers: [ WikipediaService ]
})
export class WikiSmartComponent implements OnInit {
items: Observable<string[]>;
constructor (private wikipediaService: WikipediaService) {}
// #docregion subject
private searchTermStream = new Subject<string>();
search(term: string) { this.searchTermStream.next(term); }
// #enddocregion subject
ngOnInit() {
// #docregion observable-operators
this.items = this.searchTermStream
.debounceTime(300)
.distinctUntilChanged()
.switchMap((term: string) => this.wikipediaService.search(term));
// #enddocregion observable-operators
}
}

View File

@ -0,0 +1,26 @@
// #docregion
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { WikipediaService } from './wikipedia.service';
@Component({
selector: 'my-wiki',
template: `
<h1>Wikipedia Demo</h1>
<p>Search after each keystroke</p>
<input #term (keyup)="search(term.value)"/>
<ul>
<li *ngFor="let item of items | async">{{item}}</li>
</ul>`,
providers: [ WikipediaService ]
})
export class WikiComponent {
items: Observable<string[]>;
constructor (private wikipediaService: WikipediaService) { }
search (term: string) {
this.items = this.wikipediaService.search(term);
}
}

View File

@ -0,0 +1,26 @@
// Create the query string by hand
// #docregion
import { Injectable } from '@angular/core';
import { Jsonp } from '@angular/http';
import 'rxjs/add/operator/map';
@Injectable()
export class WikipediaService {
constructor(private jsonp: Jsonp) { }
// TODO: Add error handling
search(term: string) {
let wikiUrl = 'http://en.wikipedia.org/w/api.php';
// #docregion query-string
let queryString =
`?search=${term}&action=opensearch&format=json&callback=JSONP_CALLBACK`;
return this.jsonp
.get(wikiUrl + queryString)
.map(response => <string[]> response.json()[1]);
// #enddocregion query-string
}
}

View File

@ -0,0 +1,30 @@
// #docregion
import { Injectable } from '@angular/core';
import { Jsonp, URLSearchParams } from '@angular/http';
import 'rxjs/add/operator/map';
@Injectable()
export class WikipediaService {
constructor(private jsonp: Jsonp) {}
search (term: string) {
let wikiUrl = 'http://en.wikipedia.org/w/api.php';
// #docregion search-parameters
let params = new URLSearchParams();
params.set('search', term); // the user's search value
params.set('action', 'opensearch');
params.set('format', 'json');
params.set('callback', 'JSONP_CALLBACK');
// #enddocregion search-parameters
// #docregion call-jsonp
// TODO: Add error handling
return this.jsonp
.get(wikiUrl, { search: params })
.map(response => <string[]> response.json()[1]);
// #enddocregion call-jsonp
}
}

View File

@ -1,4 +0,0 @@
{
"heroesUrl": "api/heroes",
"textfile": "assets/textfile.txt"
}

View File

@ -1 +0,0 @@
This is the downloaded text file

View File

@ -1,88 +0,0 @@
// BROWSER TESTING SHIM
// Keep it in-sync with what karma-test-shim does
// #docregion
/*global jasmine, __karma__, window*/
(function () {
Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing.
// Uncomment to get full stacktrace output. Sometimes helpful, usually not.
// Error.stackTraceLimit = Infinity; //
jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000;
var baseURL = document.baseURI;
baseURL = baseURL + baseURL[baseURL.length-1] ? '' : '/';
System.config({
baseURL: baseURL,
// Extend usual application package list with test folder
packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } },
// Assume npm: is set in `paths` in systemjs.config
// Map the angular testing umd bundles
map: {
'@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
'@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
'@angular/common/http/testing': 'npm:@angular/common/bundles/common-http-testing.umd.js',
'@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
'@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
'@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
'@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
'@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
'@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js',
},
});
System.import('systemjs.config.js')
// .then(importSystemJsExtras) // not in this project
.then(initTestBed)
.then(initTesting);
/** Optional SystemJS configuration extras. Keep going w/o it */
function importSystemJsExtras(){
return System.import('systemjs.config.extras.js')
.catch(function(reason) {
console.log(
'Note: System.import could not load "systemjs.config.extras.js" where you might have added more configuration. It is an optional file so we will continue without it.'
);
console.log(reason);
});
}
function initTestBed(){
return Promise.all([
System.import('@angular/core/testing'),
System.import('@angular/platform-browser-dynamic/testing')
])
.then(function (providers) {
var coreTesting = providers[0];
var browserTesting = providers[1];
coreTesting.TestBed.initTestEnvironment(
browserTesting.BrowserDynamicTestingModule,
browserTesting.platformBrowserDynamicTesting());
})
}
// Import all spec files defined in the html (__spec_files__)
// and start Jasmine testrunner
function initTesting () {
console.log('loading spec files: '+__spec_files__.join(', '));
return Promise.all(
__spec_files__.map(function(spec) {
return System.import(spec);
})
)
// After all imports load, re-execute `window.onload` which
// triggers the Jasmine test-runner start or explain what went wrong
.then(success, console.error.bind(console));
function success () {
console.log('Spec files loaded; starting Jasmine testrunner');
window.onload();
}
}
})();

View File

@ -1,4 +0,0 @@
<!--
Intentionally empty placeholder for Stackblitz.
Do not need index.html in zip-download either as you should run tests with `npm test`
-->

View File

@ -1,14 +1,27 @@
<!doctype html>
<!DOCTYPE html>
<!-- #docregion -->
<html lang="en">
<head>
<meta charset="utf-8">
<title>HttpClient Demo</title>
<base href="/">
<head>
<title>Angular Http Demo</title>
<base href="/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfills -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('main.js').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<my-app></my-app>
</body>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>

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