refactor: move angular source to /packages rather than modules/@angular
This commit is contained in:
13
packages/common/.babelrc
Normal file
13
packages/common/.babelrc
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": [["transform-es2015-modules-umd", {
|
||||
"globals": {
|
||||
"@angular/common": "ng.common",
|
||||
"@angular/core": "ng.core",
|
||||
"rxjs/Subject": "Rx"
|
||||
},
|
||||
"exactGlobals": true
|
||||
}]],
|
||||
"moduleId": "@angular/common"
|
||||
}
|
14
packages/common/.babelrc-testing
Normal file
14
packages/common/.babelrc-testing
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": [["transform-es2015-modules-umd", {
|
||||
"globals": {
|
||||
"@angular/common": "ng.common",
|
||||
"@angular/common/testing": "ng.common.testing",
|
||||
"@angular/core": "ng.core",
|
||||
"rxjs/Subject": "Rx"
|
||||
},
|
||||
"exactGlobals": true
|
||||
}]],
|
||||
"moduleId": "@angular/common/testing"
|
||||
}
|
14
packages/common/index.ts
Normal file
14
packages/common/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
// This file is not used to build this module. It is only used during editing
|
||||
// by the TypeScript language service and during build for verification. `ngc`
|
||||
// replaces this file with production index.ts when it rewrites private symbol
|
||||
// names.
|
||||
|
||||
export * from './public_api';
|
18
packages/common/package.json
Normal file
18
packages/common/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@angular/common",
|
||||
"version": "0.0.0-PLACEHOLDER",
|
||||
"description": "Angular - commonly needed directives and services",
|
||||
"main": "./bundles/common.umd.js",
|
||||
"module": "./@angular/common.es5.js",
|
||||
"es2015": "./@angular/common.js",
|
||||
"typings": "./typings/common.d.ts",
|
||||
"author": "angular",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@angular/core": "0.0.0-PLACEHOLDER"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/angular/angular.git"
|
||||
}
|
||||
}
|
16
packages/common/public_api.ts
Normal file
16
packages/common/public_api.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @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
|
||||
* @description
|
||||
* Entry point for all public APIs of the common package.
|
||||
*/
|
||||
export * from './src/common';
|
||||
|
||||
// This file only reexports content of the `src` folder. Keep it that way.
|
20
packages/common/src/common.ts
Normal file
20
packages/common/src/common.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @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
|
||||
* @description
|
||||
* Entry point for all public APIs of the common package.
|
||||
*/
|
||||
export * from './location/index';
|
||||
export {NgLocaleLocalization, NgLocalization} from './localization';
|
||||
export {CommonModule} from './common_module';
|
||||
export {NgClass, NgFor, NgForOf, NgIf, NgPlural, NgPluralCase, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet, NgComponentOutlet} from './directives/index';
|
||||
export {AsyncPipe, DatePipe, I18nPluralPipe, I18nSelectPipe, JsonPipe, LowerCasePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe, UpperCasePipe, TitleCasePipe} from './pipes/index';
|
||||
export {PLATFORM_BROWSER_ID as ɵPLATFORM_BROWSER_ID, PLATFORM_SERVER_ID as ɵPLATFORM_SERVER_ID, PLATFORM_WORKER_APP_ID as ɵPLATFORM_WORKER_APP_ID, PLATFORM_WORKER_UI_ID as ɵPLATFORM_WORKER_UI_ID, isPlatformBrowser, isPlatformServer, isPlatformWorkerApp, isPlatformWorkerUi} from './platform_id';
|
||||
export {VERSION} from './version';
|
31
packages/common/src/common_module.ts
Normal file
31
packages/common/src/common_module.ts
Normal file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
import {COMMON_DEPRECATED_DIRECTIVES, COMMON_DIRECTIVES} from './directives/index';
|
||||
import {NgLocaleLocalization, NgLocalization} from './localization';
|
||||
import {COMMON_PIPES} from './pipes/index';
|
||||
|
||||
|
||||
// Note: This does not contain the location providers,
|
||||
// as they need some platform specific implementations to work.
|
||||
/**
|
||||
* The module that includes all the basic Angular directives like {@link NgIf}, {@link NgForOf}, ...
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@NgModule({
|
||||
declarations: [COMMON_DIRECTIVES, COMMON_PIPES],
|
||||
exports: [COMMON_DIRECTIVES, COMMON_PIPES],
|
||||
providers: [
|
||||
{provide: NgLocalization, useClass: NgLocaleLocalization},
|
||||
],
|
||||
})
|
||||
export class CommonModule {
|
||||
}
|
58
packages/common/src/directives/index.ts
Normal file
58
packages/common/src/directives/index.ts
Normal file
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Provider} from '@angular/core';
|
||||
|
||||
import {NgClass} from './ng_class';
|
||||
import {NgComponentOutlet} from './ng_component_outlet';
|
||||
import {NgFor, NgForOf} from './ng_for_of';
|
||||
import {NgIf} from './ng_if';
|
||||
import {NgPlural, NgPluralCase} from './ng_plural';
|
||||
import {NgStyle} from './ng_style';
|
||||
import {NgSwitch, NgSwitchCase, NgSwitchDefault} from './ng_switch';
|
||||
import {NgTemplateOutlet} from './ng_template_outlet';
|
||||
|
||||
export {
|
||||
NgClass,
|
||||
NgComponentOutlet,
|
||||
NgFor,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
NgPlural,
|
||||
NgPluralCase,
|
||||
NgStyle,
|
||||
NgSwitch,
|
||||
NgSwitchCase,
|
||||
NgSwitchDefault,
|
||||
NgTemplateOutlet
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A collection of Angular directives that are likely to be used in each and every Angular
|
||||
* application.
|
||||
*/
|
||||
export const COMMON_DIRECTIVES: Provider[] = [
|
||||
NgClass,
|
||||
NgComponentOutlet,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
NgTemplateOutlet,
|
||||
NgStyle,
|
||||
NgSwitch,
|
||||
NgSwitchCase,
|
||||
NgSwitchDefault,
|
||||
NgPlural,
|
||||
NgPluralCase,
|
||||
];
|
||||
|
||||
/**
|
||||
* A colletion of deprecated directives that are no longer part of the core module.
|
||||
*/
|
||||
export const COMMON_DEPRECATED_DIRECTIVES: Provider[] = [NgFor];
|
142
packages/common/src/directives/ng_class.ts
Normal file
142
packages/common/src/directives/ng_class.ts
Normal file
@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Directive, DoCheck, ElementRef, Input, IterableChanges, IterableDiffer, IterableDiffers, KeyValueChanges, KeyValueDiffer, KeyValueDiffers, Renderer, ɵisListLikeIterable as isListLikeIterable, ɵstringify as stringify} from '@angular/core';
|
||||
|
||||
/**
|
||||
* @ngModule CommonModule
|
||||
*
|
||||
* @whatItDoes Adds and removes CSS classes on an HTML element.
|
||||
*
|
||||
* @howToUse
|
||||
* ```
|
||||
* <some-element [ngClass]="'first second'">...</some-element>
|
||||
*
|
||||
* <some-element [ngClass]="['first', 'second']">...</some-element>
|
||||
*
|
||||
* <some-element [ngClass]="{'first': true, 'second': true, 'third': false}">...</some-element>
|
||||
*
|
||||
* <some-element [ngClass]="stringExp|arrayExp|objExp">...</some-element>
|
||||
*
|
||||
* <some-element [ngClass]="{'class1 class2 class3' : true}">...</some-element>
|
||||
* ```
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* The CSS classes are updated as follows, depending on the type of the expression evaluation:
|
||||
* - `string` - the CSS classes listed in the string (space delimited) are added,
|
||||
* - `Array` - the CSS classes declared as Array elements are added,
|
||||
* - `Object` - keys are CSS classes that get added when the expression given in the value
|
||||
* evaluates to a truthy value, otherwise they are removed.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Directive({selector: '[ngClass]'})
|
||||
export class NgClass implements DoCheck {
|
||||
private _iterableDiffer: IterableDiffer<string>;
|
||||
private _keyValueDiffer: KeyValueDiffer<string, any>;
|
||||
private _initialClasses: string[] = [];
|
||||
private _rawClass: string[]|Set<string>|{[klass: string]: any};
|
||||
|
||||
constructor(
|
||||
private _iterableDiffers: IterableDiffers, private _keyValueDiffers: KeyValueDiffers,
|
||||
private _ngEl: ElementRef, private _renderer: Renderer) {}
|
||||
|
||||
@Input('class')
|
||||
set klass(v: string) {
|
||||
this._applyInitialClasses(true);
|
||||
this._initialClasses = typeof v === 'string' ? v.split(/\s+/) : [];
|
||||
this._applyInitialClasses(false);
|
||||
this._applyClasses(this._rawClass, false);
|
||||
}
|
||||
|
||||
@Input()
|
||||
set ngClass(v: string|string[]|Set<string>|{[klass: string]: any}) {
|
||||
this._cleanupClasses(this._rawClass);
|
||||
|
||||
this._iterableDiffer = null;
|
||||
this._keyValueDiffer = null;
|
||||
|
||||
this._rawClass = typeof v === 'string' ? v.split(/\s+/) : v;
|
||||
|
||||
if (this._rawClass) {
|
||||
if (isListLikeIterable(this._rawClass)) {
|
||||
this._iterableDiffer = this._iterableDiffers.find(this._rawClass).create();
|
||||
} else {
|
||||
this._keyValueDiffer = this._keyValueDiffers.find(this._rawClass).create();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngDoCheck(): void {
|
||||
if (this._iterableDiffer) {
|
||||
const iterableChanges = this._iterableDiffer.diff(this._rawClass as string[]);
|
||||
if (iterableChanges) {
|
||||
this._applyIterableChanges(iterableChanges);
|
||||
}
|
||||
} else if (this._keyValueDiffer) {
|
||||
const keyValueChanges = this._keyValueDiffer.diff(this._rawClass as{[k: string]: any});
|
||||
if (keyValueChanges) {
|
||||
this._applyKeyValueChanges(keyValueChanges);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _cleanupClasses(rawClassVal: string[]|{[klass: string]: any}): void {
|
||||
this._applyClasses(rawClassVal, true);
|
||||
this._applyInitialClasses(false);
|
||||
}
|
||||
|
||||
private _applyKeyValueChanges(changes: KeyValueChanges<string, any>): void {
|
||||
changes.forEachAddedItem((record) => this._toggleClass(record.key, record.currentValue));
|
||||
changes.forEachChangedItem((record) => this._toggleClass(record.key, record.currentValue));
|
||||
changes.forEachRemovedItem((record) => {
|
||||
if (record.previousValue) {
|
||||
this._toggleClass(record.key, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _applyIterableChanges(changes: IterableChanges<string>): void {
|
||||
changes.forEachAddedItem((record) => {
|
||||
if (typeof record.item === 'string') {
|
||||
this._toggleClass(record.item, true);
|
||||
} else {
|
||||
throw new Error(
|
||||
`NgClass can only toggle CSS classes expressed as strings, got ${stringify(record.item)}`);
|
||||
}
|
||||
});
|
||||
|
||||
changes.forEachRemovedItem((record) => this._toggleClass(record.item, false));
|
||||
}
|
||||
|
||||
private _applyInitialClasses(isCleanup: boolean) {
|
||||
this._initialClasses.forEach(klass => this._toggleClass(klass, !isCleanup));
|
||||
}
|
||||
|
||||
private _applyClasses(
|
||||
rawClassVal: string[]|Set<string>|{[klass: string]: any}, isCleanup: boolean) {
|
||||
if (rawClassVal) {
|
||||
if (Array.isArray(rawClassVal) || rawClassVal instanceof Set) {
|
||||
(<any>rawClassVal).forEach((klass: string) => this._toggleClass(klass, !isCleanup));
|
||||
} else {
|
||||
Object.keys(rawClassVal).forEach(klass => {
|
||||
if (rawClassVal[klass] != null) this._toggleClass(klass, !isCleanup);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleClass(klass: string, enabled: any): void {
|
||||
klass = klass.trim();
|
||||
if (klass) {
|
||||
klass.split(/\s+/g).forEach(
|
||||
klass => { this._renderer.setElementClass(this._ngEl.nativeElement, klass, !!enabled); });
|
||||
}
|
||||
}
|
||||
}
|
115
packages/common/src/directives/ng_component_outlet.ts
Normal file
115
packages/common/src/directives/ng_component_outlet.ts
Normal file
@ -0,0 +1,115 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {ComponentFactoryResolver, ComponentRef, Directive, Injector, Input, NgModuleFactory, NgModuleRef, OnChanges, OnDestroy, Provider, SimpleChanges, Type, ViewContainerRef} from '@angular/core';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Instantiates a single {@link Component} type and inserts its Host View into current View.
|
||||
* `NgComponentOutlet` provides a declarative approach for dynamic component creation.
|
||||
*
|
||||
* `NgComponentOutlet` requires a component type, if a falsy value is set the view will clear and
|
||||
* any existing component will get destroyed.
|
||||
*
|
||||
* ### Fine tune control
|
||||
*
|
||||
* You can control the component creation process by using the following optional attributes:
|
||||
*
|
||||
* * `ngComponentOutletInjector`: Optional custom {@link Injector} that will be used as parent for
|
||||
* the Component. Defaults to the injector of the current view container.
|
||||
*
|
||||
* * `ngComponentOutletProviders`: Optional injectable objects ({@link Provider}) that are visible
|
||||
* to the component.
|
||||
*
|
||||
* * `ngComponentOutletContent`: Optional list of projectable nodes to insert into the content
|
||||
* section of the component, if exists.
|
||||
*
|
||||
* * `ngComponentOutletNgModuleFactory`: Optional module factory to allow dynamically loading other
|
||||
* module, then load a component from that module.
|
||||
*
|
||||
* ### Syntax
|
||||
*
|
||||
* Simple
|
||||
* ```
|
||||
* <ng-container *ngComponentOutlet="componentTypeExpression"></ng-container>
|
||||
* ```
|
||||
*
|
||||
* Customized injector/content
|
||||
* ```
|
||||
* <ng-container *ngComponentOutlet="componentTypeExpression;
|
||||
* injector: injectorExpression;
|
||||
* content: contentNodesExpression;">
|
||||
* </ng-container>
|
||||
* ```
|
||||
*
|
||||
* Customized ngModuleFactory
|
||||
* ```
|
||||
* <ng-container *ngComponentOutlet="componentTypeExpression;
|
||||
* ngModuleFactory: moduleFactory;">
|
||||
* </ng-container>
|
||||
* ```
|
||||
* # Example
|
||||
*
|
||||
* {@example common/ngComponentOutlet/ts/module.ts region='SimpleExample'}
|
||||
*
|
||||
* A more complete example with additional options:
|
||||
*
|
||||
* {@example common/ngComponentOutlet/ts/module.ts region='CompleteExample'}
|
||||
|
||||
* A more complete example with ngModuleFactory:
|
||||
*
|
||||
* {@example common/ngComponentOutlet/ts/module.ts region='NgModuleFactoryExample'}
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
@Directive({selector: '[ngComponentOutlet]'})
|
||||
export class NgComponentOutlet implements OnChanges, OnDestroy {
|
||||
@Input() ngComponentOutlet: Type<any>;
|
||||
@Input() ngComponentOutletInjector: Injector;
|
||||
@Input() ngComponentOutletContent: any[][];
|
||||
@Input() ngComponentOutletNgModuleFactory: NgModuleFactory<any>;
|
||||
|
||||
private _componentRef: ComponentRef<any> = null;
|
||||
private _moduleRef: NgModuleRef<any> = null;
|
||||
|
||||
constructor(private _viewContainerRef: ViewContainerRef) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (this._componentRef) {
|
||||
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._componentRef.hostView));
|
||||
}
|
||||
this._viewContainerRef.clear();
|
||||
this._componentRef = null;
|
||||
|
||||
if (this.ngComponentOutlet) {
|
||||
let injector = this.ngComponentOutletInjector || this._viewContainerRef.parentInjector;
|
||||
|
||||
if ((changes as any).ngComponentOutletNgModuleFactory) {
|
||||
if (this._moduleRef) this._moduleRef.destroy();
|
||||
if (this.ngComponentOutletNgModuleFactory) {
|
||||
this._moduleRef = this.ngComponentOutletNgModuleFactory.create(injector);
|
||||
} else {
|
||||
this._moduleRef = null;
|
||||
}
|
||||
}
|
||||
if (this._moduleRef) {
|
||||
injector = this._moduleRef.injector;
|
||||
}
|
||||
|
||||
let componentFactory =
|
||||
injector.get(ComponentFactoryResolver).resolveComponentFactory(this.ngComponentOutlet);
|
||||
|
||||
this._componentRef = this._viewContainerRef.createComponent(
|
||||
componentFactory, this._viewContainerRef.length, injector, this.ngComponentOutletContent);
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
if (this._moduleRef) this._moduleRef.destroy();
|
||||
}
|
||||
}
|
199
packages/common/src/directives/ng_for_of.ts
Normal file
199
packages/common/src/directives/ng_for_of.ts
Normal file
@ -0,0 +1,199 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {ChangeDetectorRef, Directive, DoCheck, EmbeddedViewRef, Input, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDiffers, NgIterable, OnChanges, SimpleChanges, TemplateRef, TrackByFunction, ViewContainerRef, forwardRef, isDevMode} from '@angular/core';
|
||||
|
||||
export class NgForOfRow<T> {
|
||||
constructor(public $implicit: T, public index: number, public count: number) {}
|
||||
|
||||
get first(): boolean { return this.index === 0; }
|
||||
|
||||
get last(): boolean { return this.index === this.count - 1; }
|
||||
|
||||
get even(): boolean { return this.index % 2 === 0; }
|
||||
|
||||
get odd(): boolean { return !this.even; }
|
||||
}
|
||||
|
||||
/**
|
||||
* The `NgForOf` directive instantiates a template once per item from an iterable. The context
|
||||
* for each instantiated template inherits from the outer context with the given loop variable
|
||||
* set to the current item from the iterable.
|
||||
*
|
||||
* ### Local Variables
|
||||
*
|
||||
* `NgForOf` provides several exported values that can be aliased to local variables:
|
||||
*
|
||||
* * `index` will be set to the current loop iteration for each template context.
|
||||
* * `first` will be set to a boolean value indicating whether the item is the first one in the
|
||||
* iteration.
|
||||
* * `last` will be set to a boolean value indicating whether the item is the last one in the
|
||||
* iteration.
|
||||
* * `even` will be set to a boolean value indicating whether this item has an even index.
|
||||
* * `odd` will be set to a boolean value indicating whether this item has an odd index.
|
||||
*
|
||||
* ### Change Propagation
|
||||
*
|
||||
* When the contents of the iterator changes, `NgForOf` makes the corresponding changes to the DOM:
|
||||
*
|
||||
* * When an item is added, a new instance of the template is added to the DOM.
|
||||
* * When an item is removed, its template instance is removed from the DOM.
|
||||
* * When items are reordered, their respective templates are reordered in the DOM.
|
||||
* * Otherwise, the DOM element for that item will remain the same.
|
||||
*
|
||||
* Angular uses object identity to track insertions and deletions within the iterator and reproduce
|
||||
* those changes in the DOM. This has important implications for animations and any stateful
|
||||
* controls (such as `<input>` elements which accept user input) that are present. Inserted rows can
|
||||
* be animated in, deleted rows can be animated out, and unchanged rows retain any unsaved state
|
||||
* such as user input.
|
||||
*
|
||||
* It is possible for the identities of elements in the iterator to change while the data does not.
|
||||
* This can happen, for example, if the iterator produced from an RPC to the server, and that
|
||||
* RPC is re-run. Even if the data hasn't changed, the second response will produce objects with
|
||||
* different identities, and Angular will tear down the entire DOM and rebuild it (as if all old
|
||||
* elements were deleted and all new elements inserted). This is an expensive operation and should
|
||||
* be avoided if possible.
|
||||
*
|
||||
* To customize the default tracking algorithm, `NgForOf` supports `trackBy` option.
|
||||
* `trackBy` takes a function which has two arguments: `index` and `item`.
|
||||
* If `trackBy` is given, Angular tracks changes by the return value of the function.
|
||||
*
|
||||
* ### Syntax
|
||||
*
|
||||
* - `<li *ngFor="let item of items; let i = index; trackBy: trackByFn">...</li>`
|
||||
* - `<li template="ngFor let item of items; let i = index; trackBy: trackByFn">...</li>`
|
||||
*
|
||||
* With `<ng-template>` element:
|
||||
*
|
||||
* ```
|
||||
* <ng-template ngFor let-item [ngForOf]="items" let-i="index" [ngForTrackBy]="trackByFn">
|
||||
* <li>...</li>
|
||||
* </ng-template>
|
||||
* ```
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* See a [live demo](http://plnkr.co/edit/KVuXxDp0qinGDyo307QW?p=preview) for a more detailed
|
||||
* example.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Directive({selector: '[ngFor][ngForOf]'})
|
||||
export class NgForOf<T> implements DoCheck, OnChanges {
|
||||
@Input() ngForOf: NgIterable<T>;
|
||||
@Input()
|
||||
set ngForTrackBy(fn: TrackByFunction<T>) {
|
||||
if (isDevMode() && fn != null && typeof fn !== 'function') {
|
||||
// TODO(vicb): use a log service once there is a public one available
|
||||
if (<any>console && <any>console.warn) {
|
||||
console.warn(
|
||||
`trackBy must be a function, but received ${JSON.stringify(fn)}. ` +
|
||||
`See https://angular.io/docs/ts/latest/api/common/index/NgFor-directive.html#!#change-propagation for more information.`);
|
||||
}
|
||||
}
|
||||
this._trackByFn = fn;
|
||||
}
|
||||
|
||||
get ngForTrackBy(): TrackByFunction<T> { return this._trackByFn; }
|
||||
|
||||
private _differ: IterableDiffer<T> = null;
|
||||
private _trackByFn: TrackByFunction<T>;
|
||||
|
||||
constructor(
|
||||
private _viewContainer: ViewContainerRef, private _template: TemplateRef<NgForOfRow<T>>,
|
||||
private _differs: IterableDiffers) {}
|
||||
|
||||
@Input()
|
||||
set ngForTemplate(value: TemplateRef<NgForOfRow<T>>) {
|
||||
// TODO(TS2.1): make TemplateRef<Partial<NgForRowOf<T>>> once we move to TS v2.1
|
||||
// The current type is too restrictive; a template that just uses index, for example,
|
||||
// should be acceptable.
|
||||
if (value) {
|
||||
this._template = value;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if ('ngForOf' in changes) {
|
||||
// React on ngForOf changes only once all inputs have been initialized
|
||||
const value = changes['ngForOf'].currentValue;
|
||||
if (!this._differ && value) {
|
||||
try {
|
||||
this._differ = this._differs.find(value).create(this.ngForTrackBy);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Cannot find a differ supporting object '${value}' of type '${getTypeNameForDebugging(value)}'. NgFor only supports binding to Iterables such as Arrays.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngDoCheck(): void {
|
||||
if (this._differ) {
|
||||
const changes = this._differ.diff(this.ngForOf);
|
||||
if (changes) this._applyChanges(changes);
|
||||
}
|
||||
}
|
||||
|
||||
private _applyChanges(changes: IterableChanges<T>) {
|
||||
const insertTuples: RecordViewTuple<T>[] = [];
|
||||
changes.forEachOperation(
|
||||
(item: IterableChangeRecord<any>, adjustedPreviousIndex: number, currentIndex: number) => {
|
||||
if (item.previousIndex == null) {
|
||||
const view = this._viewContainer.createEmbeddedView(
|
||||
this._template, new NgForOfRow(null, null, null), currentIndex);
|
||||
const tuple = new RecordViewTuple(item, view);
|
||||
insertTuples.push(tuple);
|
||||
} else if (currentIndex == null) {
|
||||
this._viewContainer.remove(adjustedPreviousIndex);
|
||||
} else {
|
||||
const view = this._viewContainer.get(adjustedPreviousIndex);
|
||||
this._viewContainer.move(view, currentIndex);
|
||||
const tuple = new RecordViewTuple(item, <EmbeddedViewRef<NgForOfRow<T>>>view);
|
||||
insertTuples.push(tuple);
|
||||
}
|
||||
});
|
||||
|
||||
for (let i = 0; i < insertTuples.length; i++) {
|
||||
this._perViewChange(insertTuples[i].view, insertTuples[i].record);
|
||||
}
|
||||
|
||||
for (let i = 0, ilen = this._viewContainer.length; i < ilen; i++) {
|
||||
const viewRef = <EmbeddedViewRef<NgForOfRow<T>>>this._viewContainer.get(i);
|
||||
viewRef.context.index = i;
|
||||
viewRef.context.count = ilen;
|
||||
}
|
||||
|
||||
changes.forEachIdentityChange((record: any) => {
|
||||
const viewRef = <EmbeddedViewRef<NgForOfRow<T>>>this._viewContainer.get(record.currentIndex);
|
||||
viewRef.context.$implicit = record.item;
|
||||
});
|
||||
}
|
||||
|
||||
private _perViewChange(view: EmbeddedViewRef<NgForOfRow<T>>, record: IterableChangeRecord<any>) {
|
||||
view.context.$implicit = record.item;
|
||||
}
|
||||
}
|
||||
|
||||
class RecordViewTuple<T> {
|
||||
constructor(public record: any, public view: EmbeddedViewRef<NgForOfRow<T>>) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated from v4.0.0 - Use NgForOf<any> instead.
|
||||
*/
|
||||
export type NgFor = NgForOf<any>;
|
||||
|
||||
/**
|
||||
* @deprecated from v4.0.0 - Use NgForOf instead.
|
||||
*/
|
||||
export const NgFor = NgForOf;
|
||||
|
||||
export function getTypeNameForDebugging(type: any): string {
|
||||
return type['name'] || typeof type;
|
||||
}
|
157
packages/common/src/directives/ng_if.ts
Normal file
157
packages/common/src/directives/ng_if.ts
Normal file
@ -0,0 +1,157 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Directive, EmbeddedViewRef, Input, TemplateRef, ViewContainerRef} from '@angular/core';
|
||||
|
||||
|
||||
/**
|
||||
* Conditionally includes a template based on the value of an `expression`.
|
||||
*
|
||||
* `ngIf` evaluates the `expression` and then renders the `then` or `else` template in its place
|
||||
* when expression is truthy or falsy respectively. Typically the:
|
||||
* - `then` template is the inline template of `ngIf` unless bound to a different value.
|
||||
* - `else` template is blank unless it is bound.
|
||||
*
|
||||
* # Most common usage
|
||||
*
|
||||
* The most common usage of the `ngIf` directive is to conditionally show the inline template as
|
||||
* seen in this example:
|
||||
* {@example common/ngIf/ts/module.ts region='NgIfSimple'}
|
||||
*
|
||||
* # Showing an alternative template using `else`
|
||||
*
|
||||
* If it is necessary to display a template when the `expression` is falsy use the `else` template
|
||||
* binding as shown. Note that the `else` binding points to a `<ng-template>` labeled `#elseBlock`.
|
||||
* The template can be defined anywhere in the component view but is typically placed right after
|
||||
* `ngIf` for readability.
|
||||
*
|
||||
* {@example common/ngIf/ts/module.ts region='NgIfElse'}
|
||||
*
|
||||
* # Using non-inlined `then` template
|
||||
*
|
||||
* Usually the `then` template is the inlined template of the `ngIf`, but it can be changed using
|
||||
* a binding (just like `else`). Because `then` and `else` are bindings, the template references can
|
||||
* change at runtime as shown in this example.
|
||||
*
|
||||
* {@example common/ngIf/ts/module.ts region='NgIfThenElse'}
|
||||
*
|
||||
* # Storing conditional result in a variable
|
||||
*
|
||||
* A common pattern is that we need to show a set of properties from the same object. If the
|
||||
* object is undefined, then we have to use the safe-traversal-operator `?.` to guard against
|
||||
* dereferencing a `null` value. This is especially the case when waiting on async data such as
|
||||
* when using the `async` pipe as shown in folowing example:
|
||||
*
|
||||
* ```
|
||||
* Hello {{ (userStream|async)?.last }}, {{ (userStream|async)?.first }}!
|
||||
* ```
|
||||
*
|
||||
* There are several inefficiencies in the above example:
|
||||
* - We create multiple subscriptions on `userStream`. One for each `async` pipe, or two in the
|
||||
* example above.
|
||||
* - We cannot display an alternative screen while waiting for the data to arrive asynchronously.
|
||||
* - We have to use the safe-traversal-operator `?.` to access properties, which is cumbersome.
|
||||
* - We have to place the `async` pipe in parenthesis.
|
||||
*
|
||||
* A better way to do this is to use `ngIf` and store the result of the condition in a local
|
||||
* variable as shown in the the example below:
|
||||
*
|
||||
* {@example common/ngIf/ts/module.ts region='NgIfLet'}
|
||||
*
|
||||
* Notice that:
|
||||
* - We use only one `async` pipe and hence only one subscription gets created.
|
||||
* - `ngIf` stores the result of the `userStream|async` in the local variable `user`.
|
||||
* - The local `user` can then be bound repeatedly in a more efficient way.
|
||||
* - No need to use the safe-traversal-operator `?.` to access properties as `ngIf` will only
|
||||
* display the data if `userStream` returns a value.
|
||||
* - We can display an alternative template while waiting for the data.
|
||||
*
|
||||
* ### Syntax
|
||||
*
|
||||
* Simple form:
|
||||
* - `<div *ngIf="condition">...</div>`
|
||||
* - `<div template="ngIf condition">...</div>`
|
||||
* - `<ng-template [ngIf]="condition"><div>...</div></ng-template>`
|
||||
*
|
||||
* Form with an else block:
|
||||
* ```
|
||||
* <div *ngIf="condition; else elseBlock">...</div>
|
||||
* <ng-template #elseBlock>...</ng-template>
|
||||
* ```
|
||||
*
|
||||
* Form with a `then` and `else` block:
|
||||
* ```
|
||||
* <div *ngIf="condition; then thenBlock else elseBlock"></div>
|
||||
* <ng-template #thenBlock>...</ng-template>
|
||||
* <ng-template #elseBlock>...</ng-template>
|
||||
* ```
|
||||
*
|
||||
* Form with storing the value locally:
|
||||
* ```
|
||||
* <div *ngIf="condition; else elseBlock; let value">{{value}}</div>
|
||||
* <ng-template #elseBlock>...</ng-template>
|
||||
* ```
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Directive({selector: '[ngIf]'})
|
||||
export class NgIf {
|
||||
private _context: NgIfContext = new NgIfContext();
|
||||
private _thenTemplateRef: TemplateRef<NgIfContext> = null;
|
||||
private _elseTemplateRef: TemplateRef<NgIfContext> = null;
|
||||
private _thenViewRef: EmbeddedViewRef<NgIfContext> = null;
|
||||
private _elseViewRef: EmbeddedViewRef<NgIfContext> = null;
|
||||
|
||||
constructor(private _viewContainer: ViewContainerRef, templateRef: TemplateRef<NgIfContext>) {
|
||||
this._thenTemplateRef = templateRef;
|
||||
}
|
||||
|
||||
@Input()
|
||||
set ngIf(condition: any) {
|
||||
this._context.$implicit = condition;
|
||||
this._updateView();
|
||||
}
|
||||
|
||||
@Input()
|
||||
set ngIfThen(templateRef: TemplateRef<NgIfContext>) {
|
||||
this._thenTemplateRef = templateRef;
|
||||
this._thenViewRef = null; // clear previous view if any.
|
||||
this._updateView();
|
||||
}
|
||||
|
||||
@Input()
|
||||
set ngIfElse(templateRef: TemplateRef<NgIfContext>) {
|
||||
this._elseTemplateRef = templateRef;
|
||||
this._elseViewRef = null; // clear previous view if any.
|
||||
this._updateView();
|
||||
}
|
||||
|
||||
private _updateView() {
|
||||
if (this._context.$implicit) {
|
||||
if (!this._thenViewRef) {
|
||||
this._viewContainer.clear();
|
||||
this._elseViewRef = null;
|
||||
if (this._thenTemplateRef) {
|
||||
this._thenViewRef =
|
||||
this._viewContainer.createEmbeddedView(this._thenTemplateRef, this._context);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!this._elseViewRef) {
|
||||
this._viewContainer.clear();
|
||||
this._thenViewRef = null;
|
||||
if (this._elseTemplateRef) {
|
||||
this._elseViewRef =
|
||||
this._viewContainer.createEmbeddedView(this._elseTemplateRef, this._context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NgIfContext { public $implicit: any = null; }
|
109
packages/common/src/directives/ng_plural.ts
Normal file
109
packages/common/src/directives/ng_plural.ts
Normal file
@ -0,0 +1,109 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Attribute, Directive, Host, Input, TemplateRef, ViewContainerRef} from '@angular/core';
|
||||
|
||||
import {NgLocalization, getPluralCategory} from '../localization';
|
||||
|
||||
import {SwitchView} from './ng_switch';
|
||||
|
||||
|
||||
/**
|
||||
* @ngModule CommonModule
|
||||
*
|
||||
* @whatItDoes Adds / removes DOM sub-trees based on a numeric value. Tailored for pluralization.
|
||||
*
|
||||
* @howToUse
|
||||
* ```
|
||||
* <some-element [ngPlural]="value">
|
||||
* <ng-template ngPluralCase="=0">there is nothing</ng-template>
|
||||
* <ng-template ngPluralCase="=1">there is one</ng-template>
|
||||
* <ng-template ngPluralCase="few">there are a few</ng-template>
|
||||
* </some-element>
|
||||
* ```
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* Displays DOM sub-trees that match the switch expression value, or failing that, DOM sub-trees
|
||||
* that match the switch expression's pluralization category.
|
||||
*
|
||||
* To use this directive you must provide a container element that sets the `[ngPlural]` attribute
|
||||
* to a switch expression. Inner elements with a `[ngPluralCase]` will display based on their
|
||||
* expression:
|
||||
* - if `[ngPluralCase]` is set to a value starting with `=`, it will only display if the value
|
||||
* matches the switch expression exactly,
|
||||
* - otherwise, the view will be treated as a "category match", and will only display if exact
|
||||
* value matches aren't found and the value maps to its category for the defined locale.
|
||||
*
|
||||
* See http://cldr.unicode.org/index/cldr-spec/plural-rules
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
@Directive({selector: '[ngPlural]'})
|
||||
export class NgPlural {
|
||||
private _switchValue: number;
|
||||
private _activeView: SwitchView;
|
||||
private _caseViews: {[k: string]: SwitchView} = {};
|
||||
|
||||
constructor(private _localization: NgLocalization) {}
|
||||
|
||||
@Input()
|
||||
set ngPlural(value: number) {
|
||||
this._switchValue = value;
|
||||
this._updateView();
|
||||
}
|
||||
|
||||
addCase(value: string, switchView: SwitchView): void { this._caseViews[value] = switchView; }
|
||||
|
||||
private _updateView(): void {
|
||||
this._clearViews();
|
||||
|
||||
const cases = Object.keys(this._caseViews);
|
||||
const key = getPluralCategory(this._switchValue, cases, this._localization);
|
||||
this._activateView(this._caseViews[key]);
|
||||
}
|
||||
|
||||
private _clearViews() {
|
||||
if (this._activeView) this._activeView.destroy();
|
||||
}
|
||||
|
||||
private _activateView(view: SwitchView) {
|
||||
if (view) {
|
||||
this._activeView = view;
|
||||
this._activeView.create();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngModule CommonModule
|
||||
*
|
||||
* @whatItDoes Creates a view that will be added/removed from the parent {@link NgPlural} when the
|
||||
* given expression matches the plural expression according to CLDR rules.
|
||||
*
|
||||
* @howToUse
|
||||
* ```
|
||||
* <some-element [ngPlural]="value">
|
||||
* <ng-template ngPluralCase="=0">...</ng-template>
|
||||
* <ng-template ngPluralCase="other">...</ng-template>
|
||||
* </some-element>
|
||||
*```
|
||||
*
|
||||
* See {@link NgPlural} for more details and example.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
@Directive({selector: '[ngPluralCase]'})
|
||||
export class NgPluralCase {
|
||||
constructor(
|
||||
@Attribute('ngPluralCase') public value: string, template: TemplateRef<Object>,
|
||||
viewContainer: ViewContainerRef, @Host() ngPlural: NgPlural) {
|
||||
const isANumber: boolean = !isNaN(Number(value));
|
||||
ngPlural.addCase(isANumber ? `=${value}` : value, new SwitchView(viewContainer, template));
|
||||
}
|
||||
}
|
70
packages/common/src/directives/ng_style.ts
Normal file
70
packages/common/src/directives/ng_style.ts
Normal file
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Directive, DoCheck, ElementRef, Input, KeyValueChanges, KeyValueDiffer, KeyValueDiffers, Renderer} from '@angular/core';
|
||||
|
||||
/**
|
||||
* @ngModule CommonModule
|
||||
*
|
||||
* @whatItDoes Update an HTML element styles.
|
||||
*
|
||||
* @howToUse
|
||||
* ```
|
||||
* <some-element [ngStyle]="{'font-style': styleExp}">...</some-element>
|
||||
*
|
||||
* <some-element [ngStyle]="{'max-width.px': widthExp}">...</some-element>
|
||||
*
|
||||
* <some-element [ngStyle]="objExp">...</some-element>
|
||||
* ```
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* The styles are updated according to the value of the expression evaluation:
|
||||
* - keys are style names with an optional `.<unit>` suffix (ie 'top.px', 'font-style.em'),
|
||||
* - values are the values assigned to those properties (expressed in the given unit).
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Directive({selector: '[ngStyle]'})
|
||||
export class NgStyle implements DoCheck {
|
||||
private _ngStyle: {[key: string]: string};
|
||||
private _differ: KeyValueDiffer<string, string|number>;
|
||||
|
||||
constructor(
|
||||
private _differs: KeyValueDiffers, private _ngEl: ElementRef, private _renderer: Renderer) {}
|
||||
|
||||
@Input()
|
||||
set ngStyle(v: {[key: string]: string}) {
|
||||
this._ngStyle = v;
|
||||
if (!this._differ && v) {
|
||||
this._differ = this._differs.find(v).create();
|
||||
}
|
||||
}
|
||||
|
||||
ngDoCheck() {
|
||||
if (this._differ) {
|
||||
const changes = this._differ.diff(this._ngStyle);
|
||||
if (changes) {
|
||||
this._applyChanges(changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _applyChanges(changes: KeyValueChanges<string, string|number>): void {
|
||||
changes.forEachRemovedItem((record) => this._setStyle(record.key, null));
|
||||
changes.forEachAddedItem((record) => this._setStyle(record.key, record.currentValue));
|
||||
changes.forEachChangedItem((record) => this._setStyle(record.key, record.currentValue));
|
||||
}
|
||||
|
||||
private _setStyle(nameAndUnit: string, value: string|number): void {
|
||||
const [name, unit] = nameAndUnit.split('.');
|
||||
value = value != null && unit ? `${value}${unit}` : value;
|
||||
|
||||
this._renderer.setElementStyle(this._ngEl.nativeElement, name, value as string);
|
||||
}
|
||||
}
|
200
packages/common/src/directives/ng_switch.ts
Normal file
200
packages/common/src/directives/ng_switch.ts
Normal file
@ -0,0 +1,200 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Directive, DoCheck, Host, Input, TemplateRef, ViewContainerRef} from '@angular/core';
|
||||
|
||||
export class SwitchView {
|
||||
private _created = false;
|
||||
|
||||
constructor(
|
||||
private _viewContainerRef: ViewContainerRef, private _templateRef: TemplateRef<Object>) {}
|
||||
|
||||
create(): void {
|
||||
this._created = true;
|
||||
this._viewContainerRef.createEmbeddedView(this._templateRef);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this._created = false;
|
||||
this._viewContainerRef.clear();
|
||||
}
|
||||
|
||||
enforceState(created: boolean) {
|
||||
if (created && !this._created) {
|
||||
this.create();
|
||||
} else if (!created && this._created) {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngModule CommonModule
|
||||
*
|
||||
* @whatItDoes Adds / removes DOM sub-trees when the nest match expressions matches the switch
|
||||
* expression.
|
||||
*
|
||||
* @howToUse
|
||||
* ```
|
||||
* <container-element [ngSwitch]="switch_expression">
|
||||
* <some-element *ngSwitchCase="match_expression_1">...</some-element>
|
||||
* <some-element *ngSwitchCase="match_expression_2">...</some-element>
|
||||
* <some-other-element *ngSwitchCase="match_expression_3">...</some-other-element>
|
||||
* <ng-container *ngSwitchCase="match_expression_3">
|
||||
* <!-- use a ng-container to group multiple root nodes -->
|
||||
* <inner-element></inner-element>
|
||||
* <inner-other-element></inner-other-element>
|
||||
* </ng-container>
|
||||
* <some-element *ngSwitchDefault>...</some-element>
|
||||
* </container-element>
|
||||
* ```
|
||||
* @description
|
||||
*
|
||||
* `NgSwitch` stamps out nested views when their match expression value matches the value of the
|
||||
* switch expression.
|
||||
*
|
||||
* In other words:
|
||||
* - you define a container element (where you place the directive with a switch expression on the
|
||||
* `[ngSwitch]="..."` attribute)
|
||||
* - you define inner views inside the `NgSwitch` and place a `*ngSwitchCase` attribute on the view
|
||||
* root elements.
|
||||
*
|
||||
* Elements within `NgSwitch` but outside of a `NgSwitchCase` or `NgSwitchDefault` directives will
|
||||
* be preserved at the location.
|
||||
*
|
||||
* The `ngSwitchCase` directive informs the parent `NgSwitch` of which view to display when the
|
||||
* expression is evaluated.
|
||||
* When no matching expression is found on a `ngSwitchCase` view, the `ngSwitchDefault` view is
|
||||
* stamped out.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Directive({selector: '[ngSwitch]'})
|
||||
export class NgSwitch {
|
||||
private _defaultViews: SwitchView[];
|
||||
private _defaultUsed = false;
|
||||
private _caseCount = 0;
|
||||
private _lastCaseCheckIndex = 0;
|
||||
private _lastCasesMatched = false;
|
||||
private _ngSwitch: any;
|
||||
|
||||
@Input()
|
||||
set ngSwitch(newValue: any) {
|
||||
this._ngSwitch = newValue;
|
||||
if (this._caseCount === 0) {
|
||||
this._updateDefaultCases(true);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_addCase(): number { return this._caseCount++; }
|
||||
|
||||
/** @internal */
|
||||
_addDefault(view: SwitchView) {
|
||||
if (!this._defaultViews) {
|
||||
this._defaultViews = [];
|
||||
}
|
||||
this._defaultViews.push(view);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_matchCase(value: any): boolean {
|
||||
const matched = value == this._ngSwitch;
|
||||
this._lastCasesMatched = this._lastCasesMatched || matched;
|
||||
this._lastCaseCheckIndex++;
|
||||
if (this._lastCaseCheckIndex === this._caseCount) {
|
||||
this._updateDefaultCases(!this._lastCasesMatched);
|
||||
this._lastCaseCheckIndex = 0;
|
||||
this._lastCasesMatched = false;
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
private _updateDefaultCases(useDefault: boolean) {
|
||||
if (this._defaultViews && useDefault !== this._defaultUsed) {
|
||||
this._defaultUsed = useDefault;
|
||||
for (let i = 0; i < this._defaultViews.length; i++) {
|
||||
const defaultView = this._defaultViews[i];
|
||||
defaultView.enforceState(useDefault);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngModule CommonModule
|
||||
*
|
||||
* @whatItDoes Creates a view that will be added/removed from the parent {@link NgSwitch} when the
|
||||
* given expression evaluate to respectively the same/different value as the switch
|
||||
* expression.
|
||||
*
|
||||
* @howToUse
|
||||
* ```
|
||||
* <container-element [ngSwitch]="switch_expression">
|
||||
* <some-element *ngSwitchCase="match_expression_1">...</some-element>
|
||||
* </container-element>
|
||||
*```
|
||||
* @description
|
||||
*
|
||||
* Insert the sub-tree when the expression evaluates to the same value as the enclosing switch
|
||||
* expression.
|
||||
*
|
||||
* If multiple match expressions match the switch expression value, all of them are displayed.
|
||||
*
|
||||
* See {@link NgSwitch} for more details and example.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Directive({selector: '[ngSwitchCase]'})
|
||||
export class NgSwitchCase implements DoCheck {
|
||||
private _view: SwitchView;
|
||||
|
||||
@Input()
|
||||
ngSwitchCase: any;
|
||||
|
||||
constructor(
|
||||
viewContainer: ViewContainerRef, templateRef: TemplateRef<Object>,
|
||||
@Host() private ngSwitch: NgSwitch) {
|
||||
ngSwitch._addCase();
|
||||
this._view = new SwitchView(viewContainer, templateRef);
|
||||
}
|
||||
|
||||
ngDoCheck() { this._view.enforceState(this.ngSwitch._matchCase(this.ngSwitchCase)); }
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngModule CommonModule
|
||||
* @whatItDoes Creates a view that is added to the parent {@link NgSwitch} when no case expressions
|
||||
* match the
|
||||
* switch expression.
|
||||
*
|
||||
* @howToUse
|
||||
* ```
|
||||
* <container-element [ngSwitch]="switch_expression">
|
||||
* <some-element *ngSwitchCase="match_expression_1">...</some-element>
|
||||
* <some-other-element *ngSwitchDefault>...</some-other-element>
|
||||
* </container-element>
|
||||
* ```
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* Insert the sub-tree when no case expressions evaluate to the same value as the enclosing switch
|
||||
* expression.
|
||||
*
|
||||
* See {@link NgSwitch} for more details and example.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Directive({selector: '[ngSwitchDefault]'})
|
||||
export class NgSwitchDefault {
|
||||
constructor(
|
||||
viewContainer: ViewContainerRef, templateRef: TemplateRef<Object>,
|
||||
@Host() ngSwitch: NgSwitch) {
|
||||
ngSwitch._addDefault(new SwitchView(viewContainer, templateRef));
|
||||
}
|
||||
}
|
61
packages/common/src/directives/ng_template_outlet.ts
Normal file
61
packages/common/src/directives/ng_template_outlet.ts
Normal file
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Directive, EmbeddedViewRef, Input, OnChanges, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core';
|
||||
|
||||
/**
|
||||
* @ngModule CommonModule
|
||||
*
|
||||
* @whatItDoes Inserts an embedded view from a prepared `TemplateRef`
|
||||
*
|
||||
* @howToUse
|
||||
* ```
|
||||
* <ng-container *ngTemplateOutlet="templateRefExp; context: contextExp"></ng-container>
|
||||
* ```
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* You can attach a context object to the `EmbeddedViewRef` by setting `[ngTemplateOutletContext]`.
|
||||
* `[ngTemplateOutletContext]` should be an object, the object's keys will be available for binding
|
||||
* by the local template `let` declarations.
|
||||
*
|
||||
* Note: using the key `$implicit` in the context object will set it's value as default.
|
||||
*
|
||||
* # Example
|
||||
*
|
||||
* {@example common/ngTemplateOutlet/ts/module.ts region='NgTemplateOutlet'}
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
@Directive({selector: '[ngTemplateOutlet]'})
|
||||
export class NgTemplateOutlet implements OnChanges {
|
||||
private _viewRef: EmbeddedViewRef<any>;
|
||||
|
||||
@Input() public ngTemplateOutletContext: Object;
|
||||
|
||||
@Input() public ngTemplateOutlet: TemplateRef<any>;
|
||||
|
||||
constructor(private _viewContainerRef: ViewContainerRef) {}
|
||||
|
||||
/**
|
||||
* @deprecated v4.0.0 - Renamed to ngTemplateOutletContext.
|
||||
*/
|
||||
@Input()
|
||||
set ngOutletContext(context: Object) { this.ngTemplateOutletContext = context; }
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (this._viewRef) {
|
||||
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._viewRef));
|
||||
}
|
||||
|
||||
if (this.ngTemplateOutlet) {
|
||||
this._viewRef = this._viewContainerRef.createEmbeddedView(
|
||||
this.ngTemplateOutlet, this.ngTemplateOutletContext);
|
||||
}
|
||||
}
|
||||
}
|
434
packages/common/src/localization.ts
Normal file
434
packages/common/src/localization.ts
Normal file
@ -0,0 +1,434 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Inject, Injectable, LOCALE_ID} from '@angular/core';
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export abstract class NgLocalization { abstract getPluralCategory(value: any): string; }
|
||||
|
||||
|
||||
/**
|
||||
* Returns the plural category for a given value.
|
||||
* - "=value" when the case exists,
|
||||
* - the plural category otherwise
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function getPluralCategory(
|
||||
value: number, cases: string[], ngLocalization: NgLocalization): string {
|
||||
let key = `=${value}`;
|
||||
|
||||
if (cases.indexOf(key) > -1) {
|
||||
return key;
|
||||
}
|
||||
|
||||
key = ngLocalization.getPluralCategory(value);
|
||||
|
||||
if (cases.indexOf(key) > -1) {
|
||||
return key;
|
||||
}
|
||||
|
||||
if (cases.indexOf('other') > -1) {
|
||||
return 'other';
|
||||
}
|
||||
|
||||
throw new Error(`No plural message found for value "${value}"`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plural case based on the locale
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
@Injectable()
|
||||
export class NgLocaleLocalization extends NgLocalization {
|
||||
constructor(@Inject(LOCALE_ID) protected locale: string) { super(); }
|
||||
|
||||
getPluralCategory(value: any): string {
|
||||
const plural = getPluralCase(this.locale, value);
|
||||
|
||||
switch (plural) {
|
||||
case Plural.Zero:
|
||||
return 'zero';
|
||||
case Plural.One:
|
||||
return 'one';
|
||||
case Plural.Two:
|
||||
return 'two';
|
||||
case Plural.Few:
|
||||
return 'few';
|
||||
case Plural.Many:
|
||||
return 'many';
|
||||
default:
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is generated code DO NOT MODIFY
|
||||
// see angular2/script/cldr/gen_plural_rules.js
|
||||
|
||||
/** @experimental */
|
||||
export enum Plural {
|
||||
Zero,
|
||||
One,
|
||||
Two,
|
||||
Few,
|
||||
Many,
|
||||
Other,
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plural case based on the locale
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export function getPluralCase(locale: string, nLike: number | string): Plural {
|
||||
// TODO(vicb): lazy compute
|
||||
if (typeof nLike === 'string') {
|
||||
nLike = parseInt(<string>nLike, 10);
|
||||
}
|
||||
const n: number = nLike as number;
|
||||
const nDecimal = n.toString().replace(/^[^.]*\.?/, '');
|
||||
const i = Math.floor(Math.abs(n));
|
||||
const v = nDecimal.length;
|
||||
const f = parseInt(nDecimal, 10);
|
||||
const t = parseInt(n.toString().replace(/^[^.]*\.?|0+$/g, ''), 10) || 0;
|
||||
|
||||
const lang = locale.split('-')[0].toLowerCase();
|
||||
|
||||
switch (lang) {
|
||||
case 'af':
|
||||
case 'asa':
|
||||
case 'az':
|
||||
case 'bem':
|
||||
case 'bez':
|
||||
case 'bg':
|
||||
case 'brx':
|
||||
case 'ce':
|
||||
case 'cgg':
|
||||
case 'chr':
|
||||
case 'ckb':
|
||||
case 'ee':
|
||||
case 'el':
|
||||
case 'eo':
|
||||
case 'es':
|
||||
case 'eu':
|
||||
case 'fo':
|
||||
case 'fur':
|
||||
case 'gsw':
|
||||
case 'ha':
|
||||
case 'haw':
|
||||
case 'hu':
|
||||
case 'jgo':
|
||||
case 'jmc':
|
||||
case 'ka':
|
||||
case 'kk':
|
||||
case 'kkj':
|
||||
case 'kl':
|
||||
case 'ks':
|
||||
case 'ksb':
|
||||
case 'ky':
|
||||
case 'lb':
|
||||
case 'lg':
|
||||
case 'mas':
|
||||
case 'mgo':
|
||||
case 'ml':
|
||||
case 'mn':
|
||||
case 'nb':
|
||||
case 'nd':
|
||||
case 'ne':
|
||||
case 'nn':
|
||||
case 'nnh':
|
||||
case 'nyn':
|
||||
case 'om':
|
||||
case 'or':
|
||||
case 'os':
|
||||
case 'ps':
|
||||
case 'rm':
|
||||
case 'rof':
|
||||
case 'rwk':
|
||||
case 'saq':
|
||||
case 'seh':
|
||||
case 'sn':
|
||||
case 'so':
|
||||
case 'sq':
|
||||
case 'ta':
|
||||
case 'te':
|
||||
case 'teo':
|
||||
case 'tk':
|
||||
case 'tr':
|
||||
case 'ug':
|
||||
case 'uz':
|
||||
case 'vo':
|
||||
case 'vun':
|
||||
case 'wae':
|
||||
case 'xog':
|
||||
if (n === 1) return Plural.One;
|
||||
return Plural.Other;
|
||||
case 'agq':
|
||||
case 'bas':
|
||||
case 'cu':
|
||||
case 'dav':
|
||||
case 'dje':
|
||||
case 'dua':
|
||||
case 'dyo':
|
||||
case 'ebu':
|
||||
case 'ewo':
|
||||
case 'guz':
|
||||
case 'kam':
|
||||
case 'khq':
|
||||
case 'ki':
|
||||
case 'kln':
|
||||
case 'kok':
|
||||
case 'ksf':
|
||||
case 'lrc':
|
||||
case 'lu':
|
||||
case 'luo':
|
||||
case 'luy':
|
||||
case 'mer':
|
||||
case 'mfe':
|
||||
case 'mgh':
|
||||
case 'mua':
|
||||
case 'mzn':
|
||||
case 'nmg':
|
||||
case 'nus':
|
||||
case 'qu':
|
||||
case 'rn':
|
||||
case 'rw':
|
||||
case 'sbp':
|
||||
case 'twq':
|
||||
case 'vai':
|
||||
case 'yav':
|
||||
case 'yue':
|
||||
case 'zgh':
|
||||
case 'ak':
|
||||
case 'ln':
|
||||
case 'mg':
|
||||
case 'pa':
|
||||
case 'ti':
|
||||
if (n === Math.floor(n) && n >= 0 && n <= 1) return Plural.One;
|
||||
return Plural.Other;
|
||||
case 'am':
|
||||
case 'as':
|
||||
case 'bn':
|
||||
case 'fa':
|
||||
case 'gu':
|
||||
case 'hi':
|
||||
case 'kn':
|
||||
case 'mr':
|
||||
case 'zu':
|
||||
if (i === 0 || n === 1) return Plural.One;
|
||||
return Plural.Other;
|
||||
case 'ar':
|
||||
if (n === 0) return Plural.Zero;
|
||||
if (n === 1) return Plural.One;
|
||||
if (n === 2) return Plural.Two;
|
||||
if (n % 100 === Math.floor(n % 100) && n % 100 >= 3 && n % 100 <= 10) return Plural.Few;
|
||||
if (n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 99) return Plural.Many;
|
||||
return Plural.Other;
|
||||
case 'ast':
|
||||
case 'ca':
|
||||
case 'de':
|
||||
case 'en':
|
||||
case 'et':
|
||||
case 'fi':
|
||||
case 'fy':
|
||||
case 'gl':
|
||||
case 'it':
|
||||
case 'nl':
|
||||
case 'sv':
|
||||
case 'sw':
|
||||
case 'ur':
|
||||
case 'yi':
|
||||
if (i === 1 && v === 0) return Plural.One;
|
||||
return Plural.Other;
|
||||
case 'be':
|
||||
if (n % 10 === 1 && !(n % 100 === 11)) return Plural.One;
|
||||
if (n % 10 === Math.floor(n % 10) && n % 10 >= 2 && n % 10 <= 4 &&
|
||||
!(n % 100 >= 12 && n % 100 <= 14))
|
||||
return Plural.Few;
|
||||
if (n % 10 === 0 || n % 10 === Math.floor(n % 10) && n % 10 >= 5 && n % 10 <= 9 ||
|
||||
n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 14)
|
||||
return Plural.Many;
|
||||
return Plural.Other;
|
||||
case 'br':
|
||||
if (n % 10 === 1 && !(n % 100 === 11 || n % 100 === 71 || n % 100 === 91)) return Plural.One;
|
||||
if (n % 10 === 2 && !(n % 100 === 12 || n % 100 === 72 || n % 100 === 92)) return Plural.Two;
|
||||
if (n % 10 === Math.floor(n % 10) && (n % 10 >= 3 && n % 10 <= 4 || n % 10 === 9) &&
|
||||
!(n % 100 >= 10 && n % 100 <= 19 || n % 100 >= 70 && n % 100 <= 79 ||
|
||||
n % 100 >= 90 && n % 100 <= 99))
|
||||
return Plural.Few;
|
||||
if (!(n === 0) && n % 1e6 === 0) return Plural.Many;
|
||||
return Plural.Other;
|
||||
case 'bs':
|
||||
case 'hr':
|
||||
case 'sr':
|
||||
if (v === 0 && i % 10 === 1 && !(i % 100 === 11) || f % 10 === 1 && !(f % 100 === 11))
|
||||
return Plural.One;
|
||||
if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 &&
|
||||
!(i % 100 >= 12 && i % 100 <= 14) ||
|
||||
f % 10 === Math.floor(f % 10) && f % 10 >= 2 && f % 10 <= 4 &&
|
||||
!(f % 100 >= 12 && f % 100 <= 14))
|
||||
return Plural.Few;
|
||||
return Plural.Other;
|
||||
case 'cs':
|
||||
case 'sk':
|
||||
if (i === 1 && v === 0) return Plural.One;
|
||||
if (i === Math.floor(i) && i >= 2 && i <= 4 && v === 0) return Plural.Few;
|
||||
if (!(v === 0)) return Plural.Many;
|
||||
return Plural.Other;
|
||||
case 'cy':
|
||||
if (n === 0) return Plural.Zero;
|
||||
if (n === 1) return Plural.One;
|
||||
if (n === 2) return Plural.Two;
|
||||
if (n === 3) return Plural.Few;
|
||||
if (n === 6) return Plural.Many;
|
||||
return Plural.Other;
|
||||
case 'da':
|
||||
if (n === 1 || !(t === 0) && (i === 0 || i === 1)) return Plural.One;
|
||||
return Plural.Other;
|
||||
case 'dsb':
|
||||
case 'hsb':
|
||||
if (v === 0 && i % 100 === 1 || f % 100 === 1) return Plural.One;
|
||||
if (v === 0 && i % 100 === 2 || f % 100 === 2) return Plural.Two;
|
||||
if (v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 3 && i % 100 <= 4 ||
|
||||
f % 100 === Math.floor(f % 100) && f % 100 >= 3 && f % 100 <= 4)
|
||||
return Plural.Few;
|
||||
return Plural.Other;
|
||||
case 'ff':
|
||||
case 'fr':
|
||||
case 'hy':
|
||||
case 'kab':
|
||||
if (i === 0 || i === 1) return Plural.One;
|
||||
return Plural.Other;
|
||||
case 'fil':
|
||||
if (v === 0 && (i === 1 || i === 2 || i === 3) ||
|
||||
v === 0 && !(i % 10 === 4 || i % 10 === 6 || i % 10 === 9) ||
|
||||
!(v === 0) && !(f % 10 === 4 || f % 10 === 6 || f % 10 === 9))
|
||||
return Plural.One;
|
||||
return Plural.Other;
|
||||
case 'ga':
|
||||
if (n === 1) return Plural.One;
|
||||
if (n === 2) return Plural.Two;
|
||||
if (n === Math.floor(n) && n >= 3 && n <= 6) return Plural.Few;
|
||||
if (n === Math.floor(n) && n >= 7 && n <= 10) return Plural.Many;
|
||||
return Plural.Other;
|
||||
case 'gd':
|
||||
if (n === 1 || n === 11) return Plural.One;
|
||||
if (n === 2 || n === 12) return Plural.Two;
|
||||
if (n === Math.floor(n) && (n >= 3 && n <= 10 || n >= 13 && n <= 19)) return Plural.Few;
|
||||
return Plural.Other;
|
||||
case 'gv':
|
||||
if (v === 0 && i % 10 === 1) return Plural.One;
|
||||
if (v === 0 && i % 10 === 2) return Plural.Two;
|
||||
if (v === 0 &&
|
||||
(i % 100 === 0 || i % 100 === 20 || i % 100 === 40 || i % 100 === 60 || i % 100 === 80))
|
||||
return Plural.Few;
|
||||
if (!(v === 0)) return Plural.Many;
|
||||
return Plural.Other;
|
||||
case 'he':
|
||||
if (i === 1 && v === 0) return Plural.One;
|
||||
if (i === 2 && v === 0) return Plural.Two;
|
||||
if (v === 0 && !(n >= 0 && n <= 10) && n % 10 === 0) return Plural.Many;
|
||||
return Plural.Other;
|
||||
case 'is':
|
||||
if (t === 0 && i % 10 === 1 && !(i % 100 === 11) || !(t === 0)) return Plural.One;
|
||||
return Plural.Other;
|
||||
case 'ksh':
|
||||
if (n === 0) return Plural.Zero;
|
||||
if (n === 1) return Plural.One;
|
||||
return Plural.Other;
|
||||
case 'kw':
|
||||
case 'naq':
|
||||
case 'se':
|
||||
case 'smn':
|
||||
if (n === 1) return Plural.One;
|
||||
if (n === 2) return Plural.Two;
|
||||
return Plural.Other;
|
||||
case 'lag':
|
||||
if (n === 0) return Plural.Zero;
|
||||
if ((i === 0 || i === 1) && !(n === 0)) return Plural.One;
|
||||
return Plural.Other;
|
||||
case 'lt':
|
||||
if (n % 10 === 1 && !(n % 100 >= 11 && n % 100 <= 19)) return Plural.One;
|
||||
if (n % 10 === Math.floor(n % 10) && n % 10 >= 2 && n % 10 <= 9 &&
|
||||
!(n % 100 >= 11 && n % 100 <= 19))
|
||||
return Plural.Few;
|
||||
if (!(f === 0)) return Plural.Many;
|
||||
return Plural.Other;
|
||||
case 'lv':
|
||||
case 'prg':
|
||||
if (n % 10 === 0 || n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 19 ||
|
||||
v === 2 && f % 100 === Math.floor(f % 100) && f % 100 >= 11 && f % 100 <= 19)
|
||||
return Plural.Zero;
|
||||
if (n % 10 === 1 && !(n % 100 === 11) || v === 2 && f % 10 === 1 && !(f % 100 === 11) ||
|
||||
!(v === 2) && f % 10 === 1)
|
||||
return Plural.One;
|
||||
return Plural.Other;
|
||||
case 'mk':
|
||||
if (v === 0 && i % 10 === 1 || f % 10 === 1) return Plural.One;
|
||||
return Plural.Other;
|
||||
case 'mt':
|
||||
if (n === 1) return Plural.One;
|
||||
if (n === 0 || n % 100 === Math.floor(n % 100) && n % 100 >= 2 && n % 100 <= 10)
|
||||
return Plural.Few;
|
||||
if (n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 19) return Plural.Many;
|
||||
return Plural.Other;
|
||||
case 'pl':
|
||||
if (i === 1 && v === 0) return Plural.One;
|
||||
if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 &&
|
||||
!(i % 100 >= 12 && i % 100 <= 14))
|
||||
return Plural.Few;
|
||||
if (v === 0 && !(i === 1) && i % 10 === Math.floor(i % 10) && i % 10 >= 0 && i % 10 <= 1 ||
|
||||
v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 5 && i % 10 <= 9 ||
|
||||
v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 12 && i % 100 <= 14)
|
||||
return Plural.Many;
|
||||
return Plural.Other;
|
||||
case 'pt':
|
||||
if (n === Math.floor(n) && n >= 0 && n <= 2 && !(n === 2)) return Plural.One;
|
||||
return Plural.Other;
|
||||
case 'ro':
|
||||
if (i === 1 && v === 0) return Plural.One;
|
||||
if (!(v === 0) || n === 0 ||
|
||||
!(n === 1) && n % 100 === Math.floor(n % 100) && n % 100 >= 1 && n % 100 <= 19)
|
||||
return Plural.Few;
|
||||
return Plural.Other;
|
||||
case 'ru':
|
||||
case 'uk':
|
||||
if (v === 0 && i % 10 === 1 && !(i % 100 === 11)) return Plural.One;
|
||||
if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 &&
|
||||
!(i % 100 >= 12 && i % 100 <= 14))
|
||||
return Plural.Few;
|
||||
if (v === 0 && i % 10 === 0 ||
|
||||
v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 5 && i % 10 <= 9 ||
|
||||
v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 11 && i % 100 <= 14)
|
||||
return Plural.Many;
|
||||
return Plural.Other;
|
||||
case 'shi':
|
||||
if (i === 0 || n === 1) return Plural.One;
|
||||
if (n === Math.floor(n) && n >= 2 && n <= 10) return Plural.Few;
|
||||
return Plural.Other;
|
||||
case 'si':
|
||||
if (n === 0 || n === 1 || i === 0 && f === 1) return Plural.One;
|
||||
return Plural.Other;
|
||||
case 'sl':
|
||||
if (v === 0 && i % 100 === 1) return Plural.One;
|
||||
if (v === 0 && i % 100 === 2) return Plural.Two;
|
||||
if (v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 3 && i % 100 <= 4 || !(v === 0))
|
||||
return Plural.Few;
|
||||
return Plural.Other;
|
||||
case 'tzm':
|
||||
if (n === Math.floor(n) && n >= 0 && n <= 1 || n === Math.floor(n) && n >= 11 && n <= 99)
|
||||
return Plural.One;
|
||||
return Plural.Other;
|
||||
default:
|
||||
return Plural.Other;
|
||||
}
|
||||
}
|
87
packages/common/src/location/hash_location_strategy.ts
Normal file
87
packages/common/src/location/hash_location_strategy.ts
Normal file
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Inject, Injectable, Optional} from '@angular/core';
|
||||
|
||||
|
||||
import {Location} from './location';
|
||||
import {APP_BASE_HREF, LocationStrategy} from './location_strategy';
|
||||
import {LocationChangeListener, PlatformLocation} from './platform_location';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @whatItDoes Use URL hash for storing application location data.
|
||||
* @description
|
||||
* `HashLocationStrategy` is a {@link LocationStrategy} used to configure the
|
||||
* {@link Location} service to represent its state in the
|
||||
* [hash fragment](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax)
|
||||
* of the browser's URL.
|
||||
*
|
||||
* For instance, if you call `location.go('/foo')`, the browser's URL will become
|
||||
* `example.com#/foo`.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* {@example common/location/ts/hash_location_component.ts region='LocationComponent'}
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Injectable()
|
||||
export class HashLocationStrategy extends LocationStrategy {
|
||||
private _baseHref: string = '';
|
||||
constructor(
|
||||
private _platformLocation: PlatformLocation,
|
||||
@Optional() @Inject(APP_BASE_HREF) _baseHref?: string) {
|
||||
super();
|
||||
if (_baseHref != null) {
|
||||
this._baseHref = _baseHref;
|
||||
}
|
||||
}
|
||||
|
||||
onPopState(fn: LocationChangeListener): void {
|
||||
this._platformLocation.onPopState(fn);
|
||||
this._platformLocation.onHashChange(fn);
|
||||
}
|
||||
|
||||
getBaseHref(): string { return this._baseHref; }
|
||||
|
||||
path(includeHash: boolean = false): string {
|
||||
// the hash value is always prefixed with a `#`
|
||||
// and if it is empty then it will stay empty
|
||||
let path = this._platformLocation.hash;
|
||||
if (path == null) path = '#';
|
||||
|
||||
return path.length > 0 ? path.substring(1) : path;
|
||||
}
|
||||
|
||||
prepareExternalUrl(internal: string): string {
|
||||
const url = Location.joinWithSlash(this._baseHref, internal);
|
||||
return url.length > 0 ? ('#' + url) : url;
|
||||
}
|
||||
|
||||
pushState(state: any, title: string, path: string, queryParams: string) {
|
||||
let url = this.prepareExternalUrl(path + Location.normalizeQueryParams(queryParams));
|
||||
if (url.length == 0) {
|
||||
url = this._platformLocation.pathname;
|
||||
}
|
||||
this._platformLocation.pushState(state, title, url);
|
||||
}
|
||||
|
||||
replaceState(state: any, title: string, path: string, queryParams: string) {
|
||||
let url = this.prepareExternalUrl(path + Location.normalizeQueryParams(queryParams));
|
||||
if (url.length == 0) {
|
||||
url = this._platformLocation.pathname;
|
||||
}
|
||||
this._platformLocation.replaceState(state, title, url);
|
||||
}
|
||||
|
||||
forward(): void { this._platformLocation.forward(); }
|
||||
|
||||
back(): void { this._platformLocation.back(); }
|
||||
}
|
13
packages/common/src/location/index.ts
Normal file
13
packages/common/src/location/index.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
export * from './platform_location';
|
||||
export * from './location_strategy';
|
||||
export * from './hash_location_strategy';
|
||||
export * from './path_location_strategy';
|
||||
export * from './location';
|
182
packages/common/src/location/location.ts
Normal file
182
packages/common/src/location/location.ts
Normal file
@ -0,0 +1,182 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {EventEmitter, Injectable} from '@angular/core';
|
||||
|
||||
import {LocationStrategy} from './location_strategy';
|
||||
|
||||
/** @experimental */
|
||||
export interface PopStateEvent {
|
||||
pop?: boolean;
|
||||
type?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes `Location` is a service that applications can use to interact with a browser's URL.
|
||||
* @description
|
||||
* Depending on which {@link LocationStrategy} is used, `Location` will either persist
|
||||
* to the URL's path or the URL's hash segment.
|
||||
*
|
||||
* Note: it's better to use {@link Router#navigate} service to trigger route changes. Use
|
||||
* `Location` only if you need to interact with or create normalized URLs outside of
|
||||
* routing.
|
||||
*
|
||||
* `Location` is responsible for normalizing the URL against the application's base href.
|
||||
* A normalized URL is absolute from the URL host, includes the application's base href, and has no
|
||||
* trailing slash:
|
||||
* - `/my/app/user/123` is normalized
|
||||
* - `my/app/user/123` **is not** normalized
|
||||
* - `/my/app/user/123/` **is not** normalized
|
||||
*
|
||||
* ### Example
|
||||
* {@example common/location/ts/path_location_component.ts region='LocationComponent'}
|
||||
* @stable
|
||||
*/
|
||||
@Injectable()
|
||||
export class Location {
|
||||
/** @internal */
|
||||
_subject: EventEmitter<any> = new EventEmitter();
|
||||
/** @internal */
|
||||
_baseHref: string;
|
||||
/** @internal */
|
||||
_platformStrategy: LocationStrategy;
|
||||
|
||||
constructor(platformStrategy: LocationStrategy) {
|
||||
this._platformStrategy = platformStrategy;
|
||||
const browserBaseHref = this._platformStrategy.getBaseHref();
|
||||
this._baseHref = Location.stripTrailingSlash(_stripIndexHtml(browserBaseHref));
|
||||
this._platformStrategy.onPopState((ev) => {
|
||||
this._subject.emit({
|
||||
'url': this.path(true),
|
||||
'pop': true,
|
||||
'type': ev.type,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normalized URL path.
|
||||
*/
|
||||
// TODO: vsavkin. Remove the boolean flag and always include hash once the deprecated router is
|
||||
// removed.
|
||||
path(includeHash: boolean = false): string {
|
||||
return this.normalize(this._platformStrategy.path(includeHash));
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the given path and compares to the current normalized path.
|
||||
*/
|
||||
isCurrentPathEqualTo(path: string, query: string = ''): boolean {
|
||||
return this.path() == this.normalize(path + Location.normalizeQueryParams(query));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string representing a URL, returns the normalized URL path without leading or
|
||||
* trailing slashes.
|
||||
*/
|
||||
normalize(url: string): string {
|
||||
return Location.stripTrailingSlash(_stripBaseHref(this._baseHref, _stripIndexHtml(url)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string representing a URL, returns the platform-specific external URL path.
|
||||
* If the given URL doesn't begin with a leading slash (`'/'`), this method adds one
|
||||
* before normalizing. This method will also add a hash if `HashLocationStrategy` is
|
||||
* used, or the `APP_BASE_HREF` if the `PathLocationStrategy` is in use.
|
||||
*/
|
||||
prepareExternalUrl(url: string): string {
|
||||
if (url && url[0] !== '/') {
|
||||
url = '/' + url;
|
||||
}
|
||||
return this._platformStrategy.prepareExternalUrl(url);
|
||||
}
|
||||
|
||||
// TODO: rename this method to pushState
|
||||
/**
|
||||
* Changes the browsers URL to the normalized version of the given URL, and pushes a
|
||||
* new item onto the platform's history.
|
||||
*/
|
||||
go(path: string, query: string = ''): void {
|
||||
this._platformStrategy.pushState(null, '', path, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the browsers URL to the normalized version of the given URL, and replaces
|
||||
* the top item on the platform's history stack.
|
||||
*/
|
||||
replaceState(path: string, query: string = ''): void {
|
||||
this._platformStrategy.replaceState(null, '', path, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates forward in the platform's history.
|
||||
*/
|
||||
forward(): void { this._platformStrategy.forward(); }
|
||||
|
||||
/**
|
||||
* Navigates back in the platform's history.
|
||||
*/
|
||||
back(): void { this._platformStrategy.back(); }
|
||||
|
||||
/**
|
||||
* Subscribe to the platform's `popState` events.
|
||||
*/
|
||||
subscribe(
|
||||
onNext: (value: PopStateEvent) => void, onThrow: (exception: any) => void = null,
|
||||
onReturn: () => void = null): Object {
|
||||
return this._subject.subscribe({next: onNext, error: onThrow, complete: onReturn});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string of url parameters, prepend with '?' if needed, otherwise return parameters as
|
||||
* is.
|
||||
*/
|
||||
public static normalizeQueryParams(params: string): string {
|
||||
return params && params[0] !== '?' ? '?' + params : params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given 2 parts of a url, join them with a slash if needed.
|
||||
*/
|
||||
public static joinWithSlash(start: string, end: string): string {
|
||||
if (start.length == 0) {
|
||||
return end;
|
||||
}
|
||||
if (end.length == 0) {
|
||||
return start;
|
||||
}
|
||||
let slashes = 0;
|
||||
if (start.endsWith('/')) {
|
||||
slashes++;
|
||||
}
|
||||
if (end.startsWith('/')) {
|
||||
slashes++;
|
||||
}
|
||||
if (slashes == 2) {
|
||||
return start + end.substring(1);
|
||||
}
|
||||
if (slashes == 1) {
|
||||
return start + end;
|
||||
}
|
||||
return start + '/' + end;
|
||||
}
|
||||
|
||||
/**
|
||||
* If url has a trailing slash, remove it, otherwise return url as is.
|
||||
*/
|
||||
public static stripTrailingSlash(url: string): string { return url.replace(/\/$/, ''); }
|
||||
}
|
||||
|
||||
function _stripBaseHref(baseHref: string, url: string): string {
|
||||
return baseHref && url.startsWith(baseHref) ? url.substring(baseHref.length) : url;
|
||||
}
|
||||
|
||||
function _stripIndexHtml(url: string): string {
|
||||
return url.replace(/\/index.html$/, '');
|
||||
}
|
64
packages/common/src/location/location_strategy.ts
Normal file
64
packages/common/src/location/location_strategy.ts
Normal file
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {InjectionToken} from '@angular/core';
|
||||
import {LocationChangeListener} from './platform_location';
|
||||
|
||||
/**
|
||||
* `LocationStrategy` is responsible for representing and reading route state
|
||||
* from the browser's URL. Angular provides two strategies:
|
||||
* {@link HashLocationStrategy} and {@link PathLocationStrategy}.
|
||||
*
|
||||
* This is used under the hood of the {@link Location} service.
|
||||
*
|
||||
* Applications should use the {@link Router} or {@link Location} services to
|
||||
* interact with application route state.
|
||||
*
|
||||
* For instance, {@link HashLocationStrategy} produces URLs like
|
||||
* `http://example.com#/foo`, and {@link PathLocationStrategy} produces
|
||||
* `http://example.com/foo` as an equivalent URL.
|
||||
*
|
||||
* See these two classes for more.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export abstract class LocationStrategy {
|
||||
abstract path(includeHash?: boolean): string;
|
||||
abstract prepareExternalUrl(internal: string): string;
|
||||
abstract pushState(state: any, title: string, url: string, queryParams: string): void;
|
||||
abstract replaceState(state: any, title: string, url: string, queryParams: string): void;
|
||||
abstract forward(): void;
|
||||
abstract back(): void;
|
||||
abstract onPopState(fn: LocationChangeListener): void;
|
||||
abstract getBaseHref(): string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The `APP_BASE_HREF` token represents the base href to be used with the
|
||||
* {@link PathLocationStrategy}.
|
||||
*
|
||||
* If you're using {@link PathLocationStrategy}, you must provide a provider to a string
|
||||
* representing the URL prefix that should be preserved when generating and recognizing
|
||||
* URLs.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```typescript
|
||||
* import {Component, NgModule} from '@angular/core';
|
||||
* import {APP_BASE_HREF} from '@angular/common';
|
||||
*
|
||||
* @NgModule({
|
||||
* providers: [{provide: APP_BASE_HREF, useValue: '/my/app'}]
|
||||
* })
|
||||
* class AppModule {}
|
||||
* ```
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export const APP_BASE_HREF = new InjectionToken<string>('appBaseHref');
|
96
packages/common/src/location/path_location_strategy.ts
Normal file
96
packages/common/src/location/path_location_strategy.ts
Normal file
@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Inject, Injectable, Optional} from '@angular/core';
|
||||
|
||||
|
||||
import {Location} from './location';
|
||||
import {APP_BASE_HREF, LocationStrategy} from './location_strategy';
|
||||
import {LocationChangeListener, PlatformLocation} from './platform_location';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @whatItDoes Use URL for storing application location data.
|
||||
* @description
|
||||
* `PathLocationStrategy` is a {@link LocationStrategy} used to configure the
|
||||
* {@link Location} service to represent its state in the
|
||||
* [path](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax) of the
|
||||
* browser's URL.
|
||||
*
|
||||
* If you're using `PathLocationStrategy`, you must provide a {@link APP_BASE_HREF}
|
||||
* or add a base element to the document. This URL prefix that will be preserved
|
||||
* when generating and recognizing URLs.
|
||||
*
|
||||
* For instance, if you provide an `APP_BASE_HREF` of `'/my/app'` and call
|
||||
* `location.go('/foo')`, the browser's URL will become
|
||||
* `example.com/my/app/foo`.
|
||||
*
|
||||
* Similarly, if you add `<base href='/my/app'/>` to the document and call
|
||||
* `location.go('/foo')`, the browser's URL will become
|
||||
* `example.com/my/app/foo`.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* {@example common/location/ts/path_location_component.ts region='LocationComponent'}
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Injectable()
|
||||
export class PathLocationStrategy extends LocationStrategy {
|
||||
private _baseHref: string;
|
||||
|
||||
constructor(
|
||||
private _platformLocation: PlatformLocation,
|
||||
@Optional() @Inject(APP_BASE_HREF) href?: string) {
|
||||
super();
|
||||
|
||||
if (href == null) {
|
||||
href = this._platformLocation.getBaseHrefFromDOM();
|
||||
}
|
||||
|
||||
if (href == null) {
|
||||
throw new Error(
|
||||
`No base href set. Please provide a value for the APP_BASE_HREF token or add a base element to the document.`);
|
||||
}
|
||||
|
||||
this._baseHref = href;
|
||||
}
|
||||
|
||||
onPopState(fn: LocationChangeListener): void {
|
||||
this._platformLocation.onPopState(fn);
|
||||
this._platformLocation.onHashChange(fn);
|
||||
}
|
||||
|
||||
getBaseHref(): string { return this._baseHref; }
|
||||
|
||||
prepareExternalUrl(internal: string): string {
|
||||
return Location.joinWithSlash(this._baseHref, internal);
|
||||
}
|
||||
|
||||
path(includeHash: boolean = false): string {
|
||||
const pathname = this._platformLocation.pathname +
|
||||
Location.normalizeQueryParams(this._platformLocation.search);
|
||||
const hash = this._platformLocation.hash;
|
||||
return hash && includeHash ? `${pathname}${hash}` : pathname;
|
||||
}
|
||||
|
||||
pushState(state: any, title: string, url: string, queryParams: string) {
|
||||
const externalUrl = this.prepareExternalUrl(url + Location.normalizeQueryParams(queryParams));
|
||||
this._platformLocation.pushState(state, title, externalUrl);
|
||||
}
|
||||
|
||||
replaceState(state: any, title: string, url: string, queryParams: string) {
|
||||
const externalUrl = this.prepareExternalUrl(url + Location.normalizeQueryParams(queryParams));
|
||||
this._platformLocation.replaceState(state, title, externalUrl);
|
||||
}
|
||||
|
||||
forward(): void { this._platformLocation.forward(); }
|
||||
|
||||
back(): void { this._platformLocation.back(); }
|
||||
}
|
70
packages/common/src/location/platform_location.ts
Normal file
70
packages/common/src/location/platform_location.ts
Normal file
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {InjectionToken} from '@angular/core';
|
||||
/**
|
||||
* This class should not be used directly by an application developer. Instead, use
|
||||
* {@link Location}.
|
||||
*
|
||||
* `PlatformLocation` encapsulates all calls to DOM apis, which allows the Router to be platform
|
||||
* agnostic.
|
||||
* This means that we can have different implementation of `PlatformLocation` for the different
|
||||
* platforms
|
||||
* that angular supports. For example, the default `PlatformLocation` is {@link
|
||||
* BrowserPlatformLocation},
|
||||
* however when you run your app in a WebWorker you use {@link WebWorkerPlatformLocation}.
|
||||
*
|
||||
* The `PlatformLocation` class is used directly by all implementations of {@link LocationStrategy}
|
||||
* when
|
||||
* they need to interact with the DOM apis like pushState, popState, etc...
|
||||
*
|
||||
* {@link LocationStrategy} in turn is used by the {@link Location} service which is used directly
|
||||
* by
|
||||
* the {@link Router} in order to navigate between routes. Since all interactions between {@link
|
||||
* Router} /
|
||||
* {@link Location} / {@link LocationStrategy} and DOM apis flow through the `PlatformLocation`
|
||||
* class
|
||||
* they are all platform independent.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export abstract class PlatformLocation {
|
||||
abstract getBaseHrefFromDOM(): string;
|
||||
abstract onPopState(fn: LocationChangeListener): void;
|
||||
abstract onHashChange(fn: LocationChangeListener): void;
|
||||
|
||||
get pathname(): string { return null; }
|
||||
get search(): string { return null; }
|
||||
get hash(): string { return null; }
|
||||
|
||||
abstract replaceState(state: any, title: string, url: string): void;
|
||||
|
||||
abstract pushState(state: any, title: string, url: string): void;
|
||||
|
||||
abstract forward(): void;
|
||||
|
||||
abstract back(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes indicates when a location is initialized
|
||||
* @experimental
|
||||
*/
|
||||
export const LOCATION_INITIALIZED = new InjectionToken<Promise<any>>('Location Initialized');
|
||||
|
||||
/**
|
||||
* A serializable version of the event from onPopState or onHashChange
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export interface LocationChangeEvent { type: string; }
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export interface LocationChangeListener { (e: LocationChangeEvent): any; }
|
143
packages/common/src/pipes/async_pipe.ts
Normal file
143
packages/common/src/pipes/async_pipe.ts
Normal file
@ -0,0 +1,143 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {ChangeDetectorRef, EventEmitter, OnDestroy, Pipe, PipeTransform, WrappedValue, ɵisObservable, ɵisPromise} from '@angular/core';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
|
||||
import {invalidPipeArgumentError} from './invalid_pipe_argument_error';
|
||||
|
||||
interface SubscriptionStrategy {
|
||||
createSubscription(async: any, updateLatestValue: any): any;
|
||||
dispose(subscription: any): void;
|
||||
onDestroy(subscription: any): void;
|
||||
}
|
||||
|
||||
class ObservableStrategy implements SubscriptionStrategy {
|
||||
createSubscription(async: any, updateLatestValue: any): any {
|
||||
return async.subscribe({next: updateLatestValue, error: (e: any) => { throw e; }});
|
||||
}
|
||||
|
||||
dispose(subscription: any): void { subscription.unsubscribe(); }
|
||||
|
||||
onDestroy(subscription: any): void { subscription.unsubscribe(); }
|
||||
}
|
||||
|
||||
class PromiseStrategy implements SubscriptionStrategy {
|
||||
createSubscription(async: Promise<any>, updateLatestValue: (v: any) => any): any {
|
||||
return async.then(updateLatestValue, e => { throw e; });
|
||||
}
|
||||
|
||||
dispose(subscription: any): void {}
|
||||
|
||||
onDestroy(subscription: any): void {}
|
||||
}
|
||||
|
||||
const _promiseStrategy = new PromiseStrategy();
|
||||
const _observableStrategy = new ObservableStrategy();
|
||||
|
||||
/**
|
||||
* @ngModule CommonModule
|
||||
* @whatItDoes Unwraps a value from an asynchronous primitive.
|
||||
* @howToUse `observable_or_promise_expression | async`
|
||||
* @description
|
||||
* The `async` pipe subscribes to an `Observable` or `Promise` and returns the latest value it has
|
||||
* emitted. When a new value is emitted, the `async` pipe marks the component to be checked for
|
||||
* changes. When the component gets destroyed, the `async` pipe unsubscribes automatically to avoid
|
||||
* potential memory leaks.
|
||||
*
|
||||
*
|
||||
* ## Examples
|
||||
*
|
||||
* This example binds a `Promise` to the view. Clicking the `Resolve` button resolves the
|
||||
* promise.
|
||||
*
|
||||
* {@example common/pipes/ts/async_pipe.ts region='AsyncPipePromise'}
|
||||
*
|
||||
* It's also possible to use `async` with Observables. The example below binds the `time` Observable
|
||||
* to the view. The Observable continuously updates the view with the current time.
|
||||
*
|
||||
* {@example common/pipes/ts/async_pipe.ts region='AsyncPipeObservable'}
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Pipe({name: 'async', pure: false})
|
||||
export class AsyncPipe implements OnDestroy, PipeTransform {
|
||||
private _latestValue: Object = null;
|
||||
private _latestReturnedValue: Object = null;
|
||||
|
||||
private _subscription: Object = null;
|
||||
private _obj: Observable<any>|Promise<any>|EventEmitter<any> = null;
|
||||
private _strategy: SubscriptionStrategy = null;
|
||||
|
||||
constructor(private _ref: ChangeDetectorRef) {}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this._subscription) {
|
||||
this._dispose();
|
||||
}
|
||||
}
|
||||
|
||||
transform<T>(obj: Observable<T>): T|null;
|
||||
transform<T>(obj: Promise<T>): T|null;
|
||||
transform<T>(obj: EventEmitter<T>): T|null;
|
||||
transform(obj: Observable<any>|Promise<any>|EventEmitter<any>): any {
|
||||
if (!this._obj) {
|
||||
if (obj) {
|
||||
this._subscribe(obj);
|
||||
}
|
||||
this._latestReturnedValue = this._latestValue;
|
||||
return this._latestValue;
|
||||
}
|
||||
|
||||
if (obj !== this._obj) {
|
||||
this._dispose();
|
||||
return this.transform(obj as any);
|
||||
}
|
||||
|
||||
if (this._latestValue === this._latestReturnedValue) {
|
||||
return this._latestReturnedValue;
|
||||
}
|
||||
|
||||
this._latestReturnedValue = this._latestValue;
|
||||
return WrappedValue.wrap(this._latestValue);
|
||||
}
|
||||
|
||||
private _subscribe(obj: Observable<any>|Promise<any>|EventEmitter<any>): void {
|
||||
this._obj = obj;
|
||||
this._strategy = this._selectStrategy(obj);
|
||||
this._subscription = this._strategy.createSubscription(
|
||||
obj, (value: Object) => this._updateLatestValue(obj, value));
|
||||
}
|
||||
|
||||
private _selectStrategy(obj: Observable<any>|Promise<any>|EventEmitter<any>): any {
|
||||
if (ɵisPromise(obj)) {
|
||||
return _promiseStrategy;
|
||||
}
|
||||
|
||||
if (ɵisObservable(obj)) {
|
||||
return _observableStrategy;
|
||||
}
|
||||
|
||||
throw invalidPipeArgumentError(AsyncPipe, obj);
|
||||
}
|
||||
|
||||
private _dispose(): void {
|
||||
this._strategy.dispose(this._subscription);
|
||||
this._latestValue = null;
|
||||
this._latestReturnedValue = null;
|
||||
this._subscription = null;
|
||||
this._obj = null;
|
||||
}
|
||||
|
||||
private _updateLatestValue(async: any, value: Object): void {
|
||||
if (async === this._obj) {
|
||||
this._latestValue = value;
|
||||
this._ref.markForCheck();
|
||||
}
|
||||
}
|
||||
}
|
72
packages/common/src/pipes/case_conversion_pipes.ts
Normal file
72
packages/common/src/pipes/case_conversion_pipes.ts
Normal file
@ -0,0 +1,72 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Pipe, PipeTransform} from '@angular/core';
|
||||
import {invalidPipeArgumentError} from './invalid_pipe_argument_error';
|
||||
|
||||
/**
|
||||
* Transforms text to lowercase.
|
||||
*
|
||||
* {@example common/pipes/ts/lowerupper_pipe.ts region='LowerUpperPipe' }
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Pipe({name: 'lowercase'})
|
||||
export class LowerCasePipe implements PipeTransform {
|
||||
transform(value: string): string {
|
||||
if (!value) return value;
|
||||
if (typeof value !== 'string') {
|
||||
throw invalidPipeArgumentError(LowerCasePipe, value);
|
||||
}
|
||||
return value.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper method to transform a single word to titlecase.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
function titleCaseWord(word: string) {
|
||||
if (!word) return word;
|
||||
return word[0].toUpperCase() + word.substr(1).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms text to titlecase.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Pipe({name: 'titlecase'})
|
||||
export class TitleCasePipe implements PipeTransform {
|
||||
transform(value: string): string {
|
||||
if (!value) return value;
|
||||
if (typeof value !== 'string') {
|
||||
throw invalidPipeArgumentError(TitleCasePipe, value);
|
||||
}
|
||||
|
||||
return value.split(/\b/g).map(word => titleCaseWord(word)).join('');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms text to uppercase.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Pipe({name: 'uppercase'})
|
||||
export class UpperCasePipe implements PipeTransform {
|
||||
transform(value: string): string {
|
||||
if (!value) return value;
|
||||
if (typeof value !== 'string') {
|
||||
throw invalidPipeArgumentError(UpperCasePipe, value);
|
||||
}
|
||||
return value.toUpperCase();
|
||||
}
|
||||
}
|
175
packages/common/src/pipes/date_pipe.ts
Normal file
175
packages/common/src/pipes/date_pipe.ts
Normal file
@ -0,0 +1,175 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Inject, LOCALE_ID, Pipe, PipeTransform} from '@angular/core';
|
||||
import {DateFormatter} from './intl';
|
||||
import {invalidPipeArgumentError} from './invalid_pipe_argument_error';
|
||||
import {isNumeric} from './number_pipe';
|
||||
|
||||
const ISO8601_DATE_REGEX =
|
||||
/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;
|
||||
// 1 2 3 4 5 6 7 8 9 10 11
|
||||
|
||||
/**
|
||||
* @ngModule CommonModule
|
||||
* @whatItDoes Formats a date according to locale rules.
|
||||
* @howToUse `date_expression | date[:format]`
|
||||
* @description
|
||||
*
|
||||
* Where:
|
||||
* - `expression` is a date object or a number (milliseconds since UTC epoch) or an ISO string
|
||||
* (https://www.w3.org/TR/NOTE-datetime).
|
||||
* - `format` indicates which date/time components to include. The format can be predefined as
|
||||
* shown below or custom as shown in the table.
|
||||
* - `'medium'`: equivalent to `'yMMMdjms'` (e.g. `Sep 3, 2010, 12:05:08 PM` for `en-US`)
|
||||
* - `'short'`: equivalent to `'yMdjm'` (e.g. `9/3/2010, 12:05 PM` for `en-US`)
|
||||
* - `'fullDate'`: equivalent to `'yMMMMEEEEd'` (e.g. `Friday, September 3, 2010` for `en-US`)
|
||||
* - `'longDate'`: equivalent to `'yMMMMd'` (e.g. `September 3, 2010` for `en-US`)
|
||||
* - `'mediumDate'`: equivalent to `'yMMMd'` (e.g. `Sep 3, 2010` for `en-US`)
|
||||
* - `'shortDate'`: equivalent to `'yMd'` (e.g. `9/3/2010` for `en-US`)
|
||||
* - `'mediumTime'`: equivalent to `'jms'` (e.g. `12:05:08 PM` for `en-US`)
|
||||
* - `'shortTime'`: equivalent to `'jm'` (e.g. `12:05 PM` for `en-US`)
|
||||
*
|
||||
*
|
||||
* | Component | Symbol | Narrow | Short Form | Long Form | Numeric | 2-digit |
|
||||
* |-----------|:------:|--------|--------------|-------------------|-----------|-----------|
|
||||
* | era | G | G (A) | GGG (AD) | GGGG (Anno Domini)| - | - |
|
||||
* | year | y | - | - | - | y (2015) | yy (15) |
|
||||
* | month | M | L (S) | MMM (Sep) | MMMM (September) | M (9) | MM (09) |
|
||||
* | day | d | - | - | - | d (3) | dd (03) |
|
||||
* | weekday | E | E (S) | EEE (Sun) | EEEE (Sunday) | - | - |
|
||||
* | hour | j | - | - | - | j (13) | jj (13) |
|
||||
* | hour12 | h | - | - | - | h (1 PM) | hh (01 PM)|
|
||||
* | hour24 | H | - | - | - | H (13) | HH (13) |
|
||||
* | minute | m | - | - | - | m (5) | mm (05) |
|
||||
* | second | s | - | - | - | s (9) | ss (09) |
|
||||
* | timezone | z | - | - | z (Pacific Standard Time)| - | - |
|
||||
* | timezone | Z | - | Z (GMT-8:00) | - | - | - |
|
||||
* | timezone | a | - | a (PM) | - | - | - |
|
||||
*
|
||||
* In javascript, only the components specified will be respected (not the ordering,
|
||||
* punctuations, ...) and details of the formatting will be dependent on the locale.
|
||||
*
|
||||
* Timezone of the formatted text will be the local system timezone of the end-user's machine.
|
||||
*
|
||||
* When the expression is a ISO string without time (e.g. 2016-09-19) the time zone offset is not
|
||||
* applied and the formatted text will have the same day, month and year of the expression.
|
||||
*
|
||||
* WARNINGS:
|
||||
* - this pipe is marked as pure hence it will not be re-evaluated when the input is mutated.
|
||||
* Instead users should treat the date as an immutable object and change the reference when the
|
||||
* pipe needs to re-run (this is to avoid reformatting the date on every change detection run
|
||||
* which would be an expensive operation).
|
||||
* - this pipe uses the Internationalization API. Therefore it is only reliable in Chrome and Opera
|
||||
* browsers.
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* Assuming `dateObj` is (year: 2015, month: 6, day: 15, hour: 21, minute: 43, second: 11)
|
||||
* in the _local_ time and locale is 'en-US':
|
||||
*
|
||||
* ```
|
||||
* {{ dateObj | date }} // output is 'Jun 15, 2015'
|
||||
* {{ dateObj | date:'medium' }} // output is 'Jun 15, 2015, 9:43:11 PM'
|
||||
* {{ dateObj | date:'shortTime' }} // output is '9:43 PM'
|
||||
* {{ dateObj | date:'mmss' }} // output is '43:11'
|
||||
* ```
|
||||
*
|
||||
* {@example common/pipes/ts/date_pipe.ts region='DatePipe'}
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Pipe({name: 'date', pure: true})
|
||||
export class DatePipe implements PipeTransform {
|
||||
/** @internal */
|
||||
static _ALIASES: {[key: string]: string} = {
|
||||
'medium': 'yMMMdjms',
|
||||
'short': 'yMdjm',
|
||||
'fullDate': 'yMMMMEEEEd',
|
||||
'longDate': 'yMMMMd',
|
||||
'mediumDate': 'yMMMd',
|
||||
'shortDate': 'yMd',
|
||||
'mediumTime': 'jms',
|
||||
'shortTime': 'jm'
|
||||
};
|
||||
|
||||
constructor(@Inject(LOCALE_ID) private _locale: string) {}
|
||||
|
||||
transform(value: any, pattern: string = 'mediumDate'): string {
|
||||
let date: Date;
|
||||
|
||||
if (isBlank(value) || value !== value) return null;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
value = value.trim();
|
||||
}
|
||||
|
||||
if (isDate(value)) {
|
||||
date = value;
|
||||
} else if (isNumeric(value)) {
|
||||
date = new Date(parseFloat(value));
|
||||
} else if (typeof value === 'string' && /^(\d{4}-\d{1,2}-\d{1,2})$/.test(value)) {
|
||||
/**
|
||||
* For ISO Strings without time the day, month and year must be extracted from the ISO String
|
||||
* before Date creation to avoid time offset and errors in the new Date.
|
||||
* If we only replace '-' with ',' in the ISO String ("2015,01,01"), and try to create a new
|
||||
* date, some browsers (e.g. IE 9) will throw an invalid Date error
|
||||
* If we leave the '-' ("2015-01-01") and try to create a new Date("2015-01-01") the timeoffset
|
||||
* is applied
|
||||
* Note: ISO months are 0 for January, 1 for February, ...
|
||||
*/
|
||||
const [y, m, d] = value.split('-').map((val: string) => parseInt(val, 10));
|
||||
date = new Date(y, m - 1, d);
|
||||
} else {
|
||||
date = new Date(value);
|
||||
}
|
||||
|
||||
if (!isDate(date)) {
|
||||
let match: RegExpMatchArray;
|
||||
if ((typeof value === 'string') && (match = value.match(ISO8601_DATE_REGEX))) {
|
||||
date = isoStringToDate(match);
|
||||
} else {
|
||||
throw invalidPipeArgumentError(DatePipe, value);
|
||||
}
|
||||
}
|
||||
|
||||
return DateFormatter.format(date, this._locale, DatePipe._ALIASES[pattern] || pattern);
|
||||
}
|
||||
}
|
||||
|
||||
function isBlank(obj: any): boolean {
|
||||
return obj == null || obj === '';
|
||||
}
|
||||
|
||||
function isDate(obj: any): obj is Date {
|
||||
return obj instanceof Date && !isNaN(obj.valueOf());
|
||||
}
|
||||
|
||||
function isoStringToDate(match: RegExpMatchArray): Date {
|
||||
const date = new Date(0);
|
||||
let tzHour = 0;
|
||||
let tzMin = 0;
|
||||
const dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear;
|
||||
const timeSetter = match[8] ? date.setUTCHours : date.setHours;
|
||||
|
||||
if (match[9]) {
|
||||
tzHour = toInt(match[9] + match[10]);
|
||||
tzMin = toInt(match[9] + match[11]);
|
||||
}
|
||||
dateSetter.call(date, toInt(match[1]), toInt(match[2]) - 1, toInt(match[3]));
|
||||
const h = toInt(match[4] || '0') - tzHour;
|
||||
const m = toInt(match[5] || '0') - tzMin;
|
||||
const s = toInt(match[6] || '0');
|
||||
const ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000);
|
||||
timeSetter.call(date, h, m, s, ms);
|
||||
return date;
|
||||
}
|
||||
|
||||
function toInt(str: string): number {
|
||||
return parseInt(str, 10);
|
||||
}
|
47
packages/common/src/pipes/i18n_plural_pipe.ts
Normal file
47
packages/common/src/pipes/i18n_plural_pipe.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Pipe, PipeTransform} from '@angular/core';
|
||||
import {NgLocalization, getPluralCategory} from '../localization';
|
||||
import {invalidPipeArgumentError} from './invalid_pipe_argument_error';
|
||||
|
||||
const _INTERPOLATION_REGEXP: RegExp = /#/g;
|
||||
|
||||
/**
|
||||
* @ngModule CommonModule
|
||||
* @whatItDoes Maps a value to a string that pluralizes the value according to locale rules.
|
||||
* @howToUse `expression | i18nPlural:mapping`
|
||||
* @description
|
||||
*
|
||||
* Where:
|
||||
* - `expression` is a number.
|
||||
* - `mapping` is an object that mimics the ICU format, see
|
||||
* http://userguide.icu-project.org/formatparse/messages
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* {@example common/pipes/ts/i18n_pipe.ts region='I18nPluralPipeComponent'}
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
@Pipe({name: 'i18nPlural', pure: true})
|
||||
export class I18nPluralPipe implements PipeTransform {
|
||||
constructor(private _localization: NgLocalization) {}
|
||||
|
||||
transform(value: number, pluralMap: {[count: string]: string}): string {
|
||||
if (value == null) return '';
|
||||
|
||||
if (typeof pluralMap !== 'object' || pluralMap === null) {
|
||||
throw invalidPipeArgumentError(I18nPluralPipe, pluralMap);
|
||||
}
|
||||
|
||||
const key = getPluralCategory(value, Object.keys(pluralMap), this._localization);
|
||||
|
||||
return pluralMap[key].replace(_INTERPOLATION_REGEXP, value.toString());
|
||||
}
|
||||
}
|
48
packages/common/src/pipes/i18n_select_pipe.ts
Normal file
48
packages/common/src/pipes/i18n_select_pipe.ts
Normal file
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Pipe, PipeTransform} from '@angular/core';
|
||||
import {invalidPipeArgumentError} from './invalid_pipe_argument_error';
|
||||
|
||||
/**
|
||||
* @ngModule CommonModule
|
||||
* @whatItDoes Generic selector that displays the string that matches the current value.
|
||||
* @howToUse `expression | i18nSelect:mapping`
|
||||
* @description
|
||||
*
|
||||
* Where `mapping` is an object that indicates the text that should be displayed
|
||||
* for different values of the provided `expression`.
|
||||
* If none of the keys of the mapping match the value of the `expression`, then the content
|
||||
* of the `other` key is returned when present, otherwise an empty string is returned.
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* {@example common/pipes/ts/i18n_pipe.ts region='I18nSelectPipeComponent'}
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
@Pipe({name: 'i18nSelect', pure: true})
|
||||
export class I18nSelectPipe implements PipeTransform {
|
||||
transform(value: string, mapping: {[key: string]: string}): string {
|
||||
if (value == null) return '';
|
||||
|
||||
if (typeof mapping !== 'object' || typeof value !== 'string') {
|
||||
throw invalidPipeArgumentError(I18nSelectPipe, mapping);
|
||||
}
|
||||
|
||||
if (mapping.hasOwnProperty(value)) {
|
||||
return mapping[value];
|
||||
}
|
||||
|
||||
if (mapping.hasOwnProperty('other')) {
|
||||
return mapping['other'];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
55
packages/common/src/pipes/index.ts
Normal file
55
packages/common/src/pipes/index.ts
Normal file
@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @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
|
||||
* @description
|
||||
* This module provides a set of common Pipes.
|
||||
*/
|
||||
import {AsyncPipe} from './async_pipe';
|
||||
import {LowerCasePipe, TitleCasePipe, UpperCasePipe} from './case_conversion_pipes';
|
||||
import {DatePipe} from './date_pipe';
|
||||
import {I18nPluralPipe} from './i18n_plural_pipe';
|
||||
import {I18nSelectPipe} from './i18n_select_pipe';
|
||||
import {JsonPipe} from './json_pipe';
|
||||
import {CurrencyPipe, DecimalPipe, PercentPipe} from './number_pipe';
|
||||
import {SlicePipe} from './slice_pipe';
|
||||
|
||||
export {
|
||||
AsyncPipe,
|
||||
CurrencyPipe,
|
||||
DatePipe,
|
||||
DecimalPipe,
|
||||
I18nPluralPipe,
|
||||
I18nSelectPipe,
|
||||
JsonPipe,
|
||||
LowerCasePipe,
|
||||
PercentPipe,
|
||||
SlicePipe,
|
||||
TitleCasePipe,
|
||||
UpperCasePipe
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A collection of Angular pipes that are likely to be used in each and every application.
|
||||
*/
|
||||
export const COMMON_PIPES = [
|
||||
AsyncPipe,
|
||||
UpperCasePipe,
|
||||
LowerCasePipe,
|
||||
JsonPipe,
|
||||
SlicePipe,
|
||||
DecimalPipe,
|
||||
PercentPipe,
|
||||
TitleCasePipe,
|
||||
CurrencyPipe,
|
||||
DatePipe,
|
||||
I18nPluralPipe,
|
||||
I18nSelectPipe,
|
||||
];
|
226
packages/common/src/pipes/intl.ts
Normal file
226
packages/common/src/pipes/intl.ts
Normal file
@ -0,0 +1,226 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
export enum NumberFormatStyle {
|
||||
Decimal,
|
||||
Percent,
|
||||
Currency,
|
||||
}
|
||||
|
||||
export class NumberFormatter {
|
||||
static format(
|
||||
num: number, locale: string, style: NumberFormatStyle,
|
||||
{minimumIntegerDigits, minimumFractionDigits, maximumFractionDigits, currency,
|
||||
currencyAsSymbol = false}: {
|
||||
minimumIntegerDigits?: number,
|
||||
minimumFractionDigits?: number,
|
||||
maximumFractionDigits?: number,
|
||||
currency?: string,
|
||||
currencyAsSymbol?: boolean
|
||||
} = {}): string {
|
||||
const options: Intl.NumberFormatOptions = {
|
||||
minimumIntegerDigits,
|
||||
minimumFractionDigits,
|
||||
maximumFractionDigits,
|
||||
style: NumberFormatStyle[style].toLowerCase()
|
||||
};
|
||||
|
||||
if (style == NumberFormatStyle.Currency) {
|
||||
options.currency = currency;
|
||||
options.currencyDisplay = currencyAsSymbol ? 'symbol' : 'code';
|
||||
}
|
||||
return new Intl.NumberFormat(locale, options).format(num);
|
||||
}
|
||||
}
|
||||
|
||||
type DateFormatterFn = (date: Date, locale: string) => string;
|
||||
|
||||
const DATE_FORMATS_SPLIT =
|
||||
/((?:[^yMLdHhmsazZEwGjJ']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|J+|j+|m+|s+|a|z|Z|G+|w+))(.*)/;
|
||||
|
||||
const PATTERN_ALIASES: {[format: string]: DateFormatterFn} = {
|
||||
// Keys are quoted so they do not get renamed during closure compilation.
|
||||
'yMMMdjms': datePartGetterFactory(combine([
|
||||
digitCondition('year', 1),
|
||||
nameCondition('month', 3),
|
||||
digitCondition('day', 1),
|
||||
digitCondition('hour', 1),
|
||||
digitCondition('minute', 1),
|
||||
digitCondition('second', 1),
|
||||
])),
|
||||
'yMdjm': datePartGetterFactory(combine([
|
||||
digitCondition('year', 1), digitCondition('month', 1), digitCondition('day', 1),
|
||||
digitCondition('hour', 1), digitCondition('minute', 1)
|
||||
])),
|
||||
'yMMMMEEEEd': datePartGetterFactory(combine([
|
||||
digitCondition('year', 1), nameCondition('month', 4), nameCondition('weekday', 4),
|
||||
digitCondition('day', 1)
|
||||
])),
|
||||
'yMMMMd': datePartGetterFactory(
|
||||
combine([digitCondition('year', 1), nameCondition('month', 4), digitCondition('day', 1)])),
|
||||
'yMMMd': datePartGetterFactory(
|
||||
combine([digitCondition('year', 1), nameCondition('month', 3), digitCondition('day', 1)])),
|
||||
'yMd': datePartGetterFactory(
|
||||
combine([digitCondition('year', 1), digitCondition('month', 1), digitCondition('day', 1)])),
|
||||
'jms': datePartGetterFactory(combine(
|
||||
[digitCondition('hour', 1), digitCondition('second', 1), digitCondition('minute', 1)])),
|
||||
'jm': datePartGetterFactory(combine([digitCondition('hour', 1), digitCondition('minute', 1)]))
|
||||
};
|
||||
|
||||
const DATE_FORMATS: {[format: string]: DateFormatterFn} = {
|
||||
// Keys are quoted so they do not get renamed.
|
||||
'yyyy': datePartGetterFactory(digitCondition('year', 4)),
|
||||
'yy': datePartGetterFactory(digitCondition('year', 2)),
|
||||
'y': datePartGetterFactory(digitCondition('year', 1)),
|
||||
'MMMM': datePartGetterFactory(nameCondition('month', 4)),
|
||||
'MMM': datePartGetterFactory(nameCondition('month', 3)),
|
||||
'MM': datePartGetterFactory(digitCondition('month', 2)),
|
||||
'M': datePartGetterFactory(digitCondition('month', 1)),
|
||||
'LLLL': datePartGetterFactory(nameCondition('month', 4)),
|
||||
'L': datePartGetterFactory(nameCondition('month', 1)),
|
||||
'dd': datePartGetterFactory(digitCondition('day', 2)),
|
||||
'd': datePartGetterFactory(digitCondition('day', 1)),
|
||||
'HH': digitModifier(
|
||||
hourExtractor(datePartGetterFactory(hour12Modify(digitCondition('hour', 2), false)))),
|
||||
'H': hourExtractor(datePartGetterFactory(hour12Modify(digitCondition('hour', 1), false))),
|
||||
'hh': digitModifier(
|
||||
hourExtractor(datePartGetterFactory(hour12Modify(digitCondition('hour', 2), true)))),
|
||||
'h': hourExtractor(datePartGetterFactory(hour12Modify(digitCondition('hour', 1), true))),
|
||||
'jj': datePartGetterFactory(digitCondition('hour', 2)),
|
||||
'j': datePartGetterFactory(digitCondition('hour', 1)),
|
||||
'mm': digitModifier(datePartGetterFactory(digitCondition('minute', 2))),
|
||||
'm': datePartGetterFactory(digitCondition('minute', 1)),
|
||||
'ss': digitModifier(datePartGetterFactory(digitCondition('second', 2))),
|
||||
's': datePartGetterFactory(digitCondition('second', 1)),
|
||||
// while ISO 8601 requires fractions to be prefixed with `.` or `,`
|
||||
// we can be just safely rely on using `sss` since we currently don't support single or two digit
|
||||
// fractions
|
||||
'sss': datePartGetterFactory(digitCondition('second', 3)),
|
||||
'EEEE': datePartGetterFactory(nameCondition('weekday', 4)),
|
||||
'EEE': datePartGetterFactory(nameCondition('weekday', 3)),
|
||||
'EE': datePartGetterFactory(nameCondition('weekday', 2)),
|
||||
'E': datePartGetterFactory(nameCondition('weekday', 1)),
|
||||
'a': hourClockExtractor(datePartGetterFactory(hour12Modify(digitCondition('hour', 1), true))),
|
||||
'Z': timeZoneGetter('short'),
|
||||
'z': timeZoneGetter('long'),
|
||||
'ww': datePartGetterFactory({}), // Week of year, padded (00-53). Week 01 is the week with the
|
||||
// first Thursday of the year. not support ?
|
||||
'w':
|
||||
datePartGetterFactory({}), // Week of year (0-53). Week 1 is the week with the first Thursday
|
||||
// of the year not support ?
|
||||
'G': datePartGetterFactory(nameCondition('era', 1)),
|
||||
'GG': datePartGetterFactory(nameCondition('era', 2)),
|
||||
'GGG': datePartGetterFactory(nameCondition('era', 3)),
|
||||
'GGGG': datePartGetterFactory(nameCondition('era', 4))
|
||||
};
|
||||
|
||||
|
||||
function digitModifier(inner: DateFormatterFn): DateFormatterFn {
|
||||
return function(date: Date, locale: string): string {
|
||||
const result = inner(date, locale);
|
||||
return result.length == 1 ? '0' + result : result;
|
||||
};
|
||||
}
|
||||
|
||||
function hourClockExtractor(inner: DateFormatterFn): DateFormatterFn {
|
||||
return function(date: Date, locale: string): string { return inner(date, locale).split(' ')[1]; };
|
||||
}
|
||||
|
||||
function hourExtractor(inner: DateFormatterFn): DateFormatterFn {
|
||||
return function(date: Date, locale: string): string { return inner(date, locale).split(' ')[0]; };
|
||||
}
|
||||
|
||||
function intlDateFormat(date: Date, locale: string, options: Intl.DateTimeFormatOptions): string {
|
||||
return new Intl.DateTimeFormat(locale, options).format(date).replace(/[\u200e\u200f]/g, '');
|
||||
}
|
||||
|
||||
function timeZoneGetter(timezone: string): DateFormatterFn {
|
||||
// To workaround `Intl` API restriction for single timezone let format with 24 hours
|
||||
const options = {hour: '2-digit', hour12: false, timeZoneName: timezone};
|
||||
return function(date: Date, locale: string): string {
|
||||
const result = intlDateFormat(date, locale, options);
|
||||
// Then extract first 3 letters that related to hours
|
||||
return result ? result.substring(3) : '';
|
||||
};
|
||||
}
|
||||
|
||||
function hour12Modify(
|
||||
options: Intl.DateTimeFormatOptions, value: boolean): Intl.DateTimeFormatOptions {
|
||||
options.hour12 = value;
|
||||
return options;
|
||||
}
|
||||
|
||||
function digitCondition(prop: string, len: number): Intl.DateTimeFormatOptions {
|
||||
const result: {[k: string]: string} = {};
|
||||
result[prop] = len === 2 ? '2-digit' : 'numeric';
|
||||
return result;
|
||||
}
|
||||
|
||||
function nameCondition(prop: string, len: number): Intl.DateTimeFormatOptions {
|
||||
const result: {[k: string]: string} = {};
|
||||
if (len < 4) {
|
||||
result[prop] = len > 1 ? 'short' : 'narrow';
|
||||
} else {
|
||||
result[prop] = 'long';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function combine(options: Intl.DateTimeFormatOptions[]): Intl.DateTimeFormatOptions {
|
||||
return (<any>Object).assign({}, ...options);
|
||||
}
|
||||
|
||||
function datePartGetterFactory(ret: Intl.DateTimeFormatOptions): DateFormatterFn {
|
||||
return (date: Date, locale: string): string => intlDateFormat(date, locale, ret);
|
||||
}
|
||||
|
||||
const DATE_FORMATTER_CACHE = new Map<string, string[]>();
|
||||
|
||||
function dateFormatter(format: string, date: Date, locale: string): string {
|
||||
const fn = PATTERN_ALIASES[format];
|
||||
|
||||
if (fn) return fn(date, locale);
|
||||
|
||||
const cacheKey = format;
|
||||
let parts = DATE_FORMATTER_CACHE.get(cacheKey);
|
||||
|
||||
if (!parts) {
|
||||
parts = [];
|
||||
let match: RegExpExecArray;
|
||||
DATE_FORMATS_SPLIT.exec(format);
|
||||
|
||||
while (format) {
|
||||
match = DATE_FORMATS_SPLIT.exec(format);
|
||||
if (match) {
|
||||
parts = parts.concat(match.slice(1));
|
||||
format = parts.pop();
|
||||
} else {
|
||||
parts.push(format);
|
||||
format = null;
|
||||
}
|
||||
}
|
||||
|
||||
DATE_FORMATTER_CACHE.set(cacheKey, parts);
|
||||
}
|
||||
|
||||
return parts.reduce((text, part) => {
|
||||
const fn = DATE_FORMATS[part];
|
||||
return text + (fn ? fn(date, locale) : partToTime(part));
|
||||
}, '');
|
||||
}
|
||||
|
||||
function partToTime(part: string): string {
|
||||
return part === '\'\'' ? '\'' : part.replace(/(^'|'$)/g, '').replace(/''/g, '\'');
|
||||
}
|
||||
|
||||
export class DateFormatter {
|
||||
static format(date: Date, locale: string, pattern: string): string {
|
||||
return dateFormatter(pattern, date, locale);
|
||||
}
|
||||
}
|
13
packages/common/src/pipes/invalid_pipe_argument_error.ts
Normal file
13
packages/common/src/pipes/invalid_pipe_argument_error.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Type, ɵstringify as stringify} from '@angular/core';
|
||||
|
||||
export function invalidPipeArgumentError(type: Type<any>, value: Object) {
|
||||
return Error(`InvalidPipeArgument: '${value}' for pipe '${stringify(type)}'`);
|
||||
}
|
27
packages/common/src/pipes/json_pipe.ts
Normal file
27
packages/common/src/pipes/json_pipe.ts
Normal file
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Pipe, PipeTransform} from '@angular/core';
|
||||
|
||||
/**
|
||||
* @ngModule CommonModule
|
||||
* @whatItDoes Converts value into JSON string.
|
||||
* @howToUse `expression | json`
|
||||
* @description
|
||||
*
|
||||
* Converts value into string using `JSON.stringify`. Useful for debugging.
|
||||
*
|
||||
* ### Example
|
||||
* {@example common/pipes/ts/json_pipe.ts region='JsonPipe'}
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Pipe({name: 'json', pure: false})
|
||||
export class JsonPipe implements PipeTransform {
|
||||
transform(value: any): string { return JSON.stringify(value, null, 2); }
|
||||
}
|
173
packages/common/src/pipes/number_pipe.ts
Normal file
173
packages/common/src/pipes/number_pipe.ts
Normal file
@ -0,0 +1,173 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Inject, LOCALE_ID, Pipe, PipeTransform, Type} from '@angular/core';
|
||||
import {NumberFormatStyle, NumberFormatter} from './intl';
|
||||
import {invalidPipeArgumentError} from './invalid_pipe_argument_error';
|
||||
|
||||
const _NUMBER_FORMAT_REGEXP = /^(\d+)?\.((\d+)(-(\d+))?)?$/;
|
||||
|
||||
function formatNumber(
|
||||
pipe: Type<any>, locale: string, value: number | string, style: NumberFormatStyle,
|
||||
digits: string, currency: string = null, currencyAsSymbol: boolean = false): string {
|
||||
if (value == null) return null;
|
||||
|
||||
// Convert strings to numbers
|
||||
value = typeof value === 'string' && isNumeric(value) ? +value : value;
|
||||
if (typeof value !== 'number') {
|
||||
throw invalidPipeArgumentError(pipe, value);
|
||||
}
|
||||
|
||||
let minInt: number;
|
||||
let minFraction: number;
|
||||
let maxFraction: number;
|
||||
if (style !== NumberFormatStyle.Currency) {
|
||||
// rely on Intl default for currency
|
||||
minInt = 1;
|
||||
minFraction = 0;
|
||||
maxFraction = 3;
|
||||
}
|
||||
|
||||
if (digits) {
|
||||
const parts = digits.match(_NUMBER_FORMAT_REGEXP);
|
||||
if (parts === null) {
|
||||
throw new Error(`${digits} is not a valid digit info for number pipes`);
|
||||
}
|
||||
if (parts[1] != null) { // min integer digits
|
||||
minInt = parseIntAutoRadix(parts[1]);
|
||||
}
|
||||
if (parts[3] != null) { // min fraction digits
|
||||
minFraction = parseIntAutoRadix(parts[3]);
|
||||
}
|
||||
if (parts[5] != null) { // max fraction digits
|
||||
maxFraction = parseIntAutoRadix(parts[5]);
|
||||
}
|
||||
}
|
||||
|
||||
return NumberFormatter.format(value as number, locale, style, {
|
||||
minimumIntegerDigits: minInt,
|
||||
minimumFractionDigits: minFraction,
|
||||
maximumFractionDigits: maxFraction,
|
||||
currency: currency,
|
||||
currencyAsSymbol: currencyAsSymbol,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngModule CommonModule
|
||||
* @whatItDoes Formats a number according to locale rules.
|
||||
* @howToUse `number_expression | number[:digitInfo]`
|
||||
*
|
||||
* Formats a number as text. Group sizing and separator and other locale-specific
|
||||
* configurations are based on the active locale.
|
||||
*
|
||||
* where `expression` is a number:
|
||||
* - `digitInfo` is a `string` which has a following format: <br>
|
||||
* <code>{minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}</code>
|
||||
* - `minIntegerDigits` is the minimum number of integer digits to use. Defaults to `1`.
|
||||
* - `minFractionDigits` is the minimum number of digits after fraction. Defaults to `0`.
|
||||
* - `maxFractionDigits` is the maximum number of digits after fraction. Defaults to `3`.
|
||||
*
|
||||
* For more information on the acceptable range for each of these numbers and other
|
||||
* details see your native internationalization library.
|
||||
*
|
||||
* WARNING: this pipe uses the Internationalization API which is not yet available in all browsers
|
||||
* and may require a polyfill. See {@linkDocs guide/browser-support} for details.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* {@example common/pipes/ts/number_pipe.ts region='NumberPipe'}
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Pipe({name: 'number'})
|
||||
export class DecimalPipe implements PipeTransform {
|
||||
constructor(@Inject(LOCALE_ID) private _locale: string) {}
|
||||
|
||||
transform(value: any, digits: string = null): string {
|
||||
return formatNumber(DecimalPipe, this._locale, value, NumberFormatStyle.Decimal, digits);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngModule CommonModule
|
||||
* @whatItDoes Formats a number as a percentage according to locale rules.
|
||||
* @howToUse `number_expression | percent[:digitInfo]`
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* Formats a number as percentage.
|
||||
*
|
||||
* - `digitInfo` See {@link DecimalPipe} for detailed description.
|
||||
*
|
||||
* WARNING: this pipe uses the Internationalization API which is not yet available in all browsers
|
||||
* and may require a polyfill. See {@linkDocs guide/browser-support} for details.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* {@example common/pipes/ts/number_pipe.ts region='PercentPipe'}
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Pipe({name: 'percent'})
|
||||
export class PercentPipe implements PipeTransform {
|
||||
constructor(@Inject(LOCALE_ID) private _locale: string) {}
|
||||
|
||||
transform(value: any, digits: string = null): string {
|
||||
return formatNumber(PercentPipe, this._locale, value, NumberFormatStyle.Percent, digits);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngModule CommonModule
|
||||
* @whatItDoes Formats a number as currency using locale rules.
|
||||
* @howToUse `number_expression | currency[:currencyCode[:symbolDisplay[:digitInfo]]]`
|
||||
* @description
|
||||
*
|
||||
* Use `currency` to format a number as currency.
|
||||
*
|
||||
* - `currencyCode` is the [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) currency code, such
|
||||
* as `USD` for the US dollar and `EUR` for the euro.
|
||||
* - `symbolDisplay` is a boolean indicating whether to use the currency symbol or code.
|
||||
* - `true`: use symbol (e.g. `$`).
|
||||
* - `false`(default): use code (e.g. `USD`).
|
||||
* - `digitInfo` See {@link DecimalPipe} for detailed description.
|
||||
*
|
||||
* WARNING: this pipe uses the Internationalization API which is not yet available in all browsers
|
||||
* and may require a polyfill. See {@linkDocs guide/browser-support} for details.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* {@example common/pipes/ts/number_pipe.ts region='CurrencyPipe'}
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Pipe({name: 'currency'})
|
||||
export class CurrencyPipe implements PipeTransform {
|
||||
constructor(@Inject(LOCALE_ID) private _locale: string) {}
|
||||
|
||||
transform(
|
||||
value: any, currencyCode: string = 'USD', symbolDisplay: boolean = false,
|
||||
digits: string = null): string {
|
||||
return formatNumber(
|
||||
CurrencyPipe, this._locale, value, NumberFormatStyle.Currency, digits, currencyCode,
|
||||
symbolDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
function parseIntAutoRadix(text: string): number {
|
||||
const result: number = parseInt(text);
|
||||
if (isNaN(result)) {
|
||||
throw new Error('Invalid integer literal when parsing ' + text);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function isNumeric(value: any): boolean {
|
||||
return !isNaN(value - parseFloat(value));
|
||||
}
|
70
packages/common/src/pipes/slice_pipe.ts
Normal file
70
packages/common/src/pipes/slice_pipe.ts
Normal file
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Pipe, PipeTransform} from '@angular/core';
|
||||
import {invalidPipeArgumentError} from './invalid_pipe_argument_error';
|
||||
|
||||
/**
|
||||
* @ngModule CommonModule
|
||||
* @whatItDoes Creates a new List or String containing a subset (slice) of the elements.
|
||||
* @howToUse `array_or_string_expression | slice:start[:end]`
|
||||
* @description
|
||||
*
|
||||
* Where the input expression is a `List` or `String`, and:
|
||||
* - `start`: The starting index of the subset to return.
|
||||
* - **a positive integer**: return the item at `start` index and all items after
|
||||
* in the list or string expression.
|
||||
* - **a negative integer**: return the item at `start` index from the end and all items after
|
||||
* in the list or string expression.
|
||||
* - **if positive and greater than the size of the expression**: return an empty list or string.
|
||||
* - **if negative and greater than the size of the expression**: return entire list or string.
|
||||
* - `end`: The ending index of the subset to return.
|
||||
* - **omitted**: return all items until the end.
|
||||
* - **if positive**: return all items before `end` index of the list or string.
|
||||
* - **if negative**: return all items before `end` index from the end of the list or string.
|
||||
*
|
||||
* All behavior is based on the expected behavior of the JavaScript API `Array.prototype.slice()`
|
||||
* and `String.prototype.slice()`.
|
||||
*
|
||||
* When operating on a [List], the returned list is always a copy even when all
|
||||
* the elements are being returned.
|
||||
*
|
||||
* When operating on a blank value, the pipe returns the blank value.
|
||||
*
|
||||
* ## List Example
|
||||
*
|
||||
* This `ngFor` example:
|
||||
*
|
||||
* {@example common/pipes/ts/slice_pipe.ts region='SlicePipe_list'}
|
||||
*
|
||||
* produces the following:
|
||||
*
|
||||
* <li>b</li>
|
||||
* <li>c</li>
|
||||
*
|
||||
* ## String Examples
|
||||
*
|
||||
* {@example common/pipes/ts/slice_pipe.ts region='SlicePipe_string'}
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
|
||||
@Pipe({name: 'slice', pure: false})
|
||||
export class SlicePipe implements PipeTransform {
|
||||
transform(value: any, start: number, end?: number): any {
|
||||
if (value == null) return value;
|
||||
|
||||
if (!this.supports(value)) {
|
||||
throw invalidPipeArgumentError(SlicePipe, value);
|
||||
}
|
||||
|
||||
return value.slice(start, end);
|
||||
}
|
||||
|
||||
private supports(obj: any): boolean { return typeof obj === 'string' || Array.isArray(obj); }
|
||||
}
|
44
packages/common/src/platform_id.ts
Normal file
44
packages/common/src/platform_id.ts
Normal file
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
export const PLATFORM_BROWSER_ID = 'browser';
|
||||
export const PLATFORM_SERVER_ID = 'server';
|
||||
export const PLATFORM_WORKER_APP_ID = 'browserWorkerApp';
|
||||
export const PLATFORM_WORKER_UI_ID = 'browserWorkerUi';
|
||||
|
||||
/**
|
||||
* Returns whether a platform id represents a browser platform.
|
||||
* @experimental
|
||||
*/
|
||||
export function isPlatformBrowser(platformId: Object): boolean {
|
||||
return platformId === PLATFORM_BROWSER_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a platform id represents a server platform.
|
||||
* @experimental
|
||||
*/
|
||||
export function isPlatformServer(platformId: Object): boolean {
|
||||
return platformId === PLATFORM_SERVER_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a platform id represents a web worker app platform.
|
||||
* @experimental
|
||||
*/
|
||||
export function isPlatformWorkerApp(platformId: Object): boolean {
|
||||
return platformId === PLATFORM_WORKER_APP_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a platform id represents a web worker UI platform.
|
||||
* @experimental
|
||||
*/
|
||||
export function isPlatformWorkerUi(platformId: Object): boolean {
|
||||
return platformId === PLATFORM_WORKER_UI_ID;
|
||||
}
|
19
packages/common/src/version.ts
Normal file
19
packages/common/src/version.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @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
|
||||
* @description
|
||||
* Entry point for all public APIs of the common package.
|
||||
*/
|
||||
|
||||
import {Version} from '@angular/core';
|
||||
/**
|
||||
* @stable
|
||||
*/
|
||||
export const VERSION = new Version('0.0.0-PLACEHOLDER');
|
362
packages/common/test/directives/ng_class_spec.ts
Normal file
362
packages/common/test/directives/ng_class_spec.ts
Normal file
@ -0,0 +1,362 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Component} from '@angular/core';
|
||||
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
|
||||
|
||||
export function main() {
|
||||
describe('binding to CSS class list', () => {
|
||||
let fixture: ComponentFixture<any>;
|
||||
|
||||
function normalizeClassNames(classes: string) {
|
||||
return classes.trim().split(' ').sort().join(' ');
|
||||
}
|
||||
|
||||
function detectChangesAndExpectClassName(classes: string): void {
|
||||
fixture.detectChanges();
|
||||
let nonNormalizedClassName = fixture.debugElement.children[0].nativeElement.className;
|
||||
expect(normalizeClassNames(nonNormalizedClassName)).toEqual(normalizeClassNames(classes));
|
||||
}
|
||||
|
||||
function getComponent(): TestComponent { return fixture.debugElement.componentInstance; }
|
||||
|
||||
afterEach(() => { fixture = null; });
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TestComponent],
|
||||
});
|
||||
});
|
||||
|
||||
it('should clean up when the directive is destroyed', async(() => {
|
||||
fixture = createTestComponent('<div *ngFor="let item of items" [ngClass]="item"></div>');
|
||||
|
||||
getComponent().items = [['0']];
|
||||
fixture.detectChanges();
|
||||
getComponent().items = [['1']];
|
||||
detectChangesAndExpectClassName('1');
|
||||
}));
|
||||
|
||||
describe('expressions evaluating to objects', () => {
|
||||
|
||||
it('should add classes specified in an object literal', async(() => {
|
||||
fixture = createTestComponent('<div [ngClass]="{foo: true, bar: false}"></div>');
|
||||
|
||||
detectChangesAndExpectClassName('foo');
|
||||
}));
|
||||
|
||||
it('should add classes specified in an object literal without change in class names',
|
||||
async(() => {
|
||||
fixture =
|
||||
createTestComponent(`<div [ngClass]="{'foo-bar': true, 'fooBar': true}"></div>`);
|
||||
|
||||
detectChangesAndExpectClassName('foo-bar fooBar');
|
||||
}));
|
||||
|
||||
it('should add and remove classes based on changes in object literal values', async(() => {
|
||||
fixture =
|
||||
createTestComponent('<div [ngClass]="{foo: condition, bar: !condition}"></div>');
|
||||
|
||||
detectChangesAndExpectClassName('foo');
|
||||
|
||||
getComponent().condition = false;
|
||||
detectChangesAndExpectClassName('bar');
|
||||
}));
|
||||
|
||||
it('should add and remove classes based on changes to the expression object', async(() => {
|
||||
fixture = createTestComponent('<div [ngClass]="objExpr"></div>');
|
||||
const objExpr = getComponent().objExpr;
|
||||
|
||||
detectChangesAndExpectClassName('foo');
|
||||
|
||||
objExpr['bar'] = true;
|
||||
detectChangesAndExpectClassName('foo bar');
|
||||
|
||||
objExpr['baz'] = true;
|
||||
detectChangesAndExpectClassName('foo bar baz');
|
||||
|
||||
delete (objExpr['bar']);
|
||||
detectChangesAndExpectClassName('foo baz');
|
||||
}));
|
||||
|
||||
it('should add and remove classes based on reference changes to the expression object',
|
||||
async(() => {
|
||||
fixture = createTestComponent('<div [ngClass]="objExpr"></div>');
|
||||
|
||||
detectChangesAndExpectClassName('foo');
|
||||
|
||||
getComponent().objExpr = {foo: true, bar: true};
|
||||
detectChangesAndExpectClassName('foo bar');
|
||||
|
||||
getComponent().objExpr = {baz: true};
|
||||
detectChangesAndExpectClassName('baz');
|
||||
}));
|
||||
|
||||
it('should remove active classes when expression evaluates to null', async(() => {
|
||||
fixture = createTestComponent('<div [ngClass]="objExpr"></div>');
|
||||
|
||||
detectChangesAndExpectClassName('foo');
|
||||
|
||||
getComponent().objExpr = null;
|
||||
detectChangesAndExpectClassName('');
|
||||
|
||||
getComponent().objExpr = {'foo': false, 'bar': true};
|
||||
detectChangesAndExpectClassName('bar');
|
||||
}));
|
||||
|
||||
|
||||
it('should allow multiple classes per expression', async(() => {
|
||||
fixture = createTestComponent('<div [ngClass]="objExpr"></div>');
|
||||
|
||||
getComponent().objExpr = {'bar baz': true, 'bar1 baz1': true};
|
||||
detectChangesAndExpectClassName('bar baz bar1 baz1');
|
||||
|
||||
getComponent().objExpr = {'bar baz': false, 'bar1 baz1': true};
|
||||
detectChangesAndExpectClassName('bar1 baz1');
|
||||
}));
|
||||
|
||||
it('should split by one or more spaces between classes', async(() => {
|
||||
fixture = createTestComponent('<div [ngClass]="objExpr"></div>');
|
||||
|
||||
getComponent().objExpr = {'foo bar baz': true};
|
||||
detectChangesAndExpectClassName('foo bar baz');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('expressions evaluating to lists', () => {
|
||||
|
||||
it('should add classes specified in a list literal', async(() => {
|
||||
fixture =
|
||||
createTestComponent(`<div [ngClass]="['foo', 'bar', 'foo-bar', 'fooBar']"></div>`);
|
||||
|
||||
detectChangesAndExpectClassName('foo bar foo-bar fooBar');
|
||||
}));
|
||||
|
||||
it('should add and remove classes based on changes to the expression', async(() => {
|
||||
fixture = createTestComponent('<div [ngClass]="arrExpr"></div>');
|
||||
const arrExpr = getComponent().arrExpr;
|
||||
detectChangesAndExpectClassName('foo');
|
||||
|
||||
arrExpr.push('bar');
|
||||
detectChangesAndExpectClassName('foo bar');
|
||||
|
||||
arrExpr[1] = 'baz';
|
||||
detectChangesAndExpectClassName('foo baz');
|
||||
|
||||
getComponent().arrExpr = arrExpr.filter((v: string) => v !== 'baz');
|
||||
detectChangesAndExpectClassName('foo');
|
||||
}));
|
||||
|
||||
it('should add and remove classes when a reference changes', async(() => {
|
||||
fixture = createTestComponent('<div [ngClass]="arrExpr"></div>');
|
||||
detectChangesAndExpectClassName('foo');
|
||||
|
||||
getComponent().arrExpr = ['bar'];
|
||||
detectChangesAndExpectClassName('bar');
|
||||
}));
|
||||
|
||||
it('should take initial classes into account when a reference changes', async(() => {
|
||||
fixture = createTestComponent('<div class="foo" [ngClass]="arrExpr"></div>');
|
||||
detectChangesAndExpectClassName('foo');
|
||||
|
||||
getComponent().arrExpr = ['bar'];
|
||||
detectChangesAndExpectClassName('foo bar');
|
||||
}));
|
||||
|
||||
it('should ignore empty or blank class names', async(() => {
|
||||
fixture = createTestComponent('<div class="foo" [ngClass]="arrExpr"></div>');
|
||||
getComponent().arrExpr = ['', ' '];
|
||||
detectChangesAndExpectClassName('foo');
|
||||
}));
|
||||
|
||||
it('should trim blanks from class names', async(() => {
|
||||
fixture = createTestComponent('<div class="foo" [ngClass]="arrExpr"></div>');
|
||||
|
||||
getComponent().arrExpr = [' bar '];
|
||||
detectChangesAndExpectClassName('foo bar');
|
||||
}));
|
||||
|
||||
|
||||
it('should allow multiple classes per item in arrays', async(() => {
|
||||
fixture = createTestComponent('<div [ngClass]="arrExpr"></div>');
|
||||
|
||||
getComponent().arrExpr = ['foo bar baz', 'foo1 bar1 baz1'];
|
||||
detectChangesAndExpectClassName('foo bar baz foo1 bar1 baz1');
|
||||
|
||||
getComponent().arrExpr = ['foo bar baz foobar'];
|
||||
detectChangesAndExpectClassName('foo bar baz foobar');
|
||||
}));
|
||||
|
||||
it('should throw with descriptive error message when CSS class is not a string', () => {
|
||||
fixture = createTestComponent(`<div [ngClass]="['foo', {}]"></div>`);
|
||||
expect(() => fixture.detectChanges())
|
||||
.toThrowError(
|
||||
/NgClass can only toggle CSS classes expressed as strings, got \[object Object\]/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expressions evaluating to sets', () => {
|
||||
|
||||
it('should add and remove classes if the set instance changed', async(() => {
|
||||
fixture = createTestComponent('<div [ngClass]="setExpr"></div>');
|
||||
let setExpr = new Set<string>();
|
||||
setExpr.add('bar');
|
||||
getComponent().setExpr = setExpr;
|
||||
detectChangesAndExpectClassName('bar');
|
||||
|
||||
setExpr = new Set<string>();
|
||||
setExpr.add('baz');
|
||||
getComponent().setExpr = setExpr;
|
||||
detectChangesAndExpectClassName('baz');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('expressions evaluating to string', () => {
|
||||
|
||||
it('should add classes specified in a string literal', async(() => {
|
||||
fixture = createTestComponent(`<div [ngClass]="'foo bar foo-bar fooBar'"></div>`);
|
||||
detectChangesAndExpectClassName('foo bar foo-bar fooBar');
|
||||
}));
|
||||
|
||||
it('should add and remove classes based on changes to the expression', async(() => {
|
||||
fixture = createTestComponent('<div [ngClass]="strExpr"></div>');
|
||||
detectChangesAndExpectClassName('foo');
|
||||
|
||||
getComponent().strExpr = 'foo bar';
|
||||
detectChangesAndExpectClassName('foo bar');
|
||||
|
||||
|
||||
getComponent().strExpr = 'baz';
|
||||
detectChangesAndExpectClassName('baz');
|
||||
}));
|
||||
|
||||
it('should remove active classes when switching from string to null', async(() => {
|
||||
fixture = createTestComponent(`<div [ngClass]="strExpr"></div>`);
|
||||
detectChangesAndExpectClassName('foo');
|
||||
|
||||
getComponent().strExpr = null;
|
||||
detectChangesAndExpectClassName('');
|
||||
}));
|
||||
|
||||
it('should take initial classes into account when switching from string to null',
|
||||
async(() => {
|
||||
fixture = createTestComponent(`<div class="foo" [ngClass]="strExpr"></div>`);
|
||||
detectChangesAndExpectClassName('foo');
|
||||
|
||||
getComponent().strExpr = null;
|
||||
detectChangesAndExpectClassName('foo');
|
||||
}));
|
||||
|
||||
it('should ignore empty and blank strings', async(() => {
|
||||
fixture = createTestComponent(`<div class="foo" [ngClass]="strExpr"></div>`);
|
||||
getComponent().strExpr = '';
|
||||
detectChangesAndExpectClassName('foo');
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('cooperation with other class-changing constructs', () => {
|
||||
|
||||
it('should co-operate with the class attribute', async(() => {
|
||||
fixture = createTestComponent('<div [ngClass]="objExpr" class="init foo"></div>');
|
||||
const objExpr = getComponent().objExpr;
|
||||
|
||||
objExpr['bar'] = true;
|
||||
detectChangesAndExpectClassName('init foo bar');
|
||||
|
||||
objExpr['foo'] = false;
|
||||
detectChangesAndExpectClassName('init bar');
|
||||
|
||||
getComponent().objExpr = null;
|
||||
detectChangesAndExpectClassName('init foo');
|
||||
}));
|
||||
|
||||
it('should co-operate with the interpolated class attribute', async(() => {
|
||||
fixture = createTestComponent(`<div [ngClass]="objExpr" class="{{'init foo'}}"></div>`);
|
||||
const objExpr = getComponent().objExpr;
|
||||
|
||||
objExpr['bar'] = true;
|
||||
detectChangesAndExpectClassName(`init foo bar`);
|
||||
|
||||
objExpr['foo'] = false;
|
||||
detectChangesAndExpectClassName(`init bar`);
|
||||
|
||||
getComponent().objExpr = null;
|
||||
detectChangesAndExpectClassName(`init foo`);
|
||||
}));
|
||||
|
||||
it('should co-operate with the class attribute and binding to it', async(() => {
|
||||
fixture =
|
||||
createTestComponent(`<div [ngClass]="objExpr" class="init" [class]="'foo'"></div>`);
|
||||
const objExpr = getComponent().objExpr;
|
||||
|
||||
objExpr['bar'] = true;
|
||||
detectChangesAndExpectClassName(`init foo bar`);
|
||||
|
||||
objExpr['foo'] = false;
|
||||
detectChangesAndExpectClassName(`init bar`);
|
||||
|
||||
getComponent().objExpr = null;
|
||||
detectChangesAndExpectClassName(`init foo`);
|
||||
}));
|
||||
|
||||
it('should co-operate with the class attribute and class.name binding', async(() => {
|
||||
const template =
|
||||
'<div class="init foo" [ngClass]="objExpr" [class.baz]="condition"></div>';
|
||||
fixture = createTestComponent(template);
|
||||
const objExpr = getComponent().objExpr;
|
||||
|
||||
detectChangesAndExpectClassName('init foo baz');
|
||||
|
||||
objExpr['bar'] = true;
|
||||
detectChangesAndExpectClassName('init foo baz bar');
|
||||
|
||||
objExpr['foo'] = false;
|
||||
detectChangesAndExpectClassName('init baz bar');
|
||||
|
||||
getComponent().condition = false;
|
||||
detectChangesAndExpectClassName('init bar');
|
||||
}));
|
||||
|
||||
it('should co-operate with initial class and class attribute binding when binding changes',
|
||||
async(() => {
|
||||
const template = '<div class="init" [ngClass]="objExpr" [class]="strExpr"></div>';
|
||||
fixture = createTestComponent(template);
|
||||
const cmp = getComponent();
|
||||
|
||||
detectChangesAndExpectClassName('init foo');
|
||||
|
||||
cmp.objExpr['bar'] = true;
|
||||
detectChangesAndExpectClassName('init foo bar');
|
||||
|
||||
cmp.strExpr = 'baz';
|
||||
detectChangesAndExpectClassName('init bar baz foo');
|
||||
|
||||
cmp.objExpr = null;
|
||||
detectChangesAndExpectClassName('init baz');
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Component({selector: 'test-cmp', template: ''})
|
||||
class TestComponent {
|
||||
condition: boolean = true;
|
||||
items: any[];
|
||||
arrExpr: string[] = ['foo'];
|
||||
setExpr: Set<string> = new Set<string>();
|
||||
objExpr: {[klass: string]: any} = {'foo': true, 'bar': false};
|
||||
strExpr = 'foo';
|
||||
|
||||
constructor() { this.setExpr.add('foo'); }
|
||||
}
|
||||
|
||||
function createTestComponent(template: string): ComponentFixture<TestComponent> {
|
||||
return TestBed.overrideComponent(TestComponent, {set: {template: template}})
|
||||
.createComponent(TestComponent);
|
||||
}
|
278
packages/common/test/directives/ng_component_outlet_spec.ts
Normal file
278
packages/common/test/directives/ng_component_outlet_spec.ts
Normal file
@ -0,0 +1,278 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {NgComponentOutlet} from '@angular/common/src/directives/ng_component_outlet';
|
||||
import {Compiler, Component, ComponentRef, Inject, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, NgModuleFactory, Optional, Provider, QueryList, ReflectiveInjector, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core';
|
||||
import {TestBed, async, fakeAsync} from '@angular/core/testing';
|
||||
import {expect} from '@angular/platform-browser/testing/matchers';
|
||||
|
||||
export function main() {
|
||||
describe('insert/remove', () => {
|
||||
|
||||
beforeEach(() => { TestBed.configureTestingModule({imports: [TestModule]}); });
|
||||
|
||||
it('should do nothing if component is null', async(() => {
|
||||
const template = `<ng-template *ngComponentOutlet="currentComponent"></ng-template>`;
|
||||
TestBed.overrideComponent(TestComponent, {set: {template: template}});
|
||||
let fixture = TestBed.createComponent(TestComponent);
|
||||
|
||||
fixture.componentInstance.currentComponent = null;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement).toHaveText('');
|
||||
}));
|
||||
|
||||
it('should insert content specified by a component', async(() => {
|
||||
let fixture = TestBed.createComponent(TestComponent);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('');
|
||||
|
||||
fixture.componentInstance.currentComponent = InjectedComponent;
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('foo');
|
||||
}));
|
||||
|
||||
it('should emit a ComponentRef once a component was created', async(() => {
|
||||
let fixture = TestBed.createComponent(TestComponent);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('');
|
||||
|
||||
fixture.componentInstance.cmpRef = null;
|
||||
fixture.componentInstance.currentComponent = InjectedComponent;
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('foo');
|
||||
expect(fixture.componentInstance.cmpRef).toBeAnInstanceOf(ComponentRef);
|
||||
expect(fixture.componentInstance.cmpRef.instance).toBeAnInstanceOf(InjectedComponent);
|
||||
}));
|
||||
|
||||
|
||||
it('should clear view if component becomes null', async(() => {
|
||||
let fixture = TestBed.createComponent(TestComponent);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('');
|
||||
|
||||
fixture.componentInstance.currentComponent = InjectedComponent;
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('foo');
|
||||
|
||||
fixture.componentInstance.currentComponent = null;
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('');
|
||||
}));
|
||||
|
||||
|
||||
it('should swap content if component changes', async(() => {
|
||||
let fixture = TestBed.createComponent(TestComponent);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('');
|
||||
|
||||
fixture.componentInstance.currentComponent = InjectedComponent;
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('foo');
|
||||
|
||||
fixture.componentInstance.currentComponent = InjectedComponentAgain;
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('bar');
|
||||
}));
|
||||
|
||||
it('should use the injector, if one supplied', async(() => {
|
||||
let fixture = TestBed.createComponent(TestComponent);
|
||||
|
||||
const uniqueValue = {};
|
||||
fixture.componentInstance.currentComponent = InjectedComponent;
|
||||
fixture.componentInstance.injector = ReflectiveInjector.resolveAndCreate(
|
||||
[{provide: TEST_TOKEN, useValue: uniqueValue}], fixture.componentRef.injector);
|
||||
|
||||
fixture.detectChanges();
|
||||
let cmpRef: ComponentRef<InjectedComponent> = fixture.componentInstance.cmpRef;
|
||||
expect(cmpRef).toBeAnInstanceOf(ComponentRef);
|
||||
expect(cmpRef.instance).toBeAnInstanceOf(InjectedComponent);
|
||||
expect(cmpRef.instance.testToken).toBe(uniqueValue);
|
||||
|
||||
}));
|
||||
|
||||
it('should resolve a with injector', async(() => {
|
||||
let fixture = TestBed.createComponent(TestComponent);
|
||||
|
||||
fixture.componentInstance.cmpRef = null;
|
||||
fixture.componentInstance.currentComponent = InjectedComponent;
|
||||
fixture.detectChanges();
|
||||
let cmpRef: ComponentRef<InjectedComponent> = fixture.componentInstance.cmpRef;
|
||||
expect(cmpRef).toBeAnInstanceOf(ComponentRef);
|
||||
expect(cmpRef.instance).toBeAnInstanceOf(InjectedComponent);
|
||||
expect(cmpRef.instance.testToken).toBeNull();
|
||||
}));
|
||||
|
||||
it('should render projectable nodes, if supplied', async(() => {
|
||||
const template = `<ng-template>projected foo</ng-template>${TEST_CMP_TEMPLATE}`;
|
||||
TestBed.overrideComponent(TestComponent, {set: {template: template}})
|
||||
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});
|
||||
|
||||
TestBed
|
||||
.overrideComponent(InjectedComponent, {set: {template: `<ng-content></ng-content>`}})
|
||||
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});
|
||||
|
||||
let fixture = TestBed.createComponent(TestComponent);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('');
|
||||
|
||||
fixture.componentInstance.currentComponent = InjectedComponent;
|
||||
fixture.componentInstance.projectables =
|
||||
[fixture.componentInstance.vcRef
|
||||
.createEmbeddedView(fixture.componentInstance.tplRefs.first)
|
||||
.rootNodes];
|
||||
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('projected foo');
|
||||
}));
|
||||
|
||||
it('should resolve components from other modules, if supplied', async(() => {
|
||||
const compiler = TestBed.get(Compiler) as Compiler;
|
||||
let fixture = TestBed.createComponent(TestComponent);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('');
|
||||
|
||||
fixture.componentInstance.module = compiler.compileModuleSync(TestModule2);
|
||||
fixture.componentInstance.currentComponent = Module2InjectedComponent;
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('baz');
|
||||
}));
|
||||
|
||||
it('should clean up moduleRef, if supplied', async(() => {
|
||||
let destroyed = false;
|
||||
const compiler = TestBed.get(Compiler) as Compiler;
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
fixture.componentInstance.module = compiler.compileModuleSync(TestModule2);
|
||||
fixture.componentInstance.currentComponent = Module2InjectedComponent;
|
||||
fixture.detectChanges();
|
||||
|
||||
const moduleRef = fixture.componentInstance.ngComponentOutlet['_moduleRef'];
|
||||
spyOn(moduleRef, 'destroy').and.callThrough();
|
||||
|
||||
expect(moduleRef.destroy).not.toHaveBeenCalled();
|
||||
fixture.destroy();
|
||||
expect(moduleRef.destroy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should not re-create moduleRef when it didn\'t actually change', async(() => {
|
||||
const compiler = TestBed.get(Compiler) as Compiler;
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
fixture.componentInstance.module = compiler.compileModuleSync(TestModule2);
|
||||
fixture.componentInstance.currentComponent = Module2InjectedComponent;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement).toHaveText('baz');
|
||||
|
||||
const moduleRef = fixture.componentInstance.ngComponentOutlet['_moduleRef'];
|
||||
fixture.componentInstance.currentComponent = Module2InjectedComponent2;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement).toHaveText('baz2');
|
||||
expect(moduleRef).toBe(fixture.componentInstance.ngComponentOutlet['_moduleRef']);
|
||||
}));
|
||||
|
||||
it('should re-create moduleRef when changed', async(() => {
|
||||
const compiler = TestBed.get(Compiler) as Compiler;
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
fixture.componentInstance.module = compiler.compileModuleSync(TestModule2);
|
||||
fixture.componentInstance.currentComponent = Module2InjectedComponent;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement).toHaveText('baz');
|
||||
|
||||
fixture.componentInstance.module = compiler.compileModuleSync(TestModule3);
|
||||
fixture.componentInstance.currentComponent = Module3InjectedComponent;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement).toHaveText('bat');
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
const TEST_TOKEN = new InjectionToken('TestToken');
|
||||
@Component({selector: 'injected-component', template: 'foo'})
|
||||
class InjectedComponent {
|
||||
constructor(@Optional() @Inject(TEST_TOKEN) public testToken: any) {}
|
||||
}
|
||||
|
||||
|
||||
@Component({selector: 'injected-component-again', template: 'bar'})
|
||||
class InjectedComponentAgain {
|
||||
}
|
||||
|
||||
const TEST_CMP_TEMPLATE =
|
||||
`<ng-template *ngComponentOutlet="currentComponent; injector: injector; content: projectables; ngModuleFactory: module;"></ng-template>`;
|
||||
@Component({selector: 'test-cmp', template: TEST_CMP_TEMPLATE})
|
||||
class TestComponent {
|
||||
currentComponent: Type<any>;
|
||||
injector: Injector;
|
||||
projectables: any[][];
|
||||
module: NgModuleFactory<any>;
|
||||
|
||||
get cmpRef(): ComponentRef<any> { return this.ngComponentOutlet['_componentRef']; }
|
||||
set cmpRef(value: ComponentRef<any>) { this.ngComponentOutlet['_componentRef'] = value; }
|
||||
|
||||
@ViewChildren(TemplateRef) tplRefs: QueryList<TemplateRef<any>>;
|
||||
@ViewChild(NgComponentOutlet) ngComponentOutlet: NgComponentOutlet;
|
||||
|
||||
constructor(public vcRef: ViewContainerRef) {}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
declarations: [TestComponent, InjectedComponent, InjectedComponentAgain],
|
||||
exports: [TestComponent, InjectedComponent, InjectedComponentAgain],
|
||||
entryComponents: [InjectedComponent, InjectedComponentAgain]
|
||||
})
|
||||
export class TestModule {
|
||||
}
|
||||
|
||||
@Component({selector: 'mdoule-2-injected-component', template: 'baz'})
|
||||
class Module2InjectedComponent {
|
||||
}
|
||||
|
||||
@Component({selector: 'mdoule-2-injected-component-2', template: 'baz2'})
|
||||
class Module2InjectedComponent2 {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
declarations: [Module2InjectedComponent, Module2InjectedComponent2],
|
||||
exports: [Module2InjectedComponent, Module2InjectedComponent2],
|
||||
entryComponents: [Module2InjectedComponent, Module2InjectedComponent2]
|
||||
})
|
||||
export class TestModule2 {
|
||||
}
|
||||
|
||||
@Component({selector: 'mdoule-3-injected-component', template: 'bat'})
|
||||
class Module3InjectedComponent {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
declarations: [Module3InjectedComponent],
|
||||
exports: [Module3InjectedComponent],
|
||||
entryComponents: [Module3InjectedComponent]
|
||||
})
|
||||
export class TestModule3 {
|
||||
}
|
388
packages/common/test/directives/ng_for_spec.ts
Normal file
388
packages/common/test/directives/ng_for_spec.ts
Normal file
@ -0,0 +1,388 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {CommonModule, NgFor, NgForOf} from '@angular/common';
|
||||
import {Component, Directive} from '@angular/core';
|
||||
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
|
||||
import {By} from '@angular/platform-browser/src/dom/debug/by';
|
||||
import {expect} from '@angular/platform-browser/testing/matchers';
|
||||
|
||||
let thisArg: any;
|
||||
|
||||
export function main() {
|
||||
describe('ngFor', () => {
|
||||
let fixture: ComponentFixture<any>;
|
||||
|
||||
function getComponent(): TestComponent { return fixture.componentInstance; }
|
||||
|
||||
function detectChangesAndExpectText(text: string): void {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText(text);
|
||||
}
|
||||
|
||||
afterEach(() => { fixture = null; });
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TestComponent, TestDirective],
|
||||
imports: [CommonModule],
|
||||
});
|
||||
});
|
||||
|
||||
it('should reflect initial elements', async(() => {
|
||||
fixture = createTestComponent();
|
||||
|
||||
detectChangesAndExpectText('1;2;');
|
||||
}));
|
||||
|
||||
it('should reflect added elements', async(() => {
|
||||
fixture = createTestComponent();
|
||||
fixture.detectChanges();
|
||||
getComponent().items.push(3);
|
||||
detectChangesAndExpectText('1;2;3;');
|
||||
}));
|
||||
|
||||
it('should reflect removed elements', async(() => {
|
||||
fixture = createTestComponent();
|
||||
fixture.detectChanges();
|
||||
getComponent().items.splice(1, 1);
|
||||
detectChangesAndExpectText('1;');
|
||||
}));
|
||||
|
||||
it('should reflect moved elements', async(() => {
|
||||
fixture = createTestComponent();
|
||||
fixture.detectChanges();
|
||||
getComponent().items.splice(0, 1);
|
||||
getComponent().items.push(1);
|
||||
detectChangesAndExpectText('2;1;');
|
||||
}));
|
||||
|
||||
it('should reflect a mix of all changes (additions/removals/moves)', async(() => {
|
||||
fixture = createTestComponent();
|
||||
|
||||
getComponent().items = [0, 1, 2, 3, 4, 5];
|
||||
fixture.detectChanges();
|
||||
|
||||
getComponent().items = [6, 2, 7, 0, 4, 8];
|
||||
|
||||
detectChangesAndExpectText('6;2;7;0;4;8;');
|
||||
}));
|
||||
|
||||
it('should iterate over an array of objects', async(() => {
|
||||
const template = '<ul><li *ngFor="let item of items">{{item["name"]}};</li></ul>';
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
// INIT
|
||||
getComponent().items = [{'name': 'misko'}, {'name': 'shyam'}];
|
||||
detectChangesAndExpectText('misko;shyam;');
|
||||
|
||||
// GROW
|
||||
getComponent().items.push({'name': 'adam'});
|
||||
detectChangesAndExpectText('misko;shyam;adam;');
|
||||
|
||||
// SHRINK
|
||||
getComponent().items.splice(2, 1);
|
||||
getComponent().items.splice(0, 1);
|
||||
detectChangesAndExpectText('shyam;');
|
||||
}));
|
||||
|
||||
it('should gracefully handle nulls', async(() => {
|
||||
const template = '<ul><li *ngFor="let item of null">{{item}};</li></ul>';
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
detectChangesAndExpectText('');
|
||||
}));
|
||||
|
||||
it('should gracefully handle ref changing to null and back', async(() => {
|
||||
fixture = createTestComponent();
|
||||
|
||||
detectChangesAndExpectText('1;2;');
|
||||
|
||||
getComponent().items = null;
|
||||
detectChangesAndExpectText('');
|
||||
|
||||
getComponent().items = [1, 2, 3];
|
||||
detectChangesAndExpectText('1;2;3;');
|
||||
}));
|
||||
|
||||
it('should throw on non-iterable ref and suggest using an array', async(() => {
|
||||
fixture = createTestComponent();
|
||||
|
||||
getComponent().items = <any>'whaaa';
|
||||
expect(() => fixture.detectChanges())
|
||||
.toThrowError(
|
||||
/Cannot find a differ supporting object 'whaaa' of type 'string'. NgFor only supports binding to Iterables such as Arrays/);
|
||||
}));
|
||||
|
||||
it('should throw on ref changing to string', async(() => {
|
||||
fixture = createTestComponent();
|
||||
|
||||
detectChangesAndExpectText('1;2;');
|
||||
|
||||
getComponent().items = <any>'whaaa';
|
||||
expect(() => fixture.detectChanges()).toThrowError();
|
||||
}));
|
||||
|
||||
it('should works with duplicates', async(() => {
|
||||
fixture = createTestComponent();
|
||||
|
||||
const a = new Foo();
|
||||
getComponent().items = [a, a];
|
||||
detectChangesAndExpectText('foo;foo;');
|
||||
}));
|
||||
|
||||
it('should repeat over nested arrays', async(() => {
|
||||
const template = '<div *ngFor="let item of items">' +
|
||||
'<div *ngFor="let subitem of item">{{subitem}}-{{item.length}};</div>|' +
|
||||
'</div>';
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().items = [['a', 'b'], ['c']];
|
||||
detectChangesAndExpectText('a-2;b-2;|c-1;|');
|
||||
|
||||
getComponent().items = [['e'], ['f', 'g']];
|
||||
detectChangesAndExpectText('e-1;|f-2;g-2;|');
|
||||
}));
|
||||
|
||||
it('should repeat over nested arrays with no intermediate element', async(() => {
|
||||
const template = '<div *ngFor="let item of items">' +
|
||||
'<div *ngFor="let subitem of item">{{subitem}}-{{item.length}};</div>' +
|
||||
'</div>';
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().items = [['a', 'b'], ['c']];
|
||||
detectChangesAndExpectText('a-2;b-2;c-1;');
|
||||
|
||||
getComponent().items = [['e'], ['f', 'g']];
|
||||
detectChangesAndExpectText('e-1;f-2;g-2;');
|
||||
}));
|
||||
|
||||
it('should repeat over nested ngIf that are the last node in the ngFor template', async(() => {
|
||||
const template = `<div *ngFor="let item of items; let i=index">` +
|
||||
`<div>{{i}}|</div>` +
|
||||
`<div *ngIf="i % 2 == 0">even|</div>` +
|
||||
`</div>`;
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
const items = [1];
|
||||
getComponent().items = items;
|
||||
detectChangesAndExpectText('0|even|');
|
||||
|
||||
items.push(1);
|
||||
detectChangesAndExpectText('0|even|1|');
|
||||
|
||||
items.push(1);
|
||||
detectChangesAndExpectText('0|even|1|2|even|');
|
||||
}));
|
||||
|
||||
it('should display indices correctly', async(() => {
|
||||
const template = '<span *ngFor ="let item of items; let i=index">{{i.toString()}}</span>';
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
detectChangesAndExpectText('0123456789');
|
||||
|
||||
getComponent().items = [1, 2, 6, 7, 4, 3, 5, 8, 9, 0];
|
||||
detectChangesAndExpectText('0123456789');
|
||||
}));
|
||||
|
||||
it('should display first item correctly', async(() => {
|
||||
const template =
|
||||
'<span *ngFor="let item of items; let isFirst=first">{{isFirst.toString()}}</span>';
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().items = [0, 1, 2];
|
||||
detectChangesAndExpectText('truefalsefalse');
|
||||
|
||||
getComponent().items = [2, 1];
|
||||
detectChangesAndExpectText('truefalse');
|
||||
}));
|
||||
|
||||
it('should display last item correctly', async(() => {
|
||||
const template =
|
||||
'<span *ngFor="let item of items; let isLast=last">{{isLast.toString()}}</span>';
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().items = [0, 1, 2];
|
||||
detectChangesAndExpectText('falsefalsetrue');
|
||||
|
||||
getComponent().items = [2, 1];
|
||||
detectChangesAndExpectText('falsetrue');
|
||||
}));
|
||||
|
||||
it('should display even items correctly', async(() => {
|
||||
const template =
|
||||
'<span *ngFor="let item of items; let isEven=even">{{isEven.toString()}}</span>';
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().items = [0, 1, 2];
|
||||
detectChangesAndExpectText('truefalsetrue');
|
||||
|
||||
getComponent().items = [2, 1];
|
||||
detectChangesAndExpectText('truefalse');
|
||||
}));
|
||||
|
||||
it('should display odd items correctly', async(() => {
|
||||
const template =
|
||||
'<span *ngFor="let item of items; let isOdd=odd">{{isOdd.toString()}}</span>';
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().items = [0, 1, 2, 3];
|
||||
detectChangesAndExpectText('falsetruefalsetrue');
|
||||
|
||||
getComponent().items = [2, 1];
|
||||
detectChangesAndExpectText('falsetrue');
|
||||
}));
|
||||
|
||||
it('should allow to use a custom template', async(() => {
|
||||
const template =
|
||||
'<ng-container *ngFor="let item of items; template: tpl"></ng-container>' +
|
||||
'<ng-template let-item let-i="index" #tpl><p>{{i}}: {{item}};</p></ng-template>';
|
||||
fixture = createTestComponent(template);
|
||||
getComponent().items = ['a', 'b', 'c'];
|
||||
fixture.detectChanges();
|
||||
detectChangesAndExpectText('0: a;1: b;2: c;');
|
||||
}));
|
||||
|
||||
it('should use a default template if a custom one is null', async(() => {
|
||||
const template =
|
||||
`<ul><ng-container *ngFor="let item of items; template: null; let i=index">{{i}}: {{item}};</ng-container></ul>`;
|
||||
fixture = createTestComponent(template);
|
||||
getComponent().items = ['a', 'b', 'c'];
|
||||
fixture.detectChanges();
|
||||
detectChangesAndExpectText('0: a;1: b;2: c;');
|
||||
}));
|
||||
|
||||
it('should use a custom template when both default and a custom one are present', async(() => {
|
||||
const template =
|
||||
'<ng-container *ngFor="let item of items; template: tpl">{{i}};</ng-container>' +
|
||||
'<ng-template let-item let-i="index" #tpl>{{i}}: {{item}};</ng-template>';
|
||||
fixture = createTestComponent(template);
|
||||
getComponent().items = ['a', 'b', 'c'];
|
||||
fixture.detectChanges();
|
||||
detectChangesAndExpectText('0: a;1: b;2: c;');
|
||||
}));
|
||||
|
||||
describe('track by', () => {
|
||||
it('should console.warn if trackBy is not a function', async(() => {
|
||||
// TODO(vicb): expect a warning message when we have a proper log service
|
||||
const template = `<p *ngFor="let item of items; trackBy: value"></p>`;
|
||||
fixture = createTestComponent(template);
|
||||
fixture.componentInstance.value = 0;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should track by identity when trackBy is to `null` or `undefined`', async(() => {
|
||||
// TODO(vicb): expect no warning message when we have a proper log service
|
||||
const template = `<p *ngFor="let item of items; trackBy: value">{{ item }}</p>`;
|
||||
fixture = createTestComponent(template);
|
||||
fixture.componentInstance.items = ['a', 'b', 'c'];
|
||||
fixture.componentInstance.value = null;
|
||||
detectChangesAndExpectText('abc');
|
||||
fixture.componentInstance.value = undefined;
|
||||
detectChangesAndExpectText('abc');
|
||||
}));
|
||||
|
||||
it('should set the context to the component instance', async(() => {
|
||||
const template =
|
||||
`<p *ngFor="let item of items; trackBy: trackByContext.bind(this)"></p>`;
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
thisArg = null;
|
||||
fixture.detectChanges();
|
||||
expect(thisArg).toBe(getComponent());
|
||||
}));
|
||||
|
||||
it('should not replace tracked items', async(() => {
|
||||
const template =
|
||||
`<p *ngFor="let item of items; trackBy: trackById; let i=index">{{items[i]}}</p>`;
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
const buildItemList = () => {
|
||||
getComponent().items = [{'id': 'a'}];
|
||||
fixture.detectChanges();
|
||||
return fixture.debugElement.queryAll(By.css('p'))[0];
|
||||
};
|
||||
|
||||
const firstP = buildItemList();
|
||||
const finalP = buildItemList();
|
||||
expect(finalP.nativeElement).toBe(firstP.nativeElement);
|
||||
}));
|
||||
|
||||
it('should update implicit local variable on view', async(() => {
|
||||
const template =
|
||||
`<div *ngFor="let item of items; trackBy: trackById">{{item['color']}}</div>`;
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().items = [{'id': 'a', 'color': 'blue'}];
|
||||
detectChangesAndExpectText('blue');
|
||||
|
||||
getComponent().items = [{'id': 'a', 'color': 'red'}];
|
||||
detectChangesAndExpectText('red');
|
||||
}));
|
||||
|
||||
it('should move items around and keep them updated ', async(() => {
|
||||
const template =
|
||||
`<div *ngFor="let item of items; trackBy: trackById">{{item['color']}}</div>`;
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().items = [{'id': 'a', 'color': 'blue'}, {'id': 'b', 'color': 'yellow'}];
|
||||
detectChangesAndExpectText('blueyellow');
|
||||
|
||||
getComponent().items = [{'id': 'b', 'color': 'orange'}, {'id': 'a', 'color': 'red'}];
|
||||
detectChangesAndExpectText('orangered');
|
||||
}));
|
||||
|
||||
it('should handle added and removed items properly when tracking by index', async(() => {
|
||||
const template = `<div *ngFor="let item of items; trackBy: trackByIndex">{{item}}</div>`;
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().items = ['a', 'b', 'c', 'd'];
|
||||
fixture.detectChanges();
|
||||
getComponent().items = ['e', 'f', 'g', 'h'];
|
||||
fixture.detectChanges();
|
||||
getComponent().items = ['e', 'f', 'h'];
|
||||
detectChangesAndExpectText('efh');
|
||||
}));
|
||||
|
||||
it('should support injecting `NgFor` and get an instance of `NgForOf`', async(() => {
|
||||
const template = `<ng-template ngFor [ngForOf]='items' let-item test></ng-template>`;
|
||||
fixture = createTestComponent(template);
|
||||
const testDirective = fixture.debugElement.childNodes[0].injector.get(TestDirective);
|
||||
const ngForOf = fixture.debugElement.childNodes[0].injector.get(NgForOf);
|
||||
expect(testDirective.ngFor).toBe(ngForOf);
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class Foo {
|
||||
toString() { return 'foo'; }
|
||||
}
|
||||
|
||||
@Component({selector: 'test-cmp', template: ''})
|
||||
class TestComponent {
|
||||
value: any;
|
||||
items: any[] = [1, 2];
|
||||
trackById(index: number, item: any): string { return item['id']; }
|
||||
trackByIndex(index: number, item: any): number { return index; }
|
||||
trackByContext(): void { thisArg = this; }
|
||||
}
|
||||
|
||||
@Directive({selector: '[test]'})
|
||||
class TestDirective {
|
||||
constructor(public ngFor: NgFor) {}
|
||||
}
|
||||
|
||||
const TEMPLATE = '<div><span *ngFor="let item of items">{{item.toString()}};</span></div>';
|
||||
|
||||
function createTestComponent(template: string = TEMPLATE): ComponentFixture<TestComponent> {
|
||||
return TestBed.overrideComponent(TestComponent, {set: {template: template}})
|
||||
.createComponent(TestComponent);
|
||||
}
|
221
packages/common/test/directives/ng_if_spec.ts
Normal file
221
packages/common/test/directives/ng_if_spec.ts
Normal file
@ -0,0 +1,221 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {Component} from '@angular/core';
|
||||
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
|
||||
import {By} from '@angular/platform-browser/src/dom/debug/by';
|
||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
import {expect} from '@angular/platform-browser/testing/matchers';
|
||||
|
||||
export function main() {
|
||||
describe('ngIf directive', () => {
|
||||
let fixture: ComponentFixture<any>;
|
||||
|
||||
function getComponent(): TestComponent { return fixture.componentInstance; }
|
||||
|
||||
afterEach(() => { fixture = null; });
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TestComponent],
|
||||
imports: [CommonModule],
|
||||
});
|
||||
});
|
||||
|
||||
it('should work in a template attribute', async(() => {
|
||||
const template = '<span *ngIf="booleanCondition">hello</span>';
|
||||
fixture = createTestComponent(template);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(1);
|
||||
expect(fixture.nativeElement).toHaveText('hello');
|
||||
}));
|
||||
|
||||
it('should work on a template element', async(() => {
|
||||
const template = '<ng-template [ngIf]="booleanCondition">hello2</ng-template>';
|
||||
fixture = createTestComponent(template);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('hello2');
|
||||
}));
|
||||
|
||||
it('should toggle node when condition changes', async(() => {
|
||||
const template = '<span *ngIf="booleanCondition">hello</span>';
|
||||
fixture = createTestComponent(template);
|
||||
getComponent().booleanCondition = false;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(0);
|
||||
expect(fixture.nativeElement).toHaveText('');
|
||||
|
||||
getComponent().booleanCondition = true;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(1);
|
||||
expect(fixture.nativeElement).toHaveText('hello');
|
||||
|
||||
getComponent().booleanCondition = false;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(0);
|
||||
expect(fixture.nativeElement).toHaveText('');
|
||||
}));
|
||||
|
||||
it('should handle nested if correctly', async(() => {
|
||||
const template =
|
||||
'<div *ngIf="booleanCondition"><span *ngIf="nestedBooleanCondition">hello</span></div>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().booleanCondition = false;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(0);
|
||||
expect(fixture.nativeElement).toHaveText('');
|
||||
|
||||
getComponent().booleanCondition = true;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(1);
|
||||
expect(fixture.nativeElement).toHaveText('hello');
|
||||
|
||||
getComponent().nestedBooleanCondition = false;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(0);
|
||||
expect(fixture.nativeElement).toHaveText('');
|
||||
|
||||
getComponent().nestedBooleanCondition = true;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(1);
|
||||
expect(fixture.nativeElement).toHaveText('hello');
|
||||
|
||||
getComponent().booleanCondition = false;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(0);
|
||||
expect(fixture.nativeElement).toHaveText('');
|
||||
}));
|
||||
|
||||
it('should update several nodes with if', async(() => {
|
||||
const template = '<span *ngIf="numberCondition + 1 >= 2">helloNumber</span>' +
|
||||
'<span *ngIf="stringCondition == \'foo\'">helloString</span>' +
|
||||
'<span *ngIf="functionCondition(stringCondition, numberCondition)">helloFunction</span>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(3);
|
||||
expect(getDOM().getText(fixture.nativeElement))
|
||||
.toEqual('helloNumberhelloStringhelloFunction');
|
||||
|
||||
getComponent().numberCondition = 0;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(1);
|
||||
expect(fixture.nativeElement).toHaveText('helloString');
|
||||
|
||||
getComponent().numberCondition = 1;
|
||||
getComponent().stringCondition = 'bar';
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(1);
|
||||
expect(fixture.nativeElement).toHaveText('helloNumber');
|
||||
}));
|
||||
|
||||
it('should not add the element twice if the condition goes from truthy to truthy', async(() => {
|
||||
const template = '<span *ngIf="numberCondition">hello</span>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
fixture.detectChanges();
|
||||
let els = fixture.debugElement.queryAll(By.css('span'));
|
||||
expect(els.length).toEqual(1);
|
||||
getDOM().addClass(els[0].nativeElement, 'marker');
|
||||
expect(fixture.nativeElement).toHaveText('hello');
|
||||
|
||||
getComponent().numberCondition = 2;
|
||||
fixture.detectChanges();
|
||||
els = fixture.debugElement.queryAll(By.css('span'));
|
||||
expect(els.length).toEqual(1);
|
||||
expect(getDOM().hasClass(els[0].nativeElement, 'marker')).toBe(true);
|
||||
|
||||
expect(fixture.nativeElement).toHaveText('hello');
|
||||
}));
|
||||
|
||||
describe('else', () => {
|
||||
it('should support else', async(() => {
|
||||
const template = '<span *ngIf="booleanCondition; else elseBlock">TRUE</span>' +
|
||||
'<ng-template #elseBlock>FALSE</ng-template>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('TRUE');
|
||||
|
||||
getComponent().booleanCondition = false;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('FALSE');
|
||||
}));
|
||||
|
||||
it('should support then and else', async(() => {
|
||||
const template =
|
||||
'<span *ngIf="booleanCondition; then thenBlock; else elseBlock">IGNORE</span>' +
|
||||
'<ng-template #thenBlock>THEN</ng-template>' +
|
||||
'<ng-template #elseBlock>ELSE</ng-template>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('THEN');
|
||||
|
||||
getComponent().booleanCondition = false;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('ELSE');
|
||||
}));
|
||||
|
||||
it('should support dynamic else', async(() => {
|
||||
const template =
|
||||
'<span *ngIf="booleanCondition; else nestedBooleanCondition ? b1 : b2">TRUE</span>' +
|
||||
'<ng-template #b1>FALSE1</ng-template>' +
|
||||
'<ng-template #b2>FALSE2</ng-template>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('TRUE');
|
||||
|
||||
getComponent().booleanCondition = false;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('FALSE1');
|
||||
|
||||
getComponent().nestedBooleanCondition = false;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('FALSE2');
|
||||
}));
|
||||
|
||||
it('should support binding to variable', async(() => {
|
||||
const template = '<span *ngIf="booleanCondition; else elseBlock; let v">{{v}}</span>' +
|
||||
'<ng-template #elseBlock let-v>{{v}}</ng-template>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('true');
|
||||
|
||||
getComponent().booleanCondition = false;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('false');
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Component({selector: 'test-cmp', template: ''})
|
||||
class TestComponent {
|
||||
booleanCondition: boolean = true;
|
||||
nestedBooleanCondition: boolean = true;
|
||||
numberCondition: number = 1;
|
||||
stringCondition: string = 'foo';
|
||||
functionCondition: Function = (s: any, n: any): boolean => s == 'foo' && n == 1;
|
||||
}
|
||||
|
||||
function createTestComponent(template: string): ComponentFixture<TestComponent> {
|
||||
return TestBed.overrideComponent(TestComponent, {set: {template: template}})
|
||||
.createComponent(TestComponent);
|
||||
}
|
162
packages/common/test/directives/ng_plural_spec.ts
Normal file
162
packages/common/test/directives/ng_plural_spec.ts
Normal file
@ -0,0 +1,162 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {CommonModule, NgLocalization} from '@angular/common';
|
||||
import {Component, Injectable} from '@angular/core';
|
||||
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
|
||||
import {expect} from '@angular/platform-browser/testing/matchers';
|
||||
|
||||
export function main() {
|
||||
describe('ngPlural', () => {
|
||||
let fixture: ComponentFixture<any>;
|
||||
|
||||
function getComponent(): TestComponent { return fixture.componentInstance; }
|
||||
|
||||
function detectChangesAndExpectText<T>(text: string): void {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText(text);
|
||||
}
|
||||
|
||||
afterEach(() => { fixture = null; });
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TestComponent],
|
||||
providers: [{provide: NgLocalization, useClass: TestLocalization}],
|
||||
imports: [CommonModule]
|
||||
});
|
||||
});
|
||||
|
||||
it('should display the template according to the exact value', async(() => {
|
||||
const template = '<ul [ngPlural]="switchValue">' +
|
||||
'<ng-template ngPluralCase="=0"><li>you have no messages.</li></ng-template>' +
|
||||
'<ng-template ngPluralCase="=1"><li>you have one message.</li></ng-template>' +
|
||||
'</ul>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().switchValue = 0;
|
||||
detectChangesAndExpectText('you have no messages.');
|
||||
|
||||
getComponent().switchValue = 1;
|
||||
detectChangesAndExpectText('you have one message.');
|
||||
}));
|
||||
|
||||
it('should display the template according to the exact numeric value', async(() => {
|
||||
const template = '<div>' +
|
||||
'<ul [ngPlural]="switchValue">' +
|
||||
'<ng-template ngPluralCase="0"><li>you have no messages.</li></ng-template>' +
|
||||
'<ng-template ngPluralCase="1"><li>you have one message.</li></ng-template>' +
|
||||
'</ul></div>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().switchValue = 0;
|
||||
detectChangesAndExpectText('you have no messages.');
|
||||
|
||||
getComponent().switchValue = 1;
|
||||
detectChangesAndExpectText('you have one message.');
|
||||
}));
|
||||
|
||||
// https://github.com/angular/angular/issues/9868
|
||||
// https://github.com/angular/angular/issues/9882
|
||||
it('should not throw when ngPluralCase contains expressions', async(() => {
|
||||
const template = '<ul [ngPlural]="switchValue">' +
|
||||
'<ng-template ngPluralCase="=0"><li>{{ switchValue }}</li></ng-template>' +
|
||||
'</ul>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().switchValue = 0;
|
||||
expect(() => fixture.detectChanges()).not.toThrow();
|
||||
}));
|
||||
|
||||
|
||||
it('should be applicable to <ng-container> elements', async(() => {
|
||||
const template = '<ng-container [ngPlural]="switchValue">' +
|
||||
'<ng-template ngPluralCase="=0">you have no messages.</ng-template>' +
|
||||
'<ng-template ngPluralCase="=1">you have one message.</ng-template>' +
|
||||
'</ng-container>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().switchValue = 0;
|
||||
detectChangesAndExpectText('you have no messages.');
|
||||
|
||||
getComponent().switchValue = 1;
|
||||
detectChangesAndExpectText('you have one message.');
|
||||
}));
|
||||
|
||||
it('should display the template according to the category', async(() => {
|
||||
const template = '<ul [ngPlural]="switchValue">' +
|
||||
'<ng-template ngPluralCase="few"><li>you have a few messages.</li></ng-template>' +
|
||||
'<ng-template ngPluralCase="many"><li>you have many messages.</li></ng-template>' +
|
||||
'</ul>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().switchValue = 2;
|
||||
detectChangesAndExpectText('you have a few messages.');
|
||||
|
||||
getComponent().switchValue = 8;
|
||||
detectChangesAndExpectText('you have many messages.');
|
||||
}));
|
||||
|
||||
it('should default to other when no matches are found', async(() => {
|
||||
const template = '<ul [ngPlural]="switchValue">' +
|
||||
'<ng-template ngPluralCase="few"><li>you have a few messages.</li></ng-template>' +
|
||||
'<ng-template ngPluralCase="other"><li>default message.</li></ng-template>' +
|
||||
'</ul>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().switchValue = 100;
|
||||
detectChangesAndExpectText('default message.');
|
||||
}));
|
||||
|
||||
it('should prioritize value matches over category matches', async(() => {
|
||||
const template = '<ul [ngPlural]="switchValue">' +
|
||||
'<ng-template ngPluralCase="few"><li>you have a few messages.</li></ng-template>' +
|
||||
'<ng-template ngPluralCase="=2">you have two messages.</ng-template>' +
|
||||
'</ul>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().switchValue = 2;
|
||||
detectChangesAndExpectText('you have two messages.');
|
||||
|
||||
getComponent().switchValue = 3;
|
||||
detectChangesAndExpectText('you have a few messages.');
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class TestLocalization extends NgLocalization {
|
||||
getPluralCategory(value: number): string {
|
||||
if (value > 1 && value < 4) {
|
||||
return 'few';
|
||||
}
|
||||
|
||||
if (value >= 4 && value < 10) {
|
||||
return 'many';
|
||||
}
|
||||
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'test-cmp', template: ''})
|
||||
class TestComponent {
|
||||
switchValue: number = null;
|
||||
}
|
||||
|
||||
function createTestComponent(template: string): ComponentFixture<TestComponent> {
|
||||
return TestBed.overrideComponent(TestComponent, {set: {template: template}})
|
||||
.createComponent(TestComponent);
|
||||
}
|
158
packages/common/test/directives/ng_style_spec.ts
Normal file
158
packages/common/test/directives/ng_style_spec.ts
Normal file
@ -0,0 +1,158 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {Component} from '@angular/core';
|
||||
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
|
||||
|
||||
export function main() {
|
||||
describe('NgStyle', () => {
|
||||
let fixture: ComponentFixture<any>;
|
||||
|
||||
function getComponent(): TestComponent { return fixture.componentInstance; }
|
||||
|
||||
function expectNativeEl(fixture: ComponentFixture<any>): any {
|
||||
return expect(fixture.debugElement.children[0].nativeElement);
|
||||
}
|
||||
|
||||
afterEach(() => { fixture = null; });
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({declarations: [TestComponent], imports: [CommonModule]});
|
||||
});
|
||||
|
||||
it('should add styles specified in an object literal', async(() => {
|
||||
const template = `<div [ngStyle]="{'max-width': '40px'}"></div>`;
|
||||
fixture = createTestComponent(template);
|
||||
fixture.detectChanges();
|
||||
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40px'});
|
||||
}));
|
||||
|
||||
it('should add and change styles specified in an object expression', async(() => {
|
||||
const template = `<div [ngStyle]="expr"></div>`;
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().expr = {'max-width': '40px'};
|
||||
fixture.detectChanges();
|
||||
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40px'});
|
||||
|
||||
let expr = getComponent().expr;
|
||||
expr['max-width'] = '30%';
|
||||
fixture.detectChanges();
|
||||
expectNativeEl(fixture).toHaveCssStyle({'max-width': '30%'});
|
||||
}));
|
||||
|
||||
it('should add and remove styles specified using style.unit notation', async(() => {
|
||||
const template = `<div [ngStyle]="{'max-width.px': expr}"></div>`;
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().expr = '40';
|
||||
fixture.detectChanges();
|
||||
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40px'});
|
||||
|
||||
getComponent().expr = null;
|
||||
fixture.detectChanges();
|
||||
expectNativeEl(fixture).not.toHaveCssStyle('max-width');
|
||||
}));
|
||||
|
||||
it('should update styles using style.unit notation when unit changes', async(() => {
|
||||
const template = `<div [ngStyle]="expr"></div>`;
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().expr = {'max-width.px': '40'};
|
||||
fixture.detectChanges();
|
||||
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40px'});
|
||||
|
||||
getComponent().expr = {'max-width.em': '40'};
|
||||
fixture.detectChanges();
|
||||
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40em'});
|
||||
}));
|
||||
|
||||
// keyValueDiffer is sensitive to key order #9115
|
||||
it('should change styles specified in an object expression', async(() => {
|
||||
const template = `<div [ngStyle]="expr"></div>`;
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().expr = {
|
||||
// height, width order is important here
|
||||
height: '10px',
|
||||
width: '10px'
|
||||
};
|
||||
|
||||
fixture.detectChanges();
|
||||
expectNativeEl(fixture).toHaveCssStyle({'height': '10px', 'width': '10px'});
|
||||
|
||||
getComponent().expr = {
|
||||
// width, height order is important here
|
||||
width: '5px',
|
||||
height: '5px',
|
||||
};
|
||||
|
||||
fixture.detectChanges();
|
||||
expectNativeEl(fixture).toHaveCssStyle({'height': '5px', 'width': '5px'});
|
||||
}));
|
||||
|
||||
it('should remove styles when deleting a key in an object expression', async(() => {
|
||||
const template = `<div [ngStyle]="expr"></div>`;
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().expr = {'max-width': '40px'};
|
||||
fixture.detectChanges();
|
||||
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40px'});
|
||||
|
||||
delete getComponent().expr['max-width'];
|
||||
fixture.detectChanges();
|
||||
expectNativeEl(fixture).not.toHaveCssStyle('max-width');
|
||||
}));
|
||||
|
||||
it('should co-operate with the style attribute', async(() => {
|
||||
const template = `<div style="font-size: 12px" [ngStyle]="expr"></div>`;
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().expr = {'max-width': '40px'};
|
||||
fixture.detectChanges();
|
||||
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40px', 'font-size': '12px'});
|
||||
|
||||
delete getComponent().expr['max-width'];
|
||||
fixture.detectChanges();
|
||||
expectNativeEl(fixture).not.toHaveCssStyle('max-width');
|
||||
expectNativeEl(fixture).toHaveCssStyle({'font-size': '12px'});
|
||||
}));
|
||||
|
||||
it('should co-operate with the style.[styleName]="expr" special-case in the compiler',
|
||||
async(() => {
|
||||
const template = `<div [style.font-size.px]="12" [ngStyle]="expr"></div>`;
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().expr = {'max-width': '40px'};
|
||||
fixture.detectChanges();
|
||||
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40px', 'font-size': '12px'});
|
||||
|
||||
delete getComponent().expr['max-width'];
|
||||
fixture.detectChanges();
|
||||
expectNativeEl(fixture).not.toHaveCssStyle('max-width');
|
||||
expectNativeEl(fixture).toHaveCssStyle({'font-size': '12px'});
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@Component({selector: 'test-cmp', template: ''})
|
||||
class TestComponent {
|
||||
expr: any;
|
||||
}
|
||||
|
||||
function createTestComponent(template: string): ComponentFixture<TestComponent> {
|
||||
return TestBed.overrideComponent(TestComponent, {set: {template: template}})
|
||||
.createComponent(TestComponent);
|
||||
}
|
190
packages/common/test/directives/ng_switch_spec.ts
Normal file
190
packages/common/test/directives/ng_switch_spec.ts
Normal file
@ -0,0 +1,190 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {Attribute, Component, Directive} from '@angular/core';
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {expect} from '@angular/platform-browser/testing/matchers';
|
||||
|
||||
export function main() {
|
||||
describe('NgSwitch', () => {
|
||||
let fixture: ComponentFixture<any>;
|
||||
|
||||
function getComponent(): TestComponent { return fixture.componentInstance; }
|
||||
|
||||
function detectChangesAndExpectText(text: string): void {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText(text);
|
||||
}
|
||||
|
||||
afterEach(() => { fixture = null; });
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TestComponent],
|
||||
imports: [CommonModule],
|
||||
});
|
||||
});
|
||||
|
||||
describe('switch value changes', () => {
|
||||
it('should switch amongst when values', () => {
|
||||
const template = '<ul [ngSwitch]="switchValue">' +
|
||||
'<li *ngSwitchCase="\'a\'">when a</li>' +
|
||||
'<li *ngSwitchCase="\'b\'">when b</li>' +
|
||||
'</ul>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
detectChangesAndExpectText('');
|
||||
|
||||
getComponent().switchValue = 'a';
|
||||
detectChangesAndExpectText('when a');
|
||||
|
||||
getComponent().switchValue = 'b';
|
||||
detectChangesAndExpectText('when b');
|
||||
});
|
||||
|
||||
it('should switch amongst when values with fallback to default', () => {
|
||||
const template = '<ul [ngSwitch]="switchValue">' +
|
||||
'<li *ngSwitchCase="\'a\'">when a</li>' +
|
||||
'<li *ngSwitchDefault>when default</li>' +
|
||||
'</ul>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
detectChangesAndExpectText('when default');
|
||||
|
||||
getComponent().switchValue = 'a';
|
||||
detectChangesAndExpectText('when a');
|
||||
|
||||
getComponent().switchValue = 'b';
|
||||
detectChangesAndExpectText('when default');
|
||||
|
||||
getComponent().switchValue = 'c';
|
||||
detectChangesAndExpectText('when default');
|
||||
});
|
||||
|
||||
it('should support multiple whens with the same value', () => {
|
||||
const template = '<ul [ngSwitch]="switchValue">' +
|
||||
'<li *ngSwitchCase="\'a\'">when a1;</li>' +
|
||||
'<li *ngSwitchCase="\'b\'">when b1;</li>' +
|
||||
'<li *ngSwitchCase="\'a\'">when a2;</li>' +
|
||||
'<li *ngSwitchCase="\'b\'">when b2;</li>' +
|
||||
'<li *ngSwitchDefault>when default1;</li>' +
|
||||
'<li *ngSwitchDefault>when default2;</li>' +
|
||||
'</ul>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
detectChangesAndExpectText('when default1;when default2;');
|
||||
|
||||
getComponent().switchValue = 'a';
|
||||
detectChangesAndExpectText('when a1;when a2;');
|
||||
|
||||
getComponent().switchValue = 'b';
|
||||
detectChangesAndExpectText('when b1;when b2;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when values changes', () => {
|
||||
it('should switch amongst when values', () => {
|
||||
const template = '<ul [ngSwitch]="switchValue">' +
|
||||
'<li *ngSwitchCase="when1">when 1;</li>' +
|
||||
'<li *ngSwitchCase="when2">when 2;</li>' +
|
||||
'<li *ngSwitchDefault>when default;</li>' +
|
||||
'</ul>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
getComponent().when1 = 'a';
|
||||
getComponent().when2 = 'b';
|
||||
getComponent().switchValue = 'a';
|
||||
detectChangesAndExpectText('when 1;');
|
||||
|
||||
getComponent().switchValue = 'b';
|
||||
detectChangesAndExpectText('when 2;');
|
||||
|
||||
getComponent().switchValue = 'c';
|
||||
detectChangesAndExpectText('when default;');
|
||||
|
||||
getComponent().when1 = 'c';
|
||||
detectChangesAndExpectText('when 1;');
|
||||
|
||||
getComponent().when1 = 'd';
|
||||
detectChangesAndExpectText('when default;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('corner cases', () => {
|
||||
|
||||
it('should not create the default case if another case matches', () => {
|
||||
const log: string[] = [];
|
||||
|
||||
@Directive({selector: '[test]'})
|
||||
class TestDirective {
|
||||
constructor(@Attribute('test') test: string) { log.push(test); }
|
||||
}
|
||||
|
||||
const template = '<div [ngSwitch]="switchValue">' +
|
||||
'<div *ngSwitchCase="\'a\'" test="aCase"></div>' +
|
||||
'<div *ngSwitchDefault test="defaultCase"></div>' +
|
||||
'</div>';
|
||||
|
||||
TestBed.configureTestingModule({declarations: [TestDirective]});
|
||||
TestBed.overrideComponent(TestComponent, {set: {template: template}})
|
||||
.createComponent(TestComponent);
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
fixture.componentInstance.switchValue = 'a';
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(log).toEqual(['aCase']);
|
||||
});
|
||||
|
||||
it('should create the default case if there is no other case', () => {
|
||||
const template = '<ul [ngSwitch]="switchValue">' +
|
||||
'<li *ngSwitchDefault>when default1;</li>' +
|
||||
'<li *ngSwitchDefault>when default2;</li>' +
|
||||
'</ul>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
detectChangesAndExpectText('when default1;when default2;');
|
||||
|
||||
});
|
||||
|
||||
it('should allow defaults before cases', () => {
|
||||
const template = '<ul [ngSwitch]="switchValue">' +
|
||||
'<li *ngSwitchDefault>when default1;</li>' +
|
||||
'<li *ngSwitchDefault>when default2;</li>' +
|
||||
'<li *ngSwitchCase="\'a\'">when a1;</li>' +
|
||||
'<li *ngSwitchCase="\'b\'">when b1;</li>' +
|
||||
'<li *ngSwitchCase="\'a\'">when a2;</li>' +
|
||||
'<li *ngSwitchCase="\'b\'">when b2;</li>' +
|
||||
'</ul>';
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
detectChangesAndExpectText('when default1;when default2;');
|
||||
|
||||
getComponent().switchValue = 'a';
|
||||
detectChangesAndExpectText('when a1;when a2;');
|
||||
|
||||
getComponent().switchValue = 'b';
|
||||
detectChangesAndExpectText('when b1;when b2;');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Component({selector: 'test-cmp', template: ''})
|
||||
class TestComponent {
|
||||
switchValue: any = null;
|
||||
when1: any = null;
|
||||
when2: any = null;
|
||||
}
|
||||
|
||||
function createTestComponent(template: string): ComponentFixture<TestComponent> {
|
||||
return TestBed.overrideComponent(TestComponent, {set: {template: template}})
|
||||
.createComponent(TestComponent);
|
||||
}
|
146
packages/common/test/directives/ng_template_outlet_spec.ts
Normal file
146
packages/common/test/directives/ng_template_outlet_spec.ts
Normal file
@ -0,0 +1,146 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {Component, ContentChildren, Directive, NO_ERRORS_SCHEMA, QueryList, TemplateRef} from '@angular/core';
|
||||
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
|
||||
import {expect} from '@angular/platform-browser/testing/matchers';
|
||||
|
||||
export function main() {
|
||||
describe('NgTemplateOutlet', () => {
|
||||
let fixture: ComponentFixture<any>;
|
||||
|
||||
function setTplRef(value: any): void { fixture.componentInstance.currentTplRef = value; }
|
||||
|
||||
function detectChangesAndExpectText(text: string): void {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText(text);
|
||||
}
|
||||
|
||||
afterEach(() => { fixture = null; });
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
TestComponent,
|
||||
CaptureTplRefs,
|
||||
],
|
||||
imports: [CommonModule],
|
||||
});
|
||||
});
|
||||
|
||||
// https://github.com/angular/angular/issues/14778
|
||||
it('should accept the component as the context', async(() => {
|
||||
const template = `<ng-container *ngTemplateOutlet="tpl; context: this"></ng-container>` +
|
||||
`<ng-template #tpl>{{context.foo}}</ng-template>`;
|
||||
|
||||
fixture = createTestComponent(template);
|
||||
detectChangesAndExpectText('bar');
|
||||
}));
|
||||
|
||||
it('should do nothing if templateRef is `null`', async(() => {
|
||||
const template = `<ng-container [ngTemplateOutlet]="null"></ng-container>`;
|
||||
fixture = createTestComponent(template);
|
||||
detectChangesAndExpectText('');
|
||||
}));
|
||||
|
||||
it('should insert content specified by TemplateRef', async(() => {
|
||||
const template = `<ng-template #tpl>foo</ng-template>` +
|
||||
`<ng-container [ngTemplateOutlet]="tpl"></ng-container>`;
|
||||
fixture = createTestComponent(template);
|
||||
detectChangesAndExpectText('foo');
|
||||
}));
|
||||
|
||||
it('should clear content if TemplateRef becomes `null`', async(() => {
|
||||
const template = `<tpl-refs #refs="tplRefs"><ng-template>foo</ng-template></tpl-refs>` +
|
||||
`<ng-container [ngTemplateOutlet]="currentTplRef"></ng-container>`;
|
||||
fixture = createTestComponent(template);
|
||||
fixture.detectChanges();
|
||||
const refs = fixture.debugElement.children[0].references['refs'];
|
||||
|
||||
setTplRef(refs.tplRefs.first);
|
||||
detectChangesAndExpectText('foo');
|
||||
|
||||
setTplRef(null);
|
||||
detectChangesAndExpectText('');
|
||||
}));
|
||||
|
||||
it('should swap content if TemplateRef changes', async(() => {
|
||||
const template =
|
||||
`<tpl-refs #refs="tplRefs"><ng-template>foo</ng-template><ng-template>bar</ng-template></tpl-refs>` +
|
||||
`<ng-container [ngTemplateOutlet]="currentTplRef"></ng-container>`;
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
fixture.detectChanges();
|
||||
const refs = fixture.debugElement.children[0].references['refs'];
|
||||
|
||||
setTplRef(refs.tplRefs.first);
|
||||
detectChangesAndExpectText('foo');
|
||||
|
||||
setTplRef(refs.tplRefs.last);
|
||||
detectChangesAndExpectText('bar');
|
||||
}));
|
||||
|
||||
it('should display template if context is `null`', async(() => {
|
||||
const template = `<ng-template #tpl>foo</ng-template>` +
|
||||
`<ng-container *ngTemplateOutlet="tpl; context: null"></ng-container>`;
|
||||
fixture = createTestComponent(template);
|
||||
detectChangesAndExpectText('foo');
|
||||
}));
|
||||
|
||||
it('should reflect initial context and changes', async(() => {
|
||||
const template = `<ng-template let-foo="foo" #tpl>{{foo}}</ng-template>` +
|
||||
`<ng-container *ngTemplateOutlet="tpl; context: context"></ng-container>`;
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
fixture.detectChanges();
|
||||
detectChangesAndExpectText('bar');
|
||||
|
||||
fixture.componentInstance.context.foo = 'alter-bar';
|
||||
detectChangesAndExpectText('alter-bar');
|
||||
}));
|
||||
|
||||
it('should reflect user defined `$implicit` property in the context', async(() => {
|
||||
const template = `<ng-template let-ctx #tpl>{{ctx.foo}}</ng-template>` +
|
||||
`<ng-container *ngTemplateOutlet="tpl; context: context"></ng-container>`;
|
||||
fixture = createTestComponent(template);
|
||||
fixture.componentInstance.context = {$implicit: {foo: 'bra'}};
|
||||
detectChangesAndExpectText('bra');
|
||||
}));
|
||||
|
||||
it('should reflect context re-binding', async(() => {
|
||||
const template =
|
||||
`<ng-template let-shawshank="shawshank" #tpl>{{shawshank}}</ng-template>` +
|
||||
`<ng-container *ngTemplateOutlet="tpl; context: context"></ng-container>`;
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
fixture.componentInstance.context = {shawshank: 'brooks'};
|
||||
detectChangesAndExpectText('brooks');
|
||||
|
||||
fixture.componentInstance.context = {shawshank: 'was here'};
|
||||
detectChangesAndExpectText('was here');
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@Directive({selector: 'tpl-refs', exportAs: 'tplRefs'})
|
||||
class CaptureTplRefs {
|
||||
@ContentChildren(TemplateRef) tplRefs: QueryList<TemplateRef<any>>;
|
||||
}
|
||||
|
||||
@Component({selector: 'test-cmp', template: ''})
|
||||
class TestComponent {
|
||||
currentTplRef: TemplateRef<any>;
|
||||
context: any = {foo: 'bar'};
|
||||
}
|
||||
|
||||
function createTestComponent(template: string): ComponentFixture<TestComponent> {
|
||||
return TestBed.overrideComponent(TestComponent, {set: {template: template}})
|
||||
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]})
|
||||
.createComponent(TestComponent);
|
||||
}
|
67
packages/common/test/directives/non_bindable_spec.ts
Normal file
67
packages/common/test/directives/non_bindable_spec.ts
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Component, Directive} from '@angular/core';
|
||||
import {ElementRef} from '@angular/core/src/linker/element_ref';
|
||||
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
|
||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
import {expect} from '@angular/platform-browser/testing/matchers';
|
||||
|
||||
export function main() {
|
||||
describe('non-bindable', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TestComponent, TestDirective],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not interpolate children', async(() => {
|
||||
const template = '<div>{{text}}<span ngNonBindable>{{text}}</span></div>';
|
||||
const fixture = createTestComponent(template);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('foo{{text}}');
|
||||
}));
|
||||
|
||||
it('should ignore directives on child nodes', async(() => {
|
||||
const template = '<div ngNonBindable><span id=child test-dec>{{text}}</span></div>';
|
||||
const fixture = createTestComponent(template);
|
||||
fixture.detectChanges();
|
||||
|
||||
// We must use getDOM().querySelector instead of fixture.query here
|
||||
// since the elements inside are not compiled.
|
||||
const span = getDOM().querySelector(fixture.nativeElement, '#child');
|
||||
expect(getDOM().hasClass(span, 'compiled')).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should trigger directives on the same node', async(() => {
|
||||
const template = '<div><span id=child ngNonBindable test-dec>{{text}}</span></div>';
|
||||
const fixture = createTestComponent(template);
|
||||
fixture.detectChanges();
|
||||
const span = getDOM().querySelector(fixture.nativeElement, '#child');
|
||||
expect(getDOM().hasClass(span, 'compiled')).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@Directive({selector: '[test-dec]'})
|
||||
class TestDirective {
|
||||
constructor(el: ElementRef) { getDOM().addClass(el.nativeElement, 'compiled'); }
|
||||
}
|
||||
|
||||
@Component({selector: 'test-cmp', template: ''})
|
||||
class TestComponent {
|
||||
text: string;
|
||||
constructor() { this.text = 'foo'; }
|
||||
}
|
||||
|
||||
function createTestComponent(template: string): ComponentFixture<TestComponent> {
|
||||
return TestBed.overrideComponent(TestComponent, {set: {template: template}})
|
||||
.createComponent(TestComponent);
|
||||
}
|
160
packages/common/test/localization_spec.ts
Normal file
160
packages/common/test/localization_spec.ts
Normal file
@ -0,0 +1,160 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {LOCALE_ID} from '@angular/core';
|
||||
import {TestBed, inject} from '@angular/core/testing';
|
||||
|
||||
import {NgLocaleLocalization, NgLocalization, getPluralCategory} from '../src/localization';
|
||||
|
||||
export function main() {
|
||||
describe('l10n', () => {
|
||||
|
||||
describe('NgLocalization', () => {
|
||||
describe('ro', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{provide: LOCALE_ID, useValue: 'ro'}],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return plural cases for the provided locale',
|
||||
inject([NgLocalization], (l10n: NgLocalization) => {
|
||||
expect(l10n.getPluralCategory(0)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(1)).toEqual('one');
|
||||
expect(l10n.getPluralCategory(1212)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(1223)).toEqual('other');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('sr', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{provide: LOCALE_ID, useValue: 'sr'}],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return plural cases for the provided locale',
|
||||
inject([NgLocalization], (l10n: NgLocalization) => {
|
||||
expect(l10n.getPluralCategory(1)).toEqual('one');
|
||||
expect(l10n.getPluralCategory(2.1)).toEqual('one');
|
||||
|
||||
expect(l10n.getPluralCategory(3)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(0.2)).toEqual('few');
|
||||
|
||||
expect(l10n.getPluralCategory(2.11)).toEqual('other');
|
||||
expect(l10n.getPluralCategory(2.12)).toEqual('other');
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('NgLocaleLocalization', () => {
|
||||
it('should return the correct values for the "en" locale', () => {
|
||||
const l10n = new NgLocaleLocalization('en-US');
|
||||
|
||||
expect(l10n.getPluralCategory(0)).toEqual('other');
|
||||
expect(l10n.getPluralCategory(1)).toEqual('one');
|
||||
expect(l10n.getPluralCategory(2)).toEqual('other');
|
||||
});
|
||||
|
||||
it('should return the correct values for the "ro" locale', () => {
|
||||
const l10n = new NgLocaleLocalization('ro');
|
||||
|
||||
expect(l10n.getPluralCategory(0)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(1)).toEqual('one');
|
||||
expect(l10n.getPluralCategory(2)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(12)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(23)).toEqual('other');
|
||||
expect(l10n.getPluralCategory(1212)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(1223)).toEqual('other');
|
||||
});
|
||||
|
||||
it('should return the correct values for the "sr" locale', () => {
|
||||
const l10n = new NgLocaleLocalization('sr');
|
||||
|
||||
expect(l10n.getPluralCategory(1)).toEqual('one');
|
||||
expect(l10n.getPluralCategory(31)).toEqual('one');
|
||||
expect(l10n.getPluralCategory(0.1)).toEqual('one');
|
||||
expect(l10n.getPluralCategory(1.1)).toEqual('one');
|
||||
expect(l10n.getPluralCategory(2.1)).toEqual('one');
|
||||
|
||||
expect(l10n.getPluralCategory(3)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(33)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(0.2)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(0.3)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(0.4)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(2.2)).toEqual('few');
|
||||
|
||||
expect(l10n.getPluralCategory(2.11)).toEqual('other');
|
||||
expect(l10n.getPluralCategory(2.12)).toEqual('other');
|
||||
expect(l10n.getPluralCategory(2.13)).toEqual('other');
|
||||
expect(l10n.getPluralCategory(2.14)).toEqual('other');
|
||||
expect(l10n.getPluralCategory(2.15)).toEqual('other');
|
||||
|
||||
expect(l10n.getPluralCategory(0)).toEqual('other');
|
||||
expect(l10n.getPluralCategory(5)).toEqual('other');
|
||||
expect(l10n.getPluralCategory(10)).toEqual('other');
|
||||
expect(l10n.getPluralCategory(35)).toEqual('other');
|
||||
expect(l10n.getPluralCategory(37)).toEqual('other');
|
||||
expect(l10n.getPluralCategory(40)).toEqual('other');
|
||||
expect(l10n.getPluralCategory(0.0)).toEqual('other');
|
||||
expect(l10n.getPluralCategory(0.5)).toEqual('other');
|
||||
expect(l10n.getPluralCategory(0.6)).toEqual('other');
|
||||
|
||||
expect(l10n.getPluralCategory(2)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(2.1)).toEqual('one');
|
||||
expect(l10n.getPluralCategory(2.2)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(2.3)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(2.4)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(2.5)).toEqual('other');
|
||||
|
||||
expect(l10n.getPluralCategory(20)).toEqual('other');
|
||||
expect(l10n.getPluralCategory(21)).toEqual('one');
|
||||
expect(l10n.getPluralCategory(22)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(23)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(24)).toEqual('few');
|
||||
expect(l10n.getPluralCategory(25)).toEqual('other');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPluralCategory', () => {
|
||||
it('should return plural category', () => {
|
||||
const l10n = new NgLocaleLocalization('fr');
|
||||
|
||||
expect(getPluralCategory(0, ['one', 'other'], l10n)).toEqual('one');
|
||||
expect(getPluralCategory(1, ['one', 'other'], l10n)).toEqual('one');
|
||||
expect(getPluralCategory(5, ['one', 'other'], l10n)).toEqual('other');
|
||||
});
|
||||
|
||||
it('should return discrete cases', () => {
|
||||
const l10n = new NgLocaleLocalization('fr');
|
||||
|
||||
expect(getPluralCategory(0, ['one', 'other', '=0'], l10n)).toEqual('=0');
|
||||
expect(getPluralCategory(1, ['one', 'other'], l10n)).toEqual('one');
|
||||
expect(getPluralCategory(5, ['one', 'other', '=5'], l10n)).toEqual('=5');
|
||||
expect(getPluralCategory(6, ['one', 'other', '=5'], l10n)).toEqual('other');
|
||||
});
|
||||
|
||||
it('should fallback to other when the case is not present', () => {
|
||||
const l10n = new NgLocaleLocalization('ro');
|
||||
expect(getPluralCategory(1, ['one', 'other'], l10n)).toEqual('one');
|
||||
// 2 -> 'few'
|
||||
expect(getPluralCategory(2, ['one', 'other'], l10n)).toEqual('other');
|
||||
});
|
||||
|
||||
describe('errors', () => {
|
||||
it('should report an error when the "other" category is not present', () => {
|
||||
expect(() => {
|
||||
const l10n = new NgLocaleLocalization('ro');
|
||||
// 2 -> 'few'
|
||||
getPluralCategory(2, ['one'], l10n);
|
||||
}).toThrowError('No plural message found for value "2"');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
216
packages/common/test/pipes/async_pipe_spec.ts
Normal file
216
packages/common/test/pipes/async_pipe_spec.ts
Normal file
@ -0,0 +1,216 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {AsyncPipe} from '@angular/common';
|
||||
import {EventEmitter, WrappedValue} from '@angular/core';
|
||||
import {AsyncTestCompleter, beforeEach, describe, expect, inject, it} from '@angular/core/testing/testing_internal';
|
||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
import {browserDetection} from '@angular/platform-browser/testing/browser_util';
|
||||
|
||||
import {SpyChangeDetectorRef} from '../spies';
|
||||
|
||||
export function main() {
|
||||
describe('AsyncPipe', () => {
|
||||
|
||||
describe('Observable', () => {
|
||||
let emitter: EventEmitter<any>;
|
||||
let pipe: AsyncPipe;
|
||||
let ref: any;
|
||||
const message = {};
|
||||
|
||||
beforeEach(() => {
|
||||
emitter = new EventEmitter();
|
||||
ref = new SpyChangeDetectorRef();
|
||||
pipe = new AsyncPipe(ref);
|
||||
});
|
||||
|
||||
describe('transform', () => {
|
||||
it('should return null when subscribing to an observable',
|
||||
() => { expect(pipe.transform(emitter)).toBe(null); });
|
||||
|
||||
it('should return the latest available value wrapped',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
pipe.transform(emitter);
|
||||
emitter.emit(message);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(pipe.transform(emitter)).toEqual(new WrappedValue(message));
|
||||
async.done();
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
|
||||
it('should return same value when nothing has changed since the last call',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
pipe.transform(emitter);
|
||||
emitter.emit(message);
|
||||
|
||||
setTimeout(() => {
|
||||
pipe.transform(emitter);
|
||||
expect(pipe.transform(emitter)).toBe(message);
|
||||
async.done();
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
it('should dispose of the existing subscription when subscribing to a new observable',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
pipe.transform(emitter);
|
||||
|
||||
const newEmitter = new EventEmitter();
|
||||
expect(pipe.transform(newEmitter)).toBe(null);
|
||||
emitter.emit(message);
|
||||
|
||||
// this should not affect the pipe
|
||||
setTimeout(() => {
|
||||
expect(pipe.transform(newEmitter)).toBe(null);
|
||||
async.done();
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
it('should request a change detection check upon receiving a new value',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
pipe.transform(emitter);
|
||||
emitter.emit(message);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(ref.spy('markForCheck')).toHaveBeenCalled();
|
||||
async.done();
|
||||
}, 10);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('ngOnDestroy', () => {
|
||||
it('should do nothing when no subscription',
|
||||
() => { expect(() => pipe.ngOnDestroy()).not.toThrow(); });
|
||||
|
||||
it('should dispose of the existing subscription',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
pipe.transform(emitter);
|
||||
pipe.ngOnDestroy();
|
||||
emitter.emit(message);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(pipe.transform(emitter)).toBe(null);
|
||||
async.done();
|
||||
}, 0);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Promise', () => {
|
||||
const message = new Object();
|
||||
let pipe: AsyncPipe;
|
||||
let resolve: (result: any) => void;
|
||||
let reject: (error: any) => void;
|
||||
let promise: Promise<any>;
|
||||
let ref: SpyChangeDetectorRef;
|
||||
// adds longer timers for passing tests in IE
|
||||
const timer = (getDOM() && browserDetection.isIE) ? 50 : 10;
|
||||
|
||||
beforeEach(() => {
|
||||
promise = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
ref = new SpyChangeDetectorRef();
|
||||
pipe = new AsyncPipe(<any>ref);
|
||||
});
|
||||
|
||||
describe('transform', () => {
|
||||
it('should return null when subscribing to a promise',
|
||||
() => { expect(pipe.transform(promise)).toBe(null); });
|
||||
|
||||
it('should return the latest available value',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
pipe.transform(promise);
|
||||
|
||||
resolve(message);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(pipe.transform(promise)).toEqual(new WrappedValue(message));
|
||||
async.done();
|
||||
}, timer);
|
||||
}));
|
||||
|
||||
it('should return unwrapped value when nothing has changed since the last call',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
pipe.transform(promise);
|
||||
resolve(message);
|
||||
|
||||
setTimeout(() => {
|
||||
pipe.transform(promise);
|
||||
expect(pipe.transform(promise)).toBe(message);
|
||||
async.done();
|
||||
}, timer);
|
||||
}));
|
||||
|
||||
it('should dispose of the existing subscription when subscribing to a new promise',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
pipe.transform(promise);
|
||||
|
||||
promise = new Promise<any>(() => {});
|
||||
expect(pipe.transform(promise)).toBe(null);
|
||||
|
||||
// this should not affect the pipe, so it should return WrappedValue
|
||||
resolve(message);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(pipe.transform(promise)).toBe(null);
|
||||
async.done();
|
||||
}, timer);
|
||||
}));
|
||||
|
||||
it('should request a change detection check upon receiving a new value',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
const markForCheck = ref.spy('markForCheck');
|
||||
pipe.transform(promise);
|
||||
resolve(message);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(markForCheck).toHaveBeenCalled();
|
||||
async.done();
|
||||
}, timer);
|
||||
}));
|
||||
|
||||
describe('ngOnDestroy', () => {
|
||||
it('should do nothing when no source',
|
||||
() => { expect(() => pipe.ngOnDestroy()).not.toThrow(); });
|
||||
|
||||
it('should dispose of the existing source',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
pipe.transform(promise);
|
||||
expect(pipe.transform(promise)).toBe(null);
|
||||
resolve(message);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
expect(pipe.transform(promise)).toEqual(new WrappedValue(message));
|
||||
pipe.ngOnDestroy();
|
||||
expect(pipe.transform(promise)).toBe(null);
|
||||
async.done();
|
||||
}, timer);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('null', () => {
|
||||
it('should return null when given null', () => {
|
||||
const pipe = new AsyncPipe(null);
|
||||
expect(pipe.transform(null)).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('other types', () => {
|
||||
it('should throw when given an invalid object', () => {
|
||||
const pipe = new AsyncPipe(null);
|
||||
expect(() => pipe.transform(<any>'some bogus object')).toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
67
packages/common/test/pipes/case_conversion_pipes_spec.ts
Normal file
67
packages/common/test/pipes/case_conversion_pipes_spec.ts
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {LowerCasePipe, TitleCasePipe, UpperCasePipe} from '@angular/common';
|
||||
|
||||
export function main() {
|
||||
describe('LowerCasePipe', () => {
|
||||
let pipe: LowerCasePipe;
|
||||
|
||||
beforeEach(() => { pipe = new LowerCasePipe(); });
|
||||
|
||||
it('should return lowercase', () => { expect(pipe.transform('FOO')).toEqual('foo'); });
|
||||
|
||||
it('should lowercase when there is a new value', () => {
|
||||
expect(pipe.transform('FOO')).toEqual('foo');
|
||||
expect(pipe.transform('BAr')).toEqual('bar');
|
||||
});
|
||||
|
||||
it('should not support other objects',
|
||||
() => { expect(() => pipe.transform(<any>{})).toThrowError(); });
|
||||
});
|
||||
|
||||
describe('TitleCasePipe', () => {
|
||||
let pipe: TitleCasePipe;
|
||||
|
||||
beforeEach(() => { pipe = new TitleCasePipe(); });
|
||||
|
||||
it('should return titlecase', () => { expect(pipe.transform('foo')).toEqual('Foo'); });
|
||||
|
||||
it('should return titlecase for subsequent words',
|
||||
() => { expect(pipe.transform('one TWO Three fouR')).toEqual('One Two Three Four'); });
|
||||
|
||||
it('should support empty strings', () => { expect(pipe.transform('')).toEqual(''); });
|
||||
|
||||
it('should persist whitespace',
|
||||
() => { expect(pipe.transform('one two')).toEqual('One Two'); });
|
||||
|
||||
it('should titlecase when there is a new value', () => {
|
||||
expect(pipe.transform('bar')).toEqual('Bar');
|
||||
expect(pipe.transform('foo')).toEqual('Foo');
|
||||
});
|
||||
|
||||
it('should not support other objects',
|
||||
() => { expect(() => pipe.transform(<any>{})).toThrowError(); });
|
||||
});
|
||||
|
||||
describe('UpperCasePipe', () => {
|
||||
let pipe: UpperCasePipe;
|
||||
|
||||
beforeEach(() => { pipe = new UpperCasePipe(); });
|
||||
|
||||
it('should return uppercase', () => { expect(pipe.transform('foo')).toEqual('FOO'); });
|
||||
|
||||
it('should uppercase when there is a new value', () => {
|
||||
expect(pipe.transform('foo')).toEqual('FOO');
|
||||
expect(pipe.transform('bar')).toEqual('BAR');
|
||||
});
|
||||
|
||||
it('should not support other objects',
|
||||
() => { expect(() => pipe.transform(<any>{})).toThrowError(); });
|
||||
});
|
||||
}
|
203
packages/common/test/pipes/date_pipe_spec.ts
Normal file
203
packages/common/test/pipes/date_pipe_spec.ts
Normal file
@ -0,0 +1,203 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {DatePipe} from '@angular/common';
|
||||
import {PipeResolver} from '@angular/compiler/src/pipe_resolver';
|
||||
import {browserDetection} from '@angular/platform-browser/testing/browser_util';
|
||||
|
||||
export function main() {
|
||||
describe('DatePipe', () => {
|
||||
let date: Date;
|
||||
const isoStringWithoutTime = '2015-01-01';
|
||||
let pipe: DatePipe;
|
||||
|
||||
// Check the transformation of a date into a pattern
|
||||
function expectDateFormatAs(date: Date | string, pattern: any, output: string): void {
|
||||
expect(pipe.transform(date, pattern)).toEqual(output);
|
||||
}
|
||||
|
||||
// TODO: reactivate the disabled expectations once emulators are fixed in SauceLabs
|
||||
// In some old versions of Chrome in Android emulators, time formatting returns dates in the
|
||||
// timezone of the VM host,
|
||||
// instead of the device timezone. Same symptoms as
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=406382
|
||||
// This happens locally and in SauceLabs, so some checks are disabled to avoid failures.
|
||||
// Tracking issue: https://github.com/angular/angular/issues/11187
|
||||
|
||||
beforeEach(() => {
|
||||
date = new Date(2015, 5, 15, 9, 3, 1);
|
||||
pipe = new DatePipe('en-US');
|
||||
});
|
||||
|
||||
it('should be marked as pure',
|
||||
() => { expect(new PipeResolver().resolve(DatePipe).pure).toEqual(true); });
|
||||
|
||||
describe('supports', () => {
|
||||
it('should support date', () => { expect(() => pipe.transform(date)).not.toThrow(); });
|
||||
|
||||
it('should support int', () => { expect(() => pipe.transform(123456789)).not.toThrow(); });
|
||||
|
||||
it('should support numeric strings',
|
||||
() => { expect(() => pipe.transform('123456789')).not.toThrow(); });
|
||||
|
||||
it('should support decimal strings',
|
||||
() => { expect(() => pipe.transform('123456789.11')).not.toThrow(); });
|
||||
|
||||
it('should support ISO string',
|
||||
() => expect(() => pipe.transform('2015-06-15T21:43:11Z')).not.toThrow());
|
||||
|
||||
it('should return null for empty string', () => expect(pipe.transform('')).toEqual(null));
|
||||
|
||||
it('should return null for NaN', () => expect(pipe.transform(Number.NaN)).toEqual(null));
|
||||
|
||||
it('should support ISO string without time',
|
||||
() => { expect(() => pipe.transform(isoStringWithoutTime)).not.toThrow(); });
|
||||
|
||||
it('should not support other objects',
|
||||
() => expect(() => pipe.transform({})).toThrowError(/InvalidPipeArgument/));
|
||||
});
|
||||
|
||||
describe('transform', () => {
|
||||
it('should format each component correctly', () => {
|
||||
const dateFixtures: any = {
|
||||
'y': '2015',
|
||||
'yy': '15',
|
||||
'M': '6',
|
||||
'MM': '06',
|
||||
'MMM': 'Jun',
|
||||
'MMMM': 'June',
|
||||
'd': '15',
|
||||
'dd': '15',
|
||||
'EEE': 'Mon',
|
||||
'EEEE': 'Monday'
|
||||
};
|
||||
|
||||
const isoStringWithoutTimeFixtures: any = {
|
||||
'y': '2015',
|
||||
'yy': '15',
|
||||
'M': '1',
|
||||
'MM': '01',
|
||||
'MMM': 'Jan',
|
||||
'MMMM': 'January',
|
||||
'd': '1',
|
||||
'dd': '01',
|
||||
'EEE': 'Thu',
|
||||
'EEEE': 'Thursday'
|
||||
};
|
||||
|
||||
if (!browserDetection.isOldChrome) {
|
||||
dateFixtures['h'] = '9';
|
||||
dateFixtures['hh'] = '09';
|
||||
dateFixtures['j'] = '9 AM';
|
||||
isoStringWithoutTimeFixtures['h'] = '12';
|
||||
isoStringWithoutTimeFixtures['hh'] = '12';
|
||||
isoStringWithoutTimeFixtures['j'] = '12 AM';
|
||||
}
|
||||
|
||||
// IE and Edge can't format a date to minutes and seconds without hours
|
||||
if (!browserDetection.isEdge && !browserDetection.isIE ||
|
||||
!browserDetection.supportsNativeIntlApi) {
|
||||
if (!browserDetection.isOldChrome) {
|
||||
dateFixtures['HH'] = '09';
|
||||
isoStringWithoutTimeFixtures['HH'] = '00';
|
||||
}
|
||||
dateFixtures['E'] = 'M';
|
||||
dateFixtures['L'] = 'J';
|
||||
dateFixtures['m'] = '3';
|
||||
dateFixtures['s'] = '1';
|
||||
dateFixtures['mm'] = '03';
|
||||
dateFixtures['ss'] = '01';
|
||||
isoStringWithoutTimeFixtures['m'] = '0';
|
||||
isoStringWithoutTimeFixtures['s'] = '0';
|
||||
isoStringWithoutTimeFixtures['mm'] = '00';
|
||||
isoStringWithoutTimeFixtures['ss'] = '00';
|
||||
}
|
||||
|
||||
Object.keys(dateFixtures).forEach((pattern: string) => {
|
||||
expectDateFormatAs(date, pattern, dateFixtures[pattern]);
|
||||
});
|
||||
|
||||
Object.keys(isoStringWithoutTimeFixtures).forEach((pattern: string) => {
|
||||
expectDateFormatAs(isoStringWithoutTime, pattern, isoStringWithoutTimeFixtures[pattern]);
|
||||
});
|
||||
|
||||
expect(pipe.transform(date, 'Z')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should format common multi component patterns', () => {
|
||||
const dateFixtures: any = {
|
||||
'EEE, M/d/y': 'Mon, 6/15/2015',
|
||||
'EEE, M/d': 'Mon, 6/15',
|
||||
'MMM d': 'Jun 15',
|
||||
'dd/MM/yyyy': '15/06/2015',
|
||||
'MM/dd/yyyy': '06/15/2015',
|
||||
'yMEEEd': '20156Mon15',
|
||||
'MEEEd': '6Mon15',
|
||||
'MMMd': 'Jun15',
|
||||
'yMMMMEEEEd': 'Monday, June 15, 2015'
|
||||
};
|
||||
|
||||
// IE and Edge can't format a date to minutes and seconds without hours
|
||||
if (!browserDetection.isEdge && !browserDetection.isIE ||
|
||||
!browserDetection.supportsNativeIntlApi) {
|
||||
dateFixtures['ms'] = '31';
|
||||
}
|
||||
|
||||
if (!browserDetection.isOldChrome) {
|
||||
dateFixtures['jm'] = '9:03 AM';
|
||||
}
|
||||
|
||||
Object.keys(dateFixtures).forEach((pattern: string) => {
|
||||
expectDateFormatAs(date, pattern, dateFixtures[pattern]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should format with pattern aliases', () => {
|
||||
const dateFixtures: any = {
|
||||
'MM/dd/yyyy': '06/15/2015',
|
||||
'fullDate': 'Monday, June 15, 2015',
|
||||
'longDate': 'June 15, 2015',
|
||||
'mediumDate': 'Jun 15, 2015',
|
||||
'shortDate': '6/15/2015'
|
||||
};
|
||||
|
||||
if (!browserDetection.isOldChrome) {
|
||||
// IE and Edge do not add a coma after the year in these 2 cases
|
||||
if ((browserDetection.isEdge || browserDetection.isIE) &&
|
||||
browserDetection.supportsNativeIntlApi) {
|
||||
dateFixtures['medium'] = 'Jun 15, 2015 9:03:01 AM';
|
||||
dateFixtures['short'] = '6/15/2015 9:03 AM';
|
||||
} else {
|
||||
dateFixtures['medium'] = 'Jun 15, 2015, 9:03:01 AM';
|
||||
dateFixtures['short'] = '6/15/2015, 9:03 AM';
|
||||
}
|
||||
}
|
||||
|
||||
if (!browserDetection.isOldChrome) {
|
||||
dateFixtures['mediumTime'] = '9:03:01 AM';
|
||||
dateFixtures['shortTime'] = '9:03 AM';
|
||||
}
|
||||
|
||||
Object.keys(dateFixtures).forEach((pattern: string) => {
|
||||
expectDateFormatAs(date, pattern, dateFixtures[pattern]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should format invalid in IE ISO date',
|
||||
() => expect(pipe.transform('2017-01-11T09:25:14.014-0500')).toEqual('Jan 11, 2017'));
|
||||
|
||||
it('should format invalid in Safari ISO date',
|
||||
() => expect(pipe.transform('2017-01-20T19:00:00+0000')).toEqual('Jan 20, 2017'));
|
||||
|
||||
it('should remove bidi control characters',
|
||||
() => expect(pipe.transform(date, 'MM/dd/yyyy').length).toEqual(10));
|
||||
});
|
||||
});
|
||||
}
|
68
packages/common/test/pipes/i18n_plural_pipe_spec.ts
Normal file
68
packages/common/test/pipes/i18n_plural_pipe_spec.ts
Normal file
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {I18nPluralPipe, NgLocalization} from '@angular/common';
|
||||
import {PipeResolver} from '@angular/compiler/src/pipe_resolver';
|
||||
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
|
||||
|
||||
export function main() {
|
||||
describe('I18nPluralPipe', () => {
|
||||
let localization: NgLocalization;
|
||||
let pipe: I18nPluralPipe;
|
||||
|
||||
const mapping = {
|
||||
'=0': 'No messages.',
|
||||
'=1': 'One message.',
|
||||
'many': 'Many messages.',
|
||||
'other': 'There are # messages, that is #.',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
localization = new TestLocalization();
|
||||
pipe = new I18nPluralPipe(localization);
|
||||
});
|
||||
|
||||
it('should be marked as pure',
|
||||
() => { expect(new PipeResolver().resolve(I18nPluralPipe).pure).toEqual(true); });
|
||||
|
||||
describe('transform', () => {
|
||||
it('should return 0 text if value is 0', () => {
|
||||
const val = pipe.transform(0, mapping);
|
||||
expect(val).toEqual('No messages.');
|
||||
});
|
||||
|
||||
it('should return 1 text if value is 1', () => {
|
||||
const val = pipe.transform(1, mapping);
|
||||
expect(val).toEqual('One message.');
|
||||
});
|
||||
|
||||
it('should return category messages', () => {
|
||||
const val = pipe.transform(4, mapping);
|
||||
expect(val).toEqual('Many messages.');
|
||||
});
|
||||
|
||||
it('should interpolate the value into the text where indicated', () => {
|
||||
const val = pipe.transform(6, mapping);
|
||||
expect(val).toEqual('There are 6 messages, that is 6.');
|
||||
});
|
||||
|
||||
it('should use "" if value is undefined', () => {
|
||||
const val = pipe.transform(void(0), mapping);
|
||||
expect(val).toEqual('');
|
||||
});
|
||||
|
||||
it('should not support bad arguments',
|
||||
() => { expect(() => pipe.transform(0, <any>'hey')).toThrowError(); });
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
class TestLocalization extends NgLocalization {
|
||||
getPluralCategory(value: number): string { return value > 1 && value < 6 ? 'many' : 'other'; }
|
||||
}
|
44
packages/common/test/pipes/i18n_select_pipe_spec.ts
Normal file
44
packages/common/test/pipes/i18n_select_pipe_spec.ts
Normal file
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {I18nSelectPipe} from '@angular/common';
|
||||
import {PipeResolver} from '@angular/compiler/src/pipe_resolver';
|
||||
|
||||
export function main() {
|
||||
describe('I18nSelectPipe', () => {
|
||||
const pipe: I18nSelectPipe = new I18nSelectPipe();
|
||||
const mapping = {'male': 'Invite him.', 'female': 'Invite her.', 'other': 'Invite them.'};
|
||||
|
||||
it('should be marked as pure',
|
||||
() => { expect(new PipeResolver().resolve(I18nSelectPipe).pure).toEqual(true); });
|
||||
|
||||
describe('transform', () => {
|
||||
it('should return the "male" text if value is "male"', () => {
|
||||
const val = pipe.transform('male', mapping);
|
||||
expect(val).toEqual('Invite him.');
|
||||
});
|
||||
|
||||
it('should return the "female" text if value is "female"', () => {
|
||||
const val = pipe.transform('female', mapping);
|
||||
expect(val).toEqual('Invite her.');
|
||||
});
|
||||
|
||||
it('should return the "other" text if value is neither "male" nor "female"',
|
||||
() => { expect(pipe.transform('Anything else', mapping)).toEqual('Invite them.'); });
|
||||
|
||||
it('should return an empty text if value is null or undefined', () => {
|
||||
expect(pipe.transform(null, mapping)).toEqual('');
|
||||
expect(pipe.transform(void 0, mapping)).toEqual('');
|
||||
});
|
||||
|
||||
it('should throw on bad arguments',
|
||||
() => { expect(() => pipe.transform('male', <any>'hey')).toThrowError(); });
|
||||
});
|
||||
|
||||
});
|
||||
}
|
78
packages/common/test/pipes/json_pipe_spec.ts
Normal file
78
packages/common/test/pipes/json_pipe_spec.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {CommonModule, JsonPipe} from '@angular/common';
|
||||
import {Component} from '@angular/core';
|
||||
import {TestBed, async} from '@angular/core/testing';
|
||||
import {expect} from '@angular/platform-browser/testing/matchers';
|
||||
|
||||
export function main() {
|
||||
describe('JsonPipe', () => {
|
||||
const regNewLine = '\n';
|
||||
let inceptionObj: any;
|
||||
let inceptionObjString: string;
|
||||
let pipe: JsonPipe;
|
||||
|
||||
function normalize(obj: string): string { return obj.replace(regNewLine, ''); }
|
||||
|
||||
beforeEach(() => {
|
||||
inceptionObj = {dream: {dream: {dream: 'Limbo'}}};
|
||||
inceptionObjString = '{\n' +
|
||||
' "dream": {\n' +
|
||||
' "dream": {\n' +
|
||||
' "dream": "Limbo"\n' +
|
||||
' }\n' +
|
||||
' }\n' +
|
||||
'}';
|
||||
|
||||
|
||||
pipe = new JsonPipe();
|
||||
});
|
||||
|
||||
describe('transform', () => {
|
||||
it('should return JSON-formatted string',
|
||||
() => { expect(pipe.transform(inceptionObj)).toEqual(inceptionObjString); });
|
||||
|
||||
it('should return JSON-formatted string even when normalized', () => {
|
||||
const dream1 = normalize(pipe.transform(inceptionObj));
|
||||
const dream2 = normalize(inceptionObjString);
|
||||
expect(dream1).toEqual(dream2);
|
||||
});
|
||||
|
||||
it('should return JSON-formatted string similar to Json.stringify', () => {
|
||||
const dream1 = normalize(pipe.transform(inceptionObj));
|
||||
const dream2 = normalize(JSON.stringify(inceptionObj, null, 2));
|
||||
expect(dream1).toEqual(dream2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration', () => {
|
||||
|
||||
@Component({selector: 'test-comp', template: '{{data | json}}'})
|
||||
class TestComp {
|
||||
data: any;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({declarations: [TestComp], imports: [CommonModule]});
|
||||
});
|
||||
|
||||
it('should work with mutable objects', async(() => {
|
||||
const fixture = TestBed.createComponent(TestComp);
|
||||
const mutable: number[] = [1];
|
||||
fixture.componentInstance.data = mutable;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('[\n 1\n]');
|
||||
|
||||
mutable.push(2);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('[\n 1,\n 2\n]');
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
110
packages/common/test/pipes/number_pipe_spec.ts
Normal file
110
packages/common/test/pipes/number_pipe_spec.ts
Normal file
@ -0,0 +1,110 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {CurrencyPipe, DecimalPipe, PercentPipe} from '@angular/common';
|
||||
import {isNumeric} from '@angular/common/src/pipes/number_pipe';
|
||||
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
|
||||
import {browserDetection} from '@angular/platform-browser/testing/browser_util';
|
||||
|
||||
export function main() {
|
||||
describe('Number pipes', () => {
|
||||
describe('DecimalPipe', () => {
|
||||
let pipe: DecimalPipe;
|
||||
|
||||
beforeEach(() => { pipe = new DecimalPipe('en-US'); });
|
||||
|
||||
describe('transform', () => {
|
||||
it('should return correct value for numbers', () => {
|
||||
expect(pipe.transform(12345)).toEqual('12,345');
|
||||
expect(pipe.transform(123, '.2')).toEqual('123.00');
|
||||
expect(pipe.transform(1, '3.')).toEqual('001');
|
||||
expect(pipe.transform(1.1, '3.4-5')).toEqual('001.1000');
|
||||
expect(pipe.transform(1.123456, '3.4-5')).toEqual('001.12346');
|
||||
expect(pipe.transform(1.1234)).toEqual('1.123');
|
||||
});
|
||||
|
||||
it('should support strings', () => {
|
||||
expect(pipe.transform('12345')).toEqual('12,345');
|
||||
expect(pipe.transform('123', '.2')).toEqual('123.00');
|
||||
expect(pipe.transform('1', '3.')).toEqual('001');
|
||||
expect(pipe.transform('1.1', '3.4-5')).toEqual('001.1000');
|
||||
expect(pipe.transform('1.123456', '3.4-5')).toEqual('001.12346');
|
||||
expect(pipe.transform('1.1234')).toEqual('1.123');
|
||||
});
|
||||
|
||||
it('should not support other objects', () => {
|
||||
expect(() => pipe.transform(new Object())).toThrowError();
|
||||
expect(() => pipe.transform('123abc')).toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PercentPipe', () => {
|
||||
let pipe: PercentPipe;
|
||||
|
||||
beforeEach(() => { pipe = new PercentPipe('en-US'); });
|
||||
|
||||
describe('transform', () => {
|
||||
it('should return correct value for numbers', () => {
|
||||
expect(normalize(pipe.transform(1.23))).toEqual('123%');
|
||||
expect(normalize(pipe.transform(1.2, '.2'))).toEqual('120.00%');
|
||||
});
|
||||
|
||||
it('should not support other objects',
|
||||
() => { expect(() => pipe.transform(new Object())).toThrowError(); });
|
||||
});
|
||||
});
|
||||
|
||||
describe('CurrencyPipe', () => {
|
||||
let pipe: CurrencyPipe;
|
||||
|
||||
beforeEach(() => { pipe = new CurrencyPipe('en-US'); });
|
||||
|
||||
describe('transform', () => {
|
||||
it('should return correct value for numbers', () => {
|
||||
// In old Chrome, default formatiing for USD is different
|
||||
if (browserDetection.isOldChrome) {
|
||||
expect(normalize(pipe.transform(123))).toEqual('USD123');
|
||||
} else {
|
||||
expect(normalize(pipe.transform(123))).toEqual('USD123.00');
|
||||
}
|
||||
expect(normalize(pipe.transform(12, 'EUR', false, '.1'))).toEqual('EUR12.0');
|
||||
expect(normalize(pipe.transform(5.1234, 'USD', false, '.0-3'))).toEqual('USD5.123');
|
||||
});
|
||||
|
||||
it('should not support other objects',
|
||||
() => { expect(() => pipe.transform(new Object())).toThrowError(); });
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNumeric', () => {
|
||||
it('should return true when passing correct numeric string',
|
||||
() => { expect(isNumeric('2')).toBe(true); });
|
||||
|
||||
it('should return true when passing correct double string',
|
||||
() => { expect(isNumeric('1.123')).toBe(true); });
|
||||
|
||||
it('should return true when passing correct negative string',
|
||||
() => { expect(isNumeric('-2')).toBe(true); });
|
||||
|
||||
it('should return true when passing correct scientific notation string',
|
||||
() => { expect(isNumeric('1e5')).toBe(true); });
|
||||
|
||||
it('should return false when passing incorrect numeric',
|
||||
() => { expect(isNumeric('a')).toBe(false); });
|
||||
|
||||
it('should return false when passing parseable but non numeric',
|
||||
() => { expect(isNumeric('2a')).toBe(false); });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Between the symbol and the number, Edge adds a no breaking space and IE11 adds a standard space
|
||||
function normalize(s: string): string {
|
||||
return s.replace(/\u00A0| /g, '');
|
||||
}
|
108
packages/common/test/pipes/slice_pipe_spec.ts
Normal file
108
packages/common/test/pipes/slice_pipe_spec.ts
Normal file
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {CommonModule, SlicePipe} from '@angular/common';
|
||||
import {Component} from '@angular/core';
|
||||
import {TestBed, async} from '@angular/core/testing';
|
||||
import {expect} from '@angular/platform-browser/testing/matchers';
|
||||
|
||||
export function main() {
|
||||
describe('SlicePipe', () => {
|
||||
let list: number[];
|
||||
let str: string;
|
||||
let pipe: SlicePipe;
|
||||
|
||||
beforeEach(() => {
|
||||
list = [1, 2, 3, 4, 5];
|
||||
str = 'tuvwxyz';
|
||||
pipe = new SlicePipe();
|
||||
});
|
||||
|
||||
describe('supports', () => {
|
||||
it('should support strings', () => { expect(() => pipe.transform(str, 0)).not.toThrow(); });
|
||||
it('should support lists', () => { expect(() => pipe.transform(list, 0)).not.toThrow(); });
|
||||
|
||||
it('should not support other objects',
|
||||
() => { expect(() => pipe.transform({}, 0)).toThrow(); });
|
||||
});
|
||||
|
||||
describe('transform', () => {
|
||||
|
||||
it('should return null if the value is null',
|
||||
() => { expect(pipe.transform(null, 1)).toBe(null); });
|
||||
|
||||
it('should return all items after START index when START is positive and END is omitted',
|
||||
() => {
|
||||
expect(pipe.transform(list, 3)).toEqual([4, 5]);
|
||||
expect(pipe.transform(str, 3)).toEqual('wxyz');
|
||||
});
|
||||
|
||||
it('should return last START items when START is negative and END is omitted', () => {
|
||||
expect(pipe.transform(list, -3)).toEqual([3, 4, 5]);
|
||||
expect(pipe.transform(str, -3)).toEqual('xyz');
|
||||
});
|
||||
|
||||
it('should return all items between START and END index when START and END are positive',
|
||||
() => {
|
||||
expect(pipe.transform(list, 1, 3)).toEqual([2, 3]);
|
||||
expect(pipe.transform(str, 1, 3)).toEqual('uv');
|
||||
});
|
||||
|
||||
it('should return all items between START and END from the end when START and END are negative',
|
||||
() => {
|
||||
expect(pipe.transform(list, -4, -2)).toEqual([2, 3]);
|
||||
expect(pipe.transform(str, -4, -2)).toEqual('wx');
|
||||
});
|
||||
|
||||
it('should return an empty value if START is greater than END', () => {
|
||||
expect(pipe.transform(list, 4, 2)).toEqual([]);
|
||||
expect(pipe.transform(str, 4, 2)).toEqual('');
|
||||
});
|
||||
|
||||
it('should return an empty value if START greater than input length', () => {
|
||||
expect(pipe.transform(list, 99)).toEqual([]);
|
||||
expect(pipe.transform(str, 99)).toEqual('');
|
||||
});
|
||||
|
||||
it('should return entire input if START is negative and greater than input length', () => {
|
||||
expect(pipe.transform(list, -99)).toEqual([1, 2, 3, 4, 5]);
|
||||
expect(pipe.transform(str, -99)).toEqual('tuvwxyz');
|
||||
});
|
||||
|
||||
it('should not modify the input list', () => {
|
||||
expect(pipe.transform(list, 2)).toEqual([3, 4, 5]);
|
||||
expect(list).toEqual([1, 2, 3, 4, 5]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('integration', () => {
|
||||
|
||||
@Component({selector: 'test-comp', template: '{{(data | slice:1).join(",") }}'})
|
||||
class TestComp {
|
||||
data: any;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({declarations: [TestComp], imports: [CommonModule]});
|
||||
});
|
||||
|
||||
it('should work with mutable arrays', async(() => {
|
||||
const fixture = TestBed.createComponent(TestComp);
|
||||
const mutable: number[] = [1, 2];
|
||||
fixture.componentInstance.data = mutable;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('2');
|
||||
|
||||
mutable.push(3);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('2,3');
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
21
packages/common/test/spies.ts
Normal file
21
packages/common/test/spies.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {ChangeDetectorRef} from '@angular/core/src/change_detection/change_detector_ref';
|
||||
import {SpyObject} from '@angular/core/testing/testing_internal';
|
||||
|
||||
export class SpyChangeDetectorRef extends SpyObject {
|
||||
constructor() {
|
||||
super(ChangeDetectorRef);
|
||||
this.spy('markForCheck');
|
||||
}
|
||||
}
|
||||
|
||||
export class SpyNgControl extends SpyObject {}
|
||||
|
||||
export class SpyValueAccessor extends SpyObject { writeValue: any; }
|
15
packages/common/testing/src/index.ts
Normal file
15
packages/common/testing/src/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @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
|
||||
* @description
|
||||
* Entry point for all public APIs of the common/testing package.
|
||||
*/
|
||||
export {SpyLocation} from './location_mock';
|
||||
export {MockLocationStrategy} from './mock_location_strategy';
|
124
packages/common/testing/src/location_mock.ts
Normal file
124
packages/common/testing/src/location_mock.ts
Normal file
@ -0,0 +1,124 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Location, LocationStrategy} from '@angular/common';
|
||||
import {EventEmitter, Injectable} from '@angular/core';
|
||||
|
||||
|
||||
/**
|
||||
* A spy for {@link Location} that allows tests to fire simulated location events.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
@Injectable()
|
||||
export class SpyLocation implements Location {
|
||||
urlChanges: string[] = [];
|
||||
private _history: LocationState[] = [new LocationState('', '')];
|
||||
private _historyIndex: number = 0;
|
||||
/** @internal */
|
||||
_subject: EventEmitter<any> = new EventEmitter();
|
||||
/** @internal */
|
||||
_baseHref: string = '';
|
||||
/** @internal */
|
||||
_platformStrategy: LocationStrategy = null;
|
||||
|
||||
setInitialPath(url: string) { this._history[this._historyIndex].path = url; }
|
||||
|
||||
setBaseHref(url: string) { this._baseHref = url; }
|
||||
|
||||
path(): string { return this._history[this._historyIndex].path; }
|
||||
|
||||
isCurrentPathEqualTo(path: string, query: string = ''): boolean {
|
||||
const givenPath = path.endsWith('/') ? path.substring(0, path.length - 1) : path;
|
||||
const currPath =
|
||||
this.path().endsWith('/') ? this.path().substring(0, this.path().length - 1) : this.path();
|
||||
|
||||
return currPath == givenPath + (query.length > 0 ? ('?' + query) : '');
|
||||
}
|
||||
|
||||
simulateUrlPop(pathname: string) { this._subject.emit({'url': pathname, 'pop': true}); }
|
||||
|
||||
simulateHashChange(pathname: string) {
|
||||
// Because we don't prevent the native event, the browser will independently update the path
|
||||
this.setInitialPath(pathname);
|
||||
this.urlChanges.push('hash: ' + pathname);
|
||||
this._subject.emit({'url': pathname, 'pop': true, 'type': 'hashchange'});
|
||||
}
|
||||
|
||||
prepareExternalUrl(url: string): string {
|
||||
if (url.length > 0 && !url.startsWith('/')) {
|
||||
url = '/' + url;
|
||||
}
|
||||
return this._baseHref + url;
|
||||
}
|
||||
|
||||
go(path: string, query: string = '') {
|
||||
path = this.prepareExternalUrl(path);
|
||||
|
||||
if (this._historyIndex > 0) {
|
||||
this._history.splice(this._historyIndex + 1);
|
||||
}
|
||||
this._history.push(new LocationState(path, query));
|
||||
this._historyIndex = this._history.length - 1;
|
||||
|
||||
const locationState = this._history[this._historyIndex - 1];
|
||||
if (locationState.path == path && locationState.query == query) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = path + (query.length > 0 ? ('?' + query) : '');
|
||||
this.urlChanges.push(url);
|
||||
this._subject.emit({'url': url, 'pop': false});
|
||||
}
|
||||
|
||||
replaceState(path: string, query: string = '') {
|
||||
path = this.prepareExternalUrl(path);
|
||||
|
||||
const history = this._history[this._historyIndex];
|
||||
if (history.path == path && history.query == query) {
|
||||
return;
|
||||
}
|
||||
|
||||
history.path = path;
|
||||
history.query = query;
|
||||
|
||||
const url = path + (query.length > 0 ? ('?' + query) : '');
|
||||
this.urlChanges.push('replace: ' + url);
|
||||
}
|
||||
|
||||
forward() {
|
||||
if (this._historyIndex < (this._history.length - 1)) {
|
||||
this._historyIndex++;
|
||||
this._subject.emit({'url': this.path(), 'pop': true});
|
||||
}
|
||||
}
|
||||
|
||||
back() {
|
||||
if (this._historyIndex > 0) {
|
||||
this._historyIndex--;
|
||||
this._subject.emit({'url': this.path(), 'pop': true});
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(
|
||||
onNext: (value: any) => void, onThrow: (error: any) => void = null,
|
||||
onReturn: () => void = null): Object {
|
||||
return this._subject.subscribe({next: onNext, error: onThrow, complete: onReturn});
|
||||
}
|
||||
|
||||
normalize(url: string): string { return null; }
|
||||
}
|
||||
|
||||
class LocationState {
|
||||
path: string;
|
||||
query: string;
|
||||
constructor(path: string, query: string) {
|
||||
this.path = path;
|
||||
this.query = query;
|
||||
}
|
||||
}
|
83
packages/common/testing/src/mock_location_strategy.ts
Normal file
83
packages/common/testing/src/mock_location_strategy.ts
Normal file
@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {LocationStrategy} from '@angular/common';
|
||||
import {EventEmitter, Injectable} from '@angular/core';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A mock implementation of {@link LocationStrategy} that allows tests to fire simulated
|
||||
* location events.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Injectable()
|
||||
export class MockLocationStrategy extends LocationStrategy {
|
||||
internalBaseHref: string = '/';
|
||||
internalPath: string = '/';
|
||||
internalTitle: string = '';
|
||||
urlChanges: string[] = [];
|
||||
/** @internal */
|
||||
_subject: EventEmitter<any> = new EventEmitter();
|
||||
constructor() { super(); }
|
||||
|
||||
simulatePopState(url: string): void {
|
||||
this.internalPath = url;
|
||||
this._subject.emit(new _MockPopStateEvent(this.path()));
|
||||
}
|
||||
|
||||
path(includeHash: boolean = false): string { return this.internalPath; }
|
||||
|
||||
prepareExternalUrl(internal: string): string {
|
||||
if (internal.startsWith('/') && this.internalBaseHref.endsWith('/')) {
|
||||
return this.internalBaseHref + internal.substring(1);
|
||||
}
|
||||
return this.internalBaseHref + internal;
|
||||
}
|
||||
|
||||
pushState(ctx: any, title: string, path: string, query: string): void {
|
||||
this.internalTitle = title;
|
||||
|
||||
const url = path + (query.length > 0 ? ('?' + query) : '');
|
||||
this.internalPath = url;
|
||||
|
||||
const externalUrl = this.prepareExternalUrl(url);
|
||||
this.urlChanges.push(externalUrl);
|
||||
}
|
||||
|
||||
replaceState(ctx: any, title: string, path: string, query: string): void {
|
||||
this.internalTitle = title;
|
||||
|
||||
const url = path + (query.length > 0 ? ('?' + query) : '');
|
||||
this.internalPath = url;
|
||||
|
||||
const externalUrl = this.prepareExternalUrl(url);
|
||||
this.urlChanges.push('replace: ' + externalUrl);
|
||||
}
|
||||
|
||||
onPopState(fn: (value: any) => void): void { this._subject.subscribe({next: fn}); }
|
||||
|
||||
getBaseHref(): string { return this.internalBaseHref; }
|
||||
|
||||
back(): void {
|
||||
if (this.urlChanges.length > 0) {
|
||||
this.urlChanges.pop();
|
||||
const nextUrl = this.urlChanges.length > 0 ? this.urlChanges[this.urlChanges.length - 1] : '';
|
||||
this.simulatePopState(nextUrl);
|
||||
}
|
||||
}
|
||||
|
||||
forward(): void { throw 'not implemented'; }
|
||||
}
|
||||
|
||||
class _MockPopStateEvent {
|
||||
pop: boolean = true;
|
||||
type: string = 'popstate';
|
||||
constructor(public newUrl: string) {}
|
||||
}
|
17
packages/common/testing/tsconfig-build.json
Normal file
17
packages/common/testing/tsconfig-build.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig-build",
|
||||
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@angular/core": ["../../../dist/packages-dist/core/"],
|
||||
"@angular/common": ["../../../dist/packages-dist/common"]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"testing/index.ts",
|
||||
"../../../node_modules/zone.js/dist/zone.js.d.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"strictMetadataEmit": true
|
||||
}
|
||||
}
|
32
packages/common/tsconfig-build.json
Normal file
32
packages/common/tsconfig-build.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"declaration": true,
|
||||
"stripInternal": true,
|
||||
"experimentalDecorators": true,
|
||||
"module": "es2015",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "../../../dist/packages-dist/common",
|
||||
"paths": {
|
||||
"@angular/core": ["../../../dist/packages-dist/core"]
|
||||
},
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"target": "es2015",
|
||||
"skipLibCheck": true,
|
||||
"lib": [ "es2015", "dom" ],
|
||||
// don't auto-discover @types/node, it results in a ///<reference in the .d.ts output
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"public_api.ts",
|
||||
"../../../node_modules/zone.js/dist/zone.js.d.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"annotateForClosureCompiler": true,
|
||||
"strictMetadataEmit": true,
|
||||
"flatModuleOutFile": "index.js",
|
||||
"flatModuleId": "@angular/common"
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user