repackaging: all the file moves
This commit is contained in:
6
modules/@angular/README.dart.md
Normal file
6
modules/@angular/README.dart.md
Normal file
@ -0,0 +1,6 @@
|
||||
Angular2
|
||||
=========
|
||||
|
||||
The sources for this package are in the main [Angular2](https://github.com/angular/angular) repo. Please file issues and pull requests against that repo. This is the repository for the upcoming 2.0 version. If you're looking for the current official version of Angular you should go to [angular/angular.js](https://github.com/angular/angular.js)
|
||||
|
||||
License: Apache MIT 2.0
|
15
modules/@angular/README.js.md
Normal file
15
modules/@angular/README.js.md
Normal file
@ -0,0 +1,15 @@
|
||||
Angular2
|
||||
=========
|
||||
|
||||
The sources for this package are in the main [Angular2](https://github.com/angular/angular) repo. Please file issues and pull requests against that repo. This is the repository for the upcoming 2.0 version. If you're looking for the current official version of Angular you should go to [angular/angular.js](https://github.com/angular/angular.js)
|
||||
|
||||
This package contains different sources for different users:
|
||||
|
||||
1. The files located in the root folder can be consumed using CommonJS.
|
||||
2. The files under `/es6` are es6 compatible files that can be transpiled to
|
||||
es5 using any transpiler. This contains:
|
||||
* `dev/`: a development version that includes runtime type assertions
|
||||
* `prod/`: a production version that does not include runtime type assertions
|
||||
3. The files under `/ts` are the TypeScript source files.
|
||||
|
||||
License: Apache MIT 2.0
|
49
modules/@angular/common/src/common_directives.ts
Normal file
49
modules/@angular/common/src/common_directives.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import {Type} from 'angular2/src/facade/lang';
|
||||
|
||||
import {FORM_DIRECTIVES} from './forms';
|
||||
import {CORE_DIRECTIVES} from './directives';
|
||||
|
||||
/**
|
||||
* A collection of Angular core directives that are likely to be used in each and every Angular
|
||||
* application. This includes core directives (e.g., NgIf and NgFor), and forms directives (e.g.,
|
||||
* NgModel).
|
||||
*
|
||||
* This collection can be used to quickly enumerate all the built-in directives in the `directives`
|
||||
* property of the `@Component` decorator.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* Instead of writing:
|
||||
*
|
||||
* ```typescript
|
||||
* import {NgClass, NgIf, NgFor, NgSwitch, NgSwitchWhen, NgSwitchDefault, NgModel, NgForm} from
|
||||
* 'angular2/common';
|
||||
* import {OtherDirective} from './myDirectives';
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'my-component',
|
||||
* templateUrl: 'myComponent.html',
|
||||
* directives: [NgClass, NgIf, NgFor, NgSwitch, NgSwitchWhen, NgSwitchDefault, NgModel, NgForm,
|
||||
* OtherDirective]
|
||||
* })
|
||||
* export class MyComponent {
|
||||
* ...
|
||||
* }
|
||||
* ```
|
||||
* one could import all the common directives at once:
|
||||
*
|
||||
* ```typescript
|
||||
* import {COMMON_DIRECTIVES} from 'angular2/common';
|
||||
* import {OtherDirective} from './myDirectives';
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'my-component',
|
||||
* templateUrl: 'myComponent.html',
|
||||
* directives: [COMMON_DIRECTIVES, OtherDirective]
|
||||
* })
|
||||
* export class MyComponent {
|
||||
* ...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const COMMON_DIRECTIVES: Type[][] = /*@ts2dart_const*/[CORE_DIRECTIVES, FORM_DIRECTIVES];
|
14
modules/@angular/common/src/directives.ts
Normal file
14
modules/@angular/common/src/directives.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @module
|
||||
* @description
|
||||
* Common directives shipped with Angular.
|
||||
*/
|
||||
export {NgClass} from './directives/ng_class';
|
||||
export {NgFor} from './directives/ng_for';
|
||||
export {NgIf} from './directives/ng_if';
|
||||
export {NgTemplateOutlet} from './directives/ng_template_outlet';
|
||||
export {NgStyle} from './directives/ng_style';
|
||||
export {NgSwitch, NgSwitchWhen, NgSwitchDefault} from './directives/ng_switch';
|
||||
export {NgPlural, NgPluralCase, NgLocalization} from './directives/ng_plural';
|
||||
export * from './directives/observable_list_diff';
|
||||
export {CORE_DIRECTIVES} from './directives/core_directives';
|
61
modules/@angular/common/src/directives/core_directives.ts
Normal file
61
modules/@angular/common/src/directives/core_directives.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import {Type} from 'angular2/src/facade/lang';
|
||||
import {NgClass} from './ng_class';
|
||||
import {NgFor} from './ng_for';
|
||||
import {NgIf} from './ng_if';
|
||||
import {NgTemplateOutlet} from './ng_template_outlet';
|
||||
import {NgStyle} from './ng_style';
|
||||
import {NgSwitch, NgSwitchWhen, NgSwitchDefault} from './ng_switch';
|
||||
import {NgPlural, NgPluralCase} from './ng_plural';
|
||||
|
||||
/**
|
||||
* A collection of Angular core directives that are likely to be used in each and every Angular
|
||||
* application.
|
||||
*
|
||||
* This collection can be used to quickly enumerate all the built-in directives in the `directives`
|
||||
* property of the `@Component` annotation.
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/yakGwpCdUkg0qfzX5m8g?p=preview))
|
||||
*
|
||||
* Instead of writing:
|
||||
*
|
||||
* ```typescript
|
||||
* import {NgClass, NgIf, NgFor, NgSwitch, NgSwitchWhen, NgSwitchDefault} from 'angular2/common';
|
||||
* import {OtherDirective} from './myDirectives';
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'my-component',
|
||||
* templateUrl: 'myComponent.html',
|
||||
* directives: [NgClass, NgIf, NgFor, NgSwitch, NgSwitchWhen, NgSwitchDefault, OtherDirective]
|
||||
* })
|
||||
* export class MyComponent {
|
||||
* ...
|
||||
* }
|
||||
* ```
|
||||
* one could import all the core directives at once:
|
||||
*
|
||||
* ```typescript
|
||||
* import {CORE_DIRECTIVES} from 'angular2/common';
|
||||
* import {OtherDirective} from './myDirectives';
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'my-component',
|
||||
* templateUrl: 'myComponent.html',
|
||||
* directives: [CORE_DIRECTIVES, OtherDirective]
|
||||
* })
|
||||
* export class MyComponent {
|
||||
* ...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const CORE_DIRECTIVES: Type[] = /*@ts2dart_const*/[
|
||||
NgClass,
|
||||
NgFor,
|
||||
NgIf,
|
||||
NgTemplateOutlet,
|
||||
NgStyle,
|
||||
NgSwitch,
|
||||
NgSwitchWhen,
|
||||
NgSwitchDefault,
|
||||
NgPlural,
|
||||
NgPluralCase
|
||||
];
|
186
modules/@angular/common/src/directives/ng_class.ts
Normal file
186
modules/@angular/common/src/directives/ng_class.ts
Normal file
@ -0,0 +1,186 @@
|
||||
import {isPresent, isString, isArray} from 'angular2/src/facade/lang';
|
||||
import {
|
||||
DoCheck,
|
||||
OnDestroy,
|
||||
Directive,
|
||||
ElementRef,
|
||||
IterableDiffers,
|
||||
KeyValueDiffers,
|
||||
Renderer,
|
||||
IterableDiffer,
|
||||
KeyValueDiffer,
|
||||
CollectionChangeRecord,
|
||||
KeyValueChangeRecord
|
||||
} from 'angular2/core';
|
||||
import {StringMapWrapper, isListLikeIterable} from 'angular2/src/facade/collection';
|
||||
|
||||
/**
|
||||
* The `NgClass` directive conditionally adds and removes CSS classes on an HTML element based on
|
||||
* an expression's evaluation result.
|
||||
*
|
||||
* The result of an expression evaluation is interpreted differently depending on type of
|
||||
* the expression evaluation result:
|
||||
* - `string` - all the CSS classes listed in a string (space delimited) are added
|
||||
* - `Array` - all the CSS classes (Array elements) are added
|
||||
* - `Object` - each key corresponds to a CSS class name while values are interpreted as expressions
|
||||
* evaluating to `Boolean`. If a given expression evaluates to `true` a corresponding CSS class
|
||||
* is added - otherwise it is removed.
|
||||
*
|
||||
* While the `NgClass` directive can interpret expressions evaluating to `string`, `Array`
|
||||
* or `Object`, the `Object`-based version is the most often used and has an advantage of keeping
|
||||
* all the CSS class names in a template.
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/a4YdtmWywhJ33uqfpPPn?p=preview)):
|
||||
*
|
||||
* ```
|
||||
* import {Component} from 'angular2/core';
|
||||
* import {NgClass} from 'angular2/common';
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'toggle-button',
|
||||
* inputs: ['isDisabled'],
|
||||
* template: `
|
||||
* <div class="button" [ngClass]="{active: isOn, disabled: isDisabled}"
|
||||
* (click)="toggle(!isOn)">
|
||||
* Click me!
|
||||
* </div>`,
|
||||
* styles: [`
|
||||
* .button {
|
||||
* width: 120px;
|
||||
* border: medium solid black;
|
||||
* }
|
||||
*
|
||||
* .active {
|
||||
* background-color: red;
|
||||
* }
|
||||
*
|
||||
* .disabled {
|
||||
* color: gray;
|
||||
* border: medium solid gray;
|
||||
* }
|
||||
* `]
|
||||
* directives: [NgClass]
|
||||
* })
|
||||
* class ToggleButton {
|
||||
* isOn = false;
|
||||
* isDisabled = false;
|
||||
*
|
||||
* toggle(newState) {
|
||||
* if (!this.isDisabled) {
|
||||
* this.isOn = newState;
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Directive({selector: '[ngClass]', inputs: ['rawClass: ngClass', 'initialClasses: class']})
|
||||
export class NgClass implements DoCheck, OnDestroy {
|
||||
private _iterableDiffer: IterableDiffer;
|
||||
private _keyValueDiffer: KeyValueDiffer;
|
||||
private _initialClasses: string[] = [];
|
||||
private _rawClass: string[] | Set<string>;
|
||||
|
||||
constructor(private _iterableDiffers: IterableDiffers, private _keyValueDiffers: KeyValueDiffers,
|
||||
private _ngEl: ElementRef, private _renderer: Renderer) {}
|
||||
|
||||
set initialClasses(v: string) {
|
||||
this._applyInitialClasses(true);
|
||||
this._initialClasses = isPresent(v) && isString(v) ? v.split(' ') : [];
|
||||
this._applyInitialClasses(false);
|
||||
this._applyClasses(this._rawClass, false);
|
||||
}
|
||||
|
||||
set rawClass(v: string | string[] | Set<string>| {[key: string]: any}) {
|
||||
this._cleanupClasses(this._rawClass);
|
||||
|
||||
if (isString(v)) {
|
||||
v = (<string>v).split(' ');
|
||||
}
|
||||
|
||||
this._rawClass = <string[] | Set<string>>v;
|
||||
this._iterableDiffer = null;
|
||||
this._keyValueDiffer = null;
|
||||
if (isPresent(v)) {
|
||||
if (isListLikeIterable(v)) {
|
||||
this._iterableDiffer = this._iterableDiffers.find(v).create(null);
|
||||
} else {
|
||||
this._keyValueDiffer = this._keyValueDiffers.find(v).create(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngDoCheck(): void {
|
||||
if (isPresent(this._iterableDiffer)) {
|
||||
var changes = this._iterableDiffer.diff(this._rawClass);
|
||||
if (isPresent(changes)) {
|
||||
this._applyIterableChanges(changes);
|
||||
}
|
||||
}
|
||||
if (isPresent(this._keyValueDiffer)) {
|
||||
var changes = this._keyValueDiffer.diff(this._rawClass);
|
||||
if (isPresent(changes)) {
|
||||
this._applyKeyValueChanges(changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void { this._cleanupClasses(this._rawClass); }
|
||||
|
||||
private _cleanupClasses(rawClassVal: string[] | Set<string>| {[key: string]: any}): void {
|
||||
this._applyClasses(rawClassVal, true);
|
||||
this._applyInitialClasses(false);
|
||||
}
|
||||
|
||||
private _applyKeyValueChanges(changes: any): void {
|
||||
changes.forEachAddedItem(
|
||||
(record: KeyValueChangeRecord) => { this._toggleClass(record.key, record.currentValue); });
|
||||
changes.forEachChangedItem(
|
||||
(record: KeyValueChangeRecord) => { this._toggleClass(record.key, record.currentValue); });
|
||||
changes.forEachRemovedItem((record: KeyValueChangeRecord) => {
|
||||
if (record.previousValue) {
|
||||
this._toggleClass(record.key, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _applyIterableChanges(changes: any): void {
|
||||
changes.forEachAddedItem(
|
||||
(record: CollectionChangeRecord) => { this._toggleClass(record.item, true); });
|
||||
changes.forEachRemovedItem(
|
||||
(record: CollectionChangeRecord) => { this._toggleClass(record.item, false); });
|
||||
}
|
||||
|
||||
private _applyInitialClasses(isCleanup: boolean) {
|
||||
this._initialClasses.forEach(className => this._toggleClass(className, !isCleanup));
|
||||
}
|
||||
|
||||
private _applyClasses(rawClassVal: string[] | Set<string>| {[key: string]: any},
|
||||
isCleanup: boolean) {
|
||||
if (isPresent(rawClassVal)) {
|
||||
if (isArray(rawClassVal)) {
|
||||
(<string[]>rawClassVal).forEach(className => this._toggleClass(className, !isCleanup));
|
||||
} else if (rawClassVal instanceof Set) {
|
||||
(<Set<string>>rawClassVal).forEach(className => this._toggleClass(className, !isCleanup));
|
||||
} else {
|
||||
StringMapWrapper.forEach(<{[k: string]: any}>rawClassVal,
|
||||
(expVal: any, className: string) => {
|
||||
if (isPresent(expVal)) this._toggleClass(className, !isCleanup);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleClass(className: string, enabled: boolean): void {
|
||||
className = className.trim();
|
||||
if (className.length > 0) {
|
||||
if (className.indexOf(' ') > -1) {
|
||||
var classes = className.split(/\s+/g);
|
||||
for (var i = 0, len = classes.length; i < len; i++) {
|
||||
this._renderer.setElementClass(this._ngEl.nativeElement, classes[i], enabled);
|
||||
}
|
||||
} else {
|
||||
this._renderer.setElementClass(this._ngEl.nativeElement, className, enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
197
modules/@angular/common/src/directives/ng_for.ts
Normal file
197
modules/@angular/common/src/directives/ng_for.ts
Normal file
@ -0,0 +1,197 @@
|
||||
import {
|
||||
DoCheck,
|
||||
Directive,
|
||||
ChangeDetectorRef,
|
||||
IterableDiffer,
|
||||
IterableDiffers,
|
||||
ViewContainerRef,
|
||||
TemplateRef,
|
||||
EmbeddedViewRef,
|
||||
TrackByFn
|
||||
} from 'angular2/core';
|
||||
import {isPresent, isBlank, stringify, getTypeNameForDebugging} from 'angular2/src/facade/lang';
|
||||
import {
|
||||
DefaultIterableDiffer,
|
||||
CollectionChangeRecord
|
||||
} from "../../core/change_detection/differs/default_iterable_differ";
|
||||
import {BaseException} from "../../facade/exceptions";
|
||||
|
||||
export class NgForRow {
|
||||
constructor(public $implicit: any, 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 `NgFor` 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
|
||||
*
|
||||
* `NgFor` 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, `NgFor` 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.
|
||||
*
|
||||
* ### Syntax
|
||||
*
|
||||
* - `<li *ngFor="let item of items; #i = index">...</li>`
|
||||
* - `<li template="ngFor #item of items; #i = index">...</li>`
|
||||
* - `<template ngFor #item [ngForOf]="items" #i="index"><li>...</li></template>`
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* See a [live demo](http://plnkr.co/edit/KVuXxDp0qinGDyo307QW?p=preview) for a more detailed
|
||||
* example.
|
||||
*/
|
||||
@Directive({selector: '[ngFor][ngForOf]', inputs: ['ngForTrackBy', 'ngForOf', 'ngForTemplate']})
|
||||
export class NgFor implements DoCheck {
|
||||
/** @internal */
|
||||
_ngForOf: any;
|
||||
/** @internal */
|
||||
_ngForTrackBy: TrackByFn;
|
||||
private _differ: IterableDiffer;
|
||||
|
||||
constructor(private _viewContainer: ViewContainerRef, private _templateRef: TemplateRef<NgForRow>,
|
||||
private _iterableDiffers: IterableDiffers, private _cdr: ChangeDetectorRef) {}
|
||||
|
||||
set ngForOf(value: any) {
|
||||
this._ngForOf = value;
|
||||
if (isBlank(this._differ) && isPresent(value)) {
|
||||
try {
|
||||
this._differ = this._iterableDiffers.find(value).create(this._cdr, this._ngForTrackBy);
|
||||
} catch (e) {
|
||||
throw new BaseException(
|
||||
`Cannot find a differ supporting object '${value}' of type '${getTypeNameForDebugging(value)}'. NgFor only supports binding to Iterables such as Arrays.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set ngForTemplate(value: TemplateRef<NgForRow>) {
|
||||
if (isPresent(value)) {
|
||||
this._templateRef = value;
|
||||
}
|
||||
}
|
||||
|
||||
set ngForTrackBy(value: TrackByFn) { this._ngForTrackBy = value; }
|
||||
|
||||
ngDoCheck() {
|
||||
if (isPresent(this._differ)) {
|
||||
var changes = this._differ.diff(this._ngForOf);
|
||||
if (isPresent(changes)) this._applyChanges(changes);
|
||||
}
|
||||
}
|
||||
|
||||
private _applyChanges(changes: DefaultIterableDiffer) {
|
||||
// TODO(rado): check if change detection can produce a change record that is
|
||||
// easier to consume than current.
|
||||
var recordViewTuples: RecordViewTuple[] = [];
|
||||
changes.forEachRemovedItem((removedRecord: CollectionChangeRecord) =>
|
||||
recordViewTuples.push(new RecordViewTuple(removedRecord, null)));
|
||||
|
||||
changes.forEachMovedItem((movedRecord: CollectionChangeRecord) =>
|
||||
recordViewTuples.push(new RecordViewTuple(movedRecord, null)));
|
||||
|
||||
var insertTuples = this._bulkRemove(recordViewTuples);
|
||||
|
||||
changes.forEachAddedItem((addedRecord: CollectionChangeRecord) =>
|
||||
insertTuples.push(new RecordViewTuple(addedRecord, null)));
|
||||
|
||||
this._bulkInsert(insertTuples);
|
||||
|
||||
for (var i = 0; i < insertTuples.length; i++) {
|
||||
this._perViewChange(insertTuples[i].view, insertTuples[i].record);
|
||||
}
|
||||
|
||||
for (var i = 0, ilen = this._viewContainer.length; i < ilen; i++) {
|
||||
var viewRef = <EmbeddedViewRef<NgForRow>>this._viewContainer.get(i);
|
||||
viewRef.context.index = i;
|
||||
viewRef.context.count = ilen;
|
||||
}
|
||||
|
||||
changes.forEachIdentityChange((record) => {
|
||||
var viewRef = <EmbeddedViewRef<NgForRow>>this._viewContainer.get(record.currentIndex);
|
||||
viewRef.context.$implicit = record.item;
|
||||
});
|
||||
}
|
||||
|
||||
private _perViewChange(view: EmbeddedViewRef<NgForRow>, record: CollectionChangeRecord) {
|
||||
view.context.$implicit = record.item;
|
||||
}
|
||||
|
||||
private _bulkRemove(tuples: RecordViewTuple[]): RecordViewTuple[] {
|
||||
tuples.sort((a: RecordViewTuple, b: RecordViewTuple) =>
|
||||
a.record.previousIndex - b.record.previousIndex);
|
||||
var movedTuples: RecordViewTuple[] = [];
|
||||
for (var i = tuples.length - 1; i >= 0; i--) {
|
||||
var tuple = tuples[i];
|
||||
// separate moved views from removed views.
|
||||
if (isPresent(tuple.record.currentIndex)) {
|
||||
tuple.view =
|
||||
<EmbeddedViewRef<NgForRow>>this._viewContainer.detach(tuple.record.previousIndex);
|
||||
movedTuples.push(tuple);
|
||||
} else {
|
||||
this._viewContainer.remove(tuple.record.previousIndex);
|
||||
}
|
||||
}
|
||||
return movedTuples;
|
||||
}
|
||||
|
||||
private _bulkInsert(tuples: RecordViewTuple[]): RecordViewTuple[] {
|
||||
tuples.sort((a, b) => a.record.currentIndex - b.record.currentIndex);
|
||||
for (var i = 0; i < tuples.length; i++) {
|
||||
var tuple = tuples[i];
|
||||
if (isPresent(tuple.view)) {
|
||||
this._viewContainer.insert(tuple.view, tuple.record.currentIndex);
|
||||
} else {
|
||||
tuple.view = this._viewContainer.createEmbeddedView(
|
||||
this._templateRef, new NgForRow(null, null, null), tuple.record.currentIndex);
|
||||
}
|
||||
}
|
||||
return tuples;
|
||||
}
|
||||
}
|
||||
|
||||
class RecordViewTuple {
|
||||
view: EmbeddedViewRef<NgForRow>;
|
||||
record: any;
|
||||
constructor(record: any, view: EmbeddedViewRef<NgForRow>) {
|
||||
this.record = record;
|
||||
this.view = view;
|
||||
}
|
||||
}
|
42
modules/@angular/common/src/directives/ng_if.ts
Normal file
42
modules/@angular/common/src/directives/ng_if.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import {Directive, ViewContainerRef, TemplateRef} from 'angular2/core';
|
||||
import {isBlank} from 'angular2/src/facade/lang';
|
||||
|
||||
/**
|
||||
* Removes or recreates a portion of the DOM tree based on an {expression}.
|
||||
*
|
||||
* If the expression assigned to `ngIf` evaluates to a false value then the element
|
||||
* is removed from the DOM, otherwise a clone of the element is reinserted into the DOM.
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/fe0kgemFBtmQOY31b4tw?p=preview)):
|
||||
*
|
||||
* ```
|
||||
* <div *ngIf="errorCount > 0" class="error">
|
||||
* <!-- Error message displayed when the errorCount property on the current context is greater
|
||||
* than 0. -->
|
||||
* {{errorCount}} errors detected
|
||||
* </div>
|
||||
* ```
|
||||
*
|
||||
* ### Syntax
|
||||
*
|
||||
* - `<div *ngIf="condition">...</div>`
|
||||
* - `<div template="ngIf condition">...</div>`
|
||||
* - `<template [ngIf]="condition"><div>...</div></template>`
|
||||
*/
|
||||
@Directive({selector: '[ngIf]', inputs: ['ngIf']})
|
||||
export class NgIf {
|
||||
private _prevCondition: boolean = null;
|
||||
|
||||
constructor(private _viewContainer: ViewContainerRef, private _templateRef: TemplateRef<Object>) {
|
||||
}
|
||||
|
||||
set ngIf(newCondition: any /* boolean */) {
|
||||
if (newCondition && (isBlank(this._prevCondition) || !this._prevCondition)) {
|
||||
this._prevCondition = true;
|
||||
this._viewContainer.createEmbeddedView(this._templateRef);
|
||||
} else if (!newCondition && (isBlank(this._prevCondition) || this._prevCondition)) {
|
||||
this._prevCondition = false;
|
||||
this._viewContainer.clear();
|
||||
}
|
||||
}
|
||||
}
|
147
modules/@angular/common/src/directives/ng_plural.ts
Normal file
147
modules/@angular/common/src/directives/ng_plural.ts
Normal file
@ -0,0 +1,147 @@
|
||||
import {
|
||||
Directive,
|
||||
ViewContainerRef,
|
||||
TemplateRef,
|
||||
ContentChildren,
|
||||
QueryList,
|
||||
Attribute,
|
||||
AfterContentInit,
|
||||
Input
|
||||
} from 'angular2/core';
|
||||
import {isPresent, NumberWrapper} from 'angular2/src/facade/lang';
|
||||
import {Map} from 'angular2/src/facade/collection';
|
||||
import {SwitchView} from './ng_switch';
|
||||
|
||||
const _CATEGORY_DEFAULT = 'other';
|
||||
|
||||
export abstract class NgLocalization { abstract getPluralCategory(value: any): string; }
|
||||
|
||||
/**
|
||||
* `ngPlural` is an i18n directive that 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 an extension of `NgLocalization` that maps values to
|
||||
* category names. You then define a container element that sets the `[ngPlural]` attribute to a
|
||||
* switch expression.
|
||||
* - Inner elements defined with an `[ngPluralCase]` attribute 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 using the `getPluralCategory`
|
||||
* function provided.
|
||||
*
|
||||
* If no matching views are found for a switch expression, inner elements marked
|
||||
* `[ngPluralCase]="other"` will be displayed.
|
||||
*
|
||||
* ```typescript
|
||||
* class MyLocalization extends NgLocalization {
|
||||
* getPluralCategory(value: any) {
|
||||
* if(value < 5) {
|
||||
* return 'few';
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'app',
|
||||
* providers: [provide(NgLocalization, {useClass: MyLocalization})]
|
||||
* })
|
||||
* @View({
|
||||
* template: `
|
||||
* <p>Value = {{value}}</p>
|
||||
* <button (click)="inc()">Increment</button>
|
||||
*
|
||||
* <div [ngPlural]="value">
|
||||
* <template ngPluralCase="=0">there is nothing</template>
|
||||
* <template ngPluralCase="=1">there is one</template>
|
||||
* <template ngPluralCase="few">there are a few</template>
|
||||
* <template ngPluralCase="other">there is some number</template>
|
||||
* </div>
|
||||
* `,
|
||||
* directives: [NgPlural, NgPluralCase]
|
||||
* })
|
||||
* export class App {
|
||||
* value = 'init';
|
||||
*
|
||||
* inc() {
|
||||
* this.value = this.value === 'init' ? 0 : this.value + 1;
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* ```
|
||||
*/
|
||||
|
||||
@Directive({selector: '[ngPluralCase]'})
|
||||
export class NgPluralCase {
|
||||
/** @internal */
|
||||
_view: SwitchView;
|
||||
constructor(@Attribute('ngPluralCase') public value: string, template: TemplateRef<Object>,
|
||||
viewContainer: ViewContainerRef) {
|
||||
this._view = new SwitchView(viewContainer, template);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Directive({selector: '[ngPlural]'})
|
||||
export class NgPlural implements AfterContentInit {
|
||||
private _switchValue: number;
|
||||
private _activeView: SwitchView;
|
||||
private _caseViews = new Map<any, SwitchView>();
|
||||
@ContentChildren(NgPluralCase) cases: QueryList<NgPluralCase> = null;
|
||||
|
||||
constructor(private _localization: NgLocalization) {}
|
||||
|
||||
@Input()
|
||||
set ngPlural(value: number) {
|
||||
this._switchValue = value;
|
||||
this._updateView();
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this.cases.forEach((pluralCase: NgPluralCase): void => {
|
||||
this._caseViews.set(this._formatValue(pluralCase), pluralCase._view);
|
||||
});
|
||||
this._updateView();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_updateView(): void {
|
||||
this._clearViews();
|
||||
|
||||
var view: SwitchView = this._caseViews.get(this._switchValue);
|
||||
if (!isPresent(view)) view = this._getCategoryView(this._switchValue);
|
||||
|
||||
this._activateView(view);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_clearViews() {
|
||||
if (isPresent(this._activeView)) this._activeView.destroy();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_activateView(view: SwitchView) {
|
||||
if (!isPresent(view)) return;
|
||||
this._activeView = view;
|
||||
this._activeView.create();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_getCategoryView(value: number): SwitchView {
|
||||
var category: string = this._localization.getPluralCategory(value);
|
||||
var categoryView: SwitchView = this._caseViews.get(category);
|
||||
return isPresent(categoryView) ? categoryView : this._caseViews.get(_CATEGORY_DEFAULT);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_isValueView(pluralCase: NgPluralCase): boolean { return pluralCase.value[0] === "="; }
|
||||
|
||||
/** @internal */
|
||||
_formatValue(pluralCase: NgPluralCase): any {
|
||||
return this._isValueView(pluralCase) ? this._stripValue(pluralCase.value) : pluralCase.value;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_stripValue(value: string): number { return NumberWrapper.parseInt(value.substring(1), 10); }
|
||||
}
|
101
modules/@angular/common/src/directives/ng_style.ts
Normal file
101
modules/@angular/common/src/directives/ng_style.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import {
|
||||
DoCheck,
|
||||
KeyValueDiffer,
|
||||
KeyValueDiffers,
|
||||
ElementRef,
|
||||
Directive,
|
||||
Renderer
|
||||
} from 'angular2/core';
|
||||
import {isPresent, isBlank, print} from 'angular2/src/facade/lang';
|
||||
import {KeyValueChangeRecord} from "../../core/change_detection/differs/default_keyvalue_differ";
|
||||
|
||||
/**
|
||||
* The `NgStyle` directive changes styles based on a result of expression evaluation.
|
||||
*
|
||||
* An expression assigned to the `ngStyle` property must evaluate to an object and the
|
||||
* corresponding element styles are updated based on changes to this object. Style names to update
|
||||
* are taken from the object's keys, and values - from the corresponding object's values.
|
||||
*
|
||||
* ### Syntax
|
||||
*
|
||||
* - `<div [ngStyle]="{'font-style': style}"></div>`
|
||||
* - `<div [ngStyle]="styleExp"></div>` - here the `styleExp` must evaluate to an object
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/YamGS6GkUh9GqWNQhCyM?p=preview)):
|
||||
*
|
||||
* ```
|
||||
* import {Component} from 'angular2/core';
|
||||
* import {NgStyle} from 'angular2/common';
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'ngStyle-example',
|
||||
* template: `
|
||||
* <h1 [ngStyle]="{'font-style': style, 'font-size': size, 'font-weight': weight}">
|
||||
* Change style of this text!
|
||||
* </h1>
|
||||
*
|
||||
* <hr>
|
||||
*
|
||||
* <label>Italic: <input type="checkbox" (change)="changeStyle($event)"></label>
|
||||
* <label>Bold: <input type="checkbox" (change)="changeWeight($event)"></label>
|
||||
* <label>Size: <input type="text" [value]="size" (change)="size = $event.target.value"></label>
|
||||
* `,
|
||||
* directives: [NgStyle]
|
||||
* })
|
||||
* export class NgStyleExample {
|
||||
* style = 'normal';
|
||||
* weight = 'normal';
|
||||
* size = '20px';
|
||||
*
|
||||
* changeStyle($event: any) {
|
||||
* this.style = $event.target.checked ? 'italic' : 'normal';
|
||||
* }
|
||||
*
|
||||
* changeWeight($event: any) {
|
||||
* this.weight = $event.target.checked ? 'bold' : 'normal';
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* In this example the `font-style`, `font-size` and `font-weight` styles will be updated
|
||||
* based on the `style` property's value changes.
|
||||
*/
|
||||
@Directive({selector: '[ngStyle]', inputs: ['rawStyle: ngStyle']})
|
||||
export class NgStyle implements DoCheck {
|
||||
/** @internal */
|
||||
_rawStyle: {[key: string]: string};
|
||||
/** @internal */
|
||||
_differ: KeyValueDiffer;
|
||||
|
||||
constructor(private _differs: KeyValueDiffers, private _ngEl: ElementRef,
|
||||
private _renderer: Renderer) {}
|
||||
|
||||
set rawStyle(v: {[key: string]: string}) {
|
||||
this._rawStyle = v;
|
||||
if (isBlank(this._differ) && isPresent(v)) {
|
||||
this._differ = this._differs.find(this._rawStyle).create(null);
|
||||
}
|
||||
}
|
||||
|
||||
ngDoCheck() {
|
||||
if (isPresent(this._differ)) {
|
||||
var changes = this._differ.diff(this._rawStyle);
|
||||
if (isPresent(changes)) {
|
||||
this._applyChanges(changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _applyChanges(changes: any): void {
|
||||
changes.forEachAddedItem(
|
||||
(record: KeyValueChangeRecord) => { this._setStyle(record.key, record.currentValue); });
|
||||
changes.forEachChangedItem(
|
||||
(record: KeyValueChangeRecord) => { this._setStyle(record.key, record.currentValue); });
|
||||
changes.forEachRemovedItem(
|
||||
(record: KeyValueChangeRecord) => { this._setStyle(record.key, null); });
|
||||
}
|
||||
|
||||
private _setStyle(name: string, val: string): void {
|
||||
this._renderer.setElementStyle(this._ngEl.nativeElement, name, val);
|
||||
}
|
||||
}
|
203
modules/@angular/common/src/directives/ng_switch.ts
Normal file
203
modules/@angular/common/src/directives/ng_switch.ts
Normal file
@ -0,0 +1,203 @@
|
||||
import {Directive, Host, ViewContainerRef, TemplateRef} from 'angular2/core';
|
||||
import {isPresent, isBlank, normalizeBlank} from 'angular2/src/facade/lang';
|
||||
import {ListWrapper, Map} from 'angular2/src/facade/collection';
|
||||
|
||||
const _WHEN_DEFAULT = /*@ts2dart_const*/ new Object();
|
||||
|
||||
export class SwitchView {
|
||||
constructor(private _viewContainerRef: ViewContainerRef,
|
||||
private _templateRef: TemplateRef<Object>) {}
|
||||
|
||||
create(): void { this._viewContainerRef.createEmbeddedView(this._templateRef); }
|
||||
|
||||
destroy(): void { this._viewContainerRef.clear(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or removes DOM sub-trees when their match expressions match the switch expression.
|
||||
*
|
||||
* Elements within `NgSwitch` but without `NgSwitchWhen` or `NgSwitchDefault` directives will be
|
||||
* preserved at the location as specified in the template.
|
||||
*
|
||||
* `NgSwitch` simply inserts nested elements based on which match expression matches the value
|
||||
* obtained from the evaluated switch expression. In other words, you define a container element
|
||||
* (where you place the directive with a switch expression on the
|
||||
* `[ngSwitch]="..."` attribute), define any inner elements inside of the directive and
|
||||
* place a `[ngSwitchWhen]` attribute per element.
|
||||
*
|
||||
* The `ngSwitchWhen` property is used to inform `NgSwitch` which element to display when the
|
||||
* expression is evaluated. If a matching expression is not found via a `ngSwitchWhen` property
|
||||
* then an element with the `ngSwitchDefault` attribute is displayed.
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/DQMTII95CbuqWrl3lYAs?p=preview))
|
||||
*
|
||||
* ```typescript
|
||||
* @Component({
|
||||
* selector: 'app',
|
||||
* template: `
|
||||
* <p>Value = {{value}}</p>
|
||||
* <button (click)="inc()">Increment</button>
|
||||
*
|
||||
* <div [ngSwitch]="value">
|
||||
* <p *ngSwitchWhen="'init'">increment to start</p>
|
||||
* <p *ngSwitchWhen="0">0, increment again</p>
|
||||
* <p *ngSwitchWhen="1">1, increment again</p>
|
||||
* <p *ngSwitchWhen="2">2, stop incrementing</p>
|
||||
* <p *ngSwitchDefault>> 2, STOP!</p>
|
||||
* </div>
|
||||
*
|
||||
* <!-- alternate syntax -->
|
||||
*
|
||||
* <p [ngSwitch]="value">
|
||||
* <template ngSwitchWhen="init">increment to start</template>
|
||||
* <template [ngSwitchWhen]="0">0, increment again</template>
|
||||
* <template [ngSwitchWhen]="1">1, increment again</template>
|
||||
* <template [ngSwitchWhen]="2">2, stop incrementing</template>
|
||||
* <template ngSwitchDefault>> 2, STOP!</template>
|
||||
* </p>
|
||||
* `,
|
||||
* directives: [NgSwitch, NgSwitchWhen, NgSwitchDefault]
|
||||
* })
|
||||
* export class App {
|
||||
* value = 'init';
|
||||
*
|
||||
* inc() {
|
||||
* this.value = this.value === 'init' ? 0 : this.value + 1;
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* bootstrap(App).catch(err => console.error(err));
|
||||
* ```
|
||||
*/
|
||||
@Directive({selector: '[ngSwitch]', inputs: ['ngSwitch']})
|
||||
export class NgSwitch {
|
||||
private _switchValue: any;
|
||||
private _useDefault: boolean = false;
|
||||
private _valueViews = new Map<any, SwitchView[]>();
|
||||
private _activeViews: SwitchView[] = [];
|
||||
|
||||
set ngSwitch(value: any) {
|
||||
// Empty the currently active ViewContainers
|
||||
this._emptyAllActiveViews();
|
||||
|
||||
// Add the ViewContainers matching the value (with a fallback to default)
|
||||
this._useDefault = false;
|
||||
var views = this._valueViews.get(value);
|
||||
if (isBlank(views)) {
|
||||
this._useDefault = true;
|
||||
views = normalizeBlank(this._valueViews.get(_WHEN_DEFAULT));
|
||||
}
|
||||
this._activateViews(views);
|
||||
|
||||
this._switchValue = value;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_onWhenValueChanged(oldWhen: any, newWhen: any, view: SwitchView): void {
|
||||
this._deregisterView(oldWhen, view);
|
||||
this._registerView(newWhen, view);
|
||||
|
||||
if (oldWhen === this._switchValue) {
|
||||
view.destroy();
|
||||
ListWrapper.remove(this._activeViews, view);
|
||||
} else if (newWhen === this._switchValue) {
|
||||
if (this._useDefault) {
|
||||
this._useDefault = false;
|
||||
this._emptyAllActiveViews();
|
||||
}
|
||||
view.create();
|
||||
this._activeViews.push(view);
|
||||
}
|
||||
|
||||
// Switch to default when there is no more active ViewContainers
|
||||
if (this._activeViews.length === 0 && !this._useDefault) {
|
||||
this._useDefault = true;
|
||||
this._activateViews(this._valueViews.get(_WHEN_DEFAULT));
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_emptyAllActiveViews(): void {
|
||||
var activeContainers = this._activeViews;
|
||||
for (var i = 0; i < activeContainers.length; i++) {
|
||||
activeContainers[i].destroy();
|
||||
}
|
||||
this._activeViews = [];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_activateViews(views: SwitchView[]): void {
|
||||
// TODO(vicb): assert(this._activeViews.length === 0);
|
||||
if (isPresent(views)) {
|
||||
for (var i = 0; i < views.length; i++) {
|
||||
views[i].create();
|
||||
}
|
||||
this._activeViews = views;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_registerView(value: any, view: SwitchView): void {
|
||||
var views = this._valueViews.get(value);
|
||||
if (isBlank(views)) {
|
||||
views = [];
|
||||
this._valueViews.set(value, views);
|
||||
}
|
||||
views.push(view);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_deregisterView(value: any, view: SwitchView): void {
|
||||
// `_WHEN_DEFAULT` is used a marker for non-registered whens
|
||||
if (value === _WHEN_DEFAULT) return;
|
||||
var views = this._valueViews.get(value);
|
||||
if (views.length == 1) {
|
||||
this._valueViews.delete(value);
|
||||
} else {
|
||||
ListWrapper.remove(views, view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the sub-tree when the `ngSwitchWhen` expression evaluates to the same value as the
|
||||
* enclosing switch expression.
|
||||
*
|
||||
* If multiple match expression match the switch expression value, all of them are displayed.
|
||||
*
|
||||
* See {@link NgSwitch} for more details and example.
|
||||
*/
|
||||
@Directive({selector: '[ngSwitchWhen]', inputs: ['ngSwitchWhen']})
|
||||
export class NgSwitchWhen {
|
||||
// `_WHEN_DEFAULT` is used as a marker for a not yet initialized value
|
||||
/** @internal */
|
||||
_value: any = _WHEN_DEFAULT;
|
||||
/** @internal */
|
||||
_view: SwitchView;
|
||||
private _switch: NgSwitch;
|
||||
|
||||
constructor(viewContainer: ViewContainerRef, templateRef: TemplateRef<Object>,
|
||||
@Host() ngSwitch: NgSwitch) {
|
||||
this._switch = ngSwitch;
|
||||
this._view = new SwitchView(viewContainer, templateRef);
|
||||
}
|
||||
|
||||
set ngSwitchWhen(value: any) {
|
||||
this._switch._onWhenValueChanged(this._value, value, this._view);
|
||||
this._value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default case statements are displayed when no match expression matches the switch expression
|
||||
* value.
|
||||
*
|
||||
* See {@link NgSwitch} for more details and example.
|
||||
*/
|
||||
@Directive({selector: '[ngSwitchDefault]'})
|
||||
export class NgSwitchDefault {
|
||||
constructor(viewContainer: ViewContainerRef, templateRef: TemplateRef<Object>,
|
||||
@Host() sswitch: NgSwitch) {
|
||||
sswitch._registerView(_WHEN_DEFAULT, new SwitchView(viewContainer, templateRef));
|
||||
}
|
||||
}
|
26
modules/@angular/common/src/directives/ng_template_outlet.ts
Normal file
26
modules/@angular/common/src/directives/ng_template_outlet.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {Directive, Input, ViewContainerRef, ViewRef, TemplateRef} from 'angular2/core';
|
||||
import {isPresent} from 'angular2/src/facade/lang';
|
||||
|
||||
/**
|
||||
* Creates and inserts an embedded view based on a prepared `TemplateRef`.
|
||||
*
|
||||
* ### Syntax
|
||||
* - `<template [ngTemplateOutlet]="templateRefExpression"></template>`
|
||||
*/
|
||||
@Directive({selector: '[ngTemplateOutlet]'})
|
||||
export class NgTemplateOutlet {
|
||||
private _insertedViewRef: ViewRef;
|
||||
|
||||
constructor(private _viewContainerRef: ViewContainerRef) {}
|
||||
|
||||
@Input()
|
||||
set ngTemplateOutlet(templateRef: TemplateRef<Object>) {
|
||||
if (isPresent(this._insertedViewRef)) {
|
||||
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._insertedViewRef));
|
||||
}
|
||||
|
||||
if (isPresent(templateRef)) {
|
||||
this._insertedViewRef = this._viewContainerRef.createEmbeddedView(templateRef);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
library angular2.directives.observable_list_iterable_diff;
|
||||
|
||||
import 'package:observe/observe.dart' show ObservableList;
|
||||
import 'package:angular2/core.dart';
|
||||
import 'package:angular2/src/core/change_detection/differs/default_iterable_differ.dart';
|
||||
import 'dart:async';
|
||||
|
||||
class ObservableListDiff extends DefaultIterableDiffer {
|
||||
ChangeDetectorRef _ref;
|
||||
ObservableListDiff(this._ref);
|
||||
|
||||
bool _updated = true;
|
||||
ObservableList _collection;
|
||||
StreamSubscription _subscription;
|
||||
|
||||
onDestroy() {
|
||||
if (this._subscription != null) {
|
||||
this._subscription.cancel();
|
||||
this._subscription = null;
|
||||
this._collection = null;
|
||||
}
|
||||
}
|
||||
|
||||
DefaultIterableDiffer diff(ObservableList collection) {
|
||||
if (collection is! ObservableList) {
|
||||
throw "Cannot change the type of a collection";
|
||||
}
|
||||
|
||||
// A new collection instance is passed in.
|
||||
// - We need to set up a listener.
|
||||
// - We need to diff collection.
|
||||
if (!identical(_collection, collection)) {
|
||||
_collection = collection;
|
||||
|
||||
if (_subscription != null) _subscription.cancel();
|
||||
_subscription = collection.changes.listen((_) {
|
||||
_updated = true;
|
||||
_ref.markForCheck();
|
||||
});
|
||||
_updated = false;
|
||||
return super.diff(collection);
|
||||
|
||||
// An update has been registered since the last change detection check.
|
||||
// - We reset the flag.
|
||||
// - We diff the collection.
|
||||
} else if (_updated) {
|
||||
_updated = false;
|
||||
return super.diff(collection);
|
||||
|
||||
// No updates has been registered.
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ObservableListDiffFactory implements IterableDifferFactory {
|
||||
const ObservableListDiffFactory();
|
||||
bool supports(obj) => obj is ObservableList;
|
||||
IterableDiffer create(ChangeDetectorRef cdRef, [Function trackByFn]) {
|
||||
return new ObservableListDiff(cdRef);
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
// TS does not have Observables
|
||||
|
||||
// I need to be here to make TypeScript think this is a module.
|
||||
import {} from 'angular2/src/facade/lang';
|
||||
|
||||
/**
|
||||
* This module exists in Dart, but not in Typescript. This exported symbol
|
||||
* is only here to help Typescript think this is a module.
|
||||
*/
|
||||
export var workaround_empty_observable_list_diff: any;
|
64
modules/@angular/common/src/forms.ts
Normal file
64
modules/@angular/common/src/forms.ts
Normal file
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @module
|
||||
* @description
|
||||
* This module is used for handling user input, by defining and building a {@link ControlGroup} that
|
||||
* consists of
|
||||
* {@link Control} objects, and mapping them onto the DOM. {@link Control} objects can then be used
|
||||
* to read information
|
||||
* from the form DOM elements.
|
||||
*
|
||||
* This module is not included in the `angular2` module; you must import the forms module
|
||||
* explicitly.
|
||||
*
|
||||
*/
|
||||
export {AbstractControl, Control, ControlGroup, ControlArray} from './forms/model';
|
||||
|
||||
export {AbstractControlDirective} from './forms/directives/abstract_control_directive';
|
||||
export {Form} from './forms/directives/form_interface';
|
||||
export {ControlContainer} from './forms/directives/control_container';
|
||||
export {NgControlName} from './forms/directives/ng_control_name';
|
||||
export {NgFormControl} from './forms/directives/ng_form_control';
|
||||
export {NgModel} from './forms/directives/ng_model';
|
||||
export {NgControl} from './forms/directives/ng_control';
|
||||
export {NgControlGroup} from './forms/directives/ng_control_group';
|
||||
export {NgFormModel} from './forms/directives/ng_form_model';
|
||||
export {NgForm} from './forms/directives/ng_form';
|
||||
export {ControlValueAccessor, NG_VALUE_ACCESSOR} from './forms/directives/control_value_accessor';
|
||||
export {DefaultValueAccessor} from './forms/directives/default_value_accessor';
|
||||
export {NgControlStatus} from './forms/directives/ng_control_status';
|
||||
export {CheckboxControlValueAccessor} from './forms/directives/checkbox_value_accessor';
|
||||
export {
|
||||
NgSelectOption,
|
||||
SelectControlValueAccessor
|
||||
} from './forms/directives/select_control_value_accessor';
|
||||
export {FORM_DIRECTIVES, RadioButtonState} from './forms/directives';
|
||||
export {NG_VALIDATORS, NG_ASYNC_VALIDATORS, Validators} from './forms/validators';
|
||||
export {
|
||||
RequiredValidator,
|
||||
MinLengthValidator,
|
||||
MaxLengthValidator,
|
||||
PatternValidator,
|
||||
Validator
|
||||
} from './forms/directives/validators';
|
||||
export {FormBuilder} from './forms/form_builder';
|
||||
import {FormBuilder} from './forms/form_builder';
|
||||
import {RadioControlRegistry} from './forms/directives/radio_control_value_accessor';
|
||||
import {Type} from 'angular2/src/facade/lang';
|
||||
|
||||
/**
|
||||
* Shorthand set of providers used for building Angular forms.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```typescript
|
||||
* bootstrap(MyApp, [FORM_PROVIDERS]);
|
||||
* ```
|
||||
*/
|
||||
export const FORM_PROVIDERS: Type[] = /*@ts2dart_const*/[FormBuilder, RadioControlRegistry];
|
||||
|
||||
/**
|
||||
* See {@link FORM_PROVIDERS} instead.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
export const FORM_BINDINGS = /*@ts2dart_const*/ FORM_PROVIDERS;
|
88
modules/@angular/common/src/forms/directives.ts
Normal file
88
modules/@angular/common/src/forms/directives.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import {Type} from 'angular2/src/facade/lang';
|
||||
import {NgControlName} from './directives/ng_control_name';
|
||||
import {NgFormControl} from './directives/ng_form_control';
|
||||
import {NgModel} from './directives/ng_model';
|
||||
import {NgControlGroup} from './directives/ng_control_group';
|
||||
import {NgFormModel} from './directives/ng_form_model';
|
||||
import {NgForm} from './directives/ng_form';
|
||||
import {DefaultValueAccessor} from './directives/default_value_accessor';
|
||||
import {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor';
|
||||
import {NumberValueAccessor} from './directives/number_value_accessor';
|
||||
import {RadioControlValueAccessor} from './directives/radio_control_value_accessor';
|
||||
import {NgControlStatus} from './directives/ng_control_status';
|
||||
import {
|
||||
SelectControlValueAccessor,
|
||||
NgSelectOption
|
||||
} from './directives/select_control_value_accessor';
|
||||
import {
|
||||
RequiredValidator,
|
||||
MinLengthValidator,
|
||||
MaxLengthValidator,
|
||||
PatternValidator
|
||||
} from './directives/validators';
|
||||
|
||||
export {NgControlName} from './directives/ng_control_name';
|
||||
export {NgFormControl} from './directives/ng_form_control';
|
||||
export {NgModel} from './directives/ng_model';
|
||||
export {NgControlGroup} from './directives/ng_control_group';
|
||||
export {NgFormModel} from './directives/ng_form_model';
|
||||
export {NgForm} from './directives/ng_form';
|
||||
export {DefaultValueAccessor} from './directives/default_value_accessor';
|
||||
export {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor';
|
||||
export {
|
||||
RadioControlValueAccessor,
|
||||
RadioButtonState
|
||||
} from './directives/radio_control_value_accessor';
|
||||
export {NumberValueAccessor} from './directives/number_value_accessor';
|
||||
export {NgControlStatus} from './directives/ng_control_status';
|
||||
export {
|
||||
SelectControlValueAccessor,
|
||||
NgSelectOption
|
||||
} from './directives/select_control_value_accessor';
|
||||
export {
|
||||
RequiredValidator,
|
||||
MinLengthValidator,
|
||||
MaxLengthValidator,
|
||||
PatternValidator
|
||||
} from './directives/validators';
|
||||
export {NgControl} from './directives/ng_control';
|
||||
export {ControlValueAccessor} from './directives/control_value_accessor';
|
||||
|
||||
/**
|
||||
*
|
||||
* A list of all the form directives used as part of a `@Component` annotation.
|
||||
*
|
||||
* This is a shorthand for importing them each individually.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```typescript
|
||||
* @Component({
|
||||
* selector: 'my-app',
|
||||
* directives: [FORM_DIRECTIVES]
|
||||
* })
|
||||
* class MyApp {}
|
||||
* ```
|
||||
*/
|
||||
export const FORM_DIRECTIVES: Type[] = /*@ts2dart_const*/[
|
||||
NgControlName,
|
||||
NgControlGroup,
|
||||
|
||||
NgFormControl,
|
||||
NgModel,
|
||||
NgFormModel,
|
||||
NgForm,
|
||||
|
||||
NgSelectOption,
|
||||
DefaultValueAccessor,
|
||||
NumberValueAccessor,
|
||||
CheckboxControlValueAccessor,
|
||||
SelectControlValueAccessor,
|
||||
RadioControlValueAccessor,
|
||||
NgControlStatus,
|
||||
|
||||
RequiredValidator,
|
||||
MinLengthValidator,
|
||||
MaxLengthValidator,
|
||||
PatternValidator
|
||||
];
|
@ -0,0 +1,30 @@
|
||||
import {AbstractControl} from '../model';
|
||||
import {isPresent} from 'angular2/src/facade/lang';
|
||||
import {unimplemented} from 'angular2/src/facade/exceptions';
|
||||
|
||||
/**
|
||||
* Base class for control directives.
|
||||
*
|
||||
* Only used internally in the forms module.
|
||||
*/
|
||||
export abstract class AbstractControlDirective {
|
||||
get control(): AbstractControl { return unimplemented(); }
|
||||
|
||||
get value(): any { return isPresent(this.control) ? this.control.value : null; }
|
||||
|
||||
get valid(): boolean { return isPresent(this.control) ? this.control.valid : null; }
|
||||
|
||||
get errors(): {[key: string]: any} {
|
||||
return isPresent(this.control) ? this.control.errors : null;
|
||||
}
|
||||
|
||||
get pristine(): boolean { return isPresent(this.control) ? this.control.pristine : null; }
|
||||
|
||||
get dirty(): boolean { return isPresent(this.control) ? this.control.dirty : null; }
|
||||
|
||||
get touched(): boolean { return isPresent(this.control) ? this.control.touched : null; }
|
||||
|
||||
get untouched(): boolean { return isPresent(this.control) ? this.control.untouched : null; }
|
||||
|
||||
get path(): string[] { return null; }
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import {Directive, Renderer, ElementRef, Self, forwardRef, Provider} from 'angular2/core';
|
||||
|
||||
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from './control_value_accessor';
|
||||
|
||||
export const CHECKBOX_VALUE_ACCESSOR: any = /*@ts2dart_const*/ {
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => CheckboxControlValueAccessor),
|
||||
multi: true
|
||||
};
|
||||
|
||||
/**
|
||||
* The accessor for writing a value and listening to changes on a checkbox input element.
|
||||
*
|
||||
* ### Example
|
||||
* ```
|
||||
* <input type="checkbox" ngControl="rememberLogin">
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector:
|
||||
'input[type=checkbox][ngControl],input[type=checkbox][ngFormControl],input[type=checkbox][ngModel]',
|
||||
host: {'(change)': 'onChange($event.target.checked)', '(blur)': 'onTouched()'},
|
||||
providers: [CHECKBOX_VALUE_ACCESSOR]
|
||||
})
|
||||
export class CheckboxControlValueAccessor implements ControlValueAccessor {
|
||||
onChange = (_: any) => {};
|
||||
onTouched = () => {};
|
||||
|
||||
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
|
||||
|
||||
writeValue(value: any): void {
|
||||
this._renderer.setElementProperty(this._elementRef.nativeElement, 'checked', value);
|
||||
}
|
||||
registerOnChange(fn: (_: any) => {}): void { this.onChange = fn; }
|
||||
registerOnTouched(fn: () => {}): void { this.onTouched = fn; }
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import {Form} from './form_interface';
|
||||
import {AbstractControlDirective} from './abstract_control_directive';
|
||||
|
||||
/**
|
||||
* A directive that contains multiple {@link NgControl}s.
|
||||
*
|
||||
* Only used by the forms module.
|
||||
*/
|
||||
export class ControlContainer extends AbstractControlDirective {
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Get the form to which this container belongs.
|
||||
*/
|
||||
get formDirective(): Form { return null; }
|
||||
|
||||
/**
|
||||
* Get the path to this container.
|
||||
*/
|
||||
get path(): string[] { return null; }
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import {OpaqueToken} from 'angular2/core';
|
||||
|
||||
/**
|
||||
* A bridge between a control and a native element.
|
||||
*
|
||||
* A `ControlValueAccessor` abstracts the operations of writing a new value to a
|
||||
* DOM element representing an input control.
|
||||
*
|
||||
* Please see {@link DefaultValueAccessor} for more information.
|
||||
*/
|
||||
export interface ControlValueAccessor {
|
||||
/**
|
||||
* Write a new value to the element.
|
||||
*/
|
||||
writeValue(obj: any): void;
|
||||
|
||||
/**
|
||||
* Set the function to be called when the control receives a change event.
|
||||
*/
|
||||
registerOnChange(fn: any): void;
|
||||
|
||||
/**
|
||||
* Set the function to be called when the control receives a touch event.
|
||||
*/
|
||||
registerOnTouched(fn: any): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to provide a {@link ControlValueAccessor} for form controls.
|
||||
*
|
||||
* See {@link DefaultValueAccessor} for how to implement one.
|
||||
*/
|
||||
export const NG_VALUE_ACCESSOR: OpaqueToken =
|
||||
/*@ts2dart_const*/ new OpaqueToken("NgValueAccessor");
|
@ -0,0 +1,43 @@
|
||||
import {Directive, ElementRef, Renderer, Self, forwardRef, Provider} from 'angular2/core';
|
||||
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from './control_value_accessor';
|
||||
import {isBlank} from 'angular2/src/facade/lang';
|
||||
|
||||
export const DEFAULT_VALUE_ACCESSOR: any = /*@ts2dart_const*/
|
||||
/* @ts2dart_Provider */ {
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => DefaultValueAccessor),
|
||||
multi: true
|
||||
};
|
||||
|
||||
/**
|
||||
* The default accessor for writing a value and listening to changes that is used by the
|
||||
* {@link NgModel}, {@link NgFormControl}, and {@link NgControlName} directives.
|
||||
*
|
||||
* ### Example
|
||||
* ```
|
||||
* <input type="text" ngControl="searchQuery">
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector:
|
||||
'input:not([type=checkbox])[ngControl],textarea[ngControl],input:not([type=checkbox])[ngFormControl],textarea[ngFormControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]',
|
||||
// TODO: vsavkin replace the above selector with the one below it once
|
||||
// https://github.com/angular/angular/issues/3011 is implemented
|
||||
// selector: '[ngControl],[ngModel],[ngFormControl]',
|
||||
host: {'(input)': 'onChange($event.target.value)', '(blur)': 'onTouched()'},
|
||||
bindings: [DEFAULT_VALUE_ACCESSOR]
|
||||
})
|
||||
export class DefaultValueAccessor implements ControlValueAccessor {
|
||||
onChange = (_: any) => {};
|
||||
onTouched = () => {};
|
||||
|
||||
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
|
||||
|
||||
writeValue(value: any): void {
|
||||
var normalizedValue = isBlank(value) ? '' : value;
|
||||
this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', normalizedValue);
|
||||
}
|
||||
|
||||
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
|
||||
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import {NgControl} from './ng_control';
|
||||
import {NgControlGroup} from './ng_control_group';
|
||||
import {Control, ControlGroup} from '../model';
|
||||
|
||||
/**
|
||||
* An interface that {@link NgFormModel} and {@link NgForm} implement.
|
||||
*
|
||||
* Only used by the forms module.
|
||||
*/
|
||||
export interface Form {
|
||||
/**
|
||||
* Add a control to this form.
|
||||
*/
|
||||
addControl(dir: NgControl): void;
|
||||
|
||||
/**
|
||||
* Remove a control from this form.
|
||||
*/
|
||||
removeControl(dir: NgControl): void;
|
||||
|
||||
/**
|
||||
* Look up the {@link Control} associated with a particular {@link NgControl}.
|
||||
*/
|
||||
getControl(dir: NgControl): Control;
|
||||
|
||||
/**
|
||||
* Add a group of controls to this form.
|
||||
*/
|
||||
addControlGroup(dir: NgControlGroup): void;
|
||||
|
||||
/**
|
||||
* Remove a group of controls from this form.
|
||||
*/
|
||||
removeControlGroup(dir: NgControlGroup): void;
|
||||
|
||||
/**
|
||||
* Look up the {@link ControlGroup} associated with a particular {@link NgControlGroup}.
|
||||
*/
|
||||
getControlGroup(dir: NgControlGroup): ControlGroup;
|
||||
|
||||
/**
|
||||
* Update the model for a particular control with a new value.
|
||||
*/
|
||||
updateModel(dir: NgControl, value: any): void;
|
||||
}
|
20
modules/@angular/common/src/forms/directives/ng_control.ts
Normal file
20
modules/@angular/common/src/forms/directives/ng_control.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import {ControlValueAccessor} from './control_value_accessor';
|
||||
import {AbstractControlDirective} from './abstract_control_directive';
|
||||
import {unimplemented} from 'angular2/src/facade/exceptions';
|
||||
import {AsyncValidatorFn, ValidatorFn} from './validators';
|
||||
|
||||
/**
|
||||
* A base class that all control directive extend.
|
||||
* It binds a {@link Control} object to a DOM element.
|
||||
*
|
||||
* Used internally by Angular forms.
|
||||
*/
|
||||
export abstract class NgControl extends AbstractControlDirective {
|
||||
name: string = null;
|
||||
valueAccessor: ControlValueAccessor = null;
|
||||
|
||||
get validator(): ValidatorFn { return <ValidatorFn>unimplemented(); }
|
||||
get asyncValidator(): AsyncValidatorFn { return <AsyncValidatorFn>unimplemented(); }
|
||||
|
||||
abstract viewToModelUpdate(newValue: any): void;
|
||||
}
|
112
modules/@angular/common/src/forms/directives/ng_control_group.ts
Normal file
112
modules/@angular/common/src/forms/directives/ng_control_group.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import {
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
Directive,
|
||||
Optional,
|
||||
Inject,
|
||||
Host,
|
||||
SkipSelf,
|
||||
forwardRef,
|
||||
Provider,
|
||||
Self
|
||||
} from 'angular2/core';
|
||||
|
||||
import {ControlContainer} from './control_container';
|
||||
import {controlPath, composeValidators, composeAsyncValidators} from './shared';
|
||||
import {ControlGroup} from '../model';
|
||||
import {Form} from './form_interface';
|
||||
import {NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators';
|
||||
import {AsyncValidatorFn, ValidatorFn} from './validators';
|
||||
|
||||
export const controlGroupProvider: any =
|
||||
/*@ts2dart_const*/ /* @ts2dart_Provider */ {
|
||||
provide: ControlContainer,
|
||||
useExisting: forwardRef(() => NgControlGroup)
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates and binds a control group to a DOM element.
|
||||
*
|
||||
* This directive can only be used as a child of {@link NgForm} or {@link NgFormModel}.
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/7EJ11uGeaggViYM6T5nq?p=preview))
|
||||
*
|
||||
* ```typescript
|
||||
* @Component({
|
||||
* selector: 'my-app',
|
||||
* directives: [FORM_DIRECTIVES],
|
||||
* template: `
|
||||
* <div>
|
||||
* <h2>Angular2 Control & ControlGroup Example</h2>
|
||||
* <form #f="ngForm">
|
||||
* <div ngControlGroup="name" #cg-name="form">
|
||||
* <h3>Enter your name:</h3>
|
||||
* <p>First: <input ngControl="first" required></p>
|
||||
* <p>Middle: <input ngControl="middle"></p>
|
||||
* <p>Last: <input ngControl="last" required></p>
|
||||
* </div>
|
||||
* <h3>Name value:</h3>
|
||||
* <pre>{{valueOf(cgName)}}</pre>
|
||||
* <p>Name is {{cgName?.control?.valid ? "valid" : "invalid"}}</p>
|
||||
* <h3>What's your favorite food?</h3>
|
||||
* <p><input ngControl="food"></p>
|
||||
* <h3>Form value</h3>
|
||||
* <pre>{{valueOf(f)}}</pre>
|
||||
* </form>
|
||||
* </div>
|
||||
* `
|
||||
* })
|
||||
* export class App {
|
||||
* valueOf(cg: NgControlGroup): string {
|
||||
* if (cg.control == null) {
|
||||
* return null;
|
||||
* }
|
||||
* return JSON.stringify(cg.control.value, null, 2);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* This example declares a control group for a user's name. The value and validation state of
|
||||
* this group can be accessed separately from the overall form.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[ngControlGroup]',
|
||||
providers: [controlGroupProvider],
|
||||
inputs: ['name: ngControlGroup'],
|
||||
exportAs: 'ngForm'
|
||||
})
|
||||
export class NgControlGroup extends ControlContainer implements OnInit,
|
||||
OnDestroy {
|
||||
/** @internal */
|
||||
_parent: ControlContainer;
|
||||
|
||||
constructor(@Host() @SkipSelf() parent: ControlContainer,
|
||||
@Optional() @Self() @Inject(NG_VALIDATORS) private _validators: any[],
|
||||
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[]) {
|
||||
super();
|
||||
this._parent = parent;
|
||||
}
|
||||
|
||||
ngOnInit(): void { this.formDirective.addControlGroup(this); }
|
||||
|
||||
ngOnDestroy(): void { this.formDirective.removeControlGroup(this); }
|
||||
|
||||
/**
|
||||
* Get the {@link ControlGroup} backing this binding.
|
||||
*/
|
||||
get control(): ControlGroup { return this.formDirective.getControlGroup(this); }
|
||||
|
||||
/**
|
||||
* Get the path to this control group.
|
||||
*/
|
||||
get path(): string[] { return controlPath(this.name, this._parent); }
|
||||
|
||||
/**
|
||||
* Get the {@link Form} to which this group belongs.
|
||||
*/
|
||||
get formDirective(): Form { return this._parent.formDirective; }
|
||||
|
||||
get validator(): ValidatorFn { return composeValidators(this._validators); }
|
||||
|
||||
get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._asyncValidators); }
|
||||
}
|
147
modules/@angular/common/src/forms/directives/ng_control_name.ts
Normal file
147
modules/@angular/common/src/forms/directives/ng_control_name.ts
Normal file
@ -0,0 +1,147 @@
|
||||
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
|
||||
|
||||
import {
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
SimpleChange,
|
||||
Query,
|
||||
Directive,
|
||||
forwardRef,
|
||||
Host,
|
||||
SkipSelf,
|
||||
Provider,
|
||||
Inject,
|
||||
Optional,
|
||||
Self
|
||||
} from 'angular2/core';
|
||||
|
||||
import {ControlContainer} from './control_container';
|
||||
import {NgControl} from './ng_control';
|
||||
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
|
||||
import {
|
||||
controlPath,
|
||||
composeValidators,
|
||||
composeAsyncValidators,
|
||||
isPropertyUpdated,
|
||||
selectValueAccessor
|
||||
} from './shared';
|
||||
import {Control} from '../model';
|
||||
import {NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators';
|
||||
import {ValidatorFn, AsyncValidatorFn} from './validators';
|
||||
|
||||
|
||||
export const controlNameBinding: any =
|
||||
/*@ts2dart_const*/ /* @ts2dart_Provider */ {
|
||||
provide: NgControl,
|
||||
useExisting: forwardRef(() => NgControlName)
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates and binds a control with a specified name to a DOM element.
|
||||
*
|
||||
* This directive can only be used as a child of {@link NgForm} or {@link NgFormModel}.
|
||||
|
||||
* ### Example
|
||||
*
|
||||
* In this example, we create the login and password controls.
|
||||
* We can work with each control separately: check its validity, get its value, listen to its
|
||||
* changes.
|
||||
*
|
||||
* ```
|
||||
* @Component({
|
||||
* selector: "login-comp",
|
||||
* directives: [FORM_DIRECTIVES],
|
||||
* template: `
|
||||
* <form #f="ngForm" (submit)='onLogIn(f.value)'>
|
||||
* Login <input type='text' ngControl='login' #l="form">
|
||||
* <div *ngIf="!l.valid">Login is invalid</div>
|
||||
*
|
||||
* Password <input type='password' ngControl='password'>
|
||||
* <button type='submit'>Log in!</button>
|
||||
* </form>
|
||||
* `})
|
||||
* class LoginComp {
|
||||
* onLogIn(value): void {
|
||||
* // value === {login: 'some login', password: 'some password'}
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* We can also use ngModel to bind a domain model to the form.
|
||||
*
|
||||
* ```
|
||||
* @Component({
|
||||
* selector: "login-comp",
|
||||
* directives: [FORM_DIRECTIVES],
|
||||
* template: `
|
||||
* <form (submit)='onLogIn()'>
|
||||
* Login <input type='text' ngControl='login' [(ngModel)]="credentials.login">
|
||||
* Password <input type='password' ngControl='password'
|
||||
* [(ngModel)]="credentials.password">
|
||||
* <button type='submit'>Log in!</button>
|
||||
* </form>
|
||||
* `})
|
||||
* class LoginComp {
|
||||
* credentials: {login:string, password:string};
|
||||
*
|
||||
* onLogIn(): void {
|
||||
* // this.credentials.login === "some login"
|
||||
* // this.credentials.password === "some password"
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[ngControl]',
|
||||
bindings: [controlNameBinding],
|
||||
inputs: ['name: ngControl', 'model: ngModel'],
|
||||
outputs: ['update: ngModelChange'],
|
||||
exportAs: 'ngForm'
|
||||
})
|
||||
export class NgControlName extends NgControl implements OnChanges,
|
||||
OnDestroy {
|
||||
/** @internal */
|
||||
update = new EventEmitter();
|
||||
model: any;
|
||||
viewModel: any;
|
||||
private _added = false;
|
||||
|
||||
constructor(@Host() @SkipSelf() private _parent: ControlContainer,
|
||||
@Optional() @Self() @Inject(NG_VALIDATORS) private _validators:
|
||||
/* Array<Validator|Function> */ any[],
|
||||
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators:
|
||||
/* Array<Validator|Function> */ any[],
|
||||
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
|
||||
valueAccessors: ControlValueAccessor[]) {
|
||||
super();
|
||||
this.valueAccessor = selectValueAccessor(this, valueAccessors);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: {[key: string]: SimpleChange}) {
|
||||
if (!this._added) {
|
||||
this.formDirective.addControl(this);
|
||||
this._added = true;
|
||||
}
|
||||
if (isPropertyUpdated(changes, this.viewModel)) {
|
||||
this.viewModel = this.model;
|
||||
this.formDirective.updateModel(this, this.model);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void { this.formDirective.removeControl(this); }
|
||||
|
||||
viewToModelUpdate(newValue: any): void {
|
||||
this.viewModel = newValue;
|
||||
ObservableWrapper.callEmit(this.update, newValue);
|
||||
}
|
||||
|
||||
get path(): string[] { return controlPath(this.name, this._parent); }
|
||||
|
||||
get formDirective(): any { return this._parent.formDirective; }
|
||||
|
||||
get validator(): ValidatorFn { return composeValidators(this._validators); }
|
||||
|
||||
get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._asyncValidators); }
|
||||
|
||||
get control(): Control { return this.formDirective.getControl(this); }
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import {Directive, Self} from 'angular2/core';
|
||||
import {NgControl} from './ng_control';
|
||||
import {isBlank, isPresent} from 'angular2/src/facade/lang';
|
||||
|
||||
/**
|
||||
* Directive automatically applied to Angular forms that sets CSS classes
|
||||
* based on control status (valid/invalid/dirty/etc).
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[ngControl],[ngModel],[ngFormControl]',
|
||||
host: {
|
||||
'[class.ng-untouched]': 'ngClassUntouched',
|
||||
'[class.ng-touched]': 'ngClassTouched',
|
||||
'[class.ng-pristine]': 'ngClassPristine',
|
||||
'[class.ng-dirty]': 'ngClassDirty',
|
||||
'[class.ng-valid]': 'ngClassValid',
|
||||
'[class.ng-invalid]': 'ngClassInvalid'
|
||||
}
|
||||
})
|
||||
export class NgControlStatus {
|
||||
private _cd: NgControl;
|
||||
|
||||
constructor(@Self() cd: NgControl) { this._cd = cd; }
|
||||
|
||||
get ngClassUntouched(): boolean {
|
||||
return isPresent(this._cd.control) ? this._cd.control.untouched : false;
|
||||
}
|
||||
get ngClassTouched(): boolean {
|
||||
return isPresent(this._cd.control) ? this._cd.control.touched : false;
|
||||
}
|
||||
get ngClassPristine(): boolean {
|
||||
return isPresent(this._cd.control) ? this._cd.control.pristine : false;
|
||||
}
|
||||
get ngClassDirty(): boolean {
|
||||
return isPresent(this._cd.control) ? this._cd.control.dirty : false;
|
||||
}
|
||||
get ngClassValid(): boolean {
|
||||
return isPresent(this._cd.control) ? this._cd.control.valid : false;
|
||||
}
|
||||
get ngClassInvalid(): boolean {
|
||||
return isPresent(this._cd.control) ? !this._cd.control.valid : false;
|
||||
}
|
||||
}
|
174
modules/@angular/common/src/forms/directives/ng_form.ts
Normal file
174
modules/@angular/common/src/forms/directives/ng_form.ts
Normal file
@ -0,0 +1,174 @@
|
||||
import {
|
||||
PromiseWrapper,
|
||||
ObservableWrapper,
|
||||
EventEmitter,
|
||||
PromiseCompleter
|
||||
} from 'angular2/src/facade/async';
|
||||
import {StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {isPresent, isBlank} from 'angular2/src/facade/lang';
|
||||
import {Directive, forwardRef, Provider, Optional, Inject, Self} from 'angular2/core';
|
||||
import {NgControl} from './ng_control';
|
||||
import {Form} from './form_interface';
|
||||
import {NgControlGroup} from './ng_control_group';
|
||||
import {ControlContainer} from './control_container';
|
||||
import {AbstractControl, ControlGroup, Control} from '../model';
|
||||
import {setUpControl, setUpControlGroup, composeValidators, composeAsyncValidators} from './shared';
|
||||
import {Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators';
|
||||
|
||||
export const formDirectiveProvider: any =
|
||||
/*@ts2dart_const*/ /* @ts2dart_Provider */ {
|
||||
provide: ControlContainer,
|
||||
useExisting: forwardRef(() => NgForm)
|
||||
};
|
||||
|
||||
/**
|
||||
* If `NgForm` is bound in a component, `<form>` elements in that component will be
|
||||
* upgraded to use the Angular form system.
|
||||
*
|
||||
* ### Typical Use
|
||||
*
|
||||
* Include `FORM_DIRECTIVES` in the `directives` section of a {@link View} annotation
|
||||
* to use `NgForm` and its associated controls.
|
||||
*
|
||||
* ### Structure
|
||||
*
|
||||
* An Angular form is a collection of `Control`s in some hierarchy.
|
||||
* `Control`s can be at the top level or can be organized in `ControlGroup`s
|
||||
* or `ControlArray`s. This hierarchy is reflected in the form's `value`, a
|
||||
* JSON object that mirrors the form structure.
|
||||
*
|
||||
* ### Submission
|
||||
*
|
||||
* The `ngSubmit` event signals when the user triggers a form submission.
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/ltdgYj4P0iY64AR71EpL?p=preview))
|
||||
*
|
||||
* ```typescript
|
||||
* @Component({
|
||||
* selector: 'my-app',
|
||||
* template: `
|
||||
* <div>
|
||||
* <p>Submit the form to see the data object Angular builds</p>
|
||||
* <h2>NgForm demo</h2>
|
||||
* <form #f="ngForm" (ngSubmit)="onSubmit(f.value)">
|
||||
* <h3>Control group: credentials</h3>
|
||||
* <div ngControlGroup="credentials">
|
||||
* <p>Login: <input type="text" ngControl="login"></p>
|
||||
* <p>Password: <input type="password" ngControl="password"></p>
|
||||
* </div>
|
||||
* <h3>Control group: person</h3>
|
||||
* <div ngControlGroup="person">
|
||||
* <p>First name: <input type="text" ngControl="firstName"></p>
|
||||
* <p>Last name: <input type="text" ngControl="lastName"></p>
|
||||
* </div>
|
||||
* <button type="submit">Submit Form</button>
|
||||
* <p>Form data submitted:</p>
|
||||
* </form>
|
||||
* <pre>{{data}}</pre>
|
||||
* </div>
|
||||
* `,
|
||||
* directives: [CORE_DIRECTIVES, FORM_DIRECTIVES]
|
||||
* })
|
||||
* export class App {
|
||||
* constructor() {}
|
||||
*
|
||||
* data: string;
|
||||
*
|
||||
* onSubmit(data) {
|
||||
* this.data = JSON.stringify(data, null, 2);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'form:not([ngNoForm]):not([ngFormModel]),ngForm,[ngForm]',
|
||||
bindings: [formDirectiveProvider],
|
||||
host: {
|
||||
'(submit)': 'onSubmit()',
|
||||
},
|
||||
outputs: ['ngSubmit'],
|
||||
exportAs: 'ngForm'
|
||||
})
|
||||
export class NgForm extends ControlContainer implements Form {
|
||||
form: ControlGroup;
|
||||
ngSubmit = new EventEmitter();
|
||||
|
||||
constructor(@Optional() @Self() @Inject(NG_VALIDATORS) validators: any[],
|
||||
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) {
|
||||
super();
|
||||
this.form = new ControlGroup({}, null, composeValidators(validators),
|
||||
composeAsyncValidators(asyncValidators));
|
||||
}
|
||||
|
||||
get formDirective(): Form { return this; }
|
||||
|
||||
get control(): ControlGroup { return this.form; }
|
||||
|
||||
get path(): string[] { return []; }
|
||||
|
||||
get controls(): {[key: string]: AbstractControl} { return this.form.controls; }
|
||||
|
||||
addControl(dir: NgControl): void {
|
||||
PromiseWrapper.scheduleMicrotask(() => {
|
||||
var container = this._findContainer(dir.path);
|
||||
var ctrl = new Control();
|
||||
setUpControl(ctrl, dir);
|
||||
container.addControl(dir.name, ctrl);
|
||||
ctrl.updateValueAndValidity({emitEvent: false});
|
||||
});
|
||||
}
|
||||
|
||||
getControl(dir: NgControl): Control { return <Control>this.form.find(dir.path); }
|
||||
|
||||
removeControl(dir: NgControl): void {
|
||||
PromiseWrapper.scheduleMicrotask(() => {
|
||||
var container = this._findContainer(dir.path);
|
||||
if (isPresent(container)) {
|
||||
container.removeControl(dir.name);
|
||||
container.updateValueAndValidity({emitEvent: false});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addControlGroup(dir: NgControlGroup): void {
|
||||
PromiseWrapper.scheduleMicrotask(() => {
|
||||
var container = this._findContainer(dir.path);
|
||||
var group = new ControlGroup({});
|
||||
setUpControlGroup(group, dir);
|
||||
container.addControl(dir.name, group);
|
||||
group.updateValueAndValidity({emitEvent: false});
|
||||
});
|
||||
}
|
||||
|
||||
removeControlGroup(dir: NgControlGroup): void {
|
||||
PromiseWrapper.scheduleMicrotask(() => {
|
||||
var container = this._findContainer(dir.path);
|
||||
if (isPresent(container)) {
|
||||
container.removeControl(dir.name);
|
||||
container.updateValueAndValidity({emitEvent: false});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getControlGroup(dir: NgControlGroup): ControlGroup {
|
||||
return <ControlGroup>this.form.find(dir.path);
|
||||
}
|
||||
|
||||
updateModel(dir: NgControl, value: any): void {
|
||||
PromiseWrapper.scheduleMicrotask(() => {
|
||||
var ctrl = <Control>this.form.find(dir.path);
|
||||
ctrl.updateValue(value);
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
ObservableWrapper.callEmit(this.ngSubmit, null);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_findContainer(path: string[]): ControlGroup {
|
||||
path.pop();
|
||||
return ListWrapper.isEmpty(path) ? this.form : <ControlGroup>this.form.find(path);
|
||||
}
|
||||
}
|
130
modules/@angular/common/src/forms/directives/ng_form_control.ts
Normal file
130
modules/@angular/common/src/forms/directives/ng_form_control.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import {StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
|
||||
import {
|
||||
OnChanges,
|
||||
SimpleChange,
|
||||
Query,
|
||||
Directive,
|
||||
forwardRef,
|
||||
Provider,
|
||||
Inject,
|
||||
Optional,
|
||||
Self
|
||||
} from 'angular2/core';
|
||||
import {NgControl} from './ng_control';
|
||||
import {Control} from '../model';
|
||||
import {Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators';
|
||||
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
|
||||
import {
|
||||
setUpControl,
|
||||
composeValidators,
|
||||
composeAsyncValidators,
|
||||
isPropertyUpdated,
|
||||
selectValueAccessor
|
||||
} from './shared';
|
||||
import {ValidatorFn, AsyncValidatorFn} from './validators';
|
||||
|
||||
export const formControlBinding: any =
|
||||
/*@ts2dart_const*/ /* @ts2dart_Provider */ {
|
||||
provide: NgControl,
|
||||
useExisting: forwardRef(() => NgFormControl)
|
||||
};
|
||||
|
||||
/**
|
||||
* Binds an existing {@link Control} to a DOM element.
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/jcQlZ2tTh22BZZ2ucNAT?p=preview))
|
||||
*
|
||||
* In this example, we bind the control to an input element. When the value of the input element
|
||||
* changes, the value of the control will reflect that change. Likewise, if the value of the
|
||||
* control changes, the input element reflects that change.
|
||||
*
|
||||
* ```typescript
|
||||
* @Component({
|
||||
* selector: 'my-app',
|
||||
* template: `
|
||||
* <div>
|
||||
* <h2>NgFormControl Example</h2>
|
||||
* <form>
|
||||
* <p>Element with existing control: <input type="text"
|
||||
* [ngFormControl]="loginControl"></p>
|
||||
* <p>Value of existing control: {{loginControl.value}}</p>
|
||||
* </form>
|
||||
* </div>
|
||||
* `,
|
||||
* directives: [CORE_DIRECTIVES, FORM_DIRECTIVES]
|
||||
* })
|
||||
* export class App {
|
||||
* loginControl: Control = new Control('');
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### ngModel
|
||||
*
|
||||
* We can also use `ngModel` to bind a domain model to the form.
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/yHMLuHO7DNgT8XvtjTDH?p=preview))
|
||||
*
|
||||
* ```typescript
|
||||
* @Component({
|
||||
* selector: "login-comp",
|
||||
* directives: [FORM_DIRECTIVES],
|
||||
* template: "<input type='text' [ngFormControl]='loginControl' [(ngModel)]='login'>"
|
||||
* })
|
||||
* class LoginComp {
|
||||
* loginControl: Control = new Control('');
|
||||
* login:string;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[ngFormControl]',
|
||||
bindings: [formControlBinding],
|
||||
inputs: ['form: ngFormControl', 'model: ngModel'],
|
||||
outputs: ['update: ngModelChange'],
|
||||
exportAs: 'ngForm'
|
||||
})
|
||||
export class NgFormControl extends NgControl implements OnChanges {
|
||||
form: Control;
|
||||
update = new EventEmitter();
|
||||
model: any;
|
||||
viewModel: any;
|
||||
|
||||
constructor(@Optional() @Self() @Inject(NG_VALIDATORS) private _validators:
|
||||
/* Array<Validator|Function> */ any[],
|
||||
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators:
|
||||
/* Array<Validator|Function> */ any[],
|
||||
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
|
||||
valueAccessors: ControlValueAccessor[]) {
|
||||
super();
|
||||
this.valueAccessor = selectValueAccessor(this, valueAccessors);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: {[key: string]: SimpleChange}): void {
|
||||
if (this._isControlChanged(changes)) {
|
||||
setUpControl(this.form, this);
|
||||
this.form.updateValueAndValidity({emitEvent: false});
|
||||
}
|
||||
if (isPropertyUpdated(changes, this.viewModel)) {
|
||||
this.form.updateValue(this.model);
|
||||
this.viewModel = this.model;
|
||||
}
|
||||
}
|
||||
|
||||
get path(): string[] { return []; }
|
||||
|
||||
get validator(): ValidatorFn { return composeValidators(this._validators); }
|
||||
|
||||
get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._asyncValidators); }
|
||||
|
||||
get control(): Control { return this.form; }
|
||||
|
||||
viewToModelUpdate(newValue: any): void {
|
||||
this.viewModel = newValue;
|
||||
ObservableWrapper.callEmit(this.update, newValue);
|
||||
}
|
||||
|
||||
private _isControlChanged(changes: {[key: string]: any}): boolean {
|
||||
return StringMapWrapper.contains(changes, "form");
|
||||
}
|
||||
}
|
188
modules/@angular/common/src/forms/directives/ng_form_model.ts
Normal file
188
modules/@angular/common/src/forms/directives/ng_form_model.ts
Normal file
@ -0,0 +1,188 @@
|
||||
import {isBlank} from 'angular2/src/facade/lang';
|
||||
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||
import {ObservableWrapper, EventEmitter} from 'angular2/src/facade/async';
|
||||
import {
|
||||
SimpleChange,
|
||||
OnChanges,
|
||||
Directive,
|
||||
forwardRef,
|
||||
Provider,
|
||||
Inject,
|
||||
Optional,
|
||||
Self
|
||||
} from 'angular2/core';
|
||||
import {NgControl} from './ng_control';
|
||||
import {NgControlGroup} from './ng_control_group';
|
||||
import {ControlContainer} from './control_container';
|
||||
import {Form} from './form_interface';
|
||||
import {Control, ControlGroup} from '../model';
|
||||
import {setUpControl, setUpControlGroup, composeValidators, composeAsyncValidators} from './shared';
|
||||
import {Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators';
|
||||
|
||||
export const formDirectiveProvider: any =
|
||||
/*@ts2dart_const*/ /* @ts2dart_Provider */ {
|
||||
provide: ControlContainer,
|
||||
useExisting: forwardRef(() => NgFormModel)
|
||||
};
|
||||
|
||||
/**
|
||||
* Binds an existing control group to a DOM element.
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/jqrVirudY8anJxTMUjTP?p=preview))
|
||||
*
|
||||
* In this example, we bind the control group to the form element, and we bind the login and
|
||||
* password controls to the login and password elements.
|
||||
*
|
||||
* ```typescript
|
||||
* @Component({
|
||||
* selector: 'my-app',
|
||||
* template: `
|
||||
* <div>
|
||||
* <h2>NgFormModel Example</h2>
|
||||
* <form [ngFormModel]="loginForm">
|
||||
* <p>Login: <input type="text" ngControl="login"></p>
|
||||
* <p>Password: <input type="password" ngControl="password"></p>
|
||||
* </form>
|
||||
* <p>Value:</p>
|
||||
* <pre>{{value}}</pre>
|
||||
* </div>
|
||||
* `,
|
||||
* directives: [FORM_DIRECTIVES]
|
||||
* })
|
||||
* export class App {
|
||||
* loginForm: ControlGroup;
|
||||
*
|
||||
* constructor() {
|
||||
* this.loginForm = new ControlGroup({
|
||||
* login: new Control(""),
|
||||
* password: new Control("")
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* get value(): string {
|
||||
* return JSON.stringify(this.loginForm.value, null, 2);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* We can also use ngModel to bind a domain model to the form.
|
||||
*
|
||||
* ```typescript
|
||||
* @Component({
|
||||
* selector: "login-comp",
|
||||
* directives: [FORM_DIRECTIVES],
|
||||
* template: `
|
||||
* <form [ngFormModel]='loginForm'>
|
||||
* Login <input type='text' ngControl='login' [(ngModel)]='credentials.login'>
|
||||
* Password <input type='password' ngControl='password'
|
||||
* [(ngModel)]='credentials.password'>
|
||||
* <button (click)="onLogin()">Login</button>
|
||||
* </form>`
|
||||
* })
|
||||
* class LoginComp {
|
||||
* credentials: {login: string, password: string};
|
||||
* loginForm: ControlGroup;
|
||||
*
|
||||
* constructor() {
|
||||
* this.loginForm = new ControlGroup({
|
||||
* login: new Control(""),
|
||||
* password: new Control("")
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* onLogin(): void {
|
||||
* // this.credentials.login === 'some login'
|
||||
* // this.credentials.password === 'some password'
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[ngFormModel]',
|
||||
bindings: [formDirectiveProvider],
|
||||
inputs: ['form: ngFormModel'],
|
||||
host: {'(submit)': 'onSubmit()'},
|
||||
outputs: ['ngSubmit'],
|
||||
exportAs: 'ngForm'
|
||||
})
|
||||
export class NgFormModel extends ControlContainer implements Form,
|
||||
OnChanges {
|
||||
form: ControlGroup = null;
|
||||
directives: NgControl[] = [];
|
||||
ngSubmit = new EventEmitter();
|
||||
|
||||
constructor(@Optional() @Self() @Inject(NG_VALIDATORS) private _validators: any[],
|
||||
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: {[key: string]: SimpleChange}): void {
|
||||
this._checkFormPresent();
|
||||
if (StringMapWrapper.contains(changes, "form")) {
|
||||
var sync = composeValidators(this._validators);
|
||||
this.form.validator = Validators.compose([this.form.validator, sync]);
|
||||
|
||||
var async = composeAsyncValidators(this._asyncValidators);
|
||||
this.form.asyncValidator = Validators.composeAsync([this.form.asyncValidator, async]);
|
||||
|
||||
this.form.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
||||
}
|
||||
|
||||
this._updateDomValue();
|
||||
}
|
||||
|
||||
get formDirective(): Form { return this; }
|
||||
|
||||
get control(): ControlGroup { return this.form; }
|
||||
|
||||
get path(): string[] { return []; }
|
||||
|
||||
addControl(dir: NgControl): void {
|
||||
var ctrl: any = this.form.find(dir.path);
|
||||
setUpControl(ctrl, dir);
|
||||
ctrl.updateValueAndValidity({emitEvent: false});
|
||||
this.directives.push(dir);
|
||||
}
|
||||
|
||||
getControl(dir: NgControl): Control { return <Control>this.form.find(dir.path); }
|
||||
|
||||
removeControl(dir: NgControl): void { ListWrapper.remove(this.directives, dir); }
|
||||
|
||||
addControlGroup(dir: NgControlGroup) {
|
||||
var ctrl: any = this.form.find(dir.path);
|
||||
setUpControlGroup(ctrl, dir);
|
||||
ctrl.updateValueAndValidity({emitEvent: false});
|
||||
}
|
||||
|
||||
removeControlGroup(dir: NgControlGroup) {}
|
||||
|
||||
getControlGroup(dir: NgControlGroup): ControlGroup {
|
||||
return <ControlGroup>this.form.find(dir.path);
|
||||
}
|
||||
|
||||
updateModel(dir: NgControl, value: any): void {
|
||||
var ctrl = <Control>this.form.find(dir.path);
|
||||
ctrl.updateValue(value);
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
ObservableWrapper.callEmit(this.ngSubmit, null);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_updateDomValue() {
|
||||
this.directives.forEach(dir => {
|
||||
var ctrl: any = this.form.find(dir.path);
|
||||
dir.valueAccessor.writeValue(ctrl.value);
|
||||
});
|
||||
}
|
||||
|
||||
private _checkFormPresent() {
|
||||
if (isBlank(this.form)) {
|
||||
throw new BaseException(
|
||||
`ngFormModel expects a form. Please pass one in. Example: <form [ngFormModel]="myCoolForm">`);
|
||||
}
|
||||
}
|
||||
}
|
101
modules/@angular/common/src/forms/directives/ng_model.ts
Normal file
101
modules/@angular/common/src/forms/directives/ng_model.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
|
||||
import {
|
||||
OnChanges,
|
||||
SimpleChange,
|
||||
Directive,
|
||||
forwardRef,
|
||||
Provider,
|
||||
Inject,
|
||||
Optional,
|
||||
Self
|
||||
} from 'angular2/core';
|
||||
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
|
||||
import {NgControl} from './ng_control';
|
||||
import {Control} from '../model';
|
||||
import {NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators';
|
||||
import {
|
||||
setUpControl,
|
||||
isPropertyUpdated,
|
||||
selectValueAccessor,
|
||||
composeValidators,
|
||||
composeAsyncValidators
|
||||
} from './shared';
|
||||
import {ValidatorFn, AsyncValidatorFn} from './validators';
|
||||
|
||||
export const formControlBinding: any =
|
||||
/*@ts2dart_const*/ /* @ts2dart_Provider */ {
|
||||
provide: NgControl,
|
||||
useExisting: forwardRef(() => NgModel)
|
||||
};
|
||||
|
||||
/**
|
||||
* Binds a domain model to a form control.
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* `ngModel` binds an existing domain model to a form control. For a
|
||||
* two-way binding, use `[(ngModel)]` to ensure the model updates in
|
||||
* both directions.
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/R3UX5qDaUqFO2VYR0UzH?p=preview))
|
||||
* ```typescript
|
||||
* @Component({
|
||||
* selector: "search-comp",
|
||||
* directives: [FORM_DIRECTIVES],
|
||||
* template: `<input type='text' [(ngModel)]="searchQuery">`
|
||||
* })
|
||||
* class SearchComp {
|
||||
* searchQuery: string;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[ngModel]:not([ngControl]):not([ngFormControl])',
|
||||
bindings: [formControlBinding],
|
||||
inputs: ['model: ngModel'],
|
||||
outputs: ['update: ngModelChange'],
|
||||
exportAs: 'ngForm'
|
||||
})
|
||||
export class NgModel extends NgControl implements OnChanges {
|
||||
/** @internal */
|
||||
_control = new Control();
|
||||
/** @internal */
|
||||
_added = false;
|
||||
update = new EventEmitter();
|
||||
model: any;
|
||||
viewModel: any;
|
||||
|
||||
constructor(@Optional() @Self() @Inject(NG_VALIDATORS) private _validators: any[],
|
||||
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[],
|
||||
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
|
||||
valueAccessors: ControlValueAccessor[]) {
|
||||
super();
|
||||
this.valueAccessor = selectValueAccessor(this, valueAccessors);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: {[key: string]: SimpleChange}) {
|
||||
if (!this._added) {
|
||||
setUpControl(this._control, this);
|
||||
this._control.updateValueAndValidity({emitEvent: false});
|
||||
this._added = true;
|
||||
}
|
||||
|
||||
if (isPropertyUpdated(changes, this.viewModel)) {
|
||||
this._control.updateValue(this.model);
|
||||
this.viewModel = this.model;
|
||||
}
|
||||
}
|
||||
|
||||
get control(): Control { return this._control; }
|
||||
|
||||
get path(): string[] { return []; }
|
||||
|
||||
get validator(): ValidatorFn { return composeValidators(this._validators); }
|
||||
|
||||
get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._asyncValidators); }
|
||||
|
||||
viewToModelUpdate(newValue: any): void {
|
||||
this.viewModel = newValue;
|
||||
ObservableWrapper.callEmit(this.update, newValue);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
library angular2.core.forms.normalize_validators;
|
||||
|
||||
import 'package:angular2/src/common/forms/directives/validators.dart' show Validator;
|
||||
|
||||
Function normalizeValidator(dynamic validator){
|
||||
if (validator is Validator) {
|
||||
return (c) => validator.validate(c);
|
||||
} else {
|
||||
return validator;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Function normalizeAsyncValidator(dynamic validator){
|
||||
if (validator is Validator) {
|
||||
return (c) => validator.validate(c);
|
||||
} else {
|
||||
return validator;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import {AbstractControl} from "../model";
|
||||
import {Validator, ValidatorFn, AsyncValidatorFn} from './validators';
|
||||
|
||||
export function normalizeValidator(validator: ValidatorFn | Validator): ValidatorFn {
|
||||
if ((<Validator>validator).validate !== undefined) {
|
||||
return (c: AbstractControl) => (<Validator>validator).validate(c);
|
||||
} else {
|
||||
return <ValidatorFn>validator;
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeAsyncValidator(validator: AsyncValidatorFn | Validator): AsyncValidatorFn {
|
||||
if ((<Validator>validator).validate !== undefined) {
|
||||
return (c: AbstractControl) => Promise.resolve((<Validator>validator).validate(c));
|
||||
} else {
|
||||
return <AsyncValidatorFn>validator;
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import {Directive, ElementRef, Renderer, Self, forwardRef} from 'angular2/core';
|
||||
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from './control_value_accessor';
|
||||
import {isBlank, NumberWrapper} from 'angular2/src/facade/lang';
|
||||
|
||||
export const NUMBER_VALUE_ACCESSOR: any = /*@ts2dart_const*/ /*@ts2dart_Provider*/ {
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => NumberValueAccessor),
|
||||
multi: true
|
||||
};
|
||||
|
||||
/**
|
||||
* The accessor for writing a number value and listening to changes that is used by the
|
||||
* {@link NgModel}, {@link NgFormControl}, and {@link NgControlName} directives.
|
||||
*
|
||||
* ### Example
|
||||
* ```
|
||||
* <input type="number" [(ngModel)]="age">
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector:
|
||||
'input[type=number][ngControl],input[type=number][ngFormControl],input[type=number][ngModel]',
|
||||
host: {
|
||||
'(change)': 'onChange($event.target.value)',
|
||||
'(input)': 'onChange($event.target.value)',
|
||||
'(blur)': 'onTouched()'
|
||||
},
|
||||
bindings: [NUMBER_VALUE_ACCESSOR]
|
||||
})
|
||||
export class NumberValueAccessor implements ControlValueAccessor {
|
||||
onChange = (_: any) => {};
|
||||
onTouched = () => {};
|
||||
|
||||
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
|
||||
|
||||
writeValue(value: number): void {
|
||||
this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', value);
|
||||
}
|
||||
|
||||
registerOnChange(fn: (_: number) => void): void {
|
||||
this.onChange = (value) => { fn(value == '' ? null : NumberWrapper.parseFloat(value)); };
|
||||
}
|
||||
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
import {
|
||||
Directive,
|
||||
ElementRef,
|
||||
Renderer,
|
||||
Self,
|
||||
forwardRef,
|
||||
Provider,
|
||||
Attribute,
|
||||
Input,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
Injector,
|
||||
Injectable
|
||||
} from 'angular2/core';
|
||||
import {
|
||||
NG_VALUE_ACCESSOR,
|
||||
ControlValueAccessor
|
||||
} from 'angular2/src/common/forms/directives/control_value_accessor';
|
||||
import {NgControl} from 'angular2/src/common/forms/directives/ng_control';
|
||||
import {looseIdentical, isPresent} from 'angular2/src/facade/lang';
|
||||
import {ListWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
export const RADIO_VALUE_ACCESSOR: any = /*@ts2dart_const*/ /*@ts2dart_Provider*/ {
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => RadioControlValueAccessor),
|
||||
multi: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal class used by Angular to uncheck radio buttons with the matching name.
|
||||
*/
|
||||
@Injectable()
|
||||
export class RadioControlRegistry {
|
||||
private _accessors: any[] = [];
|
||||
|
||||
add(control: NgControl, accessor: RadioControlValueAccessor) {
|
||||
this._accessors.push([control, accessor]);
|
||||
}
|
||||
|
||||
remove(accessor: RadioControlValueAccessor) {
|
||||
var indexToRemove = -1;
|
||||
for (var i = 0; i < this._accessors.length; ++i) {
|
||||
if (this._accessors[i][1] === accessor) {
|
||||
indexToRemove = i;
|
||||
}
|
||||
}
|
||||
ListWrapper.removeAt(this._accessors, indexToRemove);
|
||||
}
|
||||
|
||||
select(accessor: RadioControlValueAccessor) {
|
||||
this._accessors.forEach((c) => {
|
||||
if (c[0].control.root === accessor._control.control.root && c[1] !== accessor) {
|
||||
c[1].fireUncheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The value provided by the forms API for radio buttons.
|
||||
*/
|
||||
export class RadioButtonState {
|
||||
constructor(public checked: boolean, public value: string) {}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The accessor for writing a radio control value and listening to changes that is used by the
|
||||
* {@link NgModel}, {@link NgFormControl}, and {@link NgControlName} directives.
|
||||
*
|
||||
* ### Example
|
||||
* ```
|
||||
* @Component({
|
||||
* template: `
|
||||
* <input type="radio" name="food" [(ngModel)]="foodChicken">
|
||||
* <input type="radio" name="food" [(ngModel)]="foodFish">
|
||||
* `
|
||||
* })
|
||||
* class FoodCmp {
|
||||
* foodChicken = new RadioButtonState(true, "chicken");
|
||||
* foodFish = new RadioButtonState(false, "fish");
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector:
|
||||
'input[type=radio][ngControl],input[type=radio][ngFormControl],input[type=radio][ngModel]',
|
||||
host: {'(change)': 'onChange()', '(blur)': 'onTouched()'},
|
||||
providers: [RADIO_VALUE_ACCESSOR]
|
||||
})
|
||||
export class RadioControlValueAccessor implements ControlValueAccessor,
|
||||
OnDestroy, OnInit {
|
||||
/** @internal */
|
||||
_state: RadioButtonState;
|
||||
/** @internal */
|
||||
_control: NgControl;
|
||||
@Input() name: string;
|
||||
/** @internal */
|
||||
_fn: Function;
|
||||
onChange = () => {};
|
||||
onTouched = () => {};
|
||||
|
||||
constructor(private _renderer: Renderer, private _elementRef: ElementRef,
|
||||
private _registry: RadioControlRegistry, private _injector: Injector) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._control = this._injector.get(NgControl);
|
||||
this._registry.add(this._control, this);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void { this._registry.remove(this); }
|
||||
|
||||
writeValue(value: any): void {
|
||||
this._state = value;
|
||||
if (isPresent(value) && value.checked) {
|
||||
this._renderer.setElementProperty(this._elementRef.nativeElement, 'checked', true);
|
||||
}
|
||||
}
|
||||
|
||||
registerOnChange(fn: (_: any) => {}): void {
|
||||
this._fn = fn;
|
||||
this.onChange = () => {
|
||||
fn(new RadioButtonState(true, this._state.value));
|
||||
this._registry.select(this);
|
||||
};
|
||||
}
|
||||
|
||||
fireUncheck(): void { this._fn(new RadioButtonState(false, this._state.value)); }
|
||||
|
||||
registerOnTouched(fn: () => {}): void { this.onTouched = fn; }
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
import {
|
||||
Directive,
|
||||
Renderer,
|
||||
forwardRef,
|
||||
Provider,
|
||||
ElementRef,
|
||||
Input,
|
||||
Host,
|
||||
OnDestroy,
|
||||
Optional
|
||||
} from 'angular2/core';
|
||||
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from './control_value_accessor';
|
||||
import {
|
||||
StringWrapper,
|
||||
isPrimitive,
|
||||
isPresent,
|
||||
isBlank,
|
||||
looseIdentical
|
||||
} from 'angular2/src/facade/lang';
|
||||
|
||||
import {MapWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
export const SELECT_VALUE_ACCESSOR: any = /*@ts2dart_const*/ /*@ts2dart_Provider*/ {
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => SelectControlValueAccessor),
|
||||
multi: true
|
||||
};
|
||||
|
||||
function _buildValueString(id: string, value: any): string {
|
||||
if (isBlank(id)) return `${value}`;
|
||||
if (!isPrimitive(value)) value = "Object";
|
||||
return StringWrapper.slice(`${id}: ${value}`, 0, 50);
|
||||
}
|
||||
|
||||
function _extractId(valueString: string): string {
|
||||
return valueString.split(":")[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* The accessor for writing a value and listening to changes on a select element.
|
||||
*
|
||||
* Note: We have to listen to the 'change' event because 'input' events aren't fired
|
||||
* for selects in Firefox and IE:
|
||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=1024350
|
||||
* https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/4660045/
|
||||
*
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'select[ngControl],select[ngFormControl],select[ngModel]',
|
||||
host: {'(change)': 'onChange($event.target.value)', '(blur)': 'onTouched()'},
|
||||
providers: [SELECT_VALUE_ACCESSOR]
|
||||
})
|
||||
export class SelectControlValueAccessor implements ControlValueAccessor {
|
||||
value: any;
|
||||
/** @internal */
|
||||
_optionMap: Map<string, any> = new Map<string, any>();
|
||||
/** @internal */
|
||||
_idCounter: number = 0;
|
||||
|
||||
onChange = (_: any) => {};
|
||||
onTouched = () => {};
|
||||
|
||||
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
|
||||
|
||||
writeValue(value: any): void {
|
||||
this.value = value;
|
||||
var valueString = _buildValueString(this._getOptionId(value), value);
|
||||
this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', valueString);
|
||||
}
|
||||
|
||||
registerOnChange(fn: (value: any) => any): void {
|
||||
this.onChange = (valueString: string) => { fn(this._getOptionValue(valueString)); };
|
||||
}
|
||||
registerOnTouched(fn: () => any): void { this.onTouched = fn; }
|
||||
|
||||
/** @internal */
|
||||
_registerOption(): string { return (this._idCounter++).toString(); }
|
||||
|
||||
/** @internal */
|
||||
_getOptionId(value: any): string {
|
||||
for (let id of MapWrapper.keys(this._optionMap)) {
|
||||
if (looseIdentical(this._optionMap.get(id), value)) return id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_getOptionValue(valueString: string): any {
|
||||
let value = this._optionMap.get(_extractId(valueString));
|
||||
return isPresent(value) ? value : valueString;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks `<option>` as dynamic, so Angular can be notified when options change.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```
|
||||
* <select ngControl="city">
|
||||
* <option *ngFor="let c of cities" [value]="c"></option>
|
||||
* </select>
|
||||
* ```
|
||||
*/
|
||||
@Directive({selector: 'option'})
|
||||
export class NgSelectOption implements OnDestroy {
|
||||
id: string;
|
||||
|
||||
constructor(private _element: ElementRef, private _renderer: Renderer,
|
||||
@Optional() @Host() private _select: SelectControlValueAccessor) {
|
||||
if (isPresent(this._select)) this.id = this._select._registerOption();
|
||||
}
|
||||
|
||||
@Input('ngValue')
|
||||
set ngValue(value: any) {
|
||||
if (this._select == null) return;
|
||||
this._select._optionMap.set(this.id, value);
|
||||
this._setElementValue(_buildValueString(this.id, value));
|
||||
this._select.writeValue(this._select.value);
|
||||
}
|
||||
|
||||
@Input('value')
|
||||
set value(value: any) {
|
||||
this._setElementValue(value);
|
||||
if (isPresent(this._select)) this._select.writeValue(this._select.value);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_setElementValue(value: string): void {
|
||||
this._renderer.setElementProperty(this._element.nativeElement, 'value', value);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (isPresent(this._select)) {
|
||||
this._select._optionMap.delete(this.id);
|
||||
this._select.writeValue(this._select.value);
|
||||
}
|
||||
}
|
||||
}
|
111
modules/@angular/common/src/forms/directives/shared.ts
Normal file
111
modules/@angular/common/src/forms/directives/shared.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
import {isBlank, isPresent, looseIdentical, hasConstructor} from 'angular2/src/facade/lang';
|
||||
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
|
||||
|
||||
import {ControlContainer} from './control_container';
|
||||
import {NgControl} from './ng_control';
|
||||
import {AbstractControlDirective} from './abstract_control_directive';
|
||||
import {NgControlGroup} from './ng_control_group';
|
||||
import {Control, ControlGroup} from '../model';
|
||||
import {Validators} from '../validators';
|
||||
import {ControlValueAccessor} from './control_value_accessor';
|
||||
import {DefaultValueAccessor} from './default_value_accessor';
|
||||
import {NumberValueAccessor} from './number_value_accessor';
|
||||
import {CheckboxControlValueAccessor} from './checkbox_value_accessor';
|
||||
import {SelectControlValueAccessor} from './select_control_value_accessor';
|
||||
import {RadioControlValueAccessor} from './radio_control_value_accessor';
|
||||
import {normalizeValidator, normalizeAsyncValidator} from './normalize_validator';
|
||||
import {ValidatorFn, AsyncValidatorFn} from './validators';
|
||||
|
||||
|
||||
export function controlPath(name: string, parent: ControlContainer): string[] {
|
||||
var p = ListWrapper.clone(parent.path);
|
||||
p.push(name);
|
||||
return p;
|
||||
}
|
||||
|
||||
export function setUpControl(control: Control, dir: NgControl): void {
|
||||
if (isBlank(control)) _throwError(dir, "Cannot find control");
|
||||
if (isBlank(dir.valueAccessor)) _throwError(dir, "No value accessor for");
|
||||
|
||||
control.validator = Validators.compose([control.validator, dir.validator]);
|
||||
control.asyncValidator = Validators.composeAsync([control.asyncValidator, dir.asyncValidator]);
|
||||
dir.valueAccessor.writeValue(control.value);
|
||||
|
||||
// view -> model
|
||||
dir.valueAccessor.registerOnChange((newValue: any) => {
|
||||
dir.viewToModelUpdate(newValue);
|
||||
control.updateValue(newValue, {emitModelToViewChange: false});
|
||||
control.markAsDirty();
|
||||
});
|
||||
|
||||
// model -> view
|
||||
control.registerOnChange((newValue: any) => dir.valueAccessor.writeValue(newValue));
|
||||
|
||||
// touched
|
||||
dir.valueAccessor.registerOnTouched(() => control.markAsTouched());
|
||||
}
|
||||
|
||||
export function setUpControlGroup(control: ControlGroup, dir: NgControlGroup) {
|
||||
if (isBlank(control)) _throwError(dir, "Cannot find control");
|
||||
control.validator = Validators.compose([control.validator, dir.validator]);
|
||||
control.asyncValidator = Validators.composeAsync([control.asyncValidator, dir.asyncValidator]);
|
||||
}
|
||||
|
||||
function _throwError(dir: AbstractControlDirective, message: string): void {
|
||||
var path = dir.path.join(" -> ");
|
||||
throw new BaseException(`${message} '${path}'`);
|
||||
}
|
||||
|
||||
export function composeValidators(validators: /* Array<Validator|Function> */ any[]): ValidatorFn {
|
||||
return isPresent(validators) ? Validators.compose(validators.map(normalizeValidator)) : null;
|
||||
}
|
||||
|
||||
export function composeAsyncValidators(
|
||||
validators: /* Array<Validator|Function> */ any[]): AsyncValidatorFn {
|
||||
return isPresent(validators) ? Validators.composeAsync(validators.map(normalizeAsyncValidator)) :
|
||||
null;
|
||||
}
|
||||
|
||||
export function isPropertyUpdated(changes: {[key: string]: any}, viewModel: any): boolean {
|
||||
if (!StringMapWrapper.contains(changes, "model")) return false;
|
||||
var change = changes["model"];
|
||||
|
||||
if (change.isFirstChange()) return true;
|
||||
return !looseIdentical(viewModel, change.currentValue);
|
||||
}
|
||||
|
||||
// TODO: vsavkin remove it once https://github.com/angular/angular/issues/3011 is implemented
|
||||
export function selectValueAccessor(dir: NgControl,
|
||||
valueAccessors: ControlValueAccessor[]): ControlValueAccessor {
|
||||
if (isBlank(valueAccessors)) return null;
|
||||
|
||||
var defaultAccessor: ControlValueAccessor;
|
||||
var builtinAccessor: ControlValueAccessor;
|
||||
var customAccessor: ControlValueAccessor;
|
||||
valueAccessors.forEach((v: ControlValueAccessor) => {
|
||||
if (hasConstructor(v, DefaultValueAccessor)) {
|
||||
defaultAccessor = v;
|
||||
|
||||
} else if (hasConstructor(v, CheckboxControlValueAccessor) ||
|
||||
hasConstructor(v, NumberValueAccessor) ||
|
||||
hasConstructor(v, SelectControlValueAccessor) ||
|
||||
hasConstructor(v, RadioControlValueAccessor)) {
|
||||
if (isPresent(builtinAccessor))
|
||||
_throwError(dir, "More than one built-in value accessor matches");
|
||||
builtinAccessor = v;
|
||||
|
||||
} else {
|
||||
if (isPresent(customAccessor))
|
||||
_throwError(dir, "More than one custom value accessor matches");
|
||||
customAccessor = v;
|
||||
}
|
||||
});
|
||||
|
||||
if (isPresent(customAccessor)) return customAccessor;
|
||||
if (isPresent(builtinAccessor)) return builtinAccessor;
|
||||
if (isPresent(defaultAccessor)) return defaultAccessor;
|
||||
|
||||
_throwError(dir, "No valid value accessor for");
|
||||
return null;
|
||||
}
|
150
modules/@angular/common/src/forms/directives/validators.ts
Normal file
150
modules/@angular/common/src/forms/directives/validators.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import {forwardRef, Attribute, Directive} from 'angular2/core';
|
||||
import {NumberWrapper} from 'angular2/src/facade/lang';
|
||||
import {Validators, NG_VALIDATORS} from '../validators';
|
||||
import {AbstractControl} from '../model';
|
||||
import * as modelModule from '../model';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* An interface that can be implemented by classes that can act as validators.
|
||||
*
|
||||
* ## Usage
|
||||
*
|
||||
* ```typescript
|
||||
* @Directive({
|
||||
* selector: '[custom-validator]',
|
||||
* providers: [provide(NG_VALIDATORS, {useExisting: CustomValidatorDirective, multi: true})]
|
||||
* })
|
||||
* class CustomValidatorDirective implements Validator {
|
||||
* validate(c: Control): {[key: string]: any} {
|
||||
* return {"custom": true};
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface Validator { validate(c: modelModule.AbstractControl): {[key: string]: any}; }
|
||||
|
||||
const REQUIRED = /*@ts2dart_const*/ Validators.required;
|
||||
|
||||
export const REQUIRED_VALIDATOR: any = /*@ts2dart_const*/ /*@ts2dart_Provider*/ {
|
||||
provide: NG_VALIDATORS,
|
||||
useValue: REQUIRED,
|
||||
multi: true
|
||||
};
|
||||
|
||||
/**
|
||||
* A Directive that adds the `required` validator to any controls marked with the
|
||||
* `required` attribute, via the {@link NG_VALIDATORS} binding.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```
|
||||
* <input ngControl="fullName" required>
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[required][ngControl],[required][ngFormControl],[required][ngModel]',
|
||||
providers: [REQUIRED_VALIDATOR]
|
||||
})
|
||||
export class RequiredValidator {
|
||||
}
|
||||
|
||||
export interface ValidatorFn { (c: AbstractControl): {[key: string]: any}; }
|
||||
export interface AsyncValidatorFn {
|
||||
(c: AbstractControl): any /*Promise<{[key: string]: any}>|Observable<{[key: string]: any}>*/;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provivder which adds {@link MinLengthValidator} to {@link NG_VALIDATORS}.
|
||||
*
|
||||
* ## Example:
|
||||
*
|
||||
* {@example common/forms/ts/validators/validators.ts region='min'}
|
||||
*/
|
||||
export const MIN_LENGTH_VALIDATOR: any = /*@ts2dart_const*/ /*@ts2dart_Provider*/ {
|
||||
provide: NG_VALIDATORS,
|
||||
useExisting: forwardRef(() => MinLengthValidator),
|
||||
multi: true
|
||||
};
|
||||
|
||||
/**
|
||||
* A directive which installs the {@link MinLengthValidator} for any `ngControl`,
|
||||
* `ngFormControl`, or control with `ngModel` that also has a `minlength` attribute.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[minlength][ngControl],[minlength][ngFormControl],[minlength][ngModel]',
|
||||
providers: [MIN_LENGTH_VALIDATOR]
|
||||
})
|
||||
export class MinLengthValidator implements Validator {
|
||||
private _validator: ValidatorFn;
|
||||
|
||||
constructor(@Attribute("minlength") minLength: string) {
|
||||
this._validator = Validators.minLength(NumberWrapper.parseInt(minLength, 10));
|
||||
}
|
||||
|
||||
validate(c: AbstractControl): {[key: string]: any} { return this._validator(c); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider which adds {@link MaxLengthValidator} to {@link NG_VALIDATORS}.
|
||||
*
|
||||
* ## Example:
|
||||
*
|
||||
* {@example common/forms/ts/validators/validators.ts region='max'}
|
||||
*/
|
||||
export const MAX_LENGTH_VALIDATOR: any = /*@ts2dart_const*/ /*@ts2dart_Provider*/ {
|
||||
provide: NG_VALIDATORS,
|
||||
useExisting: forwardRef(() => MaxLengthValidator),
|
||||
multi: true
|
||||
};
|
||||
|
||||
/**
|
||||
* A directive which installs the {@link MaxLengthValidator} for any `ngControl, `ngFormControl`,
|
||||
* or control with `ngModel` that also has a `maxlength` attribute.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[maxlength][ngControl],[maxlength][ngFormControl],[maxlength][ngModel]',
|
||||
providers: [MAX_LENGTH_VALIDATOR]
|
||||
})
|
||||
export class MaxLengthValidator implements Validator {
|
||||
private _validator: ValidatorFn;
|
||||
|
||||
constructor(@Attribute("maxlength") maxLength: string) {
|
||||
this._validator = Validators.maxLength(NumberWrapper.parseInt(maxLength, 10));
|
||||
}
|
||||
|
||||
validate(c: AbstractControl): {[key: string]: any} { return this._validator(c); }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A Directive that adds the `pattern` validator to any controls marked with the
|
||||
* `pattern` attribute, via the {@link NG_VALIDATORS} binding. Uses attribute value
|
||||
* as the regex to validate Control value against. Follows pattern attribute
|
||||
* semantics; i.e. regex must match entire Control value.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```
|
||||
* <input [ngControl]="fullName" pattern="[a-zA-Z ]*">
|
||||
* ```
|
||||
*/
|
||||
export const PATTERN_VALIDATOR: any = /*@ts2dart_const*/ /*@ts2dart_Provider*/ {
|
||||
provide: NG_VALIDATORS,
|
||||
useExisting: forwardRef(() => PatternValidator),
|
||||
multi: true
|
||||
};
|
||||
@Directive({
|
||||
selector: '[pattern][ngControl],[pattern][ngFormControl],[pattern][ngModel]',
|
||||
providers: [PATTERN_VALIDATOR]
|
||||
})
|
||||
export class PatternValidator implements Validator {
|
||||
private _validator: ValidatorFn;
|
||||
|
||||
constructor(@Attribute("pattern") pattern: string) {
|
||||
this._validator = Validators.pattern(pattern);
|
||||
}
|
||||
|
||||
validate(c: AbstractControl): {[key: string]: any} { return this._validator(c); }
|
||||
}
|
112
modules/@angular/common/src/forms/form_builder.ts
Normal file
112
modules/@angular/common/src/forms/form_builder.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import {Injectable} from 'angular2/core';
|
||||
import {StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
import {isPresent, isArray, Type} from 'angular2/src/facade/lang';
|
||||
import * as modelModule from './model';
|
||||
import {ValidatorFn, AsyncValidatorFn} from './directives/validators';
|
||||
|
||||
|
||||
/**
|
||||
* Creates a form object from a user-specified configuration.
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/ENgZo8EuIECZNensZCVr?p=preview))
|
||||
*
|
||||
* ```typescript
|
||||
* @Component({
|
||||
* selector: 'my-app',
|
||||
* viewBindings: [FORM_BINDINGS]
|
||||
* template: `
|
||||
* <form [ngFormModel]="loginForm">
|
||||
* <p>Login <input ngControl="login"></p>
|
||||
* <div ngControlGroup="passwordRetry">
|
||||
* <p>Password <input type="password" ngControl="password"></p>
|
||||
* <p>Confirm password <input type="password" ngControl="passwordConfirmation"></p>
|
||||
* </div>
|
||||
* </form>
|
||||
* <h3>Form value:</h3>
|
||||
* <pre>{{value}}</pre>
|
||||
* `,
|
||||
* directives: [FORM_DIRECTIVES]
|
||||
* })
|
||||
* export class App {
|
||||
* loginForm: ControlGroup;
|
||||
*
|
||||
* constructor(builder: FormBuilder) {
|
||||
* this.loginForm = builder.group({
|
||||
* login: ["", Validators.required],
|
||||
* passwordRetry: builder.group({
|
||||
* password: ["", Validators.required],
|
||||
* passwordConfirmation: ["", Validators.required, asyncValidator]
|
||||
* })
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* get value(): string {
|
||||
* return JSON.stringify(this.loginForm.value, null, 2);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Injectable()
|
||||
export class FormBuilder {
|
||||
/**
|
||||
* Construct a new {@link ControlGroup} with the given map of configuration.
|
||||
* Valid keys for the `extra` parameter map are `optionals` and `validator`.
|
||||
*
|
||||
* See the {@link ControlGroup} constructor for more details.
|
||||
*/
|
||||
group(controlsConfig: {[key: string]: any},
|
||||
extra: {[key: string]: any} = null): modelModule.ControlGroup {
|
||||
var controls = this._reduceControls(controlsConfig);
|
||||
var optionals = <{[key: string]: boolean}>(
|
||||
isPresent(extra) ? StringMapWrapper.get(extra, "optionals") : null);
|
||||
var validator: ValidatorFn = isPresent(extra) ? StringMapWrapper.get(extra, "validator") : null;
|
||||
var asyncValidator: AsyncValidatorFn =
|
||||
isPresent(extra) ? StringMapWrapper.get(extra, "asyncValidator") : null;
|
||||
return new modelModule.ControlGroup(controls, optionals, validator, asyncValidator);
|
||||
}
|
||||
/**
|
||||
* Construct a new {@link Control} with the given `value`,`validator`, and `asyncValidator`.
|
||||
*/
|
||||
control(value: Object, validator: ValidatorFn = null,
|
||||
asyncValidator: AsyncValidatorFn = null): modelModule.Control {
|
||||
return new modelModule.Control(value, validator, asyncValidator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an array of {@link Control}s from the given `controlsConfig` array of
|
||||
* configuration, with the given optional `validator` and `asyncValidator`.
|
||||
*/
|
||||
array(controlsConfig: any[], validator: ValidatorFn = null,
|
||||
asyncValidator: AsyncValidatorFn = null): modelModule.ControlArray {
|
||||
var controls = controlsConfig.map(c => this._createControl(c));
|
||||
return new modelModule.ControlArray(controls, validator, asyncValidator);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_reduceControls(controlsConfig: {[k: string]:
|
||||
any}): {[key: string]: modelModule.AbstractControl} {
|
||||
var controls: {[key: string]: modelModule.AbstractControl} = {};
|
||||
StringMapWrapper.forEach(controlsConfig, (controlConfig: any, controlName: string) => {
|
||||
controls[controlName] = this._createControl(controlConfig);
|
||||
});
|
||||
return controls;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_createControl(controlConfig: any): modelModule.AbstractControl {
|
||||
if (controlConfig instanceof modelModule.Control ||
|
||||
controlConfig instanceof modelModule.ControlGroup ||
|
||||
controlConfig instanceof modelModule.ControlArray) {
|
||||
return controlConfig;
|
||||
|
||||
} else if (isArray(controlConfig)) {
|
||||
var value = controlConfig[0];
|
||||
var validator: ValidatorFn = controlConfig.length > 1 ? controlConfig[1] : null;
|
||||
var asyncValidator: AsyncValidatorFn = controlConfig.length > 2 ? controlConfig[2] : null;
|
||||
return this.control(value, validator, asyncValidator);
|
||||
|
||||
} else {
|
||||
return this.control(controlConfig);
|
||||
}
|
||||
}
|
||||
}
|
510
modules/@angular/common/src/forms/model.ts
Normal file
510
modules/@angular/common/src/forms/model.ts
Normal file
@ -0,0 +1,510 @@
|
||||
import {isPresent, isBlank, normalizeBool} from 'angular2/src/facade/lang';
|
||||
import {Observable, EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
|
||||
import {PromiseWrapper} from 'angular2/src/facade/promise';
|
||||
import {StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {ValidatorFn, AsyncValidatorFn} from './directives/validators';
|
||||
|
||||
/**
|
||||
* Indicates that a Control is valid, i.e. that no errors exist in the input value.
|
||||
*/
|
||||
export const VALID = "VALID";
|
||||
|
||||
/**
|
||||
* Indicates that a Control is invalid, i.e. that an error exists in the input value.
|
||||
*/
|
||||
export const INVALID = "INVALID";
|
||||
|
||||
/**
|
||||
* Indicates that a Control is pending, i.e. that async validation is occurring and
|
||||
* errors are not yet available for the input value.
|
||||
*/
|
||||
export const PENDING = "PENDING";
|
||||
|
||||
export function isControl(control: Object): boolean {
|
||||
return control instanceof AbstractControl;
|
||||
}
|
||||
|
||||
function _find(control: AbstractControl, path: Array<string | number>| string) {
|
||||
if (isBlank(path)) return null;
|
||||
|
||||
if (!(path instanceof Array)) {
|
||||
path = (<string>path).split("/");
|
||||
}
|
||||
if (path instanceof Array && ListWrapper.isEmpty(path)) return null;
|
||||
|
||||
return (<Array<string | number>>path)
|
||||
.reduce((v, name) => {
|
||||
if (v instanceof ControlGroup) {
|
||||
return isPresent(v.controls[name]) ? v.controls[name] : null;
|
||||
} else if (v instanceof ControlArray) {
|
||||
var index = <number>name;
|
||||
return isPresent(v.at(index)) ? v.at(index) : null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, control);
|
||||
}
|
||||
|
||||
function toObservable(r: any): Observable<any> {
|
||||
return PromiseWrapper.isPromise(r) ? ObservableWrapper.fromPromise(r) : r;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export abstract class AbstractControl {
|
||||
/** @internal */
|
||||
_value: any;
|
||||
|
||||
private _valueChanges: EventEmitter<any>;
|
||||
private _statusChanges: EventEmitter<any>;
|
||||
private _status: string;
|
||||
private _errors: {[key: string]: any};
|
||||
private _pristine: boolean = true;
|
||||
private _touched: boolean = false;
|
||||
private _parent: ControlGroup | ControlArray;
|
||||
private _asyncValidationSubscription: any;
|
||||
|
||||
constructor(public validator: ValidatorFn, public asyncValidator: AsyncValidatorFn) {}
|
||||
|
||||
get value(): any { return this._value; }
|
||||
|
||||
get status(): string { return this._status; }
|
||||
|
||||
get valid(): boolean { return this._status === VALID; }
|
||||
|
||||
/**
|
||||
* Returns the errors of this control.
|
||||
*/
|
||||
get errors(): {[key: string]: any} { return this._errors; }
|
||||
|
||||
get pristine(): boolean { return this._pristine; }
|
||||
|
||||
get dirty(): boolean { return !this.pristine; }
|
||||
|
||||
get touched(): boolean { return this._touched; }
|
||||
|
||||
get untouched(): boolean { return !this._touched; }
|
||||
|
||||
get valueChanges(): Observable<any> { return this._valueChanges; }
|
||||
|
||||
get statusChanges(): Observable<any> { return this._statusChanges; }
|
||||
|
||||
get pending(): boolean { return this._status == PENDING; }
|
||||
|
||||
markAsTouched(): void { this._touched = true; }
|
||||
|
||||
markAsDirty({onlySelf}: {onlySelf?: boolean} = {}): void {
|
||||
onlySelf = normalizeBool(onlySelf);
|
||||
this._pristine = false;
|
||||
|
||||
if (isPresent(this._parent) && !onlySelf) {
|
||||
this._parent.markAsDirty({onlySelf: onlySelf});
|
||||
}
|
||||
}
|
||||
|
||||
markAsPending({onlySelf}: {onlySelf?: boolean} = {}): void {
|
||||
onlySelf = normalizeBool(onlySelf);
|
||||
this._status = PENDING;
|
||||
|
||||
if (isPresent(this._parent) && !onlySelf) {
|
||||
this._parent.markAsPending({onlySelf: onlySelf});
|
||||
}
|
||||
}
|
||||
|
||||
setParent(parent: ControlGroup | ControlArray): void { this._parent = parent; }
|
||||
|
||||
updateValueAndValidity(
|
||||
{onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
|
||||
onlySelf = normalizeBool(onlySelf);
|
||||
emitEvent = isPresent(emitEvent) ? emitEvent : true;
|
||||
|
||||
this._updateValue();
|
||||
|
||||
this._errors = this._runValidator();
|
||||
this._status = this._calculateStatus();
|
||||
|
||||
if (this._status == VALID || this._status == PENDING) {
|
||||
this._runAsyncValidator(emitEvent);
|
||||
}
|
||||
|
||||
if (emitEvent) {
|
||||
ObservableWrapper.callEmit(this._valueChanges, this._value);
|
||||
ObservableWrapper.callEmit(this._statusChanges, this._status);
|
||||
}
|
||||
|
||||
if (isPresent(this._parent) && !onlySelf) {
|
||||
this._parent.updateValueAndValidity({onlySelf: onlySelf, emitEvent: emitEvent});
|
||||
}
|
||||
}
|
||||
|
||||
private _runValidator(): {[key: string]: any} {
|
||||
return isPresent(this.validator) ? this.validator(this) : null;
|
||||
}
|
||||
|
||||
private _runAsyncValidator(emitEvent: boolean): void {
|
||||
if (isPresent(this.asyncValidator)) {
|
||||
this._status = PENDING;
|
||||
this._cancelExistingSubscription();
|
||||
var obs = toObservable(this.asyncValidator(this));
|
||||
this._asyncValidationSubscription = ObservableWrapper.subscribe(
|
||||
obs, (res: {[key: string]: any}) => this.setErrors(res, {emitEvent: emitEvent}));
|
||||
}
|
||||
}
|
||||
|
||||
private _cancelExistingSubscription(): void {
|
||||
if (isPresent(this._asyncValidationSubscription)) {
|
||||
ObservableWrapper.dispose(this._asyncValidationSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets errors on a control.
|
||||
*
|
||||
* This is used when validations are run not automatically, but manually by the user.
|
||||
*
|
||||
* Calling `setErrors` will also update the validity of the parent control.
|
||||
*
|
||||
* ## Usage
|
||||
*
|
||||
* ```
|
||||
* var login = new Control("someLogin");
|
||||
* login.setErrors({
|
||||
* "notUnique": true
|
||||
* });
|
||||
*
|
||||
* expect(login.valid).toEqual(false);
|
||||
* expect(login.errors).toEqual({"notUnique": true});
|
||||
*
|
||||
* login.updateValue("someOtherLogin");
|
||||
*
|
||||
* expect(login.valid).toEqual(true);
|
||||
* ```
|
||||
*/
|
||||
setErrors(errors: {[key: string]: any}, {emitEvent}: {emitEvent?: boolean} = {}): void {
|
||||
emitEvent = isPresent(emitEvent) ? emitEvent : true;
|
||||
|
||||
this._errors = errors;
|
||||
this._status = this._calculateStatus();
|
||||
|
||||
if (emitEvent) {
|
||||
ObservableWrapper.callEmit(this._statusChanges, this._status);
|
||||
}
|
||||
|
||||
if (isPresent(this._parent)) {
|
||||
this._parent._updateControlsErrors();
|
||||
}
|
||||
}
|
||||
|
||||
find(path: Array<string | number>| string): AbstractControl { return _find(this, path); }
|
||||
|
||||
getError(errorCode: string, path: string[] = null): any {
|
||||
var control = isPresent(path) && !ListWrapper.isEmpty(path) ? this.find(path) : this;
|
||||
if (isPresent(control) && isPresent(control._errors)) {
|
||||
return StringMapWrapper.get(control._errors, errorCode);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
hasError(errorCode: string, path: string[] = null): boolean {
|
||||
return isPresent(this.getError(errorCode, path));
|
||||
}
|
||||
|
||||
get root(): AbstractControl {
|
||||
let x: AbstractControl = this;
|
||||
|
||||
while (isPresent(x._parent)) {
|
||||
x = x._parent;
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_updateControlsErrors(): void {
|
||||
this._status = this._calculateStatus();
|
||||
|
||||
if (isPresent(this._parent)) {
|
||||
this._parent._updateControlsErrors();
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_initObservables() {
|
||||
this._valueChanges = new EventEmitter();
|
||||
this._statusChanges = new EventEmitter();
|
||||
}
|
||||
|
||||
|
||||
private _calculateStatus(): string {
|
||||
if (isPresent(this._errors)) return INVALID;
|
||||
if (this._anyControlsHaveStatus(PENDING)) return PENDING;
|
||||
if (this._anyControlsHaveStatus(INVALID)) return INVALID;
|
||||
return VALID;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
abstract _updateValue(): void;
|
||||
|
||||
/** @internal */
|
||||
abstract _anyControlsHaveStatus(status: string): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a part of a form that cannot be divided into other controls. `Control`s have values and
|
||||
* validation state, which is determined by an optional validation function.
|
||||
*
|
||||
* `Control` is one of the three fundamental building blocks used to define forms in Angular, along
|
||||
* with {@link ControlGroup} and {@link ControlArray}.
|
||||
*
|
||||
* ## Usage
|
||||
*
|
||||
* By default, a `Control` is created for every `<input>` or other form component.
|
||||
* With {@link NgFormControl} or {@link NgFormModel} an existing {@link Control} can be
|
||||
* bound to a DOM element instead. This `Control` can be configured with a custom
|
||||
* validation function.
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/23DESOpbNnBpBHZt1BR4?p=preview))
|
||||
*/
|
||||
export class Control extends AbstractControl {
|
||||
/** @internal */
|
||||
_onChange: Function;
|
||||
|
||||
constructor(value: any = null, validator: ValidatorFn = null,
|
||||
asyncValidator: AsyncValidatorFn = null) {
|
||||
super(validator, asyncValidator);
|
||||
this._value = value;
|
||||
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
||||
this._initObservables();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of the control to `value`.
|
||||
*
|
||||
* If `onlySelf` is `true`, this change will only affect the validation of this `Control`
|
||||
* and not its parent component. If `emitEvent` is `true`, this change will cause a
|
||||
* `valueChanges` event on the `Control` to be emitted. Both of these options default to
|
||||
* `false`.
|
||||
*
|
||||
* If `emitModelToViewChange` is `true`, the view will be notified about the new value
|
||||
* via an `onChange` event. This is the default behavior if `emitModelToViewChange` is not
|
||||
* specified.
|
||||
*/
|
||||
updateValue(value: any, {onlySelf, emitEvent, emitModelToViewChange}: {
|
||||
onlySelf?: boolean,
|
||||
emitEvent?: boolean,
|
||||
emitModelToViewChange?: boolean
|
||||
} = {}): void {
|
||||
emitModelToViewChange = isPresent(emitModelToViewChange) ? emitModelToViewChange : true;
|
||||
this._value = value;
|
||||
if (isPresent(this._onChange) && emitModelToViewChange) this._onChange(this._value);
|
||||
this.updateValueAndValidity({onlySelf: onlySelf, emitEvent: emitEvent});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_updateValue() {}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_anyControlsHaveStatus(status: string): boolean { return false; }
|
||||
|
||||
/**
|
||||
* Register a listener for change events.
|
||||
*/
|
||||
registerOnChange(fn: Function): void { this._onChange = fn; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a part of a form, of fixed length, that can contain other controls.
|
||||
*
|
||||
* A `ControlGroup` aggregates the values of each {@link Control} in the group.
|
||||
* The status of a `ControlGroup` depends on the status of its children.
|
||||
* If one of the controls in a group is invalid, the entire group is invalid.
|
||||
* Similarly, if a control changes its value, the entire group changes as well.
|
||||
*
|
||||
* `ControlGroup` is one of the three fundamental building blocks used to define forms in Angular,
|
||||
* along with {@link Control} and {@link ControlArray}. {@link ControlArray} can also contain other
|
||||
* controls, but is of variable length.
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/23DESOpbNnBpBHZt1BR4?p=preview))
|
||||
*/
|
||||
export class ControlGroup extends AbstractControl {
|
||||
private _optionals: {[key: string]: boolean};
|
||||
|
||||
constructor(public controls: {[key: string]: AbstractControl},
|
||||
optionals: {[key: string]: boolean} = null, validator: ValidatorFn = null,
|
||||
asyncValidator: AsyncValidatorFn = null) {
|
||||
super(validator, asyncValidator);
|
||||
this._optionals = isPresent(optionals) ? optionals : {};
|
||||
this._initObservables();
|
||||
this._setParentForControls();
|
||||
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a control to this group.
|
||||
*/
|
||||
addControl(name: string, control: AbstractControl): void {
|
||||
this.controls[name] = control;
|
||||
control.setParent(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a control from this group.
|
||||
*/
|
||||
removeControl(name: string): void { StringMapWrapper.delete(this.controls, name); }
|
||||
|
||||
/**
|
||||
* Mark the named control as non-optional.
|
||||
*/
|
||||
include(controlName: string): void {
|
||||
StringMapWrapper.set(this._optionals, controlName, true);
|
||||
this.updateValueAndValidity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the named control as optional.
|
||||
*/
|
||||
exclude(controlName: string): void {
|
||||
StringMapWrapper.set(this._optionals, controlName, false);
|
||||
this.updateValueAndValidity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether there is a control with the given name in the group.
|
||||
*/
|
||||
contains(controlName: string): boolean {
|
||||
var c = StringMapWrapper.contains(this.controls, controlName);
|
||||
return c && this._included(controlName);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_setParentForControls() {
|
||||
StringMapWrapper.forEach(
|
||||
this.controls, (control: AbstractControl, name: string) => { control.setParent(this); });
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_updateValue() { this._value = this._reduceValue(); }
|
||||
|
||||
/** @internal */
|
||||
_anyControlsHaveStatus(status: string): boolean {
|
||||
var res = false;
|
||||
StringMapWrapper.forEach(this.controls, (control: AbstractControl, name: string) => {
|
||||
res = res || (this.contains(name) && control.status == status);
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_reduceValue() {
|
||||
return this._reduceChildren(
|
||||
{}, (acc: {[k: string]: AbstractControl}, control: AbstractControl, name: string) => {
|
||||
acc[name] = control.value;
|
||||
return acc;
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_reduceChildren(initValue: any, fn: Function) {
|
||||
var res = initValue;
|
||||
StringMapWrapper.forEach(this.controls, (control: AbstractControl, name: string) => {
|
||||
if (this._included(name)) {
|
||||
res = fn(res, control, name);
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_included(controlName: string): boolean {
|
||||
var isOptional = StringMapWrapper.contains(this._optionals, controlName);
|
||||
return !isOptional || StringMapWrapper.get(this._optionals, controlName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a part of a form, of variable length, that can contain other controls.
|
||||
*
|
||||
* A `ControlArray` aggregates the values of each {@link Control} in the group.
|
||||
* The status of a `ControlArray` depends on the status of its children.
|
||||
* If one of the controls in a group is invalid, the entire array is invalid.
|
||||
* Similarly, if a control changes its value, the entire array changes as well.
|
||||
*
|
||||
* `ControlArray` is one of the three fundamental building blocks used to define forms in Angular,
|
||||
* along with {@link Control} and {@link ControlGroup}. {@link ControlGroup} can also contain
|
||||
* other controls, but is of fixed length.
|
||||
*
|
||||
* ## Adding or removing controls
|
||||
*
|
||||
* To change the controls in the array, use the `push`, `insert`, or `removeAt` methods
|
||||
* in `ControlArray` itself. These methods ensure the controls are properly tracked in the
|
||||
* form's hierarchy. Do not modify the array of `AbstractControl`s used to instantiate
|
||||
* the `ControlArray` directly, as that will result in strange and unexpected behavior such
|
||||
* as broken change detection.
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/23DESOpbNnBpBHZt1BR4?p=preview))
|
||||
*/
|
||||
export class ControlArray extends AbstractControl {
|
||||
constructor(public controls: AbstractControl[], validator: ValidatorFn = null,
|
||||
asyncValidator: AsyncValidatorFn = null) {
|
||||
super(validator, asyncValidator);
|
||||
this._initObservables();
|
||||
this._setParentForControls();
|
||||
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link AbstractControl} at the given `index` in the array.
|
||||
*/
|
||||
at(index: number): AbstractControl { return this.controls[index]; }
|
||||
|
||||
/**
|
||||
* Insert a new {@link AbstractControl} at the end of the array.
|
||||
*/
|
||||
push(control: AbstractControl): void {
|
||||
this.controls.push(control);
|
||||
control.setParent(this);
|
||||
this.updateValueAndValidity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new {@link AbstractControl} at the given `index` in the array.
|
||||
*/
|
||||
insert(index: number, control: AbstractControl): void {
|
||||
ListWrapper.insert(this.controls, index, control);
|
||||
control.setParent(this);
|
||||
this.updateValueAndValidity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the control at the given `index` in the array.
|
||||
*/
|
||||
removeAt(index: number): void {
|
||||
ListWrapper.removeAt(this.controls, index);
|
||||
this.updateValueAndValidity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Length of the control array.
|
||||
*/
|
||||
get length(): number { return this.controls.length; }
|
||||
|
||||
/** @internal */
|
||||
_updateValue(): void { this._value = this.controls.map((control) => control.value); }
|
||||
|
||||
/** @internal */
|
||||
_anyControlsHaveStatus(status: string): boolean {
|
||||
return this.controls.some(c => c.status == status);
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
_setParentForControls(): void {
|
||||
this.controls.forEach((control) => { control.setParent(this); });
|
||||
}
|
||||
}
|
144
modules/@angular/common/src/forms/validators.ts
Normal file
144
modules/@angular/common/src/forms/validators.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import {isBlank, isPresent, isString} from 'angular2/src/facade/lang';
|
||||
import {PromiseWrapper} from 'angular2/src/facade/promise';
|
||||
import {ObservableWrapper} from 'angular2/src/facade/async';
|
||||
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
import {OpaqueToken} from 'angular2/core';
|
||||
|
||||
import * as modelModule from './model';
|
||||
import {ValidatorFn, AsyncValidatorFn} from './directives/validators';
|
||||
|
||||
/**
|
||||
* Providers for validators to be used for {@link Control}s in a form.
|
||||
*
|
||||
* Provide this using `multi: true` to add validators.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* {@example core/forms/ts/ng_validators/ng_validators.ts region='ng_validators'}
|
||||
*/
|
||||
export const NG_VALIDATORS: OpaqueToken = /*@ts2dart_const*/ new OpaqueToken("NgValidators");
|
||||
|
||||
/**
|
||||
* Providers for asynchronous validators to be used for {@link Control}s
|
||||
* in a form.
|
||||
*
|
||||
* Provide this using `multi: true` to add validators.
|
||||
*
|
||||
* See {@link NG_VALIDATORS} for more details.
|
||||
*/
|
||||
export const NG_ASYNC_VALIDATORS: OpaqueToken =
|
||||
/*@ts2dart_const*/ new OpaqueToken("NgAsyncValidators");
|
||||
|
||||
/**
|
||||
* Provides a set of validators used by form controls.
|
||||
*
|
||||
* A validator is a function that processes a {@link Control} or collection of
|
||||
* controls and returns a map of errors. A null map means that validation has passed.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```typescript
|
||||
* var loginControl = new Control("", Validators.required)
|
||||
* ```
|
||||
*/
|
||||
export class Validators {
|
||||
/**
|
||||
* Validator that requires controls to have a non-empty value.
|
||||
*/
|
||||
static required(control: modelModule.AbstractControl): {[key: string]: boolean} {
|
||||
return isBlank(control.value) || (isString(control.value) && control.value == "") ?
|
||||
{"required": true} :
|
||||
null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validator that requires controls to have a value of a minimum length.
|
||||
*/
|
||||
static minLength(minLength: number): ValidatorFn {
|
||||
return (control: modelModule.AbstractControl): {[key: string]: any} => {
|
||||
if (isPresent(Validators.required(control))) return null;
|
||||
var v: string = control.value;
|
||||
return v.length < minLength ?
|
||||
{"minlength": {"requiredLength": minLength, "actualLength": v.length}} :
|
||||
null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validator that requires controls to have a value of a maximum length.
|
||||
*/
|
||||
static maxLength(maxLength: number): ValidatorFn {
|
||||
return (control: modelModule.AbstractControl): {[key: string]: any} => {
|
||||
if (isPresent(Validators.required(control))) return null;
|
||||
var v: string = control.value;
|
||||
return v.length > maxLength ?
|
||||
{"maxlength": {"requiredLength": maxLength, "actualLength": v.length}} :
|
||||
null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validator that requires a control to match a regex to its value.
|
||||
*/
|
||||
static pattern(pattern: string): ValidatorFn {
|
||||
return (control: modelModule.AbstractControl): {[key: string]: any} => {
|
||||
if (isPresent(Validators.required(control))) return null;
|
||||
let regex = new RegExp(`^${pattern}$`);
|
||||
let v: string = control.value;
|
||||
return regex.test(v) ? null :
|
||||
{"pattern": {"requiredPattern": `^${pattern}$`, "actualValue": v}};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op validator.
|
||||
*/
|
||||
static nullValidator(c: modelModule.AbstractControl): {[key: string]: boolean} { return null; }
|
||||
|
||||
/**
|
||||
* Compose multiple validators into a single function that returns the union
|
||||
* of the individual error maps.
|
||||
*/
|
||||
static compose(validators: ValidatorFn[]): ValidatorFn {
|
||||
if (isBlank(validators)) return null;
|
||||
var presentValidators = validators.filter(isPresent);
|
||||
if (presentValidators.length == 0) return null;
|
||||
|
||||
return function(control: modelModule.AbstractControl) {
|
||||
return _mergeErrors(_executeValidators(control, presentValidators));
|
||||
};
|
||||
}
|
||||
|
||||
static composeAsync(validators: AsyncValidatorFn[]): AsyncValidatorFn {
|
||||
if (isBlank(validators)) return null;
|
||||
var presentValidators = validators.filter(isPresent);
|
||||
if (presentValidators.length == 0) return null;
|
||||
|
||||
return function(control: modelModule.AbstractControl) {
|
||||
let promises = _executeAsyncValidators(control, presentValidators).map(_convertToPromise);
|
||||
return PromiseWrapper.all(promises).then(_mergeErrors);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function _convertToPromise(obj: any): any {
|
||||
return PromiseWrapper.isPromise(obj) ? obj : ObservableWrapper.toPromise(obj);
|
||||
}
|
||||
|
||||
function _executeValidators(control: modelModule.AbstractControl,
|
||||
validators: ValidatorFn[]): any[] {
|
||||
return validators.map(v => v(control));
|
||||
}
|
||||
|
||||
function _executeAsyncValidators(control: modelModule.AbstractControl,
|
||||
validators: AsyncValidatorFn[]): any[] {
|
||||
return validators.map(v => v(control));
|
||||
}
|
||||
|
||||
function _mergeErrors(arrayOfErrors: any[]): {[key: string]: any} {
|
||||
var res: {[key: string]: any} =
|
||||
arrayOfErrors.reduce((res: {[key: string]: any}, errors: {[key: string]: any}) => {
|
||||
return isPresent(errors) ? StringMapWrapper.merge(res, errors) : res;
|
||||
}, {});
|
||||
return StringMapWrapper.isEmpty(res) ? null : res;
|
||||
}
|
5
modules/@angular/common/src/location.ts
Normal file
5
modules/@angular/common/src/location.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './browser/location/platform_location';
|
||||
export * from './browser/location/location_strategy';
|
||||
export * from './browser/location/hash_location_strategy';
|
||||
export * from './browser/location/path_location_strategy';
|
||||
export * from './browser/location/location';
|
101
modules/@angular/common/src/location/hash_location_strategy.ts
Normal file
101
modules/@angular/common/src/location/hash_location_strategy.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import {Injectable, Inject, Optional} from 'angular2/core';
|
||||
import {LocationStrategy, APP_BASE_HREF} from './location_strategy';
|
||||
import {Location} from './location';
|
||||
import {UrlChangeListener, PlatformLocation} from './platform_location';
|
||||
import {isPresent} from 'angular2/src/facade/lang';
|
||||
|
||||
/**
|
||||
* `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
|
||||
*
|
||||
* ```
|
||||
* import {Component, provide} from 'angular2/core';
|
||||
* import {
|
||||
* Location,
|
||||
* LocationStrategy,
|
||||
* HashLocationStrategy
|
||||
* } from 'angular2/platform/common';
|
||||
* import {
|
||||
* ROUTER_DIRECTIVES,
|
||||
* ROUTER_PROVIDERS,
|
||||
* RouteConfig
|
||||
* } from 'angular2/router';
|
||||
*
|
||||
* @Component({directives: [ROUTER_DIRECTIVES]})
|
||||
* @RouteConfig([
|
||||
* {...},
|
||||
* ])
|
||||
* class AppCmp {
|
||||
* constructor(location: Location) {
|
||||
* location.go('/foo');
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* bootstrap(AppCmp, [
|
||||
* ROUTER_PROVIDERS,
|
||||
* provide(LocationStrategy, {useClass: HashLocationStrategy})
|
||||
* ]);
|
||||
* ```
|
||||
*/
|
||||
@Injectable()
|
||||
export class HashLocationStrategy extends LocationStrategy {
|
||||
private _baseHref: string = '';
|
||||
constructor(private _platformLocation: PlatformLocation,
|
||||
@Optional() @Inject(APP_BASE_HREF) _baseHref?: string) {
|
||||
super();
|
||||
if (isPresent(_baseHref)) {
|
||||
this._baseHref = _baseHref;
|
||||
}
|
||||
}
|
||||
|
||||
onPopState(fn: UrlChangeListener): void {
|
||||
this._platformLocation.onPopState(fn);
|
||||
this._platformLocation.onHashChange(fn);
|
||||
}
|
||||
|
||||
getBaseHref(): string { return this._baseHref; }
|
||||
|
||||
path(): string {
|
||||
// the hash value is always prefixed with a `#`
|
||||
// and if it is empty then it will stay empty
|
||||
var path = this._platformLocation.hash;
|
||||
if (!isPresent(path)) path = '#';
|
||||
|
||||
// Dart will complain if a call to substring is
|
||||
// executed with a position value that extends the
|
||||
// length of string.
|
||||
return (path.length > 0 ? path.substring(1) : path);
|
||||
}
|
||||
|
||||
prepareExternalUrl(internal: string): string {
|
||||
var url = Location.joinWithSlash(this._baseHref, internal);
|
||||
return url.length > 0 ? ('#' + url) : url;
|
||||
}
|
||||
|
||||
pushState(state: any, title: string, path: string, queryParams: string) {
|
||||
var 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) {
|
||||
var 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(); }
|
||||
}
|
179
modules/@angular/common/src/location/location.ts
Normal file
179
modules/@angular/common/src/location/location.ts
Normal file
@ -0,0 +1,179 @@
|
||||
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
|
||||
import {Injectable, Inject} from 'angular2/core';
|
||||
import {LocationStrategy} from './location_strategy';
|
||||
|
||||
/**
|
||||
* `Location` is a service that applications can use to interact with a browser's URL.
|
||||
* 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
|
||||
*
|
||||
* ```
|
||||
* import {Component} from 'angular2/core';
|
||||
* import {Location} from 'angular2/platform/common';
|
||||
* import {
|
||||
* ROUTER_DIRECTIVES,
|
||||
* ROUTER_PROVIDERS,
|
||||
* RouteConfig
|
||||
* } from 'angular2/router';
|
||||
*
|
||||
* @Component({directives: [ROUTER_DIRECTIVES]})
|
||||
* @RouteConfig([
|
||||
* {...},
|
||||
* ])
|
||||
* class AppCmp {
|
||||
* constructor(location: Location) {
|
||||
* location.go('/foo');
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* bootstrap(AppCmp, [ROUTER_PROVIDERS]);
|
||||
* ```
|
||||
*/
|
||||
@Injectable()
|
||||
export class Location {
|
||||
/** @internal */
|
||||
_subject: EventEmitter<any> = new EventEmitter();
|
||||
/** @internal */
|
||||
_baseHref: string;
|
||||
|
||||
constructor(public platformStrategy: LocationStrategy) {
|
||||
var browserBaseHref = this.platformStrategy.getBaseHref();
|
||||
this._baseHref = Location.stripTrailingSlash(_stripIndexHtml(browserBaseHref));
|
||||
this.platformStrategy.onPopState((ev) => {
|
||||
ObservableWrapper.callEmit(this._subject, {'url': this.path(), 'pop': true, 'type': ev.type});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normalized URL path.
|
||||
*/
|
||||
path(): string { return this.normalize(this.platformStrategy.path()); }
|
||||
|
||||
/**
|
||||
* 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.length > 0 && !url.startsWith('/')) {
|
||||
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: any) => void, onThrow: (exception: any) => void = null,
|
||||
onReturn: () => void = null): Object {
|
||||
return ObservableWrapper.subscribe(this._subject, onNext, onThrow, onReturn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string of url parameters, prepend with '?' if needed, otherwise return parameters as
|
||||
* is.
|
||||
*/
|
||||
public static normalizeQueryParams(params: string): string {
|
||||
return (params.length > 0 && params.substring(0, 1) != '?') ? ('?' + 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;
|
||||
}
|
||||
var 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 {
|
||||
if (/\/$/g.test(url)) {
|
||||
url = url.substring(0, url.length - 1);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
function _stripBaseHref(baseHref: string, url: string): string {
|
||||
if (baseHref.length > 0 && url.startsWith(baseHref)) {
|
||||
return url.substring(baseHref.length);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function _stripIndexHtml(url: string): string {
|
||||
if (/\/index.html$/g.test(url)) {
|
||||
// '/index.html'.length == 11
|
||||
return url.substring(0, url.length - 11);
|
||||
}
|
||||
return url;
|
||||
}
|
61
modules/@angular/common/src/location/location_strategy.ts
Normal file
61
modules/@angular/common/src/location/location_strategy.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import {OpaqueToken} from 'angular2/core';
|
||||
import {UrlChangeListener} 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} (default).
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
export abstract class LocationStrategy {
|
||||
abstract path(): 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: UrlChangeListener): 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
|
||||
*
|
||||
* ```
|
||||
* import {Component} from 'angular2/core';
|
||||
* import {ROUTER_DIRECTIVES, ROUTER_PROVIDERS, RouteConfig} from 'angular2/router';
|
||||
* import {APP_BASE_HREF} from 'angular2/platform/common';
|
||||
*
|
||||
* @Component({directives: [ROUTER_DIRECTIVES]})
|
||||
* @RouteConfig([
|
||||
* {...},
|
||||
* ])
|
||||
* class AppCmp {
|
||||
* // ...
|
||||
* }
|
||||
*
|
||||
* bootstrap(AppCmp, [
|
||||
* ROUTER_PROVIDERS,
|
||||
* provide(APP_BASE_HREF, {useValue: '/my/app'})
|
||||
* ]);
|
||||
* ```
|
||||
*/
|
||||
export const APP_BASE_HREF: OpaqueToken = /*@ts2dart_const*/ new OpaqueToken('appBaseHref');
|
105
modules/@angular/common/src/location/path_location_strategy.ts
Normal file
105
modules/@angular/common/src/location/path_location_strategy.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import {Injectable, Inject, Optional} from 'angular2/core';
|
||||
import {isBlank} from 'angular2/src/facade/lang';
|
||||
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||
import {PlatformLocation, UrlChangeListener} from './platform_location';
|
||||
import {LocationStrategy, APP_BASE_HREF} from './location_strategy';
|
||||
import {Location} from './location';
|
||||
|
||||
/**
|
||||
* `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.
|
||||
*
|
||||
* `PathLocationStrategy` is the default binding for {@link LocationStrategy}
|
||||
* provided in {@link ROUTER_PROVIDERS}.
|
||||
*
|
||||
* If you're using `PathLocationStrategy`, you must provide a provider for
|
||||
* {@link APP_BASE_HREF} to a string representing the URL prefix that should
|
||||
* 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`.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```
|
||||
* import {Component, provide} from 'angular2/core';
|
||||
* import {bootstrap} from 'angular2/platform/browser';
|
||||
* import {
|
||||
* Location,
|
||||
* APP_BASE_HREF
|
||||
* } from 'angular2/platform/common';
|
||||
* import {
|
||||
* ROUTER_DIRECTIVES,
|
||||
* ROUTER_PROVIDERS,
|
||||
* RouteConfig
|
||||
* } from 'angular2/router';
|
||||
*
|
||||
* @Component({directives: [ROUTER_DIRECTIVES]})
|
||||
* @RouteConfig([
|
||||
* {...},
|
||||
* ])
|
||||
* class AppCmp {
|
||||
* constructor(location: Location) {
|
||||
* location.go('/foo');
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* bootstrap(AppCmp, [
|
||||
* ROUTER_PROVIDERS, // includes binding to PathLocationStrategy
|
||||
* provide(APP_BASE_HREF, {useValue: '/my/app'})
|
||||
* ]);
|
||||
* ```
|
||||
*/
|
||||
@Injectable()
|
||||
export class PathLocationStrategy extends LocationStrategy {
|
||||
private _baseHref: string;
|
||||
|
||||
constructor(private _platformLocation: PlatformLocation,
|
||||
@Optional() @Inject(APP_BASE_HREF) href?: string) {
|
||||
super();
|
||||
|
||||
if (isBlank(href)) {
|
||||
href = this._platformLocation.getBaseHrefFromDOM();
|
||||
}
|
||||
|
||||
if (isBlank(href)) {
|
||||
throw new BaseException(
|
||||
`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: UrlChangeListener): 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(): string {
|
||||
return this._platformLocation.pathname +
|
||||
Location.normalizeQueryParams(this._platformLocation.search);
|
||||
}
|
||||
|
||||
pushState(state: any, title: string, url: string, queryParams: string) {
|
||||
var externalUrl = this.prepareExternalUrl(url + Location.normalizeQueryParams(queryParams));
|
||||
this._platformLocation.pushState(state, title, externalUrl);
|
||||
}
|
||||
|
||||
replaceState(state: any, title: string, url: string, queryParams: string) {
|
||||
var externalUrl = this.prepareExternalUrl(url + Location.normalizeQueryParams(queryParams));
|
||||
this._platformLocation.replaceState(state, title, externalUrl);
|
||||
}
|
||||
|
||||
forward(): void { this._platformLocation.forward(); }
|
||||
|
||||
back(): void { this._platformLocation.back(); }
|
||||
}
|
48
modules/@angular/common/src/location/platform_location.ts
Normal file
48
modules/@angular/common/src/location/platform_location.ts
Normal file
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export abstract class PlatformLocation {
|
||||
abstract getBaseHrefFromDOM(): string;
|
||||
abstract onPopState(fn: UrlChangeListener): void;
|
||||
abstract onHashChange(fn: UrlChangeListener): void;
|
||||
|
||||
/* abstract */ get pathname(): string { return null; }
|
||||
/* abstract */ get search(): string { return null; }
|
||||
/* abstract */ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* A serializable version of the event from onPopState or onHashChange
|
||||
*/
|
||||
export interface UrlChangeEvent { type: string; }
|
||||
|
||||
export interface UrlChangeListener { (e: UrlChangeEvent): any; }
|
17
modules/@angular/common/src/pipes.ts
Normal file
17
modules/@angular/common/src/pipes.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @module
|
||||
* @description
|
||||
* This module provides a set of common Pipes.
|
||||
*/
|
||||
|
||||
export {AsyncPipe} from './pipes/async_pipe';
|
||||
export {DatePipe} from './pipes/date_pipe';
|
||||
export {JsonPipe} from './pipes/json_pipe';
|
||||
export {SlicePipe} from './pipes/slice_pipe';
|
||||
export {LowerCasePipe} from './pipes/lowercase_pipe';
|
||||
export {NumberPipe, DecimalPipe, PercentPipe, CurrencyPipe} from './pipes/number_pipe';
|
||||
export {UpperCasePipe} from './pipes/uppercase_pipe';
|
||||
export {ReplacePipe} from './pipes/replace_pipe';
|
||||
export {I18nPluralPipe} from './pipes/i18n_plural_pipe';
|
||||
export {I18nSelectPipe} from './pipes/i18n_select_pipe';
|
||||
export {COMMON_PIPES} from './pipes/common_pipes';
|
129
modules/@angular/common/src/pipes/async_pipe.ts
Normal file
129
modules/@angular/common/src/pipes/async_pipe.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import {isBlank, isPresent, isPromise} from 'angular2/src/facade/lang';
|
||||
import {ObservableWrapper, Observable, EventEmitter} from 'angular2/src/facade/async';
|
||||
import {Pipe, Injectable, ChangeDetectorRef, OnDestroy, WrappedValue} from 'angular2/core';
|
||||
|
||||
import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception';
|
||||
|
||||
class ObservableStrategy {
|
||||
createSubscription(async: any, updateLatestValue: any): any {
|
||||
return ObservableWrapper.subscribe(async, updateLatestValue, e => { throw e; });
|
||||
}
|
||||
|
||||
dispose(subscription: any): void { ObservableWrapper.dispose(subscription); }
|
||||
|
||||
onDestroy(subscription: any): void { ObservableWrapper.dispose(subscription); }
|
||||
}
|
||||
|
||||
class PromiseStrategy {
|
||||
createSubscription(async: Promise<any>, updateLatestValue: (v: any) => any): any {
|
||||
return async.then(updateLatestValue);
|
||||
}
|
||||
|
||||
dispose(subscription: any): void {}
|
||||
|
||||
onDestroy(subscription: any): void {}
|
||||
}
|
||||
|
||||
var _promiseStrategy = new PromiseStrategy();
|
||||
var _observableStrategy = new ObservableStrategy();
|
||||
var __unused: Promise<any>; // avoid unused import when Promise union types are erased
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* This example binds a `Promise` to the view. Clicking the `Resolve` button resolves the
|
||||
* promise.
|
||||
*
|
||||
* {@example core/pipes/ts/async_pipe/async_pipe_example.ts region='AsyncPipe'}
|
||||
*
|
||||
* It's also possible to use `async` with Observables. The example below binds the `time` Observable
|
||||
* to the view. Every 500ms, the `time` Observable updates the view with the current time.
|
||||
*
|
||||
* ```typescript
|
||||
* ```
|
||||
*/
|
||||
@Pipe({name: 'async', pure: false})
|
||||
@Injectable()
|
||||
export class AsyncPipe implements OnDestroy {
|
||||
/** @internal */
|
||||
_latestValue: Object = null;
|
||||
/** @internal */
|
||||
_latestReturnedValue: Object = null;
|
||||
|
||||
/** @internal */
|
||||
_subscription: Object = null;
|
||||
/** @internal */
|
||||
_obj: Observable<any>| Promise<any>| EventEmitter<any> = null;
|
||||
private _strategy: any = null;
|
||||
/** @internal */
|
||||
public _ref: ChangeDetectorRef;
|
||||
constructor(_ref: ChangeDetectorRef) { this._ref = _ref; }
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (isPresent(this._subscription)) {
|
||||
this._dispose();
|
||||
}
|
||||
}
|
||||
|
||||
transform(obj: Observable<any>| Promise<any>| EventEmitter<any>): any {
|
||||
if (isBlank(this._obj)) {
|
||||
if (isPresent(obj)) {
|
||||
this._subscribe(obj);
|
||||
}
|
||||
this._latestReturnedValue = this._latestValue;
|
||||
return this._latestValue;
|
||||
}
|
||||
|
||||
if (obj !== this._obj) {
|
||||
this._dispose();
|
||||
return this.transform(obj);
|
||||
}
|
||||
|
||||
if (this._latestValue === this._latestReturnedValue) {
|
||||
return this._latestReturnedValue;
|
||||
} else {
|
||||
this._latestReturnedValue = this._latestValue;
|
||||
return WrappedValue.wrap(this._latestValue);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_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));
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_selectStrategy(obj: Observable<any>| Promise<any>| EventEmitter<any>): any {
|
||||
if (isPromise(obj)) {
|
||||
return _promiseStrategy;
|
||||
} else if (ObservableWrapper.isObservable(obj)) {
|
||||
return _observableStrategy;
|
||||
} else {
|
||||
throw new InvalidPipeArgumentException(AsyncPipe, obj);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_dispose(): void {
|
||||
this._strategy.dispose(this._subscription);
|
||||
this._latestValue = null;
|
||||
this._latestReturnedValue = null;
|
||||
this._subscription = null;
|
||||
this._obj = null;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_updateLatestValue(async: any, value: Object) {
|
||||
if (async === this._obj) {
|
||||
this._latestValue = value;
|
||||
this._ref.markForCheck();
|
||||
}
|
||||
}
|
||||
}
|
37
modules/@angular/common/src/pipes/common_pipes.ts
Normal file
37
modules/@angular/common/src/pipes/common_pipes.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @module
|
||||
* @description
|
||||
* This module provides a set of common Pipes.
|
||||
*/
|
||||
import {AsyncPipe} from './async_pipe';
|
||||
import {UpperCasePipe} from './uppercase_pipe';
|
||||
import {LowerCasePipe} from './lowercase_pipe';
|
||||
import {JsonPipe} from './json_pipe';
|
||||
import {SlicePipe} from './slice_pipe';
|
||||
import {DatePipe} from './date_pipe';
|
||||
import {DecimalPipe, PercentPipe, CurrencyPipe} from './number_pipe';
|
||||
import {ReplacePipe} from './replace_pipe';
|
||||
import {I18nPluralPipe} from './i18n_plural_pipe';
|
||||
import {I18nSelectPipe} from './i18n_select_pipe';
|
||||
|
||||
/**
|
||||
* A collection of Angular core pipes that are likely to be used in each and every
|
||||
* application.
|
||||
*
|
||||
* This collection can be used to quickly enumerate all the built-in pipes in the `pipes`
|
||||
* property of the `@Component` decorator.
|
||||
*/
|
||||
export const COMMON_PIPES = /*@ts2dart_const*/[
|
||||
AsyncPipe,
|
||||
UpperCasePipe,
|
||||
LowerCasePipe,
|
||||
JsonPipe,
|
||||
SlicePipe,
|
||||
DecimalPipe,
|
||||
PercentPipe,
|
||||
CurrencyPipe,
|
||||
DatePipe,
|
||||
ReplacePipe,
|
||||
I18nPluralPipe,
|
||||
I18nSelectPipe
|
||||
];
|
119
modules/@angular/common/src/pipes/date_pipe.ts
Normal file
119
modules/@angular/common/src/pipes/date_pipe.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import {
|
||||
isDate,
|
||||
isNumber,
|
||||
isPresent,
|
||||
Date,
|
||||
DateWrapper,
|
||||
isBlank,
|
||||
FunctionWrapper
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {DateFormatter} from 'angular2/src/facade/intl';
|
||||
import {PipeTransform, WrappedValue, Pipe, Injectable} from 'angular2/core';
|
||||
import {StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception';
|
||||
|
||||
|
||||
// TODO: move to a global configurable location along with other i18n components.
|
||||
var defaultLocale: string = 'en-US';
|
||||
|
||||
/**
|
||||
* Formats a date value to a string based on the requested format.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* ## Usage
|
||||
*
|
||||
* expression | date[:format]
|
||||
*
|
||||
* where `expression` is a date object or a number (milliseconds since UTC epoch) and
|
||||
* `format` indicates which date/time components to include:
|
||||
*
|
||||
* | Component | Symbol | Short Form | Long Form | Numeric | 2-digit |
|
||||
* |-----------|:------:|--------------|-------------------|-----------|-----------|
|
||||
* | era | G | G (AD) | GGGG (Anno Domini)| - | - |
|
||||
* | year | y | - | - | y (2015) | yy (15) |
|
||||
* | month | M | MMM (Sep) | MMMM (September) | M (9) | MM (09) |
|
||||
* | day | d | - | - | d (3) | dd (03) |
|
||||
* | weekday | E | 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) | - | - | - |
|
||||
*
|
||||
* In javascript, only the components specified will be respected (not the ordering,
|
||||
* punctuations, ...) and details of the formatting will be dependent on the locale.
|
||||
* On the other hand in Dart version, you can also include quoted text as well as some extra
|
||||
* date/time components such as quarter. For more information see:
|
||||
* https://api.dartlang.org/apidocs/channels/stable/dartdoc-viewer/intl/intl.DateFormat.
|
||||
*
|
||||
* `format` can also be one of the following predefined formats:
|
||||
*
|
||||
* - `'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)
|
||||
* - `'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)
|
||||
*
|
||||
* Timezone of the formatted text will be the local system timezone of the end-users machine.
|
||||
*
|
||||
* ### 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 core/pipes/ts/date_pipe/date_pipe_example.ts region='DatePipe'}
|
||||
*/
|
||||
@Pipe({name: 'date', pure: true})
|
||||
@Injectable()
|
||||
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'
|
||||
};
|
||||
|
||||
|
||||
transform(value: any, pattern: string = 'mediumDate'): string {
|
||||
if (isBlank(value)) return null;
|
||||
|
||||
if (!this.supports(value)) {
|
||||
throw new InvalidPipeArgumentException(DatePipe, value);
|
||||
}
|
||||
|
||||
if (isNumber(value)) {
|
||||
value = DateWrapper.fromMillis(value);
|
||||
}
|
||||
if (StringMapWrapper.contains(DatePipe._ALIASES, pattern)) {
|
||||
pattern = <string>StringMapWrapper.get(DatePipe._ALIASES, pattern);
|
||||
}
|
||||
return DateFormatter.format(value, defaultLocale, pattern);
|
||||
}
|
||||
|
||||
supports(obj: any): boolean { return isDate(obj) || isNumber(obj); }
|
||||
}
|
54
modules/@angular/common/src/pipes/i18n_plural_pipe.ts
Normal file
54
modules/@angular/common/src/pipes/i18n_plural_pipe.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import {isStringMap, StringWrapper, isPresent, RegExpWrapper} from 'angular2/src/facade/lang';
|
||||
import {Injectable, PipeTransform, Pipe} from 'angular2/core';
|
||||
import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception';
|
||||
|
||||
var interpolationExp: RegExp = RegExpWrapper.create('#');
|
||||
|
||||
/**
|
||||
*
|
||||
* Maps a value to a string that pluralizes the value properly.
|
||||
*
|
||||
* ## Usage
|
||||
*
|
||||
* expression | i18nPlural:mapping
|
||||
*
|
||||
* where `expression` is a number and `mapping` is an object that indicates the proper text for
|
||||
* when the `expression` evaluates to 0, 1, or some other number. You can interpolate the actual
|
||||
* value into the text using the `#` sign.
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* ```
|
||||
* <div>
|
||||
* {{ messages.length | i18nPlural: messageMapping }}
|
||||
* </div>
|
||||
*
|
||||
* class MyApp {
|
||||
* messages: any[];
|
||||
* messageMapping: any = {
|
||||
* '=0': 'No messages.',
|
||||
* '=1': 'One message.',
|
||||
* 'other': '# messages.'
|
||||
* }
|
||||
* ...
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
@Pipe({name: 'i18nPlural', pure: true})
|
||||
@Injectable()
|
||||
export class I18nPluralPipe implements PipeTransform {
|
||||
transform(value: number, pluralMap: {[count: string]: string}): string {
|
||||
var key: string;
|
||||
var valueStr: string;
|
||||
|
||||
if (!isStringMap(pluralMap)) {
|
||||
throw new InvalidPipeArgumentException(I18nPluralPipe, pluralMap);
|
||||
}
|
||||
|
||||
key = value === 0 || value === 1 ? `=${value}` : 'other';
|
||||
valueStr = isPresent(value) ? value.toString() : '';
|
||||
|
||||
return StringWrapper.replaceAll(pluralMap[key], interpolationExp, valueStr);
|
||||
}
|
||||
}
|
45
modules/@angular/common/src/pipes/i18n_select_pipe.ts
Normal file
45
modules/@angular/common/src/pipes/i18n_select_pipe.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import {isStringMap} from 'angular2/src/facade/lang';
|
||||
import {StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
import {Injectable, PipeTransform, Pipe} from 'angular2/core';
|
||||
import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception';
|
||||
|
||||
/**
|
||||
*
|
||||
* Generic selector that displays the string that matches the current value.
|
||||
*
|
||||
* ## Usage
|
||||
*
|
||||
* expression | i18nSelect:mapping
|
||||
*
|
||||
* where `mapping` is an object that indicates the text that should be displayed
|
||||
* for different values of the provided `expression`.
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* ```
|
||||
* <div>
|
||||
* {{ gender | i18nSelect: inviteMap }}
|
||||
* </div>
|
||||
*
|
||||
* class MyApp {
|
||||
* gender: string = 'male';
|
||||
* inviteMap: any = {
|
||||
* 'male': 'Invite her.',
|
||||
* 'female': 'Invite him.',
|
||||
* 'other': 'Invite them.'
|
||||
* }
|
||||
* ...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Pipe({name: 'i18nSelect', pure: true})
|
||||
@Injectable()
|
||||
export class I18nSelectPipe implements PipeTransform {
|
||||
transform(value: string, mapping: {[key: string]: string}): string {
|
||||
if (!isStringMap(mapping)) {
|
||||
throw new InvalidPipeArgumentException(I18nSelectPipe, mapping);
|
||||
}
|
||||
|
||||
return StringMapWrapper.contains(mapping, value) ? mapping[value] : mapping['other'];
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import {Type, stringify} from 'angular2/src/facade/lang';
|
||||
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
|
||||
|
||||
export class InvalidPipeArgumentException extends BaseException {
|
||||
constructor(type: Type, value: Object) {
|
||||
super(`Invalid argument '${value}' for pipe '${stringify(type)}'`);
|
||||
}
|
||||
}
|
15
modules/@angular/common/src/pipes/json_pipe.ts
Normal file
15
modules/@angular/common/src/pipes/json_pipe.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {isBlank, isPresent, Json} from 'angular2/src/facade/lang';
|
||||
import {Injectable, PipeTransform, WrappedValue, Pipe} from 'angular2/core';
|
||||
|
||||
/**
|
||||
* Transforms any input value using `JSON.stringify`. Useful for debugging.
|
||||
*
|
||||
* ### Example
|
||||
* {@example core/pipes/ts/json_pipe/json_pipe_example.ts region='JsonPipe'}
|
||||
*/
|
||||
/* @ts2dart_const */
|
||||
@Pipe({name: 'json', pure: false})
|
||||
@Injectable()
|
||||
export class JsonPipe implements PipeTransform {
|
||||
transform(value: any): string { return Json.stringify(value); }
|
||||
}
|
22
modules/@angular/common/src/pipes/lowercase_pipe.ts
Normal file
22
modules/@angular/common/src/pipes/lowercase_pipe.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import {isString, isBlank} from 'angular2/src/facade/lang';
|
||||
import {Injectable, PipeTransform, WrappedValue, Pipe} from 'angular2/core';
|
||||
import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception';
|
||||
|
||||
/**
|
||||
* Transforms text to lowercase.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* {@example core/pipes/ts/lowerupper_pipe/lowerupper_pipe_example.ts region='LowerUpperPipe'}
|
||||
*/
|
||||
@Pipe({name: 'lowercase'})
|
||||
@Injectable()
|
||||
export class LowerCasePipe implements PipeTransform {
|
||||
transform(value: string): string {
|
||||
if (isBlank(value)) return value;
|
||||
if (!isString(value)) {
|
||||
throw new InvalidPipeArgumentException(LowerCasePipe, value);
|
||||
}
|
||||
return value.toLowerCase();
|
||||
}
|
||||
}
|
143
modules/@angular/common/src/pipes/number_pipe.ts
Normal file
143
modules/@angular/common/src/pipes/number_pipe.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import {
|
||||
isNumber,
|
||||
isPresent,
|
||||
isBlank,
|
||||
StringWrapper,
|
||||
NumberWrapper,
|
||||
RegExpWrapper,
|
||||
FunctionWrapper
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
|
||||
import {NumberFormatter, NumberFormatStyle} from 'angular2/src/facade/intl';
|
||||
import {Injectable, PipeTransform, WrappedValue, Pipe} from 'angular2/core';
|
||||
|
||||
import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception';
|
||||
|
||||
var defaultLocale: string = 'en-US';
|
||||
var _re = RegExpWrapper.create('^(\\d+)?\\.((\\d+)(\\-(\\d+))?)?$');
|
||||
|
||||
/**
|
||||
* Internal base class for numeric pipes.
|
||||
*/
|
||||
@Injectable()
|
||||
export class NumberPipe {
|
||||
/** @internal */
|
||||
static _format(value: number, style: NumberFormatStyle, digits: string, currency: string = null,
|
||||
currencyAsSymbol: boolean = false): string {
|
||||
if (isBlank(value)) return null;
|
||||
if (!isNumber(value)) {
|
||||
throw new InvalidPipeArgumentException(NumberPipe, value);
|
||||
}
|
||||
var minInt = 1, minFraction = 0, maxFraction = 3;
|
||||
if (isPresent(digits)) {
|
||||
var parts = RegExpWrapper.firstMatch(_re, digits);
|
||||
if (isBlank(parts)) {
|
||||
throw new BaseException(`${digits} is not a valid digit info for number pipes`);
|
||||
}
|
||||
if (isPresent(parts[1])) { // min integer digits
|
||||
minInt = NumberWrapper.parseIntAutoRadix(parts[1]);
|
||||
}
|
||||
if (isPresent(parts[3])) { // min fraction digits
|
||||
minFraction = NumberWrapper.parseIntAutoRadix(parts[3]);
|
||||
}
|
||||
if (isPresent(parts[5])) { // max fraction digits
|
||||
maxFraction = NumberWrapper.parseIntAutoRadix(parts[5]);
|
||||
}
|
||||
}
|
||||
return NumberFormatter.format(value, defaultLocale, style, {
|
||||
minimumIntegerDigits: minInt,
|
||||
minimumFractionDigits: minFraction,
|
||||
maximumFractionDigits: maxFraction,
|
||||
currency: currency,
|
||||
currencyAsSymbol: currencyAsSymbol
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: this pipe uses the Internationalization API.
|
||||
* Therefore it is only reliable in Chrome and Opera browsers.
|
||||
*
|
||||
* Formats a number as local text. i.e. group sizing and separator and other locale-specific
|
||||
* configurations are based on the active locale.
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* expression | number[:digitInfo]
|
||||
*
|
||||
* where `expression` is a number and `digitInfo` has the following format:
|
||||
*
|
||||
* {minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}
|
||||
*
|
||||
* - 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.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* {@example core/pipes/ts/number_pipe/number_pipe_example.ts region='NumberPipe'}
|
||||
*/
|
||||
@Pipe({name: 'number'})
|
||||
@Injectable()
|
||||
export class DecimalPipe extends NumberPipe implements PipeTransform {
|
||||
transform(value: any, digits: string = null): string {
|
||||
return NumberPipe._format(value, NumberFormatStyle.Decimal, digits);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: this pipe uses the Internationalization API.
|
||||
* Therefore it is only reliable in Chrome and Opera browsers.
|
||||
*
|
||||
* Formats a number as local percent.
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* expression | percent[:digitInfo]
|
||||
*
|
||||
* For more information about `digitInfo` see {@link DecimalPipe}
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* {@example core/pipes/ts/number_pipe/number_pipe_example.ts region='PercentPipe'}
|
||||
*/
|
||||
@Pipe({name: 'percent'})
|
||||
@Injectable()
|
||||
export class PercentPipe extends NumberPipe implements PipeTransform {
|
||||
transform(value: any, digits: string = null): string {
|
||||
return NumberPipe._format(value, NumberFormatStyle.Percent, digits);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: this pipe uses the Internationalization API.
|
||||
* Therefore it is only reliable in Chrome and Opera browsers.
|
||||
*
|
||||
* Formats a number as local currency.
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* expression | currency[:currencyCode[:symbolDisplay[:digitInfo]]]
|
||||
*
|
||||
* where `currencyCode` is the 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 (e.g. $) or the currency code (e.g. USD) in the output. The default for this value
|
||||
* is `false`.
|
||||
* For more information about `digitInfo` see {@link DecimalPipe}
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* {@example core/pipes/ts/number_pipe/number_pipe_example.ts region='CurrencyPipe'}
|
||||
*/
|
||||
@Pipe({name: 'currency'})
|
||||
@Injectable()
|
||||
export class CurrencyPipe extends NumberPipe implements PipeTransform {
|
||||
transform(value: any, currencyCode: string = 'USD', symbolDisplay: boolean = false,
|
||||
digits: string = null): string {
|
||||
return NumberPipe._format(value, NumberFormatStyle.Currency, digits, currencyCode,
|
||||
symbolDisplay);
|
||||
}
|
||||
}
|
83
modules/@angular/common/src/pipes/replace_pipe.ts
Normal file
83
modules/@angular/common/src/pipes/replace_pipe.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import {
|
||||
isBlank,
|
||||
isString,
|
||||
isNumber,
|
||||
isFunction,
|
||||
RegExpWrapper,
|
||||
StringWrapper
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {Injectable, PipeTransform, Pipe} from 'angular2/core';
|
||||
import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception';
|
||||
|
||||
/**
|
||||
* Creates a new String with some or all of the matches of a pattern replaced by
|
||||
* a replacement.
|
||||
*
|
||||
* The pattern to be matched is specified by the 'pattern' parameter.
|
||||
*
|
||||
* The replacement to be set is specified by the 'replacement' parameter.
|
||||
*
|
||||
* An optional 'flags' parameter can be set.
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* expression | replace:pattern:replacement
|
||||
*
|
||||
* All behavior is based on the expected behavior of the JavaScript API
|
||||
* String.prototype.replace() function.
|
||||
*
|
||||
* Where the input expression is a [String] or [Number] (to be treated as a string),
|
||||
* the `pattern` is a [String] or [RegExp],
|
||||
* the 'replacement' is a [String] or [Function].
|
||||
*
|
||||
* --Note--: The 'pattern' parameter will be converted to a RegExp instance. Make sure to escape the
|
||||
* string properly if you are matching for regular expression special characters like parenthesis,
|
||||
* brackets etc.
|
||||
*/
|
||||
|
||||
@Pipe({name: 'replace'})
|
||||
@Injectable()
|
||||
export class ReplacePipe implements PipeTransform {
|
||||
transform(value: any, pattern: string | RegExp, replacement: Function | string): any {
|
||||
if (isBlank(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (!this._supportedInput(value)) {
|
||||
throw new InvalidPipeArgumentException(ReplacePipe, value);
|
||||
}
|
||||
|
||||
var input = value.toString();
|
||||
|
||||
if (!this._supportedPattern(pattern)) {
|
||||
throw new InvalidPipeArgumentException(ReplacePipe, pattern);
|
||||
}
|
||||
if (!this._supportedReplacement(replacement)) {
|
||||
throw new InvalidPipeArgumentException(ReplacePipe, replacement);
|
||||
}
|
||||
// template fails with literal RegExp e.g /pattern/igm
|
||||
// var rgx = pattern instanceof RegExp ? pattern : RegExpWrapper.create(pattern);
|
||||
|
||||
if (isFunction(replacement)) {
|
||||
var rgxPattern = isString(pattern) ? RegExpWrapper.create(<string>pattern) : <RegExp>pattern;
|
||||
|
||||
return StringWrapper.replaceAllMapped(input, rgxPattern, <Function>replacement);
|
||||
}
|
||||
if (pattern instanceof RegExp) {
|
||||
// use the replaceAll variant
|
||||
return StringWrapper.replaceAll(input, pattern, <string>replacement);
|
||||
}
|
||||
|
||||
return StringWrapper.replace(input, <string>pattern, <string>replacement);
|
||||
}
|
||||
|
||||
private _supportedInput(input: any): boolean { return isString(input) || isNumber(input); }
|
||||
|
||||
private _supportedPattern(pattern: any): boolean {
|
||||
return isString(pattern) || pattern instanceof RegExp;
|
||||
}
|
||||
|
||||
private _supportedReplacement(replacement: any): boolean {
|
||||
return isString(replacement) || isFunction(replacement);
|
||||
}
|
||||
}
|
73
modules/@angular/common/src/pipes/slice_pipe.ts
Normal file
73
modules/@angular/common/src/pipes/slice_pipe.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import {isBlank, isString, isArray, StringWrapper} from 'angular2/src/facade/lang';
|
||||
import {ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {Injectable, PipeTransform, WrappedValue, Pipe} from 'angular2/core';
|
||||
import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception';
|
||||
|
||||
/**
|
||||
* Creates a new List or String containing only a subset (slice) of the
|
||||
* elements.
|
||||
*
|
||||
* The starting index of the subset to return is specified by the `start` parameter.
|
||||
*
|
||||
* The ending index of the subset to return is specified by the optional `end` parameter.
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* expression | slice:start[:end]
|
||||
*
|
||||
* All behavior is based on the expected behavior of the JavaScript API
|
||||
* Array.prototype.slice() and String.prototype.slice()
|
||||
*
|
||||
* Where the input expression is a [List] or [String], and `start` is:
|
||||
*
|
||||
* - **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.
|
||||
* - **`|start|` greater than the size of the expression**: return an empty list or string.
|
||||
* - **`|start|` negative greater than the size of the expression**: return entire list or
|
||||
* string expression.
|
||||
*
|
||||
* and where `end` is:
|
||||
*
|
||||
* - **omitted**: return all items until the end of the input
|
||||
* - **a positive integer**: return all items before _end_ index of the list or string
|
||||
* expression.
|
||||
* - **a negative integer**: return all items before _end_ index from the end of the list
|
||||
* or string expression.
|
||||
*
|
||||
* When operating on a [List], the returned list is always a copy even when all
|
||||
* the elements are being returned.
|
||||
*
|
||||
* ## List Example
|
||||
*
|
||||
* This `ngFor` example:
|
||||
*
|
||||
* {@example core/pipes/ts/slice_pipe/slice_pipe_example.ts region='SlicePipe_list'}
|
||||
*
|
||||
* produces the following:
|
||||
*
|
||||
* <li>b</li>
|
||||
* <li>c</li>
|
||||
*
|
||||
* ## String Examples
|
||||
*
|
||||
* {@example core/pipes/ts/slice_pipe/slice_pipe_example.ts region='SlicePipe_string'}
|
||||
*/
|
||||
|
||||
@Pipe({name: 'slice', pure: false})
|
||||
@Injectable()
|
||||
export class SlicePipe implements PipeTransform {
|
||||
transform(value: any, start: number, end: number = null): any {
|
||||
if (!this.supports(value)) {
|
||||
throw new InvalidPipeArgumentException(SlicePipe, value);
|
||||
}
|
||||
if (isBlank(value)) return value;
|
||||
if (isString(value)) {
|
||||
return StringWrapper.slice(value, start, end);
|
||||
}
|
||||
return ListWrapper.slice(value, start, end);
|
||||
}
|
||||
|
||||
private supports(obj: any): boolean { return isString(obj) || isArray(obj); }
|
||||
}
|
22
modules/@angular/common/src/pipes/uppercase_pipe.ts
Normal file
22
modules/@angular/common/src/pipes/uppercase_pipe.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import {isString, isBlank} from 'angular2/src/facade/lang';
|
||||
import {PipeTransform, WrappedValue, Injectable, Pipe} from 'angular2/core';
|
||||
import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception';
|
||||
|
||||
/**
|
||||
* Implements uppercase transforms to text.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* {@example core/pipes/ts/lowerupper_pipe/lowerupper_pipe_example.ts region='LowerUpperPipe'}
|
||||
*/
|
||||
@Pipe({name: 'uppercase'})
|
||||
@Injectable()
|
||||
export class UpperCasePipe implements PipeTransform {
|
||||
transform(value: string): string {
|
||||
if (isBlank(value)) return value;
|
||||
if (!isString(value)) {
|
||||
throw new InvalidPipeArgumentException(UpperCasePipe, value);
|
||||
}
|
||||
return value.toUpperCase();
|
||||
}
|
||||
}
|
539
modules/@angular/common/test/directives/ng_class_spec.ts
Normal file
539
modules/@angular/common/test/directives/ng_class_spec.ts
Normal file
@ -0,0 +1,539 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
AsyncTestCompleter,
|
||||
TestComponentBuilder,
|
||||
beforeEach,
|
||||
beforeEachProviders,
|
||||
ddescribe,
|
||||
xdescribe,
|
||||
describe,
|
||||
el,
|
||||
expect,
|
||||
iit,
|
||||
inject,
|
||||
it,
|
||||
xit,
|
||||
} from 'angular2/testing_internal';
|
||||
import {ListWrapper, StringMapWrapper, SetWrapper} from 'angular2/src/facade/collection';
|
||||
import {Component, provide} from 'angular2/core';
|
||||
import {NgFor} from 'angular2/common';
|
||||
import {NgClass} from 'angular2/src/common/directives/ng_class';
|
||||
|
||||
function detectChangesAndCheck(fixture: ComponentFixture<any>, classes: string) {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.children[0].nativeElement.className).toEqual(classes);
|
||||
}
|
||||
|
||||
export function main() {
|
||||
describe('binding to CSS class list', () => {
|
||||
|
||||
it('should clean up when the directive is destroyed',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div *ngFor="let item of items" [ngClass]="item"></div>';
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.items = [['0']];
|
||||
fixture.detectChanges();
|
||||
fixture.debugElement.componentInstance.items = [['1']];
|
||||
|
||||
detectChangesAndCheck(fixture, '1');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
describe('expressions evaluating to objects', () => {
|
||||
|
||||
it('should add classes specified in an object literal',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div [ngClass]="{foo: true, bar: false}"></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
detectChangesAndCheck(fixture, 'foo');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should add classes specified in an object literal without change in class names',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = `<div [ngClass]="{'foo-bar': true, 'fooBar': true}"></div>`;
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
detectChangesAndCheck(fixture, 'foo-bar fooBar');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should add and remove classes based on changes in object literal values',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div [ngClass]="{foo: condition, bar: !condition}"></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
detectChangesAndCheck(fixture, 'foo');
|
||||
|
||||
fixture.debugElement.componentInstance.condition = false;
|
||||
detectChangesAndCheck(fixture, 'bar');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should add and remove classes based on changes to the expression object',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div [ngClass]="objExpr"></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
detectChangesAndCheck(fixture, 'foo');
|
||||
|
||||
StringMapWrapper.set(fixture.debugElement.componentInstance.objExpr, 'bar', true);
|
||||
detectChangesAndCheck(fixture, 'foo bar');
|
||||
|
||||
StringMapWrapper.set(fixture.debugElement.componentInstance.objExpr, 'baz', true);
|
||||
detectChangesAndCheck(fixture, 'foo bar baz');
|
||||
|
||||
StringMapWrapper.delete(fixture.debugElement.componentInstance.objExpr, 'bar');
|
||||
detectChangesAndCheck(fixture, 'foo baz');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should add and remove classes based on reference changes to the expression object',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div [ngClass]="objExpr"></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
detectChangesAndCheck(fixture, 'foo');
|
||||
|
||||
fixture.debugElement.componentInstance.objExpr = {foo: true, bar: true};
|
||||
detectChangesAndCheck(fixture, 'foo bar');
|
||||
|
||||
fixture.debugElement.componentInstance.objExpr = {baz: true};
|
||||
detectChangesAndCheck(fixture, 'baz');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should remove active classes when expression evaluates to null',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div [ngClass]="objExpr"></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
detectChangesAndCheck(fixture, 'foo');
|
||||
|
||||
fixture.debugElement.componentInstance.objExpr = null;
|
||||
detectChangesAndCheck(fixture, '');
|
||||
|
||||
fixture.debugElement.componentInstance.objExpr = {'foo': false, 'bar': true};
|
||||
detectChangesAndCheck(fixture, 'bar');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should allow multiple classes per expression',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div [ngClass]="objExpr"></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
|
||||
fixture.debugElement.componentInstance.objExpr = {
|
||||
'bar baz': true,
|
||||
'bar1 baz1': true
|
||||
};
|
||||
detectChangesAndCheck(fixture, 'bar baz bar1 baz1');
|
||||
|
||||
fixture.debugElement.componentInstance.objExpr = {
|
||||
'bar baz': false,
|
||||
'bar1 baz1': true
|
||||
};
|
||||
detectChangesAndCheck(fixture, 'bar1 baz1');
|
||||
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should split by one or more spaces between classes',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div [ngClass]="objExpr"></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
|
||||
fixture.debugElement.componentInstance.objExpr = {'foo bar baz': true};
|
||||
detectChangesAndCheck(fixture, 'foo bar baz');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('expressions evaluating to lists', () => {
|
||||
|
||||
it('should add classes specified in a list literal',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = `<div [ngClass]="['foo', 'bar', 'foo-bar', 'fooBar']"></div>`;
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
detectChangesAndCheck(fixture, 'foo bar foo-bar fooBar');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should add and remove classes based on changes to the expression',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div [ngClass]="arrExpr"></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
var arrExpr: string[] = fixture.debugElement.componentInstance.arrExpr;
|
||||
detectChangesAndCheck(fixture, 'foo');
|
||||
|
||||
arrExpr.push('bar');
|
||||
detectChangesAndCheck(fixture, 'foo bar');
|
||||
|
||||
arrExpr[1] = 'baz';
|
||||
detectChangesAndCheck(fixture, 'foo baz');
|
||||
|
||||
ListWrapper.remove(fixture.debugElement.componentInstance.arrExpr, 'baz');
|
||||
detectChangesAndCheck(fixture, 'foo');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should add and remove classes when a reference changes',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div [ngClass]="arrExpr"></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
detectChangesAndCheck(fixture, 'foo');
|
||||
|
||||
fixture.debugElement.componentInstance.arrExpr = ['bar'];
|
||||
detectChangesAndCheck(fixture, 'bar');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should take initial classes into account when a reference changes',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div class="foo" [ngClass]="arrExpr"></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
detectChangesAndCheck(fixture, 'foo');
|
||||
|
||||
fixture.debugElement.componentInstance.arrExpr = ['bar'];
|
||||
detectChangesAndCheck(fixture, 'foo bar');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should ignore empty or blank class names',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div class="foo" [ngClass]="arrExpr"></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
|
||||
fixture.debugElement.componentInstance.arrExpr = ['', ' '];
|
||||
detectChangesAndCheck(fixture, 'foo');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should trim blanks from class names',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div class="foo" [ngClass]="arrExpr"></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
|
||||
fixture.debugElement.componentInstance.arrExpr = [' bar '];
|
||||
detectChangesAndCheck(fixture, 'foo bar');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should allow multiple classes per item in arrays',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div [ngClass]="arrExpr"></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
|
||||
fixture.debugElement.componentInstance.arrExpr =
|
||||
['foo bar baz', 'foo1 bar1 baz1'];
|
||||
detectChangesAndCheck(fixture, 'foo bar baz foo1 bar1 baz1');
|
||||
|
||||
fixture.debugElement.componentInstance.arrExpr = ['foo bar baz foobar'];
|
||||
detectChangesAndCheck(fixture, 'foo bar baz foobar');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('expressions evaluating to sets', () => {
|
||||
|
||||
it('should add and remove classes if the set instance changed',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div [ngClass]="setExpr"></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
var setExpr = new Set<string>();
|
||||
setExpr.add('bar');
|
||||
fixture.debugElement.componentInstance.setExpr = setExpr;
|
||||
detectChangesAndCheck(fixture, 'bar');
|
||||
|
||||
setExpr = new Set<string>();
|
||||
setExpr.add('baz');
|
||||
fixture.debugElement.componentInstance.setExpr = setExpr;
|
||||
detectChangesAndCheck(fixture, 'baz');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
});
|
||||
describe('expressions evaluating to string', () => {
|
||||
|
||||
it('should add classes specified in a string literal',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = `<div [ngClass]="'foo bar foo-bar fooBar'"></div>`;
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
detectChangesAndCheck(fixture, 'foo bar foo-bar fooBar');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should add and remove classes based on changes to the expression',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div [ngClass]="strExpr"></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
detectChangesAndCheck(fixture, 'foo');
|
||||
|
||||
fixture.debugElement.componentInstance.strExpr = 'foo bar';
|
||||
detectChangesAndCheck(fixture, 'foo bar');
|
||||
|
||||
|
||||
fixture.debugElement.componentInstance.strExpr = 'baz';
|
||||
detectChangesAndCheck(fixture, 'baz');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should remove active classes when switching from string to null',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = `<div [ngClass]="strExpr"></div>`;
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
detectChangesAndCheck(fixture, 'foo');
|
||||
|
||||
fixture.debugElement.componentInstance.strExpr = null;
|
||||
detectChangesAndCheck(fixture, '');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should take initial classes into account when switching from string to null',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = `<div class="foo" [ngClass]="strExpr"></div>`;
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
detectChangesAndCheck(fixture, 'foo');
|
||||
|
||||
fixture.debugElement.componentInstance.strExpr = null;
|
||||
detectChangesAndCheck(fixture, 'foo');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should ignore empty and blank strings',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = `<div class="foo" [ngClass]="strExpr"></div>`;
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.strExpr = '';
|
||||
detectChangesAndCheck(fixture, 'foo');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('cooperation with other class-changing constructs', () => {
|
||||
|
||||
it('should co-operate with the class attribute',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div [ngClass]="objExpr" class="init foo"></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
StringMapWrapper.set(fixture.debugElement.componentInstance.objExpr, 'bar', true);
|
||||
detectChangesAndCheck(fixture, 'init foo bar');
|
||||
|
||||
StringMapWrapper.set(fixture.debugElement.componentInstance.objExpr, 'foo', false);
|
||||
detectChangesAndCheck(fixture, 'init bar');
|
||||
|
||||
fixture.debugElement.componentInstance.objExpr = null;
|
||||
detectChangesAndCheck(fixture, 'init foo');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should co-operate with the interpolated class attribute',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = `<div [ngClass]="objExpr" class="{{'init foo'}}"></div>`;
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
StringMapWrapper.set(fixture.debugElement.componentInstance.objExpr, 'bar', true);
|
||||
detectChangesAndCheck(fixture, `init foo bar`);
|
||||
|
||||
StringMapWrapper.set(fixture.debugElement.componentInstance.objExpr, 'foo', false);
|
||||
detectChangesAndCheck(fixture, `init bar`);
|
||||
|
||||
fixture.debugElement.componentInstance.objExpr = null;
|
||||
detectChangesAndCheck(fixture, `init foo`);
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should co-operate with the class attribute and binding to it',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = `<div [ngClass]="objExpr" class="init" [class]="'foo'"></div>`;
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
StringMapWrapper.set(fixture.debugElement.componentInstance.objExpr, 'bar', true);
|
||||
detectChangesAndCheck(fixture, `init foo bar`);
|
||||
|
||||
StringMapWrapper.set(fixture.debugElement.componentInstance.objExpr, 'foo', false);
|
||||
detectChangesAndCheck(fixture, `init bar`);
|
||||
|
||||
fixture.debugElement.componentInstance.objExpr = null;
|
||||
detectChangesAndCheck(fixture, `init foo`);
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should co-operate with the class attribute and class.name binding',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template =
|
||||
'<div class="init foo" [ngClass]="objExpr" [class.baz]="condition"></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
detectChangesAndCheck(fixture, 'init foo baz');
|
||||
|
||||
StringMapWrapper.set(fixture.debugElement.componentInstance.objExpr, 'bar', true);
|
||||
detectChangesAndCheck(fixture, 'init foo baz bar');
|
||||
|
||||
StringMapWrapper.set(fixture.debugElement.componentInstance.objExpr, 'foo', false);
|
||||
detectChangesAndCheck(fixture, 'init baz bar');
|
||||
|
||||
fixture.debugElement.componentInstance.condition = false;
|
||||
detectChangesAndCheck(fixture, 'init bar');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should co-operate with initial class and class attribute binding when binding changes',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div class="init" [ngClass]="objExpr" [class]="strExpr"></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
detectChangesAndCheck(fixture, 'init foo');
|
||||
|
||||
StringMapWrapper.set(fixture.debugElement.componentInstance.objExpr, 'bar', true);
|
||||
detectChangesAndCheck(fixture, 'init foo bar');
|
||||
|
||||
fixture.debugElement.componentInstance.strExpr = 'baz';
|
||||
detectChangesAndCheck(fixture, 'init bar baz foo');
|
||||
|
||||
fixture.debugElement.componentInstance.objExpr = null;
|
||||
detectChangesAndCheck(fixture, 'init baz');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
@Component({selector: 'test-cmp', directives: [NgClass, NgFor], template: ''})
|
||||
class TestComponent {
|
||||
condition: boolean = true;
|
||||
items: any[];
|
||||
arrExpr: string[] = ['foo'];
|
||||
setExpr: Set<string> = new Set<string>();
|
||||
objExpr = {'foo': true, 'bar': false};
|
||||
strExpr = 'foo';
|
||||
|
||||
constructor() { this.setExpr.add('foo'); }
|
||||
}
|
526
modules/@angular/common/test/directives/ng_for_spec.ts
Normal file
526
modules/@angular/common/test/directives/ng_for_spec.ts
Normal file
@ -0,0 +1,526 @@
|
||||
import {
|
||||
AsyncTestCompleter,
|
||||
TestComponentBuilder,
|
||||
beforeEach,
|
||||
beforeEachProviders,
|
||||
ddescribe,
|
||||
describe,
|
||||
el,
|
||||
expect,
|
||||
iit,
|
||||
inject,
|
||||
it,
|
||||
xit,
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {IS_DART} from 'angular2/src/facade/lang';
|
||||
import {Component, TemplateRef, ContentChild} from 'angular2/core';
|
||||
import {NgFor} from 'angular2/src/common/directives/ng_for';
|
||||
import {NgIf} from 'angular2/src/common/directives/ng_if';
|
||||
import {By} from 'angular2/platform/common_dom';
|
||||
|
||||
export function main() {
|
||||
describe('ngFor', () => {
|
||||
var TEMPLATE =
|
||||
'<div><copy-me template="ngFor let item of items">{{item.toString()}};</copy-me></div>';
|
||||
|
||||
it('should reflect initial elements',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
tcb.overrideTemplate(TestComponent, TEMPLATE)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('1;2;');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should reflect added elements',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
tcb.overrideTemplate(TestComponent, TEMPLATE)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
|
||||
(<number[]>fixture.debugElement.componentInstance.items).push(3);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('1;2;3;');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should reflect removed elements',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
tcb.overrideTemplate(TestComponent, TEMPLATE)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
|
||||
ListWrapper.removeAt(fixture.debugElement.componentInstance.items, 1);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('1;');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should reflect moved elements',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
tcb.overrideTemplate(TestComponent, TEMPLATE)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
|
||||
ListWrapper.removeAt(fixture.debugElement.componentInstance.items, 0);
|
||||
(<number[]>fixture.debugElement.componentInstance.items).push(1);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('2;1;');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should reflect a mix of all changes (additions/removals/moves)',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
tcb.overrideTemplate(TestComponent, TEMPLATE)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.items = [0, 1, 2, 3, 4, 5];
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.debugElement.componentInstance.items = [6, 2, 7, 0, 4, 8];
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('6;2;7;0;4;8;');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should iterate over an array of objects',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<ul><li template="ngFor let item of items">{{item["name"]}};</li></ul>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
|
||||
// INIT
|
||||
fixture.debugElement.componentInstance.items =
|
||||
[{'name': 'misko'}, {'name': 'shyam'}];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('misko;shyam;');
|
||||
|
||||
// GROW
|
||||
(<any[]>fixture.debugElement.componentInstance.items).push({'name': 'adam'});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('misko;shyam;adam;');
|
||||
|
||||
// SHRINK
|
||||
ListWrapper.removeAt(fixture.debugElement.componentInstance.items, 2);
|
||||
ListWrapper.removeAt(fixture.debugElement.componentInstance.items, 0);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('shyam;');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should gracefully handle nulls',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<ul><li template="ngFor let item of null">{{item}};</li></ul>';
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should gracefully handle ref changing to null and back',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
tcb.overrideTemplate(TestComponent, TEMPLATE)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('1;2;');
|
||||
|
||||
fixture.debugElement.componentInstance.items = null;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('');
|
||||
|
||||
fixture.debugElement.componentInstance.items = [1, 2, 3];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('1;2;3;');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
if (!IS_DART) {
|
||||
it('should throw on non-iterable ref and suggest using an array',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
tcb.overrideTemplate(TestComponent, TEMPLATE)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.items = 'whaaa';
|
||||
try {
|
||||
fixture.detectChanges()
|
||||
} catch (e) {
|
||||
expect(e.message).toContain(
|
||||
`Cannot find a differ supporting object 'whaaa' of type 'string'. NgFor only supports binding to Iterables such as Arrays.`);
|
||||
async.done();
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
it('should throw on ref changing to string',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
tcb.overrideTemplate(TestComponent, TEMPLATE)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('1;2;');
|
||||
|
||||
fixture.debugElement.componentInstance.items = 'whaaa';
|
||||
expect(() => fixture.detectChanges()).toThrowError();
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should works with duplicates',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
tcb.overrideTemplate(TestComponent, TEMPLATE)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
var a = new Foo();
|
||||
fixture.debugElement.componentInstance.items = [a, a];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('foo;foo;');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should repeat over nested arrays',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div>' +
|
||||
'<div template="ngFor let item of items">' +
|
||||
'<div template="ngFor let subitem of item">' +
|
||||
'{{subitem}}-{{item.length}};' +
|
||||
'</div>|' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.items = [['a', 'b'], ['c']];
|
||||
fixture.detectChanges();
|
||||
fixture.detectChanges();
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('a-2;b-2;|c-1;|');
|
||||
|
||||
fixture.debugElement.componentInstance.items = [['e'], ['f', 'g']];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('e-1;|f-2;g-2;|');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should repeat over nested arrays with no intermediate element',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div><template ngFor let-item [ngForOf]="items">' +
|
||||
'<div template="ngFor let subitem of item">' +
|
||||
'{{subitem}}-{{item.length}};' +
|
||||
'</div></template></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.items = [['a', 'b'], ['c']];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('a-2;b-2;c-1;');
|
||||
|
||||
fixture.debugElement.componentInstance.items = [['e'], ['f', 'g']];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('e-1;f-2;g-2;');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should repeat over nested ngIf that are the last node in the ngFor temlate',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template =
|
||||
`<div><template ngFor let-item [ngForOf]="items" let-i="index"><div>{{i}}|</div>` +
|
||||
`<div *ngIf="i % 2 == 0">even|</div></template></div>`;
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
var el = fixture.debugElement.nativeElement;
|
||||
var items = [1];
|
||||
fixture.debugElement.componentInstance.items = items;
|
||||
fixture.detectChanges();
|
||||
expect(el).toHaveText('0|even|');
|
||||
|
||||
items.push(1);
|
||||
fixture.detectChanges();
|
||||
expect(el).toHaveText('0|even|1|');
|
||||
|
||||
items.push(1);
|
||||
fixture.detectChanges();
|
||||
expect(el).toHaveText('0|even|1|2|even|');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should display indices correctly',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template =
|
||||
'<div><copy-me template="ngFor: let item of items; let i=index">{{i.toString()}}</copy-me></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('0123456789');
|
||||
|
||||
fixture.debugElement.componentInstance.items = [1, 2, 6, 7, 4, 3, 5, 8, 9, 0];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('0123456789');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should display first item correctly',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template =
|
||||
'<div><copy-me template="ngFor: let item of items; let isFirst=first">{{isFirst.toString()}}</copy-me></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.items = [0, 1, 2];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('truefalsefalse');
|
||||
|
||||
fixture.debugElement.componentInstance.items = [2, 1];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('truefalse');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should display last item correctly',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template =
|
||||
'<div><copy-me template="ngFor: let item of items; let isLast=last">{{isLast.toString()}}</copy-me></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.items = [0, 1, 2];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('falsefalsetrue');
|
||||
|
||||
fixture.debugElement.componentInstance.items = [2, 1];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('falsetrue');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should display even items correctly',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template =
|
||||
'<div><copy-me template="ngFor: let item of items; let isEven=even">{{isEven.toString()}}</copy-me></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.items = [0, 1, 2];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('truefalsetrue');
|
||||
|
||||
fixture.debugElement.componentInstance.items = [2, 1];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('truefalse');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should display odd items correctly',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template =
|
||||
'<div><copy-me template="ngFor: let item of items; let isOdd=odd">{{isOdd.toString()}}</copy-me></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.items = [0, 1, 2, 3];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('falsetruefalsetrue');
|
||||
|
||||
fixture.debugElement.componentInstance.items = [2, 1];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('falsetrue');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should allow to use a custom template',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
tcb.overrideTemplate(
|
||||
TestComponent,
|
||||
'<ul><template ngFor [ngForOf]="items" [ngForTemplate]="contentTpl"></template></ul>')
|
||||
.overrideTemplate(
|
||||
ComponentUsingTestComponent,
|
||||
'<test-cmp><li template="let item; let i=index">{{i}}: {{item}};</li></test-cmp>')
|
||||
.createAsync(ComponentUsingTestComponent)
|
||||
.then((fixture) => {
|
||||
var testComponent = fixture.debugElement.children[0];
|
||||
testComponent.componentInstance.items = ['a', 'b', 'c'];
|
||||
fixture.detectChanges();
|
||||
expect(testComponent.nativeElement).toHaveText('0: a;1: b;2: c;');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should use a default template if a custom one is null',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
tcb.overrideTemplate(TestComponent, `<ul><template ngFor let-item [ngForOf]="items"
|
||||
[ngForTemplate]="contentTpl" let-i="index">{{i}}: {{item}};</template></ul>`)
|
||||
.overrideTemplate(ComponentUsingTestComponent, '<test-cmp></test-cmp>')
|
||||
.createAsync(ComponentUsingTestComponent)
|
||||
.then((fixture) => {
|
||||
var testComponent = fixture.debugElement.children[0];
|
||||
testComponent.componentInstance.items = ['a', 'b', 'c'];
|
||||
fixture.detectChanges();
|
||||
expect(testComponent.nativeElement).toHaveText('0: a;1: b;2: c;');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should use a custom template when both default and a custom one are present',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
tcb.overrideTemplate(TestComponent, `<ul><template ngFor let-item [ngForOf]="items"
|
||||
[ngForTemplate]="contentTpl" let-i="index">{{i}}=> {{item}};</template></ul>`)
|
||||
.overrideTemplate(
|
||||
ComponentUsingTestComponent,
|
||||
'<test-cmp><li template="let item; let i=index">{{i}}: {{item}};</li></test-cmp>')
|
||||
.createAsync(ComponentUsingTestComponent)
|
||||
.then((fixture) => {
|
||||
var testComponent = fixture.debugElement.children[0];
|
||||
testComponent.componentInstance.items = ['a', 'b', 'c'];
|
||||
fixture.detectChanges();
|
||||
expect(testComponent.nativeElement).toHaveText('0: a;1: b;2: c;');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
describe('track by', function() {
|
||||
it('should not replace tracked items',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template =
|
||||
`<template ngFor let-item [ngForOf]="items" [ngForTrackBy]="trackById" let-i="index">
|
||||
<p>{{items[i]}}</p>
|
||||
</template>`;
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
var buildItemList =
|
||||
() => {
|
||||
fixture.debugElement.componentInstance.items = [{'id': 'a'}];
|
||||
fixture.detectChanges();
|
||||
return fixture.debugElement.queryAll(By.css('p'))[0];
|
||||
}
|
||||
|
||||
var firstP = buildItemList();
|
||||
var finalP = buildItemList();
|
||||
expect(finalP.nativeElement).toBe(firstP.nativeElement);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
it('should update implicit local variable on view',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template =
|
||||
`<div><template ngFor let-item [ngForOf]="items" [ngForTrackBy]="trackById">{{item['color']}}</template></div>`;
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.items = [{'id': 'a', 'color': 'blue'}];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('blue');
|
||||
fixture.debugElement.componentInstance.items = [{'id': 'a', 'color': 'red'}];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('red');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
it('should move items around and keep them updated ',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template =
|
||||
`<div><template ngFor let-item [ngForOf]="items" [ngForTrackBy]="trackById">{{item['color']}}</template></div>`;
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.items =
|
||||
[{'id': 'a', 'color': 'blue'}, {'id': 'b', 'color': 'yellow'}];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('blueyellow');
|
||||
fixture.debugElement.componentInstance.items =
|
||||
[{'id': 'b', 'color': 'orange'}, {'id': 'a', 'color': 'red'}];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('orangered');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should handle added and removed items properly when tracking by index',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template =
|
||||
`<div><template ngFor let-item [ngForOf]="items" [ngForTrackBy]="trackByIndex">{{item}}</template></div>`;
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.items = ['a', 'b', 'c', 'd'];
|
||||
fixture.detectChanges();
|
||||
fixture.debugElement.componentInstance.items = ['e', 'f', 'g', 'h'];
|
||||
fixture.detectChanges();
|
||||
fixture.debugElement.componentInstance.items = ['e', 'f', 'h'];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('efh');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class Foo {
|
||||
toString() { return 'foo'; }
|
||||
}
|
||||
|
||||
@Component({selector: 'test-cmp', directives: [NgFor, NgIf], template: ''})
|
||||
class TestComponent {
|
||||
@ContentChild(TemplateRef) contentTpl: TemplateRef<Object>;
|
||||
items: any;
|
||||
constructor() { this.items = [1, 2]; }
|
||||
trackById(index: number, item: any): string { return item['id']; }
|
||||
trackByIndex(index: number, item: any): number { return index; }
|
||||
}
|
||||
|
||||
@Component({selector: 'outer-cmp', directives: [TestComponent], template: ''})
|
||||
class ComponentUsingTestComponent {
|
||||
items: any;
|
||||
constructor() { this.items = [1, 2]; }
|
||||
}
|
241
modules/@angular/common/test/directives/ng_if_spec.ts
Normal file
241
modules/@angular/common/test/directives/ng_if_spec.ts
Normal file
@ -0,0 +1,241 @@
|
||||
import {
|
||||
AsyncTestCompleter,
|
||||
TestComponentBuilder,
|
||||
beforeEach,
|
||||
ddescribe,
|
||||
describe,
|
||||
el,
|
||||
expect,
|
||||
iit,
|
||||
inject,
|
||||
it,
|
||||
xit,
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
|
||||
|
||||
import {Component} from 'angular2/core';
|
||||
import {NgIf} from 'angular2/common';
|
||||
|
||||
import {IS_DART} from 'angular2/src/facade/lang';
|
||||
|
||||
export function main() {
|
||||
describe('ngIf directive', () => {
|
||||
it('should work in a template attribute',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var html = '<div><copy-me template="ngIf booleanCondition">hello</copy-me></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, html)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
expect(DOM.querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length)
|
||||
.toEqual(1);
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('hello');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should work in a template element',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var html =
|
||||
'<div><template [ngIf]="booleanCondition"><copy-me>hello2</copy-me></template></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, html)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
expect(DOM.querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length)
|
||||
.toEqual(1);
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('hello2');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should toggle node when condition changes',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var html = '<div><copy-me template="ngIf booleanCondition">hello</copy-me></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, html)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.booleanCondition = false;
|
||||
fixture.detectChanges();
|
||||
expect(DOM.querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length)
|
||||
.toEqual(0);
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('');
|
||||
|
||||
fixture.debugElement.componentInstance.booleanCondition = true;
|
||||
fixture.detectChanges();
|
||||
expect(DOM.querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length)
|
||||
.toEqual(1);
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('hello');
|
||||
|
||||
fixture.debugElement.componentInstance.booleanCondition = false;
|
||||
fixture.detectChanges();
|
||||
expect(DOM.querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length)
|
||||
.toEqual(0);
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should handle nested if correctly',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var html =
|
||||
'<div><template [ngIf]="booleanCondition"><copy-me *ngIf="nestedBooleanCondition">hello</copy-me></template></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, html)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.booleanCondition = false;
|
||||
fixture.detectChanges();
|
||||
expect(DOM.querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length)
|
||||
.toEqual(0);
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('');
|
||||
|
||||
fixture.debugElement.componentInstance.booleanCondition = true;
|
||||
fixture.detectChanges();
|
||||
expect(DOM.querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length)
|
||||
.toEqual(1);
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('hello');
|
||||
|
||||
fixture.debugElement.componentInstance.nestedBooleanCondition = false;
|
||||
fixture.detectChanges();
|
||||
expect(DOM.querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length)
|
||||
.toEqual(0);
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('');
|
||||
|
||||
fixture.debugElement.componentInstance.nestedBooleanCondition = true;
|
||||
fixture.detectChanges();
|
||||
expect(DOM.querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length)
|
||||
.toEqual(1);
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('hello');
|
||||
|
||||
fixture.debugElement.componentInstance.booleanCondition = false;
|
||||
fixture.detectChanges();
|
||||
expect(DOM.querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length)
|
||||
.toEqual(0);
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should update several nodes with if',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var html =
|
||||
'<div>' +
|
||||
'<copy-me template="ngIf numberCondition + 1 >= 2">helloNumber</copy-me>' +
|
||||
'<copy-me template="ngIf stringCondition == \'foo\'">helloString</copy-me>' +
|
||||
'<copy-me template="ngIf functionCondition(stringCondition, numberCondition)">helloFunction</copy-me>' +
|
||||
'</div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, html)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
expect(DOM.querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length)
|
||||
.toEqual(3);
|
||||
expect(DOM.getText(fixture.debugElement.nativeElement))
|
||||
.toEqual('helloNumberhelloStringhelloFunction');
|
||||
|
||||
fixture.debugElement.componentInstance.numberCondition = 0;
|
||||
fixture.detectChanges();
|
||||
expect(DOM.querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length)
|
||||
.toEqual(1);
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('helloString');
|
||||
|
||||
fixture.debugElement.componentInstance.numberCondition = 1;
|
||||
fixture.debugElement.componentInstance.stringCondition = "bar";
|
||||
fixture.detectChanges();
|
||||
expect(DOM.querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length)
|
||||
.toEqual(1);
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('helloNumber');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
if (!IS_DART) {
|
||||
it('should not add the element twice if the condition goes from true to true (JS)',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var html = '<div><copy-me template="ngIf numberCondition">hello</copy-me></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, html)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
expect(DOM.querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length)
|
||||
.toEqual(1);
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('hello');
|
||||
|
||||
fixture.debugElement.componentInstance.numberCondition = 2;
|
||||
fixture.detectChanges();
|
||||
expect(DOM.querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length)
|
||||
.toEqual(1);
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('hello');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should not recreate the element if the condition goes from true to true (JS)',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var html = '<div><copy-me template="ngIf numberCondition">hello</copy-me></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, html)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
DOM.addClass(DOM.querySelector(fixture.debugElement.nativeElement, 'copy-me'),
|
||||
"foo");
|
||||
|
||||
fixture.debugElement.componentInstance.numberCondition = 2;
|
||||
fixture.detectChanges();
|
||||
expect(
|
||||
DOM.hasClass(DOM.querySelector(fixture.debugElement.nativeElement, 'copy-me'),
|
||||
"foo"))
|
||||
.toBe(true);
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
if (IS_DART) {
|
||||
it('should not create the element if the condition is not a boolean (DART)',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var html = '<div><copy-me template="ngIf numberCondition">hello</copy-me></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, html)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
expect(() => fixture.detectChanges()).toThrowError();
|
||||
expect(DOM.querySelectorAll(fixture.debugElement.nativeElement, 'copy-me').length)
|
||||
.toEqual(0);
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Component({selector: 'test-cmp', directives: [NgIf], template: ''})
|
||||
class TestComponent {
|
||||
booleanCondition: boolean;
|
||||
nestedBooleanCondition: boolean;
|
||||
numberCondition: number;
|
||||
stringCondition: string;
|
||||
functionCondition: Function;
|
||||
constructor() {
|
||||
this.booleanCondition = true;
|
||||
this.nestedBooleanCondition = true;
|
||||
this.numberCondition = 1;
|
||||
this.stringCondition = "foo";
|
||||
this.functionCondition = function(s, n) { return s == "foo" && n == 1; };
|
||||
}
|
||||
}
|
136
modules/@angular/common/test/directives/ng_plural_spec.ts
Normal file
136
modules/@angular/common/test/directives/ng_plural_spec.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import {
|
||||
AsyncTestCompleter,
|
||||
TestComponentBuilder,
|
||||
beforeEachProviders,
|
||||
beforeEach,
|
||||
ddescribe,
|
||||
describe,
|
||||
el,
|
||||
expect,
|
||||
iit,
|
||||
inject,
|
||||
it,
|
||||
xit,
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {Component, Injectable, provide} from 'angular2/core';
|
||||
import {NgPlural, NgPluralCase, NgLocalization} from 'angular2/common';
|
||||
|
||||
export function main() {
|
||||
describe('switch', () => {
|
||||
beforeEachProviders(() => [provide(NgLocalization, {useClass: TestLocalizationMap})]);
|
||||
|
||||
it('should display the template according to the exact value',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div>' +
|
||||
'<ul [ngPlural]="switchValue">' +
|
||||
'<template ngPluralCase="=0"><li>you have no messages.</li></template>' +
|
||||
'<template ngPluralCase="=1"><li>you have one message.</li></template>' +
|
||||
'</ul></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.switchValue = 0;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('you have no messages.');
|
||||
|
||||
fixture.debugElement.componentInstance.switchValue = 1;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('you have one message.');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should display the template according to the category',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template =
|
||||
'<div>' +
|
||||
'<ul [ngPlural]="switchValue">' +
|
||||
'<template ngPluralCase="few"><li>you have a few messages.</li></template>' +
|
||||
'<template ngPluralCase="many"><li>you have many messages.</li></template>' +
|
||||
'</ul></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.switchValue = 2;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('you have a few messages.');
|
||||
|
||||
fixture.debugElement.componentInstance.switchValue = 8;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('you have many messages.');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should default to other when no matches are found',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template =
|
||||
'<div>' +
|
||||
'<ul [ngPlural]="switchValue">' +
|
||||
'<template ngPluralCase="few"><li>you have a few messages.</li></template>' +
|
||||
'<template ngPluralCase="other"><li>default message.</li></template>' +
|
||||
'</ul></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.switchValue = 100;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('default message.');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should prioritize value matches over category matches',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template =
|
||||
'<div>' +
|
||||
'<ul [ngPlural]="switchValue">' +
|
||||
'<template ngPluralCase="few"><li>you have a few messages.</li></template>' +
|
||||
'<template ngPluralCase="=2">you have two messages.</template>' +
|
||||
'</ul></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.switchValue = 2;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('you have two messages.');
|
||||
|
||||
fixture.debugElement.componentInstance.switchValue = 3;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('you have a few messages.');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class TestLocalizationMap extends NgLocalization {
|
||||
getPluralCategory(value: number): string {
|
||||
if (value > 1 && value < 4) {
|
||||
return 'few';
|
||||
} else if (value >= 4 && value < 10) {
|
||||
return 'many';
|
||||
} else {
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Component({selector: 'test-cmp', directives: [NgPlural, NgPluralCase], template: ''})
|
||||
class TestComponent {
|
||||
switchValue: number;
|
||||
|
||||
constructor() { this.switchValue = null; }
|
||||
}
|
143
modules/@angular/common/test/directives/ng_style_spec.ts
Normal file
143
modules/@angular/common/test/directives/ng_style_spec.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import {
|
||||
AsyncTestCompleter,
|
||||
TestComponentBuilder,
|
||||
beforeEach,
|
||||
beforeEachProviders,
|
||||
ddescribe,
|
||||
xdescribe,
|
||||
describe,
|
||||
el,
|
||||
expect,
|
||||
iit,
|
||||
inject,
|
||||
it,
|
||||
xit,
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
import {Component} from 'angular2/core';
|
||||
|
||||
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
|
||||
import {NgStyle} from 'angular2/src/common/directives/ng_style';
|
||||
|
||||
export function main() {
|
||||
describe('binding to CSS styles', () => {
|
||||
|
||||
it('should add styles specified in an object literal',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = `<div [ngStyle]="{'max-width': '40px'}"></div>`;
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
expect(DOM.getStyle(fixture.debugElement.children[0].nativeElement, 'max-width'))
|
||||
.toEqual('40px');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should add and change styles specified in an object expression',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = `<div [ngStyle]="expr"></div>`;
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
var expr: Map<string, any>;
|
||||
|
||||
fixture.debugElement.componentInstance.expr = {'max-width': '40px'};
|
||||
fixture.detectChanges();
|
||||
expect(DOM.getStyle(fixture.debugElement.children[0].nativeElement, 'max-width'))
|
||||
.toEqual('40px');
|
||||
|
||||
expr = fixture.debugElement.componentInstance.expr;
|
||||
expr['max-width'] = '30%';
|
||||
fixture.detectChanges();
|
||||
expect(DOM.getStyle(fixture.debugElement.children[0].nativeElement, 'max-width'))
|
||||
.toEqual('30%');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should remove styles when deleting a key in an object expression',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = `<div [ngStyle]="expr"></div>`;
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.expr = {'max-width': '40px'};
|
||||
fixture.detectChanges();
|
||||
expect(DOM.getStyle(fixture.debugElement.children[0].nativeElement, 'max-width'))
|
||||
.toEqual('40px');
|
||||
|
||||
StringMapWrapper.delete(fixture.debugElement.componentInstance.expr, 'max-width');
|
||||
fixture.detectChanges();
|
||||
expect(DOM.getStyle(fixture.debugElement.children[0].nativeElement, 'max-width'))
|
||||
.toEqual('');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should co-operate with the style attribute',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = `<div style="font-size: 12px" [ngStyle]="expr"></div>`;
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.expr = {'max-width': '40px'};
|
||||
fixture.detectChanges();
|
||||
expect(DOM.getStyle(fixture.debugElement.children[0].nativeElement, 'max-width'))
|
||||
.toEqual('40px');
|
||||
expect(DOM.getStyle(fixture.debugElement.children[0].nativeElement, 'font-size'))
|
||||
.toEqual('12px');
|
||||
|
||||
StringMapWrapper.delete(fixture.debugElement.componentInstance.expr, 'max-width');
|
||||
fixture.detectChanges();
|
||||
expect(DOM.getStyle(fixture.debugElement.children[0].nativeElement, 'max-width'))
|
||||
.toEqual('');
|
||||
expect(DOM.getStyle(fixture.debugElement.children[0].nativeElement, 'font-size'))
|
||||
.toEqual('12px');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should co-operate with the style.[styleName]="expr" special-case in the compiler',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = `<div [style.font-size.px]="12" [ngStyle]="expr"></div>`;
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.expr = {'max-width': '40px'};
|
||||
fixture.detectChanges();
|
||||
expect(DOM.getStyle(fixture.debugElement.children[0].nativeElement, 'max-width'))
|
||||
.toEqual('40px');
|
||||
expect(DOM.getStyle(fixture.debugElement.children[0].nativeElement, 'font-size'))
|
||||
.toEqual('12px');
|
||||
|
||||
StringMapWrapper.delete(fixture.debugElement.componentInstance.expr, 'max-width');
|
||||
expect(DOM.getStyle(fixture.debugElement.children[0].nativeElement, 'font-size'))
|
||||
.toEqual('12px');
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(DOM.getStyle(fixture.debugElement.children[0].nativeElement, 'max-width'))
|
||||
.toEqual('');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
})
|
||||
}
|
||||
|
||||
@Component({selector: 'test-cmp', directives: [NgStyle], template: ''})
|
||||
class TestComponent {
|
||||
expr;
|
||||
}
|
160
modules/@angular/common/test/directives/ng_switch_spec.ts
Normal file
160
modules/@angular/common/test/directives/ng_switch_spec.ts
Normal file
@ -0,0 +1,160 @@
|
||||
import {
|
||||
AsyncTestCompleter,
|
||||
TestComponentBuilder,
|
||||
beforeEach,
|
||||
ddescribe,
|
||||
describe,
|
||||
el,
|
||||
expect,
|
||||
iit,
|
||||
inject,
|
||||
it,
|
||||
xit,
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {Component} from 'angular2/core';
|
||||
|
||||
import {NgSwitch, NgSwitchWhen, NgSwitchDefault} from 'angular2/src/common/directives/ng_switch';
|
||||
|
||||
export function main() {
|
||||
describe('switch', () => {
|
||||
describe('switch value changes', () => {
|
||||
it('should switch amongst when values',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div>' +
|
||||
'<ul [ngSwitch]="switchValue">' +
|
||||
'<template ngSwitchWhen="a"><li>when a</li></template>' +
|
||||
'<template ngSwitchWhen="b"><li>when b</li></template>' +
|
||||
'</ul></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('');
|
||||
|
||||
fixture.debugElement.componentInstance.switchValue = 'a';
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('when a');
|
||||
|
||||
fixture.debugElement.componentInstance.switchValue = 'b';
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('when b');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should switch amongst when values with fallback to default',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div>' +
|
||||
'<ul [ngSwitch]="switchValue">' +
|
||||
'<li template="ngSwitchWhen \'a\'">when a</li>' +
|
||||
'<li template="ngSwitchDefault">when default</li>' +
|
||||
'</ul></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('when default');
|
||||
|
||||
fixture.debugElement.componentInstance.switchValue = 'a';
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('when a');
|
||||
|
||||
fixture.debugElement.componentInstance.switchValue = 'b';
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('when default');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should support multiple whens with the same value',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div>' +
|
||||
'<ul [ngSwitch]="switchValue">' +
|
||||
'<template ngSwitchWhen="a"><li>when a1;</li></template>' +
|
||||
'<template ngSwitchWhen="b"><li>when b1;</li></template>' +
|
||||
'<template ngSwitchWhen="a"><li>when a2;</li></template>' +
|
||||
'<template ngSwitchWhen="b"><li>when b2;</li></template>' +
|
||||
'<template ngSwitchDefault><li>when default1;</li></template>' +
|
||||
'<template ngSwitchDefault><li>when default2;</li></template>' +
|
||||
'</ul></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement)
|
||||
.toHaveText('when default1;when default2;');
|
||||
|
||||
fixture.debugElement.componentInstance.switchValue = 'a';
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('when a1;when a2;');
|
||||
|
||||
fixture.debugElement.componentInstance.switchValue = 'b';
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('when b1;when b2;');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when values changes', () => {
|
||||
it('should switch amongst when values',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div>' +
|
||||
'<ul [ngSwitch]="switchValue">' +
|
||||
'<template [ngSwitchWhen]="when1"><li>when 1;</li></template>' +
|
||||
'<template [ngSwitchWhen]="when2"><li>when 2;</li></template>' +
|
||||
'<template ngSwitchDefault><li>when default;</li></template>' +
|
||||
'</ul></div>';
|
||||
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.debugElement.componentInstance.when1 = 'a';
|
||||
fixture.debugElement.componentInstance.when2 = 'b';
|
||||
fixture.debugElement.componentInstance.switchValue = 'a';
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('when 1;');
|
||||
|
||||
fixture.debugElement.componentInstance.switchValue = 'b';
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('when 2;');
|
||||
|
||||
fixture.debugElement.componentInstance.switchValue = 'c';
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('when default;');
|
||||
|
||||
fixture.debugElement.componentInstance.when1 = 'c';
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('when 1;');
|
||||
|
||||
fixture.debugElement.componentInstance.when1 = 'd';
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('when default;');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Component(
|
||||
{selector: 'test-cmp', directives: [NgSwitch, NgSwitchWhen, NgSwitchDefault], template: ''})
|
||||
class TestComponent {
|
||||
switchValue: any;
|
||||
when1: any;
|
||||
when2: any;
|
||||
|
||||
constructor() {
|
||||
this.switchValue = null;
|
||||
this.when1 = null;
|
||||
this.when2 = null;
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
import {
|
||||
AsyncTestCompleter,
|
||||
TestComponentBuilder,
|
||||
beforeEach,
|
||||
ddescribe,
|
||||
describe,
|
||||
el,
|
||||
expect,
|
||||
iit,
|
||||
inject,
|
||||
it,
|
||||
xit,
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {Component, Directive, TemplateRef, ContentChildren, QueryList} from 'angular2/core';
|
||||
|
||||
import {NgTemplateOutlet} from 'angular2/src/common/directives/ng_template_outlet';
|
||||
|
||||
export function main() {
|
||||
describe('insert', () => {
|
||||
it('should do nothing if templateRef is null',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = `<template [ngTemplateOutlet]="null"></template>`;
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should insert content specified by TemplateRef',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template =
|
||||
`<tpl-refs #refs="tplRefs"><template>foo</template></tpl-refs><template [ngTemplateOutlet]="currentTplRef"></template>`;
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('');
|
||||
|
||||
var refs = fixture.debugElement.children[0].references['refs'];
|
||||
|
||||
fixture.componentInstance.currentTplRef = refs.tplRefs.first;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('foo');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should clear content if TemplateRef becomes null',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template =
|
||||
`<tpl-refs #refs="tplRefs"><template>foo</template></tpl-refs><template [ngTemplateOutlet]="currentTplRef"></template>`;
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
|
||||
fixture.detectChanges();
|
||||
var refs = fixture.debugElement.children[0].references['refs'];
|
||||
|
||||
fixture.componentInstance.currentTplRef = refs.tplRefs.first;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('foo');
|
||||
|
||||
fixture.componentInstance.currentTplRef = null;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should swap content if TemplateRef changes',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = `<tpl-refs #refs="tplRefs"><template>foo</template><template>bar</template></tpl-refs><template [ngTemplateOutlet]="currentTplRef"></template>`;
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
|
||||
fixture.detectChanges();
|
||||
var refs = fixture.debugElement.children[0].references['refs'];
|
||||
|
||||
fixture.componentInstance.currentTplRef = refs.tplRefs.first;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('foo');
|
||||
|
||||
fixture.componentInstance.currentTplRef = refs.tplRefs.last;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('bar');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Directive({selector: 'tpl-refs', exportAs: 'tplRefs'})
|
||||
class CaptureTplRefs {
|
||||
@ContentChildren(TemplateRef) tplRefs: QueryList<TemplateRef<any>>;
|
||||
}
|
||||
|
||||
@Component({selector: 'test-cmp', directives: [NgTemplateOutlet, CaptureTplRefs], template: ''})
|
||||
class TestComponent {
|
||||
currentTplRef: TemplateRef<any>;
|
||||
}
|
72
modules/@angular/common/test/directives/non_bindable_spec.ts
Normal file
72
modules/@angular/common/test/directives/non_bindable_spec.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import {
|
||||
AsyncTestCompleter,
|
||||
TestComponentBuilder,
|
||||
beforeEach,
|
||||
ddescribe,
|
||||
describe,
|
||||
el,
|
||||
expect,
|
||||
iit,
|
||||
inject,
|
||||
it,
|
||||
xit,
|
||||
} from 'angular2/testing_internal';
|
||||
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
|
||||
import {Component, Directive} from 'angular2/core';
|
||||
import {ElementRef} from 'angular2/src/core/linker/element_ref';
|
||||
|
||||
export function main() {
|
||||
describe('non-bindable', () => {
|
||||
it('should not interpolate children',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div>{{text}}<span ngNonBindable>{{text}}</span></div>';
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('foo{{text}}');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should ignore directives on child nodes',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div ngNonBindable><span id=child test-dec>{{text}}</span></div>';
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
|
||||
// We must use DOM.querySelector instead of fixture.query here
|
||||
// since the elements inside are not compiled.
|
||||
var span = DOM.querySelector(fixture.debugElement.nativeElement, '#child');
|
||||
expect(DOM.hasClass(span, 'compiled')).toBeFalsy();
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should trigger directives on the same node',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
var template = '<div><span id=child ngNonBindable test-dec>{{text}}</span></div>';
|
||||
tcb.overrideTemplate(TestComponent, template)
|
||||
.createAsync(TestComponent)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
var span = DOM.querySelector(fixture.debugElement.nativeElement, '#child');
|
||||
expect(DOM.hasClass(span, 'compiled')).toBeTruthy();
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
})
|
||||
}
|
||||
|
||||
@Directive({selector: '[test-dec]'})
|
||||
class TestDirective {
|
||||
constructor(el: ElementRef) { DOM.addClass(el.nativeElement, 'compiled'); }
|
||||
}
|
||||
|
||||
@Component({selector: 'test-cmp', directives: [TestDirective], template: ''})
|
||||
class TestComponent {
|
||||
text: string;
|
||||
constructor() { this.text = 'foo'; }
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
library angular2.test.directives.observable_list_iterable_diff_spec;
|
||||
|
||||
import 'package:angular2/testing_internal.dart';
|
||||
import 'package:observe/observe.dart' show ObservableList;
|
||||
import 'package:angular2/core.dart' show ChangeDetectorRef;
|
||||
import 'package:angular2/common.dart' show ObservableListDiffFactory;
|
||||
|
||||
@proxy
|
||||
class SpyChangeDetectorRef extends SpyObject implements ChangeDetectorRef {}
|
||||
|
||||
main() {
|
||||
describe('ObservableListDiff', () {
|
||||
var factory, changeDetectorRef;
|
||||
|
||||
beforeEach(() {
|
||||
factory = const ObservableListDiffFactory();
|
||||
changeDetectorRef = new SpyChangeDetectorRef();
|
||||
});
|
||||
|
||||
describe("supports", () {
|
||||
it("should be true for ObservableList", () {
|
||||
expect(factory.supports(new ObservableList())).toBe(true);
|
||||
});
|
||||
|
||||
it("should be false otherwise", () {
|
||||
expect(factory.supports([1, 2, 3])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return itself when called the first time", () {
|
||||
final d = factory.create(changeDetectorRef);
|
||||
final c = new ObservableList.from([1, 2]);
|
||||
expect(d.diff(c)).toBe(d);
|
||||
});
|
||||
|
||||
it("should return itself when no changes between the calls", () {
|
||||
final d = factory.create(changeDetectorRef);
|
||||
|
||||
final c = new ObservableList.from([1, 2]);
|
||||
|
||||
d.diff(c);
|
||||
|
||||
expect(d.diff(c)).toBe(null);
|
||||
});
|
||||
|
||||
it("should return the wrapped value once a change has been triggered",
|
||||
fakeAsync(() {
|
||||
final d = factory.create(changeDetectorRef);
|
||||
|
||||
final c = new ObservableList.from([1, 2]);
|
||||
|
||||
d.diff(c);
|
||||
|
||||
c.add(3);
|
||||
|
||||
// same value, because we have not detected the change yet
|
||||
expect(d.diff(c)).toBe(null);
|
||||
|
||||
// now we detect the change
|
||||
flushMicrotasks();
|
||||
expect(d.diff(c)).toBe(d);
|
||||
}));
|
||||
|
||||
it("should request a change detection check upon receiving a change",
|
||||
fakeAsync(() {
|
||||
final d = factory.create(changeDetectorRef);
|
||||
|
||||
final c = new ObservableList.from([1, 2]);
|
||||
d.diff(c);
|
||||
|
||||
c.add(3);
|
||||
flushMicrotasks();
|
||||
|
||||
expect(changeDetectorRef.spy("markForCheck")).toHaveBeenCalledOnce();
|
||||
}));
|
||||
|
||||
it("should return the wrapped value after changing a collection", () {
|
||||
final d = factory.create(changeDetectorRef);
|
||||
|
||||
final c1 = new ObservableList.from([1, 2]);
|
||||
final c2 = new ObservableList.from([3, 4]);
|
||||
|
||||
expect(d.diff(c1)).toBe(d);
|
||||
expect(d.diff(c2)).toBe(d);
|
||||
});
|
||||
|
||||
it("should not unbsubscribe from the stream of chagnes after changing a collection",
|
||||
() {
|
||||
final d = factory.create(changeDetectorRef);
|
||||
|
||||
final c1 = new ObservableList.from([1, 2]);
|
||||
expect(d.diff(c1)).toBe(d);
|
||||
|
||||
final c2 = new ObservableList.from([3, 4]);
|
||||
expect(d.diff(c2)).toBe(d);
|
||||
|
||||
// pushing into the first collection has no effect, and we do not see the change
|
||||
c1.add(3);
|
||||
expect(d.diff(c2)).toBe(null);
|
||||
});
|
||||
});
|
||||
}
|
493
modules/@angular/common/test/forms/directives_spec.ts
Normal file
493
modules/@angular/common/test/forms/directives_spec.ts
Normal file
@ -0,0 +1,493 @@
|
||||
import {
|
||||
ddescribe,
|
||||
describe,
|
||||
fakeAsync,
|
||||
flushMicrotasks,
|
||||
it,
|
||||
iit,
|
||||
xit,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
el,
|
||||
AsyncTestCompleter,
|
||||
inject,
|
||||
tick
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {SpyNgControl, SpyValueAccessor} from '../spies';
|
||||
|
||||
import {
|
||||
ControlGroup,
|
||||
Control,
|
||||
NgControlName,
|
||||
NgControlGroup,
|
||||
NgFormModel,
|
||||
ControlValueAccessor,
|
||||
Validators,
|
||||
NgForm,
|
||||
NgModel,
|
||||
NgFormControl,
|
||||
NgControl,
|
||||
DefaultValueAccessor,
|
||||
CheckboxControlValueAccessor,
|
||||
SelectControlValueAccessor,
|
||||
Validator
|
||||
} from 'angular2/common';
|
||||
|
||||
|
||||
import {selectValueAccessor, composeValidators} from 'angular2/src/common/forms/directives/shared';
|
||||
import {TimerWrapper} from 'angular2/src/facade/async';
|
||||
import {PromiseWrapper} from 'angular2/src/facade/promise';
|
||||
import {SimpleChange} from 'angular2/src/core/change_detection';
|
||||
|
||||
class DummyControlValueAccessor implements ControlValueAccessor {
|
||||
writtenValue;
|
||||
|
||||
registerOnChange(fn) {}
|
||||
registerOnTouched(fn) {}
|
||||
|
||||
writeValue(obj: any): void { this.writtenValue = obj; }
|
||||
}
|
||||
|
||||
class CustomValidatorDirective implements Validator {
|
||||
validate(c: Control): {[key: string]: any} { return {"custom": true}; }
|
||||
}
|
||||
|
||||
function asyncValidator(expected, timeout = 0) {
|
||||
return (c) => {
|
||||
var completer = PromiseWrapper.completer();
|
||||
var res = c.value != expected ? {"async": true} : null;
|
||||
if (timeout == 0) {
|
||||
completer.resolve(res);
|
||||
} else {
|
||||
TimerWrapper.setTimeout(() => { completer.resolve(res); }, timeout);
|
||||
}
|
||||
return completer.promise;
|
||||
};
|
||||
}
|
||||
|
||||
export function main() {
|
||||
describe("Form Directives", () => {
|
||||
var defaultAccessor;
|
||||
|
||||
beforeEach(() => { defaultAccessor = new DefaultValueAccessor(null, null); });
|
||||
|
||||
describe("shared", () => {
|
||||
describe("selectValueAccessor", () => {
|
||||
var dir: NgControl;
|
||||
|
||||
beforeEach(() => { dir = <any>new SpyNgControl(); });
|
||||
|
||||
it("should throw when given an empty array",
|
||||
() => { expect(() => selectValueAccessor(dir, [])).toThrowError(); });
|
||||
|
||||
it("should return the default value accessor when no other provided",
|
||||
() => { expect(selectValueAccessor(dir, [defaultAccessor])).toEqual(defaultAccessor); });
|
||||
|
||||
it("should return checkbox accessor when provided", () => {
|
||||
var checkboxAccessor = new CheckboxControlValueAccessor(null, null);
|
||||
expect(selectValueAccessor(dir, [defaultAccessor, checkboxAccessor]))
|
||||
.toEqual(checkboxAccessor);
|
||||
});
|
||||
|
||||
it("should return select accessor when provided", () => {
|
||||
var selectAccessor = new SelectControlValueAccessor(null, null);
|
||||
expect(selectValueAccessor(dir, [defaultAccessor, selectAccessor]))
|
||||
.toEqual(selectAccessor);
|
||||
});
|
||||
|
||||
it("should throw when more than one build-in accessor is provided", () => {
|
||||
var checkboxAccessor = new CheckboxControlValueAccessor(null, null);
|
||||
var selectAccessor = new SelectControlValueAccessor(null, null);
|
||||
expect(() => selectValueAccessor(dir, [checkboxAccessor, selectAccessor])).toThrowError();
|
||||
});
|
||||
|
||||
it("should return custom accessor when provided", () => {
|
||||
var customAccessor = new SpyValueAccessor();
|
||||
var checkboxAccessor = new CheckboxControlValueAccessor(null, null);
|
||||
expect(selectValueAccessor(dir, [defaultAccessor, customAccessor, checkboxAccessor]))
|
||||
.toEqual(customAccessor);
|
||||
});
|
||||
|
||||
it("should throw when more than one custom accessor is provided", () => {
|
||||
var customAccessor: ControlValueAccessor = <any>new SpyValueAccessor();
|
||||
expect(() => selectValueAccessor(dir, [customAccessor, customAccessor])).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe("composeValidators", () => {
|
||||
it("should compose functions", () => {
|
||||
var dummy1 = (_) => ({"dummy1": true});
|
||||
var dummy2 = (_) => ({"dummy2": true});
|
||||
var v = composeValidators([dummy1, dummy2]);
|
||||
expect(v(new Control(""))).toEqual({"dummy1": true, "dummy2": true});
|
||||
});
|
||||
|
||||
it("should compose validator directives", () => {
|
||||
var dummy1 = (_) => ({"dummy1": true});
|
||||
var v = composeValidators([dummy1, new CustomValidatorDirective()]);
|
||||
expect(v(new Control(""))).toEqual({"dummy1": true, "custom": true});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("NgFormModel", () => {
|
||||
var form;
|
||||
var formModel: ControlGroup;
|
||||
var loginControlDir;
|
||||
|
||||
beforeEach(() => {
|
||||
form = new NgFormModel([], []);
|
||||
formModel = new ControlGroup({
|
||||
"login": new Control(),
|
||||
"passwords":
|
||||
new ControlGroup({"password": new Control(), "passwordConfirm": new Control()})
|
||||
});
|
||||
form.form = formModel;
|
||||
|
||||
loginControlDir = new NgControlName(form, [Validators.required],
|
||||
[asyncValidator("expected")], [defaultAccessor]);
|
||||
loginControlDir.name = "login";
|
||||
loginControlDir.valueAccessor = new DummyControlValueAccessor();
|
||||
});
|
||||
|
||||
it("should reexport control properties", () => {
|
||||
expect(form.control).toBe(formModel);
|
||||
expect(form.value).toBe(formModel.value);
|
||||
expect(form.valid).toBe(formModel.valid);
|
||||
expect(form.errors).toBe(formModel.errors);
|
||||
expect(form.pristine).toBe(formModel.pristine);
|
||||
expect(form.dirty).toBe(formModel.dirty);
|
||||
expect(form.touched).toBe(formModel.touched);
|
||||
expect(form.untouched).toBe(formModel.untouched);
|
||||
});
|
||||
|
||||
describe("addControl", () => {
|
||||
it("should throw when no control found", () => {
|
||||
var dir = new NgControlName(form, null, null, [defaultAccessor]);
|
||||
dir.name = "invalidName";
|
||||
|
||||
expect(() => form.addControl(dir))
|
||||
.toThrowError(new RegExp("Cannot find control 'invalidName'"));
|
||||
});
|
||||
|
||||
it("should throw when no value accessor", () => {
|
||||
var dir = new NgControlName(form, null, null, null);
|
||||
dir.name = "login";
|
||||
|
||||
expect(() => form.addControl(dir))
|
||||
.toThrowError(new RegExp("No value accessor for 'login'"));
|
||||
});
|
||||
|
||||
it("should set up validators", fakeAsync(() => {
|
||||
form.addControl(loginControlDir);
|
||||
|
||||
// sync validators are set
|
||||
expect(formModel.hasError("required", ["login"])).toBe(true);
|
||||
expect(formModel.hasError("async", ["login"])).toBe(false);
|
||||
|
||||
(<Control>formModel.find(["login"])).updateValue("invalid value");
|
||||
|
||||
// sync validator passes, running async validators
|
||||
expect(formModel.pending).toBe(true);
|
||||
|
||||
tick();
|
||||
|
||||
expect(formModel.hasError("required", ["login"])).toBe(false);
|
||||
expect(formModel.hasError("async", ["login"])).toBe(true);
|
||||
}));
|
||||
|
||||
it("should write value to the DOM", () => {
|
||||
(<Control>formModel.find(["login"])).updateValue("initValue");
|
||||
|
||||
form.addControl(loginControlDir);
|
||||
|
||||
expect((<any>loginControlDir.valueAccessor).writtenValue).toEqual("initValue");
|
||||
});
|
||||
|
||||
it("should add the directive to the list of directives included in the form", () => {
|
||||
form.addControl(loginControlDir);
|
||||
expect(form.directives).toEqual([loginControlDir]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addControlGroup", () => {
|
||||
var matchingPasswordsValidator = (g) => {
|
||||
if (g.controls["password"].value != g.controls["passwordConfirm"].value) {
|
||||
return {"differentPasswords": true};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
it("should set up validator", fakeAsync(() => {
|
||||
var group = new NgControlGroup(form, [matchingPasswordsValidator],
|
||||
[asyncValidator('expected')]);
|
||||
group.name = "passwords";
|
||||
form.addControlGroup(group);
|
||||
|
||||
(<Control>formModel.find(["passwords", "password"])).updateValue("somePassword");
|
||||
(<Control>formModel.find(["passwords", "passwordConfirm"]))
|
||||
.updateValue("someOtherPassword");
|
||||
|
||||
// sync validators are set
|
||||
expect(formModel.hasError("differentPasswords", ["passwords"])).toEqual(true);
|
||||
|
||||
(<Control>formModel.find(["passwords", "passwordConfirm"]))
|
||||
.updateValue("somePassword");
|
||||
|
||||
// sync validators pass, running async validators
|
||||
expect(formModel.pending).toBe(true);
|
||||
|
||||
tick();
|
||||
|
||||
expect(formModel.hasError("async", ["passwords"])).toBe(true);
|
||||
}));
|
||||
});
|
||||
|
||||
describe("removeControl", () => {
|
||||
it("should remove the directive to the list of directives included in the form", () => {
|
||||
form.addControl(loginControlDir);
|
||||
form.removeControl(loginControlDir);
|
||||
expect(form.directives).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ngOnChanges", () => {
|
||||
it("should update dom values of all the directives", () => {
|
||||
form.addControl(loginControlDir);
|
||||
|
||||
(<Control>formModel.find(["login"])).updateValue("new value");
|
||||
|
||||
form.ngOnChanges({});
|
||||
|
||||
expect((<any>loginControlDir.valueAccessor).writtenValue).toEqual("new value");
|
||||
});
|
||||
|
||||
it("should set up a sync validator", () => {
|
||||
var formValidator = (c) => ({"custom": true});
|
||||
var f = new NgFormModel([formValidator], []);
|
||||
f.form = formModel;
|
||||
f.ngOnChanges({"form": new SimpleChange(null, null)});
|
||||
|
||||
expect(formModel.errors).toEqual({"custom": true});
|
||||
});
|
||||
|
||||
it("should set up an async validator", fakeAsync(() => {
|
||||
var f = new NgFormModel([], [asyncValidator("expected")]);
|
||||
f.form = formModel;
|
||||
f.ngOnChanges({"form": new SimpleChange(null, null)});
|
||||
|
||||
tick();
|
||||
|
||||
expect(formModel.errors).toEqual({"async": true});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("NgForm", () => {
|
||||
var form;
|
||||
var formModel: ControlGroup;
|
||||
var loginControlDir;
|
||||
var personControlGroupDir;
|
||||
|
||||
beforeEach(() => {
|
||||
form = new NgForm([], []);
|
||||
formModel = form.form;
|
||||
|
||||
personControlGroupDir = new NgControlGroup(form, [], []);
|
||||
personControlGroupDir.name = "person";
|
||||
|
||||
loginControlDir = new NgControlName(personControlGroupDir, null, null, [defaultAccessor]);
|
||||
loginControlDir.name = "login";
|
||||
loginControlDir.valueAccessor = new DummyControlValueAccessor();
|
||||
});
|
||||
|
||||
it("should reexport control properties", () => {
|
||||
expect(form.control).toBe(formModel);
|
||||
expect(form.value).toBe(formModel.value);
|
||||
expect(form.valid).toBe(formModel.valid);
|
||||
expect(form.errors).toBe(formModel.errors);
|
||||
expect(form.pristine).toBe(formModel.pristine);
|
||||
expect(form.dirty).toBe(formModel.dirty);
|
||||
expect(form.touched).toBe(formModel.touched);
|
||||
expect(form.untouched).toBe(formModel.untouched);
|
||||
});
|
||||
|
||||
describe("addControl & addControlGroup", () => {
|
||||
it("should create a control with the given name", fakeAsync(() => {
|
||||
form.addControlGroup(personControlGroupDir);
|
||||
form.addControl(loginControlDir);
|
||||
|
||||
flushMicrotasks();
|
||||
|
||||
expect(formModel.find(["person", "login"])).not.toBeNull;
|
||||
}));
|
||||
|
||||
// should update the form's value and validity
|
||||
});
|
||||
|
||||
describe("removeControl & removeControlGroup", () => {
|
||||
it("should remove control", fakeAsync(() => {
|
||||
form.addControlGroup(personControlGroupDir);
|
||||
form.addControl(loginControlDir);
|
||||
|
||||
form.removeControlGroup(personControlGroupDir);
|
||||
form.removeControl(loginControlDir);
|
||||
|
||||
flushMicrotasks();
|
||||
|
||||
expect(formModel.find(["person"])).toBeNull();
|
||||
expect(formModel.find(["person", "login"])).toBeNull();
|
||||
}));
|
||||
|
||||
// should update the form's value and validity
|
||||
});
|
||||
|
||||
it("should set up sync validator", fakeAsync(() => {
|
||||
var formValidator = (c) => ({"custom": true});
|
||||
var f = new NgForm([formValidator], []);
|
||||
|
||||
tick();
|
||||
|
||||
expect(f.form.errors).toEqual({"custom": true});
|
||||
}));
|
||||
|
||||
it("should set up async validator", fakeAsync(() => {
|
||||
var f = new NgForm([], [asyncValidator("expected")]);
|
||||
|
||||
tick();
|
||||
|
||||
expect(f.form.errors).toEqual({"async": true});
|
||||
}));
|
||||
});
|
||||
|
||||
describe("NgControlGroup", () => {
|
||||
var formModel;
|
||||
var controlGroupDir;
|
||||
|
||||
beforeEach(() => {
|
||||
formModel = new ControlGroup({"login": new Control(null)});
|
||||
|
||||
var parent = new NgFormModel([], []);
|
||||
parent.form = new ControlGroup({"group": formModel});
|
||||
controlGroupDir = new NgControlGroup(parent, [], []);
|
||||
controlGroupDir.name = "group";
|
||||
});
|
||||
|
||||
it("should reexport control properties", () => {
|
||||
expect(controlGroupDir.control).toBe(formModel);
|
||||
expect(controlGroupDir.value).toBe(formModel.value);
|
||||
expect(controlGroupDir.valid).toBe(formModel.valid);
|
||||
expect(controlGroupDir.errors).toBe(formModel.errors);
|
||||
expect(controlGroupDir.pristine).toBe(formModel.pristine);
|
||||
expect(controlGroupDir.dirty).toBe(formModel.dirty);
|
||||
expect(controlGroupDir.touched).toBe(formModel.touched);
|
||||
expect(controlGroupDir.untouched).toBe(formModel.untouched);
|
||||
});
|
||||
});
|
||||
|
||||
describe("NgFormControl", () => {
|
||||
var controlDir;
|
||||
var control;
|
||||
var checkProperties = function(control) {
|
||||
expect(controlDir.control).toBe(control);
|
||||
expect(controlDir.value).toBe(control.value);
|
||||
expect(controlDir.valid).toBe(control.valid);
|
||||
expect(controlDir.errors).toBe(control.errors);
|
||||
expect(controlDir.pristine).toBe(control.pristine);
|
||||
expect(controlDir.dirty).toBe(control.dirty);
|
||||
expect(controlDir.touched).toBe(control.touched);
|
||||
expect(controlDir.untouched).toBe(control.untouched);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
controlDir = new NgFormControl([Validators.required], [], [defaultAccessor]);
|
||||
controlDir.valueAccessor = new DummyControlValueAccessor();
|
||||
|
||||
control = new Control(null);
|
||||
controlDir.form = control;
|
||||
});
|
||||
|
||||
it("should reexport control properties", () => { checkProperties(control); });
|
||||
|
||||
it("should reexport new control properties", () => {
|
||||
var newControl = new Control(null);
|
||||
controlDir.form = newControl;
|
||||
controlDir.ngOnChanges({"form": new SimpleChange(control, newControl)});
|
||||
|
||||
checkProperties(newControl);
|
||||
});
|
||||
|
||||
it("should set up validator", () => {
|
||||
expect(control.valid).toBe(true);
|
||||
|
||||
// this will add the required validator and recalculate the validity
|
||||
controlDir.ngOnChanges({"form": new SimpleChange(null, control)});
|
||||
|
||||
expect(control.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("NgModel", () => {
|
||||
var ngModel;
|
||||
|
||||
beforeEach(() => {
|
||||
ngModel =
|
||||
new NgModel([Validators.required], [asyncValidator("expected")], [defaultAccessor]);
|
||||
ngModel.valueAccessor = new DummyControlValueAccessor();
|
||||
});
|
||||
|
||||
it("should reexport control properties", () => {
|
||||
var control = ngModel.control;
|
||||
expect(ngModel.control).toBe(control);
|
||||
expect(ngModel.value).toBe(control.value);
|
||||
expect(ngModel.valid).toBe(control.valid);
|
||||
expect(ngModel.errors).toBe(control.errors);
|
||||
expect(ngModel.pristine).toBe(control.pristine);
|
||||
expect(ngModel.dirty).toBe(control.dirty);
|
||||
expect(ngModel.touched).toBe(control.touched);
|
||||
expect(ngModel.untouched).toBe(control.untouched);
|
||||
});
|
||||
|
||||
it("should set up validator", fakeAsync(() => {
|
||||
// this will add the required validator and recalculate the validity
|
||||
ngModel.ngOnChanges({});
|
||||
tick();
|
||||
|
||||
expect(ngModel.control.errors).toEqual({"required": true});
|
||||
|
||||
ngModel.control.updateValue("someValue");
|
||||
tick();
|
||||
|
||||
expect(ngModel.control.errors).toEqual({"async": true});
|
||||
}));
|
||||
});
|
||||
|
||||
describe("NgControlName", () => {
|
||||
var formModel;
|
||||
var controlNameDir;
|
||||
|
||||
beforeEach(() => {
|
||||
formModel = new Control("name");
|
||||
|
||||
var parent = new NgFormModel([], []);
|
||||
parent.form = new ControlGroup({"name": formModel});
|
||||
controlNameDir = new NgControlName(parent, [], [], [defaultAccessor]);
|
||||
controlNameDir.name = "name";
|
||||
});
|
||||
|
||||
it("should reexport control properties", () => {
|
||||
expect(controlNameDir.control).toBe(formModel);
|
||||
expect(controlNameDir.value).toBe(formModel.value);
|
||||
expect(controlNameDir.valid).toBe(formModel.valid);
|
||||
expect(controlNameDir.errors).toBe(formModel.errors);
|
||||
expect(controlNameDir.pristine).toBe(formModel.pristine);
|
||||
expect(controlNameDir.dirty).toBe(formModel.dirty);
|
||||
expect(controlNameDir.touched).toBe(formModel.touched);
|
||||
expect(controlNameDir.untouched).toBe(formModel.untouched);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
72
modules/@angular/common/test/forms/form_builder_spec.ts
Normal file
72
modules/@angular/common/test/forms/form_builder_spec.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import {
|
||||
ddescribe,
|
||||
describe,
|
||||
it,
|
||||
iit,
|
||||
xit,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
el
|
||||
} from 'angular2/testing_internal';
|
||||
import {Control, FormBuilder} from 'angular2/common';
|
||||
import {PromiseWrapper} from 'angular2/src/facade/promise';
|
||||
|
||||
export function main() {
|
||||
function syncValidator(_) { return null; }
|
||||
function asyncValidator(_) { return PromiseWrapper.resolve(null); }
|
||||
|
||||
describe("Form Builder", () => {
|
||||
var b;
|
||||
|
||||
beforeEach(() => { b = new FormBuilder(); });
|
||||
|
||||
it("should create controls from a value", () => {
|
||||
var g = b.group({"login": "some value"});
|
||||
|
||||
expect(g.controls["login"].value).toEqual("some value");
|
||||
});
|
||||
|
||||
it("should create controls from an array", () => {
|
||||
var g = b.group(
|
||||
{"login": ["some value"], "password": ["some value", syncValidator, asyncValidator]});
|
||||
|
||||
expect(g.controls["login"].value).toEqual("some value");
|
||||
expect(g.controls["password"].value).toEqual("some value");
|
||||
expect(g.controls["password"].validator).toEqual(syncValidator);
|
||||
expect(g.controls["password"].asyncValidator).toEqual(asyncValidator);
|
||||
});
|
||||
|
||||
it("should use controls", () => {
|
||||
var g = b.group({"login": b.control("some value", syncValidator, asyncValidator)});
|
||||
|
||||
expect(g.controls["login"].value).toEqual("some value");
|
||||
expect(g.controls["login"].validator).toBe(syncValidator);
|
||||
expect(g.controls["login"].asyncValidator).toBe(asyncValidator);
|
||||
});
|
||||
|
||||
it("should create groups with optional controls", () => {
|
||||
var g = b.group({"login": "some value"}, {"optionals": {"login": false}});
|
||||
|
||||
expect(g.contains("login")).toEqual(false);
|
||||
});
|
||||
|
||||
it("should create groups with a custom validator", () => {
|
||||
var g = b.group({"login": "some value"},
|
||||
{"validator": syncValidator, "asyncValidator": asyncValidator});
|
||||
|
||||
expect(g.validator).toBe(syncValidator);
|
||||
expect(g.asyncValidator).toBe(asyncValidator);
|
||||
});
|
||||
|
||||
it("should create control arrays", () => {
|
||||
var c = b.control("three");
|
||||
var a = b.array(["one", ["two", syncValidator], c, b.array(['four'])], syncValidator,
|
||||
asyncValidator);
|
||||
|
||||
expect(a.value).toEqual(['one', 'two', 'three', ['four']]);
|
||||
expect(a.validator).toBe(syncValidator);
|
||||
expect(a.asyncValidator).toBe(asyncValidator);
|
||||
});
|
||||
});
|
||||
}
|
1410
modules/@angular/common/test/forms/integration_spec.ts
Normal file
1410
modules/@angular/common/test/forms/integration_spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
806
modules/@angular/common/test/forms/model_spec.ts
Normal file
806
modules/@angular/common/test/forms/model_spec.ts
Normal file
@ -0,0 +1,806 @@
|
||||
import {
|
||||
ddescribe,
|
||||
describe,
|
||||
it,
|
||||
iit,
|
||||
xit,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
el,
|
||||
AsyncTestCompleter,
|
||||
fakeAsync,
|
||||
tick,
|
||||
inject
|
||||
} from 'angular2/testing_internal';
|
||||
import {ControlGroup, Control, ControlArray, Validators} from 'angular2/common';
|
||||
import {IS_DART, isPresent} from 'angular2/src/facade/lang';
|
||||
import {PromiseWrapper} from 'angular2/src/facade/promise';
|
||||
import {TimerWrapper, ObservableWrapper, EventEmitter} from 'angular2/src/facade/async';
|
||||
|
||||
export function main() {
|
||||
function asyncValidator(expected, timeouts = /*@ts2dart_const*/ {}) {
|
||||
return (c) => {
|
||||
var completer = PromiseWrapper.completer();
|
||||
var t = isPresent(timeouts[c.value]) ? timeouts[c.value] : 0;
|
||||
var res = c.value != expected ? {"async": true} : null;
|
||||
|
||||
if (t == 0) {
|
||||
completer.resolve(res);
|
||||
} else {
|
||||
TimerWrapper.setTimeout(() => { completer.resolve(res); }, t);
|
||||
}
|
||||
|
||||
return completer.promise;
|
||||
};
|
||||
}
|
||||
|
||||
function asyncValidatorReturningObservable(c) {
|
||||
var e = new EventEmitter();
|
||||
PromiseWrapper.scheduleMicrotask(() => ObservableWrapper.callEmit(e, {"async": true}));
|
||||
return e;
|
||||
}
|
||||
|
||||
describe("Form Model", () => {
|
||||
describe("Control", () => {
|
||||
it("should default the value to null", () => {
|
||||
var c = new Control();
|
||||
expect(c.value).toBe(null);
|
||||
});
|
||||
|
||||
describe("validator", () => {
|
||||
it("should run validator with the initial value", () => {
|
||||
var c = new Control("value", Validators.required);
|
||||
expect(c.valid).toEqual(true);
|
||||
});
|
||||
|
||||
it("should rerun the validator when the value changes", () => {
|
||||
var c = new Control("value", Validators.required);
|
||||
c.updateValue(null);
|
||||
expect(c.valid).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return errors", () => {
|
||||
var c = new Control(null, Validators.required);
|
||||
expect(c.errors).toEqual({"required": true});
|
||||
});
|
||||
});
|
||||
|
||||
describe("asyncValidator", () => {
|
||||
it("should run validator with the initial value", fakeAsync(() => {
|
||||
var c = new Control("value", null, asyncValidator("expected"));
|
||||
tick();
|
||||
|
||||
expect(c.valid).toEqual(false);
|
||||
expect(c.errors).toEqual({"async": true});
|
||||
}));
|
||||
|
||||
it("should support validators returning observables", fakeAsync(() => {
|
||||
var c = new Control("value", null, asyncValidatorReturningObservable);
|
||||
tick();
|
||||
|
||||
expect(c.valid).toEqual(false);
|
||||
expect(c.errors).toEqual({"async": true});
|
||||
}));
|
||||
|
||||
it("should rerun the validator when the value changes", fakeAsync(() => {
|
||||
var c = new Control("value", null, asyncValidator("expected"));
|
||||
|
||||
c.updateValue("expected");
|
||||
tick();
|
||||
|
||||
expect(c.valid).toEqual(true);
|
||||
}));
|
||||
|
||||
it("should run the async validator only when the sync validator passes", fakeAsync(() => {
|
||||
var c = new Control("", Validators.required, asyncValidator("expected"));
|
||||
tick();
|
||||
|
||||
expect(c.errors).toEqual({"required": true});
|
||||
|
||||
c.updateValue("some value");
|
||||
tick();
|
||||
|
||||
expect(c.errors).toEqual({"async": true});
|
||||
}));
|
||||
|
||||
it("should mark the control as pending while running the async validation",
|
||||
fakeAsync(() => {
|
||||
var c = new Control("", null, asyncValidator("expected"));
|
||||
|
||||
expect(c.pending).toEqual(true);
|
||||
|
||||
tick();
|
||||
|
||||
expect(c.pending).toEqual(false);
|
||||
}));
|
||||
|
||||
it("should only use the latest async validation run", fakeAsync(() => {
|
||||
var c =
|
||||
new Control("", null, asyncValidator("expected", {"long": 200, "expected": 100}));
|
||||
|
||||
c.updateValue("long");
|
||||
c.updateValue("expected");
|
||||
|
||||
tick(300);
|
||||
|
||||
expect(c.valid).toEqual(true);
|
||||
}));
|
||||
});
|
||||
|
||||
describe("dirty", () => {
|
||||
it("should be false after creating a control", () => {
|
||||
var c = new Control("value");
|
||||
expect(c.dirty).toEqual(false);
|
||||
});
|
||||
|
||||
it("should be true after changing the value of the control", () => {
|
||||
var c = new Control("value");
|
||||
c.markAsDirty();
|
||||
expect(c.dirty).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateValue", () => {
|
||||
var g, c;
|
||||
beforeEach(() => {
|
||||
c = new Control("oldValue");
|
||||
g = new ControlGroup({"one": c});
|
||||
});
|
||||
|
||||
it("should update the value of the control", () => {
|
||||
c.updateValue("newValue");
|
||||
expect(c.value).toEqual("newValue");
|
||||
});
|
||||
|
||||
it("should invoke ngOnChanges if it is present", () => {
|
||||
var ngOnChanges;
|
||||
c.registerOnChange((v) => ngOnChanges = ["invoked", v]);
|
||||
|
||||
c.updateValue("newValue");
|
||||
|
||||
expect(ngOnChanges).toEqual(["invoked", "newValue"]);
|
||||
});
|
||||
|
||||
it("should not invoke on change when explicitly specified", () => {
|
||||
var onChange = null;
|
||||
c.registerOnChange((v) => onChange = ["invoked", v]);
|
||||
|
||||
c.updateValue("newValue", {emitModelToViewChange: false});
|
||||
|
||||
expect(onChange).toBeNull();
|
||||
});
|
||||
|
||||
it("should update the parent", () => {
|
||||
c.updateValue("newValue");
|
||||
expect(g.value).toEqual({"one": "newValue"});
|
||||
});
|
||||
|
||||
it("should not update the parent when explicitly specified", () => {
|
||||
c.updateValue("newValue", {onlySelf: true});
|
||||
expect(g.value).toEqual({"one": "oldValue"});
|
||||
});
|
||||
|
||||
it("should fire an event", fakeAsync(() => {
|
||||
ObservableWrapper.subscribe(c.valueChanges,
|
||||
(value) => { expect(value).toEqual("newValue"); });
|
||||
|
||||
c.updateValue("newValue");
|
||||
tick();
|
||||
}));
|
||||
|
||||
it("should not fire an event when explicitly specified", fakeAsync(() => {
|
||||
ObservableWrapper.subscribe(c.valueChanges, (value) => { throw "Should not happen"; });
|
||||
|
||||
c.updateValue("newValue", {emitEvent: false});
|
||||
|
||||
tick();
|
||||
}));
|
||||
});
|
||||
|
||||
describe("valueChanges & statusChanges", () => {
|
||||
var c;
|
||||
|
||||
beforeEach(() => { c = new Control("old", Validators.required); });
|
||||
|
||||
it("should fire an event after the value has been updated",
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
ObservableWrapper.subscribe(c.valueChanges, (value) => {
|
||||
expect(c.value).toEqual('new');
|
||||
expect(value).toEqual('new');
|
||||
async.done();
|
||||
});
|
||||
c.updateValue("new");
|
||||
}));
|
||||
|
||||
it("should fire an event after the status has been updated to invalid", fakeAsync(() => {
|
||||
ObservableWrapper.subscribe(c.statusChanges, (status) => {
|
||||
expect(c.status).toEqual('INVALID');
|
||||
expect(status).toEqual('INVALID');
|
||||
});
|
||||
|
||||
c.updateValue("");
|
||||
tick();
|
||||
}));
|
||||
|
||||
it("should fire an event after the status has been updated to pending", fakeAsync(() => {
|
||||
var c = new Control("old", Validators.required, asyncValidator("expected"));
|
||||
|
||||
var log = [];
|
||||
ObservableWrapper.subscribe(c.valueChanges, (value) => log.push(`value: '${value}'`));
|
||||
ObservableWrapper.subscribe(c.statusChanges,
|
||||
(status) => log.push(`status: '${status}'`));
|
||||
|
||||
c.updateValue("");
|
||||
tick();
|
||||
|
||||
c.updateValue("nonEmpty");
|
||||
tick();
|
||||
|
||||
c.updateValue("expected");
|
||||
tick();
|
||||
|
||||
expect(log).toEqual([
|
||||
"" + "value: ''",
|
||||
"status: 'INVALID'",
|
||||
"value: 'nonEmpty'",
|
||||
"status: 'PENDING'",
|
||||
"status: 'INVALID'",
|
||||
"value: 'expected'",
|
||||
"status: 'PENDING'",
|
||||
"status: 'VALID'",
|
||||
]);
|
||||
}));
|
||||
|
||||
// TODO: remove the if statement after making observable delivery sync
|
||||
if (!IS_DART) {
|
||||
it("should update set errors and status before emitting an event",
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
c.valueChanges.subscribe(value => {
|
||||
expect(c.valid).toEqual(false);
|
||||
expect(c.errors).toEqual({"required": true});
|
||||
async.done();
|
||||
});
|
||||
c.updateValue("");
|
||||
}));
|
||||
}
|
||||
|
||||
it("should return a cold observable", inject([AsyncTestCompleter], (async) => {
|
||||
c.updateValue("will be ignored");
|
||||
ObservableWrapper.subscribe(c.valueChanges, (value) => {
|
||||
expect(value).toEqual('new');
|
||||
async.done();
|
||||
});
|
||||
c.updateValue("new");
|
||||
}));
|
||||
});
|
||||
|
||||
describe("setErrors", () => {
|
||||
it("should set errors on a control", () => {
|
||||
var c = new Control("someValue");
|
||||
|
||||
c.setErrors({"someError": true});
|
||||
|
||||
expect(c.valid).toEqual(false);
|
||||
expect(c.errors).toEqual({"someError": true});
|
||||
});
|
||||
|
||||
it("should reset the errors and validity when the value changes", () => {
|
||||
var c = new Control("someValue", Validators.required);
|
||||
|
||||
c.setErrors({"someError": true});
|
||||
c.updateValue("");
|
||||
|
||||
expect(c.errors).toEqual({"required": true});
|
||||
});
|
||||
|
||||
it("should update the parent group's validity", () => {
|
||||
var c = new Control("someValue");
|
||||
var g = new ControlGroup({"one": c});
|
||||
|
||||
expect(g.valid).toEqual(true);
|
||||
|
||||
c.setErrors({"someError": true});
|
||||
|
||||
expect(g.valid).toEqual(false);
|
||||
});
|
||||
|
||||
it("should not reset parent's errors", () => {
|
||||
var c = new Control("someValue");
|
||||
var g = new ControlGroup({"one": c});
|
||||
|
||||
g.setErrors({"someGroupError": true});
|
||||
c.setErrors({"someError": true});
|
||||
|
||||
expect(g.errors).toEqual({"someGroupError": true});
|
||||
});
|
||||
|
||||
it("should reset errors when updating a value", () => {
|
||||
var c = new Control("oldValue");
|
||||
var g = new ControlGroup({"one": c});
|
||||
|
||||
g.setErrors({"someGroupError": true});
|
||||
c.setErrors({"someError": true});
|
||||
|
||||
c.updateValue("newValue");
|
||||
|
||||
expect(c.errors).toEqual(null);
|
||||
expect(g.errors).toEqual(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ControlGroup", () => {
|
||||
describe("value", () => {
|
||||
it("should be the reduced value of the child controls", () => {
|
||||
var g = new ControlGroup({"one": new Control("111"), "two": new Control("222")});
|
||||
expect(g.value).toEqual({"one": "111", "two": "222"});
|
||||
});
|
||||
|
||||
it("should be empty when there are no child controls", () => {
|
||||
var g = new ControlGroup({});
|
||||
expect(g.value).toEqual({});
|
||||
});
|
||||
|
||||
it("should support nested groups", () => {
|
||||
var g = new ControlGroup(
|
||||
{"one": new Control("111"), "nested": new ControlGroup({"two": new Control("222")})});
|
||||
expect(g.value).toEqual({"one": "111", "nested": {"two": "222"}});
|
||||
|
||||
(<Control>(g.controls["nested"].find("two"))).updateValue("333");
|
||||
|
||||
expect(g.value).toEqual({"one": "111", "nested": {"two": "333"}});
|
||||
});
|
||||
});
|
||||
|
||||
describe("errors", () => {
|
||||
it("should run the validator when the value changes", () => {
|
||||
var simpleValidator = (c) =>
|
||||
c.controls["one"].value != "correct" ? {"broken": true} : null;
|
||||
|
||||
var c = new Control(null);
|
||||
var g = new ControlGroup({"one": c}, null, simpleValidator);
|
||||
|
||||
c.updateValue("correct");
|
||||
|
||||
expect(g.valid).toEqual(true);
|
||||
expect(g.errors).toEqual(null);
|
||||
|
||||
c.updateValue("incorrect");
|
||||
|
||||
expect(g.valid).toEqual(false);
|
||||
expect(g.errors).toEqual({"broken": true});
|
||||
});
|
||||
});
|
||||
|
||||
describe("dirty", () => {
|
||||
var c, g;
|
||||
|
||||
beforeEach(() => {
|
||||
c = new Control('value');
|
||||
g = new ControlGroup({"one": c});
|
||||
});
|
||||
|
||||
it("should be false after creating a control", () => { expect(g.dirty).toEqual(false); });
|
||||
|
||||
it("should be false after changing the value of the control", () => {
|
||||
c.markAsDirty();
|
||||
|
||||
expect(g.dirty).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("optional components", () => {
|
||||
describe("contains", () => {
|
||||
var group;
|
||||
|
||||
beforeEach(() => {
|
||||
group = new ControlGroup(
|
||||
{
|
||||
"required": new Control("requiredValue"),
|
||||
"optional": new Control("optionalValue")
|
||||
},
|
||||
{"optional": false});
|
||||
});
|
||||
|
||||
// rename contains into has
|
||||
it("should return false when the component is not included",
|
||||
() => { expect(group.contains("optional")).toEqual(false); })
|
||||
|
||||
it("should return false when there is no component with the given name",
|
||||
() => { expect(group.contains("something else")).toEqual(false); });
|
||||
|
||||
it("should return true when the component is included", () => {
|
||||
expect(group.contains("required")).toEqual(true);
|
||||
|
||||
group.include("optional");
|
||||
|
||||
expect(group.contains("optional")).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not include an inactive component into the group value", () => {
|
||||
var group = new ControlGroup(
|
||||
{"required": new Control("requiredValue"), "optional": new Control("optionalValue")},
|
||||
{"optional": false});
|
||||
|
||||
expect(group.value).toEqual({"required": "requiredValue"});
|
||||
|
||||
group.include("optional");
|
||||
|
||||
expect(group.value).toEqual({"required": "requiredValue", "optional": "optionalValue"});
|
||||
});
|
||||
|
||||
it("should not run Validators on an inactive component", () => {
|
||||
var group = new ControlGroup(
|
||||
{
|
||||
"required": new Control("requiredValue", Validators.required),
|
||||
"optional": new Control("", Validators.required)
|
||||
},
|
||||
{"optional": false});
|
||||
|
||||
expect(group.valid).toEqual(true);
|
||||
|
||||
group.include("optional");
|
||||
|
||||
expect(group.valid).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("valueChanges", () => {
|
||||
var g, c1, c2;
|
||||
|
||||
beforeEach(() => {
|
||||
c1 = new Control("old1");
|
||||
c2 = new Control("old2");
|
||||
g = new ControlGroup({"one": c1, "two": c2}, {"two": true});
|
||||
});
|
||||
|
||||
it("should fire an event after the value has been updated",
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
ObservableWrapper.subscribe(g.valueChanges, (value) => {
|
||||
expect(g.value).toEqual({'one': 'new1', 'two': 'old2'});
|
||||
expect(value).toEqual({'one': 'new1', 'two': 'old2'});
|
||||
async.done();
|
||||
});
|
||||
c1.updateValue("new1");
|
||||
}));
|
||||
|
||||
it("should fire an event after the control's observable fired an event",
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
var controlCallbackIsCalled = false;
|
||||
|
||||
ObservableWrapper.subscribe(c1.valueChanges,
|
||||
(value) => { controlCallbackIsCalled = true; });
|
||||
|
||||
ObservableWrapper.subscribe(g.valueChanges, (value) => {
|
||||
expect(controlCallbackIsCalled).toBe(true);
|
||||
async.done();
|
||||
});
|
||||
|
||||
c1.updateValue("new1");
|
||||
}));
|
||||
|
||||
it("should fire an event when a control is excluded",
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
ObservableWrapper.subscribe(g.valueChanges, (value) => {
|
||||
expect(value).toEqual({'one': 'old1'});
|
||||
async.done();
|
||||
});
|
||||
|
||||
g.exclude("two");
|
||||
}));
|
||||
|
||||
it("should fire an event when a control is included",
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
g.exclude("two");
|
||||
|
||||
ObservableWrapper.subscribe(g.valueChanges, (value) => {
|
||||
expect(value).toEqual({'one': 'old1', 'two': 'old2'});
|
||||
async.done();
|
||||
});
|
||||
|
||||
g.include("two");
|
||||
}));
|
||||
|
||||
it("should fire an event every time a control is updated",
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
var loggedValues = [];
|
||||
|
||||
ObservableWrapper.subscribe(g.valueChanges, (value) => {
|
||||
loggedValues.push(value);
|
||||
|
||||
if (loggedValues.length == 2) {
|
||||
expect(loggedValues)
|
||||
.toEqual([{"one": "new1", "two": "old2"}, {"one": "new1", "two": "new2"}]);
|
||||
async.done();
|
||||
}
|
||||
});
|
||||
|
||||
c1.updateValue("new1");
|
||||
c2.updateValue("new2");
|
||||
}));
|
||||
|
||||
xit("should not fire an event when an excluded control is updated",
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
// hard to test without hacking zones
|
||||
}));
|
||||
});
|
||||
|
||||
describe("getError", () => {
|
||||
it("should return the error when it is present", () => {
|
||||
var c = new Control("", Validators.required);
|
||||
var g = new ControlGroup({"one": c});
|
||||
expect(c.getError("required")).toEqual(true);
|
||||
expect(g.getError("required", ["one"])).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return null otherwise", () => {
|
||||
var c = new Control("not empty", Validators.required);
|
||||
var g = new ControlGroup({"one": c});
|
||||
expect(c.getError("invalid")).toEqual(null);
|
||||
expect(g.getError("required", ["one"])).toEqual(null);
|
||||
expect(g.getError("required", ["invalid"])).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("asyncValidator", () => {
|
||||
it("should run the async validator", fakeAsync(() => {
|
||||
var c = new Control("value");
|
||||
var g = new ControlGroup({"one": c}, null, null, asyncValidator("expected"));
|
||||
|
||||
expect(g.pending).toEqual(true);
|
||||
|
||||
tick(1);
|
||||
|
||||
expect(g.errors).toEqual({"async": true});
|
||||
expect(g.pending).toEqual(false);
|
||||
}));
|
||||
|
||||
it("should set the parent group's status to pending", fakeAsync(() => {
|
||||
var c = new Control("value", null, asyncValidator("expected"));
|
||||
var g = new ControlGroup({"one": c});
|
||||
|
||||
expect(g.pending).toEqual(true);
|
||||
|
||||
tick(1);
|
||||
|
||||
expect(g.pending).toEqual(false);
|
||||
}));
|
||||
|
||||
it("should run the parent group's async validator when children are pending",
|
||||
fakeAsync(() => {
|
||||
var c = new Control("value", null, asyncValidator("expected"));
|
||||
var g = new ControlGroup({"one": c}, null, null, asyncValidator("expected"));
|
||||
|
||||
tick(1);
|
||||
|
||||
expect(g.errors).toEqual({"async": true});
|
||||
expect(g.find(["one"]).errors).toEqual({"async": true});
|
||||
}));
|
||||
})
|
||||
});
|
||||
|
||||
describe("ControlArray", () => {
|
||||
describe("adding/removing", () => {
|
||||
var a: ControlArray;
|
||||
var c1, c2, c3;
|
||||
|
||||
beforeEach(() => {
|
||||
a = new ControlArray([]);
|
||||
c1 = new Control(1);
|
||||
c2 = new Control(2);
|
||||
c3 = new Control(3);
|
||||
});
|
||||
|
||||
it("should support pushing", () => {
|
||||
a.push(c1);
|
||||
expect(a.length).toEqual(1);
|
||||
expect(a.controls).toEqual([c1]);
|
||||
});
|
||||
|
||||
it("should support removing", () => {
|
||||
a.push(c1);
|
||||
a.push(c2);
|
||||
a.push(c3);
|
||||
|
||||
a.removeAt(1);
|
||||
|
||||
expect(a.controls).toEqual([c1, c3]);
|
||||
});
|
||||
|
||||
it("should support inserting", () => {
|
||||
a.push(c1);
|
||||
a.push(c3);
|
||||
|
||||
a.insert(1, c2);
|
||||
|
||||
expect(a.controls).toEqual([c1, c2, c3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("value", () => {
|
||||
it("should be the reduced value of the child controls", () => {
|
||||
var a = new ControlArray([new Control(1), new Control(2)]);
|
||||
expect(a.value).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it("should be an empty array when there are no child controls", () => {
|
||||
var a = new ControlArray([]);
|
||||
expect(a.value).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("errors", () => {
|
||||
it("should run the validator when the value changes", () => {
|
||||
var simpleValidator = (c) => c.controls[0].value != "correct" ? {"broken": true} : null;
|
||||
|
||||
var c = new Control(null);
|
||||
var g = new ControlArray([c], simpleValidator);
|
||||
|
||||
c.updateValue("correct");
|
||||
|
||||
expect(g.valid).toEqual(true);
|
||||
expect(g.errors).toEqual(null);
|
||||
|
||||
c.updateValue("incorrect");
|
||||
|
||||
expect(g.valid).toEqual(false);
|
||||
expect(g.errors).toEqual({"broken": true});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("dirty", () => {
|
||||
var c: Control;
|
||||
var a: ControlArray;
|
||||
|
||||
beforeEach(() => {
|
||||
c = new Control('value');
|
||||
a = new ControlArray([c]);
|
||||
});
|
||||
|
||||
it("should be false after creating a control", () => { expect(a.dirty).toEqual(false); });
|
||||
|
||||
it("should be false after changing the value of the control", () => {
|
||||
c.markAsDirty();
|
||||
|
||||
expect(a.dirty).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pending", () => {
|
||||
var c: Control;
|
||||
var a: ControlArray;
|
||||
|
||||
beforeEach(() => {
|
||||
c = new Control('value');
|
||||
a = new ControlArray([c]);
|
||||
});
|
||||
|
||||
it("should be false after creating a control", () => {
|
||||
expect(c.pending).toEqual(false);
|
||||
expect(a.pending).toEqual(false);
|
||||
});
|
||||
|
||||
it("should be true after changing the value of the control", () => {
|
||||
c.markAsPending();
|
||||
|
||||
expect(c.pending).toEqual(true);
|
||||
expect(a.pending).toEqual(true);
|
||||
});
|
||||
|
||||
it("should not update the parent when onlySelf = true", () => {
|
||||
c.markAsPending({onlySelf: true});
|
||||
|
||||
expect(c.pending).toEqual(true);
|
||||
expect(a.pending).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("valueChanges", () => {
|
||||
var a: ControlArray;
|
||||
var c1, c2;
|
||||
|
||||
beforeEach(() => {
|
||||
c1 = new Control("old1");
|
||||
c2 = new Control("old2");
|
||||
a = new ControlArray([c1, c2]);
|
||||
});
|
||||
|
||||
it("should fire an event after the value has been updated",
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
ObservableWrapper.subscribe(a.valueChanges, (value) => {
|
||||
expect(a.value).toEqual(['new1', 'old2']);
|
||||
expect(value).toEqual(['new1', 'old2']);
|
||||
async.done();
|
||||
});
|
||||
c1.updateValue("new1");
|
||||
}));
|
||||
|
||||
it("should fire an event after the control's observable fired an event",
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
var controlCallbackIsCalled = false;
|
||||
|
||||
ObservableWrapper.subscribe(c1.valueChanges,
|
||||
(value) => { controlCallbackIsCalled = true; });
|
||||
|
||||
ObservableWrapper.subscribe(a.valueChanges, (value) => {
|
||||
expect(controlCallbackIsCalled).toBe(true);
|
||||
async.done();
|
||||
});
|
||||
|
||||
c1.updateValue("new1");
|
||||
}));
|
||||
|
||||
it("should fire an event when a control is removed",
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
ObservableWrapper.subscribe(a.valueChanges, (value) => {
|
||||
expect(value).toEqual(['old1']);
|
||||
async.done();
|
||||
});
|
||||
|
||||
a.removeAt(1);
|
||||
}));
|
||||
|
||||
it("should fire an event when a control is added", inject([AsyncTestCompleter], (async) => {
|
||||
a.removeAt(1);
|
||||
|
||||
ObservableWrapper.subscribe(a.valueChanges, (value) => {
|
||||
expect(value).toEqual(['old1', 'old2']);
|
||||
async.done();
|
||||
});
|
||||
|
||||
a.push(c2);
|
||||
}));
|
||||
});
|
||||
|
||||
describe("find", () => {
|
||||
it("should return null when path is null", () => {
|
||||
var g = new ControlGroup({});
|
||||
expect(g.find(null)).toEqual(null);
|
||||
});
|
||||
|
||||
it("should return null when path is empty", () => {
|
||||
var g = new ControlGroup({});
|
||||
expect(g.find([])).toEqual(null);
|
||||
});
|
||||
|
||||
it("should return null when path is invalid", () => {
|
||||
var g = new ControlGroup({});
|
||||
expect(g.find(["one", "two"])).toEqual(null);
|
||||
});
|
||||
|
||||
it("should return a child of a control group", () => {
|
||||
var g = new ControlGroup(
|
||||
{"one": new Control("111"), "nested": new ControlGroup({"two": new Control("222")})});
|
||||
|
||||
expect(g.find(["nested", "two"]).value).toEqual("222");
|
||||
expect(g.find(["one"]).value).toEqual("111");
|
||||
expect(g.find("nested/two").value).toEqual("222");
|
||||
expect(g.find("one").value).toEqual("111");
|
||||
});
|
||||
|
||||
it("should return an element of an array", () => {
|
||||
var g = new ControlGroup({"array": new ControlArray([new Control("111")])});
|
||||
|
||||
expect(g.find(["array", 0]).value).toEqual("111");
|
||||
});
|
||||
});
|
||||
|
||||
describe("asyncValidator", () => {
|
||||
it("should run the async validator", fakeAsync(() => {
|
||||
var c = new Control("value");
|
||||
var g = new ControlArray([c], null, asyncValidator("expected"));
|
||||
|
||||
expect(g.pending).toEqual(true);
|
||||
|
||||
tick(1);
|
||||
|
||||
expect(g.errors).toEqual({"async": true});
|
||||
expect(g.pending).toEqual(false);
|
||||
}));
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
172
modules/@angular/common/test/forms/validators_spec.ts
Normal file
172
modules/@angular/common/test/forms/validators_spec.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import {
|
||||
ddescribe,
|
||||
describe,
|
||||
it,
|
||||
iit,
|
||||
xit,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
fakeAsync,
|
||||
tick,
|
||||
el
|
||||
} from 'angular2/testing_internal';
|
||||
import {ControlGroup, Control, Validators, AbstractControl, ControlArray} from 'angular2/common';
|
||||
import {PromiseWrapper} from 'angular2/src/facade/promise';
|
||||
import {EventEmitter, ObservableWrapper, TimerWrapper} from 'angular2/src/facade/async';
|
||||
|
||||
export function main() {
|
||||
function validator(key: string, error: any) {
|
||||
return function(c: AbstractControl) {
|
||||
var r = {};
|
||||
r[key] = error;
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
describe("Validators", () => {
|
||||
describe("required", () => {
|
||||
it("should error on an empty string",
|
||||
() => { expect(Validators.required(new Control(""))).toEqual({"required": true}); });
|
||||
|
||||
it("should error on null",
|
||||
() => { expect(Validators.required(new Control(null))).toEqual({"required": true}); });
|
||||
|
||||
it("should not error on a non-empty string",
|
||||
() => { expect(Validators.required(new Control("not empty"))).toEqual(null); });
|
||||
|
||||
it("should accept zero as valid",
|
||||
() => { expect(Validators.required(new Control(0))).toEqual(null); });
|
||||
});
|
||||
|
||||
describe("minLength", () => {
|
||||
it("should not error on an empty string",
|
||||
() => { expect(Validators.minLength(2)(new Control(""))).toEqual(null); });
|
||||
|
||||
it("should not error on null",
|
||||
() => { expect(Validators.minLength(2)(new Control(null))).toEqual(null); });
|
||||
|
||||
it("should not error on valid strings",
|
||||
() => { expect(Validators.minLength(2)(new Control("aa"))).toEqual(null); });
|
||||
|
||||
it("should error on short strings", () => {
|
||||
expect(Validators.minLength(2)(new Control("a")))
|
||||
.toEqual({"minlength": {"requiredLength": 2, "actualLength": 1}});
|
||||
});
|
||||
});
|
||||
|
||||
describe("maxLength", () => {
|
||||
it("should not error on an empty string",
|
||||
() => { expect(Validators.maxLength(2)(new Control(""))).toEqual(null); });
|
||||
|
||||
it("should not error on null",
|
||||
() => { expect(Validators.maxLength(2)(new Control(null))).toEqual(null); });
|
||||
|
||||
it("should not error on valid strings",
|
||||
() => { expect(Validators.maxLength(2)(new Control("aa"))).toEqual(null); });
|
||||
|
||||
it("should error on long strings", () => {
|
||||
expect(Validators.maxLength(2)(new Control("aaa")))
|
||||
.toEqual({"maxlength": {"requiredLength": 2, "actualLength": 3}});
|
||||
});
|
||||
});
|
||||
|
||||
describe("pattern", () => {
|
||||
it("should not error on an empty string",
|
||||
() => { expect(Validators.pattern("[a-zA-Z ]*")(new Control(""))).toEqual(null); });
|
||||
|
||||
it("should not error on null",
|
||||
() => { expect(Validators.pattern("[a-zA-Z ]*")(new Control(null))).toEqual(null); });
|
||||
|
||||
it("should not error on valid strings",
|
||||
() => { expect(Validators.pattern("[a-zA-Z ]*")(new Control("aaAA"))).toEqual(null); });
|
||||
|
||||
it("should error on failure to match string", () => {
|
||||
expect(Validators.pattern("[a-zA-Z ]*")(new Control("aaa0")))
|
||||
.toEqual({"pattern": {"requiredPattern": "^[a-zA-Z ]*$", "actualValue": "aaa0"}});
|
||||
});
|
||||
});
|
||||
|
||||
describe("compose", () => {
|
||||
it("should return null when given null",
|
||||
() => { expect(Validators.compose(null)).toBe(null); });
|
||||
|
||||
it("should collect errors from all the validators", () => {
|
||||
var c = Validators.compose([validator("a", true), validator("b", true)]);
|
||||
expect(c(new Control(""))).toEqual({"a": true, "b": true});
|
||||
});
|
||||
|
||||
it("should run validators left to right", () => {
|
||||
var c = Validators.compose([validator("a", 1), validator("a", 2)]);
|
||||
expect(c(new Control(""))).toEqual({"a": 2});
|
||||
});
|
||||
|
||||
it("should return null when no errors", () => {
|
||||
var c = Validators.compose([Validators.nullValidator, Validators.nullValidator]);
|
||||
expect(c(new Control(""))).toEqual(null);
|
||||
});
|
||||
|
||||
it("should ignore nulls", () => {
|
||||
var c = Validators.compose([null, Validators.required]);
|
||||
expect(c(new Control(""))).toEqual({"required": true});
|
||||
});
|
||||
});
|
||||
|
||||
describe("composeAsync", () => {
|
||||
function asyncValidator(expected, response) {
|
||||
return (c) => {
|
||||
var emitter = new EventEmitter();
|
||||
var res = c.value != expected ? response : null;
|
||||
|
||||
PromiseWrapper.scheduleMicrotask(() => {
|
||||
ObservableWrapper.callEmit(emitter, res);
|
||||
// this is required because of a bug in ObservableWrapper
|
||||
// where callComplete can fire before callEmit
|
||||
// remove this one the bug is fixed
|
||||
TimerWrapper.setTimeout(() => { ObservableWrapper.callComplete(emitter); }, 0);
|
||||
});
|
||||
return emitter;
|
||||
};
|
||||
}
|
||||
|
||||
it("should return null when given null",
|
||||
() => { expect(Validators.composeAsync(null)).toEqual(null); });
|
||||
|
||||
it("should collect errors from all the validators", fakeAsync(() => {
|
||||
var c = Validators.composeAsync([
|
||||
asyncValidator("expected", {"one": true}),
|
||||
asyncValidator("expected", {"two": true})
|
||||
]);
|
||||
|
||||
var value = null;
|
||||
(<Promise<any>>c(new Control("invalid"))).then(v => value = v);
|
||||
|
||||
tick(1);
|
||||
|
||||
expect(value).toEqual({"one": true, "two": true});
|
||||
}));
|
||||
|
||||
it("should return null when no errors", fakeAsync(() => {
|
||||
var c = Validators.composeAsync([asyncValidator("expected", {"one": true})]);
|
||||
|
||||
var value = null;
|
||||
(<Promise<any>>c(new Control("expected"))).then(v => value = v);
|
||||
|
||||
tick(1);
|
||||
|
||||
expect(value).toEqual(null);
|
||||
}));
|
||||
|
||||
it("should ignore nulls", fakeAsync(() => {
|
||||
var c = Validators.composeAsync([asyncValidator("expected", {"one": true}), null]);
|
||||
|
||||
var value = null;
|
||||
(<Promise<any>>c(new Control("invalid"))).then(v => value = v);
|
||||
|
||||
tick(1);
|
||||
|
||||
expect(value).toEqual({"one": true});
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
222
modules/@angular/common/test/pipes/async_pipe_spec.ts
Normal file
222
modules/@angular/common/test/pipes/async_pipe_spec.ts
Normal file
@ -0,0 +1,222 @@
|
||||
import {
|
||||
ddescribe,
|
||||
describe,
|
||||
it,
|
||||
iit,
|
||||
xit,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
AsyncTestCompleter,
|
||||
inject,
|
||||
browserDetection
|
||||
} from 'angular2/testing_internal';
|
||||
import {SpyChangeDetectorRef} from '../spies';
|
||||
|
||||
import {isBlank} from 'angular2/src/facade/lang';
|
||||
import {AsyncPipe} from 'angular2/common';
|
||||
import {WrappedValue} from 'angular2/core';
|
||||
import {
|
||||
EventEmitter,
|
||||
ObservableWrapper,
|
||||
PromiseWrapper,
|
||||
TimerWrapper
|
||||
} from 'angular2/src/facade/async';
|
||||
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
|
||||
import {PromiseCompleter} from 'angular2/src/facade/promise';
|
||||
|
||||
export function main() {
|
||||
describe("AsyncPipe", () => {
|
||||
|
||||
describe('Observable', () => {
|
||||
var emitter;
|
||||
var pipe;
|
||||
var ref;
|
||||
var message = new Object();
|
||||
|
||||
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) => {
|
||||
pipe.transform(emitter);
|
||||
|
||||
ObservableWrapper.callEmit(emitter, message);
|
||||
|
||||
TimerWrapper.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) => {
|
||||
pipe.transform(emitter);
|
||||
ObservableWrapper.callEmit(emitter, message);
|
||||
|
||||
TimerWrapper.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) => {
|
||||
pipe.transform(emitter);
|
||||
|
||||
var newEmitter = new EventEmitter();
|
||||
expect(pipe.transform(newEmitter)).toBe(null);
|
||||
|
||||
// this should not affect the pipe
|
||||
ObservableWrapper.callEmit(emitter, message);
|
||||
|
||||
TimerWrapper.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) => {
|
||||
pipe.transform(emitter);
|
||||
ObservableWrapper.callEmit(emitter, message);
|
||||
|
||||
TimerWrapper.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) => {
|
||||
pipe.transform(emitter);
|
||||
pipe.ngOnDestroy();
|
||||
|
||||
ObservableWrapper.callEmit(emitter, message);
|
||||
|
||||
TimerWrapper.setTimeout(() => {
|
||||
expect(pipe.transform(emitter)).toBe(null);
|
||||
async.done();
|
||||
}, 0)
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Promise", () => {
|
||||
var message = new Object();
|
||||
var pipe: AsyncPipe;
|
||||
var completer: PromiseCompleter<any>;
|
||||
var ref: SpyChangeDetectorRef;
|
||||
// adds longer timers for passing tests in IE
|
||||
var timer = (!isBlank(DOM) && browserDetection.isIE) ? 50 : 10;
|
||||
|
||||
beforeEach(() => {
|
||||
completer = PromiseWrapper.completer();
|
||||
ref = new SpyChangeDetectorRef();
|
||||
pipe = new AsyncPipe(<any>ref);
|
||||
});
|
||||
|
||||
describe("transform", () => {
|
||||
it("should return null when subscribing to a promise",
|
||||
() => { expect(pipe.transform(completer.promise)).toBe(null); });
|
||||
|
||||
it("should return the latest available value", inject([AsyncTestCompleter], (async) => {
|
||||
pipe.transform(completer.promise);
|
||||
|
||||
completer.resolve(message);
|
||||
|
||||
TimerWrapper.setTimeout(() => {
|
||||
expect(pipe.transform(completer.promise)).toEqual(new WrappedValue(message));
|
||||
async.done();
|
||||
}, timer)
|
||||
}));
|
||||
|
||||
it("should return unwrapped value when nothing has changed since the last call",
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
pipe.transform(completer.promise);
|
||||
completer.resolve(message);
|
||||
|
||||
TimerWrapper.setTimeout(() => {
|
||||
pipe.transform(completer.promise);
|
||||
expect(pipe.transform(completer.promise)).toBe(message);
|
||||
async.done();
|
||||
}, timer)
|
||||
}));
|
||||
|
||||
it("should dispose of the existing subscription when subscribing to a new promise",
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
pipe.transform(completer.promise);
|
||||
|
||||
var newCompleter = PromiseWrapper.completer();
|
||||
expect(pipe.transform(newCompleter.promise)).toBe(null);
|
||||
|
||||
// this should not affect the pipe, so it should return WrappedValue
|
||||
completer.resolve(message);
|
||||
|
||||
TimerWrapper.setTimeout(() => {
|
||||
expect(pipe.transform(newCompleter.promise)).toBe(null);
|
||||
async.done();
|
||||
}, timer)
|
||||
}));
|
||||
|
||||
it("should request a change detection check upon receiving a new value",
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
var markForCheck = ref.spy('markForCheck');
|
||||
pipe.transform(completer.promise);
|
||||
completer.resolve(message);
|
||||
|
||||
TimerWrapper.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) => {
|
||||
pipe.transform(completer.promise);
|
||||
expect(pipe.transform(completer.promise)).toBe(null);
|
||||
completer.resolve(message)
|
||||
|
||||
|
||||
TimerWrapper.setTimeout(() => {
|
||||
expect(pipe.transform(completer.promise)).toEqual(new WrappedValue(message));
|
||||
pipe.ngOnDestroy();
|
||||
expect(pipe.transform(completer.promise)).toBe(null);
|
||||
async.done();
|
||||
}, timer);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('null', () => {
|
||||
it('should return null when given null', () => {
|
||||
var pipe = new AsyncPipe(null);
|
||||
expect(pipe.transform(null)).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('other types', () => {
|
||||
it('should throw when given an invalid object', () => {
|
||||
var pipe = new AsyncPipe(null);
|
||||
expect(() => pipe.transform(<any>"some bogus object")).toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
82
modules/@angular/common/test/pipes/date_pipe_spec.ts
Normal file
82
modules/@angular/common/test/pipes/date_pipe_spec.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import {
|
||||
ddescribe,
|
||||
describe,
|
||||
it,
|
||||
iit,
|
||||
xit,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
browserDetection
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {DatePipe} from 'angular2/common';
|
||||
import {DateWrapper} from 'angular2/src/facade/lang';
|
||||
import {PipeResolver} from 'angular2/src/compiler/pipe_resolver';
|
||||
|
||||
export function main() {
|
||||
describe("DatePipe", () => {
|
||||
var date;
|
||||
var pipe;
|
||||
|
||||
beforeEach(() => {
|
||||
date = DateWrapper.create(2015, 6, 15, 21, 43, 11);
|
||||
pipe = new DatePipe();
|
||||
});
|
||||
|
||||
it('should be marked as pure',
|
||||
() => { expect(new PipeResolver().resolve(DatePipe).pure).toEqual(true); });
|
||||
|
||||
describe("supports", () => {
|
||||
it("should support date", () => { expect(pipe.supports(date)).toBe(true); });
|
||||
it("should support int", () => { expect(pipe.supports(123456789)).toBe(true); });
|
||||
|
||||
it("should not support other objects", () => {
|
||||
expect(pipe.supports(new Object())).toBe(false);
|
||||
expect(pipe.supports(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO(mlaval): enable tests when Intl API is no longer used, see
|
||||
// https://github.com/angular/angular/issues/3333
|
||||
if (browserDetection.supportsIntlApi) {
|
||||
describe("transform", () => {
|
||||
it('should format each component correctly', () => {
|
||||
expect(pipe.transform(date, 'y')).toEqual('2015');
|
||||
expect(pipe.transform(date, 'yy')).toEqual('15');
|
||||
expect(pipe.transform(date, 'M')).toEqual('6');
|
||||
expect(pipe.transform(date, 'MM')).toEqual('06');
|
||||
expect(pipe.transform(date, 'MMM')).toEqual('Jun');
|
||||
expect(pipe.transform(date, 'MMMM')).toEqual('June');
|
||||
expect(pipe.transform(date, 'd')).toEqual('15');
|
||||
expect(pipe.transform(date, 'E')).toEqual('Mon');
|
||||
expect(pipe.transform(date, 'EEEE')).toEqual('Monday');
|
||||
expect(pipe.transform(date, 'H')).toEqual('21');
|
||||
expect(pipe.transform(date, 'j')).toEqual('9 PM');
|
||||
expect(pipe.transform(date, 'm')).toEqual('43');
|
||||
expect(pipe.transform(date, 's')).toEqual('11');
|
||||
});
|
||||
|
||||
it('should format common multi component patterns', () => {
|
||||
expect(pipe.transform(date, 'yMEd')).toEqual('Mon, 6/15/2015');
|
||||
expect(pipe.transform(date, 'MEd')).toEqual('Mon, 6/15');
|
||||
expect(pipe.transform(date, 'MMMd')).toEqual('Jun 15');
|
||||
expect(pipe.transform(date, 'yMMMMEEEEd')).toEqual('Monday, June 15, 2015');
|
||||
expect(pipe.transform(date, 'jms')).toEqual('9:43:11 PM');
|
||||
expect(pipe.transform(date, 'ms')).toEqual('43:11');
|
||||
});
|
||||
|
||||
it('should format with pattern aliases', () => {
|
||||
expect(pipe.transform(date, 'medium')).toEqual('Jun 15, 2015, 9:43:11 PM');
|
||||
expect(pipe.transform(date, 'short')).toEqual('6/15/2015, 9:43 PM');
|
||||
expect(pipe.transform(date, 'fullDate')).toEqual('Monday, June 15, 2015');
|
||||
expect(pipe.transform(date, 'longDate')).toEqual('June 15, 2015');
|
||||
expect(pipe.transform(date, 'mediumDate')).toEqual('Jun 15, 2015');
|
||||
expect(pipe.transform(date, 'shortDate')).toEqual('6/15/2015');
|
||||
expect(pipe.transform(date, 'mediumTime')).toEqual('9:43:11 PM');
|
||||
expect(pipe.transform(date, 'shortTime')).toEqual('9:43 PM');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
59
modules/@angular/common/test/pipes/i18n_plural_pipe_spec.ts
Normal file
59
modules/@angular/common/test/pipes/i18n_plural_pipe_spec.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import {
|
||||
ddescribe,
|
||||
describe,
|
||||
it,
|
||||
iit,
|
||||
xit,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {I18nPluralPipe} from 'angular2/common';
|
||||
import {PipeResolver} from 'angular2/src/compiler/pipe_resolver';
|
||||
|
||||
export function main() {
|
||||
describe("I18nPluralPipe", () => {
|
||||
var pipe;
|
||||
var mapping = {'=0': 'No messages.', '=1': 'One message.', 'other': 'There are some messages.'};
|
||||
var interpolatedMapping =
|
||||
{'=0': 'No messages.', '=1': 'One message.', 'other': 'There are # messages, that is #.'};
|
||||
|
||||
beforeEach(() => { pipe = new I18nPluralPipe(); });
|
||||
|
||||
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", () => {
|
||||
var val = pipe.transform(0, mapping);
|
||||
expect(val).toEqual('No messages.');
|
||||
});
|
||||
|
||||
it("should return 1 text if value is 1", () => {
|
||||
var val = pipe.transform(1, mapping);
|
||||
expect(val).toEqual('One message.');
|
||||
});
|
||||
|
||||
it("should return other text if value is anything other than 0 or 1", () => {
|
||||
var val = pipe.transform(6, mapping);
|
||||
expect(val).toEqual('There are some messages.');
|
||||
});
|
||||
|
||||
it("should interpolate the value into the text where indicated", () => {
|
||||
var val = pipe.transform(6, interpolatedMapping);
|
||||
expect(val).toEqual('There are 6 messages, that is 6.');
|
||||
});
|
||||
|
||||
it("should use 'other' if value is undefined", () => {
|
||||
var messageLength;
|
||||
var val = pipe.transform(messageLength, interpolatedMapping);
|
||||
expect(val).toEqual('There are messages, that is .');
|
||||
});
|
||||
|
||||
it("should not support bad arguments",
|
||||
() => { expect(() => pipe.transform(0, 'hey')).toThrowError(); });
|
||||
});
|
||||
|
||||
});
|
||||
}
|
52
modules/@angular/common/test/pipes/i18n_select_pipe_spec.ts
Normal file
52
modules/@angular/common/test/pipes/i18n_select_pipe_spec.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import {
|
||||
ddescribe,
|
||||
describe,
|
||||
it,
|
||||
iit,
|
||||
xit,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {I18nSelectPipe} from 'angular2/common';
|
||||
import {PipeResolver} from 'angular2/src/compiler/pipe_resolver';
|
||||
|
||||
export function main() {
|
||||
describe("I18nSelectPipe", () => {
|
||||
var pipe;
|
||||
var mapping = {'male': 'Invite him.', 'female': 'Invite her.', 'other': 'Invite them.'};
|
||||
|
||||
beforeEach(() => { pipe = new I18nSelectPipe(); });
|
||||
|
||||
it('should be marked as pure',
|
||||
() => { expect(new PipeResolver().resolve(I18nSelectPipe).pure).toEqual(true); });
|
||||
|
||||
describe("transform", () => {
|
||||
it("should return male text if value is male", () => {
|
||||
var val = pipe.transform('male', mapping);
|
||||
expect(val).toEqual('Invite him.');
|
||||
});
|
||||
|
||||
it("should return female text if value is female", () => {
|
||||
var val = pipe.transform('female', mapping);
|
||||
expect(val).toEqual('Invite her.');
|
||||
});
|
||||
|
||||
it("should return other text if value is anything other than male or female", () => {
|
||||
var val = pipe.transform('Anything else', mapping);
|
||||
expect(val).toEqual('Invite them.');
|
||||
});
|
||||
|
||||
it("should use 'other' if value is undefined", () => {
|
||||
var gender;
|
||||
var val = pipe.transform(gender, mapping);
|
||||
expect(val).toEqual('Invite them.');
|
||||
});
|
||||
|
||||
it("should not support bad arguments",
|
||||
() => { expect(() => pipe.transform('male', 'hey')).toThrowError(); });
|
||||
});
|
||||
|
||||
});
|
||||
}
|
77
modules/@angular/common/test/pipes/json_pipe_spec.ts
Normal file
77
modules/@angular/common/test/pipes/json_pipe_spec.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import {
|
||||
ddescribe,
|
||||
describe,
|
||||
it,
|
||||
iit,
|
||||
xit,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
AsyncTestCompleter,
|
||||
inject,
|
||||
TestComponentBuilder
|
||||
} from 'angular2/testing_internal';
|
||||
import {Json, RegExp, NumberWrapper, StringWrapper} from 'angular2/src/facade/lang';
|
||||
|
||||
import {Component} from 'angular2/core';
|
||||
import {JsonPipe} from 'angular2/common';
|
||||
|
||||
export function main() {
|
||||
describe("JsonPipe", () => {
|
||||
var regNewLine = '\n';
|
||||
var inceptionObj;
|
||||
var inceptionObjString;
|
||||
var pipe;
|
||||
|
||||
function normalize(obj: string): string { return StringWrapper.replace(obj, 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", () => {
|
||||
var dream1 = normalize(pipe.transform(inceptionObj));
|
||||
var dream2 = normalize(inceptionObjString);
|
||||
expect(dream1).toEqual(dream2);
|
||||
});
|
||||
|
||||
it("should return JSON-formatted string similar to Json.stringify", () => {
|
||||
var dream1 = normalize(pipe.transform(inceptionObj));
|
||||
var dream2 = normalize(Json.stringify(inceptionObj));
|
||||
expect(dream1).toEqual(dream2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration', () => {
|
||||
it('should work with mutable objects',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
tcb.createAsync(TestComp).then((fixture) => {
|
||||
let mutable: number[] = [1];
|
||||
fixture.debugElement.componentInstance.data = mutable;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText("[\n 1\n]");
|
||||
|
||||
mutable.push(2);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText("[\n 1,\n 2\n]");
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Component({selector: 'test-comp', template: '{{data | json}}', pipes: [JsonPipe]})
|
||||
class TestComp {
|
||||
data: any;
|
||||
}
|
44
modules/@angular/common/test/pipes/lowercase_pipe_spec.ts
Normal file
44
modules/@angular/common/test/pipes/lowercase_pipe_spec.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import {
|
||||
ddescribe,
|
||||
describe,
|
||||
it,
|
||||
iit,
|
||||
xit,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {LowerCasePipe} from 'angular2/common';
|
||||
|
||||
export function main() {
|
||||
describe("LowerCasePipe", () => {
|
||||
var upper;
|
||||
var lower;
|
||||
var pipe;
|
||||
|
||||
beforeEach(() => {
|
||||
lower = 'something';
|
||||
upper = 'SOMETHING';
|
||||
pipe = new LowerCasePipe();
|
||||
});
|
||||
|
||||
describe("transform", () => {
|
||||
it("should return lowercase", () => {
|
||||
var val = pipe.transform(upper);
|
||||
expect(val).toEqual(lower);
|
||||
});
|
||||
|
||||
it("should lowercase when there is a new value", () => {
|
||||
var val = pipe.transform(upper);
|
||||
expect(val).toEqual(lower);
|
||||
var val2 = pipe.transform('WAT');
|
||||
expect(val2).toEqual('wat');
|
||||
});
|
||||
|
||||
it("should not support other objects",
|
||||
() => { expect(() => pipe.transform(new Object())).toThrowError(); });
|
||||
});
|
||||
|
||||
});
|
||||
}
|
74
modules/@angular/common/test/pipes/number_pipe_spec.ts
Normal file
74
modules/@angular/common/test/pipes/number_pipe_spec.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import {
|
||||
ddescribe,
|
||||
describe,
|
||||
it,
|
||||
iit,
|
||||
xit,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
browserDetection
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {DecimalPipe, PercentPipe, CurrencyPipe} from 'angular2/common';
|
||||
|
||||
export function main() {
|
||||
describe('Number pipes', () => {
|
||||
// TODO(mlaval): enable tests when Intl API is no longer used, see
|
||||
// https://github.com/angular/angular/issues/3333
|
||||
if (browserDetection.supportsIntlApi) {
|
||||
describe("DecimalPipe", () => {
|
||||
var pipe;
|
||||
|
||||
beforeEach(() => { pipe = new DecimalPipe(); });
|
||||
|
||||
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 not support other objects",
|
||||
() => { expect(() => pipe.transform(new Object())).toThrowError(); });
|
||||
});
|
||||
});
|
||||
|
||||
describe("PercentPipe", () => {
|
||||
var pipe;
|
||||
|
||||
beforeEach(() => { pipe = new PercentPipe(); });
|
||||
|
||||
describe("transform", () => {
|
||||
it('should return correct value for numbers', () => {
|
||||
expect(pipe.transform(1.23)).toEqual('123%');
|
||||
expect(pipe.transform(1.2, '.2')).toEqual('120.00%');
|
||||
});
|
||||
|
||||
it("should not support other objects",
|
||||
() => { expect(() => pipe.transform(new Object())).toThrowError(); });
|
||||
});
|
||||
});
|
||||
|
||||
describe("CurrencyPipe", () => {
|
||||
var pipe;
|
||||
|
||||
beforeEach(() => { pipe = new CurrencyPipe(); });
|
||||
|
||||
describe("transform", () => {
|
||||
it('should return correct value for numbers', () => {
|
||||
expect(pipe.transform(123)).toEqual('USD123');
|
||||
expect(pipe.transform(12, 'EUR', false, '.2')).toEqual('EUR12.00');
|
||||
});
|
||||
|
||||
it("should not support other objects",
|
||||
() => { expect(() => pipe.transform(new Object())).toThrowError(); });
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
73
modules/@angular/common/test/pipes/replace_pipe_spec.ts
Normal file
73
modules/@angular/common/test/pipes/replace_pipe_spec.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import {
|
||||
ddescribe,
|
||||
describe,
|
||||
it,
|
||||
iit,
|
||||
xit,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
browserDetection,
|
||||
inject,
|
||||
TestComponentBuilder,
|
||||
AsyncTestCompleter
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {ReplacePipe} from 'angular2/common';
|
||||
import {RegExpWrapper, StringJoiner} from 'angular2/src/facade/lang';
|
||||
|
||||
export function main() {
|
||||
describe("ReplacePipe", () => {
|
||||
var someNumber: number;
|
||||
var str;
|
||||
var pipe;
|
||||
|
||||
beforeEach(() => {
|
||||
someNumber = 42;
|
||||
str = 'Douglas Adams';
|
||||
pipe = new ReplacePipe();
|
||||
});
|
||||
|
||||
describe("transform", () => {
|
||||
|
||||
it("should not support input other than strings and numbers", () => {
|
||||
expect(() => pipe.transform({}, "Douglas", "Hugh")).toThrow();
|
||||
expect(() => pipe.transform([1, 2, 3], "Douglas", "Hugh")).toThrow();
|
||||
});
|
||||
|
||||
it("should not support patterns other than strings and regular expressions", () => {
|
||||
expect(() => pipe.transform(str, {}, "Hugh")).toThrow();
|
||||
expect(() => pipe.transform(str, null, "Hugh")).toThrow();
|
||||
expect(() => pipe.transform(str, 123, "Hugh")).toThrow();
|
||||
});
|
||||
|
||||
it("should not support replacements other than strings and functions", () => {
|
||||
expect(() => pipe.transform(str, "Douglas", {})).toThrow();
|
||||
expect(() => pipe.transform(str, "Douglas", null)).toThrow();
|
||||
expect(() => pipe.transform(str, "Douglas", 123)).toThrow();
|
||||
});
|
||||
|
||||
it("should return a new string with the pattern replaced", () => {
|
||||
var result1 = pipe.transform(str, "Douglas", "Hugh");
|
||||
|
||||
var result2 = pipe.transform(str, RegExpWrapper.create("a"), "_");
|
||||
|
||||
var result3 = pipe.transform(str, RegExpWrapper.create("a", "i"), "_");
|
||||
|
||||
var f = (x => { return "Adams!"; });
|
||||
|
||||
var result4 = pipe.transform(str, "Adams", f);
|
||||
|
||||
var result5 = pipe.transform(someNumber, "2", "4");
|
||||
|
||||
expect(result1).toEqual("Hugh Adams");
|
||||
expect(result2).toEqual("Dougl_s Ad_ms");
|
||||
expect(result3).toEqual("Dougl_s _d_ms");
|
||||
expect(result4).toEqual("Douglas Adams!");
|
||||
expect(result5).toEqual("44");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
}
|
115
modules/@angular/common/test/pipes/slice_pipe_spec.ts
Normal file
115
modules/@angular/common/test/pipes/slice_pipe_spec.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import {
|
||||
ddescribe,
|
||||
describe,
|
||||
it,
|
||||
iit,
|
||||
xit,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
browserDetection,
|
||||
inject,
|
||||
TestComponentBuilder,
|
||||
AsyncTestCompleter
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {Component} from 'angular2/core';
|
||||
import {SlicePipe} from 'angular2/common';
|
||||
|
||||
export function main() {
|
||||
describe("SlicePipe", () => {
|
||||
var list: number[];
|
||||
var str;
|
||||
var pipe;
|
||||
|
||||
beforeEach(() => {
|
||||
list = [1, 2, 3, 4, 5];
|
||||
str = 'tuvwxyz';
|
||||
pipe = new SlicePipe();
|
||||
});
|
||||
|
||||
describe("supports", () => {
|
||||
it("should support strings", () => { expect(pipe.supports(str)).toBe(true); });
|
||||
it("should support lists", () => { expect(pipe.supports(list)).toBe(true); });
|
||||
|
||||
it("should not support other objects", () => {
|
||||
expect(pipe.supports(new Object())).toBe(false);
|
||||
expect(pipe.supports(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("transform", () => {
|
||||
|
||||
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('');
|
||||
});
|
||||
|
||||
// Makes Edge to disconnect when running the full unit test campaign
|
||||
// TODO: remove when issue is solved: https://github.com/angular/angular/issues/4756
|
||||
if (!browserDetection.isEdge) {
|
||||
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', () => {
|
||||
it('should work with mutable arrays',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
tcb.createAsync(TestComp).then((fixture) => {
|
||||
let mutable: number[] = [1, 2];
|
||||
fixture.debugElement.componentInstance.data = mutable;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('2');
|
||||
|
||||
mutable.push(3);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('2,3');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Component({selector: 'test-comp', template: '{{(data | slice:1).join(",") }}', pipes: [SlicePipe]})
|
||||
class TestComp {
|
||||
data: any;
|
||||
}
|
45
modules/@angular/common/test/pipes/uppercase_pipe_spec.ts
Normal file
45
modules/@angular/common/test/pipes/uppercase_pipe_spec.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import {
|
||||
ddescribe,
|
||||
describe,
|
||||
it,
|
||||
iit,
|
||||
xit,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {UpperCasePipe} from 'angular2/common';
|
||||
|
||||
export function main() {
|
||||
describe("UpperCasePipe", () => {
|
||||
var upper;
|
||||
var lower;
|
||||
var pipe;
|
||||
|
||||
beforeEach(() => {
|
||||
lower = 'something';
|
||||
upper = 'SOMETHING';
|
||||
pipe = new UpperCasePipe();
|
||||
});
|
||||
|
||||
describe("transform", () => {
|
||||
|
||||
it("should return uppercase", () => {
|
||||
var val = pipe.transform(lower);
|
||||
expect(val).toEqual(upper);
|
||||
});
|
||||
|
||||
it("should uppercase when there is a new value", () => {
|
||||
var val = pipe.transform(lower);
|
||||
expect(val).toEqual(upper);
|
||||
var val2 = pipe.transform('wat');
|
||||
expect(val2).toEqual('WAT');
|
||||
});
|
||||
|
||||
it("should not support other objects",
|
||||
() => { expect(() => pipe.transform(new Object())).toThrowError(); });
|
||||
});
|
||||
|
||||
});
|
||||
}
|
14
modules/@angular/common/test/spies.dart
Normal file
14
modules/@angular/common/test/spies.dart
Normal file
@ -0,0 +1,14 @@
|
||||
library core.spies;
|
||||
|
||||
import 'package:angular2/common.dart';
|
||||
import 'package:angular2/src/core/change_detection/change_detection.dart';
|
||||
import 'package:angular2/testing_internal.dart';
|
||||
|
||||
@proxy
|
||||
class SpyNgControl extends SpyObject implements NgControl {}
|
||||
|
||||
@proxy
|
||||
class SpyValueAccessor extends SpyObject implements ControlValueAccessor {}
|
||||
|
||||
@proxy
|
||||
class SpyChangeDetectorRef extends SpyObject implements ChangeDetectorRef {}
|
13
modules/@angular/common/test/spies.ts
Normal file
13
modules/@angular/common/test/spies.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {ChangeDetectorRef} from 'angular2/src/core/change_detection/change_detector_ref';
|
||||
import {SpyObject, proxy} from 'angular2/testing_internal';
|
||||
|
||||
export class SpyChangeDetectorRef extends SpyObject {
|
||||
constructor() {
|
||||
super(ChangeDetectorRef);
|
||||
this.spy('markForCheck');
|
||||
}
|
||||
}
|
||||
|
||||
export class SpyNgControl extends SpyObject {}
|
||||
|
||||
export class SpyValueAccessor extends SpyObject {}
|
82
modules/@angular/common/testing/location_mock.ts
Normal file
82
modules/@angular/common/testing/location_mock.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import {Injectable} from 'angular2/src/core/di';
|
||||
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
|
||||
import {ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {Location} from 'angular2/platform/common';
|
||||
|
||||
/**
|
||||
* A spy for {@link Location} that allows tests to fire simulated location events.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SpyLocation implements Location {
|
||||
urlChanges: string[] = [];
|
||||
/** @internal */
|
||||
_path: string = '';
|
||||
/** @internal */
|
||||
_query: string = '';
|
||||
/** @internal */
|
||||
_subject: EventEmitter<any> = new EventEmitter();
|
||||
/** @internal */
|
||||
_baseHref: string = '';
|
||||
|
||||
setInitialPath(url: string) { this._path = url; }
|
||||
|
||||
setBaseHref(url: string) { this._baseHref = url; }
|
||||
|
||||
path(): string { return this._path; }
|
||||
|
||||
simulateUrlPop(pathname: string) {
|
||||
ObservableWrapper.callEmit(this._subject, {'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);
|
||||
ObservableWrapper.callEmit(this._subject, {'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._path == path && this._query == query) {
|
||||
return;
|
||||
}
|
||||
this._path = path;
|
||||
this._query = query;
|
||||
|
||||
var url = path + (query.length > 0 ? ('?' + query) : '');
|
||||
this.urlChanges.push(url);
|
||||
}
|
||||
|
||||
replaceState(path: string, query: string = '') {
|
||||
path = this.prepareExternalUrl(path);
|
||||
this._path = path;
|
||||
this._query = query;
|
||||
|
||||
var url = path + (query.length > 0 ? ('?' + query) : '');
|
||||
this.urlChanges.push('replace: ' + url);
|
||||
}
|
||||
|
||||
forward() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
back() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
subscribe(onNext: (value: any) => void, onThrow: (error: any) => void = null,
|
||||
onReturn: () => void = null): Object {
|
||||
return ObservableWrapper.subscribe(this._subject, onNext, onThrow, onReturn);
|
||||
}
|
||||
|
||||
// TODO: remove these once Location is an interface, and can be implemented cleanly
|
||||
platformStrategy: any = null;
|
||||
normalize(url: string): string { return null; }
|
||||
}
|
73
modules/@angular/common/testing/mock_location_strategy.ts
Normal file
73
modules/@angular/common/testing/mock_location_strategy.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import {Injectable} from 'angular2/src/core/di';
|
||||
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
|
||||
import {LocationStrategy} from 'angular2/platform/common';
|
||||
|
||||
|
||||
/**
|
||||
* A mock implementation of {@link LocationStrategy} that allows tests to fire simulated
|
||||
* location events.
|
||||
*/
|
||||
@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;
|
||||
ObservableWrapper.callEmit(this._subject, new _MockPopStateEvent(this.path()));
|
||||
}
|
||||
|
||||
path(): 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;
|
||||
|
||||
var url = path + (query.length > 0 ? ('?' + query) : '');
|
||||
this.internalPath = url;
|
||||
|
||||
var externalUrl = this.prepareExternalUrl(url);
|
||||
this.urlChanges.push(externalUrl);
|
||||
}
|
||||
|
||||
replaceState(ctx: any, title: string, path: string, query: string): void {
|
||||
this.internalTitle = title;
|
||||
|
||||
var url = path + (query.length > 0 ? ('?' + query) : '');
|
||||
this.internalPath = url;
|
||||
|
||||
var externalUrl = this.prepareExternalUrl(url);
|
||||
this.urlChanges.push('replace: ' + externalUrl);
|
||||
}
|
||||
|
||||
onPopState(fn: (value: any) => void): void { ObservableWrapper.subscribe(this._subject, fn); }
|
||||
|
||||
getBaseHref(): string { return this.internalBaseHref; }
|
||||
|
||||
back(): void {
|
||||
if (this.urlChanges.length > 0) {
|
||||
this.urlChanges.pop();
|
||||
var 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) {}
|
||||
}
|
37
modules/@angular/compiler/compiler.ts
Normal file
37
modules/@angular/compiler/compiler.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @module
|
||||
* @description
|
||||
* Starting point to import all compiler APIs.
|
||||
*/
|
||||
export {
|
||||
PLATFORM_DIRECTIVES,
|
||||
PLATFORM_PIPES,
|
||||
COMPILER_PROVIDERS,
|
||||
TEMPLATE_TRANSFORMS,
|
||||
CompilerConfig,
|
||||
RenderTypes,
|
||||
UrlResolver,
|
||||
DEFAULT_PACKAGE_URL_PROVIDER,
|
||||
createOfflineCompileUrlResolver,
|
||||
XHR,
|
||||
ViewResolver,
|
||||
DirectiveResolver,
|
||||
PipeResolver,
|
||||
SourceModule,
|
||||
NormalizedComponentWithViewDirectives,
|
||||
OfflineCompiler,
|
||||
CompileMetadataWithIdentifier,
|
||||
CompileMetadataWithType,
|
||||
CompileIdentifierMetadata,
|
||||
CompileDiDependencyMetadata,
|
||||
CompileProviderMetadata,
|
||||
CompileFactoryMetadata,
|
||||
CompileTokenMetadata,
|
||||
CompileTypeMetadata,
|
||||
CompileQueryMetadata,
|
||||
CompileTemplateMetadata,
|
||||
CompileDirectiveMetadata,
|
||||
CompilePipeMetadata
|
||||
} from 'angular2/src/compiler/compiler';
|
||||
|
||||
export * from 'angular2/src/compiler/template_ast';
|
3
modules/@angular/compiler/src/assertions.dart
Normal file
3
modules/@angular/compiler/src/assertions.dart
Normal file
@ -0,0 +1,3 @@
|
||||
library angular2.core.util.asserions;
|
||||
|
||||
void assertArrayOfStrings(String identifier, Object value) {}
|
16
modules/@angular/compiler/src/assertions.ts
Normal file
16
modules/@angular/compiler/src/assertions.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import {isArray, isString, isBlank, assertionsEnabled} from '../facade/lang';
|
||||
import {BaseException} from '../facade/exceptions';
|
||||
|
||||
export function assertArrayOfStrings(identifier: string, value: any) {
|
||||
if (!assertionsEnabled() || isBlank(value)) {
|
||||
return;
|
||||
}
|
||||
if (!isArray(value)) {
|
||||
throw new BaseException(`Expected '${identifier}' to be an array of strings.`);
|
||||
}
|
||||
for (var i = 0; i < value.length; i += 1) {
|
||||
if (!isString(value[i])) {
|
||||
throw new BaseException(`Expected '${identifier}' to be an array of strings.`);
|
||||
}
|
||||
}
|
||||
}
|
64
modules/@angular/compiler/src/chars.ts
Normal file
64
modules/@angular/compiler/src/chars.ts
Normal file
@ -0,0 +1,64 @@
|
||||
export const $EOF = /*@ts2dart_const*/ 0;
|
||||
export const $TAB = /*@ts2dart_const*/ 9;
|
||||
export const $LF = /*@ts2dart_const*/ 10;
|
||||
export const $VTAB = /*@ts2dart_const*/ 11;
|
||||
export const $FF = /*@ts2dart_const*/ 12;
|
||||
export const $CR = /*@ts2dart_const*/ 13;
|
||||
export const $SPACE = /*@ts2dart_const*/ 32;
|
||||
export const $BANG = /*@ts2dart_const*/ 33;
|
||||
export const $DQ = /*@ts2dart_const*/ 34;
|
||||
export const $HASH = /*@ts2dart_const*/ 35;
|
||||
export const $$ = /*@ts2dart_const*/ 36;
|
||||
export const $PERCENT = /*@ts2dart_const*/ 37;
|
||||
export const $AMPERSAND = /*@ts2dart_const*/ 38;
|
||||
export const $SQ = /*@ts2dart_const*/ 39;
|
||||
export const $LPAREN = /*@ts2dart_const*/ 40;
|
||||
export const $RPAREN = /*@ts2dart_const*/ 41;
|
||||
export const $STAR = /*@ts2dart_const*/ 42;
|
||||
export const $PLUS = /*@ts2dart_const*/ 43;
|
||||
export const $COMMA = /*@ts2dart_const*/ 44;
|
||||
export const $MINUS = /*@ts2dart_const*/ 45;
|
||||
export const $PERIOD = /*@ts2dart_const*/ 46;
|
||||
export const $SLASH = /*@ts2dart_const*/ 47;
|
||||
export const $COLON = /*@ts2dart_const*/ 58;
|
||||
export const $SEMICOLON = /*@ts2dart_const*/ 59;
|
||||
export const $LT = /*@ts2dart_const*/ 60;
|
||||
export const $EQ = /*@ts2dart_const*/ 61;
|
||||
export const $GT = /*@ts2dart_const*/ 62;
|
||||
export const $QUESTION = /*@ts2dart_const*/ 63;
|
||||
|
||||
export const $0 = /*@ts2dart_const*/ 48;
|
||||
export const $9 = /*@ts2dart_const*/ 57;
|
||||
|
||||
export const $A = /*@ts2dart_const*/ 65;
|
||||
export const $E = /*@ts2dart_const*/ 69;
|
||||
export const $Z = /*@ts2dart_const*/ 90;
|
||||
|
||||
export const $LBRACKET = /*@ts2dart_const*/ 91;
|
||||
export const $BACKSLASH = /*@ts2dart_const*/ 92;
|
||||
export const $RBRACKET = /*@ts2dart_const*/ 93;
|
||||
export const $CARET = /*@ts2dart_const*/ 94;
|
||||
export const $_ = /*@ts2dart_const*/ 95;
|
||||
|
||||
export const $a = /*@ts2dart_const*/ 97;
|
||||
export const $e = /*@ts2dart_const*/ 101;
|
||||
export const $f = /*@ts2dart_const*/ 102;
|
||||
export const $n = /*@ts2dart_const*/ 110;
|
||||
export const $r = /*@ts2dart_const*/ 114;
|
||||
export const $t = /*@ts2dart_const*/ 116;
|
||||
export const $u = /*@ts2dart_const*/ 117;
|
||||
export const $v = /*@ts2dart_const*/ 118;
|
||||
export const $z = /*@ts2dart_const*/ 122;
|
||||
|
||||
export const $LBRACE = /*@ts2dart_const*/ 123;
|
||||
export const $BAR = /*@ts2dart_const*/ 124;
|
||||
export const $RBRACE = /*@ts2dart_const*/ 125;
|
||||
export const $NBSP = /*@ts2dart_const*/ 160;
|
||||
|
||||
export const $PIPE = /*@ts2dart_const*/ 124;
|
||||
export const $TILDA = /*@ts2dart_const*/ 126;
|
||||
export const $AT = /*@ts2dart_const*/ 64;
|
||||
|
||||
export function isWhitespace(code: number): boolean {
|
||||
return (code >= $TAB && code <= $SPACE) || (code == $NBSP);
|
||||
}
|
801
modules/@angular/compiler/src/compile_metadata.ts
Normal file
801
modules/@angular/compiler/src/compile_metadata.ts
Normal file
@ -0,0 +1,801 @@
|
||||
import {
|
||||
isPresent,
|
||||
isBlank,
|
||||
isNumber,
|
||||
isBoolean,
|
||||
normalizeBool,
|
||||
normalizeBlank,
|
||||
serializeEnum,
|
||||
Type,
|
||||
isString,
|
||||
RegExpWrapper,
|
||||
StringWrapper,
|
||||
isArray
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {unimplemented, BaseException} from 'angular2/src/facade/exceptions';
|
||||
import {
|
||||
StringMapWrapper,
|
||||
MapWrapper,
|
||||
SetWrapper,
|
||||
ListWrapper
|
||||
} from 'angular2/src/facade/collection';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
CHANGE_DETECTION_STRATEGY_VALUES
|
||||
} from 'angular2/src/core/change_detection/change_detection';
|
||||
import {ViewEncapsulation, VIEW_ENCAPSULATION_VALUES} from 'angular2/src/core/metadata/view';
|
||||
import {CssSelector} from 'angular2/src/compiler/selector';
|
||||
import {splitAtColon, sanitizeIdentifier} from './util';
|
||||
import {LifecycleHooks, LIFECYCLE_HOOKS_VALUES} from 'angular2/src/core/metadata/lifecycle_hooks';
|
||||
import {getUrlScheme} from './url_resolver';
|
||||
|
||||
// group 1: "property" from "[property]"
|
||||
// group 2: "event" from "(event)"
|
||||
var HOST_REG_EXP = /^(?:(?:\[([^\]]+)\])|(?:\(([^\)]+)\)))$/g;
|
||||
|
||||
export abstract class CompileMetadataWithIdentifier {
|
||||
abstract toJson(): {[key: string]: any};
|
||||
|
||||
get identifier(): CompileIdentifierMetadata { return <CompileIdentifierMetadata>unimplemented(); }
|
||||
}
|
||||
|
||||
export abstract class CompileMetadataWithType extends CompileMetadataWithIdentifier {
|
||||
abstract toJson(): {[key: string]: any};
|
||||
|
||||
get type(): CompileTypeMetadata { return <CompileTypeMetadata>unimplemented(); }
|
||||
|
||||
get identifier(): CompileIdentifierMetadata { return <CompileIdentifierMetadata>unimplemented(); }
|
||||
}
|
||||
|
||||
export function metadataFromJson(data: {[key: string]: any}): any {
|
||||
return _COMPILE_METADATA_FROM_JSON[data['class']](data);
|
||||
}
|
||||
|
||||
export class CompileIdentifierMetadata implements CompileMetadataWithIdentifier {
|
||||
runtime: any;
|
||||
name: string;
|
||||
prefix: string;
|
||||
moduleUrl: string;
|
||||
value: any;
|
||||
|
||||
constructor(
|
||||
{runtime, name, moduleUrl, prefix, value}:
|
||||
{runtime?: any, name?: string, moduleUrl?: string, prefix?: string, value?: any} = {}) {
|
||||
this.runtime = runtime;
|
||||
this.name = name;
|
||||
this.prefix = prefix;
|
||||
this.moduleUrl = moduleUrl;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
static fromJson(data: {[key: string]: any}): CompileIdentifierMetadata {
|
||||
let value = isArray(data['value']) ? _arrayFromJson(data['value'], metadataFromJson) :
|
||||
_objFromJson(data['value'], metadataFromJson);
|
||||
return new CompileIdentifierMetadata(
|
||||
{name: data['name'], prefix: data['prefix'], moduleUrl: data['moduleUrl'], value: value});
|
||||
}
|
||||
|
||||
toJson(): {[key: string]: any} {
|
||||
let value = isArray(this.value) ? _arrayToJson(this.value) : _objToJson(this.value);
|
||||
return {
|
||||
// Note: Runtime type can't be serialized...
|
||||
'class': 'Identifier',
|
||||
'name': this.name,
|
||||
'moduleUrl': this.moduleUrl,
|
||||
'prefix': this.prefix,
|
||||
'value': value
|
||||
};
|
||||
}
|
||||
|
||||
get identifier(): CompileIdentifierMetadata { return this; }
|
||||
}
|
||||
|
||||
export class CompileDiDependencyMetadata {
|
||||
isAttribute: boolean;
|
||||
isSelf: boolean;
|
||||
isHost: boolean;
|
||||
isSkipSelf: boolean;
|
||||
isOptional: boolean;
|
||||
isValue: boolean;
|
||||
query: CompileQueryMetadata;
|
||||
viewQuery: CompileQueryMetadata;
|
||||
token: CompileTokenMetadata;
|
||||
value: any;
|
||||
|
||||
constructor({isAttribute, isSelf, isHost, isSkipSelf, isOptional, isValue, query, viewQuery,
|
||||
token, value}: {
|
||||
isAttribute?: boolean,
|
||||
isSelf?: boolean,
|
||||
isHost?: boolean,
|
||||
isSkipSelf?: boolean,
|
||||
isOptional?: boolean,
|
||||
isValue?: boolean,
|
||||
query?: CompileQueryMetadata,
|
||||
viewQuery?: CompileQueryMetadata,
|
||||
token?: CompileTokenMetadata,
|
||||
value?: any
|
||||
} = {}) {
|
||||
this.isAttribute = normalizeBool(isAttribute);
|
||||
this.isSelf = normalizeBool(isSelf);
|
||||
this.isHost = normalizeBool(isHost);
|
||||
this.isSkipSelf = normalizeBool(isSkipSelf);
|
||||
this.isOptional = normalizeBool(isOptional);
|
||||
this.isValue = normalizeBool(isValue);
|
||||
this.query = query;
|
||||
this.viewQuery = viewQuery;
|
||||
this.token = token;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
static fromJson(data: {[key: string]: any}): CompileDiDependencyMetadata {
|
||||
return new CompileDiDependencyMetadata({
|
||||
token: _objFromJson(data['token'], CompileTokenMetadata.fromJson),
|
||||
query: _objFromJson(data['query'], CompileQueryMetadata.fromJson),
|
||||
viewQuery: _objFromJson(data['viewQuery'], CompileQueryMetadata.fromJson),
|
||||
value: data['value'],
|
||||
isAttribute: data['isAttribute'],
|
||||
isSelf: data['isSelf'],
|
||||
isHost: data['isHost'],
|
||||
isSkipSelf: data['isSkipSelf'],
|
||||
isOptional: data['isOptional'],
|
||||
isValue: data['isValue']
|
||||
});
|
||||
}
|
||||
|
||||
toJson(): {[key: string]: any} {
|
||||
return {
|
||||
'token': _objToJson(this.token),
|
||||
'query': _objToJson(this.query),
|
||||
'viewQuery': _objToJson(this.viewQuery),
|
||||
'value': this.value,
|
||||
'isAttribute': this.isAttribute,
|
||||
'isSelf': this.isSelf,
|
||||
'isHost': this.isHost,
|
||||
'isSkipSelf': this.isSkipSelf,
|
||||
'isOptional': this.isOptional,
|
||||
'isValue': this.isValue
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class CompileProviderMetadata {
|
||||
token: CompileTokenMetadata;
|
||||
useClass: CompileTypeMetadata;
|
||||
useValue: any;
|
||||
useExisting: CompileTokenMetadata;
|
||||
useFactory: CompileFactoryMetadata;
|
||||
deps: CompileDiDependencyMetadata[];
|
||||
multi: boolean;
|
||||
|
||||
constructor({token, useClass, useValue, useExisting, useFactory, deps, multi}: {
|
||||
token?: CompileTokenMetadata,
|
||||
useClass?: CompileTypeMetadata,
|
||||
useValue?: any,
|
||||
useExisting?: CompileTokenMetadata,
|
||||
useFactory?: CompileFactoryMetadata,
|
||||
deps?: CompileDiDependencyMetadata[],
|
||||
multi?: boolean
|
||||
}) {
|
||||
this.token = token;
|
||||
this.useClass = useClass;
|
||||
this.useValue = useValue;
|
||||
this.useExisting = useExisting;
|
||||
this.useFactory = useFactory;
|
||||
this.deps = normalizeBlank(deps);
|
||||
this.multi = normalizeBool(multi);
|
||||
}
|
||||
|
||||
static fromJson(data: {[key: string]: any}): CompileProviderMetadata {
|
||||
return new CompileProviderMetadata({
|
||||
token: _objFromJson(data['token'], CompileTokenMetadata.fromJson),
|
||||
useClass: _objFromJson(data['useClass'], CompileTypeMetadata.fromJson),
|
||||
useExisting: _objFromJson(data['useExisting'], CompileTokenMetadata.fromJson),
|
||||
useValue: _objFromJson(data['useValue'], CompileIdentifierMetadata.fromJson),
|
||||
useFactory: _objFromJson(data['useFactory'], CompileFactoryMetadata.fromJson),
|
||||
multi: data['multi'],
|
||||
deps: _arrayFromJson(data['deps'], CompileDiDependencyMetadata.fromJson)
|
||||
});
|
||||
}
|
||||
|
||||
toJson(): {[key: string]: any} {
|
||||
return {
|
||||
// Note: Runtime type can't be serialized...
|
||||
'class': 'Provider',
|
||||
'token': _objToJson(this.token),
|
||||
'useClass': _objToJson(this.useClass),
|
||||
'useExisting': _objToJson(this.useExisting),
|
||||
'useValue': _objToJson(this.useValue),
|
||||
'useFactory': _objToJson(this.useFactory),
|
||||
'multi': this.multi,
|
||||
'deps': _arrayToJson(this.deps)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class CompileFactoryMetadata implements CompileIdentifierMetadata,
|
||||
CompileMetadataWithIdentifier {
|
||||
runtime: Function;
|
||||
name: string;
|
||||
prefix: string;
|
||||
moduleUrl: string;
|
||||
value: any;
|
||||
diDeps: CompileDiDependencyMetadata[];
|
||||
|
||||
constructor({runtime, name, moduleUrl, prefix, diDeps, value}: {
|
||||
runtime?: Function,
|
||||
name?: string,
|
||||
prefix?: string,
|
||||
moduleUrl?: string,
|
||||
value?: boolean,
|
||||
diDeps?: CompileDiDependencyMetadata[]
|
||||
}) {
|
||||
this.runtime = runtime;
|
||||
this.name = name;
|
||||
this.prefix = prefix;
|
||||
this.moduleUrl = moduleUrl;
|
||||
this.diDeps = _normalizeArray(diDeps);
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
get identifier(): CompileIdentifierMetadata { return this; }
|
||||
|
||||
static fromJson(data: {[key: string]: any}): CompileFactoryMetadata {
|
||||
return new CompileFactoryMetadata({
|
||||
name: data['name'],
|
||||
prefix: data['prefix'],
|
||||
moduleUrl: data['moduleUrl'],
|
||||
value: data['value'],
|
||||
diDeps: _arrayFromJson(data['diDeps'], CompileDiDependencyMetadata.fromJson)
|
||||
});
|
||||
}
|
||||
|
||||
toJson(): {[key: string]: any} {
|
||||
return {
|
||||
'class': 'Factory',
|
||||
'name': this.name,
|
||||
'prefix': this.prefix,
|
||||
'moduleUrl': this.moduleUrl,
|
||||
'value': this.value,
|
||||
'diDeps': _arrayToJson(this.diDeps)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class CompileTokenMetadata implements CompileMetadataWithIdentifier {
|
||||
value: any;
|
||||
identifier: CompileIdentifierMetadata;
|
||||
identifierIsInstance: boolean;
|
||||
|
||||
constructor({value, identifier, identifierIsInstance}: {
|
||||
value?: any,
|
||||
identifier?: CompileIdentifierMetadata,
|
||||
identifierIsInstance?: boolean
|
||||
}) {
|
||||
this.value = value;
|
||||
this.identifier = identifier;
|
||||
this.identifierIsInstance = normalizeBool(identifierIsInstance);
|
||||
}
|
||||
|
||||
static fromJson(data: {[key: string]: any}): CompileTokenMetadata {
|
||||
return new CompileTokenMetadata({
|
||||
value: data['value'],
|
||||
identifier: _objFromJson(data['identifier'], CompileIdentifierMetadata.fromJson),
|
||||
identifierIsInstance: data['identifierIsInstance']
|
||||
});
|
||||
}
|
||||
|
||||
toJson(): {[key: string]: any} {
|
||||
return {
|
||||
'value': this.value,
|
||||
'identifier': _objToJson(this.identifier),
|
||||
'identifierIsInstance': this.identifierIsInstance
|
||||
};
|
||||
}
|
||||
|
||||
get runtimeCacheKey(): any {
|
||||
if (isPresent(this.identifier)) {
|
||||
return this.identifier.runtime;
|
||||
} else {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
get assetCacheKey(): any {
|
||||
if (isPresent(this.identifier)) {
|
||||
return isPresent(this.identifier.moduleUrl) &&
|
||||
isPresent(getUrlScheme(this.identifier.moduleUrl)) ?
|
||||
`${this.identifier.name}|${this.identifier.moduleUrl}|${this.identifierIsInstance}` :
|
||||
null;
|
||||
} else {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
equalsTo(token2: CompileTokenMetadata): boolean {
|
||||
var rk = this.runtimeCacheKey;
|
||||
var ak = this.assetCacheKey;
|
||||
return (isPresent(rk) && rk == token2.runtimeCacheKey) ||
|
||||
(isPresent(ak) && ak == token2.assetCacheKey);
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return isPresent(this.value) ? sanitizeIdentifier(this.value) : this.identifier.name;
|
||||
}
|
||||
}
|
||||
|
||||
export class CompileTokenMap<VALUE> {
|
||||
private _valueMap = new Map<any, VALUE>();
|
||||
private _values: VALUE[] = [];
|
||||
|
||||
add(token: CompileTokenMetadata, value: VALUE) {
|
||||
var existing = this.get(token);
|
||||
if (isPresent(existing)) {
|
||||
throw new BaseException(`Can only add to a TokenMap! Token: ${token.name}`);
|
||||
}
|
||||
this._values.push(value);
|
||||
var rk = token.runtimeCacheKey;
|
||||
if (isPresent(rk)) {
|
||||
this._valueMap.set(rk, value);
|
||||
}
|
||||
var ak = token.assetCacheKey;
|
||||
if (isPresent(ak)) {
|
||||
this._valueMap.set(ak, value);
|
||||
}
|
||||
}
|
||||
get(token: CompileTokenMetadata): VALUE {
|
||||
var rk = token.runtimeCacheKey;
|
||||
var ak = token.assetCacheKey;
|
||||
var result;
|
||||
if (isPresent(rk)) {
|
||||
result = this._valueMap.get(rk);
|
||||
}
|
||||
if (isBlank(result) && isPresent(ak)) {
|
||||
result = this._valueMap.get(ak);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
values(): VALUE[] { return this._values; }
|
||||
get size(): number { return this._values.length; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata regarding compilation of a type.
|
||||
*/
|
||||
export class CompileTypeMetadata implements CompileIdentifierMetadata, CompileMetadataWithType {
|
||||
runtime: Type;
|
||||
name: string;
|
||||
prefix: string;
|
||||
moduleUrl: string;
|
||||
isHost: boolean;
|
||||
value: any;
|
||||
diDeps: CompileDiDependencyMetadata[];
|
||||
|
||||
constructor({runtime, name, moduleUrl, prefix, isHost, value, diDeps}: {
|
||||
runtime?: Type,
|
||||
name?: string,
|
||||
moduleUrl?: string,
|
||||
prefix?: string,
|
||||
isHost?: boolean,
|
||||
value?: any,
|
||||
diDeps?: CompileDiDependencyMetadata[]
|
||||
} = {}) {
|
||||
this.runtime = runtime;
|
||||
this.name = name;
|
||||
this.moduleUrl = moduleUrl;
|
||||
this.prefix = prefix;
|
||||
this.isHost = normalizeBool(isHost);
|
||||
this.value = value;
|
||||
this.diDeps = _normalizeArray(diDeps);
|
||||
}
|
||||
|
||||
static fromJson(data: {[key: string]: any}): CompileTypeMetadata {
|
||||
return new CompileTypeMetadata({
|
||||
name: data['name'],
|
||||
moduleUrl: data['moduleUrl'],
|
||||
prefix: data['prefix'],
|
||||
isHost: data['isHost'],
|
||||
value: data['value'],
|
||||
diDeps: _arrayFromJson(data['diDeps'], CompileDiDependencyMetadata.fromJson)
|
||||
});
|
||||
}
|
||||
|
||||
get identifier(): CompileIdentifierMetadata { return this; }
|
||||
get type(): CompileTypeMetadata { return this; }
|
||||
|
||||
toJson(): {[key: string]: any} {
|
||||
return {
|
||||
// Note: Runtime type can't be serialized...
|
||||
'class': 'Type',
|
||||
'name': this.name,
|
||||
'moduleUrl': this.moduleUrl,
|
||||
'prefix': this.prefix,
|
||||
'isHost': this.isHost,
|
||||
'value': this.value,
|
||||
'diDeps': _arrayToJson(this.diDeps)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class CompileQueryMetadata {
|
||||
selectors: Array<CompileTokenMetadata>;
|
||||
descendants: boolean;
|
||||
first: boolean;
|
||||
propertyName: string;
|
||||
read: CompileTokenMetadata;
|
||||
|
||||
constructor({selectors, descendants, first, propertyName, read}: {
|
||||
selectors?: Array<CompileTokenMetadata>,
|
||||
descendants?: boolean,
|
||||
first?: boolean,
|
||||
propertyName?: string,
|
||||
read?: CompileTokenMetadata
|
||||
} = {}) {
|
||||
this.selectors = selectors;
|
||||
this.descendants = normalizeBool(descendants);
|
||||
this.first = normalizeBool(first);
|
||||
this.propertyName = propertyName;
|
||||
this.read = read;
|
||||
}
|
||||
|
||||
static fromJson(data: {[key: string]: any}): CompileQueryMetadata {
|
||||
return new CompileQueryMetadata({
|
||||
selectors: _arrayFromJson(data['selectors'], CompileTokenMetadata.fromJson),
|
||||
descendants: data['descendants'],
|
||||
first: data['first'],
|
||||
propertyName: data['propertyName'],
|
||||
read: _objFromJson(data['read'], CompileTokenMetadata.fromJson)
|
||||
});
|
||||
}
|
||||
|
||||
toJson(): {[key: string]: any} {
|
||||
return {
|
||||
'selectors': _arrayToJson(this.selectors),
|
||||
'descendants': this.descendants,
|
||||
'first': this.first,
|
||||
'propertyName': this.propertyName,
|
||||
'read': _objToJson(this.read)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata regarding compilation of a template.
|
||||
*/
|
||||
export class CompileTemplateMetadata {
|
||||
encapsulation: ViewEncapsulation;
|
||||
template: string;
|
||||
templateUrl: string;
|
||||
styles: string[];
|
||||
styleUrls: string[];
|
||||
ngContentSelectors: string[];
|
||||
baseUrl: string;
|
||||
constructor({encapsulation, template, templateUrl, styles, styleUrls, ngContentSelectors,
|
||||
baseUrl}: {
|
||||
encapsulation?: ViewEncapsulation,
|
||||
template?: string,
|
||||
templateUrl?: string,
|
||||
styles?: string[],
|
||||
styleUrls?: string[],
|
||||
ngContentSelectors?: string[],
|
||||
baseUrl?: string
|
||||
} = {}) {
|
||||
this.encapsulation = isPresent(encapsulation) ? encapsulation : ViewEncapsulation.Emulated;
|
||||
this.template = template;
|
||||
this.templateUrl = templateUrl;
|
||||
this.styles = isPresent(styles) ? styles : [];
|
||||
this.styleUrls = isPresent(styleUrls) ? styleUrls : [];
|
||||
this.ngContentSelectors = isPresent(ngContentSelectors) ? ngContentSelectors : [];
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
static fromJson(data: {[key: string]: any}): CompileTemplateMetadata {
|
||||
return new CompileTemplateMetadata({
|
||||
encapsulation: isPresent(data['encapsulation']) ?
|
||||
VIEW_ENCAPSULATION_VALUES[data['encapsulation']] :
|
||||
data['encapsulation'],
|
||||
template: data['template'],
|
||||
templateUrl: data['templateUrl'],
|
||||
styles: data['styles'],
|
||||
styleUrls: data['styleUrls'],
|
||||
ngContentSelectors: data['ngContentSelectors'],
|
||||
baseUrl: data['baseUrl']
|
||||
});
|
||||
}
|
||||
|
||||
toJson(): {[key: string]: any} {
|
||||
return {
|
||||
'encapsulation':
|
||||
isPresent(this.encapsulation) ? serializeEnum(this.encapsulation) : this.encapsulation,
|
||||
'template': this.template,
|
||||
'templateUrl': this.templateUrl,
|
||||
'styles': this.styles,
|
||||
'styleUrls': this.styleUrls,
|
||||
'ngContentSelectors': this.ngContentSelectors,
|
||||
'baseUrl': this.baseUrl
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata regarding compilation of a directive.
|
||||
*/
|
||||
export class CompileDirectiveMetadata implements CompileMetadataWithType {
|
||||
static create({type, isComponent, selector, exportAs, changeDetection, inputs, outputs, host,
|
||||
lifecycleHooks, providers, viewProviders, queries, viewQueries, template}: {
|
||||
type?: CompileTypeMetadata,
|
||||
isComponent?: boolean,
|
||||
selector?: string,
|
||||
exportAs?: string,
|
||||
changeDetection?: ChangeDetectionStrategy,
|
||||
inputs?: string[],
|
||||
outputs?: string[],
|
||||
host?: {[key: string]: string},
|
||||
lifecycleHooks?: LifecycleHooks[],
|
||||
providers?:
|
||||
Array<CompileProviderMetadata | CompileTypeMetadata | CompileIdentifierMetadata | any[]>,
|
||||
viewProviders?:
|
||||
Array<CompileProviderMetadata | CompileTypeMetadata | CompileIdentifierMetadata | any[]>,
|
||||
queries?: CompileQueryMetadata[],
|
||||
viewQueries?: CompileQueryMetadata[],
|
||||
template?: CompileTemplateMetadata
|
||||
} = {}): CompileDirectiveMetadata {
|
||||
var hostListeners: {[key: string]: string} = {};
|
||||
var hostProperties: {[key: string]: string} = {};
|
||||
var hostAttributes: {[key: string]: string} = {};
|
||||
if (isPresent(host)) {
|
||||
StringMapWrapper.forEach(host, (value: string, key: string) => {
|
||||
var matches = RegExpWrapper.firstMatch(HOST_REG_EXP, key);
|
||||
if (isBlank(matches)) {
|
||||
hostAttributes[key] = value;
|
||||
} else if (isPresent(matches[1])) {
|
||||
hostProperties[matches[1]] = value;
|
||||
} else if (isPresent(matches[2])) {
|
||||
hostListeners[matches[2]] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
var inputsMap: {[key: string]: string} = {};
|
||||
if (isPresent(inputs)) {
|
||||
inputs.forEach((bindConfig: string) => {
|
||||
// canonical syntax: `dirProp: elProp`
|
||||
// if there is no `:`, use dirProp = elProp
|
||||
var parts = splitAtColon(bindConfig, [bindConfig, bindConfig]);
|
||||
inputsMap[parts[0]] = parts[1];
|
||||
});
|
||||
}
|
||||
var outputsMap: {[key: string]: string} = {};
|
||||
if (isPresent(outputs)) {
|
||||
outputs.forEach((bindConfig: string) => {
|
||||
// canonical syntax: `dirProp: elProp`
|
||||
// if there is no `:`, use dirProp = elProp
|
||||
var parts = splitAtColon(bindConfig, [bindConfig, bindConfig]);
|
||||
outputsMap[parts[0]] = parts[1];
|
||||
});
|
||||
}
|
||||
|
||||
return new CompileDirectiveMetadata({
|
||||
type: type,
|
||||
isComponent: normalizeBool(isComponent),
|
||||
selector: selector,
|
||||
exportAs: exportAs,
|
||||
changeDetection: changeDetection,
|
||||
inputs: inputsMap,
|
||||
outputs: outputsMap,
|
||||
hostListeners: hostListeners,
|
||||
hostProperties: hostProperties,
|
||||
hostAttributes: hostAttributes,
|
||||
lifecycleHooks: isPresent(lifecycleHooks) ? lifecycleHooks : [],
|
||||
providers: providers,
|
||||
viewProviders: viewProviders,
|
||||
queries: queries,
|
||||
viewQueries: viewQueries,
|
||||
template: template
|
||||
});
|
||||
}
|
||||
type: CompileTypeMetadata;
|
||||
isComponent: boolean;
|
||||
selector: string;
|
||||
exportAs: string;
|
||||
changeDetection: ChangeDetectionStrategy;
|
||||
inputs: {[key: string]: string};
|
||||
outputs: {[key: string]: string};
|
||||
hostListeners: {[key: string]: string};
|
||||
hostProperties: {[key: string]: string};
|
||||
hostAttributes: {[key: string]: string};
|
||||
lifecycleHooks: LifecycleHooks[];
|
||||
providers: CompileProviderMetadata[];
|
||||
viewProviders: CompileProviderMetadata[];
|
||||
queries: CompileQueryMetadata[];
|
||||
viewQueries: CompileQueryMetadata[];
|
||||
|
||||
template: CompileTemplateMetadata;
|
||||
constructor({type, isComponent, selector, exportAs, changeDetection, inputs, outputs,
|
||||
hostListeners, hostProperties, hostAttributes, lifecycleHooks, providers,
|
||||
viewProviders, queries, viewQueries, template}: {
|
||||
type?: CompileTypeMetadata,
|
||||
isComponent?: boolean,
|
||||
selector?: string,
|
||||
exportAs?: string,
|
||||
changeDetection?: ChangeDetectionStrategy,
|
||||
inputs?: {[key: string]: string},
|
||||
outputs?: {[key: string]: string},
|
||||
hostListeners?: {[key: string]: string},
|
||||
hostProperties?: {[key: string]: string},
|
||||
hostAttributes?: {[key: string]: string},
|
||||
lifecycleHooks?: LifecycleHooks[],
|
||||
providers?:
|
||||
Array<CompileProviderMetadata | CompileTypeMetadata | CompileIdentifierMetadata | any[]>,
|
||||
viewProviders?:
|
||||
Array<CompileProviderMetadata | CompileTypeMetadata | CompileIdentifierMetadata | any[]>,
|
||||
queries?: CompileQueryMetadata[],
|
||||
viewQueries?: CompileQueryMetadata[],
|
||||
template?: CompileTemplateMetadata
|
||||
} = {}) {
|
||||
this.type = type;
|
||||
this.isComponent = isComponent;
|
||||
this.selector = selector;
|
||||
this.exportAs = exportAs;
|
||||
this.changeDetection = changeDetection;
|
||||
this.inputs = inputs;
|
||||
this.outputs = outputs;
|
||||
this.hostListeners = hostListeners;
|
||||
this.hostProperties = hostProperties;
|
||||
this.hostAttributes = hostAttributes;
|
||||
this.lifecycleHooks = _normalizeArray(lifecycleHooks);
|
||||
this.providers = _normalizeArray(providers);
|
||||
this.viewProviders = _normalizeArray(viewProviders);
|
||||
this.queries = _normalizeArray(queries);
|
||||
this.viewQueries = _normalizeArray(viewQueries);
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
get identifier(): CompileIdentifierMetadata { return this.type; }
|
||||
|
||||
static fromJson(data: {[key: string]: any}): CompileDirectiveMetadata {
|
||||
return new CompileDirectiveMetadata({
|
||||
isComponent: data['isComponent'],
|
||||
selector: data['selector'],
|
||||
exportAs: data['exportAs'],
|
||||
type: isPresent(data['type']) ? CompileTypeMetadata.fromJson(data['type']) : data['type'],
|
||||
changeDetection: isPresent(data['changeDetection']) ?
|
||||
CHANGE_DETECTION_STRATEGY_VALUES[data['changeDetection']] :
|
||||
data['changeDetection'],
|
||||
inputs: data['inputs'],
|
||||
outputs: data['outputs'],
|
||||
hostListeners: data['hostListeners'],
|
||||
hostProperties: data['hostProperties'],
|
||||
hostAttributes: data['hostAttributes'],
|
||||
lifecycleHooks:
|
||||
(<any[]>data['lifecycleHooks']).map(hookValue => LIFECYCLE_HOOKS_VALUES[hookValue]),
|
||||
template: isPresent(data['template']) ? CompileTemplateMetadata.fromJson(data['template']) :
|
||||
data['template'],
|
||||
providers: _arrayFromJson(data['providers'], metadataFromJson),
|
||||
viewProviders: _arrayFromJson(data['viewProviders'], metadataFromJson),
|
||||
queries: _arrayFromJson(data['queries'], CompileQueryMetadata.fromJson),
|
||||
viewQueries: _arrayFromJson(data['viewQueries'], CompileQueryMetadata.fromJson)
|
||||
});
|
||||
}
|
||||
|
||||
toJson(): {[key: string]: any} {
|
||||
return {
|
||||
'class': 'Directive',
|
||||
'isComponent': this.isComponent,
|
||||
'selector': this.selector,
|
||||
'exportAs': this.exportAs,
|
||||
'type': isPresent(this.type) ? this.type.toJson() : this.type,
|
||||
'changeDetection': isPresent(this.changeDetection) ? serializeEnum(this.changeDetection) :
|
||||
this.changeDetection,
|
||||
'inputs': this.inputs,
|
||||
'outputs': this.outputs,
|
||||
'hostListeners': this.hostListeners,
|
||||
'hostProperties': this.hostProperties,
|
||||
'hostAttributes': this.hostAttributes,
|
||||
'lifecycleHooks': this.lifecycleHooks.map(hook => serializeEnum(hook)),
|
||||
'template': isPresent(this.template) ? this.template.toJson() : this.template,
|
||||
'providers': _arrayToJson(this.providers),
|
||||
'viewProviders': _arrayToJson(this.viewProviders),
|
||||
'queries': _arrayToJson(this.queries),
|
||||
'viewQueries': _arrayToJson(this.viewQueries)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct {@link CompileDirectiveMetadata} from {@link ComponentTypeMetadata} and a selector.
|
||||
*/
|
||||
export function createHostComponentMeta(componentType: CompileTypeMetadata,
|
||||
componentSelector: string): CompileDirectiveMetadata {
|
||||
var template = CssSelector.parse(componentSelector)[0].getMatchingElementTemplate();
|
||||
return CompileDirectiveMetadata.create({
|
||||
type: new CompileTypeMetadata({
|
||||
runtime: Object,
|
||||
name: `${componentType.name}_Host`,
|
||||
moduleUrl: componentType.moduleUrl,
|
||||
isHost: true
|
||||
}),
|
||||
template: new CompileTemplateMetadata(
|
||||
{template: template, templateUrl: '', styles: [], styleUrls: [], ngContentSelectors: []}),
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
host: {},
|
||||
lifecycleHooks: [],
|
||||
isComponent: true,
|
||||
selector: '*',
|
||||
providers: [],
|
||||
viewProviders: [],
|
||||
queries: [],
|
||||
viewQueries: []
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export class CompilePipeMetadata implements CompileMetadataWithType {
|
||||
type: CompileTypeMetadata;
|
||||
name: string;
|
||||
pure: boolean;
|
||||
lifecycleHooks: LifecycleHooks[];
|
||||
|
||||
constructor({type, name, pure, lifecycleHooks}: {
|
||||
type?: CompileTypeMetadata,
|
||||
name?: string,
|
||||
pure?: boolean,
|
||||
lifecycleHooks?: LifecycleHooks[]
|
||||
} = {}) {
|
||||
this.type = type;
|
||||
this.name = name;
|
||||
this.pure = normalizeBool(pure);
|
||||
this.lifecycleHooks = _normalizeArray(lifecycleHooks);
|
||||
}
|
||||
get identifier(): CompileIdentifierMetadata { return this.type; }
|
||||
|
||||
static fromJson(data: {[key: string]: any}): CompilePipeMetadata {
|
||||
return new CompilePipeMetadata({
|
||||
type: isPresent(data['type']) ? CompileTypeMetadata.fromJson(data['type']) : data['type'],
|
||||
name: data['name'],
|
||||
pure: data['pure']
|
||||
});
|
||||
}
|
||||
|
||||
toJson(): {[key: string]: any} {
|
||||
return {
|
||||
'class': 'Pipe',
|
||||
'type': isPresent(this.type) ? this.type.toJson() : null,
|
||||
'name': this.name,
|
||||
'pure': this.pure
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var _COMPILE_METADATA_FROM_JSON = {
|
||||
'Directive': CompileDirectiveMetadata.fromJson,
|
||||
'Pipe': CompilePipeMetadata.fromJson,
|
||||
'Type': CompileTypeMetadata.fromJson,
|
||||
'Provider': CompileProviderMetadata.fromJson,
|
||||
'Identifier': CompileIdentifierMetadata.fromJson,
|
||||
'Factory': CompileFactoryMetadata.fromJson
|
||||
};
|
||||
|
||||
function _arrayFromJson(obj: any[], fn: (a: {[key: string]: any}) => any): any {
|
||||
return isBlank(obj) ? null : obj.map(o => _objFromJson(o, fn));
|
||||
}
|
||||
|
||||
function _arrayToJson(obj: any[]): string | {[key: string]: any} {
|
||||
return isBlank(obj) ? null : obj.map(_objToJson);
|
||||
}
|
||||
|
||||
function _objFromJson(obj: any, fn: (a: {[key: string]: any}) => any): any {
|
||||
if (isArray(obj)) return _arrayFromJson(obj, fn);
|
||||
if (isString(obj) || isBlank(obj) || isBoolean(obj) || isNumber(obj)) return obj;
|
||||
return fn(obj);
|
||||
}
|
||||
|
||||
function _objToJson(obj: any): string | {[key: string]: any} {
|
||||
if (isArray(obj)) return _arrayToJson(obj);
|
||||
if (isString(obj) || isBlank(obj) || isBoolean(obj) || isNumber(obj)) return obj;
|
||||
return obj.toJson();
|
||||
}
|
||||
|
||||
function _normalizeArray(obj: any[]): any[] {
|
||||
return isPresent(obj) ? obj : [];
|
||||
}
|
62
modules/@angular/compiler/src/compiler.ts
Normal file
62
modules/@angular/compiler/src/compiler.ts
Normal file
@ -0,0 +1,62 @@
|
||||
export {PLATFORM_DIRECTIVES, PLATFORM_PIPES} from 'angular2/src/core/platform_directives_and_pipes';
|
||||
export * from 'angular2/src/compiler/template_ast';
|
||||
export {TEMPLATE_TRANSFORMS} from 'angular2/src/compiler/template_parser';
|
||||
export {CompilerConfig, RenderTypes} from './config';
|
||||
export * from './compile_metadata';
|
||||
export * from './offline_compiler';
|
||||
export {RuntimeCompiler} from './runtime_compiler';
|
||||
export * from 'angular2/src/compiler/url_resolver';
|
||||
export * from 'angular2/src/compiler/xhr';
|
||||
|
||||
export {ViewResolver} from './view_resolver';
|
||||
export {DirectiveResolver} from './directive_resolver';
|
||||
export {PipeResolver} from './pipe_resolver';
|
||||
|
||||
import {assertionsEnabled, Type} from 'angular2/src/facade/lang';
|
||||
import {TemplateParser} from 'angular2/src/compiler/template_parser';
|
||||
import {HtmlParser} from 'angular2/src/compiler/html_parser';
|
||||
import {DirectiveNormalizer} from 'angular2/src/compiler/directive_normalizer';
|
||||
import {CompileMetadataResolver} from 'angular2/src/compiler/metadata_resolver';
|
||||
import {StyleCompiler} from 'angular2/src/compiler/style_compiler';
|
||||
import {ViewCompiler} from 'angular2/src/compiler/view_compiler/view_compiler';
|
||||
import {CompilerConfig} from './config';
|
||||
import {ComponentResolver} from 'angular2/src/core/linker/component_resolver';
|
||||
import {RuntimeCompiler} from 'angular2/src/compiler/runtime_compiler';
|
||||
import {ElementSchemaRegistry} from 'angular2/src/compiler/schema/element_schema_registry';
|
||||
import {DomElementSchemaRegistry} from 'angular2/src/compiler/schema/dom_element_schema_registry';
|
||||
import {UrlResolver, DEFAULT_PACKAGE_URL_PROVIDER} from 'angular2/src/compiler/url_resolver';
|
||||
import {Parser} from './expression_parser/parser';
|
||||
import {Lexer} from './expression_parser/lexer';
|
||||
import {ViewResolver} from './view_resolver';
|
||||
import {DirectiveResolver} from './directive_resolver';
|
||||
import {PipeResolver} from './pipe_resolver';
|
||||
|
||||
function _createCompilerConfig() {
|
||||
return new CompilerConfig(assertionsEnabled(), false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* A set of providers that provide `RuntimeCompiler` and its dependencies to use for
|
||||
* template compilation.
|
||||
*/
|
||||
export const COMPILER_PROVIDERS: Array<any | Type | {[k: string]: any} | any[]> =
|
||||
/*@ts2dart_const*/[
|
||||
Lexer,
|
||||
Parser,
|
||||
HtmlParser,
|
||||
TemplateParser,
|
||||
DirectiveNormalizer,
|
||||
CompileMetadataResolver,
|
||||
DEFAULT_PACKAGE_URL_PROVIDER,
|
||||
StyleCompiler,
|
||||
ViewCompiler,
|
||||
/*@ts2dart_Provider*/ {provide: CompilerConfig, useFactory: _createCompilerConfig, deps: []},
|
||||
RuntimeCompiler,
|
||||
/*@ts2dart_Provider*/ {provide: ComponentResolver, useExisting: RuntimeCompiler},
|
||||
DomElementSchemaRegistry,
|
||||
/*@ts2dart_Provider*/ {provide: ElementSchemaRegistry, useExisting: DomElementSchemaRegistry},
|
||||
UrlResolver,
|
||||
ViewResolver,
|
||||
DirectiveResolver,
|
||||
PipeResolver
|
||||
];
|
38
modules/@angular/compiler/src/config.ts
Normal file
38
modules/@angular/compiler/src/config.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import {isBlank} from 'angular2/src/facade/lang';
|
||||
import {unimplemented} from 'angular2/src/facade/exceptions';
|
||||
import {Identifiers} from './identifiers';
|
||||
import {CompileIdentifierMetadata} from './compile_metadata';
|
||||
|
||||
export class CompilerConfig {
|
||||
public renderTypes: RenderTypes;
|
||||
constructor(public genDebugInfo: boolean, public logBindingUpdate: boolean,
|
||||
public useJit: boolean, renderTypes: RenderTypes = null) {
|
||||
if (isBlank(renderTypes)) {
|
||||
renderTypes = new DefaultRenderTypes();
|
||||
}
|
||||
this.renderTypes = renderTypes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Types used for the renderer.
|
||||
* Can be replaced to specialize the generated output to a specific renderer
|
||||
* to help tree shaking.
|
||||
*/
|
||||
export abstract class RenderTypes {
|
||||
get renderer(): CompileIdentifierMetadata { return unimplemented(); }
|
||||
get renderText(): CompileIdentifierMetadata { return unimplemented(); }
|
||||
get renderElement(): CompileIdentifierMetadata { return unimplemented(); }
|
||||
get renderComment(): CompileIdentifierMetadata { return unimplemented(); }
|
||||
get renderNode(): CompileIdentifierMetadata { return unimplemented(); }
|
||||
get renderEvent(): CompileIdentifierMetadata { return unimplemented(); }
|
||||
}
|
||||
|
||||
export class DefaultRenderTypes implements RenderTypes {
|
||||
renderer = Identifiers.Renderer;
|
||||
renderText = null;
|
||||
renderElement = null;
|
||||
renderComment = null;
|
||||
renderNode = null;
|
||||
renderEvent = null;
|
||||
}
|
756
modules/@angular/compiler/src/css/lexer.ts
Normal file
756
modules/@angular/compiler/src/css/lexer.ts
Normal file
@ -0,0 +1,756 @@
|
||||
import {NumberWrapper, StringWrapper, isPresent, resolveEnumToken} from "angular2/src/facade/lang";
|
||||
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||
|
||||
import {
|
||||
isWhitespace,
|
||||
$EOF,
|
||||
$HASH,
|
||||
$TILDA,
|
||||
$CARET,
|
||||
$PERCENT,
|
||||
$$,
|
||||
$_,
|
||||
$COLON,
|
||||
$SQ,
|
||||
$DQ,
|
||||
$EQ,
|
||||
$SLASH,
|
||||
$BACKSLASH,
|
||||
$PERIOD,
|
||||
$STAR,
|
||||
$PLUS,
|
||||
$LPAREN,
|
||||
$RPAREN,
|
||||
$LBRACE,
|
||||
$RBRACE,
|
||||
$LBRACKET,
|
||||
$RBRACKET,
|
||||
$PIPE,
|
||||
$COMMA,
|
||||
$SEMICOLON,
|
||||
$MINUS,
|
||||
$BANG,
|
||||
$QUESTION,
|
||||
$AT,
|
||||
$AMPERSAND,
|
||||
$GT,
|
||||
$a,
|
||||
$A,
|
||||
$z,
|
||||
$Z,
|
||||
$0,
|
||||
$9,
|
||||
$FF,
|
||||
$CR,
|
||||
$LF,
|
||||
$VTAB
|
||||
} from "angular2/src/compiler/chars";
|
||||
|
||||
export {
|
||||
$EOF,
|
||||
$AT,
|
||||
$RBRACE,
|
||||
$LBRACE,
|
||||
$LBRACKET,
|
||||
$RBRACKET,
|
||||
$LPAREN,
|
||||
$RPAREN,
|
||||
$COMMA,
|
||||
$COLON,
|
||||
$SEMICOLON,
|
||||
isWhitespace
|
||||
} from "angular2/src/compiler/chars";
|
||||
|
||||
export enum CssTokenType {
|
||||
EOF,
|
||||
String,
|
||||
Comment,
|
||||
Identifier,
|
||||
Number,
|
||||
IdentifierOrNumber,
|
||||
AtKeyword,
|
||||
Character,
|
||||
Whitespace,
|
||||
Invalid
|
||||
}
|
||||
|
||||
export enum CssLexerMode {
|
||||
ALL,
|
||||
ALL_TRACK_WS,
|
||||
SELECTOR,
|
||||
PSEUDO_SELECTOR,
|
||||
ATTRIBUTE_SELECTOR,
|
||||
AT_RULE_QUERY,
|
||||
MEDIA_QUERY,
|
||||
BLOCK,
|
||||
KEYFRAME_BLOCK,
|
||||
STYLE_BLOCK,
|
||||
STYLE_VALUE,
|
||||
STYLE_VALUE_FUNCTION,
|
||||
STYLE_CALC_FUNCTION
|
||||
}
|
||||
|
||||
export class LexedCssResult {
|
||||
constructor(public error: CssScannerError, public token: CssToken) {}
|
||||
}
|
||||
|
||||
export function generateErrorMessage(input: string, message: string, errorValue: string,
|
||||
index: number, row: number, column: number): string {
|
||||
return `${message} at column ${row}:${column} in expression [` +
|
||||
findProblemCode(input, errorValue, index, column) + ']';
|
||||
}
|
||||
|
||||
export function findProblemCode(input: string, errorValue: string, index: number,
|
||||
column: number): string {
|
||||
var endOfProblemLine = index;
|
||||
var current = charCode(input, index);
|
||||
while (current > 0 && !isNewline(current)) {
|
||||
current = charCode(input, ++endOfProblemLine);
|
||||
}
|
||||
var choppedString = input.substring(0, endOfProblemLine);
|
||||
var pointerPadding = "";
|
||||
for (var i = 0; i < column; i++) {
|
||||
pointerPadding += " ";
|
||||
}
|
||||
var pointerString = "";
|
||||
for (var i = 0; i < errorValue.length; i++) {
|
||||
pointerString += "^";
|
||||
}
|
||||
return choppedString + "\n" + pointerPadding + pointerString + "\n";
|
||||
}
|
||||
|
||||
export class CssToken {
|
||||
numValue: number;
|
||||
constructor(public index: number, public column: number, public line: number,
|
||||
public type: CssTokenType, public strValue: string) {
|
||||
this.numValue = charCode(strValue, 0);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssLexer {
|
||||
scan(text: string, trackComments: boolean = false): CssScanner {
|
||||
return new CssScanner(text, trackComments);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssScannerError extends BaseException {
|
||||
public rawMessage: string;
|
||||
public message: string;
|
||||
|
||||
constructor(public token: CssToken, message) {
|
||||
super('Css Parse Error: ' + message);
|
||||
this.rawMessage = message;
|
||||
}
|
||||
|
||||
toString(): string { return this.message; }
|
||||
}
|
||||
|
||||
function _trackWhitespace(mode: CssLexerMode) {
|
||||
switch (mode) {
|
||||
case CssLexerMode.SELECTOR:
|
||||
case CssLexerMode.ALL_TRACK_WS:
|
||||
case CssLexerMode.STYLE_VALUE:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class CssScanner {
|
||||
peek: number;
|
||||
peekPeek: number;
|
||||
length: number = 0;
|
||||
index: number = -1;
|
||||
column: number = -1;
|
||||
line: number = 0;
|
||||
|
||||
/** @internal */
|
||||
_currentMode: CssLexerMode = CssLexerMode.BLOCK;
|
||||
/** @internal */
|
||||
_currentError: CssScannerError = null;
|
||||
|
||||
constructor(public input: string, private _trackComments: boolean = false) {
|
||||
this.length = this.input.length;
|
||||
this.peekPeek = this.peekAt(0);
|
||||
this.advance();
|
||||
}
|
||||
|
||||
getMode(): CssLexerMode { return this._currentMode; }
|
||||
|
||||
setMode(mode: CssLexerMode) {
|
||||
if (this._currentMode != mode) {
|
||||
if (_trackWhitespace(this._currentMode)) {
|
||||
this.consumeWhitespace();
|
||||
}
|
||||
this._currentMode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
advance(): void {
|
||||
if (isNewline(this.peek)) {
|
||||
this.column = 0;
|
||||
this.line++;
|
||||
} else {
|
||||
this.column++;
|
||||
}
|
||||
|
||||
this.index++;
|
||||
this.peek = this.peekPeek;
|
||||
this.peekPeek = this.peekAt(this.index + 1);
|
||||
}
|
||||
|
||||
peekAt(index: number): number {
|
||||
return index >= this.length ? $EOF : StringWrapper.charCodeAt(this.input, index);
|
||||
}
|
||||
|
||||
consumeEmptyStatements(): void {
|
||||
this.consumeWhitespace();
|
||||
while (this.peek == $SEMICOLON) {
|
||||
this.advance();
|
||||
this.consumeWhitespace();
|
||||
}
|
||||
}
|
||||
|
||||
consumeWhitespace(): void {
|
||||
while (isWhitespace(this.peek) || isNewline(this.peek)) {
|
||||
this.advance();
|
||||
if (!this._trackComments && isCommentStart(this.peek, this.peekPeek)) {
|
||||
this.advance(); // /
|
||||
this.advance(); // *
|
||||
while (!isCommentEnd(this.peek, this.peekPeek)) {
|
||||
if (this.peek == $EOF) {
|
||||
this.error('Unterminated comment');
|
||||
}
|
||||
this.advance();
|
||||
}
|
||||
this.advance(); // *
|
||||
this.advance(); // /
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
consume(type: CssTokenType, value: string = null): LexedCssResult {
|
||||
var mode = this._currentMode;
|
||||
this.setMode(CssLexerMode.ALL);
|
||||
|
||||
var previousIndex = this.index;
|
||||
var previousLine = this.line;
|
||||
var previousColumn = this.column;
|
||||
|
||||
var output = this.scan();
|
||||
|
||||
// just incase the inner scan method returned an error
|
||||
if (isPresent(output.error)) {
|
||||
this.setMode(mode);
|
||||
return output;
|
||||
}
|
||||
|
||||
var next = output.token;
|
||||
if (!isPresent(next)) {
|
||||
next = new CssToken(0, 0, 0, CssTokenType.EOF, "end of file");
|
||||
}
|
||||
|
||||
var isMatchingType;
|
||||
if (type == CssTokenType.IdentifierOrNumber) {
|
||||
// TODO (matsko): implement array traversal for lookup here
|
||||
isMatchingType = next.type == CssTokenType.Number || next.type == CssTokenType.Identifier;
|
||||
} else {
|
||||
isMatchingType = next.type == type;
|
||||
}
|
||||
|
||||
// before throwing the error we need to bring back the former
|
||||
// mode so that the parser can recover...
|
||||
this.setMode(mode);
|
||||
|
||||
var error = null;
|
||||
if (!isMatchingType || (isPresent(value) && value != next.strValue)) {
|
||||
var errorMessage = resolveEnumToken(CssTokenType, next.type) + " does not match expected " +
|
||||
resolveEnumToken(CssTokenType, type) + " value";
|
||||
|
||||
if (isPresent(value)) {
|
||||
errorMessage += ' ("' + next.strValue + '" should match "' + value + '")';
|
||||
}
|
||||
|
||||
error = new CssScannerError(
|
||||
next, generateErrorMessage(this.input, errorMessage, next.strValue, previousIndex,
|
||||
previousLine, previousColumn));
|
||||
}
|
||||
|
||||
return new LexedCssResult(error, next);
|
||||
}
|
||||
|
||||
|
||||
scan(): LexedCssResult {
|
||||
var trackWS = _trackWhitespace(this._currentMode);
|
||||
if (this.index == 0 && !trackWS) { // first scan
|
||||
this.consumeWhitespace();
|
||||
}
|
||||
|
||||
var token = this._scan();
|
||||
if (token == null) return null;
|
||||
|
||||
var error = this._currentError;
|
||||
this._currentError = null;
|
||||
|
||||
if (!trackWS) {
|
||||
this.consumeWhitespace();
|
||||
}
|
||||
return new LexedCssResult(error, token);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_scan(): CssToken {
|
||||
var peek = this.peek;
|
||||
var peekPeek = this.peekPeek;
|
||||
if (peek == $EOF) return null;
|
||||
|
||||
if (isCommentStart(peek, peekPeek)) {
|
||||
// even if comments are not tracked we still lex the
|
||||
// comment so we can move the pointer forward
|
||||
var commentToken = this.scanComment();
|
||||
if (this._trackComments) {
|
||||
return commentToken;
|
||||
}
|
||||
}
|
||||
|
||||
if (_trackWhitespace(this._currentMode) && (isWhitespace(peek) || isNewline(peek))) {
|
||||
return this.scanWhitespace();
|
||||
}
|
||||
|
||||
peek = this.peek;
|
||||
peekPeek = this.peekPeek;
|
||||
if (peek == $EOF) return null;
|
||||
|
||||
if (isStringStart(peek, peekPeek)) {
|
||||
return this.scanString();
|
||||
}
|
||||
|
||||
// something like url(cool)
|
||||
if (this._currentMode == CssLexerMode.STYLE_VALUE_FUNCTION) {
|
||||
return this.scanCssValueFunction();
|
||||
}
|
||||
|
||||
var isModifier = peek == $PLUS || peek == $MINUS;
|
||||
var digitA = isModifier ? false : isDigit(peek);
|
||||
var digitB = isDigit(peekPeek);
|
||||
if (digitA || (isModifier && (peekPeek == $PERIOD || digitB)) || (peek == $PERIOD && digitB)) {
|
||||
return this.scanNumber();
|
||||
}
|
||||
|
||||
if (peek == $AT) {
|
||||
return this.scanAtExpression();
|
||||
}
|
||||
|
||||
if (isIdentifierStart(peek, peekPeek)) {
|
||||
return this.scanIdentifier();
|
||||
}
|
||||
|
||||
if (isValidCssCharacter(peek, this._currentMode)) {
|
||||
return this.scanCharacter();
|
||||
}
|
||||
|
||||
return this.error(`Unexpected character [${StringWrapper.fromCharCode(peek)}]`);
|
||||
}
|
||||
|
||||
scanComment(): CssToken {
|
||||
if (this.assertCondition(isCommentStart(this.peek, this.peekPeek),
|
||||
"Expected comment start value")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var start = this.index;
|
||||
var startingColumn = this.column;
|
||||
var startingLine = this.line;
|
||||
|
||||
this.advance(); // /
|
||||
this.advance(); // *
|
||||
|
||||
while (!isCommentEnd(this.peek, this.peekPeek)) {
|
||||
if (this.peek == $EOF) {
|
||||
this.error('Unterminated comment');
|
||||
}
|
||||
this.advance();
|
||||
}
|
||||
|
||||
this.advance(); // *
|
||||
this.advance(); // /
|
||||
|
||||
var str = this.input.substring(start, this.index);
|
||||
return new CssToken(start, startingColumn, startingLine, CssTokenType.Comment, str);
|
||||
}
|
||||
|
||||
scanWhitespace(): CssToken {
|
||||
var start = this.index;
|
||||
var startingColumn = this.column;
|
||||
var startingLine = this.line;
|
||||
while (isWhitespace(this.peek) && this.peek != $EOF) {
|
||||
this.advance();
|
||||
}
|
||||
var str = this.input.substring(start, this.index);
|
||||
return new CssToken(start, startingColumn, startingLine, CssTokenType.Whitespace, str);
|
||||
}
|
||||
|
||||
scanString(): CssToken {
|
||||
if (this.assertCondition(isStringStart(this.peek, this.peekPeek),
|
||||
"Unexpected non-string starting value")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var target = this.peek;
|
||||
var start = this.index;
|
||||
var startingColumn = this.column;
|
||||
var startingLine = this.line;
|
||||
var previous = target;
|
||||
this.advance();
|
||||
|
||||
while (!isCharMatch(target, previous, this.peek)) {
|
||||
if (this.peek == $EOF || isNewline(this.peek)) {
|
||||
this.error('Unterminated quote');
|
||||
}
|
||||
previous = this.peek;
|
||||
this.advance();
|
||||
}
|
||||
|
||||
if (this.assertCondition(this.peek == target, "Unterminated quote")) {
|
||||
return null;
|
||||
}
|
||||
this.advance();
|
||||
|
||||
var str = this.input.substring(start, this.index);
|
||||
return new CssToken(start, startingColumn, startingLine, CssTokenType.String, str);
|
||||
}
|
||||
|
||||
scanNumber(): CssToken {
|
||||
var start = this.index;
|
||||
var startingColumn = this.column;
|
||||
if (this.peek == $PLUS || this.peek == $MINUS) {
|
||||
this.advance();
|
||||
}
|
||||
var periodUsed = false;
|
||||
while (isDigit(this.peek) || this.peek == $PERIOD) {
|
||||
if (this.peek == $PERIOD) {
|
||||
if (periodUsed) {
|
||||
this.error('Unexpected use of a second period value');
|
||||
}
|
||||
periodUsed = true;
|
||||
}
|
||||
this.advance();
|
||||
}
|
||||
var strValue = this.input.substring(start, this.index);
|
||||
return new CssToken(start, startingColumn, this.line, CssTokenType.Number, strValue);
|
||||
}
|
||||
|
||||
scanIdentifier(): CssToken {
|
||||
if (this.assertCondition(isIdentifierStart(this.peek, this.peekPeek),
|
||||
'Expected identifier starting value')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var start = this.index;
|
||||
var startingColumn = this.column;
|
||||
while (isIdentifierPart(this.peek)) {
|
||||
this.advance();
|
||||
}
|
||||
var strValue = this.input.substring(start, this.index);
|
||||
return new CssToken(start, startingColumn, this.line, CssTokenType.Identifier, strValue);
|
||||
}
|
||||
|
||||
scanCssValueFunction(): CssToken {
|
||||
var start = this.index;
|
||||
var startingColumn = this.column;
|
||||
while (this.peek != $EOF && this.peek != $RPAREN) {
|
||||
this.advance();
|
||||
}
|
||||
var strValue = this.input.substring(start, this.index);
|
||||
return new CssToken(start, startingColumn, this.line, CssTokenType.Identifier, strValue);
|
||||
}
|
||||
|
||||
scanCharacter(): CssToken {
|
||||
var start = this.index;
|
||||
var startingColumn = this.column;
|
||||
if (this.assertCondition(isValidCssCharacter(this.peek, this._currentMode),
|
||||
charStr(this.peek) + ' is not a valid CSS character')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var c = this.input.substring(start, start + 1);
|
||||
this.advance();
|
||||
|
||||
return new CssToken(start, startingColumn, this.line, CssTokenType.Character, c);
|
||||
}
|
||||
|
||||
scanAtExpression(): CssToken {
|
||||
if (this.assertCondition(this.peek == $AT, 'Expected @ value')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var start = this.index;
|
||||
var startingColumn = this.column;
|
||||
this.advance();
|
||||
if (isIdentifierStart(this.peek, this.peekPeek)) {
|
||||
var ident = this.scanIdentifier();
|
||||
var strValue = '@' + ident.strValue;
|
||||
return new CssToken(start, startingColumn, this.line, CssTokenType.AtKeyword, strValue);
|
||||
} else {
|
||||
return this.scanCharacter();
|
||||
}
|
||||
}
|
||||
|
||||
assertCondition(status: boolean, errorMessage: string): boolean {
|
||||
if (!status) {
|
||||
this.error(errorMessage);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
error(message: string, errorTokenValue: string = null, doNotAdvance: boolean = false): CssToken {
|
||||
var index: number = this.index;
|
||||
var column: number = this.column;
|
||||
var line: number = this.line;
|
||||
errorTokenValue =
|
||||
isPresent(errorTokenValue) ? errorTokenValue : StringWrapper.fromCharCode(this.peek);
|
||||
var invalidToken = new CssToken(index, column, line, CssTokenType.Invalid, errorTokenValue);
|
||||
var errorMessage =
|
||||
generateErrorMessage(this.input, message, errorTokenValue, index, line, column);
|
||||
if (!doNotAdvance) {
|
||||
this.advance();
|
||||
}
|
||||
this._currentError = new CssScannerError(invalidToken, errorMessage);
|
||||
return invalidToken;
|
||||
}
|
||||
}
|
||||
|
||||
function isAtKeyword(current: CssToken, next: CssToken): boolean {
|
||||
return current.numValue == $AT && next.type == CssTokenType.Identifier;
|
||||
}
|
||||
|
||||
function isCharMatch(target: number, previous: number, code: number): boolean {
|
||||
return code == target && previous != $BACKSLASH;
|
||||
}
|
||||
|
||||
function isDigit(code: number): boolean {
|
||||
return $0 <= code && code <= $9;
|
||||
}
|
||||
|
||||
function isCommentStart(code: number, next: number): boolean {
|
||||
return code == $SLASH && next == $STAR;
|
||||
}
|
||||
|
||||
function isCommentEnd(code: number, next: number): boolean {
|
||||
return code == $STAR && next == $SLASH;
|
||||
}
|
||||
|
||||
function isStringStart(code: number, next: number): boolean {
|
||||
var target = code;
|
||||
if (target == $BACKSLASH) {
|
||||
target = next;
|
||||
}
|
||||
return target == $DQ || target == $SQ;
|
||||
}
|
||||
|
||||
function isIdentifierStart(code: number, next: number): boolean {
|
||||
var target = code;
|
||||
if (target == $MINUS) {
|
||||
target = next;
|
||||
}
|
||||
|
||||
return ($a <= target && target <= $z) || ($A <= target && target <= $Z) || target == $BACKSLASH ||
|
||||
target == $MINUS || target == $_;
|
||||
}
|
||||
|
||||
function isIdentifierPart(target: number): boolean {
|
||||
return ($a <= target && target <= $z) || ($A <= target && target <= $Z) || target == $BACKSLASH ||
|
||||
target == $MINUS || target == $_ || isDigit(target);
|
||||
}
|
||||
|
||||
function isValidPseudoSelectorCharacter(code: number): boolean {
|
||||
switch (code) {
|
||||
case $LPAREN:
|
||||
case $RPAREN:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidKeyframeBlockCharacter(code: number): boolean {
|
||||
return code == $PERCENT;
|
||||
}
|
||||
|
||||
function isValidAttributeSelectorCharacter(code: number): boolean {
|
||||
// value^*|$~=something
|
||||
switch (code) {
|
||||
case $$:
|
||||
case $PIPE:
|
||||
case $CARET:
|
||||
case $TILDA:
|
||||
case $STAR:
|
||||
case $EQ:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidSelectorCharacter(code: number): boolean {
|
||||
// selector [ key = value ]
|
||||
// IDENT C IDENT C IDENT C
|
||||
// #id, .class, *+~>
|
||||
// tag:PSEUDO
|
||||
switch (code) {
|
||||
case $HASH:
|
||||
case $PERIOD:
|
||||
case $TILDA:
|
||||
case $STAR:
|
||||
case $PLUS:
|
||||
case $GT:
|
||||
case $COLON:
|
||||
case $PIPE:
|
||||
case $COMMA:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidStyleBlockCharacter(code: number): boolean {
|
||||
// key:value;
|
||||
// key:calc(something ... )
|
||||
switch (code) {
|
||||
case $HASH:
|
||||
case $SEMICOLON:
|
||||
case $COLON:
|
||||
case $PERCENT:
|
||||
case $SLASH:
|
||||
case $BACKSLASH:
|
||||
case $BANG:
|
||||
case $PERIOD:
|
||||
case $LPAREN:
|
||||
case $RPAREN:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidMediaQueryRuleCharacter(code: number): boolean {
|
||||
// (min-width: 7.5em) and (orientation: landscape)
|
||||
switch (code) {
|
||||
case $LPAREN:
|
||||
case $RPAREN:
|
||||
case $COLON:
|
||||
case $PERCENT:
|
||||
case $PERIOD:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidAtRuleCharacter(code: number): boolean {
|
||||
// @document url(http://www.w3.org/page?something=on#hash),
|
||||
switch (code) {
|
||||
case $LPAREN:
|
||||
case $RPAREN:
|
||||
case $COLON:
|
||||
case $PERCENT:
|
||||
case $PERIOD:
|
||||
case $SLASH:
|
||||
case $BACKSLASH:
|
||||
case $HASH:
|
||||
case $EQ:
|
||||
case $QUESTION:
|
||||
case $AMPERSAND:
|
||||
case $STAR:
|
||||
case $COMMA:
|
||||
case $MINUS:
|
||||
case $PLUS:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidStyleFunctionCharacter(code: number): boolean {
|
||||
switch (code) {
|
||||
case $PERIOD:
|
||||
case $MINUS:
|
||||
case $PLUS:
|
||||
case $STAR:
|
||||
case $SLASH:
|
||||
case $LPAREN:
|
||||
case $RPAREN:
|
||||
case $COMMA:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidBlockCharacter(code: number): boolean {
|
||||
// @something { }
|
||||
// IDENT
|
||||
return code == $AT;
|
||||
}
|
||||
|
||||
function isValidCssCharacter(code: number, mode: CssLexerMode): boolean {
|
||||
switch (mode) {
|
||||
case CssLexerMode.ALL:
|
||||
case CssLexerMode.ALL_TRACK_WS:
|
||||
return true;
|
||||
|
||||
case CssLexerMode.SELECTOR:
|
||||
return isValidSelectorCharacter(code);
|
||||
|
||||
case CssLexerMode.PSEUDO_SELECTOR:
|
||||
return isValidPseudoSelectorCharacter(code);
|
||||
|
||||
case CssLexerMode.ATTRIBUTE_SELECTOR:
|
||||
return isValidAttributeSelectorCharacter(code);
|
||||
|
||||
case CssLexerMode.MEDIA_QUERY:
|
||||
return isValidMediaQueryRuleCharacter(code);
|
||||
|
||||
case CssLexerMode.AT_RULE_QUERY:
|
||||
return isValidAtRuleCharacter(code);
|
||||
|
||||
case CssLexerMode.KEYFRAME_BLOCK:
|
||||
return isValidKeyframeBlockCharacter(code);
|
||||
|
||||
case CssLexerMode.STYLE_BLOCK:
|
||||
case CssLexerMode.STYLE_VALUE:
|
||||
return isValidStyleBlockCharacter(code);
|
||||
|
||||
case CssLexerMode.STYLE_CALC_FUNCTION:
|
||||
return isValidStyleFunctionCharacter(code);
|
||||
|
||||
case CssLexerMode.BLOCK:
|
||||
return isValidBlockCharacter(code);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function charCode(input, index): number {
|
||||
return index >= input.length ? $EOF : StringWrapper.charCodeAt(input, index);
|
||||
}
|
||||
|
||||
function charStr(code: number): string {
|
||||
return StringWrapper.fromCharCode(code);
|
||||
}
|
||||
|
||||
export function isNewline(code): boolean {
|
||||
switch (code) {
|
||||
case $FF:
|
||||
case $CR:
|
||||
case $LF:
|
||||
case $VTAB:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
740
modules/@angular/compiler/src/css/parser.ts
Normal file
740
modules/@angular/compiler/src/css/parser.ts
Normal file
@ -0,0 +1,740 @@
|
||||
import {
|
||||
ParseSourceSpan,
|
||||
ParseSourceFile,
|
||||
ParseLocation,
|
||||
ParseError
|
||||
} from "angular2/src/compiler/parse_util";
|
||||
|
||||
import {
|
||||
bitWiseOr,
|
||||
bitWiseAnd,
|
||||
NumberWrapper,
|
||||
StringWrapper,
|
||||
isPresent
|
||||
} from "angular2/src/facade/lang";
|
||||
|
||||
import {
|
||||
CssLexerMode,
|
||||
CssToken,
|
||||
CssTokenType,
|
||||
CssScanner,
|
||||
CssScannerError,
|
||||
generateErrorMessage,
|
||||
$AT,
|
||||
$EOF,
|
||||
$RBRACE,
|
||||
$LBRACE,
|
||||
$LBRACKET,
|
||||
$RBRACKET,
|
||||
$LPAREN,
|
||||
$RPAREN,
|
||||
$COMMA,
|
||||
$COLON,
|
||||
$SEMICOLON,
|
||||
isNewline
|
||||
} from "angular2/src/compiler/css/lexer";
|
||||
|
||||
export {CssToken} from "angular2/src/compiler/css/lexer";
|
||||
|
||||
export enum BlockType {
|
||||
Import,
|
||||
Charset,
|
||||
Namespace,
|
||||
Supports,
|
||||
Keyframes,
|
||||
MediaQuery,
|
||||
Selector,
|
||||
FontFace,
|
||||
Page,
|
||||
Document,
|
||||
Viewport,
|
||||
Unsupported
|
||||
}
|
||||
|
||||
const EOF_DELIM = 1;
|
||||
const RBRACE_DELIM = 2;
|
||||
const LBRACE_DELIM = 4;
|
||||
const COMMA_DELIM = 8;
|
||||
const COLON_DELIM = 16;
|
||||
const SEMICOLON_DELIM = 32;
|
||||
const NEWLINE_DELIM = 64;
|
||||
const RPAREN_DELIM = 128;
|
||||
|
||||
function mergeTokens(tokens: CssToken[], separator: string = ""): CssToken {
|
||||
var mainToken = tokens[0];
|
||||
var str = mainToken.strValue;
|
||||
for (var i = 1; i < tokens.length; i++) {
|
||||
str += separator + tokens[i].strValue;
|
||||
}
|
||||
|
||||
return new CssToken(mainToken.index, mainToken.column, mainToken.line, mainToken.type, str);
|
||||
}
|
||||
|
||||
function getDelimFromToken(token: CssToken): number {
|
||||
return getDelimFromCharacter(token.numValue);
|
||||
}
|
||||
|
||||
function getDelimFromCharacter(code: number): number {
|
||||
switch (code) {
|
||||
case $EOF:
|
||||
return EOF_DELIM;
|
||||
case $COMMA:
|
||||
return COMMA_DELIM;
|
||||
case $COLON:
|
||||
return COLON_DELIM;
|
||||
case $SEMICOLON:
|
||||
return SEMICOLON_DELIM;
|
||||
case $RBRACE:
|
||||
return RBRACE_DELIM;
|
||||
case $LBRACE:
|
||||
return LBRACE_DELIM;
|
||||
case $RPAREN:
|
||||
return RPAREN_DELIM;
|
||||
default:
|
||||
return isNewline(code) ? NEWLINE_DELIM : 0;
|
||||
}
|
||||
}
|
||||
|
||||
function characterContainsDelimiter(code: number, delimiters: number): boolean {
|
||||
return bitWiseAnd([getDelimFromCharacter(code), delimiters]) > 0;
|
||||
}
|
||||
|
||||
export class CssAST {
|
||||
visit(visitor: CssASTVisitor, context?: any): void {}
|
||||
}
|
||||
|
||||
export interface CssASTVisitor {
|
||||
visitCssValue(ast: CssStyleValueAST, context?: any): void;
|
||||
visitInlineCssRule(ast: CssInlineRuleAST, context?: any): void;
|
||||
visitCssKeyframeRule(ast: CssKeyframeRuleAST, context?: any): void;
|
||||
visitCssKeyframeDefinition(ast: CssKeyframeDefinitionAST, context?: any): void;
|
||||
visitCssMediaQueryRule(ast: CssMediaQueryRuleAST, context?: any): void;
|
||||
visitCssSelectorRule(ast: CssSelectorRuleAST, context?: any): void;
|
||||
visitCssSelector(ast: CssSelectorAST, context?: any): void;
|
||||
visitCssDefinition(ast: CssDefinitionAST, context?: any): void;
|
||||
visitCssBlock(ast: CssBlockAST, context?: any): void;
|
||||
visitCssStyleSheet(ast: CssStyleSheetAST, context?: any): void;
|
||||
visitUnkownRule(ast: CssUnknownTokenListAST, context?: any): void;
|
||||
}
|
||||
|
||||
export class ParsedCssResult {
|
||||
constructor(public errors: CssParseError[], public ast: CssStyleSheetAST) {}
|
||||
}
|
||||
|
||||
export class CssParser {
|
||||
private _errors: CssParseError[] = [];
|
||||
private _file: ParseSourceFile;
|
||||
|
||||
constructor(private _scanner: CssScanner, private _fileName: string) {
|
||||
this._file = new ParseSourceFile(this._scanner.input, _fileName);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_resolveBlockType(token: CssToken): BlockType {
|
||||
switch (token.strValue) {
|
||||
case '@-o-keyframes':
|
||||
case '@-moz-keyframes':
|
||||
case '@-webkit-keyframes':
|
||||
case '@keyframes':
|
||||
return BlockType.Keyframes;
|
||||
|
||||
case '@charset':
|
||||
return BlockType.Charset;
|
||||
|
||||
case '@import':
|
||||
return BlockType.Import;
|
||||
|
||||
case '@namespace':
|
||||
return BlockType.Namespace;
|
||||
|
||||
case '@page':
|
||||
return BlockType.Page;
|
||||
|
||||
case '@document':
|
||||
return BlockType.Document;
|
||||
|
||||
case '@media':
|
||||
return BlockType.MediaQuery;
|
||||
|
||||
case '@font-face':
|
||||
return BlockType.FontFace;
|
||||
|
||||
case '@viewport':
|
||||
return BlockType.Viewport;
|
||||
|
||||
case '@supports':
|
||||
return BlockType.Supports;
|
||||
|
||||
default:
|
||||
return BlockType.Unsupported;
|
||||
}
|
||||
}
|
||||
|
||||
parse(): ParsedCssResult {
|
||||
var delimiters: number = EOF_DELIM;
|
||||
var ast = this._parseStyleSheet(delimiters);
|
||||
|
||||
var errors = this._errors;
|
||||
this._errors = [];
|
||||
|
||||
return new ParsedCssResult(errors, ast);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseStyleSheet(delimiters): CssStyleSheetAST {
|
||||
var results = [];
|
||||
this._scanner.consumeEmptyStatements();
|
||||
while (this._scanner.peek != $EOF) {
|
||||
this._scanner.setMode(CssLexerMode.BLOCK);
|
||||
results.push(this._parseRule(delimiters));
|
||||
}
|
||||
return new CssStyleSheetAST(results);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseRule(delimiters: number): CssRuleAST {
|
||||
if (this._scanner.peek == $AT) {
|
||||
return this._parseAtRule(delimiters);
|
||||
}
|
||||
return this._parseSelectorRule(delimiters);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseAtRule(delimiters: number): CssRuleAST {
|
||||
this._scanner.setMode(CssLexerMode.BLOCK);
|
||||
|
||||
var token = this._scan();
|
||||
|
||||
this._assertCondition(token.type == CssTokenType.AtKeyword,
|
||||
`The CSS Rule ${token.strValue} is not a valid [@] rule.`, token);
|
||||
|
||||
var block, type = this._resolveBlockType(token);
|
||||
switch (type) {
|
||||
case BlockType.Charset:
|
||||
case BlockType.Namespace:
|
||||
case BlockType.Import:
|
||||
var value = this._parseValue(delimiters);
|
||||
this._scanner.setMode(CssLexerMode.BLOCK);
|
||||
this._scanner.consumeEmptyStatements();
|
||||
return new CssInlineRuleAST(type, value);
|
||||
|
||||
case BlockType.Viewport:
|
||||
case BlockType.FontFace:
|
||||
block = this._parseStyleBlock(delimiters);
|
||||
return new CssBlockRuleAST(type, block);
|
||||
|
||||
case BlockType.Keyframes:
|
||||
var tokens = this._collectUntilDelim(bitWiseOr([delimiters, RBRACE_DELIM, LBRACE_DELIM]));
|
||||
// keyframes only have one identifier name
|
||||
var name = tokens[0];
|
||||
return new CssKeyframeRuleAST(name, this._parseKeyframeBlock(delimiters));
|
||||
|
||||
case BlockType.MediaQuery:
|
||||
this._scanner.setMode(CssLexerMode.MEDIA_QUERY);
|
||||
var tokens = this._collectUntilDelim(bitWiseOr([delimiters, RBRACE_DELIM, LBRACE_DELIM]));
|
||||
return new CssMediaQueryRuleAST(tokens, this._parseBlock(delimiters));
|
||||
|
||||
case BlockType.Document:
|
||||
case BlockType.Supports:
|
||||
case BlockType.Page:
|
||||
this._scanner.setMode(CssLexerMode.AT_RULE_QUERY);
|
||||
var tokens = this._collectUntilDelim(bitWiseOr([delimiters, RBRACE_DELIM, LBRACE_DELIM]));
|
||||
return new CssBlockDefinitionRuleAST(type, tokens, this._parseBlock(delimiters));
|
||||
|
||||
// if a custom @rule { ... } is used it should still tokenize the insides
|
||||
default:
|
||||
var listOfTokens = [];
|
||||
this._scanner.setMode(CssLexerMode.ALL);
|
||||
this._error(generateErrorMessage(
|
||||
this._scanner.input,
|
||||
`The CSS "at" rule "${token.strValue}" is not allowed to used here`,
|
||||
token.strValue, token.index, token.line, token.column),
|
||||
token);
|
||||
|
||||
this._collectUntilDelim(bitWiseOr([delimiters, LBRACE_DELIM, SEMICOLON_DELIM]))
|
||||
.forEach((token) => { listOfTokens.push(token); });
|
||||
if (this._scanner.peek == $LBRACE) {
|
||||
this._consume(CssTokenType.Character, '{');
|
||||
this._collectUntilDelim(bitWiseOr([delimiters, RBRACE_DELIM, LBRACE_DELIM]))
|
||||
.forEach((token) => { listOfTokens.push(token); });
|
||||
this._consume(CssTokenType.Character, '}');
|
||||
}
|
||||
return new CssUnknownTokenListAST(token, listOfTokens);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseSelectorRule(delimiters: number): CssSelectorRuleAST {
|
||||
var selectors = this._parseSelectors(delimiters);
|
||||
var block = this._parseStyleBlock(delimiters);
|
||||
this._scanner.setMode(CssLexerMode.BLOCK);
|
||||
this._scanner.consumeEmptyStatements();
|
||||
return new CssSelectorRuleAST(selectors, block);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseSelectors(delimiters: number): CssSelectorAST[] {
|
||||
delimiters = bitWiseOr([delimiters, LBRACE_DELIM]);
|
||||
|
||||
var selectors = [];
|
||||
var isParsingSelectors = true;
|
||||
while (isParsingSelectors) {
|
||||
selectors.push(this._parseSelector(delimiters));
|
||||
|
||||
isParsingSelectors = !characterContainsDelimiter(this._scanner.peek, delimiters);
|
||||
|
||||
if (isParsingSelectors) {
|
||||
this._consume(CssTokenType.Character, ',');
|
||||
isParsingSelectors = !characterContainsDelimiter(this._scanner.peek, delimiters);
|
||||
}
|
||||
}
|
||||
|
||||
return selectors;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_scan(): CssToken {
|
||||
var output = this._scanner.scan();
|
||||
var token = output.token;
|
||||
var error = output.error;
|
||||
if (isPresent(error)) {
|
||||
this._error(error.rawMessage, token);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_consume(type: CssTokenType, value: string = null): CssToken {
|
||||
var output = this._scanner.consume(type, value);
|
||||
var token = output.token;
|
||||
var error = output.error;
|
||||
if (isPresent(error)) {
|
||||
this._error(error.rawMessage, token);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseKeyframeBlock(delimiters: number): CssBlockAST {
|
||||
delimiters = bitWiseOr([delimiters, RBRACE_DELIM]);
|
||||
this._scanner.setMode(CssLexerMode.KEYFRAME_BLOCK);
|
||||
|
||||
this._consume(CssTokenType.Character, '{');
|
||||
|
||||
var definitions = [];
|
||||
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
|
||||
definitions.push(this._parseKeyframeDefinition(delimiters));
|
||||
}
|
||||
|
||||
this._consume(CssTokenType.Character, '}');
|
||||
|
||||
return new CssBlockAST(definitions);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseKeyframeDefinition(delimiters: number): CssKeyframeDefinitionAST {
|
||||
var stepTokens = [];
|
||||
delimiters = bitWiseOr([delimiters, LBRACE_DELIM]);
|
||||
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
|
||||
stepTokens.push(this._parseKeyframeLabel(bitWiseOr([delimiters, COMMA_DELIM])));
|
||||
if (this._scanner.peek != $LBRACE) {
|
||||
this._consume(CssTokenType.Character, ',');
|
||||
}
|
||||
}
|
||||
var styles = this._parseStyleBlock(bitWiseOr([delimiters, RBRACE_DELIM]));
|
||||
this._scanner.setMode(CssLexerMode.BLOCK);
|
||||
return new CssKeyframeDefinitionAST(stepTokens, styles);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseKeyframeLabel(delimiters: number): CssToken {
|
||||
this._scanner.setMode(CssLexerMode.KEYFRAME_BLOCK);
|
||||
return mergeTokens(this._collectUntilDelim(delimiters));
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseSelector(delimiters: number): CssSelectorAST {
|
||||
delimiters = bitWiseOr([delimiters, COMMA_DELIM, LBRACE_DELIM]);
|
||||
this._scanner.setMode(CssLexerMode.SELECTOR);
|
||||
|
||||
var selectorCssTokens = [];
|
||||
var isComplex = false;
|
||||
var wsCssToken;
|
||||
|
||||
var previousToken;
|
||||
var parenCount = 0;
|
||||
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
|
||||
var code = this._scanner.peek;
|
||||
switch (code) {
|
||||
case $LPAREN:
|
||||
parenCount++;
|
||||
break;
|
||||
|
||||
case $RPAREN:
|
||||
parenCount--;
|
||||
break;
|
||||
|
||||
case $COLON:
|
||||
this._scanner.setMode(CssLexerMode.PSEUDO_SELECTOR);
|
||||
previousToken = this._consume(CssTokenType.Character, ':');
|
||||
selectorCssTokens.push(previousToken);
|
||||
continue;
|
||||
|
||||
case $LBRACKET:
|
||||
// if we are already inside an attribute selector then we can't
|
||||
// jump into the mode again. Therefore this error will get picked
|
||||
// up when the scan method is called below.
|
||||
if (this._scanner.getMode() != CssLexerMode.ATTRIBUTE_SELECTOR) {
|
||||
selectorCssTokens.push(this._consume(CssTokenType.Character, '['));
|
||||
this._scanner.setMode(CssLexerMode.ATTRIBUTE_SELECTOR);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
|
||||
case $RBRACKET:
|
||||
selectorCssTokens.push(this._consume(CssTokenType.Character, ']'));
|
||||
this._scanner.setMode(CssLexerMode.SELECTOR);
|
||||
continue;
|
||||
}
|
||||
|
||||
var token = this._scan();
|
||||
|
||||
// special case for the ":not(" selector since it
|
||||
// contains an inner selector that needs to be parsed
|
||||
// in isolation
|
||||
if (this._scanner.getMode() == CssLexerMode.PSEUDO_SELECTOR && isPresent(previousToken) &&
|
||||
previousToken.numValue == $COLON && token.strValue == "not" &&
|
||||
this._scanner.peek == $LPAREN) {
|
||||
selectorCssTokens.push(token);
|
||||
selectorCssTokens.push(this._consume(CssTokenType.Character, '('));
|
||||
|
||||
// the inner selector inside of :not(...) can only be one
|
||||
// CSS selector (no commas allowed) therefore we parse only
|
||||
// one selector by calling the method below
|
||||
this._parseSelector(bitWiseOr([delimiters, RPAREN_DELIM]))
|
||||
.tokens.forEach(
|
||||
(innerSelectorToken) => { selectorCssTokens.push(innerSelectorToken); });
|
||||
|
||||
selectorCssTokens.push(this._consume(CssTokenType.Character, ')'));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
previousToken = token;
|
||||
|
||||
if (token.type == CssTokenType.Whitespace) {
|
||||
wsCssToken = token;
|
||||
} else {
|
||||
if (isPresent(wsCssToken)) {
|
||||
selectorCssTokens.push(wsCssToken);
|
||||
wsCssToken = null;
|
||||
isComplex = true;
|
||||
}
|
||||
selectorCssTokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
if (this._scanner.getMode() == CssLexerMode.ATTRIBUTE_SELECTOR) {
|
||||
this._error(
|
||||
`Unbalanced CSS attribute selector at column ${previousToken.line}:${previousToken.column}`,
|
||||
previousToken);
|
||||
} else if (parenCount > 0) {
|
||||
this._error(
|
||||
`Unbalanced pseudo selector function value at column ${previousToken.line}:${previousToken.column}`,
|
||||
previousToken);
|
||||
}
|
||||
|
||||
return new CssSelectorAST(selectorCssTokens, isComplex);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseValue(delimiters: number): CssStyleValueAST {
|
||||
delimiters = bitWiseOr([delimiters, RBRACE_DELIM, SEMICOLON_DELIM, NEWLINE_DELIM]);
|
||||
|
||||
this._scanner.setMode(CssLexerMode.STYLE_VALUE);
|
||||
|
||||
var strValue = "";
|
||||
var tokens = [];
|
||||
var previous: CssToken;
|
||||
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
|
||||
var token;
|
||||
if (isPresent(previous) && previous.type == CssTokenType.Identifier &&
|
||||
this._scanner.peek == $LPAREN) {
|
||||
token = this._consume(CssTokenType.Character, '(');
|
||||
tokens.push(token);
|
||||
strValue += token.strValue;
|
||||
|
||||
this._scanner.setMode(CssLexerMode.STYLE_VALUE_FUNCTION);
|
||||
|
||||
token = this._scan();
|
||||
tokens.push(token);
|
||||
strValue += token.strValue;
|
||||
|
||||
this._scanner.setMode(CssLexerMode.STYLE_VALUE);
|
||||
|
||||
token = this._consume(CssTokenType.Character, ')');
|
||||
tokens.push(token);
|
||||
strValue += token.strValue;
|
||||
} else {
|
||||
token = this._scan();
|
||||
if (token.type != CssTokenType.Whitespace) {
|
||||
tokens.push(token);
|
||||
}
|
||||
strValue += token.strValue;
|
||||
}
|
||||
|
||||
previous = token;
|
||||
}
|
||||
|
||||
this._scanner.consumeWhitespace();
|
||||
|
||||
var code = this._scanner.peek;
|
||||
if (code == $SEMICOLON) {
|
||||
this._consume(CssTokenType.Character, ';');
|
||||
} else if (code != $RBRACE) {
|
||||
this._error(
|
||||
generateErrorMessage(this._scanner.input,
|
||||
`The CSS key/value definition did not end with a semicolon`,
|
||||
previous.strValue, previous.index, previous.line, previous.column),
|
||||
previous);
|
||||
}
|
||||
|
||||
return new CssStyleValueAST(tokens, strValue);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_collectUntilDelim(delimiters: number, assertType: CssTokenType = null): CssToken[] {
|
||||
var tokens = [];
|
||||
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
|
||||
var val = isPresent(assertType) ? this._consume(assertType) : this._scan();
|
||||
tokens.push(val);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseBlock(delimiters: number): CssBlockAST {
|
||||
delimiters = bitWiseOr([delimiters, RBRACE_DELIM]);
|
||||
|
||||
this._scanner.setMode(CssLexerMode.BLOCK);
|
||||
|
||||
this._consume(CssTokenType.Character, '{');
|
||||
this._scanner.consumeEmptyStatements();
|
||||
|
||||
var results = [];
|
||||
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
|
||||
results.push(this._parseRule(delimiters));
|
||||
}
|
||||
|
||||
this._consume(CssTokenType.Character, '}');
|
||||
|
||||
this._scanner.setMode(CssLexerMode.BLOCK);
|
||||
this._scanner.consumeEmptyStatements();
|
||||
|
||||
return new CssBlockAST(results);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseStyleBlock(delimiters: number): CssBlockAST {
|
||||
delimiters = bitWiseOr([delimiters, RBRACE_DELIM, LBRACE_DELIM]);
|
||||
|
||||
this._scanner.setMode(CssLexerMode.STYLE_BLOCK);
|
||||
|
||||
this._consume(CssTokenType.Character, '{');
|
||||
this._scanner.consumeEmptyStatements();
|
||||
|
||||
var definitions = [];
|
||||
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
|
||||
definitions.push(this._parseDefinition(delimiters));
|
||||
this._scanner.consumeEmptyStatements();
|
||||
}
|
||||
|
||||
this._consume(CssTokenType.Character, '}');
|
||||
|
||||
this._scanner.setMode(CssLexerMode.STYLE_BLOCK);
|
||||
this._scanner.consumeEmptyStatements();
|
||||
|
||||
return new CssBlockAST(definitions);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseDefinition(delimiters: number): CssDefinitionAST {
|
||||
this._scanner.setMode(CssLexerMode.STYLE_BLOCK);
|
||||
|
||||
var prop = this._consume(CssTokenType.Identifier);
|
||||
var parseValue, value = null;
|
||||
|
||||
// the colon value separates the prop from the style.
|
||||
// there are a few cases as to what could happen if it
|
||||
// is missing
|
||||
switch (this._scanner.peek) {
|
||||
case $COLON:
|
||||
this._consume(CssTokenType.Character, ':');
|
||||
parseValue = true;
|
||||
break;
|
||||
|
||||
case $SEMICOLON:
|
||||
case $RBRACE:
|
||||
case $EOF:
|
||||
parseValue = false;
|
||||
break;
|
||||
|
||||
default:
|
||||
var propStr = [prop.strValue];
|
||||
if (this._scanner.peek != $COLON) {
|
||||
// this will throw the error
|
||||
var nextValue = this._consume(CssTokenType.Character, ':');
|
||||
propStr.push(nextValue.strValue);
|
||||
|
||||
var remainingTokens = this._collectUntilDelim(
|
||||
bitWiseOr([delimiters, COLON_DELIM, SEMICOLON_DELIM]), CssTokenType.Identifier);
|
||||
if (remainingTokens.length > 0) {
|
||||
remainingTokens.forEach((token) => { propStr.push(token.strValue); });
|
||||
}
|
||||
|
||||
prop = new CssToken(prop.index, prop.column, prop.line, prop.type, propStr.join(" "));
|
||||
}
|
||||
|
||||
// this means we've reached the end of the definition and/or block
|
||||
if (this._scanner.peek == $COLON) {
|
||||
this._consume(CssTokenType.Character, ':');
|
||||
parseValue = true;
|
||||
} else {
|
||||
parseValue = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (parseValue) {
|
||||
value = this._parseValue(delimiters);
|
||||
} else {
|
||||
this._error(generateErrorMessage(this._scanner.input,
|
||||
`The CSS property was not paired with a style value`,
|
||||
prop.strValue, prop.index, prop.line, prop.column),
|
||||
prop);
|
||||
}
|
||||
|
||||
return new CssDefinitionAST(prop, value);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_assertCondition(status: boolean, errorMessage: string, problemToken: CssToken): boolean {
|
||||
if (!status) {
|
||||
this._error(errorMessage, problemToken);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_error(message: string, problemToken: CssToken) {
|
||||
var length = problemToken.strValue.length;
|
||||
var error = CssParseError.create(this._file, 0, problemToken.line, problemToken.column, length,
|
||||
message);
|
||||
this._errors.push(error);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssStyleValueAST extends CssAST {
|
||||
constructor(public tokens: CssToken[], public strValue: string) { super(); }
|
||||
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssValue(this); }
|
||||
}
|
||||
|
||||
export class CssRuleAST extends CssAST {}
|
||||
|
||||
export class CssBlockRuleAST extends CssRuleAST {
|
||||
constructor(public type: BlockType, public block: CssBlockAST, public name: CssToken = null) {
|
||||
super();
|
||||
}
|
||||
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssBlock(this.block, context); }
|
||||
}
|
||||
|
||||
export class CssKeyframeRuleAST extends CssBlockRuleAST {
|
||||
constructor(name: CssToken, block: CssBlockAST) { super(BlockType.Keyframes, block, name); }
|
||||
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssKeyframeRule(this, context); }
|
||||
}
|
||||
|
||||
export class CssKeyframeDefinitionAST extends CssBlockRuleAST {
|
||||
public steps;
|
||||
constructor(_steps: CssToken[], block: CssBlockAST) {
|
||||
super(BlockType.Keyframes, block, mergeTokens(_steps, ","));
|
||||
this.steps = _steps;
|
||||
}
|
||||
visit(visitor: CssASTVisitor, context?: any) {
|
||||
visitor.visitCssKeyframeDefinition(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssBlockDefinitionRuleAST extends CssBlockRuleAST {
|
||||
public strValue: string;
|
||||
constructor(type: BlockType, public query: CssToken[], block: CssBlockAST) {
|
||||
super(type, block);
|
||||
this.strValue = query.map(token => token.strValue).join("");
|
||||
var firstCssToken: CssToken = query[0];
|
||||
this.name = new CssToken(firstCssToken.index, firstCssToken.column, firstCssToken.line,
|
||||
CssTokenType.Identifier, this.strValue);
|
||||
}
|
||||
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssBlock(this.block, context); }
|
||||
}
|
||||
|
||||
export class CssMediaQueryRuleAST extends CssBlockDefinitionRuleAST {
|
||||
constructor(query: CssToken[], block: CssBlockAST) { super(BlockType.MediaQuery, query, block); }
|
||||
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssMediaQueryRule(this, context); }
|
||||
}
|
||||
|
||||
export class CssInlineRuleAST extends CssRuleAST {
|
||||
constructor(public type: BlockType, public value: CssStyleValueAST) { super(); }
|
||||
visit(visitor: CssASTVisitor, context?: any) { visitor.visitInlineCssRule(this, context); }
|
||||
}
|
||||
|
||||
export class CssSelectorRuleAST extends CssBlockRuleAST {
|
||||
public strValue: string;
|
||||
|
||||
constructor(public selectors: CssSelectorAST[], block: CssBlockAST) {
|
||||
super(BlockType.Selector, block);
|
||||
this.strValue = selectors.map(selector => selector.strValue).join(",");
|
||||
}
|
||||
|
||||
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssSelectorRule(this, context); }
|
||||
}
|
||||
|
||||
export class CssDefinitionAST extends CssAST {
|
||||
constructor(public property: CssToken, public value: CssStyleValueAST) { super(); }
|
||||
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssDefinition(this, context); }
|
||||
}
|
||||
|
||||
export class CssSelectorAST extends CssAST {
|
||||
public strValue;
|
||||
constructor(public tokens: CssToken[], public isComplex: boolean = false) {
|
||||
super();
|
||||
this.strValue = tokens.map(token => token.strValue).join("");
|
||||
}
|
||||
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssSelector(this, context); }
|
||||
}
|
||||
|
||||
export class CssBlockAST extends CssAST {
|
||||
constructor(public entries: CssAST[]) { super(); }
|
||||
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssBlock(this, context); }
|
||||
}
|
||||
|
||||
export class CssStyleSheetAST extends CssAST {
|
||||
constructor(public rules: CssAST[]) { super(); }
|
||||
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssStyleSheet(this, context); }
|
||||
}
|
||||
|
||||
export class CssParseError extends ParseError {
|
||||
static create(file: ParseSourceFile, offset: number, line: number, col: number, length: number,
|
||||
errMsg: string): CssParseError {
|
||||
var start = new ParseLocation(file, offset, line, col);
|
||||
var end = new ParseLocation(file, offset, line, col + length);
|
||||
var span = new ParseSourceSpan(start, end);
|
||||
return new CssParseError(span, "CSS Parse Error: " + errMsg);
|
||||
}
|
||||
|
||||
constructor(span: ParseSourceSpan, message: string) { super(span, message); }
|
||||
}
|
||||
|
||||
export class CssUnknownTokenListAST extends CssRuleAST {
|
||||
constructor(public name, public tokens: CssToken[]) { super(); }
|
||||
visit(visitor: CssASTVisitor, context?: any) { visitor.visitUnkownRule(this, context); }
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
library angular2.src.core.compiler.directive_lifecycle_reflector;
|
||||
|
||||
import 'package:angular2/src/core/reflection/reflection.dart';
|
||||
import 'package:angular2/src/core/metadata/lifecycle_hooks.dart';
|
||||
|
||||
const INTERFACES = const {
|
||||
LifecycleHooks.OnInit: OnInit,
|
||||
LifecycleHooks.OnDestroy: OnDestroy,
|
||||
LifecycleHooks.DoCheck: DoCheck,
|
||||
LifecycleHooks.OnChanges: OnChanges,
|
||||
LifecycleHooks.AfterContentInit: AfterContentInit,
|
||||
LifecycleHooks.AfterContentChecked: AfterContentChecked,
|
||||
LifecycleHooks.AfterViewInit: AfterViewInit,
|
||||
LifecycleHooks.AfterViewChecked: AfterViewChecked,
|
||||
};
|
||||
|
||||
bool hasLifecycleHook(LifecycleHooks interface, token) {
|
||||
if (token is! Type) return false;
|
||||
Type interfaceType = INTERFACES[interface];
|
||||
return reflector.interfaces(token).contains(interfaceType);
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import {Type} from 'angular2/src/facade/lang';
|
||||
import {LifecycleHooks} from 'angular2/src/core/metadata/lifecycle_hooks';
|
||||
|
||||
export function hasLifecycleHook(lcInterface: LifecycleHooks, token): boolean {
|
||||
if (!(token instanceof Type)) return false;
|
||||
|
||||
var proto = (<any>token).prototype;
|
||||
|
||||
switch (lcInterface) {
|
||||
case LifecycleHooks.AfterContentInit:
|
||||
return !!proto.ngAfterContentInit;
|
||||
case LifecycleHooks.AfterContentChecked:
|
||||
return !!proto.ngAfterContentChecked;
|
||||
case LifecycleHooks.AfterViewInit:
|
||||
return !!proto.ngAfterViewInit;
|
||||
case LifecycleHooks.AfterViewChecked:
|
||||
return !!proto.ngAfterViewChecked;
|
||||
case LifecycleHooks.OnChanges:
|
||||
return !!proto.ngOnChanges;
|
||||
case LifecycleHooks.DoCheck:
|
||||
return !!proto.ngDoCheck;
|
||||
case LifecycleHooks.OnDestroy:
|
||||
return !!proto.ngOnDestroy;
|
||||
case LifecycleHooks.OnInit:
|
||||
return !!proto.ngOnInit;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
166
modules/@angular/compiler/src/directive_normalizer.ts
Normal file
166
modules/@angular/compiler/src/directive_normalizer.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import {
|
||||
CompileTypeMetadata,
|
||||
CompileDirectiveMetadata,
|
||||
CompileTemplateMetadata,
|
||||
CompileProviderMetadata,
|
||||
CompileTokenMetadata
|
||||
} from './compile_metadata';
|
||||
import {isPresent, isBlank, isArray} from 'angular2/src/facade/lang';
|
||||
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||
import {PromiseWrapper} from 'angular2/src/facade/async';
|
||||
|
||||
import {XHR} from 'angular2/src/compiler/xhr';
|
||||
import {UrlResolver} from 'angular2/src/compiler/url_resolver';
|
||||
import {extractStyleUrls, isStyleUrlResolvable} from './style_url_resolver';
|
||||
import {Injectable} from 'angular2/src/core/di';
|
||||
import {ViewEncapsulation} from 'angular2/src/core/metadata/view';
|
||||
|
||||
|
||||
import {
|
||||
HtmlAstVisitor,
|
||||
HtmlElementAst,
|
||||
HtmlTextAst,
|
||||
HtmlAttrAst,
|
||||
HtmlAst,
|
||||
HtmlCommentAst,
|
||||
HtmlExpansionAst,
|
||||
HtmlExpansionCaseAst,
|
||||
htmlVisitAll
|
||||
} from './html_ast';
|
||||
import {HtmlParser} from './html_parser';
|
||||
|
||||
import {preparseElement, PreparsedElement, PreparsedElementType} from './template_preparser';
|
||||
|
||||
@Injectable()
|
||||
export class DirectiveNormalizer {
|
||||
constructor(private _xhr: XHR, private _urlResolver: UrlResolver,
|
||||
private _htmlParser: HtmlParser) {}
|
||||
|
||||
normalizeDirective(directive: CompileDirectiveMetadata): Promise<CompileDirectiveMetadata> {
|
||||
if (!directive.isComponent) {
|
||||
// For non components there is nothing to be normalized yet.
|
||||
return PromiseWrapper.resolve(directive);
|
||||
}
|
||||
return this.normalizeTemplate(directive.type, directive.template)
|
||||
.then((normalizedTemplate: CompileTemplateMetadata) => new CompileDirectiveMetadata({
|
||||
type: directive.type,
|
||||
isComponent: directive.isComponent,
|
||||
selector: directive.selector,
|
||||
exportAs: directive.exportAs,
|
||||
changeDetection: directive.changeDetection,
|
||||
inputs: directive.inputs,
|
||||
outputs: directive.outputs,
|
||||
hostListeners: directive.hostListeners,
|
||||
hostProperties: directive.hostProperties,
|
||||
hostAttributes: directive.hostAttributes,
|
||||
lifecycleHooks: directive.lifecycleHooks,
|
||||
providers: directive.providers,
|
||||
viewProviders: directive.viewProviders,
|
||||
queries: directive.queries,
|
||||
viewQueries: directive.viewQueries,
|
||||
template: normalizedTemplate
|
||||
}));
|
||||
}
|
||||
|
||||
normalizeTemplate(directiveType: CompileTypeMetadata,
|
||||
template: CompileTemplateMetadata): Promise<CompileTemplateMetadata> {
|
||||
if (isPresent(template.template)) {
|
||||
return PromiseWrapper.resolve(this.normalizeLoadedTemplate(
|
||||
directiveType, template, template.template, template.baseUrl));
|
||||
} else if (isPresent(template.templateUrl)) {
|
||||
var sourceAbsUrl = this._urlResolver.resolve(template.baseUrl, template.templateUrl);
|
||||
return this._xhr.get(sourceAbsUrl)
|
||||
.then(templateContent => this.normalizeLoadedTemplate(directiveType, template,
|
||||
templateContent, sourceAbsUrl));
|
||||
} else {
|
||||
throw new BaseException(`No template specified for component ${directiveType.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
normalizeLoadedTemplate(directiveType: CompileTypeMetadata, templateMeta: CompileTemplateMetadata,
|
||||
template: string, templateAbsUrl: string): CompileTemplateMetadata {
|
||||
var rootNodesAndErrors = this._htmlParser.parse(template, directiveType.name);
|
||||
if (rootNodesAndErrors.errors.length > 0) {
|
||||
var errorString = rootNodesAndErrors.errors.join('\n');
|
||||
throw new BaseException(`Template parse errors:\n${errorString}`);
|
||||
}
|
||||
|
||||
var visitor = new TemplatePreparseVisitor();
|
||||
htmlVisitAll(visitor, rootNodesAndErrors.rootNodes);
|
||||
var allStyles = templateMeta.styles.concat(visitor.styles);
|
||||
|
||||
var allStyleAbsUrls =
|
||||
visitor.styleUrls.filter(isStyleUrlResolvable)
|
||||
.map(url => this._urlResolver.resolve(templateAbsUrl, url))
|
||||
.concat(templateMeta.styleUrls.filter(isStyleUrlResolvable)
|
||||
.map(url => this._urlResolver.resolve(templateMeta.baseUrl, url)));
|
||||
|
||||
var allResolvedStyles = allStyles.map(style => {
|
||||
var styleWithImports = extractStyleUrls(this._urlResolver, templateAbsUrl, style);
|
||||
styleWithImports.styleUrls.forEach(styleUrl => allStyleAbsUrls.push(styleUrl));
|
||||
return styleWithImports.style;
|
||||
});
|
||||
|
||||
var encapsulation = templateMeta.encapsulation;
|
||||
if (encapsulation === ViewEncapsulation.Emulated && allResolvedStyles.length === 0 &&
|
||||
allStyleAbsUrls.length === 0) {
|
||||
encapsulation = ViewEncapsulation.None;
|
||||
}
|
||||
return new CompileTemplateMetadata({
|
||||
encapsulation: encapsulation,
|
||||
template: template,
|
||||
templateUrl: templateAbsUrl,
|
||||
styles: allResolvedStyles,
|
||||
styleUrls: allStyleAbsUrls,
|
||||
ngContentSelectors: visitor.ngContentSelectors
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class TemplatePreparseVisitor implements HtmlAstVisitor {
|
||||
ngContentSelectors: string[] = [];
|
||||
styles: string[] = [];
|
||||
styleUrls: string[] = [];
|
||||
ngNonBindableStackCount: number = 0;
|
||||
|
||||
visitElement(ast: HtmlElementAst, context: any): any {
|
||||
var preparsedElement = preparseElement(ast);
|
||||
switch (preparsedElement.type) {
|
||||
case PreparsedElementType.NG_CONTENT:
|
||||
if (this.ngNonBindableStackCount === 0) {
|
||||
this.ngContentSelectors.push(preparsedElement.selectAttr);
|
||||
}
|
||||
break;
|
||||
case PreparsedElementType.STYLE:
|
||||
var textContent = '';
|
||||
ast.children.forEach(child => {
|
||||
if (child instanceof HtmlTextAst) {
|
||||
textContent += (<HtmlTextAst>child).value;
|
||||
}
|
||||
});
|
||||
this.styles.push(textContent);
|
||||
break;
|
||||
case PreparsedElementType.STYLESHEET:
|
||||
this.styleUrls.push(preparsedElement.hrefAttr);
|
||||
break;
|
||||
default:
|
||||
// DDC reports this as error. See:
|
||||
// https://github.com/dart-lang/dev_compiler/issues/428
|
||||
break;
|
||||
}
|
||||
if (preparsedElement.nonBindable) {
|
||||
this.ngNonBindableStackCount++;
|
||||
}
|
||||
htmlVisitAll(this, ast.children);
|
||||
if (preparsedElement.nonBindable) {
|
||||
this.ngNonBindableStackCount--;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
visitComment(ast: HtmlCommentAst, context: any): any { return null; }
|
||||
visitAttr(ast: HtmlAttrAst, context: any): any { return null; }
|
||||
visitText(ast: HtmlTextAst, context: any): any { return null; }
|
||||
visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
|
||||
|
||||
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
|
||||
}
|
169
modules/@angular/compiler/src/directive_resolver.ts
Normal file
169
modules/@angular/compiler/src/directive_resolver.ts
Normal file
@ -0,0 +1,169 @@
|
||||
import {resolveForwardRef, Injectable} from 'angular2/src/core/di';
|
||||
import {Type, isPresent, isBlank, stringify} from 'angular2/src/facade/lang';
|
||||
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
import {
|
||||
DirectiveMetadata,
|
||||
ComponentMetadata,
|
||||
InputMetadata,
|
||||
OutputMetadata,
|
||||
HostBindingMetadata,
|
||||
HostListenerMetadata,
|
||||
ContentChildrenMetadata,
|
||||
ViewChildrenMetadata,
|
||||
ContentChildMetadata,
|
||||
ViewChildMetadata
|
||||
} from 'angular2/src/core/metadata';
|
||||
import {reflector} from 'angular2/src/core/reflection/reflection';
|
||||
import {ReflectorReader} from 'angular2/src/core/reflection/reflector_reader';
|
||||
|
||||
function _isDirectiveMetadata(type: any): boolean {
|
||||
return type instanceof DirectiveMetadata;
|
||||
}
|
||||
|
||||
/*
|
||||
* Resolve a `Type` for {@link DirectiveMetadata}.
|
||||
*
|
||||
* This interface can be overridden by the application developer to create custom behavior.
|
||||
*
|
||||
* See {@link Compiler}
|
||||
*/
|
||||
@Injectable()
|
||||
export class DirectiveResolver {
|
||||
private _reflector: ReflectorReader;
|
||||
|
||||
constructor(_reflector?: ReflectorReader) {
|
||||
if (isPresent(_reflector)) {
|
||||
this._reflector = _reflector;
|
||||
} else {
|
||||
this._reflector = reflector;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@link DirectiveMetadata} for a given `Type`.
|
||||
*/
|
||||
resolve(type: Type): DirectiveMetadata {
|
||||
var typeMetadata = this._reflector.annotations(resolveForwardRef(type));
|
||||
if (isPresent(typeMetadata)) {
|
||||
var metadata = typeMetadata.find(_isDirectiveMetadata);
|
||||
if (isPresent(metadata)) {
|
||||
var propertyMetadata = this._reflector.propMetadata(type);
|
||||
return this._mergeWithPropertyMetadata(metadata, propertyMetadata, type);
|
||||
}
|
||||
}
|
||||
|
||||
throw new BaseException(`No Directive annotation found on ${stringify(type)}`);
|
||||
}
|
||||
|
||||
private _mergeWithPropertyMetadata(dm: DirectiveMetadata,
|
||||
propertyMetadata: {[key: string]: any[]},
|
||||
directiveType: Type): DirectiveMetadata {
|
||||
var inputs = [];
|
||||
var outputs = [];
|
||||
var host: {[key: string]: string} = {};
|
||||
var queries: {[key: string]: any} = {};
|
||||
|
||||
StringMapWrapper.forEach(propertyMetadata, (metadata: any[], propName: string) => {
|
||||
metadata.forEach(a => {
|
||||
if (a instanceof InputMetadata) {
|
||||
if (isPresent(a.bindingPropertyName)) {
|
||||
inputs.push(`${propName}: ${a.bindingPropertyName}`);
|
||||
} else {
|
||||
inputs.push(propName);
|
||||
}
|
||||
}
|
||||
|
||||
if (a instanceof OutputMetadata) {
|
||||
if (isPresent(a.bindingPropertyName)) {
|
||||
outputs.push(`${propName}: ${a.bindingPropertyName}`);
|
||||
} else {
|
||||
outputs.push(propName);
|
||||
}
|
||||
}
|
||||
|
||||
if (a instanceof HostBindingMetadata) {
|
||||
if (isPresent(a.hostPropertyName)) {
|
||||
host[`[${a.hostPropertyName}]`] = propName;
|
||||
} else {
|
||||
host[`[${propName}]`] = propName;
|
||||
}
|
||||
}
|
||||
|
||||
if (a instanceof HostListenerMetadata) {
|
||||
var args = isPresent(a.args) ? (<any[]>a.args).join(', ') : '';
|
||||
host[`(${a.eventName})`] = `${propName}(${args})`;
|
||||
}
|
||||
|
||||
if (a instanceof ContentChildrenMetadata) {
|
||||
queries[propName] = a;
|
||||
}
|
||||
|
||||
if (a instanceof ViewChildrenMetadata) {
|
||||
queries[propName] = a;
|
||||
}
|
||||
|
||||
if (a instanceof ContentChildMetadata) {
|
||||
queries[propName] = a;
|
||||
}
|
||||
|
||||
if (a instanceof ViewChildMetadata) {
|
||||
queries[propName] = a;
|
||||
}
|
||||
});
|
||||
});
|
||||
return this._merge(dm, inputs, outputs, host, queries, directiveType);
|
||||
}
|
||||
|
||||
private _merge(dm: DirectiveMetadata, inputs: string[], outputs: string[],
|
||||
host: {[key: string]: string}, queries: {[key: string]: any},
|
||||
directiveType: Type): DirectiveMetadata {
|
||||
var mergedInputs = isPresent(dm.inputs) ? ListWrapper.concat(dm.inputs, inputs) : inputs;
|
||||
|
||||
var mergedOutputs;
|
||||
if (isPresent(dm.outputs)) {
|
||||
dm.outputs.forEach((propName: string) => {
|
||||
if (ListWrapper.contains(outputs, propName)) {
|
||||
throw new BaseException(
|
||||
`Output event '${propName}' defined multiple times in '${stringify(directiveType)}'`);
|
||||
}
|
||||
});
|
||||
mergedOutputs = ListWrapper.concat(dm.outputs, outputs);
|
||||
} else {
|
||||
mergedOutputs = outputs;
|
||||
}
|
||||
|
||||
var mergedHost = isPresent(dm.host) ? StringMapWrapper.merge(dm.host, host) : host;
|
||||
var mergedQueries =
|
||||
isPresent(dm.queries) ? StringMapWrapper.merge(dm.queries, queries) : queries;
|
||||
|
||||
if (dm instanceof ComponentMetadata) {
|
||||
return new ComponentMetadata({
|
||||
selector: dm.selector,
|
||||
inputs: mergedInputs,
|
||||
outputs: mergedOutputs,
|
||||
host: mergedHost,
|
||||
exportAs: dm.exportAs,
|
||||
moduleId: dm.moduleId,
|
||||
queries: mergedQueries,
|
||||
changeDetection: dm.changeDetection,
|
||||
providers: dm.providers,
|
||||
viewProviders: dm.viewProviders
|
||||
});
|
||||
|
||||
} else {
|
||||
return new DirectiveMetadata({
|
||||
selector: dm.selector,
|
||||
inputs: mergedInputs,
|
||||
outputs: mergedOutputs,
|
||||
host: mergedHost,
|
||||
exportAs: dm.exportAs,
|
||||
queries: mergedQueries,
|
||||
providers: dm.providers
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export var CODEGEN_DIRECTIVE_RESOLVER = new DirectiveResolver(reflector);
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user