Compare commits

..

53 Commits

Author SHA1 Message Date
1c04b83ea3 docs: add changelog for 4.2.0 2017-06-08 15:13:37 -07:00
429bc9d3cd release: cut the 4.2.0 release 2017-06-08 15:04:22 -07:00
41a765d715 fix(aio): re-color code pretty print
closes #17246
2017-06-08 14:00:16 -07:00
f370fd36e0 docs: "TypeScript to JavaScript" code tab renamed “ES6 + Decorators”
closes #17260
2017-06-08 14:00:06 -07:00
a222c3e609 fix(forms): fix min and max validator behavior on non-numbers 2017-06-08 13:59:17 -07:00
5a71df0cb3 build(aio): rename features header 2017-06-08 12:57:42 -07:00
6a46cabb10 docs: security guide images
closes #17263
2017-06-08 12:57:13 -07:00
74673545c0 docs(aio): document the non-null assertion operator 2017-06-08 12:55:19 -07:00
75a311f250 fix(aio): style and fix hover color for features cta button (#17342) 2017-06-08 12:09:33 -07:00
391ed6334d fix(aio): copy code button overlap fix on mobile (#17302) 2017-06-08 12:08:03 -07:00
0940e6d6ed fix(aio): contributor img offset and hover fix (#17338) 2017-06-08 12:00:43 -07:00
4d2ee51bb0 fix(aio): remove glossary l-sub-section class (#17305) 2017-06-08 11:58:49 -07:00
680128bc09 fix(aio): alert, subsection and callout width fix (#17303) 2017-06-08 11:57:09 -07:00
5364b51979 fix(aio): scroll to top immediately when doc changes (#17310) 2017-06-08 11:56:36 -07:00
67ef0f0c8f fix(aio): api list column overflow fix (#17300) 2017-06-08 11:54:57 -07:00
e9886d701d fix(animations): evaluate substitutions on option param values 2017-06-08 10:12:52 -07:00
022835bab2 ci: update Chromium to 464641 (Chrome v59) 2017-06-08 09:36:35 -07:00
8be2e4c325 fix(aio): make the ToC left hand-side bar shorter (#17280)
Make the ToC left hand-side bar shorter, so that it starts at the initial bullet
and ends at the final bullet.

Fixes #17221
2017-06-08 00:56:00 -07:00
33ba3e31ed fix(aio): smoother transition from page with SideNav to homepage (#17312)
When navigating from a page with open SideNav to a page without closed SideNav,
the main content area animates from a non-zero left margin to zero left margin.
Additionally, the top-bar on the homepage is transparent, which allows the white
background behind the main content to be seen while the left margin is animated
to zero, making it appear as if something (e.g. the SideNav covers the top-bar).

This commit works around this issue, by not making the top-bar transparent
immediately when navigating to the homepage, but animating it from its blue
color to transparent with a delay.

Fixes #17248
2017-06-08 00:49:54 -07:00
cba2b3c72d docs(aio): add “nougat” to compat chart
closes #17262
2017-06-07 12:40:50 -07:00
a4a2901294 fix(aio): remove ... separator from search results
An ellipsis was used to separate the most relevant search
results from the alphabetic list. The separator was confusing
because it was not clear what it represented.

This has been removed and the most relevant results are now
indicated by styling with a more bold font and a bit of whitespace
between them and the rest of the results.

To keep things consistent, if there are fewer than 5 results all the
results are now displayed as priorityPages.

Closes #17233
2017-06-07 12:40:39 -07:00
bb46f54ad7 refactor(aio): use the SelectMenuComponent for all select menus
The API filters and the docs version switcher now use
the SelectMenuComponent.

Fixes #16367 and #17055
2017-06-07 11:27:10 -07:00
c9b930dd82 feat(aio): add aio-select component
Provide the functionality for select menus in a single reusable component.
2017-06-07 11:27:10 -07:00
a39f7d63bb docs(aio): use h1 rather than divs and hide heading anchors
In the marketing pages we do not want to show heading anchors on hover.
Previously, this was achieved by using div rather than heading elements.
Now we can use semantically accurate headings while hiding the anchor.

Closes #17244
Closes #17264
2017-06-07 11:26:27 -07:00
e317f7d51c feat(aio): do not display anchor if heading has no-anchor class 2017-06-07 11:26:27 -07:00
3ddd28d37d fix(aio): update contrast on mediumgray color 2017-06-07 11:26:13 -07:00
fe6b39d585 perf(animations): do not create a closure each time a node is removed 2017-06-07 11:16:17 -07:00
d837bfc2d7 fix(aio): fix scrolling to an element
Previously, the top-bar's height wasn't taken into account when scrolling an
element into view. As a result, the element would be hidden behind the top-bar.
Taking the top-bar height into account was not necessary before #17155, because
the top-bar was not fixed (i.e. it scrolled away).

This commit fixes the scrolling behavior by accounting for the top-bar's height
when scrolling an element into view.

(This partially reverts #17102.)

Fixes #17219
Fixes #17226
2017-06-07 14:37:46 +01:00
4759975be6 fix(aio): adjust SideNav top to avoid extra whitespace
Partially addresses #17096.
2017-06-07 13:37:30 +01:00
078a4b00a7 fix(aio): fix button alignment and jump
- Fix alignment on get started button on home page
- Remove jump by removing arrow from learn more button on home page
2017-06-07 09:11:20 +01:00
b00b80a45b feat(compiler-cli): introduce synchronous codegen API 2017-06-06 14:12:02 -07:00
269bbe0e7d fix(upgrade): call setInterval outside the Angular zone
This wraps the $interval service when using upgrade to run the
$interval() call outside the Angular zone. However, the callback is
invoked within the Angular zone, so changes still propagate to
downgraded components.
2017-06-06 14:11:21 -07:00
bb2fc6b8da refactor(platform-browser): Remove setGlobalVar from DOM adapter 2017-06-06 13:26:23 -07:00
d4e196035c build(aio): update features and presskit marketing pages 2017-06-06 13:08:48 -07:00
b8979c8701 fix(aio): fix and clean up topbar styling
Restore the changes introduced in #17075, which wre accidentally overwritten
while rebasing #17155. Also, simplify the topbar positioning rules.
2017-06-06 13:08:39 -07:00
bfdd3398f6 fix(aio): make ScrollSpy respond quicker to scroll events
Fixes ##17220
2017-06-06 13:08:30 -07:00
3a121a621f build(aio): migrate contribute page to html 2017-06-06 16:38:29 +01:00
c06f4fc702 docs(aio): remove tutorial from docs landing page 2017-06-06 16:38:09 +01:00
95f1ea2f12 fix(aio): topnav toolbar position styles
- Fixed topnav on all mobile
- Fixed topnav on all docs pages
- Absolute topnav on all marketing pages
- Cleanup and code consolidation for all top-menu styles
- Add styling to topnav links on focus
2017-06-06 14:51:19 +01:00
784347f61f fix(aio): ensure all views can indicate the active node (#17194)
When more than one node matches a url, the last
node defined in the navigation.json file won. This
meant that, for instance, items in both the
TopBarNarrow and the Footer views would not
indicate that they were active.

Now, each url is associated with a map of current
nodes keyed off their view.

Closes #17022
2017-06-05 23:36:22 -07:00
42176a7ac4 build(aio): upgrade ngo and purify (#17210) 2017-06-05 23:03:26 -07:00
76b4b80a23 fix(aio): fix search box overlap for small devices (#17075)
- Adjust search box height
- Adjust search box standard width and width for smaller devices
- Fix search jump outside of specified max width
2017-06-05 22:57:17 -07:00
7c78282ce8 docs: fix typo in 'Routes' docs 2017-06-05 11:18:43 -07:00
47c2a2e411 ci: switch CircleCI to 2.0
This lets us use a docker container to provide things like the Bazel build tool
2017-06-05 11:18:20 -07:00
5faf520067 build: Introduce Bazel build rules
So far this just compiles the core and common packages.
2017-06-05 11:18:20 -07:00
02d74cafba ci: update pullapprove to reflect animation ownership 2017-06-05 11:17:24 -07:00
734f30d14c build(aio): ignore .ngsummary.ts files in examples 2017-06-05 11:12:59 -07:00
f2d810febc fix(aio): fix ToC styling
- Make the left bar gray (instead of blue).
- Show gray dot when hovering over an element.
- Hide left bar and dots in embedded mode.
2017-06-04 15:12:29 +01:00
819514aeba fix(animations): ensure web-animations understands a numeric CSS perspective value
Closes #14007
2017-06-02 17:05:15 -07:00
b55adee982 perf(animations): only apply :leave flags if animations are set to run 2017-06-02 17:05:06 -07:00
4c32cb952f fix(router): opening links in new window
Shift-clicks on router-links should not prevent browser default action.

A follow on to:
1ac9dda93d
2017-06-02 17:32:12 -04:00
35f714e438 fix(aio): place progress bar at the top
Previously, the progress bar would be placed right under the static top bar. Now
that the top bar i not tatic any more, it makes more sense to place the progress
bar at the top of the page.

Fixes #17103
2017-06-02 17:31:53 -04:00
d5ce086089 build(aio): fix up API doc-gen templates
* Remove whitespace before type specifiers
* Generate `new` and `call` member info for interfaces
* Ensure that there is no double space after class names
2017-06-02 13:18:09 -04:00
114 changed files with 1639 additions and 1239 deletions

6
.bazelrc Normal file
View File

@ -0,0 +1,6 @@
# Disable sandboxing because it's too slow.
# https://github.com/bazelbuild/bazel/issues/2424
build --spawn_strategy=standalone
# Performance: avoid stat'ing input files
build --watchfs

20
.circleci/config.yml Normal file
View File

@ -0,0 +1,20 @@
version: 2
jobs:
build:
working_directory: ~/ng
docker:
- image: alexeagle/ngcontainer
steps:
- checkout
- restore_cache:
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
- run: npm install
- run: npm run postinstall
- run: ./node_modules/.bin/gulp lint
# Build twice, workaround for
# https://github.com/bazelbuild/bazel/issues/3114
- run: bazel build ... || bazel build ...
- save_cache:
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
paths:
- "node_modules"

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
.DS_STORE .DS_STORE
/dist/ /dist/
bazel-*
node_modules node_modules
bower_components bower_components

View File

@ -97,15 +97,15 @@ groups:
- vicb - vicb
- IgorMinar #fallback - IgorMinar #fallback
compiler/animations: animations:
conditions: conditions:
files: files:
- "packages/compiler/src/animation/*" - "packages/animation/*"
- "packages/platform-browser/animations/*"
users: users:
- matsko #primary - matsko #primary
- tbosch
- IgorMinar #fallback
- mhevery #fallback - mhevery #fallback
- IgorMinar #fallback
compiler/i18n: compiler/i18n:
conditions: conditions:

18
BUILD Normal file
View File

@ -0,0 +1,18 @@
package(default_visibility = ["//visibility:public"])
exports_files(["tsconfig.json"])
# This rule belongs in node_modules/BUILD
# It's here as a workaround for
# https://github.com/bazelbuild/bazel/issues/374#issuecomment-296217940
filegroup(
name = "node_modules",
srcs = glob([
# Performance workaround: list individual files
# This won't scale in the general case.
# TODO(alexeagle): figure out what to do
"node_modules/typescript/lib/**",
"node_modules/zone.js/**/*.d.ts",
"node_modules/rxjs/**/*.d.ts",
"node_modules/@types/**/*.d.ts",
]),
)

View File

@ -1,3 +1,28 @@
<a name="4.2.0"></a>
# [4.2.0](https://github.com/angular/angular/compare/4.2.0-rc.2...4.2.0) salubrious-stratagem (2017-06-08)
### Bug Fixes
* **animations:** ensure web-animations understands a numeric CSS perspective value ([819514a](https://github.com/angular/angular/commit/819514a)), closes [#14007](https://github.com/angular/angular/issues/14007)
* **animations:** evaluate substitutions on option param values ([e9886d7](https://github.com/angular/angular/commit/e9886d7))
* **forms:** fix min and max validator behavior on non-numbers ([a222c3e](https://github.com/angular/angular/commit/a222c3e))
* **router:** opening links in new window ([4c32cb9](https://github.com/angular/angular/commit/4c32cb9))
* **upgrade:** call setInterval outside the Angular zone ([269bbe0](https://github.com/angular/angular/commit/269bbe0))
### Features
* **compiler-cli:** introduce synchronous codegen API ([b00b80a](https://github.com/angular/angular/commit/b00b80a))
### Performance Improvements
* **animations:** do not create a closure each time a node is removed ([fe6b39d](https://github.com/angular/angular/commit/fe6b39d))
* **animations:** only apply `:leave` flags if animations are set to run ([b55adee](https://github.com/angular/angular/commit/b55adee))
<a name="4.2.0-rc.2"></a> <a name="4.2.0-rc.2"></a>
# [4.2.0-rc.2](https://github.com/angular/angular/compare/4.2.0-rc.1...4.2.0-rc.2) (2017-06-01) # [4.2.0-rc.2](https://github.com/angular/angular/compare/4.2.0-rc.1...4.2.0-rc.2) (2017-06-01)

12
WORKSPACE Normal file
View File

@ -0,0 +1,12 @@
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
git_repository(
name = "io_bazel_rules_typescript",
remote = "https://github.com/bazelbuild/rules_typescript.git",
tag = "0.0.3",
)
load("@io_bazel_rules_typescript//:defs.bzl", "node_repositories", "yarn_install")
node_repositories()
yarn_install(package_json = "//:package.json")

View File

@ -45,6 +45,7 @@ dist/
# aot # aot
**/*.ngfactory.ts **/*.ngfactory.ts
**/*.ngsummary.json **/*.ngsummary.json
**/*.ngsummary.ts
**/*.shim.ngstyle.ts **/*.shim.ngstyle.ts
**/*.metadata.json **/*.metadata.json
!aot/bs-config.json !aot/bs-config.json

View File

@ -803,6 +803,15 @@ The null hero's name is {{nullHero && nullHero.name}}
<!-- #enddocregion safe-6 --> <!-- #enddocregion safe-6 -->
</div> </div>
<div>
<!-- #docregion non-null-assertion-1 -->
<!--No hero, no text -->
<div *ngIf="hero">
The hero's name is {{hero!.name}}
</div>
<!-- #enddocregion non-null-assertion-1 -->
</div>
<a class="to-toc" href="#toc">top</a> <a class="to-toc" href="#toc">top</a>
<!-- TODO: discuss this in the Style binding section --> <!-- TODO: discuss this in the Style binding section -->

View File

@ -68,7 +68,7 @@ Angular supports most recent browsers. This includes the following specific vers
</td> </td>
<td> <td>
Marshmallow (6.0) Nougat (7.0)<br>Marshmallow (6.0)
</td> </td>
<td> <td>

View File

@ -19,26 +19,14 @@ unexpected definitions.
## Ahead-of-time (AOT) compilation ## Ahead-of-time (AOT) compilation
<div class="l-sub-section">
You can compile Angular applications at build time. You can compile Angular applications at build time.
By compiling your application using the compiler-cli, `ngc`, you can bootstrap directly By compiling your application using the compiler-cli, `ngc`, you can bootstrap directly
to a module factory, meaning you don't need to include the Angular compiler in your JavaScript bundle. to a module factory, meaning you don't need to include the Angular compiler in your JavaScript bundle.
Ahead-of-time compiled applications also benefit from decreased load time and increased performance. Ahead-of-time compiled applications also benefit from decreased load time and increased performance.
</div>
## Angular module ## Angular module
<div class="l-sub-section">
Helps you organize an application into cohesive blocks of functionality. Helps you organize an application into cohesive blocks of functionality.
An Angular module identifies the components, directives, and pipes that the application uses along with the list of external Angular modules that the application needs, such as `FormsModule`. An Angular module identifies the components, directives, and pipes that the application uses along with the list of external Angular modules that the application needs, such as `FormsModule`.
@ -48,23 +36,11 @@ called `AppModule` and resides in a file named `app.module.ts`.
For details and examples, see the [Angular Modules (NgModule)](guide/ngmodule) page. For details and examples, see the [Angular Modules (NgModule)](guide/ngmodule) page.
</div>
## Annotation ## Annotation
<div class="l-sub-section">
In practice, a synonym for [Decoration](guide/glossary#decorator). In practice, a synonym for [Decoration](guide/glossary#decorator).
</div>
{@a attribute-directive} {@a attribute-directive}
@ -73,10 +49,6 @@ In practice, a synonym for [Decoration](guide/glossary#decorator).
## Attribute directives ## Attribute directives
<div class="l-sub-section">
A category of [directive](guide/glossary#directive) that can listen to and modify the behavior of A category of [directive](guide/glossary#directive) that can listen to and modify the behavior of
other HTML elements, attributes, properties, and components. They are usually represented other HTML elements, attributes, properties, and components. They are usually represented
as HTML attributes, hence the name. as HTML attributes, hence the name.
@ -86,18 +58,10 @@ For example, you can use the `ngClass` directive to add and remove CSS class nam
Learn about them in the [_Attribute Directives_](guide/attribute-directives) guide. Learn about them in the [_Attribute Directives_](guide/attribute-directives) guide.
</div>
{@a B} {@a B}
## Barrel ## Barrel
<div class="l-sub-section">
A way to *roll up exports* from several ES2015 modules into a single convenient ES2015 module. A way to *roll up exports* from several ES2015 modules into a single convenient ES2015 module.
The barrel itself is an ES2015 module file that re-exports *selected* exports of other ES2015 modules. The barrel itself is an ES2015 module file that re-exports *selected* exports of other ES2015 modules.
@ -157,17 +121,8 @@ You can often achieve the same result using [Angular modules](guide/glossary#ang
</div> </div>
</div>
## Binding ## Binding
<div class="l-sub-section">
Usually refers to [data binding](guide/glossary#data-binding) and the act of Usually refers to [data binding](guide/glossary#data-binding) and the act of
binding an HTML object property to a data object property. binding an HTML object property to a data object property.
@ -175,16 +130,8 @@ Sometimes refers to a [dependency-injection](guide/glossary#dependency-injection
between a "token"&mdash;also referred to as a "key"&mdash;and a dependency [provider](guide/glossary#provider). between a "token"&mdash;also referred to as a "key"&mdash;and a dependency [provider](guide/glossary#provider).
</div>
## Bootstrap ## Bootstrap
<div class="l-sub-section">
You launch an Angular application by "bootstrapping" it using the application root Angular module (`AppModule`). You launch an Angular application by "bootstrapping" it using the application root Angular module (`AppModule`).
Bootstrapping identifies an application's top level "root" [component](guide/glossary#component), Bootstrapping identifies an application's top level "root" [component](guide/glossary#component),
which is the first component that is loaded for the application. which is the first component that is loaded for the application.
@ -193,17 +140,10 @@ For more information, see the [Setup](guide/setup) page.
You can bootstrap multiple apps in the same `index.html`, each app with its own top-level root. You can bootstrap multiple apps in the same `index.html`, each app with its own top-level root.
</div>
{@a C} {@a C}
## camelCase ## camelCase
<div class="l-sub-section">
The practice of writing compound words or phrases such that each word or abbreviation begins with a capital letter The practice of writing compound words or phrases such that each word or abbreviation begins with a capital letter
_except the first letter, which is lowercase_. _except the first letter, which is lowercase_.
@ -213,19 +153,11 @@ camelCase is also known as *lower camel case* to distinguish it from *upper came
In Angular documentation, "camelCase" always means *lower camel case*. In Angular documentation, "camelCase" always means *lower camel case*.
</div>
{@a component} {@a component}
## Component ## Component
<div class="l-sub-section">
An Angular class responsible for exposing data to a [view](guide/glossary#view) and handling most of the views display and user-interaction logic. An Angular class responsible for exposing data to a [view](guide/glossary#view) and handling most of the views display and user-interaction logic.
The *component* is one of the most important building blocks in the Angular system. The *component* is one of the most important building blocks in the Angular system.
@ -240,17 +172,10 @@ Those familiar with "MVC" and "MVVM" patterns will recognize
the component in the role of "controller" or "view model". the component in the role of "controller" or "view model".
</div>
{@a D} {@a D}
## dash-case ## dash-case
<div class="l-sub-section">
The practice of writing compound words or phrases such that each word is separated by a dash or hyphen (`-`). The practice of writing compound words or phrases such that each word is separated by a dash or hyphen (`-`).
This form is also known as kebab-case. This form is also known as kebab-case.
@ -259,16 +184,8 @@ the root of filenames (such as `hero-list.component.ts`) are often
spelled in dash-case. spelled in dash-case.
</div>
## Data binding ## Data binding
<div class="l-sub-section">
Applications display data values to a user and respond to user Applications display data values to a user and respond to user
actions (such as clicks, touches, and keystrokes). actions (such as clicks, touches, and keystrokes).
@ -292,10 +209,6 @@ operations and supporting declaration syntax.
* [Two-way data binding with ngModel](guide/template-syntax#ngModel). * [Two-way data binding with ngModel](guide/template-syntax#ngModel).
</div>
{@a decorator} {@a decorator}
@ -304,10 +217,6 @@ operations and supporting declaration syntax.
## Decorator | decoration ## Decorator | decoration
<div class="l-sub-section">
A *function* that adds metadata to a class, its members (properties, methods) and function arguments. A *function* that adds metadata to a class, its members (properties, methods) and function arguments.
Decorators are an experimental (stage 2), JavaScript language [feature](https://github.com/wycats/javascript-decorators). TypeScript adds support for decorators. Decorators are an experimental (stage 2), JavaScript language [feature](https://github.com/wycats/javascript-decorators). TypeScript adds support for decorators.
@ -340,17 +249,8 @@ Always include parentheses `()` when applying a decorator.
</div> </div>
</div>
## Dependency injection ## Dependency injection
<div class="l-sub-section">
A design pattern and mechanism A design pattern and mechanism
for creating and delivering parts of an application to other for creating and delivering parts of an application to other
parts of an application that request them. parts of an application that request them.
@ -401,10 +301,6 @@ You can register your own providers.
Read more in the [Dependency Injection](guide/dependency-injection) page. Read more in the [Dependency Injection](guide/dependency-injection) page.
</div>
{@a directive} {@a directive}
@ -413,10 +309,6 @@ Read more in the [Dependency Injection](guide/dependency-injection) page.
## Directive ## Directive
<div class="l-sub-section">
An Angular class responsible for creating, reshaping, and interacting with HTML elements An Angular class responsible for creating, reshaping, and interacting with HTML elements
in the browser DOM. The directive is Angular's most fundamental feature. in the browser DOM. The directive is Angular's most fundamental feature.
@ -447,17 +339,10 @@ shaping or reshaping HTML layout, typically by adding, removing, or manipulating
elements and their children. elements and their children.
</div>
{@a E} {@a E}
## ECMAScript ## ECMAScript
<div class="l-sub-section">
The [official JavaScript language specification](https://en.wikipedia.org/wiki/ECMAScript). The [official JavaScript language specification](https://en.wikipedia.org/wiki/ECMAScript).
The latest approved version of JavaScript is The latest approved version of JavaScript is
@ -473,47 +358,21 @@ to ES5 JavaScript.
Angular developers can write in ES5 directly. Angular developers can write in ES5 directly.
</div>
## ES2015 ## ES2015
<div class="l-sub-section">
Short hand for [ECMAScript](guide/glossary#ecmascript) 2015. Short hand for [ECMAScript](guide/glossary#ecmascript) 2015.
</div>
## ES5 ## ES5
<div class="l-sub-section">
Short hand for [ECMAScript](guide/glossary#ecmascript) 5, the version of JavaScript run by most modern browsers. Short hand for [ECMAScript](guide/glossary#ecmascript) 5, the version of JavaScript run by most modern browsers.
</div>
## ES6 ## ES6
<div class="l-sub-section">
Short hand for [ECMAScript](guide/glossary#ecmascript) 2015. Short hand for [ECMAScript](guide/glossary#ecmascript) 2015.
</div>
{@a F} {@a F}
@ -526,25 +385,13 @@ Short hand for [ECMAScript](guide/glossary#ecmascript) 2015.
## Injector ## Injector
<div class="l-sub-section">
An object in the Angular [dependency-injection system](guide/glossary#dependency-injection) An object in the Angular [dependency-injection system](guide/glossary#dependency-injection)
that can find a named dependency in its cache or create a dependency that can find a named dependency in its cache or create a dependency
with a registered [provider](guide/glossary#provider). with a registered [provider](guide/glossary#provider).
</div>
## Input ## Input
<div class="l-sub-section">
A directive property that can be the *target* of a A directive property that can be the *target* of a
[property binding](guide/template-syntax#property-binding) (explained in detail in the [Template Syntax](guide/template-syntax) page). [property binding](guide/template-syntax#property-binding) (explained in detail in the [Template Syntax](guide/template-syntax) page).
Data values flow *into* this property from the data source identified Data values flow *into* this property from the data source identified
@ -553,16 +400,8 @@ in the template expression to the right of the equal sign.
See the [Input and output properties](guide/template-syntax#inputs-outputs) section of the [Template Syntax](guide/template-syntax) page. See the [Input and output properties](guide/template-syntax#inputs-outputs) section of the [Template Syntax](guide/template-syntax) page.
</div>
## Interpolation ## Interpolation
<div class="l-sub-section">
A form of [property data binding](guide/glossary#data-binding) in which a A form of [property data binding](guide/glossary#data-binding) in which a
[template expression](guide/glossary#template-expression) between double-curly braces [template expression](guide/glossary#template-expression) between double-curly braces
renders as text. That text may be concatenated with neighboring text renders as text. That text may be concatenated with neighboring text
@ -581,9 +420,6 @@ Read more about [interpolation](guide/template-syntax#interpolation) in the
[Template Syntax](guide/template-syntax) page. [Template Syntax](guide/template-syntax) page.
</div>
{@a J} {@a J}
{@a jit} {@a jit}
@ -591,40 +427,22 @@ Read more about [interpolation](guide/template-syntax#interpolation) in the
## Just-in-time (JIT) compilation ## Just-in-time (JIT) compilation
<div class="l-sub-section">
A bootstrapping method of compiling components and modules in the browser A bootstrapping method of compiling components and modules in the browser
and launching the application dynamically. Just-in-time mode is a good choice during development. and launching the application dynamically. Just-in-time mode is a good choice during development.
Consider using the [ahead-of-time](guide/glossary#aot) mode for production apps. Consider using the [ahead-of-time](guide/glossary#aot) mode for production apps.
</div>
{@a K} {@a K}
## kebab-case ## kebab-case
<div class="l-sub-section">
See [dash-case](guide/glossary#dash-case). See [dash-case](guide/glossary#dash-case).
</div>
{@a L} {@a L}
## Lifecycle hooks ## Lifecycle hooks
<div class="l-sub-section">
[Directives](guide/glossary#directive) and [components](guide/glossary#component) have a lifecycle [Directives](guide/glossary#directive) and [components](guide/glossary#component) have a lifecycle
managed by Angular as it creates, updates, and destroys them. managed by Angular as it creates, updates, and destroys them.
@ -648,21 +466,13 @@ Angular calls these hook methods in the following order:
Read more in the [Lifecycle Hooks](guide/lifecycle-hooks) page. Read more in the [Lifecycle Hooks](guide/lifecycle-hooks) page.
</div>
{@a M} {@a M}
## Module ## Module
<div class="l-sub-section">
<div class="alert is-important"> <div class="alert is-important">
Angular has the following types of modules: Angular has the following types of modules:
* [Angular modules](guide/glossary#angular-module). * [Angular modules](guide/glossary#angular-module).
@ -699,20 +509,12 @@ class belongs to a feature module named `date_pipe` in the file `date_pipe.ts`.
You rarely access Angular feature modules directly. You usually import them from an Angular [scoped package](guide/glossary#scoped-package) such as `@angular/core`. You rarely access Angular feature modules directly. You usually import them from an Angular [scoped package](guide/glossary#scoped-package) such as `@angular/core`.
</div>
{@a N} {@a N}
{@a O} {@a O}
## Observable ## Observable
<div class="l-sub-section">
An array whose items arrive asynchronously over time. An array whose items arrive asynchronously over time.
Observables help you manage asynchronous data, such as data coming from a backend service. Observables help you manage asynchronous data, such as data coming from a backend service.
Observables are used within Angular itself, including Angular's event system and its HTTP client service. Observables are used within Angular itself, including Angular's event system and its HTTP client service.
@ -721,14 +523,8 @@ To use observables, Angular uses a third-party library called Reactive Extension
Observables are a proposed feature for ES2016, the next version of JavaScript. Observables are a proposed feature for ES2016, the next version of JavaScript.
</div>
## Output ## Output
<div class="l-sub-section">
A directive property that can be the *target* of event binding A directive property that can be the *target* of event binding
(read more in the [event binding](guide/template-syntax#event-binding) (read more in the [event binding](guide/template-syntax#event-binding)
section of the [Template Syntax](guide/template-syntax) page). section of the [Template Syntax](guide/template-syntax) page).
@ -738,17 +534,10 @@ in the template expression to the right of the equal sign.
See the [Input and output properties](guide/template-syntax#inputs-outputs) section of the [Template Syntax](guide/template-syntax) page. See the [Input and output properties](guide/template-syntax#inputs-outputs) section of the [Template Syntax](guide/template-syntax) page.
</div>
{@a P} {@a P}
## PascalCase ## PascalCase
<div class="l-sub-section">
The practice of writing individual words, compound words, or phrases such that each word or abbreviation begins with a capital letter. The practice of writing individual words, compound words, or phrases such that each word or abbreviation begins with a capital letter.
Class names are typically spelled in PascalCase. For example, `Person` and `HeroDetailComponent`. Class names are typically spelled in PascalCase. For example, `Person` and `HeroDetailComponent`.
@ -756,16 +545,8 @@ This form is also known as *upper camel case* to distinguish it from *lower came
In this documentation, "PascalCase" means *upper camel case* and "camelCase" means *lower camel case*. In this documentation, "PascalCase" means *upper camel case* and "camelCase" means *lower camel case*.
</div>
## Pipe ## Pipe
<div class="l-sub-section">
An Angular pipe is a function that transforms input values to output values for An Angular pipe is a function that transforms input values to output values for
display in a [view](guide/glossary#view). display in a [view](guide/glossary#view).
Here's an example that uses the built-in `currency` pipe to display Here's an example that uses the built-in `currency` pipe to display
@ -783,35 +564,19 @@ You can also write your own custom pipes.
Read more in the page on [pipes](guide/pipes). Read more in the page on [pipes](guide/pipes).
</div>
## Provider ## Provider
<div class="l-sub-section">
A _provider_ creates a new instance of a dependency for the A _provider_ creates a new instance of a dependency for the
[dependency injection](guide/glossary#dependency-injection) system. [dependency injection](guide/glossary#dependency-injection) system.
It relates a lookup token to code&mdash;sometimes called a "recipe"&mdash;that can create a dependency value. It relates a lookup token to code&mdash;sometimes called a "recipe"&mdash;that can create a dependency value.
</div>
{@a Q} {@a Q}
{@a R} {@a R}
## Reactive forms ## Reactive forms
<div class="l-sub-section">
A technique for building Angular forms through code in a component. A technique for building Angular forms through code in a component.
The alternative technique is [template-driven forms](guide/glossary#template-driven-forms). The alternative technique is [template-driven forms](guide/glossary#template-driven-forms).
@ -825,16 +590,8 @@ When building reactive forms:
Reactive forms are powerful, flexible, and a good choice for more complex data-entry form scenarios, such as dynamic generation of form controls. Reactive forms are powerful, flexible, and a good choice for more complex data-entry form scenarios, such as dynamic generation of form controls.
</div>
## Router ## Router
<div class="l-sub-section">
Most applications consist of many screens or [views](guide/glossary#view). Most applications consist of many screens or [views](guide/glossary#view).
The user navigates among them by clicking links and buttons, The user navigates among them by clicking links and buttons,
and performing other similar actions that cause the application to and performing other similar actions that cause the application to
@ -855,47 +612,24 @@ directives that users can click to navigate.
For more information, see the [Routing & Navigation](guide/router) page. For more information, see the [Routing & Navigation](guide/router) page.
</div>
## Router module ## Router module
<div class="l-sub-section">
A separate [Angular module](guide/glossary#angular-module) that provides the necessary service providers and directives for navigating through application views. A separate [Angular module](guide/glossary#angular-module) that provides the necessary service providers and directives for navigating through application views.
For more information, see the [Routing & Navigation](guide/router) page. For more information, see the [Routing & Navigation](guide/router) page.
</div>
## Routing component ## Routing component
<div class="l-sub-section">
An Angular [component](guide/glossary#component) with a `RouterOutlet` that displays views based on router navigations. An Angular [component](guide/glossary#component) with a `RouterOutlet` that displays views based on router navigations.
For more information, see the [Routing & Navigation](guide/router) page. For more information, see the [Routing & Navigation](guide/router) page.
</div>
{@a S} {@a S}
## Scoped package ## Scoped package
<div class="l-sub-section">
A way to group related *npm* packages. A way to group related *npm* packages.
Read more at the [npm-scope](https://docs.npmjs.com/misc/scope) page. Read more at the [npm-scope](https://docs.npmjs.com/misc/scope) page.
@ -912,17 +646,8 @@ is that the scoped package name begins with the Angular *scope name*, `@angular`
</code-example> </code-example>
</div>
## Service ## Service
<div class="l-sub-section">
For data or logic that is not associated For data or logic that is not associated
with a specific view or that you want to share across components, build services. with a specific view or that you want to share across components, build services.
@ -938,27 +663,15 @@ Applications often require services such as a data service or a logging service.
For more information, see the [Services](tutorial/toh-pt4) page of the [Tour of Heroes](tutorial) tutorial. For more information, see the [Services](tutorial/toh-pt4) page of the [Tour of Heroes](tutorial) tutorial.
</div>
{@a snake-case} {@a snake-case}
## snake_case ## snake_case
<div class="l-sub-section">
The practice of writing compound words or phrases such that an The practice of writing compound words or phrases such that an
underscore (`_`) separates one word from the next. This form is also known as *underscore case*. underscore (`_`) separates one word from the next. This form is also known as *underscore case*.
</div>
{@a structural-directive} {@a structural-directive}
@ -967,10 +680,6 @@ underscore (`_`) separates one word from the next. This form is also known as *u
## Structural directives ## Structural directives
<div class="l-sub-section">
A category of [directive](guide/glossary#directive) that can A category of [directive](guide/glossary#directive) that can
shape or reshape HTML layout, typically by adding and removing elements in the DOM. shape or reshape HTML layout, typically by adding and removing elements in the DOM.
The `ngIf` "conditional element" directive and the `ngFor` "repeater" directive are well-known examples. The `ngIf` "conditional element" directive and the `ngFor` "repeater" directive are well-known examples.
@ -978,32 +687,17 @@ The `ngIf` "conditional element" directive and the `ngFor` "repeater" directive
Read more in the [Structural Directives](guide/structural-directives) page. Read more in the [Structural Directives](guide/structural-directives) page.
</div>
{@a T} {@a T}
## Template ## Template
<div class="l-sub-section">
A chunk of HTML that Angular uses to render a [view](guide/glossary#view) with A chunk of HTML that Angular uses to render a [view](guide/glossary#view) with
the support and guidance of an Angular [directive](guide/glossary#directive), the support and guidance of an Angular [directive](guide/glossary#directive),
most notably a [component](guide/glossary#component). most notably a [component](guide/glossary#component).
</div>
## Template-driven forms ## Template-driven forms
<div class="l-sub-section">
A technique for building Angular forms using HTML forms and input elements in the view. A technique for building Angular forms using HTML forms and input elements in the view.
The alternate technique is [Reactive Forms](guide/glossary#reactive-forms). The alternate technique is [Reactive Forms](guide/glossary#reactive-forms).
@ -1020,16 +714,8 @@ Read about how to build template-driven forms
in the [Forms](guide/forms) page. in the [Forms](guide/forms) page.
</div>
## Template expression ## Template expression
<div class="l-sub-section">
A TypeScript-like syntax that Angular evaluates within A TypeScript-like syntax that Angular evaluates within
a [data binding](guide/glossary#data-binding). a [data binding](guide/glossary#data-binding).
@ -1038,30 +724,14 @@ in the [Template expressions](guide/template-syntax#template-expressions) sectio
of the [Template Syntax](guide/template-syntax) page. of the [Template Syntax](guide/template-syntax) page.
</div>
## Transpile ## Transpile
<div class="l-sub-section">
The process of transforming code written in one form of JavaScript The process of transforming code written in one form of JavaScript
(such as TypeScript) into another form of JavaScript (such as [ES5](guide/glossary#es5)). (such as TypeScript) into another form of JavaScript (such as [ES5](guide/glossary#es5)).
</div>
## TypeScript ## TypeScript
<div class="l-sub-section">
A version of JavaScript that supports most [ECMAScript 2015](guide/glossary#es2015) A version of JavaScript that supports most [ECMAScript 2015](guide/glossary#es2015)
language features such as [decorators](guide/glossary#decorator). language features such as [decorators](guide/glossary#decorator).
@ -1076,20 +746,12 @@ you can use other JavaScript dialects such as [ES5](guide/glossary#es5).
Read more about TypeScript at [typescriptlang.org](http://www.typescriptlang.org/). Read more about TypeScript at [typescriptlang.org](http://www.typescriptlang.org/).
</div>
{@a U} {@a U}
{@a V} {@a V}
## View ## View
<div class="l-sub-section">
A portion of the screen that displays information and responds A portion of the screen that displays information and responds
to user actions such as clicks, mouse moves, and keystrokes. to user actions such as clicks, mouse moves, and keystrokes.
@ -1103,10 +765,6 @@ dynamically as the user navigates through the application, typically
under the control of a [router](guide/glossary#router). under the control of a [router](guide/glossary#router).
</div>
{@a W} {@a W}
@ -1120,10 +778,6 @@ under the control of a [router](guide/glossary#router).
## Zone ## Zone
<div class="l-sub-section">
A mechanism for encapsulating and intercepting A mechanism for encapsulating and intercepting
a JavaScript application's asynchronous activity. a JavaScript application's asynchronous activity.
@ -1142,6 +796,3 @@ the information it displays via [data bindings](guide/glossary#data-binding).
Learn more about zones in this Learn more about zones in this
[Brian Ford video](https://www.youtube.com/watch?v=3IqtmUscE_U). [Brian Ford video](https://www.youtube.com/watch?v=3IqtmUscE_U).
</div>

View File

@ -104,7 +104,7 @@ including:
Other notable differences from JavaScript syntax include: Other notable differences from JavaScript syntax include:
* no support for the bitwise operators `|` and `&` * no support for the bitwise operators `|` and `&`
* new [template expression operators](guide/template-syntax#expression-operators), such as `|` and `?.` * new [template expression operators](guide/template-syntax#expression-operators), such as `|`, `?.` and `!`.
{@a expression-context} {@a expression-context}
@ -1931,6 +1931,27 @@ It works perfectly with long property paths such as `a?.b?.c?.d`.
<hr/> <hr/>
{@a non-null-assertion-operator}
### The non-null assertion operator ( <span class="syntax">!</span> )
The Angular **non-null assertion operator (`!`)** is a post-fix operator that asserts that the preceeding property path
will never be null or undefined.
Unlike the [_safe navigation operator_](guide/template-syntax#safe-navigation-operator "Safe naviation operator (?.)")
the **non-null assertion operator** does not guard against a null or undefined; rather, it informs the TypeScript type
checker that there is something it might be unaware of that ensures that this property path is defined. This prevents
TypeScript from reporting that the path is possibly null or undefined when strict null checking is enabled.
For example, if you use [*ngIf](guide/template-syntax#ngIf) to check if `hero` is defined, you can assert the uses of
`hero` are defined in the body of the template.
<code-example path="template-syntax/src/app/app.component.html" region="non-null-assertion-1" title="src/app/app.component.html" linenums="false">
</code-example>
The Angular **non-null assertion operator (`!`)** is like TypeScript's _non-null assertion operator (!)_
introduced in [TypeScript 2.0](http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html).
## Summary ## Summary
You've completed this survey of template syntax. You've completed this survey of template syntax.
Now it's time to put that knowledge to work on your own components and directives. Now it's time to put that knowledge to work on your own components and directives.

View File

@ -66,7 +66,7 @@ Anything you can import from `@angular` is a nested member of this `ng` object:
<code-tabs> <code-tabs>
<code-pane title="TypeScript" path="ts-to-js/ts/src/app/app.module.ts" region="ng2import"> <code-pane title="TypeScript" path="ts-to-js/ts/src/app/app.module.ts" region="ng2import">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript with decorators" path="ts-to-js/js-es6-decorators/src/app/app.module.es6" region="ng2import"> <code-pane title="ES6 + Decorators" path="ts-to-js/js-es6-decorators/src/app/app.module.es6" region="ng2import">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/app.module.es6" region="ng2import"> <code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/app.module.es6" region="ng2import">
</code-pane> </code-pane>
@ -104,7 +104,7 @@ Here is a `HeroComponent` as it might be defined and "exported" in each of the f
<code-tabs> <code-tabs>
<code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero.component.ts" region="appexport"> <code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero.component.ts" region="appexport">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript with decorators" path="ts-to-js/js-es6-decorators/src/app/hero.component.es6" region="appexport"> <code-pane title="ES6 + Decorators" path="ts-to-js/js-es6-decorators/src/app/hero.component.es6" region="appexport">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero.component.es6" region="appexport"> <code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero.component.es6" region="appexport">
</code-pane> </code-pane>
@ -121,7 +121,7 @@ In _ES5_ you use the shared namespace object to access "exported" entities from
<code-tabs> <code-tabs>
<code-pane title="TypeScript" path="ts-to-js/ts/src/app/app.module.ts" region="appimport"> <code-pane title="TypeScript" path="ts-to-js/ts/src/app/app.module.ts" region="appimport">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript with decorators" path="ts-to-js/js-es6-decorators/src/app/app.module.es6" region="appimport"> <code-pane title="ES6 + Decorators" path="ts-to-js/js-es6-decorators/src/app/app.module.es6" region="appimport">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/app.module.es6" region="appimport"> <code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/app.module.es6" region="appimport">
</code-pane> </code-pane>
@ -167,7 +167,7 @@ Use the constructor function pattern instead, adding methods to the prototype.
<code-tabs> <code-tabs>
<code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero.component.ts" region="class"> <code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero.component.ts" region="class">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript with decorators" path="ts-to-js/js-es6-decorators/src/app/hero.component.es6" region="class"> <code-pane title="ES6 + Decorators" path="ts-to-js/js-es6-decorators/src/app/hero.component.es6" region="class">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero.component.es6" region="class"> <code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero.component.es6" region="class">
</code-pane> </code-pane>
@ -193,7 +193,7 @@ See these variations side-by-side:
<code-tabs> <code-tabs>
<code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero.component.ts" region="metadata"> <code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero.component.ts" region="metadata">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript with decorators" path="ts-to-js/js-es6-decorators/src/app/hero.component.es6" region="metadata"> <code-pane title="ES6 + Decorators" path="ts-to-js/js-es6-decorators/src/app/hero.component.es6" region="metadata">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero.component.es6" region="metadata"> <code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero.component.es6" region="metadata">
</code-pane> </code-pane>
@ -213,7 +213,7 @@ The component, `HeroTitleComponent` in this case, then references the template f
<code-tabs> <code-tabs>
<code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero-title.component.ts" region="templateUrl"> <code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero-title.component.ts" region="templateUrl">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript with decorators" path="ts-to-js/js-es6-decorators/src/app/hero-title.component.es6" region="templateUrl"> <code-pane title="ES6 + Decorators" path="ts-to-js/js-es6-decorators/src/app/hero-title.component.es6" region="templateUrl">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero-title.component.es6" region="templateUrl"> <code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero-title.component.es6" region="templateUrl">
</code-pane> </code-pane>
@ -322,7 +322,7 @@ Just implement the methods and ignore interfaces when translating code samples f
<code-tabs> <code-tabs>
<code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero-lifecycle.component.ts"> <code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero-lifecycle.component.ts">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript with decorators" path="ts-to-js/js-es6-decorators/src/app/hero-lifecycle.component.es6"> <code-pane title="ES6 + Decorators" path="ts-to-js/js-es6-decorators/src/app/hero-lifecycle.component.es6">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero-lifecycle.component.es6"> <code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero-lifecycle.component.es6">
</code-pane> </code-pane>
@ -354,7 +354,7 @@ combined in the metadata `inputs` and `outputs` _arrays_.
<code-tabs> <code-tabs>
<code-pane title="TypeScript" path="ts-to-js/ts/src/app/confirm.component.ts"> <code-pane title="TypeScript" path="ts-to-js/ts/src/app/confirm.component.ts">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript with decorators" path="ts-to-js/js-es6-decorators/src/app/confirm.component.es6"> <code-pane title="ES6 + Decorators" path="ts-to-js/js-es6-decorators/src/app/confirm.component.es6">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/confirm.component.es6"> <code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/confirm.component.es6">
</code-pane> </code-pane>
@ -412,7 +412,7 @@ This format should be familiar to AngularJS developers.
<code-tabs> <code-tabs>
<code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero-di.component.ts"> <code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero-di.component.ts">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript with decorators" path="ts-to-js/js-es6-decorators/src/app/hero-di.component.es6"> <code-pane title="ES6 + Decorators" path="ts-to-js/js-es6-decorators/src/app/hero-di.component.es6">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero-di.component.es6"> <code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero-di.component.es6">
</code-pane> </code-pane>
@ -430,8 +430,8 @@ In _TypeScript_ and _ES6-with-decorators_, you precede the class constructor par
by calling the `@Inject()` decorator with the injection token. by calling the `@Inject()` decorator with the injection token.
In the following example, the token is the string `'heroName'`. In the following example, the token is the string `'heroName'`.
The other JavaScript dialects add a `parameters` array to the class contructor function. The other JavaScript dialects add a `parameters` array to the class constructor function.
Each item constains a new instance of `Inject`: Each item constrains a new instance of `Inject`:
* _Plain ES6_&mdash;each item is a new instance of `Inject(token)` in a sub-array. * _Plain ES6_&mdash;each item is a new instance of `Inject(token)` in a sub-array.
* _ES5_&mdash;simply list the string tokens. * _ES5_&mdash;simply list the string tokens.
@ -442,7 +442,7 @@ array as before. Create a new instance of `ng.core.Inject(token)` for each param
<code-tabs> <code-tabs>
<code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero-di-inject.component.ts"> <code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero-di-inject.component.ts">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript with decorators" path="ts-to-js/js-es6-decorators/src/app/hero-di-inject.component.es6"> <code-pane title="ES6 + Decorators" path="ts-to-js/js-es6-decorators/src/app/hero-di-inject.component.es6">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero-di-inject.component.es6"> <code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero-di-inject.component.es6">
</code-pane> </code-pane>
@ -475,7 +475,7 @@ array as before. Use a nested array to define a parameter's complete injection s
<code-tabs> <code-tabs>
<code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero-title.component.ts"> <code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero-title.component.ts">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript with decorators" path="ts-to-js/js-es6-decorators/src/app/hero-title.component.es6"> <code-pane title="ES6 + Decorators" path="ts-to-js/js-es6-decorators/src/app/hero-title.component.es6">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero-title.component.es6"> <code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero-title.component.es6">
</code-pane> </code-pane>
@ -522,7 +522,7 @@ The `host` value is an object whose properties are host property and listener b
<code-tabs> <code-tabs>
<code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero-host.component.ts"> <code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero-host.component.ts">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript with decorators" path="ts-to-js/js-es6-decorators/src/app/hero-host.component.es6"> <code-pane title="ES6 + Decorators" path="ts-to-js/js-es6-decorators/src/app/hero-host.component.es6">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero-host.component.es6"> <code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero-host.component.es6">
</code-pane> </code-pane>
@ -545,7 +545,7 @@ These particular _TypeScript_ and _ES6_ code snippets happen to be identical.
<code-tabs> <code-tabs>
<code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero-host-meta.component.ts"> <code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero-host-meta.component.ts">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript with decorators" path="ts-to-js/js-es6-decorators/src/app/hero-host-meta.component.es6"> <code-pane title="ES6 + Decorators" path="ts-to-js/js-es6-decorators/src/app/hero-host-meta.component.es6">
</code-pane> </code-pane>
</code-tabs> </code-tabs>
@ -578,7 +578,7 @@ The `queries` property value is a hash map.
<code-tabs> <code-tabs>
<code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero-queries.component.ts" region="view"> <code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero-queries.component.ts" region="view">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript with decorators" path="ts-to-js/js-es6-decorators/src/app/hero-queries.component.es6" region="view"> <code-pane title="ES6 + Decorators" path="ts-to-js/js-es6-decorators/src/app/hero-queries.component.es6" region="view">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero-queries.component.es6" region="view"> <code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero-queries.component.es6" region="view">
</code-pane> </code-pane>
@ -597,7 +597,7 @@ They can be added in the same way as [`@ViewChild`](api/core/ViewChild) and
<code-tabs> <code-tabs>
<code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero-queries.component.ts" region="content"> <code-pane title="TypeScript" path="ts-to-js/ts/src/app/hero-queries.component.ts" region="content">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript with decorators" path="ts-to-js/js-es6-decorators/src/app/hero-queries.component.es6" region="content"> <code-pane title="ES6 + Decorators" path="ts-to-js/js-es6-decorators/src/app/hero-queries.component.es6" region="content">
</code-pane> </code-pane>
<code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero-queries.component.es6" region="content"> <code-pane title="ES6 JavaScript" path="ts-to-js/js-es6/src/app/hero-queries.component.es6" region="content">
</code-pane> </code-pane>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -0,0 +1,34 @@
<header class="marketing-banner">
<h1 class="banner-headline no-toc no-anchor">Contribute to Angular</h1>
</header>
<article class="contribute-container">
<h2 class="no-anchor">Angular Projects</h2>
<p>We'd love for you to contribute to our source code and to make Angular projects even better.</p>
<div class="l-sub-section">
<h3 class="no-anchor">Angular</h3>
Angular is a next generation mobile and desktop application development platform.
<a href="https://github.com/angular/angular/blob/master/CONTRIBUTING.md" class="button" md-button>Contribute to Angular</a>
</div>
<div class="l-sub-section">
<h3 class="no-anchor">Angular Material</h3>
Our goal is to deliver a lean, lightweight set of Angular-based UI elements that implement the material design specification for use in Angular single-page applications (SPAs).
<a href="https://github.com/angular/material2/blob/master/CONTRIBUTING.md" class="button" md-button>Contribute to Angular Material</a>
</div>
<div class="l-sub-section">
<h3 class="no-anchor">AngularFire</h3>
AngularFire is the officially supported Angular binding for Firebase. Firebase is a full backend so you don't need servers to build your Angular app.
<a href="https://github.com/angular/angularfire2" class="button" md-button> Contribute to AngularFire</a>
</div>
</article>

View File

@ -1,37 +0,0 @@
<h1 class="no-toc">Contribute to Angular</h1>
Help us build the framework of the future!
## Angular Projects
We'd love for you to contribute to our source code and to make Angular projects even better.
~~~ {.l-sub-section}
### Angular
Angular is a next generation mobile and desktop application development platform.
<a href="https://github.com/angular/angular/blob/master/CONTRIBUTING.md" class="button" md-button>Contribute to Angular</a>
~~~
~~~ {.l-sub-section}
### Angular Material
Our goal is to deliver a lean, lightweight set of Angular-based UI elements that implement the material design specification for use in Angular single-page applications (SPAs).
<a href="https://github.com/angular/material/blob/master/.github/CONTRIBUTING.md" class="button" md-button>Contribute to Angular Material</a>
~~~
~~~ {.l-sub-section}
### AngularFire
AngularFire is the officially supported Angular binding for Firebase. Firebase is a full backend so you don't need servers to build your Angular app.
<a href="https://github.com/firebase/angularFire" class="button" md-button> Contribute to AngularFire</a>
~~~

View File

@ -12,10 +12,9 @@ Angular is a platform that makes it easy to build applications with the web. Ang
<div class="docs-card"> <div class="docs-card">
<section>Get Going with Angular</section> <section>Get Going with Angular</section>
<p>Get going on your own environment with the Quickstart and Tutorial</p> <p>Get going on your own environment with the Quickstart.</p>
<p class="card-footer center" > <p class="card-footer" >
<a href="guide/quickstart" title="Angular Quickstart">Quickstart</a> &nbsp; <a href="guide/quickstart" title="Angular Quickstart">Quickstart</a>
<a href="tutorial" title="Angular Tutorial">Tutorial</a>
</p> </p>
<!--<p class="card-footer"><a href="guide/quickstart">Quickstart</a></p> <!--<p class="card-footer"><a href="guide/quickstart">Quickstart</a></p>
<p class="card-footer"><a href="guide/tutorial">Tutorial</a></p>--> <p class="card-footer"><a href="guide/tutorial">Tutorial</a></p>-->

View File

@ -1,5 +1,5 @@
<header class="marketing-banner"> <header class="marketing-banner">
<div class="banner-headline no-toc">Events</div> <h1 class="banner-headline no-toc no-anchor">Events</h1>
</header> </header>
<article class="l-content "> <article class="l-content ">

View File

@ -1,3 +1,7 @@
<header class="marketing-banner">
<h1 class="banner-headline no-toc no-anchor">Features & Benefits</h1>
</header>
<article class="l-content "> <article class="l-content ">
<div class="flex-center"> <div class="flex-center">
<div> <div>
@ -100,6 +104,10 @@
</div> </div>
</div> </div>
<div class="cta-bar"><a href="guide/quickstart" md-button="md-button" class="button button-large hero-cta">Get Started</a></div> <div class="cta-bar announcement-bar">
<button class="button">
<a href="guide/quickstart">Get Started</a>
</button>
</div>
</article> </article>

View File

@ -35,7 +35,7 @@
<img src="assets/images/logos/angular/angular-banner-logo-grey.png" width="64"/> <img src="assets/images/logos/angular/angular-banner-logo-grey.png" width="64"/>
<p>Angular v4.0 is out! Smaller, faster, no biggie...</p> <p>Angular v4.0 is out! Smaller, faster, no biggie...</p>
<button class="button"> <button class="button">
<a href="http://angularjs.blogspot.com/2017/03/angular-400-now-available.html" target="_blank">Learn More</a><span class="material-icons">arrow_forward</span> <a href="http://angularjs.blogspot.com/2017/03/angular-400-now-available.html" target="_blank">Learn More</a>
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,5 +1,8 @@
<h1 class="no-toc">Press Kit</h1> <header class="marketing-banner">
<div class="presskit-container l-space-neg-top-8"> <h1 class="banner-headline no-toc no-anchor">Press kit</h1>
</header>
<article class="presskit-container">
<div class="presskit-row"> <div class="presskit-row">
<div class="presskit-inner"> <div class="presskit-inner">
<div> <div>
@ -189,4 +192,4 @@
</div> </div>
</div> </div>
</div> </div>
</div> </article>

View File

@ -1,5 +1,5 @@
<header class="marketing-banner"> <header class="marketing-banner">
<div class="banner-headline no-toc">Explore Angular Resources</div> <h1 class="banner-headline no-toc no-anchor">Explore Angular Resources</h1>
</header> </header>
<article> <article>

View File

@ -12,20 +12,17 @@
<a class="nav-link home" href="/"><img src="{{ homeImageUrl }}" title="Home" alt="Home"></a> <a class="nav-link home" href="/"><img src="{{ homeImageUrl }}" title="Home" alt="Home"></a>
<aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes"></aio-top-menu> <aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes"></aio-top-menu>
<aio-search-box class="search-container" #searchBox (search)="doSearch($event)"></aio-search-box> <aio-search-box class="search-container" #searchBox (search)="doSearch($event)"></aio-search-box>
<span class="fill-remaining-space"></span>
</md-toolbar> </md-toolbar>
<aio-search-results #searchResults *ngIf="showSearchResults" (resultSelected)="hideSearchResults()"></aio-search-results> <aio-search-results #searchResults *ngIf="showSearchResults" (resultSelected)="hideSearchResults()"></aio-search-results>
<md-sidenav-container class="sidenav-container" [class.starting]="isStarting" role="main"> <md-sidenav-container class="sidenav-container" [class.starting]="isStarting" role="main">
<md-sidenav [ngClass]="{'collapsed': !isSideBySide }" #sidenav class="sidenav" [opened]="isOpened" [mode]="mode" (open)="updateHostClasses()" (close)="updateHostClasses()"> <md-sidenav [ngClass]="{'collapsed': !isSideBySide }" #sidenav class="sidenav" [opened]="isOpened" [mode]="mode" (open)="updateHostClasses()" (close)="updateHostClasses()">
<aio-nav-menu *ngIf="!isSideBySide" [nodes]="topMenuNarrowNodes" [currentNode]="currentNode"></aio-nav-menu> <aio-nav-menu *ngIf="!isSideBySide" [nodes]="topMenuNarrowNodes" [currentNode]="currentNodes?.TopBarNarrow"></aio-nav-menu>
<aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNode" ></aio-nav-menu> <aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes?.SideNav" ></aio-nav-menu>
<div class="doc-version" title="Angular docs version {{currentDocVersion?.title}}"> <div class="doc-version" title="Angular docs version {{currentDocVersion?.title}}">
<select (change)="onDocVersionChange($event.target.selectedIndex)"> <aio-select (change)="onDocVersionChange($event.index)" [options]="docVersions" [selected]="docVersions && docVersions[0]"></aio-select>
<option *ngFor="let version of docVersions" [value]="version.title">{{version.title}}</option>
</select>
</div> </div>
</md-sidenav> </md-sidenav>

View File

@ -24,6 +24,7 @@ import { ScrollService } from 'app/shared/scroll.service';
import { SearchBoxComponent } from 'app/search/search-box/search-box.component'; import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
import { SearchResultsComponent } from 'app/search/search-results/search-results.component'; import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
import { SearchService } from 'app/search/search.service'; import { SearchService } from 'app/search/search.service';
import { SelectComponent, Option } from 'app/shared/select/select.component';
import { SwUpdateNotificationsService } from 'app/sw-updates/sw-update-notifications.service'; import { SwUpdateNotificationsService } from 'app/sw-updates/sw-update-notifications.service';
import { TocComponent } from 'app/embedded/toc/toc.component'; import { TocComponent } from 'app/embedded/toc/toc.component';
import { MdSidenav } from '@angular/material'; import { MdSidenav } from '@angular/material';
@ -221,26 +222,28 @@ describe('AppComponent', () => {
}); });
describe('SideNav version selector', () => { describe('SideNav version selector', () => {
let selectElement: DebugElement;
let selectComponent: SelectComponent;
beforeEach(() => { beforeEach(() => {
component.onResize(sideBySideBreakPoint + 1); // side-by-side component.onResize(sideBySideBreakPoint + 1); // side-by-side
selectElement = fixture.debugElement.query(By.directive(SelectComponent));
selectComponent = selectElement.componentInstance;
}); });
it('should pick first (current) version by default', () => { it('should pick first (current) version by default', () => {
const versionSelector = sidenav.querySelector('select'); expect(selectComponent.selected.title).toEqual(component.versionInfo.raw);
expect(versionSelector.value).toEqual(component.versionInfo.raw);
expect(versionSelector.selectedIndex).toEqual(0);
}); });
// Older docs versions have an href // Older docs versions have an href
it('should navigate when change to a version with an href', () => { it('should navigate when change to a version with an href', () => {
component.onDocVersionChange(1); selectElement.triggerEventHandler('change', { option: component.docVersions[1] as Option, index: 1});
expect(locationService.go).toHaveBeenCalledWith(TestHttp.docVersions[0].url); expect(locationService.go).toHaveBeenCalledWith(TestHttp.docVersions[0].url);
}); });
// The current docs version should not have an href // The current docs version should not have an href
// This may change when we perfect our docs versioning approach // This may change when we perfect our docs versioning approach
it('should not navigate when change to a version without an href', () => { it('should not navigate when change to a version without an href', () => {
component.onDocVersionChange(0); selectElement.triggerEventHandler('change', { option: component.docVersions[0] as Option, index: 0});
expect(locationService.go).not.toHaveBeenCalled(); expect(locationService.go).not.toHaveBeenCalled();
}); });
}); });

View File

@ -2,7 +2,7 @@ import { Component, ElementRef, HostBinding, HostListener, OnInit,
QueryList, ViewChild, ViewChildren } from '@angular/core'; QueryList, ViewChild, ViewChildren } from '@angular/core';
import { MdSidenav } from '@angular/material'; import { MdSidenav } from '@angular/material';
import { CurrentNode, NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service'; import { CurrentNodes, NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
import { DocumentService, DocumentContents } from 'app/documents/document.service'; import { DocumentService, DocumentContents } from 'app/documents/document.service';
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component'; import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
import { LocationService } from 'app/shared/location.service'; import { LocationService } from 'app/shared/location.service';
@ -25,7 +25,7 @@ export class AppComponent implements OnInit {
currentDocument: DocumentContents; currentDocument: DocumentContents;
currentDocVersion: NavigationNode; currentDocVersion: NavigationNode;
currentNode: CurrentNode; currentNodes: CurrentNodes;
currentPath: string; currentPath: string;
docVersions: NavigationNode[]; docVersions: NavigationNode[];
dtOn = false; dtOn = false;
@ -59,7 +59,6 @@ export class AppComponent implements OnInit {
isSideBySide = false; isSideBySide = false;
private isFetchingTimeout: any; private isFetchingTimeout: any;
private isSideNavDoc = false; private isSideNavDoc = false;
private previousNavView: string;
private sideBySideWidth = 992; private sideBySideWidth = 992;
sideNavNodes: NavigationNode[]; sideNavNodes: NavigationNode[];
@ -139,17 +138,17 @@ export class AppComponent implements OnInit {
} }
}); });
this.navigationService.currentNode.subscribe(currentNode => { this.navigationService.currentNodes.subscribe(currentNodes => {
this.currentNode = currentNode; this.currentNodes = currentNodes;
// Preserve current sidenav open state by default // Preserve current sidenav open state by default
let openSideNav = this.sidenav.opened; let openSideNav = this.sidenav.opened;
const isSideNavDoc = !!currentNodes[sideNavView];
if (this.previousNavView !== currentNode.view) { if (this.isSideNavDoc !== isSideNavDoc) {
this.previousNavView = currentNode.view;
// View type changed. Is it now a sidenav view (e.g, guide or tutorial)? // 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. // Open if changed to a sidenav doc; close if changed to a marketing doc.
openSideNav = this.isSideNavDoc = currentNode.view === sideNavView; openSideNav = this.isSideNavDoc = isSideNavDoc;
} }
// May be open or closed when wide; always closed when narrow // May be open or closed when wide; always closed when narrow
this.sideNavToggle(this.isSideBySide ? openSideNav : false); this.sideNavToggle(this.isSideBySide ? openSideNav : false);
@ -186,6 +185,9 @@ export class AppComponent implements OnInit {
// Stop fetching timeout (which, when render is fast, means progress bar never shown) // Stop fetching timeout (which, when render is fast, means progress bar never shown)
clearTimeout(this.isFetchingTimeout); clearTimeout(this.isFetchingTimeout);
// Put page in a clean visual state
this.scrollService.scrollToTop();
// Scroll 500ms after the doc-viewer has finished rendering the new doc // Scroll 500ms after the doc-viewer has finished rendering the new doc
// The delay is to allow time for async layout to complete // The delay is to allow time for async layout to complete
setTimeout(() => { setTimeout(() => {
@ -253,9 +255,9 @@ export class AppComponent implements OnInit {
const sideNavOpen = `sidenav-${this.sidenav.opened ? 'open' : 'closed'}`; const sideNavOpen = `sidenav-${this.sidenav.opened ? 'open' : 'closed'}`;
const pageClass = `page-${this.pageId}`; const pageClass = `page-${this.pageId}`;
const folderClass = `folder-${this.folderId}`; const folderClass = `folder-${this.folderId}`;
const viewClass = `view-${this.currentNode && this.currentNode.view}`; const viewClasses = Object.keys(this.currentNodes || {}).map(view => `view-${view}`).join(' ');
this.hostClasses = `${sideNavOpen} ${pageClass} ${folderClass} ${viewClass}`; this.hostClasses = `${sideNavOpen} ${pageClass} ${folderClass} ${viewClasses}`;
} }
// Dynamically change height of table of contents container // Dynamically change height of table of contents container

View File

@ -45,6 +45,8 @@ import { SearchResultsComponent } from './search/search-results/search-results.c
import { SearchBoxComponent } from './search/search-box/search-box.component'; import { SearchBoxComponent } from './search/search-box/search-box.component';
import { TocService } from 'app/shared/toc.service'; import { TocService } from 'app/shared/toc.service';
import { SharedModule } from 'app/shared/shared.module';
// These are the hardcoded inline svg sources to be used by the `<md-icon>` component // These are the hardcoded inline svg sources to be used by the `<md-icon>` component
export const svgIconProviders = [ export const svgIconProviders = [
{ {
@ -80,7 +82,8 @@ export const svgIconProviders = [
MdSidenavModule, MdSidenavModule,
MdTabsModule, MdTabsModule,
MdToolbarModule, MdToolbarModule,
SwUpdatesModule SwUpdatesModule,
SharedModule
], ],
declarations: [ declarations: [
AppComponent, AppComponent,

View File

@ -1,26 +1,17 @@
<div class="l-flex-wrap info-banner api-filter"> <div class="l-flex-wrap info-banner api-filter">
<div class="form-select-menu"> <aio-select (change)="setType($event.option)"
<button class="form-select-button has-symbol" (click)="toggleTypeMenu()"> [options]="types"
<strong>Type:</strong><span class="symbol {{type.name}}"></span>{{type.title}} [selected]="type"
</button> [showSymbol]="true"
<ul class="form-select-dropdown" *ngIf="showTypeMenu"> label="Type:">
<li *ngFor="let t of types" (click)="setType(t)" [class.selected]="t === type"> </aio-select>
<span class="symbol {{t.name}}"></span>{{t.title}}
</li>
</ul>
</div>
<div class="form-select-menu"> <aio-select (change)="setStatus($event.option)"
<button class="form-select-button" (click)="toggleStatusMenu()"> [options]="statuses"
<strong>Status:</strong>{{status.title}} [selected]="status"
</button> label="Status:">
<ul class="form-select-dropdown" *ngIf="showStatusMenu"> </aio-select>
<li *ngFor="let s of statuses" (click)="setStatus(s)" [class.selected]="s === status">
{{s.title}}
</li>
</ul>
</div>
<div class="form-search"> <div class="form-search">
<input #filter placeholder="Filter" (input)="setQuery($event.target.value)"> <input #filter placeholder="Filter" (input)="setQuery($event.target.value)">

View File

@ -4,6 +4,7 @@ import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { ApiListComponent } from './api-list.component'; import { ApiListComponent } from './api-list.component';
import { ApiItem, ApiSection, ApiService } from './api.service'; import { ApiItem, ApiSection, ApiService } from './api.service';
import { LocationService } from 'app/shared/location.service'; import { LocationService } from 'app/shared/location.service';
import { SharedModule } from 'app/shared/shared.module';
describe('ApiListComponent', () => { describe('ApiListComponent', () => {
let component: ApiListComponent; let component: ApiListComponent;
@ -12,6 +13,7 @@ describe('ApiListComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ SharedModule ],
declarations: [ ApiListComponent ], declarations: [ ApiListComponent ],
providers: [ providers: [
{ provide: ApiService, useClass: TestApiService }, { provide: ApiService, useClass: TestApiService },
@ -75,17 +77,17 @@ describe('ApiListComponent', () => {
}); });
it('item.show should be true for items with selected status', () => { it('item.show should be true for items with selected status', () => {
component.setStatus({name: 'stable', title: 'Stable'}); component.setStatus({value: 'stable', title: 'Stable'});
expectFilteredResult('status: stable', item => item.stability === 'stable'); expectFilteredResult('status: stable', item => item.stability === 'stable');
}); });
it('item.show should be true for items with "security-risk" status when selected', () => { it('item.show should be true for items with "security-risk" status when selected', () => {
component.setStatus({name: 'security-risk', title: 'Security Risk'}); component.setStatus({value: 'security-risk', title: 'Security Risk'});
expectFilteredResult('status: security-risk', item => item.securityRisk); expectFilteredResult('status: security-risk', item => item.securityRisk);
}); });
it('item.show should be true for items of selected type', () => { it('item.show should be true for items of selected type', () => {
component.setType({name: 'class', title: 'Class'}); component.setType({value: 'class', title: 'Class'});
expectFilteredResult('type: class', item => item.docType === 'class'); expectFilteredResult('type: class', item => item.docType === 'class');
}); });
@ -189,8 +191,8 @@ describe('ApiListComponent', () => {
it('should have query, status, and type', () => { it('should have query, status, and type', () => {
component.setQuery('foo'); component.setQuery('foo');
component.setStatus({name: 'stable', title: 'Stable'}); component.setStatus({value: 'stable', title: 'Stable'});
component.setType({name: 'class', title: 'Class'}); component.setType({value: 'class', title: 'Class'});
const search = locationService.setSearch.calls.mostRecent().args[1]; const search = locationService.setSearch.calls.mostRecent().args[1];
expect(search.query).toBe('foo'); expect(search.query).toBe('foo');

View File

@ -15,10 +15,7 @@ import { combineLatest } from 'rxjs/observable/combineLatest';
import { LocationService } from 'app/shared/location.service'; import { LocationService } from 'app/shared/location.service';
import { ApiItem, ApiSection, ApiService } from './api.service'; import { ApiItem, ApiSection, ApiService } from './api.service';
interface MenuItem { import { Option } from 'app/shared/select/select.component';
name: string;
title: string;
}
class SearchCriteria { class SearchCriteria {
query? = ''; query? = '';
@ -40,29 +37,29 @@ export class ApiListComponent implements OnInit {
private criteriaSubject = new ReplaySubject<SearchCriteria>(1); private criteriaSubject = new ReplaySubject<SearchCriteria>(1);
private searchCriteria = new SearchCriteria(); private searchCriteria = new SearchCriteria();
status: MenuItem; status: Option;
type: MenuItem; type: Option;
// API types // API types
types: MenuItem[] = [ types: Option[] = [
{ name: 'all', title: 'All' }, { value: 'all', title: 'All' },
{ name: 'directive', title: 'Directive' }, { value: 'directive', title: 'Directive' },
{ name: 'pipe', title: 'Pipe'}, { value: 'pipe', title: 'Pipe'},
{ name: 'decorator', title: 'Decorator' }, { value: 'decorator', title: 'Decorator' },
{ name: 'class', title: 'Class' }, { value: 'class', title: 'Class' },
{ name: 'interface', title: 'Interface' }, { value: 'interface', title: 'Interface' },
{ name: 'function', title: 'Function' }, { value: 'function', title: 'Function' },
{ name: 'enum', title: 'Enum' }, { value: 'enum', title: 'Enum' },
{ name: 'type-alias', title: 'Type Alias' }, { value: 'type-alias', title: 'Type Alias' },
{ name: 'const', title: 'Const'} { value: 'const', title: 'Const'}
]; ];
statuses: MenuItem[] = [ statuses: Option[] = [
{ name: 'all', title: 'All' }, { value: 'all', title: 'All' },
{ name: 'stable', title: 'Stable' }, { value: 'stable', title: 'Stable' },
{ name: 'deprecated', title: 'Deprecated' }, { value: 'deprecated', title: 'Deprecated' },
{ name: 'experimental', title: 'Experimental' }, { value: 'experimental', title: 'Experimental' },
{ name: 'security-risk', title: 'Security Risk' } { value: 'security-risk', title: 'Security Risk' }
]; ];
@ViewChild('filter') queryEl: ElementRef; @ViewChild('filter') queryEl: ElementRef;
@ -90,16 +87,16 @@ export class ApiListComponent implements OnInit {
this.setSearchCriteria({query: (query || '').toLowerCase().trim() }); this.setSearchCriteria({query: (query || '').toLowerCase().trim() });
} }
setStatus(status: MenuItem) { setStatus(status: Option) {
this.toggleStatusMenu(); this.toggleStatusMenu();
this.status = status; this.status = status;
this.setSearchCriteria({status: status.name}); this.setSearchCriteria({status: status.value});
} }
setType(type: MenuItem) { setType(type: Option) {
this.toggleTypeMenu(); this.toggleTypeMenu();
this.type = type; this.type = type;
this.setSearchCriteria({type: type.name}); this.setSearchCriteria({type: type.value});
} }
toggleStatusMenu() { toggleStatusMenu() {
@ -150,13 +147,13 @@ export class ApiListComponent implements OnInit {
// Hack: can't bind to query because input cursor always forced to end-of-line. // Hack: can't bind to query because input cursor always forced to end-of-line.
this.queryEl.nativeElement.value = q; this.queryEl.nativeElement.value = q;
this.status = this.statuses.find(x => x.name === status) || this.statuses[0]; this.status = this.statuses.find(x => x.value === status) || this.statuses[0];
this.type = this.types.find(x => x.name === type) || this.types[0]; this.type = this.types.find(x => x.value === type) || this.types[0];
this.searchCriteria = { this.searchCriteria = {
query: q, query: q,
status: this.status.name, status: this.status.value,
type: this.type.name type: this.type.value
}; };
this.criteriaSubject.next(this.searchCriteria); this.criteriaSubject.next(this.searchCriteria);

View File

@ -13,16 +13,16 @@ import { CONTENT_URL_PREFIX } from 'app/documents/document.service';
<div *ngIf="person.picture" class="contributor-image" [style.background-image]="'url('+pictureBase+person.picture+')'"> <div *ngIf="person.picture" class="contributor-image" [style.background-image]="'url('+pictureBase+person.picture+')'">
<div class="contributor-info"> <div class="contributor-info">
<button> <button *ngIf="person.bio" >
<a *ngIf="person.bio" aria-label="View Bio">View Bio</a> <a aria-label="View Bio">View Bio</a>
</button> </button>
<button class="icon"> <button *ngIf="person.twitter" class="icon">
<a *ngIf="person.twitter" href="https://twitter.com/{{person.twitter}}" target="_blank"> <a href="https://twitter.com/{{person.twitter}}" target="_blank">
<span class="fa fa-twitter fa-2x"></span> <span class="fa fa-twitter fa-2x"></span>
</a> </a>
</button> </button>
<button class="icon"> <button *ngIf="person.website" class="icon">
<a *ngIf="person.website" href="{{person.website}}" target="_blank"> <a href="{{person.website}}" target="_blank">
<span class="fa fa-link fa-2x"></span> <span class="fa fa-link fa-2x"></span>
</a> </a>
</button> </button>
@ -31,16 +31,16 @@ import { CONTENT_URL_PREFIX } from 'app/documents/document.service';
<div *ngIf="!person.picture" class="contributor-image" [style.background-image]="'url('+pictureBase+noPicture+')'"> <div *ngIf="!person.picture" class="contributor-image" [style.background-image]="'url('+pictureBase+noPicture+')'">
<div class="contributor-info"> <div class="contributor-info">
<button> <button *ngIf="person.bio">
<a *ngIf="person.bio" aria-label="View Bio">View Bio</a> <a aria-label="View Bio">View Bio</a>
</button> </button>
<button class="icon"> <button *ngIf="person.twitter" class="icon">
<a *ngIf="person.twitter" href="https://twitter.com/{{person.twitter}}" target="_blank"> <a href="https://twitter.com/{{person.twitter}}" target="_blank">
<span class="fa fa-twitter fa-2x"></span> <span class="fa fa-twitter fa-2x"></span>
</a> </a>
</button> </button>
<button class="icon"> <button *ngIf="person.website" class="icon">
<a *ngIf="person.website" href="{{person.website}}" target="_blank"> <a href="{{person.website}}" target="_blank">
<span class="fa fa-link fa-2x"></span> <span class="fa fa-link fa-2x"></span>
</a> </a>
</button> </button>

View File

@ -11,6 +11,7 @@ import { PrettyPrinter } from './code/pretty-printer.service';
// Reusable components (used inside embedded components) // Reusable components (used inside embedded components)
import { MdIconModule, MdTabsModule } from '@angular/material'; import { MdIconModule, MdTabsModule } from '@angular/material';
import { CodeComponent } from './code/code.component'; import { CodeComponent } from './code/code.component';
import { SharedModule } from 'app/shared/shared.module';
// Embedded Components // Embedded Components
import { ApiListComponent } from './api/api-list.component'; import { ApiListComponent } from './api/api-list.component';
@ -41,7 +42,8 @@ export class EmbeddedComponents {
imports: [ imports: [
CommonModule, CommonModule,
MdIconModule, MdIconModule,
MdTabsModule MdTabsModule,
SharedModule
], ],
declarations: [ declarations: [
embeddedComponents, embeddedComponents,

View File

@ -13,7 +13,7 @@
<md-icon class="rotating-icon" svgIcon="keyboard_arrow_right" [class.collapsed]="isCollapsed"></md-icon> <md-icon class="rotating-icon" svgIcon="keyboard_arrow_right" [class.collapsed]="isCollapsed"></md-icon>
</button> </button>
<ul class="toc-list"> <ul class="toc-list" [class.embedded]="type !== 'Floating'">
<ng-container *ngFor="let toc of tocList; let i = index"> <ng-container *ngFor="let toc of tocList; let i = index">
<li #tocItem title="{{toc.title}}" *ngIf="type === 'Floating' || toc.level !== 'h1'" <li #tocItem title="{{toc.title}}" *ngIf="type === 'Floating' || toc.level !== 'h1'"
class="{{toc.level}}" [class.secondary]="type === 'EmbeddedExpandable' && i >= primaryMax" [class.active]="i === activeIndex"> class="{{toc.level}}" [class.secondary]="type === 'EmbeddedExpandable' && i >= primaryMax" [class.active]="i === activeIndex">

View File

@ -16,9 +16,13 @@ export class NavItemComponent implements OnChanges {
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (changes['selectedNodes'] || changes['node']) { if (changes['selectedNodes'] || changes['node']) {
const ix = this.selectedNodes.indexOf(this.node); if (this.selectedNodes) {
this.isSelected = ix !== -1; const ix = this.selectedNodes.indexOf(this.node);
if (ix !== 0) { this.isExpanded = this.isSelected; } this.isSelected = ix !== -1;
if (ix !== 0) { this.isExpanded = this.isSelected; }
} else {
this.isSelected = false;
}
} }
this.setClasses(); this.setClasses();
} }

View File

@ -4,7 +4,7 @@ import { CurrentNode, NavigationNode } from 'app/navigation/navigation.service';
@Component({ @Component({
selector: 'aio-nav-menu', selector: 'aio-nav-menu',
template: ` template: `
<aio-nav-item *ngFor="let node of filteredNodes" [node]="node" [selectedNodes]="currentNode.nodes"> <aio-nav-item *ngFor="let node of filteredNodes" [node]="node" [selectedNodes]="currentNode?.nodes">
</aio-nav-item>` </aio-nav-item>`
}) })
export class NavMenuComponent { export class NavMenuComponent {

View File

@ -19,7 +19,7 @@ export interface NavigationViews {
/** /**
* Navigation information about a node at specific URL * Navigation information about a node at specific URL
* url: the current URL * url: the current URL
* view: 'SideNav' | 'TopBar' | 'Footer' * view: 'SideNav' | 'TopBar' | 'Footer' | etc
* nodes: the current node and its ancestor nodes within that view * nodes: the current node and its ancestor nodes within that view
*/ */
export interface CurrentNode { export interface CurrentNode {
@ -28,6 +28,15 @@ export interface CurrentNode {
nodes: NavigationNode[]; nodes: NavigationNode[];
} }
/**
* A map of current nodes by view.
* This is needed because some urls map to nodes in more than one view.
* If a view does not contain a node that matches the current url then the value will be undefined.
*/
export interface CurrentNodes {
[view: string]: CurrentNode;
}
export interface VersionInfo { export interface VersionInfo {
raw: string; raw: string;
major: number; major: number;

View File

@ -1,7 +1,7 @@
import { ReflectiveInjector } from '@angular/core'; import { ReflectiveInjector } from '@angular/core';
import { Http, ConnectionBackend, RequestOptions, BaseRequestOptions, Response, ResponseOptions } from '@angular/http'; import { Http, ConnectionBackend, RequestOptions, BaseRequestOptions, Response, ResponseOptions } from '@angular/http';
import { MockBackend } from '@angular/http/testing'; import { MockBackend } from '@angular/http/testing';
import { CurrentNode, NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service'; import { CurrentNodes, NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
import { LocationService } from 'app/shared/location.service'; import { LocationService } from 'app/shared/location.service';
import { MockLocationService } from 'testing/location.service'; import { MockLocationService } from 'testing/location.service';
import { Logger } from 'app/shared/logger.service'; import { Logger } from 'app/shared/logger.service';
@ -113,7 +113,7 @@ describe('NavigationService', () => {
}); });
describe('currentNode', () => { describe('currentNode', () => {
let currentNode: CurrentNode; let currentNodes: CurrentNodes;
let locationService: MockLocationService; let locationService: MockLocationService;
const topBarNodes: NavigationNode[] = [ const topBarNodes: NavigationNode[] = [
@ -138,80 +138,92 @@ describe('NavigationService', () => {
beforeEach(() => { beforeEach(() => {
locationService = injector.get(LocationService); locationService = injector.get(LocationService);
navService.currentNode.subscribe(selected => currentNode = selected); navService.currentNodes.subscribe(selected => currentNodes = selected);
backend.connectionsArray[0].mockRespond(createResponse(navJson)); backend.connectionsArray[0].mockRespond(createResponse(navJson));
}); });
it('should list the side navigation node that matches the current location, and all its ancestors', () => { it('should list the side navigation node that matches the current location, and all its ancestors', () => {
locationService.go('b'); locationService.go('b');
expect(currentNode).toEqual({ expect(currentNodes).toEqual({
url: 'b', SideNav: {
view: 'SideNav', url: 'b',
nodes: [ view: 'SideNav',
sideNavNodes[0].children[0], nodes: [
sideNavNodes[0] sideNavNodes[0].children[0],
] sideNavNodes[0]
]
}
}); });
locationService.go('d'); locationService.go('d');
expect(currentNode).toEqual({ expect(currentNodes).toEqual({
url: 'd', SideNav: {
view: 'SideNav', url: 'd',
nodes: [ view: 'SideNav',
sideNavNodes[0].children[0].children[1], nodes: [
sideNavNodes[0].children[0], sideNavNodes[0].children[0].children[1],
sideNavNodes[0] sideNavNodes[0].children[0],
] sideNavNodes[0]
]
}
}); });
locationService.go('f'); locationService.go('f');
expect(currentNode).toEqual({ expect(currentNodes).toEqual({
url: 'f', SideNav: {
view: 'SideNav', url: 'f',
nodes: [ sideNavNodes[1] ] view: 'SideNav',
nodes: [ sideNavNodes[1] ]
}
}); });
}); });
it('should be a TopBar selected node if the current location is a top menu node', () => { it('should be a TopBar selected node if the current location is a top menu node', () => {
locationService.go('features'); locationService.go('features');
expect(currentNode).toEqual({ expect(currentNodes).toEqual({
url: 'features', TopBar: {
view: 'TopBar', url: 'features',
nodes: [ topBarNodes[0] ] view: 'TopBar',
nodes: [ topBarNodes[0] ]
}
}); });
}); });
it('should be a plain object if no side navigation node matches the current location', () => { it('should be a plain object if no navigation node matches the current location', () => {
locationService.go('g?search=moo#anchor-1'); locationService.go('g?search=moo#anchor-1');
expect(currentNode).toEqual({ expect(currentNodes).toEqual({
url: 'g', '': {
view: '', url: 'g',
nodes: [] view: '',
nodes: []
}
}); });
}); });
it('should ignore trailing slashes, hashes, and search params on URLs in the navmap', () => { it('should ignore trailing slashes, hashes, and search params on URLs in the navmap', () => {
const cnode: CurrentNode = { const cnode: CurrentNodes = {
url: 'c', SideNav: {
view: 'SideNav', url: 'c',
nodes: [ view: 'SideNav',
sideNavNodes[0].children[0].children[0], nodes: [
sideNavNodes[0].children[0], sideNavNodes[0].children[0].children[0],
sideNavNodes[0] sideNavNodes[0].children[0],
] sideNavNodes[0]
]
}
}; };
locationService.go('c'); locationService.go('c');
expect(currentNode).toEqual(cnode, 'location: c'); expect(currentNodes).toEqual(cnode, 'location: c');
locationService.go('c#foo'); locationService.go('c#foo');
expect(currentNode).toEqual(cnode, 'location: c#foo'); expect(currentNodes).toEqual(cnode, 'location: c#foo');
locationService.go('c?foo=1'); locationService.go('c?foo=1');
expect(currentNode).toEqual(cnode, 'location: c?foo=1'); expect(currentNodes).toEqual(cnode, 'location: c?foo=1');
locationService.go('c#foo?bar=1&baz=2'); locationService.go('c#foo?bar=1&baz=2');
expect(currentNode).toEqual(cnode, 'location: c#foo?bar=1&baz=2'); expect(currentNodes).toEqual(cnode, 'location: c#foo?bar=1&baz=2');
}); });
}); });

View File

@ -13,8 +13,8 @@ import { LocationService } from 'app/shared/location.service';
import { CONTENT_URL_PREFIX } from 'app/documents/document.service'; import { CONTENT_URL_PREFIX } from 'app/documents/document.service';
// Import and re-export the Navigation model types // Import and re-export the Navigation model types
import { CurrentNode, NavigationNode, NavigationResponse, NavigationViews, VersionInfo } from './navigation.model'; import { CurrentNodes, NavigationNode, NavigationResponse, NavigationViews, VersionInfo } from './navigation.model';
export { CurrentNode, NavigationNode, NavigationResponse, NavigationViews, VersionInfo } from './navigation.model'; export { CurrentNodes, CurrentNode, NavigationNode, NavigationResponse, NavigationViews, VersionInfo } from './navigation.model';
const navigationPath = CONTENT_URL_PREFIX + 'navigation.json'; const navigationPath = CONTENT_URL_PREFIX + 'navigation.json';
@ -35,13 +35,13 @@ export class NavigationService {
* node (if any) that matches the current URL location * node (if any) that matches the current URL location
* including its navigation view and its ancestor nodes in that view * including its navigation view and its ancestor nodes in that view
*/ */
currentNode: Observable<CurrentNode>; currentNodes: Observable<CurrentNodes>;
constructor(private http: Http, private location: LocationService, private logger: Logger) { constructor(private http: Http, private location: LocationService, private logger: Logger) {
const navigationInfo = this.fetchNavigationInfo(); const navigationInfo = this.fetchNavigationInfo();
this.navigationViews = this.getNavigationViews(navigationInfo); this.navigationViews = this.getNavigationViews(navigationInfo);
this.currentNode = this.getCurrentNode(this.navigationViews); this.currentNodes = this.getCurrentNodes(this.navigationViews);
// The version information is packaged inside the navigation response to save us an extra request. // The version information is packaged inside the navigation response to save us an extra request.
this.versionInfo = this.getVersionInfo(navigationInfo); this.versionInfo = this.getVersionInfo(navigationInfo);
} }
@ -88,23 +88,23 @@ export class NavigationService {
} }
/** /**
* Get an observable of the current node (the one that matches the current URL) * Get an observable of the current nodes (the ones that match the current URL)
* We use `publishReplay(1)` because otherwise subscribers will have to wait until the next * We use `publishReplay(1)` because otherwise subscribers will have to wait until the next
* URL change before they receive an emission. * URL change before they receive an emission.
* See above for discussion of using `connect`. * See above for discussion of using `connect`.
*/ */
private getCurrentNode(navigationViews: Observable<NavigationViews>): Observable<CurrentNode> { private getCurrentNodes(navigationViews: Observable<NavigationViews>): Observable<CurrentNodes> {
const currentNode = combineLatest( const currentNodes = combineLatest(
navigationViews.map(views => this.computeUrlToNavNodesMap(views)), navigationViews.map(views => this.computeUrlToNavNodesMap(views)),
this.location.currentPath, this.location.currentPath,
(navMap, url) => { (navMap, url) => {
const urlKey = url.startsWith('api/') ? 'api' : url; const urlKey = url.startsWith('api/') ? 'api' : url;
return navMap[urlKey] || { view: '', url: urlKey, nodes: [] }; return navMap[urlKey] || { '' : { view: '', url: urlKey, nodes: [] }};
}) })
.publishReplay(1); .publishReplay(1);
currentNode.connect(); currentNodes.connect();
return currentNode; return currentNodes;
} }
/** /**
@ -114,7 +114,7 @@ export class NavigationService {
* @param navigation - A collection of navigation nodes that are to be mapped * @param navigation - A collection of navigation nodes that are to be mapped
*/ */
private computeUrlToNavNodesMap(navigation: NavigationViews) { private computeUrlToNavNodesMap(navigation: NavigationViews) {
const navMap = new Map<string, CurrentNode>(); const navMap = new Map<string, CurrentNodes>();
Object.keys(navigation) Object.keys(navigation)
.forEach(view => navigation[view] .forEach(view => navigation[view]
.forEach(node => this.walkNodes(view, navMap, node))); .forEach(node => this.walkNodes(view, navMap, node)));
@ -138,7 +138,7 @@ export class NavigationService {
* patching them and computing their ancestor nodes * patching them and computing their ancestor nodes
*/ */
private walkNodes( private walkNodes(
view: string, navMap: Map<string, CurrentNode>, view: string, navMap: Map<string, CurrentNodes>,
node: NavigationNode, ancestors: NavigationNode[] = []) { node: NavigationNode, ancestors: NavigationNode[] = []) {
const nodes = [node, ...ancestors]; const nodes = [node, ...ancestors];
const url = node.url; const url = node.url;
@ -147,7 +147,9 @@ export class NavigationService {
// only map to this node if it has a url // only map to this node if it has a url
if (url) { if (url) {
// Strip off trailing slashes from nodes in the navMap - they are not relevant to matching // Strip off trailing slashes from nodes in the navMap - they are not relevant to matching
navMap[url.replace(/\/$/, '')] = { url, view, nodes }; const cleanedUrl = url.replace(/\/$/, '');
const navMapItem = navMap[cleanedUrl] = navMap[cleanedUrl] || {};
navMapItem[view] = { url, view, nodes };
} }
if (node.children) { if (node.children) {

View File

@ -5,7 +5,7 @@
<ng-template #searchResults> <ng-template #searchResults>
<h2 class="visually-hidden">Search Results</h2> <h2 class="visually-hidden">Search Results</h2>
<div class="search-area" *ngFor="let area of searchAreas"> <div class="search-area" *ngFor="let area of searchAreas">
<h3>{{area.name}} ({{area.pages.length}})</h3> <h3>{{area.name}} ({{area.pages.length + area.priorityPages.length}})</h3>
<ul class="priority-pages" > <ul class="priority-pages" >
<li class="search-page" *ngFor="let page of area.priorityPages"> <li class="search-page" *ngFor="let page of area.priorityPages">
<a class="search-result-item" href="{{ page.path }}" (click)="onResultSelected(page)"> <a class="search-result-item" href="{{ page.path }}" (click)="onResultSelected(page)">
@ -13,7 +13,6 @@
</a> </a>
</li> </li>
</ul> </ul>
<div class="more-items material-icons" *ngIf="area.priorityPages.length > 0">more_horiz</div>
<ul> <ul>
<li class="search-page" *ngFor="let page of area.pages"> <li class="search-page" *ngFor="let page of area.pages">
<a class="search-result-item" href="{{ page.path }}" (click)="onResultSelected(page)"> <a class="search-result-item" href="{{ page.path }}" (click)="onResultSelected(page)">

View File

@ -33,6 +33,11 @@ describe('SearchResultsComponent', () => {
return take === undefined ? results : results.slice(0, take); return take === undefined ? results : results.slice(0, take);
} }
function compareTitle(l: {title: string}, r: {title: string}) {
return l.title.toUpperCase() > r.title.toUpperCase() ? 1 : -1;
}
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ SearchResultsComponent ], declarations: [ SearchResultsComponent ],
@ -54,13 +59,13 @@ describe('SearchResultsComponent', () => {
searchResults.next({ query: '', results: results}); searchResults.next({ query: '', results: results});
expect(component.searchAreas).toEqual([ expect(component.searchAreas).toEqual([
{ name: 'api', pages: [ { name: 'api', priorityPages: [
{ path: 'api/d', title: 'API D', type: '', keywords: '', titleWords: '' } { path: 'api/d', title: 'API D', type: '', keywords: '', titleWords: '' }
], priorityPages: [] }, ], pages: [] },
{ name: 'guide', pages: [ { name: 'guide', priorityPages: [
{ path: 'guide/a', title: 'Guide A', type: '', keywords: '', titleWords: '' }, { path: 'guide/a', title: 'Guide A', type: '', keywords: '', titleWords: '' },
{ path: 'guide/b', title: 'Guide B', type: '', keywords: '', titleWords: '' }, { path: 'guide/b', title: 'Guide B', type: '', keywords: '', titleWords: '' },
], priorityPages: [] } ], pages: [] }
]); ]);
}); });
@ -70,48 +75,35 @@ describe('SearchResultsComponent', () => {
{ path: 'tutorial/toh-pt1', title: 'Tutorial - part 1', type: '', keywords: '', titleWords: '' }, { path: 'tutorial/toh-pt1', title: 'Tutorial - part 1', type: '', keywords: '', titleWords: '' },
]}); ]});
expect(component.searchAreas).toEqual([ expect(component.searchAreas).toEqual([
{ name: 'tutorial', pages: [ { name: 'tutorial', priorityPages: [
{ path: 'tutorial/toh-pt1', title: 'Tutorial - part 1', type: '', keywords: '', titleWords: '' },
{ path: 'tutorial', title: 'Tutorial index', type: '', keywords: '', titleWords: '' }, { path: 'tutorial', title: 'Tutorial index', type: '', keywords: '', titleWords: '' },
], priorityPages: [] } { path: 'tutorial/toh-pt1', title: 'Tutorial - part 1', type: '', keywords: '', titleWords: '' },
], pages: [] }
]); ]);
}); });
it('should sort by title within sorted area', () => { it('should put first 5 results for each area into priorityPages', () => {
const results = getTestResults(5);
searchResults.next({ query: '', results: results });
expect(component.searchAreas).toEqual([
{ name: 'api', pages: [
{ path: 'api/c', title: 'API C', type: '', keywords: '', titleWords: '' },
{ path: 'api/d', title: 'API D', type: '', keywords: '', titleWords: '' },
], priorityPages: [] },
{ name: 'guide', pages: [
{ path: 'guide/a', title: 'Guide A', type: '', keywords: '', titleWords: '' },
{ path: 'guide/a/c', title: 'Guide A - C', type: '', keywords: '', titleWords: '' },
{ path: 'guide/b', title: 'Guide B', type: '', keywords: '', titleWords: '' },
], priorityPages: [] }
]);
});
it('should put first 5 area results into priorityPages when more than 10 pages', () => {
const results = getTestResults(); const results = getTestResults();
const sorted = results.slice().sort((l, r) => l.title > r.title ? 1 : -1);
const expected = [
{
name: 'api',
pages: sorted.filter(p => p.path.startsWith('api')),
priorityPages: []
},
{
name: 'guide',
pages: sorted.filter(p => p.path.startsWith('guide')),
priorityPages: results.filter(p => p.path.startsWith('guide')).slice(0, 5)
}
];
searchResults.next({ query: '', results: results }); searchResults.next({ query: '', results: results });
expect(component.searchAreas).toEqual(expected); expect(component.searchAreas[0].priorityPages).toEqual(results.filter(p => p.path.startsWith('api')).slice(0, 5));
expect(component.searchAreas[1].priorityPages).toEqual(results.filter(p => p.path.startsWith('guide')).slice(0, 5));
});
it('should put the nonPriorityPages into the pages array, sorted by title', () => {
const results = getTestResults();
searchResults.next({ query: '', results: results });
expect(component.searchAreas[0].pages).toEqual([]);
expect(component.searchAreas[1].pages).toEqual(results.filter(p => p.path.startsWith('guide')).slice(5).sort(compareTitle));
});
it('should put a total count in the header of each area of search results', () => {
const results = getTestResults();
searchResults.next({ query: '', results: results });
fixture.detectChanges();
const headers = fixture.debugElement.queryAll(By.css('h3'));
expect(headers.length).toEqual(2);
expect(headers[0].nativeElement.textContent).toContain('(2)');
expect(headers[1].nativeElement.textContent).toContain('(13)');
}); });
it('should put search results with no containing folder into the default area (other)', () => { it('should put search results with no containing folder into the default area (other)', () => {
@ -121,9 +113,9 @@ describe('SearchResultsComponent', () => {
searchResults.next({ query: '', results: results }); searchResults.next({ query: '', results: results });
expect(component.searchAreas).toEqual([ expect(component.searchAreas).toEqual([
{ name: 'other', pages: [ { name: 'other', priorityPages: [
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '' } { path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '' }
], priorityPages: [] } ], pages: [] }
]); ]);
}); });

View File

@ -60,8 +60,10 @@ export class SearchResultsComponent implements OnInit, OnDestroy {
}); });
const keys = Object.keys(searchAreaMap).sort((l, r) => l > r ? 1 : -1); const keys = Object.keys(searchAreaMap).sort((l, r) => l > r ? 1 : -1);
return keys.map(name => { return keys.map(name => {
let pages = searchAreaMap[name]; let pages: SearchResult[] = searchAreaMap[name];
const priorityPages = pages.length > 10 ? searchAreaMap[name].slice(0, 5) : [];
// Extract the top 5 most relevant results as priorityPages
const priorityPages = pages.splice(0, 5);
pages = pages.sort(compareResults); pages = pages.sort(compareResults);
return { name, pages, priorityPages }; return { name, pages, priorityPages };
}); });

View File

@ -300,6 +300,7 @@ describe('ScrollSpyService', () => {
}); });
describe('window resize events', () => { describe('window resize events', () => {
const RESIZE_EVENT_DELAY = 300;
let onResizeSpy: jasmine.Spy; let onResizeSpy: jasmine.Spy;
beforeEach(() => { beforeEach(() => {
@ -317,7 +318,7 @@ describe('ScrollSpyService', () => {
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
expect(onResizeSpy).not.toHaveBeenCalled(); expect(onResizeSpy).not.toHaveBeenCalled();
tick(300); tick(RESIZE_EVENT_DELAY);
expect(onResizeSpy).toHaveBeenCalled(); expect(onResizeSpy).toHaveBeenCalled();
})); }));
@ -327,58 +328,59 @@ describe('ScrollSpyService', () => {
onResizeSpy.calls.reset(); onResizeSpy.calls.reset();
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
tick(300); tick(RESIZE_EVENT_DELAY);
expect(onResizeSpy).toHaveBeenCalled(); expect(onResizeSpy).toHaveBeenCalled();
info1.unspy(); info1.unspy();
onResizeSpy.calls.reset(); onResizeSpy.calls.reset();
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
tick(300); tick(RESIZE_EVENT_DELAY);
expect(onResizeSpy).toHaveBeenCalled(); expect(onResizeSpy).toHaveBeenCalled();
info2.unspy(); info2.unspy();
onResizeSpy.calls.reset(); onResizeSpy.calls.reset();
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
tick(300); tick(RESIZE_EVENT_DELAY);
expect(onResizeSpy).not.toHaveBeenCalled(); expect(onResizeSpy).not.toHaveBeenCalled();
})); }));
it('should only fire every 300ms', fakeAsync(() => { it(`should only fire every ${RESIZE_EVENT_DELAY}ms`, fakeAsync(() => {
scrollSpyService.spyOn([]); scrollSpyService.spyOn([]);
onResizeSpy.calls.reset(); onResizeSpy.calls.reset();
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
tick(100); tick(RESIZE_EVENT_DELAY - 2);
expect(onResizeSpy).not.toHaveBeenCalled(); expect(onResizeSpy).not.toHaveBeenCalled();
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
tick(100); tick(1);
expect(onResizeSpy).not.toHaveBeenCalled(); expect(onResizeSpy).not.toHaveBeenCalled();
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
tick(100); tick(1);
expect(onResizeSpy).toHaveBeenCalledTimes(1); expect(onResizeSpy).toHaveBeenCalledTimes(1);
onResizeSpy.calls.reset(); onResizeSpy.calls.reset();
tick(150); tick(RESIZE_EVENT_DELAY / 2);
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
tick(100); tick(RESIZE_EVENT_DELAY - 2);
expect(onResizeSpy).not.toHaveBeenCalled(); expect(onResizeSpy).not.toHaveBeenCalled();
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
tick(100); tick(1);
expect(onResizeSpy).not.toHaveBeenCalled(); expect(onResizeSpy).not.toHaveBeenCalled();
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
tick(100); tick(1);
expect(onResizeSpy).toHaveBeenCalledTimes(1); expect(onResizeSpy).toHaveBeenCalledTimes(1);
})); }));
}); });
describe('window scroll events', () => { describe('window scroll events', () => {
const SCROLL_EVENT_DELAY = 10;
let onScrollSpy: jasmine.Spy; let onScrollSpy: jasmine.Spy;
beforeEach(() => { beforeEach(() => {
@ -395,7 +397,7 @@ describe('ScrollSpyService', () => {
window.dispatchEvent(new Event('scroll')); window.dispatchEvent(new Event('scroll'));
expect(onScrollSpy).not.toHaveBeenCalled(); expect(onScrollSpy).not.toHaveBeenCalled();
tick(300); tick(SCROLL_EVENT_DELAY);
expect(onScrollSpy).toHaveBeenCalled(); expect(onScrollSpy).toHaveBeenCalled();
})); }));
@ -404,52 +406,52 @@ describe('ScrollSpyService', () => {
const info2 = scrollSpyService.spyOn([]); const info2 = scrollSpyService.spyOn([]);
window.dispatchEvent(new Event('scroll')); window.dispatchEvent(new Event('scroll'));
tick(300); tick(SCROLL_EVENT_DELAY);
expect(onScrollSpy).toHaveBeenCalled(); expect(onScrollSpy).toHaveBeenCalled();
info1.unspy(); info1.unspy();
onScrollSpy.calls.reset(); onScrollSpy.calls.reset();
window.dispatchEvent(new Event('scroll')); window.dispatchEvent(new Event('scroll'));
tick(300); tick(SCROLL_EVENT_DELAY);
expect(onScrollSpy).toHaveBeenCalled(); expect(onScrollSpy).toHaveBeenCalled();
info2.unspy(); info2.unspy();
onScrollSpy.calls.reset(); onScrollSpy.calls.reset();
window.dispatchEvent(new Event('scroll')); window.dispatchEvent(new Event('scroll'));
tick(300); tick(SCROLL_EVENT_DELAY);
expect(onScrollSpy).not.toHaveBeenCalled(); expect(onScrollSpy).not.toHaveBeenCalled();
})); }));
it('should only fire every 300ms', fakeAsync(() => { it(`should only fire every ${SCROLL_EVENT_DELAY}ms`, fakeAsync(() => {
scrollSpyService.spyOn([]); scrollSpyService.spyOn([]);
window.dispatchEvent(new Event('scroll')); window.dispatchEvent(new Event('scroll'));
tick(100); tick(SCROLL_EVENT_DELAY - 2);
expect(onScrollSpy).not.toHaveBeenCalled(); expect(onScrollSpy).not.toHaveBeenCalled();
window.dispatchEvent(new Event('scroll')); window.dispatchEvent(new Event('scroll'));
tick(100); tick(1);
expect(onScrollSpy).not.toHaveBeenCalled(); expect(onScrollSpy).not.toHaveBeenCalled();
window.dispatchEvent(new Event('scroll')); window.dispatchEvent(new Event('scroll'));
tick(100); tick(1);
expect(onScrollSpy).toHaveBeenCalledTimes(1); expect(onScrollSpy).toHaveBeenCalledTimes(1);
onScrollSpy.calls.reset(); onScrollSpy.calls.reset();
tick(150); tick(SCROLL_EVENT_DELAY / 2);
window.dispatchEvent(new Event('scroll')); window.dispatchEvent(new Event('scroll'));
tick(100); tick(SCROLL_EVENT_DELAY - 2);
expect(onScrollSpy).not.toHaveBeenCalled(); expect(onScrollSpy).not.toHaveBeenCalled();
window.dispatchEvent(new Event('scroll')); window.dispatchEvent(new Event('scroll'));
tick(100); tick(1);
expect(onScrollSpy).not.toHaveBeenCalled(); expect(onScrollSpy).not.toHaveBeenCalled();
window.dispatchEvent(new Event('scroll')); window.dispatchEvent(new Event('scroll'));
tick(100); tick(1);
expect(onScrollSpy).toHaveBeenCalledTimes(1); expect(onScrollSpy).toHaveBeenCalledTimes(1);
})); }));
}); });

View File

@ -124,7 +124,7 @@ export class ScrollSpyService {
private spiedElementGroups: ScrollSpiedElementGroup[] = []; private spiedElementGroups: ScrollSpiedElementGroup[] = [];
private onStopListening = new Subject(); private onStopListening = new Subject();
private resizeEvents = Observable.fromEvent(window, 'resize').auditTime(300).takeUntil(this.onStopListening); private resizeEvents = Observable.fromEvent(window, 'resize').auditTime(300).takeUntil(this.onStopListening);
private scrollEvents = Observable.fromEvent(window, 'scroll').auditTime(300).takeUntil(this.onStopListening); private scrollEvents = Observable.fromEvent(window, 'scroll').auditTime(10).takeUntil(this.onStopListening);
private lastContentHeight: number; private lastContentHeight: number;
private lastMaxScrollTop: number; private lastMaxScrollTop: number;

View File

@ -5,6 +5,7 @@ import { DOCUMENT } from '@angular/platform-browser';
import { ScrollService, topMargin } from './scroll.service'; import { ScrollService, topMargin } from './scroll.service';
describe('ScrollService', () => { describe('ScrollService', () => {
const topOfPageElem = {} as Element;
let injector: ReflectiveInjector; let injector: ReflectiveInjector;
let document: MockDocument; let document: MockDocument;
let location: MockPlatformLocation; let location: MockPlatformLocation;
@ -16,7 +17,8 @@ describe('ScrollService', () => {
class MockDocument { class MockDocument {
body = new MockElement(); body = new MockElement();
getElementById = jasmine.createSpy('Document getElementById'); getElementById = jasmine.createSpy('Document getElementById').and.returnValue(topOfPageElem);
querySelector = jasmine.createSpy('Document querySelector');
} }
class MockElement { class MockElement {
@ -38,6 +40,69 @@ describe('ScrollService', () => {
scrollService = injector.get(ScrollService); scrollService = injector.get(ScrollService);
}); });
describe('#topOffset', () => {
it('should query for the top-bar by CSS selector', () => {
expect(document.querySelector).not.toHaveBeenCalled();
expect(scrollService.topOffset).toBe(topMargin);
expect(document.querySelector).toHaveBeenCalled();
});
it('should be calculated based on the top-bar\'s height + margin', () => {
(document.querySelector as jasmine.Spy).and.returnValue({clientHeight: 50});
expect(scrollService.topOffset).toBe(50 + topMargin);
});
it('should only query for the top-bar once', () => {
expect(scrollService.topOffset).toBe(topMargin);
(document.querySelector as jasmine.Spy).calls.reset();
expect(scrollService.topOffset).toBe(topMargin);
expect(document.querySelector).not.toHaveBeenCalled();
});
it('should retrieve the top-bar\'s height again after resize', () => {
let clientHeight = 50;
(document.querySelector as jasmine.Spy).and.callFake(() => ({clientHeight}));
expect(scrollService.topOffset).toBe(50 + topMargin);
expect(document.querySelector).toHaveBeenCalled();
(document.querySelector as jasmine.Spy).calls.reset();
clientHeight = 100;
expect(scrollService.topOffset).toBe(50 + topMargin);
expect(document.querySelector).not.toHaveBeenCalled();
window.dispatchEvent(new Event('resize'));
expect(scrollService.topOffset).toBe(100 + topMargin);
expect(document.querySelector).toHaveBeenCalled();
});
});
describe('#topOfPageElement', () => {
it('should query for the top-of-page element by ID', () => {
expect(document.getElementById).not.toHaveBeenCalled();
expect(scrollService.topOfPageElement).toBe(topOfPageElem);
expect(document.getElementById).toHaveBeenCalled();
});
it('should only query for the top-of-page element once', () => {
expect(scrollService.topOfPageElement).toBe(topOfPageElem);
(document.getElementById as jasmine.Spy).calls.reset();
expect(scrollService.topOfPageElement).toBe(topOfPageElem);
expect(document.getElementById).not.toHaveBeenCalled();
});
it('should return `<body>` if unable to find the top-of-page element', () => {
(document.getElementById as jasmine.Spy).and.returnValue(null);
expect(scrollService.topOfPageElement).toBe(document.body as any);
});
});
describe('#scroll', () => { describe('#scroll', () => {
it('should scroll to the top if there is no hash', () => { it('should scroll to the top if there is no hash', () => {
location.hash = ''; location.hash = '';
@ -73,10 +138,28 @@ describe('ScrollService', () => {
describe('#scrollToElement', () => { describe('#scrollToElement', () => {
it('should scroll to element', () => { it('should scroll to element', () => {
const element = <Element><any> new MockElement(); const element: Element = new MockElement() as any;
scrollService.scrollToElement(element); scrollService.scrollToElement(element);
expect(element.scrollIntoView).toHaveBeenCalled(); expect(element.scrollIntoView).toHaveBeenCalled();
expect(window.scrollBy).toHaveBeenCalledWith(0, -topMargin); expect(window.scrollBy).toHaveBeenCalledWith(0, -scrollService.topOffset);
});
it('should scroll all the way to the top if close enough', () => {
const element: Element = new MockElement() as any;
(window as any).pageYOffset = 25;
scrollService.scrollToElement(element);
expect(element.scrollIntoView).toHaveBeenCalled();
expect(window.scrollBy).toHaveBeenCalledWith(0, -scrollService.topOffset);
(window.scrollBy as jasmine.Spy).calls.reset();
(window as any).pageYOffset = 15;
scrollService.scrollToElement(element);
expect(element.scrollIntoView).toHaveBeenCalled();
expect(window.scrollBy).toHaveBeenCalledWith(0, -scrollService.topOffset);
expect(window.scrollBy).toHaveBeenCalledWith(0, -15);
}); });
it('should do nothing if no element', () => { it('should do nothing if no element', () => {

View File

@ -1,6 +1,7 @@
import { Injectable, Inject } from '@angular/core'; import { Injectable, Inject } from '@angular/core';
import { PlatformLocation } from '@angular/common'; import { PlatformLocation } from '@angular/common';
import { DOCUMENT } from '@angular/platform-browser'; import { DOCUMENT } from '@angular/platform-browser';
import {fromEvent} from 'rxjs/observable/fromEvent';
export const topMargin = 16; export const topMargin = 16;
/** /**
@ -9,20 +10,20 @@ export const topMargin = 16;
@Injectable() @Injectable()
export class ScrollService { export class ScrollService {
private _topOffset: number; private _topOffset: number | null;
private _topOfPageElement: Element; private _topOfPageElement: Element;
// Offset from the top of the document to bottom of any static elements // Offset from the top of the document to bottom of any static elements
// at the top (e.g. toolbar) + some margin // at the top (e.g. toolbar) + some margin
get topOffset() { get topOffset() {
if (!this._topOffset) { if (!this._topOffset) {
// Since the toolbar is not static, we don't need to account for its height. const toolbar = this.document.querySelector('md-toolbar.app-toolbar');
this._topOffset = topMargin; this._topOffset = (toolbar && toolbar.clientHeight || 0) + topMargin;
} }
return this._topOffset; return this._topOffset;
} }
private get topOfPageElement() { get topOfPageElement() {
if (!this._topOfPageElement) { if (!this._topOfPageElement) {
this._topOfPageElement = this.document.getElementById('top-of-page') || this.document.body; this._topOfPageElement = this.document.getElementById('top-of-page') || this.document.body;
} }
@ -31,7 +32,10 @@ export class ScrollService {
constructor( constructor(
@Inject(DOCUMENT) private document: any, @Inject(DOCUMENT) private document: any,
private location: PlatformLocation) { } private location: PlatformLocation) {
// On resize, the toolbar might change height, so "invalidate" the top offset.
fromEvent(window, 'resize').subscribe(() => this._topOffset = null);
}
/** /**
* Scroll to the element with id extracted from the current location hash fragment. * Scroll to the element with id extracted from the current location hash fragment.
@ -53,7 +57,14 @@ export class ScrollService {
scrollToElement(element: Element) { scrollToElement(element: Element) {
if (element) { if (element) {
element.scrollIntoView(); element.scrollIntoView();
if (window && window.scrollBy) { window.scrollBy(0, -this.topOffset); } if (window && window.scrollBy) {
window.scrollBy(0, -this.topOffset);
if (window.pageYOffset < 20) {
// If we are very close to the top (<20px), then scroll all the way up.
// (This can happen if `element` is at the top of the page, but has a small top-margin.)
window.scrollBy(0, -window.pageYOffset);
}
}
} }
} }

View File

@ -0,0 +1,16 @@
<div class="form-select-menu">
<button class="form-select-button" (click)="toggleOptions()">
<strong>{{label}}</strong><span *ngIf="showSymbol" class="symbol {{selected?.value}}"></span>{{selected?.title}}
</button>
<ul class="form-select-dropdown" *ngIf="showOptions">
<li *ngFor="let option of options; index as i"
[class.selected]="option === selected"
role="button"
tabindex="0"
(click)="select(option, i)"
(keydown.enter)="select(option, i)"
(keydown.space)="select(option, i); $event.preventDefault()">
<span *ngIf="showSymbol" class="symbol {{option.value}}"></span>{{option.title}}
</li>
</ul>
</div>

View File

@ -0,0 +1,165 @@
import { Component, DebugElement } from '@angular/core';
import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { SelectComponent, Option } from './select.component';
const options = [
{ title: 'Option A', value: 'option-a' },
{ title: 'Option B', value: 'option-b' }
];
let component: SelectComponent;
let host: HostComponent;
let fixture: ComponentFixture<HostComponent>;
let element: DebugElement;
describe('SelectComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ SelectComponent, HostComponent ],
});
});
beforeEach(() => {
fixture = TestBed.createComponent(HostComponent);
host = fixture.componentInstance;
element = fixture.debugElement.query(By.directive(SelectComponent));
component = element.componentInstance;
});
describe('(initially)', () => {
it('should show the button and no options', () => {
expect(getButton()).toBeDefined();
expect(getOptionContainer()).toEqual(null);
});
});
describe('button', () => {
it('should display the label if provided', () => {
expect(getButton().textContent.trim()).toEqual('');
host.label = 'Label:';
fixture.detectChanges();
expect(getButton().textContent.trim()).toEqual('Label:');
});
it('should contain a symbol `<span>` if hasSymbol is true', () => {
expect(getButton().querySelector('span')).toEqual(null);
host.showSymbol = true;
fixture.detectChanges();
const span = getButton().querySelector('span');
expect(span).not.toEqual(null);
expect(span.className).toContain('symbol');
});
it('should display the selected option, if there is one', () => {
host.showSymbol = true;
host.selected = options[0];
fixture.detectChanges();
expect(getButton().textContent).toContain(options[0].title);
expect(getButton().querySelector('span').className).toContain(options[0].value);
});
it('should toggle the visibility of the options list when clicked', () => {
host.options = options;
getButton().click();
fixture.detectChanges();
expect(getOptionContainer()).not.toEqual(null);
getButton().click();
fixture.detectChanges();
expect(getOptionContainer()).toEqual(null);
});
});
describe('options list', () => {
beforeEach(() => {
host.options = options;
host.showSymbol = true;
getButton().click(); // ensure the the options are visible
fixture.detectChanges();
});
it('should show the corresponding title of each option', () => {
getOptions().forEach((li, index) => {
expect(li.textContent).toContain(options[index].title);
});
});
it('should select the option that is clicked', () => {
getOptions()[0].click();
fixture.detectChanges();
expect(host.onChange).toHaveBeenCalledWith({ option: options[0], index: 0 });
expect(getButton().textContent).toContain(options[0].title);
expect(getButton().querySelector('span').className).toContain(options[0].value);
});
it('should select the current option when enter is pressed', () => {
const e = new KeyboardEvent('keydown', {bubbles: true, cancelable: true, key: 'Enter'});
getOptions()[0].dispatchEvent(e);
fixture.detectChanges();
expect(host.onChange).toHaveBeenCalledWith({ option: options[0], index: 0 });
expect(getButton().textContent).toContain(options[0].title);
expect(getButton().querySelector('span').className).toContain(options[0].value);
});
it('should select the current option when space is pressed', () => {
const e = new KeyboardEvent('keydown', {bubbles: true, cancelable: true, key: ' '});
getOptions()[0].dispatchEvent(e);
fixture.detectChanges();
expect(host.onChange).toHaveBeenCalledWith({ option: options[0], index: 0 });
expect(getButton().textContent).toContain(options[0].title);
expect(getButton().querySelector('span').className).toContain(options[0].value);
});
it('should hide when an option is clicked', () => {
getOptions()[0].click();
fixture.detectChanges();
expect(getOptionContainer()).toEqual(null);
});
it('should hide when there is a click that is not on the option list', () => {
fixture.nativeElement.click();
fixture.detectChanges();
expect(getOptionContainer()).toEqual(null);
});
it('should hide if the escape button is pressed', () => {
const e = new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: 'Escape' });
document.dispatchEvent(e);
fixture.detectChanges();
expect(getOptionContainer()).toEqual(null);
});
});
});
@Component({
template: `
<aio-select (change)="onChange($event)"
[options]="options"
[selected]="selected"
[label]="label"
[showSymbol]="showSymbol">
</aio-select>`
})
class HostComponent {
onChange = jasmine.createSpy('onChange');
options: Option[];
selected: Option;
label: string;
showSymbol: boolean;
}
function getButton(): HTMLButtonElement {
return element.query(By.css('button')).nativeElement;
}
function getOptionContainer(): HTMLUListElement {
const de = element.query(By.css('ul'));
return de && de.nativeElement;
}
function getOptions(): HTMLLIElement[] {
return element.queryAll(By.css('li')).map(de => de.nativeElement);
}

View File

@ -0,0 +1,62 @@
import { Component, ElementRef, EventEmitter, HostListener, Input, Output, OnInit } from '@angular/core';
export interface Option {
title: string;
value?: any;
}
@Component({
selector: 'aio-select',
templateUrl: 'select.component.html'
})
export class SelectComponent implements OnInit {
@Input()
selected: Option;
@Input()
options: Option[];
@Output()
change = new EventEmitter<{option: Option, index: number}>();
@Input()
showSymbol = false;
@Input()
label: string;
showOptions = false;
constructor(private hostElement: ElementRef) {}
ngOnInit() {
this.label = this.label || '';
}
toggleOptions() {
this.showOptions = !this.showOptions;
}
hideOptions() {
this.showOptions = false;
}
select(option: Option, index: number) {
this.selected = option;
this.change.emit({option, index});
this.hideOptions();
}
@HostListener('document:click', ['$event.target'])
onClick(eventTarget: HTMLElement) {
// Hide the options if we clicked outside the component
if (!this.hostElement.nativeElement.contains(eventTarget)) {
this.hideOptions();
}
}
@HostListener('document:keydown.escape')
onKeyDown() {
this.hideOptions();
}
}

View File

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SelectComponent } from './select/select.component';
@NgModule({
imports: [
CommonModule
],
exports: [
SelectComponent
],
declarations: [
SelectComponent
]
})
export class SharedModule {}

View File

@ -8,7 +8,7 @@ body {
} }
h1 { h1 {
display:inline-block; display: inline-block;
font-size: 24px; font-size: 24px;
font-weight: 500; font-weight: 500;
margin: 8px 0px; margin: 8px 0px;

View File

@ -7,11 +7,7 @@ aio-shell.page-docs {
.sidenav-content { .sidenav-content {
min-height: 100vh; min-height: 100vh;
padding: 6rem 3rem 1rem; padding: 80px 3rem 1rem;
}
.fill-remaining-space {
flex: 1 1 auto;
} }
@media (max-width: 600px) { @media (max-width: 600px) {

View File

@ -57,7 +57,7 @@ section#intro {
button { button {
margin: 0; margin: 0;
display: inline-block; height: 60px;
} }
} }
@ -146,16 +146,10 @@ section#intro {
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
height: 30px; height: 40px;
justify-content: center; justify-content: center;
&:hover { &:hover {
.material-icons {
display: inline-block;
color: $white;
opacity: 0.7;
}
a { a {
color: $white; color: $white;
opacity: 0.7; opacity: 0.7;
@ -247,7 +241,8 @@ section#intro {
button.hero-cta { button.hero-cta {
border-radius: 48px; border-radius: 48px;
display: block; display: flex;
height: 40px;
box-shadow: 0 2px 5px 0 rgba(0,0,0,.26); box-shadow: 0 2px 5px 0 rgba(0,0,0,.26);
cursor: pointer; cursor: pointer;
@ -263,7 +258,7 @@ button.hero-cta {
} }
aio-shell { aio-shell {
&.page-resources, &.page-events { &.page-resources, &.page-events, &.page-features, &.page-presskit, &.page-contribute {
section { section {
padding: 0rem 0rem 3rem; padding: 0rem 0rem 3rem;
} }
@ -275,12 +270,12 @@ aio-shell {
} }
} }
&.page-home, &.page-resources, &.page-events { &.page-home, &.page-resources, &.page-events, &.page-contribute {
article { article {
padding: 32px; padding: 32px;
@media (max-width: 800px) { @media (max-width: 800px) {
padding: auto; padding: 24px;
} }
} }
} }
@ -302,16 +297,6 @@ aio-shell {
padding-top: 0; padding-top: 0;
} }
} }
@media (min-width: 992px) {
md-toolbar {
padding-left: 24px;
button.hamburger {
display: none;
}
}
}
} }
.cta-bar .hero-cta { .cta-bar .hero-cta {
@ -319,6 +304,11 @@ aio-shell {
} }
} }
.cta-bar.announcement-bar {
background: none;
box-shadow: none;
}
.text-headline { .text-headline {
font-size: 20px; font-size: 20px;
margin-top: 10px; margin-top: 10px;
@ -455,6 +445,9 @@ header.bckground-sky.l-relative {
font-size: 24px; font-size: 24px;
font-weight: 300; font-weight: 300;
color: white; color: white;
margin: 0;
-webkit-margin-before: 0;
-webkit-margin-after: 0;
@media (max-width: 600px) { @media (max-width: 600px) {
font-size: 18px; font-size: 18px;
@ -466,3 +459,6 @@ header.bckground-sky.l-relative {
} }
} }
} }
.page-features .marketing-banner {
margin-bottom: 20px;
}

View File

@ -25,15 +25,17 @@ aio-nav-menu {
md-sidenav.mat-sidenav.sidenav { md-sidenav.mat-sidenav.sidenav {
position: fixed; position: fixed;
top: 64px;
bottom: 0; bottom: 0;
left: 0; left: 0;
padding: 80px 0px 0px; padding: 0;
min-width: 260px; min-width: 260px;
background-color: $offwhite; background-color: $offwhite;
box-shadow: 6px 0 6px rgba(0,0,0,0.10); box-shadow: 6px 0 6px rgba(0,0,0,0.10);
height: calc(100vh - 64px);
&.collapsed { &.collapsed {
padding-top: 64px; top: 56px;
} }
} }
@ -102,7 +104,7 @@ button.vertical-menu-item {
} }
.heading { .heading {
color: $blue-grey-700; color: $darkgray;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
text-transform: uppercase; text-transform: uppercase;

View File

@ -1,17 +1,3 @@
.fill-remaining-space {
flex: 1 1 auto;
}
aio-top-menu a.nav-link {
margin: 0;
padding: 24px 16px;
cursor: pointer;
&:focus {
outline: none;
}
}
.nav-link.home img { .nav-link.home img {
position: relative; position: relative;
margin-top: -21px; margin-top: -21px;
@ -20,22 +6,6 @@ aio-top-menu a.nav-link {
height: 40px; height: 40px;
} }
@media (max-width: 700px) {
.nav-link {
font-size: 80%;
margin-right: 8px;
margin-left: 0px;
}
}
@media (max-width: 600px) {
.nav-link {
font-size: 80%;
margin-right: 8px;
margin-left: 0px;
}
}
aio-top-menu { aio-top-menu {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -65,60 +35,102 @@ aio-top-menu {
} }
} }
} }
}
aio-shell.page-home md-toolbar.app-toolbar.mat-toolbar { a.nav-link {
background-color: transparent; margin: 0;
} padding: 24px 16px;
cursor: pointer;
md-toolbar.mat-toolbar { &:focus {
position: absolute; outline: none;
perspective: 2000px; background: rgba($white, 0.15);
top: 0px; border-radius: 4px;
right: 0; padding: 8px 16px;
left: 0;
z-index: 10;
padding: 0 16px 0 0;
.mat-toolbar-row {
@media (max-width: 600px) {
} }
} }
} }
aio-shell.sidenav-open { // HOME PAGE OVERRIDE: TOPNAV TOOLBAR HAMBURGER MENU
md-toolbar.mat-toolbar md-icon { aio-shell.page-home md-toolbar.app-toolbar.mat-toolbar {
color: $mediumgray; background-color: transparent;
transition: background-color .2s linear .3s;
@media (max-width: 480px) {
background-color: $blue;
} }
} }
.search-container { // DOCS PAGE / STANDARD: TOPNAV TOOLBAR FIXED
md-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);
md-icon {
color: $white;
}
}
// MARKETING PAGES OVERRIDE: TOPNAV TOOLBAR AND HAMBURGER
aio-shell.page-home md-toolbar.mat-toolbar,
aio-shell.page-features md-toolbar.mat-toolbar,
aio-shell.page-events md-toolbar.mat-toolbar,
aio-shell.page-resources md-toolbar.mat-toolbar {
// FIXED TOPNAV TOOLBAR FOR SMALL MOBILE
@media (min-width: 481px) {
position: absolute;
}
@media (min-width: 992px) {
padding-left: 24px;
button.hamburger {
display: none;
}
}
}
// REMOVE BOX SHADOW ON CERTAIN MARKETING PAGES
aio-shell.page-home md-toolbar.mat-toolbar,
aio-shell.page-events md-toolbar.mat-toolbar,
aio-shell.page-resources md-toolbar.mat-toolbar {
box-shadow: none;
}
// SEARCH BOX
aio-search-box.search-container {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
width: 100%; width: 100%;
min-width: 150px; min-width: 150px;
height: 100%; height: 100%;
}
aio-search-box input { input {
color: $darkgray; color: $darkgray;
border: none; border: none;
border-radius: 100px; border-radius: 100px;
background-color: $offwhite; background-color: $offwhite;
padding: 5px 16px; padding: 5px 16px;
margin-left: 8px; margin-left: 8px;
width: 150px; width: 180px;
height: 40%; max-width: 240px;
height: 50%;
@include bp(big) { @include bp(big) {
transition: width 0.4s ease-in-out; transition: width 0.4s ease-in-out;
&:focus {
width: 50%; &:focus {
max-width: 240px; width: 50%;
}
}
@media (max-width: 480px) {
width: 150px;
} }
} }
@media (max-width: 480px) {
width: 180px;
}
} }

View File

@ -3,6 +3,9 @@
margin: 24px 0px; margin: 24px 0px;
font-size: 14px; font-size: 14px;
color: $darkgray; color: $darkgray;
width: 100%;
box-sizing: border-box;
&.is-critical { &.is-critical {
border-left: 10px solid $brightred; border-left: 10px solid $brightred;
background-color: rgba($brightred, 0.05); background-color: rgba($brightred, 0.05);

View File

@ -37,7 +37,7 @@ aio-api-list > div {
margin: 16px auto; margin: 16px auto;
} }
> div { .form-select-menu, .form-search {
margin: 8px; margin: 8px;
} }
} }
@ -115,82 +115,6 @@ $tablet-breakpoint: 800px;
} }
} }
/* SELECT MENU */
$form-select-width: 200px;
.form-select-menu {
position: relative;
}
.form-select-button {
background: $white;
box-shadow: 0 2px 2px rgba($black, 0.24), 0 0 2px rgba($black, 0.12);
box-sizing: border-box;
border: 1px solid $white;
color: $blue-grey-600;
font-size: 12px;
font-weight: 400;
height: 32px;
line-height: 32px;
outline: none;
padding: 0 16px;
text-align: left;
width: $form-select-width - 16px;
strong {
font-weight: 600;
margin-right: 8px;
text-transform: uppercase;
}
&.has-symbol {
.symbol {
margin-right: 8px;
}
}
}
.form-select-dropdown {
background: $white;
box-shadow: 0 16px 16px rgba($black, 0.24), 0 0 16px rgba($black, 0.12);
border-radius: 4px;
left: -8px;
list-style-type: none;
margin: 0;
padding: 8px 0;
position: absolute;
top: -8px;
width: 200px;
z-index: $layer-2;
li {
cursor: pointer;
font-size: 14px;
line-height: 32px;
margin: 0;
padding: 0 16px 0 40px;
position: relative;
transition: all .2s;
&:hover {
background: $blue-grey-50;
color: $blue-500;
}
&.selected {
background-color: $blue-grey-100;
}
.symbol {
left: 16px;
position: absolute;
top: 8px;
z-index: $layer-5;
}
}
}
/* API SYMBOLS */ /* API SYMBOLS */
/* SYMBOL CLASS */ /* SYMBOL CLASS */
@ -224,8 +148,12 @@ $form-select-width: 200px;
/* API FILTER MENU */ /* API FILTER MENU */
.api-filter { .api-filter {
.form-select-menu { aio-select {
float: left; width: 200px;
.symbol {
margin-right: 8px;
}
} }
.form-search { .form-search {
@ -237,21 +165,22 @@ $form-select-width: 200px;
.docs-content .api-list { .docs-content .api-list {
list-style: none; list-style: none;
margin: 0 0 48px 0; margin: 0 0 48px -8px;
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
margin: 0; margin: 0 0 0 -8px;
} }
li { li {
font-size: 14px; font-size: 14px;
margin: 0 0 16px 0; margin: 8px;
line-height: 14px; line-height: 14px;
padding: 0; padding: 0;
float: left; float: left;
width: 33%; width: 33%;
overflow: hidden;
min-width: 220px; min-width: 220px;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -356,11 +285,6 @@ p {
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
// Override highlight.js
.kwd {
color: #1E88E5 !important;
}
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
} }

View File

@ -69,40 +69,26 @@ code-tabs mat-tab-body-content .fadeIn {
aio-code pre { aio-code pre {
display: flex; display: flex;
min-height: 32px; min-height: 32px;
margin: 16px; margin: 16px 32px;
white-space: pre-wrap; white-space: pre-wrap;
align-items: center; align-items: center;
code span, code ol li { code span {
line-height: 24px; line-height: 24px;
} }
} }
code ol {
margin: 0;
font-family: $main-font;
color: $lightgray;
li {
margin: 0;
font-family: $code-font;
font-size: 90%;
}
}
.code-missing { .code-missing {
color: $darkred; color: $darkred;
} }
.prettyprint {
position: relative;
}
.copy-button { .copy-button {
display: inline-block; display: inline-block;
position: absolute; position: absolute;
top: 6px; top: -8px;
right: 8px; right: -32px;
color: $blue-grey-200; color: $blue-grey-200;
background-color: transparent; background-color: transparent;
@ -111,11 +97,6 @@ code ol {
&:hover { &:hover {
color: $mediumgray; color: $mediumgray;
} }
@media (max-width: 480px) {
top: 0;
right: 0;
}
} }
.lang-sh .copy-button, .lang-bash .copy-button { .lang-sh .copy-button, .lang-bash .copy-button {
@ -130,102 +111,6 @@ code ol {
transform: none; transform: none;
} }
/* SCREEN COLORS */
.pnk,
.blk {
border-radius: 4px;
padding: 2px 4px;
}
.pnk {
background: $blue-grey-50;
color: $blue-grey-900;
}
.blk {
background: $blue-grey-900;
}
.otl {
outline: 1px solid rgba($blue-grey-700, .56);
}
.kwd {
color: $darkgray;
}
.typ,
.tag {
color: $darkred;
}
.str,
.atv {
color: $blue;
}
.atn {
color: $blue;
}
.com {
color: $mediumgray;
}
.lit {
color: $blue;
}
.pun {
color: $blue-grey-700;
}
.pln {
color: $green-800;
}
.dec {
color: $blue;
}
/* SHELL / TERMINAL CODE BLOCKS */
code-example.code-shell, code-example[language=sh], code-example[language=bash] {
& .pnk, .blk,.pln, .otl, .kwd, .typ, .tag, .str, .atv, .atn, .com, .lit, .pun, .dec {
color: $codegreen;
}
}
/* PRINT COLORS */
@media print {
border: none;
box-shadow: none;
ol {
background: $white;
}
.kwd {
color: $darkgray;
}
.typ,
.tag {
color: $darkred;
}
.str,
.atv {
color: $blue;
}
.atn {
color: $blue;
}
.com {
color: $mediumgray;
}
.lit {
color: $blue;
}
.pun {
color: $blue-grey-700;
}
.pln {
color: $green-800;
}
.dec {
color: $blue;
}
}
// REMOVE RIPPLE EFFECT FROM MATERIAL TABS // REMOVE RIPPLE EFFECT FROM MATERIAL TABS
code-tabs md-tab-group *.mat-ripple-element, code-tabs md-tab-group *.mat-tab-body-active, code-tabs md-tab-group *.mat-tab-body-content, code-tabs md-tab-group *.mat-tab-body-content { code-tabs md-tab-group *.mat-ripple-element, code-tabs md-tab-group *.mat-tab-body-active, code-tabs md-tab-group *.mat-tab-body-content, code-tabs md-tab-group *.mat-tab-body-content {
@ -240,3 +125,63 @@ code-tabs md-tab-group *.mat-ripple-element, code-tabs md-tab-group *.mat-tab-bo
color: inherit; color: inherit;
font-size: inherit; font-size: inherit;
} }
/* PRETTY PRINTING STYLES for prettify.js. */
.prettyprint {
position: relative;
}
/* Specify class=linenums on a pre to get line numbering */
ol.linenums {
margin: 0;
font-family: $main-font;
color: #B3B6B7;
li {
margin: 0;
font-family: $code-font;
font-size: 90%;
line-height: 24px;
}
}
/* The following class|color styles are derived from https://github.com/google/code-prettify/blob/master/src/prettify.css*/
/* SPAN elements with the classes below are added by prettyprint. */
.pln { color: #000 } /* plain text */
@media screen {
.str { color: #800 } /* string content */
.kwd { color: #00f } /* a keyword */
.com { color: #060 } /* a comment */
.typ { color: red } /* a type name */
.lit { color: #08c } /* a literal value */
/* punctuation, lisp open bracket, lisp close bracket */
.pun, .opn, .clo { color: #660 }
.tag { color: #008 } /* a markup tag name */
.atn { color: #606 } /* a markup attribute name */
.atv { color: #800 } /* a markup attribute value */
.dec, .var { color: #606 } /* a declaration; a variable name */
.fun { color: red } /* a function name */
}
/* Use higher contrast and text-weight for printable form. */
@media print, projection {
.str { color: #060 }
.kwd { color: #006; font-weight: bold }
.com { color: #600; font-style: italic }
.typ { color: #404; font-weight: bold }
.lit { color: #044 }
.pun, .opn, .clo { color: #440 }
.tag { color: #006; font-weight: bold }
.atn { color: #404 }
.atv { color: #060 }
}
/* SHELL / TERMINAL CODE BLOCKS */
code-example.code-shell, code-example[language=sh], code-example[language=bash] {
& .pnk, .blk,.pln, .otl, .kwd, .typ, .tag, .str, .atv, .atn, .com, .lit, .pun, .dec {
color: $codegreen;
}
}

View File

@ -0,0 +1,14 @@
.contribute-container {
h2 {
margin: 0;
}
.l-sub-section {
width: 90%;
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
}

View File

@ -59,8 +59,8 @@ aio-contributor {
.contributor-info { .contributor-info {
background: rgba($darkgray, 0.5); background: rgba($darkgray, 0.5);
height: 200px; height: 168px;
width: 200px; width: 168px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
@ -94,7 +94,7 @@ aio-contributor {
} }
&:hover { &:hover {
color: $blue; color: $lightgray;
} }
} }
} }
@ -112,6 +112,10 @@ aio-contributor {
transform-style:preserve-3d; transform-style:preserve-3d;
transition:transform ease 500ms; transition:transform ease 500ms;
h3 {
margin: 8px 0;
}
.card-front, .card-back { .card-front, .card-back {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -159,8 +163,8 @@ aio-contributor {
justify-content: center; justify-content: center;
border-radius: 50%; border-radius: 50%;
align-items: center; align-items: center;
height: 200px; height: 168px;
width: 200px; width: 168px;
background-size: cover; background-size: cover;
background-position: center; background-position: center;
margin: 8px auto; margin: 8px auto;

View File

@ -1,6 +1,10 @@
.sidenav-content { .sidenav-content {
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
&.no-anchor .header-link {
display: none;
}
.mat-icon, .material-icons { .mat-icon, .material-icons {
visibility: hidden; visibility: hidden;
display: inline-block; display: inline-block;

View File

@ -10,6 +10,7 @@
@import 'callout'; @import 'callout';
@import 'card'; @import 'card';
@import 'code'; @import 'code';
@import 'contribute';
@import 'contributor'; @import 'contributor';
@import 'edit-page-cta'; @import 'edit-page-cta';
@import 'features'; @import 'features';
@ -26,3 +27,4 @@
@import 'search-results'; @import 'search-results';
@import 'subsection'; @import 'subsection';
@import 'toc'; @import 'toc';
@import 'select-menu';

View File

@ -1,4 +1,6 @@
.presskit-container { .presskit-container {
padding: 0 32px 32px 32px;
h2 { h2 {
color: #37474F; color: #37474F;
} }
@ -71,4 +73,12 @@
} }
} }
} }
.presskit-row:first-child {
margin-top: 0;
@media(max-width: 599px) {
margin-top: 48px;
}
}
} }

View File

@ -2,11 +2,7 @@
height: 2px; height: 2px;
overflow: hidden; overflow: hidden;
position: fixed; position: fixed;
top: 64px; top: 0;
width: 100vw; width: 100vw;
z-index: 5; z-index: 11;
@media (max-width: 600px) {
top: 56px;
}
} }

View File

@ -55,7 +55,7 @@ aio-search-results {
font-size: 14px; font-size: 14px;
color: $lightgray; color: $lightgray;
text-decoration: none; text-decoration: none;
font-weight: 300; font-weight: normal;
&:hover { &:hover {
color: $white; color: $white;
} }
@ -68,11 +68,11 @@ aio-search-results {
} }
} }
.more-items { .priority-pages {
content: 'more_horiz'; padding: 0.5rem 0;
font-size: 20px; a {
color: $mediumgray; font-weight: bold;
padding: 0; }
} }
@include bp(tiny) { @include bp(tiny) {

View File

@ -0,0 +1,72 @@
/* SELECT MENU */
.form-select-menu {
position: relative;
}
.form-select-button {
background: $white;
box-shadow: 0 2px 2px rgba($black, 0.24), 0 0 2px rgba($black, 0.12);
box-sizing: border-box;
border: 1px solid $white;
color: $blue-grey-600;
font-size: 12px;
font-weight: 400;
height: 32px;
line-height: 32px;
outline: none;
padding: 0 16px;
text-align: left;
width: 100%;
cursor: pointer;
strong {
font-weight: 600;
margin-right: 8px;
text-transform: uppercase;
}
&:focus {
border: 1px solid $blue-400;
box-shadow: 0 2px 2px rgba($blue-400, 0.24), 0 0 2px rgba($blue-400, 0.12);
}
}
.form-select-dropdown {
background: $white;
box-shadow: 0 16px 16px rgba($black, 0.24), 0 0 16px rgba($black, 0.12);
border-radius: 4px;
list-style-type: none;
margin: 0;
padding: 0;
position: absolute;
top: 0;
width: 100%;
z-index: $layer-2;
li {
cursor: pointer;
font-size: 14px;
line-height: 32px;
margin: 0;
padding: 0 16px 0 40px;
position: relative;
transition: all .2s;
&:hover {
background: $blue-grey-50;
color: $blue-500;
}
&.selected {
background-color: $blue-grey-100;
}
.symbol {
left: 16px;
position: absolute;
top: 8px;
z-index: $layer-5;
}
}
}

View File

@ -6,6 +6,8 @@
margin-bottom: 8px; margin-bottom: 8px;
display: table; display: table;
clear: both; clear: both;
width: 100%;
box-sizing: border-box;
h3 { h3 {
margin: 8px 0 0; margin: 8px 0 0;

View File

@ -109,59 +109,84 @@ aio-toc {
width: auto; width: auto;
} }
} li {
box-sizing: border-box;
ul.toc-list li {
box-sizing: border-box;
font-size: 12px;
line-height: 16px;
padding: 3px 0 3px 12px;
position: relative;
transition: all 0.3s ease-in-out;
&.h1:after {
content: '';
display: block;
height: 1px;
width: 40%;
margin: 7px 0 4px 0;
background: #DBDBDB;
clear: both;
}
&.h1, &.h2, &.h3 {
border-left: 1px solid $blue;
}
&.h3 {
padding-left: 24px;
}
a {
font-size: inherit;
color: lighten($darkgray, 10);
display:table-cell;
overflow: visible;
font-size: 12px; font-size: 12px;
display: table-cell; line-height: 16px;
padding: 3px 0 3px 12px;
position: relative;
transition: all 0.3s ease-in-out;
&:hover { &.h1:after {
content: '';
display: block;
height: 1px;
width: 40%;
margin: 7px 0 4px 0;
background: #DBDBDB;
clear: both;
}
&.h3 {
padding-left: 24px;
}
a {
font-size: inherit;
color: lighten($darkgray, 10);
display:table-cell;
overflow: visible;
font-size: 12px;
display: table-cell;
}
&:hover a {
color: $accentblue; color: $accentblue;
} }
&.active {
a {
color: $blue;
font-weight: 500;
&:before {
content: '';
border-radius: 50%;
left: -3px;
top: 12px;
background: $blue;
position: absolute;
width: 6px;
height: 6px;
}
}
}
} }
&.active { &:not(.embedded) li {
a { &:before {
color: $blue; border-left: 1px solid $lightgray;
font-weight: 500; bottom: 0;
content: '';
left: 0;
position: absolute;
top: 0;
} }
&:before { &:first-child:before {
top: 13px;
}
&:last-child:before {
bottom: calc(100% - 14px);
}
&:not(.active):hover a:before {
content: ''; content: '';
border-radius: 50%; border-radius: 50%;
left: -4px; left: -3px;
top: 12px; top: 12px;
background: $blue; background: $lightgray;
position: absolute; position: absolute;
width: 6px; width: 6px;
height: 6px; height: 6px;

View File

@ -22,7 +22,7 @@ $offwhite: #FAFAFA;
$backgroundgray: #F1F1F1; $backgroundgray: #F1F1F1;
$lightgray: #DBDBDB; $lightgray: #DBDBDB;
$mist: #ECEFF1; $mist: #ECEFF1;
$mediumgray: #7E7E7E; $mediumgray: #6e6e6e;
$darkgray: #333; $darkgray: #333;
$black: #0A1014; $black: #0A1014;
$orange: #FF9800; $orange: #FF9800;

View File

@ -1,11 +1,11 @@
{% macro renderMember(member) %}{% if not member.internal -%} {% macro renderMember(member) %}{% if not member.internal -%}
<a class="code-anchor" href="#{$ member.name $}">{$ member.name $}</a> {$ params.paramList(member.parameters) | indent(4, false) | trim() $}{$ params.returnType(member.returnType) $} <a class="code-anchor" href="#{$ member.name $}">{$ member.name $}</a>{$ params.paramList(member.parameters) | indent(4, false) | trim() $}{$ params.returnType(member.returnType) $}
{%- endif %}{% endmacro -%} {%- endif %}{% endmacro -%}
<section class="class-overview"> <section class="class-overview">
<h2>Overview</h2> <h2>Overview</h2>
<code-example language="ts" hideCopy="true"> <code-example language="ts" hideCopy="true">
{$ doc.docType $} {$ doc.name $} {$ doc.heritage $} { {$ doc.docType $} {$ doc.name $}{$ doc.heritage $} {
{%- if doc.statics.length %}{% for member in doc.statics %} {%- if doc.statics.length %}{% for member in doc.statics %}
static {$ renderMember(member) $}{% endfor %}{% endif %} static {$ renderMember(member) $}{% endfor %}{% endif %}
{%- if doc.constructorDoc %} {%- if doc.constructorDoc %}

View File

@ -1,10 +1,10 @@
<section class="interface-overview"> <section class="interface-overview">
<h2>Interface Overview</h2> <h2>Interface Overview</h2>
<code-example language="ts" hideCopy="true"> <code-example language="ts" hideCopy="true">
interface {$ doc.name $} {$ doc.heritage $} { {% if doc.newMember %} interface {$ doc.name $}{$ doc.heritage $} { {% if doc.newMember %}
<a class="code-anchor" href="#{$ doc.newMember.name $}">{$ doc.newMember.name | indent(6, false) | trim $}</a> {$ params.paramList(doc.newMember.parameters) | indent(8, false) | trim $}{$ params.returnType(doc.newMember.returnType) $}{% endif %}{% if doc.callMember %} <a class="code-anchor" href="#{$ doc.newMember.name $}">{$ doc.newMember.name | indent(6, false) | trim $}</a>{$ params.paramList(doc.newMember.parameters) | indent(8, false) | trim $}{$ params.returnType(doc.newMember.returnType) $}{% endif %}{% if doc.callMember %}
<a class="code-anchor" href="#{$ doc.callMember.name $}">{$ doc.callMember.name | indent(6, false) | trim $}</a> {$ params.paramList(doc.callMember.parameters) | indent(8, false) | trim $}{$ params.returnType(doc.callMember.returnType) $}{% endif %}{% if doc.members.length %}{% for member in doc.members %}{% if not member.internal %} <a class="code-anchor" href="#{$ doc.callMember.name $}">{$ doc.callMember.name | indent(6, false) | trim $}</a>{$ params.paramList(doc.callMember.parameters) | indent(8, false) | trim $}{$ params.returnType(doc.callMember.returnType) $}{% endif %}{% if doc.members.length %}{% for member in doc.members %}{% if not member.internal %}
<a class="code-anchor" href="#{$ member.name $}">{$ member.name | indent(6, false) | trim $}</a> {$ params.paramList(member.parameters) | indent(8, false) | trim $}{$ params.returnType(member.returnType) $}{% endif %}{% endfor %}{% endif %} <a class="code-anchor" href="#{$ member.name $}">{$ member.name | indent(6, false) | trim $}</a>{$ params.paramList(member.parameters) | indent(8, false) | trim $}{$ params.returnType(member.returnType) $}{% endif %}{% endfor %}{% endif %}
} }
</code-example> </code-example>
</section> </section>

View File

@ -1,19 +1,29 @@
{% if doc.members.length %} {% if doc.members.length or doc.newMember or doc.callMember %}
<section class="member-members"> <section class="member-members">
<h2>Members</h2> <h2>Members</h2>
{% if doc.newMember %}
<div class="new-member">
<a id="{$ doc.newMember.name $}"></a>
<code-example hideCopy="true">{$ doc.newMember.name $}{$ params.paramList(doc.newMember.parameters) | trim $}{$ params.returnType(doc.newMember.returnType) $}</code-example>
{% if not doc.newMember.notYetDocumented %}{$ doc.newMember.description | marked $}{% endif %}
</div>
{% if doc.members.length or doc.callMember %}<hr>{% endif %}
{% endif %}
{% if doc.callMember %}
<div class="call-member">
<a id="{$ doc.callMember.name $}"></a>
<code-example hideCopy="true">{$ doc.callMember.name $}{$ params.paramList(doc.callMember.parameters) | trim $}{$ params.returnType(doc.callMember.returnType) $}</code-example>
{% if not doc.callMember.notYetDocumented %}{$ doc.callMember.description | marked $}{% endif %}
</div>
{% if doc.members.length %}<hr>{% endif %}
{% endif %}
{% for member in doc.members %}{% if not member.internal %} {% for member in doc.members %}{% if not member.internal %}
<div class="instance-member"> <div class="instance-member">
<a id="{$ member.name $}"></a> <a id="{$ member.name $}"></a>
<code-example hideCopy="true">{$ member.name $}{$ params.paramList(member.parameters) | trim $}{$ params.returnType(member.returnType) $}</code-example> <code-example hideCopy="true">{$ member.name $}{$ params.paramList(member.parameters) | trim $}{$ params.returnType(member.returnType) $}</code-example>
{%- if not member.notYetDocumented %} {% if not member.notYetDocumented %}{$ member.description | marked $}{% endif %}
{$ member.description | marked $}
{% endif %}
</div> </div>
{% if not loop.last %}<hr>{% endif %}
{% if not loop.last %}
<hr>
{% endif %}
{% endif %}{% endfor %} {% endif %}{% endfor %}
</section> </section>
{% endif %} {% endif %}

View File

@ -4,6 +4,5 @@
{% block details %} {% block details %}
{% include "includes/interface-overview.html" %} {% include "includes/interface-overview.html" %}
{% include "includes/description.html" %} {% include "includes/description.html" %}
<!-- TODO include callMember and newMember -->
{% include "includes/members.html" %} {% include "includes/members.html" %}
{% endblock %} {% endblock %}

View File

@ -8,5 +8,5 @@
{% macro returnType(returnType) -%} {% macro returnType(returnType) -%}
{%- if returnType %} : {$ returnType | escape $}{% endif -%} {%- if returnType %}: {$ returnType | escape $}{% endif -%}
{%- endmacro -%} {%- endmacro -%}

View File

@ -4660,8 +4660,8 @@ ng-pwa-tools@^0.0.10:
ts-node "^3.0.2" ts-node "^3.0.2"
ngo-loader@alxhub/ngo: ngo-loader@alxhub/ngo:
version "0.0.5" version "0.0.6"
resolved "https://codeload.github.com/alxhub/ngo/tar.gz/2c0d731289bbbfeef558f3aaee0596f1a7d229ab" resolved "https://codeload.github.com/alxhub/ngo/tar.gz/4b589d8ffb2b6278c387fc492bf17fd42227508f"
dependencies: dependencies:
typescript "2.1" typescript "2.1"
@ -5607,8 +5607,8 @@ punycode@^1.2.4, punycode@^1.3.2, punycode@^1.4.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
purify@igorminar/purify: purify@igorminar/purify:
version "0.0.19" version "0.0.23"
resolved "https://codeload.github.com/igorminar/purify/tar.gz/cdf9c2959ffae1f02ed93e5031afe5a79e9d4d4c" resolved "https://codeload.github.com/igorminar/purify/tar.gz/98a5c8f39e3dc2954bf87062b5d91c5e4576a26e"
dependencies: dependencies:
tslib "^1.7.1" tslib "^1.7.1"

View File

@ -1,12 +0,0 @@
machine:
node:
version: 6.9.5
dependencies:
pre:
- npm install -g npm@3.10.7
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 0.21.3
test:
override:
- gulp lint

View File

@ -1,6 +1,6 @@
{ {
"name": "angular-srcs", "name": "angular-srcs",
"version": "4.2.0-rc.2", "version": "4.2.0",
"private": true, "private": true,
"branchPattern": "2.0.*", "branchPattern": "2.0.*",
"description": "Angular - a web framework for modern web apps", "description": "Angular - a web framework for modern web apps",
@ -10,7 +10,7 @@
"engines": { "engines": {
"node": ">=6.9.5 <7.0.0", "node": ">=6.9.5 <7.0.0",
"npm": ">=3.10.7 <4.0.0", "npm": ">=3.10.7 <4.0.0",
"yarn": ">=0.21.3 <0.22.0" "yarn": ">=0.21.3 <1.0.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -199,34 +199,35 @@ export class AnimationTimelineBuilderVisitor implements AstVisitor {
visitSequence(ast: SequenceAst, context: AnimationTimelineContext) { visitSequence(ast: SequenceAst, context: AnimationTimelineContext) {
const subContextCount = context.subContextCount; const subContextCount = context.subContextCount;
let ctx = context;
const options = ast.options; const options = ast.options;
if (options && (options.params || options.delay)) { if (options && (options.params || options.delay)) {
context.createSubContext(options); ctx = context.createSubContext(options);
context.transformIntoNewTimeline(); ctx.transformIntoNewTimeline();
if (options.delay != null) { if (options.delay != null) {
if (context.previousNode instanceof StyleAst) { if (ctx.previousNode instanceof StyleAst) {
context.currentTimeline.snapshotCurrentStyles(); ctx.currentTimeline.snapshotCurrentStyles();
context.previousNode = DEFAULT_NOOP_PREVIOUS_NODE; ctx.previousNode = DEFAULT_NOOP_PREVIOUS_NODE;
} }
const delay = resolveTimingValue(options.delay); const delay = resolveTimingValue(options.delay);
context.delayNextStep(delay); ctx.delayNextStep(delay);
} }
} }
if (ast.steps.length) { if (ast.steps.length) {
ast.steps.forEach(s => s.visit(this, context)); ast.steps.forEach(s => s.visit(this, ctx));
// this is here just incase the inner steps only contain or end with a style() call // this is here just incase the inner steps only contain or end with a style() call
context.currentTimeline.applyStylesToKeyframe(); ctx.currentTimeline.applyStylesToKeyframe();
// this means that some animation function within the sequence // this means that some animation function within the sequence
// ended up creating a sub timeline (which means the current // ended up creating a sub timeline (which means the current
// timeline cannot overlap with the contents of the sequence) // timeline cannot overlap with the contents of the sequence)
if (context.subContextCount > subContextCount) { if (ctx.subContextCount > subContextCount) {
context.transformIntoNewTimeline(); ctx.transformIntoNewTimeline();
} }
} }
@ -475,7 +476,7 @@ export class AnimationTimelineContext {
Object.keys(newParams).forEach(name => { Object.keys(newParams).forEach(name => {
if (!skipIfExists || !paramsToUpdate.hasOwnProperty(name)) { if (!skipIfExists || !paramsToUpdate.hasOwnProperty(name)) {
paramsToUpdate[name] = newParams[name]; paramsToUpdate[name] = interpolateParams(newParams[name], paramsToUpdate, this.errors);
} }
}); });
} }

View File

@ -35,7 +35,7 @@ export class WebAnimationsStyleNormalizer extends AnimationStyleNormalizer {
} }
const DIMENSIONAL_PROP_MAP = makeBooleanMap( const DIMENSIONAL_PROP_MAP = makeBooleanMap(
'width,height,minWidth,minHeight,maxWidth,maxHeight,left,top,bottom,right,fontSize,outlineWidth,outlineOffset,paddingTop,paddingLeft,paddingBottom,paddingRight,marginTop,marginLeft,marginBottom,marginRight,borderRadius,borderWidth,borderTopWidth,borderLeftWidth,borderRightWidth,borderBottomWidth,textIndent' 'width,height,minWidth,minHeight,maxWidth,maxHeight,left,top,bottom,right,fontSize,outlineWidth,outlineOffset,paddingTop,paddingLeft,paddingBottom,paddingRight,marginTop,marginLeft,marginBottom,marginRight,borderRadius,borderWidth,borderTopWidth,borderLeftWidth,borderRightWidth,borderBottomWidth,textIndent,perspective'
.split(',')); .split(','));
function makeBooleanMap(keys: string[]): {[key: string]: boolean} { function makeBooleanMap(keys: string[]): {[key: string]: boolean} {

View File

@ -13,12 +13,24 @@ import {AnimationTransitionInstruction} from '../dsl/animation_transition_instru
import {AnimationTrigger} from '../dsl/animation_trigger'; import {AnimationTrigger} from '../dsl/animation_trigger';
import {ElementInstructionMap} from '../dsl/element_instruction_map'; import {ElementInstructionMap} from '../dsl/element_instruction_map';
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer'; import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
import {ENTER_CLASSNAME, LEAVE_CLASSNAME, LEAVE_SELECTOR, NG_ANIMATING_CLASSNAME, NG_TRIGGER_CLASSNAME, NG_TRIGGER_SELECTOR, copyObj, eraseStyles, setStyles} from '../util'; import {ENTER_CLASSNAME, LEAVE_CLASSNAME, NG_ANIMATING_CLASSNAME, NG_TRIGGER_CLASSNAME, NG_TRIGGER_SELECTOR, copyObj, eraseStyles, setStyles} from '../util';
import {AnimationDriver} from './animation_driver'; import {AnimationDriver} from './animation_driver';
import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared'; import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared';
const EMPTY_PLAYER_ARRAY: AnimationPlayer[] = []; const EMPTY_PLAYER_ARRAY: AnimationPlayer[] = [];
const NULL_REMOVAL_STATE: ElementAnimationState = {
namespaceId: '',
setForRemoval: null,
hasAnimation: false,
removedBeforeQueried: false
};
const NULL_REMOVED_QUERIED_STATE: ElementAnimationState = {
namespaceId: '',
setForRemoval: null,
hasAnimation: false,
removedBeforeQueried: true
};
interface TriggerListener { interface TriggerListener {
name: string; name: string;
@ -36,6 +48,15 @@ export interface QueueInstruction {
isFallbackTransition: boolean; isFallbackTransition: boolean;
} }
export const REMOVAL_FLAG = '__ng_removed';
export interface ElementAnimationState {
setForRemoval: any;
hasAnimation: boolean;
namespaceId: string;
removedBeforeQueried: boolean;
}
export class StateValue { export class StateValue {
public value: string; public value: string;
public options: AnimationOptions; public options: AnimationOptions;
@ -244,7 +265,7 @@ export class AnimationTransitionNamespace {
}); });
} }
private _onElementDestroy(element: any) { clearElementCache(element: any) {
this._engine.statesByElement.delete(element); this._engine.statesByElement.delete(element);
this._elementListeners.delete(element); this._elementListeners.delete(element);
const elementPlayers = this._engine.playersByElement.get(element); const elementPlayers = this._engine.playersByElement.get(element);
@ -266,14 +287,13 @@ export class AnimationTransitionNamespace {
this.removeNode(elm, context, true); this.removeNode(elm, context, true);
} else { } else {
this._onElementDestroy(elm); this.clearElementCache(elm);
} }
}); });
} }
removeNode(element: any, context: any, doNotRecurse?: boolean): void { removeNode(element: any, context: any, doNotRecurse?: boolean): void {
const engine = this._engine; const engine = this._engine;
engine.markElementAsRemoved(element);
if (!doNotRecurse && element.childElementCount) { if (!doNotRecurse && element.childElementCount) {
this._destroyInnerNodes(element, context, true); this._destroyInnerNodes(element, context, true);
@ -294,12 +314,8 @@ export class AnimationTransitionNamespace {
}); });
if (players.length) { if (players.length) {
optimizeGroupPlayer(players).onDone(() => { engine.markElementAsRemoved(this.id, element, true, context);
engine.destroyInnerAnimations(element); optimizeGroupPlayer(players).onDone(() => engine.processLeaveNode(element));
this._onElementDestroy(element);
engine._onRemovalComplete(element, context);
});
return; return;
} }
} }
@ -364,15 +380,11 @@ export class AnimationTransitionNamespace {
// whether or not a parent has an animation we need to delay the deferral of the leave // whether or not a parent has an animation we need to delay the deferral of the leave
// operation until we have more information (which we do after flush() has been called) // operation until we have more information (which we do after flush() has been called)
if (containsPotentialParentTransition) { if (containsPotentialParentTransition) {
engine.queuedRemovals.set(element, () => { engine.markElementAsRemoved(this.id, element, false, context);
engine.destroyInnerAnimations(element);
this._onElementDestroy(element);
engine._onRemovalComplete(element, context);
});
} else { } else {
// we do this after the flush has occurred such // we do this after the flush has occurred such
// that the callbacks can be fired // that the callbacks can be fired
engine.afterFlush(() => this._onElementDestroy(element)); engine.afterFlush(() => this.clearElementCache(element));
engine.destroyInnerAnimations(element); engine.destroyInnerAnimations(element);
engine._onRemovalComplete(element, context); engine._onRemovalComplete(element, context);
} }
@ -446,7 +458,6 @@ export interface QueuedTransition {
export class TransitionAnimationEngine { export class TransitionAnimationEngine {
public players: TransitionAnimationPlayer[] = []; public players: TransitionAnimationPlayer[] = [];
public queuedRemovals = new Map<any, () => any>();
public newHostElements = new Map<any, AnimationTransitionNamespace>(); public newHostElements = new Map<any, AnimationTransitionNamespace>();
public playersByElement = new Map<any, TransitionAnimationPlayer[]>(); public playersByElement = new Map<any, TransitionAnimationPlayer[]>();
public playersByQueriedElement = new Map<any, TransitionAnimationPlayer[]>(); public playersByQueriedElement = new Map<any, TransitionAnimationPlayer[]>();
@ -460,7 +471,8 @@ export class TransitionAnimationEngine {
private _whenQuietFns: (() => any)[] = []; private _whenQuietFns: (() => any)[] = [];
public namespacesByHostElement = new Map<any, AnimationTransitionNamespace>(); public namespacesByHostElement = new Map<any, AnimationTransitionNamespace>();
public collectedElements: any[] = []; public collectedEnterElements: any[] = [];
public collectedLeaveElements: any[] = [];
// this method is designed to be overridden by the code that uses this engine // this method is designed to be overridden by the code that uses this engine
public onRemovalComplete = (element: any, context: any) => {}; public onRemovalComplete = (element: any, context: any) => {};
@ -496,7 +508,7 @@ export class TransitionAnimationEngine {
// animation renderer type. If this happens then we can still have // animation renderer type. If this happens then we can still have
// access to this item when we query for :enter nodes. If the parent // access to this item when we query for :enter nodes. If the parent
// is a renderer then the set data-structure will normalize the entry // is a renderer then the set data-structure will normalize the entry
this.updateElementEpoch(hostElement); this.collectEnterElement(hostElement);
} }
return this._namespaceLookup[namespaceId] = ns; return this._namespaceLookup[namespaceId] = ns;
} }
@ -571,9 +583,9 @@ export class TransitionAnimationEngine {
// special case for when an element is removed and reinserted (move operation) // special case for when an element is removed and reinserted (move operation)
// when this occurs we do not want to use the element for deletion later // when this occurs we do not want to use the element for deletion later
if (this.queuedRemovals.has(element)) { const details = element[REMOVAL_FLAG] as ElementAnimationState;
this.markElementAsRemoved(element, true); if (details && details.setForRemoval) {
this.queuedRemovals.delete(element); details.setForRemoval = false;
} }
// in the event that the namespaceId is blank then the caller // in the event that the namespaceId is blank then the caller
@ -585,33 +597,33 @@ export class TransitionAnimationEngine {
// only *directives and host elements are inserted before // only *directives and host elements are inserted before
if (insertBefore) { if (insertBefore) {
this.updateElementEpoch(element); this.collectEnterElement(element);
} }
} }
updateElementEpoch(element: any, isRemoval?: boolean) { this.collectedElements.push(element); } collectEnterElement(element: any) { this.collectedEnterElements.push(element); }
markElementAsRemoved(element: any, unmark?: boolean) {
if (unmark) {
removeClass(element, LEAVE_CLASSNAME);
} else {
addClass(element, LEAVE_CLASSNAME);
this.afterFlush(() => removeClass(element, LEAVE_CLASSNAME));
}
}
removeNode(namespaceId: string, element: any, context: any, doNotRecurse?: boolean): void { removeNode(namespaceId: string, element: any, context: any, doNotRecurse?: boolean): void {
if (namespaceId) { if (!isElementNode(element)) {
const ns = this._fetchNamespace(namespaceId); this._onRemovalComplete(element, context);
if (!isElementNode(element) || !ns) { return;
this._onRemovalComplete(element, context);
} else {
ns.removeNode(element, context, doNotRecurse);
}
} else {
this.markElementAsRemoved(element);
this.queuedRemovals.set(element, () => this._onRemovalComplete(element, context));
} }
const ns = namespaceId ? this._fetchNamespace(namespaceId) : null;
if (ns) {
ns.removeNode(element, context, doNotRecurse);
} else {
this.markElementAsRemoved(namespaceId, element, false, context);
}
}
markElementAsRemoved(namespaceId: string, element: any, hasAnimation?: boolean, context?: any) {
this.collectedLeaveElements.push(element);
element[REMOVAL_FLAG] = {
namespaceId,
setForRemoval: context, hasAnimation,
removedBeforeQueried: false
};
} }
listen( listen(
@ -661,6 +673,22 @@ export class TransitionAnimationEngine {
}); });
} }
processLeaveNode(element: any) {
const details = element[REMOVAL_FLAG] as ElementAnimationState;
if (details && details.setForRemoval) {
// this will prevent it from removing it twice
element[REMOVAL_FLAG] = NULL_REMOVAL_STATE;
if (details.namespaceId) {
this.destroyInnerAnimations(element);
const ns = this._fetchNamespace(details.namespaceId);
if (ns) {
ns.clearElementCache(element);
}
}
this._onRemovalComplete(element, details.setForRemoval);
}
}
flush(microtaskId: number = -1) { flush(microtaskId: number = -1) {
let players: AnimationPlayer[] = []; let players: AnimationPlayer[] = [];
if (this.newHostElements.size) { if (this.newHostElements.size) {
@ -668,15 +696,19 @@ export class TransitionAnimationEngine {
this.newHostElements.clear(); this.newHostElements.clear();
} }
if (this._namespaceList.length && (this.totalQueuedPlayers || this.queuedRemovals.size)) { if (this._namespaceList.length &&
(this.totalQueuedPlayers || this.collectedLeaveElements.length)) {
players = this._flushAnimations(microtaskId); players = this._flushAnimations(microtaskId);
} else { } else {
this.queuedRemovals.forEach(fn => fn()); for (let i = 0; i < this.collectedLeaveElements.length; i++) {
const element = this.collectedLeaveElements[i];
this.processLeaveNode(element);
}
} }
this.totalQueuedPlayers = 0; this.totalQueuedPlayers = 0;
this.collectedElements = []; this.collectedEnterElements.length = 0;
this.queuedRemovals.clear(); this.collectedLeaveElements.length = 0;
this._flushFns.forEach(fn => fn()); this._flushFns.forEach(fn => fn());
this._flushFns = []; this._flushFns = [];
@ -708,10 +740,22 @@ export class TransitionAnimationEngine {
// the :enter queries match the elements (since the timeline queries // the :enter queries match the elements (since the timeline queries
// are fired during instruction building). // are fired during instruction building).
const bodyNode = getBodyNode(); const bodyNode = getBodyNode();
const allEnterNodes: any[] = this.collectedElements; const allEnterNodes: any[] = this.collectedEnterElements;
const enterNodes: any[] = const enterNodes: any[] =
allEnterNodes.length ? collectEnterElements(this.driver, allEnterNodes) : []; allEnterNodes.length ? collectEnterElements(this.driver, allEnterNodes) : [];
const leaveNodes: any[] = [];
for (let i = 0; i < this.collectedLeaveElements.length; i++) {
const element = this.collectedLeaveElements[i];
if (isElementNode(element)) {
const details = element[REMOVAL_FLAG] as ElementAnimationState;
if (details && details.setForRemoval) {
addClass(element, LEAVE_CLASSNAME);
leaveNodes.push(element);
}
}
}
for (let i = this._namespaceList.length - 1; i >= 0; i--) { for (let i = this._namespaceList.length - 1; i >= 0; i--) {
const ns = this._namespaceList[i]; const ns = this._namespaceList[i];
ns.drainQueuedTransitions(microtaskId).forEach(entry => { ns.drainQueuedTransitions(microtaskId).forEach(entry => {
@ -794,10 +838,6 @@ export class TransitionAnimationEngine {
allPreviousPlayersMap.forEach(players => players.forEach(player => player.destroy())); allPreviousPlayersMap.forEach(players => players.forEach(player => player.destroy()));
const leaveNodes: any[] = bodyNode && allPostStyleElements.size ?
this.driver.query(bodyNode, LEAVE_SELECTOR, true) :
[];
// PRE STAGE: fill the ! styles // PRE STAGE: fill the ! styles
const preStylesMap = allPreStyleElements.size ? const preStylesMap = allPreStyleElements.size ?
cloakAndComputeStyles(this.driver, enterNodes, allPreStyleElements, PRE_STYLE) : cloakAndComputeStyles(this.driver, enterNodes, allPreStyleElements, PRE_STYLE) :
@ -867,14 +907,18 @@ export class TransitionAnimationEngine {
// run through all of the queued removals and see if they // run through all of the queued removals and see if they
// were picked up by a query. If not then perform the removal // were picked up by a query. If not then perform the removal
// operation right away unless a parent animation is ongoing. // operation right away unless a parent animation is ongoing.
this.queuedRemovals.forEach((fn, element) => { for (let i = 0; i < leaveNodes.length; i++) {
const element = leaveNodes[i];
const players = queriedElements.get(element); const players = queriedElements.get(element);
if (players) { if (players) {
optimizeGroupPlayer(players).onDone(fn); removeNodesAfterAnimationDone(this, element, players);
} else { } else {
fn(); const details = element[REMOVAL_FLAG] as ElementAnimationState;
if (details && !details.hasAnimation) {
this.processLeaveNode(element);
}
} }
}); }
rootPlayers.forEach(player => { rootPlayers.forEach(player => {
this.players.push(player); this.players.push(player);
@ -894,7 +938,8 @@ export class TransitionAnimationEngine {
elementContainsData(namespaceId: string, element: any) { elementContainsData(namespaceId: string, element: any) {
let containsData = false; let containsData = false;
if (this.queuedRemovals.has(element)) containsData = true; const details = element[REMOVAL_FLAG] as ElementAnimationState;
if (details && details.setForRemoval) containsData = true;
if (this.playersByElement.has(element)) containsData = true; if (this.playersByElement.has(element)) containsData = true;
if (this.playersByQueriedElement.has(element)) containsData = true; if (this.playersByQueriedElement.has(element)) containsData = true;
if (this.statesByElement.has(element)) containsData = true; if (this.statesByElement.has(element)) containsData = true;
@ -985,7 +1030,8 @@ export class TransitionAnimationEngine {
const element = timelineInstruction.element; const element = timelineInstruction.element;
// FIXME (matsko): make sure to-be-removed animations are removed properly // FIXME (matsko): make sure to-be-removed animations are removed properly
if (element['REMOVED']) return new NoopAnimationPlayer(); const details = element[REMOVAL_FLAG];
if (details && details.removedBeforeQueried) return new NoopAnimationPlayer();
const isQueriedElement = element !== rootElement; const isQueriedElement = element !== rootElement;
let previousPlayers: AnimationPlayer[] = EMPTY_PLAYER_ARRAY; let previousPlayers: AnimationPlayer[] = EMPTY_PLAYER_ARRAY;
@ -1235,7 +1281,7 @@ function cloakAndComputeStyles(
// there is no easy way to detect this because a sub element could be removed // there is no easy way to detect this because a sub element could be removed
// by a parent animation element being detached. // by a parent animation element being detached.
if (!value || value.length == 0) { if (!value || value.length == 0) {
element['REMOVED'] = true; element[REMOVAL_FLAG] = NULL_REMOVED_QUERIED_STATE;
} }
}); });
valuesMap.set(element, styles); valuesMap.set(element, styles);
@ -1286,17 +1332,14 @@ function removeClass(element: any, className: string) {
} }
} }
function setAttribute(element: any, attr: string, value: any) {
if (element.setAttribute) {
element.setAttribute(attr, value);
} else {
element[attr] = value;
}
}
function getBodyNode(): any|null { function getBodyNode(): any|null {
if (typeof document != 'undefined') { if (typeof document != 'undefined') {
return document.body; return document.body;
} }
return null; return null;
} }
function removeNodesAfterAnimationDone(
engine: TransitionAnimationEngine, element: any, players: AnimationPlayer[]) {
optimizeGroupPlayer(players).onDone(() => engine.processLeaveNode(element));
}

View File

@ -358,6 +358,56 @@ export function main() {
]); ]);
}); });
it('should substitute in values that are defined as parameters for inner areas of a sequence',
() => {
const steps = sequence(
[
sequence(
[
sequence(
[
style({height: '{{ x0 }}px'}),
animate(1000, style({height: '{{ x2 }}px'})),
],
buildParams({x2: '{{ x1 }}3'})),
],
buildParams({x1: '{{ x0 }}2'})),
],
buildParams({x0: '1'}));
const players = invokeAnimationSequence(rootElement, steps);
expect(players.length).toEqual(1);
const [player] = players;
expect(player.keyframes).toEqual([
{offset: 0, height: '1px'}, {offset: 1, height: '123px'}
]);
});
it('should substitute in values that are defined as parameters for reusable animations',
() => {
const anim = animation([
style({height: '{{ start }}'}),
animate(1000, style({height: '{{ end }}'})),
]);
const steps = sequence(
[
sequence(
[
useAnimation(anim, buildParams({start: '{{ a }}', end: '{{ b }}'})),
],
buildParams({a: '100px', b: '200px'})),
],
buildParams({a: '0px'}));
const players = invokeAnimationSequence(rootElement, steps);
expect(players.length).toEqual(1);
const [player] = players;
expect(player.keyframes).toEqual([
{offset: 0, height: '100px'}, {offset: 1, height: '200px'}
]);
});
it('should throw an error when an input variable is not provided when invoked and is not a default value', it('should throw an error when an input variable is not provided when invoked and is not a default value',
() => { () => {
expect(() => {invokeAnimationSequence(rootElement, [style({color: '{{ color }}'})])}) expect(() => {invokeAnimationSequence(rootElement, [style({color: '{{ color }}'})])})

View File

@ -59,6 +59,12 @@ export function main() {
expect(normalize('borderWidth', 'inherit')).toEqual('inherit'); expect(normalize('borderWidth', 'inherit')).toEqual('inherit');
expect(normalize('paddingTop', 'calc(500px + 200px)')).toEqual('calc(500px + 200px)'); expect(normalize('paddingTop', 'calc(500px + 200px)')).toEqual('calc(500px + 200px)');
}); });
it('should allow `perspective` to be a numerical property', () => {
expect(normalize('perspective', 10)).toEqual('10px');
expect(normalize('perspective', '100pt')).toEqual('100pt');
expect(normalize('perspective', 'none')).toEqual('none');
});
}); });
}); });
} }

13
packages/common/BUILD Normal file
View File

@ -0,0 +1,13 @@
package(default_visibility=["//visibility:public"])
load("@io_bazel_rules_typescript//:defs.bzl", "ts_library")
ts_library(
name = "common",
srcs = glob(["**/*.ts"], exclude=[
"test/**",
"testing/**",
]),
module_name = "@angular/common",
deps = ["//packages/core"],
tsconfig = ":tsconfig-build.json",
)

View File

@ -1,35 +1,41 @@
{ {
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true,
"flatModuleOutFile": "index.js",
"flatModuleId": "@angular/common"
},
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"declaration": true,
"stripInternal": true, "stripInternal": true,
"experimentalDecorators": true,
"strictNullChecks": true, "strictNullChecks": true,
"noImplicitAny": true, "noImplicitAny": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"module": "es2015", "module": "es2015",
"moduleResolution": "node", "moduleResolution": "node",
"target": "es2015",
"skipLibCheck": true,
"lib": [ "es2015", "dom" ],
// don't auto-discover @types/node, it results in a ///<reference in the .d.ts output
"types": [],
/**
* The rest of the file is configuration that's overridden by bazel.
* It can be removed after we fully migrate.
*/
"baseUrl": ".",
"declaration": true,
"experimentalDecorators": true,
"outDir": "../../dist/packages/common", "outDir": "../../dist/packages/common",
"paths": { "paths": {
"@angular/core": ["../../dist/packages/core"] "@angular/core": ["../../dist/packages/core"]
}, },
"rootDir": ".", "rootDir": ".",
"sourceMap": true, "sourceMap": true,
"inlineSources": true, "inlineSources": true
"target": "es2015",
"skipLibCheck": true,
"lib": [ "es2015", "dom" ],
// don't auto-discover @types/node, it results in a ///<reference in the .d.ts output
"types": []
}, },
"files": [ "files": [
"public_api.ts", "public_api.ts",
"../../node_modules/zone.js/dist/zone.js.d.ts" "../../node_modules/zone.js/dist/zone.js.d.ts"
], ]
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true,
"flatModuleOutFile": "index.js",
"flatModuleId": "@angular/common"
}
} }

View File

@ -9,7 +9,7 @@
"ng-xi18n": "./src/extract_i18n.js" "ng-xi18n": "./src/extract_i18n.js"
}, },
"dependencies": { "dependencies": {
"@angular/tsc-wrapped": "4.2.0-rc.2", "@angular/tsc-wrapped": "4.2.0",
"reflect-metadata": "^0.1.2", "reflect-metadata": "^0.1.2",
"minimist": "^1.2.0" "minimist": "^1.2.0"
}, },

View File

@ -40,17 +40,24 @@ export class CodeGenerator {
return this.compiler return this.compiler
.analyzeModulesAsync(this.program.getSourceFiles().map( .analyzeModulesAsync(this.program.getSourceFiles().map(
sf => this.ngCompilerHost.getCanonicalFileName(sf.fileName))) sf => this.ngCompilerHost.getCanonicalFileName(sf.fileName)))
.then(analyzedModules => this.compiler.emitAllImpls(analyzedModules)) .then(analyzedModules => this.emit(analyzedModules));
.then(generatedModules => { }
return generatedModules.map(generatedModule => {
const sourceFile = this.program.getSourceFile(generatedModule.srcFileUrl); codegenSync(): string[] {
const emitPath = this.ngCompilerHost.calculateEmitPath(generatedModule.genFileUrl); const analyzed = this.compiler.analyzeModulesSync(this.program.getSourceFiles().map(
const source = sf => this.ngCompilerHost.getCanonicalFileName(sf.fileName)));
generatedModule.source || compiler.toTypeScript(generatedModule, PREAMBLE); return this.emit(analyzed);
this.host.writeFile(emitPath, source, false, () => {}, [sourceFile]); }
return emitPath;
}); private emit(analyzedModules: compiler.NgAnalyzedModules) {
}); const generatedModules = this.compiler.emitAllImpls(analyzedModules);
return generatedModules.map(generatedModule => {
const sourceFile = this.program.getSourceFile(generatedModule.srcFileUrl);
const emitPath = this.ngCompilerHost.calculateEmitPath(generatedModule.genFileUrl);
const source = generatedModule.source || compiler.toTypeScript(generatedModule, PREAMBLE);
this.host.writeFile(emitPath, source, false, () => {}, [sourceFile]);
return emitPath;
});
} }
static create( static create(

View File

@ -258,7 +258,9 @@ export class CompilerHost implements AotCompilerHost {
return v3Metadata; return v3Metadata;
} }
loadResource(filePath: string): Promise<string> { return this.context.readResource(filePath); } loadResource(filePath: string): Promise<string>|string {
return this.context.readResource(filePath);
}
loadSummary(filePath: string): string|null { loadSummary(filePath: string): string|null {
if (this.context.fileExists(filePath)) { if (this.context.fileExists(filePath)) {

View File

@ -41,7 +41,7 @@ export interface ExtractorHost extends StaticSymbolResolverHost, AotSummaryResol
/** /**
* Loads a resource (e.g. html / css) * Loads a resource (e.g. html / css)
*/ */
loadResource(path: string): Promise<string>; loadResource(path: string): Promise<string>|string;
} }
export class Extractor { export class Extractor {

View File

@ -28,6 +28,7 @@ const ALL_HTML_TAGS =
// Elements missing from Chrome (HtmlUnknownElement), to be manually added // Elements missing from Chrome (HtmlUnknownElement), to be manually added
const MISSING_FROM_CHROME: {[el: string]: string[]} = { const MISSING_FROM_CHROME: {[el: string]: string[]} = {
'data^[HTMLElement]': ['value'], 'data^[HTMLElement]': ['value'],
'keygen^[HTMLElement]': ['!autofocus', 'challenge', '!disabled', 'form', 'keytype', 'name'],
// TODO(vicb): Figure out why Chrome and WhatWG do not agree on the props // TODO(vicb): Figure out why Chrome and WhatWG do not agree on the props
// 'menu^[HTMLElement]': ['type', 'label'], // 'menu^[HTMLElement]': ['type', 'label'],
'menuitem^[HTMLElement]': 'menuitem^[HTMLElement]':

13
packages/core/BUILD Normal file
View File

@ -0,0 +1,13 @@
package(default_visibility=["//visibility:public"])
load("@io_bazel_rules_typescript//:defs.bzl", "ts_library")
ts_library(
name = "core",
srcs = glob(["**/*.ts"], exclude=[
"test/**",
"testing/**",
]),
module_name = "@angular/core",
deps = [],
tsconfig = ":tsconfig-build.json",
)

View File

@ -16,6 +16,7 @@ import {NgModuleFactoryLoader} from './ng_module_factory_loader';
const _SEPARATOR = '#'; const _SEPARATOR = '#';
const FACTORY_CLASS_SUFFIX = 'NgFactory'; const FACTORY_CLASS_SUFFIX = 'NgFactory';
declare var System: any;
/** /**
* Configuration for SystemJsNgModuleLoader. * Configuration for SystemJsNgModuleLoader.

View File

@ -6,6 +6,10 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
// Import zero symbols from zone.js. This causes the zone ambient type to be
// added to the type-checker, without emitting any runtime module load statement
import {} from 'zone.js';
// TODO(jteplitz602): Load WorkerGlobalScope from lib.webworker.d.ts file #3492 // TODO(jteplitz602): Load WorkerGlobalScope from lib.webworker.d.ts file #3492
declare var WorkerGlobalScope: any /** TODO #9100 */; declare var WorkerGlobalScope: any /** TODO #9100 */;
// CommonJS / Node have global context exposed as "global" variable. // CommonJS / Node have global context exposed as "global" variable.

View File

@ -6,6 +6,9 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
// Import zero symbols from zone.js. This causes the zone ambient type to be
// added to the type-checker, without emitting any runtime module load statement
import {} from 'zone.js';
import {EventEmitter} from '../event_emitter'; import {EventEmitter} from '../event_emitter';
/** /**

View File

@ -1,34 +1,40 @@
{ {
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true,
"flatModuleOutFile": "index.js",
"flatModuleId": "@angular/core"
},
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"declaration": true, "declaration": true,
"stripInternal": true, "stripInternal": true,
"experimentalDecorators": true,
"strictNullChecks": true, "strictNullChecks": true,
"module": "es2015", "module": "es2015",
"moduleResolution": "node", "moduleResolution": "node",
"target": "es2015",
"lib": ["es2015", "dom"],
"skipLibCheck": true,
// don't auto-discover @types/node, it results in a ///<reference in the .d.ts output
"types": [],
/**
* The rest of the file is configuration that's overridden by bazel.
* It can be removed after we fully migrate.
*/
"baseUrl": ".",
"experimentalDecorators": true,
"outDir": "../../dist/packages/core", "outDir": "../../dist/packages/core",
"paths": { "paths": {
"rxjs/*": ["../../node_modules/rxjs/*"] "rxjs/*": ["../../node_modules/rxjs/*"]
}, },
"rootDir": ".", "rootDir": ".",
"sourceMap": true, "sourceMap": true,
"inlineSources": true, "inlineSources": true
"target": "es2015",
"lib": ["es2015", "dom"],
"skipLibCheck": true,
// don't auto-discover @types/node, it results in a ///<reference in the .d.ts output
"types": []
}, },
"files": [ "files": [
"public_api.ts", "public_api.ts",
"../../node_modules/zone.js/dist/zone.js.d.ts", "../../node_modules/zone.js/dist/zone.js.d.ts",
"../system.d.ts" "../system.d.ts"
], ]
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true,
"flatModuleOutFile": "index.js",
"flatModuleId": "@angular/core"
}
} }

View File

@ -64,11 +64,13 @@ export class Validators {
*/ */
static min(min: number): ValidatorFn { static min(min: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => { return (control: AbstractControl): ValidationErrors | null => {
if (isEmptyInputValue(control.value)) { if (isEmptyInputValue(control.value) || isEmptyInputValue(min)) {
return null; // don't validate empty values to allow optional controls return null; // don't validate empty values to allow optional controls
} }
const value = parseFloat(control.value); const value = parseFloat(control.value);
return isNaN(value) || value < min ? {'min': {'min': min, 'actual': control.value}} : null; // Controls with NaN values after parsing should be treated as not having a
// minimum, per the HTML forms spec: https://www.w3.org/TR/html5/forms.html#attr-input-min
return !isNaN(value) && value < min ? {'min': {'min': min, 'actual': control.value}} : null;
}; };
} }
@ -77,11 +79,13 @@ export class Validators {
*/ */
static max(max: number): ValidatorFn { static max(max: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => { return (control: AbstractControl): ValidationErrors | null => {
if (isEmptyInputValue(control.value)) { if (isEmptyInputValue(control.value) || isEmptyInputValue(max)) {
return null; // don't validate empty values to allow optional controls return null; // don't validate empty values to allow optional controls
} }
const value = parseFloat(control.value); const value = parseFloat(control.value);
return isNaN(value) || value > max ? {'max': {'max': max, 'actual': control.value}} : null; // Controls with NaN values after parsing should be treated as not having a
// maximum, per the HTML forms spec: https://www.w3.org/TR/html5/forms.html#attr-input-max
return !isNaN(value) && value > max ? {'max': {'max': max, 'actual': control.value}} : null;
}; };
} }

View File

@ -49,12 +49,15 @@ export function main() {
it('should not error on undefined', it('should not error on undefined',
() => { expect(Validators.min(2)(new FormControl(undefined))).toBeNull(); }); () => { expect(Validators.min(2)(new FormControl(undefined))).toBeNull(); });
it('should error on non numbers', () => { it('should return null if NaN after parsing',
expect(Validators.min(2)(new FormControl('a'))).toEqual({'min': {'min': 2, 'actual': 'a'}}); () => { expect(Validators.min(2)(new FormControl('a'))).toBeNull(); });
it('should return a validation error on small values', () => {
expect(Validators.min(2)(new FormControl(1))).toEqual({'min': {'min': 2, 'actual': 1}});
}); });
it('should error on small values', () => { it('should return a validation error on small values converted from strings', () => {
expect(Validators.min(2)(new FormControl(1))).toEqual({'min': {'min': 2, 'actual': 1}}); expect(Validators.min(2)(new FormControl('1'))).toEqual({'min': {'min': 2, 'actual': '1'}});
}); });
it('should not error on big values', it('should not error on big values',
@ -65,6 +68,18 @@ export function main() {
it('should not error on equal values when value is string', it('should not error on equal values when value is string',
() => { expect(Validators.min(2)(new FormControl('2'))).toBeNull(); }); () => { expect(Validators.min(2)(new FormControl('2'))).toBeNull(); });
it('should validate as expected when min value is a string', () => {
expect(Validators.min('2' as any)(new FormControl(1))).toEqual({
'min': {'min': '2', 'actual': 1}
});
});
it('should return null if min value is undefined',
() => { expect(Validators.min(undefined as any)(new FormControl(3))).toBeNull(); });
it('should return null if min value is null',
() => { expect(Validators.min(null as any)(new FormControl(3))).toBeNull(); });
}); });
describe('max', () => { describe('max', () => {
@ -77,14 +92,15 @@ export function main() {
it('should not error on undefined', it('should not error on undefined',
() => { expect(Validators.max(2)(new FormControl(undefined))).toBeNull(); }); () => { expect(Validators.max(2)(new FormControl(undefined))).toBeNull(); });
it('should error on non numbers', () => { it('should return null if NaN after parsing',
expect(Validators.max(2)(new FormControl('aaa'))).toEqual({ () => { expect(Validators.max(2)(new FormControl('aaa'))).toBeNull(); });
'max': {'max': 2, 'actual': 'aaa'}
}); it('should return a validation error on big values', () => {
expect(Validators.max(2)(new FormControl(3))).toEqual({'max': {'max': 2, 'actual': 3}});
}); });
it('should error on big values', () => { it('should return a validation error on big values converted from strings', () => {
expect(Validators.max(2)(new FormControl(3))).toEqual({'max': {'max': 2, 'actual': 3}}); expect(Validators.max(2)(new FormControl('3'))).toEqual({'max': {'max': 2, 'actual': '3'}});
}); });
it('should not error on small values', it('should not error on small values',
@ -95,6 +111,18 @@ export function main() {
it('should not error on equal values when value is string', it('should not error on equal values when value is string',
() => { expect(Validators.max(2)(new FormControl('2'))).toBeNull(); }); () => { expect(Validators.max(2)(new FormControl('2'))).toBeNull(); });
it('should validate as expected when max value is a string', () => {
expect(Validators.max('2' as any)(new FormControl(3))).toEqual({
'max': {'max': '2', 'actual': 3}
});
});
it('should return null if max value is undefined',
() => { expect(Validators.max(undefined as any)(new FormControl(3))).toBeNull(); });
it('should return null if max value is null',
() => { expect(Validators.max(null as any)(new FormControl(3))).toBeNull(); });
}); });

View File

@ -363,7 +363,6 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
} }
getComputedStyle(element: any): any { return getComputedStyle(element); } getComputedStyle(element: any): any { return getComputedStyle(element); }
// TODO(tbosch): move this into a separate environment class once we have it // TODO(tbosch): move this into a separate environment class once we have it
setGlobalVar(path: string, value: any) { setValueOnPath(global, path, value); }
supportsWebAnimation(): boolean { supportsWebAnimation(): boolean {
return typeof(<any>Element).prototype['animate'] === 'function'; return typeof(<any>Element).prototype['animate'] === 'function';
} }
@ -419,20 +418,3 @@ export function parseCookieValue(cookieStr: string, name: string): string|null {
} }
return null; return null;
} }
export function setValueOnPath(global: any, path: string, value: any) {
const parts = path.split('.');
let obj: any = global;
while (parts.length > 1) {
const name = parts.shift() !;
if (obj.hasOwnProperty(name) && obj[name] != null) {
obj = obj[name];
} else {
obj = obj[name] = {};
}
}
if (obj === undefined || obj === null) {
obj = {};
}
obj[parts.shift() !] = value;
}

View File

@ -7,11 +7,10 @@
*/ */
import {ComponentRef} from '@angular/core'; import {ComponentRef} from '@angular/core';
import {getDOM} from '../../dom/dom_adapter'; import {exportNgVar} from '../../dom/util';
import {AngularProfiler} from './common_tools'; import {AngularProfiler} from './common_tools';
const PROFILER_GLOBAL_NAME = 'ng.profiler'; const PROFILER_GLOBAL_NAME = 'profiler';
/** /**
* Enabled Angular debug tools that are accessible via your browser's * Enabled Angular debug tools that are accessible via your browser's
@ -27,7 +26,7 @@ const PROFILER_GLOBAL_NAME = 'ng.profiler';
* @experimental All debugging apis are currently experimental. * @experimental All debugging apis are currently experimental.
*/ */
export function enableDebugTools<T>(ref: ComponentRef<T>): ComponentRef<T> { export function enableDebugTools<T>(ref: ComponentRef<T>): ComponentRef<T> {
getDOM().setGlobalVar(PROFILER_GLOBAL_NAME, new AngularProfiler(ref)); exportNgVar(PROFILER_GLOBAL_NAME, new AngularProfiler(ref));
return ref; return ref;
} }
@ -37,5 +36,5 @@ export function enableDebugTools<T>(ref: ComponentRef<T>): ComponentRef<T> {
* @experimental All debugging apis are currently experimental. * @experimental All debugging apis are currently experimental.
*/ */
export function disableDebugTools(): void { export function disableDebugTools(): void {
getDOM().setGlobalVar(PROFILER_GLOBAL_NAME, null); exportNgVar(PROFILER_GLOBAL_NAME, null);
} }

View File

@ -7,15 +7,15 @@
*/ */
import * as core from '@angular/core'; import * as core from '@angular/core';
import {getDOM} from '../dom_adapter'; import {exportNgVar} from '../util';
const CORE_TOKENS = { const CORE_TOKENS = {
'ApplicationRef': core.ApplicationRef, 'ApplicationRef': core.ApplicationRef,
'NgZone': core.NgZone, 'NgZone': core.NgZone,
}; };
const INSPECT_GLOBAL_NAME = 'ng.probe'; const INSPECT_GLOBAL_NAME = 'probe';
const CORE_TOKENS_GLOBAL_NAME = 'ng.coreTokens'; const CORE_TOKENS_GLOBAL_NAME = 'coreTokens';
/** /**
* Returns a {@link DebugElement} for the given native DOM element, or * Returns a {@link DebugElement} for the given native DOM element, or
@ -36,9 +36,8 @@ export class NgProbeToken {
export function _createNgProbe(extraTokens: NgProbeToken[], coreTokens: core.NgProbeToken[]): any { export function _createNgProbe(extraTokens: NgProbeToken[], coreTokens: core.NgProbeToken[]): any {
const tokens = (extraTokens || []).concat(coreTokens || []); const tokens = (extraTokens || []).concat(coreTokens || []);
getDOM().setGlobalVar(INSPECT_GLOBAL_NAME, inspectNativeElement); exportNgVar(INSPECT_GLOBAL_NAME, inspectNativeElement);
getDOM().setGlobalVar( exportNgVar(CORE_TOKENS_GLOBAL_NAME, {...CORE_TOKENS, ..._ngProbeTokensToMap(tokens || [])});
CORE_TOKENS_GLOBAL_NAME, {...CORE_TOKENS, ..._ngProbeTokensToMap(tokens || [])});
return () => inspectNativeElement; return () => inspectNativeElement;
} }

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