Compare commits

..

23 Commits

Author SHA1 Message Date
69ad99dca6 chore(release): cut the 2.2.0-beta.0 release and add changelog 2016-10-20 14:36:46 -07:00
da5fc696bb fix(router): do not update primary route if only secondary outlet is given (#11797) 2016-10-20 10:59:08 -07:00
b44b6ef8f5 fix(router): module loader should start compiling modules when stubbedModules are set (#11742) 2016-10-20 10:58:53 -07:00
0f21a5823b cleanup(router): add a test verifying than NavigationEnd is not emitted after NavigationCancel 2016-10-20 10:56:12 -07:00
5ae6915600 fix(router): fix lazy loading triggered by redirects from wildcard routes
Closes #12183
2016-10-20 10:56:12 -07:00
8b9ab44eee feat(router): add support for ng1/ng2 migration (#12160) 2016-10-20 10:44:44 -07:00
b0a03fcab3 refactor(compiler): introduce directive wrappers to generate less code
- for now only wraps the `@Input` properties and calls
  to `ngOnInit`, `ngDoCheck` and `ngOnChanges` of directives.
- also groups eval sources by NgModule.

Part of #11683
2016-10-20 10:41:43 -07:00
c951822c35 refactor(compiler): don’t use the OfflineCompiler in extract_i18n 2016-10-20 10:41:43 -07:00
acda82c1ed refactor(compiler): remove private exports
All of `@angular/compiler` is private, so we can export
everything we need directly.
2016-10-20 10:41:43 -07:00
a8815d6b08 chore(ci): re-enable browserstack tests in ci 2016-10-20 10:01:51 -07:00
d6791ff0e0 feat(ngUpgrade): add support for AoT compiled upgrade applications
This commit introduces a new API to the ngUpgrade module, which is compatible
with AoT compilation. Primarily, it removes the dependency on reflection
over the Angular 2 metadata by introducing an API where this information
is explicitly defined, in the source code, in a way that is not lost through
AoT compilation.

This commit is a collaboration between @mhevery (who provided the original
design of the API); @gkalpak & @petebacondarwin (who implemented the
API and migrated the specs from the original ngUpgrade tests) and @alexeagle
(who provided input and review).

This commit is an starting point, there is still work to be done:

* add more documentation
* validate the API via internal projects
* align the ngUpgrade compilation of A1 directives closer to the real A1
  compiler
* add more unit tests
* consider support for async `templateUrl` A1 upgraded components

Closes #12239
2016-10-19 15:27:49 -07:00
a2d35641e3 chore(tslint.json): semicolon rule expects an array 2016-10-19 22:38:14 +01:00
76dd026447 refactor: remove some facades (#12335) 2016-10-19 13:42:39 -07:00
0ecd9b2df0 chore(ci): make browserstack tests optional until they are fixed 2016-10-19 10:41:00 -07:00
0e9503b500 feat(forms) range values need to be numbers instead of strings (#11792) 2016-10-19 10:12:13 -07:00
f77ab6a2d2 feat(datePipe): support narrow forms for month and weekdays (#12297)
Closes #12294
2016-10-19 10:05:13 -07:00
97bc97153b feat(forms): add ng-pending CSS class during async validation (#11243)
Closes #10336
2016-10-19 09:56:31 -07:00
445e5922ec feat(forms): make 'parent' a public property of 'AbstractControl' (#11855) 2016-10-19 09:55:50 -07:00
b9fc090143 feat(forms): Added emitEvent to AbstractControl methods (#11949)
* feat(forms): Added emitEvent to AbstractControl methods

* style(forms): unified named parameter
2016-10-19 09:54:54 -07:00
592f40aa9c feat(forms): add hasError and getError to AbstractControlDirective (#11985)
Allows cleaner expressions in template-driven forms.

Before:

    <label>Username</label><input name="username" ngModel required #username="ngModel">
    <div *ngIf="username.dirty && username.control.hasError('required')">Username is required</div>

After:

    <label>Username</label><input name="username" ngModel required #username="ngModel">
    <div *ngIf="username.dirty && username.hasError('required')">Username is required</div>

Fixes #7255
2016-10-19 09:49:02 -07:00
24facdea2d feat(benchmark): add large form benchmark
This benchmark tracks the generated file size for large forms
as well as the time to create and destroy many form fields.
2016-10-19 09:39:16 -07:00
aa2d3372a5 fix(benchmarks): fix method name in targetable spec 2016-10-19 09:39:16 -07:00
bf60418fdc feat(forms): Validator.pattern accepts a RegExp (#12323) 2016-10-19 09:37:54 -07:00
58 changed files with 4148 additions and 203 deletions

View File

@ -1,4 +1,6 @@
# [2.1.1](https://github.com/angular/angular/compare/2.1.0...2.1.1) (2016-10-20)
<a name="2.2.0-beta.0"></a>
# [2.2.0-beta.0](https://github.com/angular/angular/compare/2.1.0...2.2.0-beta.0) (2016-10-20)
### Bug Fixes
@ -13,15 +15,6 @@
* **router:** module loader should start compiling modules when stubbedModules are set ([#11742](https://github.com/angular/angular/issues/11742)) ([b44b6ef](https://github.com/angular/angular/commit/b44b6ef))
### Performance Improvements
* **common:** optimize NgSwitch default case ([fdf4309](https://github.com/angular/angular/commit/fdf4309))
<a name="2.2.0-beta.0"></a>
# [2.2.0-beta.0](https://github.com/angular/angular/compare/2.1.0...2.2.0-beta.0) (2016-10-20)
### Features
* **common:** support narrow forms for month and weekdays in DatePipe ([#12297](https://github.com/angular/angular/issues/12297)) ([f77ab6a](https://github.com/angular/angular/commit/f77ab6a)), closes [#12294](https://github.com/angular/angular/issues/12294)
@ -33,7 +26,10 @@
* **upgrade:** add support for AoT compiled upgrade applications ([d6791ff](https://github.com/angular/angular/commit/d6791ff)), closes [#12239](https://github.com/angular/angular/issues/12239)
* **router:** add support for ng1/ng2 migration ([#12160](https://github.com/angular/angular/issues/12160)) ([8b9ab44](https://github.com/angular/angular/commit/8b9ab44))
Note: The 2.2.0-beta.0 release also contains all the changes present in the 2.1.1 release.
### Performance Improvements
* **common:** optimize NgSwitch default case ([fdf4309](https://github.com/angular/angular/commit/fdf4309))

View File

@ -23,7 +23,7 @@ module.exports = function(config) {
'node_modules/core-js/client/core.js',
// include Angular v1 for upgrade module testing
'node_modules/angular/angular.min.js',
'node_modules/angular/angular.js',
'node_modules/zone.js/dist/zone.js', 'node_modules/zone.js/dist/long-stack-trace-zone.js',
'node_modules/zone.js/dist/proxy.js', 'node_modules/zone.js/dist/sync-test.js',

View File

@ -33,21 +33,21 @@ import {InvalidPipeArgumentError} from './invalid_pipe_argument_error';
* - `'shortTime'`: equivalent to `'jm'` (e.g. `12:05 PM` for `en-US`)
*
*
* | 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) | - | - | - |
* | timezone | a | a (PM) | - | - | - |
* | Component | Symbol | Narrow | Short Form | Long Form | Numeric | 2-digit |
* |-----------|:------:|--------|--------------|-------------------|-----------|-----------|
* | era | G | G (A) | GGG (AD) | GGGG (Anno Domini)| - | - |
* | year | y | - | - | - | y (2015) | yy (15) |
* | month | M | L (S) | MMM (Sep) | MMMM (September) | M (9) | MM (09) |
* | day | d | - | - | - | d (3) | dd (03) |
* | weekday | E | E (S) | EEE (Sun) | EEEE (Sunday) | - | - |
* | hour | j | - | - | - | j (13) | jj (13) |
* | hour12 | h | - | - | - | h (1 PM) | hh (01 PM)|
* | hour24 | H | - | - | - | H (13) | HH (13) |
* | minute | m | - | - | - | m (5) | mm (05) |
* | second | s | - | - | - | s (9) | ss (09) |
* | timezone | z | - | - | z (Pacific Standard Time)| - | - |
* | timezone | Z | - | Z (GMT-8:00) | - | - | - |
* | timezone | a | - | a (PM) | - | - | - |
*
* In javascript, only the components specified will be respected (not the ordering,
* punctuations, ...) and details of the formatting will be dependent on the locale.

View File

@ -59,7 +59,7 @@ export function main() {
expect(pipe.transform(date, 'MMM')).toEqual('Jun');
expect(pipe.transform(date, 'MMMM')).toEqual('June');
expect(pipe.transform(date, 'd')).toEqual('15');
expect(pipe.transform(date, 'E')).toEqual('Mon');
expect(pipe.transform(date, 'EEE')).toEqual('Mon');
expect(pipe.transform(date, 'EEEE')).toEqual('Monday');
if (!browserDetection.isOldChrome) {
expect(pipe.transform(date, 'h')).toEqual('9');
@ -72,6 +72,9 @@ export function main() {
if (!browserDetection.isOldChrome) {
expect(pipe.transform(date, 'HH')).toEqual('09');
}
expect(pipe.transform(date, 'E')).toEqual('M');
expect(pipe.transform(date, 'L')).toEqual('J');
expect(pipe.transform(date, 'm')).toEqual('3');
expect(pipe.transform(date, 's')).toEqual('1');
expect(pipe.transform(date, 'mm')).toEqual('03');
@ -81,13 +84,13 @@ export function main() {
});
it('should format common multi component patterns', () => {
expect(pipe.transform(date, 'E, M/d/y')).toEqual('Mon, 6/15/2015');
expect(pipe.transform(date, 'E, M/d')).toEqual('Mon, 6/15');
expect(pipe.transform(date, 'EEE, M/d/y')).toEqual('Mon, 6/15/2015');
expect(pipe.transform(date, 'EEE, M/d')).toEqual('Mon, 6/15');
expect(pipe.transform(date, 'MMM d')).toEqual('Jun 15');
expect(pipe.transform(date, 'dd/MM/yyyy')).toEqual('15/06/2015');
expect(pipe.transform(date, 'MM/dd/yyyy')).toEqual('06/15/2015');
expect(pipe.transform(date, 'yMEd')).toEqual('20156Mon15');
expect(pipe.transform(date, 'MEd')).toEqual('6Mon15');
expect(pipe.transform(date, 'yMEEEd')).toEqual('20156Mon15');
expect(pipe.transform(date, 'MEEEd')).toEqual('6Mon15');
expect(pipe.transform(date, 'MMMd')).toEqual('Jun15');
expect(pipe.transform(date, 'yMMMMEEEEd')).toEqual('Monday, June 15, 2015');
// IE and Edge can't format a date to minutes and seconds without hours

View File

@ -77,6 +77,7 @@ var DATE_FORMATS = {
MM: datePartGetterFactory(digitCondition('month', 2)),
M: datePartGetterFactory(digitCondition('month', 1)),
LLLL: datePartGetterFactory(nameCondition('month', 4)),
L: datePartGetterFactory(nameCondition('month', 1)),
dd: datePartGetterFactory(digitCondition('day', 2)),
d: datePartGetterFactory(digitCondition('day', 1)),
HH: digitModifier(
@ -161,13 +162,19 @@ function hour12Modify(
}
function digitCondition(prop: string, len: number): Intl.DateTimeFormatOptions {
var result: {[k: string]: string} = {};
result[prop] = len == 2 ? '2-digit' : 'numeric';
const result: {[k: string]: string} = {};
result[prop] = len === 2 ? '2-digit' : 'numeric';
return result;
}
function nameCondition(prop: string, len: number): Intl.DateTimeFormatOptions {
var result: {[k: string]: string} = {};
result[prop] = len < 4 ? 'short' : 'long';
const result: {[k: string]: string} = {};
if (len < 4) {
result[prop] = len > 1 ? 'short' : 'narrow';
} else {
result[prop] = 'long';
}
return result;
}

View File

@ -16,6 +16,7 @@ import {NgModel} from './directives/ng_model';
import {NgModelGroup} from './directives/ng_model_group';
import {NumberValueAccessor} from './directives/number_value_accessor';
import {RadioControlValueAccessor} from './directives/radio_control_value_accessor';
import {RangeValueAccessor} from './directives/range_value_accessor';
import {FormControlDirective} from './directives/reactive_directives/form_control_directive';
import {FormControlName} from './directives/reactive_directives/form_control_name';
import {FormGroupDirective} from './directives/reactive_directives/form_group_directive';
@ -34,6 +35,7 @@ export {NgModel} from './directives/ng_model';
export {NgModelGroup} from './directives/ng_model_group';
export {NumberValueAccessor} from './directives/number_value_accessor';
export {RadioControlValueAccessor} from './directives/radio_control_value_accessor';
export {RangeValueAccessor} from './directives/range_value_accessor';
export {FormControlDirective} from './directives/reactive_directives/form_control_directive';
export {FormControlName} from './directives/reactive_directives/form_control_name';
export {FormGroupDirective} from './directives/reactive_directives/form_group_directive';
@ -44,9 +46,9 @@ export {MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValida
export const SHARED_FORM_DIRECTIVES: Type<any>[] = [
NgSelectOption, NgSelectMultipleOption, DefaultValueAccessor, NumberValueAccessor,
CheckboxControlValueAccessor, SelectControlValueAccessor, SelectMultipleControlValueAccessor,
RadioControlValueAccessor, NgControlStatus, NgControlStatusGroup, RequiredValidator,
MinLengthValidator, MaxLengthValidator, PatternValidator
RangeValueAccessor, CheckboxControlValueAccessor, SelectControlValueAccessor,
SelectMultipleControlValueAccessor, RadioControlValueAccessor, NgControlStatus,
NgControlStatusGroup, RequiredValidator, MinLengthValidator, MaxLengthValidator, PatternValidator
];
export const TEMPLATE_DRIVEN_DIRECTIVES: Type<any>[] = [NgModel, NgModelGroup, NgForm];

View File

@ -57,4 +57,12 @@ export abstract class AbstractControlDirective {
reset(value: any = undefined): void {
if (isPresent(this.control)) this.control.reset(value);
}
hasError(errorCode: string, path: string[] = null): boolean {
return isPresent(this.control) ? this.control.hasError(errorCode, path) : false;
}
getError(errorCode: string, path: string[] = null): any {
return isPresent(this.control) ? this.control.getError(errorCode, path) : null;
}
}

View File

@ -37,6 +37,9 @@ export class AbstractControlStatus {
get ngClassInvalid(): boolean {
return isPresent(this._cd.control) ? this._cd.control.invalid : false;
}
get ngClassPending(): boolean {
return isPresent(this._cd.control) ? this._cd.control.pending : false;
}
}
export const ngControlStatusHost = {
@ -45,7 +48,8 @@ export const ngControlStatusHost = {
'[class.ng-pristine]': 'ngClassPristine',
'[class.ng-dirty]': 'ngClassDirty',
'[class.ng-valid]': 'ngClassValid',
'[class.ng-invalid]': 'ngClassInvalid'
'[class.ng-invalid]': 'ngClassInvalid',
'[class.ng-pending]': 'ngClassPending'
};
/**

View File

@ -0,0 +1,57 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, ElementRef, Provider, Renderer, forwardRef} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
export const RANGE_VALUE_ACCESSOR: Provider = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RangeValueAccessor),
multi: true
};
/**
* The accessor for writing a range value and listening to changes that is used by the
* {@link NgModel}, {@link FormControlDirective}, and {@link FormControlName} directives.
*
* ### Example
* ```
* <input type="range" [(ngModel)]="age" >
* ```
*/
@Directive({
selector:
'input[type=range][formControlName],input[type=range][formControl],input[type=range][ngModel]',
host: {
'(change)': 'onChange($event.target.value)',
'(input)': 'onChange($event.target.value)',
'(blur)': 'onTouched()'
},
providers: [RANGE_VALUE_ACCESSOR]
})
export class RangeValueAccessor implements ControlValueAccessor {
onChange = (_: any) => {};
onTouched = () => {};
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
writeValue(value: any): void {
this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', parseFloat(value));
}
registerOnChange(fn: (_: number) => void): void {
this.onChange = (value) => { fn(value == '' ? null : parseFloat(value)); };
}
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
setDisabledState(isDisabled: boolean): void {
this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
}
}

View File

@ -20,6 +20,7 @@ import {NgControl} from './ng_control';
import {normalizeAsyncValidator, normalizeValidator} from './normalize_validator';
import {NumberValueAccessor} from './number_value_accessor';
import {RadioControlValueAccessor} from './radio_control_value_accessor';
import {RangeValueAccessor} from './range_value_accessor';
import {FormArrayName} from './reactive_directives/form_group_name';
import {SelectControlValueAccessor} from './select_control_value_accessor';
import {SelectMultipleControlValueAccessor} from './select_multiple_control_value_accessor';
@ -125,6 +126,7 @@ export function isPropertyUpdated(changes: {[key: string]: any}, viewModel: any)
const BUILTIN_ACCESSORS = [
CheckboxControlValueAccessor,
RangeValueAccessor,
NumberValueAccessor,
SelectControlValueAccessor,
SelectMultipleControlValueAccessor,

View File

@ -108,6 +108,11 @@ export abstract class AbstractControl {
*/
get value(): any { return this._value; }
/**
* The parent control.
*/
get parent(): FormGroup|FormArray { return this._parent; }
/**
* The validation status of the control. There are four possible
* validation statuses:
@ -248,7 +253,7 @@ export abstract class AbstractControl {
this._touched = true;
if (isPresent(this._parent) && !onlySelf) {
this._parent.markAsTouched({onlySelf: onlySelf});
this._parent.markAsTouched({onlySelf});
}
}
@ -266,7 +271,7 @@ export abstract class AbstractControl {
(control: AbstractControl) => { control.markAsUntouched({onlySelf: true}); });
if (isPresent(this._parent) && !onlySelf) {
this._parent._updateTouched({onlySelf: onlySelf});
this._parent._updateTouched({onlySelf});
}
}
@ -281,7 +286,7 @@ export abstract class AbstractControl {
this._pristine = false;
if (isPresent(this._parent) && !onlySelf) {
this._parent.markAsDirty({onlySelf: onlySelf});
this._parent.markAsDirty({onlySelf});
}
}
@ -298,7 +303,7 @@ export abstract class AbstractControl {
this._forEachChild((control: AbstractControl) => { control.markAsPristine({onlySelf: true}); });
if (isPresent(this._parent) && !onlySelf) {
this._parent._updatePristine({onlySelf: onlySelf});
this._parent._updatePristine({onlySelf});
}
}
@ -310,7 +315,7 @@ export abstract class AbstractControl {
this._status = PENDING;
if (isPresent(this._parent) && !onlySelf) {
this._parent.markAsPending({onlySelf: onlySelf});
this._parent.markAsPending({onlySelf});
}
}
@ -347,7 +352,7 @@ export abstract class AbstractControl {
enable({onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
this._status = VALID;
this._forEachChild((control: AbstractControl) => { control.enable({onlySelf: true}); });
this.updateValueAndValidity({onlySelf: true, emitEvent: emitEvent});
this.updateValueAndValidity({onlySelf: true, emitEvent});
this._updateAncestors(onlySelf);
this._onDisabledChange.forEach((changeFn) => changeFn(false));
@ -406,7 +411,7 @@ export abstract class AbstractControl {
}
if (isPresent(this._parent) && !onlySelf) {
this._parent.updateValueAndValidity({onlySelf: onlySelf, emitEvent: emitEvent});
this._parent.updateValueAndValidity({onlySelf, emitEvent});
}
}
@ -427,8 +432,8 @@ export abstract class AbstractControl {
this._status = PENDING;
this._cancelExistingSubscription();
var obs = toObservable(this.asyncValidator(this));
this._asyncValidationSubscription = obs.subscribe(
{next: (res: {[key: string]: any}) => this.setErrors(res, {emitEvent: emitEvent})});
this._asyncValidationSubscription =
obs.subscribe({next: (res: {[key: string]: any}) => this.setErrors(res, {emitEvent})});
}
}
@ -581,7 +586,7 @@ export abstract class AbstractControl {
this._pristine = !this._anyControlsDirty();
if (isPresent(this._parent) && !onlySelf) {
this._parent._updatePristine({onlySelf: onlySelf});
this._parent._updatePristine({onlySelf});
}
}
@ -590,7 +595,7 @@ export abstract class AbstractControl {
this._touched = this._anyControlsTouched();
if (isPresent(this._parent) && !onlySelf) {
this._parent._updateTouched({onlySelf: onlySelf});
this._parent._updateTouched({onlySelf});
}
}
@ -693,7 +698,7 @@ export class FormControl extends AbstractControl {
if (this._onChange.length && emitModelToViewChange) {
this._onChange.forEach((changeFn) => changeFn(this._value, emitViewToModelChange));
}
this.updateValueAndValidity({onlySelf: onlySelf, emitEvent: emitEvent});
this.updateValueAndValidity({onlySelf, emitEvent});
}
/**
@ -740,11 +745,12 @@ export class FormControl extends AbstractControl {
* console.log(this.control.status); // 'DISABLED'
* ```
*/
reset(formState: any = null, {onlySelf}: {onlySelf?: boolean} = {}): void {
reset(formState: any = null, {onlySelf, emitEvent}: {onlySelf?: boolean,
emitEvent?: boolean} = {}): void {
this._applyFormState(formState);
this.markAsPristine({onlySelf});
this.markAsUntouched({onlySelf});
this.setValue(this._value, {onlySelf});
this.setValue(this._value, {onlySelf, emitEvent});
}
/**
@ -937,13 +943,15 @@ export class FormGroup extends AbstractControl {
*
* ```
*/
setValue(value: {[key: string]: any}, {onlySelf}: {onlySelf?: boolean} = {}): void {
setValue(
value: {[key: string]: any},
{onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
this._checkAllValuesPresent(value);
Object.keys(value).forEach(name => {
this._throwIfControlMissing(name);
this.controls[name].setValue(value[name], {onlySelf: true});
this.controls[name].setValue(value[name], {onlySelf: true, emitEvent});
});
this.updateValueAndValidity({onlySelf: onlySelf});
this.updateValueAndValidity({onlySelf, emitEvent});
}
/**
@ -967,13 +975,15 @@ export class FormGroup extends AbstractControl {
*
* ```
*/
patchValue(value: {[key: string]: any}, {onlySelf}: {onlySelf?: boolean} = {}): void {
patchValue(
value: {[key: string]: any},
{onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
Object.keys(value).forEach(name => {
if (this.controls[name]) {
this.controls[name].patchValue(value[name], {onlySelf: true});
this.controls[name].patchValue(value[name], {onlySelf: true, emitEvent});
}
});
this.updateValueAndValidity({onlySelf: onlySelf});
this.updateValueAndValidity({onlySelf, emitEvent});
}
/**
@ -1008,13 +1018,14 @@ export class FormGroup extends AbstractControl {
* console.log(this.form.get('first').status); // 'DISABLED'
* ```
*/
reset(value: any = {}, {onlySelf}: {onlySelf?: boolean} = {}): void {
reset(value: any = {}, {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}):
void {
this._forEachChild((control: AbstractControl, name: string) => {
control.reset(value[name], {onlySelf: true});
control.reset(value[name], {onlySelf: true, emitEvent});
});
this.updateValueAndValidity({onlySelf: onlySelf});
this._updatePristine({onlySelf: onlySelf});
this._updateTouched({onlySelf: onlySelf});
this.updateValueAndValidity({onlySelf, emitEvent});
this._updatePristine({onlySelf});
this._updateTouched({onlySelf});
}
/**
@ -1240,13 +1251,14 @@ export class FormArray extends AbstractControl {
* console.log(arr.value); // ['Nancy', 'Drew']
* ```
*/
setValue(value: any[], {onlySelf}: {onlySelf?: boolean} = {}): void {
setValue(value: any[], {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}):
void {
this._checkAllValuesPresent(value);
value.forEach((newValue: any, index: number) => {
this._throwIfControlMissing(index);
this.at(index).setValue(newValue, {onlySelf: true});
this.at(index).setValue(newValue, {onlySelf: true, emitEvent});
});
this.updateValueAndValidity({onlySelf: onlySelf});
this.updateValueAndValidity({onlySelf, emitEvent});
}
/**
@ -1269,13 +1281,14 @@ export class FormArray extends AbstractControl {
* console.log(arr.value); // ['Nancy', null]
* ```
*/
patchValue(value: any[], {onlySelf}: {onlySelf?: boolean} = {}): void {
patchValue(value: any[], {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}):
void {
value.forEach((newValue: any, index: number) => {
if (this.at(index)) {
this.at(index).patchValue(newValue, {onlySelf: true});
this.at(index).patchValue(newValue, {onlySelf: true, emitEvent});
}
});
this.updateValueAndValidity({onlySelf: onlySelf});
this.updateValueAndValidity({onlySelf, emitEvent});
}
/**
@ -1309,13 +1322,14 @@ export class FormArray extends AbstractControl {
* console.log(this.arr.get(0).status); // 'DISABLED'
* ```
*/
reset(value: any = [], {onlySelf}: {onlySelf?: boolean} = {}): void {
reset(value: any = [], {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}):
void {
this._forEachChild((control: AbstractControl, index: number) => {
control.reset(value[index], {onlySelf: true});
control.reset(value[index], {onlySelf: true, emitEvent});
});
this.updateValueAndValidity({onlySelf: onlySelf});
this._updatePristine({onlySelf: onlySelf});
this._updateTouched({onlySelf: onlySelf});
this.updateValueAndValidity({onlySelf, emitEvent});
this._updatePristine({onlySelf});
this._updateTouched({onlySelf});
}
/**

View File

@ -8,7 +8,6 @@
import {OpaqueToken} from '@angular/core';
import {toPromise} from 'rxjs/operator/toPromise';
import {AsyncValidatorFn, ValidatorFn} from './directives/validators';
import {StringMapWrapper} from './facade/collection';
import {isPresent} from './facade/lang';
@ -95,16 +94,24 @@ export class Validators {
/**
* Validator that requires a control to match a regex to its value.
*/
static pattern(pattern: string): ValidatorFn {
static pattern(pattern: string|RegExp): ValidatorFn {
if (!pattern) return Validators.nullValidator;
let regex: RegExp;
let regexStr: string;
if (typeof pattern === 'string') {
regexStr = `^${pattern}$`;
regex = new RegExp(regexStr);
} else {
regexStr = pattern.toString();
regex = pattern;
}
return (control: AbstractControl): {[key: string]: any} => {
if (isEmptyInputValue(control.value)) {
return null; // don't validate empty values to allow optional controls
}
const regex = new RegExp(`^${pattern}$`);
const value: string = control.value;
return regex.test(value) ?
null :
{'pattern': {'requiredPattern': `^${pattern}$`, 'actualValue': value}};
return regex.test(value) ? null :
{'pattern': {'requiredPattern': regexStr, 'actualValue': value}};
};
}
@ -119,7 +126,7 @@ export class Validators {
*/
static compose(validators: ValidatorFn[]): ValidatorFn {
if (!validators) return null;
var presentValidators = validators.filter(isPresent);
const presentValidators = validators.filter(isPresent);
if (presentValidators.length == 0) return null;
return function(control: AbstractControl) {
@ -129,7 +136,7 @@ export class Validators {
static composeAsync(validators: AsyncValidatorFn[]): AsyncValidatorFn {
if (!validators) return null;
var presentValidators = validators.filter(isPresent);
const presentValidators = validators.filter(isPresent);
if (presentValidators.length == 0) return null;
return function(control: AbstractControl) {
@ -152,7 +159,7 @@ function _executeAsyncValidators(control: AbstractControl, validators: AsyncVali
}
function _mergeErrors(arrayOfErrors: any[]): {[key: string]: any} {
var res: {[key: string]: any} =
const res: {[key: string]: any} =
arrayOfErrors.reduce((res: {[key: string]: any}, errors: {[key: string]: any}) => {
return isPresent(errors) ? StringMapWrapper.merge(res, errors) : res;
}, {});

View File

@ -158,6 +158,15 @@ export function main() {
expect(form.valueChanges).toBe(formModel.valueChanges);
});
it('should reexport control methods', () => {
expect(form.hasError('required')).toBe(formModel.hasError('required'));
expect(form.getError('required')).toBe(formModel.getError('required'));
formModel.setErrors({required: true});
expect(form.hasError('required')).toBe(formModel.hasError('required'));
expect(form.getError('required')).toBe(formModel.getError('required'));
});
describe('addControl', () => {
it('should throw when no control found', () => {
const dir = new FormControlName(form, null, null, [defaultAccessor]);
@ -329,6 +338,15 @@ export function main() {
expect(form.enabled).toBe(formModel.enabled);
});
it('should reexport control methods', () => {
expect(form.hasError('required')).toBe(formModel.hasError('required'));
expect(form.getError('required')).toBe(formModel.getError('required'));
formModel.setErrors({required: true});
expect(form.hasError('required')).toBe(formModel.hasError('required'));
expect(form.getError('required')).toBe(formModel.getError('required'));
});
describe('addControl & addFormGroup', () => {
it('should create a control with the given name', fakeAsync(() => {
form.addFormGroup(personControlGroupDir);
@ -406,6 +424,15 @@ export function main() {
expect(controlGroupDir.disabled).toBe(formModel.disabled);
expect(controlGroupDir.enabled).toBe(formModel.enabled);
});
it('should reexport control methods', () => {
expect(controlGroupDir.hasError('required')).toBe(formModel.hasError('required'));
expect(controlGroupDir.getError('required')).toBe(formModel.getError('required'));
formModel.setErrors({required: true});
expect(controlGroupDir.hasError('required')).toBe(formModel.hasError('required'));
expect(controlGroupDir.getError('required')).toBe(formModel.getError('required'));
});
});
describe('FormArrayName', () => {
@ -434,6 +461,15 @@ export function main() {
expect(formArrayDir.disabled).toBe(formModel.disabled);
expect(formArrayDir.enabled).toBe(formModel.enabled);
});
it('should reexport control methods', () => {
expect(formArrayDir.hasError('required')).toBe(formModel.hasError('required'));
expect(formArrayDir.getError('required')).toBe(formModel.getError('required'));
formModel.setErrors({required: true});
expect(formArrayDir.hasError('required')).toBe(formModel.hasError('required'));
expect(formArrayDir.getError('required')).toBe(formModel.getError('required'));
});
});
describe('FormControlDirective', () => {
@ -466,6 +502,15 @@ export function main() {
it('should reexport control properties', () => { checkProperties(control); });
it('should reexport control methods', () => {
expect(controlDir.hasError('required')).toBe(control.hasError('required'));
expect(controlDir.getError('required')).toBe(control.getError('required'));
control.setErrors({required: true});
expect(controlDir.hasError('required')).toBe(control.hasError('required'));
expect(controlDir.getError('required')).toBe(control.getError('required'));
});
it('should reexport new control properties', () => {
var newControl = new FormControl(null);
controlDir.form = newControl;
@ -486,15 +531,16 @@ export function main() {
describe('NgModel', () => {
let ngModel: NgModel;
let control: FormControl;
beforeEach(() => {
ngModel = new NgModel(
null, [Validators.required], [asyncValidator('expected')], [defaultAccessor]);
ngModel.valueAccessor = new DummyControlValueAccessor();
control = ngModel.control;
});
it('should reexport control properties', () => {
var control = ngModel.control;
expect(ngModel.control).toBe(control);
expect(ngModel.value).toBe(control.value);
expect(ngModel.valid).toBe(control.valid);
@ -511,6 +557,15 @@ export function main() {
expect(ngModel.enabled).toBe(control.enabled);
});
it('should reexport control methods', () => {
expect(ngModel.hasError('required')).toBe(control.hasError('required'));
expect(ngModel.getError('required')).toBe(control.getError('required'));
control.setErrors({required: true});
expect(ngModel.hasError('required')).toBe(control.hasError('required'));
expect(ngModel.getError('required')).toBe(control.getError('required'));
});
it('should throw when no value accessor with named control', () => {
const namedDir = new NgModel(null, null, null, null);
namedDir.name = 'one';
@ -610,6 +665,15 @@ export function main() {
expect(controlNameDir.disabled).toBe(formModel.disabled);
expect(controlNameDir.enabled).toBe(formModel.enabled);
});
it('should reexport control methods', () => {
expect(controlNameDir.hasError('required')).toBe(formModel.hasError('required'));
expect(controlNameDir.getError('required')).toBe(formModel.getError('required'));
formModel.setErrors({required: true});
expect(controlNameDir.hasError('required')).toBe(formModel.hasError('required'));
expect(controlNameDir.getError('required')).toBe(formModel.getError('required'));
});
});
});
}

View File

@ -172,6 +172,16 @@ export function main() {
expect(logger).toEqual(['control1', 'control2', 'array', 'form']);
});
it('should not fire an event when explicitly specified', fakeAsync(() => {
form.valueChanges.subscribe((value) => { throw 'Should not happen'; });
a.valueChanges.subscribe((value) => { throw 'Should not happen'; });
c.valueChanges.subscribe((value) => { throw 'Should not happen'; });
c2.valueChanges.subscribe((value) => { throw 'Should not happen'; });
a.setValue(['one', 'two'], {emitEvent: false});
tick();
}));
it('should emit one statusChange event per control', () => {
form.statusChanges.subscribe(() => logger.push('form'));
a.statusChanges.subscribe(() => logger.push('array'));
@ -277,6 +287,16 @@ export function main() {
expect(logger).toEqual(['control1', 'array', 'form']);
});
it('should not fire an event when explicitly specified', fakeAsync(() => {
form.valueChanges.subscribe((value) => { throw 'Should not happen'; });
a.valueChanges.subscribe((value) => { throw 'Should not happen'; });
c.valueChanges.subscribe((value) => { throw 'Should not happen'; });
c2.valueChanges.subscribe((value) => { throw 'Should not happen'; });
a.patchValue(['one', 'two'], {emitEvent: false});
tick();
}));
it('should emit one statusChange event per control', () => {
form.statusChanges.subscribe(() => logger.push('form'));
a.statusChanges.subscribe(() => logger.push('array'));
@ -478,6 +498,17 @@ export function main() {
expect(logger).toEqual(['control1', 'control2', 'array', 'form']);
});
it('should not fire an event when explicitly specified', fakeAsync(() => {
form.valueChanges.subscribe((value) => { throw 'Should not happen'; });
a.valueChanges.subscribe((value) => { throw 'Should not happen'; });
c.valueChanges.subscribe((value) => { throw 'Should not happen'; });
c2.valueChanges.subscribe((value) => { throw 'Should not happen'; });
c3.valueChanges.subscribe((value) => { throw 'Should not happen'; });
a.reset([], {emitEvent: false});
tick();
}));
it('should emit one statusChange event per reset control', () => {
form.statusChanges.subscribe(() => logger.push('form'));
a.statusChanges.subscribe(() => logger.push('array'));

View File

@ -555,6 +555,16 @@ export function main() {
expect(logger).toEqual(['control1', 'group']);
});
it('should not fire an event when explicitly specified', fakeAsync(() => {
g.valueChanges.subscribe((value) => { throw 'Should not happen'; });
c.valueChanges.subscribe((value) => { throw 'Should not happen'; });
c2.valueChanges.subscribe((value) => { throw 'Should not happen'; });
c.reset(null, {emitEvent: false});
tick();
}));
it('should emit one statusChange event per reset control', () => {
g.statusChanges.subscribe(() => logger.push('group'));
c.statusChanges.subscribe(() => logger.push('control1'));

View File

@ -236,6 +236,15 @@ export function main() {
expect(logger).toEqual(['control1', 'control2', 'group', 'form']);
});
it('should not fire an event when explicitly specified', fakeAsync(() => {
form.valueChanges.subscribe((value) => { throw 'Should not happen'; });
g.valueChanges.subscribe((value) => { throw 'Should not happen'; });
c.valueChanges.subscribe((value) => { throw 'Should not happen'; });
g.setValue({'one': 'one', 'two': 'two'}, {emitEvent: false});
tick();
}));
it('should emit one statusChange event per control', () => {
form.statusChanges.subscribe(() => logger.push('form'));
g.statusChanges.subscribe(() => logger.push('group'));
@ -341,6 +350,15 @@ export function main() {
expect(logger).toEqual(['control1', 'group', 'form']);
});
it('should not fire an event when explicitly specified', fakeAsync(() => {
form.valueChanges.subscribe((value) => { throw 'Should not happen'; });
g.valueChanges.subscribe((value) => { throw 'Should not happen'; });
c.valueChanges.subscribe((value) => { throw 'Should not happen'; });
g.patchValue({'one': 'one', 'two': 'two'}, {emitEvent: false});
tick();
}));
it('should emit one statusChange event per control', () => {
form.statusChanges.subscribe(() => logger.push('form'));
g.statusChanges.subscribe(() => logger.push('group'));
@ -541,6 +559,15 @@ export function main() {
expect(logger).toEqual(['control1', 'control2', 'group', 'form']);
});
it('should not fire an event when explicitly specified', fakeAsync(() => {
form.valueChanges.subscribe((value) => { throw 'Should not happen'; });
g.valueChanges.subscribe((value) => { throw 'Should not happen'; });
c.valueChanges.subscribe((value) => { throw 'Should not happen'; });
g.reset({}, {emitEvent: false});
tick();
}));
it('should emit one statusChange event per reset control', () => {
form.statusChanges.subscribe(() => logger.push('form'));
g.statusChanges.subscribe(() => logger.push('group'));

View File

@ -22,11 +22,26 @@ export function main() {
TestBed.configureTestingModule({
imports: [FormsModule, ReactiveFormsModule],
declarations: [
FormControlComp, FormGroupComp, FormArrayComp, FormArrayNestedGroup,
FormControlNameSelect, FormControlNumberInput, FormControlRadioButtons, WrappedValue,
WrappedValueForm, MyInput, MyInputForm, FormGroupNgModel, FormControlNgModel,
LoginIsEmptyValidator, LoginIsEmptyWrapper, ValidationBindingsForm, UniqLoginValidator,
UniqLoginWrapper, NestedFormGroupComp
FormControlComp,
FormGroupComp,
FormArrayComp,
FormArrayNestedGroup,
FormControlNameSelect,
FormControlNumberInput,
FormControlRangeInput,
FormControlRadioButtons,
WrappedValue,
WrappedValueForm,
MyInput,
MyInputForm,
FormGroupNgModel,
FormControlNgModel,
LoginIsEmptyValidator,
LoginIsEmptyWrapper,
ValidationBindingsForm,
UniqLoginValidator,
UniqLoginWrapper,
NestedFormGroupComp
]
});
});
@ -646,6 +661,60 @@ export function main() {
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
});
it('should work with single fields and async validators', fakeAsync(() => {
const fixture = TestBed.createComponent(FormControlComp);
const control = new FormControl('', null, uniqLoginAsyncValidator('good'));
fixture.debugElement.componentInstance.control = control;
fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input')).nativeElement;
expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-untouched']);
dispatchEvent(input, 'blur');
fixture.detectChanges();
expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-touched']);
input.value = 'good';
dispatchEvent(input, 'input');
tick();
fixture.detectChanges();
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
}));
it('should work with single fields that combines async and sync validators', fakeAsync(() => {
const fixture = TestBed.createComponent(FormControlComp);
const control =
new FormControl('', Validators.required, uniqLoginAsyncValidator('good'));
fixture.debugElement.componentInstance.control = control;
fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input')).nativeElement;
expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']);
dispatchEvent(input, 'blur');
fixture.detectChanges();
expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-touched']);
input.value = 'bad';
dispatchEvent(input, 'input');
fixture.detectChanges();
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-pending', 'ng-touched']);
tick();
fixture.detectChanges();
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-invalid', 'ng-touched']);
input.value = 'good';
dispatchEvent(input, 'input');
tick();
fixture.detectChanges();
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
}));
it('should work with single fields in parent forms', () => {
const fixture = TestBed.createComponent(FormGroupComp);
const form = new FormGroup({'login': new FormControl('', Validators.required)});
@ -1072,6 +1141,57 @@ export function main() {
});
describe('should support <type=range>', () => {
it('with basic use case', () => {
const fixture = TestBed.createComponent(FormControlRangeInput);
const control = new FormControl(10);
fixture.componentInstance.control = control;
fixture.detectChanges();
// model -> view
const input = fixture.debugElement.query(By.css('input'));
expect(input.nativeElement.value).toEqual('10');
input.nativeElement.value = '20';
dispatchEvent(input.nativeElement, 'input');
// view -> model
expect(control.value).toEqual(20);
});
it('when value is cleared in the UI', () => {
const fixture = TestBed.createComponent(FormControlNumberInput);
const control = new FormControl(10, Validators.required);
fixture.componentInstance.control = control;
fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input'));
input.nativeElement.value = '';
dispatchEvent(input.nativeElement, 'input');
expect(control.valid).toBe(false);
expect(control.value).toEqual(null);
input.nativeElement.value = '0';
dispatchEvent(input.nativeElement, 'input');
expect(control.valid).toBe(true);
expect(control.value).toEqual(0);
});
it('when value is cleared programmatically', () => {
const fixture = TestBed.createComponent(FormControlNumberInput);
const control = new FormControl(10);
fixture.componentInstance.control = control;
fixture.detectChanges();
control.setValue(null);
const input = fixture.debugElement.query(By.css('input'));
expect(input.nativeElement.value).toEqual('');
});
});
describe('custom value accessors', () => {
it('should support basic functionality', () => {
const fixture = TestBed.createComponent(WrappedValueForm);
@ -1736,7 +1856,7 @@ class LoginIsEmptyValidator {
}]
})
class UniqLoginValidator implements Validator {
@Input('uniq-login-validator') expected: any /** TODO #9100 */;
@Input('uniq-login-validator') expected: any;
validate(c: AbstractControl) { return uniqLoginAsyncValidator(this.expected)(c); }
}
@ -1798,6 +1918,16 @@ class FormControlNumberInput {
control: FormControl;
}
@Component({
selector: 'form-control-range-input',
template: `
<input type="range" [formControl]="control">
`
})
class FormControlRangeInput {
control: FormControl;
}
@Component({
selector: 'form-control-radio-buttons',
template: `

View File

@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Input} from '@angular/core';
import {Component, Directive, Input, forwardRef} from '@angular/core';
import {TestBed, async, fakeAsync, tick} from '@angular/core/testing';
import {ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, NgForm} from '@angular/forms';
import {AbstractControl, ControlValueAccessor, FormsModule, NG_ASYNC_VALIDATORS, NG_VALUE_ACCESSOR, NgForm, Validator} from '@angular/forms';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {dispatchEvent} from '@angular/platform-browser/testing/browser_util';
@ -20,9 +20,10 @@ export function main() {
TestBed.configureTestingModule({
declarations: [
StandaloneNgModel, NgModelForm, NgModelGroupForm, NgModelValidBinding, NgModelNgIfForm,
NgModelRadioForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName,
NgModelRadioForm, NgModelRangeForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName,
NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper,
NgModelValidationBindings, NgModelMultipleValidators
NgModelValidationBindings, NgModelMultipleValidators, NgAsyncValidator,
NgModelAsyncValidation
],
imports: [FormsModule]
});
@ -139,7 +140,6 @@ export function main() {
fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input')).nativeElement;
const form = fixture.debugElement.children[0].injector.get(NgForm);
expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']);
dispatchEvent(input, 'blur');
@ -154,6 +154,29 @@ export function main() {
});
}));
it('should set status classes with ngModel and async validators', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelAsyncValidation);
fixture.whenStable().then(() => {
fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input')).nativeElement;
expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-untouched']);
dispatchEvent(input, 'blur');
fixture.detectChanges();
expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-touched']);
input.value = 'updatedValue';
dispatchEvent(input, 'input');
tick();
fixture.detectChanges();
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
});
}));
it('should set status classes with ngModelGroup and ngForm', async(() => {
const fixture = TestBed.createComponent(NgModelGroupForm);
fixture.componentInstance.first = '';
@ -480,6 +503,26 @@ export function main() {
});
describe('range control', () => {
it('should support <type=range>', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelRangeForm);
// model -> view
fixture.componentInstance.val = 4;
fixture.detectChanges();
tick();
let input = fixture.debugElement.query(By.css('input'));
expect(input.nativeElement.value).toBe('4');
fixture.detectChanges();
tick();
let newVal = '4';
input.triggerEventHandler('input', {target: {value: newVal}});
tick();
// view -> model
fixture.detectChanges();
expect(typeof(fixture.componentInstance.val)).toBe('number');
}));
});
describe('radio controls', () => {
it('should support <type=radio>', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelRadioForm);
@ -883,7 +926,7 @@ export function main() {
});
});
};
}
@Component({
selector: 'standalone-ng-model',
@ -1000,6 +1043,11 @@ class NgModelOptionsStandalone {
two: string;
}
@Component({selector: 'ng-model-range-form', template: '<input type="range" [(ngModel)]="val">'})
class NgModelRangeForm {
val: any;
}
@Component({
selector: 'ng-model-radio-form',
template: `
@ -1096,6 +1144,23 @@ class NgModelMultipleValidators {
pattern: string;
}
@Directive({
selector: '[ng-async-validator]',
providers: [
{provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => NgAsyncValidator), multi: true}
]
})
class NgAsyncValidator implements Validator {
validate(c: AbstractControl) { return Promise.resolve(null); }
}
@Component({
selector: 'ng-model-async-validation',
template: `<input name="async" ngModel ng-async-validator>`
})
class NgModelAsyncValidation {
}
function sortedClassList(el: HTMLElement) {
const l = getDOM().classList(el);
l.sort();

View File

@ -17,7 +17,7 @@ import {EventEmitter} from '../src/facade/async';
export function main() {
function validator(key: string, error: any) {
return function(c: AbstractControl) {
var r: {[k: string]: string} = {};
const r: {[k: string]: string} = {};
r[key] = error;
return r;
};
@ -101,13 +101,31 @@ export function main() {
() => { expect(Validators.pattern('null')(new FormControl(null))).toBeNull(); });
it('should not error on valid strings',
() => { expect(Validators.pattern('[a-zA-Z ]*')(new FormControl('aaAA'))).toBeNull(); });
() => expect(Validators.pattern('[a-zA-Z ]*')(new FormControl('aaAA'))).toBeNull());
it('should error on failure to match string', () => {
expect(Validators.pattern('[a-zA-Z ]*')(new FormControl('aaa0'))).toEqual({
'pattern': {'requiredPattern': '^[a-zA-Z ]*$', 'actualValue': 'aaa0'}
});
});
it('should accept RegExp object', () => {
const pattern: RegExp = new RegExp('[a-zA-Z ]+');
expect(Validators.pattern(pattern)(new FormControl('aaAA'))).toBeNull();
});
it('should error on failure to match RegExp object', () => {
const pattern: RegExp = new RegExp('^[a-zA-Z ]*$');
expect(Validators.pattern(pattern)(new FormControl('aaa0'))).toEqual({
'pattern': {'requiredPattern': '/^[a-zA-Z ]*$/', 'actualValue': 'aaa0'}
});
});
it('should not error on "null" pattern',
() => expect(Validators.pattern(null)(new FormControl('aaAA'))).toBeNull());
it('should not error on "undefined" pattern',
() => expect(Validators.pattern(undefined)(new FormControl('aaAA'))).toBeNull());
});
describe('compose', () => {
@ -115,22 +133,22 @@ export function main() {
() => { expect(Validators.compose(null)).toBe(null); });
it('should collect errors from all the validators', () => {
var c = Validators.compose([validator('a', true), validator('b', true)]);
const c = Validators.compose([validator('a', true), validator('b', true)]);
expect(c(new FormControl(''))).toEqual({'a': true, 'b': true});
});
it('should run validators left to right', () => {
var c = Validators.compose([validator('a', 1), validator('a', 2)]);
const c = Validators.compose([validator('a', 1), validator('a', 2)]);
expect(c(new FormControl(''))).toEqual({'a': 2});
});
it('should return null when no errors', () => {
var c = Validators.compose([Validators.nullValidator, Validators.nullValidator]);
const c = Validators.compose([Validators.nullValidator, Validators.nullValidator]);
expect(c(new FormControl(''))).toBeNull();
});
it('should ignore nulls', () => {
var c = Validators.compose([null, Validators.required]);
const c = Validators.compose([null, Validators.required]);
expect(c(new FormControl(''))).toEqual({'required': true});
});
});

View File

@ -21,7 +21,6 @@ function createNode(curr: TreeNode<ActivatedRouteSnapshot>, prevState?: TreeNode
if (prevState && equalRouteSnapshots(prevState.value.snapshot, curr.value)) {
const value = prevState.value;
value._futureSnapshot = curr.value;
const children = createOrReuseChildren(curr, prevState);
return new TreeNode<ActivatedRoute>(value, children);

View File

@ -18,6 +18,7 @@ export {RouterOutletMap} from './router_outlet_map';
export {NoPreloading, PreloadAllModules, PreloadingStrategy} from './router_preloader';
export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
export {PRIMARY_OUTLET, Params} from './shared';
export {DefaultUrlSerializer, UrlSegment, UrlSerializer, UrlTree} from './url_tree';
export {UrlHandlingStrategy} from './url_handling_strategy';
export {DefaultUrlSerializer, UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
export * from './private_export'

View File

@ -30,6 +30,7 @@ import {LoadedRouterConfig, RouterConfigLoader} from './router_config_loader';
import {RouterOutletMap} from './router_outlet_map';
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from './router_state';
import {NavigationCancelingError, PRIMARY_OUTLET, Params} from './shared';
import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy';
import {UrlSerializer, UrlTree, containsTree, createEmptyUrlTree} from './url_tree';
import {andObservables, forEach, merge, shallowEqual, waitForMap, wrapIntoObservable} from './utils/collection';
import {TreeNode} from './utils/tree';
@ -285,6 +286,9 @@ function defaultErrorHandler(error: any): any {
*/
export class Router {
private currentUrlTree: UrlTree;
private rawUrlTree: UrlTree;
private lastNavigation: UrlTree;
private currentRouterState: RouterState;
private locationSubscription: Subscription;
private routerEvents: Subject<Event>;
@ -303,6 +307,11 @@ export class Router {
*/
navigated: boolean = false;
/**
* Extracts and merges URLs. Used for Angular 1 to Angular 2 migrations.
*/
urlHandlingStrategy: UrlHandlingStrategy = new DefaultUrlHandlingStrategy();
/**
* Creates the router service.
*/
@ -314,6 +323,7 @@ export class Router {
this.resetConfig(config);
this.routerEvents = new Subject<Event>();
this.currentUrlTree = createEmptyUrlTree();
this.rawUrlTree = this.currentUrlTree;
this.configLoader = new RouterConfigLoader(loader, compiler);
this.currentRouterState = createEmptyState(this.currentUrlTree, this.rootComponentType);
}
@ -344,12 +354,20 @@ export class Router {
// Zone.current.wrap is needed because of the issue with RxJS scheduler,
// which does not work properly with zone.js in IE and Safari
this.locationSubscription = <any>this.location.subscribe(Zone.current.wrap((change: any) => {
const tree = this.urlSerializer.parse(change['url']);
// we fire multiple events for a single URL change
// we should navigate only once
return this.currentUrlTree.toString() !== tree.toString() ?
this.scheduleNavigation(tree, {skipLocationChange: change['pop'], replaceUrl: true}) :
null;
const rawUrlTree = this.urlSerializer.parse(change['url']);
const tree = this.urlHandlingStrategy.extract(rawUrlTree);
setTimeout(() => {
// we fire multiple events for a single URL change
// we should navigate only once
if (!this.lastNavigation || this.lastNavigation.toString() !== tree.toString()) {
this.scheduleNavigation(
rawUrlTree, tree, {skipLocationChange: change['pop'], replaceUrl: true});
} else {
this.rawUrlTree = rawUrlTree;
}
}, 0);
}));
}
@ -470,10 +488,10 @@ export class Router {
navigateByUrl(url: string|UrlTree, extras: NavigationExtras = {skipLocationChange: false}):
Promise<boolean> {
if (url instanceof UrlTree) {
return this.scheduleNavigation(url, extras);
return this.scheduleNavigation(this.rawUrlTree, url, extras);
} else {
const urlTree = this.urlSerializer.parse(url);
return this.scheduleNavigation(urlTree, extras);
return this.scheduleNavigation(this.rawUrlTree, urlTree, extras);
}
}
@ -500,7 +518,7 @@ export class Router {
*/
navigate(commands: any[], extras: NavigationExtras = {skipLocationChange: false}):
Promise<boolean> {
return this.scheduleNavigation(this.createUrlTree(commands, extras), extras);
return this.scheduleNavigation(this.rawUrlTree, this.createUrlTree(commands, extras), extras);
}
/**
@ -525,16 +543,34 @@ export class Router {
}
}
private scheduleNavigation(url: UrlTree, extras: NavigationExtras): Promise<boolean> {
const id = ++this.navigationId;
this.routerEvents.next(new NavigationStart(id, this.serializeUrl(url)));
return Promise.resolve().then(
(_) => this.runNavigate(url, extras.skipLocationChange, extras.replaceUrl, id));
private scheduleNavigation(rawUrl: UrlTree, url: UrlTree, extras: NavigationExtras):
Promise<boolean> {
if (this.urlHandlingStrategy.shouldProcessUrl(url)) {
const id = ++this.navigationId;
this.routerEvents.next(new NavigationStart(id, this.serializeUrl(url)));
return Promise.resolve().then(
(_) => this.runNavigate(
rawUrl, url, extras.skipLocationChange, extras.replaceUrl, id, null));
// we cannot process the current URL, but we could process the previous one =>
// we need to do some cleanup
} else if (this.urlHandlingStrategy.shouldProcessUrl(this.rawUrlTree)) {
const id = ++this.navigationId;
this.routerEvents.next(new NavigationStart(id, this.serializeUrl(url)));
return Promise.resolve().then(
(_) => this.runNavigate(
rawUrl, url, false, false, id, createEmptyState(url, this.rootComponentType)));
} else {
this.rawUrlTree = rawUrl;
return Promise.resolve(null);
}
}
private runNavigate(
url: UrlTree, shouldPreventPushState: boolean, shouldReplaceUrl: boolean,
id: number): Promise<boolean> {
rawUrl: UrlTree, url: UrlTree, shouldPreventPushState: boolean, shouldReplaceUrl: boolean,
id: number, precreatedState: RouterState): Promise<boolean> {
if (id !== this.navigationId) {
this.location.go(this.urlSerializer.serialize(this.currentUrlTree));
this.routerEvents.next(new NavigationCancel(
@ -553,23 +589,33 @@ export class Router {
const storedState = this.currentRouterState;
const storedUrl = this.currentUrlTree;
const redirectsApplied$ = applyRedirects(this.injector, this.configLoader, url, this.config);
let routerState$: any;
const snapshot$ = mergeMap.call(redirectsApplied$, (u: UrlTree) => {
appliedUrl = u;
return recognize(
this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl));
});
if (!precreatedState) {
const redirectsApplied$ =
applyRedirects(this.injector, this.configLoader, url, this.config);
const emitRecognzied$ = map.call(snapshot$, (newRouterStateSnapshot: RouterStateSnapshot) => {
this.routerEvents.next(new RoutesRecognized(
id, this.serializeUrl(url), this.serializeUrl(appliedUrl), newRouterStateSnapshot));
return newRouterStateSnapshot;
});
const snapshot$ = mergeMap.call(redirectsApplied$, (u: UrlTree) => {
appliedUrl = u;
return recognize(
this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl));
});
const routerState$ = map.call(emitRecognzied$, (routerStateSnapshot: RouterStateSnapshot) => {
return createRouterState(routerStateSnapshot, this.currentRouterState);
});
const emitRecognzied$ =
map.call(snapshot$, (newRouterStateSnapshot: RouterStateSnapshot) => {
this.routerEvents.next(new RoutesRecognized(
id, this.serializeUrl(url), this.serializeUrl(appliedUrl),
newRouterStateSnapshot));
return newRouterStateSnapshot;
});
routerState$ = map.call(emitRecognzied$, (routerStateSnapshot: RouterStateSnapshot) => {
return createRouterState(routerStateSnapshot, this.currentRouterState);
});
} else {
appliedUrl = url;
routerState$ = of (precreatedState);
}
const preactivation$ = map.call(routerState$, (newState: RouterState) => {
state = newState;
@ -595,11 +641,14 @@ export class Router {
return;
}
this.lastNavigation = appliedUrl;
this.currentUrlTree = appliedUrl;
this.rawUrlTree = this.urlHandlingStrategy.merge(this.currentUrlTree, rawUrl);
this.currentRouterState = state;
if (!shouldPreventPushState) {
let path = this.urlSerializer.serialize(appliedUrl);
let path = this.urlSerializer.serialize(this.rawUrlTree);
if (this.location.isCurrentPathEqualTo(path) || shouldReplaceUrl) {
this.location.replaceState(path);
} else {
@ -641,7 +690,8 @@ export class Router {
if (id === this.navigationId) {
this.currentRouterState = storedState;
this.currentUrlTree = storedUrl;
this.location.replaceState(this.serializeUrl(storedUrl));
this.rawUrlTree = this.urlHandlingStrategy.merge(this.currentUrlTree, rawUrl);
this.location.replaceState(this.serializeUrl(this.rawUrlTree));
}
});
});

View File

@ -18,6 +18,7 @@ import {ROUTES} from './router_config_loader';
import {RouterOutletMap} from './router_outlet_map';
import {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader';
import {ActivatedRoute} from './router_state';
import {UrlHandlingStrategy} from './url_handling_strategy';
import {DefaultUrlSerializer, UrlSerializer} from './url_tree';
import {flatten} from './utils/collection';
@ -55,7 +56,7 @@ export const ROUTER_PROVIDERS: Provider[] = [
useFactory: setupRouter,
deps: [
ApplicationRef, UrlSerializer, RouterOutletMap, Location, Injector, NgModuleFactoryLoader,
Compiler, ROUTES, ROUTER_CONFIGURATION
Compiler, ROUTES, ROUTER_CONFIGURATION, [UrlHandlingStrategy, new Optional()]
]
},
RouterOutletMap, {provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]},
@ -236,16 +237,20 @@ export interface ExtraOptions {
export function setupRouter(
ref: ApplicationRef, urlSerializer: UrlSerializer, outletMap: RouterOutletMap,
location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler,
config: Route[][], opts: ExtraOptions = {}) {
const r = new Router(
config: Route[][], opts: ExtraOptions = {}, urlHandlingStrategy?: UrlHandlingStrategy) {
const router = new Router(
null, urlSerializer, outletMap, location, injector, loader, compiler, flatten(config));
if (urlHandlingStrategy) {
router.urlHandlingStrategy = urlHandlingStrategy;
}
if (opts.errorHandler) {
r.errorHandler = opts.errorHandler;
router.errorHandler = opts.errorHandler;
}
if (opts.enableTracing) {
r.events.subscribe(e => {
router.events.subscribe(e => {
console.group(`Router Event: ${(<any>e.constructor).name}`);
console.log(e.toString());
console.log(e);
@ -253,7 +258,7 @@ export function setupRouter(
});
}
return r;
return router;
}
export function rootRoute(router: Router): ActivatedRoute {

View File

@ -0,0 +1,46 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {UrlTree} from './url_tree';
/**
* @whatItDoes Provides a way to migrate Angular 1 applications to Angular 2.
*
* @experimental
*/
export abstract class UrlHandlingStrategy {
/**
* Tells the router if this URL should be processed.
*
* When it returns true, the router will execute the regular navigation.
* When it returns false, the router will set the router state to an empty state.
* As a result, all the active components will be destroyed.
*
*/
abstract shouldProcessUrl(url: UrlTree): boolean;
/**
* Extracts the part of the URL that should be handled by the router.
* The rest of the URL will remain untouched.
*/
abstract extract(url: UrlTree): UrlTree;
/**
* Merges the URL fragment with the rest of the URL.
*/
abstract merge(newUrlPart: UrlTree, rawUrl: UrlTree): UrlTree;
}
/**
* @experimental
*/
export class DefaultUrlHandlingStrategy implements UrlHandlingStrategy {
shouldProcessUrl(url: UrlTree): boolean { return true; }
extract(url: UrlTree): UrlTree { return url; }
merge(newUrlPart: UrlTree, wholeUrl: UrlTree): UrlTree { return newUrlPart; }
}

View File

@ -14,8 +14,9 @@ import {Observable} from 'rxjs/Observable';
import {of } from 'rxjs/observable/of';
import {map} from 'rxjs/operator/map';
import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Params, PreloadAllModules, PreloadingStrategy, Resolve, Router, RouterModule, RouterStateSnapshot, RoutesRecognized} from '../index';
import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, Params, PreloadAllModules, PreloadingStrategy, Resolve, Router, RouterModule, RouterStateSnapshot, RoutesRecognized, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '../index';
import {RouterPreloader} from '../src/router_preloader';
import {forEach} from '../src/utils/collection';
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
@ -58,7 +59,6 @@ describe('Integration', () => {
})));
it('should work when an outlet is in an ngIf (and is removed)', fakeAsync(() => {
@Component({
selector: 'someRoot',
template: `<div *ngIf="cond"><router-outlet></router-outlet></div>`
@ -87,6 +87,7 @@ describe('Integration', () => {
let recordedError: any = null;
router.navigateByUrl('/blank').catch(e => recordedError = e);
advance(fixture);
expect(recordedError.message).toEqual('Cannot find primary outlet to load \'BlankCmp\'');
}));
@ -1813,10 +1814,146 @@ describe('Integration', () => {
})));
});
describe('custom url handling strategies', () => {
class CustomUrlHandlingStrategy implements UrlHandlingStrategy {
shouldProcessUrl(url: UrlTree): boolean {
return url.toString().startsWith('/include') || url.toString() === '/';
}
extract(url: UrlTree): UrlTree {
const oldRoot = url.root;
const root = new UrlSegmentGroup(
oldRoot.segments, {[PRIMARY_OUTLET]: oldRoot.children[PRIMARY_OUTLET]});
return new UrlTree(root, url.queryParams, url.fragment);
}
merge(newUrlPart: UrlTree, wholeUrl: UrlTree): UrlTree {
const oldRoot = newUrlPart.root;
const children: any = {};
if (oldRoot.children[PRIMARY_OUTLET]) {
children[PRIMARY_OUTLET] = oldRoot.children[PRIMARY_OUTLET];
}
forEach(wholeUrl.root.children, (v: any, k: any) => {
if (k !== PRIMARY_OUTLET) {
children[k] = v;
}
v.parent = this;
});
const root = new UrlSegmentGroup(oldRoot.segments, children);
return new UrlTree(root, newUrlPart.queryParams, newUrlPart.fragment);
}
}
beforeEach(() => {
TestBed.configureTestingModule(
{providers: [{provide: UrlHandlingStrategy, useClass: CustomUrlHandlingStrategy}]});
});
it('should work',
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
const fixture = createRoot(router, RootCmp);
router.resetConfig([{
path: 'include',
component: TeamCmp,
children: [
{path: 'user/:name', component: UserCmp}, {path: 'simple', component: SimpleCmp}
]
}]);
let events: any[] = [];
router.events.subscribe(e => events.push(e));
// supported URL
router.navigateByUrl('/include/user/kate');
advance(fixture);
expect(location.path()).toEqual('/include/user/kate');
expectEvents(events, [
[NavigationStart, '/include/user/kate'], [RoutesRecognized, '/include/user/kate'],
[NavigationEnd, '/include/user/kate']
]);
expect(fixture.nativeElement).toHaveText('team [ user kate, right: ]');
events.splice(0);
// unsupported URL
router.navigateByUrl('/exclude/one');
advance(fixture);
expect(location.path()).toEqual('/exclude/one');
expect(Object.keys(router.routerState.root.children).length).toEqual(0);
expect(fixture.nativeElement).toHaveText('');
expectEvents(
events, [[NavigationStart, '/exclude/one'], [NavigationEnd, '/exclude/one']]);
events.splice(0);
// another unsupported URL
location.go('/exclude/two');
advance(fixture);
expect(location.path()).toEqual('/exclude/two');
expectEvents(events, []);
// back to a supported URL
location.go('/include/simple');
advance(fixture);
expect(location.path()).toEqual('/include/simple');
expect(fixture.nativeElement).toHaveText('team [ simple, right: ]');
expectEvents(events, [
[NavigationStart, '/include/simple'], [RoutesRecognized, '/include/simple'],
[NavigationEnd, '/include/simple']
]);
})));
it('should handle the case when the router takes only the primary url',
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
const fixture = createRoot(router, RootCmp);
router.resetConfig([{
path: 'include',
component: TeamCmp,
children: [
{path: 'user/:name', component: UserCmp}, {path: 'simple', component: SimpleCmp}
]
}]);
let events: any[] = [];
router.events.subscribe(e => events.push(e));
location.go('/include/user/kate(aux:excluded)');
advance(fixture);
expect(location.path()).toEqual('/include/user/kate(aux:excluded)');
expectEvents(events, [
[NavigationStart, '/include/user/kate'], [RoutesRecognized, '/include/user/kate'],
[NavigationEnd, '/include/user/kate']
]);
events.splice(0);
location.go('/include/user/kate(aux:excluded2)');
advance(fixture);
expectEvents(events, []);
router.navigateByUrl('/include/simple');
advance(fixture);
expect(location.path()).toEqual('/include/simple(aux:excluded2)');
expectEvents(events, [
[NavigationStart, '/include/simple'], [RoutesRecognized, '/include/simple'],
[NavigationEnd, '/include/simple']
]);
})));
});
});
});
function expectEvents(events: Event[], pairs: any[]) {
expect(events.length).toEqual(pairs.length);
for (let i = 0; i < events.length; ++i) {
expect((<any>events[i].constructor).name).toBe(pairs[i][0].name);
expect((<any>events[i]).url).toBe(pairs[i][1]);
@ -1827,11 +1964,7 @@ function expectEvents(events: Event[], pairs: any[]) {
class StringLinkCmp {
}
@Component({
selector: 'link-cmp',
template: `<button routerLink
="/team/33/simple">link</button>`
})
@Component({selector: 'link-cmp', template: `<button routerLink="/team/33/simple">link</button>`})
class StringLinkButtonCmp {
}

View File

@ -8,12 +8,13 @@
import {Location, LocationStrategy} from '@angular/common';
import {MockLocationStrategy, SpyLocation} from '@angular/common/testing';
import {Compiler, Injectable, Injector, ModuleWithProviders, NgModule, NgModuleFactory, NgModuleFactoryLoader} from '@angular/core';
import {NoPreloading, PreloadingStrategy, Route, Router, RouterModule, RouterOutletMap, Routes, UrlSerializer, provideRoutes} from '@angular/router';
import {Compiler, Injectable, Injector, ModuleWithProviders, NgModule, NgModuleFactory, NgModuleFactoryLoader, Optional} from '@angular/core';
import {NoPreloading, PreloadingStrategy, Route, Router, RouterModule, RouterOutletMap, Routes, UrlHandlingStrategy, UrlSerializer, provideRoutes} from '@angular/router';
import {ROUTER_PROVIDERS, ROUTES, flatten} from './private_import_router';
/**
* @whatItDoes Allows to simulate the loading of ng modules in tests.
*
@ -84,9 +85,14 @@ export class SpyNgModuleFactoryLoader implements NgModuleFactoryLoader {
*/
export function setupTestingRouter(
urlSerializer: UrlSerializer, outletMap: RouterOutletMap, location: Location,
loader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector, routes: Route[][]) {
return new Router(
loader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector, routes: Route[][],
urlHandlingStrategy?: UrlHandlingStrategy) {
const router = new Router(
null, urlSerializer, outletMap, location, injector, loader, compiler, flatten(routes));
if (urlHandlingStrategy) {
router.urlHandlingStrategy = urlHandlingStrategy;
}
return router;
}
/**
@ -123,7 +129,8 @@ export function setupTestingRouter(
provide: Router,
useFactory: setupTestingRouter,
deps: [
UrlSerializer, RouterOutletMap, Location, NgModuleFactoryLoader, Compiler, Injector, ROUTES
UrlSerializer, RouterOutletMap, Location, NgModuleFactoryLoader, Compiler, Injector, ROUTES,
[UrlHandlingStrategy, new Optional()]
]
},
{provide: PreloadingStrategy, useExisting: NoPreloading}, provideRoutes([])

View File

@ -12,5 +12,5 @@
* Entry point for all public APIs of the upgrade package.
*/
export * from './src/upgrade';
export * from './src/aot';
// This file only reexports content of the `src` folder. Keep it that way.

View File

@ -6,20 +6,28 @@
* found in the LICENSE file at https://angular.io/license
*/
export type Ng1Token = string;
export interface IAnnotatedFunction extends Function { $inject?: Ng1Token[]; }
export type IInjectable = (Ng1Token | Function)[] | IAnnotatedFunction;
export interface IModule {
config(fn: any): IModule;
directive(selector: string, factory: any): IModule;
name: string;
requires: (string|IInjectable)[];
config(fn: IInjectable): IModule;
directive(selector: string, factory: IInjectable): IModule;
component(selector: string, component: IComponent): IModule;
controller(name: string, type: any): IModule;
factory(key: string, factoryFn: any): IModule;
value(key: string, value: any): IModule;
run(a: any): void;
controller(name: string, type: IInjectable): IModule;
factory(key: Ng1Token, factoryFn: IInjectable): IModule;
value(key: Ng1Token, value: any): IModule;
run(a: IInjectable): IModule;
}
export interface ICompileService {
(element: Element|NodeList|string, transclude?: Function): ILinkFn;
}
export interface ILinkFn {
(scope: IScope, cloneAttachFn?: Function, options?: ILinkFnOptions): void;
(scope: IScope, cloneAttachFn?: ICloneAttachFunction, options?: ILinkFnOptions): IAugmentedJQuery;
}
export interface ILinkFnOptions {
parentBoundTranscludeFn?: Function;
@ -29,35 +37,42 @@ export interface ILinkFnOptions {
export interface IRootScopeService {
$new(isolate?: boolean): IScope;
$id: string;
$parent: IScope;
$root: IScope;
$watch(expr: any, fn?: (a1?: any, a2?: any) => void): Function;
$destroy(): any;
$apply(): any;
$apply(exp: string): any;
$apply(exp: Function): any;
$evalAsync(): any;
$on(event: string, fn?: (event?: any, ...args: any[]) => void): Function;
$$childTail: IScope;
$$childHead: IScope;
$$nextSibling: IScope;
[key: string]: any;
}
export interface IScope extends IRootScopeService {}
export interface IAngularBootstrapConfig {}
;
export interface IAngularBootstrapConfig { strictDi?: boolean; }
export interface IDirective {
compile?: IDirectiveCompileFn;
controller?: any;
controller?: IController;
controllerAs?: string;
bindToController?: boolean|Object;
bindToController?: boolean|{[key: string]: string};
link?: IDirectiveLinkFn|IDirectivePrePost;
name?: string;
priority?: number;
replace?: boolean;
require?: any;
require?: DirectiveRequireProperty;
restrict?: string;
scope?: any;
template?: any;
templateUrl?: any;
scope?: boolean|{[key: string]: string};
template?: string|Function;
templateUrl?: string|Function;
templateNamespace?: string;
terminal?: boolean;
transclude?: any;
transclude?: boolean|'element'|{[key: string]: string};
}
export type DirectiveRequireProperty = Ng1Token[] | Ng1Token | {[key: string]: Ng1Token};
export interface IDirectiveCompileFn {
(templateElement: IAugmentedJQuery, templateAttributes: IAttributes,
transclude: ITranscludeFunction): IDirectivePrePost;
@ -71,13 +86,13 @@ export interface IDirectiveLinkFn {
controller: any, transclude: ITranscludeFunction): void;
}
export interface IComponent {
bindings?: Object;
controller?: any;
bindings?: {[key: string]: string};
controller?: string|IInjectable;
controllerAs?: string;
require?: any;
template?: any;
templateUrl?: any;
transclude?: any;
require?: DirectiveRequireProperty;
template?: string|Function;
templateUrl?: string|Function;
transclude?: boolean;
}
export interface IAttributes { $observe(attr: string, fn: (v: string) => void): void; }
export interface ITranscludeFunction {
@ -90,14 +105,25 @@ export interface ICloneAttachFunction {
// Let's hint but not force cloneAttachFn's signature
(clonedElement?: IAugmentedJQuery, scope?: IScope): any;
}
export interface IAugmentedJQuery {
bind(name: string, fn: () => void): void;
data(name: string, value?: any): any;
inheritedData(name: string, value?: any): any;
contents(): IAugmentedJQuery;
parent(): IAugmentedJQuery;
length: number;
[index: number]: Node;
export type IAugmentedJQuery = Node[] & {
bind?: (name: string, fn: () => void) => void;
data?: (name: string, value?: any) => any;
inheritedData?: (name: string, value?: any) => any;
contents?: () => IAugmentedJQuery;
parent?: () => IAugmentedJQuery;
empty?: () => void;
append?: (content: IAugmentedJQuery | string) => IAugmentedJQuery;
controller?: (name: string) => any;
isolateScope?: () => IScope;
};
export interface IProvider { $get: IInjectable; }
export interface IProvideService {
provider(token: Ng1Token, provider: IProvider): IProvider;
factory(token: Ng1Token, factory: IInjectable): IProvider;
service(token: Ng1Token, type: IInjectable): IProvider;
value(token: Ng1Token, value: any): IProvider;
constant(token: Ng1Token, value: any): void;
decorator(token: Ng1Token, factory: IInjectable): void;
}
export interface IParseService { (expression: string): ICompiledExpression; }
export interface ICompiledExpression { assign(context: any, value: any): any; }
@ -110,8 +136,9 @@ export interface ICacheObject {
get(key: string): any;
}
export interface ITemplateCacheService extends ICacheObject {}
export type IController = string | IInjectable;
export interface IControllerService {
(controllerConstructor: Function, locals?: any, later?: any, ident?: any): any;
(controllerConstructor: IController, locals?: any, later?: any, ident?: any): any;
(controllerName: string, locals?: any): any;
}
@ -133,7 +160,8 @@ function noNg() {
}
var angular: {
bootstrap: (e: Element, modules: string[], config: IAngularBootstrapConfig) => void,
bootstrap: (e: Element, modules: (string | IInjectable)[], config: IAngularBootstrapConfig) =>
void,
module: (prefix: string, dependencies?: string[]) => IModule,
element: (e: Element) => IAugmentedJQuery,
version: {major: number}, resumeBootstrap?: () => void,

View File

@ -0,0 +1,12 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export {downgradeComponent} from './aot/downgrade_component';
export {downgradeInjectable} from './aot/downgrade_injectable';
export {UpgradeComponent} from './aot/upgrade_component';
export {UpgradeModule} from './aot/upgrade_module';

View File

@ -0,0 +1,46 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as angular from '../angular_js';
// We have to do a little dance to get the ng1 injector into the module injector.
// We store the ng1 injector so that the provider in the module injector can access it
// Then we "get" the ng1 injector from the module injector, which triggers the provider to read
// the stored injector and release the reference to it.
let tempInjectorRef: angular.IInjectorService;
export function setTempInjectorRef(injector: angular.IInjectorService) {
tempInjectorRef = injector;
}
export function injectorFactory() {
const injector: angular.IInjectorService = tempInjectorRef;
tempInjectorRef = null; // clear the value to prevent memory leaks
return injector;
}
export function rootScopeFactory(i: angular.IInjectorService) {
return i.get('$rootScope');
}
export function compileFactory(i: angular.IInjectorService) {
return i.get('$compile');
}
export function parseFactory(i: angular.IInjectorService) {
return i.get('$parse');
}
export const angular1Providers = [
// We must use exported named functions for the ng2 factories to keep the compiler happy:
// > Metadata collected contains an error that will be reported at runtime:
// > Function calls are not supported.
// > Consider replacing the function or lambda with a reference to an exported function
{provide: '$injector', useFactory: injectorFactory},
{provide: '$rootScope', useFactory: rootScopeFactory, deps: ['$injector']},
{provide: '$compile', useFactory: compileFactory, deps: ['$injector']},
{provide: '$parse', useFactory: parseFactory, deps: ['$injector']}
];

View File

@ -0,0 +1,41 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Type} from '@angular/core';
export interface ComponentInfo {
component: Type<any>;
inputs?: string[];
outputs?: string[];
}
export class PropertyBinding {
prop: string;
attr: string;
bracketAttr: string;
bracketParenAttr: string;
parenAttr: string;
onAttr: string;
bindAttr: string;
bindonAttr: string;
constructor(public binding: string) { this.parseBinding(); }
private parseBinding() {
const parts = this.binding.split(':');
this.prop = parts[0].trim();
this.attr = (parts[1] || this.prop).trim();
this.bracketAttr = `[${this.attr}]`;
this.parenAttr = `(${this.attr})`;
this.bracketParenAttr = `[(${this.attr})]`;
const capitalAttr = this.attr.charAt(0).toUpperCase() + this.attr.substr(1);
this.onAttr = `on${capitalAttr}`;
this.bindAttr = `bind${capitalAttr}`;
this.bindonAttr = `bindon${capitalAttr}`;
}
}

View File

@ -0,0 +1,19 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export const UPGRADE_MODULE_NAME = '$$UpgradeModule';
export const INJECTOR_KEY = '$$angularInjector';
export const $INJECTOR = '$injector';
export const $PARSE = '$parse';
export const $SCOPE = '$scope';
export const $COMPILE = '$compile';
export const $TEMPLATE_CACHE = '$templateCache';
export const $HTTP_BACKEND = '$httpBackend';
export const $CONTROLLER = '$controller';

View File

@ -0,0 +1,64 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ComponentFactory, ComponentFactoryResolver, Injector} from '@angular/core';
import * as angular from '../angular_js';
import {ComponentInfo} from './component_info';
import {$INJECTOR, $PARSE, INJECTOR_KEY} from './constants';
import {DowngradeComponentAdapter} from './downgrade_component_adapter';
let downgradeCount = 0;
/**
* @experimental
*/
export function downgradeComponent(info: ComponentInfo): angular.IInjectable {
const idPrefix = `NG2_UPGRADE_${downgradeCount++}_`;
let idCount = 0;
const directiveFactory:
angular.IAnnotatedFunction = function(
$injector: angular.IInjectorService,
$parse: angular.IParseService): angular.IDirective {
return {
restrict: 'E',
require: '?^' + INJECTOR_KEY,
link: (scope: angular.IScope, element: angular.IAugmentedJQuery, attrs: angular.IAttributes,
parentInjector: Injector, transclude: angular.ITranscludeFunction) => {
if (parentInjector === null) {
parentInjector = $injector.get(INJECTOR_KEY);
}
const componentFactoryResolver: ComponentFactoryResolver =
parentInjector.get(ComponentFactoryResolver);
const componentFactory: ComponentFactory<any> =
componentFactoryResolver.resolveComponentFactory(info.component);
if (!componentFactory) {
throw new Error('Expecting ComponentFactory for: ' + info.component);
}
const facade = new DowngradeComponentAdapter(
idPrefix + (idCount++), info, element, attrs, scope, parentInjector, $parse,
componentFactory);
facade.setupInputs();
facade.createComponent();
facade.projectContent();
facade.setupOutputs();
facade.registerCleanup();
}
};
};
directiveFactory.$inject = [$INJECTOR, $PARSE];
return directiveFactory;
}

View File

@ -0,0 +1,180 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, ReflectiveInjector, SimpleChange, SimpleChanges, Type} from '@angular/core';
import * as angular from '../angular_js';
import {ComponentInfo, PropertyBinding} from './component_info';
import {$SCOPE} from './constants';
const INITIAL_VALUE = {
__UNINITIALIZED__: true
};
export class DowngradeComponentAdapter {
component: any = null;
inputs: Attr;
inputChangeCount: number = 0;
inputChanges: SimpleChanges = null;
componentRef: ComponentRef<any> = null;
changeDetector: ChangeDetectorRef = null;
componentScope: angular.IScope;
childNodes: Node[];
contentInsertionPoint: Node = null;
constructor(
private id: string, private info: ComponentInfo, private element: angular.IAugmentedJQuery,
private attrs: angular.IAttributes, private scope: angular.IScope,
private parentInjector: Injector, private parse: angular.IParseService,
private componentFactory: ComponentFactory<any>) {
(<any>this.element[0]).id = id;
this.componentScope = scope.$new();
this.childNodes = <Node[]><any>element.contents();
}
createComponent() {
var childInjector = ReflectiveInjector.resolveAndCreate(
[{provide: $SCOPE, useValue: this.componentScope}], this.parentInjector);
this.contentInsertionPoint = document.createComment('ng1 insertion point');
this.componentRef = this.componentFactory.create(
childInjector, [[this.contentInsertionPoint]], this.element[0]);
this.changeDetector = this.componentRef.changeDetectorRef;
this.component = this.componentRef.instance;
}
setupInputs(): void {
var attrs = this.attrs;
var inputs = this.info.inputs || [];
for (var i = 0; i < inputs.length; i++) {
var input = new PropertyBinding(inputs[i]);
var expr: any /** TODO #9100 */ = null;
if (attrs.hasOwnProperty(input.attr)) {
var observeFn = ((prop: any /** TODO #9100 */) => {
var prevValue = INITIAL_VALUE;
return (value: any /** TODO #9100 */) => {
if (this.inputChanges !== null) {
this.inputChangeCount++;
this.inputChanges[prop] =
new Ng1Change(value, prevValue === INITIAL_VALUE ? value : prevValue);
prevValue = value;
}
this.component[prop] = value;
};
})(input.prop);
attrs.$observe(input.attr, observeFn);
} else if (attrs.hasOwnProperty(input.bindAttr)) {
expr = (attrs as any /** TODO #9100 */)[input.bindAttr];
} else if (attrs.hasOwnProperty(input.bracketAttr)) {
expr = (attrs as any /** TODO #9100 */)[input.bracketAttr];
} else if (attrs.hasOwnProperty(input.bindonAttr)) {
expr = (attrs as any /** TODO #9100 */)[input.bindonAttr];
} else if (attrs.hasOwnProperty(input.bracketParenAttr)) {
expr = (attrs as any /** TODO #9100 */)[input.bracketParenAttr];
}
if (expr != null) {
var watchFn =
((prop: any /** TODO #9100 */) =>
(value: any /** TODO #9100 */, prevValue: any /** TODO #9100 */) => {
if (this.inputChanges != null) {
this.inputChangeCount++;
this.inputChanges[prop] = new Ng1Change(prevValue, value);
}
this.component[prop] = value;
})(input.prop);
this.componentScope.$watch(expr, watchFn);
}
}
var prototype = this.info.component.prototype;
if (prototype && (<OnChanges>prototype).ngOnChanges) {
// Detect: OnChanges interface
this.inputChanges = {};
this.componentScope.$watch(() => this.inputChangeCount, () => {
var inputChanges = this.inputChanges;
this.inputChanges = {};
(<OnChanges>this.component).ngOnChanges(inputChanges);
});
}
this.componentScope.$watch(() => this.changeDetector && this.changeDetector.detectChanges());
}
projectContent() {
var childNodes = this.childNodes;
var parent = this.contentInsertionPoint.parentNode;
if (parent) {
for (var i = 0, ii = childNodes.length; i < ii; i++) {
parent.insertBefore(childNodes[i], this.contentInsertionPoint);
}
}
}
setupOutputs() {
var attrs = this.attrs;
var outputs = this.info.outputs || [];
for (var j = 0; j < outputs.length; j++) {
var output = new PropertyBinding(outputs[j]);
var expr: any /** TODO #9100 */ = null;
var assignExpr = false;
var bindonAttr =
output.bindonAttr ? output.bindonAttr.substring(0, output.bindonAttr.length - 6) : null;
var bracketParenAttr = output.bracketParenAttr ?
`[(${output.bracketParenAttr.substring(2, output.bracketParenAttr.length - 8)})]` :
null;
if (attrs.hasOwnProperty(output.onAttr)) {
expr = (attrs as any /** TODO #9100 */)[output.onAttr];
} else if (attrs.hasOwnProperty(output.parenAttr)) {
expr = (attrs as any /** TODO #9100 */)[output.parenAttr];
} else if (attrs.hasOwnProperty(bindonAttr)) {
expr = (attrs as any /** TODO #9100 */)[bindonAttr];
assignExpr = true;
} else if (attrs.hasOwnProperty(bracketParenAttr)) {
expr = (attrs as any /** TODO #9100 */)[bracketParenAttr];
assignExpr = true;
}
if (expr != null && assignExpr != null) {
var getter = this.parse(expr);
var setter = getter.assign;
if (assignExpr && !setter) {
throw new Error(`Expression '${expr}' is not assignable!`);
}
var emitter = this.component[output.prop] as EventEmitter<any>;
if (emitter) {
emitter.subscribe({
next: assignExpr ?
((setter: any) => (v: any /** TODO #9100 */) => setter(this.scope, v))(setter) :
((getter: any) => (v: any /** TODO #9100 */) =>
getter(this.scope, {$event: v}))(getter)
});
} else {
throw new Error(
`Missing emitter '${output.prop}' on component '${this.info.component}'!`);
}
}
}
}
registerCleanup() {
this.element.bind('$destroy', () => {
this.componentScope.$destroy();
this.componentRef.destroy();
});
}
}
class Ng1Change implements SimpleChange {
constructor(public previousValue: any, public currentValue: any) {}
isFirstChange(): boolean { return this.previousValue === this.currentValue; }
}

View File

@ -0,0 +1,26 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector} from '@angular/core';
import {INJECTOR_KEY} from './constants';
/**
* Create an Angular 1 factory that will return an Angular 2 injectable thing
* (e.g. service, pipe, component, etc)
*
* Usage:
*
* ```
* angular1Module.factory('someService', downgradeInjectable(SomeService))
* ```
*
* @experimental
*/
export function downgradeInjectable(token: any) {
return [INJECTOR_KEY, (i: Injector) => i.get(token)];
}

View File

@ -0,0 +1,301 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {DoCheck, ElementRef, EventEmitter, Injector, OnChanges, OnInit, SimpleChanges} from '@angular/core';
import * as angular from '../angular_js';
import {looseIdentical} from '../facade/lang';
import {controllerKey} from '../util';
import {$COMPILE, $CONTROLLER, $HTTP_BACKEND, $INJECTOR, $SCOPE, $TEMPLATE_CACHE} from './constants';
const NOT_SUPPORTED: any = 'NOT_SUPPORTED';
const INITIAL_VALUE = {
__UNINITIALIZED__: true
};
class Bindings {
twoWayBoundProperties: string[] = [];
twoWayBoundLastValues: any[] = [];
expressionBoundProperties: string[] = [];
propertyToOutputMap: {[propName: string]: string} = {};
}
interface IBindingDestination {
[key: string]: any;
$onChanges?: (changes: SimpleChanges) => void;
}
interface IControllerInstance extends IBindingDestination {
$onInit?: () => void;
}
/**
* @experimental
*/
export class UpgradeComponent implements OnInit, OnChanges, DoCheck {
private $injector: angular.IInjectorService;
private $compile: angular.ICompileService;
private $templateCache: angular.ITemplateCacheService;
private $httpBackend: angular.IHttpBackendService;
private $controller: angular.IControllerService;
private element: Element;
private $element: angular.IAugmentedJQuery;
private $componentScope: angular.IScope;
private directive: angular.IDirective;
private bindings: Bindings;
private linkFn: angular.ILinkFn;
private controllerInstance: IControllerInstance = null;
private bindingDestination: IBindingDestination = null;
constructor(private name: string, private elementRef: ElementRef, private injector: Injector) {
this.$injector = injector.get($INJECTOR);
this.$compile = this.$injector.get($COMPILE);
this.$templateCache = this.$injector.get($TEMPLATE_CACHE);
this.$httpBackend = this.$injector.get($HTTP_BACKEND);
this.$controller = this.$injector.get($CONTROLLER);
this.element = elementRef.nativeElement;
this.$element = angular.element(this.element);
this.directive = this.getDirective(name);
this.bindings = this.initializeBindings(this.directive);
this.linkFn = this.compileTemplate(this.directive);
// We ask for the Angular 1 scope from the Angular 2 injector, since
// we will put the new component scope onto the new injector for each component
const $parentScope = injector.get($SCOPE);
// QUESTION 1: Should we create an isolated scope if the scope is only true?
// QUESTION 2: Should we make the scope accessible through `$element.scope()/isolateScope()`?
this.$componentScope = $parentScope.$new(!!this.directive.scope);
const controllerType = this.directive.controller;
// QUESTION: shouldn't we be building the controller in any case?
if (this.directive.bindToController) {
if (controllerType) {
this.bindingDestination = this.controllerInstance = this.buildController(
controllerType, this.$componentScope, this.$element, this.directive.controllerAs);
} else {
throw new Error(
`Upgraded directive '${name}' specifies 'bindToController' but no controller.`);
}
} else {
this.bindingDestination = this.$componentScope;
}
this.setupOutputs();
}
ngOnInit() {
// QUESTION: why not just use $compile instead of reproducing parts of it
if (!this.directive.bindToController && this.directive.controller) {
this.controllerInstance = this.buildController(
this.directive.controller, this.$componentScope, this.$element,
this.directive.controllerAs);
}
const attrs: angular.IAttributes = NOT_SUPPORTED;
const transcludeFn: angular.ITranscludeFunction = NOT_SUPPORTED;
const linkController = this.resolveRequired(this.$element, this.directive.require);
const link = this.directive.link;
const preLink = (typeof link == 'object') && (link as angular.IDirectivePrePost).pre;
const postLink = (typeof link == 'object') ? (link as angular.IDirectivePrePost).post : link;
if (preLink) {
preLink(this.$componentScope, this.$element, attrs, linkController, transcludeFn);
}
var childNodes: Node[] = [];
var childNode: Node;
while (childNode = this.element.firstChild) {
this.element.removeChild(childNode);
childNodes.push(childNode);
}
const attachElement: angular.ICloneAttachFunction =
(clonedElements, scope) => { this.$element.append(clonedElements); };
const attachChildNodes: angular.ILinkFn = (scope, cloneAttach) => cloneAttach(childNodes);
this.linkFn(this.$componentScope, attachElement, {parentBoundTranscludeFn: attachChildNodes});
if (postLink) {
postLink(this.$componentScope, this.$element, attrs, linkController, transcludeFn);
}
if (this.controllerInstance && this.controllerInstance.$onInit) {
this.controllerInstance.$onInit();
}
}
ngOnChanges(changes: SimpleChanges) {
// Forward input changes to `bindingDestination`
Object.keys(changes).forEach(
propName => { this.bindingDestination[propName] = changes[propName].currentValue; });
if (this.bindingDestination.$onChanges) {
this.bindingDestination.$onChanges(changes);
}
}
ngDoCheck() {
const twoWayBoundProperties = this.bindings.twoWayBoundProperties;
const twoWayBoundLastValues = this.bindings.twoWayBoundLastValues;
const propertyToOutputMap = this.bindings.propertyToOutputMap;
twoWayBoundProperties.forEach((propName, idx) => {
const newValue = this.bindingDestination[propName];
const oldValue = twoWayBoundLastValues[idx];
if (!looseIdentical(newValue, oldValue)) {
const outputName = propertyToOutputMap[propName];
const eventEmitter: EventEmitter<any> = (this as any)[outputName];
eventEmitter.emit(newValue);
twoWayBoundLastValues[idx] = newValue;
}
});
}
private getDirective(name: string): angular.IDirective {
const directives: angular.IDirective[] = this.$injector.get(name + 'Directive');
if (directives.length > 1) {
throw new Error('Only support single directive definition for: ' + this.name);
}
const directive = directives[0];
if (directive.replace) this.notSupported('replace');
if (directive.terminal) this.notSupported('terminal');
if (directive.compile) this.notSupported('compile');
const link = directive.link;
// QUESTION: why not support link.post?
if (typeof link == 'object') {
if ((<angular.IDirectivePrePost>link).post) this.notSupported('link.post');
}
return directive;
}
private initializeBindings(directive: angular.IDirective) {
const btcIsObject = typeof directive.bindToController === 'object';
if (btcIsObject && Object.keys(directive.scope).length) {
throw new Error(
`Binding definitions on scope and controller at the same time is not supported.`);
}
const context = (btcIsObject) ? directive.bindToController : directive.scope;
const bindings = new Bindings();
if (typeof context == 'object') {
Object.keys(context).forEach(propName => {
const definition = context[propName];
const bindingType = definition.charAt(0);
// QUESTION: What about `=*`? Ignore? Throw? Support?
switch (bindingType) {
case '@':
case '<':
// We don't need to do anything special. They will be defined as inputs on the
// upgraded component facade and the change propagation will be handled by
// `ngOnChanges()`.
break;
case '=':
bindings.twoWayBoundProperties.push(propName);
bindings.twoWayBoundLastValues.push(INITIAL_VALUE);
bindings.propertyToOutputMap[propName] = propName + 'Change';
break;
case '&':
bindings.expressionBoundProperties.push(propName);
bindings.propertyToOutputMap[propName] = propName;
break;
default:
var json = JSON.stringify(context);
throw new Error(
`Unexpected mapping '${bindingType}' in '${json}' in '${this.name}' directive.`);
}
});
}
return bindings;
}
private compileTemplate(directive: angular.IDirective): angular.ILinkFn {
if (this.directive.template !== undefined) {
return this.compileHtml(getOrCall(this.directive.template));
} else if (this.directive.templateUrl) {
var url = getOrCall(this.directive.templateUrl);
var html = this.$templateCache.get(url) as string;
if (html !== undefined) {
return this.compileHtml(html);
} else {
throw new Error('loading directive templates asynchronously is not supported');
// return new Promise((resolve, reject) => {
// this.$httpBackend('GET', url, null, (status: number, response: string) => {
// if (status == 200) {
// resolve(this.compileHtml(this.$templateCache.put(url, response)));
// } else {
// reject(`GET component template from '${url}' returned '${status}: ${response}'`);
// }
// });
// });
}
} else {
throw new Error(`Directive '${this.name}' is not a component, it is missing template.`);
}
}
private buildController(
controllerType: angular.IController, $scope: angular.IScope,
$element: angular.IAugmentedJQuery, controllerAs: string) {
var locals = {$scope, $element};
var controller = this.$controller(controllerType, locals, null, controllerAs);
$element.data(controllerKey(this.directive.name), controller);
return controller;
}
private resolveRequired(
$element: angular.IAugmentedJQuery, require: angular.DirectiveRequireProperty) {
// TODO
}
private setupOutputs() {
// Set up the outputs for `=` bindings
this.bindings.twoWayBoundProperties.forEach(propName => {
const outputName = this.bindings.propertyToOutputMap[propName];
(this as any)[outputName] = new EventEmitter();
});
// Set up the outputs for `&` bindings
this.bindings.expressionBoundProperties.forEach(propName => {
const outputName = this.bindings.propertyToOutputMap[propName];
const emitter = (this as any)[outputName] = new EventEmitter();
// QUESTION: Do we want the ng1 component to call the function with `<value>` or with
// `{$event: <value>}`. The former is closer to ng2, the latter to ng1.
this.bindingDestination[propName] = (value: any) => emitter.emit(value);
});
}
private notSupported(feature: string) {
throw new Error(
`Upgraded directive '${this.name}' contains unsupported feature: '${feature}'.`);
}
private compileHtml(html: string): angular.ILinkFn {
const div = document.createElement('div');
div.innerHTML = html;
return this.$compile(div.childNodes);
}
}
function getOrCall<T>(property: Function | T): T {
return typeof(property) === 'function' ? property() : property;
}

View File

@ -0,0 +1,65 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector, NgModule, NgZone} from '@angular/core';
import * as angular from '../angular_js';
import {controllerKey} from '../util';
import {angular1Providers, setTempInjectorRef} from './angular1_providers';
import {$INJECTOR, INJECTOR_KEY, UPGRADE_MODULE_NAME} from './constants';
/**
* The Ng1Module contains providers for the Ng1Adapter and all the core Angular 1 services;
* and also holds the `bootstrapNg1()` method fo bootstrapping an upgraded Angular 1 app.
* @experimental
*/
@NgModule({providers: angular1Providers})
export class UpgradeModule {
public $injector: angular.IInjectorService;
constructor(public injector: Injector, public ngZone: NgZone) {}
/**
* Bootstrap an Angular 1 application from this NgModule
* @param element the element on which to bootstrap the Angular 1 application
* @param [modules] the Angular 1 modules to bootstrap for this application
* @param [config] optional extra Angular 1 bootstrap configuration
*/
bootstrap(element: Element, modules: string[] = [], config?: angular.IAngularBootstrapConfig) {
// Create an ng1 module to bootstrap
const upgradeModule =
angular
.module(UPGRADE_MODULE_NAME, modules)
.value(INJECTOR_KEY, this.injector)
.run([
$INJECTOR,
($injector: angular.IInjectorService) => {
this.$injector = $injector;
// Initialize the ng1 $injector provider
setTempInjectorRef($injector);
this.injector.get($INJECTOR);
// Put the injector on the DOM, so that it can be "required"
angular.element(element).data(controllerKey(INJECTOR_KEY), this.injector);
// Wire up the ng1 rootScope to run a digest cycle whenever the zone settles
var $rootScope = $injector.get('$rootScope');
this.ngZone.onMicrotaskEmpty.subscribe(
() => this.ngZone.runOutsideAngular(() => $rootScope.$evalAsync()));
}
]);
// Bootstrap the angular 1 application inside our zone
this.ngZone.run(() => { angular.bootstrap(element, [upgradeModule.name], config); });
}
}

View File

@ -11,6 +11,7 @@ export const NG2_INJECTOR = 'ng2.Injector';
export const NG2_COMPONENT_FACTORY_REF_MAP = 'ng2.ComponentFactoryRefMap';
export const NG2_ZONE = 'ng2.NgZone';
export const NG1_PROVIDE = '$provide';
export const NG1_CONTROLLER = '$controller';
export const NG1_SCOPE = '$scope';
export const NG1_ROOT_SCOPE = '$rootScope';

View File

@ -49,7 +49,7 @@ export class DowngradeNg2ComponentAdapter {
setupInputs(): void {
var attrs = this.attrs;
var inputs = this.info.inputs;
var inputs = this.info.inputs || [];
for (var i = 0; i < inputs.length; i++) {
var input = inputs[i];
var expr: any /** TODO #9100 */ = null;
@ -115,7 +115,7 @@ export class DowngradeNg2ComponentAdapter {
setupOutputs() {
var attrs = this.attrs;
var outputs = this.info.outputs;
var outputs = this.info.outputs || [];
for (var j = 0; j < outputs.length; j++) {
var output = outputs[j];
var expr: any /** TODO #9100 */ = null;

View File

@ -0,0 +1 @@
../../facade/src

View File

@ -27,8 +27,8 @@ export interface AttrProp {
export interface ComponentInfo {
type: Type<any>;
selector: string;
inputs: AttrProp[];
outputs: AttrProp[];
inputs?: AttrProp[];
outputs?: AttrProp[];
}
export function getComponentInfo(type: Type<any>): ComponentInfo {

View File

@ -248,7 +248,7 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
this.element.removeChild(childNode);
childNodes.push(childNode);
}
this.linkFn(this.componentScope, (clonedElement: Node[], scope: angular.IScope) => {
this.linkFn(this.componentScope, (clonedElement, scope) => {
for (var i = 0, ii = clonedElement.length; i < ii; i++) {
this.element.appendChild(clonedElement[i]);
}
@ -302,7 +302,8 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
return controller;
}
private resolveRequired($element: angular.IAugmentedJQuery, require: string|string[]): any {
private resolveRequired(
$element: angular.IAugmentedJQuery, require: angular.DirectiveRequireProperty): any {
if (!require) {
return undefined;
} else if (typeof require == 'string') {

View File

@ -0,0 +1,58 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Ng1Token} from '@angular/upgrade/src/angular_js';
import {compileFactory, injectorFactory, parseFactory, rootScopeFactory, setTempInjectorRef} from '@angular/upgrade/src/aot/angular1_providers';
export function main() {
describe('upgrade angular1_providers', () => {
describe('compileFactory', () => {
it('should retrieve and return `$compile`', () => {
const services: {[key: string]: any} = {$compile: 'foo'};
const mockInjector = {get: (name: Ng1Token): any => services[name], has: () => true};
expect(compileFactory(mockInjector)).toBe('foo');
});
});
describe('injectorFactory', () => {
it('should return the injector value that was previously set', () => {
const mockInjector = {get: () => {}, has: () => false};
setTempInjectorRef(mockInjector);
const injector = injectorFactory();
expect(injector).toBe(mockInjector);
});
it('should unset the injector after the first call (to prevent memory leaks)', () => {
const mockInjector = {get: () => {}, has: () => false};
setTempInjectorRef(mockInjector);
injectorFactory();
const injector = injectorFactory();
expect(injector).toBe(null);
});
});
describe('parseFactory', () => {
it('should retrieve and return `$parse`', () => {
const services: {[key: string]: any} = {$parse: 'bar'};
const mockInjector = {get: (name: Ng1Token): any => services[name], has: () => true};
expect(parseFactory(mockInjector)).toBe('bar');
});
});
describe('rootScopeFactory', () => {
it('should retrieve and return `$rootScope`', () => {
const services: {[key: string]: any} = {$rootScope: 'baz'};
const mockInjector = {get: (name: Ng1Token): any => services[name], has: () => true};
expect(rootScopeFactory(mockInjector)).toBe('baz');
});
});
});
}

View File

@ -0,0 +1,52 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {PropertyBinding} from '@angular/upgrade/src/aot/component_info';
export function main() {
describe('PropertyBinding', () => {
it('should process a simple binding', () => {
const binding = new PropertyBinding('someBinding');
expect(binding.binding).toEqual('someBinding');
expect(binding.prop).toEqual('someBinding');
expect(binding.attr).toEqual('someBinding');
expect(binding.bracketAttr).toEqual('[someBinding]');
expect(binding.bracketParenAttr).toEqual('[(someBinding)]');
expect(binding.parenAttr).toEqual('(someBinding)');
expect(binding.onAttr).toEqual('onSomeBinding');
expect(binding.bindAttr).toEqual('bindSomeBinding');
expect(binding.bindonAttr).toEqual('bindonSomeBinding');
});
it('should process a two-part binding', () => {
const binding = new PropertyBinding('someProp:someAttr');
expect(binding.binding).toEqual('someProp:someAttr');
expect(binding.prop).toEqual('someProp');
expect(binding.attr).toEqual('someAttr');
expect(binding.bracketAttr).toEqual('[someAttr]');
expect(binding.bracketParenAttr).toEqual('[(someAttr)]');
expect(binding.parenAttr).toEqual('(someAttr)');
expect(binding.onAttr).toEqual('onSomeAttr');
expect(binding.bindAttr).toEqual('bindSomeAttr');
expect(binding.bindonAttr).toEqual('bindonSomeAttr');
});
it('should cope with whitespace', () => {
const binding = new PropertyBinding(' someProp : someAttr ');
expect(binding.binding).toEqual(' someProp : someAttr ');
expect(binding.prop).toEqual('someProp');
expect(binding.attr).toEqual('someAttr');
expect(binding.bracketAttr).toEqual('[someAttr]');
expect(binding.bracketParenAttr).toEqual('[(someAttr)]');
expect(binding.parenAttr).toEqual('(someAttr)');
expect(binding.onAttr).toEqual('onSomeAttr');
expect(binding.bindAttr).toEqual('bindSomeAttr');
expect(binding.bindonAttr).toEqual('bindonSomeAttr');
});
});
}

View File

@ -0,0 +1,24 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {INJECTOR_KEY} from '@angular/upgrade/src/aot/constants';
import {downgradeInjectable} from '@angular/upgrade/src/aot/downgrade_injectable';
export function main() {
describe('downgradeInjectable', () => {
it('should return an Angular 1 annotated factory for the token', () => {
const factory = downgradeInjectable('someToken');
expect(factory[0]).toEqual(INJECTOR_KEY);
expect(factory[1]).toEqual(jasmine.any(Function));
const injector = {get: jasmine.createSpy('get').and.returnValue('service value')};
const value = (factory as any)[1](injector);
expect(injector.get).toHaveBeenCalledWith('someToken');
expect(value).toEqual('service value');
});
});
}

View File

@ -0,0 +1,80 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Directive, ElementRef, Injector, NgModule, destroyPlatform} from '@angular/core';
import {async} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {UpgradeComponent, UpgradeModule, downgradeComponent} from '@angular/upgrade';
import * as angular from '@angular/upgrade/src/angular_js';
import {bootstrap, html} from '../test_helpers';
export function main() {
describe('scope/component change-detection', () => {
beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform());
it('should interleave scope and component expressions', async(() => {
const log: any[] /** TODO #9100 */ = [];
const l = (value: any /** TODO #9100 */) => {
log.push(value);
return value + ';';
};
@Directive({selector: 'ng1a'})
class Ng1aComponent extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1a', elementRef, injector);
}
}
@Directive({selector: 'ng1b'})
class Ng1bComponent extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1b', elementRef, injector);
}
}
@Component({
selector: 'ng2',
template: `{{l('2A')}}<ng1a></ng1a>{{l('2B')}}<ng1b></ng1b>{{l('2C')}}`
})
class Ng2Component {
l: (value: any) => string;
constructor() { this.l = l; }
}
@NgModule({
declarations: [Ng1aComponent, Ng1bComponent, Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module = angular.module('ng1', [])
.directive('ng1a', () => ({template: '{{ l(\'ng1a\') }}'}))
.directive('ng1b', () => ({template: '{{ l(\'ng1b\') }}'}))
.directive('ng2', downgradeComponent({component: Ng2Component}))
.run(($rootScope: any /** TODO #9100 */) => {
$rootScope.l = l;
$rootScope.reset = () => log.length = 0;
});
const element =
html('<div>{{reset(); l(\'1A\');}}<ng2>{{l(\'1B\')}}</ng2>{{l(\'1C\')}}</div>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(document.body.textContent).toEqual('1A;2A;ng1a;2B;ng1b;2C;1C;');
// https://github.com/angular/angular.js/issues/12983
expect(log).toEqual(['1A', '1B', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']);
});
}));
});
}

View File

@ -0,0 +1,99 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Directive, ElementRef, Injector, NgModule, destroyPlatform} from '@angular/core';
import {async} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {UpgradeComponent, UpgradeModule, downgradeComponent} from '@angular/upgrade';
import * as angular from '@angular/upgrade/src/angular_js';
import {bootstrap, html} from '../test_helpers';
export function main() {
describe('content projection', () => {
beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform());
it('should instantiate ng2 in ng1 template and project content', async(() => {
// the ng2 component that will be used in ng1 (downgraded)
@Component({selector: 'ng2', template: `{{ 'NG2' }}(<ng-content></ng-content>)`})
class Ng2Component {
}
// our upgrade module to host the component to downgrade
@NgModule({
imports: [BrowserModule, UpgradeModule],
declarations: [Ng2Component],
entryComponents: [Ng2Component]
})
class Ng2Module {
ngDoBootstrap() {}
}
// the ng1 app module that will consume the downgraded component
const ng1Module = angular
.module('ng1', [])
// create an ng1 facade of the ng2 component
.directive('ng2', downgradeComponent({component: Ng2Component}));
const element =
html('<div>{{ \'ng1[\' }}<ng2>~{{ \'ng-content\' }}~</ng2>{{ \']\' }}</div>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(document.body.textContent).toEqual('ng1[NG2(~ng-content~)]');
});
}));
it('should instantiate ng1 in ng2 template and project content', async(() => {
@Component({
selector: 'ng2',
template: `{{ 'ng2(' }}<ng1>{{'transclude'}}</ng1>{{ ')' }}`,
})
class Ng2Component {
}
@Directive({selector: 'ng1'})
class Ng1WrapperComponent extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1', elementRef, injector);
}
}
@NgModule({
declarations: [Ng1WrapperComponent, Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module = angular.module('ng1', [])
.directive(
'ng1',
() => {
return {
transclude: true,
template: '{{ "ng1" }}(<ng-transclude></ng-transclude>)'
};
})
.directive('ng2', downgradeComponent({component: Ng2Component}));
const element = html('<div>{{\'ng1(\'}}<ng2></ng2>{{\')\'}}</div>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(document.body.textContent).toEqual('ng1(ng2(ng1(transclude)))');
});
}));
});
}

View File

@ -0,0 +1,259 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, EventEmitter, NgModule, OnChanges, OnDestroy, SimpleChanges, destroyPlatform} from '@angular/core';
import {async} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {UpgradeModule, downgradeComponent} from '@angular/upgrade';
import * as angular from '@angular/upgrade/src/angular_js';
import {bootstrap, html, multiTrim} from '../test_helpers';
export function main() {
describe('downgrade ng2 component', () => {
beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform());
it('should bind properties, events', async(() => {
const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => {
$rootScope['dataA'] = 'A';
$rootScope['dataB'] = 'B';
$rootScope['modelA'] = 'initModelA';
$rootScope['modelB'] = 'initModelB';
$rootScope['eventA'] = '?';
$rootScope['eventB'] = '?';
});
@Component({
selector: 'ng2',
inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'],
outputs: [
'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', 'twoWayBEmitter: twoWayBChange'
],
template: 'ignore: {{ignore}}; ' +
'literal: {{literal}}; interpolate: {{interpolate}}; ' +
'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' +
'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})'
})
class Ng2Component implements OnChanges {
ngOnChangesCount = 0;
ignore = '-';
literal = '?';
interpolate = '?';
oneWayA = '?';
oneWayB = '?';
twoWayA = '?';
twoWayB = '?';
eventA = new EventEmitter();
eventB = new EventEmitter();
twoWayAEmitter = new EventEmitter();
twoWayBEmitter = new EventEmitter();
ngOnChanges(changes: SimpleChanges) {
const assert = (prop: string, value: any) => {
const propVal = (this as any)[prop];
if (propVal != value) {
throw new Error(`Expected: '${prop}' to be '${value}' but was '${propVal}'`);
}
};
const assertChange = (prop: string, value: any) => {
assert(prop, value);
if (!changes[prop]) {
throw new Error(`Changes record for '${prop}' not found.`);
}
const actualValue = changes[prop].currentValue;
if (actualValue != value) {
throw new Error(
`Expected changes record for'${prop}' to be '${value}' but was '${actualValue}'`);
}
};
switch (this.ngOnChangesCount++) {
case 0:
assert('ignore', '-');
assertChange('literal', 'Text');
assertChange('interpolate', 'Hello world');
assertChange('oneWayA', 'A');
assertChange('oneWayB', 'B');
assertChange('twoWayA', 'initModelA');
assertChange('twoWayB', 'initModelB');
this.twoWayAEmitter.emit('newA');
this.twoWayBEmitter.emit('newB');
this.eventA.emit('aFired');
this.eventB.emit('bFired');
break;
case 1:
assertChange('twoWayA', 'newA');
break;
case 2:
assertChange('twoWayB', 'newB');
break;
default:
throw new Error('Called too many times! ' + JSON.stringify(changes));
}
};
}
ng1Module.directive(
'ng2', downgradeComponent({
component: Ng2Component,
inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'],
outputs: [
'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange',
'twoWayBEmitter: twoWayBChange'
]
}));
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const element = html(`
<div>
<ng2 literal="Text" interpolate="Hello {{'world'}}"
bind-one-way-a="dataA" [one-way-b]="dataB"
bindon-two-way-a="modelA" [(two-way-b)]="modelB"
on-event-a='eventA=$event' (event-b)="eventB=$event"></ng2>
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
</div>`);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(multiTrim(document.body.textContent))
.toEqual(
'ignore: -; ' +
'literal: Text; interpolate: Hello world; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
});
}));
it('should properly run cleanup when ng1 directive is destroyed', async(() => {
let destroyed = false;
@Component({selector: 'ng2', template: 'test'})
class Ng2Component implements OnDestroy {
ngOnDestroy() { destroyed = true; }
}
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module =
angular.module('ng1', [])
.directive(
'ng1',
() => { return {template: '<div ng-if="!destroyIt"><ng2></ng2></div>'}; })
.directive('ng2', downgradeComponent({component: Ng2Component}));
const element = html('<ng1></ng1>');
platformBrowserDynamic().bootstrapModule(Ng2Module).then((ref) => {
const adapter = ref.injector.get(UpgradeModule) as UpgradeModule;
adapter.bootstrap(element, [ng1Module.name]);
expect(element.textContent).toContain('test');
expect(destroyed).toBe(false);
const $rootScope = adapter.$injector.get('$rootScope');
$rootScope.$apply('destroyIt = true');
expect(element.textContent).not.toContain('test');
expect(destroyed).toBe(true);
});
}));
it('should work when compiled outside the dom (by fallback to the root ng2.injector)',
async(() => {
@Component({selector: 'ng2', template: 'test'})
class Ng2Component {
}
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module =
angular.module('ng1', [])
.directive(
'ng1',
[
'$compile',
($compile: angular.ICompileService) => {
return {
link: function(
$scope: angular.IScope, $element: angular.IAugmentedJQuery,
$attrs: angular.IAttributes) {
// here we compile some HTML that contains a downgraded component
// since it is not currently in the DOM it is not able to "require"
// an ng2 injector so it should use the `moduleInjector` instead.
const compiled = $compile('<ng2></ng2>');
const template = compiled($scope);
$element.append(template);
}
};
}
])
.directive('ng2', downgradeComponent({component: Ng2Component}));
const element = html('<ng1></ng1>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
// the fact that the body contains the correct text means that the
// downgraded component was able to access the moduleInjector
// (since there is no other injector in this system)
expect(multiTrim(document.body.textContent)).toEqual('test');
});
}));
it('should allow attribute selectors for components in ng2', async(() => {
@Component({selector: '[itWorks]', template: 'It works'})
class WorksComponent {
}
@Component({selector: 'root-component', template: '<span itWorks></span>!'})
class RootComponent {
}
@NgModule({
declarations: [RootComponent, WorksComponent],
entryComponents: [RootComponent],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module = angular.module('ng1', []).directive(
'rootComponent', downgradeComponent({component: RootComponent}));
const element = html('<root-component></root-component>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(multiTrim(document.body.textContent)).toBe('It works!');
});
}));
});
}

View File

@ -0,0 +1,89 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Directive, ElementRef, Injector, Input, NgModule, destroyPlatform} from '@angular/core';
import {async} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {UpgradeComponent, UpgradeModule, downgradeComponent} from '@angular/upgrade';
import * as angular from '@angular/upgrade/src/angular_js';
import {bootstrap, html, multiTrim} from '../test_helpers';
export function main() {
describe('examples', () => {
beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform());
it('should have angular 1 loaded', () => expect(angular.version.major).toBe(1));
it('should verify UpgradeAdapter example', async(() => {
// This is wrapping (upgrading) an Angular 1 component to be used in an Angular 2
// component
@Directive({selector: 'ng1'})
class Ng1Component extends UpgradeComponent {
@Input() title: string;
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1', elementRef, injector);
}
}
// This is an Angular 2 component that will be downgraded
@Component({
selector: 'ng2',
template: 'ng2[<ng1 [title]="nameProp">transclude</ng1>](<ng-content></ng-content>)'
})
class Ng2Component {
@Input('name') nameProp: string;
}
// This module represents the Angular 2 pieces of the application
@NgModule({
declarations: [Ng1Component, Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() { /* this is a placeholder to stop the boostrapper from complaining */
}
}
// This module represents the Angular 1 pieces of the application
const ng1Module =
angular
.module('myExample', [])
// This is an Angular 1 component that will be upgraded
.directive(
'ng1',
() => {
return {
scope: {title: '='},
transclude: true,
template: 'ng1[Hello {{title}}!](<span ng-transclude></span>)'
};
})
// This is wrapping (downgrading) an Angular 2 component to be used in Angular 1
.directive(
'ng2',
downgradeComponent({component: Ng2Component, inputs: ['nameProp: name']}));
// This is the (Angular 1) application bootstrap element
// Notice that it is actually a downgraded Angular 2 component
const element = html('<ng2 name="World">project</ng2>');
// Let's use a helper function to make this simpler
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
expect(multiTrim(element.textContent))
.toBe('ng2[ng1[Hello World!](transclude)](project)');
});
}));
});
}

View File

@ -0,0 +1,80 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {NgModule, OpaqueToken, destroyPlatform} from '@angular/core';
import {async} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {UpgradeModule, downgradeInjectable} from '@angular/upgrade';
import * as angular from '@angular/upgrade/src/angular_js';
import {bootstrap, html} from '../test_helpers';
export function main() {
describe('injection', () => {
beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform());
it('should downgrade ng2 service to ng1', async(() => {
// Tokens used in ng2 to identify services
const Ng2Service = new OpaqueToken('ng2-service');
// Sample ng1 NgModule for tests
@NgModule({
imports: [BrowserModule, UpgradeModule],
providers: [
{provide: Ng2Service, useValue: 'ng2 service value'},
]
})
class Ng2Module {
ngDoBootstrap() {}
}
// create the ng1 module that will import an ng2 service
const ng1Module =
angular.module('ng1Module', []).factory('ng2Service', downgradeInjectable(Ng2Service));
bootstrap(platformBrowserDynamic(), Ng2Module, html('<div>'), ng1Module)
.then((upgrade) => {
const ng1Injector = upgrade.$injector;
expect(ng1Injector.get('ng2Service')).toBe('ng2 service value');
});
}));
it('should upgrade ng1 service to ng2', async(() => {
// Tokens used in ng2 to identify services
const Ng1Service = new OpaqueToken('ng1-service');
// Sample ng1 NgModule for tests
@NgModule({
imports: [BrowserModule, UpgradeModule],
providers: [
// the following line is the "upgrade" of an Angular 1 service
{
provide: Ng1Service,
useFactory: (i: angular.IInjectorService) => i.get('ng1Service'),
deps: ['$injector']
}
]
})
class Ng2Module {
ngDoBootstrap() {}
}
// create the ng1 module that will import an ng2 service
const ng1Module = angular.module('ng1Module', []).value('ng1Service', 'ng1 service value');
bootstrap(platformBrowserDynamic(), Ng2Module, html('<div>'), ng1Module)
.then((upgrade) => {
var ng2Injector = upgrade.injector;
expect(ng2Injector.get(Ng1Service)).toBe('ng1 service value');
});
}));
});
}

View File

@ -0,0 +1,71 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {NgModule, Testability, destroyPlatform} from '@angular/core';
import {fakeAsync, tick} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {UpgradeModule} from '@angular/upgrade';
import * as angular from '@angular/upgrade/src/angular_js';
import {bootstrap, html} from '../test_helpers';
export function main() {
describe('testability', () => {
beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform());
@NgModule({imports: [BrowserModule, UpgradeModule]})
class Ng2Module {
ngDoBootstrap() {}
}
it('should handle deferred bootstrap', fakeAsync(() => {
let applicationRunning = false;
const ng1Module = angular.module('ng1', []).run(() => { applicationRunning = true; });
const element = html('<div></div>');
window.name = 'NG_DEFER_BOOTSTRAP!' + window.name;
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module);
setTimeout(() => { (<any>window).angular.resumeBootstrap(); }, 100);
expect(applicationRunning).toEqual(false);
tick(100);
expect(applicationRunning).toEqual(true);
}));
it('should wait for ng2 testability', fakeAsync(() => {
const ng1Module = angular.module('ng1', []);
const element = html('<div></div>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
const ng2Testability: Testability = upgrade.injector.get(Testability);
ng2Testability.increasePendingRequestCount();
let ng2Stable = false;
let ng1Stable = false;
angular.getTestability(element).whenStable(() => { ng1Stable = true; });
setTimeout(() => {
ng2Stable = true;
ng2Testability.decreasePendingRequestCount();
}, 100);
expect(ng1Stable).toEqual(false);
expect(ng2Stable).toEqual(false);
tick(100);
expect(ng1Stable).toEqual(true);
expect(ng2Stable).toEqual(true);
});
}));
});
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {PlatformRef, Type} from '@angular/core';
import {UpgradeModule} from '@angular/upgrade';
import * as angular from '@angular/upgrade/src/angular_js';
export function bootstrap(
platform: PlatformRef, Ng2Module: Type<{}>, element: Element, ng1Module: angular.IModule) {
// We bootstrap the Angular 2 module first; then when it is ready (async)
// We bootstrap the Angular 1 module on the bootstrap element
return platform.bootstrapModule(Ng2Module).then(ref => {
var upgrade = ref.injector.get(UpgradeModule) as UpgradeModule;
upgrade.bootstrap(element, [ng1Module.name]);
return upgrade;
});
}
export function html(html: string): Element {
// Don't return `body` itself, because using it as a `$rootElement` for ng1
// will attach `$injector` to it and that will affect subsequent tests.
const body = document.body;
body.innerHTML = `<div>${html.trim()}</div>`;
const div = document.body.firstChild as Element;
if (div.childNodes.length === 1 && div.firstChild instanceof HTMLElement) {
return div.firstChild;
}
return div;
}
export function multiTrim(text: string): string {
return text.replace(/\n/g, '').replace(/\s\s+/g, ' ').trim();
}

View File

@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "2.1.1",
"version": "2.2.0-beta.0",
"private": true,
"branchPattern": "2.0.*",
"description": "Angular 2 - a web framework for modern web apps",

View File

@ -11,3 +11,4 @@ platform-browser-dynamic/src
platform-server/src
platform-webworker/src
platform-webworker-dynamic/src
upgrade/src

View File

@ -8,6 +8,7 @@ export declare abstract class AbstractControl {
[key: string]: any;
};
invalid: boolean;
parent: FormGroup | FormArray;
pending: boolean;
pristine: boolean;
root: AbstractControl;
@ -84,6 +85,8 @@ export declare abstract class AbstractControlDirective {
valid: boolean;
value: any;
valueChanges: Observable<any>;
getError(errorCode: string, path?: string[]): any;
hasError(errorCode: string, path?: string[]): boolean;
reset(value?: any): void;
}
@ -159,17 +162,20 @@ export declare class FormArray extends AbstractControl {
at(index: number): AbstractControl;
getRawValue(): any[];
insert(index: number, control: AbstractControl): void;
patchValue(value: any[], {onlySelf}?: {
patchValue(value: any[], {onlySelf, emitEvent}?: {
onlySelf?: boolean;
emitEvent?: boolean;
}): void;
push(control: AbstractControl): void;
removeAt(index: number): void;
reset(value?: any, {onlySelf}?: {
reset(value?: any, {onlySelf, emitEvent}?: {
onlySelf?: boolean;
emitEvent?: boolean;
}): void;
setControl(index: number, control: AbstractControl): void;
setValue(value: any[], {onlySelf}?: {
setValue(value: any[], {onlySelf, emitEvent}?: {
onlySelf?: boolean;
emitEvent?: boolean;
}): void;
}
@ -208,8 +214,9 @@ export declare class FormControl extends AbstractControl {
}): void;
registerOnChange(fn: Function): void;
registerOnDisabledChange(fn: (isDisabled: boolean) => void): void;
reset(formState?: any, {onlySelf}?: {
reset(formState?: any, {onlySelf, emitEvent}?: {
onlySelf?: boolean;
emitEvent?: boolean;
}): void;
setValue(value: any, {onlySelf, emitEvent, emitModelToViewChange, emitViewToModelChange}?: {
onlySelf?: boolean;
@ -265,19 +272,22 @@ export declare class FormGroup extends AbstractControl {
getRawValue(): Object;
patchValue(value: {
[key: string]: any;
}, {onlySelf}?: {
}, {onlySelf, emitEvent}?: {
onlySelf?: boolean;
emitEvent?: boolean;
}): void;
registerControl(name: string, control: AbstractControl): AbstractControl;
removeControl(name: string): void;
reset(value?: any, {onlySelf}?: {
reset(value?: any, {onlySelf, emitEvent}?: {
onlySelf?: boolean;
emitEvent?: boolean;
}): void;
setControl(name: string, control: AbstractControl): void;
setValue(value: {
[key: string]: any;
}, {onlySelf}?: {
}, {onlySelf, emitEvent}?: {
onlySelf?: boolean;
emitEvent?: boolean;
}): void;
}
@ -517,7 +527,7 @@ export declare class Validators {
static nullValidator(c: AbstractControl): {
[key: string]: boolean;
};
static pattern(pattern: string): ValidatorFn;
static pattern(pattern: string | RegExp): ValidatorFn;
static required(control: AbstractControl): {
[key: string]: boolean;
};

View File

@ -202,6 +202,7 @@ export declare class Router {
navigated: boolean;
routerState: RouterState;
url: string;
urlHandlingStrategy: UrlHandlingStrategy;
constructor(rootComponentType: Type<any>, urlSerializer: UrlSerializer, outletMap: RouterOutletMap, location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler, config: Routes);
createUrlTree(commands: any[], {relativeTo, queryParams, fragment, preserveQueryParams, preserveFragment}?: NavigationExtras): UrlTree;
dispose(): void;
@ -322,6 +323,13 @@ export declare class RoutesRecognized {
toString(): string;
}
/** @experimental */
export declare abstract class UrlHandlingStrategy {
abstract extract(url: UrlTree): UrlTree;
abstract merge(newUrlPart: UrlTree, rawUrl: UrlTree): UrlTree;
abstract shouldProcessUrl(url: UrlTree): boolean;
}
/** @stable */
export declare class UrlSegment {
parameters: {
@ -336,6 +344,23 @@ export declare class UrlSegment {
toString(): string;
}
/** @stable */
export declare class UrlSegmentGroup {
children: {
[key: string]: UrlSegmentGroup;
};
numberOfChildren: number;
parent: UrlSegmentGroup;
segments: UrlSegment[];
constructor(
segments: UrlSegment[],
children: {
[key: string]: UrlSegmentGroup;
});
hasChildren(): boolean;
toString(): string;
}
/** @stable */
export declare abstract class UrlSerializer {
abstract parse(url: string): UrlTree;

View File

@ -1,3 +1,9 @@
/** @experimental */
export declare function downgradeComponent(info: ComponentInfo): angular.IInjectable;
/** @experimental */
export declare function downgradeInjectable(token: any): (string | ((i: Injector) => any))[];
/** @stable */
export declare class UpgradeAdapter {
constructor(ng2AppModule: Type<any>, compilerOptions?: CompilerOptions);
@ -19,3 +25,20 @@ export declare class UpgradeAdapterRef {
dispose(): void;
ready(fn: (upgradeAdapterRef?: UpgradeAdapterRef) => void): void;
}
/** @experimental */
export declare class UpgradeComponent implements OnInit, OnChanges, DoCheck {
constructor(name: string, elementRef: ElementRef, injector: Injector);
ngDoCheck(): void;
ngOnChanges(changes: SimpleChanges): void;
ngOnInit(): void;
}
/** @experimental */
export declare class UpgradeModule {
$injector: angular.IInjectorService;
injector: Injector;
ngZone: NgZone;
constructor(injector: Injector, ngZone: NgZone);
bootstrap(element: Element, modules?: string[], config?: angular.IAngularBootstrapConfig): void;
}