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:
Peter Bacon Darwin 2016-10-19 21:41:04 +01:00 committed by Alex Rickabaugh
parent a2d35641e3
commit d6791ff0e0
30 changed files with 3263 additions and 39 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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