Compare commits

...

46 Commits
5.1.1 ... 5.1.3

Author SHA1 Message Date
d138b38bdb docs: add changelog for 5.1.3 2018-01-03 15:54:57 -08:00
4f409954f8 release: cut the 5.1.3 release 2018-01-03 15:53:22 -08:00
00308fcc9f build: update rules_typescript in integration test 2018-01-03 14:12:42 -08:00
a7a7bd3ca0 build: update rules_typescript (#21290)
picks up a bugfix to prevent floating versions on install:
https://github.com/bazelbuild/rules_typescript/compare/0.6.1...0.6.2

PR Close #21290
2018-01-03 13:43:56 -08:00
67630f82b6 docs(aio): fix trailing space breaking lint 2018-01-02 14:12:47 -08:00
8006be8d8c docs(aio): fix trailing space breaking lint 2018-01-02 14:02:43 -08:00
48a1f32222 fix(language-service): ignore null metadatas (#20557)
There can be null metadatas in certain cases, for example with locales.

Fixes #20260

PR Close #20557
2018-01-02 12:49:27 -08:00
521f0d4c7d docs(aio): HttpClientXsrfModule withConfig => withOptions (#21185)
Docummentation suggests use of HttpClientXsrfModule#withConfig but this method looks like it's renamed to #withConfig.
https://angular.io/guide/http#configuring-custom-cookieheader-names
PR Close #21185
2018-01-02 12:49:13 -08:00
941cd102b8 build: force upstream fetch before merge (#21192)
PR Close #21192
2018-01-02 12:48:55 -08:00
66043e09a2 docs(aio): Sort in the api type dropdown (#21030) (#21176)
Change the order of elements in the api type dropdown to be alphabetical order

PR Close #21030

PR Close #21176
2017-12-27 13:17:10 -08:00
12702b20c7 docs(changelog): fix typo in 5.1.1 (#21007)
PR Close #21007
2017-12-22 21:39:23 -08:00
5de91fe93e docs(forms): add text about min() and max() as functions (#21110)
PR Close #21110
2017-12-22 21:36:48 -08:00
d787b55b31 build(common): generate ts declarations for i18n locale files (#21127)
Fixes #21120
PR Close #21127
2017-12-22 21:35:11 -08:00
3e34fa8651 fix(animations): avoid infinite loop with multiple blocked sub triggers (#21119)
This patch fixes animations so that if multiple sub @triggers are used
and are blocked by a parent animation then the engine will not lead
itself into an infinite loop.

PR Close #21119
2017-12-22 09:23:37 -08:00
8c991756fa fix(router): fix wildcard route with lazy loaded module (again) (#18139)
Closes #13848

Description:
We doesn't handle children of wildcard route properly link. It's always an empty array.

Created from #13851

PR Close #18139
2017-12-22 09:20:21 -08:00
fa0e8ef92c fix(common): handle JS floating point errors in percent pipe (#20329)
Fixes #20136
PR Close #20329
2017-12-22 09:03:19 -08:00
41abcc34f4 docs(service-worker): fix word wrap (#21114)
The fix removes space between 'c' and 'aches' in docs

PR Close #21114
2017-12-21 20:12:27 -08:00
0aa6341e31 build: make umd.min.js source map paths relative (#21147)
I'm not quite sure how to test this since we don't have any infrastructure for these kinds of tests.
I did verify the fix manually though.

Fixes #15740

PR Close #21147
2017-12-21 20:11:27 -08:00
43377684d9 build: fix pullapprove (#21140)
Currently it gives a green status if I edit a file I'm an owner of,
even without anyone else's approval.

PR Close #21140
2017-12-21 14:04:30 -08:00
dc53248b15 build: update pullapprove:
- remove ex-team members
- allow author to approve their own change
- move more bazel files under the bazel group
2017-12-21 13:20:16 -08:00
d1f45002d3 fix(animations): renaming issue with DOMAnimation. (#21125)
Closure Compiler renames all properties that are "internal" to the
program. `DOMAnimation` however is external, it is a browser API, so its
fields must not be renamed.

This change marks `DOMAnimation` as external using `declare interface`,
which will cause Closure Compiler to back off and prevent renaming of
any of its fields.

PR Close #21125
2017-12-21 09:45:25 -08:00
6353b77f89 docs: add changelog for 5.1.2 2017-12-20 12:50:50 -08:00
d9c40b7dd5 release: cut the 5.1.2 release 2017-12-20 12:49:12 -08:00
a36dfd453b ci(router): update the public API guard for the router
This fixes a badly applied revert earlier.
2017-12-20 12:03:51 -08:00
ced575fd10 fix(compiler): report an error for recursive module references
Modules that directly or indirectly export or imported themselves
reports an error instead of generating a stack fault.

Fixes: #19979
2017-12-20 10:54:58 -08:00
3b63e168e2 fix(compiler-cli): do not force type checking on .js files
The compiler host would force any file that is in node_modules
into the list of files that needed to be type checked which
captures .js files if `allowJs` is set to `true`. This should
have only forced .d.ts files into the project to enable
generation of factories.

Fixes: #19757
2017-12-20 10:01:30 -08:00
a1d4c2dbf1 fix(compiler-cli): do not emit invalid .metadata.json files
If no metadata is collected the `ngc` would generate file
that contained `[null]` instead of eliding the `.metadata.json`
file.

Fixes: #20479
2017-12-20 09:59:06 -08:00
a33182c4ee fix(service-worker): check for updates on navigation
Currently the Service Worker checks for updates only on SW startup,
an event which happens frequently but also nondeterministically. This
makes it hard for developers to observe the update process or reason
about how updates will be delivered to users. This problem is
exacerbated by the DevTools behavior of keeping the SW alive
indefinitely while opened, effectively preventing the page from
updating at all.

This change causes the SW to additionally check for updates on
navigation requests (app page reloads). This creates deterministic
update behavior, and is much easier for developers to reason about.
It does leave the old update-on-SW-startup behavior in place, as
removing that would be a breaking change.

Fixes #20877
2017-12-20 08:37:20 -08:00
66cc2fabf1 fix(upgrade): replaces get/setAngularLib with get/setAngularJSGlobal
The current names are confusing because Angular should refer to the latest version of the framework.
2017-12-20 08:37:02 -08:00
cf8269ee94 docs(aio): Rename service worker files, update examples, move service worker under Techniques 2017-12-19 11:08:25 -08:00
c7241ca0e6 docs(aio): fix inconsistency in lifecycle hooks table 2017-12-19 10:58:43 -08:00
4a49e19bd7 ci: add router/testing to public API guard 2017-12-19 10:58:43 -08:00
4a3a74b7b0 fix(aio): improve transitions between pages
- Avoid unnecessary animations, style transitions, repositioning on
  initial rendering.
- Better handle transitioning from/to Home page (which is the only page
  with transparent top-menu).
- Better coordinate sidenav and hamburger animations with page
  transitions.
- Improve fade-in/out animations.

Fixes #20996
2017-12-19 10:58:43 -08:00
6aa013cb63 refactor(aio): clean up top-menu CSS
- Clean-up and re-organize top-menu styles.
- Clean-up and merge hamburger styles into top-menu styles.
2017-12-19 10:58:43 -08:00
b9531f90b8 feat(aio): support disabling DocViewer animations via class 2017-12-19 10:58:43 -08:00
f239e8496e build(aio): make zipper work correctly with CLI projects 2017-12-19 10:58:43 -08:00
57bed3fe73 docs(core): move core examples into examples/core/ directory
This allows examples to be found during aio's `yarn serve-and-sync`, which only
looks for examples in `packages/examples/<packageName>/**/*`, where
`packageName` is the name of the package that the modified file belonged to;
e.g. `core`, `common`, etc.).
2017-12-18 12:11:07 -08:00
c011ffae30 test(aio): correct usage of fakeAsync and inject together in test
Corrects a test which wrapped the fakeAsync call in an inject call.  This caused
the test to not actually run.
2017-12-15 07:55:23 -08:00
756dd34519 fix(compiler): make tsx file aot compatible
fixes #20555
2017-12-15 07:54:02 -08:00
267ebf323e fix(common): fix a Closure compilation issue.
Closure Compiler cannot infer that the swtich statement is exhaustive,
which causes it to complain that the method does not always return a
value.

Work around the problem by throwing an exception in the default case,
and using the `: never` type to ensure the code is unreachable.
2017-12-15 07:52:09 -08:00
49c45f336e ci(aio): move e2e tests to non-optional job
This essentially reverts #20178, since the flakes should be gone after
pinning ChromeDriver and Chrome versions to 2.32 and 59 respectively.
2017-12-14 08:56:22 -08:00
71236737ab ci: downgrade Chromium to a version that does not cause flakes
There seems to be some issue that causes Chrome/ChromeDriver to
unexpectedly reload during the aio e2e tests, causing flakes. It is not
clear what exactly is causing the reloading, but to the best of my
knowledge it is something inside Chrome or ChromeDriver.

Pinning Chrome to r494239 (between 62.0.3185.0 and 62.0.3186.0) fixes
the flakes.

Fixes #20159
2017-12-14 08:56:22 -08:00
390837f76e test(aio): disable DocViewer animations during e2e tests 2017-12-14 08:56:22 -08:00
fbcf7dc889 test(aio): fix and clean up e2e tests 2017-12-14 08:56:22 -08:00
5c8c7d8515 build(common): don't generate .d.ts & .metadata.json files for i18n locales
Fixes #20880
2017-12-14 08:55:50 -08:00
99cc591f63 ci: parallelize bazel build and test
The current setup can cause a de-optimization where unit tests can't start running until the slowest build target completes.
2017-12-14 08:55:42 -08:00
101 changed files with 1449 additions and 712 deletions

View File

@ -52,10 +52,14 @@ jobs:
<<: *post_checkout
- restore_cache:
key: *cache_key
- run: bazel info release
- run: bazel run @yarn//:yarn
- run: bazel build --config=ci packages/...
- run: bazel test --config=ci packages/... @angular//...
# Use bazel query so that we explicitly ask for all buildable targets to be built as well
# This avoids waiting for a build command to finish before running the first test
# See https://github.com/bazelbuild/bazel/issues/4257
- run: bazel query --output=label '//packages/... union @angular//...' | xargs bazel test --config=ci
- save_cache:
key: *cache_key
paths:

View File

@ -15,7 +15,6 @@
# hansl - Hans Larsen
# IgorMinar - Igor Minar
# jasonaden - Jason Aden
# juleskremer - Jules Kremer
# kara - Kara Erickson
# matsko - Matias Niemelä
# mhevery - Misko Hevery
@ -36,14 +35,32 @@ group_defaults:
enabled: true
approve_by_comment:
enabled: false
# see http://docs.pullapprove.com/groups/author_approval/
author_approval:
# If the author is a reviewer on the PR, they will automatically have an "approved" status.
auto: true
groups:
# Require all PRs to have at least one approval from *someone*
all:
users: all
required: 1
# In this group, your self-approval does not count
author_approval:
auto: false
ignored: true
files:
include:
- "*"
root:
conditions:
files:
include:
- "*"
exclude:
- "WORKSPACE"
- "BUILD.bazel"
- ".circleci/*"
- "aio/*"
- "integration/*"
@ -71,6 +88,7 @@ groups:
- "*.bazel"
- "*.bzl"
- "packages/bazel/*"
- "tools/bazel.rc"
users:
- alexeagle #primary
- chuckjaz
@ -86,6 +104,7 @@ groups:
- "*.lock"
- "tools/*"
exclude:
- "tools/bazel.rc"
- "tools/public_api_guard/*"
- "aio/*"
users:
@ -281,7 +300,7 @@ groups:
files:
- "packages/benchpress/*"
users:
# needs primary
- alxhub # primary
# needs secondary
- IgorMinar #fallback
- mhevery #fallback
@ -309,10 +328,8 @@ groups:
- "aio/content/navigation.json"
- "aio/content/license.md"
users:
- juleskremer #primary
- Foxandxss
- stephenfluin
- wardbell
- Foxandxss
- petebacondarwin
- gkalpak
- IgorMinar #fallback
@ -326,7 +343,6 @@ groups:
- "aio/content/navigation.json"
- "aio/content/license.md"
users:
- juleskremer #primary
- stephenfluin
- petebacondarwin
- gkalpak

View File

@ -54,7 +54,6 @@ env:
- CI_MODE=browserstack_optional
- CI_MODE=aio_tools_test
- CI_MODE=aio
- CI_MODE=aio_optional
- CI_MODE=aio_e2e AIO_SHARD=0
- CI_MODE=aio_e2e AIO_SHARD=1
- CI_MODE=bazel
@ -64,7 +63,6 @@ matrix:
allow_failures:
- env: "CI_MODE=saucelabs_optional"
- env: "CI_MODE=browserstack_optional"
- env: "CI_MODE=aio_optional"
before_install:
# source the env.sh script so that the exported variables are available to other scripts later on

View File

@ -1,3 +1,33 @@
<a name="5.1.3"></a>
## [5.1.3](https://github.com/angular/angular/compare/5.1.2...5.1.3) (2018-01-03)
### Bug Fixes
* **animations:** avoid infinite loop with multiple blocked sub triggers ([#21119](https://github.com/angular/angular/issues/21119)) ([3e34fa8](https://github.com/angular/angular/commit/3e34fa8))
* **animations:** renaming issue with DOMAnimation. ([#21125](https://github.com/angular/angular/issues/21125)) ([d1f4500](https://github.com/angular/angular/commit/d1f4500))
* **common:** handle JS floating point errors in percent pipe ([#20329](https://github.com/angular/angular/issues/20329)) ([fa0e8ef](https://github.com/angular/angular/commit/fa0e8ef)), closes [#20136](https://github.com/angular/angular/issues/20136)
* **language-service:** ignore null metadatas ([#20557](https://github.com/angular/angular/issues/20557)) ([48a1f32](https://github.com/angular/angular/commit/48a1f32)), closes [#20260](https://github.com/angular/angular/issues/20260)
* **router:** fix wildcard route with lazy loaded module (again) ([#18139](https://github.com/angular/angular/issues/18139)) ([8c99175](https://github.com/angular/angular/commit/8c99175)), closes [#13848](https://github.com/angular/angular/issues/13848)
<a name="5.1.2"></a>
## [5.1.2](https://github.com/angular/angular/compare/5.1.1...5.1.2) (2017-12-20)
### Bug Fixes
* **common:** fix a Closure compilation issue. ([267ebf3](https://github.com/angular/angular/commit/267ebf3))
* **compiler:** make tsx file aot compatible ([756dd34](https://github.com/angular/angular/commit/756dd34)), closes [#20555](https://github.com/angular/angular/issues/20555)
* **compiler:** report an error for recursive module references ([ced575f](https://github.com/angular/angular/commit/ced575f))
* **compiler-cli:** do not emit invalid .metadata.json files ([a1d4c2d](https://github.com/angular/angular/commit/a1d4c2d))
* **compiler-cli:** do not force type checking on .js files ([3b63e16](https://github.com/angular/angular/commit/3b63e16))
* **service-worker:** check for updates on navigation ([a33182c](https://github.com/angular/angular/commit/a33182c)), closes [#20877](https://github.com/angular/angular/issues/20877)
* **upgrade:** replaces get/setAngularLib with get/setAngularJSGlobal ([66cc2fa](https://github.com/angular/angular/commit/66cc2fa))
<a name="5.1.1"></a>
## [5.1.1](https://github.com/angular/angular/compare/5.1.0...5.1.1) (2017-12-13)
@ -13,7 +43,7 @@
* **compiler-cli:** disable checkTypes in emit. ([#20828](https://github.com/angular/angular/issues/20828)) ([160a154](https://github.com/angular/angular/commit/160a154))
* **compiler-cli:** fix swallowed Error messages ([#20846](https://github.com/angular/angular/issues/20846)) ([6727336](https://github.com/angular/angular/commit/6727336))
* **compiler-cli:** merge [@fileoverview](https://github.com/fileoverview) comments. ([#20870](https://github.com/angular/angular/issues/20870)) ([be9a737](https://github.com/angular/angular/commit/be9a737))
* **router:** NavigatonError and NavigationCancel should be emitted after resetting the URL ([#20803](https://github.com/angular/angular/issues/20803)) ([baeec4d](https://github.com/angular/angular/commit/baeec4d))
* **router:** NavigationError and NavigationCancel should be emitted after resetting the URL ([#20803](https://github.com/angular/angular/issues/20803)) ([baeec4d](https://github.com/angular/angular/commit/baeec4d))

View File

@ -16,7 +16,7 @@ node_repositories(package_json = ["//:package.json"])
git_repository(
name = "build_bazel_rules_typescript",
remote = "https://github.com/bazelbuild/rules_typescript.git",
tag = "0.6.0",
tag = "0.6.2",
)
load("@build_bazel_rules_typescript//:defs.bzl", "ts_repositories")

View File

@ -5,7 +5,7 @@
"!**/*.d.ts",
"!**/*.js",
"!**/*.[0,1,2].*",
"**/dummy.module.ts"
"!**/dummy.module.ts"
],
"tags": ["dependency", "di"]
}

View File

@ -5,17 +5,6 @@
<title>NgModule Minimal</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfills -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('main.0.js').catch(function(err){ console.error(err); });
</script>
</head>
<body>

View File

@ -5,17 +5,6 @@
<title>NgModule Minimal</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfills -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('main.1.js').catch(function(err){ console.error(err); });
</script>
</head>
<body>

View File

@ -5,17 +5,6 @@
<title>NgModule Minimal</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfills -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('main.1b.js').catch(function(err){ console.error(err); });
</script>
</head>
<body>

View File

@ -5,17 +5,6 @@
<title>NgModule Minimal</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfills -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('main.2.js').catch(function(err){ console.error(err); });
</script>
</head>
<body>

View File

@ -5,17 +5,6 @@
<title>NgModule Minimal</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfills -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('main.3.js').catch(function(err){ console.error(err); });
</script>
</head>
<body>

View File

@ -5,13 +5,17 @@
"styles.css",
"app/app.component.ts",
"app/app.component.html",
"app/app.component.css",
"app/app.module.ts",
"app/data-model.ts",
"app/hero.service.ts",
"app/hero-detail.component.html",
"app/hero-detail.component.ts",
"app/hero-list.component.html",
"app/hero-list.component.ts",
"app/hero-detail/hero-detail.component.html",
"app/hero-detail/hero-detail.component.ts",
"app/hero-detail/hero-detail.component.css",
"app/hero-list/hero-list.component.html",
"app/hero-list/hero-list.component.ts",
"app/hero-list/hero-list.component.css",
"main-final.ts",
"index-final.html"

View File

@ -6,18 +6,6 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="styles.css">
<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/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('main-final.js').catch(function(err){ console.error(err); });
</script>
</head>
<body>

View File

@ -1,10 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'Service Workers';
}

View File

@ -1,9 +1,9 @@
import { AppPage } from './app.po';
import { browser, element, by } from 'protractor';
describe('sw-example App', () => {
let page: AppPage;
let logo = element(by.css('img'));
beforeEach(() => {
page = new AppPage();
@ -15,17 +15,18 @@ describe('sw-example App', () => {
});
it('should display the Angular logo', () => {
let logo = element(by.css('img'));
page.navigateTo();
expect(logo.isPresent()).toBe(true);
});
it('should show a header for the list of links', function () {
it('should show a header for the list of links', () => {
const listHeader = element(by.css('app-root > h2'));
expect(listHeader.getText()).toEqual('Here are some links to help you start:');
});
it('should show a list of links', function () {
element.all(by.css('ul > li > h2 > a')).then(function(items) {
element.all(by.css('ul > li > h2 > a')).then((items) => {
expect(items.length).toBe(4);
expect(items[0].getText()).toBe('Angular Service Worker Intro');
expect(items[1].getText()).toBe('Tour of Heroes');
@ -33,5 +34,11 @@ describe('sw-example App', () => {
expect(items[3].getText()).toBe('Angular blog');
});
});
// Check for a rejected promise as the service worker is not enabled
it('SwUpdate.checkForUpdate() should return a rejected promise', () => {
const button = element(by.css('button'));
const rejectMessage = element(by.css('p'));
button.click();
expect(rejectMessage.getText()).toContain('rejected: ');
});
});

View File

@ -0,0 +1,50 @@
{
"name": "angular.io-example",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "^5.0.0",
"@angular/common": "^5.0.0",
"@angular/compiler": "^5.0.0",
"@angular/core": "^5.0.0",
"@angular/forms": "^5.0.0",
"@angular/http": "^5.0.0",
"@angular/service-worker": "^5.0.0",
"@angular/platform-browser": "^5.0.0",
"@angular/platform-browser-dynamic": "^5.0.0",
"@angular/router": "^5.0.0",
"core-js": "^2.4.1",
"rxjs": "^5.5.2",
"zone.js": "^0.8.14"
},
"devDependencies": {
"@angular/cli": "1.5.4",
"@angular/compiler-cli": "^5.0.0",
"@angular/language-service": "^5.0.0",
"@types/jasmine": "~2.5.53",
"@types/jasminewd2": "~2.0.2",
"@types/node": "~6.0.60",
"codelyzer": "^4.0.1",
"jasmine-core": "~2.6.2",
"jasmine-spec-reporter": "~4.1.0",
"karma": "~1.7.0",
"karma-chrome-launcher": "~2.1.1",
"karma-cli": "~1.0.1",
"karma-coverage-istanbul-reporter": "^1.2.1",
"karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.1.2",
"ts-node": "~3.2.0",
"tslint": "~5.7.0",
"typescript": "~2.4.2"
}
}

View File

@ -0,0 +1,5 @@
{
"description": "Service Worker",
"basePath": "src/",
"tags": ["service worker"]
}

View File

@ -5,6 +5,10 @@
</h1>
<img width="300" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
</div>
<button id="check" (click)="updateCheck()">Check for Update</button>
<p id="checkResult">{{updateCheckText}}</p>
<h2>Here are some links to help you start: </h2>
<ul>
<li>

View File

@ -16,12 +16,12 @@ describe('AppComponent', () => {
it(`should have as title 'app'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app');
expect(app.title).toEqual('Service Workers');
}));
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
expect(compiled.querySelector('h1').textContent).toContain('Welcome to Service Workers!');
}));
});

View File

@ -0,0 +1,20 @@
import { Component } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
title = 'Service Workers';
updateCheckText = '';
constructor(private update: SwUpdate) {}
updateCheck(): void {
this.update
.checkForUpdate()
.then(() => this.updateCheckText = 'resolved')
.catch(err => this.updateCheckText = `rejected: ${err.message}`);
}
}

View File

@ -1,19 +1,15 @@
import { Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/interval';
function promptUser(event): boolean {
return true;
}
// #docregion sw-check-update
import { interval } from 'rxjs/observable/interval';
@Injectable()
export class CheckForUpdateService {
constructor(updates: SwUpdate) {
Observable.interval(6 * 60 * 60).subscribe(() => updates.checkForUpdate());
interval(6 * 60 * 60).subscribe(() => updates.checkForUpdate());
}
}
// #enddocregion sw-check-update

View File

@ -5,8 +5,6 @@
<title>SwExample</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>

View File

@ -547,12 +547,12 @@ In order to prevent collisions in environments where multiple Angular apps share
### Configuring custom cookie/header names
If your backend service uses different names for the XSRF token cookie or header, use `HttpClientXsrfModule.withConfig()` to override the defaults.
If your backend service uses different names for the XSRF token cookie or header, use `HttpClientXsrfModule.withOptions()` to override the defaults.
```javascript
imports: [
HttpClientModule,
HttpClientXsrfModule.withConfig({
HttpClientXsrfModule.withOptions({
cookieName: 'My-Xsrf-Cookie',
headerName: 'My-Xsrf-Header',
}),

View File

@ -140,7 +140,7 @@ calls the lifecycle hook methods in the following sequence at specific moments:
</tr>
<tr style='vertical-align:top'>
<td>
<code>ngOnDestroy</code>
<code>ngOnDestroy()</code>
</td>
<td>

View File

@ -1,7 +1,15 @@
# Communicating with service workers
# Service Worker Communication
Importing `ServiceWorkerModule` into your `AppModule` doesn't just register the service worker, it also provides a few services you can use to interact with the service worker and control the caching of your app.
#### Prerequisites
A basic understanding of the following:
* [Getting Started with Service Workers](guide/service-worker-getting-started).
<hr />
## `SwUpdate` service
The `SwUpdate` service gives you access to events that indicate when the service worker has discovered an available update for your app or when it has activated such an update&mdash;meaning it is now serving content from that update to your app.
@ -16,7 +24,7 @@ The `SwUpdate` service supports four separate operations:
The two update events, `available` and `activated`, are `Observable` properties of `SwUpdate`:
<code-example path="service-worker-getstart/src/app/log-update.service.ts" linenums="false" title="log-update.service.ts" region="sw-update"> </code-example>
<code-example path="service-worker-getting-started/src/app/log-update.service.ts" linenums="false" title="log-update.service.ts" region="sw-update"> </code-example>
You can use these events to notify the user of a pending update or to refresh their pages when the code they are running is out of date.
@ -27,7 +35,7 @@ It's possible to ask the service worker to check if any updates have been deploy
Do this with the `checkForUpdate()` method:
<code-example path="service-worker-getstart/src/app/check-for-update.service.ts" linenums="false" title="check-for-update.service.ts" region="sw-check-update"> </code-example>
<code-example path="service-worker-getting-started/src/app/check-for-update.service.ts" linenums="false" title="check-for-update.service.ts" region="sw-check-update"> </code-example>
This method returns a `Promise` which indicates that the update check has completed successfully, though it does not indicate whether an update was discovered as a result of the check. Even if one is found, the service worker must still successfully download the changed files, which can fail. If successful, the `available` event will indicate availability of a new version of the app.
@ -36,6 +44,11 @@ This method returns a `Promise` which indicates that the update check has comple
If the current tab needs to be updated to the latest app version immediately, it can ask to do so with the `activateUpdate()` method:
<code-example path="service-worker-getstart/src/app/prompt-update.service.ts" linenums="false" title="prompt-update.service.ts" region="sw-activate"> </code-example>
<code-example path="service-worker-getting-started/src/app/prompt-update.service.ts" linenums="false" title="prompt-update.service.ts" region="sw-activate"> </code-example>
Doing this could break lazy-loading into currently running apps, especially if the lazy-loaded chunks use filenames with hashes, which change every version.
## More on Angular service workers
You may also be interested in the following:
* [Service Worker in Production](guide/service-worker-devops).

View File

@ -1,6 +1,13 @@
{@a glob}
# Reference: Configuration file
# Service Worker Configuration
#### Prerequisites
A basic understanding of the following:
* [Service Worker in Production](guide/service-worker-devops).
<hr />
The `src/ngsw-config.json` configuration file specifies which files and data URLs the Angular
service worker should cache and how it should update the cached files and data. The
@ -8,7 +15,7 @@ CLI processes the configuration file during `ng build --prod`. Manually, you can
it with the `ngsw-config` tool:
```sh
ngsw-config dist src/ngswn-config.json /base/href
ngsw-config dist src/ngsw-config.json /base/href
```
The configuration file uses the JSON format. All file paths must begin with `/`, which is the deployment directory&mdash;usually `dist` in CLI projects.
@ -159,3 +166,4 @@ The Angular service worker can use either of two caching strategies for data res
* `performance`, the default, optimizes for responses that are as fast as possible. If a resource exists in the cache, the cached version is used. This allows for some staleness, depending on the `maxAge`, in exchange for better performance. This is suitable for resources that don't change often; for example, user avatar images.
* `freshness` optimizes for currency of data, preferentially fetching requested data from the network. Only if the network times out, according to `timeout`, does the request fall back to the cache. This is useful for resources that change frequently; for example, account balances.

View File

@ -1,7 +1,14 @@
# DevOps: Angular service worker in production
# Service Worker in Production
This page is a reference for deploying and supporting production apps that use the Angular service worker. It explains how the Angular service worker fits into the larger production environment, the service worker's behavior under various conditions, and available recourses and fail-safes.
#### Prerequisites
A basic understanding of the following:
* [Service Worker Communication](guide/service-worker-communications).
<hr />
## Service worker and caching of app resources
Conceptually, you can imagine the Angular service worker as a forward cache or a CDN edge that is installed in the end user's web browser. The service worker's job is to satisfy requests made by the Angular app for resources or data from a local cache, without needing to wait for the network. Like any cache, it has rules for how content is expired and updated.
@ -32,12 +39,10 @@ server can ensure that the Angular app always has a consistent set of files.
#### Update checks
Every time the Angular service worker starts, it checks for updates to the
app by looking for updates to the `ngsw.json` manifest.
Note that the service worker starts periodically throughout the usage of
the app because the web browser terminates the service worker if the page
is idle beyond a given timeout.
Every time the user opens or refreshes the application, the Angular service worker
checks for updates to the app by looking for updates to the `ngsw.json` manifest. If
an update is found, it is downloaded and cached automatically, and will be served
the next time the application is loaded.
### Resource integrity
@ -195,8 +200,8 @@ versions are safe to use, so existing tabs continue to run from
cache, but new loads of the app will be served from the network.
* `SAFE_MODE`: the service worker cannot guarantee the safety of
using cached data. Either an unexpected error occurred or all c
ached versions are invalid. All traffic will be served from the
using cached data. Either an unexpected error occurred or all
cached versions are invalid. All traffic will be served from the
network, running as little service worker code as possible.
In both cases, the parenthetical annotation provides the
@ -276,8 +281,8 @@ with service workers. Such tools can be powerful when used properly,
but there are a few things to keep in mind.
* When using developer tools, the service worker is kept running
in the background and never restarts. For the Angular service
worker, this means that update checks to the app will generally not happen.
in the background and never restarts. This can cause behavior with Dev
Tools open to differ from behavior a user might experience.
* If you look in the Cache Storage viewer, the cache is frequently
out of date. Right click the Cache Storage title and refresh the caches.
@ -299,4 +304,8 @@ for `ngsw.json` returns a `404`, then the service worker
removes all of its caches and de-registers itself,
essentially self-destructing.
## More on Angular service workers
You may also be interested in the following:
* [Service Worker Configuration](guide/service-worker-config).

View File

@ -1,10 +1,15 @@
# Getting started
# Getting Started with Service Workers
#### Prerequisites
A basic understanding of the following:
* [Introduction to Angular service workers](guide/service-worker-intro).
<hr />
Beginning in Angular 5.0.0, you can easily enable Angular service worker support in any CLI project. This document explains how to enable Angular service worker support in new and existing projects. It then uses a simple example to show you a service worker in action, demonstrating loading and basic caching.
See the <live-example></live-example>.
## Adding a service worker to a new application
If you're generating a new CLI project, you can use the CLI to set up the Angular service worker as part of creating the project. To do so, add the `--service-worker` flag to the `ng new` command:
@ -54,12 +59,12 @@ To import and register the Angular service worker:
At the top of the root module, `src/app/app.module.ts`, import `ServiceWorkerModule` and `environment`.
<code-example path="service-worker-getstart/src/app/app.module.ts" linenums="false" title="src/app/app.module.ts" region="sw-import"> </code-example>
<code-example path="service-worker-getting-started/src/app/app.module.ts" linenums="false" title="src/app/app.module.ts" region="sw-import"> </code-example>
Add `ServiceWorkerModule` to the `@NgModule` `imports` array. Use the `register()` helper to take care of registering the service worker, taking care to disable the service worker when not running in production mode.
<code-example path="service-worker-getstart/src/app/app.module.ts" linenums="false" title="src/app/app.module.ts" region="sw-module"> </code-example>
<code-example path="service-worker-getting-started/src/app/app.module.ts" linenums="false" title="src/app/app.module.ts" region="sw-module"> </code-example>
The file `ngsw-worker.js` is the name of the prebuilt service worker script, which the CLI copies into `dist/` to deploy along with your server.
@ -72,7 +77,7 @@ You can begin with the boilerplate version from the CLI, which configures sensib
Alternately, save the following as `src/ngsw-config.json`:
<code-example path="service-worker-getstart/src/ngsw-config.json" linenums="false" title="src/ngsw-config.json"> </code-example>
<code-example path="service-worker-getting-started/src/ngsw-config.json" linenums="false" title="src/ngsw-config.json"> </code-example>
### Step 5: Build the project
@ -92,7 +97,9 @@ using an example application.
### Serving with `http-server`
As `ng serve` does not work with service workers, you must use a real HTTP server to test your project locally. It's a good idea to test on a dedicated port.
Because `ng serve` does not work with service workers, you must use a seperate HTTP server to test your project locally. You can use any HTTP server. The example below uses the [http-server](https://www.npmjs.com/package/http-server) package from npm. To reduce the possibility of conflicts, test on a dedicated port.
To serve with `http-server`, change to the directory containing your web files and start the web server:
```sh
cd dist
@ -181,7 +188,6 @@ What went wrong? Nothing, actually. The Angular service worker is doing its job
If you look at the `http-server` logs, you can see the service worker requesting `/ngsw.json`. This is how the service worker checks for updates.
2. Refresh the page.
![The text has changed to say "Bienvenue à app!"](generated/images/guide/service-worker/welcome-msg-fr.png)
<figure>
<img src="generated/images/guide/service-worker/welcome-msg-fr.png" alt="The text has changed to say Bienvenue à app!">
@ -189,3 +195,9 @@ If you look at the `http-server` logs, you can see the service worker requesting
The service worker installed the updated version of your app *in the background*, and the next time the page is loaded or reloaded, the service worker switches to the latest version.
<hr />
## More on Angular service workers
You may also be interested in the following:
* [Communicating with service workers](guide/service-worker-communications).

View File

@ -1,4 +1,4 @@
# Introduction to Angular service workers
# Angular Service Worker Introduction
Service workers augment the traditional web deployment model and empower applications to deliver a user experience with the reliability and performance on par with natively-installed code.
@ -46,3 +46,8 @@ For more information about browser support, see the [browser support](https://de
[Can I Use](http://caniuse.com/#feat=serviceworkers).
The remainder of this Angular documentation specifically addresses the Angular implementation of service workers.
## More on Angular service workers
You may also be interested in the following:
* [Getting Started with service workers](guide/service-worker-getting-started).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -276,38 +276,6 @@
"title": "Routing & Navigation",
"tooltip": "Discover the basics of screen navigation with the Angular Router."
},
{
"title": "Service Workers",
"tooltip": "Angular service workers: Controlling caching of application resources.",
"children": [
{
"url": "guide/service-worker-intro",
"title": "Introduction",
"tooltip": "Angular's implementation of service workers improves user experience with slow or unreliable network connectivity."
},
{
"url": "guide/service-worker-getstart",
"title": "Getting Started",
"tooltip": "Enabling the service worker in a CLI project and observing behavior in the browser."
},
{
"url": "guide/service-worker-comm",
"title": "Communication",
"tooltip": "Services that enable you to interact with an Angular service worker."
},
{
"url": "guide/service-worker-devops",
"title": "Service Workers in Production",
"tooltip": "Information about running applications with service workers, including application update management, debugging, and killing applications."
},
{
"url": "guide/service-worker-configref",
"title": "Reference: Configuration File",
"tooltip": "The ngsw-config.json configuration file controls service worker caching behavior."
}
]
},
{
"url": "guide/testing",
"title": "Testing",
@ -382,6 +350,37 @@
}
]
},
{
"title": "Service Workers",
"tooltip": "Angular service workers: Controlling caching of application resources.",
"children": [
{
"url": "guide/service-worker-intro",
"title": "Introduction",
"tooltip": "Angular's implementation of service workers improves user experience with slow or unreliable network connectivity."
},
{
"url": "guide/service-worker-getting-started",
"title": "Getting Started",
"tooltip": "Enabling the service worker in a CLI project and observing behavior in the browser."
},
{
"url": "guide/service-worker-communications",
"title": "Service Worker Communication",
"tooltip": "Services that enable you to interact with an Angular service worker."
},
{
"url": "guide/service-worker-devops",
"title": "Service Worker in Production",
"tooltip": "Running applications with service workers, managing application update, debugging, and killing applications."
},
{
"url": "guide/service-worker-config",
"title": "Service Worker Configuration",
"tooltip": "Configuring service worker caching behavior."
}
]
},
{
"title": "Upgrading",

View File

@ -7,15 +7,16 @@ describe('site App', function() {
beforeEach(() => {
SitePage.setWindowWidth(1050); // Make the window wide enough to show the SideNav side-by-side.
page = new SitePage();
page.navigateTo();
});
it('should show features text after clicking "Features"', () => {
page.navigateTo('');
page.getTopMenuLink('features').click();
expect(page.getDocViewerText()).toMatch(/Progressive web apps/i);
});
it('should set appropriate window titles', () => {
page.navigateTo('');
expect(browser.getTitle()).toBe('Angular');
page.getTopMenuLink('features').click();
@ -25,9 +26,9 @@ describe('site App', function() {
expect(browser.getTitle()).toBe('Angular');
});
it('should show the tutorial index page at `/tutorial/` after jitterbugging through features', () => {
it('should show the tutorial index page at `/tutorial` after jitterbugging through features', () => {
// check that we can navigate directly to the tutorial page
page.navigateTo('tutorial/');
page.navigateTo('tutorial');
expect(page.getDocViewerText()).toMatch(/Tutorial: Tour of Heroes/i);
// navigate to a different page
@ -52,24 +53,24 @@ describe('site App', function() {
describe('scrolling to the top', () => {
it('should scroll to the top when navigating to another page', () => {
page.navigateTo('guide/security');
browser.sleep(1000); // Wait for initial async scroll-to-top after `onDocRendered`.
page.scrollToBottom();
page.getScrollTop().then(scrollTop => expect(scrollTop).toBeGreaterThan(0));
expect(page.getScrollTop()).toBeGreaterThan(0);
page.navigateTo('api');
page.getScrollTop().then(scrollTop => expect(scrollTop).toBe(0));
page.getNavItem(/api/i).click();
expect(page.locationPath()).toBe('/api');
expect(page.getScrollTop()).toBe(0);
});
it('should scroll to the top when navigating to the same page', () => {
page.navigateTo('guide/security');
browser.sleep(1000); // Wait for initial async scroll-to-top after `onDocRendered`.
page.scrollToBottom();
page.getScrollTop().then(scrollTop => expect(scrollTop).toBeGreaterThan(0));
expect(page.getScrollTop()).toBeGreaterThan(0);
page.navigateTo('guide/security');
page.getScrollTop().then(scrollTop => expect(scrollTop).toBe(0));
page.getNavItem(/security/i).click();
expect(page.locationPath()).toBe('/guide/security');
expect(page.getScrollTop()).toBe(0);
});
});
@ -87,42 +88,50 @@ describe('site App', function() {
page.navigateTo('api');
page.locationPath()
.then(p => path = p)
.then(() => page.ga().then(calls => {
.then(() => page.ga())
.then(calls => {
// The last call (length-1) will be the `send` command
// The second to last call (length-2) will be the command to `set` the page url
expect(calls[calls.length - 2]).toEqual(['set', 'page', path]);
done();
}));
});
});
it('should call ga with new URL on navigation', done => {
let path: string;
page.navigateTo('');
page.getTopMenuLink('features').click();
page.locationPath()
.then(p => path = p)
.then(() => page.ga().then(calls => {
.then(() => page.ga())
.then(calls => {
// The last call (length-1) will be the `send` command
// The second to last call (length-2) will be the command to `set` the page url
expect(calls[calls.length - 2]).toEqual(['set', 'page', path]);
done();
}));
});
});
});
describe('search', () => {
it('should find pages when searching by a partial word in the title', () => {
page.navigateTo('');
page.enterSearch('ngCont');
expect(page.getSearchResults().map(link => link.getText())).toContain('NgControl');
expect(page.getSearchResults()).toContain('NgControl');
page.enterSearch('accessor');
expect(page.getSearchResults().map(link => link.getText())).toContain('ControlValueAccessor');
expect(page.getSearchResults()).toContain('ControlValueAccessor');
});
});
describe('404 page', () => {
it('should search the index for words found in the url', () => {
page.navigateTo('http/router');
expect(page.getSearchResults().map(link => link.getText())).toContain('Http');
expect(page.getSearchResults().map(link => link.getText())).toContain('Router');
const results = page.getSearchResults();
expect(results).toContain('Http');
expect(results).toContain('Router');
});
});
});

View File

@ -28,8 +28,11 @@ export class SitePage {
ga() { return browser.executeScript('return window["ga"].q') as promise.Promise<any[][]>; }
locationPath() { return browser.executeScript('return document.location.pathname') as promise.Promise<string>; }
navigateTo(pageUrl = '') {
return browser.get('/' + pageUrl);
navigateTo(pageUrl) {
// Navigate to the page, disable animations, and wait for Angular.
return browser.get('/' + pageUrl)
.then(() => browser.executeScript('document.body.classList.add(\'no-animations\')'))
.then(() => browser.waitForAngular());
}
getDocViewerText() {
@ -59,6 +62,6 @@ export class SitePage {
getSearchResults() {
const results = element.all(by.css('.search-results li'));
browser.wait(ExpectedConditions.presenceOf(results.first()), 8000);
return results;
return results.map(link => link.getText());
}
}

View File

@ -40,7 +40,12 @@
{"type": 301, "source": "/docs/ts/latest/:any*", "destination": "/:any*"},
// aot-compiler.md and metadata.md combined into aot-compiler.md - issue #19510
{"type": 301, "source": "/guide/metadata", "destination": "/guide/aot-compiler"}
{"type": 301, "source": "/guide/metadata", "destination": "/guide/aot-compiler"},
// service-worker-getstart.md, service-worker-comm.md, service-worker-configref.md
{"type": 301, "source": "/guide/service-worker-getstart", "destination": "/guide/service-worker-getting-started"},
{"type": 301, "source": "/guide/service-worker-comm", "destination": "/guide/service-worker-communications"},
{"type": 301, "source": "/guide/service-worker-configref", "destination": "/guide/service-worker-config"}
],
"rewrites": [
{

View File

@ -4,12 +4,14 @@
<mat-progress-bar mode="indeterminate" color="warn"></mat-progress-bar>
</div>
<mat-toolbar color="primary" class="app-toolbar">
<button class="hamburger" [class.starting]="isStarting" mat-button
(click)="sidenav.toggle()" title="Docs menu">
<mat-icon [ngClass]="{'sidenav-open': !isSideBySide }" svgIcon="menu"></mat-icon>
<mat-toolbar color="primary" class="app-toolbar" [class.transitioning]="isTransitioning">
<button mat-button class="hamburger" (click)="sidenav.toggle()" title="Docs menu">
<mat-icon svgIcon="menu"></mat-icon>
</button>
<a class="nav-link home" href="/"><img src="{{ homeImageUrl }}" title="Home" alt="Home"></a>
<a class="nav-link home" href="/" [ngSwitch]="isSideBySide">
<img *ngSwitchCase="true" src="assets/images/logos/angular/logo-nav@2x.png" width="150" height="40" title="Home" alt="Home">
<img *ngSwitchDefault src="assets/images/logos/angular/shield-large.svg" width="37" height="40" title="Home" alt="Home">
</a>
<aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes"></aio-top-menu>
<aio-search-box class="search-container" #searchBox (onSearch)="doSearch($event)" (onFocus)="doSearch($event)"></aio-search-box>
</mat-toolbar>
@ -17,7 +19,7 @@
<mat-sidenav-container class="sidenav-container" [class.starting]="isStarting" [class.has-floating-toc]="hasFloatingToc" role="main">
<mat-sidenav [ngClass]="{'collapsed': !isSideBySide }" #sidenav class="sidenav" [opened]="isOpened" [mode]="mode" (open)="updateHostClasses()" (close)="updateHostClasses()">
<mat-sidenav [ngClass]="{'collapsed': !isSideBySide}" #sidenav class="sidenav" [opened]="isOpened" [mode]="mode" (open)="updateHostClasses()" (close)="updateHostClasses()">
<aio-nav-menu *ngIf="!isSideBySide" [nodes]="topMenuNarrowNodes" [currentNode]="currentNodes?.TopBarNarrow" [isWide]="false"></aio-nav-menu>
<aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes?.SideNav" [isWide]="isSideBySide"></aio-nav-menu>
@ -28,7 +30,13 @@
<section class="sidenav-content" [id]="pageId" role="content">
<aio-mode-banner [mode]="deployment.mode" [version]="versionInfo"></aio-mode-banner>
<aio-doc-viewer [doc]="currentDocument" (docReady)="onDocReady()" (docRemoved)="onDocRemoved()" (docInserted)="onDocInserted()"></aio-doc-viewer>
<aio-doc-viewer [class.no-animations]="isStarting"
[doc]="currentDocument"
(docReady)="onDocReady()"
(docRemoved)="onDocRemoved()"
(docInserted)="onDocInserted()"
(docRendered)="onDocRendered()">
</aio-doc-viewer>
<aio-dt [on]="dtOn" [(doc)]="currentDocument"></aio-dt>
</section>

View File

@ -154,34 +154,35 @@ describe('AppComponent', () => {
});
describe('SideNav when side-by-side (wide)', () => {
const navigateTo = (path: string) => {
locationService.go(path);
component.updateSideNav();
fixture.detectChanges();
};
beforeEach(() => {
component.onResize(sideBySideBreakPoint + 1); // side-by-side
});
it('should open when nav to a guide page (guide/pipes)', () => {
locationService.go('guide/pipes');
fixture.detectChanges();
navigateTo('guide/pipes');
expect(sidenav.opened).toBe(true);
});
it('should open when nav to an api page', () => {
locationService.go('api/a/b/c/d');
fixture.detectChanges();
navigateTo('api/a/b/c/d');
expect(sidenav.opened).toBe(true);
});
it('should be closed when nav to a marketing page (features)', () => {
locationService.go('features');
fixture.detectChanges();
navigateTo('features');
expect(sidenav.opened).toBe(false);
});
describe('when manually closed', () => {
beforeEach(() => {
locationService.go('guide/pipes');
fixture.detectChanges();
navigateTo('guide/pipes');
hamburger.click();
fixture.detectChanges();
});
@ -191,56 +192,53 @@ describe('AppComponent', () => {
});
it('should stay closed when nav from one guide page to another', () => {
locationService.go('guide/bags');
fixture.detectChanges();
navigateTo('guide/bags');
expect(sidenav.opened).toBe(false);
});
it('should stay closed when nav from a guide page to api page', () => {
locationService.go('api');
fixture.detectChanges();
navigateTo('api');
expect(sidenav.opened).toBe(false);
});
it('should reopen when nav to market page and back to guide page', () => {
locationService.go('features');
fixture.detectChanges();
locationService.go('guide/bags');
fixture.detectChanges();
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();
};
beforeEach(() => {
component.onResize(sideBySideBreakPoint - 1); // NOT side-by-side
});
it('should be closed when nav to a guide page (guide/pipes)', () => {
locationService.go('guide/pipes');
fixture.detectChanges();
navigateTo('guide/pipes');
expect(sidenav.opened).toBe(false);
});
it('should be closed when nav to an api page', () => {
locationService.go('api/a/b/c/d');
fixture.detectChanges();
navigateTo('api/a/b/c/d');
expect(sidenav.opened).toBe(false);
});
it('should be closed when nav to a marketing page (features)', () => {
locationService.go('features');
fixture.detectChanges();
navigateTo('features');
expect(sidenav.opened).toBe(false);
});
describe('when manually opened', () => {
beforeEach(() => {
locationService.go('guide/pipes');
fixture.detectChanges();
navigateTo('guide/pipes');
hamburger.click();
fixture.detectChanges();
});
@ -257,20 +255,17 @@ describe('AppComponent', () => {
});
it('should close when nav to another guide page', () => {
locationService.go('guide/bags');
fixture.detectChanges();
navigateTo('guide/bags');
expect(sidenav.opened).toBe(false);
});
it('should close when nav to api page', () => {
locationService.go('api');
fixture.detectChanges();
navigateTo('api');
expect(sidenav.opened).toBe(false);
});
it('should close again when nav to market page', () => {
locationService.go('features');
fixture.detectChanges();
navigateTo('features');
expect(sidenav.opened).toBe(false);
});
@ -325,101 +320,6 @@ describe('AppComponent', () => {
});
});
describe('pageId', () => {
it('should set the id of the doc viewer container based on the current doc', () => {
const container = fixture.debugElement.query(By.css('section.sidenav-content'));
locationService.go('guide/pipes');
fixture.detectChanges();
expect(component.pageId).toEqual('guide-pipes');
expect(container.properties['id']).toEqual('guide-pipes');
locationService.go('news');
fixture.detectChanges();
expect(component.pageId).toEqual('news');
expect(container.properties['id']).toEqual('news');
locationService.go('');
fixture.detectChanges();
expect(component.pageId).toEqual('home');
expect(container.properties['id']).toEqual('home');
});
it('should not be affected by changes to the query', () => {
const container = fixture.debugElement.query(By.css('section.sidenav-content'));
locationService.go('guide/pipes');
fixture.detectChanges();
locationService.go('guide/other?search=http');
fixture.detectChanges();
expect(component.pageId).toEqual('guide-other');
expect(container.properties['id']).toEqual('guide-other');
});
});
describe('hostClasses', () => {
it('should set the css classes of the host container based on the current doc and navigation view', () => {
locationService.go('guide/pipes');
fixture.detectChanges();
checkHostClass('page', 'guide-pipes');
checkHostClass('folder', 'guide');
checkHostClass('view', 'SideNav');
locationService.go('features');
fixture.detectChanges();
checkHostClass('page', 'features');
checkHostClass('folder', 'features');
checkHostClass('view', 'TopBar');
locationService.go('');
fixture.detectChanges();
checkHostClass('page', 'home');
checkHostClass('folder', 'home');
checkHostClass('view', '');
});
it('should set the css class of the host container based on the open/closed state of the side nav', async () => {
locationService.go('guide/pipes');
fixture.detectChanges();
checkHostClass('sidenav', 'open');
sidenav.close();
await waitForEmit(sidenav.onClose);
fixture.detectChanges();
checkHostClass('sidenav', 'closed');
sidenav.open();
await waitForEmit(sidenav.onOpen);
fixture.detectChanges();
checkHostClass('sidenav', 'open');
function waitForEmit(emitter: Observable<void>): Promise<void> {
return new Promise(resolve => {
emitter.subscribe(resolve);
fixture.detectChanges();
});
}
});
it('should set the css class of the host container based on the initial deployment mode', () => {
createTestingModule('a/b', 'archive');
initializeTest();
checkHostClass('mode', 'archive');
});
function checkHostClass(type, value) {
const host = fixture.debugElement;
const classes = host.properties['className'];
const classArray = classes.split(' ').filter(c => c.indexOf(`${type}-`) === 0);
expect(classArray.length).toBeLessThanOrEqual(1, `"${classes}" should have only one class matching ${type}-*`);
expect(classArray).toEqual([`${type}-${value}`], `"${classes}" should contain ${type}-${value}`);
}
});
describe('currentDocument', () => {
it('should display a guide page (guide/pipes)', () => {
locationService.go('guide/pipes');
@ -895,7 +795,9 @@ describe('AppComponent', () => {
describe('with mocked DocViewer', () => {
const getDocViewer = () => fixture.debugElement.query(By.css('aio-doc-viewer'));
const triggerDocReady = () => getDocViewer().triggerEventHandler('docReady', undefined);
const triggerDocViewerEvent =
(evt: 'docReady' | 'docRemoved' | 'docInserted' | 'docRendered') =>
getDocViewer().triggerEventHandler(evt, undefined);
beforeEach(() => {
createTestingModule('a/b');
@ -907,7 +809,7 @@ describe('AppComponent', () => {
});
describe('initial rendering', () => {
it('should initially add the starting class until the first document is ready', fakeAsync(() => {
it('should initially add the starting class until a document is rendered', () => {
const getSidenavContainer = () => fixture.debugElement.query(By.css('mat-sidenav-container'));
initializeTest();
@ -915,21 +817,181 @@ describe('AppComponent', () => {
expect(component.isStarting).toBe(true);
expect(getSidenavContainer().classes['starting']).toBe(true);
triggerDocReady();
fixture.detectChanges();
expect(component.isStarting).toBe(true);
expect(getSidenavContainer().classes['starting']).toBe(true);
tick(499);
fixture.detectChanges();
expect(component.isStarting).toBe(true);
expect(getSidenavContainer().classes['starting']).toBe(true);
tick(2);
triggerDocViewerEvent('docRendered');
fixture.detectChanges();
expect(component.isStarting).toBe(false);
expect(getSidenavContainer().classes['starting']).toBe(false);
}));
});
it('should initially disable animations on the DocViewer for the first rendering', () => {
initializeTest();
expect(component.isStarting).toBe(true);
expect(docViewer.classList.contains('no-animations')).toBe(true);
triggerDocViewerEvent('docRendered');
fixture.detectChanges();
expect(component.isStarting).toBe(false);
expect(docViewer.classList.contains('no-animations')).toBe(false);
});
});
describe('subsequent rendering', () => {
beforeEach(jasmine.clock().install);
afterEach(jasmine.clock().uninstall);
it('should set the transitioning class on `.app-toolbar` while a document is being rendered', () => {
const getToolbar = () => fixture.debugElement.query(By.css('.app-toolbar'));
initializeTest();
// Initially, `isTransitoning` is true.
expect(component.isTransitioning).toBe(true);
expect(getToolbar().classes['transitioning']).toBe(true);
triggerDocViewerEvent('docRendered');
fixture.detectChanges();
expect(component.isTransitioning).toBe(false);
expect(getToolbar().classes['transitioning']).toBe(false);
// While a document is being rendered, `isTransitoning` is set to true.
triggerDocViewerEvent('docReady');
fixture.detectChanges();
expect(component.isTransitioning).toBe(true);
expect(getToolbar().classes['transitioning']).toBe(true);
triggerDocViewerEvent('docRendered');
fixture.detectChanges();
expect(component.isTransitioning).toBe(false);
expect(getToolbar().classes['transitioning']).toBe(false);
});
it('should update the sidenav state as soon as a new document is inserted', () => {
initializeTest();
const updateSideNavSpy = spyOn(component, 'updateSideNav');
triggerDocViewerEvent('docInserted');
jasmine.clock().tick(0);
expect(updateSideNavSpy).toHaveBeenCalledTimes(1);
triggerDocViewerEvent('docInserted');
jasmine.clock().tick(0);
expect(updateSideNavSpy).toHaveBeenCalledTimes(2);
});
});
describe('pageId', () => {
const navigateTo = (path: string) => {
locationService.go(path);
triggerDocViewerEvent('docInserted');
jasmine.clock().tick(0);
fixture.detectChanges();
};
beforeEach(jasmine.clock().install);
afterEach(jasmine.clock().uninstall);
it('should set the id of the doc viewer container based on the current doc', () => {
initializeTest();
const container = fixture.debugElement.query(By.css('section.sidenav-content'));
navigateTo('guide/pipes');
expect(component.pageId).toEqual('guide-pipes');
expect(container.properties['id']).toEqual('guide-pipes');
navigateTo('news');
expect(component.pageId).toEqual('news');
expect(container.properties['id']).toEqual('news');
navigateTo('');
expect(component.pageId).toEqual('home');
expect(container.properties['id']).toEqual('home');
});
it('should not be affected by changes to the query', () => {
initializeTest();
const container = fixture.debugElement.query(By.css('section.sidenav-content'));
navigateTo('guide/pipes');
navigateTo('guide/other?search=http');
expect(component.pageId).toEqual('guide-other');
expect(container.properties['id']).toEqual('guide-other');
});
});
describe('hostClasses', () => {
const triggerUpdateHostClasses = () => {
triggerDocViewerEvent('docInserted');
jasmine.clock().tick(0);
fixture.detectChanges();
};
const navigateTo = (path: string) => {
locationService.go(path);
triggerUpdateHostClasses();
};
beforeEach(jasmine.clock().install);
afterEach(jasmine.clock().uninstall);
it('should set the css classes of the host container based on the current doc and navigation view', () => {
initializeTest();
navigateTo('guide/pipes');
checkHostClass('page', 'guide-pipes');
checkHostClass('folder', 'guide');
checkHostClass('view', 'SideNav');
navigateTo('features');
checkHostClass('page', 'features');
checkHostClass('folder', 'features');
checkHostClass('view', 'TopBar');
navigateTo('');
checkHostClass('page', 'home');
checkHostClass('folder', 'home');
checkHostClass('view', '');
});
it('should set the css class of the host container based on the open/closed state of the side nav', async () => {
initializeTest();
navigateTo('guide/pipes');
checkHostClass('sidenav', 'open');
sidenav.close();
await waitForEmit(sidenav.onClose);
fixture.detectChanges();
checkHostClass('sidenav', 'closed');
sidenav.open();
await waitForEmit(sidenav.onOpen);
fixture.detectChanges();
checkHostClass('sidenav', 'open');
function waitForEmit(emitter: Observable<void>): Promise<void> {
return new Promise(resolve => {
emitter.subscribe(resolve);
fixture.detectChanges();
});
}
});
it('should set the css class of the host container based on the initial deployment mode', () => {
createTestingModule('a/b', 'archive');
initializeTest();
triggerUpdateHostClasses();
checkHostClass('mode', 'archive');
});
function checkHostClass(type, value) {
const host = fixture.debugElement;
const classes = host.properties['className'];
const classArray = classes.split(' ').filter(c => c.indexOf(`${type}-`) === 0);
expect(classArray.length).toBeLessThanOrEqual(1, `"${classes}" should have only one class matching ${type}-*`);
expect(classArray).toEqual([`${type}-${value}`], `"${classes}" should contain ${type}-${value}`);
}
});
describe('progress bar', () => {
@ -938,7 +1000,7 @@ describe('AppComponent', () => {
const getProgressBar = () => fixture.debugElement.query(By.directive(MatProgressBar));
const initializeAndCompleteNavigation = () => {
initializeTest();
triggerDocReady();
triggerDocViewerEvent('docReady');
tick(HIDE_DELAY);
};
@ -975,7 +1037,7 @@ describe('AppComponent', () => {
it('should not be shown when re-navigating to the empty path', fakeAsync(() => {
initializeAndCompleteNavigation();
locationService.urlSubject.next('');
triggerDocReady();
triggerDocViewerEvent('docReady');
locationService.urlSubject.next('');
@ -991,7 +1053,7 @@ describe('AppComponent', () => {
locationService.urlSubject.next('c/d');
tick(SHOW_DELAY - 1);
triggerDocReady();
triggerDocViewerEvent('docReady');
tick(1);
fixture.detectChanges();
@ -1005,7 +1067,7 @@ describe('AppComponent', () => {
locationService.urlSubject.next('c/d');
tick(SHOW_DELAY);
triggerDocReady();
triggerDocViewerEvent('docReady');
fixture.detectChanges();
expect(getProgressBar()).toBeTruthy();
@ -1018,7 +1080,7 @@ describe('AppComponent', () => {
locationService.urlSubject.next('c/d');
tick(SHOW_DELAY);
triggerDocReady();
triggerDocViewerEvent('docReady');
fixture.detectChanges();
expect(getProgressBar()).toBeTruthy();
@ -1037,8 +1099,8 @@ describe('AppComponent', () => {
locationService.urlSubject.next('c/d'); // The URL changes.
locationService.urlSubject.next('e/f'); // The URL changes again before `onDocReady()`.
tick(SHOW_DELAY - 1); // `onDocReady()` is triggered (for the last doc),
triggerDocReady(); // before the progress bar is shown.
tick(SHOW_DELAY - 1); // `onDocReady()` is triggered (for the last doc),
triggerDocViewerEvent('docReady'); // before the progress bar is shown.
tick(1);
fixture.detectChanges();

View File

@ -4,7 +4,6 @@ import { MatSidenav } from '@angular/material/sidenav';
import { CurrentNodes, NavigationService, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
import { DocumentService, DocumentContents } from 'app/documents/document.service';
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
import { Deployment } from 'app/shared/deployment.service';
import { LocationService } from 'app/shared/location.service';
import { ScrollService } from 'app/shared/scroll.service';
@ -16,6 +15,7 @@ import { TocService } from 'app/shared/toc.service';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { combineLatest } from 'rxjs/observable/combineLatest';
import 'rxjs/add/operator/first';
const sideNavView = 'SideNav';
@ -58,6 +58,7 @@ export class AppComponent implements OnInit {
isFetching = false;
isStarting = true;
isTransitioning = true;
isSideBySide = false;
private isFetchingTimeout: any;
private isSideNavDoc = false;
@ -75,18 +76,9 @@ export class AppComponent implements OnInit {
versionInfo: VersionInfo;
get homeImageUrl() {
return this.isSideBySide ?
'assets/images/logos/angular/logo-nav@2x.png' :
'assets/images/logos/angular/shield-large.svg';
}
get isOpened() { return this.isSideBySide && this.isSideNavDoc; }
get mode() { return this.isSideBySide ? 'side' : 'over'; }
// Need the doc-viewer element for scrolling the contents
@ViewChild(DocViewerComponent, { read: ElementRef })
docViewer: ElementRef;
// Search related properties
showSearchResults = false;
searchResults: Observable<SearchResults>;
@ -120,12 +112,13 @@ export class AppComponent implements OnInit {
/* No need to unsubscribe because this root component never dies */
this.documentService.currentDocument.subscribe(doc => {
this.currentDocument = doc;
this.setPageId(doc.id);
this.setFolderId(doc.id);
this.updateHostClasses();
});
this.documentService.currentDocument.subscribe(doc => this.currentDocument = doc);
// Generally, we want to delay updating the host classes for the new document, until after the
// leaving document has been removed (to avoid having the styles for the new document applied
// prematurely).
// On the first document, though, (when we know there is no previous document), we want to
// ensure the styles are applied as soon as possible to avoid flicker.
this.documentService.currentDocument.first().subscribe(doc => this.updateHostClassesForDoc(doc));
this.locationService.currentPath.subscribe(path => {
// Redirect to docs if we are in not in stable mode and are not hitting a docs page
@ -146,21 +139,7 @@ export class AppComponent implements OnInit {
}
});
this.navigationService.currentNodes.subscribe(currentNodes => {
this.currentNodes = currentNodes;
// Preserve current sidenav open state by default
let openSideNav = this.sidenav.opened;
const isSideNavDoc = !!currentNodes[sideNavView];
if (this.isSideNavDoc !== isSideNavDoc) {
// View type changed. Is it now a sidenav view (e.g, guide or tutorial)?
// Open if changed to a sidenav doc; close if changed to a marketing doc.
openSideNav = this.isSideNavDoc = isSideNavDoc;
}
// May be open or closed when wide; always closed when narrow
this.sideNavToggle(this.isSideBySide ? openSideNav : false);
});
this.navigationService.currentNodes.subscribe(currentNodes => this.currentNodes = currentNodes);
// Compute the version picker list from the current version and the versions in the navigation map
combineLatest(
@ -204,14 +183,14 @@ export class AppComponent implements OnInit {
}
onDocReady() {
// About to transition to new view.
this.isTransitioning = true;
// Stop fetching timeout (which, when render is fast, means progress bar never shown)
clearTimeout(this.isFetchingTimeout);
// If progress bar has been shown, keep it for at least 500ms (to avoid flashing).
setTimeout(() => {
this.isStarting = false;
this.isFetching = false;
}, 500);
setTimeout(() => this.isFetching = false, 500);
}
onDocRemoved() {
@ -221,11 +200,25 @@ export class AppComponent implements OnInit {
}
onDocInserted() {
// TODO: Find a better way to avoid `ExpressionChangedAfterItHasBeenChecked` error.
setTimeout(() => {
// Update the SideNav state (if necessary).
this.updateSideNav();
// Update the host classes to match the new document.
this.updateHostClassesForDoc(this.currentDocument);
});
// Scroll 500ms after the new document has been inserted into the doc-viewer.
// The delay is to allow time for async layout to complete.
setTimeout(() => this.autoScroll(), 500);
}
onDocRendered() {
this.isStarting = false;
this.isTransitioning = false;
}
onDocVersionChange(versionIndex: number) {
const version = this.docVersions[versionIndex];
if (version.url) {
@ -290,6 +283,27 @@ export class AppComponent implements OnInit {
this.hostClasses = `${mode} ${sideNavOpen} ${pageClass} ${folderClass} ${viewClasses}`;
}
updateHostClassesForDoc(doc: DocumentContents) {
this.setPageId(doc.id);
this.setFolderId(doc.id);
this.updateHostClasses();
}
updateSideNav() {
// Preserve current sidenav open state by default.
let openSideNav = this.sidenav.opened;
const isSideNavDoc = !!this.currentNodes[sideNavView];
if (this.isSideNavDoc !== isSideNavDoc) {
// View type changed. Is it now a sidenav view (e.g, guide or tutorial)?
// Open if changed to a sidenav doc; close if changed to a marketing doc.
openSideNav = this.isSideNavDoc = isSideNavDoc;
}
// May be open or closed when wide; always closed when narrow.
this.sideNavToggle(this.isSideBySide && openSideNav);
}
// Dynamically change height of table of contents container
@HostListener('window:scroll')
onScroll() {

View File

@ -43,15 +43,15 @@ export class ApiListComponent implements OnInit {
// API types
types: Option[] = [
{ value: 'all', title: 'All' },
{ value: 'directive', title: 'Directive' },
{ value: 'pipe', title: 'Pipe'},
{ value: 'decorator', title: 'Decorator' },
{ value: 'class', title: 'Class' },
{ value: 'interface', title: 'Interface' },
{ value: 'function', title: 'Function' },
{ value: 'const', title: 'Const' },
{ value: 'decorator', title: 'Decorator' },
{ value: 'directive', title: 'Directive' },
{ value: 'enum', title: 'Enum' },
{ value: 'type-alias', title: 'Type Alias' },
{ value: 'const', title: 'Const'}
{ value: 'function', title: 'Function' },
{ value: 'interface', title: 'Interface' },
{ value: 'pipe', title: 'Pipe' },
{ value: 'type-alias', title: 'Type Alias' }
];
statuses: Option[] = [

View File

@ -13,7 +13,7 @@ import {
TestDocViewerComponent, TestModule, TestParentComponent
} from 'testing/doc-viewer-utils';
import { MockLogger } from 'testing/logger.service';
import { DocViewerComponent } from './doc-viewer.component';
import { DocViewerComponent, NO_ANIMATIONS } from './doc-viewer.component';
describe('DocViewerComponent', () => {
@ -368,7 +368,7 @@ describe('DocViewerComponent', () => {
});
it('should display nothing if the document has no contents', async () => {
docViewer.currViewContainer.innerHTML = 'Test';
await doRender('Test');
expect(docViewerEl.textContent).toBe('Test');
await doRender('');
@ -647,6 +647,8 @@ describe('DocViewerComponent', () => {
oldCurrViewContainer.innerHTML = 'Current view';
oldNextViewContainer.innerHTML = 'Next view';
docViewerEl.appendChild(oldCurrViewContainer);
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
});
@ -656,122 +658,142 @@ describe('DocViewerComponent', () => {
beforeEach(() => DocViewerComponent.animationsEnabled = animationsEnabled);
afterEach(() => DocViewerComponent.animationsEnabled = true);
it('should return an observable', done => {
docViewer.swapViews().subscribe(done, done.fail);
});
[true, false].forEach(noAnimations => {
describe(`(.${NO_ANIMATIONS}: ${noAnimations})`, () => {
beforeEach(() => docViewerEl.classList[noAnimations ? 'add' : 'remove'](NO_ANIMATIONS));
it('should swap the views', async () => {
await doSwapViews();
it('should return an observable', done => {
docViewer.swapViews().subscribe(done, done.fail);
});
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
expect(docViewer.currViewContainer).toBe(oldNextViewContainer);
expect(docViewer.nextViewContainer).toBe(oldCurrViewContainer);
it('should swap the views', async () => {
await doSwapViews();
await doSwapViews();
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
expect(docViewer.currViewContainer).toBe(oldNextViewContainer);
expect(docViewer.nextViewContainer).toBe(oldCurrViewContainer);
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
expect(docViewer.currViewContainer).toBe(oldCurrViewContainer);
expect(docViewer.nextViewContainer).toBe(oldNextViewContainer);
});
await doSwapViews();
it('should emit `docRemoved` after removing the leaving view', async () => {
const onDocRemovedSpy = jasmine.createSpy('onDocRemoved').and.callFake(() => {
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
expect(docViewer.currViewContainer).toBe(oldCurrViewContainer);
expect(docViewer.nextViewContainer).toBe(oldNextViewContainer);
});
it('should emit `docRemoved` after removing the leaving view', async () => {
const onDocRemovedSpy = jasmine.createSpy('onDocRemoved').and.callFake(() => {
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
});
docViewer.docRemoved.subscribe(onDocRemovedSpy);
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
await doSwapViews();
expect(onDocRemovedSpy).toHaveBeenCalledTimes(1);
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
});
it('should not emit `docRemoved` if the leaving view is already removed', async () => {
const onDocRemovedSpy = jasmine.createSpy('onDocRemoved');
docViewer.docRemoved.subscribe(onDocRemovedSpy);
docViewerEl.removeChild(oldCurrViewContainer);
await doSwapViews();
expect(onDocRemovedSpy).not.toHaveBeenCalled();
});
it('should emit `docInserted` after inserting the entering view', async () => {
const onDocInsertedSpy = jasmine.createSpy('onDocInserted').and.callFake(() => {
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
});
docViewer.docInserted.subscribe(onDocInsertedSpy);
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
await doSwapViews();
expect(onDocInsertedSpy).toHaveBeenCalledTimes(1);
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
});
it('should call the callback after inserting the entering view', async () => {
const onInsertedCb = jasmine.createSpy('onInsertedCb').and.callFake(() => {
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
});
const onDocInsertedSpy = jasmine.createSpy('onDocInserted');
docViewer.docInserted.subscribe(onDocInsertedSpy);
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
await doSwapViews(onInsertedCb);
expect(onInsertedCb).toHaveBeenCalledTimes(1);
expect(onInsertedCb).toHaveBeenCalledBefore(onDocInsertedSpy);
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
});
it('should empty the previous view', async () => {
await doSwapViews();
expect(docViewer.currViewContainer.innerHTML).toBe('Next view');
expect(docViewer.nextViewContainer.innerHTML).toBe('');
docViewer.nextViewContainer.innerHTML = 'Next view 2';
await doSwapViews();
expect(docViewer.currViewContainer.innerHTML).toBe('Next view 2');
expect(docViewer.nextViewContainer.innerHTML).toBe('');
});
if (animationsEnabled && !noAnimations) {
// Only test this when there are animations. Without animations, the views are swapped
// synchronously, so there is no need (or way) to abort.
it('should abort swapping if the returned observable is unsubscribed from', async () => {
docViewer.swapViews().subscribe().unsubscribe();
await doSwapViews();
// Since the first call was cancelled, only one swapping should have taken place.
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
expect(docViewer.currViewContainer).toBe(oldNextViewContainer);
expect(docViewer.nextViewContainer).toBe(oldCurrViewContainer);
expect(docViewer.currViewContainer.innerHTML).toBe('Next view');
expect(docViewer.nextViewContainer.innerHTML).toBe('');
});
} else {
it('should swap views synchronously when animations are disabled', () => {
const cbSpy = jasmine.createSpy('cb');
docViewer.swapViews(cbSpy).subscribe();
expect(cbSpy).toHaveBeenCalledTimes(1);
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
expect(docViewer.currViewContainer).toBe(oldNextViewContainer);
expect(docViewer.nextViewContainer).toBe(oldCurrViewContainer);
expect(docViewer.currViewContainer.innerHTML).toBe('Next view');
expect(docViewer.nextViewContainer.innerHTML).toBe('');
});
}
});
docViewer.docRemoved.subscribe(onDocRemovedSpy);
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
await doSwapViews();
expect(onDocRemovedSpy).toHaveBeenCalledTimes(1);
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
});
it('should not emit `docRemoved` if the leaving view is already removed', async () => {
const onDocRemovedSpy = jasmine.createSpy('onDocRemoved');
docViewer.docRemoved.subscribe(onDocRemovedSpy);
docViewerEl.removeChild(oldCurrViewContainer);
await doSwapViews();
expect(onDocRemovedSpy).not.toHaveBeenCalled();
});
it('should emit `docInserted` after inserting the entering view', async () => {
const onDocInsertedSpy = jasmine.createSpy('onDocInserted').and.callFake(() => {
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
});
docViewer.docInserted.subscribe(onDocInsertedSpy);
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
await doSwapViews();
expect(onDocInsertedSpy).toHaveBeenCalledTimes(1);
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
});
it('should call the callback after inserting the entering view', async () => {
const onInsertedCb = jasmine.createSpy('onInsertedCb').and.callFake(() => {
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
});
const onDocInsertedSpy = jasmine.createSpy('onDocInserted');
docViewer.docInserted.subscribe(onDocInsertedSpy);
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
await doSwapViews(onInsertedCb);
expect(onInsertedCb).toHaveBeenCalledTimes(1);
expect(onInsertedCb).toHaveBeenCalledBefore(onDocInsertedSpy);
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
});
it('should empty the previous view', async () => {
await doSwapViews();
expect(docViewer.currViewContainer.innerHTML).toBe('Next view');
expect(docViewer.nextViewContainer.innerHTML).toBe('');
docViewer.nextViewContainer.innerHTML = 'Next view 2';
await doSwapViews();
expect(docViewer.currViewContainer.innerHTML).toBe('Next view 2');
expect(docViewer.nextViewContainer.innerHTML).toBe('');
});
if (animationsEnabled) {
// Without animations, the views are swapped synchronously,
// so there is no need (or way) to abort.
it('should abort swapping if the returned observable is unsubscribed from', async () => {
docViewer.swapViews().subscribe().unsubscribe();
await doSwapViews();
// Since the first call was cancelled, only one swapping should have taken place.
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
expect(docViewer.currViewContainer).toBe(oldNextViewContainer);
expect(docViewer.nextViewContainer).toBe(oldCurrViewContainer);
expect(docViewer.currViewContainer.innerHTML).toBe('Next view');
expect(docViewer.nextViewContainer.innerHTML).toBe('');
});
}
});
});
});

View File

@ -15,6 +15,9 @@ import { Logger } from 'app/shared/logger.service';
import { TocService } from 'app/shared/toc.service';
// Constants
export const NO_ANIMATIONS = 'no-animations';
// Initialization prevents flicker once pre-rendering is on
const initialDocViewerElement = document.querySelector('aio-doc-viewer');
const initialDocViewerContent = initialDocViewerElement ? initialDocViewerElement.innerHTML : '';
@ -77,8 +80,6 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
if (this.hostElement.firstElementChild) {
this.currViewContainer = this.hostElement.firstElementChild as HTMLElement;
} else {
this.hostElement.appendChild(this.currViewContainer);
}
this.onDestroy$.subscribe(() => this.destroyEmbeddedComponents());
@ -176,10 +177,21 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
return () => cancelAnimationFrame(rafId);
});
// Get the actual transition duration (taking global styles into account).
// According to the [CSSOM spec](https://drafts.csswg.org/cssom/#serializing-css-values),
// `time` values should be returned in seconds.
const getActualDuration = (elem: HTMLElement) => {
const cssValue = getComputedStyle(elem).transitionDuration;
const seconds = Number(cssValue.replace(/s$/, ''));
return 1000 * seconds;
};
const animateProp =
(elem: HTMLElement, prop: string, from: string, to: string, duration = 333) => {
(elem: HTMLElement, prop: string, from: string, to: string, duration = 200) => {
const animationsDisabled = !DocViewerComponent.animationsEnabled
|| this.hostElement.classList.contains(NO_ANIMATIONS);
elem.style.transition = '';
return !DocViewerComponent.animationsEnabled
return animationsDisabled
? this.void$.do(() => elem.style[prop] = to)
: this.void$
// In order to ensure that the `from` value will be applied immediately (i.e.
@ -189,11 +201,11 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
.switchMap(() => raf$).do(() => elem.style[prop] = from)
.switchMap(() => raf$).do(() => elem.style.transition = `all ${duration}ms ease-in-out`)
.switchMap(() => raf$).do(() => elem.style[prop] = to)
.switchMap(() => timer(duration)).switchMap(() => this.void$);
.switchMap(() => timer(getActualDuration(elem))).switchMap(() => this.void$);
};
const animateLeave = (elem: HTMLElement) => animateProp(elem, 'opacity', '1', '0.25');
const animateEnter = (elem: HTMLElement) => animateProp(elem, 'opacity', '0.25', '1');
const animateLeave = (elem: HTMLElement) => animateProp(elem, 'opacity', '1', '0.1');
const animateEnter = (elem: HTMLElement) => animateProp(elem, 'opacity', '0.1', '1');
let done$ = this.void$;

View File

@ -36,7 +36,7 @@ describe('SearchBoxComponent', () => {
describe('initialisation', () => {
it('should get the current search query from the location service',
inject([LocationService], (location: MockLocationService) => fakeAsync(() => {
fakeAsync(inject([LocationService], (location: MockLocationService) => {
location.search.and.returnValue({ search: 'initial search' });
component.ngOnInit();
expect(location.search).toHaveBeenCalled();

View File

@ -0,0 +1,4 @@
.no-animations aio-doc-viewer > * {
// Disable view transition animations.
transition: none !important;
}

View File

@ -4,6 +4,7 @@
@import 'api-page';
@import 'content-layout';
@import 'doc-viewer';
@import 'footer';
@import 'layout-global';
@import 'marketing-layout';

View File

@ -1,3 +1,92 @@
// VARIABLES
$hamburgerShownMargin: 0;
$hamburgerHiddenMargin: 0 24px 0 -88px;
// DOCS PAGE / STANDARD: TOPNAV TOOLBAR FIXED
mat-toolbar.mat-toolbar {
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 10;
padding: 0 16px 0 0;
box-shadow: 0 2px 5px 0 rgba(0,0,0,0.30);
mat-icon {
color: $white;
}
}
// HOME PAGE OVERRIDE: TOPNAV TOOLBAR
aio-shell.page-home mat-toolbar.mat-toolbar {
background-color: $blue;
@media (min-width: 481px) {
&:not(.transitioning) {
background-color: transparent;
transition: background-color .2s linear;
}
}
}
// MARKETING PAGES OVERRIDE: TOPNAV TOOLBAR AND HAMBURGER
aio-shell.page-home mat-toolbar.mat-toolbar,
aio-shell.page-features mat-toolbar.mat-toolbar,
aio-shell.page-events mat-toolbar.mat-toolbar,
aio-shell.page-resources mat-toolbar.mat-toolbar {
box-shadow: none;
// FIXED TOPNAV TOOLBAR FOR SMALL MOBILE
@media (min-width: 481px) {
position: absolute;
}
}
// DOCS PAGES OVERRIDE: HAMBURGER
aio-shell.folder-api mat-toolbar.mat-toolbar,
aio-shell.folder-docs mat-toolbar.mat-toolbar,
aio-shell.folder-guide mat-toolbar.mat-toolbar,
aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
@media (min-width: 992px) {
.hamburger.mat-button {
// Hamburger shown on non-marketing pages on large screens.
margin: $hamburgerShownMargin;
}
}
}
// HAMBURGER BUTTON
.hamburger.mat-button {
height: 100%;
margin: $hamburgerShownMargin;
padding: 0;
transition-duration: .4s;
transition-property: color, margin;
transition-timing-function: cubic-bezier(.25, .8, .25, 1);
@media (min-width: 992px) {
// Hamburger hidden by default on large screens.
// (Will be shown per doc.)
margin: $hamburgerHiddenMargin;
}
&:hover {
color: $offwhite;
}
& .mat-icon {
color: white;
position: inherit;
}
}
// HOME NAV-LINK
.nav-link.home img {
position: relative;
margin-top: -21px;
@ -12,6 +101,8 @@
}
}
// TOP MENU
aio-top-menu {
display: flex;
flex-direction: row;
@ -56,55 +147,6 @@ aio-top-menu {
}
}
// HOME PAGE OVERRIDE: TOPNAV TOOLBAR HAMBURGER MENU
aio-shell.page-home mat-toolbar.app-toolbar.mat-toolbar {
background-color: transparent;
transition: background-color .2s linear .3s;
@media (max-width: 480px) {
background-color: $blue;
}
}
// DOCS PAGE / STANDARD: TOPNAV TOOLBAR FIXED
mat-toolbar.mat-toolbar {
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 10;
padding: 0 16px 0 0;
box-shadow: 0 2px 5px 0 rgba(0,0,0,0.30);
mat-icon {
color: $white;
}
}
// MARKETING PAGES OVERRIDE: TOPNAV TOOLBAR AND HAMBURGER
aio-shell.page-home mat-toolbar.mat-toolbar,
aio-shell.page-features mat-toolbar.mat-toolbar,
aio-shell.page-events mat-toolbar.mat-toolbar,
aio-shell.page-resources mat-toolbar.mat-toolbar {
// FIXED TOPNAV TOOLBAR FOR SMALL MOBILE
@media (min-width: 481px) {
position: absolute;
}
@media (min-width: 992px) {
button.hamburger {
margin: 0 24px 0 -88px;
}
}
}
// REMOVE BOX SHADOW ON CERTAIN MARKETING PAGES
aio-shell.page-home mat-toolbar.mat-toolbar,
aio-shell.page-events mat-toolbar.mat-toolbar,
aio-shell.page-resources mat-toolbar.mat-toolbar {
box-shadow: none;
}
// SEARCH BOX
aio-search-box.search-container {

View File

@ -1,29 +0,0 @@
.hamburger {
transition-duration: 150ms;
transition-property: background-color, color;
transition-timing-function: ease-in-out;
&:hover {
color: $lightgray;
}
}
.hamburger.mat-button {
height: 100%;
margin: 0;
padding: 0;
&:not(.starting) {
transition-duration: .4s;
transition-property: color, margin;
transition-timing-function: cubic-bezier(.25, .8, .25, 1);
}
}
.hamburger.mat-button:hover {
color: $offwhite;
}
.hamburger .mat-icon {
position: inherit;
color: white;
}

View File

@ -16,7 +16,6 @@
@import 'edit-page-cta';
@import 'features';
@import 'filetree';
@import 'hamburger';
@import 'heading-anchors';
@import 'hr';
@import 'images';

View File

@ -1,6 +1,7 @@
{
"scripts": [
{ "name": "ng", "command": "ng" },
{ "name": "build", "command": "ng build" },
{ "name": "start", "command": "ng serve" },
{ "name": "test", "command": "ng test" },
{ "name": "lint", "command": "ng lint" },

View File

@ -54,6 +54,19 @@ class ExampleZipper {
}
}
// rename a custom main.ts or index.html file
_renameFile(file) {
if (/src\/main[-.]\w+\.ts$/.test(file)) {
return 'src/main.ts';
}
if (/src\/index[-.]\w+\.html$/.test(file)) {
return 'src/index.html';
}
return file;
}
_zipExample(configFileName, sourceDirName, outputDirName) {
let json = require(configFileName, 'utf-8');
const basePath = json.basePath || '';
@ -79,12 +92,16 @@ class ExampleZipper {
'tslint.json',
'karma-test-shim.js',
'karma.conf.js',
'tsconfig.json',
'src/testing/**/*',
'src/.babelrc',
'src/favicon.ico',
'src/typings.d.ts'
'src/polyfills.ts',
'src/typings.d.ts',
'src/environments/**/*',
'src/tsconfig.*'
];
var defaultExcludes = [
var alwaysExcludes = [
'!**/bs-config.e2e.json',
'!**/*plnkr.*',
'!**/*zipper.*',
@ -132,13 +149,14 @@ class ExampleZipper {
}
});
Array.prototype.push.apply(gpaths, defaultExcludes);
Array.prototype.push.apply(gpaths, alwaysExcludes);
let fileNames = globby.sync(gpaths, { ignore: ['**/node_modules/**']});
let zip = this._createZipArchive(outputFileName);
fileNames.forEach((fileName) => {
let relativePath = path.relative(exampleDirName, fileName);
relativePath = this._renameFile(relativePath);
let content = fs.readFileSync(fileName, 'utf8');
let extn = path.extname(fileName).substr(1);
// if we don't need to clean up the file then we can do the following.

View File

@ -38,6 +38,8 @@ module.exports = new Package('angular-api', [basePackage, typeScriptPackage])
readTypeScriptModules.basePath = API_SOURCE_PATH;
readTypeScriptModules.ignoreExportsMatching = [/^[_ɵ]|^VERSION$/];
readTypeScriptModules.hidePrivateMembers = true;
// NOTE: This list shold be in sync with tools/gulp-tasks/public-api.js
readTypeScriptModules.sourceFiles = [
'animations/index.ts',
'animations/browser/index.ts',

View File

@ -206,7 +206,7 @@ minify() {
base_file=$( basename "${file}" )
if [[ "${base_file}" =~ $regex && "${base_file##*.}" != "map" ]]; then
local out_file=$(dirname "${file}")/${BASH_REMATCH[1]}.min.js
$UGLIFYJS -c --screw-ie8 --comments -o ${out_file} --source-map ${out_file}.map --source-map-include-sources ${file}
$UGLIFYJS -c --screw-ie8 --comments -o ${out_file} --source-map ${out_file}.map --prefix relative --source-map-include-sources ${file}
fi
done
}
@ -476,7 +476,7 @@ do
if [[ ${PACKAGE} == "common" ]]; then
echo "====== Copy i18n locale data"
rsync -a --exclude=*.d.ts --exclude=*.metadata.json ${OUT_DIR}/locales/ ${NPM_DIR}/locales
rsync -a ${OUT_DIR}/locales/ ${NPM_DIR}/locales
fi
else
echo "====== Copy ${PACKAGE} node tool"

View File

@ -7,30 +7,11 @@ Caretaker is responsible for merging PRs into the individual branches and intern
- Draining the queue of PRs ready to be merged. (PRs with [`PR action: merge`](https://github.com/angular/angular/pulls?q=is%3Aopen+is%3Apr+label%3A%22PR+action%3A+merge%22) label)
- Assigining [new issues](https://github.com/angular/angular/issues?q=is%3Aopen+is%3Aissue+no%3Alabel) to individual component authors.
## Setup
### Set `upstream` to fetch PRs into your local repo
Use this conmmands to configure your `git` to fetch PRs into your local repo.
```
git remote add upstream git@github.com:angular/angular.git
git config --add remote.upstream.fetch +refs/pull/*/head:refs/remotes/upstream/pr/*
```
## Merging the PR
A PR needs to have `PR action: merge` and `PR target: *` labels to be considered
ready to merge. Merging is performed by running `merge-pr` with a PR number to merge.
NOTE: before running `merge-pr` ensure that you have synced all of the PRs
locally by running:
```
$ git fetch upstream
```
To merge a PR run:
```
@ -40,6 +21,7 @@ $ ./scripts/github/merge-pr 1234
The `merge-pr` script will:
- Ensure that all approriate labels are on the PR.
- That the current branch (`master` or `?.?.x` patch) mathches the `PR target: *` label.
- Fetches the latest PR code from the `angular/angular` repo.
- It will `cherry-pick` all of the SHAs from the PR into the current branch.
- It will rewrite commit history by automatically adding `Close #1234` and `(#1234)` into the commit message.
@ -53,8 +35,8 @@ $ ./scripts/github/merge-pr 1234
======================
GitHub Merge PR Steps
======================
git cherry-pick upstream/pr/1234~1..upstream/pr/1234
git filter-branch -f --msg-filter "/usr/local/google/home/misko/angular-pr/scripts/github/utils/github.closes 1234" HEAD~1..HEAD
git cherry-pick angular/pr/1234~1..angular/pr/1234
git filter-branch -f --msg-filter "/home/misko/angular/scripts/github/utils/github.closes 1234" HEAD~1..HEAD
```
If the `cherry-pick` command fails than resolve conflicts and use `git cherry-pick --continue` once ready. After the `cherry-pick` is done cut&paste and run the `filter-branch` command to properly rewrite the messages

View File

@ -14,7 +14,7 @@ node_repositories(package_json = ["//:package.json"])
git_repository(
name = "build_bazel_rules_typescript",
remote = "https://github.com/bazelbuild/rules_typescript.git",
tag = "0.6.0",
tag = "0.6.2",
)
load("@build_bazel_rules_typescript//:defs.bzl", "ts_repositories")

View File

@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "5.1.1",
"version": "5.1.3",
"private": true,
"branchPattern": "2.0.*",
"description": "Angular - a web framework for modern web apps",

View File

@ -6,7 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
export interface DOMAnimation {
/**
* DOMAnimation represents the Animation Web API.
*
* It is an external API by the browser, and must thus use "declare interface",
* to prevent renaming by Closure Compiler.
*
* @see https://developer.mozilla.org/de/docs/Web/API/Animation
*/
export declare interface DOMAnimation {
cancel(): void;
play(): void;
pause(): void;

View File

@ -33,17 +33,17 @@ export class AnimationGroupPlayer implements AnimationPlayer {
} else {
this.players.forEach(player => {
player.onDone(() => {
if (++doneCount >= total) {
if (++doneCount == total) {
this._onFinish();
}
});
player.onDestroy(() => {
if (++destroyCount >= total) {
if (++destroyCount == total) {
this._onDestroy();
}
});
player.onStart(() => {
if (++startCount >= total) {
if (++startCount == total) {
this._onStart();
}
});
@ -67,9 +67,9 @@ export class AnimationGroupPlayer implements AnimationPlayer {
private _onStart() {
if (!this.hasStarted()) {
this._started = true;
this._onStartFns.forEach(fn => fn());
this._onStartFns = [];
this._started = true;
}
}

View File

@ -21,6 +21,7 @@
"./closure-locale.ts"
],
"angularCompilerOptions": {
"skipTemplateCodegen": true
"skipTemplateCodegen": true,
"skipMetadataEmit": true
}
}

View File

@ -270,6 +270,13 @@ function getDateTranslation(
return getLocaleDayPeriods(locale, form, <TranslationWidth>width)[currentHours < 12 ? 0 : 1];
case TranslationType.Eras:
return getLocaleEraNames(locale, <TranslationWidth>width)[date.getFullYear() <= 0 ? 0 : 1];
default:
// This default case is not needed by TypeScript compiler, as the switch is exhaustive.
// However Closure Compiler does not understand that and reports an error in typed mode.
// The `throw new Error` below works around the problem, and the unexpected: never variable
// makes sure tsc still checks this code is unreachable.
const unexpected: never = name;
throw new Error(`unexpected translation type ${unexpected}`);
}
}

View File

@ -46,11 +46,6 @@ export function formatNumber(
num = value;
}
if (style === NumberFormatStyle.Percent) {
num = num * 100;
}
const numStr = Math.abs(num) + '';
const pattern = parseNumberFormat(format, getLocaleNumberSymbol(locale, NumberSymbol.MinusSign));
let formattedText = '';
let isZero = false;
@ -58,7 +53,11 @@ export function formatNumber(
if (!isFinite(num)) {
formattedText = getLocaleNumberSymbol(locale, NumberSymbol.Infinity);
} else {
const parsedNumber = parseNumber(numStr);
let parsedNumber = parseNumber(num);
if (style === NumberFormatStyle.Percent) {
parsedNumber = toPercent(parsedNumber);
}
let minInt = pattern.minInt;
let minFraction = pattern.minFrac;
@ -249,11 +248,35 @@ interface ParsedNumber {
integerLen: number;
}
// Transforms a parsed number into a percentage by multiplying it by 100
function toPercent(parsedNumber: ParsedNumber): ParsedNumber {
// if the number is 0, don't do anything
if (parsedNumber.digits[0] === 0) {
return parsedNumber;
}
// Getting the current number of decimals
const fractionLen = parsedNumber.digits.length - parsedNumber.integerLen;
if (parsedNumber.exponent) {
parsedNumber.exponent += 2;
} else {
if (fractionLen === 0) {
parsedNumber.digits.push(0, 0);
} else if (fractionLen === 1) {
parsedNumber.digits.push(0);
}
parsedNumber.integerLen += 2;
}
return parsedNumber;
}
/**
* Parse a number (as a string)
* Parses a number.
* Significant bits of this parse algorithm came from https://github.com/MikeMcl/big.js/
*/
function parseNumber(numStr: string): ParsedNumber {
function parseNumber(num: number): ParsedNumber {
let numStr = Math.abs(num) + '';
let exponent = 0, digits, integerLen;
let i, j, zeros;
@ -356,12 +379,23 @@ function roundNumber(parsedNumber: ParsedNumber, minFrac: number, maxFrac: numbe
// Pad out with zeros to get the required fraction length
for (; fractionLen < Math.max(0, fractionSize); fractionLen++) digits.push(0);
let dropTrailingZeros = fractionSize !== 0;
// Minimal length = nb of decimals required + current nb of integers
// Any number besides that is optional and can be removed if it's a trailing 0
const minLen = minFrac + parsedNumber.integerLen;
// Do any carrying, e.g. a digit was rounded up to 10
const carry = digits.reduceRight(function(carry, d, i, digits) {
d = d + carry;
digits[i] = d % 10;
return Math.floor(d / 10);
digits[i] = d < 10 ? d : d - 10; // d % 10
if (dropTrailingZeros) {
// Do not keep meaningless fractional trailing zeros (e.g. 15.52000 --> 15.52)
if (digits[i] === 0 && i >= minLen) {
digits.pop();
} else {
dropTrailingZeros = false;
}
}
return d >= 10 ? 1 : 0; // Math.floor(d / 10);
}, 0);
if (carry) {
digits.unshift(carry);

View File

@ -79,6 +79,22 @@ export function main() {
expect(pipe.transform(1.2, '.2')).toEqual('120.00%');
expect(pipe.transform(1.2, '4.2')).toEqual('0,120.00%');
expect(pipe.transform(1.2, '4.2', 'fr')).toEqual('0 120,00 %');
// see issue #20136
expect(pipe.transform(0.12345674, '0.0-10')).toEqual('12.345674%');
expect(pipe.transform(0, '0.0-10')).toEqual('0%');
expect(pipe.transform(0.00, '0.0-10')).toEqual('0%');
expect(pipe.transform(1, '0.0-10')).toEqual('100%');
expect(pipe.transform(0.1, '0.0-10')).toEqual('10%');
expect(pipe.transform(0.12, '0.0-10')).toEqual('12%');
expect(pipe.transform(0.123, '0.0-10')).toEqual('12.3%');
expect(pipe.transform(12.3456, '0.0-10')).toEqual('1,234.56%');
expect(pipe.transform(12.345600, '0.0-10')).toEqual('1,234.56%');
expect(pipe.transform(12.345699999, '0.0-6')).toEqual('1,234.57%');
expect(pipe.transform(12.345699999, '0.4-6')).toEqual('1,234.5700%');
expect(pipe.transform(100, '0.4-6')).toEqual('10,000.0000%');
expect(pipe.transform(100, '0.0-10')).toEqual('10,000%');
expect(pipe.transform(1.5e2)).toEqual('15,000%');
expect(pipe.transform(1e100)).toEqual('1E+102%');
});
it('should not support other objects', () => {

View File

@ -130,7 +130,7 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter implements ts.CompilerHos
moduleName, containingFile.replace(/\\/g, '/'), this.options, this,
this.moduleResolutionCache)
.resolvedModule;
if (rm && this.isSourceFile(rm.resolvedFileName)) {
if (rm && this.isSourceFile(rm.resolvedFileName) && DTS.test(rm.resolvedFileName)) {
// Case: generateCodeForLibraries = true and moduleName is
// a .d.ts file in a node_modules folder.
// Need to set isExternalLibraryImport to false so that generated files for that file
@ -326,7 +326,7 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter implements ts.CompilerHos
return {generate: false};
}
const [, base, genSuffix, suffix] = genMatch;
if (suffix !== 'ts') {
if (suffix !== 'ts' && suffix !== 'tsx') {
return {generate: false};
}
let baseFileName: string|undefined;
@ -337,9 +337,9 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter implements ts.CompilerHos
}
} else {
// Note: on-the-fly generated files always have a `.ts` suffix,
// but the file from which we generated it can be a `.ts`/ `.d.ts`
// but the file from which we generated it can be a `.ts`/ `.tsx`/ `.d.ts`
// (see options.generateCodeForLibraries).
baseFileName = [`${base}.ts`, `${base}.d.ts`].find(
baseFileName = [`${base}.ts`, `${base}.tsx`, `${base}.d.ts`].find(
baseFileName => this.isSourceFile(baseFileName) && this.originalFileExists(baseFileName));
if (!baseFileName) {
return {generate: false};

View File

@ -336,9 +336,11 @@ class AngularCompilerProgram implements Program {
if (!sf.isDeclarationFile && !GENERATED_FILES.test(sf.fileName)) {
metadataJsonCount++;
const metadata = this.metadataCache.getMetadata(sf);
const metadataText = JSON.stringify([metadata]);
const outFileName = srcToOutPath(sf.fileName.replace(/\.ts$/, '.metadata.json'));
this.writeFile(outFileName, metadataText, false, undefined, undefined, [sf]);
if (metadata) {
const metadataText = JSON.stringify([metadata]);
const outFileName = srcToOutPath(sf.fileName.replace(/\.tsx?$/, '.metadata.json'));
this.writeFile(outFileName, metadataText, false, undefined, undefined, [sf]);
}
}
});
}

View File

@ -1474,6 +1474,26 @@ describe('ngc transformer command-line', () => {
});
describe('regressions', () => {
//#20479
it('should not generate an invalid metadata file', () => {
write('src/tsconfig.json', `{
"extends": "../tsconfig-base.json",
"files": ["lib.ts"],
"angularCompilerOptions": {
"skipTemplateCodegen": true
}
}`);
write('src/lib.ts', `
export namespace A{
export class C1 {
}
export interface I1{
}
}`);
expect(main(['-p', path.join(basePath, 'src/tsconfig.json')])).toBe(0);
shouldNotExist('src/lib.metadata.json');
});
//#19544
it('should recognize @NgModule() directive with a redundant @Injectable()', () => {
write('src/tsconfig.json', `{
@ -1616,6 +1636,68 @@ describe('ngc transformer command-line', () => {
expect(messages[0]).toContain('Parser Error: Unexpected token');
});
// Regression test for #19979
it('should not stack overflow on a recursive module export', () => {
write('src/tsconfig.json', `{
"extends": "../tsconfig-base.json",
"files": ["test-module.ts"]
}`);
write('src/test-module.ts', `
import {Component, NgModule} from '@angular/core';
@Component({
template: 'Hello'
})
export class MyFaultyComponent {}
@NgModule({
exports: [MyFaultyModule],
declarations: [MyFaultyComponent],
providers: [],
})
export class MyFaultyModule { }
`);
const messages: string[] = [];
expect(
main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message)))
.toBe(1, 'Compile was expected to fail');
expect(messages[0]).toContain(`module 'MyFaultyModule' is exported recursively`);
});
// Regression test for #19979
it('should not stack overflow on a recursive module import', () => {
write('src/tsconfig.json', `{
"extends": "../tsconfig-base.json",
"files": ["test-module.ts"]
}`);
write('src/test-module.ts', `
import {Component, NgModule, forwardRef} from '@angular/core';
@Component({
template: 'Hello'
})
export class MyFaultyComponent {}
@NgModule({
imports: [forwardRef(() => MyFaultyModule)]
})
export class MyFaultyImport {}
@NgModule({
imports: [MyFaultyImport],
declarations: [MyFaultyComponent]
})
export class MyFaultyModule { }
`);
const messages: string[] = [];
expect(
main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message)))
.toBe(1, 'Compile was expected to fail');
expect(messages[0]).toContain(`is imported recursively by the module 'MyFaultyImport`);
});
it('should allow using 2 classes with the same name in declarations with noEmitOnError=true',
() => {
write('src/tsconfig.json', `{
@ -1647,6 +1729,38 @@ describe('ngc transformer command-line', () => {
`);
expect(main(['-p', path.join(basePath, 'src/tsconfig.json')])).toBe(0);
});
it('should not type check a .js files from node_modules with allowJs', () => {
write('src/tsconfig.json', `{
"extends": "../tsconfig-base.json",
"compilerOptions": {
"noEmitOnError": true,
"allowJs": true,
"declaration": false
},
"files": ["test-module.ts"]
}`);
write('src/test-module.ts', `
import {Component, NgModule} from '@angular/core';
import 'my-library';
@Component({
template: 'hello'
})
export class HelloCmp {}
@NgModule({
declarations: [HelloCmp],
})
export class MyModule {}
`);
write('src/node_modules/t.txt', ``);
write('src/node_modules/my-library/index.js', `
export someVar = 1;
export someOtherVar = undefined + 1;
`);
expect(main(['-p', path.join(basePath, 'src/tsconfig.json')])).toBe(0);
});
});
describe('formatted messages', () => {

View File

@ -285,6 +285,22 @@ describe('NgCompilerHost', () => {
expect(sf.referencedFiles.length).toBe(1);
expect(sf.referencedFiles[0].fileName).toBe('main.ts');
});
it('should generate for tsx files', () => {
codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']);
codeGenerator.generateFile.and.returnValue(aGeneratedFile);
const host = createHost({files: {'tmp': {'src': {'index.tsx': ``}}}});
const genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
expect(genSf.text).toBe(aGeneratedFileText);
const sf = host.getSourceFile('/tmp/src/index.tsx', ts.ScriptTarget.Latest);
expect(sf.referencedFiles[0].fileName).toBe('/tmp/src/index.ngfactory.ts');
// the codegen should have been cached
expect(codeGenerator.generateFile).toHaveBeenCalledTimes(1);
expect(codeGenerator.findGeneratedFileNames).toHaveBeenCalledTimes(1);
});
});
describe('updateSourceFile', () => {

View File

@ -391,6 +391,22 @@ describe('ng program', () => {
testSupport.shouldExist('built/src/main.ngfactory.d.ts');
});
it('should work with tsx files', () => {
// create a temporary ts program to get the list of all files from angular...
testSupport.writeFiles({
'src/main.tsx': createModuleAndCompSource('main'),
});
const allRootNames = resolveFiles([path.resolve(testSupport.basePath, 'src/main.tsx')]);
const program = compile(undefined, {jsx: ts.JsxEmit.React}, allRootNames);
testSupport.shouldExist('built/src/main.js');
testSupport.shouldExist('built/src/main.d.ts');
testSupport.shouldExist('built/src/main.ngfactory.js');
testSupport.shouldExist('built/src/main.ngfactory.d.ts');
testSupport.shouldExist('built/src/main.ngsummary.json');
});
it('should emit also empty generated files depending on the options', () => {
testSupport.writeFiles({
'src/main.ts': `

View File

@ -34,7 +34,7 @@ import {StaticReflector} from './static_reflector';
import {StaticSymbol} from './static_symbol';
import {ResolvedStaticSymbol, StaticSymbolResolver} from './static_symbol_resolver';
import {createForJitStub, serializeSummaries} from './summary_serializer';
import {ngfactoryFilePath, splitTypescriptSuffix, summaryFileName, summaryForJitFileName, summaryForJitName} from './util';
import {ngfactoryFilePath, normalizeGenFileSuffix, splitTypescriptSuffix, summaryFileName, summaryForJitFileName, summaryForJitName} from './util';
enum StubEmitFlags {
Basic = 1 << 0,
@ -103,7 +103,7 @@ export class AotCompiler {
genFileNames.push(summaryForJitFileName(file.fileName, true));
}
}
const fileSuffix = splitTypescriptSuffix(file.fileName, true)[1];
const fileSuffix = normalizeGenFileSuffix(splitTypescriptSuffix(file.fileName, true)[1]);
file.directives.forEach((dirSymbol) => {
const compMeta =
this._metadataResolver.getNonNormalizedDirectiveMetadata(dirSymbol) !.metadata;
@ -318,7 +318,7 @@ export class AotCompiler {
srcFileUrl: string, ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>,
directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: CompileNgModuleMetadata[],
injectables: StaticSymbol[]): GeneratedFile[] {
const fileSuffix = splitTypescriptSuffix(srcFileUrl, true)[1];
const fileSuffix = normalizeGenFileSuffix(splitTypescriptSuffix(srcFileUrl, true)[1]);
const generatedFiles: GeneratedFile[] = [];
const outputCtx = this._createOutputContext(ngfactoryFilePath(srcFileUrl, true));

View File

@ -481,7 +481,7 @@ export class StaticSymbolResolver {
if (moduleMetadatas) {
let maxVersion = -1;
moduleMetadatas.forEach((md) => {
if (md['version'] > maxVersion) {
if (md && md['version'] > maxVersion) {
maxVersion = md['version'];
moduleMetadata = md;
}

View File

@ -13,7 +13,7 @@ const JIT_SUMMARY_NAME = /NgSummary$/;
export function ngfactoryFilePath(filePath: string, forceSourceFile = false): string {
const urlWithSuffix = splitTypescriptSuffix(filePath, forceSourceFile);
return `${urlWithSuffix[0]}.ngfactory${urlWithSuffix[1]}`;
return `${urlWithSuffix[0]}.ngfactory${normalizeGenFileSuffix(urlWithSuffix[1])}`;
}
export function stripGeneratedFileSuffix(filePath: string): string {
@ -38,6 +38,10 @@ export function splitTypescriptSuffix(path: string, forceSourceFile = false): st
return [path, ''];
}
export function normalizeGenFileSuffix(srcFileSuffix: string): string {
return srcFileSuffix === '.tsx' ? '.ts' : srcFileSuffix;
}
export function summaryFileName(fileName: string): string {
const fileNameWithoutSuffix = fileName.replace(STRIP_SRC_FILE_SUFFIXES, '');
return `${fileNameWithoutSuffix}.ngsummary.json`;

View File

@ -441,11 +441,12 @@ export class CompileMetadataResolver {
this._ngModuleResolver.isNgModule(type);
}
getNgModuleSummary(moduleType: any): cpl.CompileNgModuleSummary|null {
getNgModuleSummary(moduleType: any, alreadyCollecting: Set<any>|null = null):
cpl.CompileNgModuleSummary|null {
let moduleSummary: cpl.CompileNgModuleSummary|null =
<cpl.CompileNgModuleSummary>this._loadSummary(moduleType, cpl.CompileSummaryKind.NgModule);
if (!moduleSummary) {
const moduleMeta = this.getNgModuleMetadata(moduleType, false);
const moduleMeta = this.getNgModuleMetadata(moduleType, false, alreadyCollecting);
moduleSummary = moduleMeta ? moduleMeta.toSummary() : null;
if (moduleSummary) {
this._summaryCache.set(moduleType, moduleSummary);
@ -473,7 +474,9 @@ export class CompileMetadataResolver {
return Promise.all(loading);
}
getNgModuleMetadata(moduleType: any, throwIfNotFound = true): cpl.CompileNgModuleMetadata|null {
getNgModuleMetadata(
moduleType: any, throwIfNotFound = true,
alreadyCollecting: Set<any>|null = null): cpl.CompileNgModuleMetadata|null {
moduleType = resolveForwardRef(moduleType);
let compileMeta = this._ngModuleCache.get(moduleType);
if (compileMeta) {
@ -511,7 +514,18 @@ export class CompileMetadataResolver {
if (importedModuleType) {
if (this._checkSelfImport(moduleType, importedModuleType)) return;
const importedModuleSummary = this.getNgModuleSummary(importedModuleType);
if (!alreadyCollecting) alreadyCollecting = new Set();
if (alreadyCollecting.has(importedModuleType)) {
this._reportError(
syntaxError(
`${this._getTypeDescriptor(importedModuleType)} '${stringifyType(importedType)}' is imported recursively by the module '${stringifyType(moduleType)}'.`),
moduleType);
return;
}
alreadyCollecting.add(importedModuleType);
const importedModuleSummary =
this.getNgModuleSummary(importedModuleType, alreadyCollecting);
alreadyCollecting.delete(importedModuleType);
if (!importedModuleSummary) {
this._reportError(
syntaxError(
@ -539,7 +553,17 @@ export class CompileMetadataResolver {
moduleType);
return;
}
const exportedModuleSummary = this.getNgModuleSummary(exportedType);
if (!alreadyCollecting) alreadyCollecting = new Set();
if (alreadyCollecting.has(exportedType)) {
this._reportError(
syntaxError(
`${this._getTypeDescriptor(exportedType)} '${stringify(exportedType)}' is exported recursively by the module '${stringifyType(moduleType)}'`),
moduleType);
return;
}
alreadyCollecting.add(exportedType);
const exportedModuleSummary = this.getNgModuleSummary(exportedType, alreadyCollecting);
alreadyCollecting.delete(exportedType);
if (exportedModuleSummary) {
exportedModules.push(exportedModuleSummary);
} else {

View File

@ -905,6 +905,14 @@ describe('compiler (bundled Angular)', () => {
expect(genFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl))).toBeDefined();
expect(genFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl))).toBeDefined();
});
it('should support tsx', () => {
const tsOptions = {jsx: ts.JsxEmit.React};
const {genFiles} =
compile([QUICKSTART_TSX, angularFiles], /* options */ undefined, tsOptions);
expect(genFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl))).toBeDefined();
expect(genFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl))).toBeDefined();
});
});
describe('Bundled library', () => {
@ -978,6 +986,34 @@ const QUICKSTART: MockDirectory = {
}
};
const QUICKSTART_TSX: MockDirectory = {
quickstart: {
app: {
// #20555
'app.component.tsx': `
import {Component} from '@angular/core';
@Component({
template: '<h1>Hello {{name}}</h1>'
})
export class AppComponent {
name = 'Angular';
}
`,
'app.module.ts': `
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
`
}
}
};
const LIBRARY: MockDirectory = {
bolder: {
'public-api.ts': `

View File

@ -2823,6 +2823,96 @@ export function main() {
expect(child.log).toEqual(['child-start', 'child-done']);
}));
it('should fire and synchronize the start/done callbacks on multiple blocked sub triggers',
fakeAsync(() => {
@Component({
selector: 'cmp',
animations: [
trigger(
'parent1',
[
transition(
'* => go, * => go-again',
[
style({opacity: 0}),
animate('1s', style({opacity: 1})),
]),
]),
trigger(
'parent2',
[
transition(
'* => go, * => go-again',
[
style({lineHeight: '0px'}),
animate('1s', style({lineHeight: '10px'})),
]),
]),
trigger(
'child1',
[
transition(
'* => go, * => go-again',
[
style({width: '0px'}),
animate('1s', style({width: '100px'})),
]),
]),
trigger(
'child2',
[
transition(
'* => go, * => go-again',
[
style({height: '0px'}),
animate('1s', style({height: '100px'})),
]),
]),
],
template: `
<div [@parent1]="parent1Exp" (@parent1.start)="track($event)"
[@parent2]="parent2Exp" (@parent2.start)="track($event)">
<div [@child1]="child1Exp" (@child1.start)="track($event)"
[@child2]="child2Exp" (@child2.start)="track($event)"></div>
</div>
`
})
class Cmp {
public parent1Exp = '';
public parent2Exp = '';
public child1Exp = '';
public child2Exp = '';
public log: string[] = [];
track(event: any) { this.log.push(`${event.triggerName}-${event.phaseName}`); }
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
flushMicrotasks();
const cmp = fixture.componentInstance;
cmp.log = [];
cmp.parent1Exp = 'go';
cmp.parent2Exp = 'go';
cmp.child1Exp = 'go';
cmp.child2Exp = 'go';
fixture.detectChanges();
flushMicrotasks();
expect(cmp.log).toEqual(
['parent1-start', 'parent2-start', 'child1-start', 'child2-start']);
cmp.parent1Exp = 'go-again';
cmp.parent2Exp = 'go-again';
cmp.child1Exp = 'go-again';
cmp.child2Exp = 'go-again';
fixture.detectChanges();
flushMicrotasks();
}));
it('should stretch the starting keyframe of a child animation queries are issued by the parent',
() => {
@Component({

View File

@ -40,7 +40,7 @@ let _inFakeAsyncCall = false;
*
* ## Example
*
* {@example testing/ts/fake_async.ts region='basic'}
* {@example core/testing/ts/fake_async.ts region='basic'}
*
* @param fn
* @returns The function wrapped to be executed in the fakeAsync zone
@ -107,7 +107,7 @@ function _getFakeAsyncZoneSpec(): any {
*
* ## Example
*
* {@example testing/ts/fake_async.ts region='basic'}
* {@example core/testing/ts/fake_async.ts region='basic'}
*
* @experimental
*/

View File

@ -75,6 +75,8 @@ const EMAIL_REGEXP =
export class Validators {
/**
* Validator that requires controls to have a value greater than a number.
*`min()` exists only as a function, not as a directive. For example,
* `control = new FormControl('', Validators.min(3));`.
*/
static min(min: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
@ -90,6 +92,8 @@ export class Validators {
/**
* Validator that requires controls to have a value less than a number.
* `max()` exists only as a function, not as a directive. For example,
* `control = new FormControl('', Validators.max(15));`.
*/
static max(max: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {

View File

@ -109,29 +109,33 @@ class Recognizer {
if ((route.outlet || PRIMARY_OUTLET) !== outlet) throw new NoMatch();
let snapshot: ActivatedRouteSnapshot;
let consumedSegments: UrlSegment[] = [];
let rawSlicedSegments: UrlSegment[] = [];
if (route.path === '**') {
const params = segments.length > 0 ? last(segments) !.parameters : {};
const snapshot = new ActivatedRouteSnapshot(
snapshot = new ActivatedRouteSnapshot(
segments, params, Object.freeze(this.urlTree.queryParams), this.urlTree.fragment !,
getData(route), outlet, route.component !, route, getSourceSegmentGroup(rawSegment),
getPathIndexShift(rawSegment) + segments.length, getResolve(route));
return [new TreeNode<ActivatedRouteSnapshot>(snapshot, [])];
} else {
const result: MatchResult = match(rawSegment, route, segments);
consumedSegments = result.consumedSegments;
rawSlicedSegments = segments.slice(result.lastChild);
snapshot = new ActivatedRouteSnapshot(
consumedSegments, result.parameters, Object.freeze(this.urlTree.queryParams),
this.urlTree.fragment !, getData(route), outlet, route.component !, route,
getSourceSegmentGroup(rawSegment),
getPathIndexShift(rawSegment) + consumedSegments.length, getResolve(route));
}
const {consumedSegments, parameters, lastChild} = match(rawSegment, route, segments);
const rawSlicedSegments = segments.slice(lastChild);
const childConfig = getChildConfig(route);
const childConfig: Route[] = getChildConfig(route);
const {segmentGroup, slicedSegments} =
split(rawSegment, consumedSegments, rawSlicedSegments, childConfig);
const snapshot = new ActivatedRouteSnapshot(
consumedSegments, parameters, Object.freeze(this.urlTree.queryParams),
this.urlTree.fragment !, getData(route), outlet, route.component !, route,
getSourceSegmentGroup(rawSegment), getPathIndexShift(rawSegment) + consumedSegments.length,
getResolve(route));
if (slicedSegments.length === 0 && segmentGroup.hasChildren()) {
const children = this.processChildren(childConfig, segmentGroup);
return [new TreeNode<ActivatedRouteSnapshot>(snapshot, children)];
@ -166,7 +170,13 @@ function getChildConfig(route: Route): Route[] {
return [];
}
function match(segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[]) {
interface MatchResult {
consumedSegments: UrlSegment[];
lastChild: number;
parameters: any;
}
function match(segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[]): MatchResult {
if (route.path === '') {
if (route.pathMatch === 'full' && (segmentGroup.hasChildren() || segments.length > 0)) {
throw new NoMatch();

View File

@ -3470,10 +3470,10 @@ describe('Integration', () => {
declarations: [LazyLoadedComponent],
imports: [RouterModule.forChild([{path: '', component: LazyLoadedComponent}])],
})
class LoadedModule {
class LazyLoadedModule {
}
loader.stubbedModules = {lazy: LoadedModule};
loader.stubbedModules = {lazy: LazyLoadedModule};
const fixture = createRoot(router, RootCmp);
router.resetConfig([{path: '**', loadChildren: 'lazy'}]);
@ -3482,6 +3482,7 @@ describe('Integration', () => {
advance(fixture);
expect(location.path()).toEqual('/lazy');
expect(fixture.nativeElement).toHaveText('lazy-loaded');
})));
describe('preloading', () => {

View File

@ -88,6 +88,11 @@ export class Driver implements Debuggable, UpdateSource {
private lastUpdateCheck: number|null = null;
/**
* Whether there is a check for updates currently scheduled due to navigation.
*/
private scheduledNavUpdateCheck: boolean = false;
/**
* A scheduler which manages a queue of tasks that need to be executed when the SW is
* not doing any other work (not processing any other requests).
@ -327,6 +332,15 @@ export class Driver implements Debuggable, UpdateSource {
return this.safeFetch(event.request);
}
// On navigation requests, check for new updates.
if (event.request.mode === 'navigate' && !this.scheduledNavUpdateCheck) {
this.scheduledNavUpdateCheck = true;
this.idle.schedule('check-updates-on-navigation', async() => {
this.scheduledNavUpdateCheck = false;
await this.checkForUpdate();
});
}
// Decide which version of the app to use to serve this request. This is asynchronous as in
// some cases, a record will need to be written to disk about the assignment that is made.
const appVersion = await this.assignVersion(event);

View File

@ -337,6 +337,41 @@ export function main() {
serverUpdate.assertNoOtherRequests();
});
async_it('checks for updates on navigation', async() => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
server.clearRequests();
expect(await makeRequest(scope, '/foo.txt', 'default', {
mode: 'navigate',
})).toEqual('this is foo');
scope.advance(12000);
await driver.idle.empty;
server.assertSawRequestFor('ngsw.json');
});
async_it('does not make concurrent checks for updates on navigation', async() => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
server.clearRequests();
expect(await makeRequest(scope, '/foo.txt', 'default', {
mode: 'navigate',
})).toEqual('this is foo');
expect(await makeRequest(scope, '/foo.txt', 'default', {
mode: 'navigate',
})).toEqual('this is foo');
scope.advance(12000);
await driver.idle.empty;
server.assertSawRequestFor('ngsw.json');
server.assertNoOtherRequests();
});
async_it('preserves multiple client assignments across restarts', async() => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;

View File

@ -243,22 +243,36 @@ try {
}
/**
* Resets the AngularJS library.
* @deprecated Use {@link setAngularJSGlobal} instead.
*/
export function setAngularLib(ng: any): void {
setAngularJSGlobal(ng);
}
/**
* @deprecated Use {@link getAngularJSGlobal} instead.
*/
export function getAngularLib(): any {
return getAngularJSGlobal();
}
/**
* Resets the AngularJS global.
*
* Used when angularjs is loaded lazily, and not available on `window`.
* Used when AngularJS is loaded lazily, and not available on `window`.
*
* @stable
*/
export function setAngularLib(ng: any): void {
export function setAngularJSGlobal(ng: any): void {
angular = ng;
}
/**
* Returns the current version of the AngularJS library.
* Returns the current AngularJS global.
*
* @stable
*/
export function getAngularLib(): any {
export function getAngularJSGlobal(): any {
return angular;
}

View File

@ -12,7 +12,7 @@
* Entry point for all public APIs of this package. allowing
* Angular 1 and Angular 2+ to run side by side in the same application.
*/
export {getAngularLib, setAngularLib} from './src/common/angular1';
export {getAngularJSGlobal, getAngularLib, setAngularJSGlobal, setAngularLib} from './src/common/angular1';
export {downgradeComponent} from './src/common/downgrade_component';
export {downgradeInjectable} from './src/common/downgrade_injectable';
export {VERSION} from './src/common/version';
@ -20,4 +20,5 @@ export {downgradeModule} from './src/static/downgrade_module';
export {UpgradeComponent} from './src/static/upgrade_component';
export {UpgradeModule} from './src/static/upgrade_module';
// This file only re-exports content of the `src` folder. Keep it that way.

View File

@ -12,7 +12,7 @@ import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import * as angular from '@angular/upgrade/src/common/angular1';
import {$INJECTOR, INJECTOR_KEY} from '@angular/upgrade/src/common/constants';
import {UpgradeModule, downgradeInjectable, getAngularLib, setAngularLib} from '@angular/upgrade/static';
import {UpgradeModule, downgradeInjectable, getAngularJSGlobal, setAngularJSGlobal} from '@angular/upgrade/static';
import {bootstrap, html} from '../test_helpers';
@ -103,9 +103,9 @@ export function main() {
it('should allow resetting angular at runtime', async(() => {
let wrappedBootstrapepedCalled = false;
const n: any = getAngularLib();
const n: any = getAngularJSGlobal();
setAngularLib({
setAngularJSGlobal({
bootstrap: (...args: any[]) => {
wrappedBootstrapepedCalled = true;
n.bootstrap(...args);

View File

@ -36,7 +36,9 @@ fi
setEnvVar NODE_VERSION 8.9.1
setEnvVar YARN_VERSION 1.0.2
setEnvVar CHROMIUM_VERSION 499098 # Chrome 62 linux stable, see https://www.chromium.org/developers/calendar
# Pin to a Chromium version that does not cause the aio e2e tests to flake. (See https://github.com/angular/angular/pull/20403.)
# Revision 494239 (which was part of Chrome 62.0.3186.0) is the last version that does not cause flakes. (Latest revision checked: 508578)
setEnvVar CHROMIUM_VERSION 494239 # Chrome 62 linux stable, see https://www.chromium.org/developers/calendar
setEnvVar CHROMEDRIVER_VERSION_ARG "--versions.chrome 2.33"
setEnvVar BAZEL_VERSION 0.8.1
setEnvVar SAUCE_CONNECT_VERSION 4.4.9

View File

@ -50,8 +50,7 @@ travisFoldEnd "bower-install"
if [[ ${TRAVIS} &&
${CI_MODE} == "aio" ||
${CI_MODE} == "aio_e2e" ||
${CI_MODE} == "aio_tools_test" ||
${CI_MODE} == "aio_optional"
${CI_MODE} == "aio_tools_test"
]]; then
# angular.io: Install all yarn dependencies according to angular.io/yarn.lock
travisFoldStart "yarn-install.aio"
@ -84,8 +83,7 @@ if [[ ${TRAVIS} &&
${CI_MODE} == "e2e" ||
${CI_MODE} == "e2e_2" ||
${CI_MODE} == "aio" ||
${CI_MODE} == "aio_e2e" ||
${CI_MODE} == "aio_optional"
${CI_MODE} == "aio_e2e"
]]; then
travisFoldStart "install-chromium"
(

View File

@ -1,18 +0,0 @@
#!/usr/bin/env bash
set -u -e -o pipefail
# Setup environment
readonly thisDir=$(cd $(dirname $0); pwd)
source ${thisDir}/_travis-fold.sh
# run in subshell to avoid polluting cwd
(
cd ${PROJECT_ROOT}/aio
# Run e2e tests
travisFoldStart "test.aio.e2e"
yarn setup
yarn e2e
travisFoldEnd "test.aio.e2e"
)

View File

@ -31,6 +31,12 @@ source ${thisDir}/_travis-fold.sh
travisFoldEnd "test.aio.unit"
# Run e2e tests
travisFoldStart "test.aio.e2e"
yarn e2e
travisFoldEnd "test.aio.e2e"
# Run unit tests for aio/aio-builds-setup
travisFoldStart "test.aio.aio-builds-setup"
./aio-builds-setup/scripts/test.sh

View File

@ -46,9 +46,6 @@ case ${CI_MODE} in
aio_e2e)
${thisDir}/test-aio-e2e.sh
;;
aio_optional)
${thisDir}/test-aio-optional.sh
;;
bazel)
${thisDir}/test-bazel.sh
;;

View File

@ -67,17 +67,20 @@ else
fi
CHERRY_PICK_PR="git cherry-pick upstream/pr/$PR_NUMBER~$PR_SHA_COUNT..upstream/pr/$PR_NUMBER"
FETCH_PR="git fetch https://github.com/angular/angular.git pull/$PR_NUMBER/head:angular/pr/$PR_NUMBER"
CHERRY_PICK_PR="git cherry-pick angular/pr/$PR_NUMBER~$PR_SHA_COUNT..angular/pr/$PR_NUMBER"
REWRITE_MESSAGE="git filter-branch -f --msg-filter \"$BASEDIR/utils/github_closes.js $PR_NUMBER\" HEAD~$PR_SHA_COUNT..HEAD"
echo "======================"
echo "GitHub Merge PR Steps"
echo "======================"
echo " $FETCH_PR"
echo " $CHERRY_PICK_PR"
echo " $REWRITE_MESSAGE"
echo "----------------------"
echo ">>> Cherry Pick: $CHERRY_PICK_PR"
$FETCH_PR
$CHERRY_PICK_PR
echo

View File

@ -47,3 +47,6 @@ test --experimental_ui
# Don't be spammy in the continuous integration logs
build:ci --noshow_progress
# Don't run manual tests on CI
test:ci --test_tag_filters=-manual

View File

@ -6,29 +6,40 @@
* found in the LICENSE file at https://angular.io/license
*/
// NOTE: This list shold be in sync with aio/tools/transforms/angular-api-package/index.js
const entrypoints = [
'dist/packages-dist/core/core.d.ts', 'dist/packages-dist/core/testing.d.ts',
'dist/packages-dist/common/common.d.ts', 'dist/packages-dist/common/testing.d.ts',
'dist/packages-dist/common/http.d.ts', 'dist/packages-dist/common/http/testing.d.ts',
'dist/packages-dist/animations/animations.d.ts',
'dist/packages-dist/animations/browser.d.ts',
'dist/packages-dist/animations/browser/testing.d.ts',
'dist/packages-dist/common/common.d.ts',
'dist/packages-dist/common/testing.d.ts',
'dist/packages-dist/common/http.d.ts',
'dist/packages-dist/common/http/testing.d.ts',
// The API surface of the compiler is currently unstable - all of the important APIs are exposed
// via @angular/core, @angular/platform-browser or @angular/platform-browser-dynamic instead.
//'dist/packages-dist/compiler/index.d.ts',
//'dist/packages-dist/compiler/testing.d.ts',
'dist/packages-dist/upgrade/upgrade.d.ts', 'dist/packages-dist/upgrade/static.d.ts',
'dist/packages-dist/core/core.d.ts',
'dist/packages-dist/core/testing.d.ts',
'dist/packages-dist/forms/forms.d.ts',
'dist/packages-dist/http/http.d.ts',
'dist/packages-dist/http/testing.d.ts',
'dist/packages-dist/platform-browser/platform-browser.d.ts',
'dist/packages-dist/platform-browser/animations.d.ts',
'dist/packages-dist/platform-browser/testing.d.ts',
'dist/packages-dist/platform-browser-dynamic/platform-browser-dynamic.d.ts',
'dist/packages-dist/platform-browser-dynamic/testing.d.ts',
'dist/packages-dist/platform-webworker/platform-webworker.d.ts',
'dist/packages-dist/platform-webworker-dynamic/platform-webworker-dynamic.d.ts',
'dist/packages-dist/platform-server/platform-server.d.ts',
'dist/packages-dist/platform-server/testing.d.ts', 'dist/packages-dist/http/http.d.ts',
'dist/packages-dist/http/testing.d.ts', 'dist/packages-dist/forms/forms.d.ts',
'dist/packages-dist/router/router.d.ts', 'dist/packages-dist/animations/animations.d.ts',
'dist/packages-dist/platform-server/testing.d.ts',
'dist/packages-dist/router/router.d.ts',
'dist/packages-dist/router/testing.d.ts',
'dist/packages-dist/router/upgrade.d.ts',
'dist/packages-dist/service-worker/service-worker.d.ts',
'dist/packages-dist/service-worker/config.d.ts', 'dist/packages-dist/animations/browser.d.ts',
'dist/packages-dist/animations/browser/testing.d.ts',
'dist/packages-dist/platform-browser/animations.d.ts'
'dist/packages-dist/service-worker/config.d.ts',
'dist/packages-dist/upgrade/upgrade.d.ts',
'dist/packages-dist/upgrade/static.d.ts',
];
const publicApiDir = 'tools/public_api_guard';

View File

@ -0,0 +1,16 @@
/** @stable */
export declare class RouterTestingModule {
static withRoutes(routes: Routes): ModuleWithProviders;
}
/** @stable */
export declare function setupTestingRouter(urlSerializer: UrlSerializer, contexts: ChildrenOutletContexts, location: Location, loader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector, routes: Route[][], urlHandlingStrategy?: UrlHandlingStrategy): Router;
/** @stable */
export declare class SpyNgModuleFactoryLoader implements NgModuleFactoryLoader {
stubbedModules: {
[path: string]: any;
};
constructor(compiler: Compiler);
load(path: string): Promise<NgModuleFactory<any>>;
}

View File

@ -0,0 +1,10 @@
/** @experimental */
export declare const RouterUpgradeInitializer: {
provide: InjectionToken<((compRef: ComponentRef<any>) => void)[]>;
multi: boolean;
useFactory: (ngUpgrade: UpgradeModule) => () => void;
deps: (typeof UpgradeModule)[];
};
/** @experimental */
export declare function setUpLocationSync(ngUpgrade: UpgradeModule): void;

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