build(aio): big move of docs related files (#14361)
All the docs related files (docs-app, doc-gen, content, etc) are now to be found inside the `/aio` folder. The related gulp tasks have been moved from the top level gulp file to a new one inside the `/aio` folder. The structure of the `/aio` folder now looks like: ``` /aio/ build/ # gulp tasks content/ #MARKDOWN FILES for devguides, cheatsheet, etc devguides/ cheatsheets/ transforms/ #dgeni packages, templates, etc src/ app/ assets/ content/ #HTML + JSON build artifacts produced by dgeni from /aio/content. #This dir is .gitignored-ed e2e/ #protractor tests for the doc viewer app node_modules/ #dependencies for both the doc viewer builds and the dgeni stuff #This dir is .gitignored-ed gulpfile.js #Tasks for generating docs and building & deploying the doc viewer ``` Closes #14361


5
aio/.firebaserc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"projects": {
|
||||
"staging": "aio-staging"
|
||||
}
|
||||
}
|
31
aio/README.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Site
|
||||
|
||||
This project was generated with [angular-cli](https://github.com/angular/angular-cli) version 1.0.0-beta.26.
|
||||
|
||||
## Development server
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class/module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
Before running the tests make sure you are serving the app via `ng serve`.
|
||||
|
||||
## Deploying to GitHub Pages
|
||||
|
||||
Run `ng github-pages:deploy` to deploy to GitHub Pages.
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the `angular-cli` use `ng help` or go check out the [Angular-CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
59
aio/angular-cli.json
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"project": {
|
||||
"version": "1.0.0-beta.26",
|
||||
"name": "site"
|
||||
},
|
||||
"apps": [
|
||||
{
|
||||
"root": "src",
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"assets",
|
||||
"favicon.ico"
|
||||
],
|
||||
"index": "index.html",
|
||||
"main": "main.ts",
|
||||
"polyfills": "polyfills.ts",
|
||||
"test": "test.ts",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"prefix": "aio",
|
||||
"styles": [
|
||||
"styles.scss"
|
||||
],
|
||||
"scripts": [
|
||||
|
||||
],
|
||||
"environments": {
|
||||
"source": "environments/environment.ts",
|
||||
"dev": "environments/environment.ts",
|
||||
"prod": "environments/environment.prod.ts"
|
||||
}
|
||||
}
|
||||
],
|
||||
"e2e": {
|
||||
"protractor": {
|
||||
"config": "./protractor.conf.js"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"karma": {
|
||||
"config": "./karma.conf.js"
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"styleExt": "css",
|
||||
"prefixInterfaces": false,
|
||||
"inline": {
|
||||
"style": false,
|
||||
"template": false
|
||||
},
|
||||
"spec": {
|
||||
"class": false,
|
||||
"component": true,
|
||||
"directive": true,
|
||||
"module": false,
|
||||
"pipe": true,
|
||||
"service": true
|
||||
}
|
||||
}
|
||||
}
|
3
aio/build/docs-app.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = (gulp) => () => {
|
||||
// TODO:(petebd): hook up with whatever builds need doing for the webapp
|
||||
};
|
24
aio/build/docs.js
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
generate: (gulp) => () => {
|
||||
const path = require('path');
|
||||
const Dgeni = require('dgeni');
|
||||
const angularDocsPackage = require(path.resolve(__dirname, '../transforms/angular.io-package'));
|
||||
const dgeni = new Dgeni([angularDocsPackage]);
|
||||
return dgeni.generate();
|
||||
},
|
||||
|
||||
test: (gulp) => () => {
|
||||
const execSync = require('child_process').execSync;
|
||||
execSync(
|
||||
'node ../dist/tools/cjs-jasmine/index-tools ../../transforms/**/*.spec.js',
|
||||
{stdio: ['inherit', 'inherit', 'inherit']});
|
||||
}
|
||||
};
|
18
aio/content/cheatsheet/bootstrapping.md
Normal file
@ -0,0 +1,18 @@
|
||||
@cheatsheetSection
|
||||
Bootstrapping
|
||||
@cheatsheetIndex 0
|
||||
@description
|
||||
{@target ts}`import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';`{@endtarget}
|
||||
{@target js}Available from the `ng.platformBrowserDynamic` namespace{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`platformBrowserDynamic().bootstrapModule(AppModule);`|`platformBrowserDynamic().bootstrapModule`
|
||||
syntax(js):
|
||||
`document.addEventListener('DOMContentLoaded', function() {
|
||||
ng.platformBrowserDynamic
|
||||
.platformBrowserDynamic()
|
||||
.bootstrapModule(app.AppModule);
|
||||
});`|`platformBrowserDynamic().bootstrapModule`
|
||||
description:
|
||||
Bootstraps the app, using the root component from the specified `NgModule`. {@target js}Must be wrapped in the event listener to fire when the page loads.{@endtarget}
|
34
aio/content/cheatsheet/built-in-directives.md
Normal file
@ -0,0 +1,34 @@
|
||||
@cheatsheetSection
|
||||
Built-in directives
|
||||
@cheatsheetIndex 3
|
||||
@description
|
||||
{@target ts}`import { CommonModule } from '@angular/common';`{@endtarget}
|
||||
{@target js}Available using the `ng.common.CommonModule` module{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<section *ngIf="showSection">`|`*ngIf`
|
||||
description:
|
||||
Removes or recreates a portion of the DOM tree based on the `showSection` expression.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<li *ngFor="let item of list">`|`*ngFor`
|
||||
description:
|
||||
Turns the li element and its contents into a template, and uses that to instantiate a view for each item in list.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<div [ngSwitch]="conditionExpression">
|
||||
<template [ngSwitchCase]="case1Exp">...</template>
|
||||
<template ngSwitchCase="case2LiteralString">...</template>
|
||||
<template ngSwitchDefault>...</template>
|
||||
</div>`|`[ngSwitch]`|`[ngSwitchCase]`|`ngSwitchCase`|`ngSwitchDefault`
|
||||
description:
|
||||
Conditionally swaps the contents of the div by selecting one of the embedded templates based on the current value of `conditionExpression`.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<div [ngClass]="{active: isActive, disabled: isDisabled}">`|`[ngClass]`
|
||||
description:
|
||||
Binds the presence of CSS classes on the element to the truthiness of the associated map values. The right-hand expression should return {class-name: true/false} map.
|
49
aio/content/cheatsheet/class-decorators.md
Normal file
@ -0,0 +1,49 @@
|
||||
@cheatsheetSection
|
||||
Class decorators
|
||||
@cheatsheetIndex 5
|
||||
@description
|
||||
{@target ts}`import { Directive, ... } from '@angular/core';`{@endtarget}
|
||||
{@target js}Available from the `ng.core` namespace{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@Component({...})
|
||||
class MyComponent() {}`|`@Component({...})`
|
||||
syntax(js):
|
||||
`var MyComponent = ng.core.Component({...}).Class({...})`|`ng.core.Component({...})`
|
||||
description:
|
||||
Declares that a class is a component and provides metadata about the component.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@Directive({...})
|
||||
class MyDirective() {}`|`@Directive({...})`
|
||||
syntax(js):
|
||||
`var MyDirective = ng.core.Directive({...}).Class({...})`|`ng.core.Directive({...})`
|
||||
description:
|
||||
Declares that a class is a directive and provides metadata about the directive.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@Pipe({...})
|
||||
class MyPipe() {}`|`@Pipe({...})`
|
||||
syntax(js):
|
||||
`var MyPipe = ng.core.Pipe({...}).Class({...})`|`ng.core.Pipe({...})`
|
||||
description:
|
||||
Declares that a class is a pipe and provides metadata about the pipe.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@Injectable()
|
||||
class MyService() {}`|`@Injectable()`
|
||||
syntax(js):
|
||||
`var OtherService = ng.core.Class(
|
||||
{constructor: function() { }});
|
||||
var MyService = ng.core.Class(
|
||||
{constructor: [OtherService, function(otherService) { }]});`|`var MyService = ng.core.Class({constructor: [OtherService, function(otherService) { }]});`
|
||||
description:
|
||||
{@target ts}Declares that a class has dependencies that should be injected into the constructor when the dependency injector is creating an instance of this class.
|
||||
{@endtarget}
|
||||
{@target js}
|
||||
Declares a service to inject into a class by providing an array with the services, with the final item being the function to receive the injected services.
|
||||
{@endtarget}
|
38
aio/content/cheatsheet/component-configuration.md
Normal file
@ -0,0 +1,38 @@
|
||||
@cheatsheetSection
|
||||
Component configuration
|
||||
@cheatsheetIndex 7
|
||||
@description
|
||||
{@target js}`ng.core.Component` extends `ng.core.Directive`,
|
||||
so the `ng.core.Directive` configuration applies to components as well{@endtarget}
|
||||
{@target ts}`@Component` extends `@Directive`,
|
||||
so the `@Directive` configuration applies to components as well{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`moduleId: module.id`|`moduleId:`
|
||||
description:
|
||||
If set, the `templateUrl` and `styleUrl` are resolved relative to the component.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`viewProviders: [MyService, { provide: ... }]`|`viewProviders:`
|
||||
syntax(js):
|
||||
`viewProviders: [MyService, { provide: ... }]`|`viewProviders:`
|
||||
description:
|
||||
List of dependency injection providers scoped to this component's view.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`template: 'Hello {{name}}'
|
||||
templateUrl: 'my-component.html'`|`template:`|`templateUrl:`
|
||||
description:
|
||||
Inline template or external template URL of the component's view.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`styles: ['.primary {color: red}']
|
||||
styleUrls: ['my-component.css']`|`styles:`|`styleUrls:`
|
||||
description:
|
||||
List of inline CSS styles or external stylesheet URLs for styling the component’s view.
|
30
aio/content/cheatsheet/dependency-injection.md
Normal file
@ -0,0 +1,30 @@
|
||||
@cheatsheetSection
|
||||
Dependency injection configuration
|
||||
@cheatsheetIndex 10
|
||||
@description
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`{ provide: MyService, useClass: MyMockService }`|`provide`|`useClass`
|
||||
syntax(js):
|
||||
`{ provide: MyService, useClass: MyMockService }`|`provide`|`useClass`
|
||||
description:
|
||||
Sets or overrides the provider for `MyService` to the `MyMockService` class.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`{ provide: MyService, useFactory: myFactory }`|`provide`|`useFactory`
|
||||
syntax(js):
|
||||
`{ provide: MyService, useFactory: myFactory }`|`provide`|`useFactory`
|
||||
description:
|
||||
Sets or overrides the provider for `MyService` to the `myFactory` factory function.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`{ provide: MyValue, useValue: 41 }`|`provide`|`useValue`
|
||||
syntax(js):
|
||||
`{ provide: MyValue, useValue: 41 }`|`provide`|`useValue`
|
||||
description:
|
||||
Sets or overrides the provider for `MyValue` to the value `41`.
|
86
aio/content/cheatsheet/directive-and-component-decorators.md
Normal file
@ -0,0 +1,86 @@
|
||||
@cheatsheetSection
|
||||
Class field decorators for directives and components
|
||||
@cheatsheetIndex 8
|
||||
@description
|
||||
{@target ts}`import { Input, ... } from '@angular/core';`{@endtarget}
|
||||
{@target js}Available from the `ng.core` namespace{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@Input() myProperty;`|`@Input()`
|
||||
syntax(js):
|
||||
`ng.core.Input(myProperty, myComponent);`|`ng.core.Input(`|`);`
|
||||
description:
|
||||
Declares an input property that you can update via property binding (example:
|
||||
`<my-cmp [myProperty]="someExpression">`).
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@Output() myEvent = new EventEmitter();`|`@Output()`
|
||||
syntax(js):
|
||||
`myEvent = new ng.core.EventEmitter();
|
||||
ng.core.Output(myEvent, myComponent);`|`ng.core.Output(`|`);`
|
||||
description:
|
||||
Declares an output property that fires events that you can subscribe to with an event binding (example: `<my-cmp (myEvent)="doSomething()">`).
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@HostBinding('class.valid') isValid;`|`@HostBinding('class.valid')`
|
||||
syntax(js):
|
||||
`ng.core.HostBinding('class.valid',
|
||||
'isValid', myComponent);`|`ng.core.HostBinding('class.valid', 'isValid'`|`);`
|
||||
description:
|
||||
Binds a host element property (here, the CSS class `valid`) to a directive/component property (`isValid`).
|
||||
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@HostListener('click', ['$event']) onClick(e) {...}`|`@HostListener('click', ['$event'])`
|
||||
syntax(js):
|
||||
`ng.core.HostListener('click',
|
||||
['$event'], onClick(e) {...}, myComponent);`|`ng.core.HostListener('click', ['$event'], onClick(e)`|`);`
|
||||
description:
|
||||
Subscribes to a host element event (`click`) with a directive/component method (`onClick`), optionally passing an argument (`$event`).
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@ContentChild(myPredicate) myChildComponent;`|`@ContentChild(myPredicate)`
|
||||
syntax(js):
|
||||
`ng.core.ContentChild(myPredicate,
|
||||
'myChildComponent', myComponent);`|`ng.core.ContentChild(myPredicate,`|`);`
|
||||
description:
|
||||
Binds the first result of the component content query (`myPredicate`) to a property (`myChildComponent`) of the class.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@ContentChildren(myPredicate) myChildComponents;`|`@ContentChildren(myPredicate)`
|
||||
syntax(js):
|
||||
`ng.core.ContentChildren(myPredicate,
|
||||
'myChildComponents', myComponent);`|`ng.core.ContentChildren(myPredicate,`|`);`
|
||||
description:
|
||||
Binds the results of the component content query (`myPredicate`) to a property (`myChildComponents`) of the class.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@ViewChild(myPredicate) myChildComponent;`|`@ViewChild(myPredicate)`
|
||||
syntax(js):
|
||||
`ng.core.ViewChild(myPredicate,
|
||||
'myChildComponent', myComponent);`|`ng.core.ViewChild(myPredicate,`|`);`
|
||||
description:
|
||||
Binds the first result of the component view query (`myPredicate`) to a property (`myChildComponent`) of the class. Not available for directives.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@ViewChildren(myPredicate) myChildComponents;`|`@ViewChildren(myPredicate)`
|
||||
syntax(js):
|
||||
`ng.core.ViewChildren(myPredicate,
|
||||
'myChildComponents', myComponent);`|`ng.core.ViewChildren(myPredicate,`|`);`
|
||||
description:
|
||||
Binds the results of the component view query (`myPredicate`) to a property (`myChildComponents`) of the class. Not available for directives.
|
23
aio/content/cheatsheet/directive-configuration.md
Normal file
@ -0,0 +1,23 @@
|
||||
@cheatsheetSection
|
||||
Directive configuration
|
||||
@cheatsheetIndex 6
|
||||
@description
|
||||
{@target ts}`@Directive({ property1: value1, ... })`{@endtarget}
|
||||
{@target js}`ng.core.Directive({ property1: value1, ... }).Class({...})`{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`selector: '.cool-button:not(a)'`|`selector:`
|
||||
description:
|
||||
Specifies a CSS selector that identifies this directive within a template. Supported selectors include `element`,
|
||||
`[attribute]`, `.class`, and `:not()`.
|
||||
|
||||
Does not support parent-child relationship selectors.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`providers: [MyService, { provide: ... }]`|`providers:`
|
||||
syntax(js):
|
||||
`providers: [MyService, { provide: ... }]`|`providers:`
|
||||
description:
|
||||
List of dependency injection providers for this directive and its children.
|
12
aio/content/cheatsheet/forms.md
Normal file
@ -0,0 +1,12 @@
|
||||
@cheatsheetSection
|
||||
Forms
|
||||
@cheatsheetIndex 4
|
||||
@description
|
||||
{@target ts}`import { FormsModule } from '@angular/forms';`{@endtarget}
|
||||
{@target js}Available using the `ng.forms.FormsModule` module{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<input [(ngModel)]="userName">`|`[(ngModel)]`
|
||||
description:
|
||||
Provides two-way data-binding, parsing, and validation for form controls.
|
86
aio/content/cheatsheet/lifecycle hooks.md
Normal file
@ -0,0 +1,86 @@
|
||||
@cheatsheetSection
|
||||
Directive and component change detection and lifecycle hooks
|
||||
@cheatsheetIndex 9
|
||||
@description
|
||||
{@target ts}(implemented as class methods){@endtarget}
|
||||
{@target js}(implemented as component properties){@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`constructor(myService: MyService, ...) { ... }`|`constructor(myService: MyService, ...)`
|
||||
syntax(js):
|
||||
`constructor: function(MyService, ...) { ... }`|`constructor: function(MyService, ...)`
|
||||
description:
|
||||
Called before any other lifecycle hook. Use it to inject dependencies, but avoid any serious work here.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngOnChanges(changeRecord) { ... }`|`ngOnChanges(changeRecord)`
|
||||
syntax(js):
|
||||
`ngOnChanges: function(changeRecord) { ... }`|`ngOnChanges: function(changeRecord)`
|
||||
description:
|
||||
Called after every change to input properties and before processing content or child views.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngOnInit() { ... }`|`ngOnInit()`
|
||||
syntax(js):
|
||||
`ngOnInit: function() { ... }`|`ngOnInit: function()`
|
||||
description:
|
||||
Called after the constructor, initializing input properties, and the first call to `ngOnChanges`.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngDoCheck() { ... }`|`ngDoCheck()`
|
||||
syntax(js):
|
||||
`ngDoCheck: function() { ... }`|`ngDoCheck: function()`
|
||||
description:
|
||||
Called every time that the input properties of a component or a directive are checked. Use it to extend change detection by performing a custom check.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngAfterContentInit() { ... }`|`ngAfterContentInit()`
|
||||
syntax(js):
|
||||
`ngAfterContentInit: function() { ... }`|`ngAfterContentInit: function()`
|
||||
description:
|
||||
Called after `ngOnInit` when the component's or directive's content has been initialized.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngAfterContentChecked() { ... }`|`ngAfterContentChecked()`
|
||||
syntax(js):
|
||||
`ngAfterContentChecked: function() { ... }`|`ngAfterContentChecked: function()`
|
||||
description:
|
||||
Called after every check of the component's or directive's content.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngAfterViewInit() { ... }`|`ngAfterViewInit()`
|
||||
syntax(js):
|
||||
`ngAfterViewInit: function() { ... }`|`ngAfterViewInit: function()`
|
||||
description:
|
||||
Called after `ngAfterContentInit` when the component's view has been initialized. Applies to components only.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngAfterViewChecked() { ... }`|`ngAfterViewChecked()`
|
||||
syntax(js):
|
||||
`ngAfterViewChecked: function() { ... }`|`ngAfterViewChecked: function()`
|
||||
description:
|
||||
Called after every check of the component's view. Applies to components only.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngOnDestroy() { ... }`|`ngOnDestroy()`
|
||||
syntax(js):
|
||||
`ngOnDestroy: function() { ... }`|`ngOnDestroy: function()`
|
||||
description:
|
||||
Called once, before the instance is destroyed.
|
58
aio/content/cheatsheet/ngmodules.md
Normal file
@ -0,0 +1,58 @@
|
||||
@cheatsheetSection
|
||||
NgModules
|
||||
@cheatsheetIndex 1
|
||||
@description
|
||||
{@target ts}`import { NgModule } from '@angular/core';`{@endtarget}
|
||||
{@target js}Available from the `ng.core` namespace{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@NgModule({ declarations: ..., imports: ...,
|
||||
exports: ..., providers: ..., bootstrap: ...})
|
||||
class MyModule {}`|`NgModule`
|
||||
description:
|
||||
Defines a module that contains components, directives, pipes, and providers.
|
||||
|
||||
syntax(js):
|
||||
`ng.core.NgModule({declarations: ..., imports: ...,
|
||||
exports: ..., providers: ..., bootstrap: ...}).
|
||||
Class({ constructor: function() {}})`
|
||||
description:
|
||||
Defines a module that contains components, directives, pipes, and providers.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`declarations: [MyRedComponent, MyBlueComponent, MyDatePipe]`|`declarations:`
|
||||
description:
|
||||
List of components, directives, and pipes that belong to this module.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`imports: [BrowserModule, SomeOtherModule]`|`imports:`
|
||||
description:
|
||||
List of modules to import into this module. Everything from the imported modules
|
||||
is available to `declarations` of this module.
|
||||
|
||||
syntax(js):
|
||||
`imports: [ng.platformBrowser.BrowserModule, SomeOtherModule]`|`imports:`
|
||||
description:
|
||||
List of modules to import into this module. Everything from the imported modules
|
||||
is available to `declarations` of this module.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`exports: [MyRedComponent, MyDatePipe]`|`exports:`
|
||||
description:
|
||||
List of components, directives, and pipes visible to modules that import this module.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`providers: [MyService, { provide: ... }]`|`providers:`
|
||||
description:
|
||||
List of dependency injection providers visible both to the contents of this module and to importers of this module.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`bootstrap: [MyAppComponent]`|`bootstrap:`
|
||||
description:
|
||||
List of components to bootstrap when this module is bootstrapped.
|
170
aio/content/cheatsheet/routing.md
Normal file
@ -0,0 +1,170 @@
|
||||
@cheatsheetSection
|
||||
Routing and navigation
|
||||
@cheatsheetIndex 11
|
||||
@description
|
||||
{@target ts}`import { Routes, RouterModule, ... } from '@angular/router';`{@endtarget}
|
||||
{@target js}Available from the `ng.router` namespace{@endtarget}
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`const routes: Routes = [
|
||||
{ path: '', component: HomeComponent },
|
||||
{ path: 'path/:routeParam', component: MyComponent },
|
||||
{ path: 'staticPath', component: ... },
|
||||
{ path: '**', component: ... },
|
||||
{ path: 'oldPath', redirectTo: '/staticPath' },
|
||||
{ path: ..., component: ..., data: { message: 'Custom' } }
|
||||
]);
|
||||
|
||||
const routing = RouterModule.forRoot(routes);`|`Routes`
|
||||
syntax(js):
|
||||
`var routes = [
|
||||
{ path: '', component: HomeComponent },
|
||||
{ path: ':routeParam', component: MyComponent },
|
||||
{ path: 'staticPath', component: ... },
|
||||
{ path: '**', component: ... },
|
||||
{ path: 'oldPath', redirectTo: '/staticPath' },
|
||||
{ path: ..., component: ..., data: { message: 'Custom' } }
|
||||
]);
|
||||
|
||||
var routing = ng.router.RouterModule.forRoot(routes);`|`ng.router.Routes`
|
||||
description:
|
||||
Configures routes for the application. Supports static, parameterized, redirect, and wildcard routes. Also supports custom route data and resolve.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet name="aux"></router-outlet>
|
||||
`|`router-outlet`
|
||||
description:
|
||||
Marks the location to load the component of the active route.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`
|
||||
<a routerLink="/path">
|
||||
<a [routerLink]="[ '/path', routeParam ]">
|
||||
<a [routerLink]="[ '/path', { matrixParam: 'value' } ]">
|
||||
<a [routerLink]="[ '/path' ]" [queryParams]="{ page: 1 }">
|
||||
<a [routerLink]="[ '/path' ]" fragment="anchor">
|
||||
`|`[routerLink]`
|
||||
description:
|
||||
Creates a link to a different view based on a route instruction consisting of a route path, required and optional parameters, query parameters, and a fragment. To navigate to a root route, use the `/` prefix; for a child route, use the `./`prefix; for a sibling or parent, use the `../` prefix.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<a [routerLink]="[ '/path' ]" routerLinkActive="active">`
|
||||
description:
|
||||
The provided classes are added to the element when the `routerLink` becomes the current active route.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`class CanActivateGuard implements CanActivate {
|
||||
canActivate(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Observable<boolean>|Promise<boolean>|boolean { ... }
|
||||
}
|
||||
|
||||
{ path: ..., canActivate: [CanActivateGuard] }`|`CanActivate`
|
||||
syntax(js):
|
||||
`var CanActivateGuard = ng.core.Class({
|
||||
canActivate: function(route, state) {
|
||||
// return Observable/Promise boolean or boolean
|
||||
}
|
||||
});
|
||||
|
||||
{ path: ..., canActivate: [CanActivateGuard] }`|`CanActivate`
|
||||
description:
|
||||
An interface for defining a class that the router should call first to determine if it should activate this component. Should return a boolean or an Observable/Promise that resolves to a boolean.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`class CanDeactivateGuard implements CanDeactivate<T> {
|
||||
canDeactivate(
|
||||
component: T,
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Observable<boolean>|Promise<boolean>|boolean { ... }
|
||||
}
|
||||
|
||||
{ path: ..., canDeactivate: [CanDeactivateGuard] }`|`CanDeactivate`
|
||||
syntax(js):
|
||||
`var CanDeactivateGuard = ng.core.Class({
|
||||
canDeactivate: function(component, route, state) {
|
||||
// return Observable/Promise boolean or boolean
|
||||
}
|
||||
});
|
||||
|
||||
{ path: ..., canDeactivate: [CanDeactivateGuard] }`|`CanDeactivate`
|
||||
description:
|
||||
An interface for defining a class that the router should call first to determine if it should deactivate this component after a navigation. Should return a boolean or an Observable/Promise that resolves to a boolean.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`class CanActivateChildGuard implements CanActivateChild {
|
||||
canActivateChild(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Observable<boolean>|Promise<boolean>|boolean { ... }
|
||||
}
|
||||
|
||||
{ path: ..., canActivateChild: [CanActivateGuard],
|
||||
children: ... }`|`CanActivateChild`
|
||||
syntax(js):
|
||||
`var CanActivateChildGuard = ng.core.Class({
|
||||
canActivateChild: function(route, state) {
|
||||
// return Observable/Promise boolean or boolean
|
||||
}
|
||||
});
|
||||
|
||||
{ path: ..., canActivateChild: [CanActivateChildGuard],
|
||||
children: ... }`|`CanActivateChild`
|
||||
description:
|
||||
An interface for defining a class that the router should call first to determine if it should activate the child route. Should return a boolean or an Observable/Promise that resolves to a boolean.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`class ResolveGuard implements Resolve<T> {
|
||||
resolve(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Observable<any>|Promise<any>|any { ... }
|
||||
}
|
||||
|
||||
{ path: ..., resolve: [ResolveGuard] }`|`Resolve`
|
||||
syntax(js):
|
||||
`var ResolveGuard = ng.core.Class({
|
||||
resolve: function(route, state) {
|
||||
// return Observable/Promise value or value
|
||||
}
|
||||
});
|
||||
|
||||
{ path: ..., resolve: [ResolveGuard] }`|`Resolve`
|
||||
description:
|
||||
An interface for defining a class that the router should call first to resolve route data before rendering the route. Should return a value or an Observable/Promise that resolves to a value.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`class CanLoadGuard implements CanLoad {
|
||||
canLoad(
|
||||
route: Route
|
||||
): Observable<boolean>|Promise<boolean>|boolean { ... }
|
||||
}
|
||||
|
||||
{ path: ..., canLoad: [CanLoadGuard], loadChildren: ... }`|`CanLoad`
|
||||
syntax(js):
|
||||
`var CanLoadGuard = ng.core.Class({
|
||||
canLoad: function(route) {
|
||||
// return Observable/Promise boolean or boolean
|
||||
}
|
||||
});
|
||||
|
||||
{ path: ..., canLoad: [CanLoadGuard], loadChildren: ... }`|`CanLoad`
|
||||
description:
|
||||
An interface for defining a class that the router should call first to check if the lazy loaded module should be loaded. Should return a boolean or an Observable/Promise that resolves to a boolean.
|
||||
|
94
aio/content/cheatsheet/template-syntax.md
Normal file
@ -0,0 +1,94 @@
|
||||
@cheatsheetSection
|
||||
Template syntax
|
||||
@cheatsheetIndex 2
|
||||
@description
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<input [value]="firstName">`|`[value]`
|
||||
description:
|
||||
Binds property `value` to the result of expression `firstName`.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<div [attr.role]="myAriaRole">`|`[attr.role]`
|
||||
description:
|
||||
Binds attribute `role` to the result of expression `myAriaRole`.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<div [class.extra-sparkle]="isDelightful">`|`[class.extra-sparkle]`
|
||||
description:
|
||||
Binds the presence of the CSS class `extra-sparkle` on the element to the truthiness of the expression `isDelightful`.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<div [style.width.px]="mySize">`|`[style.width.px]`
|
||||
description:
|
||||
Binds style property `width` to the result of expression `mySize` in pixels. Units are optional.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<button (click)="readRainbow($event)">`|`(click)`
|
||||
description:
|
||||
Calls method `readRainbow` when a click event is triggered on this button element (or its children) and passes in the event object.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<div title="Hello {{ponyName}}">`|`{{ponyName}}`
|
||||
description:
|
||||
Binds a property to an interpolated string, for example, "Hello Seabiscuit". Equivalent to:
|
||||
`<div [title]="'Hello ' + ponyName">`
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<p>Hello {{ponyName}}</p>`|`{{ponyName}}`
|
||||
description:
|
||||
Binds text content to an interpolated string, for example, "Hello Seabiscuit".
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<my-cmp [(title)]="name">`|`[(title)]`
|
||||
description:
|
||||
Sets up two-way data binding. Equivalent to: `<my-cmp [title]="name" (titleChange)="name=$event">`
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<video #movieplayer ...>
|
||||
<button (click)="movieplayer.play()">
|
||||
</video>`|`#movieplayer`|`(click)`
|
||||
description:
|
||||
Creates a local variable `movieplayer` that provides access to the `video` element instance in data-binding and event-binding expressions in the current template.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<p *myUnless="myExpression">...</p>`|`*myUnless`
|
||||
description:
|
||||
The `*` symbol turns the current element into an embedded template. Equivalent to:
|
||||
`<template [myUnless]="myExpression"><p>...</p></template>`
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<p>Card No.: {{cardNumber | myCardNumberFormatter}}</p>`|`{{cardNumber | myCardNumberFormatter}}`
|
||||
description:
|
||||
Transforms the current value of expression `cardNumber` via the pipe called `myCardNumberFormatter`.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<p>Employer: {{employer?.companyName}}</p>`|`{{employer?.companyName}}`
|
||||
description:
|
||||
The safe navigation operator (`?`) means that the `employer` field is optional and if `undefined`, the rest of the expression should be ignored.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<svg:rect x="0" y="0" width="100" height="100"/>`|`svg:`
|
||||
description:
|
||||
An SVG snippet template needs an `svg:` prefix on its root element to disambiguate the SVG element from an HTML component.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<svg>
|
||||
<rect x="0" y="0" width="100" height="100"/>
|
||||
</svg>`|`svg`
|
||||
description:
|
||||
An `<svg>` root element is detected as an SVG element automatically, without the prefix.
|
160
aio/content/cookbook/component-relative-paths.md
Normal file
@ -0,0 +1,160 @@
|
||||
@title
|
||||
Component-relative Paths
|
||||
|
||||
@intro
|
||||
Use relative URLs for component templates and styles.
|
||||
|
||||
@description
|
||||
## Write *Component-Relative* URLs to component templates and style files
|
||||
|
||||
Our components often refer to external template and style files.
|
||||
We identify those files with a URL in the `templateUrl` and `styleUrls` properties of the `@Component` metadata
|
||||
as seen here:
|
||||
|
||||
{@example 'cb-component-relative-paths/ts/app/some.component.ts' -region='absolute-config' -linenums='false' }
|
||||
|
||||
By default, we *must* specify the full path back to the application root.
|
||||
We call this an ***absolute path*** because it is *absolute* with respect to the application root.
|
||||
|
||||
There are two problems with an *absolute path*:
|
||||
|
||||
1. We have to remember the full path back to the application root.
|
||||
|
||||
2. We have to update the URL when we move the component around in the application files structure.
|
||||
|
||||
It would be much easier to write and maintain our application components if we could specify template and style locations
|
||||
*relative* to their component class file.
|
||||
|
||||
*We can!*
|
||||
|
||||
~~~ {.alert.is-important}
|
||||
|
||||
We can if we build our application as `commonjs` modules and load those modules
|
||||
with a suitable package loader such as `systemjs` or `webpack`.
|
||||
Learn why [below](#why-default).
|
||||
|
||||
The Angular CLI uses these technologies and defaults to the
|
||||
*component-relative path* approach described here.
|
||||
CLI users can skip this chapter or read on to understand
|
||||
how it works.
|
||||
|
||||
~~~
|
||||
|
||||
## _Component-Relative_ Paths
|
||||
|
||||
Our goal is to specify template and style URLs *relative* to their component class files,
|
||||
hence the term ***component-relative path***.
|
||||
|
||||
The key to success is following a convention that puts related component files in well-known locations.
|
||||
|
||||
We recommend keeping component template and component-specific style files as *siblings* of their
|
||||
companion component class files.
|
||||
Here we see the three files for `SomeComponent` sitting next to each other in the `app` folder.
|
||||
|
||||
<aio-file-tree>
|
||||
<aio-folder>app
|
||||
<aio-file>some.component.css</aio-file>
|
||||
<aio-file>some.component.html</aio-file>
|
||||
<aio-file>some.component.ts</aio-file>
|
||||
<aio-file>...</aio-file>
|
||||
</aio-folder>
|
||||
</aio-file-tree>
|
||||
|
||||
We'll have more files and folders — and greater folder depth — as our application grows.
|
||||
We'll be fine as long as the component files travel together as the inseparable siblings they are.
|
||||
|
||||
### Set the *moduleId*
|
||||
|
||||
Having adopted this file structure convention, we can specify locations of the template and style files
|
||||
relative to the component class file simply by setting the `moduleId` property of the `@Component` metadata like this
|
||||
|
||||
{@example 'cb-component-relative-paths/ts/app/some.component.ts' -region='module-id' -linenums='false'}
|
||||
|
||||
We strip the `app/` base path from the `templateUrl` and `styleUrls` and replace it with `./`.
|
||||
The result looks like this:
|
||||
|
||||
{@example 'cb-component-relative-paths/ts/app/some.component.ts' -region='relative-config' -linenums='false'}
|
||||
|
||||
~~~ {.alert.is-helpful}
|
||||
|
||||
Webpack users may prefer [an alternative approach](#webpack).
|
||||
|
||||
~~~
|
||||
|
||||
|
||||
## Source
|
||||
|
||||
**We can see the <live-example name="cb-component-relative-paths"></live-example>**
|
||||
and download the source code from there
|
||||
or simply read the pertinent source here.
|
||||
|
||||
<md-tab-group>
|
||||
<md-tab label="app/some.component.ts">
|
||||
{@example 'cb-component-relative-paths/ts/app/some.component.ts'}
|
||||
</md-tab>
|
||||
<md-tab label="app/some.component.html">
|
||||
{@example 'cb-component-relative-paths/ts/app/some.component.html'}
|
||||
</md-tab>
|
||||
<md-tab label="app/some.component.css">
|
||||
{@example 'cb-component-relative-paths/ts/app/some.component.css'}
|
||||
</md-tab>
|
||||
<md-tab label="app/app.component.ts">
|
||||
{@example 'cb-component-relative-paths/ts/app/app.component.ts'}
|
||||
</md-tab>
|
||||
<md-tab-group>
|
||||
|
||||
|
||||
{@a why-default}
|
||||
|
||||
## Appendix: why *component-relative* is not the default
|
||||
|
||||
A *component-relative* path is obviously superior to an *absolute* path.
|
||||
Why did Angular default to the *absolute* path?
|
||||
Why do *we* have to set the `moduleId`? Why can't Angular set it?
|
||||
|
||||
First, let's look at what happens if we use a relative path and omit the `moduleId`.
|
||||
|
||||
`EXCEPTION: Failed to load some.component.html`
|
||||
|
||||
Angular can't find the file so it throws an error.
|
||||
|
||||
Why can't Angular calculate the template and style URLs from the component file's location?
|
||||
|
||||
Because the location of the component can't be determined without the developer's help.
|
||||
Angular apps can be loaded in many ways: from individual files, from SystemJS packages, or
|
||||
from CommonJS packages, to name a few.
|
||||
We might generate modules in any of several formats.
|
||||
We might not be writing modular code at all!
|
||||
|
||||
With this diversity of packaging and module load strategies,
|
||||
it's not possible for Angular to know with certainty where these files reside at runtime.
|
||||
|
||||
The only location Angular can be sure of is the URL of the `index.html` home page, the application root.
|
||||
So by default it resolves template and style paths relative to the URL of `index.html`.
|
||||
That's why we previously wrote our file URLs with an `app/` base path prefix.
|
||||
|
||||
But *if* we follow the recommended guidelines and we write modules in `commonjs` format
|
||||
and we use a module loader that *plays nice*,
|
||||
*then* we — the developers of the application —
|
||||
know that the semi-global `module.id` variable is available and contains
|
||||
the absolute URL of the component class module file.
|
||||
|
||||
That knowledge enables us to tell Angular where the *component* file is
|
||||
by setting the `moduleId`:
|
||||
|
||||
{@example 'cb-component-relative-paths/ts/app/some.component.ts' -region='module-id' -linenums='false'}
|
||||
|
||||
|
||||
{@a webpack}
|
||||
|
||||
## Webpack: load templates and styles
|
||||
Webpack developers have an alternative to `moduleId`.
|
||||
|
||||
They can load templates and styles at runtime by adding `./` at the beginning of the `template` and `styles` / `styleUrls`
|
||||
properties that reference *component-relative URLS.
|
||||
|
||||
{@example 'webpack/ts/src/app/app.component.ts' --linenums='false'}
|
||||
|
||||
Webpack will do a `require` behind the scenes to load the templates and styles. Read more [here](../guide/webpack.html#highlights).
|
||||
|
||||
See the [Introduction to Webpack](../guide/webpack.html).
|
42
aio/content/examples/cb-component-relative-paths/e2e-spec.ts
Normal file
@ -0,0 +1,42 @@
|
||||
'use strict'; // necessary for es6 output in node
|
||||
|
||||
import { browser, element, by, ElementFinder } from 'protractor';
|
||||
|
||||
describe('Cookbook: component-relative paths', function () {
|
||||
|
||||
interface Page {
|
||||
title: ElementFinder;
|
||||
absComp: ElementFinder;
|
||||
relComp: ElementFinder;
|
||||
|
||||
}
|
||||
function getPageStruct() {
|
||||
return {
|
||||
title: element( by.tagName( 'h1' )),
|
||||
absComp: element( by.css( 'absolute-path div' ) ),
|
||||
relComp: element( by.css( 'relative-path div' ) )
|
||||
};
|
||||
}
|
||||
|
||||
let page: Page;
|
||||
beforeAll(function () {
|
||||
browser.get('');
|
||||
page = getPageStruct();
|
||||
});
|
||||
|
||||
it('should display title of the sample', function () {
|
||||
expect(element(by.tagName('h1')).getText()).toContain('Paths');
|
||||
});
|
||||
|
||||
it('should have absolute-path element', function () {
|
||||
expect(page.absComp.isPresent()).toBe(true, 'no <absolute-path> element');
|
||||
});
|
||||
|
||||
it('should display the absolute path text', function () {
|
||||
expect(page.absComp.getText()).toContain('Absolute');
|
||||
});
|
||||
|
||||
it('should display the component-relative path text', function () {
|
||||
expect(page.relComp.getText()).toContain('Component-relative');
|
||||
});
|
||||
});
|
@ -0,0 +1,12 @@
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
template:
|
||||
`<h1>Absolute & <i>Component-Relative</i> Paths</h1>
|
||||
<absolute-path></absolute-path>
|
||||
<relative-path></relative-path>
|
||||
`
|
||||
})
|
||||
export class AppComponent {}
|
@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { SomeAbsoluteComponent, SomeRelativeComponent } from './some.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
SomeAbsoluteComponent,
|
||||
SomeRelativeComponent
|
||||
],
|
||||
bootstrap: [ AppComponent ]
|
||||
})
|
||||
export class AppModule { }
|
@ -0,0 +1,5 @@
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
@ -0,0 +1,22 @@
|
||||
/* #docregion */
|
||||
div.absolute {
|
||||
background: beige;
|
||||
border: 1px solid darkred;
|
||||
color: red;
|
||||
margin: 8px;
|
||||
max-width: 20em;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.relative {
|
||||
background: powderblue;
|
||||
border: 1px solid darkblue;
|
||||
color: Blue;
|
||||
font-style: italic;
|
||||
margin: 8px;
|
||||
max-width: 20em;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -0,0 +1,4 @@
|
||||
<!-- #docregion -->
|
||||
<div class={{class}}>
|
||||
{{type}}<br>{{path}}
|
||||
</div>
|
@ -0,0 +1,37 @@
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
///////// Using Absolute Paths ///////
|
||||
|
||||
// #docregion absolute-config
|
||||
@Component({
|
||||
selector: 'absolute-path',
|
||||
templateUrl: 'app/some.component.html',
|
||||
styleUrls: ['app/some.component.css']
|
||||
})
|
||||
// #enddocregion absolute-config
|
||||
export class SomeAbsoluteComponent {
|
||||
class = 'absolute';
|
||||
type = 'Absolute template & style URLs';
|
||||
path = 'app/path.component.html';
|
||||
}
|
||||
|
||||
///////// Using Relative Paths ///////
|
||||
|
||||
// #docregion relative-config
|
||||
@Component({
|
||||
// #docregion module-id
|
||||
moduleId: module.id,
|
||||
// #enddocregion module-id
|
||||
selector: 'relative-path',
|
||||
templateUrl: './some.component.html',
|
||||
styleUrls: ['./some.component.css']
|
||||
})
|
||||
// #enddocregion relative-config
|
||||
|
||||
export class SomeRelativeComponent {
|
||||
class = 'relative';
|
||||
type = 'Component-relative template & style URLs';
|
||||
path = 'path.component.html';
|
||||
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<base href="/">
|
||||
|
||||
<title>
|
||||
Component-Relative Paths
|
||||
</title>
|
||||
|
||||
<!-- #docregion style -->
|
||||
<link rel="stylesheet" type="text/css" href="styles.css">
|
||||
<!-- #enddocregion style -->
|
||||
|
||||
<!-- Polyfills for older browsers -->
|
||||
<script src="node_modules/core-js/client/shim.min.js"></script>
|
||||
|
||||
<script src="node_modules/zone.js/dist/zone.js"></script>
|
||||
<script src="node_modules/systemjs/dist/system.src.js"></script>
|
||||
|
||||
<script src="systemjs.config.js"></script>
|
||||
<script>
|
||||
System.import('app').catch(function(err){ console.error(err); });
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<my-app>Loading app...</my-app>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"description": "Module-relative Paths",
|
||||
"files": [
|
||||
"!**/*.d.ts",
|
||||
"!**/*.js"
|
||||
],
|
||||
"tags": [ "cookbook" ]
|
||||
}
|
21
aio/content/examples/webpack/e2e-spec.ts
Normal file
@ -0,0 +1,21 @@
|
||||
'use strict'; // necessary for es6 output in node
|
||||
|
||||
import { browser, element, by } from 'protractor';
|
||||
|
||||
describe('QuickStart E2E Tests', function () {
|
||||
|
||||
let expectedMsg = 'Hello from Angular App with Webpack';
|
||||
|
||||
beforeEach(function () {
|
||||
browser.get('');
|
||||
});
|
||||
|
||||
it(`should display: ${expectedMsg}`, function () {
|
||||
expect(element(by.css('h1')).getText()).toEqual(expectedMsg);
|
||||
});
|
||||
|
||||
it('should display an image', function () {
|
||||
expect(element(by.css('img')).isPresent()).toBe(true);
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,58 @@
|
||||
/* tslint:disable */
|
||||
// #docregion one-entry
|
||||
entry: {
|
||||
app: 'src/app.ts'
|
||||
}
|
||||
// #enddocregion one-entry
|
||||
|
||||
// #docregion app-example
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
...
|
||||
})
|
||||
export class AppComponent {}
|
||||
// #enddocregion app-example
|
||||
|
||||
// #docregion one-output
|
||||
output: {
|
||||
filename: 'app.js'
|
||||
}
|
||||
// #enddocregion one-output
|
||||
|
||||
// #docregion two-entries
|
||||
entry: {
|
||||
app: 'src/app.ts',
|
||||
vendor: 'src/vendor.ts'
|
||||
},
|
||||
|
||||
output: {
|
||||
filename: '[name].js'
|
||||
}
|
||||
// #enddocregion two-entries
|
||||
|
||||
// #docregion loaders
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/
|
||||
loader: 'awesome-typescript-loader'
|
||||
},
|
||||
{
|
||||
test: /\.css$/
|
||||
loaders: 'style-loader!css-loader'
|
||||
}
|
||||
]
|
||||
// #enddocregion loaders
|
||||
|
||||
// #docregion imports
|
||||
// #docregion single-import
|
||||
import { AppComponent } from './app.component.ts';
|
||||
// #enddocregion single-import
|
||||
import 'uiframework/dist/uiframework.css';
|
||||
// #enddocregion imports
|
||||
|
||||
// #docregion plugins
|
||||
plugins: [
|
||||
new webpack.optimize.UglifyJsPlugin()
|
||||
]
|
||||
// #enddocregion plugins
|
5
aio/content/examples/webpack/ts/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
dist
|
||||
!karma.webpack.conf.js
|
||||
!webpack.config.js
|
||||
!config/*
|
||||
!public/css/styles.css
|
12
aio/content/examples/webpack/ts/config/helpers.js
Normal file
@ -0,0 +1,12 @@
|
||||
// #docregion
|
||||
var path = require('path');
|
||||
|
||||
var _root = path.resolve(__dirname, '..');
|
||||
|
||||
function root(args) {
|
||||
args = Array.prototype.slice.call(arguments, 0);
|
||||
return path.join.apply(path, [_root].concat(args));
|
||||
}
|
||||
|
||||
exports.root = root;
|
||||
// #enddocregion
|
22
aio/content/examples/webpack/ts/config/karma-test-shim.js
Normal file
@ -0,0 +1,22 @@
|
||||
// #docregion
|
||||
Error.stackTraceLimit = Infinity;
|
||||
|
||||
require('core-js/es6');
|
||||
require('core-js/es7/reflect');
|
||||
|
||||
require('zone.js/dist/zone');
|
||||
require('zone.js/dist/long-stack-trace-zone');
|
||||
require('zone.js/dist/proxy');
|
||||
require('zone.js/dist/sync-test');
|
||||
require('zone.js/dist/jasmine-patch');
|
||||
require('zone.js/dist/async-test');
|
||||
require('zone.js/dist/fake-async-test');
|
||||
|
||||
var appContext = require.context('../src', true, /\.spec\.ts/);
|
||||
|
||||
appContext.keys().forEach(appContext);
|
||||
|
||||
var testing = require('@angular/core/testing');
|
||||
var browser = require('@angular/platform-browser-dynamic/testing');
|
||||
|
||||
testing.TestBed.initTestEnvironment(browser.BrowserDynamicTestingModule, browser.platformBrowserDynamicTesting());
|
39
aio/content/examples/webpack/ts/config/karma.conf.js
Normal file
@ -0,0 +1,39 @@
|
||||
// #docregion
|
||||
var webpackConfig = require('./webpack.test');
|
||||
|
||||
module.exports = function (config) {
|
||||
var _config = {
|
||||
basePath: '',
|
||||
|
||||
frameworks: ['jasmine'],
|
||||
|
||||
files: [
|
||||
{pattern: './config/karma-test-shim.js', watched: false}
|
||||
],
|
||||
|
||||
preprocessors: {
|
||||
'./config/karma-test-shim.js': ['webpack', 'sourcemap']
|
||||
},
|
||||
|
||||
webpack: webpackConfig,
|
||||
|
||||
webpackMiddleware: {
|
||||
stats: 'errors-only'
|
||||
},
|
||||
|
||||
webpackServer: {
|
||||
noInfo: true
|
||||
},
|
||||
|
||||
reporters: ['progress'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: false,
|
||||
browsers: ['PhantomJS'],
|
||||
singleRun: true
|
||||
};
|
||||
|
||||
config.set(_config);
|
||||
};
|
||||
// #enddocregion
|
72
aio/content/examples/webpack/ts/config/webpack.common.js
Normal file
@ -0,0 +1,72 @@
|
||||
// #docregion
|
||||
var webpack = require('webpack');
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
var ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
var helpers = require('./helpers');
|
||||
|
||||
module.exports = {
|
||||
// #docregion entries
|
||||
entry: {
|
||||
'polyfills': './src/polyfills.ts',
|
||||
'vendor': './src/vendor.ts',
|
||||
'app': './src/main.ts'
|
||||
},
|
||||
// #enddocregion
|
||||
|
||||
// #docregion resolve
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js']
|
||||
},
|
||||
// #enddocregion resolve
|
||||
|
||||
// #docregion loaders
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
loaders: ['awesome-typescript-loader', 'angular2-template-loader']
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
loader: 'html-loader'
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
|
||||
loader: 'file-loader?name=assets/[name].[hash].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
exclude: helpers.root('src', 'app'),
|
||||
loader: ExtractTextPlugin.extract({ fallbackLoader: 'style-loader', loader: 'css-loader?sourceMap' })
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
include: helpers.root('src', 'app'),
|
||||
loader: 'raw-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
// #enddocregion loaders
|
||||
|
||||
// #docregion plugins
|
||||
plugins: [
|
||||
// Workaround for angular/angular#11580
|
||||
new webpack.ContextReplacementPlugin(
|
||||
// The (\\|\/) piece accounts for path separators in *nix and Windows
|
||||
/angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/,
|
||||
helpers.root('./src'), // location of your src
|
||||
{} // a map of your routes
|
||||
),
|
||||
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: ['app', 'vendor', 'polyfills']
|
||||
}),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'src/index.html'
|
||||
})
|
||||
]
|
||||
// #enddocregion plugins
|
||||
};
|
||||
// #enddocregion
|
||||
|
26
aio/content/examples/webpack/ts/config/webpack.dev.js
Normal file
@ -0,0 +1,26 @@
|
||||
// #docregion
|
||||
var webpackMerge = require('webpack-merge');
|
||||
var ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
var commonConfig = require('./webpack.common.js');
|
||||
var helpers = require('./helpers');
|
||||
|
||||
module.exports = webpackMerge(commonConfig, {
|
||||
devtool: 'cheap-module-eval-source-map',
|
||||
|
||||
output: {
|
||||
path: helpers.root('dist'),
|
||||
publicPath: 'http://localhost:8080/',
|
||||
filename: '[name].js',
|
||||
chunkFilename: '[id].chunk.js'
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new ExtractTextPlugin('[name].css')
|
||||
],
|
||||
|
||||
devServer: {
|
||||
historyApiFallback: true,
|
||||
stats: 'minimal'
|
||||
}
|
||||
});
|
||||
// #enddocregion
|
41
aio/content/examples/webpack/ts/config/webpack.prod.js
Normal file
@ -0,0 +1,41 @@
|
||||
// #docregion
|
||||
var webpack = require('webpack');
|
||||
var webpackMerge = require('webpack-merge');
|
||||
var ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
var commonConfig = require('./webpack.common.js');
|
||||
var helpers = require('./helpers');
|
||||
|
||||
const ENV = process.env.NODE_ENV = process.env.ENV = 'production';
|
||||
|
||||
module.exports = webpackMerge(commonConfig, {
|
||||
devtool: 'source-map',
|
||||
|
||||
output: {
|
||||
path: helpers.root('dist'),
|
||||
publicPath: '/',
|
||||
filename: '[name].[hash].js',
|
||||
chunkFilename: '[id].[hash].chunk.js'
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new webpack.optimize.UglifyJsPlugin({ // https://github.com/angular/angular/issues/10618
|
||||
mangle: {
|
||||
keep_fnames: true
|
||||
}
|
||||
}),
|
||||
new ExtractTextPlugin('[name].[hash].css'),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
'ENV': JSON.stringify(ENV)
|
||||
}
|
||||
}),
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
htmlLoader: {
|
||||
minimize: false // workaround for ng2
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
// #enddocregion
|
50
aio/content/examples/webpack/ts/config/webpack.test.js
Normal file
@ -0,0 +1,50 @@
|
||||
// #docregion
|
||||
var webpack = require('webpack');
|
||||
var helpers = require('./helpers');
|
||||
|
||||
module.exports = {
|
||||
devtool: 'inline-source-map',
|
||||
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js']
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
loaders: ['awesome-typescript-loader', 'angular2-template-loader']
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
loader: 'html-loader'
|
||||
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
|
||||
loader: 'null-loader'
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
exclude: helpers.root('src', 'app'),
|
||||
loader: 'null-loader'
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
include: helpers.root('src', 'app'),
|
||||
loader: 'raw-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.ContextReplacementPlugin(
|
||||
// The (\\|\/) piece accounts for path separators in *nix and Windows
|
||||
/angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/,
|
||||
helpers.root('./src'), // location of your src
|
||||
{} // a map of your routes
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
// #enddocregion
|
4
aio/content/examples/webpack/ts/example-config.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"build": "build:webpack",
|
||||
"run": "http-server:cli"
|
||||
}
|
2
aio/content/examples/webpack/ts/karma.webpack.conf.js
Normal file
@ -0,0 +1,2 @@
|
||||
// #docregion
|
||||
module.exports = require('./config/karma.conf.js');
|
50
aio/content/examples/webpack/ts/package.webpack.json
Normal file
@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "angular2-webpack",
|
||||
"version": "1.0.0",
|
||||
"description": "A webpack starter for Angular",
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server --inline --progress --port 8080",
|
||||
"test": "karma start",
|
||||
"build": "rimraf dist && webpack --config config/webpack.prod.js --progress --profile --bail"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular/common": "~2.4.0",
|
||||
"@angular/compiler": "~2.4.0",
|
||||
"@angular/core": "~2.4.0",
|
||||
"@angular/forms": "~2.4.0",
|
||||
"@angular/http": "~2.4.0",
|
||||
"@angular/platform-browser": "~2.4.0",
|
||||
"@angular/platform-browser-dynamic": "~2.4.0",
|
||||
"@angular/router": "~3.4.0",
|
||||
"core-js": "^2.4.1",
|
||||
"rxjs": "5.0.1",
|
||||
"zone.js": "^0.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^6.0.45",
|
||||
"@types/jasmine": "^2.5.35",
|
||||
"angular2-template-loader": "^0.6.0",
|
||||
"awesome-typescript-loader": "^3.0.0-beta.18",
|
||||
"css-loader": "^0.26.1",
|
||||
"extract-text-webpack-plugin": "2.0.0-beta.5",
|
||||
"file-loader": "^0.9.0",
|
||||
"html-loader": "^0.4.3",
|
||||
"html-webpack-plugin": "^2.16.1",
|
||||
"jasmine-core": "^2.4.1",
|
||||
"karma": "^1.2.0",
|
||||
"karma-jasmine": "^1.0.2",
|
||||
"karma-phantomjs-launcher": "^1.0.2",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-webpack": "^2.0.1",
|
||||
"null-loader": "^0.1.1",
|
||||
"phantomjs-prebuilt": "^2.1.7",
|
||||
"raw-loader": "^0.5.1",
|
||||
"rimraf": "^2.5.2",
|
||||
"style-loader": "^0.13.1",
|
||||
"typescript": "~2.0.10",
|
||||
"webpack": "2.2.0",
|
||||
"webpack-dev-server": "2.2.0-rc.0",
|
||||
"webpack-merge": "^2.4.0"
|
||||
}
|
||||
}
|
6
aio/content/examples/webpack/ts/public/css/styles.css
Normal file
@ -0,0 +1,6 @@
|
||||
/* #docregion */
|
||||
body {
|
||||
background: #0147A7;
|
||||
color: #fff;
|
||||
}
|
||||
/* #enddocregion */
|
BIN
aio/content/examples/webpack/ts/public/images/angular.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
@ -0,0 +1,9 @@
|
||||
/* #docregion */
|
||||
main {
|
||||
padding: 1em;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
text-align: center;
|
||||
margin-top: 50px;
|
||||
display: block;
|
||||
}
|
||||
/* #enddocregion */
|
@ -0,0 +1,7 @@
|
||||
<!-- #docregion -->
|
||||
<main>
|
||||
<h1>Hello from Angular App with Webpack</h1>
|
||||
|
||||
<img src="../../public/images/angular.png">
|
||||
</main>
|
||||
<!-- #enddocregion -->
|
@ -0,0 +1,16 @@
|
||||
// #docregion
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('App', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({ declarations: [AppComponent]});
|
||||
});
|
||||
|
||||
it ('should work', () => {
|
||||
let fixture = TestBed.createComponent(AppComponent);
|
||||
expect(fixture.componentInstance instanceof AppComponent).toBe(true, 'should create AppComponent');
|
||||
});
|
||||
});
|
||||
// #enddocregion
|
12
aio/content/examples/webpack/ts/src/app/app.component.ts
Normal file
@ -0,0 +1,12 @@
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import '../../public/css/styles.css';
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent { }
|
||||
// #enddocregion
|
16
aio/content/examples/webpack/ts/src/app/app.module.ts
Normal file
@ -0,0 +1,16 @@
|
||||
// #docregion
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule
|
||||
],
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
bootstrap: [ AppComponent ]
|
||||
})
|
||||
export class AppModule { }
|
14
aio/content/examples/webpack/ts/src/index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!-- #docregion -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<base href="/">
|
||||
<title>Angular With Webpack</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<my-app>Loading...</my-app>
|
||||
</body>
|
||||
</html>
|
||||
<!-- #enddocregion -->
|
14
aio/content/examples/webpack/ts/src/main.ts
Normal file
@ -0,0 +1,14 @@
|
||||
// #docregion
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { enableProdMode } from '@angular/core';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
|
||||
// #docregion enable-prod
|
||||
if (process.env.ENV === 'production') {
|
||||
enableProdMode();
|
||||
}
|
||||
// #enddocregion enable-prod
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
// #enddocregion
|
12
aio/content/examples/webpack/ts/src/polyfills.ts
Normal file
@ -0,0 +1,12 @@
|
||||
// #docregion
|
||||
import 'core-js/es6';
|
||||
import 'core-js/es7/reflect';
|
||||
require('zone.js/dist/zone');
|
||||
|
||||
if (process.env.ENV === 'production') {
|
||||
// Production
|
||||
} else {
|
||||
// Development and test
|
||||
Error['stackTraceLimit'] = Infinity;
|
||||
require('zone.js/dist/long-stack-trace-zone');
|
||||
}
|
15
aio/content/examples/webpack/ts/src/vendor.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// #docregion
|
||||
// Angular
|
||||
import '@angular/platform-browser';
|
||||
import '@angular/platform-browser-dynamic';
|
||||
import '@angular/core';
|
||||
import '@angular/common';
|
||||
import '@angular/http';
|
||||
import '@angular/router';
|
||||
|
||||
// RxJS
|
||||
import 'rxjs';
|
||||
|
||||
// Other vendors for example jQuery, Lodash or Bootstrap
|
||||
// You can import js, ts, css, sass, ...
|
||||
// #enddocregion
|
13
aio/content/examples/webpack/ts/tsconfig.1.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"lib": ["es2015", "dom"],
|
||||
"noImplicitAny": true,
|
||||
"suppressImplicitAnyIndexErrors": true
|
||||
}
|
||||
}
|
6
aio/database.rules.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"rules": {
|
||||
".read": "auth != null",
|
||||
".write": "auth != null"
|
||||
}
|
||||
}
|
36
aio/e2e/app.e2e-spec.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { SitePage } from './app.po';
|
||||
|
||||
describe('site App', function() {
|
||||
let page: SitePage;
|
||||
|
||||
beforeAll(done => {
|
||||
// Hack: CI has been failing on first test so
|
||||
// buying time by giving the browser a wake-up call.
|
||||
// Todo: Find and fix the root cause for flakes.
|
||||
new SitePage().navigateTo().then(done);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
page = new SitePage();
|
||||
});
|
||||
|
||||
it('should show features text after clicking "Features"', () => {
|
||||
page.navigateTo()
|
||||
.then(() => {
|
||||
return page.featureLink.click();
|
||||
})
|
||||
.then(() => {
|
||||
expect(page.getDocViewerText()).toContain('Progressive web apps');
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert code-example in pipe.html', () => {
|
||||
page.navigateTo()
|
||||
.then(() => {
|
||||
return page.datePipeLink.click();
|
||||
})
|
||||
.then(() => {
|
||||
expect(page.codeExample.count()).toBeGreaterThan(0, 'should have code-example content');
|
||||
});
|
||||
});
|
||||
});
|
19
aio/e2e/app.po.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { browser, element, by } from 'protractor';
|
||||
|
||||
export class SitePage {
|
||||
|
||||
links = element.all(by.css('md-toolbar a'));
|
||||
datePipeLink = element(by.css('md-toolbar a[aioNavLink="docs/api/common/DatePipe"]'));
|
||||
docViewer = element(by.css('aio-doc-viewer'));
|
||||
codeExample = element.all(by.css('aio-doc-viewer pre > code'));
|
||||
featureLink = element(by.css('md-toolbar a[aioNavLink="features"]'));
|
||||
|
||||
navigateTo() {
|
||||
return browser.get('/');
|
||||
}
|
||||
|
||||
getDocViewerText() {
|
||||
return this.docViewer.getText();
|
||||
}
|
||||
|
||||
}
|
16
aio/e2e/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"declaration": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "../dist/out-tsc-e2e",
|
||||
"sourceMap": true,
|
||||
"target": "es5",
|
||||
"typeRoots": [
|
||||
"../node_modules/@types"
|
||||
]
|
||||
}
|
||||
}
|
14
aio/firebase.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"database": {
|
||||
"rules": "database.rules.json"
|
||||
},
|
||||
"hosting": {
|
||||
"public": "dist",
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
34
aio/gulpfile.js
Normal file
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// THIS CHECK SHOULD BE THE FIRST THING IN THIS FILE
|
||||
// This is to ensure that we catch env issues before we error while requiring other dependencies.
|
||||
// NOTE: we are getting the value from the parent `angular/angular` package.json not the `/aio` one.
|
||||
const engines = require('../package.json').engines;
|
||||
require('../tools/check-environment')({
|
||||
requiredNpmVersion: engines.npm,
|
||||
requiredNodeVersion: engines.node
|
||||
});
|
||||
|
||||
const gulp = require('gulp');
|
||||
|
||||
// See `tools/gulp-tasks/README.md` for information about task loading.
|
||||
function loadTask(fileName, taskName) {
|
||||
const taskModule = require('./build/' + fileName);
|
||||
const task = taskName ? taskModule[taskName] : taskModule;
|
||||
return task(gulp);
|
||||
}
|
||||
|
||||
gulp.task('docs', ['doc-gen', 'docs-app']);
|
||||
gulp.task('doc-gen', loadTask('docs', 'generate'));
|
||||
gulp.task('doc-gen-test', loadTask('docs', 'test'));
|
||||
gulp.task('docs-app', loadTask('docs-app'));
|
||||
gulp.task('docs-app-test', () => {});
|
||||
gulp.task('docs-test', ['doc-gen-test', 'docs-app-test']);
|
42
aio/karma.conf.js
Normal file
@ -0,0 +1,42 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/0.13/config/configuration-file.html
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular/cli'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-remap-istanbul'),
|
||||
require('@angular/cli/plugins/karma')
|
||||
],
|
||||
files: [
|
||||
{ pattern: './src/test.ts', watched: false }
|
||||
],
|
||||
preprocessors: {
|
||||
'./src/test.ts': ['@angular/cli']
|
||||
},
|
||||
mime: {
|
||||
'text/x-typescript': ['ts','tsx']
|
||||
},
|
||||
remapIstanbulReporter: {
|
||||
reports: {
|
||||
html: 'coverage',
|
||||
lcovonly: './coverage/coverage.lcov'
|
||||
}
|
||||
},
|
||||
angularCli: {
|
||||
config: './angular-cli.json',
|
||||
environment: 'dev'
|
||||
},
|
||||
reporters: config.angularCli && config.angularCli.codeCoverage
|
||||
? ['progress', 'karma-remap-istanbul']
|
||||
: ['progress'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false
|
||||
});
|
||||
};
|
63
aio/package.json
Normal file
@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "angular.io",
|
||||
"version": "0.0.0",
|
||||
"main": "index.js",
|
||||
"repository": "git@github.com:angular/angular.git",
|
||||
"author": "Angular",
|
||||
"license": "MIT",
|
||||
"angular-cli": {},
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"lint": "tslint \"src/**/*.ts\" --project src/tsconfig.json --type-check && tslint \"e2e/**/*.ts\" --project e2e/tsconfig.json --type-check",
|
||||
"test": "ng test",
|
||||
"pree2e": "webdriver-manager update --standalone false --gecko false",
|
||||
"e2e": "protractor",
|
||||
"deploy-staging": "firebase use staging --token \"$FIREBASE_TOKEN\" && yarn run ~~deploy",
|
||||
"pre~~deploy": "ng build --prod",
|
||||
"~~deploy": "firebase deploy --message \"Commit: $TRAVIS_COMMIT\" --non-interactive --token \"$FIREBASE_TOKEN\""
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/cli": "^1.0.0-beta.29",
|
||||
"@angular/common": "^2.3.1",
|
||||
"@angular/compiler": "^2.3.1",
|
||||
"@angular/core": "^2.3.1",
|
||||
"@angular/forms": "^2.3.1",
|
||||
"@angular/http": "^2.3.1",
|
||||
"@angular/material": "^2.0.0-beta.1",
|
||||
"@angular/platform-browser": "^2.3.1",
|
||||
"@angular/platform-browser-dynamic": "^2.3.1",
|
||||
"@angular/router": "^3.3.1",
|
||||
"core-js": "^2.4.1",
|
||||
"rxjs": "^5.0.1",
|
||||
"ts-helpers": "^1.1.1",
|
||||
"zone.js": "^0.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/compiler-cli": "^2.3.1",
|
||||
"@types/jasmine": "2.5.38",
|
||||
"@types/node": "^6.0.42",
|
||||
"canonical-path": "^0.0.2",
|
||||
"codelyzer": "~2.0.0-beta.1",
|
||||
"dgeni": "^0.4.2",
|
||||
"dgeni-packages": "^0.16.5",
|
||||
"entities": "^1.1.1",
|
||||
"firebase-tools": "^3.2.1",
|
||||
"gulp": "^3.9.1",
|
||||
"jasmine-core": "2.5.2",
|
||||
"jasmine-spec-reporter": "2.5.0",
|
||||
"karma": "1.2.0",
|
||||
"karma-chrome-launcher": "^2.0.0",
|
||||
"karma-cli": "^1.0.1",
|
||||
"karma-jasmine": "^1.0.2",
|
||||
"karma-remap-istanbul": "^0.2.1",
|
||||
"lodash": "^4.17.4",
|
||||
"protractor": "~4.0.13",
|
||||
"rho": "^0.3.0",
|
||||
"ts-node": "1.2.1",
|
||||
"tslint": "^4.3.0",
|
||||
"typescript": "2.0.10"
|
||||
}
|
||||
}
|
36
aio/protractor.conf.js
Normal file
@ -0,0 +1,36 @@
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
/*global jasmine */
|
||||
var SpecReporter = require('jasmine-spec-reporter');
|
||||
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
getPageTimeout: 30000,
|
||||
specs: [
|
||||
'./e2e/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
chromeOptions: {
|
||||
binary: process.env.CHROME_BIN
|
||||
}
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
useAllAngular2AppRoots: true,
|
||||
beforeLaunch: function() {
|
||||
require('ts-node').register({
|
||||
project: 'e2e'
|
||||
});
|
||||
},
|
||||
onPrepare: function() {
|
||||
jasmine.getEnv().addReporter(new SpecReporter());
|
||||
}
|
||||
};
|
11
aio/src/app/app.component.html
Normal file
@ -0,0 +1,11 @@
|
||||
<md-toolbar color="primary" class="app-toolbar">
|
||||
<span>Angular</span>
|
||||
<span><a class="nav-link" aioNavLink="home"> Home </a></span>
|
||||
<span><a class="nav-link" aioNavLink="news"> News</a></span>
|
||||
<span><a class="nav-link" aioNavLink="features"> Features</a></span>
|
||||
<span><a class="nav-link" aioNavLink="docs/api/common/DatePipe"> DatePipe</a></span>
|
||||
<span class="fill-remaining-space"></span>
|
||||
</md-toolbar>
|
||||
<section class="app-content">
|
||||
<aio-doc-viewer [doc]="navEngine.currentDoc"></aio-doc-viewer>
|
||||
</section>
|
8
aio/src/app/app.component.scss
Normal file
@ -0,0 +1,8 @@
|
||||
.fill-remaining-space {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.nav-link {
|
||||
margin-right: 10px;
|
||||
margin-left: 20px;
|
||||
cursor: pointer;
|
||||
}
|
29
aio/src/app/app.component.spec.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
import { NavEngine } from './nav-engine/nav-engine.service';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let component: AppComponent;
|
||||
let fixture: ComponentFixture<AppComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ AppModule ],
|
||||
providers: [
|
||||
{ provide: NavEngine, useValue: { currentDoc: undefined } }
|
||||
]
|
||||
});
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AppComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeDefined();
|
||||
});
|
||||
});
|
12
aio/src/app/app.component.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { NavEngine } from './nav-engine/nav-engine.service';
|
||||
|
||||
@Component({
|
||||
selector: 'aio-shell',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent {
|
||||
constructor(public navEngine: NavEngine) {}
|
||||
}
|
35
aio/src/app/app.module.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { HttpModule } from '@angular/http';
|
||||
import { MdToolbarModule } from '@angular/material/toolbar';
|
||||
import { MdButtonModule} from '@angular/material/button';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { DocViewerComponent } from './doc-viewer/doc-viewer.component';
|
||||
import { embeddedComponents, EmbeddedComponents } from './embedded';
|
||||
import { Logger } from './logger.service';
|
||||
import { navDirectives, navProviders } from './nav-engine';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
HttpModule,
|
||||
MdToolbarModule.forRoot(),
|
||||
MdButtonModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
embeddedComponents,
|
||||
DocViewerComponent,
|
||||
navDirectives,
|
||||
],
|
||||
providers: [
|
||||
EmbeddedComponents,
|
||||
Logger,
|
||||
navProviders
|
||||
],
|
||||
entryComponents: [ embeddedComponents ],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
308
aio/src/app/doc-viewer/doc-viewer.component.spec.ts
Normal file
@ -0,0 +1,308 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Component, DebugElement } from '@angular/core';
|
||||
|
||||
import { ComponentFactoryResolver, ElementRef, Injector, NgModule, OnInit, ViewChild } from '@angular/core';
|
||||
|
||||
import { Doc, DocMetadata } from '../nav-engine';
|
||||
import { DocViewerComponent } from '../doc-viewer/doc-viewer.component';
|
||||
|
||||
import { embeddedComponents, EmbeddedComponents } from '../embedded';
|
||||
|
||||
|
||||
/// Embedded Test Components ///
|
||||
|
||||
///// FooComponent /////
|
||||
|
||||
@Component({
|
||||
selector: 'aio-foo',
|
||||
template: `Foo Component`
|
||||
})
|
||||
class FooComponent {
|
||||
|
||||
}
|
||||
|
||||
///// BarComponent /////
|
||||
|
||||
@Component({
|
||||
selector: 'aio-bar',
|
||||
template: `
|
||||
<hr>
|
||||
<h2>Bar Component</h2>
|
||||
<p #barContent></p>
|
||||
<hr>
|
||||
`
|
||||
})
|
||||
class BarComponent implements OnInit {
|
||||
|
||||
@ViewChild('barContent') barContentRef: ElementRef;
|
||||
|
||||
constructor(public elementRef: ElementRef) { }
|
||||
|
||||
// Project content in ngOnInit just like CodeExampleComponent
|
||||
ngOnInit() {
|
||||
// Security: this is a test component; never deployed
|
||||
this.barContentRef.nativeElement.innerHTML = this.elementRef.nativeElement.aioBarContent;
|
||||
}
|
||||
}
|
||||
|
||||
///// BazComponent /////
|
||||
|
||||
@Component({
|
||||
selector: 'aio-baz',
|
||||
template: `
|
||||
<div>++++++++++++++</div>
|
||||
<h2>Baz Component</h2>
|
||||
<p #bazContent></p>
|
||||
<div>++++++++++++++</div>
|
||||
`
|
||||
})
|
||||
class BazComponent implements OnInit {
|
||||
|
||||
@ViewChild('bazContent') bazContentRef: ElementRef;
|
||||
|
||||
constructor(public elementRef: ElementRef) { }
|
||||
|
||||
// Project content in ngOnInit just like CodeExampleComponent
|
||||
ngOnInit() {
|
||||
// Security: this is a test component; never deployed
|
||||
this.bazContentRef.nativeElement.innerHTML = this.elementRef.nativeElement.aioBazContent;
|
||||
}
|
||||
}
|
||||
///// Test Module //////
|
||||
|
||||
const embeddedTestComponents = [FooComponent, BarComponent, BazComponent, ...embeddedComponents];
|
||||
|
||||
@NgModule({
|
||||
entryComponents: embeddedTestComponents
|
||||
})
|
||||
class TestModule { }
|
||||
|
||||
//// Test Component //////
|
||||
|
||||
@Component({
|
||||
selector: 'aio-test',
|
||||
template: `
|
||||
<aio-doc-viewer>Test Component</aio-doc-viewer>
|
||||
`
|
||||
})
|
||||
class TestComponent {
|
||||
private currentDoc: Doc;
|
||||
|
||||
@ViewChild(DocViewerComponent) docViewer: DocViewerComponent;
|
||||
|
||||
setDoc(doc: Doc) {
|
||||
if (this.docViewer) {
|
||||
this.docViewer.doc = doc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////// Tests //////////////
|
||||
|
||||
describe('DocViewerComponent', () => {
|
||||
const mockDocMetadata: DocMetadata = { id: 'mock', title: 'Mock Doc', url: '' };
|
||||
let component: TestComponent;
|
||||
let docViewerDE: DebugElement;
|
||||
let docViewerEl: HTMLElement;
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ TestModule ],
|
||||
declarations: [
|
||||
TestComponent,
|
||||
DocViewerComponent,
|
||||
embeddedTestComponents
|
||||
],
|
||||
providers: [
|
||||
{provide: EmbeddedComponents, useValue: {components: embeddedTestComponents}}
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
docViewerDE = fixture.debugElement.children[0];
|
||||
docViewerEl = docViewerDE.nativeElement;
|
||||
});
|
||||
|
||||
it('should create a DocViewer', () => {
|
||||
expect(component.docViewer).toBeTruthy();
|
||||
});
|
||||
|
||||
it(('should display nothing when set DocViewer.doc to doc w/o content'), () => {
|
||||
component.docViewer.doc = { metadata: mockDocMetadata, content: '' };
|
||||
expect(docViewerEl.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it(('should display simple static content doc'), () => {
|
||||
const content = '<p>Howdy, doc viewer</p>';
|
||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
||||
expect(docViewerEl.innerHTML).toEqual(content);
|
||||
});
|
||||
|
||||
it(('should display nothing after reset static content doc'), () => {
|
||||
const content = '<p>Howdy, doc viewer</p>';
|
||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
||||
fixture.detectChanges();
|
||||
component.docViewer.doc = { metadata: mockDocMetadata, content: '' };
|
||||
expect(docViewerEl.innerHTML).toEqual('');
|
||||
});
|
||||
|
||||
it(('should apply FooComponent'), () => {
|
||||
const content = `
|
||||
<p>Above Foo</p>
|
||||
<p><aio-foo></aio-foo></p>
|
||||
<p>Below Foo</p>
|
||||
`;
|
||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
||||
const fooHtml = docViewerEl.querySelector('aio-foo').innerHTML;
|
||||
expect(fooHtml).toContain('Foo Component');
|
||||
});
|
||||
|
||||
it(('should apply multiple FooComponents'), () => {
|
||||
const content = `
|
||||
<p>Above Foo</p>
|
||||
<p><aio-foo></aio-foo></p>
|
||||
<div style="margin-left: 2em;">
|
||||
Holds a
|
||||
<aio-foo>Ignored text</aio-foo>
|
||||
</div>
|
||||
<p>Below Foo</p>
|
||||
`;
|
||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
||||
const foos = docViewerEl.querySelectorAll('aio-foo');
|
||||
expect(foos.length).toBe(2);
|
||||
});
|
||||
|
||||
it(('should apply BarComponent'), () => {
|
||||
const content = `
|
||||
<p>Above Bar</p>
|
||||
<aio-bar></aio-bar>
|
||||
<p>Below Bar</p>
|
||||
`;
|
||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
||||
const barHtml = docViewerEl.querySelector('aio-bar').innerHTML;
|
||||
expect(barHtml).toContain('Bar Component');
|
||||
});
|
||||
|
||||
it(('should project bar content into BarComponent'), () => {
|
||||
const content = `
|
||||
<p>Above Bar</p>
|
||||
<aio-bar>###bar content###</aio-bar>
|
||||
<p>Below Bar</p>
|
||||
`;
|
||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
||||
|
||||
// necessary to trigger projection within ngOnInit
|
||||
fixture.detectChanges();
|
||||
|
||||
const barHtml = docViewerEl.querySelector('aio-bar').innerHTML;
|
||||
expect(barHtml).toContain('###bar content###');
|
||||
});
|
||||
|
||||
|
||||
it(('should include Foo and Bar'), () => {
|
||||
const content = `
|
||||
<p>Top</p>
|
||||
<p><aio-foo>ignored</aio-foo></p>
|
||||
<aio-bar>###bar content###</aio-bar>
|
||||
<p><aio-foo></aio-foo></p>
|
||||
<p>Bottom</p>
|
||||
`;
|
||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
||||
|
||||
// necessary to trigger Bar's projection within ngOnInit
|
||||
fixture.detectChanges();
|
||||
|
||||
const foos = docViewerEl.querySelectorAll('aio-foo');
|
||||
expect(foos.length).toBe(2, 'should have 2 foos');
|
||||
|
||||
const barHtml = docViewerEl.querySelector('aio-bar').innerHTML;
|
||||
expect(barHtml).toContain('###bar content###', 'should have bar with projected content');
|
||||
});
|
||||
|
||||
it(('should not include Bar within Foo'), () => {
|
||||
const content = `
|
||||
<p>Top</p>
|
||||
<div>
|
||||
<aio-foo>
|
||||
<aio-bar>###bar content###</aio-bar>
|
||||
</aio-foo>
|
||||
</div>
|
||||
<p><aio-foo></aio-foo><p>
|
||||
<p>Bottom</p>
|
||||
`;
|
||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
||||
|
||||
// necessary to trigger Bar's projection within ngOnInit
|
||||
fixture.detectChanges();
|
||||
|
||||
const foos = docViewerEl.querySelectorAll('aio-foo');
|
||||
expect(foos.length).toBe(2, 'should have 2 foos');
|
||||
|
||||
const bars = docViewerEl.querySelectorAll('aio-bar');
|
||||
expect(bars.length).toBe(0, 'did not expect Bar inside Foo');
|
||||
});
|
||||
|
||||
// because FooComponents are processed before BazComponents
|
||||
it(('should include Foo within Bar'), () => {
|
||||
const content = `
|
||||
<p>Top</p>
|
||||
<aio-bar>
|
||||
<div style="margin-left: 2em">
|
||||
Inner <aio-foo></aio-foo>
|
||||
</div>
|
||||
</aio-bar>
|
||||
<p><aio-foo></aio-foo></p>
|
||||
<p>Bottom</p>
|
||||
`;
|
||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
||||
|
||||
// necessary to trigger Bar's projection within ngOnInit
|
||||
fixture.detectChanges();
|
||||
|
||||
const foos = docViewerEl.querySelectorAll('aio-foo');
|
||||
expect(foos.length).toBe(2, 'should have 2 foos');
|
||||
|
||||
const bars = docViewerEl.querySelectorAll('aio-bar');
|
||||
expect(bars.length).toBe(1, 'should have a bar');
|
||||
expect(bars[0].innerHTML).toContain('Bar Component', 'should have bar template content');
|
||||
});
|
||||
|
||||
// The <aio-baz> tag and its inner content is copied
|
||||
// But the BazComponent is not created and therefore its template content is not displayed
|
||||
// because BarComponents are processed before BazComponents
|
||||
// and no chance for first Baz inside Bar to be processed by builder.
|
||||
it(('should NOT include Bar within Baz'), () => {
|
||||
const content = `
|
||||
<p>Top</p>
|
||||
<aio-bar>
|
||||
<div style="margin-left: 2em">
|
||||
Inner <aio-baz>---baz stuff---</aio-baz>
|
||||
</div>
|
||||
</aio-bar>
|
||||
<p><aio-baz>---More baz--</aio-baz></p>
|
||||
<p>Bottom</p>
|
||||
`;
|
||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
||||
|
||||
// necessary to trigger Bar's projection within ngOnInit
|
||||
fixture.detectChanges();
|
||||
const bazs = docViewerEl.querySelectorAll('aio-baz');
|
||||
|
||||
// Both baz tags are there ...
|
||||
expect(bazs.length).toBe(2, 'should have 2 bazs');
|
||||
|
||||
expect(bazs[0].innerHTML).not.toContain('Baz Component',
|
||||
'did not expect 1st Baz template content');
|
||||
|
||||
expect(bazs[1].innerHTML).toContain('Baz Component',
|
||||
'expected 2nd Baz template content');
|
||||
|
||||
});
|
||||
});
|
130
aio/src/app/doc-viewer/doc-viewer.component.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import {
|
||||
Component, ComponentFactory, ComponentFactoryResolver, ComponentRef,
|
||||
DoCheck, ElementRef, Injector, Input, OnDestroy, ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
|
||||
import { Doc, DocMetadata } from '../nav-engine';
|
||||
import { EmbeddedComponents } from '../embedded';
|
||||
|
||||
interface EmbeddedComponentFactory {
|
||||
contentPropertyName: string;
|
||||
factory: ComponentFactory<any>;
|
||||
}
|
||||
|
||||
// Initialization prevents flicker once pre-rendering is on
|
||||
const initialDocViewerElement = document.querySelector('aio-doc-viewer');
|
||||
const initialDocViewerContent = initialDocViewerElement ? initialDocViewerElement.innerHTML : '';
|
||||
|
||||
@Component({
|
||||
selector: 'aio-doc-viewer',
|
||||
template: ''
|
||||
// TODO(robwormald): shadow DOM and emulated don't work here (?!)
|
||||
// encapsulation: ViewEncapsulation.Native
|
||||
})
|
||||
export class DocViewerComponent implements DoCheck, OnDestroy {
|
||||
|
||||
private displayedDoc: DisplayedDoc;
|
||||
private embeddedComponentFactories: Map<string, EmbeddedComponentFactory> = new Map();
|
||||
private hostElement: HTMLElement;
|
||||
|
||||
constructor(
|
||||
componentFactoryResolver: ComponentFactoryResolver,
|
||||
elementRef: ElementRef,
|
||||
embeddedComponents: EmbeddedComponents,
|
||||
private injector: Injector,
|
||||
) {
|
||||
this.hostElement = elementRef.nativeElement;
|
||||
// Security: the initialDocViewerContent comes from the prerendered DOM and is considered to be secure
|
||||
this.hostElement.innerHTML = initialDocViewerContent;
|
||||
|
||||
for (const component of embeddedComponents.components) {
|
||||
const factory = componentFactoryResolver.resolveComponentFactory(component);
|
||||
const selector = factory.selector;
|
||||
const contentPropertyName = this.selectorToContentPropertyName(selector);
|
||||
this.embeddedComponentFactories.set(selector, { contentPropertyName, factory });
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
set doc(newDoc: Doc) {
|
||||
this.ngOnDestroy();
|
||||
if (newDoc) {
|
||||
window.scrollTo(0, 0);
|
||||
this.build(newDoc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add doc content to host element and build it out with embedded components
|
||||
*/
|
||||
private build(doc: Doc) {
|
||||
|
||||
const displayedDoc = this.displayedDoc = new DisplayedDoc(doc);
|
||||
|
||||
// security: the doc.content is always authored by the documentation team
|
||||
// and is considered to be safe
|
||||
this.hostElement.innerHTML = doc.content || '';
|
||||
|
||||
if (!doc.content) { return; }
|
||||
|
||||
// TODO(i): why can't I use for-of? why doesn't typescript like Map#value() iterators?
|
||||
this.embeddedComponentFactories.forEach(({ contentPropertyName, factory }, selector) => {
|
||||
const embeddedComponentElements = this.hostElement.querySelectorAll(selector);
|
||||
|
||||
// cast due to https://github.com/Microsoft/TypeScript/issues/4947
|
||||
for (const element of embeddedComponentElements as any as HTMLElement[]){
|
||||
// hack: preserve the current element content because the factory will empty it out
|
||||
// security: the source of this innerHTML is always authored by the documentation team
|
||||
// and is considered to be safe
|
||||
element[contentPropertyName] = element.innerHTML;
|
||||
displayedDoc.addEmbeddedComponent(factory.create(this.injector, [], element));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngDoCheck() {
|
||||
if (this.displayedDoc) { this.displayedDoc.detectChanges(); }
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// destroy components otherwise there will be memory leaks
|
||||
if (this.displayedDoc) {
|
||||
this.displayedDoc.destroy();
|
||||
this.displayedDoc = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the component content property name by converting the selector to camelCase and appending
|
||||
* 'Content', e.g. live-example => liveExampleContent
|
||||
*/
|
||||
private selectorToContentPropertyName(selector: string) {
|
||||
return selector.replace(/-(.)/g, (match, $1) => $1.toUpperCase()) + 'Content';
|
||||
}
|
||||
}
|
||||
|
||||
class DisplayedDoc {
|
||||
|
||||
metadata: DocMetadata;
|
||||
|
||||
private embeddedComponents: ComponentRef<any>[] = [];
|
||||
|
||||
constructor(doc: Doc) {
|
||||
// ignore doc.content ... don't need to keep it around
|
||||
this.metadata = doc.metadata;
|
||||
}
|
||||
|
||||
addEmbeddedComponent(component: ComponentRef<any>) {
|
||||
this.embeddedComponents.push(component);
|
||||
}
|
||||
|
||||
detectChanges() {
|
||||
this.embeddedComponents.forEach(comp => comp.changeDetectorRef.detectChanges());
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// destroy components otherwise there will be memory leaks
|
||||
this.embeddedComponents.forEach(comp => comp.destroy());
|
||||
this.embeddedComponents.length = 0;
|
||||
}
|
||||
}
|
28
aio/src/app/embedded/code-example.component.spec.ts
Normal file
@ -0,0 +1,28 @@
|
||||
/* tslint:disable:no-unused-variable */
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DebugElement } from '@angular/core';
|
||||
|
||||
import { CodeExampleComponent } from './code-example.component';
|
||||
|
||||
describe('CodeExampleComponent', () => {
|
||||
let component: CodeExampleComponent;
|
||||
let fixture: ComponentFixture<CodeExampleComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CodeExampleComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CodeExampleComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
73
aio/src/app/embedded/code-example.component.ts
Normal file
@ -0,0 +1,73 @@
|
||||
/* tslint:disable component-selector */
|
||||
|
||||
import { Component, OnInit, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
|
||||
|
||||
// TODO(i): add clipboard copy functionality
|
||||
|
||||
/**
|
||||
* Angular.io Code Example
|
||||
*
|
||||
* Pretty renders a code block, primarily used in the docs and API reference. Can be used within an Angular app, or
|
||||
* independently, provided that it is dynamically generated by the component resolver.
|
||||
*
|
||||
* Usage:
|
||||
* <code-example [language]="..." [escape]="..." [format]="..." [showcase]="..." [animated]="...">
|
||||
* console.log('Hello World')
|
||||
* </code-example>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'code-example',
|
||||
template: '<pre class="{{classes}}"><code class="{{animatedClasses}}" #codeContainer></code></pre>'
|
||||
})
|
||||
export class CodeExampleComponent implements OnInit, AfterViewInit {
|
||||
|
||||
@ViewChild('codeContainer') codeContainerRef: ElementRef;
|
||||
|
||||
language: string; // could be javascript, dart, typescript
|
||||
// TODO(i): escape doesn't seem to be currently supported in the original code
|
||||
escape: string; // could be 'html'
|
||||
format: string; // some css class
|
||||
showcase: string; // a string with the value 'true'
|
||||
animated = false;
|
||||
|
||||
// TODO(i): could we use @HostBinding instead or does the CSS have to be scoped to <pre> and <code>
|
||||
classes: string;
|
||||
animatedClasses: string;
|
||||
|
||||
|
||||
constructor(private elementRef: ElementRef) {
|
||||
// TODO(i): @Input should be supported for host elements and should just do a one off initialization of properties
|
||||
// from the host element => talk to Tobias
|
||||
['language', 'escape', 'format', 'showcase', 'animated'].forEach(inputName => {
|
||||
if (!this[inputName]) {
|
||||
this[inputName] = this.elementRef.nativeElement.getAttribute(inputName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
const showcaseClass = this.showcase === 'true' ? ' is-showcase' : '';
|
||||
this.classes = `
|
||||
prettyprint
|
||||
${this.format ? this.format : ''}
|
||||
${this.language ? 'lang-' + this.language : '' }
|
||||
${showcaseClass ? showcaseClass : ''}
|
||||
`.trim();
|
||||
|
||||
this.animatedClasses = `${this.animated ? 'animated fadeIn' : ''}`;
|
||||
|
||||
// Security: the codeExampleContent is the original innerHTML of the host element provided by
|
||||
// docs authors and as such its considered to be safe for innerHTML purposes
|
||||
this.codeContainerRef.nativeElement.innerHTML = this.elementRef.nativeElement.codeExampleContent;
|
||||
}
|
||||
|
||||
|
||||
ngAfterViewInit() {
|
||||
// TODO(i): import prettify.js from this file so that we don't need to preload it via index.html
|
||||
// whenever a code example is used, use syntax highlighting.
|
||||
// if(prettyPrint) {
|
||||
// prettyPrint();
|
||||
// }
|
||||
}
|
||||
}
|
11
aio/src/app/embedded/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { CodeExampleComponent } from './code-example.component';
|
||||
|
||||
/** Components that can be embedded in docs such as CodeExampleComponent, LiveExampleComponent,... */
|
||||
export const embeddedComponents = [
|
||||
CodeExampleComponent
|
||||
];
|
||||
|
||||
/** Injectable class w/ property returning components that can be embedded in docs */
|
||||
export class EmbeddedComponents {
|
||||
components = embeddedComponents;
|
||||
}
|
17
aio/src/app/logger.service.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class Logger {
|
||||
|
||||
log(value: any, ...rest) {
|
||||
console.log(value, ...rest);
|
||||
}
|
||||
|
||||
error(value: any, ...rest) {
|
||||
console.error(value, ...rest);
|
||||
}
|
||||
|
||||
warn(value: any, ...rest) {
|
||||
console.warn(value, ...rest);
|
||||
}
|
||||
}
|
2
aio/src/app/nav-engine/doc-fetching.service.spec.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import { DocFetchingService } from './doc-fetching.service';
|
||||
// Write tests when/if this service is retained.
|
45
aio/src/app/nav-engine/doc-fetching.service.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { Http, Response } from '@angular/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/operator/do';
|
||||
import 'rxjs/add/operator/map';
|
||||
|
||||
import { Logger } from '../logger.service';
|
||||
|
||||
@Injectable()
|
||||
export class DocFetchingService {
|
||||
|
||||
constructor(private http: Http, private logger: Logger) { }
|
||||
|
||||
/**
|
||||
* Fetch document from server.
|
||||
* NB: pass 404 response to caller as empty string content
|
||||
* Other errors and non-OK status responses are thrown errors.
|
||||
* TODO: add timeout and retry for lost connection
|
||||
*/
|
||||
getFile(url: string): Observable<string> {
|
||||
|
||||
if (!url) {
|
||||
const emsg = 'getFile: no URL';
|
||||
this.logger.error(emsg);
|
||||
throw new Error(emsg);
|
||||
}
|
||||
|
||||
this.logger.log('fetching document file at ', url);
|
||||
|
||||
return this.http.get(url)
|
||||
.map(res => res.text())
|
||||
.do(content => this.logger.log('fetched document file at ', url) )
|
||||
.catch((error: Response) => {
|
||||
if (error.status === 404) {
|
||||
this.logger.error(`Document file not found at '$(url)'`);
|
||||
return of('');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
10
aio/src/app/nav-engine/doc.model.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export interface DocMetadata {
|
||||
id: string; // 'home'
|
||||
title: string; // 'Home'
|
||||
url: string; // 'assets/documents/home.html'
|
||||
}
|
||||
|
||||
export interface Doc {
|
||||
metadata: DocMetadata;
|
||||
content: string;
|
||||
}
|
74
aio/src/app/nav-engine/doc.service.spec.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { fakeAsync, tick } from '@angular/core/testing';
|
||||
|
||||
import { DocService } from './doc.service';
|
||||
import { Doc, DocMetadata } from './doc.model';
|
||||
import { DocFetchingService } from './doc-fetching.service';
|
||||
import { SiteMapService } from './sitemap.service';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/operator/delay';
|
||||
|
||||
describe('DocService', () => {
|
||||
let docFetchingService: DocFetchingService;
|
||||
let getFileSpy: jasmine.Spy;
|
||||
let loggerSpy: any;
|
||||
let siteMapService: SiteMapService;
|
||||
let docService: DocService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
this.content = 'fake file contents';
|
||||
this.metadata = {
|
||||
id: 'fake',
|
||||
title: 'All about the fake',
|
||||
url: 'assets/documents/fake.html'
|
||||
};
|
||||
|
||||
loggerSpy = jasmine.createSpyObj('logger', ['log', 'warn', 'error']);
|
||||
siteMapService = new SiteMapService();
|
||||
spyOn(siteMapService, 'getDocMetadata').and
|
||||
.callFake((id: string) => of(this.metadata).delay(0));
|
||||
|
||||
docFetchingService = new DocFetchingService(null, loggerSpy);
|
||||
getFileSpy = spyOn(docFetchingService, 'getFile').and
|
||||
.callFake((url: string) => of(this.content).delay(0));
|
||||
|
||||
docService = new DocService(docFetchingService, loggerSpy, siteMapService);
|
||||
});
|
||||
|
||||
it('should return fake doc for fake id', fakeAsync(() => {
|
||||
docService.getDoc('fake').subscribe(doc =>
|
||||
expect(doc.content).toBe(this.content)
|
||||
);
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('should retrieve file once for first file request', fakeAsync(() => {
|
||||
docService.getDoc('fake').subscribe();
|
||||
expect(getFileSpy.calls.count()).toBe(0, 'no call before tick');
|
||||
tick();
|
||||
expect(getFileSpy.calls.count()).toBe(1, 'one call after tick');
|
||||
}));
|
||||
|
||||
it('should retrieve file from cache the second time', fakeAsync(() => {
|
||||
docService.getDoc('fake').subscribe();
|
||||
tick();
|
||||
expect(getFileSpy.calls.count()).toBe(1, 'one call after 1st request');
|
||||
|
||||
docService.getDoc('fake').subscribe();
|
||||
tick();
|
||||
expect(getFileSpy.calls.count()).toBe(1, 'still only one call after 2nd request');
|
||||
}));
|
||||
|
||||
it('should pass along file error through its getDoc observable result', fakeAsync(() => {
|
||||
const err = 'deliberate file error';
|
||||
getFileSpy.and.throwError(err);
|
||||
docService.getDoc('fake').subscribe(
|
||||
doc => expect(false).toBe(true, 'should have failed'),
|
||||
error => expect(error.message).toBe(err)
|
||||
);
|
||||
tick();
|
||||
}));
|
||||
});
|
60
aio/src/app/nav-engine/doc.service.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/switchMap';
|
||||
|
||||
import { Doc, DocMetadata } from './doc.model';
|
||||
import { DocFetchingService } from './doc-fetching.service';
|
||||
import { Logger } from '../logger.service';
|
||||
|
||||
import { SiteMapService } from './sitemap.service';
|
||||
|
||||
interface DocCache {
|
||||
[index: string]: Doc;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DocService {
|
||||
private cache: DocCache = {};
|
||||
|
||||
constructor(
|
||||
private fileService: DocFetchingService,
|
||||
private logger: Logger,
|
||||
private siteMapService: SiteMapService
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Get document for documentId, from cache if found else server.
|
||||
* Pass server errors along to caller
|
||||
* Caller should interpret empty string content as "404 - file not found"
|
||||
*/
|
||||
getDoc(documentId: string): Observable<Doc> {
|
||||
let doc = this.cache[documentId];
|
||||
if (doc) {
|
||||
this.logger.log('returned cached content for ', doc.metadata);
|
||||
return of(cloneDoc(doc));
|
||||
}
|
||||
|
||||
return this.siteMapService
|
||||
.getDocMetadata(documentId)
|
||||
.switchMap(metadata => {
|
||||
|
||||
return this.fileService.getFile(metadata.url)
|
||||
.map(content => {
|
||||
this.logger.log('fetched content for', metadata);
|
||||
doc = { metadata, content };
|
||||
this.cache[metadata.id] = doc;
|
||||
return cloneDoc(doc);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function cloneDoc(doc: Doc) {
|
||||
return {
|
||||
metadata: Object.assign({}, doc.metadata),
|
||||
content: doc.content
|
||||
};
|
||||
}
|
18
aio/src/app/nav-engine/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { DocService } from './doc.service';
|
||||
import { DocFetchingService } from './doc-fetching.service';
|
||||
import { NavEngine } from './nav-engine.service';
|
||||
import { NavLinkDirective } from './nav-link.directive';
|
||||
import { SiteMapService } from './sitemap.service';
|
||||
|
||||
export { Doc, DocMetadata } from './doc.model';
|
||||
|
||||
export const navDirectives = [
|
||||
NavLinkDirective
|
||||
];
|
||||
|
||||
export const navProviders = [
|
||||
DocService,
|
||||
DocFetchingService,
|
||||
NavEngine,
|
||||
SiteMapService,
|
||||
];
|
46
aio/src/app/nav-engine/nav-engine.service.spec.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { fakeAsync, tick} from '@angular/core/testing';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import 'rxjs/add/operator/delay';
|
||||
|
||||
import { DocService } from './doc.service';
|
||||
import { Doc, DocMetadata } from './doc.model';
|
||||
|
||||
import { NavEngine } from './nav-engine.service';
|
||||
|
||||
const fakeDoc: Doc = {
|
||||
metadata: {
|
||||
id: 'fake',
|
||||
title: 'All about the fake',
|
||||
url: 'assets/documents/fake.html'
|
||||
},
|
||||
content: 'fake content'
|
||||
};
|
||||
|
||||
describe('NavEngine', () => {
|
||||
|
||||
let navEngine: NavEngine;
|
||||
|
||||
beforeEach(() => {
|
||||
this.fakeDoc = {
|
||||
metadata: {
|
||||
id: 'fake',
|
||||
title: 'All about the fake',
|
||||
url: 'assets/documents/fake.html'
|
||||
},
|
||||
content: 'fake content'
|
||||
};
|
||||
|
||||
const docService: any = jasmine.createSpyObj('docService', ['getDoc']);
|
||||
docService.getDoc.and.callFake((id: string) => of(this.fakeDoc).delay(0));
|
||||
|
||||
navEngine = new NavEngine(docService);
|
||||
});
|
||||
|
||||
it('should set currentDoc to fake doc when navigate to fake id', fakeAsync(() => {
|
||||
navEngine.navigate('fake');
|
||||
tick();
|
||||
expect(navEngine.currentDoc.content).toBe(this.fakeDoc.content);
|
||||
}));
|
||||
});
|
25
aio/src/app/nav-engine/nav-engine.service.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Doc } from './doc.model';
|
||||
import { DocService } from './doc.service';
|
||||
|
||||
@Injectable()
|
||||
export class NavEngine {
|
||||
|
||||
/** Document result of most recent `navigate` call */
|
||||
currentDoc: Doc;
|
||||
constructor(private docService: DocService) {}
|
||||
|
||||
/**
|
||||
* Navigate sets `currentDoc` to the document for `documentId`.
|
||||
* TODO: handle 'Document not found', signaled by empty string content
|
||||
* TODO: handle document retrieval error
|
||||
*/
|
||||
navigate(documentId: string) {
|
||||
this.docService.getDoc(documentId).subscribe(
|
||||
doc => this.currentDoc = doc
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
19
aio/src/app/nav-engine/nav-link.directive.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Directive, HostListener, Input } from '@angular/core';
|
||||
import { NavEngine } from './nav-engine.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[aioNavLink]'
|
||||
})
|
||||
export class NavLinkDirective {
|
||||
|
||||
@Input()
|
||||
aioNavLink: string;
|
||||
|
||||
constructor(private navEngine: NavEngine) { }
|
||||
|
||||
@HostListener('click', ['$event'])
|
||||
onClick($event) {
|
||||
this.navEngine.navigate(this.aioNavLink);
|
||||
return false;
|
||||
}
|
||||
}
|
32
aio/src/app/nav-engine/sitemap.service.spec.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { fakeAsync, tick } from '@angular/core/testing';
|
||||
import { DocMetadata } from './doc.model';
|
||||
import { SiteMapService } from './sitemap.service';
|
||||
|
||||
describe('SiteMapService', () => {
|
||||
let siteMapService: SiteMapService;
|
||||
|
||||
beforeEach(() => {
|
||||
siteMapService = new SiteMapService();
|
||||
});
|
||||
|
||||
it('should get News metadata', fakeAsync(() => {
|
||||
siteMapService.getDocMetadata('news').subscribe(
|
||||
metadata => expect(metadata.url).toBe('content/news.html')
|
||||
);
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('should calculate expected doc url for unknown id', fakeAsync(() => {
|
||||
siteMapService.getDocMetadata('fizbuz').subscribe(
|
||||
metadata => expect(metadata.url).toBe('content/fizbuz.html')
|
||||
);
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('should calculate expected index doc url for unknown id ending in /', fakeAsync(() => {
|
||||
siteMapService.getDocMetadata('fizbuz/').subscribe(
|
||||
metadata => expect(metadata.url).toBe('content/fizbuz/index.html')
|
||||
);
|
||||
tick();
|
||||
}));
|
||||
});
|
38
aio/src/app/nav-engine/sitemap.service.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import 'rxjs/add/operator/map';
|
||||
|
||||
import { DocMetadata } from './doc.model';
|
||||
|
||||
const siteMap: DocMetadata[] = [
|
||||
{ 'title': 'Home', 'url': 'content/home.html', id: 'home'},
|
||||
{ 'title': 'Features', 'url': 'content/features.html', id: 'features'},
|
||||
{ 'title': 'News', 'url': 'content/news.html', id: 'news'}
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class SiteMapService {
|
||||
private siteMap = new BehaviorSubject(siteMap);
|
||||
|
||||
getDocMetadata(id: string) {
|
||||
const missing = () => this.getMissingMetadata(id);
|
||||
return this.siteMap
|
||||
.map(map =>
|
||||
map.find(d => d.id === id) || missing());
|
||||
}
|
||||
|
||||
// Alternative way to calculate metadata. Will it be used?
|
||||
private getMissingMetadata(id: string) {
|
||||
|
||||
const filename = id.startsWith('/') ? id.substring(1) : id; // strip leading '/'
|
||||
|
||||
return {
|
||||
id,
|
||||
title: id,
|
||||
url: `content/${filename}${filename.endsWith('/') ? 'index' : ''}.html`
|
||||
} as DocMetadata;
|
||||
}
|
||||
}
|
111
aio/src/app/search/search-worker-client.ts
Normal file
@ -0,0 +1,111 @@
|
||||
/*
|
||||
Copyright 2016 Google Inc. All Rights Reserved.
|
||||
Use of this source code is governed by an MIT-style license that
|
||||
can be found in the LICENSE file at http://angular.io/license
|
||||
*/
|
||||
|
||||
import {NgZone} from '@angular/core';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {Subscriber} from 'rxjs/Subscriber';
|
||||
import 'rxjs/add/observable/fromPromise';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/operator/switchMap';
|
||||
|
||||
|
||||
/**
|
||||
* We will use this client from a component with something like...
|
||||
*
|
||||
* ngOnInit() {
|
||||
* const searchWorker = new SearchWorkerClient('app/search-worker.js', this.zone);
|
||||
* this.indexReady = searchWorker.ready;
|
||||
* this.searchInput = new FormControl();
|
||||
* this.searchResult$ = this.searchInput.valueChanges
|
||||
* .switchMap((searchText: string) => searchWorker.search(searchText));
|
||||
* }
|
||||
*
|
||||
* TODO(petebd): do we need a fallback for browsers that do not support service workers?
|
||||
*/
|
||||
|
||||
type QueryResults = Object[];
|
||||
|
||||
export interface ResultsReadyMessage {
|
||||
type: 'query-results';
|
||||
id: number;
|
||||
query: string;
|
||||
results: QueryResults;
|
||||
}
|
||||
|
||||
export class SearchWorkerClient {
|
||||
ready: Promise<boolean>;
|
||||
worker: Worker;
|
||||
private _queryId = 0;
|
||||
|
||||
constructor(url: string, private zone: NgZone) {
|
||||
this.worker = new Worker(url);
|
||||
this.ready = this._waitForIndex(this.worker);
|
||||
}
|
||||
|
||||
search(query: string) {
|
||||
return Observable.fromPromise(this.ready)
|
||||
.switchMap(() => this._createQuery(query));
|
||||
}
|
||||
|
||||
private _waitForIndex(worker: Worker) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
worker.onmessage = (e) => {
|
||||
if (e.data.type === 'index-ready') {
|
||||
resolve(true);
|
||||
cleanup();
|
||||
}
|
||||
};
|
||||
|
||||
worker.onerror = (e) => {
|
||||
reject(e);
|
||||
cleanup();
|
||||
};
|
||||
});
|
||||
|
||||
function cleanup() {
|
||||
worker.onmessage = null;
|
||||
worker.onerror = null;
|
||||
}
|
||||
}
|
||||
|
||||
private _createQuery(query: string) {
|
||||
return new Observable<QueryResults>((subscriber: Subscriber<QueryResults>) => {
|
||||
|
||||
// get a new identifier for this query that we can match to results
|
||||
const id = this._queryId++;
|
||||
|
||||
const handleMessage = (message: MessageEvent) => {
|
||||
const {type, id: queryId, results} = message.data as ResultsReadyMessage;
|
||||
if (type === 'query-results' && id === queryId) {
|
||||
this.zone.run(() => {
|
||||
subscriber.next(results);
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = (error: ErrorEvent) => {
|
||||
this.zone.run(() => {
|
||||
subscriber.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
// Wire up the event listeners for this query
|
||||
this.worker.addEventListener('message', handleMessage);
|
||||
this.worker.addEventListener('error', handleError);
|
||||
|
||||
// Post the query to the web worker
|
||||
this.worker.postMessage({query, id});
|
||||
|
||||
// At completion/error unwire the event listeners
|
||||
return () => {
|
||||
this.worker.removeEventListener('message', handleMessage);
|
||||
this.worker.removeEventListener('error', handleError);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
63
aio/src/app/search/search-worker.js
Normal file
@ -0,0 +1,63 @@
|
||||
'use strict';
|
||||
|
||||
/* eslint-env worker */
|
||||
/* global importScripts, lunr */
|
||||
|
||||
importScripts('https://unpkg.com/lunr@0.7.2');
|
||||
|
||||
var index = createIndex();
|
||||
var pages = {};
|
||||
|
||||
makeRequest('search-data.json', loadIndex);
|
||||
|
||||
self.onmessage = handleMessage;
|
||||
|
||||
// Create the lunr index - the docs should be an array of objects, each object containing
|
||||
// the path and search terms for a page
|
||||
function createIndex() {
|
||||
return lunr(/** @this */function() {
|
||||
this.ref('path');
|
||||
this.field('titleWords', {boost: 50});
|
||||
this.field('members', {boost: 40});
|
||||
this.field('keywords', {boost: 20});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Use XHR to make a request to the server
|
||||
function makeRequest(url, callback) {
|
||||
var searchDataRequest = new XMLHttpRequest();
|
||||
searchDataRequest.onload = function() {
|
||||
callback(JSON.parse(this.responseText));
|
||||
};
|
||||
searchDataRequest.open('GET', url);
|
||||
searchDataRequest.send();
|
||||
}
|
||||
|
||||
|
||||
// Create the search index from the searchInfo which contains the information about each page to be indexed
|
||||
function loadIndex(searchInfo) {
|
||||
// Store the pages data to be used in mapping query results back to pages
|
||||
// Add search terms from each page to the search index
|
||||
searchInfo.forEach(function(page) {
|
||||
index.add(page);
|
||||
pages[page.path] = page;
|
||||
});
|
||||
self.postMessage({type: 'index-ready'});
|
||||
}
|
||||
|
||||
|
||||
// The worker receives a message everytime the web app wants to query the index
|
||||
function handleMessage(message) {
|
||||
var id = message.data.id;
|
||||
var query = message.data.query;
|
||||
var results = queryIndex(query);
|
||||
self.postMessage({type: 'query-results', id: id, query: query, results: results});
|
||||
}
|
||||
|
||||
|
||||
// Query the index and return the processed results
|
||||
function queryIndex(query) {
|
||||
// Only return the array of paths to pages
|
||||
return index.search(query).map(function(hit) { return pages[hit.ref]; });
|
||||
}
|
0
aio/src/assets/.gitkeep
Normal file
BIN
aio/src/assets/images/backgrounds/browser-background-template.png
Executable file
After Width: | Height: | Size: 3.0 KiB |
BIN
aio/src/assets/images/backgrounds/lon-paper.png
Executable file
After Width: | Height: | Size: 113 KiB |
BIN
aio/src/assets/images/backgrounds/sf-paper.png
Executable file
After Width: | Height: | Size: 80 KiB |
BIN
aio/src/assets/images/backgrounds/super-hero-large.png
Executable file
After Width: | Height: | Size: 119 KiB |
BIN
aio/src/assets/images/bios/alex-eagle.jpg
Executable file
After Width: | Height: | Size: 14 KiB |
BIN
aio/src/assets/images/bios/alex-rickabaugh.jpg
Executable file
After Width: | Height: | Size: 13 KiB |
BIN
aio/src/assets/images/bios/alex-wolfe.jpg
Executable file
After Width: | Height: | Size: 9.1 KiB |
BIN
aio/src/assets/images/bios/ali.jpg
Executable file
After Width: | Height: | Size: 16 KiB |
BIN
aio/src/assets/images/bios/angular-gde-bio-placeholder.png
Executable file
After Width: | Height: | Size: 10 KiB |
BIN
aio/src/assets/images/bios/brad-green.jpg
Executable file
After Width: | Height: | Size: 11 KiB |
BIN
aio/src/assets/images/bios/brandonroberts.jpg
Executable file
After Width: | Height: | Size: 8.6 KiB |
BIN
aio/src/assets/images/bios/chuckj.jpg
Executable file
After Width: | Height: | Size: 66 KiB |
BIN
aio/src/assets/images/bios/crisbeto.jpg
Executable file
After Width: | Height: | Size: 16 KiB |