Compare commits
23 Commits
2.1.1
...
2.2.0-beta
Author | SHA1 | Date | |
---|---|---|---|
69ad99dca6 | |||
da5fc696bb | |||
b44b6ef8f5 | |||
0f21a5823b | |||
5ae6915600 | |||
8b9ab44eee | |||
b0a03fcab3 | |||
c951822c35 | |||
acda82c1ed | |||
a8815d6b08 | |||
d6791ff0e0 | |||
a2d35641e3 | |||
76dd026447 | |||
0ecd9b2df0 | |||
0e9503b500 | |||
f77ab6a2d2 | |||
97bc97153b | |||
445e5922ec | |||
b9fc090143 | |||
592f40aa9c | |||
24facdea2d | |||
aa2d3372a5 | |||
bf60418fdc |
18
CHANGELOG.md
18
CHANGELOG.md
@ -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))
|
||||
|
||||
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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];
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}, {});
|
||||
|
@ -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'));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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'));
|
||||
|
@ -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'));
|
||||
|
@ -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'));
|
||||
|
@ -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: `
|
||||
|
@ -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();
|
||||
|
@ -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});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -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 {
|
||||
|
46
modules/@angular/router/src/url_handling_strategy.ts
Normal file
46
modules/@angular/router/src/url_handling_strategy.ts
Normal 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; }
|
||||
}
|
@ -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 {
|
||||
}
|
||||
|
||||
|
@ -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([])
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
12
modules/@angular/upgrade/src/aot.ts
Normal file
12
modules/@angular/upgrade/src/aot.ts
Normal 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';
|
46
modules/@angular/upgrade/src/aot/angular1_providers.ts
Normal file
46
modules/@angular/upgrade/src/aot/angular1_providers.ts
Normal 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']}
|
||||
];
|
41
modules/@angular/upgrade/src/aot/component_info.ts
Normal file
41
modules/@angular/upgrade/src/aot/component_info.ts
Normal 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}`;
|
||||
}
|
||||
}
|
19
modules/@angular/upgrade/src/aot/constants.ts
Normal file
19
modules/@angular/upgrade/src/aot/constants.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
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';
|
64
modules/@angular/upgrade/src/aot/downgrade_component.ts
Normal file
64
modules/@angular/upgrade/src/aot/downgrade_component.ts
Normal file
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {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;
|
||||
}
|
180
modules/@angular/upgrade/src/aot/downgrade_component_adapter.ts
Normal file
180
modules/@angular/upgrade/src/aot/downgrade_component_adapter.ts
Normal 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; }
|
||||
}
|
26
modules/@angular/upgrade/src/aot/downgrade_injectable.ts
Normal file
26
modules/@angular/upgrade/src/aot/downgrade_injectable.ts
Normal 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)];
|
||||
}
|
301
modules/@angular/upgrade/src/aot/upgrade_component.ts
Normal file
301
modules/@angular/upgrade/src/aot/upgrade_component.ts
Normal 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;
|
||||
}
|
65
modules/@angular/upgrade/src/aot/upgrade_module.ts
Normal file
65
modules/@angular/upgrade/src/aot/upgrade_module.ts
Normal 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); });
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -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;
|
||||
|
1
modules/@angular/upgrade/src/facade
Symbolic link
1
modules/@angular/upgrade/src/facade
Symbolic link
@ -0,0 +1 @@
|
||||
../../facade/src
|
@ -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 {
|
||||
|
@ -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') {
|
||||
|
58
modules/@angular/upgrade/test/aot/angular1_providers_spec.ts
Normal file
58
modules/@angular/upgrade/test/aot/angular1_providers_spec.ts
Normal file
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
52
modules/@angular/upgrade/test/aot/component_info_spec.ts
Normal file
52
modules/@angular/upgrade/test/aot/component_info_spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
}
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
@ -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']);
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
@ -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)))');
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
@ -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!');
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
@ -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)');
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
@ -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');
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
@ -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
39
modules/@angular/upgrade/test/aot/test_helpers.ts
Normal file
39
modules/@angular/upgrade/test/aot/test_helpers.ts
Normal 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();
|
||||
}
|
@ -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",
|
||||
|
@ -11,3 +11,4 @@ platform-browser-dynamic/src
|
||||
platform-server/src
|
||||
platform-webworker/src
|
||||
platform-webworker-dynamic/src
|
||||
upgrade/src
|
26
tools/public_api_guard/forms/index.d.ts
vendored
26
tools/public_api_guard/forms/index.d.ts
vendored
@ -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;
|
||||
};
|
||||
|
25
tools/public_api_guard/router/index.d.ts
vendored
25
tools/public_api_guard/router/index.d.ts
vendored
@ -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;
|
||||
|
23
tools/public_api_guard/upgrade/index.d.ts
vendored
23
tools/public_api_guard/upgrade/index.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user