refactor(core): move directives, pipes, and forms into common

BREAKING CHANGE

All private exports from 'angular2/src/core/{directives,pipes,forms}' should be replaced with 'angular2/src/common/{directives,pipes,formis}'

Closes #5153
This commit is contained in:
vsavkin
2015-11-05 14:58:24 -08:00
committed by Victor Savkin
parent 6edd964a83
commit 695923dcd6
82 changed files with 166 additions and 147 deletions
@@ -0,0 +1,49 @@
import {CONST_EXPR, Type} from 'angular2/src/core/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` or `@View` decorators.
*
* ### Example
*
* Instead of writing:
*
* ```typescript
* import {NgClass, NgIf, NgFor, NgSwitch, NgSwitchWhen, NgSwitchDefault, NgModel, NgForm} from
* 'angular2/angular2';
* 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/angular2';
* import {OtherDirective} from './myDirectives';
*
* @Component({
* selector: 'my-component',
* templateUrl: 'myComponent.html',
* directives: [COMMON_DIRECTIVES, OtherDirective]
* })
* export class MyComponent {
* ...
* }
* ```
*/
export const COMMON_DIRECTIVES: Type[][] = CONST_EXPR([CORE_DIRECTIVES, FORM_DIRECTIVES]);
+12
View File
@@ -0,0 +1,12 @@
/**
* @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 {NgStyle} from './directives/ng_style';
export {NgSwitch, NgSwitchWhen, NgSwitchDefault} from './directives/ng_switch';
export * from './directives/observable_list_diff';
export {CORE_DIRECTIVES} from './directives/core_directives';
@@ -0,0 +1,49 @@
import {CONST_EXPR, Type} from 'angular2/src/core/facade/lang';
import {NgClass} from './ng_class';
import {NgFor} from './ng_for';
import {NgIf} from './ng_if';
import {NgStyle} from './ng_style';
import {NgSwitch, NgSwitchWhen, NgSwitchDefault} from './ng_switch';
/**
* 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 `@View` annotation.
*
* ### Example ([live demo](http://plnkr.co/edit/yakGwpCdUkg0qfzX5m8g?p=preview))
*
* Instead of writing:
*
* ```typescript
* import {NgClass, NgIf, NgFor, NgSwitch, NgSwitchWhen, NgSwitchDefault} from 'angular2/angular2';
* 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/angular2';
* import {OtherDirective} from './myDirectives';
*
* @Component({
* selector: 'my-component',
* templateUrl: 'myComponent.html',
* directives: [CORE_DIRECTIVES, OtherDirective]
* })
* export class MyComponent {
* ...
* }
* ```
*/
export const CORE_DIRECTIVES: Type[] =
CONST_EXPR([NgClass, NgFor, NgIf, NgStyle, NgSwitch, NgSwitchWhen, NgSwitchDefault]);
@@ -0,0 +1,170 @@
import {isPresent, isString, StringWrapper, isBlank, isArray} from 'angular2/src/core/facade/lang';
import {DoCheck, OnDestroy} from 'angular2/lifecycle_hooks';
import {Directive} from 'angular2/src/core/metadata';
import {ElementRef} from 'angular2/src/core/linker';
import {
IterableDiffer,
IterableDiffers,
KeyValueDiffer,
KeyValueDiffers
} from 'angular2/src/core/change_detection';
import {Renderer} from 'angular2/src/core/render';
import {StringMapWrapper, isListLikeIterable} from 'angular2/src/core/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, NgClass} from 'angular2/angular2';
*
* @Component({
* selector: 'toggle-button',
* inputs: ['isDisabled'],
* template: `
* <div class="button" [ng-class]="{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: '[ng-class]', inputs: ['rawClass: ng-class', 'initialClasses: class']})
export class NgClass implements DoCheck, OnDestroy {
private _differ: any;
private _mode: string;
private _initialClasses = [];
private _rawClass;
constructor(private _iterableDiffers: IterableDiffers, private _keyValueDiffers: KeyValueDiffers,
private _ngEl: ElementRef, private _renderer: Renderer) {}
set initialClasses(v) {
this._applyInitialClasses(true);
this._initialClasses = isPresent(v) && isString(v) ? v.split(' ') : [];
this._applyInitialClasses(false);
this._applyClasses(this._rawClass, false);
}
set rawClass(v) {
this._cleanupClasses(this._rawClass);
if (isString(v)) {
v = v.split(' ');
}
this._rawClass = v;
if (isPresent(v)) {
if (isListLikeIterable(v)) {
this._differ = this._iterableDiffers.find(v).create(null);
this._mode = 'iterable';
} else {
this._differ = this._keyValueDiffers.find(v).create(null);
this._mode = 'keyValue';
}
} else {
this._differ = null;
}
}
doCheck(): void {
if (isPresent(this._differ)) {
var changes = this._differ.diff(this._rawClass);
if (isPresent(changes)) {
if (this._mode == 'iterable') {
this._applyIterableChanges(changes);
} else {
this._applyKeyValueChanges(changes);
}
}
}
}
onDestroy(): void { this._cleanupClasses(this._rawClass); }
private _cleanupClasses(rawClassVal): void {
this._applyClasses(rawClassVal, true);
this._applyInitialClasses(false);
}
private _applyKeyValueChanges(changes: any): void {
changes.forEachAddedItem((record) => { this._toggleClass(record.key, record.currentValue); });
changes.forEachChangedItem((record) => { this._toggleClass(record.key, record.currentValue); });
changes.forEachRemovedItem((record) => {
if (record.previousValue) {
this._toggleClass(record.key, false);
}
});
}
private _applyIterableChanges(changes: any): void {
changes.forEachAddedItem((record) => { this._toggleClass(record.item, true); });
changes.forEachRemovedItem((record) => { 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]: string},
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]: string}>rawClassVal, (expVal, className) => {
if (expVal) this._toggleClass(className, !isCleanup);
});
}
}
}
private _toggleClass(className: string, enabled): void {
className = className.trim();
if (className.length > 0) {
this._renderer.setElementClass(this._ngEl, className, enabled);
}
}
}
@@ -0,0 +1,156 @@
import {DoCheck} from 'angular2/lifecycle_hooks';
import {Directive} from 'angular2/src/core/metadata';
import {
ChangeDetectorRef,
IterableDiffer,
IterableDiffers
} from 'angular2/src/core/change_detection';
import {ViewContainerRef, TemplateRef, ViewRef} from 'angular2/src/core/linker';
import {isPresent, isBlank} from 'angular2/src/core/facade/lang';
/**
* 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.
* * `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 *ng-for="#item of items; #i = index">...</li>`
* - `<li template="ng-for #item of items; #i = index">...</li>`
* - `<template ng-for #item [ng-for-of]="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: '[ng-for][ng-for-of]', inputs: ['ngForOf', 'ngForTemplate']})
export class NgFor implements DoCheck {
/** @internal */
_ngForOf: any;
private _differ: IterableDiffer;
constructor(private _viewContainer: ViewContainerRef, private _templateRef: TemplateRef,
private _iterableDiffers: IterableDiffers, private _cdr: ChangeDetectorRef) {}
set ngForOf(value: any) {
this._ngForOf = value;
if (isBlank(this._differ) && isPresent(value)) {
this._differ = this._iterableDiffers.find(value).create(this._cdr);
}
}
set ngForTemplate(value: TemplateRef) { this._templateRef = value; }
doCheck() {
if (isPresent(this._differ)) {
var changes = this._differ.diff(this._ngForOf);
if (isPresent(changes)) this._applyChanges(changes);
}
}
private _applyChanges(changes) {
// TODO(rado): check if change detection can produce a change record that is
// easier to consume than current.
var recordViewTuples = [];
changes.forEachRemovedItem((removedRecord) =>
recordViewTuples.push(new RecordViewTuple(removedRecord, null)));
changes.forEachMovedItem((movedRecord) =>
recordViewTuples.push(new RecordViewTuple(movedRecord, null)));
var insertTuples = this._bulkRemove(recordViewTuples);
changes.forEachAddedItem((addedRecord) =>
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++) {
this._viewContainer.get(i).setLocal('last', i === ilen - 1);
}
}
private _perViewChange(view, record) {
view.setLocal('\$implicit', record.item);
view.setLocal('index', record.currentIndex);
view.setLocal('even', (record.currentIndex % 2 == 0));
view.setLocal('odd', (record.currentIndex % 2 == 1));
}
private _bulkRemove(tuples: RecordViewTuple[]): RecordViewTuple[] {
tuples.sort((a, b) => a.record.previousIndex - b.record.previousIndex);
var movedTuples = [];
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 = 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, tuple.record.currentIndex);
}
}
return tuples;
}
}
class RecordViewTuple {
view: ViewRef;
record: any;
constructor(record, view) {
this.record = record;
this.view = view;
}
}
@@ -0,0 +1,42 @@
import {Directive} from 'angular2/src/core/metadata';
import {ViewContainerRef, TemplateRef} from 'angular2/src/core/linker';
import {isBlank} from 'angular2/src/core/facade/lang';
/**
* Removes or recreates a portion of the DOM tree based on an {expression}.
*
* If the expression assigned to `ng-if` 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 *ng-if="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 *ng-if="condition">...</div>`
* - `<div template="ng-if condition">...</div>`
* - `<template [ng-if]="condition"><div>...</div></template>`
*/
@Directive({selector: '[ng-if]', inputs: ['ngIf']})
export class NgIf {
private _prevCondition: boolean = null;
constructor(private _viewContainer: ViewContainerRef, private _templateRef: TemplateRef) {}
set ngIf(newCondition /* 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();
}
}
}
@@ -0,0 +1,96 @@
import {DoCheck} from 'angular2/lifecycle_hooks';
import {
KeyValueDiffer,
KeyValueDiffers,
} from 'angular2/src/core/change_detection';
import {ElementRef} from 'angular2/src/core/linker';
import {Directive} from 'angular2/src/core/metadata';
import {Renderer} from 'angular2/src/core/render';
import {isPresent, isBlank, print} from 'angular2/src/core/facade/lang';
/**
* The `NgStyle` directive changes styles based on a result of expression evaluation.
*
* An expression assigned to the `ng-style` 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 [ng-style]="{'font-style': style}"></div>`
* - `<div [ng-style]="styleExp"></div>` - here the `styleExp` must evaluate to an object
*
* ### Example ([live demo](http://plnkr.co/edit/YamGS6GkUh9GqWNQhCyM?p=preview)):
*
* ```
* import {Component, NgStyle} from 'angular2/angular2';
*
* @Component({
* selector: 'ng-style-example',
* template: `
* <h1 [ng-style]="{'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: '[ng-style]', inputs: ['rawStyle: ng-style']})
export class NgStyle implements DoCheck {
/** @internal */
_rawStyle;
/** @internal */
_differ: KeyValueDiffer;
constructor(private _differs: KeyValueDiffers, private _ngEl: ElementRef,
private _renderer: Renderer) {}
set rawStyle(v) {
this._rawStyle = v;
if (isBlank(this._differ) && isPresent(v)) {
this._differ = this._differs.find(this._rawStyle).create(null);
}
}
doCheck() {
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) => { this._setStyle(record.key, record.currentValue); });
changes.forEachChangedItem((record) => { this._setStyle(record.key, record.currentValue); });
changes.forEachRemovedItem((record) => { this._setStyle(record.key, null); });
}
private _setStyle(name: string, val: string): void {
this._renderer.setElementStyle(this._ngEl, name, val);
}
}
@@ -0,0 +1,183 @@
import {Directive} from 'angular2/src/core/metadata';
import {Host} from 'angular2/src/core/di';
import {ViewContainerRef, TemplateRef} from 'angular2/src/core/linker';
import {isPresent, isBlank, normalizeBlank, CONST_EXPR} from 'angular2/src/core/facade/lang';
import {ListWrapper, Map} from 'angular2/src/core/facade/collection';
const _WHEN_DEFAULT = CONST_EXPR(new Object());
export class SwitchView {
constructor(private _viewContainerRef: ViewContainerRef, private _templateRef: TemplateRef) {}
create(): void { this._viewContainerRef.createEmbeddedView(this._templateRef); }
destroy(): void { this._viewContainerRef.clear(); }
}
/**
* The `NgSwitch` directive is used to conditionally swap DOM structure on your template based on a
* scope expression.
* Elements within `NgSwitch` but without `NgSwitchWhen` or `NgSwitchDefault` directives will be
* preserved at the location as specified in the template.
*
* `NgSwitch` simply chooses nested elements and makes them visible based on which element matches
* the value obtained from the evaluated expression. In other words, you define a container element
* (where you place the directive), place an expression on the **`[ng-switch]="..."` attribute**),
* define any inner elements inside of the directive and place a `[ng-switch-when]` attribute per
* element.
* The when attribute is used to inform NgSwitch which element to display when the expression is
* evaluated. If a matching expression is not found via a when attribute then an element with the
* default attribute is displayed.
*
* ### Example
*
* ```
* <ANY [ng-switch]="expression">
* <template [ng-switch-when]="whenExpression1">...</template>
* <template [ng-switch-when]="whenExpression1">...</template>
* <template ng-switch-default>...</template>
* </ANY>
* ```
*/
@Directive({selector: '[ng-switch]', inputs: ['ngSwitch']})
export class NgSwitch {
private _switchValue: any;
private _useDefault: boolean = false;
private _valueViews = new Map<any, SwitchView[]>();
private _activeViews: SwitchView[] = [];
set ngSwitch(value) {
// 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, newWhen, 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, view: SwitchView): void {
var views = this._valueViews.get(value);
if (isBlank(views)) {
views = [];
this._valueViews.set(value, views);
}
views.push(view);
}
/** @internal */
_deregisterView(value, 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);
}
}
}
/**
* Defines a case statement as an expression.
*
* If multiple `NgSwitchWhen` match the `NgSwitch` value, all of them are displayed.
*
* Example:
*
* ```
* // match against a context variable
* <template [ng-switch-when]="contextVariable">...</template>
*
* // match against a constant string
* <template ng-switch-when="stringValue">...</template>
* ```
*/
@Directive({selector: '[ng-switch-when]', 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;
constructor(viewContainer: ViewContainerRef, templateRef: TemplateRef,
@Host() private _switch: NgSwitch) {
this._view = new SwitchView(viewContainer, templateRef);
}
set ngSwitchWhen(value) {
this._switch._onWhenValueChanged(this._value, value, this._view);
this._value = value;
}
}
/**
* Defines a default case statement.
*
* Default case statements are displayed when no `NgSwitchWhen` match the `ng-switch` value.
*
* Example:
*
* ```
* <template ng-switch-default>...</template>
* ```
*/
@Directive({selector: '[ng-switch-default]'})
export class NgSwitchDefault {
constructor(viewContainer: ViewContainerRef, templateRef: TemplateRef,
@Host() sswitch: NgSwitch) {
sswitch._registerView(_WHEN_DEFAULT, new SwitchView(viewContainer, 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;
}
}
dynamic 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) {
return new ObservableListDiff(cdRef);
}
}
@@ -0,0 +1,5 @@
// TS does not have Observables
// I need to be here to make TypeScript think this is a module.
import {} from 'angular2/src/core/facade/lang';
export var workaround_empty_observable_list_diff: any;
+42
View File
@@ -0,0 +1,42 @@
/**
* @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} from './forms/directives';
export {NG_VALIDATORS, NG_ASYNC_VALIDATORS, Validators} from './forms/validators';
export {
RequiredValidator,
MinLengthValidator,
MaxLengthValidator,
Validator
} from './forms/directives/validators';
export {FormBuilder, FORM_PROVIDERS, FORM_BINDINGS} from './forms/form_builder';
@@ -0,0 +1,71 @@
import {Type, CONST_EXPR} from 'angular2/src/core/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 {NgControlStatus} from './directives/ng_control_status';
import {
SelectControlValueAccessor,
NgSelectOption
} from './directives/select_control_value_accessor';
import {RequiredValidator, MinLengthValidator, MaxLengthValidator} 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 {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} 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 `@View` 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[] = CONST_EXPR([
NgControlName,
NgControlGroup,
NgFormControl,
NgModel,
NgFormModel,
NgForm,
NgSelectOption,
DefaultValueAccessor,
NumberValueAccessor,
CheckboxControlValueAccessor,
SelectControlValueAccessor,
NgControlStatus,
RequiredValidator,
MinLengthValidator,
MaxLengthValidator
]);
@@ -0,0 +1,32 @@
import {AbstractControl} from '../model';
import {isPresent} from 'angular2/src/core/facade/lang';
import {unimplemented} from 'angular2/src/core/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 controlsErrors(): any { return isPresent(this.control) ? this.control.controlsErrors : 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} from 'angular2/src/core/metadata';
import {Renderer} from 'angular2/src/core/render';
import {ElementRef} from 'angular2/src/core/linker';
import {Self, forwardRef, Provider} from 'angular2/src/core/di';
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from './control_value_accessor';
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
import {setProperty} from './shared';
const CHECKBOX_VALUE_ACCESSOR = CONST_EXPR(new Provider(
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" ng-control="rememberLogin">
* ```
*/
@Directive({
selector:
'input[type=checkbox][ng-control],input[type=checkbox][ng-form-control],input[type=checkbox][ng-model]',
host: {'(change)': 'onChange($event.target.checked)', '(blur)': 'onTouched()'},
bindings: [CHECKBOX_VALUE_ACCESSOR]
})
export class CheckboxControlValueAccessor implements ControlValueAccessor {
onChange = (_) => {};
onTouched = () => {};
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
writeValue(value: any): void { setProperty(this._renderer, this._elementRef, "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,29 @@
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
import {OpaqueToken} from 'angular2/src/core/di';
/**
* 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;
}
export const NG_VALUE_ACCESSOR: OpaqueToken = CONST_EXPR(new OpaqueToken("NgValueAccessor"));
@@ -0,0 +1,47 @@
import {Directive} from 'angular2/src/core/metadata';
import {ElementRef} from 'angular2/src/core/linker';
import {Renderer} from 'angular2/src/core/render';
import {Self, forwardRef, Provider} from 'angular2/src/core/di';
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from './control_value_accessor';
import {isBlank, CONST_EXPR} from 'angular2/src/core/facade/lang';
import {setProperty} from './shared';
const DEFAULT_VALUE_ACCESSOR = CONST_EXPR(new Provider(
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" ng-control="searchQuery">
* ```
*/
@Directive({
selector:
'input:not([type=checkbox])[ng-control],textarea[ng-control],input:not([type=checkbox])[ng-form-control],textarea[ng-form-control],input:not([type=checkbox])[ng-model],textarea[ng-model],[ng-default-control]',
// TODO: vsavkin replace the above selector with the one below it once
// https://github.com/angular/angular/issues/3011 is implemented
// selector: '[ng-control],[ng-model],[ng-form-control]',
host: {
'(change)': 'onChange($event.target.value)',
'(input)': 'onChange($event.target.value)',
'(blur)': 'onTouched()'
},
bindings: [DEFAULT_VALUE_ACCESSOR]
})
export class DefaultValueAccessor implements ControlValueAccessor {
onChange = (_) => {};
onTouched = () => {};
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
writeValue(value: any): void {
var normalizedValue = isBlank(value) ? '' : value;
setProperty(this._renderer, this._elementRef, '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;
}
@@ -0,0 +1,19 @@
import {ControlValueAccessor} from './control_value_accessor';
import {AbstractControlDirective} from './abstract_control_directive';
import {unimplemented} from 'angular2/src/core/facade/exceptions';
/**
* 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(): Function { return unimplemented(); }
get asyncValidator(): Function { return unimplemented(); }
abstract viewToModelUpdate(newValue: any): void;
}
@@ -0,0 +1,104 @@
import {OnInit, OnDestroy} from 'angular2/lifecycle_hooks';
import {Directive} from 'angular2/src/core/metadata';
import {Optional, Inject, Host, SkipSelf, forwardRef, Provider} from 'angular2/src/core/di';
import {ListWrapper} from 'angular2/src/core/facade/collection';
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
import {ControlContainer} from './control_container';
import {controlPath, composeValidators, composeAsyncValidators} from './shared';
import {ControlGroup} from '../model';
import {Form} from './form_interface';
import {Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators';
const controlGroupProvider =
CONST_EXPR(new Provider(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],
* })
* @View({
* template: `
* <div>
* <h2>Angular2 Control &amp; ControlGroup Example</h2>
* <form #f="form">
* <div ng-control-group="name" #cg-name="form">
* <h3>Enter your name:</h3>
* <p>First: <input ng-control="first" required></p>
* <p>Middle: <input ng-control="middle"></p>
* <p>Last: <input ng-control="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 ng-control="food"></p>
* <h3>Form value</h3>
* <pre>{{valueOf(f)}}</pre>
* </form>
* </div>
* `,
* directives: [FORM_DIRECTIVES]
* })
* 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: '[ng-control-group]',
providers: [controlGroupProvider],
inputs: ['name: ng-control-group'],
exportAs: 'form'
})
export class NgControlGroup extends ControlContainer implements OnInit,
OnDestroy {
/** @internal */
_parent: ControlContainer;
constructor(@Host() @SkipSelf() parent: ControlContainer,
@Optional() @Inject(NG_VALIDATORS) private _validators: any[],
@Optional() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[]) {
super();
this._parent = parent;
}
onInit(): void { this.formDirective.addControlGroup(this); }
onDestroy(): 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(): Function { return composeValidators(this._validators); }
get asyncValidator(): Function { return composeAsyncValidators(this._asyncValidators); }
}
@@ -0,0 +1,132 @@
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
import {EventEmitter, ObservableWrapper} from 'angular2/src/core/facade/async';
import {OnChanges, OnDestroy} from 'angular2/lifecycle_hooks';
import {SimpleChange} from 'angular2/src/core/change_detection';
import {Query, Directive} from 'angular2/src/core/metadata';
import {forwardRef, Host, SkipSelf, Provider, Inject, Optional} from 'angular2/src/core/di';
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 {Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators';
const controlNameBinding =
CONST_EXPR(new Provider(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="form" (submit)='onLogIn(f.value)'>
* Login <input type='text' ng-control='login' #l="form">
* <div *ng-if="!l.valid">Login is invalid</div>
*
* Password <input type='password' ng-control='password'>
* <button type='submit'>Log in!</button>
* </form>
* `})
* class LoginComp {
* onLogIn(value): void {
* // value === {login: 'some login', password: 'some password'}
* }
* }
* ```
*
* We can also use ng-model to bind a domain model to the form.
*
* ```
* @Component({
* selector: "login-comp",
* directives: [FORM_DIRECTIVES],
* template: `
* <form (submit)='onLogIn()'>
* Login <input type='text' ng-control='login' [(ng-model)]="credentials.login">
* Password <input type='password' ng-control='password'
* [(ng-model)]="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: '[ng-control]',
bindings: [controlNameBinding],
inputs: ['name: ngControl', 'model: ngModel'],
outputs: ['update: ngModelChange'],
exportAs: 'form'
})
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() @Inject(NG_VALIDATORS) private _validators:
/* Array<Validator|Function> */ any[],
@Optional() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators:
/* Array<Validator|Function> */ any[],
@Optional() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) {
super();
this.valueAccessor = selectValueAccessor(this, valueAccessors);
}
onChanges(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);
}
}
onDestroy(): void { this.formDirective.removeControl(this); }
viewToModelUpdate(newValue: any): void {
this.viewModel = newValue;
ObservableWrapper.callNext(this.update, newValue);
}
get path(): string[] { return controlPath(this.name, this._parent); }
get formDirective(): any { return this._parent.formDirective; }
get validator(): Function { return composeValidators(this._validators); }
get asyncValidator(): Function { return composeAsyncValidators(this._asyncValidators); }
get control(): Control { return this.formDirective.getControl(this); }
}
@@ -0,0 +1,40 @@
import {Directive} from 'angular2/src/core/metadata';
import {Self} from 'angular2/src/core/di';
import {NgControl} from './ng_control';
import {isBlank, isPresent} from 'angular2/src/core/facade/lang';
@Directive({
selector: '[ng-control],[ng-model],[ng-form-control]',
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;
}
}
@@ -0,0 +1,172 @@
import {
PromiseWrapper,
ObservableWrapper,
EventEmitter,
PromiseCompleter
} from 'angular2/src/core/facade/async';
import {StringMapWrapper, ListWrapper} from 'angular2/src/core/facade/collection';
import {isPresent, isBlank, CONST_EXPR} from 'angular2/src/core/facade/lang';
import {Directive} from 'angular2/src/core/metadata';
import {forwardRef, Provider, Optional, Inject} from 'angular2/src/core/di';
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';
const formDirectiveProvider =
CONST_EXPR(new Provider(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 `ng-submit` 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="form" (ng-submit)="onSubmit(f.value)">
* <h3>Control group: credentials</h3>
* <div ng-control-group="credentials">
* <p>Login: <input type="text" ng-control="login"></p>
* <p>Password: <input type="password" ng-control="password"></p>
* </div>
* <h3>Control group: person</h3>
* <div ng-control-group="person">
* <p>First name: <input type="text" ng-control="firstName"></p>
* <p>Last name: <input type="text" ng-control="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([ng-no-form]):not([ng-form-model]),ng-form,[ng-form]',
bindings: [formDirectiveProvider],
host: {
'(submit)': 'onSubmit()',
},
outputs: ['ngSubmit'],
exportAs: 'form'
})
export class NgForm extends ControlContainer implements Form {
form: ControlGroup;
ngSubmit = new EventEmitter();
constructor(@Optional() @Inject(NG_VALIDATORS) validators: any[],
@Optional() @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.callNext(this.ngSubmit, null);
return false;
}
/** @internal */
_findContainer(path: string[]): ControlGroup {
path.pop();
return ListWrapper.isEmpty(path) ? this.form : <ControlGroup>this.form.find(path);
}
}
@@ -0,0 +1,119 @@
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
import {StringMapWrapper} from 'angular2/src/core/facade/collection';
import {EventEmitter, ObservableWrapper} from 'angular2/src/core/facade/async';
import {OnChanges} from 'angular2/lifecycle_hooks';
import {SimpleChange} from 'angular2/src/core/change_detection';
import {Query, Directive} from 'angular2/src/core/metadata';
import {forwardRef, Provider, Inject, Optional} from 'angular2/src/core/di';
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';
const formControlBinding =
CONST_EXPR(new Provider(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"
* [ng-form-control]="loginControl"></p>
* <p>Value of existing control: {{loginControl.value}}</p>
* </form>
* </div>
* `,
* directives: [CORE_DIRECTIVES, FORM_DIRECTIVES]
* })
* export class App {
* loginControl: Control = new Control('');
* }
* ```
*
*##ng-model
*
* We can also use `ng-model` 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' [ng-form-control]='loginControl' [(ng-model)]='login'>"
* })
* class LoginComp {
* loginControl: Control = new Control('');
* login:string;
* }
* ```
*/
@Directive({
selector: '[ng-form-control]',
bindings: [formControlBinding],
inputs: ['form: ngFormControl', 'model: ngModel'],
outputs: ['update: ngModelChange'],
exportAs: 'form'
})
export class NgFormControl extends NgControl implements OnChanges {
form: Control;
update = new EventEmitter();
model: any;
viewModel: any;
constructor(@Optional() @Inject(NG_VALIDATORS) private _validators:
/* Array<Validator|Function> */ any[],
@Optional() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators:
/* Array<Validator|Function> */ any[],
@Optional() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) {
super();
this.valueAccessor = selectValueAccessor(this, valueAccessors);
}
onChanges(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(): Function { return composeValidators(this._validators); }
get asyncValidator(): Function { return composeAsyncValidators(this._asyncValidators); }
get control(): Control { return this.form; }
viewToModelUpdate(newValue: any): void {
this.viewModel = newValue;
ObservableWrapper.callNext(this.update, newValue);
}
private _isControlChanged(changes: {[key: string]: any}): boolean {
return StringMapWrapper.contains(changes, "form");
}
}
@@ -0,0 +1,171 @@
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
import {ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection';
import {ObservableWrapper, EventEmitter} from 'angular2/src/core/facade/async';
import {SimpleChange} from 'angular2/src/core/change_detection';
import {OnChanges} from 'angular2/lifecycle_hooks';
import {Directive} from 'angular2/src/core/metadata';
import {forwardRef, Provider, Inject, Optional} from 'angular2/src/core/di';
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';
const formDirectiveProvider =
CONST_EXPR(new Provider(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 [ng-form-model]="loginForm">
* <p>Login: <input type="text" ng-control="login"></p>
* <p>Password: <input type="password" ng-control="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 ng-model to bind a domain model to the form.
*
* ```typescript
* @Component({
* selector: "login-comp",
* directives: [FORM_DIRECTIVES],
* template: `
* <form [ng-form-model]='loginForm'>
* Login <input type='text' ng-control='login' [(ng-model)]='credentials.login'>
* Password <input type='password' ng-control='password'
* [(ng-model)]='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: '[ng-form-model]',
bindings: [formDirectiveProvider],
inputs: ['form: ng-form-model'],
host: {'(submit)': 'onSubmit()'},
outputs: ['ngSubmit'],
exportAs: 'form'
})
export class NgFormModel extends ControlContainer implements Form,
OnChanges {
form: ControlGroup = null;
directives: NgControl[] = [];
ngSubmit = new EventEmitter();
constructor(@Optional() @Inject(NG_VALIDATORS) private _validators: any[],
@Optional() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[]) {
super();
}
onChanges(changes: {[key: string]: SimpleChange}): void {
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.callNext(this.ngSubmit, null);
return false;
}
/** @internal */
_updateDomValue() {
this.directives.forEach(dir => {
var ctrl: any = this.form.find(dir.path);
dir.valueAccessor.writeValue(ctrl.value);
});
}
}
@@ -0,0 +1,91 @@
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
import {EventEmitter, ObservableWrapper} from 'angular2/src/core/facade/async';
import {OnChanges} from 'angular2/lifecycle_hooks';
import {SimpleChange} from 'angular2/src/core/change_detection';
import {Query, Directive} from 'angular2/src/core/metadata';
import {forwardRef, Provider, Inject, Optional} from 'angular2/src/core/di';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
import {NgControl} from './ng_control';
import {Control} from '../model';
import {Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators';
import {
setUpControl,
isPropertyUpdated,
selectValueAccessor,
composeValidators,
composeAsyncValidators
} from './shared';
const formControlBinding =
CONST_EXPR(new Provider(NgControl, {useExisting: forwardRef(() => NgModel)}));
/**
* Binds a domain model to a form control.
*
*##Usage
*
* `ng-model` binds an existing domain model to a form control. For a
* two-way binding, use `[(ng-model)]` 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' [(ng-model)]="searchQuery">`
* })
* class SearchComp {
* searchQuery: string;
* }
* ```
*/
@Directive({
selector: '[ng-model]:not([ng-control]):not([ng-form-control])',
bindings: [formControlBinding],
inputs: ['model: ngModel'],
outputs: ['update: ngModelChange'],
exportAs: 'form'
})
export class NgModel extends NgControl implements OnChanges {
/** @internal */
_control = new Control();
/** @internal */
_added = false;
update = new EventEmitter();
model: any;
viewModel: any;
constructor(@Optional() @Inject(NG_VALIDATORS) private _validators: any[],
@Optional() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[],
@Optional() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) {
super();
this.valueAccessor = selectValueAccessor(this, valueAccessors);
}
onChanges(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(): Function { return composeValidators(this._validators); }
get asyncValidator(): Function { return composeAsyncValidators(this._asyncValidators); }
viewToModelUpdate(newValue: any): void {
this.viewModel = newValue;
ObservableWrapper.callNext(this.update, newValue);
}
}
@@ -0,0 +1,12 @@
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;
}
}
@@ -0,0 +1,9 @@
import {Validator} from './validators';
export function normalizeValidator(validator: Function | Validator): Function {
if ((<Validator>validator).validate !== undefined) {
return (c) => (<Validator>validator).validate(c);
} else {
return <Function>validator;
}
}
@@ -0,0 +1,43 @@
import {Directive} from 'angular2/src/core/metadata';
import {ElementRef} from 'angular2/src/core/linker';
import {Renderer} from 'angular2/src/core/render';
import {Self, forwardRef, Provider} from 'angular2/src/core/di';
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from './control_value_accessor';
import {isBlank, CONST_EXPR, NumberWrapper} from 'angular2/src/core/facade/lang';
import {setProperty} from './shared';
const NUMBER_VALUE_ACCESSOR = CONST_EXPR(new Provider(
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" [(ng-model)]="age">
* ```
*/
@Directive({
selector:
'input[type=number][ng-control],input[type=number][ng-form-control],input[type=number][ng-model]',
host: {
'(change)': 'onChange($event.target.value)',
'(input)': 'onChange($event.target.value)',
'(blur)': 'onTouched()'
},
bindings: [NUMBER_VALUE_ACCESSOR]
})
export class NumberValueAccessor implements ControlValueAccessor {
onChange = (_) => {};
onTouched = () => {};
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
writeValue(value: number): void { setProperty(this._renderer, this._elementRef, 'value', value); }
registerOnChange(fn: (_: number) => void): void {
this.onChange = (value) => { fn(NumberWrapper.parseFloat(value)); };
}
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}
@@ -0,0 +1,62 @@
import {Self, forwardRef, Provider} from 'angular2/src/core/di';
import {Renderer} from 'angular2/src/core/render';
import {ElementRef, QueryList} from 'angular2/src/core/linker';
import {Query, Directive} from 'angular2/src/core/metadata';
import {ObservableWrapper} from 'angular2/src/core/facade/async';
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from './control_value_accessor';
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
import {setProperty} from './shared';
const SELECT_VALUE_ACCESSOR = CONST_EXPR(new Provider(
NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => SelectControlValueAccessor), multi: true}));
/**
* Marks `<option>` as dynamic, so Angular can be notified when options change.
*
* ### Example
*
* ```
* <select ng-control="city">
* <option *ng-for="#c of cities" [value]="c"></option>
* </select>
* ```
*/
@Directive({selector: 'option'})
export class NgSelectOption {
}
/**
* The accessor for writing a value and listening to changes on a select element.
*/
@Directive({
selector: 'select[ng-control],select[ng-form-control],select[ng-model]',
host: {
'(change)': 'onChange($event.target.value)',
'(input)': 'onChange($event.target.value)',
'(blur)': 'onTouched()'
},
bindings: [SELECT_VALUE_ACCESSOR]
})
export class SelectControlValueAccessor implements ControlValueAccessor {
value: string;
onChange = (_) => {};
onTouched = () => {};
constructor(private _renderer: Renderer, private _elementRef: ElementRef,
@Query(NgSelectOption, {descendants: true}) query: QueryList<NgSelectOption>) {
this._updateValueWhenListOfOptionsChanges(query);
}
writeValue(value: any): void {
this.value = value;
setProperty(this._renderer, this._elementRef, "value", value);
}
registerOnChange(fn: () => any): void { this.onChange = fn; }
registerOnTouched(fn: () => any): void { this.onTouched = fn; }
private _updateValueWhenListOfOptionsChanges(query: QueryList<NgSelectOption>) {
ObservableWrapper.subscribe(query.changes, (_) => this.writeValue(this.value));
}
}
@@ -0,0 +1,114 @@
import {ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection';
import {isBlank, isPresent, looseIdentical} from 'angular2/src/core/facade/lang';
import {BaseException, WrappedException} from 'angular2/src/core/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 {ElementRef, QueryList} from 'angular2/src/core/linker';
import {Renderer} from 'angular2/src/core/render';
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 {normalizeValidator} from './normalize_validator';
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 => {
dir.viewToModelUpdate(newValue);
control.updateValue(newValue, {emitModelToViewChange: false});
control.markAsDirty();
});
// model -> view
control.registerOnChange(newValue => 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 setProperty(renderer: Renderer, elementRef: ElementRef, propName: string,
propValue: any) {
renderer.setElementProperty(elementRef, propName, propValue);
}
export function composeValidators(validators: /* Array<Validator|Function> */ any[]): Function {
return isPresent(validators) ? Validators.compose(validators.map(normalizeValidator)) : null;
}
export function composeAsyncValidators(
validators: /* Array<Validator|Function> */ any[]): Function {
return isPresent(validators) ? Validators.composeAsync(validators.map(normalizeValidator)) : 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;
var builtinAccessor;
var customAccessor;
valueAccessors.forEach(v => {
if (v instanceof DefaultValueAccessor) {
defaultAccessor = v;
} else if (v instanceof CheckboxControlValueAccessor || v instanceof NumberValueAccessor ||
v instanceof SelectControlValueAccessor) {
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;
}
@@ -0,0 +1,79 @@
import {forwardRef, Provider, OpaqueToken} from 'angular2/src/core/di';
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
import {Attribute, Directive} from 'angular2/src/core/metadata';
import {Validators, NG_VALIDATORS} from '../validators';
import {Control} from '../model';
import * as modelModule from '../model';
import {NumberWrapper} from "angular2/src/core/facade/lang";
/**
* 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.Control): {[key: string]: any}; }
const REQUIRED_VALIDATOR =
CONST_EXPR(new Provider(NG_VALIDATORS, {useValue: Validators.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 ng-control="fullName" required>
* ```
*/
@Directive({
selector: '[required][ng-control],[required][ng-form-control],[required][ng-model]',
providers: [REQUIRED_VALIDATOR]
})
export class RequiredValidator {
}
const MIN_LENGTH_VALIDATOR = CONST_EXPR(
new Provider(NG_VALIDATORS, {useExisting: forwardRef(() => MinLengthValidator), multi: true}));
@Directive({
selector: '[minlength][ng-control],[minlength][ng-form-control],[minlength][ng-model]',
providers: [MIN_LENGTH_VALIDATOR]
})
export class MinLengthValidator implements Validator {
private _validator: Function;
constructor(@Attribute("minlength") minLength: string) {
this._validator = Validators.minLength(NumberWrapper.parseInt(minLength, 10));
}
validate(c: Control): {[key: string]: any} { return this._validator(c); }
}
const MAX_LENGTH_VALIDATOR = CONST_EXPR(
new Provider(NG_VALIDATORS, {useExisting: forwardRef(() => MaxLengthValidator), multi: true}));
@Directive({
selector: '[maxlength][ng-control],[maxlength][ng-form-control],[maxlength][ng-model]',
providers: [MAX_LENGTH_VALIDATOR]
})
export class MaxLengthValidator implements Validator {
private _validator: Function;
constructor(@Attribute("maxlength") minLength: string) {
this._validator = Validators.maxLength(NumberWrapper.parseInt(minLength, 10));
}
validate(c: Control): {[key: string]: any} { return this._validator(c); }
}
@@ -0,0 +1,124 @@
import {Injectable} from 'angular2/src/core/di';
import {StringMapWrapper} from 'angular2/src/core/facade/collection';
import {isPresent, isArray, CONST_EXPR, Type} from 'angular2/src/core/facade/lang';
import * as modelModule from './model';
/**
* 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 [ng-form-model]="loginForm">
* <p>Login <input ng-control="login"></p>
* <div ng-control-group="passwordRetry">
* <p>Password <input type="password" ng-control="password"></p>
* <p>Confirm password <input type="password" ng-control="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 = isPresent(extra) ? StringMapWrapper.get(extra, "optionals") : null;
var validator = isPresent(extra) ? StringMapWrapper.get(extra, "validator") : null;
var asyncValidator = 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: Function = null,
asyncValidator: Function = 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: Function = null,
asyncValidator: Function = null): modelModule.ControlArray {
var controls = controlsConfig.map(c => this._createControl(c));
return new modelModule.ControlArray(controls, validator, asyncValidator);
}
/** @internal */
_reduceControls(controlsConfig: any): {[key: string]: modelModule.AbstractControl} {
var controls: {[key: string]: modelModule.AbstractControl} = {};
StringMapWrapper.forEach(controlsConfig, (controlConfig, controlName) => {
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 = controlConfig.length > 1 ? controlConfig[1] : null;
var asyncValidator = controlConfig.length > 2 ? controlConfig[2] : null;
return this.control(value, validator, asyncValidator);
} else {
return this.control(controlConfig);
}
}
}
/**
* Shorthand set of providers used for building Angular forms.
*
* ### Example
*
* ```typescript
* bootstrap(MyApp, [FORM_PROVIDERS]);
* ```
*/
export const FORM_PROVIDERS: Type[] = CONST_EXPR([FormBuilder]);
/**
* @deprecated
*/
export const FORM_BINDINGS = FORM_PROVIDERS;
+512
View File
@@ -0,0 +1,512 @@
import {StringWrapper, isPresent, isBlank, normalizeBool} from 'angular2/src/core/facade/lang';
import {Observable, EventEmitter, ObservableWrapper} from 'angular2/src/core/facade/async';
import {StringMapWrapper, ListWrapper} from 'angular2/src/core/facade/collection';
/**
* 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 occuring 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 ListWrapper.reduce(<Array<string | number>>path, (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);
}
/**
*
*/
export abstract class AbstractControl {
/** @internal */
_value: any;
/** @internal */
_valueChanges: EventEmitter<any>;
private _status: string;
private _errors: {[key: string]: any};
private _controlsErrors: any;
private _pristine: boolean = true;
private _touched: boolean = false;
private _parent: ControlGroup | ControlArray;
private _asyncValidationSubscription;
constructor(public validator: Function, public asyncValidator: Function) {}
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; }
/**
* Returns the errors of the child controls.
*/
get controlsErrors(): any { return this._controlsErrors; }
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 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._controlsErrors = this._calculateControlsErrors();
this._status = this._calculateStatus();
if (this._status == VALID || this._status == PENDING) {
this._runAsyncValidator();
}
if (emitEvent) {
ObservableWrapper.callNext(this._valueChanges, this._value);
}
if (isPresent(this._parent) && !onlySelf) {
this._parent.updateValueAndValidity({onlySelf: onlySelf, emitEvent: emitEvent});
}
}
private _runValidator() { return isPresent(this.validator) ? this.validator(this) : null; }
private _runAsyncValidator() {
if (isPresent(this.asyncValidator)) {
this._status = PENDING;
this._cancelExistingSubscription();
var obs = ObservableWrapper.fromPromise(this.asyncValidator(this));
this._asyncValidationSubscription =
ObservableWrapper.subscribe(obs, res => this.setErrors(res));
}
}
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}): void {
this._errors = errors;
this._status = this._calculateStatus();
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));
}
/** @internal */
_updateControlsErrors(): void {
this._controlsErrors = this._calculateControlsErrors();
this._status = this._calculateStatus();
if (isPresent(this._parent)) {
this._parent._updateControlsErrors();
}
}
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 _calculateControlsErrors(): any;
/** @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: Function = null, asyncValidator: Function = null) {
super(validator, asyncValidator);
this._value = value;
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
this._valueChanges = new EventEmitter();
}
/**
* 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
*/
_calculateControlsErrors() { return null; }
/**
* @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 and errors of each {@link Control} in the group. Thus, 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: Function = null,
asyncValidator: Function = null) {
super(validator, asyncValidator);
this._optionals = isPresent(optionals) ? optionals : {};
this._valueChanges = new EventEmitter();
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, name) => { control.setParent(this); });
}
/** @internal */
_updateValue() { this._value = this._reduceValue(); }
/** @internal */
_calculateControlsErrors() {
var res = {};
StringMapWrapper.forEach(this.controls, (control, name) => {
if (this.contains(name) && isPresent(control.errors)) {
res[name] = control.errors;
}
});
return StringMapWrapper.isEmpty(res) ? null : res;
}
/** @internal */
_anyControlsHaveStatus(status: string): boolean {
var res = false;
StringMapWrapper.forEach(this.controls, (control, name) => {
res = res || (this.contains(name) && control.status == status);
});
return res;
}
/** @internal */
_reduceValue() {
return this._reduceChildren({}, (acc, control, name) => {
acc[name] = control.value;
return acc;
});
}
/** @internal */
_reduceChildren(initValue: any, fn: Function) {
var res = initValue;
StringMapWrapper.forEach(this.controls, (control, name) => {
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 and errors of each {@link Control} in the group. Thus, 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.
*
* `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: Function = null,
asyncValidator: Function = null) {
super(validator, asyncValidator);
this._valueChanges = new EventEmitter();
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 */
_calculateControlsErrors() {
var res = [];
var anyErrors = false;
this.controls.forEach((control) => {
res.push(control.errors);
if (isPresent(control.errors)) {
anyErrors = true;
}
});
return anyErrors ? res : null;
}
/** @internal */
_anyControlsHaveStatus(status: string): boolean {
return ListWrapper.any(this.controls, c => c.status == status);
}
/** @internal */
_setParentForControls(): void {
this.controls.forEach((control) => { control.setParent(this); });
}
}
@@ -0,0 +1,110 @@
import {isBlank, isPresent, CONST_EXPR} from 'angular2/src/core/facade/lang';
import {PromiseWrapper} from 'angular2/src/core/facade/promise';
import {ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection';
import {OpaqueToken} from 'angular2/src/core/di';
import * as modelModule from './model';
/**
* Providers for validators to be used for {@link Control}s in a form.
*
* Provide this using `multi: true` to add validators.
*
* ### Example
*
* ```typescript
* var providers = [
* new Provider(NG_VALIDATORS, {useValue: myValidator, multi: true})
* ];
* ```
*/
export const NG_VALIDATORS: OpaqueToken = CONST_EXPR(new OpaqueToken("NgValidators"));
export const NG_ASYNC_VALIDATORS: OpaqueToken = CONST_EXPR(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 {@link StringMap} 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.Control): {[key: string]: boolean} {
return isBlank(control.value) || control.value == "" ? {"required": true} : null;
}
/**
* Validator that requires controls to have a value of a minimum length.
*/
static minLength(minLength: number): Function {
return (control: modelModule.Control): {[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): Function {
return (control: modelModule.Control): {[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;
};
}
/**
* No-op validator.
*/
static nullValidator(c: any): {[key: string]: boolean} { return null; }
/**
* Compose multiple validators into a single function that returns the union
* of the individual error maps.
*/
static compose(validators: Function[]): Function {
if (isBlank(validators)) return null;
var presentValidators = ListWrapper.filter(validators, isPresent);
if (presentValidators.length == 0) return null;
return function(control: modelModule.AbstractControl) {
return _mergeErrors(_executeValidators(control, presentValidators));
};
}
static composeAsync(validators: Function[]): Function {
if (isBlank(validators)) return null;
var presentValidators = ListWrapper.filter(validators, isPresent);
if (presentValidators.length == 0) return null;
return function(control: modelModule.AbstractControl) {
return PromiseWrapper.all(_executeValidators(control, presentValidators)).then(_mergeErrors);
};
}
}
function _executeValidators(control: modelModule.AbstractControl, validators: Function[]): any[] {
return validators.map(v => v(control));
}
function _mergeErrors(arrayOfErrors: any[]): {[key: string]: any} {
var res = ListWrapper.reduce(arrayOfErrors, (res, errors) => {
return isPresent(errors) ? StringMapWrapper.merge(<any>res, <any>errors) : res;
}, {});
return StringMapWrapper.isEmpty(res) ? null : res;
}
+33
View File
@@ -0,0 +1,33 @@
/**
* @module
* @description
* This module provides a set of common Pipes.
*/
import {AsyncPipe} from './pipes/async_pipe';
import {UpperCasePipe} from './pipes/uppercase_pipe';
import {LowerCasePipe} from './pipes/lowercase_pipe';
import {JsonPipe} from './pipes/json_pipe';
import {SlicePipe} from './pipes/slice_pipe';
import {DatePipe} from './pipes/date_pipe';
import {DecimalPipe, PercentPipe, CurrencyPipe} from './pipes/number_pipe';
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
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 const COMMON_PIPES = CONST_EXPR([
AsyncPipe,
UpperCasePipe,
LowerCasePipe,
JsonPipe,
SlicePipe,
DecimalPipe,
PercentPipe,
CurrencyPipe,
DatePipe
]);
@@ -0,0 +1,140 @@
import {isBlank, isPresent, isPromise, CONST} from 'angular2/src/core/facade/lang';
import {Promise, ObservableWrapper, Observable, EventEmitter} from 'angular2/src/core/facade/async';
import {Pipe} from 'angular2/src/core/metadata';
import {Injectable} from 'angular2/src/core/di';
import {
ChangeDetectorRef,
PipeOnDestroy,
PipeTransform,
WrappedValue
} from 'angular2/src/core/change_detection';
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: any, updateLatestValue: any): any {
return async.then(updateLatestValue);
}
dispose(subscription: any): void {}
onDestroy(subscription: any): void {}
}
var _promiseStrategy = new PromiseStrategy();
var _observableStrategy = new ObservableStrategy();
/**
* 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
* The example below binds the `time` Observable to the view. Every 500ms, the `time` Observable
* updates the view with the current time.
*
* ```
* import {Observable} from 'angular2/core';
* @Component({
* selector: "task-cmp",
* template: "Time: {{ time | async }}"
* })
* class Task {
* time = new Observable<number>(observer => {
* setInterval(_ =>
* observer.next(new Date().getTime()), 500);
* });
* }
* ```
*/
@Pipe({name: 'async', pure: false})
@Injectable()
export class AsyncPipe implements PipeTransform, PipeOnDestroy {
/** @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; }
onDestroy(): void {
if (isPresent(this._subscription)) {
this._dispose();
}
}
transform(obj: Observable<any>| Promise<any>| EventEmitter<any>, args?: any[]): any {
if (isBlank(this._obj)) {
if (isPresent(obj)) {
this._subscribe(obj);
}
return null;
}
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 => 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();
}
}
}
@@ -0,0 +1,25 @@
/**
* @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 {CONST_EXPR} from 'angular2/src/core/facade/lang';
export const COMMON_PIPES = CONST_EXPR([
AsyncPipe,
UpperCasePipe,
LowerCasePipe,
JsonPipe,
SlicePipe,
DecimalPipe,
PercentPipe,
CurrencyPipe,
DatePipe
]);
@@ -0,0 +1,120 @@
import {
isDate,
isNumber,
isPresent,
Date,
DateWrapper,
CONST,
isBlank,
FunctionWrapper
} from 'angular2/src/core/facade/lang';
import {DateFormatter} from 'angular2/src/core/facade/intl';
import {Injectable} from 'angular2/src/core/di';
import {Pipe} from 'angular2/src/core/metadata';
import {PipeTransform, WrappedValue} from 'angular2/src/core/change_detection';
import {StringMapWrapper, ListWrapper} from 'angular2/src/core/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'
*/
@CONST()
@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, args: any[]): string {
if (isBlank(value)) return null;
if (!this.supports(value)) {
throw new InvalidPipeArgumentException(DatePipe, value);
}
var pattern: string = isPresent(args) && args.length > 0 ? args[0] : 'mediumDate';
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); }
}
@@ -0,0 +1,8 @@
import {CONST, Type} from 'angular2/src/core/facade/lang';
import {BaseException, WrappedException} from 'angular2/src/core/facade/exceptions';
export class InvalidPipeArgumentException extends BaseException {
constructor(type: Type, value: Object) {
super(`Invalid argument '${value}' for pipe '${type}'`);
}
}
@@ -0,0 +1,32 @@
import {isBlank, isPresent, Json, CONST} from 'angular2/src/core/facade/lang';
import {Injectable} from 'angular2/src/core/di';
import {PipeTransform, WrappedValue} from 'angular2/src/core/change_detection';
import {Pipe} from 'angular2/src/core/metadata';
/**
* Implements json transforms to any object.
*
* ### Example
*
* In this example we transform the user object to json.
*
* ```
* @Component({
* selector: "user-cmp",
* template: "User: {{ user | json }}"
* })
* class Username {
* user:Object
* constructor() {
* this.user = { name: "PatrickJS" };
* }
* }
*
* ```
*/
@CONST()
@Pipe({name: 'json', pure: false})
@Injectable()
export class JsonPipe implements PipeTransform {
transform(value: any, args: any[] = null): string { return Json.stringify(value); }
}
@@ -0,0 +1,37 @@
import {isString, StringWrapper, CONST, isBlank} from 'angular2/src/core/facade/lang';
import {Injectable} from 'angular2/src/core/di';
import {PipeTransform, WrappedValue} from 'angular2/src/core/change_detection';
import {Pipe} from 'angular2/src/core/metadata';
import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception';
/**
* Implements lowercase transforms to text.
*
* ### Example
*
* In this example we transform the user text lowercase.
*
* ```
* @Component({
* selector: "username-cmp",
* template: "Username: {{ user | lowercase }}"
* })
* class Username {
* user:string;
* }
*
* ```
*/
@CONST()
@Pipe({name: 'lowercase'})
@Injectable()
export class LowerCasePipe implements PipeTransform {
transform(value: string, args: any[] = null): string {
if (isBlank(value)) return value;
if (!isString(value)) {
throw new InvalidPipeArgumentException(LowerCasePipe, value);
}
return StringWrapper.toLowerCase(value);
}
}
@@ -0,0 +1,146 @@
import {
isNumber,
isPresent,
isBlank,
StringWrapper,
NumberWrapper,
RegExpWrapper,
CONST,
FunctionWrapper
} from 'angular2/src/core/facade/lang';
import {BaseException, WrappedException} from 'angular2/src/core/facade/exceptions';
import {NumberFormatter, NumberFormatStyle} from 'angular2/src/core/facade/intl';
import {Injectable} from 'angular2/src/core/di';
import {PipeTransform, WrappedValue} from 'angular2/src/core/change_detection';
import {Pipe} from 'angular2/src/core/metadata';
import {ListWrapper} from 'angular2/src/core/facade/collection';
import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception';
var defaultLocale: string = 'en-US';
var _re = RegExpWrapper.create('^(\\d+)?\\.((\\d+)(\\-(\\d+))?)?$');
@CONST()
@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.
*
* ### Examples
*
* {{ 123 | number }} // output is 123
* {{ 123.1 | number: '.2-3' }} // output is 123.10
* {{ 1 | number: '2.2' }} // output is 01.00
*/
@CONST()
@Pipe({name: 'number'})
@Injectable()
export class DecimalPipe extends NumberPipe implements PipeTransform {
transform(value: any, args: any[]): string {
var digits: string = ListWrapper.first(args);
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}
*/
@CONST()
@Pipe({name: 'percent'})
@Injectable()
export class PercentPipe extends NumberPipe implements PipeTransform {
transform(value: any, args: any[]): string {
var digits: string = ListWrapper.first(args);
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}
*/
@CONST()
@Pipe({name: 'currency'})
@Injectable()
export class CurrencyPipe extends NumberPipe implements PipeTransform {
transform(value: any, args: any[]): string {
var currencyCode: string = isPresent(args) && args.length > 0 ? args[0] : 'USD';
var symbolDisplay: boolean = isPresent(args) && args.length > 1 ? args[1] : false;
var digits: string = isPresent(args) && args.length > 2 ? args[2] : null;
return NumberPipe._format(value, NumberFormatStyle.Currency, digits, currencyCode,
symbolDisplay);
}
}
@@ -0,0 +1,90 @@
import {isBlank, isString, isArray, StringWrapper, CONST} from 'angular2/src/core/facade/lang';
import {BaseException} from 'angular2/src/core/facade/exceptions';
import {ListWrapper} from 'angular2/src/core/facade/collection';
import {Injectable} from 'angular2/src/core/di';
import {PipeTransform, WrappedValue} from 'angular2/src/core/change_detection';
import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception';
import {Pipe} from 'angular2/src/core/metadata';
/**
* 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.
*
* ### Examples
*
* ## List Example
*
* Assuming `var collection = ['a', 'b', 'c', 'd']`, this `ng-for` directive:
*
* <li *ng-for="var i of collection | slice:1:3">{{i}}</li>
*
* produces the following:
*
* <li>b</li>
* <li>c</li>
*
* ## String Examples
*
* {{ 'abcdefghij' | slice:0:4 }} // output is 'abcd'
* {{ 'abcdefghij' | slice:4:0 }} // output is ''
* {{ 'abcdefghij' | slice:-4 }} // output is 'ghij'
* {{ 'abcdefghij' | slice:-4,-2 }} // output is 'gh'
* {{ 'abcdefghij' | slice: -100 }} // output is 'abcdefghij'
* {{ 'abcdefghij' | slice: 100 }} // output is ''
*/
@Pipe({name: 'slice', pure: false})
@Injectable()
export class SlicePipe implements PipeTransform {
transform(value: any, args: any[] = null): any {
if (isBlank(args) || args.length == 0) {
throw new BaseException('Slice pipe requires one argument');
}
if (!this.supports(value)) {
throw new InvalidPipeArgumentException(SlicePipe, value);
}
if (isBlank(value)) return value;
var start: number = args[0];
var end: number = args.length > 1 ? args[1] : null;
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); }
}
@@ -0,0 +1,36 @@
import {isString, StringWrapper, CONST, isBlank} from 'angular2/src/core/facade/lang';
import {Pipe} from 'angular2/src/core/metadata';
import {Injectable} from 'angular2/src/core/di';
import {PipeTransform, WrappedValue} from 'angular2/src/core/change_detection';
import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception';
/**
* Implements uppercase transforms to text.
*
* ### Example
*
* In this example we transform the user text uppercase.
*
* ```
* @Component({
* selector: "username-cmp",
* template: "Username: {{ user | uppercase }}"
* })
* class Username {
* user:string;
* }
*
* ```
*/
@CONST()
@Pipe({name: 'uppercase'})
@Injectable()
export class UpperCasePipe implements PipeTransform {
transform(value: string, args: any[] = null): string {
if (isBlank(value)) return value;
if (!isString(value)) {
throw new InvalidPipeArgumentException(UpperCasePipe, value);
}
return StringWrapper.toUpperCase(value);
}
}