Merge remote-tracking branch 'en/master' into aio
# Conflicts: # aio/content/guide/ajs-quick-reference.md # aio/content/guide/animations.md # aio/content/guide/aot-compiler.md # aio/content/guide/architecture.md # aio/content/guide/bootstrapping.md # aio/content/guide/browser-support.md # aio/content/guide/change-log.md # aio/content/guide/component-styles.md # aio/content/guide/deployment.md # aio/content/guide/forms.md # aio/content/guide/glossary.md # aio/content/guide/http.md # aio/content/guide/lifecycle-hooks.md # aio/content/guide/ngmodule-faq.md # aio/content/guide/ngmodule.md # aio/content/guide/pipes.md # aio/content/guide/reactive-forms.md # aio/content/guide/router.md # aio/content/guide/security.md # aio/content/guide/set-document-title.md # aio/content/guide/setup-systemjs-anatomy.md # aio/content/guide/setup.md # aio/content/guide/testing.md # aio/content/guide/typescript-configuration.md # aio/content/guide/upgrade.md # aio/content/marketing/docs.md # aio/content/marketing/features.html # aio/content/marketing/resources.json # aio/content/navigation.json # aio/src/app/layout/doc-viewer/doc-viewer.component.ts
This commit is contained in:
commit
c08a3830df
|
@ -0,0 +1,25 @@
|
||||||
|
# These options are enabled when running on CI
|
||||||
|
# We do this by copying this file to /etc/bazel.bazelrc at the start of the build.
|
||||||
|
# See remote cache documentation in /docs/BAZEL.md
|
||||||
|
|
||||||
|
# Don't be spammy in the logs
|
||||||
|
build --noshow_progress
|
||||||
|
|
||||||
|
# Don't run manual tests
|
||||||
|
test --test_tag_filters=-manual
|
||||||
|
|
||||||
|
# Enable experimental CircleCI bazel remote cache proxy
|
||||||
|
# See remote cache documentation in /docs/BAZEL.md
|
||||||
|
build --experimental_remote_spawn_cache --remote_rest_cache=http://localhost:7643
|
||||||
|
|
||||||
|
# Prevent unstable environment variables from tainting cache keys
|
||||||
|
build --experimental_strict_action_env
|
||||||
|
|
||||||
|
# Workaround https://github.com/bazelbuild/bazel/issues/3645
|
||||||
|
# Bazel doesn't calculate the memory ceiling correctly when running under Docker.
|
||||||
|
# Limit Bazel to consuming resources that fit in CircleCI "medium" class which is the default:
|
||||||
|
# https://circleci.com/docs/2.0/configuration-reference/#resource_class
|
||||||
|
build --local_resources=3072,2.0,1.0
|
||||||
|
|
||||||
|
# Retry in the event of flakes, eg. https://circleci.com/gh/angular/angular/31309
|
||||||
|
test --flaky_test_attempts=2
|
|
@ -12,8 +12,15 @@
|
||||||
## IMPORTANT
|
## IMPORTANT
|
||||||
# If you change the `docker_image` version, also change the `cache_key` suffix and the version of
|
# If you change the `docker_image` version, also change the `cache_key` suffix and the version of
|
||||||
# `com_github_bazelbuild_buildtools` in the `/WORKSPACE` file.
|
# `com_github_bazelbuild_buildtools` in the `/WORKSPACE` file.
|
||||||
var_1: &docker_image angular/ngcontainer:0.0.8
|
var_1: &docker_image angular/ngcontainer:0.1.0
|
||||||
var_2: &cache_key angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.0.8
|
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
|
# Settings common to each job
|
||||||
anchor_1: &job_defaults
|
anchor_1: &job_defaults
|
||||||
|
@ -34,10 +41,15 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- checkout:
|
- checkout:
|
||||||
<<: *post_checkout
|
<<: *post_checkout
|
||||||
# Check BUILD.bazel formatting before we have a node_modules directory
|
# See remote cache documentation in /docs/BAZEL.md
|
||||||
# Then we don't need any exclude pattern to avoid checking those files
|
- run: .circleci/setup_cache.sh
|
||||||
- run: 'buildifier -mode=check $(find . -type f \( -name BUILD.bazel -or -name BUILD \)) ||
|
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
|
||||||
(echo "BUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)'
|
- *setup-bazel-remote-cache
|
||||||
|
|
||||||
|
- run: 'yarn buildifier -mode=check ||
|
||||||
|
(echo -e "\nBUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)'
|
||||||
|
- run: 'yarn skylint ||
|
||||||
|
(echo -e "\n.bzl files have lint errors. Please run ''yarn skylint''"; exit 1)'
|
||||||
|
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: *cache_key
|
key: *cache_key
|
||||||
|
@ -51,15 +63,29 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- checkout:
|
- checkout:
|
||||||
<<: *post_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:
|
- restore_cache:
|
||||||
key: *cache_key
|
key: *cache_key
|
||||||
|
|
||||||
- run: bazel info release
|
- run: bazel info release
|
||||||
- run: bazel run @yarn//:yarn
|
- run: bazel run @yarn//:yarn
|
||||||
# Use bazel query so that we explicitly ask for all buildable targets to be built as well
|
# 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
|
# This avoids waiting for a build command to finish before running the first test
|
||||||
# See https://github.com/bazelbuild/bazel/issues/4257
|
# See https://github.com/bazelbuild/bazel/issues/4257
|
||||||
- run: bazel query --output=label '//packages/... union @angular//...' | xargs bazel test --config=ci
|
- 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
|
||||||
|
|
||||||
- save_cache:
|
- save_cache:
|
||||||
key: *cache_key
|
key: *cache_key
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Install bazel remote cache proxy
|
||||||
|
# This is temporary until the feature is no longer experimental on CircleCI.
|
||||||
|
# See remote cache documentation in /docs/BAZEL.md
|
||||||
|
|
||||||
|
set -u -e
|
||||||
|
|
||||||
|
readonly DOWNLOAD_URL="https://5-116431813-gh.circle-artifacts.com/0/pkg/bazel-remote-proxy-$(uname -s)_$(uname -m)"
|
||||||
|
|
||||||
|
curl --fail -o ~/bazel-remote-proxy "$DOWNLOAD_URL"
|
||||||
|
chmod +x ~/bazel-remote-proxy
|
|
@ -25,7 +25,7 @@ ISSUES MISSING IMPORTANT INFORMATION MAY BE CLOSED WITHOUT INVESTIGATION.
|
||||||
## Minimal reproduction of the problem with instructions
|
## 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
|
For bug reports please provide the *STEPS TO REPRODUCE* and if possible a *MINIMAL DEMO* of the problem via
|
||||||
https://plnkr.co or similar (you can use this template as a starting point: http://plnkr.co/edit/tpl:AvJOMERrnz94ekVua0u5).
|
https://stackblitz.com or similar (you can use this template as a starting point: https://stackblitz.com/fork/angular-gitter).
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## What is the motivation / use case for changing the behavior?
|
## What is the motivation / use case for changing the behavior?
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
# Configuration for angular-robot
|
||||||
|
|
||||||
|
# options for the merge plugin
|
||||||
|
merge:
|
||||||
|
# the status will be added to your pull requests
|
||||||
|
status:
|
||||||
|
# set to true to disable
|
||||||
|
disabled: false
|
||||||
|
# the name of the status
|
||||||
|
context: "ci/angular: merge status"
|
||||||
|
# text to show when all checks pass
|
||||||
|
successText: "All checks passed!"
|
||||||
|
# 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!"
|
||||||
|
|
||||||
|
# label to monitor
|
||||||
|
mergeLabel: "PR action: merge"
|
||||||
|
|
||||||
|
# list of checks that will determine if the merge label can be added
|
||||||
|
checks:
|
||||||
|
# whether the PR shouldn't have a conflict with the base branch
|
||||||
|
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: *"
|
||||||
|
- "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
|
||||||
|
requiredStatuses:
|
||||||
|
- "continuous-integration/travis-ci/pr"
|
||||||
|
- "code-review/pullapprove"
|
||||||
|
- "ci/circleci: build"
|
||||||
|
- "ci/circleci: lint"
|
||||||
|
|
||||||
|
# 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 has not been triaged yet
|
||||||
|
needsTriageMilestone: 83,
|
||||||
|
# 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/fix"
|
||||||
|
- "severity*"
|
||||||
|
- "freq*"
|
||||||
|
- "comp: *"
|
||||||
|
-
|
||||||
|
- "type: feature"
|
||||||
|
- "comp: *"
|
||||||
|
-
|
||||||
|
- "type: refactor"
|
||||||
|
- "comp: *"
|
||||||
|
-
|
||||||
|
- "type: RFC / Discussion / question"
|
||||||
|
- "comp: *"
|
|
@ -10,11 +10,11 @@
|
||||||
# brocco - Mike Brocchi
|
# brocco - Mike Brocchi
|
||||||
# chuckjaz - Chuck Jazdzewski
|
# chuckjaz - Chuck Jazdzewski
|
||||||
# filipesilva - Filipe Silva
|
# filipesilva - Filipe Silva
|
||||||
# Foxandxss - Jesús Rodríguez
|
|
||||||
# gkalpak - George Kalpakas
|
# gkalpak - George Kalpakas
|
||||||
# hansl - Hans Larsen
|
# hansl - Hans Larsen
|
||||||
# IgorMinar - Igor Minar
|
# IgorMinar - Igor Minar
|
||||||
# jasonaden - Jason Aden
|
# jasonaden - Jason Aden
|
||||||
|
# kapunahelewong - Kapunahele Wong
|
||||||
# kara - Kara Erickson
|
# kara - Kara Erickson
|
||||||
# matsko - Matias Niemelä
|
# matsko - Matias Niemelä
|
||||||
# mhevery - Misko Hevery
|
# mhevery - Misko Hevery
|
||||||
|
@ -44,6 +44,7 @@ groups:
|
||||||
all:
|
all:
|
||||||
users: all
|
users: all
|
||||||
required: 1
|
required: 1
|
||||||
|
rejection_value: -999
|
||||||
# In this group, your self-approval does not count
|
# In this group, your self-approval does not count
|
||||||
author_approval:
|
author_approval:
|
||||||
auto: false
|
auto: false
|
||||||
|
@ -91,7 +92,8 @@ groups:
|
||||||
users:
|
users:
|
||||||
- alexeagle #primary
|
- alexeagle #primary
|
||||||
- chuckjaz
|
- chuckjaz
|
||||||
- IgorMinar
|
- IgorMinar #fallback
|
||||||
|
- mhevery
|
||||||
- vikerman #fallback
|
- vikerman #fallback
|
||||||
|
|
||||||
build-and-ci:
|
build-and-ci:
|
||||||
|
@ -127,8 +129,9 @@ groups:
|
||||||
files:
|
files:
|
||||||
- "packages/core/*"
|
- "packages/core/*"
|
||||||
users:
|
users:
|
||||||
- chuckjaz #primary
|
- mhevery #primary
|
||||||
- mhevery
|
- chuckjaz
|
||||||
|
- kara
|
||||||
- vicb
|
- vicb
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
|
|
||||||
|
@ -139,7 +142,6 @@ groups:
|
||||||
- "packages/platform-browser/animations/*"
|
- "packages/platform-browser/animations/*"
|
||||||
users:
|
users:
|
||||||
- matsko #primary
|
- matsko #primary
|
||||||
- chuckjaz #fallback
|
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
|
|
||||||
|
@ -171,6 +173,7 @@ groups:
|
||||||
- hansl
|
- hansl
|
||||||
- filipesilva #fallback
|
- filipesilva #fallback
|
||||||
- brocco #fallback
|
- brocco #fallback
|
||||||
|
- IgorMinar #fallback
|
||||||
|
|
||||||
compiler-cli:
|
compiler-cli:
|
||||||
conditions:
|
conditions:
|
||||||
|
@ -204,6 +207,12 @@ groups:
|
||||||
conditions:
|
conditions:
|
||||||
files:
|
files:
|
||||||
- "packages/forms/*"
|
- "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:
|
users:
|
||||||
- kara #primary
|
- kara #primary
|
||||||
- tinayuangao #secondary
|
- tinayuangao #secondary
|
||||||
|
@ -216,9 +225,8 @@ groups:
|
||||||
- "packages/common/http/*"
|
- "packages/common/http/*"
|
||||||
- "packages/http/*"
|
- "packages/http/*"
|
||||||
users:
|
users:
|
||||||
- vikerman #primary
|
- alxhub #primary
|
||||||
- alxhub
|
- IgorMinar
|
||||||
- IgorMinar #fallback
|
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
|
|
||||||
language-service:
|
language-service:
|
||||||
|
@ -237,7 +245,7 @@ groups:
|
||||||
files:
|
files:
|
||||||
- "packages/router/*"
|
- "packages/router/*"
|
||||||
users:
|
users:
|
||||||
- jasonaden
|
- jasonaden #primary
|
||||||
- vicb
|
- vicb
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
|
@ -268,8 +276,7 @@ groups:
|
||||||
- "packages/platform-server/*"
|
- "packages/platform-server/*"
|
||||||
users:
|
users:
|
||||||
- vikerman #primary
|
- vikerman #primary
|
||||||
# needs secondary
|
- alxhub #secondary
|
||||||
- alxhub
|
|
||||||
- vicb
|
- vicb
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
|
@ -327,11 +334,11 @@ groups:
|
||||||
- "aio/content/navigation.json"
|
- "aio/content/navigation.json"
|
||||||
- "aio/content/license.md"
|
- "aio/content/license.md"
|
||||||
users:
|
users:
|
||||||
|
- kapunahelewong
|
||||||
- stephenfluin
|
- stephenfluin
|
||||||
- Foxandxss
|
|
||||||
- petebacondarwin
|
- petebacondarwin
|
||||||
- gkalpak
|
- gkalpak
|
||||||
- IgorMinar #fallback
|
- IgorMinar
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
|
|
||||||
angular.io-marketing:
|
angular.io-marketing:
|
||||||
|
@ -345,5 +352,5 @@ groups:
|
||||||
- stephenfluin
|
- stephenfluin
|
||||||
- petebacondarwin
|
- petebacondarwin
|
||||||
- gkalpak
|
- gkalpak
|
||||||
- IgorMinar #fallback
|
- IgorMinar
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
|
|
|
@ -56,7 +56,6 @@ env:
|
||||||
- CI_MODE=aio
|
- CI_MODE=aio
|
||||||
- CI_MODE=aio_e2e AIO_SHARD=0
|
- CI_MODE=aio_e2e AIO_SHARD=0
|
||||||
- CI_MODE=aio_e2e AIO_SHARD=1
|
- CI_MODE=aio_e2e AIO_SHARD=1
|
||||||
- CI_MODE=bazel
|
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
|
|
|
@ -24,9 +24,7 @@ filegroup(
|
||||||
"typescript",
|
"typescript",
|
||||||
"zone.js",
|
"zone.js",
|
||||||
"tsutils",
|
"tsutils",
|
||||||
"@types/jasmine",
|
"@types",
|
||||||
"@types/node",
|
|
||||||
"@types/source-map",
|
|
||||||
"tsickle",
|
"tsickle",
|
||||||
"hammerjs",
|
"hammerjs",
|
||||||
"protobufjs",
|
"protobufjs",
|
||||||
|
@ -34,6 +32,7 @@ filegroup(
|
||||||
"reflect-metadata",
|
"reflect-metadata",
|
||||||
"source-map-support",
|
"source-map-support",
|
||||||
"minimist",
|
"minimist",
|
||||||
|
"tslib",
|
||||||
] for ext in [
|
] for ext in [
|
||||||
"*.js",
|
"*.js",
|
||||||
"*.json",
|
"*.json",
|
||||||
|
|
306
CHANGELOG.md
306
CHANGELOG.md
|
@ -1,3 +1,309 @@
|
||||||
|
<a name="6.0.0-beta.5"></a>
|
||||||
|
# [6.0.0-beta.5](https://github.com/angular/angular/compare/6.0.0-beta.4...6.0.0-beta.5) (2018-02-22)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **animations:** report correct totalTime value even during noOp animations ([#22225](https://github.com/angular/angular/issues/22225)) ([e1bf067](https://github.com/angular/angular/commit/e1bf067))
|
||||||
|
* **common:** correct mapping of Observable methods ([#20518](https://github.com/angular/angular/issues/20518)) ([2639b4b](https://github.com/angular/angular/commit/2639b4b)), closes [#20516](https://github.com/angular/angular/issues/20516)
|
||||||
|
* **common:** then and else template might be set to null ([#22298](https://github.com/angular/angular/issues/22298)) ([8115edc](https://github.com/angular/angular/commit/8115edc))
|
||||||
|
* **compiler-cli:** add missing entry point to package, update tsickle ([#22295](https://github.com/angular/angular/issues/22295)) ([28ac244](https://github.com/angular/angular/commit/28ac244))
|
||||||
|
* **core:** properly handle function without prototype in reflector ([#22284](https://github.com/angular/angular/issues/22284)) ([a7ebf5a](https://github.com/angular/angular/commit/a7ebf5a)), closes [#19978](https://github.com/angular/angular/issues/19978)
|
||||||
|
* **core:** require factory to be provided for shakeable InjectionToken ([#22207](https://github.com/angular/angular/issues/22207)) ([f755db7](https://github.com/angular/angular/commit/f755db7)), closes [#22205](https://github.com/angular/angular/issues/22205)
|
||||||
|
* **forms:** set state before emitting a value from ngModelChange ([#21514](https://github.com/angular/angular/issues/21514)) ([3e6a86f](https://github.com/angular/angular/commit/3e6a86f)), closes [#21513](https://github.com/angular/angular/issues/21513)
|
||||||
|
* **core:** set `preserveWhitespaces` to false by default ([#22046](https://github.com/angular/angular/issues/22046)) ([f1a0632](https://github.com/angular/angular/commit/f1a0632)), closes [#22027](https://github.com/angular/angular/issues/22027)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **common:** better error message when non-template element used in NgIf ([#22274](https://github.com/angular/angular/issues/22274)) ([67cf11d](https://github.com/angular/angular/commit/67cf11d)), closes [#16410](https://github.com/angular/angular/issues/16410)
|
||||||
|
* **compiler-cli:** Check unvalidated combination of ngc and TypeScript ([#22293](https://github.com/angular/angular/issues/22293)) ([3ceee99](https://github.com/angular/angular/commit/3ceee99)), closes [#20669](https://github.com/angular/angular/issues/20669)
|
||||||
|
* **core:** support metadata reflection for native class types ([#22356](https://github.com/angular/angular/issues/22356)) ([5c89d6b](https://github.com/angular/angular/commit/5c89d6b)), closes [#21731](https://github.com/angular/angular/issues/21731)
|
||||||
|
* **platform-browser:** do not throw error when Hammer.js not loaded ([#22257](https://github.com/angular/angular/issues/22257)) ([991300b](https://github.com/angular/angular/commit/991300b)), closes [#16992](https://github.com/angular/angular/issues/16992)
|
||||||
|
* **platform-browser:** fix [#19604](https://github.com/angular/angular/issues/19604), can config hammerOptions ([#21979](https://github.com/angular/angular/issues/21979)) ([1d571b2](https://github.com/angular/angular/commit/1d571b2))
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* **animations:** When animation is triggered within a disabled zone, the
|
||||||
|
associated event (which an instance of AnimationEvent) will no longer
|
||||||
|
report the totalTime as 0 (it will emit the actual time of the
|
||||||
|
animation). To detect if an animation event is reporting a disabled
|
||||||
|
animation then the `event.disabled` property can be used instead.
|
||||||
|
|
||||||
|
* **forms:** ngModelChange is now emitted after the value/validity is updated on its control.
|
||||||
|
|
||||||
|
Previously, ngModelChange was emitted before its underlying control was updated.
|
||||||
|
This was fine if you passed through the value directly through the $event keyword, e.g.
|
||||||
|
|
||||||
|
```
|
||||||
|
<input [(ngModel)]="name" (ngModelChange)="onChange($event)">
|
||||||
|
|
||||||
|
onChange(value) {
|
||||||
|
console.log(value); // would log updated value
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
However, if you had a handler for the ngModelChange event that checked the value through the control,
|
||||||
|
you would get the old value rather than the updated value. e.g:
|
||||||
|
|
||||||
|
```
|
||||||
|
<input #modelDir="ngModel" [(ngModel)]="name" (ngModelChange)="onChange(modelDir)">
|
||||||
|
|
||||||
|
onChange(ngModel: NgModel) {
|
||||||
|
console.log(ngModel.value); // would log old value, not updated value
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now the value and validity will be updated before the ngModelChange event is emitted,
|
||||||
|
so the same setup will log the updated value.
|
||||||
|
|
||||||
|
```
|
||||||
|
onChange(ngModel: NgModel) {
|
||||||
|
console.log(ngModel.value); // will log updated value
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We think this order will be less confusing when the control is checked directly.
|
||||||
|
You will only need to update your app if it has relied on this bug to keep track of the old control value.
|
||||||
|
If that is the case, you should be able to track the old value directly by saving it on your component.
|
||||||
|
|
||||||
|
<a name="5.2.6"></a>
|
||||||
|
## [5.2.6](https://github.com/angular/angular/compare/5.2.5...5.2.6) (2018-02-22)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **common:** correct mapping of Observable methods ([#20518](https://github.com/angular/angular/issues/20518)) ([ce5e8fa](https://github.com/angular/angular/commit/ce5e8fa)), closes [#20516](https://github.com/angular/angular/issues/20516)
|
||||||
|
* **common:** then and else template might be set to null ([#22298](https://github.com/angular/angular/issues/22298)) ([af6a056](https://github.com/angular/angular/commit/af6a056))
|
||||||
|
* **compiler-cli:** add missing entry point to package, update tsickle ([#22295](https://github.com/angular/angular/issues/22295)) ([c5418c7](https://github.com/angular/angular/commit/c5418c7))
|
||||||
|
* **core:** properly handle function without prototype in reflector ([#22284](https://github.com/angular/angular/issues/22284)) ([5ec38f2](https://github.com/angular/angular/commit/5ec38f2)), closes [#19978](https://github.com/angular/angular/issues/19978)
|
||||||
|
* **core:** support metadata reflection for native class types ([#22356](https://github.com/angular/angular/issues/22356)) ([ee91de9](https://github.com/angular/angular/commit/ee91de9)), closes [#21731](https://github.com/angular/angular/issues/21731)
|
||||||
|
|
||||||
|
<a name="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)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **animations:** fix increment/decrement aliases example ([#18323](https://github.com/angular/angular/issues/18323)) ([48c1898](https://github.com/angular/angular/commit/48c1898))
|
||||||
|
* **benchpress:** should still support selenium_webdriver < 3.6.0 ([#21477](https://github.com/angular/angular/issues/21477)) ([3c6a506](https://github.com/angular/angular/commit/3c6a506))
|
||||||
|
* **common:** set correct timezone for ISO8601 dates in Safari ([#21506](https://github.com/angular/angular/issues/21506)) ([8e9cd57](https://github.com/angular/angular/commit/8e9cd57)), closes [#21491](https://github.com/angular/angular/issues/21491)
|
||||||
|
* **compiler:** cache external reference resolution ([#21359](https://github.com/angular/angular/issues/21359)) ([c32e833](https://github.com/angular/angular/commit/c32e833))
|
||||||
|
* **compiler:** make `.ngsummary.json` files idempotent ([#21448](https://github.com/angular/angular/issues/21448)) ([a931a41](https://github.com/angular/angular/commit/a931a41))
|
||||||
|
* **core:** fix chained http call ([#20924](https://github.com/angular/angular/issues/20924)) ([54e7576](https://github.com/angular/angular/commit/54e7576)), closes [#20921](https://github.com/angular/angular/issues/20921)
|
||||||
|
* **language-service:** Clear caches when program changes ([#21337](https://github.com/angular/angular/issues/21337)) ([cc9419d](https://github.com/angular/angular/commit/cc9419d)), 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)) ([51eb3d4](https://github.com/angular/angular/commit/51eb3d4))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **core:** add binding name to content changed error ([#20352](https://github.com/angular/angular/issues/20352)) ([4556532](https://github.com/angular/angular/commit/4556532))
|
||||||
|
* **forms:** handle string with and without line boundary on pattern validator ([#19256](https://github.com/angular/angular/issues/19256)) ([75f8522](https://github.com/angular/angular/commit/75f8522))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="5.2.0"></a>
|
||||||
|
# [5.2.0](https://github.com/angular/angular/compare/5.2.0-rc.0...5.2.0) (2018-01-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **bazel:** Give correct module names for ES6 output ([#21320](https://github.com/angular/angular/issues/21320)) ([9728dce](https://github.com/angular/angular/commit/9728dce)), closes [#21022](https://github.com/angular/angular/issues/21022)
|
||||||
|
* **benchpress:** forward compat with selenium_webdriver 3.6.0 ([#21399](https://github.com/angular/angular/issues/21399)) ([6040ee3](https://github.com/angular/angular/commit/6040ee3))
|
||||||
|
* **benchpress:** work around missing events from Chrome 63 ([#21396](https://github.com/angular/angular/issues/21396)) ([fa03ae1](https://github.com/angular/angular/commit/fa03ae1))
|
||||||
|
* **common:** export currencies via `getCurrencySymbol` ([#20983](https://github.com/angular/angular/issues/20983)) ([fecf768](https://github.com/angular/angular/commit/fecf768))
|
||||||
|
|
||||||
|
Note: Due to an animation fix back in 5.1.1 ([c2b3792](https://github.com/angular/angular/commit/c2b3792a3b5fa5215fe2ef7e0ac6511086ffe4c1)) which allows for nested :leave queries to work within animation sequences, all elements that are dynamically inserted (*ngIf, *ngFor, etc…) now contain the special CSS class: “ng-star-inserted”. This may cause failures within unit tests if there are any assertions that match against element.className directly. (An easy fix for this is to match using a regular expression instead of asserting the className string directly.)
|
||||||
|
|
||||||
<a name="5.2.0-rc.0"></a>
|
<a name="5.2.0-rc.0"></a>
|
||||||
# [5.2.0-rc.0](https://github.com/angular/angular/compare/5.2.0-beta.1...5.2.0-rc.0) (2018-01-04)
|
# [5.2.0-rc.0](https://github.com/angular/angular/compare/5.2.0-beta.1...5.2.0-rc.0) (2018-01-04)
|
||||||
|
|
||||||
|
|
|
@ -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.
|
Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.
|
||||||
|
|
||||||
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs we will systematically ask you to provide a minimal reproduction scenario using http://plnkr.co. Having a live, reproducible scenario gives us wealth of important information without going back & forth to you with additional questions like:
|
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs, we will systematically ask you to provide a minimal reproduction scenario using http://plnkr.co. Having a live, reproducible scenario gives us a wealth of important information without going back & forth to you with additional questions like:
|
||||||
|
|
||||||
- version of Angular used
|
- version of Angular used
|
||||||
- 3rd-party libraries and their versions
|
- 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.
|
We will be insisting on a minimal reproduce scenario in order to save maintainers time and ultimately be able to fix more bugs. Interestingly, from our experience users often find coding problems themselves while preparing a minimal plunk. We understand that sometimes it might be hard to extract essentials bits of code from a larger code-base but we really need to isolate the problem before we can fix it.
|
||||||
|
|
||||||
Unfortunately we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that don't have enough info to be reproduced.
|
Unfortunately, we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that doesn't have enough info to be reproduced.
|
||||||
|
|
||||||
You can file new issues by filling out our [new issue form](https://github.com/angular/angular/issues/new).
|
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
|
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.
|
that relates to your submission. You don't want to duplicate effort.
|
||||||
1. Please sign our [Contributor License Agreement (CLA)](#cla) before sending PRs.
|
1. Please sign our [Contributor License Agreement (CLA)](#cla) before sending PRs.
|
||||||
We cannot accept code without this.
|
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.
|
||||||
1. Fork the angular/angular repo.
|
1. Fork the angular/angular repo.
|
||||||
1. Make your changes in a new git branch:
|
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
|
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.
|
to read on GitHub as well as in various git tools.
|
||||||
|
|
||||||
Footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
|
The footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
|
||||||
|
|
||||||
Samples: (even more [samples](https://github.com/angular/angular/commits/master))
|
Samples: (even more [samples](https://github.com/angular/angular/commits/master))
|
||||||
|
|
||||||
```
|
```
|
||||||
docs(changelog): update change log to beta.5
|
docs(changelog): update changelog to beta.5
|
||||||
```
|
```
|
||||||
```
|
```
|
||||||
fix(release): need to depend on latest rxjs and zone.js
|
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
|
* **test**: Adding missing tests or correcting existing tests
|
||||||
|
|
||||||
### Scope
|
### Scope
|
||||||
The scope should be the name of the npm package affected (as perceived by person reading changelog generated from commit messages.
|
The scope should be the name of the npm package affected (as perceived by the person reading the changelog generated from commit messages.
|
||||||
|
|
||||||
The following is the list of supported scopes:
|
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`)
|
* none/empty string: useful for `style`, `test` and `refactor` changes that are done across all packages (e.g. `style: add missing semicolons`)
|
||||||
|
|
||||||
### Subject
|
### Subject
|
||||||
The subject contains succinct description of the change:
|
The subject contains a succinct description of the change:
|
||||||
|
|
||||||
* use the imperative, present tense: "change" not "changed" nor "changes"
|
* use the imperative, present tense: "change" not "changed" nor "changes"
|
||||||
* don't capitalize first letter
|
* don't capitalize the first letter
|
||||||
* no dot (.) at the end
|
* no dot (.) at the end
|
||||||
|
|
||||||
### Body
|
### Body
|
||||||
|
@ -259,6 +259,19 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise
|
||||||
* For corporations we'll need you to
|
* For corporations we'll need you to
|
||||||
[print, sign and one of scan+email, fax or mail the form][corporate-cla].
|
[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
|
[angular-group]: https://groups.google.com/forum/#!forum/angular
|
||||||
[coc]: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md
|
[coc]: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
[![CircleCI](https://circleci.com/gh/angular/angular/tree/master.svg?style=shield)](https://circleci.com/gh/angular/angular/tree/master)
|
[![CircleCI](https://circleci.com/gh/angular/angular/tree/master.svg?style=shield)](https://circleci.com/gh/angular/angular/tree/master)
|
||||||
[![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=LzF3RzBVVGt6VWE2S0hHaC9uYllOZz09LS1BVjNTclBKV0x4eVRlcjA4QVY1M0N3PT0=--eb4ce8c8dc2c1c5b2b5352d473ee12a73ac20e06)](https://www.browserstack.com/automate/public-build/LzF3RzBVVGt6VWE2S0hHaC9uYllOZz09LS1BVjNTclBKV0x4eVRlcjA4QVY1M0N3PT0=--eb4ce8c8dc2c1c5b2b5352d473ee12a73ac20e06)
|
[![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=LzF3RzBVVGt6VWE2S0hHaC9uYllOZz09LS1BVjNTclBKV0x4eVRlcjA4QVY1M0N3PT0=--eb4ce8c8dc2c1c5b2b5352d473ee12a73ac20e06)](https://www.browserstack.com/automate/public-build/LzF3RzBVVGt6VWE2S0hHaC9uYllOZz09LS1BVjNTclBKV0x4eVRlcjA4QVY1M0N3PT0=--eb4ce8c8dc2c1c5b2b5352d473ee12a73ac20e06)
|
||||||
[![Join the chat at https://gitter.im/angular/angular](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/angular/angular?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
[![Join the chat at https://gitter.im/angular/angular](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/angular/angular?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
[![Issue Stats](http://issuestats.com/github/angular/angular/badge/pr?style=flat)](http://issuestats.com/github/angular/angular)
|
|
||||||
[![Issue Stats](http://issuestats.com/github/angular/angular/badge/issue?style=flat)](http://issuestats.com/github/angular/angular)
|
|
||||||
[![npm version](https://badge.fury.io/js/%40angular%2Fcore.svg)](https://www.npmjs.com/@angular/core)
|
[![npm version](https://badge.fury.io/js/%40angular%2Fcore.svg)](https://www.npmjs.com/@angular/core)
|
||||||
|
|
||||||
|
|
||||||
|
|
75
WORKSPACE
75
WORKSPACE
|
@ -1,46 +1,49 @@
|
||||||
workspace(name = "angular_src")
|
workspace(name = "angular")
|
||||||
|
|
||||||
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
|
# Using a pre-release snapshot to pick up a commit that makes all nodejs_binary
|
||||||
|
# programs produce source-mapped stack traces and uglify sourcemaps.
|
||||||
|
RULES_NODEJS_VERSION = "4303cbef12e5e252ad66cc35cff1123e3a44ee83"
|
||||||
|
|
||||||
git_repository(
|
http_archive(
|
||||||
name = "build_bazel_rules_nodejs",
|
name = "build_bazel_rules_nodejs",
|
||||||
remote = "https://github.com/bazelbuild/rules_nodejs.git",
|
url = "https://github.com/bazelbuild/rules_nodejs/archive/%s.zip" % RULES_NODEJS_VERSION,
|
||||||
tag = "0.3.1",
|
strip_prefix = "rules_nodejs-%s" % RULES_NODEJS_VERSION,
|
||||||
|
sha256 = "fccb9a7122f339d89c9994dc0fea33c737dd76e66281d0da0cb841da5f1edec7",
|
||||||
)
|
)
|
||||||
|
|
||||||
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories")
|
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories")
|
||||||
|
|
||||||
check_bazel_version("0.8.1")
|
check_bazel_version("0.9.0")
|
||||||
node_repositories(package_json = ["//:package.json"])
|
node_repositories(package_json = ["//:package.json"])
|
||||||
|
|
||||||
git_repository(
|
RULES_TYPESCRIPT_VERSION = "d3cc5cd72d89aee0e4c2553ae1b99c707ecbef4e"
|
||||||
|
|
||||||
|
http_archive(
|
||||||
name = "build_bazel_rules_typescript",
|
name = "build_bazel_rules_typescript",
|
||||||
remote = "https://github.com/bazelbuild/rules_typescript.git",
|
url = "https://github.com/bazelbuild/rules_typescript/archive/%s.zip" % RULES_TYPESCRIPT_VERSION,
|
||||||
# tag = "0.7.1+",
|
strip_prefix = "rules_typescript-%s" % RULES_TYPESCRIPT_VERSION,
|
||||||
commit = "89d2c75066bea3d9c942f29dd1d2ea543c58d6d5"
|
sha256 = "a233fcca41c3e59f639ac71c396edb30e9e9716cf8ed5fb20b51ff8910d5d895",
|
||||||
)
|
)
|
||||||
|
|
||||||
load("@build_bazel_rules_typescript//:setup.bzl", "ts_setup_workspace")
|
load("@build_bazel_rules_typescript//:defs.bzl", "ts_setup_workspace")
|
||||||
|
|
||||||
ts_setup_workspace()
|
ts_setup_workspace()
|
||||||
|
|
||||||
local_repository(
|
|
||||||
name = "angular",
|
|
||||||
path = "packages/bazel",
|
|
||||||
)
|
|
||||||
|
|
||||||
local_repository(
|
local_repository(
|
||||||
name = "rxjs",
|
name = "rxjs",
|
||||||
path = "node_modules/rxjs/src",
|
path = "node_modules/rxjs/src",
|
||||||
)
|
)
|
||||||
|
|
||||||
git_repository(
|
# This commit matches the version of buildifier in angular/ngcontainer
|
||||||
|
# If you change this, also check if it matches the version in the angular/ngcontainer
|
||||||
|
# version in /.circleci/config.yml
|
||||||
|
BAZEL_BUILDTOOLS_VERSION = "b3b620e8bcff18ed3378cd3f35ebeb7016d71f71"
|
||||||
|
|
||||||
|
http_archive(
|
||||||
name = "com_github_bazelbuild_buildtools",
|
name = "com_github_bazelbuild_buildtools",
|
||||||
remote = "https://github.com/bazelbuild/buildtools.git",
|
url = "https://github.com/bazelbuild/buildtools/archive/%s.zip" % BAZEL_BUILDTOOLS_VERSION,
|
||||||
# Note, this commit matches the version of buildifier in angular/ngcontainer
|
strip_prefix = "buildtools-%s" % BAZEL_BUILDTOOLS_VERSION,
|
||||||
# If you change this, also check if it matches the version in the angular/ngcontainer
|
sha256 = "dad19224258ed67cbdbae9b7befb785c3b966e5a33b04b3ce58ddb7824b97d73",
|
||||||
# version in /.circleci/config.yml
|
|
||||||
commit = "b3b620e8bcff18ed3378cd3f35ebeb7016d71f71",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
http_archive(
|
http_archive(
|
||||||
|
@ -54,3 +57,31 @@ load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_too
|
||||||
go_rules_dependencies()
|
go_rules_dependencies()
|
||||||
|
|
||||||
go_register_toolchains()
|
go_register_toolchains()
|
||||||
|
|
||||||
|
# Fetching the Bazel source code allows us to compile the Skylark linter
|
||||||
|
http_archive(
|
||||||
|
name = "io_bazel",
|
||||||
|
url = "https://github.com/bazelbuild/bazel/archive/9755c72b48866ed034bd28aa033e9abd27431b1e.zip",
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
],
|
],
|
||||||
"e2e": {
|
"e2e": {
|
||||||
"protractor": {
|
"protractor": {
|
||||||
"config": "./protractor.conf.js"
|
"config": "tests/e2e/protractor.conf.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lint": [
|
"lint": [
|
||||||
|
@ -50,18 +50,21 @@
|
||||||
"project": "src/tsconfig.spec.json"
|
"project": "src/tsconfig.spec.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"project": "e2e/tsconfig.e2e.json"
|
"project": "tests/e2e/tsconfig.e2e.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"test": {
|
"test": {
|
||||||
"karma": {
|
"karma": {
|
||||||
"config": "./karma.conf.js"
|
"config": "src/karma.conf.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"styleExt": "scss",
|
"styleExt": "scss",
|
||||||
"component": {
|
"component": {
|
||||||
"inlineStyle": true
|
"inlineStyle": true
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"namedChunks": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ Everything in this folder is part of the documentation project. This includes
|
||||||
|
|
||||||
* the web site for displaying the documentation
|
* 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 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 plunkers and zip files from the examples.
|
* the tooling for setting up examples for development; and generating live-example and zip files from the examples.
|
||||||
|
|
||||||
## Developer tasks
|
## 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:
|
Here are the most important tasks you might need to use:
|
||||||
|
|
||||||
* `yarn` - install all the dependencies.
|
* `yarn` - install all the dependencies.
|
||||||
* `yarn setup` - install all the dependencies, boilerplate, plunkers, zips and run dgeni on the docs.
|
* `yarn setup` - install all the dependencies, boilerplate, stackblitz, 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 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).
|
* `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: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 boilerplate:remove` - remove all the boilerplate code that was added via `yarn boilerplate:add`.
|
||||||
* `yarn generate-plunkers` - generate the plunker files that are used by the `live-example` tags in the docs.
|
* `yarn generate-stackblitz` - generate the stackblitz 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 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
|
* `yarn example-e2e` - run all e2e tests for examples
|
||||||
|
@ -68,6 +68,11 @@ The content is written in markdown.
|
||||||
All other content is written using markdown in text files, located in the `angular/aio/content` folder.
|
All other content is written using markdown in text files, located in the `angular/aio/content` folder.
|
||||||
More specifically, there are sub-folders that contain particular types of content: guides, tutorial and marketing.
|
More specifically, there are sub-folders that contain particular types of content: guides, tutorial and marketing.
|
||||||
|
|
||||||
|
* **Code examples**: code examples need to be testable to ensure their accuracy.
|
||||||
|
Also, our examples have a specific look and feel and allow the user to copy the source code. For larger
|
||||||
|
examples they are rendered in a tabbed interface (e.g. template, HTML, and TypeScript on separate
|
||||||
|
tabs). Additionally, some are live examples, which provide links where the code can be edited, executed, and/or downloaded. For details on working with code examples, please read the [Code snippets](https://angular.io/guide/docs-style-guide#code-snippets), [Source code markup](https://angular.io/guide/docs-style-guide#source-code-markup), and [Live examples](https://angular.io/guide/docs-style-guide#live-examples) pages of the [Authors Style Guide](https://angular.io/guide/docs-style-guide).
|
||||||
|
|
||||||
We use the [dgeni](https://github.com/angular/dgeni) tool to convert these files into docs that can be viewed in the doc-viewer.
|
We use the [dgeni](https://github.com/angular/dgeni) tool to convert these files into docs that can be viewed in the doc-viewer.
|
||||||
|
|
||||||
The [Authors Style Guide](https://angular.io/guide/docs-style-guide) prescribes guidelines for
|
The [Authors Style Guide](https://angular.io/guide/docs-style-guide) prescribes guidelines for
|
||||||
|
@ -100,8 +105,7 @@ The general setup is as follows:
|
||||||
* Open a terminal, ensure the dependencies are installed; run an initial doc generation; then start the doc-viewer:
|
* Open a terminal, ensure the dependencies are installed; run an initial doc generation; then start the doc-viewer:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn
|
yarn setup
|
||||||
yarn docs
|
|
||||||
yarn start
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -76,8 +76,8 @@ aot-compiler/**/*.factory.d.ts
|
||||||
# universal
|
# universal
|
||||||
!universal/webpack.server.config.js
|
!universal/webpack.server.config.js
|
||||||
|
|
||||||
# plunkers
|
# stackblitz
|
||||||
*plnkr.no-link.html
|
*stackblitz.no-link.html
|
||||||
|
|
||||||
# ngUpgrade testing
|
# ngUpgrade testing
|
||||||
!upgrade-phonecat-*/**/karma.conf.js
|
!upgrade-phonecat-*/**/karma.conf.js
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"description": "AngularJS to Angular Quick Reference",
|
"description": "AngularJS to Angular Quick Reference",
|
||||||
"basePath": "src/",
|
|
||||||
"files":[
|
"files":[
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
"!**/*.js",
|
"!**/*.js",
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"description": "Angular Animations",
|
"description": "Angular Animations",
|
||||||
"basePath": "src/",
|
|
||||||
"files":[
|
"files":[
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
"!**/*.js"
|
"!**/*.js"
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"description": "Intro to Angular",
|
"description": "Intro to Angular",
|
||||||
"basePath": "src/",
|
|
||||||
"files":[
|
"files":[
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
"!**/*.js",
|
"!**/*.js",
|
||||||
"!app/hero-list.component.1.*"
|
"!**/*.[1].*"
|
||||||
]
|
],
|
||||||
|
"file": "src/app/app.module.ts"
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
// Not used. Keep away from plunker
|
// Not used. Keep away from stackblitz
|
||||||
// Keeps ATLS from complaining about undeclared directives.
|
// Keeps ATLS from complaining about undeclared directives.
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
{
|
{
|
||||||
"description": "Attribute Directive",
|
"description": "Attribute Directive",
|
||||||
"basePath": "src/",
|
|
||||||
"files":[
|
"files":[
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
"!**/*.js",
|
"!**/*.js",
|
||||||
"!app/*.[0,1,2,3].*"
|
"!**/*.[1,2,3].*"
|
||||||
],
|
],
|
||||||
"tags": ["attribute", "directive"]
|
"tags": ["attribute", "directive"]
|
||||||
}
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"baseDir": "src",
|
||||||
|
"routes": {
|
||||||
|
"/node_modules": "node_modules"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { AppPage } from './app.po';
|
||||||
|
|
||||||
|
describe('feature-modules App', () => {
|
||||||
|
let page: AppPage;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
page = new AppPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display message saying app works', () => {
|
||||||
|
page.navigateTo();
|
||||||
|
expect(page.getParagraphText()).toEqual('app works!');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,3 @@
|
||||||
|
<h1>
|
||||||
|
{{title}}
|
||||||
|
</h1>
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { TestBed, async } from '@angular/core/testing';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
|
describe('AppComponent', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent
|
||||||
|
],
|
||||||
|
});
|
||||||
|
TestBed.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the app', async(() => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.debugElement.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it(`should have as title 'app works!'`, async(() => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.debugElement.componentInstance;
|
||||||
|
expect(app.title).toEqual('app works!');
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should render title in a h1 tag', async(() => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
const compiled = fixture.debugElement.nativeElement;
|
||||||
|
expect(compiled.querySelector('h1').textContent).toContain('app works!');
|
||||||
|
}));
|
||||||
|
});
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrls: ['./app.component.css']
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
title = 'app works!';
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
// #docplaster
|
||||||
|
// #docregion whole-ngmodule
|
||||||
|
|
||||||
|
// imports
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { HttpModule } from '@angular/http';
|
||||||
|
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
// #docregion directive-import
|
||||||
|
import { ItemDirective } from './item.directive';
|
||||||
|
// #enddocregion directive-import
|
||||||
|
|
||||||
|
|
||||||
|
// @NgModule decorator with its metadata
|
||||||
|
@NgModule({
|
||||||
|
// #docregion declarations
|
||||||
|
declarations: [
|
||||||
|
AppComponent,
|
||||||
|
ItemDirective
|
||||||
|
],
|
||||||
|
// #enddocregion declarations
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
FormsModule,
|
||||||
|
HttpModule
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
bootstrap: [AppComponent]
|
||||||
|
})
|
||||||
|
export class AppModule { }
|
||||||
|
|
||||||
|
// #enddocregion whole-ngmodule
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { ItemDirective } from './item.directive';
|
||||||
|
|
||||||
|
describe('ItemDirective', () => {
|
||||||
|
it('should create an instance', () => {
|
||||||
|
const directive = new ItemDirective();
|
||||||
|
expect(directive).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
// #docplaster
|
||||||
|
// #docregion directive
|
||||||
|
|
||||||
|
|
||||||
|
import { Directive } from '@angular/core';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[appItem]'
|
||||||
|
})
|
||||||
|
export class ItemDirective {
|
||||||
|
// code goes here
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
}
|
||||||
|
// #enddocregion directive
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>NgmoduleDemo</title>
|
||||||
|
<base href="/">
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root>Loading...</app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { enableProdMode } from '@angular/core';
|
||||||
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||||
|
|
||||||
|
import { AppModule } from './app/app.module';
|
||||||
|
import { environment } from './environments/environment';
|
||||||
|
|
||||||
|
if (environment.production) {
|
||||||
|
enableProdMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
platformBrowserDynamic().bootstrapModule(AppModule);
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"description": "Bootstrapping",
|
||||||
|
"files": [
|
||||||
|
"!**/*.d.ts",
|
||||||
|
"!**/*.js",
|
||||||
|
"!**/*.[1,2].*"
|
||||||
|
],
|
||||||
|
"file": "src/app/app.component.ts",
|
||||||
|
"tags": ["ngmodules"]
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"description": "Component Communication Cookbook samples",
|
"description": "Component Communication Cookbook samples",
|
||||||
"basePath": "src/",
|
|
||||||
"files":[
|
"files":[
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
"!**/*.js"
|
"!**/*.js"
|
|
@ -1,5 +1,6 @@
|
||||||
/* #docregion import */
|
/* #docregion import */
|
||||||
@import 'hero-details-box.css';
|
/* The AOT compiler needs the `./` to show that this is local */
|
||||||
|
@import './hero-details-box.css';
|
||||||
/* #enddocregion import */
|
/* #enddocregion import */
|
||||||
|
|
||||||
/* #docregion host */
|
/* #docregion host */
|
||||||
|
|
|
@ -5,7 +5,8 @@ import { Hero } from './hero';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-hero-team',
|
selector: 'app-hero-team',
|
||||||
template: `
|
template: `
|
||||||
<link rel="stylesheet" href="assets/hero-team.component.css">
|
<!-- We must use a relative URL so that the AOT compiler can find the stylesheet -->
|
||||||
|
<link rel="stylesheet" href="../assets/hero-team.component.css">
|
||||||
<h3>Team</h3>
|
<h3>Team</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li *ngFor="let member of hero.team">
|
<li *ngFor="let member of hero.team">
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"description": "Component Styles",
|
"description": "Component Styles",
|
||||||
"basePath": "src/",
|
|
||||||
"files": [
|
"files": [
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
"!**/*.js",
|
"!**/*.js",
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"description": "Dependency Injection",
|
"description": "Dependency Injection",
|
||||||
"basePath": "src/",
|
|
||||||
"files":[
|
"files":[
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
"!**/*.js",
|
"!**/*.js",
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"description": "Dependency Injection",
|
"description": "Dependency Injection",
|
||||||
"basePath": "src/",
|
|
||||||
"files":[
|
"files":[
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
"!**/*.js",
|
"!**/*.js",
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"description": "Displaying Data",
|
"description": "Displaying Data",
|
||||||
"basePath": "src/",
|
|
||||||
"files": [
|
"files": [
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
"!**/*.js",
|
"!**/*.js",
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"description": "Second authors style guide plunker (non-executing)",
|
|
||||||
"basePath": "src/",
|
|
||||||
"files": [
|
|
||||||
"index.2.html"
|
|
||||||
],
|
|
||||||
"main": "index.2.html",
|
|
||||||
"tags": ["author", "style guide"]
|
|
||||||
}
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"description": "Second authors style guide stackblitz (non-executing)",
|
||||||
|
"files": [
|
||||||
|
"src/index.2.html"
|
||||||
|
],
|
||||||
|
"main": "src/index.2.html",
|
||||||
|
"tags": ["author", "style guide"]
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"description": "Authors style guide",
|
"description": "Authors style guide",
|
||||||
"basePath": "src/",
|
|
||||||
"files": [
|
"files": [
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
"!**/*.js",
|
"!**/*.js",
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"description": "Dynamic Component Loader",
|
"description": "Dynamic Component Loader",
|
||||||
"basePath": "src/",
|
|
||||||
"files":[
|
"files":[
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
"!**/*.js"
|
"!**/*.js"
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"description": "Dynamic Form",
|
"description": "Dynamic Form",
|
||||||
"basePath": "src/",
|
|
||||||
"files":[
|
"files":[
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
"!**/*.js",
|
"!**/*.js",
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { AppPage } from './app.po';
|
||||||
|
|
||||||
|
describe('feature-modules App', () => {
|
||||||
|
let page: AppPage;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
page = new AppPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display message saying app works', () => {
|
||||||
|
page.navigateTo();
|
||||||
|
expect(page.getParagraphText()).toEqual('app works!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"build": "build:cli",
|
||||||
|
"run": "serve:cli"
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
<!-- #docplaster -->
|
||||||
|
<!-- #docregion app-component-template -->
|
||||||
|
<h1>
|
||||||
|
{{title}}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- add the selector from the CustomerDashboardComponent -->
|
||||||
|
<app-customer-dashboard></app-customer-dashboard>
|
||||||
|
<!-- #enddocregion app-component-template -->
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { TestBed, async } from '@angular/core/testing';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
|
describe('AppComponent', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent
|
||||||
|
],
|
||||||
|
});
|
||||||
|
TestBed.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the app', async(() => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.debugElement.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it(`should have as title 'app works!'`, async(() => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.debugElement.componentInstance;
|
||||||
|
expect(app.title).toEqual('app works!');
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should render title in a h1 tag', async(() => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
const compiled = fixture.debugElement.nativeElement;
|
||||||
|
expect(compiled.querySelector('h1').textContent).toContain('app works!');
|
||||||
|
}));
|
||||||
|
});
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrls: ['./app.component.css']
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
title = 'app works!';
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
// #docplaster
|
||||||
|
// #docregion app-module
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { HttpModule } from '@angular/http';
|
||||||
|
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
// import the feature module here so you can add it to the imports array below
|
||||||
|
import { CustomerDashboardModule } from './customer-dashboard/customer-dashboard.module';
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
FormsModule,
|
||||||
|
HttpModule,
|
||||||
|
CustomerDashboardModule // add the feature module here
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
bootstrap: [AppComponent]
|
||||||
|
})
|
||||||
|
export class AppModule { }
|
||||||
|
// #enddocregion app-module
|
|
@ -0,0 +1,34 @@
|
||||||
|
// #docplaster
|
||||||
|
// #docregion customer-dashboard
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
// #enddocregion customer-dashboard
|
||||||
|
// #docregion customer-dashboard-component
|
||||||
|
// import the new component
|
||||||
|
import { CustomerDashboardComponent } from './customer-dashboard/customer-dashboard.component';
|
||||||
|
// #enddocregion customer-dashboard-component
|
||||||
|
|
||||||
|
|
||||||
|
// #docregion customer-dashboard-component
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
CustomerDashboardComponent
|
||||||
|
],
|
||||||
|
// #enddocregion customer-dashboard-component
|
||||||
|
// #docregion component-exports
|
||||||
|
exports: [
|
||||||
|
CustomerDashboardComponent
|
||||||
|
]
|
||||||
|
// #enddocregion component-exports
|
||||||
|
// #docregion customer-dashboard-component
|
||||||
|
})
|
||||||
|
|
||||||
|
// #enddocregion customer-dashboard-component
|
||||||
|
|
||||||
|
// #docregion customer-dashboard
|
||||||
|
export class CustomerDashboardModule { }
|
||||||
|
|
||||||
|
// #enddocregion customer-dashboard
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
<!-- #docplaster -->
|
||||||
|
<!-- #docregion feature-template -->
|
||||||
|
<p>
|
||||||
|
customer-dashboard works!
|
||||||
|
</p>
|
||||||
|
<!-- #enddocregion feature-template -->
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CustomerDashboardComponent } from './customer-dashboard.component';
|
||||||
|
|
||||||
|
describe('CustomerDashboardComponent', () => {
|
||||||
|
let component: CustomerDashboardComponent;
|
||||||
|
let fixture: ComponentFixture<CustomerDashboardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ CustomerDashboardComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CustomerDashboardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-customer-dashboard',
|
||||||
|
templateUrl: './customer-dashboard.component.html',
|
||||||
|
styleUrls: ['./customer-dashboard.component.css']
|
||||||
|
})
|
||||||
|
export class CustomerDashboardComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Feature Modules</title>
|
||||||
|
<base href="/">
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root>Loading...</app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { enableProdMode } from '@angular/core';
|
||||||
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||||
|
|
||||||
|
import { AppModule } from './app/app.module';
|
||||||
|
import { environment } from './environments/environment';
|
||||||
|
|
||||||
|
if (environment.production) {
|
||||||
|
enableProdMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
platformBrowserDynamic().bootstrapModule(AppModule);
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"description": "Feature Modules",
|
||||||
|
"files": [
|
||||||
|
"!**/*.d.ts",
|
||||||
|
"!**/*.js",
|
||||||
|
"!**/*.[1,2].*"
|
||||||
|
],
|
||||||
|
"file": "src/app/app.component.ts",
|
||||||
|
"tags": ["feature modules"]
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ describe('Form Validation Tests', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
tests('Template-Driven Form');
|
tests('Template-Driven Form');
|
||||||
|
bobTests();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Reactive form', () => {
|
describe('Reactive form', () => {
|
||||||
|
|
|
@ -20,7 +20,7 @@ export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
|
||||||
// #enddocregion directive-providers
|
// #enddocregion directive-providers
|
||||||
})
|
})
|
||||||
export class ForbiddenValidatorDirective implements Validator {
|
export class ForbiddenValidatorDirective implements Validator {
|
||||||
@Input() forbiddenName: string;
|
@Input('appForbiddenName') forbiddenName: string;
|
||||||
|
|
||||||
validate(control: AbstractControl): {[key: string]: any} {
|
validate(control: AbstractControl): {[key: string]: any} {
|
||||||
return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)
|
return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<!-- #docregion name-with-error-msg -->
|
<!-- #docregion name-with-error-msg -->
|
||||||
<!-- #docregion name-input -->
|
<!-- #docregion name-input -->
|
||||||
<input id="name" name="name" class="form-control"
|
<input id="name" name="name" class="form-control"
|
||||||
required minlength="4" forbiddenName="bob"
|
required minlength="4" appForbiddenName="bob"
|
||||||
[(ngModel)]="hero.name" #name="ngModel" >
|
[(ngModel)]="hero.name" #name="ngModel" >
|
||||||
<!-- #enddocregion name-input -->
|
<!-- #enddocregion name-input -->
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"description": "Validation",
|
"description": "Validation",
|
||||||
"basePath": "src/",
|
|
||||||
"files":[
|
"files":[
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
"!**/*.js"
|
"!**/*.js"
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"description": "Forms",
|
"description": "Forms",
|
||||||
"basePath": "src/",
|
|
||||||
"files":[
|
"files":[
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
"!**/*.js"
|
"!**/*.js"
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"description": "Hierarchical Dependency Injection",
|
"description": "Hierarchical Dependency Injection",
|
||||||
"basePath": "src/",
|
|
||||||
"files":[
|
"files":[
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
"!**/*.js"
|
"!**/*.js"
|
|
@ -1,138 +0,0 @@
|
||||||
'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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
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"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"projectType": "testing"
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
<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>
|
|
@ -1,13 +1,19 @@
|
||||||
// #docregion
|
import { Component } from '@angular/core';
|
||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-app',
|
selector: 'app-root',
|
||||||
template: `
|
templateUrl: './app.component.html'
|
||||||
<hero-list></hero-list>
|
|
||||||
<hero-list-promise></hero-list-promise>
|
|
||||||
<my-wiki></my-wiki>
|
|
||||||
<my-wiki-smart></my-wiki-smart>
|
|
||||||
`
|
|
||||||
})
|
})
|
||||||
export class AppComponent { }
|
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; }
|
||||||
|
}
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
// #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 {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,46 +1,89 @@
|
||||||
// #docplaster
|
// #docplaster
|
||||||
// #docregion
|
// #docregion sketch
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { FormsModule } from '@angular/forms';
|
// #enddocregion sketch
|
||||||
import { HttpModule, JsonpModule } from '@angular/http';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
// #docregion sketch
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
// #enddocregion sketch
|
||||||
|
import { HttpClientXsrfModule } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
|
||||||
|
import { InMemoryDataService } from './in-memory-data.service';
|
||||||
|
|
||||||
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
|
import { RequestCache, RequestCacheWithMap } from './request-cache.service';
|
||||||
import { HeroData } from './hero-data';
|
|
||||||
import { requestOptionsProvider } from './default-request-options.service';
|
|
||||||
|
|
||||||
import { AppComponent } from './app.component';
|
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 { HeroListComponent } from './toh/hero-list.component';
|
import { httpInterceptorProviders } from './http-interceptors/index';
|
||||||
import { HeroListPromiseComponent } from './toh/hero-list.component.promise';
|
// #docregion sketch
|
||||||
|
|
||||||
import { WikiComponent } from './wiki/wiki.component';
|
|
||||||
import { WikiSmartComponent } from './wiki/wiki-smart.component';
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
// #docregion xsrf
|
||||||
imports: [
|
imports: [
|
||||||
|
// #enddocregion xsrf
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
// #enddocregion sketch
|
||||||
FormsModule,
|
FormsModule,
|
||||||
HttpModule,
|
// #docregion sketch
|
||||||
JsonpModule,
|
// import HttpClientModule after BrowserModule.
|
||||||
// #docregion in-mem-web-api
|
// #docregion xsrf
|
||||||
InMemoryWebApiModule.forRoot(HeroData)
|
HttpClientModule,
|
||||||
// #enddocregion in-mem-web-api
|
// #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
|
||||||
],
|
],
|
||||||
|
// #enddocregion xsrf
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
HeroListComponent,
|
// #enddocregion sketch
|
||||||
HeroListPromiseComponent,
|
ConfigComponent,
|
||||||
WikiComponent,
|
DownloaderComponent,
|
||||||
WikiSmartComponent
|
HeroesComponent,
|
||||||
|
MessagesComponent,
|
||||||
|
UploaderComponent,
|
||||||
|
PackageSearchComponent,
|
||||||
|
// #docregion sketch
|
||||||
],
|
],
|
||||||
// #docregion provide-default-request-options
|
// #enddocregion sketch
|
||||||
providers: [ requestOptionsProvider ],
|
// #docregion interceptor-providers
|
||||||
// #enddocregion provide-default-request-options
|
providers: [
|
||||||
|
// #enddocregion interceptor-providers
|
||||||
|
AuthService,
|
||||||
|
HttpErrorHandler,
|
||||||
|
MessageService,
|
||||||
|
{ provide: RequestCache, useClass: RequestCacheWithMap },
|
||||||
|
// #docregion interceptor-providers
|
||||||
|
httpInterceptorProviders
|
||||||
|
],
|
||||||
|
// #enddocregion interceptor-providers
|
||||||
|
// #docregion sketch
|
||||||
bootstrap: [ AppComponent ]
|
bootstrap: [ AppComponent ]
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
// #enddocregion sketch
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
/** Mock client-side authentication/authorization service */
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
getAuthorizationToken() {
|
||||||
|
return 'some-auth-token';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,78 @@
|
||||||
|
// #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
|
|
@ -0,0 +1,100 @@
|
||||||
|
// #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
|
|
@ -1,16 +0,0 @@
|
||||||
// #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 };
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
<h3>Download the textfile</h3>
|
||||||
|
<button (click)="download()">Download</button>
|
||||||
|
<button (click)="clear()">clear</button>
|
||||||
|
<p *ngIf="contents">Contents: "{{contents}}"</p>
|
|
@ -0,0 +1,23 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +0,0 @@
|
||||||
// #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};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"data": [
|
|
||||||
{ "id": 1, "name": "Windstorm" },
|
|
||||||
{ "id": 2, "name": "Bombasto" },
|
|
||||||
{ "id": 3, "name": "Magneta" },
|
|
||||||
{ "id": 4, "name": "Tornado" }
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface Hero {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
/* 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;
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
<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 -->
|
|
@ -0,0 +1,76 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
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
|
||||||
|
});
|
|
@ -0,0 +1,99 @@
|
||||||
|
// #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
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
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 );
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
// #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
|
|
@ -0,0 +1,86 @@
|
||||||
|
// #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
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
// #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
|
|
@ -0,0 +1,39 @@
|
||||||
|
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
|
|
@ -0,0 +1,16 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue