Compare commits
60 Commits
rollup
...
6.0.0-beta
Author | SHA1 | Date | |
---|---|---|---|
61abba4bed | |||
5da72cc465 | |||
3db75b3f64 | |||
db3e65fb17 | |||
7a20691f13 | |||
ccd0298ec9 | |||
eeab433c8d | |||
c83c4168ca | |||
06d6c76192 | |||
9dca5f2743 | |||
81306c1f61 | |||
0365592119 | |||
407b5cf408 | |||
4c1743cce3 | |||
7305e8b45e | |||
285dd6be34 | |||
b10540a0b5 | |||
b62739a989 | |||
bb577c624b | |||
02483a01ad | |||
120bdeecdc | |||
8dff9d84ed | |||
b4cd27979b | |||
11b12670b2 | |||
dd48df105b | |||
18174e5564 | |||
72265f796f | |||
6e8bc310f0 | |||
c40ae7f7cf | |||
5bd93b1f0f | |||
a1cc02f0bd | |||
5778bb820a | |||
87754ad5ec | |||
d364117aa8 | |||
6245637e81 | |||
ab69f12e2c | |||
1278cca883 | |||
ede9cb7c2f | |||
c3fb820473 | |||
2af19c96f2 | |||
dcca799dbb | |||
d3d3f7191a | |||
e0b31dbfef | |||
8df56fe93a | |||
676d9c2c4b | |||
2b68e8d98a | |||
d964491f2a | |||
e6080527c6 | |||
3b7bab7d22 | |||
108fa15792 | |||
1b2271a3b1 | |||
f9381e42de | |||
bbb8f386f1 | |||
08aa54e1d9 | |||
fac4d8d42a | |||
170885c51b | |||
5713faa667 | |||
23596b3f30 | |||
f8fa20d71a | |||
d6d8fe829a |
@ -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",
|
||||||
|
28
CHANGELOG.md
28
CHANGELOG.md
@ -1,3 +1,31 @@
|
|||||||
|
<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>
|
<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)
|
# [6.0.0-beta.1](https://github.com/angular/angular/compare/6.0.0-beta.0...6.0.0-beta.1) (2018-01-25)
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
|
||||||
@ -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
|
||||||
|
@ -5,7 +5,7 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
|
|||||||
git_repository(
|
git_repository(
|
||||||
name = "build_bazel_rules_nodejs",
|
name = "build_bazel_rules_nodejs",
|
||||||
remote = "https://github.com/bazelbuild/rules_nodejs.git",
|
remote = "https://github.com/bazelbuild/rules_nodejs.git",
|
||||||
tag = "0.3.1",
|
commit = "230d39a391226f51c03448f91eb61370e2e58c42",
|
||||||
)
|
)
|
||||||
|
|
||||||
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories")
|
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories")
|
||||||
@ -16,7 +16,7 @@ node_repositories(package_json = ["//:package.json"])
|
|||||||
git_repository(
|
git_repository(
|
||||||
name = "build_bazel_rules_typescript",
|
name = "build_bazel_rules_typescript",
|
||||||
remote = "https://github.com/bazelbuild/rules_typescript.git",
|
remote = "https://github.com/bazelbuild/rules_typescript.git",
|
||||||
commit = "c4ea003acd7d42269b81a2d25eb832972cd24912"
|
commit = "eb3244363e1cb265c84e723b347926f28c29aa35"
|
||||||
)
|
)
|
||||||
|
|
||||||
load("@build_bazel_rules_typescript//:defs.bzl", "ts_setup_workspace")
|
load("@build_bazel_rules_typescript//:defs.bzl", "ts_setup_workspace")
|
||||||
|
BIN
aio/content/examples/.DS_Store
vendored
Normal file
BIN
aio/content/examples/.DS_Store
vendored
Normal file
Binary file not shown.
@ -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,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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
139
aio/content/examples/http/e2e/app.e2e-spec.ts
Normal file
139
aio/content/examples/http/e2e/app.e2e-spec.ts
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
18
aio/content/examples/http/specs.stackblitz.json
Normal file
18
aio/content/examples/http/specs.stackblitz.json
Normal file
@ -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"]
|
||||||
|
}
|
24
aio/content/examples/http/src/app/app.component.html
Normal file
24
aio/content/examples/http/src/app/app.component.html
Normal file
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
9
aio/content/examples/http/src/app/auth.service.ts
Normal file
9
aio/content/examples/http/src/app/auth.service.ts
Normal file
@ -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>
|
78
aio/content/examples/http/src/app/config/config.component.ts
Normal file
78
aio/content/examples/http/src/app/config/config.component.ts
Normal file
@ -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
|
100
aio/content/examples/http/src/app/config/config.service.ts
Normal file
100
aio/content/examples/http/src/app/config/config.service.ts
Normal file
@ -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" }
|
|
||||||
]
|
|
||||||
}
|
|
4
aio/content/examples/http/src/app/heroes/hero.ts
Normal file
4
aio/content/examples/http/src/app/heroes/hero.ts
Normal file
@ -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 -->
|
76
aio/content/examples/http/src/app/heroes/heroes.component.ts
Normal file
76
aio/content/examples/http/src/app/heroes/heroes.component.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
156
aio/content/examples/http/src/app/heroes/heroes.service.spec.ts
Normal file
156
aio/content/examples/http/src/app/heroes/heroes.service.spec.ts
Normal file
@ -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
|
||||||
|
});
|
99
aio/content/examples/http/src/app/heroes/heroes.service.ts
Normal file
99
aio/content/examples/http/src/app/heroes/heroes.service.ts
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
34
aio/content/examples/http/src/app/http-interceptors/index.ts
Normal file
34
aio/content/examples/http/src/app/http-interceptors/index.ts
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
HttpEvent, HttpInterceptor, HttpHandler,
|
||||||
|
HttpRequest, HttpResponse,
|
||||||
|
HttpEventType, HttpProgressEvent
|
||||||
|
} from '@angular/common/http';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { of } from 'rxjs/observable/of';
|
||||||
|
|
||||||
|
/** Simulate server replying to file upload request */
|
||||||
|
@Injectable()
|
||||||
|
export class UploadInterceptor implements HttpInterceptor {
|
||||||
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
if (req.url.indexOf('/upload/file') === -1) {
|
||||||
|
return next.handle(req);
|
||||||
|
}
|
||||||
|
const delay = 300; // Todo: inject delay?
|
||||||
|
return createUploadEvents(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create simulation of upload event stream */
|
||||||
|
function createUploadEvents(delay: number) {
|
||||||
|
// Simulate XHR behavior which would provide this information in a ProgressEvent
|
||||||
|
const chunks = 5;
|
||||||
|
const total = 12345678;
|
||||||
|
const chunkSize = Math.ceil(total / chunks);
|
||||||
|
|
||||||
|
return new Observable<HttpEvent<any>>(observer => {
|
||||||
|
// notify the event stream that the request was sent.
|
||||||
|
observer.next({type: HttpEventType.Sent});
|
||||||
|
|
||||||
|
uploadLoop(0);
|
||||||
|
|
||||||
|
function uploadLoop(loaded: number) {
|
||||||
|
// N.B.: Cannot use setInterval or rxjs delay (which uses setInterval)
|
||||||
|
// because e2e test won't complete. A zone thing?
|
||||||
|
// Use setTimeout and tail recursion instead.
|
||||||
|
setTimeout(() => {
|
||||||
|
loaded += chunkSize;
|
||||||
|
|
||||||
|
if (loaded >= total) {
|
||||||
|
const doneResponse = new HttpResponse({
|
||||||
|
status: 201, // OK but no body;
|
||||||
|
});
|
||||||
|
observer.next(doneResponse);
|
||||||
|
observer.complete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressEvent: HttpProgressEvent = {
|
||||||
|
type: HttpEventType.UploadProgress,
|
||||||
|
loaded,
|
||||||
|
total
|
||||||
|
};
|
||||||
|
observer.next(progressEvent);
|
||||||
|
uploadLoop(loaded);
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
13
aio/content/examples/http/src/app/in-memory-data.service.ts
Normal file
13
aio/content/examples/http/src/app/in-memory-data.service.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { InMemoryDbService } from 'angular-in-memory-web-api';
|
||||||
|
|
||||||
|
export class InMemoryDataService implements InMemoryDbService {
|
||||||
|
createDb() {
|
||||||
|
const heroes = [
|
||||||
|
{ id: 11, name: 'Mr. Nice' },
|
||||||
|
{ id: 12, name: 'Narco' },
|
||||||
|
{ id: 13, name: 'Bombasto' },
|
||||||
|
{ id: 14, name: 'Celeritas' },
|
||||||
|
];
|
||||||
|
return {heroes};
|
||||||
|
}
|
||||||
|
}
|
14
aio/content/examples/http/src/app/message.service.ts
Normal file
14
aio/content/examples/http/src/app/message.service.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MessageService {
|
||||||
|
messages: string[] = [];
|
||||||
|
|
||||||
|
add(message: string) {
|
||||||
|
this.messages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.messages = [];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
<div *ngIf="messageService.messages.length">
|
||||||
|
<h3>Messages</h3>
|
||||||
|
<button class="clear" (click)="messageService.clear()">clear</button>
|
||||||
|
<br>
|
||||||
|
<ol>
|
||||||
|
<li *ngFor='let message of messageService.messages'> {{message}} </li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
@ -0,0 +1,10 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MessageService } from '../message.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-messages',
|
||||||
|
templateUrl: './messages.component.html'
|
||||||
|
})
|
||||||
|
export class MessagesComponent {
|
||||||
|
constructor(public messageService: MessageService) {}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
<!-- #docplaster -->
|
||||||
|
<h3>Search Npm Packages</h3>
|
||||||
|
<p><i>Searches when typing stops. Caches for 30 seconds.</i></p>
|
||||||
|
<!-- #docregion search -->
|
||||||
|
<input (keyup)="search($event.target.value)" id="name" placeholder="Search"/>
|
||||||
|
<!-- #enddocregion search -->
|
||||||
|
<input type="checkbox" id="refresh" [checked]="withRefresh" (click)="toggleRefresh()">
|
||||||
|
<label for="refresh">with refresh</label>
|
||||||
|
<!-- #docregion search -->
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li *ngFor="let package of packages$ | async">
|
||||||
|
<b>{{package.name}} v.{{package.version}}</b> -
|
||||||
|
<i>{{package.description}}</i>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<!-- #enddocregion search -->
|
@ -0,0 +1,39 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { Subject } from 'rxjs/Subject';
|
||||||
|
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { NpmPackageInfo, PackageSearchService } from './package-search.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-package-search',
|
||||||
|
templateUrl: './package-search.component.html',
|
||||||
|
providers: [ PackageSearchService ]
|
||||||
|
})
|
||||||
|
export class PackageSearchComponent implements OnInit {
|
||||||
|
// #docregion debounce
|
||||||
|
withRefresh = false;
|
||||||
|
packages$: Observable<NpmPackageInfo[]>;
|
||||||
|
private searchText$ = new Subject<string>();
|
||||||
|
|
||||||
|
search(packageName: string) {
|
||||||
|
this.searchText$.next(packageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.packages$ = this.searchText$.pipe(
|
||||||
|
debounceTime(500),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
switchMap(packageName =>
|
||||||
|
this.searchService.search(packageName, this.withRefresh))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private searchService: PackageSearchService) { }
|
||||||
|
|
||||||
|
// #enddocregion debounce
|
||||||
|
|
||||||
|
toggleRefresh() { this.withRefresh = ! this.withRefresh; }
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { of } from 'rxjs/observable/of';
|
||||||
|
import { catchError, map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { HttpErrorHandler, HandleError } from '../http-error-handler.service';
|
||||||
|
|
||||||
|
export interface NpmPackageInfo {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const searchUrl = 'https://npmsearch.com/query';
|
||||||
|
|
||||||
|
const httpOptions = {
|
||||||
|
headers: new HttpHeaders({
|
||||||
|
'x-refresh': 'true'
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
function createHttpOptions(packageName: string, refresh = false) {
|
||||||
|
// npm package name search api
|
||||||
|
// e.g., http://npmsearch.com/query?q=dom'
|
||||||
|
const params = new HttpParams({ fromObject: { q: packageName } });
|
||||||
|
const headerMap = refresh ? {'x-refresh': 'true'} : {};
|
||||||
|
const headers = new HttpHeaders(headerMap) ;
|
||||||
|
return { headers, params };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PackageSearchService {
|
||||||
|
private handleError: HandleError;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
|
httpErrorHandler: HttpErrorHandler) {
|
||||||
|
this.handleError = httpErrorHandler.createHandleError('HeroesService');
|
||||||
|
}
|
||||||
|
|
||||||
|
search (packageName: string, refresh = false): Observable<NpmPackageInfo[]> {
|
||||||
|
// clear if no pkg name
|
||||||
|
if (!packageName.trim()) { return of([]); }
|
||||||
|
|
||||||
|
const options = createHttpOptions(packageName, refresh);
|
||||||
|
|
||||||
|
// TODO: Add error handling
|
||||||
|
return this.http.get(searchUrl, options).pipe(
|
||||||
|
map((data: any) => {
|
||||||
|
return data.results.map(entry => ({
|
||||||
|
name: entry.name[0],
|
||||||
|
version: entry.version[0],
|
||||||
|
description: entry.description[0]
|
||||||
|
} as NpmPackageInfo )
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
catchError(this.handleError('search', []))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
60
aio/content/examples/http/src/app/request-cache.service.ts
Normal file
60
aio/content/examples/http/src/app/request-cache.service.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpRequest, HttpResponse } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { MessageService } from './message.service';
|
||||||
|
|
||||||
|
export interface RequestCacheEntry {
|
||||||
|
url: string;
|
||||||
|
response: HttpResponse<any>;
|
||||||
|
lastRead: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #docregion request-cache
|
||||||
|
export abstract class RequestCache {
|
||||||
|
abstract get(req: HttpRequest<any>): HttpResponse<any> | undefined;
|
||||||
|
abstract put(req: HttpRequest<any>, response: HttpResponse<any>): void
|
||||||
|
}
|
||||||
|
// #enddocregion request-cache
|
||||||
|
|
||||||
|
const maxAge = 30000; // maximum cache age (ms)
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RequestCacheWithMap implements RequestCache {
|
||||||
|
|
||||||
|
cache = new Map<string, RequestCacheEntry>();
|
||||||
|
|
||||||
|
constructor(private messenger: MessageService) { }
|
||||||
|
|
||||||
|
get(req: HttpRequest<any>): HttpResponse<any> | undefined {
|
||||||
|
const url = req.urlWithParams;
|
||||||
|
const cached = this.cache.get(url);
|
||||||
|
|
||||||
|
if (!cached) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExpired = cached.lastRead < (Date.now() - maxAge);
|
||||||
|
const expired = isExpired ? 'expired ' : '';
|
||||||
|
this.messenger.add(
|
||||||
|
`Found ${expired}cached response for "${url}".`);
|
||||||
|
return isExpired ? undefined : cached.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
put(req: HttpRequest<any>, response: HttpResponse<any>): void {
|
||||||
|
const url = req.urlWithParams;
|
||||||
|
this.messenger.add(`Caching response from "${url}".`);
|
||||||
|
|
||||||
|
const entry = { url, response, lastRead: Date.now() };
|
||||||
|
this.cache.set(url, entry);
|
||||||
|
|
||||||
|
// remove expired cache entries
|
||||||
|
const expired = Date.now() - maxAge;
|
||||||
|
this.cache.forEach(entry => {
|
||||||
|
if (entry.lastRead < expired) {
|
||||||
|
this.cache.delete(entry.url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.messenger.add(`Request cache size: ${this.cache.size}.`);
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +0,0 @@
|
|||||||
<!-- #docregion -->
|
|
||||||
<h1>Tour of Heroes ({{mode}})</h1>
|
|
||||||
<h3>Heroes:</h3>
|
|
||||||
<ul>
|
|
||||||
<li *ngFor="let hero of heroes">{{hero.name}}</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<label>New hero name: <input #newHeroName /></label>
|
|
||||||
<button (click)="addHero(newHeroName.value); newHeroName.value=''">Add Hero</button>
|
|
||||||
|
|
||||||
<p class="error" *ngIf="errorMessage">{{errorMessage}}</p>
|
|
@ -1,40 +0,0 @@
|
|||||||
// #docregion
|
|
||||||
// Promise Version
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
|
||||||
import { Hero } from './hero';
|
|
||||||
import { HeroService } from './hero.service.promise';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'hero-list-promise',
|
|
||||||
templateUrl: './hero-list.component.html',
|
|
||||||
providers: [ HeroService ],
|
|
||||||
styles: ['.error {color:red;}']
|
|
||||||
})
|
|
||||||
// #docregion component
|
|
||||||
export class HeroListPromiseComponent implements OnInit {
|
|
||||||
errorMessage: string;
|
|
||||||
heroes: Hero[];
|
|
||||||
mode = 'Promise';
|
|
||||||
|
|
||||||
constructor (private heroService: HeroService) {}
|
|
||||||
|
|
||||||
ngOnInit() { this.getHeroes(); }
|
|
||||||
|
|
||||||
// #docregion methods
|
|
||||||
getHeroes() {
|
|
||||||
this.heroService.getHeroes()
|
|
||||||
.then(
|
|
||||||
heroes => this.heroes = heroes,
|
|
||||||
error => this.errorMessage = <any>error);
|
|
||||||
}
|
|
||||||
|
|
||||||
addHero (name: string) {
|
|
||||||
if (!name) { return; }
|
|
||||||
this.heroService.addHero(name)
|
|
||||||
.then(
|
|
||||||
hero => this.heroes.push(hero),
|
|
||||||
error => this.errorMessage = <any>error);
|
|
||||||
}
|
|
||||||
// #enddocregion methods
|
|
||||||
}
|
|
||||||
// #enddocregion component
|
|
@ -1,44 +0,0 @@
|
|||||||
// #docregion
|
|
||||||
// Observable Version
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
|
||||||
import { Hero } from './hero';
|
|
||||||
import { HeroService } from './hero.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'hero-list',
|
|
||||||
templateUrl: './hero-list.component.html',
|
|
||||||
providers: [ HeroService ],
|
|
||||||
styles: ['.error {color:red;}']
|
|
||||||
})
|
|
||||||
// #docregion component
|
|
||||||
export class HeroListComponent implements OnInit {
|
|
||||||
errorMessage: string;
|
|
||||||
heroes: Hero[];
|
|
||||||
mode = 'Observable';
|
|
||||||
|
|
||||||
constructor (private heroService: HeroService) {}
|
|
||||||
|
|
||||||
ngOnInit() { this.getHeroes(); }
|
|
||||||
|
|
||||||
// #docregion methods
|
|
||||||
// #docregion getHeroes
|
|
||||||
getHeroes() {
|
|
||||||
this.heroService.getHeroes()
|
|
||||||
.subscribe(
|
|
||||||
heroes => this.heroes = heroes,
|
|
||||||
error => this.errorMessage = <any>error);
|
|
||||||
}
|
|
||||||
// #enddocregion getHeroes
|
|
||||||
|
|
||||||
// #docregion addHero
|
|
||||||
addHero(name: string) {
|
|
||||||
if (!name) { return; }
|
|
||||||
this.heroService.create(name)
|
|
||||||
.subscribe(
|
|
||||||
hero => this.heroes.push(hero),
|
|
||||||
error => this.errorMessage = <any>error);
|
|
||||||
}
|
|
||||||
// #enddocregion addHero
|
|
||||||
// #enddocregion methods
|
|
||||||
}
|
|
||||||
// #enddocregion component
|
|
@ -1,60 +0,0 @@
|
|||||||
// #docplaster
|
|
||||||
// #docregion
|
|
||||||
// Promise Version
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Http, Response } from '@angular/http';
|
|
||||||
import { Headers, RequestOptions } from '@angular/http';
|
|
||||||
|
|
||||||
// #docregion rxjs-imports
|
|
||||||
import 'rxjs/add/operator/toPromise';
|
|
||||||
// #enddocregion rxjs-imports
|
|
||||||
|
|
||||||
import { Hero } from './hero';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class HeroService {
|
|
||||||
// URL to web api
|
|
||||||
private heroesUrl = 'app/heroes';
|
|
||||||
|
|
||||||
constructor (private http: Http) {}
|
|
||||||
|
|
||||||
// #docregion methods
|
|
||||||
getHeroes (): Promise<Hero[]> {
|
|
||||||
return this.http.get(this.heroesUrl)
|
|
||||||
.toPromise()
|
|
||||||
.then(this.extractData)
|
|
||||||
.catch(this.handleError);
|
|
||||||
}
|
|
||||||
|
|
||||||
addHero (name: string): Promise<Hero> {
|
|
||||||
let headers = new Headers({ 'Content-Type': 'application/json' });
|
|
||||||
let options = new RequestOptions({ headers: headers });
|
|
||||||
|
|
||||||
return this.http.post(this.heroesUrl, { name }, options)
|
|
||||||
.toPromise()
|
|
||||||
.then(this.extractData)
|
|
||||||
.catch(this.handleError);
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractData(res: Response) {
|
|
||||||
let body = res.json();
|
|
||||||
return body.data || { };
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleError (error: Response | any) {
|
|
||||||
// In a real world app, we might use a remote logging infrastructure
|
|
||||||
let errMsg: string;
|
|
||||||
if (error instanceof Response) {
|
|
||||||
const body = error.json() || '';
|
|
||||||
const err = body.error || JSON.stringify(body);
|
|
||||||
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
|
|
||||||
} else {
|
|
||||||
errMsg = error.message ? error.message : error.toString();
|
|
||||||
}
|
|
||||||
console.error(errMsg);
|
|
||||||
return Promise.reject(errMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// #enddocregion methods
|
|
||||||
}
|
|
||||||
// #enddocregion
|
|
@ -1,80 +0,0 @@
|
|||||||
// #docplaster
|
|
||||||
// #docregion
|
|
||||||
// Observable Version
|
|
||||||
// #docregion v1
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Http, Response } from '@angular/http';
|
|
||||||
// #enddocregion v1
|
|
||||||
// #docregion import-request-options
|
|
||||||
import { Headers, RequestOptions } from '@angular/http';
|
|
||||||
// #enddocregion import-request-options
|
|
||||||
// #docregion v1
|
|
||||||
|
|
||||||
// #docregion rxjs-imports
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
|
||||||
import 'rxjs/add/operator/catch';
|
|
||||||
import 'rxjs/add/operator/map';
|
|
||||||
// #enddocregion rxjs-imports
|
|
||||||
|
|
||||||
import { Hero } from './hero';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class HeroService {
|
|
||||||
// #docregion endpoint
|
|
||||||
private heroesUrl = 'api/heroes'; // URL to web API
|
|
||||||
// #enddocregion endpoint
|
|
||||||
|
|
||||||
// #docregion ctor
|
|
||||||
constructor (private http: Http) {}
|
|
||||||
// #enddocregion ctor
|
|
||||||
|
|
||||||
// #docregion methods, error-handling, http-get
|
|
||||||
getHeroes(): Observable<Hero[]> {
|
|
||||||
return this.http.get(this.heroesUrl)
|
|
||||||
.map(this.extractData)
|
|
||||||
.catch(this.handleError);
|
|
||||||
}
|
|
||||||
// #enddocregion error-handling, http-get, v1
|
|
||||||
|
|
||||||
// #docregion create, create-sig
|
|
||||||
create(name: string): Observable<Hero> {
|
|
||||||
// #enddocregion create-sig
|
|
||||||
let headers = new Headers({ 'Content-Type': 'application/json' });
|
|
||||||
let options = new RequestOptions({ headers: headers });
|
|
||||||
|
|
||||||
return this.http.post(this.heroesUrl, { name }, options)
|
|
||||||
.map(this.extractData)
|
|
||||||
.catch(this.handleError);
|
|
||||||
}
|
|
||||||
// #enddocregion create
|
|
||||||
|
|
||||||
// #docregion v1, extract-data
|
|
||||||
private extractData(res: Response) {
|
|
||||||
let body = res.json();
|
|
||||||
return body.data || { };
|
|
||||||
}
|
|
||||||
// #enddocregion extract-data
|
|
||||||
// #docregion error-handling
|
|
||||||
|
|
||||||
private handleError (error: Response | any) {
|
|
||||||
// In a real world app, you might use a remote logging infrastructure
|
|
||||||
let errMsg: string;
|
|
||||||
if (error instanceof Response) {
|
|
||||||
const body = error.json() || '';
|
|
||||||
const err = body.error || JSON.stringify(body);
|
|
||||||
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
|
|
||||||
} else {
|
|
||||||
errMsg = error.message ? error.message : error.toString();
|
|
||||||
}
|
|
||||||
console.error(errMsg);
|
|
||||||
return Observable.throw(errMsg);
|
|
||||||
}
|
|
||||||
// #enddocregion error-handling, methods
|
|
||||||
}
|
|
||||||
// #enddocregion
|
|
||||||
|
|
||||||
/*
|
|
||||||
// #docregion endpoint-json
|
|
||||||
private heroesUrl = 'app/heroes.json'; // URL to JSON file
|
|
||||||
// #enddocregion endpoint-json
|
|
||||||
*/
|
|
@ -1,6 +0,0 @@
|
|||||||
// #docregion
|
|
||||||
export class Hero {
|
|
||||||
constructor(
|
|
||||||
public id: number,
|
|
||||||
public name: string) { }
|
|
||||||
}
|
|
@ -0,0 +1,12 @@
|
|||||||
|
<h3>Upload file</h3>
|
||||||
|
<form enctype="multipart/form-data" method="post">
|
||||||
|
<div>
|
||||||
|
<label for="picked">Choose file to upload</label>
|
||||||
|
<div>
|
||||||
|
<input type="file" id="picked" #picked
|
||||||
|
(click)="message=''"
|
||||||
|
(change)="onPicked(picked)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p *ngIf="message">{{message}}</p>
|
||||||
|
</form>
|
@ -0,0 +1,25 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { UploaderService } from './uploader.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-uploader',
|
||||||
|
templateUrl: './uploader.component.html',
|
||||||
|
providers: [ UploaderService ]
|
||||||
|
})
|
||||||
|
export class UploaderComponent {
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
constructor(private uploaderService: UploaderService) {}
|
||||||
|
|
||||||
|
onPicked(input: HTMLInputElement) {
|
||||||
|
const file = input.files[0];
|
||||||
|
if (file) {
|
||||||
|
this.uploaderService.upload(file).subscribe(
|
||||||
|
msg => {
|
||||||
|
input.value = null;
|
||||||
|
this.message = msg;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
105
aio/content/examples/http/src/app/uploader/uploader.service.ts
Normal file
105
aio/content/examples/http/src/app/uploader/uploader.service.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
HttpClient, HttpEvent, HttpEventType, HttpProgressEvent,
|
||||||
|
HttpRequest, HttpResponse, HttpErrorResponse
|
||||||
|
} from '@angular/common/http';
|
||||||
|
|
||||||
|
import { of } from 'rxjs/observable/of';
|
||||||
|
import { catchError, last, map, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { MessageService } from '../message.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UploaderService {
|
||||||
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
|
private messenger: MessageService) {}
|
||||||
|
|
||||||
|
// If uploading multiple files, change to:
|
||||||
|
// upload(files: FileList) {
|
||||||
|
// const formData = new FormData();
|
||||||
|
// files.forEach(f => formData.append(f.name, f));
|
||||||
|
// new HttpRequest('POST', '/upload/file', formData, {reportProgress: true});
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
|
||||||
|
upload(file: File) {
|
||||||
|
if (!file) { return; }
|
||||||
|
|
||||||
|
// COULD HAVE WRITTEN:
|
||||||
|
// return this.http.post('/upload/file', file, {
|
||||||
|
// reportProgress: true,
|
||||||
|
// observe: 'events'
|
||||||
|
// }).pipe(
|
||||||
|
|
||||||
|
// Create the request object that POSTs the file to an upload endpoint.
|
||||||
|
// The `reportProgress` option tells HttpClient to listen and return
|
||||||
|
// XHR progress events.
|
||||||
|
// #docregion upload-request
|
||||||
|
const req = new HttpRequest('POST', '/upload/file', file, {
|
||||||
|
reportProgress: true
|
||||||
|
});
|
||||||
|
// #enddocregion upload-request
|
||||||
|
|
||||||
|
// #docregion upload-body
|
||||||
|
// The `HttpClient.request` API produces a raw event stream
|
||||||
|
// which includes start (sent), progress, and response events.
|
||||||
|
return this.http.request(req).pipe(
|
||||||
|
map(event => this.getEventMessage(event, file)),
|
||||||
|
tap(message => this.showProgress(message)),
|
||||||
|
last(), // return last (completed) message to caller
|
||||||
|
catchError(this.handleError(file))
|
||||||
|
);
|
||||||
|
// #enddocregion upload-body
|
||||||
|
}
|
||||||
|
|
||||||
|
// #docregion getEventMessage
|
||||||
|
/** Return distinct message for sent, upload progress, & response events */
|
||||||
|
private getEventMessage(event: HttpEvent<any>, file: File) {
|
||||||
|
switch (event.type) {
|
||||||
|
case HttpEventType.Sent:
|
||||||
|
return `Uploading file "${file.name}" of size ${file.size}.`;
|
||||||
|
|
||||||
|
case HttpEventType.UploadProgress:
|
||||||
|
// Compute and show the % done:
|
||||||
|
const percentDone = Math.round(100 * event.loaded / event.total);
|
||||||
|
return `File "${file.name}" is ${percentDone}% uploaded.`;
|
||||||
|
|
||||||
|
case HttpEventType.Response:
|
||||||
|
return `File "${file.name}" was completely uploaded!`;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return `File "${file.name}" surprising upload event: ${event.type}.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #enddocregion getEventMessage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a function that handles Http upload failures.
|
||||||
|
* @param file - File object for file being uploaded
|
||||||
|
*
|
||||||
|
* When no `UploadInterceptor` and no server,
|
||||||
|
* you'll end up here in the error handler.
|
||||||
|
*/
|
||||||
|
private handleError(file: File) {
|
||||||
|
const userMessage = `${file.name} upload failed.`;
|
||||||
|
|
||||||
|
return (error: HttpErrorResponse) => {
|
||||||
|
// TODO: send the error to remote logging infrastructure
|
||||||
|
console.error(error); // log to console instead
|
||||||
|
|
||||||
|
const message = (error.error instanceof Error) ?
|
||||||
|
error.error.message :
|
||||||
|
`server returned code ${error.status} with body "${error.error}"`;
|
||||||
|
|
||||||
|
this.messenger.add(`${userMessage} ${message}`);
|
||||||
|
|
||||||
|
// Let app keep running but indicate failure.
|
||||||
|
return of(userMessage);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private showProgress(message: string) {
|
||||||
|
this.messenger.add(message);
|
||||||
|
}
|
||||||
|
}
|
@ -1,47 +0,0 @@
|
|||||||
/* tslint:disable: member-ordering forin */
|
|
||||||
// #docplaster
|
|
||||||
// #docregion
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
|
||||||
|
|
||||||
// #docregion rxjs-imports
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
|
||||||
import 'rxjs/add/operator/debounceTime';
|
|
||||||
import 'rxjs/add/operator/distinctUntilChanged';
|
|
||||||
import 'rxjs/add/operator/switchMap';
|
|
||||||
|
|
||||||
// #docregion import-subject
|
|
||||||
import { Subject } from 'rxjs/Subject';
|
|
||||||
// #enddocregion import-subject
|
|
||||||
|
|
||||||
import { WikipediaService } from './wikipedia.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'my-wiki-smart',
|
|
||||||
template: `
|
|
||||||
<h1>Smarter Wikipedia Demo</h1>
|
|
||||||
<p>Search when typing stops</p>
|
|
||||||
<input #term (keyup)="search(term.value)"/>
|
|
||||||
<ul>
|
|
||||||
<li *ngFor="let item of items | async">{{item}}</li>
|
|
||||||
</ul>`,
|
|
||||||
providers: [ WikipediaService ]
|
|
||||||
})
|
|
||||||
export class WikiSmartComponent implements OnInit {
|
|
||||||
items: Observable<string[]>;
|
|
||||||
|
|
||||||
constructor (private wikipediaService: WikipediaService) {}
|
|
||||||
|
|
||||||
// #docregion subject
|
|
||||||
private searchTermStream = new Subject<string>();
|
|
||||||
search(term: string) { this.searchTermStream.next(term); }
|
|
||||||
// #enddocregion subject
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
// #docregion observable-operators
|
|
||||||
this.items = this.searchTermStream
|
|
||||||
.debounceTime(300)
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.switchMap((term: string) => this.wikipediaService.search(term));
|
|
||||||
// #enddocregion observable-operators
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
// #docregion
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
|
||||||
|
|
||||||
import { WikipediaService } from './wikipedia.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'my-wiki',
|
|
||||||
template: `
|
|
||||||
<h1>Wikipedia Demo</h1>
|
|
||||||
<p>Search after each keystroke</p>
|
|
||||||
<input #term (keyup)="search(term.value)"/>
|
|
||||||
<ul>
|
|
||||||
<li *ngFor="let item of items | async">{{item}}</li>
|
|
||||||
</ul>`,
|
|
||||||
providers: [ WikipediaService ]
|
|
||||||
})
|
|
||||||
export class WikiComponent {
|
|
||||||
items: Observable<string[]>;
|
|
||||||
|
|
||||||
constructor (private wikipediaService: WikipediaService) { }
|
|
||||||
|
|
||||||
search (term: string) {
|
|
||||||
this.items = this.wikipediaService.search(term);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
// Create the query string by hand
|
|
||||||
// #docregion
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Jsonp } from '@angular/http';
|
|
||||||
|
|
||||||
import 'rxjs/add/operator/map';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class WikipediaService {
|
|
||||||
constructor(private jsonp: Jsonp) { }
|
|
||||||
|
|
||||||
// TODO: Add error handling
|
|
||||||
search(term: string) {
|
|
||||||
|
|
||||||
let wikiUrl = 'http://en.wikipedia.org/w/api.php';
|
|
||||||
|
|
||||||
// #docregion query-string
|
|
||||||
let queryString =
|
|
||||||
`?search=${term}&action=opensearch&format=json&callback=JSONP_CALLBACK`;
|
|
||||||
|
|
||||||
return this.jsonp
|
|
||||||
.get(wikiUrl + queryString)
|
|
||||||
.map(response => <string[]> response.json()[1]);
|
|
||||||
// #enddocregion query-string
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
// #docregion
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Jsonp, URLSearchParams } from '@angular/http';
|
|
||||||
|
|
||||||
import 'rxjs/add/operator/map';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class WikipediaService {
|
|
||||||
constructor(private jsonp: Jsonp) {}
|
|
||||||
|
|
||||||
search (term: string) {
|
|
||||||
|
|
||||||
let wikiUrl = 'http://en.wikipedia.org/w/api.php';
|
|
||||||
|
|
||||||
// #docregion search-parameters
|
|
||||||
let params = new URLSearchParams();
|
|
||||||
params.set('search', term); // the user's search value
|
|
||||||
params.set('action', 'opensearch');
|
|
||||||
params.set('format', 'json');
|
|
||||||
params.set('callback', 'JSONP_CALLBACK');
|
|
||||||
// #enddocregion search-parameters
|
|
||||||
|
|
||||||
// #docregion call-jsonp
|
|
||||||
// TODO: Add error handling
|
|
||||||
return this.jsonp
|
|
||||||
.get(wikiUrl, { search: params })
|
|
||||||
.map(response => <string[]> response.json()[1]);
|
|
||||||
// #enddocregion call-jsonp
|
|
||||||
}
|
|
||||||
}
|
|
4
aio/content/examples/http/src/assets/config.json
Normal file
4
aio/content/examples/http/src/assets/config.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"heroesUrl": "api/heroes",
|
||||||
|
"textfile": "assets/textfile.txt"
|
||||||
|
}
|
1
aio/content/examples/http/src/assets/textfile.txt
Normal file
1
aio/content/examples/http/src/assets/textfile.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
This is the downloaded text file
|
88
aio/content/examples/http/src/browser-test-shim.js
Normal file
88
aio/content/examples/http/src/browser-test-shim.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// BROWSER TESTING SHIM
|
||||||
|
// Keep it in-sync with what karma-test-shim does
|
||||||
|
// #docregion
|
||||||
|
/*global jasmine, __karma__, window*/
|
||||||
|
(function () {
|
||||||
|
|
||||||
|
Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing.
|
||||||
|
|
||||||
|
// Uncomment to get full stacktrace output. Sometimes helpful, usually not.
|
||||||
|
// Error.stackTraceLimit = Infinity; //
|
||||||
|
|
||||||
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000;
|
||||||
|
|
||||||
|
var baseURL = document.baseURI;
|
||||||
|
baseURL = baseURL + baseURL[baseURL.length-1] ? '' : '/';
|
||||||
|
|
||||||
|
System.config({
|
||||||
|
baseURL: baseURL,
|
||||||
|
// Extend usual application package list with test folder
|
||||||
|
packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } },
|
||||||
|
|
||||||
|
// Assume npm: is set in `paths` in systemjs.config
|
||||||
|
// Map the angular testing umd bundles
|
||||||
|
map: {
|
||||||
|
'@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
|
||||||
|
'@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
|
||||||
|
'@angular/common/http/testing': 'npm:@angular/common/bundles/common-http-testing.umd.js',
|
||||||
|
'@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
|
||||||
|
'@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
|
||||||
|
'@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
|
||||||
|
'@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
|
||||||
|
'@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
|
||||||
|
'@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
System.import('systemjs.config.js')
|
||||||
|
// .then(importSystemJsExtras) // not in this project
|
||||||
|
.then(initTestBed)
|
||||||
|
.then(initTesting);
|
||||||
|
|
||||||
|
/** Optional SystemJS configuration extras. Keep going w/o it */
|
||||||
|
function importSystemJsExtras(){
|
||||||
|
return System.import('systemjs.config.extras.js')
|
||||||
|
.catch(function(reason) {
|
||||||
|
console.log(
|
||||||
|
'Note: System.import could not load "systemjs.config.extras.js" where you might have added more configuration. It is an optional file so we will continue without it.'
|
||||||
|
);
|
||||||
|
console.log(reason);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTestBed(){
|
||||||
|
return Promise.all([
|
||||||
|
System.import('@angular/core/testing'),
|
||||||
|
System.import('@angular/platform-browser-dynamic/testing')
|
||||||
|
])
|
||||||
|
|
||||||
|
.then(function (providers) {
|
||||||
|
var coreTesting = providers[0];
|
||||||
|
var browserTesting = providers[1];
|
||||||
|
|
||||||
|
coreTesting.TestBed.initTestEnvironment(
|
||||||
|
browserTesting.BrowserDynamicTestingModule,
|
||||||
|
browserTesting.platformBrowserDynamicTesting());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import all spec files defined in the html (__spec_files__)
|
||||||
|
// and start Jasmine testrunner
|
||||||
|
function initTesting () {
|
||||||
|
console.log('loading spec files: '+__spec_files__.join(', '));
|
||||||
|
return Promise.all(
|
||||||
|
__spec_files__.map(function(spec) {
|
||||||
|
return System.import(spec);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// After all imports load, re-execute `window.onload` which
|
||||||
|
// triggers the Jasmine test-runner start or explain what went wrong
|
||||||
|
.then(success, console.error.bind(console));
|
||||||
|
|
||||||
|
function success () {
|
||||||
|
console.log('Spec files loaded; starting Jasmine testrunner');
|
||||||
|
window.onload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
4
aio/content/examples/http/src/index-specs.html
Normal file
4
aio/content/examples/http/src/index-specs.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<!--
|
||||||
|
Intentionally empty placeholder for Stackblitz.
|
||||||
|
Do not need index.html in zip-download either as you should run tests with `npm test`
|
||||||
|
-->
|
@ -1,27 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<!-- #docregion -->
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Angular Http Demo</title>
|
<meta charset="utf-8">
|
||||||
<base href="/">
|
<title>HttpClient Demo</title>
|
||||||
<meta charset="UTF-8">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link rel="stylesheet" href="styles.css">
|
|
||||||
|
|
||||||
<!-- Polyfills -->
|
|
||||||
<script src="node_modules/core-js/client/shim.min.js"></script>
|
|
||||||
|
|
||||||
<script src="node_modules/zone.js/dist/zone.js"></script>
|
|
||||||
<script src="node_modules/systemjs/dist/system.src.js"></script>
|
|
||||||
|
|
||||||
<script src="systemjs.config.js"></script>
|
|
||||||
<script>
|
|
||||||
System.import('main.js').catch(function(err){ console.error(err); });
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<my-app></my-app>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
44
aio/content/examples/http/src/main-specs.ts
Normal file
44
aio/content/examples/http/src/main-specs.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import './testing/global-jasmine';
|
||||||
|
import 'jasmine-core/lib/jasmine-core/jasmine-html.js';
|
||||||
|
import 'jasmine-core/lib/jasmine-core/boot.js';
|
||||||
|
|
||||||
|
declare var jasmine;
|
||||||
|
|
||||||
|
import './polyfills';
|
||||||
|
|
||||||
|
import 'zone.js/dist/async-test';
|
||||||
|
import 'zone.js/dist/fake-async-test';
|
||||||
|
import 'zone.js/dist/long-stack-trace-zone';
|
||||||
|
import 'zone.js/dist/proxy.js';
|
||||||
|
import 'zone.js/dist/sync-test';
|
||||||
|
import 'zone.js/dist/jasmine-patch';
|
||||||
|
|
||||||
|
import { getTestBed } from '@angular/core/testing';
|
||||||
|
import {
|
||||||
|
BrowserDynamicTestingModule,
|
||||||
|
platformBrowserDynamicTesting
|
||||||
|
} from '@angular/platform-browser-dynamic/testing';
|
||||||
|
|
||||||
|
// Import spec files individually for Stackblitz
|
||||||
|
import './app/heroes/heroes.service.spec.ts';
|
||||||
|
import './testing/http-client.spec.ts';
|
||||||
|
|
||||||
|
//
|
||||||
|
bootstrap();
|
||||||
|
|
||||||
|
//
|
||||||
|
function bootstrap () {
|
||||||
|
if (window['jasmineRef']) {
|
||||||
|
location.reload();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
window.onload(undefined);
|
||||||
|
window['jasmineRef'] = jasmine.getEnv();
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, initialize the Angular testing environment.
|
||||||
|
getTestBed().initTestEnvironment(
|
||||||
|
BrowserDynamicTestingModule,
|
||||||
|
platformBrowserDynamicTesting()
|
||||||
|
);
|
||||||
|
}
|
1
aio/content/examples/http/src/test.css
Normal file
1
aio/content/examples/http/src/test.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
@import "~jasmine-core/lib/jasmine-core/jasmine.css"
|
3
aio/content/examples/http/src/testing/global-jasmine.ts
Normal file
3
aio/content/examples/http/src/testing/global-jasmine.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import jasmineRequire from 'jasmine-core/lib/jasmine-core/jasmine.js';
|
||||||
|
|
||||||
|
window['jasmineRequire'] = jasmineRequire;
|
192
aio/content/examples/http/src/testing/http-client.spec.ts
Normal file
192
aio/content/examples/http/src/testing/http-client.spec.ts
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
// #docplaster
|
||||||
|
// #docregion imports
|
||||||
|
// Http testing module and mocking controller
|
||||||
|
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||||
|
|
||||||
|
// Other imports
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||||
|
|
||||||
|
// #enddocregion imports
|
||||||
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
|
|
||||||
|
interface Data {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testUrl = '/data';
|
||||||
|
|
||||||
|
// #docregion setup
|
||||||
|
describe('HttpClient testing', () => {
|
||||||
|
let httpClient: HttpClient;
|
||||||
|
let httpTestingController: HttpTestingController;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [ HttpClientTestingModule ]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inject the http service and test controller for each test
|
||||||
|
httpClient = TestBed.get(HttpClient);
|
||||||
|
httpTestingController = TestBed.get(HttpTestingController);
|
||||||
|
});
|
||||||
|
// #enddocregion setup
|
||||||
|
// #docregion afterEach
|
||||||
|
afterEach(() => {
|
||||||
|
// After every test, assert that there are no more pending requests.
|
||||||
|
httpTestingController.verify();
|
||||||
|
});
|
||||||
|
// #enddocregion afterEach
|
||||||
|
// #docregion setup
|
||||||
|
/// Tests begin ///
|
||||||
|
// #enddocregion setup
|
||||||
|
// #docregion get-test
|
||||||
|
it('can test HttpClient.get', () => {
|
||||||
|
const testData: Data = {name: 'Test Data'};
|
||||||
|
|
||||||
|
// Make an HTTP GET request
|
||||||
|
httpClient.get<Data>(testUrl)
|
||||||
|
.subscribe(data =>
|
||||||
|
// When observable resolves, result should match test data
|
||||||
|
expect(data).toEqual(testData)
|
||||||
|
);
|
||||||
|
|
||||||
|
// The following `expectOne()` will match the request's URL.
|
||||||
|
// If no requests or multiple requests matched that URL
|
||||||
|
// `expectOne()` would throw.
|
||||||
|
const req = httpTestingController.expectOne('/data');
|
||||||
|
|
||||||
|
// Assert that the request is a GET.
|
||||||
|
expect(req.request.method).toEqual('GET');
|
||||||
|
|
||||||
|
// Respond with mock data, causing Observable to resolve.
|
||||||
|
// Subscribe callback asserts that correct data was returned.
|
||||||
|
req.flush(testData);
|
||||||
|
|
||||||
|
// Finally, assert that there are no outstanding requests.
|
||||||
|
httpTestingController.verify();
|
||||||
|
});
|
||||||
|
// #enddocregion get-test
|
||||||
|
it('can test HttpClient.get with matching header', () => {
|
||||||
|
const testData: Data = {name: 'Test Data'};
|
||||||
|
|
||||||
|
// Make an HTTP GET request with specific header
|
||||||
|
httpClient.get<Data>(testUrl, {
|
||||||
|
headers: new HttpHeaders({'Authorization': 'my-auth-token'})
|
||||||
|
})
|
||||||
|
.subscribe(data =>
|
||||||
|
expect(data).toEqual(testData)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find request with a predicate function.
|
||||||
|
// #docregion predicate
|
||||||
|
// Expect one request with an authorization header
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
req => req.headers.has('Authorization')
|
||||||
|
);
|
||||||
|
// #enddocregion predicate
|
||||||
|
req.flush(testData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can test multiple requests', () => {
|
||||||
|
let testData: Data[] = [
|
||||||
|
{ name: 'bob' }, { name: 'carol' },
|
||||||
|
{ name: 'ted' }, { name: 'alice' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Make three requests in a row
|
||||||
|
httpClient.get<Data[]>(testUrl)
|
||||||
|
.subscribe(d => expect(d.length).toEqual(0, 'should have no data'));
|
||||||
|
|
||||||
|
httpClient.get<Data[]>(testUrl)
|
||||||
|
.subscribe(d => expect(d).toEqual([testData[0]], 'should be one element array'));
|
||||||
|
|
||||||
|
httpClient.get<Data[]>(testUrl)
|
||||||
|
.subscribe(d => expect(d).toEqual(testData, 'should be expected data'));
|
||||||
|
|
||||||
|
// #docregion multi-request
|
||||||
|
// get all pending requests that match the given URL
|
||||||
|
const requests = httpTestingController.match(testUrl);
|
||||||
|
expect(requests.length).toEqual(3);
|
||||||
|
|
||||||
|
// Respond to each request with different results
|
||||||
|
requests[0].flush([]);
|
||||||
|
requests[1].flush([testData[0]]);
|
||||||
|
requests[2].flush(testData);
|
||||||
|
// #enddocregion multi-request
|
||||||
|
});
|
||||||
|
|
||||||
|
// #docregion 404
|
||||||
|
it('can test for 404 error', () => {
|
||||||
|
const emsg = 'deliberate 404 error';
|
||||||
|
|
||||||
|
httpClient.get<Data[]>(testUrl).subscribe(
|
||||||
|
data => fail('should have failed with the 404 error'),
|
||||||
|
(error: HttpErrorResponse) => {
|
||||||
|
expect(error.status).toEqual(404, 'status');
|
||||||
|
expect(error.error).toEqual(emsg, 'message');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const req = httpTestingController.expectOne(testUrl);
|
||||||
|
|
||||||
|
// Respond with mock error
|
||||||
|
req.flush(emsg, { status: 404, statusText: 'Not Found' });
|
||||||
|
});
|
||||||
|
// #enddocregion 404
|
||||||
|
|
||||||
|
// #docregion network-error
|
||||||
|
it('can test for network error', () => {
|
||||||
|
const emsg = 'simulated network error';
|
||||||
|
|
||||||
|
httpClient.get<Data[]>(testUrl).subscribe(
|
||||||
|
data => fail('should have failed with the network error'),
|
||||||
|
(error: HttpErrorResponse) => {
|
||||||
|
expect(error.error.message).toEqual(emsg, 'message');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const req = httpTestingController.expectOne(testUrl);
|
||||||
|
|
||||||
|
// Create mock ErrorEvent, raised when something goes wrong at the network level.
|
||||||
|
// Connection timeout, DNS error, offline, etc
|
||||||
|
const errorEvent = new ErrorEvent('so sad', {
|
||||||
|
message: emsg,
|
||||||
|
// #enddocregion network-error
|
||||||
|
// The rest of this is optional and not used.
|
||||||
|
// Just showing that you could provide this too.
|
||||||
|
filename: 'HeroService.ts',
|
||||||
|
lineno: 42,
|
||||||
|
colno: 21
|
||||||
|
// #docregion network-error
|
||||||
|
});
|
||||||
|
|
||||||
|
// Respond with mock error
|
||||||
|
req.error(errorEvent);
|
||||||
|
});
|
||||||
|
// #enddocregion network-error
|
||||||
|
|
||||||
|
it('httpTestingController.verify should fail if HTTP response not simulated', () => {
|
||||||
|
// Sends request
|
||||||
|
httpClient.get('some/api').subscribe();
|
||||||
|
|
||||||
|
// verify() should fail because haven't handled the pending request.
|
||||||
|
expect(() => httpTestingController.verify()).toThrow();
|
||||||
|
|
||||||
|
// Now get and flush the request so that afterEach() doesn't fail
|
||||||
|
const req = httpTestingController.expectOne('some/api');
|
||||||
|
req.flush(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Proves that verify in afterEach() really would catch error
|
||||||
|
// if test doesn't simulate the HTTP response.
|
||||||
|
//
|
||||||
|
// Must disable this test because can't catch an error in an afterEach().
|
||||||
|
// Uncomment if you want to confirm that afterEach() does the job.
|
||||||
|
// it('afterEach() should fail when HTTP response not simulated',() => {
|
||||||
|
// // Sends request which is never handled by this test
|
||||||
|
// httpClient.get('some/api').subscribe();
|
||||||
|
// });
|
||||||
|
// #docregion setup
|
||||||
|
});
|
||||||
|
// #enddocregion setup
|
@ -3,7 +3,9 @@
|
|||||||
"files":[
|
"files":[
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
"!**/*.js",
|
"!**/*.js",
|
||||||
"!**/*.[1].*"
|
|
||||||
|
"!src/testing/*.*",
|
||||||
|
"!src/index-specs.html"
|
||||||
],
|
],
|
||||||
"tags": ["http", "jsonp"]
|
"tags": ["http"]
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ export class AppModule {
|
|||||||
@Inject(PLATFORM_ID) private platformId: Object,
|
@Inject(PLATFORM_ID) private platformId: Object,
|
||||||
@Inject(APP_ID) private appId: string) {
|
@Inject(APP_ID) private appId: string) {
|
||||||
const platform = isPlatformBrowser(platformId) ?
|
const platform = isPlatformBrowser(platformId) ?
|
||||||
'on the server' : 'in the browser';
|
'in the browser' : 'on the server';
|
||||||
console.log(`Running ${platform} with appId=${appId}`);
|
console.log(`Running ${platform} with appId=${appId}`);
|
||||||
}
|
}
|
||||||
// #enddocregion platform-detection
|
// #enddocregion platform-detection
|
||||||
|
@ -92,7 +92,7 @@ You can control your app compilation by providing template compiler options in t
|
|||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"fullTemplateTypeCheck": true,
|
"fullTemplateTypeCheck": true,
|
||||||
"preserveWhiteSpace": false,
|
"preserveWhiteSpaces": false,
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ You can run the <live-example></live-example> in Stackblitz and download the cod
|
|||||||
## Using component styles
|
## Using component styles
|
||||||
|
|
||||||
For every Angular component you write, you may define not only an HTML template,
|
For every Angular component you write, you may define not only an HTML template,
|
||||||
but also the CSS styles that go with that template,
|
but also the CSS styles that go with that template,
|
||||||
specifying any selectors, rules, and media queries that you need.
|
specifying any selectors, rules, and media queries that you need.
|
||||||
|
|
||||||
One way to do this is to set the `styles` property in the component metadata.
|
One way to do this is to set the `styles` property in the component metadata.
|
||||||
@ -42,7 +42,7 @@ This scoping restriction is a ***styling modularity feature***.
|
|||||||
* You can use the CSS class names and selectors that make the most sense in the context of each component.
|
* You can use the CSS class names and selectors that make the most sense in the context of each component.
|
||||||
|
|
||||||
|
|
||||||
* Class names and selectors are local to the component and don't collide with
|
* Class names and selectors are local to the component and don't collide with
|
||||||
classes and selectors used elsewhere in the application.
|
classes and selectors used elsewhere in the application.
|
||||||
|
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ This scoping restriction is a ***styling modularity feature***.
|
|||||||
## Special selectors
|
## Special selectors
|
||||||
|
|
||||||
Component styles have a few special *selectors* from the world of shadow DOM style scoping
|
Component styles have a few special *selectors* from the world of shadow DOM style scoping
|
||||||
(described in the [CSS Scoping Module Level 1](https://www.w3.org/TR/css-scoping-1) page on the
|
(described in the [CSS Scoping Module Level 1](https://www.w3.org/TR/css-scoping-1) page on the
|
||||||
[W3C](https://www.w3.org) site).
|
[W3C](https://www.w3.org) site).
|
||||||
The following sections describe these selectors.
|
The following sections describe these selectors.
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ The `:host` selector is the only way to target the host element. You can't reach
|
|||||||
the host element from inside the component with other selectors because it's not part of the
|
the host element from inside the component with other selectors because it's not part of the
|
||||||
component's own template. The host element is in a parent component's template.
|
component's own template. The host element is in a parent component's template.
|
||||||
|
|
||||||
Use the *function form* to apply host styles conditionally by
|
Use the *function form* to apply host styles conditionally by
|
||||||
including another selector inside parentheses after `:host`.
|
including another selector inside parentheses after `:host`.
|
||||||
|
|
||||||
The next example targets the host element again, but only when it also has the `active` CSS class.
|
The next example targets the host element again, but only when it also has the `active` CSS class.
|
||||||
@ -104,15 +104,15 @@ if some ancestor element has the CSS class `theme-light`.
|
|||||||
|
|
||||||
### (deprecated) `/deep/`, `>>>`, and `::ng-deep`
|
### (deprecated) `/deep/`, `>>>`, and `::ng-deep`
|
||||||
|
|
||||||
Component styles normally apply only to the HTML in the component's own template.
|
Component styles normally apply only to the HTML in the component's own template.
|
||||||
|
|
||||||
Use the `/deep/` shadow-piercing descendant combinator to force a style down through the child
|
Use the `/deep/` shadow-piercing descendant combinator to force a style down through the child
|
||||||
component tree into all the child component views.
|
component tree into all the child component views.
|
||||||
The `/deep/` combinator works to any depth of nested components, and it applies to both the view
|
The `/deep/` combinator works to any depth of nested components, and it applies to both the view
|
||||||
children and content children of the component.
|
children and content children of the component.
|
||||||
|
|
||||||
The following example targets all `<h3>` elements, from the host element down
|
The following example targets all `<h3>` elements, from the host element down
|
||||||
through this component to all of its child elements in the DOM.
|
through this component to all of its child elements in the DOM.
|
||||||
|
|
||||||
<code-example path="component-styles/src/app/hero-details.component.css" region="deep" title="src/app/hero-details.component.css" linenums="false">
|
<code-example path="component-styles/src/app/hero-details.component.css" region="deep" title="src/app/hero-details.component.css" linenums="false">
|
||||||
|
|
||||||
@ -140,7 +140,7 @@ Until then `::ng-deep` should be preferred for a broader compatibility with the
|
|||||||
|
|
||||||
## Loading component styles
|
## Loading component styles
|
||||||
|
|
||||||
There are several ways to add styles to a component:
|
There are several ways to add styles to a component:
|
||||||
|
|
||||||
* By setting `styles` or `styleUrls` metadata.
|
* By setting `styles` or `styleUrls` metadata.
|
||||||
* Inline in the template HTML.
|
* Inline in the template HTML.
|
||||||
@ -177,8 +177,8 @@ to a component's `@Component` decorator:
|
|||||||
|
|
||||||
<code-tabs>
|
<code-tabs>
|
||||||
<code-pane title="src/app/hero-app.component.ts (CSS in file)" path="component-styles/src/app/hero-app.component.1.ts"></code-pane>
|
<code-pane title="src/app/hero-app.component.ts (CSS in file)" path="component-styles/src/app/hero-app.component.1.ts"></code-pane>
|
||||||
<code-pane title="src/app/hero-app.component.css" path="component-styles/src/app/hero-app.component.1.css"></code-pane>
|
<code-pane title="src/app/hero-app.component.css" path="component-styles/src/app/hero-app.component.css"></code-pane>
|
||||||
</code-tabs>
|
</code-tabs>
|
||||||
|
|
||||||
<div class="alert is-critical">
|
<div class="alert is-critical">
|
||||||
|
|
||||||
@ -209,14 +209,14 @@ inside `<style>` tags.
|
|||||||
|
|
||||||
### Template link tags
|
### Template link tags
|
||||||
|
|
||||||
You can also write `<link>` tags into the component's HTML template.
|
You can also write `<link>` tags into the component's HTML template.
|
||||||
|
|
||||||
<code-example path="component-styles/src/app/hero-team.component.ts" region="stylelink" title="src/app/hero-team.component.ts">
|
<code-example path="component-styles/src/app/hero-team.component.ts" region="stylelink" title="src/app/hero-team.component.ts">
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
<div class="alert is-critical">
|
<div class="alert is-critical">
|
||||||
|
|
||||||
The link tag's `href` URL must be relative to the
|
The link tag's `href` URL must be relative to the
|
||||||
_**application root**_, not relative to the component file.
|
_**application root**_, not relative to the component file.
|
||||||
|
|
||||||
When building with the CLI, be sure to include the linked style file among the assets to be copied to the server as described in the [CLI documentation](https://github.com/angular/angular-cli/wiki/stories-asset-configuration).
|
When building with the CLI, be sure to include the linked style file among the assets to be copied to the server as described in the [CLI documentation](https://github.com/angular/angular-cli/wiki/stories-asset-configuration).
|
||||||
@ -244,7 +244,7 @@ See the [CLI documentation](https://github.com/angular/angular-cli/wiki/stories-
|
|||||||
|
|
||||||
### Non-CSS style files
|
### Non-CSS style files
|
||||||
|
|
||||||
If you're building with the CLI,
|
If you're building with the CLI,
|
||||||
you can write style files in [sass](http://sass-lang.com/), [less](http://lesscss.org/), or [stylus](http://stylus-lang.com/) and specify those files in the `@Component.styleUrls` metadata with the appropriate extensions (`.scss`, `.less`, `.styl`) as in the following example:
|
you can write style files in [sass](http://sass-lang.com/), [less](http://lesscss.org/), or [stylus](http://stylus-lang.com/) and specify those files in the `@Component.styleUrls` metadata with the appropriate extensions (`.scss`, `.less`, `.styl`) as in the following example:
|
||||||
|
|
||||||
<code-example>
|
<code-example>
|
||||||
@ -259,7 +259,7 @@ you can write style files in [sass](http://sass-lang.com/), [less](http://lesscs
|
|||||||
The CLI build process runs the pertinent CSS preprocessor.
|
The CLI build process runs the pertinent CSS preprocessor.
|
||||||
|
|
||||||
When generating a component file with `ng generate component`, the CLI emits an empty CSS styles file (`.css`) by default.
|
When generating a component file with `ng generate component`, the CLI emits an empty CSS styles file (`.css`) by default.
|
||||||
You can configure the CLI to default to your preferred CSS preprocessor
|
You can configure the CLI to default to your preferred CSS preprocessor
|
||||||
as explained in the [CLI documentation](https://github.com/angular/angular-cli/wiki/stories-css-preprocessors
|
as explained in the [CLI documentation](https://github.com/angular/angular-cli/wiki/stories-css-preprocessors
|
||||||
"CSS Preprocessor integration").
|
"CSS Preprocessor integration").
|
||||||
|
|
||||||
@ -281,7 +281,7 @@ component* basis, you can set the *view encapsulation mode* in the component met
|
|||||||
Choose from the following modes:
|
Choose from the following modes:
|
||||||
|
|
||||||
* `Native` view encapsulation uses the browser's native shadow DOM implementation (see
|
* `Native` view encapsulation uses the browser's native shadow DOM implementation (see
|
||||||
[Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM)
|
[Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM)
|
||||||
on the [MDN](https://developer.mozilla.org) site)
|
on the [MDN](https://developer.mozilla.org) site)
|
||||||
to attach a shadow DOM to the component's host element, and then puts the component
|
to attach a shadow DOM to the component's host element, and then puts the component
|
||||||
view inside that shadow DOM. The component's styles are included within the shadow DOM.
|
view inside that shadow DOM. The component's styles are included within the shadow DOM.
|
||||||
@ -290,18 +290,18 @@ Choose from the following modes:
|
|||||||
(and renaming) the CSS code to effectively scope the CSS to the component's view.
|
(and renaming) the CSS code to effectively scope the CSS to the component's view.
|
||||||
For details, see [Appendix 1](guide/component-styles#inspect-generated-css).
|
For details, see [Appendix 1](guide/component-styles#inspect-generated-css).
|
||||||
|
|
||||||
* `None` means that Angular does no view encapsulation.
|
* `None` means that Angular does no view encapsulation.
|
||||||
Angular adds the CSS to the global styles.
|
Angular adds the CSS to the global styles.
|
||||||
The scoping rules, isolations, and protections discussed earlier don't apply.
|
The scoping rules, isolations, and protections discussed earlier don't apply.
|
||||||
This is essentially the same as pasting the component's styles into the HTML.
|
This is essentially the same as pasting the component's styles into the HTML.
|
||||||
|
|
||||||
To set the components encapsulation mode, use the `encapsulation` property in the component metadata:
|
To set the components encapsulation mode, use the `encapsulation` property in the component metadata:
|
||||||
|
|
||||||
<code-example path="component-styles/src/app/quest-summary.component.ts" region="encapsulation.native" title="src/app/quest-summary.component.ts" linenums="false">
|
<code-example path="component-styles/src/app/quest-summary.component.ts" region="encapsulation.native" title="src/app/quest-summary.component.ts" linenums="false">
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
`Native` view encapsulation only works on browsers that have native support
|
`Native` view encapsulation only works on browsers that have native support
|
||||||
for shadow DOM (see [Shadow DOM v0](http://caniuse.com/#feat=shadowdom) on the
|
for shadow DOM (see [Shadow DOM v0](http://caniuse.com/#feat=shadowdom) on the
|
||||||
[Can I use](http://caniuse.com) site). The support is still limited,
|
[Can I use](http://caniuse.com) site). The support is still limited,
|
||||||
which is why `Emulated` view encapsulation is the default mode and recommended
|
which is why `Emulated` view encapsulation is the default mode and recommended
|
||||||
in most cases.
|
in most cases.
|
||||||
@ -331,7 +331,7 @@ There are two kinds of generated attributes:
|
|||||||
|
|
||||||
* An element that would be a shadow DOM host in native encapsulation has a
|
* An element that would be a shadow DOM host in native encapsulation has a
|
||||||
generated `_nghost` attribute. This is typically the case for component host elements.
|
generated `_nghost` attribute. This is typically the case for component host elements.
|
||||||
* An element within a component's view has a `_ngcontent` attribute
|
* An element within a component's view has a `_ngcontent` attribute
|
||||||
that identifies to which host's emulated shadow DOM this element belongs.
|
that identifies to which host's emulated shadow DOM this element belongs.
|
||||||
|
|
||||||
The exact values of these attributes aren't important. They are automatically
|
The exact values of these attributes aren't important. They are automatically
|
||||||
@ -351,5 +351,5 @@ by the generated component styles, which are in the `<head>` section of the DOM:
|
|||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
These styles are post-processed so that each selector is augmented
|
These styles are post-processed so that each selector is augmented
|
||||||
with `_nghost` or `_ngcontent` attribute selectors.
|
with `_nghost` or `_ngcontent` attribute selectors.
|
||||||
These extra selectors enable the scoping rules described in this page.
|
These extra selectors enable the scoping rules described in this page.
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -165,7 +165,7 @@ Such [entry components](guide/ngmodule-faq#q-entry-component-defined) can never
|
|||||||
While there's no harm in exporting them, there's also no benefit.
|
While there's no harm in exporting them, there's also no benefit.
|
||||||
* Pure service modules that don't have public (exported) declarations.
|
* Pure service modules that don't have public (exported) declarations.
|
||||||
For example, there's no point in re-exporting `HttpClientModule` because it doesn't export anything.
|
For example, there's no point in re-exporting `HttpClientModule` because it doesn't export anything.
|
||||||
It's only purpose is to add http service providers to the application as a whole.
|
Its only purpose is to add http service providers to the application as a whole.
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
@ -190,7 +190,7 @@ An NgModule can export a combination of its own declarations, selected imported
|
|||||||
Don't bother re-exporting pure service modules.
|
Don't bother re-exporting pure service modules.
|
||||||
Pure service modules don't export [declarable](guide/bootstrapping#the-declarations-array) classes that another NgModule could use.
|
Pure service modules don't export [declarable](guide/bootstrapping#the-declarations-array) classes that another NgModule could use.
|
||||||
For example, there's no point in re-exporting `HttpClientModule` because it doesn't export anything.
|
For example, there's no point in re-exporting `HttpClientModule` because it doesn't export anything.
|
||||||
It's only purpose is to add http service providers to the application as a whole.
|
Its only purpose is to add http service providers to the application as a whole.
|
||||||
|
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
@ -290,20 +290,42 @@ out of date. Right click the Cache Storage title and refresh the caches.
|
|||||||
Stopping and starting the service worker in the Service Worker
|
Stopping and starting the service worker in the Service Worker
|
||||||
pane triggers a check for updates.
|
pane triggers a check for updates.
|
||||||
|
|
||||||
## Fail-safe
|
## Service Worker Safety
|
||||||
|
|
||||||
Like any complex system, bugs or broken configurations can cause
|
Like any complex system, bugs or broken configurations can cause
|
||||||
the Angular service worker to act in unforeseen ways. While its
|
the Angular service worker to act in unforeseen ways. While its
|
||||||
design attempts to minimize the impact of such problems, the
|
design attempts to minimize the impact of such problems, the
|
||||||
Angular service worker contains a failsafe mechanism in case
|
Angular service worker contains several failsafe mechanisms in case
|
||||||
an administrator ever needs to deactivate the service worker quickly.
|
an administrator ever needs to deactivate the service worker quickly.
|
||||||
|
|
||||||
|
## Fail-safe
|
||||||
|
|
||||||
To deactivate the service worker, remove or rename the
|
To deactivate the service worker, remove or rename the
|
||||||
`ngsw-config.json` file. When the service worker's request
|
`ngsw-config.json` file. When the service worker's request
|
||||||
for `ngsw.json` returns a `404`, then the service worker
|
for `ngsw.json` returns a `404`, then the service worker
|
||||||
removes all of its caches and de-registers itself,
|
removes all of its caches and de-registers itself,
|
||||||
essentially self-destructing.
|
essentially self-destructing.
|
||||||
|
|
||||||
|
### Safety Worker
|
||||||
|
|
||||||
|
Also included in the `@angular/service-worker` NPM package is a small
|
||||||
|
script `safety-worker.js`, which when loaded will unregister itself
|
||||||
|
from the browser. This script can be used as a last resort to get rid
|
||||||
|
of unwanted service workers already installed on client pages.
|
||||||
|
|
||||||
|
It's important to note that you cannot register this worker directly,
|
||||||
|
as old clients with cached state may not see a new `index.html` which
|
||||||
|
installs the different worker script. Instead, you must serve the
|
||||||
|
contents of `safety-worker.js` at the URL of the Service Worker script
|
||||||
|
you are trying to unregister, and must continue to do so until you are
|
||||||
|
certain all users have successfully unregistered the old worker. For
|
||||||
|
most sites, this means that you should serve the safety worker at the
|
||||||
|
old Service Worker URL forever.
|
||||||
|
|
||||||
|
This script can be used both to deactivate `@angular/service-worker`
|
||||||
|
as well as any other Service Workers which might have been served in
|
||||||
|
the past on your site.
|
||||||
|
|
||||||
## More on Angular service workers
|
## More on Angular service workers
|
||||||
|
|
||||||
You may also be interested in the following:
|
You may also be interested in the following:
|
||||||
|
@ -297,7 +297,7 @@ These properties include `index` and `odd` and a special property named `$implic
|
|||||||
Angular sets them to the current value of the context's `index` and `odd` properties.
|
Angular sets them to the current value of the context's `index` and `odd` properties.
|
||||||
|
|
||||||
* The context property for `let-hero` wasn't specified.
|
* The context property for `let-hero` wasn't specified.
|
||||||
It's intended source is implicit.
|
Its intended source is implicit.
|
||||||
Angular sets `let-hero` to the value of the context's `$implicit` property
|
Angular sets `let-hero` to the value of the context's `$implicit` property
|
||||||
which `NgFor` has initialized with the hero for the current iteration.
|
which `NgFor` has initialized with the hero for the current iteration.
|
||||||
|
|
||||||
|
@ -588,7 +588,7 @@ and a controller:
|
|||||||
|
|
||||||
You can *upgrade* this component to Angular using the `UpgradeComponent` class.
|
You can *upgrade* this component to Angular using the `UpgradeComponent` class.
|
||||||
By creating a new Angular **directive** that extends `UpgradeComponent` and doing a `super` call
|
By creating a new Angular **directive** that extends `UpgradeComponent` and doing a `super` call
|
||||||
inside it's constructor, you have a fully upgraded AngularJS component to be used inside Angular.
|
inside its constructor, you have a fully upgraded AngularJS component to be used inside Angular.
|
||||||
All that is left is to add it to `AppModule`'s `declarations` array.
|
All that is left is to add it to `AppModule`'s `declarations` array.
|
||||||
|
|
||||||
<code-example path="upgrade-module/src/app/upgrade-static/hero-detail.component.ts" region="hero-detail-upgrade" title="hero-detail.component.ts">
|
<code-example path="upgrade-module/src/app/upgrade-static/hero-detail.component.ts" region="hero-detail-upgrade" title="hero-detail.component.ts">
|
||||||
@ -602,7 +602,7 @@ All that is left is to add it to `AppModule`'s `declarations` array.
|
|||||||
Upgraded components are Angular **directives**, instead of **components**, because Angular
|
Upgraded components are Angular **directives**, instead of **components**, because Angular
|
||||||
is unaware that AngularJS will create elements under it. As far as Angular knows, the upgraded
|
is unaware that AngularJS will create elements under it. As far as Angular knows, the upgraded
|
||||||
component is just a directive - a tag - and Angular doesn't have to concern itself with
|
component is just a directive - a tag - and Angular doesn't have to concern itself with
|
||||||
it's children.
|
its children.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -784,7 +784,7 @@ compilation can pick it up.
|
|||||||
<code-example path="upgrade-module/src/app/ajs-to-a-providers/app.module.ts" region="register" title="app.module.ts">
|
<code-example path="upgrade-module/src/app/ajs-to-a-providers/app.module.ts" region="register" title="app.module.ts">
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
You can then inject it in Angular using it's class as a type annotation:
|
You can then inject it in Angular using its class as a type annotation:
|
||||||
|
|
||||||
<code-example path="upgrade-module/src/app/ajs-to-a-providers/hero-detail.component.ts" title="hero-detail.component.ts">
|
<code-example path="upgrade-module/src/app/ajs-to-a-providers/hero-detail.component.ts" title="hero-detail.component.ts">
|
||||||
|
|
||||||
@ -1260,7 +1260,7 @@ app. Switch to the [ngUpgrade bootstrap](#bootstrapping-hybrid-applications) met
|
|||||||
instead.
|
instead.
|
||||||
|
|
||||||
First, remove the `ng-app` attribute from `index.html`.
|
First, remove the `ng-app` attribute from `index.html`.
|
||||||
Then import `UpgradeModule` in the `AppModule`, and override it's `ngDoBootstrap` method:
|
Then import `UpgradeModule` in the `AppModule`, and override its `ngDoBootstrap` method:
|
||||||
|
|
||||||
<code-example path="upgrade-phonecat-2-hybrid/app/app.module.ts" region="upgrademodule" title="app/app.module.ts">
|
<code-example path="upgrade-phonecat-2-hybrid/app/app.module.ts" region="upgrademodule" title="app/app.module.ts">
|
||||||
</code-example>
|
</code-example>
|
||||||
@ -1619,7 +1619,7 @@ instead of the default "push state" strategy.
|
|||||||
Now update the `AppModule` to import this `AppRoutingModule` and also the
|
Now update the `AppModule` to import this `AppRoutingModule` and also the
|
||||||
declare the root `AppComponent` as the bootstrap component.
|
declare the root `AppComponent` as the bootstrap component.
|
||||||
That tells Angular that it should bootstrap the app with the _root_ `AppComponent` and
|
That tells Angular that it should bootstrap the app with the _root_ `AppComponent` and
|
||||||
insert it's view into the host web page.
|
insert its view into the host web page.
|
||||||
|
|
||||||
You must also remove the bootstrap of the AngularJS module from `ngDoBootstrap()` in `app.module.ts`
|
You must also remove the bootstrap of the AngularJS module from `ngDoBootstrap()` in `app.module.ts`
|
||||||
and the `UpgradeModule` import.
|
and the `UpgradeModule` import.
|
||||||
@ -1695,7 +1695,7 @@ module configuration files and not needed in Angular:
|
|||||||
|
|
||||||
The external typings for AngularJS may be uninstalled as well. The only ones
|
The external typings for AngularJS may be uninstalled as well. The only ones
|
||||||
you still need are for Jasmine and Angular polyfills.
|
you still need are for Jasmine and Angular polyfills.
|
||||||
The `@angular/upgrade` package and it's mapping in `systemjs.config.js` can also go.
|
The `@angular/upgrade` package and its mapping in `systemjs.config.js` can also go.
|
||||||
|
|
||||||
<code-example format="">
|
<code-example format="">
|
||||||
npm uninstall @angular/upgrade --save
|
npm uninstall @angular/upgrade --save
|
||||||
|
@ -44,7 +44,8 @@
|
|||||||
"docs-lint": "eslint --ignore-path=\"tools/transforms/.eslintignore\" tools/transforms",
|
"docs-lint": "eslint --ignore-path=\"tools/transforms/.eslintignore\" tools/transforms",
|
||||||
"docs-test": "node tools/transforms/test.js",
|
"docs-test": "node tools/transforms/test.js",
|
||||||
"tools-test": "./scripts/deploy-to-firebase.test.sh && yarn docs-test && yarn boilerplate:test && jasmine tools/ng-packages-installer/index.spec.js",
|
"tools-test": "./scripts/deploy-to-firebase.test.sh && yarn docs-test && yarn boilerplate:test && jasmine tools/ng-packages-installer/index.spec.js",
|
||||||
"serve-and-sync": "concurrently --kill-others \"yarn docs-watch\" \"yarn start\"",
|
"preserve-and-sync": "yarn docs",
|
||||||
|
"serve-and-sync": "concurrently --kill-others \"yarn docs-watch --watch-only\" \"yarn start\"",
|
||||||
"boilerplate:add": "node ./tools/examples/example-boilerplate add",
|
"boilerplate:add": "node ./tools/examples/example-boilerplate add",
|
||||||
"boilerplate:remove": "node ./tools/examples/example-boilerplate remove",
|
"boilerplate:remove": "node ./tools/examples/example-boilerplate remove",
|
||||||
"boilerplate:test": "node tools/examples/test.js",
|
"boilerplate:test": "node tools/examples/test.js",
|
||||||
@ -101,7 +102,7 @@
|
|||||||
"cross-spawn": "^5.1.0",
|
"cross-spawn": "^5.1.0",
|
||||||
"css-selector-parser": "^1.3.0",
|
"css-selector-parser": "^1.3.0",
|
||||||
"dgeni": "^0.4.7",
|
"dgeni": "^0.4.7",
|
||||||
"dgeni-packages": "0.22.1",
|
"dgeni-packages": "^0.24.0",
|
||||||
"entities": "^1.1.1",
|
"entities": "^1.1.1",
|
||||||
"eslint": "^3.19.0",
|
"eslint": "^3.19.0",
|
||||||
"eslint-plugin-jasmine": "^2.2.0",
|
"eslint-plugin-jasmine": "^2.2.0",
|
||||||
|
@ -153,122 +153,202 @@ describe('AppComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('SideNav when side-by-side (wide)', () => {
|
describe('SideNav', () => {
|
||||||
const navigateTo = (path: string) => {
|
const navigateTo = (path: string) => {
|
||||||
locationService.go(path);
|
locationService.go(path);
|
||||||
component.updateSideNav();
|
component.updateSideNav();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
};
|
};
|
||||||
|
const resizeTo = (width: number) => {
|
||||||
beforeEach(() => {
|
component.onResize(width);
|
||||||
component.onResize(sideBySideBreakPoint + 1); // side-by-side
|
fixture.detectChanges();
|
||||||
});
|
};
|
||||||
|
const toggleSidenav = () => {
|
||||||
it('should open when nav to a guide page (guide/pipes)', () => {
|
hamburger.click();
|
||||||
navigateTo('guide/pipes');
|
|
||||||
expect(sidenav.opened).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should open when nav to an api page', () => {
|
|
||||||
navigateTo('api/a/b/c/d');
|
|
||||||
expect(sidenav.opened).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be closed when nav to a marketing page (features)', () => {
|
|
||||||
navigateTo('features');
|
|
||||||
expect(sidenav.opened).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when manually closed', () => {
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
navigateTo('guide/pipes');
|
|
||||||
hamburger.click();
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be closed', () => {
|
|
||||||
expect(sidenav.opened).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stay closed when nav from one guide page to another', () => {
|
|
||||||
navigateTo('guide/bags');
|
|
||||||
expect(sidenav.opened).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stay closed when nav from a guide page to api page', () => {
|
|
||||||
navigateTo('api');
|
|
||||||
expect(sidenav.opened).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reopen when nav to market page and back to guide page', () => {
|
|
||||||
navigateTo('features');
|
|
||||||
navigateTo('guide/bags');
|
|
||||||
expect(sidenav.opened).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('SideNav when NOT side-by-side (narrow)', () => {
|
|
||||||
const navigateTo = (path: string) => {
|
|
||||||
locationService.go(path);
|
|
||||||
component.updateSideNav();
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
describe('when side-by-side (wide)', () => {
|
||||||
component.onResize(sideBySideBreakPoint - 1); // NOT side-by-side
|
beforeEach(() => resizeTo(sideBySideBreakPoint + 1)); // side-by-side
|
||||||
});
|
|
||||||
|
|
||||||
it('should be closed when nav to a guide page (guide/pipes)', () => {
|
it('should open when navigating to a guide page (guide/pipes)', () => {
|
||||||
navigateTo('guide/pipes');
|
|
||||||
expect(sidenav.opened).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be closed when nav to an api page', () => {
|
|
||||||
navigateTo('api/a/b/c/d');
|
|
||||||
expect(sidenav.opened).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be closed when nav to a marketing page (features)', () => {
|
|
||||||
navigateTo('features');
|
|
||||||
expect(sidenav.opened).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when manually opened', () => {
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
navigateTo('guide/pipes');
|
navigateTo('guide/pipes');
|
||||||
hamburger.click();
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be open', () => {
|
|
||||||
expect(sidenav.opened).toBe(true);
|
expect(sidenav.opened).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should close when click in gray content area overlay', () => {
|
it('should open when navigating to an api page', () => {
|
||||||
const sidenavBackdrop = fixture.debugElement.query(By.css('.mat-drawer-backdrop')).nativeElement;
|
navigateTo('api/a/b/c/d');
|
||||||
sidenavBackdrop.click();
|
expect(sidenav.opened).toBe(true);
|
||||||
fixture.detectChanges();
|
|
||||||
expect(sidenav.opened).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should close when nav to another guide page', () => {
|
it('should be closed when navigating to a marketing page (features)', () => {
|
||||||
navigateTo('guide/bags');
|
|
||||||
expect(sidenav.opened).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should close when nav to api page', () => {
|
|
||||||
navigateTo('api');
|
|
||||||
expect(sidenav.opened).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should close again when nav to market page', () => {
|
|
||||||
navigateTo('features');
|
navigateTo('features');
|
||||||
expect(sidenav.opened).toBe(false);
|
expect(sidenav.opened).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when manually closed', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
navigateTo('guide/pipes');
|
||||||
|
toggleSidenav();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be closed', () => {
|
||||||
|
expect(sidenav.opened).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stay closed when navigating from one guide page to another', () => {
|
||||||
|
navigateTo('guide/bags');
|
||||||
|
expect(sidenav.opened).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stay closed when navigating from a guide page to api page', () => {
|
||||||
|
navigateTo('api');
|
||||||
|
expect(sidenav.opened).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reopen when navigating to market page and back to guide page', () => {
|
||||||
|
navigateTo('features');
|
||||||
|
navigateTo('guide/bags');
|
||||||
|
expect(sidenav.opened).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when NOT side-by-side (narrow)', () => {
|
||||||
|
beforeEach(() => resizeTo(sideBySideBreakPoint - 1)); // NOT side-by-side
|
||||||
|
|
||||||
|
it('should be closed when navigating to a guide page (guide/pipes)', () => {
|
||||||
|
navigateTo('guide/pipes');
|
||||||
|
expect(sidenav.opened).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be closed when navigating to an api page', () => {
|
||||||
|
navigateTo('api/a/b/c/d');
|
||||||
|
expect(sidenav.opened).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be closed when navigating to a marketing page (features)', () => {
|
||||||
|
navigateTo('features');
|
||||||
|
expect(sidenav.opened).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when manually opened', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
navigateTo('guide/pipes');
|
||||||
|
toggleSidenav();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be open', () => {
|
||||||
|
expect(sidenav.opened).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close when clicking in gray content area overlay', () => {
|
||||||
|
const sidenavBackdrop = fixture.debugElement.query(By.css('.mat-drawer-backdrop')).nativeElement;
|
||||||
|
sidenavBackdrop.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(sidenav.opened).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close when navigating to another guide page', () => {
|
||||||
|
navigateTo('guide/bags');
|
||||||
|
expect(sidenav.opened).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close when navigating to api page', () => {
|
||||||
|
navigateTo('api');
|
||||||
|
expect(sidenav.opened).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close again when navigating to market page', () => {
|
||||||
|
navigateTo('features');
|
||||||
|
expect(sidenav.opened).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when changing side-by-side (narrow --> wide)', () => {
|
||||||
|
const sidenavDocs = ['api/a/b/c/d', 'guide/pipes'];
|
||||||
|
const nonSidenavDocs = ['features', 'about'];
|
||||||
|
|
||||||
|
sidenavDocs.forEach(doc => {
|
||||||
|
it(`should open when on a sidenav doc (${doc})`, () => {
|
||||||
|
resizeTo(sideBySideBreakPoint - 1);
|
||||||
|
|
||||||
|
navigateTo(doc);
|
||||||
|
expect(sidenav.opened).toBe(false);
|
||||||
|
|
||||||
|
resizeTo(sideBySideBreakPoint + 1);
|
||||||
|
expect(sidenav.opened).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
nonSidenavDocs.forEach(doc => {
|
||||||
|
it(`should remain closed when on a non-sidenav doc (${doc})`, () => {
|
||||||
|
resizeTo(sideBySideBreakPoint - 1);
|
||||||
|
|
||||||
|
navigateTo(doc);
|
||||||
|
expect(sidenav.opened).toBe(false);
|
||||||
|
|
||||||
|
resizeTo(sideBySideBreakPoint + 1);
|
||||||
|
expect(sidenav.opened).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when manually opened', () => {
|
||||||
|
sidenavDocs.forEach(doc => {
|
||||||
|
it(`should remain opened when on a sidenav doc (${doc})`, () => {
|
||||||
|
resizeTo(sideBySideBreakPoint - 1);
|
||||||
|
|
||||||
|
navigateTo(doc);
|
||||||
|
toggleSidenav();
|
||||||
|
expect(sidenav.opened).toBe(true);
|
||||||
|
|
||||||
|
resizeTo(sideBySideBreakPoint + 1);
|
||||||
|
expect(sidenav.opened).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
nonSidenavDocs.forEach(doc => {
|
||||||
|
it(`should close when on a non-sidenav doc (${doc})`, () => {
|
||||||
|
resizeTo(sideBySideBreakPoint - 1);
|
||||||
|
|
||||||
|
navigateTo(doc);
|
||||||
|
toggleSidenav();
|
||||||
|
expect(sidenav.opened).toBe(true);
|
||||||
|
|
||||||
|
resizeTo(sideBySideBreakPoint + 1);
|
||||||
|
expect(sidenav.opened).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when changing side-by-side (wide --> narrow)', () => {
|
||||||
|
const sidenavDocs = ['api/a/b/c/d', 'guide/pipes'];
|
||||||
|
const nonSidenavDocs = ['features', 'about'];
|
||||||
|
|
||||||
|
sidenavDocs.forEach(doc => {
|
||||||
|
it(`should close when on a sidenav doc (${doc})`, () => {
|
||||||
|
navigateTo(doc);
|
||||||
|
expect(sidenav.opened).toBe(true);
|
||||||
|
|
||||||
|
resizeTo(sideBySideBreakPoint - 1);
|
||||||
|
expect(sidenav.opened).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
nonSidenavDocs.forEach(doc => {
|
||||||
|
it(`should remain closed when on a non-sidenav doc (${doc})`, () => {
|
||||||
|
navigateTo(doc);
|
||||||
|
expect(sidenav.opened).toBe(false);
|
||||||
|
|
||||||
|
resizeTo(sideBySideBreakPoint - 1);
|
||||||
|
expect(sidenav.opened).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -387,14 +467,14 @@ describe('AppComponent', () => {
|
|||||||
expect(scrollSpy).toHaveBeenCalled();
|
expect(scrollSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should scroll again when nav to the same hash twice in succession', () => {
|
it('should scroll again when navigating to the same hash twice in succession', () => {
|
||||||
locationService.go('guide/pipes');
|
locationService.go('guide/pipes');
|
||||||
locationService.go('guide/pipes#somewhere');
|
locationService.go('guide/pipes#somewhere');
|
||||||
locationService.go('guide/pipes#somewhere');
|
locationService.go('guide/pipes#somewhere');
|
||||||
expect(scrollSpy.calls.count()).toBe(2);
|
expect(scrollSpy.calls.count()).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should scroll when nav to the same path', () => {
|
it('should scroll when navigating to the same path', () => {
|
||||||
locationService.go('guide/pipes');
|
locationService.go('guide/pipes');
|
||||||
scrollSpy.calls.reset();
|
scrollSpy.calls.reset();
|
||||||
|
|
||||||
|
@ -235,6 +235,14 @@ export class AppComponent implements OnInit {
|
|||||||
onResize(width: number) {
|
onResize(width: number) {
|
||||||
this.isSideBySide = width > this.sideBySideWidth;
|
this.isSideBySide = width > this.sideBySideWidth;
|
||||||
this.showFloatingToc.next(width > this.showFloatingTocWidth);
|
this.showFloatingToc.next(width > this.showFloatingTocWidth);
|
||||||
|
|
||||||
|
if (this.isSideBySide && !this.isSideNavDoc) {
|
||||||
|
// If this is a non-sidenav doc and the screen is wide enough so that we can display menu
|
||||||
|
// items in the top-bar, ensure the sidenav is closed.
|
||||||
|
// (This condition can only be met when the resize event changes the value of `isSideBySide`
|
||||||
|
// from `false` to `true` while on a non-sidenav doc.)
|
||||||
|
this.sideNavToggle(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('click', ['$event.target', '$event.button', '$event.ctrlKey', '$event.metaKey', '$event.altKey'])
|
@HostListener('click', ['$event.target', '$event.button', '$event.ctrlKey', '$event.metaKey', '$event.altKey'])
|
||||||
|
@ -31,14 +31,6 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="translucent">
|
||||||
|
|
||||||
<!--
|
|
||||||
Initially tell the Google crawler not to index this page.
|
|
||||||
If the page loads correctly will remove this tag (in the DocViewer).
|
|
||||||
Subsequent navigations will update the tag dynamically (i.e. soft 404).
|
|
||||||
Don't do the same for `robots` in general here, since they might not be able to handle the tag changing dynamically.
|
|
||||||
-->
|
|
||||||
<meta name="googlebot" content="noindex">
|
|
||||||
|
|
||||||
<!-- Google Analytics -->
|
<!-- Google Analytics -->
|
||||||
<script>
|
<script>
|
||||||
// Note this is a customised version of the GA tracking snippet
|
// Note this is a customised version of the GA tracking snippet
|
||||||
|
@ -35,7 +35,6 @@ mat-sidenav.mat-sidenav.sidenav {
|
|||||||
min-width: 260px;
|
min-width: 260px;
|
||||||
background-color: $offwhite;
|
background-color: $offwhite;
|
||||||
box-shadow: 6px 0 6px rgba(0,0,0,0.10);
|
box-shadow: 6px 0 6px rgba(0,0,0,0.10);
|
||||||
height: calc(100vh - 64px);
|
|
||||||
|
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
top: 56px;
|
top: 56px;
|
||||||
|
@ -17,7 +17,6 @@ const CLI_SPEC_FILENAME = 'e2e/app.e2e-spec.ts';
|
|||||||
const EXAMPLE_CONFIG_FILENAME = 'example-config.json';
|
const EXAMPLE_CONFIG_FILENAME = 'example-config.json';
|
||||||
const IGNORED_EXAMPLES = [ // temporary ignores
|
const IGNORED_EXAMPLES = [ // temporary ignores
|
||||||
'quickstart',
|
'quickstart',
|
||||||
'http',
|
|
||||||
'setup',
|
'setup',
|
||||||
'webpack',
|
'webpack',
|
||||||
'upgrade-p'
|
'upgrade-p'
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
"zone.js": "^0.8.4"
|
"zone.js": "^0.8.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/cli": "1.5.0",
|
"@angular/cli": "1.6.5",
|
||||||
"@types/angular": "^1.5.16",
|
"@types/angular": "^1.5.16",
|
||||||
"@types/angular-animate": "^1.5.5",
|
"@types/angular-animate": "^1.5.5",
|
||||||
"@types/angular-cookies": "^1.4.2",
|
"@types/angular-cookies": "^1.4.2",
|
||||||
@ -66,7 +66,7 @@
|
|||||||
"http-server": "^0.9.0",
|
"http-server": "^0.9.0",
|
||||||
"jasmine": "~2.8.0",
|
"jasmine": "~2.8.0",
|
||||||
"jasmine-core": "~2.8.0",
|
"jasmine-core": "~2.8.0",
|
||||||
"jasmine-marbles":"^0.2.0",
|
"jasmine-marbles": "^0.2.0",
|
||||||
"jasmine-spec-reporter": "^4.2.1",
|
"jasmine-spec-reporter": "^4.2.1",
|
||||||
"karma": "^1.3.0",
|
"karma": "^1.3.0",
|
||||||
"karma-chrome-launcher": "^2.0.0",
|
"karma-chrome-launcher": "^2.0.0",
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
module.exports = function filterContainedDocs() {
|
module.exports = function filterContainedDocs() {
|
||||||
return {
|
return {
|
||||||
docTypes: ['member', 'function-overload', 'get-accessor-info', 'set-accessor-info'],
|
docTypes: ['member', 'function-overload', 'get-accessor-info', 'set-accessor-info', 'parameter'],
|
||||||
$runAfter: ['extra-docs-added'],
|
$runAfter: ['extra-docs-added'],
|
||||||
$runBefore: ['computing-paths'],
|
$runBefore: ['computing-paths'],
|
||||||
$process: function(docs) {
|
$process: function(docs) {
|
||||||
|
@ -5,9 +5,9 @@
|
|||||||
<code-example language="ts" hideCopy="true">
|
<code-example language="ts" hideCopy="true">
|
||||||
{$ doc.docType $} {$ doc.name $}{$ doc.typeParams | escape $}{$ memberHelper.renderHeritage(doc) $} {
|
{$ doc.docType $} {$ doc.name $}{$ doc.typeParams | escape $}{$ memberHelper.renderHeritage(doc) $} {
|
||||||
{%- if doc.constructorDoc %}{% if not doc.constructorDoc.internal %}
|
{%- if doc.constructorDoc %}{% if not doc.constructorDoc.internal %}
|
||||||
<a class="code-anchor" href="#{$ doc.constructorDoc.anchor $}">{$ memberHelper.renderMember(doc.constructorDoc, 1) $}</a>{% endif %}{% endif -%}
|
<a class="code-anchor" href="#{$ doc.constructorDoc.anchor | urlencode $}">{$ memberHelper.renderMember(doc.constructorDoc, 1) $}</a>{% endif %}{% endif -%}
|
||||||
{%- if doc.statics.length %}{% for member in doc.statics %}{% if not member.internal %}
|
{%- if doc.statics.length %}{% for member in doc.statics %}{% if not member.internal %}
|
||||||
<a class="code-anchor" href="#{$ member.anchor $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif -%}
|
<a class="code-anchor" href="#{$ member.anchor | urlencode $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif -%}
|
||||||
{$ memberHelper.renderMembers(doc) $}
|
{$ memberHelper.renderMembers(doc) $}
|
||||||
}
|
}
|
||||||
</code-example>
|
</code-example>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
@{$ decorator.name $}({$ decorator.arguments $}){% endfor %}
|
@{$ decorator.name $}({$ decorator.arguments $}){% endfor %}
|
||||||
class {$ doc.name $}{$ doc.typeParams | escape $}{$ memberHelper.renderHeritage(doc) $} {
|
class {$ doc.name $}{$ doc.typeParams | escape $}{$ memberHelper.renderHeritage(doc) $} {
|
||||||
{%- if doc.statics.length %}{% for member in doc.statics %}{% if not member.internal %}
|
{%- if doc.statics.length %}{% for member in doc.statics %}{% if not member.internal %}
|
||||||
<a class="code-anchor" href="#{$ member.anchor $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif -%}
|
<a class="code-anchor" href="#{$ member.anchor | urlencode $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif -%}
|
||||||
{$ memberHelper.renderMembers(doc) $}
|
{$ memberHelper.renderMembers(doc) $}
|
||||||
}
|
}
|
||||||
</code-example>
|
</code-example>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<h2>Interface Overview</h2>
|
<h2>Interface Overview</h2>
|
||||||
<code-example language="ts" hideCopy="true">
|
<code-example language="ts" hideCopy="true">
|
||||||
interface {$ doc.name $}{$ doc.typeParams | escape $}{$ memberHelper.renderHeritage(doc) $} { {% if doc.members.length %}{% for member in doc.members %}{% if not member.internal %}
|
interface {$ doc.name $}{$ doc.typeParams | escape $}{$ memberHelper.renderHeritage(doc) $} { {% if doc.members.length %}{% for member in doc.members %}{% if not member.internal %}
|
||||||
<a class="code-anchor" href="#{$ member.anchor $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif %}
|
<a class="code-anchor" href="#{$ member.anchor | urlencode $}">{$ memberHelper.renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif %}
|
||||||
}
|
}
|
||||||
</code-example>
|
</code-example>
|
||||||
</section>
|
</section>
|
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
{%- macro renderMembers(doc) -%}
|
{%- macro renderMembers(doc) -%}
|
||||||
{%- if doc.members.length %}{% for member in doc.members %}{% if not member.internal %}
|
{%- if doc.members.length %}{% for member in doc.members %}{% if not member.internal %}
|
||||||
<a class="code-anchor" href="{$ doc.path $}#{$ member.anchor $}">{$ renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif %}
|
<a class="code-anchor" href="{$ doc.path $}#{$ member.anchor | urlencode $}">{$ renderMember(member, 1) $}</a>{% endif %}{% endfor %}{% endif %}
|
||||||
{%- for ancestor in doc.extendsClauses %}{% if ancestor.doc %}
|
{%- for ancestor in doc.extendsClauses %}{% if ancestor.doc %}
|
||||||
// inherited from <a class="code-anchor" href="{$ ancestor.doc.path $}">{$ ancestor.doc.id $}</a>{$ renderMembers(ancestor.doc) $}{% endif %}{% endfor %}
|
// inherited from <a class="code-anchor" href="{$ ancestor.doc.path $}">{$ ancestor.doc.id $}</a>{$ renderMembers(ancestor.doc) $}{% endif %}{% endfor %}
|
||||||
{%- endmacro -%}
|
{%- endmacro -%}
|
||||||
|
@ -2306,9 +2306,9 @@ devtools-timeline-model@1.1.6:
|
|||||||
chrome-devtools-frontend "1.0.401423"
|
chrome-devtools-frontend "1.0.401423"
|
||||||
resolve "1.1.7"
|
resolve "1.1.7"
|
||||||
|
|
||||||
dgeni-packages@0.22.1:
|
dgeni-packages@^0.24.0:
|
||||||
version "0.22.1"
|
version "0.24.0"
|
||||||
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.22.1.tgz#c4587a765689c4c9d48ed661517ed2249403bfb2"
|
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.24.0.tgz#2f995f78fecd6a9ded72d7bdccbbc4c46360c1ea"
|
||||||
dependencies:
|
dependencies:
|
||||||
canonical-path "0.0.2"
|
canonical-path "0.0.2"
|
||||||
catharsis "^0.8.1"
|
catharsis "^0.8.1"
|
||||||
@ -2331,6 +2331,7 @@ dgeni-packages@0.22.1:
|
|||||||
spdx-license-list "^2.1.0"
|
spdx-license-list "^2.1.0"
|
||||||
stringmap "^0.2.2"
|
stringmap "^0.2.2"
|
||||||
typescript "2.4"
|
typescript "2.4"
|
||||||
|
urlencode "^1.1.0"
|
||||||
|
|
||||||
dgeni@^0.4.7, dgeni@^0.4.9:
|
dgeni@^0.4.7, dgeni@^0.4.9:
|
||||||
version "0.4.9"
|
version "0.4.9"
|
||||||
@ -4081,7 +4082,7 @@ iconv-lite@0.4.15:
|
|||||||
version "0.4.15"
|
version "0.4.15"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
|
||||||
|
|
||||||
iconv-lite@0.4.19:
|
iconv-lite@0.4.19, iconv-lite@~0.4.11:
|
||||||
version "0.4.19"
|
version "0.4.19"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
|
||||||
|
|
||||||
@ -8760,6 +8761,12 @@ url@^0.11.0:
|
|||||||
punycode "1.3.2"
|
punycode "1.3.2"
|
||||||
querystring "0.2.0"
|
querystring "0.2.0"
|
||||||
|
|
||||||
|
urlencode@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/urlencode/-/urlencode-1.1.0.tgz#1f2ba26f013c85f0133f7a3ad6ff2730adf7cbb7"
|
||||||
|
dependencies:
|
||||||
|
iconv-lite "~0.4.11"
|
||||||
|
|
||||||
user-home@^2.0.0:
|
user-home@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f"
|
resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f"
|
||||||
|
@ -58,7 +58,7 @@ new as of May 2017 and not very stable yet.
|
|||||||
- Test all packages: `bazel test packages/...`
|
- Test all packages: `bazel test packages/...`
|
||||||
|
|
||||||
You can use [ibazel] to get a "watch mode" that continuously
|
You can use [ibazel] to get a "watch mode" that continuously
|
||||||
keeps the outputs up-to-date as you save sources.
|
keeps the outputs up-to-date as you save sources.
|
||||||
|
|
||||||
### Debugging a Node Test
|
### Debugging a Node Test
|
||||||
|
|
||||||
@ -68,6 +68,44 @@ keeps the outputs up-to-date as you save sources.
|
|||||||
|
|
||||||
The process should automatically connect to the debugger.
|
The process should automatically connect to the debugger.
|
||||||
|
|
||||||
|
### Debugging a Node Test in VSCode
|
||||||
|
|
||||||
|
First time setup:
|
||||||
|
- Go to Debug > Add configuration (in the menu bar) to open `launch.json`
|
||||||
|
- Add the following to the `configurations` array:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"name": "Attach (inspect)",
|
||||||
|
"type": "node",
|
||||||
|
"request": "attach",
|
||||||
|
"port": 9229,
|
||||||
|
"address": "localhost",
|
||||||
|
"restart": false,
|
||||||
|
"sourceMaps": true,
|
||||||
|
"localRoot": "${workspaceRoot}",
|
||||||
|
"remoteRoot": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Attach (no-sm,inspect)",
|
||||||
|
"type": "node",
|
||||||
|
"request": "attach",
|
||||||
|
"port": 9229,
|
||||||
|
"address": "localhost",
|
||||||
|
"restart": false,
|
||||||
|
"sourceMaps": false,
|
||||||
|
"localRoot": "${workspaceRoot}",
|
||||||
|
"remoteRoot": null
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
The easiest way to debug a test for now is to add a `debugger` statement in the code
|
||||||
|
and launch the bazel corresponding test (`bazel test <target> --config=debug`).
|
||||||
|
|
||||||
|
Bazel will wait on a connection. Go to the debug view (by clicking on the sidebar or
|
||||||
|
Apple+Shift+D on Mac) and click on the green play icon next to the configuration name
|
||||||
|
(ie `Attach (inspect)`).
|
||||||
|
|
||||||
### Debugging a Karma Test
|
### Debugging a Karma Test
|
||||||
|
|
||||||
- Run test: `bazel run packages/core/test:test_web`
|
- Run test: `bazel run packages/core/test:test_web`
|
||||||
|
4
integration/.gitignore
vendored
4
integration/.gitignore
vendored
@ -1,13 +1,9 @@
|
|||||||
built/
|
built/
|
||||||
dist/
|
dist/
|
||||||
vendor/
|
vendor/
|
||||||
yarn.lock
|
|
||||||
.ng-cli/
|
|
||||||
cli-*/**
|
|
||||||
*/src/*.d.ts
|
*/src/*.d.ts
|
||||||
*/src/*.js
|
*/src/*.js
|
||||||
**/*.ngfactory.ts
|
**/*.ngfactory.ts
|
||||||
**/*.ngsummary.json
|
**/*.ngsummary.json
|
||||||
**/*.ngsummary.ts
|
**/*.ngsummary.ts
|
||||||
*/yarn*
|
|
||||||
**/.yarn_local_cache*
|
**/.yarn_local_cache*
|
||||||
|
@ -1,11 +1,33 @@
|
|||||||
# Integration tests for Angular
|
# Integration tests for Angular
|
||||||
|
|
||||||
This directory contains end-to-end tests for Angular. Each directory is a self-contained
|
This directory contains end-to-end tests for Angular. Each directory is a self-contained application that exactly mimics how a user might expect Angular
|
||||||
application that exactly mimics how a user might expect Angular to work, so they allow
|
to work, so they allow high-fidelity reproductions of real-world issues.
|
||||||
high-fidelity reproductions of real-world issues.
|
|
||||||
|
|
||||||
For this to work, we first build the Angular distribution just like we would publish
|
For this to work, we first build the Angular distribution just like we would
|
||||||
it to npm, then install the distribution into each app.
|
publish it to npm, then install the distribution into each app.
|
||||||
|
|
||||||
|
To test Angular CLI applications, we generate integration tests such as `cli-hello-world`.
|
||||||
|
This was generated with a current version of the CLI, and the only modification was replacement of `@angular/*` packages with their counterparts coming from `file:../../dist/packages-dist/*`.
|
||||||
|
When a significant change is released in the CLI, the application should be re-generated from scratch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cd integration
|
||||||
|
$ rm -rf cli-hello-world
|
||||||
|
$ ng new cli-hello-world
|
||||||
|
# Edit cli-hello-world/package.json to point the @angular packages to dist/packages-dist, and preserve local mods to
|
||||||
|
# ng build
|
||||||
|
# ng test
|
||||||
|
# typescript version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Render3 tests
|
||||||
|
|
||||||
|
The directory `hello_world_cli` contains a test for render3 used with the angular cli.
|
||||||
|
|
||||||
|
If the Angular CLI is modified to generate a render3 application this should be replaced with that project.
|
||||||
|
|
||||||
|
If the render3 is updated to support the Angular 5 bootstrap a version of this project should be created that
|
||||||
|
uses the Angular 5 bootstrap.
|
||||||
|
|
||||||
## Writing an integration test
|
## Writing an integration test
|
||||||
|
|
||||||
@ -29,6 +51,8 @@ you can install the package directly from `file:../../node_modules`.
|
|||||||
|
|
||||||
## Running integration tests
|
## Running integration tests
|
||||||
|
|
||||||
|
First you must run `build.sh` to create the current distribution.
|
||||||
|
|
||||||
You can iterate on the tests by keeping the dist folder up-to-date.
|
You can iterate on the tests by keeping the dist folder up-to-date.
|
||||||
See the `package.json` of the test(s) you're debugging, to see which dist/ folders they install from.
|
See the `package.json` of the test(s) you're debugging, to see which dist/ folders they install from.
|
||||||
Then run the right `tsc --watch` command to keep those dist folders up-to-date, for example:
|
Then run the right `tsc --watch` command to keep those dist folders up-to-date, for example:
|
||||||
|
@ -18,15 +18,24 @@
|
|||||||
"hello_world__render3__closure": {
|
"hello_world__render3__closure": {
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"bundle": 7674
|
"bundle": 8153
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hello_world__render3__rollup": {
|
"hello_world__render3__rollup": {
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"bundle": 58662
|
"bundle": 34694
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hello_world__render3__cli": {
|
||||||
|
"master": {
|
||||||
|
"uncompressed": {
|
||||||
|
"inline": 1447,
|
||||||
|
"main": 40513,
|
||||||
|
"polyfills": 61085
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,7 +5,7 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
|
|||||||
git_repository(
|
git_repository(
|
||||||
name = "build_bazel_rules_nodejs",
|
name = "build_bazel_rules_nodejs",
|
||||||
remote = "https://github.com/bazelbuild/rules_nodejs.git",
|
remote = "https://github.com/bazelbuild/rules_nodejs.git",
|
||||||
tag = "0.3.1",
|
commit = "230d39a391226f51c03448f91eb61370e2e58c42",
|
||||||
)
|
)
|
||||||
|
|
||||||
load("@build_bazel_rules_nodejs//:defs.bzl", "node_repositories")
|
load("@build_bazel_rules_nodejs//:defs.bzl", "node_repositories")
|
||||||
@ -14,7 +14,7 @@ node_repositories(package_json = ["//:package.json"])
|
|||||||
git_repository(
|
git_repository(
|
||||||
name = "build_bazel_rules_typescript",
|
name = "build_bazel_rules_typescript",
|
||||||
remote = "https://github.com/bazelbuild/rules_typescript.git",
|
remote = "https://github.com/bazelbuild/rules_typescript.git",
|
||||||
commit = "c4ea003acd7d42269b81a2d25eb832972cd24912"
|
commit = "eb3244363e1cb265c84e723b347926f28c29aa35"
|
||||||
)
|
)
|
||||||
|
|
||||||
load("@build_bazel_rules_typescript//:defs.bzl", "ts_setup_workspace")
|
load("@build_bazel_rules_typescript//:defs.bzl", "ts_setup_workspace")
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "ngc -p angular.tsconfig.json",
|
"postinstall": "ngc -p angular.tsconfig.json",
|
||||||
"test": "WORKAROUND https://github.com/bazelbuild/bazel/issues/4242, can't build ...",
|
"test": "bazel build //... --noshow_progress"
|
||||||
"test": "bazel build //src/... --noshow_progress"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user