Compare commits
63 Commits
Author | SHA1 | Date | |
---|---|---|---|
6353b77f89 | |||
d9c40b7dd5 | |||
a36dfd453b | |||
ced575fd10 | |||
3b63e168e2 | |||
a1d4c2dbf1 | |||
a33182c4ee | |||
66cc2fabf1 | |||
cf8269ee94 | |||
c7241ca0e6 | |||
4a49e19bd7 | |||
4a3a74b7b0 | |||
6aa013cb63 | |||
b9531f90b8 | |||
f239e8496e | |||
57bed3fe73 | |||
c011ffae30 | |||
756dd34519 | |||
267ebf323e | |||
49c45f336e | |||
71236737ab | |||
390837f76e | |||
fbcf7dc889 | |||
5c8c7d8515 | |||
99cc591f63 | |||
e3140ae888 | |||
ca815106c9 | |||
d6da7988c0 | |||
41e1951ffb | |||
c02f97ce4e | |||
be9a7371b8 | |||
1f469497da | |||
8a5f0f7a64 | |||
8bcb093bfa | |||
abc3a1e844 | |||
9fbf850897 | |||
0c9f7b032a | |||
8b21be5da3 | |||
0a9a16183f | |||
28555bc1e7 | |||
d09d4971b3 | |||
f8a4d14c8f | |||
d713225128 | |||
5d3af4cad4 | |||
a7fe063aa5 | |||
affa54ddf9 | |||
02782c10ea | |||
8e5a3a42d6 | |||
dbef8ff2b0 | |||
c0f08be3fb | |||
f56cbcc1e5 | |||
1b86570b60 | |||
923227dec2 | |||
53b9de300b | |||
da587cb9ef | |||
e6a28054d8 | |||
69ed916b42 | |||
c3e8731145 | |||
501f01e9ac | |||
baeec4dbe2 | |||
160a154553 | |||
9dd60a5fb0 | |||
672733608b |
@ -7,17 +7,25 @@
|
||||
# To validate changes, use an online parser, eg.
|
||||
# http://yaml-online-parser.appspot.com/
|
||||
|
||||
# Variables
|
||||
|
||||
## IMPORTANT
|
||||
# If you change the `docker_image` version, also change the `cache_key` suffix and the version of
|
||||
# `com_github_bazelbuild_buildtools` in the `/WORKSPACE` file.
|
||||
var_1: &docker_image angular/ngcontainer:0.0.8
|
||||
var_2: &cache_key angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.0.8
|
||||
|
||||
# Settings common to each job
|
||||
anchor_1: &job_defaults
|
||||
working_directory: ~/ng
|
||||
docker:
|
||||
- image: angular/ngcontainer:0.0.6
|
||||
- image: *docker_image
|
||||
|
||||
# After checkout, rebase on top of master.
|
||||
# Similar to travis behavior, but not quite the same.
|
||||
# See https://discuss.circleci.com/t/1662
|
||||
anchor_2: &post_checkout
|
||||
post: git pull --ff-only origin "refs/pull/${CI_PULL_REQUEST//*pull\//}/merge"
|
||||
post: git pull --ff-only origin "refs/pull/${CIRCLE_PULL_REQUEST//*pull\//}/merge"
|
||||
|
||||
version: 2
|
||||
jobs:
|
||||
@ -26,8 +34,13 @@ jobs:
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
# Check BUILD.bazel formatting before we have a node_modules directory
|
||||
# Then we don't need any exclude pattern to avoid checking those files
|
||||
- run: 'buildifier -mode=check $(find . -type f \( -name BUILD.bazel -or -name BUILD \)) ||
|
||||
(echo "BUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)'
|
||||
|
||||
- restore_cache:
|
||||
key: angular-{{ .Branch }}-{{ checksum "yarn.lock" }}
|
||||
key: *cache_key
|
||||
|
||||
- run: yarn install --frozen-lockfile --non-interactive
|
||||
- run: ./node_modules/.bin/gulp lint
|
||||
@ -38,13 +51,17 @@ jobs:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: angular-{{ .Branch }}-{{ checksum "yarn.lock" }}
|
||||
key: *cache_key
|
||||
|
||||
- run: bazel info release
|
||||
- run: bazel run @yarn//:yarn
|
||||
- run: bazel build packages/...
|
||||
- run: bazel test @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: angular-{{ .Branch }}-{{ checksum "yarn.lock" }}
|
||||
key: *cache_key
|
||||
paths:
|
||||
- "node_modules"
|
||||
|
||||
|
@ -44,6 +44,7 @@ groups:
|
||||
include:
|
||||
- "*"
|
||||
exclude:
|
||||
- ".circleci/*"
|
||||
- "aio/*"
|
||||
- "integration/*"
|
||||
- "modules/*"
|
||||
@ -73,6 +74,7 @@ groups:
|
||||
users:
|
||||
- alexeagle #primary
|
||||
- chuckjaz
|
||||
- IgorMinar
|
||||
- vikerman #fallback
|
||||
|
||||
build-and-ci:
|
||||
|
@ -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
|
||||
|
15
BUILD.bazel
@ -1,4 +1,5 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
exports_files(["tsconfig.json"])
|
||||
|
||||
# This rule belongs in node_modules/BUILD
|
||||
@ -11,17 +12,25 @@ filegroup(
|
||||
# bazel query "deps(:node_modules)" | wc -l
|
||||
# This won't scale in the general case.
|
||||
# TODO(alexeagle): figure out what to do
|
||||
srcs = glob(["/".join(["node_modules", pkg, "**", ext]) for pkg in [
|
||||
srcs = glob(["/".join([
|
||||
"node_modules",
|
||||
pkg,
|
||||
"**",
|
||||
ext,
|
||||
]) for pkg in [
|
||||
"jasmine",
|
||||
"typescript",
|
||||
"zone.js",
|
||||
"rxjs",
|
||||
"@types",
|
||||
"tsutils",
|
||||
"@types/jasmine",
|
||||
"@types/node",
|
||||
"@types/source-map",
|
||||
"tsickle",
|
||||
"hammerjs",
|
||||
"protobufjs",
|
||||
"bytebuffer",
|
||||
"reflect-metadata",
|
||||
"source-map-support",
|
||||
"minimist",
|
||||
] for ext in [
|
||||
"*.js",
|
||||
|
35
CHANGELOG.md
@ -1,3 +1,38 @@
|
||||
<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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** ensure multi-level route leave animations are queryable ([#20787](https://github.com/angular/angular/issues/20787)) ([d09d497](https://github.com/angular/angular/commit/d09d497)), closes [#19807](https://github.com/angular/angular/issues/19807)
|
||||
* **animations:** ensure the web-animations driver properly handles empty keyframes ([#20648](https://github.com/angular/angular/issues/20648)) ([c3e8731](https://github.com/angular/angular/commit/c3e8731)), closes [#15858](https://github.com/angular/angular/issues/15858)
|
||||
* **animations:** properly recover and cleanup DOM when CD failures occur ([#20719](https://github.com/angular/angular/issues/20719)) ([e6a2805](https://github.com/angular/angular/commit/e6a2805)), closes [#19093](https://github.com/angular/angular/issues/19093)
|
||||
* **animations:** support webkit-based vendor prefixes for prop validations ([#19055](https://github.com/angular/angular/issues/19055)) ([501f01e](https://github.com/angular/angular/commit/501f01e)), closes [#18921](https://github.com/angular/angular/issues/18921)
|
||||
* **bazel:** don't equate moduleName with fileName ([#20895](https://github.com/angular/angular/issues/20895)) ([0c9f7b0](https://github.com/angular/angular/commit/0c9f7b0))
|
||||
* **compiler:** support referencing enums in namespaces ([#20947](https://github.com/angular/angular/issues/20947)) ([d6da798](https://github.com/angular/angular/commit/d6da798)), closes [#18170](https://github.com/angular/angular/issues/18170)
|
||||
* **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))
|
||||
|
||||
|
||||
|
||||
<a name="5.1.0"></a>
|
||||
# [5.1.0](https://github.com/angular/angular/compare/5.1.0-rc.1...5.1.0) (2017-12-06)
|
||||
|
||||
|
40
WORKSPACE
@ -5,19 +5,51 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
|
||||
git_repository(
|
||||
name = "build_bazel_rules_nodejs",
|
||||
remote = "https://github.com/bazelbuild/rules_nodejs.git",
|
||||
commit = "0.2.1",
|
||||
tag = "0.3.1",
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_nodejs//:defs.bzl", "node_repositories")
|
||||
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories")
|
||||
|
||||
check_bazel_version("0.8.1")
|
||||
node_repositories(package_json = ["//:package.json"])
|
||||
|
||||
local_repository(
|
||||
git_repository(
|
||||
name = "build_bazel_rules_typescript",
|
||||
path = "node_modules/@bazel/typescript",
|
||||
remote = "https://github.com/bazelbuild/rules_typescript.git",
|
||||
tag = "0.6.0",
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_typescript//:defs.bzl", "ts_repositories")
|
||||
|
||||
ts_repositories()
|
||||
|
||||
local_repository(
|
||||
name = "angular",
|
||||
path = "packages/bazel",
|
||||
)
|
||||
|
||||
local_repository(
|
||||
name = "rxjs",
|
||||
path = "node_modules/rxjs/src",
|
||||
)
|
||||
|
||||
git_repository(
|
||||
name = "com_github_bazelbuild_buildtools",
|
||||
remote = "https://github.com/bazelbuild/buildtools.git",
|
||||
# Note, this commit matches the version of buildifier in angular/ngcontainer
|
||||
# If you change this, also check if it matches the version in the angular/ngcontainer
|
||||
# version in /.circleci/config.yml
|
||||
commit = "b3b620e8bcff18ed3378cd3f35ebeb7016d71f71",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "io_bazel_rules_go",
|
||||
url = "https://github.com/bazelbuild/rules_go/releases/download/0.7.1/rules_go-0.7.1.tar.gz",
|
||||
sha256 = "341d5eacef704415386974bc82a1783a8b7ffbff2ab6ba02375e1ca20d9b031c",
|
||||
)
|
||||
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_toolchains")
|
||||
|
||||
go_rules_dependencies()
|
||||
|
||||
go_register_toolchains()
|
||||
|
@ -5,7 +5,7 @@
|
||||
"!**/*.d.ts",
|
||||
"!**/*.js",
|
||||
"!**/*.[0,1,2].*",
|
||||
"**/dummy.module.ts"
|
||||
"!**/dummy.module.ts"
|
||||
],
|
||||
"tags": ["dependency", "di"]
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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';
|
||||
}
|
@ -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: ');
|
||||
});
|
||||
});
|
@ -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"
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "Service Worker",
|
||||
"basePath": "src/",
|
||||
"tags": ["service worker"]
|
||||
}
|
@ -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>
|
@ -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!');
|
||||
}));
|
||||
});
|
20
aio/content/examples/service-worker-getting-started/src/app/app.component.ts
Executable 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}`);
|
||||
}
|
||||
}
|
@ -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
|
@ -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>
|
@ -100,7 +100,7 @@ locale data itself.
|
||||
If you want to register the imported locale data with another locale id, use the second parameter to
|
||||
specify a custom locale id. For example, Angular's locale data defines the locale id for French as
|
||||
"fr". You can use the second parameter to associate the imported French locale data with the custom
|
||||
locale id "fr-FR instead of "fr".
|
||||
locale id "fr-FR" instead of "fr".
|
||||
|
||||
The files in `@angular/common/locales` contain most of the locale data that you
|
||||
need, but some advanced formatting options might only be available in the extra dataset that you can
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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—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).
|
@ -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—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.
|
||||
|
@ -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
|
||||
|
||||
@ -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).
|
||||
|
||||
|
@ -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.
|
||||

|
||||
|
||||
<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).
|
@ -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).
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 28 KiB |
@ -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",
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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": [
|
||||
{
|
||||
|
@ -56,7 +56,7 @@
|
||||
"~~check-env": "node scripts/check-environment",
|
||||
"~~build": "ng build --target=production --environment=stable -sm",
|
||||
"post~~build": "yarn sw-manifest && yarn sw-copy",
|
||||
"~~update-webdriver": "webdriver-manager update --standalone false --gecko false"
|
||||
"~~update-webdriver": "webdriver-manager update --standalone false --gecko false $CHROMEDRIVER_VERSION_ARG"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9.1 <9.0.0",
|
||||
@ -64,31 +64,31 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^5.0.0",
|
||||
"@angular/cdk": "^2.0.0-beta.12",
|
||||
"@angular/common": "^5.0.0",
|
||||
"@angular/compiler": "^5.0.0",
|
||||
"@angular/core": "^5.0.0-rc.9",
|
||||
"@angular/forms": "^5.0.0",
|
||||
"@angular/http": "^5.0.0",
|
||||
"@angular/material": "^2.0.0-beta.12",
|
||||
"@angular/platform-browser": "^5.0.0",
|
||||
"@angular/platform-browser-dynamic": "^5.0.0",
|
||||
"@angular/platform-server": "^5.0.0",
|
||||
"@angular/router": "^5.0.0",
|
||||
"@angular/animations": "^5.1.0-beta.2",
|
||||
"@angular/cdk": "^5.0.0-rc.1",
|
||||
"@angular/common": "^5.1.0-beta.2",
|
||||
"@angular/compiler": "^5.1.0-beta.2",
|
||||
"@angular/core": "^5.1.0-beta.2",
|
||||
"@angular/forms": "^5.1.0-beta.2",
|
||||
"@angular/http": "^5.1.0-beta.2",
|
||||
"@angular/material": "^5.0.0-rc.1",
|
||||
"@angular/platform-browser": "^5.1.0-beta.2",
|
||||
"@angular/platform-browser-dynamic": "^5.1.0-beta.2",
|
||||
"@angular/platform-server": "^5.1.0-beta.2",
|
||||
"@angular/router": "^5.1.0-beta.2",
|
||||
"@angular/service-worker": "^1.0.0-beta.16",
|
||||
"classlist.js": "^1.1.20150312",
|
||||
"core-js": "^2.4.1",
|
||||
"jasmine": "^2.6.0",
|
||||
"ng-pwa-tools": "^0.0.10",
|
||||
"rxjs": "^5.5.0",
|
||||
"rxjs": "^5.5.2",
|
||||
"tslib": "^1.7.1",
|
||||
"web-animations-js": "^2.2.5",
|
||||
"zone.js": "0.8.16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/cli": "^1.5.0",
|
||||
"@angular/compiler-cli": "^5.0.0",
|
||||
"@angular/cli": "^1.6.0-rc.0",
|
||||
"@angular/compiler-cli": "^5.1.0-beta.2",
|
||||
"@types/jasmine": "^2.5.52",
|
||||
"@types/jasminewd2": "^2.0.3",
|
||||
"@types/node": "~6.0.60",
|
||||
@ -113,7 +113,7 @@
|
||||
"http-server": "^0.9.0",
|
||||
"ignore": "^3.3.3",
|
||||
"image-size": "^0.5.1",
|
||||
"jasmine-core": "^2.6.4",
|
||||
"jasmine-core": "^2.8.0",
|
||||
"jasmine-spec-reporter": "^4.1.0",
|
||||
"jsdom": "^9.12.0",
|
||||
"karma": "^1.7.0",
|
||||
|
@ -2,19 +2,19 @@
|
||||
"aio": {
|
||||
"master": {
|
||||
"gzip7": {
|
||||
"inline": 925,
|
||||
"main": 119519,
|
||||
"polyfills": 11863
|
||||
"inline": 941,
|
||||
"main": 116124,
|
||||
"polyfills": 11860
|
||||
},
|
||||
"gzip9": {
|
||||
"inline": 925,
|
||||
"main": 119301,
|
||||
"polyfills": 11861
|
||||
"inline": 941,
|
||||
"main": 115954,
|
||||
"polyfills": 11858
|
||||
},
|
||||
"uncompressed": {
|
||||
"inline": 1533,
|
||||
"main": 486493,
|
||||
"polyfills": 37068
|
||||
"inline": 1558,
|
||||
"main": 456432,
|
||||
"polyfills": 37070
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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" (docRendered)="onDocRendered()"></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>
|
||||
|
||||
|
@ -6,12 +6,14 @@ import { HttpClient } from '@angular/common/http';
|
||||
import { MatProgressBar, MatSidenav } from '@angular/material';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { AppModule } from './app.module';
|
||||
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
||||
import { Deployment } from 'app/shared/deployment.service';
|
||||
import { EmbedComponentsService } from 'app/embed-components/embed-components.service';
|
||||
import { GaService } from 'app/shared/ga.service';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
@ -24,7 +26,7 @@ import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
|
||||
import { SearchResultsComponent } from 'app/shared/search-results/search-results.component';
|
||||
import { SearchService } from 'app/search/search.service';
|
||||
import { SelectComponent } from 'app/shared/select/select.component';
|
||||
import { TocComponent } from 'app/embedded/toc/toc.component';
|
||||
import { TocComponent } from 'app/layout/toc/toc.component';
|
||||
import { TocItem, TocService } from 'app/shared/toc.service';
|
||||
|
||||
const sideBySideBreakPoint = 992;
|
||||
@ -55,13 +57,18 @@ describe('AppComponent', () => {
|
||||
tocService = de.injector.get(TocService);
|
||||
};
|
||||
|
||||
|
||||
describe('with proper DocViewer', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
DocViewerComponent.animationsEnabled = false;
|
||||
|
||||
createTestingModule('a/b');
|
||||
initializeTest();
|
||||
});
|
||||
|
||||
afterEach(() => DocViewerComponent.animationsEnabled = true);
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeDefined();
|
||||
});
|
||||
@ -147,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();
|
||||
});
|
||||
@ -184,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();
|
||||
});
|
||||
@ -246,25 +251,22 @@ describe('AppComponent', () => {
|
||||
const sidenavBackdrop = fixture.debugElement.query(By.css('.mat-drawer-backdrop')).nativeElement;
|
||||
sidenavBackdrop.click();
|
||||
fixture.detectChanges();
|
||||
expect(sidenav.opened).toBe(false);
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
|
||||
it('should close when nav to another guide page', () => {
|
||||
locationService.go('guide/bags');
|
||||
fixture.detectChanges();
|
||||
expect(sidenav.opened).toBe(false);
|
||||
navigateTo('guide/bags');
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
|
||||
it('should close when nav to api page', () => {
|
||||
locationService.go('api');
|
||||
fixture.detectChanges();
|
||||
expect(sidenav.opened).toBe(false);
|
||||
navigateTo('api');
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
|
||||
it('should close again when nav to market page', () => {
|
||||
locationService.go('features');
|
||||
fixture.detectChanges();
|
||||
expect(sidenav.opened).toBe(false);
|
||||
navigateTo('features');
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
@ -318,96 +320,7 @@ 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', () => {
|
||||
locationService.go('guide/pipes');
|
||||
fixture.detectChanges();
|
||||
checkHostClass('sidenav', 'open');
|
||||
|
||||
sidenav.close();
|
||||
sidenav.onClose.next();
|
||||
fixture.detectChanges();
|
||||
checkHostClass('sidenav', 'closed');
|
||||
|
||||
sidenav.open();
|
||||
sidenav.onOpen.next();
|
||||
fixture.detectChanges();
|
||||
checkHostClass('sidenav', 'open');
|
||||
});
|
||||
|
||||
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');
|
||||
fixture.detectChanges();
|
||||
@ -447,16 +360,19 @@ describe('AppComponent', () => {
|
||||
const scrollDelay = 500;
|
||||
let scrollService: ScrollService;
|
||||
let scrollSpy: jasmine.Spy;
|
||||
let scrollToTopSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
scrollService = fixture.debugElement.injector.get(ScrollService);
|
||||
scrollSpy = spyOn(scrollService, 'scroll');
|
||||
scrollToTopSpy = spyOn(scrollService, 'scrollToTop');
|
||||
});
|
||||
|
||||
it('should not scroll immediately when the docId (path) changes', () => {
|
||||
locationService.go('guide/pipes');
|
||||
// deliberately not calling `fixture.detectChanges` because don't want `onDocRendered`
|
||||
// deliberately not calling `fixture.detectChanges` because don't want `onDocInserted`
|
||||
expect(scrollSpy).not.toHaveBeenCalled();
|
||||
expect(scrollToTopSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should scroll when just the hash changes (# alone)', () => {
|
||||
@ -486,7 +402,7 @@ describe('AppComponent', () => {
|
||||
expect(scrollSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should scroll when e-nav to the empty path', () => {
|
||||
it('should scroll when re-nav to the empty path', () => {
|
||||
locationService.go('');
|
||||
scrollSpy.calls.reset();
|
||||
|
||||
@ -494,17 +410,29 @@ describe('AppComponent', () => {
|
||||
expect(scrollSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should scroll after a delay when call onDocRendered directly', fakeAsync(() => {
|
||||
component.onDocRendered();
|
||||
it('should scroll to top when call `onDocRemoved` directly', () => {
|
||||
scrollToTopSpy.calls.reset();
|
||||
|
||||
component.onDocRemoved();
|
||||
expect(scrollToTopSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should scroll after a delay when call `onDocInserted` directly', fakeAsync(() => {
|
||||
component.onDocInserted();
|
||||
expect(scrollSpy).not.toHaveBeenCalled();
|
||||
|
||||
tick(scrollDelay);
|
||||
expect(scrollSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should scroll (via onDocRendered) when finish navigating to a new doc', fakeAsync(() => {
|
||||
it('should scroll (via `onDocInserted`) when finish navigating to a new doc', fakeAsync(() => {
|
||||
expect(scrollToTopSpy).not.toHaveBeenCalled();
|
||||
|
||||
locationService.go('guide/pipes');
|
||||
fixture.detectChanges(); // triggers the event that calls onDocRendered
|
||||
fixture.detectChanges(); // triggers the event that calls `onDocInserted`
|
||||
expect(scrollToTopSpy).toHaveBeenCalled();
|
||||
expect(scrollSpy).not.toHaveBeenCalled();
|
||||
|
||||
tick(scrollDelay);
|
||||
expect(scrollSpy).toHaveBeenCalled();
|
||||
}));
|
||||
@ -867,7 +795,9 @@ describe('AppComponent', () => {
|
||||
|
||||
describe('with mocked DocViewer', () => {
|
||||
const getDocViewer = () => fixture.debugElement.query(By.css('aio-doc-viewer'));
|
||||
const triggerDocRendered = () => getDocViewer().triggerEventHandler('docRendered', {});
|
||||
const triggerDocViewerEvent =
|
||||
(evt: 'docReady' | 'docRemoved' | 'docInserted' | 'docRendered') =>
|
||||
getDocViewer().triggerEventHandler(evt, undefined);
|
||||
|
||||
beforeEach(() => {
|
||||
createTestingModule('a/b');
|
||||
@ -879,7 +809,7 @@ describe('AppComponent', () => {
|
||||
});
|
||||
|
||||
describe('initial rendering', () => {
|
||||
it('should initially add the starting class until the first document is rendered', fakeAsync(() => {
|
||||
it('should initially add the starting class until a document is rendered', () => {
|
||||
const getSidenavContainer = () => fixture.debugElement.query(By.css('mat-sidenav-container'));
|
||||
|
||||
initializeTest();
|
||||
@ -887,21 +817,181 @@ describe('AppComponent', () => {
|
||||
expect(component.isStarting).toBe(true);
|
||||
expect(getSidenavContainer().classes['starting']).toBe(true);
|
||||
|
||||
triggerDocRendered();
|
||||
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', () => {
|
||||
@ -910,7 +1000,7 @@ describe('AppComponent', () => {
|
||||
const getProgressBar = () => fixture.debugElement.query(By.directive(MatProgressBar));
|
||||
const initializeAndCompleteNavigation = () => {
|
||||
initializeTest();
|
||||
triggerDocRendered();
|
||||
triggerDocViewerEvent('docReady');
|
||||
tick(HIDE_DELAY);
|
||||
};
|
||||
|
||||
@ -947,7 +1037,7 @@ describe('AppComponent', () => {
|
||||
it('should not be shown when re-navigating to the empty path', fakeAsync(() => {
|
||||
initializeAndCompleteNavigation();
|
||||
locationService.urlSubject.next('');
|
||||
triggerDocRendered();
|
||||
triggerDocViewerEvent('docReady');
|
||||
|
||||
locationService.urlSubject.next('');
|
||||
|
||||
@ -958,12 +1048,12 @@ describe('AppComponent', () => {
|
||||
tick(HIDE_DELAY); // Fire the remaining timer or `fakeAsync()` complains.
|
||||
}));
|
||||
|
||||
it('should not be shown if the doc is rendered quickly', fakeAsync(() => {
|
||||
it('should not be shown if the doc is prepared quickly', fakeAsync(() => {
|
||||
initializeAndCompleteNavigation();
|
||||
locationService.urlSubject.next('c/d');
|
||||
|
||||
tick(SHOW_DELAY - 1);
|
||||
triggerDocRendered();
|
||||
triggerDocViewerEvent('docReady');
|
||||
|
||||
tick(1);
|
||||
fixture.detectChanges();
|
||||
@ -972,12 +1062,12 @@ describe('AppComponent', () => {
|
||||
tick(HIDE_DELAY); // Fire the remaining timer or `fakeAsync()` complains.
|
||||
}));
|
||||
|
||||
it('should be shown if rendering the doc takes too long', fakeAsync(() => {
|
||||
it('should be shown if preparing the doc takes too long', fakeAsync(() => {
|
||||
initializeAndCompleteNavigation();
|
||||
locationService.urlSubject.next('c/d');
|
||||
|
||||
tick(SHOW_DELAY);
|
||||
triggerDocRendered();
|
||||
triggerDocViewerEvent('docReady');
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(getProgressBar()).toBeTruthy();
|
||||
@ -985,12 +1075,12 @@ describe('AppComponent', () => {
|
||||
tick(HIDE_DELAY); // Fire the remaining timer or `fakeAsync()` complains.
|
||||
}));
|
||||
|
||||
it('should be hidden (after a delay) once the doc is rendered', fakeAsync(() => {
|
||||
it('should be hidden (after a delay) once the doc has been prepared', fakeAsync(() => {
|
||||
initializeAndCompleteNavigation();
|
||||
locationService.urlSubject.next('c/d');
|
||||
|
||||
tick(SHOW_DELAY);
|
||||
triggerDocRendered();
|
||||
triggerDocViewerEvent('docReady');
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(getProgressBar()).toBeTruthy();
|
||||
@ -1007,10 +1097,10 @@ describe('AppComponent', () => {
|
||||
it('should only take the latest request into account', fakeAsync(() => {
|
||||
initializeAndCompleteNavigation();
|
||||
locationService.urlSubject.next('c/d'); // The URL changes.
|
||||
locationService.urlSubject.next('e/f'); // The URL changes again before `onDocRendered()`.
|
||||
locationService.urlSubject.next('e/f'); // The URL changes again before `onDocReady()`.
|
||||
|
||||
tick(SHOW_DELAY - 1); // `onDocRendered()` is triggered (for the last doc),
|
||||
triggerDocRendered(); // 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();
|
||||
@ -1033,6 +1123,7 @@ function createTestingModule(initialUrl: string, mode: string = 'stable') {
|
||||
imports: [ AppModule ],
|
||||
providers: [
|
||||
{ provide: APP_BASE_HREF, useValue: '/' },
|
||||
{ provide: EmbedComponentsService, useClass: TestEmbedComponentsService },
|
||||
{ provide: GaService, useClass: TestGaService },
|
||||
{ provide: HttpClient, useClass: TestHttpClient },
|
||||
{ provide: LocationService, useFactory: () => mockLocationService },
|
||||
@ -1047,6 +1138,10 @@ function createTestingModule(initialUrl: string, mode: string = 'stable') {
|
||||
});
|
||||
}
|
||||
|
||||
class TestEmbedComponentsService {
|
||||
embedInto = jasmine.createSpy('embedInto').and.returnValue(of([]));
|
||||
}
|
||||
|
||||
class TestGaService {
|
||||
locationChanged = jasmine.createSpy('locationChanged');
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { Component, ElementRef, HostBinding, HostListener, OnInit,
|
||||
QueryList, ViewChild, ViewChildren } from '@angular/core';
|
||||
import { MatSidenav } from '@angular/material';
|
||||
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(
|
||||
@ -203,20 +182,41 @@ export class AppComponent implements OnInit {
|
||||
this.scrollService.scroll();
|
||||
}
|
||||
|
||||
onDocRendered() {
|
||||
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);
|
||||
|
||||
// Put page in a clean visual state
|
||||
this.scrollService.scrollToTop();
|
||||
// If progress bar has been shown, keep it for at least 500ms (to avoid flashing).
|
||||
setTimeout(() => this.isFetching = false, 500);
|
||||
}
|
||||
|
||||
// Scroll 500ms after the doc-viewer has finished rendering the new doc
|
||||
// The delay is to allow time for async layout to complete
|
||||
onDocRemoved() {
|
||||
// The previous document has been removed.
|
||||
// Scroll to top to restore a clean visual state for the new document.
|
||||
this.scrollService.scrollToTop();
|
||||
}
|
||||
|
||||
onDocInserted() {
|
||||
// TODO: Find a better way to avoid `ExpressionChangedAfterItHasBeenChecked` error.
|
||||
setTimeout(() => {
|
||||
this.autoScroll();
|
||||
this.isStarting = false;
|
||||
this.isFetching = false;
|
||||
}, 500);
|
||||
// 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) {
|
||||
@ -283,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() {
|
||||
@ -291,7 +312,7 @@ export class AppComponent implements OnInit {
|
||||
const el = this.hostElement.nativeElement as Element;
|
||||
this.tocMaxHeightOffset =
|
||||
el.querySelector('footer').clientHeight +
|
||||
el.querySelector('mat-toolbar.app-toolbar').clientHeight +
|
||||
el.querySelector('.app-toolbar').clientHeight +
|
||||
24; // fudge margin
|
||||
}
|
||||
|
||||
|
48
aio/src/app/app.module.spec.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AppModule } from 'app/app.module';
|
||||
import { ComponentsOrModulePath, EMBEDDED_COMPONENTS } from 'app/embed-components/embed-components.service';
|
||||
import { embeddedComponents } from 'app/embedded/embedded.module';
|
||||
|
||||
describe('AppModule', () => {
|
||||
let componentsMap: {[multiSelectorstring: string]: ComponentsOrModulePath};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({imports: [AppModule]});
|
||||
componentsMap = TestBed.get(EMBEDDED_COMPONENTS);
|
||||
});
|
||||
|
||||
it('should provide a map of selectors to embedded components (or module)', () => {
|
||||
const allSelectors = Object.keys(componentsMap);
|
||||
|
||||
expect(allSelectors.length).toBeGreaterThan(1);
|
||||
allSelectors.forEach(selector => {
|
||||
const value = componentsMap[selector];
|
||||
const isArrayOrString = Array.isArray(value) || (typeof value === 'string');
|
||||
expect(isArrayOrString).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide a list of eagerly-loaded embedded components', () => {
|
||||
const eagerSelector = Object.keys(componentsMap).find(selector => Array.isArray(componentsMap[selector]));
|
||||
const selectorCount = eagerSelector.split(',').length;
|
||||
|
||||
expect(eagerSelector).not.toBeNull();
|
||||
expect(selectorCount).toBe(componentsMap[eagerSelector].length);
|
||||
|
||||
// For example...
|
||||
expect(eagerSelector).toContain('aio-toc');
|
||||
});
|
||||
|
||||
it('should provide a list of lazy-loaded embedded components', () => {
|
||||
const lazySelector = Object.keys(componentsMap).find(selector => selector.includes('code-example'));
|
||||
const selectorCount = lazySelector.split(',').length;
|
||||
|
||||
expect(lazySelector).not.toBeNull();
|
||||
expect(selectorCount).toBe(embeddedComponents.length);
|
||||
|
||||
// For example...
|
||||
expect(lazySelector).toContain('code-example');
|
||||
expect(lazySelector).toContain('code-tabs');
|
||||
expect(lazySelector).toContain('live-example');
|
||||
});
|
||||
});
|
@ -5,36 +5,22 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common';
|
||||
|
||||
import {
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatIconRegistry,
|
||||
MatInputModule,
|
||||
MatProgressBarModule,
|
||||
MatSidenavModule,
|
||||
MatTabsModule,
|
||||
MatToolbarModule
|
||||
} from '@angular/material';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule, MatIconRegistry } from '@angular/material/icon';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
|
||||
import {
|
||||
Platform
|
||||
} from '@angular/cdk/platform';
|
||||
import { ROUTES } from '@angular/router';
|
||||
|
||||
|
||||
// Temporary fix for MatSidenavModule issue:
|
||||
// crashes with "missing first" operator when SideNav.mode is "over"
|
||||
import 'rxjs/add/operator/first';
|
||||
|
||||
import { SwUpdatesModule } from 'app/sw-updates/sw-updates.module';
|
||||
|
||||
import { AppComponent } from 'app/app.component';
|
||||
import { ApiService } from 'app/embedded/api/api.service';
|
||||
import { EMBEDDED_COMPONENTS, EmbeddedComponentsMap } from 'app/embed-components/embed-components.service';
|
||||
import { CustomIconRegistry, SVG_ICONS } from 'app/shared/custom-icon-registry';
|
||||
import { Deployment } from 'app/shared/deployment.service';
|
||||
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
||||
import { DtComponent } from 'app/layout/doc-viewer/dt.component';
|
||||
import { ModeBannerComponent } from 'app/layout/mode-banner/mode-banner.component';
|
||||
import { EmbeddedModule } from 'app/embedded/embedded.module';
|
||||
import { GaService } from 'app/shared/ga.service';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
@ -47,11 +33,18 @@ import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component';
|
||||
import { NavItemComponent } from 'app/layout/nav-item/nav-item.component';
|
||||
import { ScrollService } from 'app/shared/scroll.service';
|
||||
import { ScrollSpyService } from 'app/shared/scroll-spy.service';
|
||||
import { SearchBoxComponent } from './search/search-box/search-box.component';
|
||||
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
|
||||
import { TocComponent } from 'app/layout/toc/toc.component';
|
||||
import { TocService } from 'app/shared/toc.service';
|
||||
import { WindowToken, windowProvider } from 'app/shared/window';
|
||||
|
||||
import { EmbedComponentsModule } from 'app/embed-components/embed-components.module';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import { SwUpdatesModule } from 'app/sw-updates/sw-updates.module';
|
||||
|
||||
|
||||
// The path to the `EmbeddedModule`.
|
||||
const embeddedModulePath = 'app/embedded/embedded.module#EmbeddedModule';
|
||||
|
||||
// These are the hardcoded inline svg sources to be used by the `<mat-icon>` component
|
||||
export const svgIconProviders = [
|
||||
@ -78,15 +71,13 @@ export const svgIconProviders = [
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
EmbeddedModule,
|
||||
HttpClientModule,
|
||||
BrowserAnimationsModule,
|
||||
EmbedComponentsModule,
|
||||
HttpClientModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatProgressBarModule,
|
||||
MatSidenavModule,
|
||||
MatTabsModule,
|
||||
MatToolbarModule,
|
||||
SwUpdatesModule,
|
||||
SharedModule
|
||||
@ -100,10 +91,10 @@ export const svgIconProviders = [
|
||||
NavMenuComponent,
|
||||
NavItemComponent,
|
||||
SearchBoxComponent,
|
||||
TocComponent,
|
||||
TopMenuComponent,
|
||||
],
|
||||
providers: [
|
||||
ApiService,
|
||||
Deployment,
|
||||
DocumentService,
|
||||
GaService,
|
||||
@ -113,15 +104,32 @@ export const svgIconProviders = [
|
||||
LocationService,
|
||||
{ provide: MatIconRegistry, useClass: CustomIconRegistry },
|
||||
NavigationService,
|
||||
Platform,
|
||||
ScrollService,
|
||||
ScrollSpyService,
|
||||
SearchService,
|
||||
svgIconProviders,
|
||||
TocService,
|
||||
{ provide: WindowToken, useFactory: windowProvider },
|
||||
|
||||
{
|
||||
provide: EMBEDDED_COMPONENTS,
|
||||
useValue: {
|
||||
/* tslint:disable: max-line-length */
|
||||
'aio-toc': [TocComponent],
|
||||
'aio-api-list, aio-contributor-list, aio-file-not-found-search, aio-resource-list, code-example, code-tabs, current-location, live-example': embeddedModulePath,
|
||||
/* tslint:enable: max-line-length */
|
||||
} as EmbeddedComponentsMap,
|
||||
},
|
||||
{
|
||||
// This is currently the only way to get `@angular/cli`
|
||||
// to split `EmbeddedModule` into a separate chunk :(
|
||||
provide: ROUTES,
|
||||
useValue: [{ path: '/embedded', loadChildren: embeddedModulePath }],
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
entryComponents: [ TocComponent ],
|
||||
bootstrap: [ AppComponent ]
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
||||
|
13
aio/src/app/embed-components/embed-components.module.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { NgModule, NgModuleFactoryLoader, SystemJsNgModuleLoader } from '@angular/core';
|
||||
|
||||
import { EmbedComponentsService } from './embed-components.service';
|
||||
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
EmbedComponentsService,
|
||||
{ provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader },
|
||||
],
|
||||
})
|
||||
export class EmbedComponentsModule {
|
||||
}
|
378
aio/src/app/embed-components/embed-components.service.spec.ts
Normal file
@ -0,0 +1,378 @@
|
||||
import { ComponentFactory, ComponentFactoryResolver, ComponentRef, NgModuleFactoryLoader } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import {
|
||||
MockNgModuleFactoryLoader, TestEmbedComponentsService, TestModule, mockEmbeddedModulePath,
|
||||
testEagerEmbeddedComponents, testEagerEmbeddedSelectors, testLazyEmbeddedComponents
|
||||
} from 'testing/embed-components-utils';
|
||||
import { EmbedComponentsService, ComponentsOrModulePath } from './embed-components.service';
|
||||
|
||||
|
||||
describe('EmbedComponentsService', () => {
|
||||
let service: TestEmbedComponentsService;
|
||||
let host: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({imports: [TestModule]});
|
||||
|
||||
service = TestBed.get(EmbedComponentsService);
|
||||
host = document.createElement('div');
|
||||
});
|
||||
|
||||
it('should be instantiated', () => {
|
||||
expect(service).toEqual(jasmine.any(EmbedComponentsService));
|
||||
});
|
||||
|
||||
describe('#createComponentFactories()', () => {
|
||||
let factories: typeof service.componentFactories;
|
||||
let resolver: ComponentFactoryResolver;
|
||||
|
||||
const doCreateComponentFactories = () =>
|
||||
service.createComponentFactories(testEagerEmbeddedComponents, resolver);
|
||||
|
||||
beforeEach(() => {
|
||||
factories = service.componentFactories;
|
||||
resolver = TestBed.get(ComponentFactoryResolver) as ComponentFactoryResolver;
|
||||
});
|
||||
|
||||
it('should create a factory entry for each component', () => {
|
||||
expect(factories.size).toBe(0);
|
||||
|
||||
doCreateComponentFactories();
|
||||
expect(factories.size).toBe(testEagerEmbeddedComponents.length);
|
||||
});
|
||||
|
||||
it('should key the factory entries by selector', () => {
|
||||
doCreateComponentFactories();
|
||||
|
||||
const actualSelectors = Array.from(factories.keys());
|
||||
const expectedSelectors = testEagerEmbeddedSelectors;
|
||||
|
||||
expect(actualSelectors).toEqual(expectedSelectors);
|
||||
});
|
||||
|
||||
it('should store the projected content property name', () => {
|
||||
doCreateComponentFactories();
|
||||
|
||||
const actualContentPropNames = Array.from(factories.values()).map(x => x.contentPropertyName);
|
||||
const expectedContentPropNames = testEagerEmbeddedSelectors.map(x => service.selectorToContentPropertyName(x));
|
||||
|
||||
expect(actualContentPropNames).toEqual(expectedContentPropNames);
|
||||
});
|
||||
|
||||
it('should store the factory for each component', () => {
|
||||
doCreateComponentFactories();
|
||||
|
||||
const actualFactories = Array.from(factories.values()).map(x => x.factory);
|
||||
const expectedComponentTypes = testEagerEmbeddedComponents;
|
||||
|
||||
actualFactories.forEach((factory, i) => {
|
||||
expect(factory).toEqual(jasmine.any(ComponentFactory));
|
||||
expect(factory.componentType).toBe(expectedComponentTypes[i]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#createComponents()', () => {
|
||||
const FooComponent = testEagerEmbeddedComponents[0];
|
||||
const BarComponent = testEagerEmbeddedComponents[1];
|
||||
|
||||
beforeEach(() => service.prepareComponentFactories(testEagerEmbeddedComponents));
|
||||
|
||||
it('should apply all embedded components (and return the `ComponentRef`s)', () => {
|
||||
host.innerHTML = `
|
||||
<p>Header</p>
|
||||
<p><aio-eager-foo></aio-eager-foo></p>
|
||||
<p><aio-eager-bar></aio-eager-bar></p>
|
||||
<p>Footer</p>
|
||||
`;
|
||||
|
||||
const componentRefs = service.createComponents(host);
|
||||
|
||||
expect(host.innerHTML).toContain('Foo Component');
|
||||
expect(host.innerHTML).toContain('Bar Component');
|
||||
|
||||
expect(componentRefs.length).toBe(2);
|
||||
expect(componentRefs[0].instance).toEqual(jasmine.any(FooComponent));
|
||||
expect(componentRefs[1].instance).toEqual(jasmine.any(BarComponent));
|
||||
});
|
||||
|
||||
it('should apply embedded components to all matching elements', () => {
|
||||
host.innerHTML = `
|
||||
<p>Header</p>
|
||||
<p><aio-eager-foo></aio-eager-foo></p>
|
||||
<p><aio-eager-bar></aio-eager-bar></p>
|
||||
<p><aio-eager-foo></aio-eager-foo></p>
|
||||
<p><aio-eager-bar></aio-eager-bar></p>
|
||||
<p>Footer</p>
|
||||
`;
|
||||
|
||||
const componentRefs = service.createComponents(host);
|
||||
|
||||
expect(componentRefs.length).toBe(4);
|
||||
expect(componentRefs[0].instance).toEqual(jasmine.any(FooComponent));
|
||||
expect(componentRefs[1].instance).toEqual(jasmine.any(FooComponent));
|
||||
expect(componentRefs[2].instance).toEqual(jasmine.any(BarComponent));
|
||||
expect(componentRefs[3].instance).toEqual(jasmine.any(BarComponent));
|
||||
});
|
||||
|
||||
it('should allow projecting content by assigning it on the element', () => {
|
||||
const projectedContent = 'Projected content';
|
||||
host.innerHTML = `
|
||||
<p>Header</p>
|
||||
<p><aio-eager-bar>${projectedContent}</aio-eager-bar></p>
|
||||
<p>Footer</p>
|
||||
`;
|
||||
|
||||
const componentRefs = service.createComponents(host);
|
||||
componentRefs[0].changeDetectorRef.detectChanges();
|
||||
|
||||
const barEl = host.querySelector('aio-eager-bar');
|
||||
|
||||
expect(barEl['aioEagerBarContent']).toBe(projectedContent);
|
||||
expect(barEl.innerHTML).toContain(projectedContent);
|
||||
});
|
||||
|
||||
// Because `FooComponent` is processed before `BarComponent`...
|
||||
it('should apply `FooComponent` within `BarComponent`', () => {
|
||||
host.innerHTML = `
|
||||
<aio-eager-bar>
|
||||
<aio-eager-foo></aio-eager-foo>
|
||||
</aio-eager-bar>
|
||||
`;
|
||||
|
||||
const componentRefs = service.createComponents(host);
|
||||
componentRefs.forEach(ref => ref.changeDetectorRef.detectChanges());
|
||||
|
||||
expect(host.innerHTML).toContain('Foo Component');
|
||||
expect(host.innerHTML).toContain('Bar Component');
|
||||
|
||||
expect(componentRefs.length).toBe(2);
|
||||
expect(componentRefs[0].instance).toEqual(jasmine.any(FooComponent));
|
||||
expect(componentRefs[1].instance).toEqual(jasmine.any(BarComponent));
|
||||
});
|
||||
|
||||
// Because `BarComponent` is processed after `FooComponent`...
|
||||
it('should not apply `BarComponent` within `FooComponent`', () => {
|
||||
host.innerHTML = `
|
||||
<aio-eager-foo>
|
||||
<aio-eager-bar></aio-eager-bar>
|
||||
</aio-eager-foo>
|
||||
`;
|
||||
|
||||
const componentRefs = service.createComponents(host);
|
||||
componentRefs.forEach(ref => ref.changeDetectorRef.detectChanges());
|
||||
|
||||
expect(host.innerHTML).toContain('Foo Component');
|
||||
expect(host.innerHTML).not.toContain('Bar Component');
|
||||
|
||||
expect(componentRefs.length).toBe(1);
|
||||
expect(componentRefs[0].instance).toEqual(jasmine.any(FooComponent));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#embedInto()', () => {
|
||||
let mockComponentRefs: ComponentRef<any>[];
|
||||
let createComponentsSpy: jasmine.Spy;
|
||||
let prepareComponentFactoriesSpy: jasmine.Spy;
|
||||
|
||||
const doEmbed = (contents: string) =>
|
||||
new Promise<ComponentRef<any>[]>((resolve, reject) => {
|
||||
host.innerHTML = contents;
|
||||
service.embedInto(host).subscribe(resolve, reject);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockComponentRefs = [{foo: true}, {bar: true}] as any as ComponentRef<any>[];
|
||||
|
||||
createComponentsSpy = spyOn(service, 'createComponents').and.returnValue(mockComponentRefs);
|
||||
prepareComponentFactoriesSpy = spyOn(service, 'prepareComponentFactories')
|
||||
.and.returnValue(Promise.resolve());
|
||||
});
|
||||
|
||||
it('should return an observable', done => {
|
||||
service.embedInto(host).subscribe(done, done.fail);
|
||||
});
|
||||
|
||||
describe('(preparing component factories)', () => {
|
||||
it('should return an array of `ComponentRef`s', async () => {
|
||||
// When there are embedded components.
|
||||
expect(await doEmbed('<aio-eager-foo></aio-eager-foo>')).toEqual(mockComponentRefs);
|
||||
expect(await doEmbed('<aio-lazy-bar></aio-lazy-bar>')).toEqual(mockComponentRefs);
|
||||
|
||||
// When there are no embedded components.
|
||||
expect(await doEmbed('<div>Test</div>')).toEqual([]);
|
||||
expect(await doEmbed('')).toEqual([]);
|
||||
});
|
||||
|
||||
it('should prepare all component factories if there are embedded components', async () => {
|
||||
await doEmbed(`
|
||||
<div><aio-eager-foo><b>foo</b></aio-eager-foo></div>
|
||||
<span><aio-lazy-foo><i>bar</i></aio-lazy-foo></span>
|
||||
`);
|
||||
|
||||
expect(prepareComponentFactoriesSpy).toHaveBeenCalledTimes(2);
|
||||
expect(prepareComponentFactoriesSpy).toHaveBeenCalledWith(testEagerEmbeddedComponents);
|
||||
expect(prepareComponentFactoriesSpy).toHaveBeenCalledWith(mockEmbeddedModulePath);
|
||||
});
|
||||
|
||||
it('should only prepare the necessary factories', async () => {
|
||||
await doEmbed('<aio-eager-foo>Eager only</aio-eager-foo>');
|
||||
expect(prepareComponentFactoriesSpy).toHaveBeenCalledTimes(1);
|
||||
expect(prepareComponentFactoriesSpy).toHaveBeenCalledWith(testEagerEmbeddedComponents);
|
||||
|
||||
await doEmbed('<aio-lazy-foo>Lazy only</aio-lazy-foo>');
|
||||
expect(prepareComponentFactoriesSpy).toHaveBeenCalledTimes(2);
|
||||
expect(prepareComponentFactoriesSpy).toHaveBeenCalledWith(mockEmbeddedModulePath);
|
||||
});
|
||||
|
||||
it('should not load embedded components if the document does not contain any', async () => {
|
||||
await doEmbed('');
|
||||
await doEmbed('<no-aio-eager-foo></no-aio-eager-foo>');
|
||||
await doEmbed('<no-aio-lazy-foo></no-aio-lazy-foo>');
|
||||
|
||||
expect(prepareComponentFactoriesSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('(creating embedded components)', () => {
|
||||
it('should create embedded components if the element contains any', async () => {
|
||||
await doEmbed('<div><aio-eager-foo><i>blah</i></aio-eager-foo></div>');
|
||||
|
||||
expect(createComponentsSpy).toHaveBeenCalledTimes(1);
|
||||
expect(prepareComponentFactoriesSpy).toHaveBeenCalledBefore(createComponentsSpy);
|
||||
|
||||
prepareComponentFactoriesSpy.calls.reset();
|
||||
createComponentsSpy.calls.reset();
|
||||
|
||||
await doEmbed('<aio-lazy-bar><i>blah</i></aio-lazy-bar>');
|
||||
expect(createComponentsSpy).toHaveBeenCalledTimes(1);
|
||||
expect(prepareComponentFactoriesSpy).toHaveBeenCalledBefore(createComponentsSpy);
|
||||
});
|
||||
|
||||
it('should emit the created embedded components', async () => {
|
||||
const componentRefs = await doEmbed('<aio-eager-foo></aio-eager-foo>');
|
||||
expect(componentRefs).toBe(mockComponentRefs);
|
||||
});
|
||||
|
||||
it('should not create embedded components if the element does not contain any', async () => {
|
||||
await doEmbed(`
|
||||
<aio-eager-foo-not></aio-eager-foo-not>
|
||||
<aio-lazy-bar></aio-lazy-bar>
|
||||
`);
|
||||
expect(createComponentsSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not create embedded components if the document is empty', async () => {
|
||||
await doEmbed('');
|
||||
expect(createComponentsSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not create embedded components if unsubscribed from', async () => {
|
||||
const preparePromise = Promise.resolve();
|
||||
prepareComponentFactoriesSpy.and.returnValue(preparePromise);
|
||||
|
||||
// When not unsubscribed from...
|
||||
host.innerHTML = '<aio-eager-foo></aio-eager-foo>';
|
||||
service.embedInto(host).subscribe();
|
||||
await new Promise(resolve => setTimeout(resolve));
|
||||
expect(createComponentsSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
createComponentsSpy.calls.reset();
|
||||
|
||||
// When unsubscribed from...
|
||||
host.innerHTML = '<aio-eager-foo></aio-eager-foo>';
|
||||
service.embedInto(host).subscribe().unsubscribe();
|
||||
await new Promise(resolve => setTimeout(resolve));
|
||||
expect(createComponentsSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#prepareComponentFactories()', () => {
|
||||
let loader: MockNgModuleFactoryLoader;
|
||||
let resolver: ComponentFactoryResolver;
|
||||
let createComponentFactoriesSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
loader = TestBed.get(NgModuleFactoryLoader);
|
||||
resolver = TestBed.get(ComponentFactoryResolver);
|
||||
|
||||
createComponentFactoriesSpy = spyOn(service, 'createComponentFactories');
|
||||
});
|
||||
|
||||
[testLazyEmbeddedComponents, mockEmbeddedModulePath].forEach((compsOrPath: ComponentsOrModulePath) => {
|
||||
const useComponents = Array.isArray(compsOrPath);
|
||||
|
||||
describe(`(using ${useComponents ? 'component types' : 'module path'})`, () => {
|
||||
const doPrepareComponentFactories = () =>
|
||||
service.prepareComponentFactories(compsOrPath);
|
||||
|
||||
it('should return a promise', done => {
|
||||
doPrepareComponentFactories().then(done, done.fail);
|
||||
});
|
||||
|
||||
it('should create the component factories', async () => {
|
||||
expect(createComponentFactoriesSpy).not.toHaveBeenCalled();
|
||||
|
||||
await doPrepareComponentFactories();
|
||||
expect(createComponentFactoriesSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
const args = createComponentFactoriesSpy.calls.mostRecent().args;
|
||||
expect(args[0]).toBe(testLazyEmbeddedComponents);
|
||||
|
||||
if (useComponents) {
|
||||
expect(args[1]).toBe(resolver);
|
||||
} else {
|
||||
expect(args[1]).not.toBe(resolver);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not create create the component factories more than once', async () => {
|
||||
const results = await Promise.all([
|
||||
doPrepareComponentFactories(),
|
||||
doPrepareComponentFactories(),
|
||||
]);
|
||||
|
||||
expect(createComponentFactoriesSpy).toHaveBeenCalledTimes(1);
|
||||
expect(results[1]).toBe(results[0]);
|
||||
|
||||
const anotherResult = await doPrepareComponentFactories();
|
||||
|
||||
expect(createComponentFactoriesSpy).toHaveBeenCalledTimes(1);
|
||||
expect(anotherResult).toBe(results[0]);
|
||||
});
|
||||
|
||||
it(`should ${useComponents ? 'not load' : 'load'} the embedded module`, async () => {
|
||||
expect(loader.loadedPaths).toEqual([]);
|
||||
|
||||
await doPrepareComponentFactories();
|
||||
const expectedLoadedPaths = useComponents ? [] : [mockEmbeddedModulePath];
|
||||
|
||||
expect(loader.loadedPaths).toEqual(expectedLoadedPaths);
|
||||
});
|
||||
|
||||
it(`should not load the embedded module more than once`, async () => {
|
||||
await Promise.all([
|
||||
doPrepareComponentFactories(),
|
||||
doPrepareComponentFactories(),
|
||||
]);
|
||||
const loadedPathCount = loader.loadedPaths.length;
|
||||
|
||||
expect(loadedPathCount).toBeLessThan(2);
|
||||
|
||||
await doPrepareComponentFactories();
|
||||
|
||||
expect(loader.loadedPaths.length).toBe(loadedPathCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#selectorToContentPropertyName()', () => {
|
||||
it('should convert an element selector to a property name', () => {
|
||||
expect(service.selectorToContentPropertyName('foobar')).toBe('foobarContent');
|
||||
expect(service.selectorToContentPropertyName('baz-qux')).toBe('bazQuxContent');
|
||||
});
|
||||
});
|
||||
});
|
154
aio/src/app/embed-components/embed-components.service.ts
Normal file
@ -0,0 +1,154 @@
|
||||
import {
|
||||
ComponentFactory, ComponentFactoryResolver, ComponentRef, Inject, Injectable, InjectionToken,
|
||||
Injector, NgModuleFactory, NgModuleFactoryLoader, Type
|
||||
} from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import 'rxjs/add/operator/switchMap';
|
||||
|
||||
|
||||
export interface EmbeddedComponentFactory {
|
||||
contentPropertyName: string;
|
||||
factory: ComponentFactory<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A mapping from combined component selectors (keys) to the corresponding components (values). The
|
||||
* components can be specified either as a list of embedded components or a path to a module that
|
||||
* provides embedded components (i.e. implements `WithEmbeddedComponents`).
|
||||
*/
|
||||
export interface EmbeddedComponentsMap {
|
||||
[multiSelector: string]: ComponentsOrModulePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface expected to be implemented by all modules that contribute components to the
|
||||
* `EmbeddedComponentsMap`.
|
||||
*/
|
||||
export interface WithEmbeddedComponents {
|
||||
embeddedComponents: Type<any>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Either an array of components or the path to a module that implements `WithEmbeddedComponents`.
|
||||
*/
|
||||
export type ComponentsOrModulePath = Type<any>[] | string;
|
||||
|
||||
/**
|
||||
* The injection token for the `EmbeddedComponentsMap`.
|
||||
*/
|
||||
export const EMBEDDED_COMPONENTS = new InjectionToken<EmbeddedComponentsMap>('EMBEDDED_COMPONENTS');
|
||||
|
||||
/**
|
||||
* Embed components into an element. It takes care of indentifying the embedded components, loading
|
||||
* the necessary modules and instantiating the components.
|
||||
*
|
||||
* Embeddable components are identified and loaded based on the info in `EmbeddedComponentsMap`
|
||||
* (provided through dependency injection).
|
||||
*
|
||||
* The caller is responsible for trigering change detection and destroying the components as
|
||||
* necessary.
|
||||
*/
|
||||
@Injectable()
|
||||
export class EmbedComponentsService {
|
||||
private componentFactoriesReady = new Map<ComponentsOrModulePath, Promise<void>>();
|
||||
protected componentFactories = new Map<string, EmbeddedComponentFactory>();
|
||||
|
||||
constructor(
|
||||
private injector: Injector,
|
||||
private loader: NgModuleFactoryLoader,
|
||||
private resolver: ComponentFactoryResolver,
|
||||
@Inject(EMBEDDED_COMPONENTS) private embeddedComponentsMap: EmbeddedComponentsMap) { }
|
||||
|
||||
/**
|
||||
* Embed components into the specified element:
|
||||
* - Load the necessary modules (if any).
|
||||
* - Prepare the component factories.
|
||||
* - Instantiate the components.
|
||||
*
|
||||
* Return the list of `ComponentRef`s.
|
||||
*/
|
||||
embedInto(elem: HTMLElement): Observable<ComponentRef<any>[]> {
|
||||
const requiredComponents = Object.keys(this.embeddedComponentsMap)
|
||||
.filter(selector => elem.querySelector(selector))
|
||||
.map(selector => this.embeddedComponentsMap[selector]);
|
||||
|
||||
const factoriesReady = requiredComponents.map(compsOrPath => this.prepareComponentFactories(compsOrPath));
|
||||
|
||||
return !requiredComponents.length
|
||||
? of([])
|
||||
: of(undefined)
|
||||
.switchMap(() => Promise.all(factoriesReady))
|
||||
.switchMap(() => [this.createComponents(elem)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the embedded component factories (which will later be used to instantiate components).
|
||||
*/
|
||||
protected createComponentFactories(components: Type<any>[], resolver: ComponentFactoryResolver): void {
|
||||
for (const comp of components) {
|
||||
const factory = resolver.resolveComponentFactory(comp);
|
||||
const selector = factory.selector;
|
||||
const contentPropertyName = this.selectorToContentPropertyName(selector);
|
||||
this.componentFactories.set(selector, {contentPropertyName, factory});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate embedded components for the current contents of `elem`.
|
||||
* (Store the original HTML contents of each element on the corresponding property for later
|
||||
* retrieval by the component instance.)
|
||||
*/
|
||||
protected createComponents(elem: HTMLElement): ComponentRef<any>[] {
|
||||
const componentRefs: ComponentRef<any>[] = [];
|
||||
|
||||
this.componentFactories.forEach(({contentPropertyName, factory}, selector) => {
|
||||
const componentHosts = elem.querySelectorAll(selector);
|
||||
|
||||
// Cast due to https://github.com/Microsoft/TypeScript/issues/4947.
|
||||
for (const host of componentHosts as any as HTMLElement[]) {
|
||||
// Hack: Preserve the current element content, because the factory will empty it out.
|
||||
// Security: The source of this `innerHTML` should always be authored by the documentation
|
||||
// team and is considered to be safe.
|
||||
host[contentPropertyName] = host.innerHTML;
|
||||
componentRefs.push(factory.create(this.injector, [], host));
|
||||
}
|
||||
});
|
||||
|
||||
return componentRefs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the component factories for the given components.
|
||||
* If necessary, load and instantiate the module first.
|
||||
*/
|
||||
protected prepareComponentFactories(compsOrPath: ComponentsOrModulePath): Promise<void> {
|
||||
if (!this.componentFactoriesReady.has(compsOrPath)) {
|
||||
const componentsAndResolverPromise = (typeof compsOrPath !== 'string')
|
||||
? Promise.resolve({components: compsOrPath, resolver: this.resolver})
|
||||
: this.loader.load(compsOrPath).then((ngModuleFactory: NgModuleFactory<WithEmbeddedComponents>) => {
|
||||
const moduleRef = ngModuleFactory.create(this.injector);
|
||||
return {
|
||||
components: moduleRef.instance.embeddedComponents,
|
||||
resolver: moduleRef.componentFactoryResolver,
|
||||
};
|
||||
});
|
||||
|
||||
const readyPromise = componentsAndResolverPromise
|
||||
.then(({components, resolver}) => this.createComponentFactories(components, resolver));
|
||||
|
||||
this.componentFactoriesReady.set(compsOrPath, readyPromise);
|
||||
}
|
||||
|
||||
return this.componentFactoriesReady.get(compsOrPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the component content property name by converting the selector to camelCase and
|
||||
* appending `Content`, e.g. `live-example` => `liveExampleContent`.
|
||||
*/
|
||||
protected selectorToContentPropertyName(selector: string): string {
|
||||
return selector.replace(/-(.)/g, (match, $1) => $1.toUpperCase()) + 'Content';
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import { Component, ElementRef, ViewChild, OnChanges, Input } from '@angular/cor
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { PrettyPrinter } from './pretty-printer.service';
|
||||
import { CopierService } from 'app/shared/copier.service';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
|
||||
const defaultLineNumsCount = 10; // by default, show linenums over this number
|
||||
|
||||
|
@ -1,20 +1,24 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { NgModule, Type } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { ContributorService } from './contributor/contributor.service';
|
||||
import { CopierService } from 'app/shared/copier.service';
|
||||
import { PrettyPrinter } from './code/pretty-printer.service';
|
||||
import { WithEmbeddedComponents } from 'app/embed-components/embed-components.service';
|
||||
|
||||
// Any components that we want to use inside embedded components must be declared or imported here
|
||||
// It is not enough just to import them inside the AppModule
|
||||
|
||||
// Reusable components (used inside embedded components)
|
||||
import { MatIconModule, MatSnackBarModule, MatTabsModule } from '@angular/material';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { CodeComponent } from './code/code.component';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
|
||||
// Embedded Components
|
||||
import { ApiListComponent } from './api/api-list.component';
|
||||
import { ApiService } from './api/api.service';
|
||||
import { CodeExampleComponent } from './code/code-example.component';
|
||||
import { CodeTabsComponent } from './code/code-tabs.component';
|
||||
import { ContributorListComponent } from './contributor/contributor-list.component';
|
||||
@ -24,22 +28,16 @@ import { FileNotFoundSearchComponent } from './search/file-not-found-search.comp
|
||||
import { LiveExampleComponent, EmbeddedPlunkerComponent } from './live-example/live-example.component';
|
||||
import { ResourceListComponent } from './resource/resource-list.component';
|
||||
import { ResourceService } from './resource/resource.service';
|
||||
import { TocComponent } from './toc/toc.component';
|
||||
|
||||
/** Components that can be embedded in docs
|
||||
/**
|
||||
* Components that can be embedded in docs,
|
||||
* such as CodeExampleComponent, LiveExampleComponent,...
|
||||
*/
|
||||
export const embeddedComponents: any[] = [
|
||||
export const embeddedComponents: Type<any>[] = [
|
||||
ApiListComponent, CodeExampleComponent, CodeTabsComponent, ContributorListComponent,
|
||||
CurrentLocationComponent, FileNotFoundSearchComponent, LiveExampleComponent, ResourceListComponent,
|
||||
TocComponent
|
||||
CurrentLocationComponent, FileNotFoundSearchComponent, LiveExampleComponent, ResourceListComponent
|
||||
];
|
||||
|
||||
/** Injectable class w/ property returning components that can be embedded in docs */
|
||||
export class EmbeddedComponents {
|
||||
components = embeddedComponents;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -54,16 +52,15 @@ export class EmbeddedComponents {
|
||||
ContributorComponent,
|
||||
EmbeddedPlunkerComponent
|
||||
],
|
||||
exports: [
|
||||
TocComponent
|
||||
],
|
||||
providers: [
|
||||
ApiService,
|
||||
ContributorService,
|
||||
CopierService,
|
||||
EmbeddedComponents,
|
||||
PrettyPrinter,
|
||||
ResourceService
|
||||
],
|
||||
entryComponents: [ embeddedComponents ]
|
||||
})
|
||||
export class EmbeddedModule { }
|
||||
export class EmbeddedModule implements WithEmbeddedComponents {
|
||||
embeddedComponents = embeddedComponents;
|
||||
}
|
||||
|
@ -1,18 +1,22 @@
|
||||
import {
|
||||
Component, ComponentFactory, ComponentFactoryResolver, ComponentRef,
|
||||
DoCheck, ElementRef, EventEmitter, Injector, Input, OnDestroy,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
|
||||
import { EmbeddedComponents } from 'app/embedded/embedded.module';
|
||||
import { DocumentContents } from 'app/documents/document.service';
|
||||
import { Component, ComponentRef, DoCheck, ElementRef, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import { timer } from 'rxjs/observable/timer';
|
||||
import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/operator/do';
|
||||
import 'rxjs/add/operator/switchMap';
|
||||
import 'rxjs/add/operator/takeUntil';
|
||||
|
||||
import { DocumentContents } from 'app/documents/document.service';
|
||||
import { EmbedComponentsService } from 'app/embed-components/embed-components.service';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { TocService } from 'app/shared/toc.service';
|
||||
|
||||
interface EmbeddedComponentFactory {
|
||||
contentPropertyName: string;
|
||||
factory: ComponentFactory<any>;
|
||||
}
|
||||
|
||||
// Constants
|
||||
export const NO_ANIMATIONS = 'no-animations';
|
||||
|
||||
// Initialization prevents flicker once pre-rendering is on
|
||||
const initialDocViewerElement = document.querySelector('aio-doc-viewer');
|
||||
@ -25,19 +29,48 @@ const initialDocViewerContent = initialDocViewerElement ? initialDocViewerElemen
|
||||
// encapsulation: ViewEncapsulation.Native
|
||||
})
|
||||
export class DocViewerComponent implements DoCheck, OnDestroy {
|
||||
// Enable/Disable view transition animations.
|
||||
static animationsEnabled = true;
|
||||
|
||||
private embeddedComponents: ComponentRef<any>[] = [];
|
||||
private embeddedComponentFactories: Map<string, EmbeddedComponentFactory> = new Map();
|
||||
private hostElement: HTMLElement;
|
||||
|
||||
@Output()
|
||||
docRendered = new EventEmitter();
|
||||
private void$ = of<void>(undefined);
|
||||
private onDestroy$ = new EventEmitter<void>();
|
||||
private docContents$ = new EventEmitter<DocumentContents>();
|
||||
|
||||
protected embeddedComponentRefs: ComponentRef<any>[] = [];
|
||||
protected currViewContainer: HTMLElement = document.createElement('div');
|
||||
protected nextViewContainer: HTMLElement = document.createElement('div');
|
||||
|
||||
@Input()
|
||||
set doc(newDoc: DocumentContents) {
|
||||
// Ignore `undefined` values that could happen if the host component
|
||||
// does not initially specify a value for the `doc` input.
|
||||
if (newDoc) {
|
||||
this.docContents$.emit(newDoc);
|
||||
}
|
||||
}
|
||||
|
||||
// The new document is ready to be inserted into the viewer.
|
||||
// (Embedded components have been loaded and instantiated, if necessary.)
|
||||
@Output() docReady = new EventEmitter<void>();
|
||||
|
||||
// The previous document has been removed from the viewer.
|
||||
// (The leaving animation (if any) has been completed and the node has been removed from the DOM.)
|
||||
@Output() docRemoved = new EventEmitter<void>();
|
||||
|
||||
// The new document has been inserted into the viewer.
|
||||
// (The node has been inserted into the DOM, but the entering animation may still be in progress.)
|
||||
@Output() docInserted = new EventEmitter<void>();
|
||||
|
||||
// The new document has been fully rendered into the viewer.
|
||||
// (The entering animation has been completed.)
|
||||
@Output() docRendered = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
componentFactoryResolver: ComponentFactoryResolver,
|
||||
elementRef: ElementRef,
|
||||
embeddedComponents: EmbeddedComponents,
|
||||
private injector: Injector,
|
||||
private embedComponentsService: EmbedComponentsService,
|
||||
private logger: Logger,
|
||||
private titleService: Title,
|
||||
private tocService: TocService
|
||||
) {
|
||||
@ -45,84 +78,157 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
|
||||
// Security: the initialDocViewerContent comes from the prerendered DOM and is considered to be secure
|
||||
this.hostElement.innerHTML = initialDocViewerContent;
|
||||
|
||||
for (const component of embeddedComponents.components) {
|
||||
const factory = componentFactoryResolver.resolveComponentFactory(component);
|
||||
const selector = factory.selector;
|
||||
const contentPropertyName = this.selectorToContentPropertyName(selector);
|
||||
this.embeddedComponentFactories.set(selector, { contentPropertyName, factory });
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
set doc(newDoc: DocumentContents) {
|
||||
this.ngOnDestroy();
|
||||
if (newDoc) {
|
||||
this.build(newDoc);
|
||||
this.docRendered.emit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add doc content to host element and build it out with embedded components
|
||||
*/
|
||||
private build(doc: DocumentContents) {
|
||||
|
||||
// security: the doc.content is always authored by the documentation team
|
||||
// and is considered to be safe
|
||||
this.hostElement.innerHTML = doc.contents || '';
|
||||
|
||||
if (!doc.contents) { return; }
|
||||
|
||||
this.addTitleAndToc(doc.id);
|
||||
|
||||
// TODO(i): why can't I use for-of? why doesn't typescript like Map#value() iterators?
|
||||
this.embeddedComponentFactories.forEach(({ contentPropertyName, factory }, selector) => {
|
||||
const embeddedComponentElements = this.hostElement.querySelectorAll(selector);
|
||||
|
||||
// cast due to https://github.com/Microsoft/TypeScript/issues/4947
|
||||
for (const element of embeddedComponentElements as any as HTMLElement[]){
|
||||
// hack: preserve the current element content because the factory will empty it out
|
||||
// security: the source of this innerHTML is always authored by the documentation team
|
||||
// and is considered to be safe
|
||||
element[contentPropertyName] = element.innerHTML;
|
||||
this.embeddedComponents.push(factory.create(this.injector, [], element));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private addTitleAndToc(docId: string) {
|
||||
this.tocService.reset();
|
||||
const titleEl = this.hostElement.querySelector('h1');
|
||||
let title = '';
|
||||
|
||||
// Only create TOC for docs with an <h1> title
|
||||
// If you don't want a TOC, add "no-toc" class to <h1>
|
||||
if (titleEl) {
|
||||
title = (typeof titleEl.innerText === 'string') ? titleEl.innerText : titleEl.textContent;
|
||||
if (!/(no-toc|notoc)/i.test(titleEl.className)) {
|
||||
this.tocService.genToc(this.hostElement, docId);
|
||||
titleEl.insertAdjacentHTML('afterend', '<aio-toc class="embedded"></aio-toc>');
|
||||
}
|
||||
if (this.hostElement.firstElementChild) {
|
||||
this.currViewContainer = this.hostElement.firstElementChild as HTMLElement;
|
||||
}
|
||||
|
||||
this.titleService.setTitle(title ? `Angular - ${title}` : 'Angular');
|
||||
this.onDestroy$.subscribe(() => this.destroyEmbeddedComponents());
|
||||
this.docContents$
|
||||
.switchMap(newDoc => this.render(newDoc))
|
||||
.takeUntil(this.onDestroy$)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
ngDoCheck() {
|
||||
this.embeddedComponents.forEach(comp => comp.changeDetectorRef.detectChanges());
|
||||
this.embeddedComponentRefs.forEach(comp => comp.changeDetectorRef.detectChanges());
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// destroy these components else there will be memory leaks
|
||||
this.embeddedComponents.forEach(comp => comp.destroy());
|
||||
this.embeddedComponents.length = 0;
|
||||
this.onDestroy$.emit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the component content property name by converting the selector to camelCase and appending
|
||||
* 'Content', e.g. live-example => liveExampleContent
|
||||
* Destroy the embedded components to avoid memory leaks.
|
||||
*/
|
||||
private selectorToContentPropertyName(selector: string) {
|
||||
return selector.replace(/-(.)/g, (match, $1) => $1.toUpperCase()) + 'Content';
|
||||
protected destroyEmbeddedComponents(): void {
|
||||
this.embeddedComponentRefs.forEach(comp => comp.destroy());
|
||||
this.embeddedComponentRefs = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare for setting the window title and ToC.
|
||||
* Return a function to actually set them.
|
||||
*/
|
||||
protected prepareTitleAndToc(targetElem: HTMLElement, docId: string): () => void {
|
||||
const titleEl = targetElem.querySelector('h1');
|
||||
const hasToc = !!titleEl && !/no-?toc/i.test(titleEl.className);
|
||||
|
||||
if (hasToc) {
|
||||
titleEl.insertAdjacentHTML('afterend', '<aio-toc class="embedded"></aio-toc>');
|
||||
}
|
||||
|
||||
return () => {
|
||||
this.tocService.reset();
|
||||
let title = '';
|
||||
|
||||
// Only create ToC for docs with an `<h1>` heading.
|
||||
// If you don't want a ToC, add "no-toc" class to `<h1>`.
|
||||
if (titleEl) {
|
||||
title = (typeof titleEl.innerText === 'string') ? titleEl.innerText : titleEl.textContent;
|
||||
|
||||
if (hasToc) {
|
||||
this.tocService.genToc(targetElem, docId);
|
||||
}
|
||||
}
|
||||
|
||||
this.titleService.setTitle(title ? `Angular - ${title}` : 'Angular');
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add doc content to host element and build it out with embedded components.
|
||||
*/
|
||||
protected render(doc: DocumentContents): Observable<void> {
|
||||
let addTitleAndToc: () => void;
|
||||
|
||||
return this.void$
|
||||
// Security: `doc.contents` is always authored by the documentation team
|
||||
// and is considered to be safe.
|
||||
.do(() => this.nextViewContainer.innerHTML = doc.contents || '')
|
||||
.do(() => addTitleAndToc = this.prepareTitleAndToc(this.nextViewContainer, doc.id))
|
||||
.switchMap(() => this.embedComponentsService.embedInto(this.nextViewContainer))
|
||||
.do(() => this.docReady.emit())
|
||||
.do(() => this.destroyEmbeddedComponents())
|
||||
.do(componentRefs => this.embeddedComponentRefs = componentRefs)
|
||||
.switchMap(() => this.swapViews(addTitleAndToc))
|
||||
.do(() => this.docRendered.emit())
|
||||
.catch(err => {
|
||||
this.nextViewContainer.innerHTML = '';
|
||||
this.logger.error(`[DocViewer]: Error preparing document '${doc.id}'.`, err);
|
||||
return this.void$;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap the views, removing `currViewContainer` and inserting `nextViewContainer`.
|
||||
* (At this point all content should be ready, including having loaded and instantiated embedded
|
||||
* components.)
|
||||
*
|
||||
* Optionally, run a callback as soon as `nextViewContainer` has been inserted, but before the
|
||||
* entering animation has been completed. This is useful for work that needs to be done as soon as
|
||||
* the element has been attached to the DOM.
|
||||
*/
|
||||
protected swapViews(onInsertedCb = () => undefined): Observable<void> {
|
||||
const raf$ = new Observable<void>(subscriber => {
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
subscriber.next();
|
||||
subscriber.complete();
|
||||
});
|
||||
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 = 200) => {
|
||||
const animationsDisabled = !DocViewerComponent.animationsEnabled
|
||||
|| this.hostElement.classList.contains(NO_ANIMATIONS);
|
||||
|
||||
elem.style.transition = '';
|
||||
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.
|
||||
// without transition) and that the `to` value will be affected by the
|
||||
// `transition` style, we need to ensure an animation frame has passed between
|
||||
// setting each style.
|
||||
.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(getActualDuration(elem))).switchMap(() => this.void$);
|
||||
};
|
||||
|
||||
const animateLeave = (elem: HTMLElement) => animateProp(elem, 'opacity', '1', '0.1');
|
||||
const animateEnter = (elem: HTMLElement) => animateProp(elem, 'opacity', '0.1', '1');
|
||||
|
||||
let done$ = this.void$;
|
||||
|
||||
if (this.currViewContainer.parentElement) {
|
||||
done$ = done$
|
||||
// Remove the current view from the viewer.
|
||||
.switchMap(() => animateLeave(this.currViewContainer))
|
||||
.do(() => this.currViewContainer.parentElement.removeChild(this.currViewContainer))
|
||||
.do(() => this.docRemoved.emit());
|
||||
}
|
||||
|
||||
return done$
|
||||
// Insert the next view into the viewer.
|
||||
.do(() => this.hostElement.appendChild(this.nextViewContainer))
|
||||
.do(() => onInsertedCb())
|
||||
.do(() => this.docInserted.emit())
|
||||
.switchMap(() => animateEnter(this.nextViewContainer))
|
||||
// Update the view references and clean up unused nodes.
|
||||
.do(() => {
|
||||
const prevViewContainer = this.currViewContainer;
|
||||
this.currViewContainer = this.nextViewContainer;
|
||||
this.nextViewContainer = prevViewContainer;
|
||||
this.nextViewContainer.innerHTML = ''; // Empty to release memory.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { InjectionToken, Inject, Injectable } from '@angular/core';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import { MatIconRegistry } from '@angular/material';
|
||||
import { MatIconRegistry } from '@angular/material/icon';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
|
||||
@ -27,19 +27,6 @@ interface SvgIconMap {
|
||||
[iconName: string]: SVGElement;
|
||||
}
|
||||
|
||||
// <hack-alert>
|
||||
// @angular/material's `MdIconRegitry` currently (v2.0.0-beta.8) requires an instance of `Http`
|
||||
// (from @angular/http). It is only used to [get some text content][1], so we can create a wrapper
|
||||
// around `HttpClient` and pretend it is `Http`.
|
||||
// [1]: https://github.com/angular/material2/blob/2.0.0-beta.8/src/lib/icon/icon-registry.ts#L465-L466
|
||||
// </hack-alert>
|
||||
function createFakeHttp(http: HttpClient): any {
|
||||
return {
|
||||
get: (url: string) => http.get(url, {responseType: 'text'})
|
||||
.map(data => ({text: () => data}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom replacement for Angular Material's `MdIconRegistry`, which allows
|
||||
* us to provide preloaded icon SVG sources.
|
||||
@ -49,7 +36,7 @@ export class CustomIconRegistry extends MatIconRegistry {
|
||||
private preloadedSvgElements: SvgIconMap = {};
|
||||
|
||||
constructor(http: HttpClient, sanitizer: DomSanitizer, @Inject(SVG_ICONS) svgIcons: SvgIconInfo[]) {
|
||||
super(createFakeHttp(http), sanitizer);
|
||||
super(http, sanitizer);
|
||||
this.loadSvgElements(svgIcons);
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ export class ScrollService {
|
||||
// at the top (e.g. toolbar) + some margin
|
||||
get topOffset() {
|
||||
if (!this._topOffset) {
|
||||
const toolbar = this.document.querySelector('mat-toolbar.app-toolbar');
|
||||
const toolbar = this.document.querySelector('.app-toolbar');
|
||||
this._topOffset = (toolbar && toolbar.clientHeight || 0) + topMargin;
|
||||
}
|
||||
return this._topOffset;
|
||||
|
@ -100,7 +100,7 @@ a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mat-toolbar-row a {
|
||||
.app-toolbar a {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
color: white;
|
||||
|
4
aio/src/styles/1-layouts/_doc-viewer.scss
Normal file
@ -0,0 +1,4 @@
|
||||
.no-animations aio-doc-viewer > * {
|
||||
// Disable view transition animations.
|
||||
transition: none !important;
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
|
||||
@import 'api-page';
|
||||
@import 'content-layout';
|
||||
@import 'doc-viewer';
|
||||
@import 'footer';
|
||||
@import 'layout-global';
|
||||
@import 'marketing-layout';
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
@ -16,7 +16,6 @@
|
||||
@import 'edit-page-cta';
|
||||
@import 'features';
|
||||
@import 'filetree';
|
||||
@import 'hamburger';
|
||||
@import 'heading-anchors';
|
||||
@import 'hr';
|
||||
@import 'images';
|
||||
|
88
aio/src/testing/doc-viewer-utils.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { Component, ComponentRef, NgModule, ViewChild } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { DocumentContents } from 'app/documents/document.service';
|
||||
import { EmbedComponentsService } from 'app/embed-components/embed-components.service';
|
||||
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { TocService } from 'app/shared/toc.service';
|
||||
import { MockLogger } from 'testing/logger.service';
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/// `TestDocViewerComponent` (for exposing internal `DocViewerComponent` methods as public). ///
|
||||
/// Only used for type-casting; the actual implementation is irrelevant. ///
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export class TestDocViewerComponent extends DocViewerComponent {
|
||||
embeddedComponentRefs: ComponentRef<any>[];
|
||||
currViewContainer: HTMLElement;
|
||||
nextViewContainer: HTMLElement;
|
||||
|
||||
destroyEmbeddedComponents(): void { return null as any; }
|
||||
prepareTitleAndToc(targetElem: HTMLElement, docId: string): () => void { return null as any; }
|
||||
render(doc: DocumentContents): Observable<void> { return null as any; }
|
||||
swapViews(onInsertedCb?: () => void): Observable<void> { return null as any; }
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/// `TestModule` and `TestParentComponent`. ///
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Test parent component.
|
||||
@Component({
|
||||
selector: 'aio-test',
|
||||
template: '<aio-doc-viewer [doc]="currentDoc">Test Component</aio-doc-viewer>',
|
||||
})
|
||||
export class TestParentComponent {
|
||||
currentDoc: DocumentContents;
|
||||
@ViewChild(DocViewerComponent) docViewer: DocViewerComponent;
|
||||
}
|
||||
|
||||
// Mock services.
|
||||
export class MockEmbedComponentsService {
|
||||
embedInto = jasmine.createSpy('EmbedComponentsService#embedInto');
|
||||
}
|
||||
|
||||
export class MockTitle {
|
||||
setTitle = jasmine.createSpy('Title#reset');
|
||||
}
|
||||
|
||||
export class MockTocService {
|
||||
genToc = jasmine.createSpy('TocService#genToc');
|
||||
reset = jasmine.createSpy('TocService#reset');
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DocViewerComponent,
|
||||
TestParentComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: Logger, useClass: MockLogger },
|
||||
{ provide: EmbedComponentsService, useClass: MockEmbedComponentsService },
|
||||
{ provide: Title, useClass: MockTitle },
|
||||
{ provide: TocService, useClass: MockTocService },
|
||||
],
|
||||
})
|
||||
export class TestModule { }
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/// An observable with spies to test subscribing/unsubscribing. ///
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export class ObservableWithSubscriptionSpies<T = void> extends Observable<T> {
|
||||
unsubscribeSpies: jasmine.Spy[] = [];
|
||||
subscribeSpy = spyOn(this, 'subscribe').and.callFake((...args) => {
|
||||
const subscription = super.subscribe(...args);
|
||||
const unsubscribeSpy = spyOn(subscription, 'unsubscribe').and.callThrough();
|
||||
this.unsubscribeSpies.push(unsubscribeSpy);
|
||||
return subscription;
|
||||
});
|
||||
|
||||
constructor(subscriber = () => undefined) { super(subscriber); }
|
||||
}
|
138
aio/src/testing/embed-components-utils.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import {
|
||||
Component, ComponentFactoryResolver, ComponentRef, CompilerFactory, ElementRef, NgModule,
|
||||
NgModuleFactoryLoader, OnInit, Type, ViewChild, getPlatform
|
||||
} from '@angular/core';
|
||||
|
||||
import {
|
||||
ComponentsOrModulePath, EMBEDDED_COMPONENTS, EmbedComponentsService, EmbeddedComponentFactory,
|
||||
WithEmbeddedComponents
|
||||
} from 'app/embed-components/embed-components.service';
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/// `TestEmbedComponentsService` (for exposing internal methods as public). ///
|
||||
/// Only used for type-casting; the actual implementation is irrelevant. ///
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export class TestEmbedComponentsService extends EmbedComponentsService {
|
||||
componentFactories: Map<string, EmbeddedComponentFactory>;
|
||||
|
||||
createComponentFactories(components: Type<any>[], resolver: ComponentFactoryResolver): void { return null as any; }
|
||||
createComponents(elem: HTMLElement): ComponentRef<any>[] { return null as any; }
|
||||
prepareComponentFactories(compsOrPath: ComponentsOrModulePath): Promise<void> { return null as any; }
|
||||
selectorToContentPropertyName(selector: string): string { return null as any; }
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/// Mock `EmbeddedModule` and test components. ///
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Test embedded components.
|
||||
@Component({
|
||||
selector: 'aio-eager-foo',
|
||||
template: `Eager Foo Component`,
|
||||
})
|
||||
class EagerFooComponent { }
|
||||
|
||||
@Component({
|
||||
selector: 'aio-eager-bar',
|
||||
template: `
|
||||
<hr>
|
||||
<h2>Eager Bar Component</h2>
|
||||
<p #content></p>
|
||||
<hr>
|
||||
`,
|
||||
})
|
||||
class EagerBarComponent implements OnInit {
|
||||
@ViewChild('content') contentRef: ElementRef;
|
||||
|
||||
constructor(public elementRef: ElementRef) { }
|
||||
|
||||
// Project content in `ngOnInit()` just like in `CodeExampleComponent`.
|
||||
ngOnInit() {
|
||||
// Security: This is a test component; never deployed.
|
||||
this.contentRef.nativeElement.innerHTML = this.elementRef.nativeElement.aioEagerBarContent;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'aio-lazy-foo',
|
||||
template: `Lazy Foo Component`,
|
||||
})
|
||||
class LazyFooComponent { }
|
||||
|
||||
@Component({
|
||||
selector: 'aio-lazy-bar',
|
||||
template: `
|
||||
<hr>
|
||||
<h2>Lazy Bar Component</h2>
|
||||
<p #content></p>
|
||||
<hr>
|
||||
`,
|
||||
})
|
||||
class LazyBarComponent implements OnInit {
|
||||
@ViewChild('content') contentRef: ElementRef;
|
||||
|
||||
constructor(public elementRef: ElementRef) { }
|
||||
|
||||
// Project content in `ngOnInit()` just like in `CodeExampleComponent`.
|
||||
ngOnInit() {
|
||||
// Security: This is a test component; never deployed.
|
||||
this.contentRef.nativeElement.innerHTML = this.elementRef.nativeElement.aioLazyBarContent;
|
||||
}
|
||||
}
|
||||
|
||||
// Export test embedded selectors and components.
|
||||
export const testEagerEmbeddedSelectors = ['aio-eager-foo', 'aio-eager-bar'];
|
||||
export const testEagerEmbeddedComponents = [EagerFooComponent, EagerBarComponent];
|
||||
export const testLazyEmbeddedSelectors = ['aio-lazy-foo', 'aio-lazy-bar'];
|
||||
export const testLazyEmbeddedComponents = [LazyFooComponent, LazyBarComponent];
|
||||
|
||||
// Export mock `EmbeddedModule` and path.
|
||||
export const mockEmbeddedModulePath = 'mock/mock-embedded#MockEmbeddedModule';
|
||||
|
||||
@NgModule({
|
||||
declarations: [testLazyEmbeddedComponents],
|
||||
entryComponents: [testLazyEmbeddedComponents],
|
||||
})
|
||||
class MockEmbeddedModule implements WithEmbeddedComponents {
|
||||
embeddedComponents = testLazyEmbeddedComponents;
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/// `TestModule`. ///
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Mock services.
|
||||
export class MockNgModuleFactoryLoader implements NgModuleFactoryLoader {
|
||||
loadedPaths: string[] = [];
|
||||
|
||||
load(path: string) {
|
||||
this.loadedPaths.push(path);
|
||||
|
||||
const platformRef = getPlatform();
|
||||
const compilerFactory = platformRef.injector.get(CompilerFactory) as CompilerFactory;
|
||||
const compiler = compilerFactory.createCompiler([]);
|
||||
|
||||
return compiler.compileModuleAsync(MockEmbeddedModule);
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
EmbedComponentsService,
|
||||
{ provide: NgModuleFactoryLoader, useClass: MockNgModuleFactoryLoader },
|
||||
{
|
||||
provide: EMBEDDED_COMPONENTS,
|
||||
useValue: {
|
||||
[testEagerEmbeddedSelectors.join(',')]: testEagerEmbeddedComponents,
|
||||
[testLazyEmbeddedSelectors.join(',')]: mockEmbeddedModulePath,
|
||||
},
|
||||
},
|
||||
],
|
||||
declarations: [testEagerEmbeddedComponents],
|
||||
entryComponents: [testEagerEmbeddedComponents],
|
||||
})
|
||||
export class TestModule { }
|
@ -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" },
|
||||
|
@ -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.
|
||||
|
@ -200,8 +200,10 @@ function runProtractorAoT(appDir, outputFile) {
|
||||
// All protractor output is appended to the outputFile.
|
||||
// CLI version
|
||||
function runE2eTestsCLI(appDir, outputFile) {
|
||||
// --preserve-symlinks is needed due the symlinked node_modules in each example
|
||||
const e2eSpawn = spawnExt('yarn', ['e2e', '--preserve-symlinks'], { cwd: appDir });
|
||||
// `--preserve-symlinks` is needed due the symlinked `node_modules/` in each example.
|
||||
// `--no-webdriver-update` is needed to preserve the ChromeDriver version already installed.
|
||||
const args = ['e2e', '--preserve-symlinks', '--no-webdriver-update'];
|
||||
const e2eSpawn = spawnExt('yarn', args, { cwd: appDir });
|
||||
return e2eSpawn.promise.then(
|
||||
function () {
|
||||
fs.appendFileSync(outputFile, `Passed: ${appDir}\n\n`);
|
||||
|
@ -9,7 +9,6 @@
|
||||
"exclude": [
|
||||
"test.ts",
|
||||
"**/*.spec.ts",
|
||||
"testing/**",
|
||||
"**/*.1.*"
|
||||
"**/testing/*"
|
||||
]
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"http-server": "http-server",
|
||||
"protractor": "protractor",
|
||||
"webdriver:update": "webdriver-manager update --standalone false --gecko false",
|
||||
"webdriver:update": "webdriver-manager update --standalone false --gecko false $CHROMEDRIVER_VERSION_ARG",
|
||||
"postinstall": "yarn webdriver:update"
|
||||
},
|
||||
"keywords": [],
|
||||
|
@ -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',
|
||||
|
223
aio/yarn.lock
@ -17,6 +17,12 @@
|
||||
dependencies:
|
||||
source-map "^0.5.6"
|
||||
|
||||
"@angular-devkit/core@0.0.21":
|
||||
version "0.0.21"
|
||||
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-0.0.21.tgz#e2ba4ac0a4e1156f884c083b15eb1c26ddfb2ba8"
|
||||
dependencies:
|
||||
source-map "^0.5.6"
|
||||
|
||||
"@angular-devkit/schematics@~0.0.34":
|
||||
version "0.0.34"
|
||||
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-0.0.34.tgz#c3ef61b0e49e585d9982f2828e9a67b3879a6b1b"
|
||||
@ -26,27 +32,27 @@
|
||||
minimist "^1.2.0"
|
||||
rxjs "^5.4.2"
|
||||
|
||||
"@angular/animations@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-5.0.0.tgz#b5ad199c67f93f759544477effe6679e154991fb"
|
||||
"@angular/animations@^5.1.0-beta.2":
|
||||
version "5.1.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-5.1.0-beta.2.tgz#2ed05bfa0ed22d6d1550658785e965c1fbfec5a7"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
|
||||
"@angular/cdk@^2.0.0-beta.12":
|
||||
version "2.0.0-beta.12"
|
||||
resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-2.0.0-beta.12.tgz#3a243cb62b93f4e039120ba70f900dc9e235622e"
|
||||
"@angular/cdk@^5.0.0-rc.1":
|
||||
version "5.0.0-rc.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-5.0.0-rc.1.tgz#7d6d937fea50074962db8ce02664b164401d197d"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
|
||||
"@angular/cli@^1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-1.5.0.tgz#2abc3ff1648d54ad4a14f82f9d517f371370406c"
|
||||
"@angular/cli@^1.6.0-rc.0":
|
||||
version "1.6.0-rc.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-1.6.0-rc.0.tgz#e6bf974dcb2263e026d3d4ffa4480e4e78f9c7cf"
|
||||
dependencies:
|
||||
"@angular-devkit/build-optimizer" "~0.0.31"
|
||||
"@angular-devkit/schematics" "~0.0.34"
|
||||
"@ngtools/json-schema" "1.1.0"
|
||||
"@ngtools/webpack" "1.8.0"
|
||||
"@schematics/angular" "~0.1.0"
|
||||
"@ngtools/webpack" "1.9.0-rc.0"
|
||||
"@schematics/angular" "~0.1.5"
|
||||
autoprefixer "^6.5.3"
|
||||
chalk "~2.2.0"
|
||||
circular-dependency-plugin "^3.0.0"
|
||||
@ -70,13 +76,14 @@
|
||||
license-webpack-plugin "^1.0.0"
|
||||
lodash "^4.11.1"
|
||||
memory-fs "^0.4.1"
|
||||
minimatch "^3.0.4"
|
||||
node-modules-path "^1.0.0"
|
||||
nopt "^4.0.1"
|
||||
opn "~5.1.0"
|
||||
portfinder "~1.0.12"
|
||||
postcss-custom-properties "^6.1.0"
|
||||
postcss-loader "^1.3.3"
|
||||
postcss-url "^5.1.2"
|
||||
postcss-loader "^2.0.8"
|
||||
postcss-url "^7.1.2"
|
||||
raw-loader "^0.5.1"
|
||||
resolve "^1.1.7"
|
||||
rxjs "^5.5.2"
|
||||
@ -101,74 +108,74 @@
|
||||
optionalDependencies:
|
||||
node-sass "^4.3.0"
|
||||
|
||||
"@angular/common@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/common/-/common-5.0.0.tgz#f96d66a517b995d1ba9b28309f15c2e359675825"
|
||||
"@angular/common@^5.1.0-beta.2":
|
||||
version "5.1.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/@angular/common/-/common-5.1.0-beta.2.tgz#027b16b8d2f63eb14d9fbc84a0a5dd91945885d6"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
|
||||
"@angular/compiler-cli@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-5.0.0.tgz#0ecbb937d84a4f8dd94f0c2a47b07d2e4694c853"
|
||||
"@angular/compiler-cli@^5.1.0-beta.2":
|
||||
version "5.1.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-5.1.0-beta.2.tgz#62b63e30bdae910c322a62b76fa790dd4a6cdf53"
|
||||
dependencies:
|
||||
chokidar "^1.4.2"
|
||||
minimist "^1.2.0"
|
||||
reflect-metadata "^0.1.2"
|
||||
tsickle "^0.24.0"
|
||||
tsickle "^0.25.5"
|
||||
|
||||
"@angular/compiler@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-5.0.0.tgz#b9ffbf18c8a39d8b7dacec473193a90e24cc2bc9"
|
||||
"@angular/compiler@^5.1.0-beta.2":
|
||||
version "5.1.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-5.1.0-beta.2.tgz#6c88c3485558dcf85144f55edb1299d99cdb3fec"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
|
||||
"@angular/core@^5.0.0-rc.9":
|
||||
version "5.0.0-rc.9"
|
||||
resolved "https://registry.yarnpkg.com/@angular/core/-/core-5.0.0-rc.9.tgz#0aab5d2d9b31ea530a41bb1e3009504d1bd64673"
|
||||
"@angular/core@^5.1.0-beta.2":
|
||||
version "5.1.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/@angular/core/-/core-5.1.0-beta.2.tgz#e94b21cae4479caed9486239b065302a97c5fd4f"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
|
||||
"@angular/forms@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-5.0.0.tgz#c7fddfa35396759ae9852920a30cdda8c41ed1de"
|
||||
"@angular/forms@^5.1.0-beta.2":
|
||||
version "5.1.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-5.1.0-beta.2.tgz#feeee29041cce227cc50e254ebecc2f19dc529ee"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
|
||||
"@angular/http@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/http/-/http-5.0.0.tgz#0728a2be0cfbb078727c5eb87d4c85d53fec9a51"
|
||||
"@angular/http@^5.1.0-beta.2":
|
||||
version "5.1.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/@angular/http/-/http-5.1.0-beta.2.tgz#935beaba2afea192d6a75a1c3b8d7dfd787641b0"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
|
||||
"@angular/material@^2.0.0-beta.12":
|
||||
version "2.0.0-beta.12"
|
||||
resolved "https://registry.yarnpkg.com/@angular/material/-/material-2.0.0-beta.12.tgz#71b6d0b7b021891e5d0e3688c1d4bd78c7457f58"
|
||||
"@angular/material@^5.0.0-rc.1":
|
||||
version "5.0.0-rc.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/material/-/material-5.0.0-rc.1.tgz#63456b7568c102b6bb7983a2837cc317350fe270"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
|
||||
"@angular/platform-browser-dynamic@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-5.0.0.tgz#887e106c8b103b0415cf6156a425da6d83f4c89d"
|
||||
"@angular/platform-browser-dynamic@^5.1.0-beta.2":
|
||||
version "5.1.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-5.1.0-beta.2.tgz#bee1d37f7300030edd855edf66311b0d493252d5"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
|
||||
"@angular/platform-browser@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-5.0.0.tgz#c7038f7cde80705b62014897231e182eec976fed"
|
||||
"@angular/platform-browser@^5.1.0-beta.2":
|
||||
version "5.1.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-5.1.0-beta.2.tgz#ccfca9d172f033137c83f2fbe8b702fe7204c5d5"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
|
||||
"@angular/platform-server@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-5.0.0.tgz#877d25ef814af92fffc7b0b523b971a6ff222018"
|
||||
"@angular/platform-server@^5.1.0-beta.2":
|
||||
version "5.1.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-5.1.0-beta.2.tgz#a36583b274f263eeb9e1c23f6f0b83c36cc4a72a"
|
||||
dependencies:
|
||||
domino "^1.0.29"
|
||||
tslib "^1.7.1"
|
||||
xhr2 "^0.1.4"
|
||||
|
||||
"@angular/router@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/router/-/router-5.0.0.tgz#fe4b521a6738408bce30f93a53499140c93a4f76"
|
||||
"@angular/router@^5.1.0-beta.2":
|
||||
version "5.1.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/@angular/router/-/router-5.1.0-beta.2.tgz#a22b8c0e08b81c8b0950b9a668aa893e9f1c0e4c"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
|
||||
@ -254,9 +261,9 @@
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ngtools/json-schema/-/json-schema-1.1.0.tgz#c3a0c544d62392acc2813a42c8a0dc6f58f86922"
|
||||
|
||||
"@ngtools/webpack@1.8.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-1.8.0.tgz#443204e016aa3a287544eacadb4c95964ae6e2c4"
|
||||
"@ngtools/webpack@1.9.0-rc.0":
|
||||
version "1.9.0-rc.0"
|
||||
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-1.9.0-rc.0.tgz#632e91c0e888e20c717eabeef58107e24d4ed663"
|
||||
dependencies:
|
||||
chalk "~2.2.0"
|
||||
enhanced-resolve "^3.1.0"
|
||||
@ -266,11 +273,11 @@
|
||||
source-map "^0.5.6"
|
||||
tree-kill "^1.0.0"
|
||||
|
||||
"@schematics/angular@~0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-0.1.0.tgz#cd6c445ead33144a532523d1ccac68c38a2b0050"
|
||||
"@schematics/angular@~0.1.5":
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-0.1.7.tgz#2306aeec118ca185e180882eff54f5116de4ef05"
|
||||
dependencies:
|
||||
"@angular-devkit/core" "0.0.20"
|
||||
"@angular-devkit/core" "0.0.21"
|
||||
|
||||
"@types/core-js@^0.9.41":
|
||||
version "0.9.43"
|
||||
@ -704,7 +711,7 @@ async-foreach@^0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
|
||||
|
||||
async@0.2.x, async@~0.2.6:
|
||||
async@0.2.x:
|
||||
version "0.2.10"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
|
||||
|
||||
@ -1291,7 +1298,7 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
|
||||
strip-ansi "^3.0.0"
|
||||
supports-color "^2.0.0"
|
||||
|
||||
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0:
|
||||
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba"
|
||||
dependencies:
|
||||
@ -2093,6 +2100,10 @@ csv-streamify@^3.0.4:
|
||||
dependencies:
|
||||
through2 "2.0.1"
|
||||
|
||||
cuint@latest:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b"
|
||||
|
||||
currently-unhandled@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
|
||||
@ -2347,14 +2358,6 @@ diffie-hellman@^5.0.0:
|
||||
miller-rabin "^4.0.0"
|
||||
randombytes "^2.0.0"
|
||||
|
||||
directory-encoder@^0.7.2:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/directory-encoder/-/directory-encoder-0.7.2.tgz#59b4e2aa4f25422f6c63b527b462f5e2d0dd2c58"
|
||||
dependencies:
|
||||
fs-extra "^0.23.1"
|
||||
handlebars "^1.3.0"
|
||||
img-stats "^0.5.2"
|
||||
|
||||
dns-equal@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
|
||||
@ -3674,14 +3677,6 @@ handle-thing@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
|
||||
|
||||
handlebars@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-1.3.0.tgz#9e9b130a93e389491322d975cf3ec1818c37ce34"
|
||||
dependencies:
|
||||
optimist "~0.3"
|
||||
optionalDependencies:
|
||||
uglify-js "~2.3"
|
||||
|
||||
handlebars@^4.0.3:
|
||||
version "4.0.11"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc"
|
||||
@ -4123,12 +4118,6 @@ image-ssim@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/image-ssim/-/image-ssim-0.2.0.tgz#83b42c7a2e6e4b85505477fe6917f5dbc56420e5"
|
||||
|
||||
img-stats@^0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/img-stats/-/img-stats-0.5.2.tgz#c203496c42f2d9eb2e5ab8232fa756bab32c9e2b"
|
||||
dependencies:
|
||||
xmldom "^0.1.19"
|
||||
|
||||
immediate@~3.0.5:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
|
||||
@ -4627,7 +4616,7 @@ isurl@^1.0.0-alpha5:
|
||||
has-to-string-tag-x "^1.2.0"
|
||||
is-object "^1.0.1"
|
||||
|
||||
jasmine-core@^2.6.4, jasmine-core@~2.8.0:
|
||||
jasmine-core@^2.8.0, jasmine-core@~2.8.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e"
|
||||
|
||||
@ -4674,7 +4663,7 @@ jpeg-js@0.1.2, jpeg-js@^0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.1.2.tgz#135b992c0575c985cfa0f494a3227ed238583ece"
|
||||
|
||||
js-base64@^2.1.5, js-base64@^2.1.8, js-base64@^2.1.9:
|
||||
js-base64@^2.1.8, js-base64@^2.1.9:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.2.tgz#a79a923666372b580f8e27f51845c6f7e8fbfbaf"
|
||||
|
||||
@ -5863,7 +5852,7 @@ object-assign@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2"
|
||||
|
||||
object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
|
||||
object-assign@^4.0.1, object-assign@^4.1.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
|
||||
@ -5938,12 +5927,6 @@ optimist@0.6.x, optimist@^0.6.1, optimist@~0.6.0, optimist@~0.6.1:
|
||||
minimist "~0.0.1"
|
||||
wordwrap "~0.0.2"
|
||||
|
||||
optimist@~0.3, optimist@~0.3.5:
|
||||
version "0.3.7"
|
||||
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9"
|
||||
dependencies:
|
||||
wordwrap "~0.0.2"
|
||||
|
||||
optionator@^0.8.1, optionator@^0.8.2:
|
||||
version "0.8.2"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
|
||||
@ -6383,14 +6366,14 @@ postcss-load-plugins@^2.3.0:
|
||||
cosmiconfig "^2.1.1"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
postcss-loader@^1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-1.3.3.tgz#a621ea1fa29062a83972a46f54486771301916eb"
|
||||
postcss-loader@^2.0.8:
|
||||
version "2.0.8"
|
||||
resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.0.8.tgz#8c67ddb029407dfafe684a406cfc16bad2ce0814"
|
||||
dependencies:
|
||||
loader-utils "^1.0.2"
|
||||
object-assign "^4.1.1"
|
||||
postcss "^5.2.15"
|
||||
loader-utils "^1.1.0"
|
||||
postcss "^6.0.0"
|
||||
postcss-load-config "^1.2.0"
|
||||
schema-utils "^0.3.0"
|
||||
|
||||
postcss-merge-idents@^2.1.5:
|
||||
version "2.1.7"
|
||||
@ -6548,17 +6531,15 @@ postcss-unique-selectors@^2.0.2:
|
||||
postcss "^5.0.4"
|
||||
uniqs "^2.0.0"
|
||||
|
||||
postcss-url@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/postcss-url/-/postcss-url-5.1.2.tgz#98b3165be8d592471cb0caadde2c0d1f832f133e"
|
||||
postcss-url@^7.1.2:
|
||||
version "7.2.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-url/-/postcss-url-7.2.1.tgz#bf47ff1b5395538567cea19ef3241a70fb925f5c"
|
||||
dependencies:
|
||||
directory-encoder "^0.7.2"
|
||||
js-base64 "^2.1.5"
|
||||
mime "^1.2.11"
|
||||
minimatch "^3.0.0"
|
||||
mime "^1.4.1"
|
||||
minimatch "^3.0.4"
|
||||
mkdirp "^0.5.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
postcss "^5.0.0"
|
||||
postcss "^6.0.1"
|
||||
xxhashjs "^0.2.1"
|
||||
|
||||
postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0:
|
||||
version "3.3.0"
|
||||
@ -6572,7 +6553,7 @@ postcss-zindex@^2.0.1:
|
||||
postcss "^5.0.4"
|
||||
uniqs "^2.0.0"
|
||||
|
||||
postcss@^5.0.0, postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.15, postcss@^5.2.16:
|
||||
postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16:
|
||||
version "5.2.18"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5"
|
||||
dependencies:
|
||||
@ -6581,6 +6562,14 @@ postcss@^5.0.0, postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.
|
||||
source-map "^0.5.6"
|
||||
supports-color "^3.2.3"
|
||||
|
||||
postcss@^6.0.0:
|
||||
version "6.0.14"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.14.tgz#5534c72114739e75d0afcf017db853099f562885"
|
||||
dependencies:
|
||||
chalk "^2.3.0"
|
||||
source-map "^0.6.1"
|
||||
supports-color "^4.4.0"
|
||||
|
||||
postcss@^6.0.1, postcss@^6.0.13:
|
||||
version "6.0.13"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.13.tgz#b9ecab4ee00c89db3ec931145bd9590bbf3f125f"
|
||||
@ -7319,7 +7308,7 @@ rx@2.3.24:
|
||||
version "2.3.24"
|
||||
resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7"
|
||||
|
||||
rxjs@^5.4.2, rxjs@^5.5.0:
|
||||
rxjs@^5.4.2:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.0.tgz#26d8f3866eb700e247e0728a147c3d628993d812"
|
||||
dependencies:
|
||||
@ -7729,7 +7718,7 @@ source-map-support@^0.4.0, source-map-support@^0.4.1, source-map-support@^0.4.15
|
||||
dependencies:
|
||||
source-map "^0.5.6"
|
||||
|
||||
source-map@0.1.x, source-map@~0.1.7:
|
||||
source-map@0.1.x:
|
||||
version "0.1.43"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346"
|
||||
dependencies:
|
||||
@ -8379,9 +8368,9 @@ tsconfig@^6.0.0:
|
||||
strip-bom "^3.0.0"
|
||||
strip-json-comments "^2.0.0"
|
||||
|
||||
tsickle@^0.24.0:
|
||||
version "0.24.1"
|
||||
resolved "https://registry.yarnpkg.com/tsickle/-/tsickle-0.24.1.tgz#039343b205bf517a333b0703978892f80a7d848e"
|
||||
tsickle@^0.25.5:
|
||||
version "0.25.5"
|
||||
resolved "https://registry.yarnpkg.com/tsickle/-/tsickle-0.25.5.tgz#2891d29f97c4aab1306e06378d8496d1765a4bfe"
|
||||
dependencies:
|
||||
minimist "^1.2.0"
|
||||
mkdirp "^0.5.1"
|
||||
@ -8478,14 +8467,6 @@ uglify-js@^2.6, uglify-js@^2.8.29:
|
||||
optionalDependencies:
|
||||
uglify-to-browserify "~1.0.0"
|
||||
|
||||
uglify-js@~2.3:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.3.6.tgz#fa0984770b428b7a9b2a8058f46355d14fef211a"
|
||||
dependencies:
|
||||
async "~0.2.6"
|
||||
optimist "~0.3.5"
|
||||
source-map "~0.1.7"
|
||||
|
||||
uglify-to-browserify@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
|
||||
@ -9299,10 +9280,6 @@ xmlbuilder@>=1.0.0, xmlbuilder@~9.0.1:
|
||||
version "9.0.4"
|
||||
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.4.tgz#519cb4ca686d005a8420d3496f3f0caeecca580f"
|
||||
|
||||
xmldom@^0.1.19:
|
||||
version "0.1.27"
|
||||
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
|
||||
|
||||
xmlhttprequest-ssl@1.5.3:
|
||||
version "1.5.3"
|
||||
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d"
|
||||
@ -9311,6 +9288,12 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
|
||||
|
||||
xxhashjs@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/xxhashjs/-/xxhashjs-0.2.1.tgz#9bbe9be896142976dfa34c061b2d068c43d30de0"
|
||||
dependencies:
|
||||
cuint latest
|
||||
|
||||
y18n@^3.2.0, y18n@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
|
||||
|
2
build.sh
@ -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"
|
||||
|
@ -21,16 +21,16 @@ following products on your development machine:
|
||||
[Windows](http://windows.github.com)); [GitHub's Guide to Installing
|
||||
Git](https://help.github.com/articles/set-up-git) is a good source of information.
|
||||
|
||||
* [Node.js](http://nodejs.org), (version `>=6.9.5 <7.0.0`) which is used to run a development web server,
|
||||
run tests, and generate distributable files. We also use Node's Package Manager, `npm`
|
||||
(version `>=3.10.7 <4.0.0`), which comes with Node. Depending on your system, you can install Node either from
|
||||
source or as a pre-packaged bundle.
|
||||
* [Node.js](http://nodejs.org), (version specified in the engines field of [`package.json`](../package.json)) which is used to run a development web server,
|
||||
run tests, and generate distributable files.
|
||||
|
||||
* [Yarn](https://yarnpkg.com) (version `>=1.0.2 <2.0.0`) which is used to install dependencies.
|
||||
* [Yarn](https://yarnpkg.com) (version specified in the engines field of [`package.json`](../package.json)) which is used to install dependencies.
|
||||
|
||||
* [Java Development Kit](http://www.oracle.com/technetwork/es/java/javase/downloads/index.html) which is used
|
||||
to execute the selenium standalone server for e2e testing.
|
||||
|
||||
* (Optional for now) [Bazel](https://bazel.build/), please follow instructions in [Bazel.md]
|
||||
|
||||
## Getting the Sources
|
||||
|
||||
Fork and clone the Angular repository:
|
||||
@ -61,19 +61,11 @@ Next, install the JavaScript modules needed to build and test Angular:
|
||||
yarn install
|
||||
```
|
||||
|
||||
**Optional**: In this document, we make use of project local `npm` package scripts and binaries
|
||||
(stored under `./node_modules/.bin`) by prefixing these command invocations with `$(npm bin)`; in
|
||||
particular `gulp` and `protractor` commands. If you prefer, you can drop this path prefix by either:
|
||||
**Optional**: In this document, we make use of installed npm package scripts and binaries
|
||||
(stored under `./node_modules/.bin`) by prefixing these command invocations with `$(yarn bin)`; in
|
||||
particular `gulp` and `protractor` commands.
|
||||
|
||||
|
||||
## Installing Bower Modules
|
||||
|
||||
Now run `bower` to install additional dependencies:
|
||||
|
||||
```shell
|
||||
# Install other Angular project dependencies (bower.json)
|
||||
bower install
|
||||
```
|
||||
|
||||
## Windows only
|
||||
|
||||
|
@ -2,9 +2,25 @@ package(default_visibility = ["//visibility:public"])
|
||||
|
||||
filegroup(
|
||||
name = "node_modules",
|
||||
srcs = glob([
|
||||
"node_modules/**/*.js",
|
||||
"node_modules/**/*.d.ts",
|
||||
"node_modules/**/*.json",
|
||||
])
|
||||
# Workaround https://github.com/bazelbuild/bazel/issues/4242
|
||||
# Can't just glob(["node_modules/**/*.{js,d.ts,json}"])
|
||||
srcs = glob(["/".join([
|
||||
"node_modules",
|
||||
pkg,
|
||||
"**",
|
||||
ext,
|
||||
]) for pkg in [
|
||||
"@angular",
|
||||
"@types",
|
||||
"bytebuffer",
|
||||
"protobufjs",
|
||||
"reflect-metadata",
|
||||
"tsickle",
|
||||
"typescript",
|
||||
"zone.js",
|
||||
] for ext in [
|
||||
"*.js",
|
||||
"*.json",
|
||||
"*.d.ts",
|
||||
]]),
|
||||
)
|
||||
|
@ -1,24 +1,36 @@
|
||||
workspace(name = "bazel_integration_test")
|
||||
|
||||
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
|
||||
|
||||
git_repository(
|
||||
name = "build_bazel_rules_nodejs",
|
||||
remote = "https://github.com/bazelbuild/rules_nodejs.git",
|
||||
tag = "0.2.1",
|
||||
tag = "0.3.1",
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_nodejs//:defs.bzl", "node_repositories")
|
||||
node_repositories(package_json = ["//:package.json"])
|
||||
|
||||
local_repository(
|
||||
git_repository(
|
||||
name = "build_bazel_rules_typescript",
|
||||
path = "node_modules/@bazel/typescript",
|
||||
remote = "https://github.com/bazelbuild/rules_typescript.git",
|
||||
tag = "0.6.0",
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_typescript//:defs.bzl", "ts_repositories")
|
||||
|
||||
ts_repositories()
|
||||
|
||||
local_repository(
|
||||
name = "angular",
|
||||
path = "node_modules/@angular/bazel",
|
||||
)
|
||||
|
||||
local_repository(
|
||||
name = "rxjs",
|
||||
path = "node_modules/rxjs/src",
|
||||
)
|
||||
|
||||
git_repository(
|
||||
name = "io_bazel_rules_sass",
|
||||
remote = "https://github.com/bazelbuild/rules_sass.git",
|
||||
|
@ -20,6 +20,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "ngc -p angular.tsconfig.json",
|
||||
"test": "bazel build ... --noshow_progress"
|
||||
"test": "WORKAROUND https://github.com/bazelbuild/bazel/issues/4242, can't build ...",
|
||||
"test": "bazel build //src/... --noshow_progress"
|
||||
}
|
||||
}
|
@ -6,6 +6,6 @@ exports_files(["tsconfig.json"])
|
||||
ng_module(
|
||||
name = "src",
|
||||
srcs = glob(["*.ts"]),
|
||||
deps = ["//src/hello-world"],
|
||||
tsconfig = ":tsconfig.json",
|
||||
deps = ["//src/hello-world"],
|
||||
)
|
||||
|
@ -1,4 +1,5 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load("@angular//:index.bzl", "ng_module")
|
||||
load("@io_bazel_rules_sass//sass:sass.bzl", "sass_binary")
|
||||
|
||||
@ -10,6 +11,9 @@ sass_binary(
|
||||
ng_module(
|
||||
name = "hello-world",
|
||||
srcs = glob(["*.ts"]),
|
||||
tsconfig = "//src:tsconfig.json",
|
||||
assets = [":hello-world-styles.css"],
|
||||
tsconfig = "//src:tsconfig.json",
|
||||
# FIXME(alexeagle): the rxjs dep should come from Angular, but if we use the
|
||||
# npm distro of angular there is no ts_library rule to propagate the dep.
|
||||
deps = ["@rxjs"],
|
||||
)
|
||||
|
@ -10,7 +10,7 @@
|
||||
"ngc": "ngc -p tsconfig.json",
|
||||
"rollup": "rollup -f iife -c rollup.config.js -o dist/bundle.es2015.js",
|
||||
"rollup:lazy": "rollup -f cjs -c rollup.lazy.config.js -o dist/lazy.bundle.es2015.js",
|
||||
"postinstall": "webdriver-manager update --gecko false",
|
||||
"postinstall": "webdriver-manager update --gecko false --standalone false $CHROMEDRIVER_VERSION_ARG",
|
||||
"preprotractor": "tsc -p e2e",
|
||||
"protractor": "protractor e2e/protractor.config.js",
|
||||
"serve": "lite-server -c e2e/browser.config.json",
|
||||
|
@ -22,7 +22,7 @@
|
||||
"protractor": "file:../../node_modules/protractor"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "webdriver-manager update --gecko false",
|
||||
"postinstall": "webdriver-manager update --gecko false --standalone false $CHROMEDRIVER_VERSION_ARG",
|
||||
"closure": "java -jar node_modules/google-closure-compiler/compiler.jar --flagfile closure.conf",
|
||||
"test": "ngc && yarn run closure && concurrently \"yarn run serve\" \"yarn run protractor\" --kill-others --success first",
|
||||
"serve": "lite-server -c e2e/browser.config.json",
|
||||
|
@ -4,7 +4,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"postinstall": "webdriver-manager update --gecko false",
|
||||
"postinstall": "webdriver-manager update --gecko false --standalone false $CHROMEDRIVER_VERSION_ARG",
|
||||
"test": "concurrently \"npm run serve\" \"npm run protractor\" --kill-others --success first",
|
||||
"serve": "lite-server -c bs-config.e2e.json",
|
||||
"preprotractor": "tsc -p e2e",
|
||||
|
@ -22,7 +22,7 @@
|
||||
"protractor": "file:../../node_modules/protractor"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "webdriver-manager update --gecko false",
|
||||
"postinstall": "webdriver-manager update --gecko false --standalone false $CHROMEDRIVER_VERSION_ARG",
|
||||
"closure": "java -jar node_modules/google-closure-compiler/compiler.jar --flagfile closure.conf",
|
||||
"test": "ngc && yarn run closure && concurrently \"yarn run serve\" \"yarn run protractor\" --kill-others --success first",
|
||||
"serve": "lite-server -c e2e/browser.config.json",
|
||||
|
13
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "5.1.0",
|
||||
"version": "5.1.2",
|
||||
"private": true,
|
||||
"branchPattern": "2.0.*",
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
@ -16,14 +16,16 @@
|
||||
"url": "https://github.com/angular/angular.git"
|
||||
},
|
||||
"scripts": {
|
||||
"buildifier": "bazel build @com_github_bazelbuild_buildtools//buildifier && find . -type f \\( -name BUILD -or -name BUILD.bazel \\) ! -path \"*/node_modules/*\" | xargs $(bazel info bazel-bin)/external/com_github_bazelbuild_buildtools/buildifier/buildifier",
|
||||
"preinstall": "node -e \"if(process.env.npm_execpath.indexOf('yarn') === -1) throw new Error('Please use Yarn instead of NPM to install dependencies. See: https://yarnpkg.com/lang/en/docs/install/')\"",
|
||||
"postinstall": "webdriver-manager update --gecko false",
|
||||
"postinstall": "yarn update-webdriver",
|
||||
"update-webdriver": "webdriver-manager update --gecko false $CHROMEDRIVER_VERSION_ARG",
|
||||
"check-env": "gulp check-env"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^2.4.1",
|
||||
"reflect-metadata": "^0.1.3",
|
||||
"rxjs": "^5.5.2",
|
||||
"rxjs": "5.5.5",
|
||||
"tslib": "^1.7.1",
|
||||
"zone.js": "^0.8.12"
|
||||
},
|
||||
@ -31,7 +33,7 @@
|
||||
"fsevents": "1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@bazel/typescript": "0.3.2",
|
||||
"@bazel/ibazel": "^0.1.1",
|
||||
"@types/angularjs": "1.5.14-alpha",
|
||||
"@types/base64-js": "1.2.5",
|
||||
"@types/chokidar": "1.7.3",
|
||||
@ -51,7 +53,7 @@
|
||||
"canonical-path": "0.0.2",
|
||||
"chokidar": "1.7.0",
|
||||
"clang-format": "1.0.41",
|
||||
"cldr": "4.5.0",
|
||||
"cldr": "4.7.0",
|
||||
"cldr-data-downloader": "0.3.2",
|
||||
"cldrjs": "0.5.0",
|
||||
"conventional-changelog": "1.1.0",
|
||||
@ -97,6 +99,7 @@
|
||||
"tsickle": "0.25.5",
|
||||
"tslint": "5.7.0",
|
||||
"tslint-eslint-rules": "4.1.1",
|
||||
"tsutils": "2.12.1",
|
||||
"typescript": "2.5.x",
|
||||
"uglify-js": "2.8.29",
|
||||
"universal-analytics": "0.4.15",
|
||||
|
@ -1 +1,20 @@
|
||||
exports_files(["tsconfig-build.json"])
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
exports_files([
|
||||
"tsconfig-build.json",
|
||||
"tsconfig.json",
|
||||
])
|
||||
|
||||
load("@build_bazel_rules_typescript//:defs.bzl", "ts_config", "ts_library")
|
||||
|
||||
ts_config(
|
||||
name = "tsconfig",
|
||||
src = ":tsconfig-build.json",
|
||||
deps = [],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "types",
|
||||
srcs = glob(["*.ts"]),
|
||||
tsconfig = ":tsconfig",
|
||||
)
|
||||
|
18
packages/animations/BUILD.bazel
Normal file
@ -0,0 +1,18 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load("@build_bazel_rules_typescript//:defs.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "animations",
|
||||
srcs = glob(
|
||||
[
|
||||
"*.ts",
|
||||
"src/**/*.ts",
|
||||
],
|
||||
),
|
||||
module_name = "@angular/animations",
|
||||
tsconfig = "//packages:tsconfig",
|
||||
deps = [
|
||||
"//packages/core",
|
||||
],
|
||||
)
|
18
packages/animations/browser/BUILD.bazel
Normal file
@ -0,0 +1,18 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load("@build_bazel_rules_typescript//:defs.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "browser",
|
||||
srcs = glob(
|
||||
[
|
||||
"*.ts",
|
||||
"src/**/*.ts",
|
||||
],
|
||||
),
|
||||
module_name = "@angular/animations/browser",
|
||||
tsconfig = "//packages:tsconfig",
|
||||
deps = [
|
||||
"//packages/animations",
|
||||
],
|
||||
)
|
@ -65,13 +65,12 @@ function parseAnimationAlias(alias: string, errors: string[]): string|Transition
|
||||
}
|
||||
}
|
||||
|
||||
const TRUE_BOOLEAN_VALUES = new Set<string>();
|
||||
TRUE_BOOLEAN_VALUES.add('true');
|
||||
TRUE_BOOLEAN_VALUES.add('1');
|
||||
|
||||
const FALSE_BOOLEAN_VALUES = new Set<string>();
|
||||
FALSE_BOOLEAN_VALUES.add('false');
|
||||
FALSE_BOOLEAN_VALUES.add('0');
|
||||
// DO NOT REFACTOR ... keep the follow set instantiations
|
||||
// with the values intact (closure compiler for some reason
|
||||
// removes follow-up lines that add the values outside of
|
||||
// the constructor...
|
||||
const TRUE_BOOLEAN_VALUES = new Set<string>(['true', '1']);
|
||||
const FALSE_BOOLEAN_VALUES = new Set<string>(['false', '0']);
|
||||
|
||||
function makeLambdaFromStates(lhs: string, rhs: string): TransitionMatcherFn {
|
||||
const LHS_MATCH_BOOLEAN = TRUE_BOOLEAN_VALUES.has(lhs) || FALSE_BOOLEAN_VALUES.has(lhs);
|
||||
|
@ -166,12 +166,30 @@ if (typeof Element != 'undefined') {
|
||||
};
|
||||
}
|
||||
|
||||
function containsVendorPrefix(prop: string): boolean {
|
||||
// Webkit is the only real popular vendor prefix nowadays
|
||||
// cc: http://shouldiprefix.com/
|
||||
return prop.substring(1, 6) == 'ebkit'; // webkit or Webkit
|
||||
}
|
||||
|
||||
let _CACHED_BODY: {style: any}|null = null;
|
||||
let _IS_WEBKIT = false;
|
||||
export function validateStyleProperty(prop: string): boolean {
|
||||
if (!_CACHED_BODY) {
|
||||
_CACHED_BODY = getBodyNode() || {};
|
||||
_IS_WEBKIT = _CACHED_BODY !.style ? ('WebkitAppearance' in _CACHED_BODY !.style) : false;
|
||||
}
|
||||
return _CACHED_BODY !.style ? prop in _CACHED_BODY !.style : true;
|
||||
|
||||
let result = true;
|
||||
if (_CACHED_BODY !.style && !containsVendorPrefix(prop)) {
|
||||
result = prop in _CACHED_BODY !.style;
|
||||
if (!result && _IS_WEBKIT) {
|
||||
const camelProp = 'Webkit' + prop.charAt(0).toUpperCase() + prop.substr(1);
|
||||
result = camelProp in _CACHED_BODY !.style;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getBodyNode(): any|null {
|
||||
|
@ -312,9 +312,13 @@ export class AnimationTransitionNamespace {
|
||||
// If there are no animations found for any of the nodes then clear the cache
|
||||
// for the element.
|
||||
this._engine.driver.query(rootElement, NG_TRIGGER_SELECTOR, true).forEach(elm => {
|
||||
// this means that an inner remove() operation has already kicked off
|
||||
// the animation on this element...
|
||||
if (elm[REMOVAL_FLAG]) return;
|
||||
|
||||
const namespaces = this._engine.fetchNamespacesByElement(elm);
|
||||
if (namespaces.size) {
|
||||
namespaces.forEach(ns => { ns.triggerLeaveAnimation(elm, context, false, true); });
|
||||
namespaces.forEach(ns => ns.triggerLeaveAnimation(elm, context, false, true));
|
||||
} else {
|
||||
this.clearElementCache(elm);
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
||||
|
||||
const keyframes = this.keyframes.map(styles => copyStyles(styles, false));
|
||||
const previousStyleProps = Object.keys(this.previousStyles);
|
||||
if (previousStyleProps.length) {
|
||||
if (previousStyleProps.length && keyframes.length) {
|
||||
let startingKeyframe = keyframes[0];
|
||||
let missingStyleProps: string[] = [];
|
||||
previousStyleProps.forEach(prop => {
|
||||
|