feat(ngUpgrade): add support for AoT compiled upgrade applications
This commit introduces a new API to the ngUpgrade module, which is compatible with AoT compilation. Primarily, it removes the dependency on reflection over the Angular 2 metadata by introducing an API where this information is explicitly defined, in the source code, in a way that is not lost through AoT compilation. This commit is a collaboration between @mhevery (who provided the original design of the API); @gkalpak & @petebacondarwin (who implemented the API and migrated the specs from the original ngUpgrade tests) and @alexeagle (who provided input and review). This commit is an starting point, there is still work to be done: * add more documentation * validate the API via internal projects * align the ngUpgrade compilation of A1 directives closer to the real A1 compiler * add more unit tests * consider support for async `templateUrl` A1 upgraded components Closes #12239
This commit is contained in:
parent
a2d35641e3
commit
d6791ff0e0
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
|
@ -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']}
|
||||
];
|
|
@ -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}`;
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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)];
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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
|
@ -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();
|
||||
}
|
|
@ -11,3 +11,4 @@ platform-browser-dynamic/src
|
|||
platform-server/src
|
||||
platform-webworker/src
|
||||
platform-webworker-dynamic/src
|
||||
upgrade/src
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue