refactor: move angular source to /packages rather than modules/@angular
This commit is contained in:
21
packages/upgrade/.babelrc
Normal file
21
packages/upgrade/.babelrc
Normal file
@ -0,0 +1,21 @@
|
||||
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": [["transform-es2015-modules-umd", {
|
||||
"globals": {
|
||||
"@angular/core": "ng.core",
|
||||
"@angular/common": "ng.common",
|
||||
"@angular/compiler": "ng.compiler",
|
||||
"@angular/platform-browser": "ng.platformBrowser",
|
||||
"@angular/platform-browser-dynamic": "ng.platformBrowserDynamic",
|
||||
"@angular/upgrade": "ng.upgrade",
|
||||
"rxjs/Subject": "Rx",
|
||||
"rxjs/observable/PromiseObservable": "Rx", // this is wrong, but this stuff has changed in rxjs
|
||||
// b.6 so we need to fix it when we update.
|
||||
"rxjs/operator/toPromise": "Rx.Observable.prototype",
|
||||
"rxjs/Observable": "Rx"
|
||||
},
|
||||
"exactGlobals": true
|
||||
}]],
|
||||
"moduleId": "@angular/upgrade"
|
||||
}
|
12
packages/upgrade/.babelrc-static
Normal file
12
packages/upgrade/.babelrc-static
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": [["transform-es2015-modules-umd", {
|
||||
"globals": {
|
||||
"@angular/upgrade": "ng.upgrade",
|
||||
"@angular/upgrade/static": "ng.upgrade.static"
|
||||
},
|
||||
"exactGlobals": true
|
||||
}]],
|
||||
"moduleId": "@angular/upgrade/static"
|
||||
}
|
14
packages/upgrade/index.ts
Normal file
14
packages/upgrade/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
// This file is not used to build this module. It is only used during editing
|
||||
// by the TypeScript language service and during build for verification. `ngc`
|
||||
// replaces this file with production index.ts when it rewrites private symbol
|
||||
// names.
|
||||
|
||||
export * from './public_api';
|
21
packages/upgrade/package.json
Normal file
21
packages/upgrade/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@angular/upgrade",
|
||||
"version": "0.0.0-PLACEHOLDER",
|
||||
"description": "Angular - the library for easing update from v1 to v2",
|
||||
"main": "./bundles/upgrade.umd.js",
|
||||
"module": "./@angular/upgrade.es5.js",
|
||||
"es2015": "./@angular/upgrade.js",
|
||||
"typings": "./typings/upgrade.d.ts",
|
||||
"author": "angular",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@angular/core": "0.0.0-PLACEHOLDER",
|
||||
"@angular/compiler": "0.0.0-PLACEHOLDER",
|
||||
"@angular/platform-browser": "0.0.0-PLACEHOLDER",
|
||||
"@angular/platform-browser-dynamic": "0.0.0-PLACEHOLDER"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/angular/angular.git"
|
||||
}
|
||||
}
|
18
packages/upgrade/public_api.ts
Normal file
18
packages/upgrade/public_api.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module
|
||||
* @description
|
||||
* Entry point for all public APIs of the upgrade/dynamic package, allowing
|
||||
* Angular 1 and Angular 2+ to run side by side in the same application.
|
||||
*/
|
||||
export {VERSION} from './src/common/version';
|
||||
export {UpgradeAdapter, UpgradeAdapterRef} from './src/dynamic/upgrade_adapter';
|
||||
|
||||
// This file only re-exports content of the `src` folder. Keep it that way.
|
21
packages/upgrade/public_api_static.ts
Normal file
21
packages/upgrade/public_api_static.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module
|
||||
* @description
|
||||
* Entry point for all public APIs of the upgrade/static package, allowing
|
||||
* Angular 1 and Angular 2+ to run side by side in the same application.
|
||||
*/
|
||||
export {downgradeComponent} from './src/common/downgrade_component';
|
||||
export {downgradeInjectable} from './src/common/downgrade_injectable';
|
||||
export {VERSION} from './src/common/version';
|
||||
export {UpgradeComponent} from './src/static/upgrade_component';
|
||||
export {UpgradeModule} from './src/static/upgrade_module';
|
||||
|
||||
// This file only re-exports content of the `src` folder. Keep it that way.
|
234
packages/upgrade/src/common/angular1.ts
Normal file
234
packages/upgrade/src/common/angular1.ts
Normal file
@ -0,0 +1,234 @@
|
||||
/**
|
||||
* @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 type Ng1Token = string;
|
||||
|
||||
export type Ng1Expression = string | Function;
|
||||
|
||||
export interface IAnnotatedFunction extends Function { $inject?: Ng1Token[]; }
|
||||
|
||||
export type IInjectable = (Ng1Token | Function)[] | IAnnotatedFunction;
|
||||
|
||||
export type SingleOrListOrMap<T> = T | T[] | {[key: string]: T};
|
||||
|
||||
export interface 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: IInjectable): IModule;
|
||||
factory(key: Ng1Token, factoryFn: IInjectable): IModule;
|
||||
value(key: Ng1Token, value: any): IModule;
|
||||
constant(token: Ng1Token, value: any): IModule;
|
||||
run(a: IInjectable): IModule;
|
||||
}
|
||||
export interface ICompileService {
|
||||
(element: Element|NodeList|Node[]|string, transclude?: Function): ILinkFn;
|
||||
}
|
||||
export interface ILinkFn {
|
||||
(scope: IScope, cloneAttachFn?: ICloneAttachFunction, options?: ILinkFnOptions): IAugmentedJQuery;
|
||||
}
|
||||
export interface ILinkFnOptions {
|
||||
parentBoundTranscludeFn?: Function;
|
||||
transcludeControllers?: {[key: string]: any};
|
||||
futureParentElement?: Node;
|
||||
}
|
||||
export interface IRootScopeService {
|
||||
$new(isolate?: boolean): IScope;
|
||||
$id: string;
|
||||
$parent: IScope;
|
||||
$root: IScope;
|
||||
$watch(exp: Ng1Expression, fn?: (a1?: any, a2?: any) => void): Function;
|
||||
$on(event: string, fn?: (event?: any, ...args: any[]) => void): Function;
|
||||
$destroy(): any;
|
||||
$apply(exp?: Ng1Expression): any;
|
||||
$digest(): 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 { strictDi?: boolean; }
|
||||
export interface IDirective {
|
||||
compile?: IDirectiveCompileFn;
|
||||
controller?: IController;
|
||||
controllerAs?: string;
|
||||
bindToController?: boolean|{[key: string]: string};
|
||||
link?: IDirectiveLinkFn|IDirectivePrePost;
|
||||
name?: string;
|
||||
priority?: number;
|
||||
replace?: boolean;
|
||||
require?: DirectiveRequireProperty;
|
||||
restrict?: string;
|
||||
scope?: boolean|{[key: string]: string};
|
||||
template?: string|Function;
|
||||
templateUrl?: string|Function;
|
||||
templateNamespace?: string;
|
||||
terminal?: boolean;
|
||||
transclude?: boolean|'element'|{[key: string]: string};
|
||||
}
|
||||
export type DirectiveRequireProperty = SingleOrListOrMap<string>;
|
||||
export interface IDirectiveCompileFn {
|
||||
(templateElement: IAugmentedJQuery, templateAttributes: IAttributes,
|
||||
transclude: ITranscludeFunction): IDirectivePrePost;
|
||||
}
|
||||
export interface IDirectivePrePost {
|
||||
pre?: IDirectiveLinkFn;
|
||||
post?: IDirectiveLinkFn;
|
||||
}
|
||||
export interface IDirectiveLinkFn {
|
||||
(scope: IScope, instanceElement: IAugmentedJQuery, instanceAttributes: IAttributes,
|
||||
controller: any, transclude: ITranscludeFunction): void;
|
||||
}
|
||||
export interface IComponent {
|
||||
bindings?: {[key: string]: string};
|
||||
controller?: string|IInjectable;
|
||||
controllerAs?: string;
|
||||
require?: DirectiveRequireProperty;
|
||||
template?: string|Function;
|
||||
templateUrl?: string|Function;
|
||||
transclude?: boolean;
|
||||
}
|
||||
export interface IAttributes { $observe(attr: string, fn: (v: string) => void): void; }
|
||||
export interface ITranscludeFunction {
|
||||
// If the scope is provided, then the cloneAttachFn must be as well.
|
||||
(scope: IScope, cloneAttachFn: ICloneAttachFunction): IAugmentedJQuery;
|
||||
// If one argument is provided, then it's assumed to be the cloneAttachFn.
|
||||
(cloneAttachFn?: ICloneAttachFunction): IAugmentedJQuery;
|
||||
}
|
||||
export interface ICloneAttachFunction {
|
||||
// Let's hint but not force cloneAttachFn's signature
|
||||
(clonedElement?: IAugmentedJQuery, scope?: IScope): any;
|
||||
}
|
||||
export type IAugmentedJQuery = Node[] & {
|
||||
bind?: (name: string, fn: () => void) => void;
|
||||
data?: (name: string, value?: any) => any;
|
||||
text?: () => string;
|
||||
inheritedData?: (name: string, value?: any) => any;
|
||||
contents?: () => IAugmentedJQuery;
|
||||
parent?: () => IAugmentedJQuery;
|
||||
empty?: () => void;
|
||||
append?: (content: IAugmentedJQuery | string) => IAugmentedJQuery;
|
||||
controller?: (name: string) => any;
|
||||
isolateScope?: () => IScope;
|
||||
injector?: () => IInjectorService;
|
||||
};
|
||||
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; }
|
||||
export interface IHttpBackendService {
|
||||
(method: string, url: string, post?: any, callback?: Function, headers?: any, timeout?: number,
|
||||
withCredentials?: boolean): void;
|
||||
}
|
||||
export interface ICacheObject {
|
||||
put<T>(key: string, value?: T): T;
|
||||
get(key: string): any;
|
||||
}
|
||||
export interface ITemplateCacheService extends ICacheObject {}
|
||||
export interface ITemplateRequestService {
|
||||
(template: string|any /* TrustedResourceUrl */, ignoreRequestError?: boolean): Promise<string>;
|
||||
totalPendingRequests: number;
|
||||
}
|
||||
export type IController = string | IInjectable;
|
||||
export interface IControllerService {
|
||||
(controllerConstructor: IController, locals?: any, later?: any, ident?: any): any;
|
||||
(controllerName: string, locals?: any): any;
|
||||
}
|
||||
|
||||
export interface IInjectorService {
|
||||
get(key: string): any;
|
||||
has(key: string): boolean;
|
||||
}
|
||||
|
||||
export interface ITestabilityService {
|
||||
findBindings(element: Element, expression: string, opt_exactMatch?: boolean): Element[];
|
||||
findModels(element: Element, expression: string, opt_exactMatch?: boolean): Element[];
|
||||
getLocation(): string;
|
||||
setLocation(url: string): void;
|
||||
whenStable(callback: Function): void;
|
||||
}
|
||||
|
||||
export interface INgModelController {
|
||||
$render(): void;
|
||||
$isEmpty(value: any): boolean;
|
||||
$setValidity(validationErrorKey: string, isValid: boolean): void;
|
||||
$setPristine(): void;
|
||||
$setDirty(): void;
|
||||
$setUntouched(): void;
|
||||
$setTouched(): void;
|
||||
$rollbackViewValue(): void;
|
||||
$validate(): void;
|
||||
$commitViewValue(): void;
|
||||
$setViewValue(value: any, trigger: string): void;
|
||||
|
||||
$viewValue: any;
|
||||
$modelValue: any;
|
||||
$parsers: Function[];
|
||||
$formatters: Function[];
|
||||
$validators: {[key: string]: Function};
|
||||
$asyncValidators: {[key: string]: Function};
|
||||
$viewChangeListeners: Function[];
|
||||
$error: Object;
|
||||
$pending: Object;
|
||||
$untouched: boolean;
|
||||
$touched: boolean;
|
||||
$pristine: boolean;
|
||||
$dirty: boolean;
|
||||
$valid: boolean;
|
||||
$invalid: boolean;
|
||||
$name: string;
|
||||
}
|
||||
|
||||
function noNg() {
|
||||
throw new Error('AngularJS v1.x is not loaded!');
|
||||
}
|
||||
|
||||
let angular: {
|
||||
bootstrap: (e: Element, modules: (string | IInjectable)[], config: IAngularBootstrapConfig) =>
|
||||
void,
|
||||
module: (prefix: string, dependencies?: string[]) => IModule,
|
||||
element: (e: Element) => IAugmentedJQuery,
|
||||
version: {major: number}, resumeBootstrap?: () => void,
|
||||
getTestability: (e: Element) => ITestabilityService
|
||||
} = <any>{
|
||||
bootstrap: noNg,
|
||||
module: noNg,
|
||||
element: noNg,
|
||||
version: noNg,
|
||||
resumeBootstrap: noNg,
|
||||
getTestability: noNg
|
||||
};
|
||||
|
||||
|
||||
try {
|
||||
if (window.hasOwnProperty('angular')) {
|
||||
angular = (<any>window).angular;
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore in CJS mode.
|
||||
}
|
||||
|
||||
export const bootstrap = angular.bootstrap;
|
||||
export const module = angular.module;
|
||||
export const element = angular.element;
|
||||
export const version = angular.version;
|
||||
export const resumeBootstrap = angular.resumeBootstrap;
|
||||
export const getTestability = angular.getTestability;
|
47
packages/upgrade/src/common/component_info.ts
Normal file
47
packages/upgrade/src/common/component_info.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @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[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A `PropertyBinding` represents a mapping between a property name
|
||||
* and an attribute name. It is parsed from a string of the form
|
||||
* `"prop: attr"`; or simply `"propAndAttr" where the property
|
||||
* and attribute have the same identifier.
|
||||
*/
|
||||
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}`;
|
||||
}
|
||||
}
|
31
packages/upgrade/src/common/constants.ts
Normal file
31
packages/upgrade/src/common/constants.ts
Normal file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @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 $COMPILE = '$compile';
|
||||
export const $CONTROLLER = '$controller';
|
||||
export const $DELEGATE = '$delegate';
|
||||
export const $HTTP_BACKEND = '$httpBackend';
|
||||
export const $INJECTOR = '$injector';
|
||||
export const $PARSE = '$parse';
|
||||
export const $PROVIDE = '$provide';
|
||||
export const $ROOT_SCOPE = '$rootScope';
|
||||
export const $SCOPE = '$scope';
|
||||
export const $TEMPLATE_CACHE = '$templateCache';
|
||||
export const $TEMPLATE_REQUEST = '$templateRequest';
|
||||
|
||||
export const $$TESTABILITY = '$$testability';
|
||||
|
||||
export const COMPILER_KEY = '$$angularCompiler';
|
||||
export const GROUP_PROJECTABLE_NODES_KEY = '$$angularGroupProjectableNodes';
|
||||
export const INJECTOR_KEY = '$$angularInjector';
|
||||
export const NG_ZONE_KEY = '$$angularNgZone';
|
||||
|
||||
export const REQUIRE_INJECTOR = '?^^' + INJECTOR_KEY;
|
||||
export const REQUIRE_NG_MODEL = '?ngModel';
|
||||
|
||||
export const UPGRADE_MODULE_NAME = '$$UpgradeModule';
|
20
packages/upgrade/src/common/content_projection_helper.ts
Normal file
20
packages/upgrade/src/common/content_projection_helper.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @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';
|
||||
import * as angular from './angular1';
|
||||
|
||||
|
||||
export class ContentProjectionHelper {
|
||||
groupProjectableNodes($injector: angular.IInjectorService, component: Type<any>, nodes: Node[]):
|
||||
Node[][] {
|
||||
// By default, do not support multi-slot projection,
|
||||
// as `upgrade/static` does not support it yet.
|
||||
return [nodes];
|
||||
}
|
||||
}
|
167
packages/upgrade/src/common/downgrade_component.ts
Normal file
167
packages/upgrade/src/common/downgrade_component.ts
Normal file
@ -0,0 +1,167 @@
|
||||
/**
|
||||
* @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, Type} from '@angular/core';
|
||||
|
||||
import * as angular from './angular1';
|
||||
import {$COMPILE, $INJECTOR, $PARSE, INJECTOR_KEY, REQUIRE_INJECTOR, REQUIRE_NG_MODEL} from './constants';
|
||||
import {DowngradeComponentAdapter} from './downgrade_component_adapter';
|
||||
import {controllerKey, getComponentName} from './util';
|
||||
|
||||
let downgradeCount = 0;
|
||||
|
||||
/**
|
||||
* @whatItDoes
|
||||
*
|
||||
* *Part of the [upgrade/static](/docs/ts/latest/api/#!?query=upgrade%2Fstatic)
|
||||
* library for hybrid upgrade apps that support AoT compilation*
|
||||
*
|
||||
* Allows an Angular component to be used from AngularJS.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* Let's assume that you have an Angular component called `ng2Heroes` that needs
|
||||
* to be made available in AngularJS templates.
|
||||
*
|
||||
* {@example upgrade/static/ts/module.ts region="ng2-heroes"}
|
||||
*
|
||||
* We must create an AngularJS [directive](https://docs.angularjs.org/guide/directive)
|
||||
* that will make this Angular component available inside AngularJS templates.
|
||||
* The `downgradeComponent()` function returns a factory function that we
|
||||
* can use to define the AngularJS directive that wraps the "downgraded" component.
|
||||
*
|
||||
* {@example upgrade/static/ts/module.ts region="ng2-heroes-wrapper"}
|
||||
*
|
||||
* In this example you can see that we must provide information about the component being
|
||||
* "downgraded". This is because once the AoT compiler has run, all metadata about the
|
||||
* component has been removed from the code, and so cannot be inferred.
|
||||
*
|
||||
* We must do the following:
|
||||
* * specify the Angular component class that is to be downgraded
|
||||
* * specify all inputs and outputs that the AngularJS component expects
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* A helper function that returns a factory function to be used for registering an
|
||||
* AngularJS wrapper directive for "downgrading" an Angular component.
|
||||
*
|
||||
* The parameter contains information about the Component that is being downgraded:
|
||||
*
|
||||
* * `component: Type<any>`: The type of the Component that will be downgraded
|
||||
* * `inputs: string[]`: A collection of strings that specify what inputs the component accepts.
|
||||
* * `outputs: string[]`: A collection of strings that specify what outputs the component emits.
|
||||
*
|
||||
* The `inputs` and `outputs` are strings that map the names of properties to camelCased
|
||||
* attribute names. They are of the form `"prop: attr"`; or simply `"propAndAttr" where the
|
||||
* property and attribute have the same identifier.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export function downgradeComponent(info: /* ComponentInfo */ {
|
||||
component: Type<any>;
|
||||
inputs?: string[];
|
||||
outputs?: string[];
|
||||
}): any /* angular.IInjectable */ {
|
||||
const idPrefix = `NG2_UPGRADE_${downgradeCount++}_`;
|
||||
let idCount = 0;
|
||||
|
||||
const directiveFactory:
|
||||
angular.IAnnotatedFunction = function(
|
||||
$compile: angular.ICompileService,
|
||||
$injector: angular.IInjectorService,
|
||||
$parse: angular.IParseService): angular.IDirective {
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
terminal: true,
|
||||
require: [REQUIRE_INJECTOR, REQUIRE_NG_MODEL],
|
||||
link: (scope: angular.IScope, element: angular.IAugmentedJQuery, attrs: angular.IAttributes,
|
||||
required: any[]) => {
|
||||
// We might have to compile the contents asynchronously, because this might have been
|
||||
// triggered by `UpgradeNg1ComponentAdapterBuilder`, before the Angular templates have
|
||||
// been compiled.
|
||||
|
||||
const parentInjector: Injector|ParentInjectorPromise =
|
||||
required[0] || $injector.get(INJECTOR_KEY);
|
||||
const ngModel: angular.INgModelController = required[1];
|
||||
|
||||
const downgradeFn = (injector: Injector) => {
|
||||
const componentFactoryResolver: ComponentFactoryResolver =
|
||||
injector.get(ComponentFactoryResolver);
|
||||
const componentFactory: ComponentFactory<any> =
|
||||
componentFactoryResolver.resolveComponentFactory(info.component);
|
||||
|
||||
if (!componentFactory) {
|
||||
throw new Error('Expecting ComponentFactory for: ' + getComponentName(info.component));
|
||||
}
|
||||
|
||||
const id = idPrefix + (idCount++);
|
||||
const injectorPromise = new ParentInjectorPromise(element);
|
||||
const facade = new DowngradeComponentAdapter(
|
||||
id, info, element, attrs, scope, ngModel, injector, $injector, $compile, $parse,
|
||||
componentFactory);
|
||||
|
||||
const projectableNodes = facade.compileContents();
|
||||
facade.createComponent(projectableNodes);
|
||||
facade.setupInputs();
|
||||
facade.setupOutputs();
|
||||
facade.registerCleanup();
|
||||
|
||||
injectorPromise.resolve(facade.getInjector());
|
||||
};
|
||||
|
||||
if (parentInjector instanceof ParentInjectorPromise) {
|
||||
parentInjector.then(downgradeFn);
|
||||
} else {
|
||||
downgradeFn(parentInjector);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// bracket-notation because of closure - see #14441
|
||||
directiveFactory['$inject'] = [$COMPILE, $INJECTOR, $PARSE];
|
||||
return directiveFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous promise-like object to wrap parent injectors,
|
||||
* to preserve the synchronous nature of Angular 1's $compile.
|
||||
*/
|
||||
class ParentInjectorPromise {
|
||||
private injector: Injector;
|
||||
private injectorKey: string = controllerKey(INJECTOR_KEY);
|
||||
private callbacks: ((injector: Injector) => any)[] = [];
|
||||
|
||||
constructor(private element: angular.IAugmentedJQuery) {
|
||||
// Store the promise on the element.
|
||||
element.data(this.injectorKey, this);
|
||||
}
|
||||
|
||||
then(callback: (injector: Injector) => any) {
|
||||
if (this.injector) {
|
||||
callback(this.injector);
|
||||
} else {
|
||||
this.callbacks.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
resolve(injector: Injector) {
|
||||
this.injector = injector;
|
||||
|
||||
// Store the real injector on the element.
|
||||
this.element.data(this.injectorKey, injector);
|
||||
|
||||
// Release the element to prevent memory leaks.
|
||||
this.element = null;
|
||||
|
||||
// Run the queued callbacks.
|
||||
this.callbacks.forEach(callback => callback(injector));
|
||||
this.callbacks.length = 0;
|
||||
}
|
||||
}
|
189
packages/upgrade/src/common/downgrade_component_adapter.ts
Normal file
189
packages/upgrade/src/common/downgrade_component_adapter.ts
Normal file
@ -0,0 +1,189 @@
|
||||
/**
|
||||
* @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 './angular1';
|
||||
import {ComponentInfo, PropertyBinding} from './component_info';
|
||||
import {$SCOPE} from './constants';
|
||||
import {ContentProjectionHelper} from './content_projection_helper';
|
||||
import {getComponentName, hookupNgModel} from './util';
|
||||
|
||||
const INITIAL_VALUE = {
|
||||
__UNINITIALIZED__: true
|
||||
};
|
||||
|
||||
export class DowngradeComponentAdapter {
|
||||
private inputChangeCount: number = 0;
|
||||
private inputChanges: SimpleChanges = null;
|
||||
private componentScope: angular.IScope;
|
||||
private componentRef: ComponentRef<any> = null;
|
||||
private component: any = null;
|
||||
private changeDetector: ChangeDetectorRef = null;
|
||||
|
||||
constructor(
|
||||
private id: string, private info: ComponentInfo, private element: angular.IAugmentedJQuery,
|
||||
private attrs: angular.IAttributes, private scope: angular.IScope,
|
||||
private ngModel: angular.INgModelController, private parentInjector: Injector,
|
||||
private $injector: angular.IInjectorService, private $compile: angular.ICompileService,
|
||||
private $parse: angular.IParseService, private componentFactory: ComponentFactory<any>) {
|
||||
(this.element[0] as any).id = id;
|
||||
this.componentScope = scope.$new();
|
||||
}
|
||||
|
||||
compileContents(): Node[][] {
|
||||
const compiledProjectableNodes: Node[][] = [];
|
||||
|
||||
// The projected content has to be grouped, before it is compiled.
|
||||
const projectionHelper: ContentProjectionHelper =
|
||||
this.parentInjector.get(ContentProjectionHelper);
|
||||
const projectableNodes: Node[][] = projectionHelper.groupProjectableNodes(
|
||||
this.$injector, this.info.component, this.element.contents());
|
||||
const linkFns = projectableNodes.map(nodes => this.$compile(nodes));
|
||||
|
||||
this.element.empty();
|
||||
|
||||
linkFns.forEach(linkFn => {
|
||||
linkFn(this.scope, (clone: Node[]) => {
|
||||
compiledProjectableNodes.push(clone);
|
||||
this.element.append(clone);
|
||||
});
|
||||
});
|
||||
|
||||
return compiledProjectableNodes;
|
||||
}
|
||||
|
||||
createComponent(projectableNodes: Node[][]) {
|
||||
const childInjector = ReflectiveInjector.resolveAndCreate(
|
||||
[{provide: $SCOPE, useValue: this.componentScope}], this.parentInjector);
|
||||
|
||||
this.componentRef =
|
||||
this.componentFactory.create(childInjector, projectableNodes, this.element[0]);
|
||||
this.changeDetector = this.componentRef.changeDetectorRef;
|
||||
this.component = this.componentRef.instance;
|
||||
|
||||
hookupNgModel(this.ngModel, this.component);
|
||||
}
|
||||
|
||||
setupInputs(): void {
|
||||
const attrs = this.attrs;
|
||||
const inputs = this.info.inputs || [];
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const input = new PropertyBinding(inputs[i]);
|
||||
let expr: any /** TODO #9100 */ = null;
|
||||
|
||||
if (attrs.hasOwnProperty(input.attr)) {
|
||||
const observeFn = (prop => {
|
||||
let prevValue = INITIAL_VALUE;
|
||||
return (currValue: any) => {
|
||||
if (prevValue === INITIAL_VALUE) {
|
||||
prevValue = currValue;
|
||||
}
|
||||
|
||||
this.updateInput(prop, prevValue, currValue);
|
||||
prevValue = currValue;
|
||||
};
|
||||
})(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) {
|
||||
const watchFn =
|
||||
(prop => (currValue: any, prevValue: any) =>
|
||||
this.updateInput(prop, prevValue, currValue))(input.prop);
|
||||
this.componentScope.$watch(expr, watchFn);
|
||||
}
|
||||
}
|
||||
|
||||
const prototype = this.info.component.prototype;
|
||||
if (prototype && (<OnChanges>prototype).ngOnChanges) {
|
||||
// Detect: OnChanges interface
|
||||
this.inputChanges = {};
|
||||
this.componentScope.$watch(() => this.inputChangeCount, () => {
|
||||
const inputChanges = this.inputChanges;
|
||||
this.inputChanges = {};
|
||||
(<OnChanges>this.component).ngOnChanges(inputChanges);
|
||||
});
|
||||
}
|
||||
this.componentScope.$watch(() => this.changeDetector && this.changeDetector.detectChanges());
|
||||
}
|
||||
|
||||
setupOutputs() {
|
||||
const attrs = this.attrs;
|
||||
const outputs = this.info.outputs || [];
|
||||
for (let j = 0; j < outputs.length; j++) {
|
||||
const output = new PropertyBinding(outputs[j]);
|
||||
let expr: any /** TODO #9100 */ = null;
|
||||
let assignExpr = false;
|
||||
|
||||
const bindonAttr =
|
||||
output.bindonAttr ? output.bindonAttr.substring(0, output.bindonAttr.length - 6) : null;
|
||||
const 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) {
|
||||
const getter = this.$parse(expr);
|
||||
const setter = getter.assign;
|
||||
if (assignExpr && !setter) {
|
||||
throw new Error(`Expression '${expr}' is not assignable!`);
|
||||
}
|
||||
const 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 '${getComponentName(this.info.component)}'!`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerCleanup() {
|
||||
this.element.bind('$destroy', () => {
|
||||
this.componentScope.$destroy();
|
||||
this.componentRef.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
getInjector(): Injector { return this.componentRef && this.componentRef.injector; }
|
||||
|
||||
private updateInput(prop: string, prevValue: any, currValue: any) {
|
||||
if (this.inputChanges) {
|
||||
this.inputChangeCount++;
|
||||
this.inputChanges[prop] = new SimpleChange(prevValue, currValue, prevValue === currValue);
|
||||
}
|
||||
|
||||
this.component[prop] = currValue;
|
||||
}
|
||||
}
|
59
packages/upgrade/src/common/downgrade_injectable.ts
Normal file
59
packages/upgrade/src/common/downgrade_injectable.ts
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @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';
|
||||
|
||||
/**
|
||||
* @whatItDoes
|
||||
*
|
||||
* *Part of the [upgrade/static](/docs/ts/latest/api/#!?query=upgrade%2Fstatic)
|
||||
* library for hybrid upgrade apps that support AoT compilation*
|
||||
*
|
||||
* Allow an Angular service to be accessible from AngularJS.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* First ensure that the service to be downgraded is provided in an {@link NgModule}
|
||||
* that will be part of the upgrade application. For example, let's assume we have
|
||||
* defined `HeroesService`
|
||||
*
|
||||
* {@example upgrade/static/ts/module.ts region="ng2-heroes-service"}
|
||||
*
|
||||
* and that we have included this in our upgrade app {@link NgModule}
|
||||
*
|
||||
* {@example upgrade/static/ts/module.ts region="ng2-module"}
|
||||
*
|
||||
* Now we can register the `downgradeInjectable` factory function for the service
|
||||
* on an AngularJS module.
|
||||
*
|
||||
* {@example upgrade/static/ts/module.ts region="downgrade-ng2-heroes-service"}
|
||||
*
|
||||
* Inside an AngularJS component's controller we can get hold of the
|
||||
* downgraded service via the name we gave when downgrading.
|
||||
*
|
||||
* {@example upgrade/static/ts/module.ts region="example-app"}
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* Takes a `token` that identifies a service provided from Angular.
|
||||
*
|
||||
* Returns a [factory function](https://docs.angularjs.org/guide/di) that can be
|
||||
* used to register the service on an AngularJS module.
|
||||
*
|
||||
* The factory function provides access to the Angular service that
|
||||
* is identified by the `token` parameter.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export function downgradeInjectable(token: any): Function {
|
||||
const factory = function(i: Injector) { return i.get(token); };
|
||||
(factory as any).$inject = [INJECTOR_KEY];
|
||||
|
||||
return factory;
|
||||
}
|
77
packages/upgrade/src/common/util.ts
Normal file
77
packages/upgrade/src/common/util.ts
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @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';
|
||||
import * as angular from './angular1';
|
||||
|
||||
export function onError(e: any) {
|
||||
// TODO: (misko): We seem to not have a stack trace here!
|
||||
if (console.error) {
|
||||
console.error(e, e.stack);
|
||||
} else {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(e, e.stack);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
export function controllerKey(name: string): string {
|
||||
return '$' + name + 'Controller';
|
||||
}
|
||||
|
||||
export function getAttributesAsArray(node: Node): [string, string][] {
|
||||
const attributes = node.attributes;
|
||||
let asArray: [string, string][];
|
||||
if (attributes) {
|
||||
let attrLen = attributes.length;
|
||||
asArray = new Array(attrLen);
|
||||
for (let i = 0; i < attrLen; i++) {
|
||||
asArray[i] = [attributes[i].nodeName, attributes[i].nodeValue];
|
||||
}
|
||||
}
|
||||
return asArray || [];
|
||||
}
|
||||
|
||||
export function getComponentName(component: Type<any>): string {
|
||||
// Return the name of the component or the first line of its stringified version.
|
||||
return (component as any).overriddenName || component.name || component.toString().split('\n')[0];
|
||||
}
|
||||
|
||||
export class Deferred<R> {
|
||||
promise: Promise<R>;
|
||||
resolve: (value?: R|PromiseLike<R>) => void;
|
||||
reject: (error?: any) => void;
|
||||
|
||||
constructor() {
|
||||
this.promise = new Promise((res, rej) => {
|
||||
this.resolve = res;
|
||||
this.reject = rej;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether the passed-in component implements the subset of the
|
||||
* `ControlValueAccessor` interface needed for AngularJS `ng-model`
|
||||
* compatibility.
|
||||
*/
|
||||
function supportsNgModel(component: any) {
|
||||
return typeof component.writeValue === 'function' &&
|
||||
typeof component.registerOnChange === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* Glue the AngularJS `NgModelController` (if it exists) to the component
|
||||
* (if it implements the needed subset of the `ControlValueAccessor` interface).
|
||||
*/
|
||||
export function hookupNgModel(ngModel: angular.INgModelController, component: any) {
|
||||
if (ngModel && supportsNgModel(component)) {
|
||||
ngModel.$render = () => { component.writeValue(ngModel.$viewValue); };
|
||||
component.registerOnChange(ngModel.$setViewValue.bind(ngModel));
|
||||
}
|
||||
}
|
19
packages/upgrade/src/common/version.ts
Normal file
19
packages/upgrade/src/common/version.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module
|
||||
* @description
|
||||
* Entry point for all public APIs of the common package.
|
||||
*/
|
||||
|
||||
import {Version} from '@angular/core';
|
||||
/**
|
||||
* @stable
|
||||
*/
|
||||
export const VERSION = new Version('0.0.0-PLACEHOLDER');
|
70
packages/upgrade/src/dynamic/content_projection_helper.ts
Normal file
70
packages/upgrade/src/dynamic/content_projection_helper.ts
Normal file
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @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 {CssSelector, SelectorMatcher, createElementCssSelector} from '@angular/compiler';
|
||||
import {Compiler, Type} from '@angular/core';
|
||||
|
||||
import * as angular from '../common/angular1';
|
||||
import {COMPILER_KEY} from '../common/constants';
|
||||
import {ContentProjectionHelper} from '../common/content_projection_helper';
|
||||
import {getAttributesAsArray, getComponentName} from '../common/util';
|
||||
|
||||
|
||||
export class DynamicContentProjectionHelper extends ContentProjectionHelper {
|
||||
groupProjectableNodes($injector: angular.IInjectorService, component: Type<any>, nodes: Node[]):
|
||||
Node[][] {
|
||||
const ng2Compiler = $injector.get(COMPILER_KEY) as Compiler;
|
||||
const ngContentSelectors = ng2Compiler.getNgContentSelectors(component);
|
||||
|
||||
if (!ngContentSelectors) {
|
||||
throw new Error('Expecting ngContentSelectors for: ' + getComponentName(component));
|
||||
}
|
||||
|
||||
return this.groupNodesBySelector(ngContentSelectors, nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Group a set of DOM nodes into `ngContent` groups, based on the given content selectors.
|
||||
*/
|
||||
groupNodesBySelector(ngContentSelectors: string[], nodes: Node[]): Node[][] {
|
||||
const projectableNodes: Node[][] = [];
|
||||
let matcher = new SelectorMatcher();
|
||||
let wildcardNgContentIndex: number;
|
||||
|
||||
for (let i = 0, ii = ngContentSelectors.length; i < ii; ++i) {
|
||||
projectableNodes[i] = [];
|
||||
|
||||
const selector = ngContentSelectors[i];
|
||||
if (selector === '*') {
|
||||
wildcardNgContentIndex = i;
|
||||
} else {
|
||||
matcher.addSelectables(CssSelector.parse(selector), i);
|
||||
}
|
||||
}
|
||||
|
||||
for (let j = 0, jj = nodes.length; j < jj; ++j) {
|
||||
const ngContentIndices: number[] = [];
|
||||
const node = nodes[j];
|
||||
const selector =
|
||||
createElementCssSelector(node.nodeName.toLowerCase(), getAttributesAsArray(node));
|
||||
|
||||
matcher.match(selector, (_, index) => ngContentIndices.push(index));
|
||||
ngContentIndices.sort();
|
||||
|
||||
if (wildcardNgContentIndex !== undefined) {
|
||||
ngContentIndices.push(wildcardNgContentIndex);
|
||||
}
|
||||
|
||||
if (ngContentIndices.length) {
|
||||
projectableNodes[ngContentIndices[0]].push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return projectableNodes;
|
||||
}
|
||||
}
|
679
packages/upgrade/src/dynamic/upgrade_adapter.ts
Normal file
679
packages/upgrade/src/dynamic/upgrade_adapter.ts
Normal file
@ -0,0 +1,679 @@
|
||||
/**
|
||||
* @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 {DirectiveResolver} from '@angular/compiler';
|
||||
import {Compiler, CompilerOptions, Directive, Injector, NgModule, NgModuleRef, NgZone, Provider, Testability, Type} from '@angular/core';
|
||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||
|
||||
import * as angular from '../common/angular1';
|
||||
import {ComponentInfo} from '../common/component_info';
|
||||
import {$$TESTABILITY, $COMPILE, $INJECTOR, $ROOT_SCOPE, COMPILER_KEY, INJECTOR_KEY, NG_ZONE_KEY} from '../common/constants';
|
||||
import {ContentProjectionHelper} from '../common/content_projection_helper';
|
||||
import {downgradeComponent} from '../common/downgrade_component';
|
||||
import {downgradeInjectable} from '../common/downgrade_injectable';
|
||||
import {Deferred, controllerKey, onError} from '../common/util';
|
||||
|
||||
import {DynamicContentProjectionHelper} from './content_projection_helper';
|
||||
import {UpgradeNg1ComponentAdapterBuilder} from './upgrade_ng1_adapter';
|
||||
|
||||
let upgradeCount: number = 0;
|
||||
|
||||
/**
|
||||
* Use `UpgradeAdapter` to allow AngularJS and Angular to coexist in a single application.
|
||||
*
|
||||
* The `UpgradeAdapter` allows:
|
||||
* 1. creation of Angular component from AngularJS component directive
|
||||
* (See [UpgradeAdapter#upgradeNg1Component()])
|
||||
* 2. creation of AngularJS directive from Angular component.
|
||||
* (See [UpgradeAdapter#downgradeNg2Component()])
|
||||
* 3. Bootstrapping of a hybrid Angular application which contains both of the frameworks
|
||||
* coexisting in a single application.
|
||||
*
|
||||
* ## Mental Model
|
||||
*
|
||||
* When reasoning about how a hybrid application works it is useful to have a mental model which
|
||||
* describes what is happening and explains what is happening at the lowest level.
|
||||
*
|
||||
* 1. There are two independent frameworks running in a single application, each framework treats
|
||||
* the other as a black box.
|
||||
* 2. Each DOM element on the page is owned exactly by one framework. Whichever framework
|
||||
* instantiated the element is the owner. Each framework only updates/interacts with its own
|
||||
* DOM elements and ignores others.
|
||||
* 3. AngularJS directives always execute inside AngularJS framework codebase regardless of
|
||||
* where they are instantiated.
|
||||
* 4. Angular components always execute inside Angular framework codebase regardless of
|
||||
* where they are instantiated.
|
||||
* 5. An AngularJS component can be upgraded to an Angular component. This creates an
|
||||
* Angular directive, which bootstraps the AngularJS component directive in that location.
|
||||
* 6. An Angular component can be downgraded to an AngularJS component directive. This creates
|
||||
* an AngularJS directive, which bootstraps the Angular component in that location.
|
||||
* 7. Whenever an adapter component is instantiated the host element is owned by the framework
|
||||
* doing the instantiation. The other framework then instantiates and owns the view for that
|
||||
* component. This implies that component bindings will always follow the semantics of the
|
||||
* instantiation framework. The syntax is always that of Angular syntax.
|
||||
* 8. AngularJS is always bootstrapped first and owns the bottom most view.
|
||||
* 9. The new application is running in Angular zone, and therefore it no longer needs calls to
|
||||
* `$apply()`.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```
|
||||
* const adapter = new UpgradeAdapter(forwardRef(() => MyNg2Module), myCompilerOptions);
|
||||
* const module = angular.module('myExample', []);
|
||||
* module.directive('ng2Comp', adapter.downgradeNg2Component(Ng2Component));
|
||||
*
|
||||
* module.directive('ng1Hello', function() {
|
||||
* return {
|
||||
* scope: { title: '=' },
|
||||
* template: 'ng1[Hello {{title}}!](<span ng-transclude></span>)'
|
||||
* };
|
||||
* });
|
||||
*
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'ng2-comp',
|
||||
* inputs: ['name'],
|
||||
* template: 'ng2[<ng1-hello [title]="name">transclude</ng1-hello>](<ng-content></ng-content>)',
|
||||
* directives:
|
||||
* })
|
||||
* class Ng2Component {
|
||||
* }
|
||||
*
|
||||
* @NgModule({
|
||||
* declarations: [Ng2Component, adapter.upgradeNg1Component('ng1Hello')],
|
||||
* imports: [BrowserModule]
|
||||
* })
|
||||
* class MyNg2Module {}
|
||||
*
|
||||
*
|
||||
* document.body.innerHTML = '<ng2-comp name="World">project</ng2-comp>';
|
||||
*
|
||||
* adapter.bootstrap(document.body, ['myExample']).ready(function() {
|
||||
* expect(document.body.textContent).toEqual(
|
||||
* "ng2[ng1[Hello World!](transclude)](project)");
|
||||
* });
|
||||
*
|
||||
* ```
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export class UpgradeAdapter {
|
||||
private idPrefix: string = `NG2_UPGRADE_${upgradeCount++}_`;
|
||||
private directiveResolver: DirectiveResolver = new DirectiveResolver();
|
||||
private downgradedComponents: Type<any>[] = [];
|
||||
/**
|
||||
* An internal map of ng1 components which need to up upgraded to ng2.
|
||||
*
|
||||
* We can't upgrade until injector is instantiated and we can retrieve the component metadata.
|
||||
* For this reason we keep a list of components to upgrade until ng1 injector is bootstrapped.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
private ng1ComponentsToBeUpgraded: {[name: string]: UpgradeNg1ComponentAdapterBuilder} = {};
|
||||
private upgradedProviders: Provider[] = [];
|
||||
private ngZone: NgZone;
|
||||
private ng1Module: angular.IModule;
|
||||
private moduleRef: NgModuleRef<any> = null;
|
||||
private ng2BootstrapDeferred: Deferred<angular.IInjectorService>;
|
||||
|
||||
constructor(private ng2AppModule: Type<any>, private compilerOptions?: CompilerOptions) {
|
||||
if (!ng2AppModule) {
|
||||
throw new Error(
|
||||
'UpgradeAdapter cannot be instantiated without an NgModule of the Angular app.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows Angular Component to be used from AngularJS.
|
||||
*
|
||||
* Use `downgradeNg2Component` to create an AngularJS Directive Definition Factory from
|
||||
* Angular Component. The adapter will bootstrap Angular component from within the
|
||||
* AngularJS template.
|
||||
*
|
||||
* ## Mental Model
|
||||
*
|
||||
* 1. The component is instantiated by being listed in AngularJS template. This means that the
|
||||
* host element is controlled by AngularJS, but the component's view will be controlled by
|
||||
* Angular.
|
||||
* 2. Even thought the component is instantiated in AngularJS, it will be using Angular
|
||||
* syntax. This has to be done, this way because we must follow Angular components do not
|
||||
* declare how the attributes should be interpreted.
|
||||
* 3. `ng-model` is controlled by AngularJS and communicates with the downgraded Angular component
|
||||
* by way of the `ControlValueAccessor` interface from @angular/forms. Only components that
|
||||
* implement this interface are eligible.
|
||||
*
|
||||
* ## Supported Features
|
||||
*
|
||||
* - Bindings:
|
||||
* - Attribute: `<comp name="World">`
|
||||
* - Interpolation: `<comp greeting="Hello {{name}}!">`
|
||||
* - Expression: `<comp [name]="username">`
|
||||
* - Event: `<comp (close)="doSomething()">`
|
||||
* - ng-model: `<comp ng-model="name">`
|
||||
* - Content projection: yes
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```
|
||||
* const adapter = new UpgradeAdapter(forwardRef(() => MyNg2Module));
|
||||
* const module = angular.module('myExample', []);
|
||||
* module.directive('greet', adapter.downgradeNg2Component(Greeter));
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'greet',
|
||||
* template: '{{salutation}} {{name}}! - <ng-content></ng-content>'
|
||||
* })
|
||||
* class Greeter {
|
||||
* @Input() salutation: string;
|
||||
* @Input() name: string;
|
||||
* }
|
||||
*
|
||||
* @NgModule({
|
||||
* declarations: [Greeter],
|
||||
* imports: [BrowserModule]
|
||||
* })
|
||||
* class MyNg2Module {}
|
||||
*
|
||||
* document.body.innerHTML =
|
||||
* 'ng1 template: <greet salutation="Hello" [name]="world">text</greet>';
|
||||
*
|
||||
* adapter.bootstrap(document.body, ['myExample']).ready(function() {
|
||||
* expect(document.body.textContent).toEqual("ng1 template: Hello world! - text");
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
downgradeNg2Component(component: Type<any>): Function {
|
||||
this.downgradedComponents.push(component);
|
||||
|
||||
const metadata: Directive = this.directiveResolver.resolve(component);
|
||||
const info: ComponentInfo = {component, inputs: metadata.inputs, outputs: metadata.outputs};
|
||||
|
||||
return downgradeComponent(info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows AngularJS Component to be used from Angular.
|
||||
*
|
||||
* Use `upgradeNg1Component` to create an Angular component from AngularJS Component
|
||||
* directive. The adapter will bootstrap AngularJS component from within the Angular
|
||||
* template.
|
||||
*
|
||||
* ## Mental Model
|
||||
*
|
||||
* 1. The component is instantiated by being listed in Angular template. This means that the
|
||||
* host element is controlled by Angular, but the component's view will be controlled by
|
||||
* AngularJS.
|
||||
*
|
||||
* ## Supported Features
|
||||
*
|
||||
* - Bindings:
|
||||
* - Attribute: `<comp name="World">`
|
||||
* - Interpolation: `<comp greeting="Hello {{name}}!">`
|
||||
* - Expression: `<comp [name]="username">`
|
||||
* - Event: `<comp (close)="doSomething()">`
|
||||
* - Transclusion: yes
|
||||
* - Only some of the features of
|
||||
* [Directive Definition Object](https://docs.angularjs.org/api/ng/service/$compile) are
|
||||
* supported:
|
||||
* - `compile`: not supported because the host element is owned by Angular, which does
|
||||
* not allow modifying DOM structure during compilation.
|
||||
* - `controller`: supported. (NOTE: injection of `$attrs` and `$transclude` is not supported.)
|
||||
* - `controllerAs`: supported.
|
||||
* - `bindToController`: supported.
|
||||
* - `link`: supported. (NOTE: only pre-link function is supported.)
|
||||
* - `name`: supported.
|
||||
* - `priority`: ignored.
|
||||
* - `replace`: not supported.
|
||||
* - `require`: supported.
|
||||
* - `restrict`: must be set to 'E'.
|
||||
* - `scope`: supported.
|
||||
* - `template`: supported.
|
||||
* - `templateUrl`: supported.
|
||||
* - `terminal`: ignored.
|
||||
* - `transclude`: supported.
|
||||
*
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```
|
||||
* const adapter = new UpgradeAdapter(forwardRef(() => MyNg2Module));
|
||||
* const module = angular.module('myExample', []);
|
||||
*
|
||||
* module.directive('greet', function() {
|
||||
* return {
|
||||
* scope: {salutation: '=', name: '=' },
|
||||
* template: '{{salutation}} {{name}}! - <span ng-transclude></span>'
|
||||
* };
|
||||
* });
|
||||
*
|
||||
* module.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'ng2',
|
||||
* template: 'ng2 template: <greet salutation="Hello" [name]="world">text</greet>'
|
||||
* })
|
||||
* class Ng2Component {
|
||||
* }
|
||||
*
|
||||
* @NgModule({
|
||||
* declarations: [Ng2Component, adapter.upgradeNg1Component('greet')],
|
||||
* imports: [BrowserModule]
|
||||
* })
|
||||
* class MyNg2Module {}
|
||||
*
|
||||
* document.body.innerHTML = '<ng2></ng2>';
|
||||
*
|
||||
* adapter.bootstrap(document.body, ['myExample']).ready(function() {
|
||||
* expect(document.body.textContent).toEqual("ng2 template: Hello world! - text");
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
upgradeNg1Component(name: string): Type<any> {
|
||||
if ((<any>this.ng1ComponentsToBeUpgraded).hasOwnProperty(name)) {
|
||||
return this.ng1ComponentsToBeUpgraded[name].type;
|
||||
} else {
|
||||
return (this.ng1ComponentsToBeUpgraded[name] = new UpgradeNg1ComponentAdapterBuilder(name))
|
||||
.type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the adapter's AngularJS upgrade module for unit testing in AngularJS.
|
||||
* Use this instead of `angular.mock.module()` to load the upgrade module into
|
||||
* the AngularJS testing injector.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```
|
||||
* const upgradeAdapter = new UpgradeAdapter(MyNg2Module);
|
||||
*
|
||||
* // configure the adapter with upgrade/downgrade components and services
|
||||
* upgradeAdapter.downgradeNg2Component(MyComponent);
|
||||
*
|
||||
* let upgradeAdapterRef: UpgradeAdapterRef;
|
||||
* let $compile, $rootScope;
|
||||
*
|
||||
* // We must register the adapter before any calls to `inject()`
|
||||
* beforeEach(() => {
|
||||
* upgradeAdapterRef = upgradeAdapter.registerForNg1Tests(['heroApp']);
|
||||
* });
|
||||
*
|
||||
* beforeEach(inject((_$compile_, _$rootScope_) => {
|
||||
* $compile = _$compile_;
|
||||
* $rootScope = _$rootScope_;
|
||||
* }));
|
||||
*
|
||||
* it("says hello", (done) => {
|
||||
* upgradeAdapterRef.ready(() => {
|
||||
* const element = $compile("<my-component></my-component>")($rootScope);
|
||||
* $rootScope.$apply();
|
||||
* expect(element.html()).toContain("Hello World");
|
||||
* done();
|
||||
* })
|
||||
* });
|
||||
*
|
||||
* ```
|
||||
*
|
||||
* @param modules any AngularJS modules that the upgrade module should depend upon
|
||||
* @returns an {@link UpgradeAdapterRef}, which lets you register a `ready()` callback to
|
||||
* run assertions once the Angular components are ready to test through AngularJS.
|
||||
*/
|
||||
registerForNg1Tests(modules?: string[]): UpgradeAdapterRef {
|
||||
const windowNgMock = (window as any)['angular'].mock;
|
||||
if (!windowNgMock || !windowNgMock.module) {
|
||||
throw new Error('Failed to find \'angular.mock.module\'.');
|
||||
}
|
||||
this.declareNg1Module(modules);
|
||||
windowNgMock.module(this.ng1Module.name);
|
||||
const upgrade = new UpgradeAdapterRef();
|
||||
this.ng2BootstrapDeferred.promise.then(
|
||||
(ng1Injector) => { (<any>upgrade)._bootstrapDone(this.moduleRef, ng1Injector); }, onError);
|
||||
return upgrade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap a hybrid AngularJS / Angular application.
|
||||
*
|
||||
* This `bootstrap` method is a direct replacement (takes same arguments) for AngularJS
|
||||
* [`bootstrap`](https://docs.angularjs.org/api/ng/function/angular.bootstrap) method. Unlike
|
||||
* AngularJS, this bootstrap is asynchronous.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```
|
||||
* const adapter = new UpgradeAdapter(MyNg2Module);
|
||||
* const module = angular.module('myExample', []);
|
||||
* module.directive('ng2', adapter.downgradeNg2Component(Ng2));
|
||||
*
|
||||
* module.directive('ng1', function() {
|
||||
* return {
|
||||
* scope: { title: '=' },
|
||||
* template: 'ng1[Hello {{title}}!](<span ng-transclude></span>)'
|
||||
* };
|
||||
* });
|
||||
*
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'ng2',
|
||||
* inputs: ['name'],
|
||||
* template: 'ng2[<ng1 [title]="name">transclude</ng1>](<ng-content></ng-content>)'
|
||||
* })
|
||||
* class Ng2 {
|
||||
* }
|
||||
*
|
||||
* @NgModule({
|
||||
* declarations: [Ng2, adapter.upgradeNg1Component('ng1')],
|
||||
* imports: [BrowserModule]
|
||||
* })
|
||||
* class MyNg2Module {}
|
||||
*
|
||||
* document.body.innerHTML = '<ng2 name="World">project</ng2>';
|
||||
*
|
||||
* adapter.bootstrap(document.body, ['myExample']).ready(function() {
|
||||
* expect(document.body.textContent).toEqual(
|
||||
* "ng2[ng1[Hello World!](transclude)](project)");
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
bootstrap(element: Element, modules?: any[], config?: angular.IAngularBootstrapConfig):
|
||||
UpgradeAdapterRef {
|
||||
this.declareNg1Module(modules);
|
||||
|
||||
const upgrade = new UpgradeAdapterRef();
|
||||
|
||||
// Make sure resumeBootstrap() only exists if the current bootstrap is deferred
|
||||
const windowAngular = (window as any /** TODO #???? */)['angular'];
|
||||
windowAngular.resumeBootstrap = undefined;
|
||||
|
||||
this.ngZone.run(() => { angular.bootstrap(element, [this.ng1Module.name], config); });
|
||||
const ng1BootstrapPromise = new Promise((resolve) => {
|
||||
if (windowAngular.resumeBootstrap) {
|
||||
const originalResumeBootstrap: () => void = windowAngular.resumeBootstrap;
|
||||
windowAngular.resumeBootstrap = function() {
|
||||
windowAngular.resumeBootstrap = originalResumeBootstrap;
|
||||
windowAngular.resumeBootstrap.apply(this, arguments);
|
||||
resolve();
|
||||
};
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
Promise.all([this.ng2BootstrapDeferred.promise, ng1BootstrapPromise]).then(([ng1Injector]) => {
|
||||
angular.element(element).data(controllerKey(INJECTOR_KEY), this.moduleRef.injector);
|
||||
this.moduleRef.injector.get(NgZone).run(
|
||||
() => { (<any>upgrade)._bootstrapDone(this.moduleRef, ng1Injector); });
|
||||
}, onError);
|
||||
return upgrade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows AngularJS service to be accessible from Angular.
|
||||
*
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```
|
||||
* class Login { ... }
|
||||
* class Server { ... }
|
||||
*
|
||||
* @Injectable()
|
||||
* class Example {
|
||||
* constructor(@Inject('server') server, login: Login) {
|
||||
* ...
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* const module = angular.module('myExample', []);
|
||||
* module.service('server', Server);
|
||||
* module.service('login', Login);
|
||||
*
|
||||
* const adapter = new UpgradeAdapter(MyNg2Module);
|
||||
* adapter.upgradeNg1Provider('server');
|
||||
* adapter.upgradeNg1Provider('login', {asToken: Login});
|
||||
*
|
||||
* adapter.bootstrap(document.body, ['myExample']).ready((ref) => {
|
||||
* const example: Example = ref.ng2Injector.get(Example);
|
||||
* });
|
||||
*
|
||||
* ```
|
||||
*/
|
||||
upgradeNg1Provider(name: string, options?: {asToken: any}) {
|
||||
const token = options && options.asToken || name;
|
||||
this.upgradedProviders.push({
|
||||
provide: token,
|
||||
useFactory: ($injector: angular.IInjectorService) => $injector.get(name),
|
||||
deps: [$INJECTOR]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows Angular service to be accessible from AngularJS.
|
||||
*
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```
|
||||
* class Example {
|
||||
* }
|
||||
*
|
||||
* const adapter = new UpgradeAdapter(MyNg2Module);
|
||||
*
|
||||
* const module = angular.module('myExample', []);
|
||||
* module.factory('example', adapter.downgradeNg2Provider(Example));
|
||||
*
|
||||
* adapter.bootstrap(document.body, ['myExample']).ready((ref) => {
|
||||
* const example: Example = ref.ng1Injector.get('example');
|
||||
* });
|
||||
*
|
||||
* ```
|
||||
*/
|
||||
downgradeNg2Provider(token: any): Function { return downgradeInjectable(token); }
|
||||
|
||||
/**
|
||||
* Declare the AngularJS upgrade module for this adapter without bootstrapping the whole
|
||||
* hybrid application.
|
||||
*
|
||||
* This method is automatically called by `bootstrap()` and `registerForNg1Tests()`.
|
||||
*
|
||||
* @param modules The AngularJS modules that this upgrade module should depend upon.
|
||||
* @returns The AngularJS upgrade module that is declared by this method
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```
|
||||
* const upgradeAdapter = new UpgradeAdapter(MyNg2Module);
|
||||
* upgradeAdapter.declareNg1Module(['heroApp']);
|
||||
* ```
|
||||
*/
|
||||
private declareNg1Module(modules: string[] = []): angular.IModule {
|
||||
const delayApplyExps: Function[] = [];
|
||||
let original$applyFn: Function;
|
||||
let rootScopePrototype: any;
|
||||
let rootScope: angular.IRootScopeService;
|
||||
const upgradeAdapter = this;
|
||||
const ng1Module = this.ng1Module = angular.module(this.idPrefix, modules);
|
||||
const platformRef = platformBrowserDynamic();
|
||||
|
||||
this.ngZone = new NgZone({enableLongStackTrace: Zone.hasOwnProperty('longStackTraceZoneSpec')});
|
||||
this.ng2BootstrapDeferred = new Deferred();
|
||||
ng1Module.factory(INJECTOR_KEY, () => this.moduleRef.injector.get(Injector))
|
||||
.constant(NG_ZONE_KEY, this.ngZone)
|
||||
.factory(COMPILER_KEY, () => this.moduleRef.injector.get(Compiler))
|
||||
.config([
|
||||
'$provide', '$injector',
|
||||
(provide: angular.IProvideService, ng1Injector: angular.IInjectorService) => {
|
||||
provide.decorator($ROOT_SCOPE, [
|
||||
'$delegate',
|
||||
function(rootScopeDelegate: angular.IRootScopeService) {
|
||||
// Capture the root apply so that we can delay first call to $apply until we
|
||||
// bootstrap Angular and then we replay and restore the $apply.
|
||||
rootScopePrototype = rootScopeDelegate.constructor.prototype;
|
||||
if (rootScopePrototype.hasOwnProperty('$apply')) {
|
||||
original$applyFn = rootScopePrototype.$apply;
|
||||
rootScopePrototype.$apply = (exp: any) => delayApplyExps.push(exp);
|
||||
} else {
|
||||
throw new Error('Failed to find \'$apply\' on \'$rootScope\'!');
|
||||
}
|
||||
return rootScope = rootScopeDelegate;
|
||||
}
|
||||
]);
|
||||
if (ng1Injector.has($$TESTABILITY)) {
|
||||
provide.decorator($$TESTABILITY, [
|
||||
'$delegate',
|
||||
function(testabilityDelegate: angular.ITestabilityService) {
|
||||
const originalWhenStable: Function = testabilityDelegate.whenStable;
|
||||
// Cannot use arrow function below because we need the context
|
||||
const newWhenStable = function(callback: Function) {
|
||||
originalWhenStable.call(this, function() {
|
||||
const ng2Testability: Testability =
|
||||
upgradeAdapter.moduleRef.injector.get(Testability);
|
||||
if (ng2Testability.isStable()) {
|
||||
callback.apply(this, arguments);
|
||||
} else {
|
||||
ng2Testability.whenStable(newWhenStable.bind(this, callback));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
testabilityDelegate.whenStable = newWhenStable;
|
||||
return testabilityDelegate;
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
ng1Module.run([
|
||||
'$injector', '$rootScope',
|
||||
(ng1Injector: angular.IInjectorService, rootScope: angular.IRootScopeService) => {
|
||||
UpgradeNg1ComponentAdapterBuilder.resolve(this.ng1ComponentsToBeUpgraded, ng1Injector)
|
||||
.then(() => {
|
||||
// At this point we have ng1 injector and we have lifted ng1 components into ng2, we
|
||||
// now can bootstrap ng2.
|
||||
const DynamicNgUpgradeModule =
|
||||
NgModule({
|
||||
providers: [
|
||||
{provide: $INJECTOR, useFactory: () => ng1Injector},
|
||||
{provide: $COMPILE, useFactory: () => ng1Injector.get($COMPILE)},
|
||||
{provide: ContentProjectionHelper, useClass: DynamicContentProjectionHelper},
|
||||
this.upgradedProviders
|
||||
],
|
||||
imports: [this.ng2AppModule],
|
||||
entryComponents: this.downgradedComponents
|
||||
}).Class({
|
||||
constructor: function DynamicNgUpgradeModule() {},
|
||||
ngDoBootstrap: function() {}
|
||||
});
|
||||
(platformRef as any)
|
||||
._bootstrapModuleWithZone(
|
||||
DynamicNgUpgradeModule, this.compilerOptions, this.ngZone)
|
||||
.then((ref: NgModuleRef<any>) => {
|
||||
this.moduleRef = ref;
|
||||
this.ngZone.run(() => {
|
||||
if (rootScopePrototype) {
|
||||
rootScopePrototype.$apply = original$applyFn; // restore original $apply
|
||||
while (delayApplyExps.length) {
|
||||
rootScope.$apply(delayApplyExps.shift());
|
||||
}
|
||||
rootScopePrototype = null;
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(() => this.ng2BootstrapDeferred.resolve(ng1Injector), onError)
|
||||
.then(() => {
|
||||
let subscription =
|
||||
this.ngZone.onMicrotaskEmpty.subscribe({next: () => rootScope.$digest()});
|
||||
rootScope.$on('$destroy', () => { subscription.unsubscribe(); });
|
||||
});
|
||||
})
|
||||
.catch((e) => this.ng2BootstrapDeferred.reject(e));
|
||||
}
|
||||
]);
|
||||
|
||||
return ng1Module;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous promise-like object to wrap parent injectors,
|
||||
* to preserve the synchronous nature of AngularJS's $compile.
|
||||
*/
|
||||
class ParentInjectorPromise {
|
||||
private injector: Injector;
|
||||
private callbacks: ((injector: Injector) => any)[] = [];
|
||||
|
||||
constructor(private element: angular.IAugmentedJQuery) {
|
||||
// store the promise on the element
|
||||
element.data(controllerKey(INJECTOR_KEY), this);
|
||||
}
|
||||
|
||||
then(callback: (injector: Injector) => any) {
|
||||
if (this.injector) {
|
||||
callback(this.injector);
|
||||
} else {
|
||||
this.callbacks.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
resolve(injector: Injector) {
|
||||
this.injector = injector;
|
||||
|
||||
// reset the element data to point to the real injector
|
||||
this.element.data(controllerKey(INJECTOR_KEY), injector);
|
||||
|
||||
// clean out the element to prevent memory leaks
|
||||
this.element = null;
|
||||
|
||||
// run all the queued callbacks
|
||||
this.callbacks.forEach((callback) => callback(injector));
|
||||
this.callbacks.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Use `UpgradeAdapterRef` to control a hybrid AngularJS / Angular application.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export class UpgradeAdapterRef {
|
||||
/* @internal */
|
||||
private _readyFn: (upgradeAdapterRef?: UpgradeAdapterRef) => void = null;
|
||||
|
||||
public ng1RootScope: angular.IRootScopeService = null;
|
||||
public ng1Injector: angular.IInjectorService = null;
|
||||
public ng2ModuleRef: NgModuleRef<any> = null;
|
||||
public ng2Injector: Injector = null;
|
||||
|
||||
/* @internal */
|
||||
private _bootstrapDone(ngModuleRef: NgModuleRef<any>, ng1Injector: angular.IInjectorService) {
|
||||
this.ng2ModuleRef = ngModuleRef;
|
||||
this.ng2Injector = ngModuleRef.injector;
|
||||
this.ng1Injector = ng1Injector;
|
||||
this.ng1RootScope = ng1Injector.get($ROOT_SCOPE);
|
||||
this._readyFn && this._readyFn(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback function which is notified upon successful hybrid AngularJS / Angular
|
||||
* application has been bootstrapped.
|
||||
*
|
||||
* The `ready` callback function is invoked inside the Angular zone, therefore it does not
|
||||
* require a call to `$apply()`.
|
||||
*/
|
||||
public ready(fn: (upgradeAdapterRef?: UpgradeAdapterRef) => void) { this._readyFn = fn; }
|
||||
|
||||
/**
|
||||
* Dispose of running hybrid AngularJS / Angular application.
|
||||
*/
|
||||
public dispose() {
|
||||
this.ng1Injector.get($ROOT_SCOPE).$destroy();
|
||||
this.ng2ModuleRef.destroy();
|
||||
}
|
||||
}
|
385
packages/upgrade/src/dynamic/upgrade_ng1_adapter.ts
Normal file
385
packages/upgrade/src/dynamic/upgrade_ng1_adapter.ts
Normal file
@ -0,0 +1,385 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Directive, DoCheck, ElementRef, EventEmitter, Inject, OnChanges, OnInit, SimpleChange, SimpleChanges, Type} from '@angular/core';
|
||||
|
||||
import * as angular from '../common/angular1';
|
||||
import {$COMPILE, $CONTROLLER, $HTTP_BACKEND, $SCOPE, $TEMPLATE_CACHE} from '../common/constants';
|
||||
import {controllerKey} from '../common/util';
|
||||
|
||||
|
||||
interface IBindingDestination {
|
||||
[key: string]: any;
|
||||
$onChanges?: (changes: SimpleChanges) => void;
|
||||
}
|
||||
|
||||
interface IControllerInstance extends IBindingDestination {
|
||||
$doCheck?: () => void;
|
||||
$onDestroy?: () => void;
|
||||
$onInit?: () => void;
|
||||
$postLink?: () => void;
|
||||
}
|
||||
|
||||
type LifecycleHook = '$doCheck' | '$onChanges' | '$onDestroy' | '$onInit' | '$postLink';
|
||||
|
||||
|
||||
const CAMEL_CASE = /([A-Z])/g;
|
||||
const INITIAL_VALUE = {
|
||||
__UNINITIALIZED__: true
|
||||
};
|
||||
const NOT_SUPPORTED: any = 'NOT_SUPPORTED';
|
||||
|
||||
|
||||
export class UpgradeNg1ComponentAdapterBuilder {
|
||||
type: Type<any>;
|
||||
inputs: string[] = [];
|
||||
inputsRename: string[] = [];
|
||||
outputs: string[] = [];
|
||||
outputsRename: string[] = [];
|
||||
propertyOutputs: string[] = [];
|
||||
checkProperties: string[] = [];
|
||||
propertyMap: {[name: string]: string} = {};
|
||||
linkFn: angular.ILinkFn = null;
|
||||
directive: angular.IDirective = null;
|
||||
$controller: angular.IControllerService = null;
|
||||
|
||||
constructor(public name: string) {
|
||||
const selector = name.replace(
|
||||
CAMEL_CASE, (all: any /** TODO #9100 */, next: string) => '-' + next.toLowerCase());
|
||||
const self = this;
|
||||
this.type =
|
||||
Directive({selector: selector, inputs: this.inputsRename, outputs: this.outputsRename})
|
||||
.Class({
|
||||
constructor: [
|
||||
new Inject($SCOPE), ElementRef,
|
||||
function(scope: angular.IScope, elementRef: ElementRef) {
|
||||
return new UpgradeNg1ComponentAdapter(
|
||||
self.linkFn, scope, self.directive, elementRef, self.$controller, self.inputs,
|
||||
self.outputs, self.propertyOutputs, self.checkProperties, self.propertyMap);
|
||||
}
|
||||
],
|
||||
ngOnInit: function() { /* needs to be here for ng2 to properly detect it */ },
|
||||
ngOnChanges: function() { /* needs to be here for ng2 to properly detect it */ },
|
||||
ngDoCheck: function() { /* needs to be here for ng2 to properly detect it */ },
|
||||
ngOnDestroy: function() { /* needs to be here for ng2 to properly detect it */ },
|
||||
});
|
||||
}
|
||||
|
||||
extractDirective(injector: angular.IInjectorService): angular.IDirective {
|
||||
const directives: angular.IDirective[] = injector.get(this.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');
|
||||
const link = directive.link;
|
||||
if (typeof link == 'object') {
|
||||
if ((<angular.IDirectivePrePost>link).post) this.notSupported('link.post');
|
||||
}
|
||||
return directive;
|
||||
}
|
||||
|
||||
private notSupported(feature: string) {
|
||||
throw new Error(`Upgraded directive '${this.name}' does not support '${feature}'.`);
|
||||
}
|
||||
|
||||
extractBindings() {
|
||||
const btcIsObject = typeof this.directive.bindToController === 'object';
|
||||
if (btcIsObject && Object.keys(this.directive.scope).length) {
|
||||
throw new Error(
|
||||
`Binding definitions on scope and controller at the same time are not supported.`);
|
||||
}
|
||||
|
||||
const context = (btcIsObject) ? this.directive.bindToController : this.directive.scope;
|
||||
|
||||
if (typeof context == 'object') {
|
||||
for (const name in context) {
|
||||
if ((<any>context).hasOwnProperty(name)) {
|
||||
let localName = context[name];
|
||||
const type = localName.charAt(0);
|
||||
const typeOptions = localName.charAt(1);
|
||||
localName = typeOptions === '?' ? localName.substr(2) : localName.substr(1);
|
||||
localName = localName || name;
|
||||
|
||||
const outputName = 'output_' + name;
|
||||
const outputNameRename = outputName + ': ' + name;
|
||||
const outputNameRenameChange = outputName + ': ' + name + 'Change';
|
||||
const inputName = 'input_' + name;
|
||||
const inputNameRename = inputName + ': ' + name;
|
||||
switch (type) {
|
||||
case '=':
|
||||
this.propertyOutputs.push(outputName);
|
||||
this.checkProperties.push(localName);
|
||||
this.outputs.push(outputName);
|
||||
this.outputsRename.push(outputNameRenameChange);
|
||||
this.propertyMap[outputName] = localName;
|
||||
this.inputs.push(inputName);
|
||||
this.inputsRename.push(inputNameRename);
|
||||
this.propertyMap[inputName] = localName;
|
||||
break;
|
||||
case '@':
|
||||
// handle the '<' binding of angular 1.5 components
|
||||
case '<':
|
||||
this.inputs.push(inputName);
|
||||
this.inputsRename.push(inputNameRename);
|
||||
this.propertyMap[inputName] = localName;
|
||||
break;
|
||||
case '&':
|
||||
this.outputs.push(outputName);
|
||||
this.outputsRename.push(outputNameRename);
|
||||
this.propertyMap[outputName] = localName;
|
||||
break;
|
||||
default:
|
||||
let json = JSON.stringify(context);
|
||||
throw new Error(
|
||||
`Unexpected mapping '${type}' in '${json}' in '${this.name}' directive.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileTemplate(
|
||||
compile: angular.ICompileService, templateCache: angular.ITemplateCacheService,
|
||||
httpBackend: angular.IHttpBackendService): Promise<angular.ILinkFn> {
|
||||
if (this.directive.template !== undefined) {
|
||||
this.linkFn = compileHtml(
|
||||
isFunction(this.directive.template) ? this.directive.template() :
|
||||
this.directive.template);
|
||||
} else if (this.directive.templateUrl) {
|
||||
const url = isFunction(this.directive.templateUrl) ? this.directive.templateUrl() :
|
||||
this.directive.templateUrl;
|
||||
const html = templateCache.get(url);
|
||||
if (html !== undefined) {
|
||||
this.linkFn = compileHtml(html);
|
||||
} else {
|
||||
return new Promise((resolve, err) => {
|
||||
httpBackend(
|
||||
'GET', url, null,
|
||||
(status: any /** TODO #9100 */, response: any /** TODO #9100 */) => {
|
||||
if (status == 200) {
|
||||
resolve(this.linkFn = compileHtml(templateCache.put(url, response)));
|
||||
} else {
|
||||
err(`GET ${url} returned ${status}: ${response}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Directive '${this.name}' is not a component, it is missing template.`);
|
||||
}
|
||||
return null;
|
||||
function compileHtml(html: any /** TODO #9100 */): angular.ILinkFn {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
return compile(div.childNodes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade ng1 components into Angular.
|
||||
*/
|
||||
static resolve(
|
||||
exportedComponents: {[name: string]: UpgradeNg1ComponentAdapterBuilder},
|
||||
injector: angular.IInjectorService): Promise<angular.ILinkFn[]> {
|
||||
const promises: Promise<angular.ILinkFn>[] = [];
|
||||
const compile: angular.ICompileService = injector.get($COMPILE);
|
||||
const templateCache: angular.ITemplateCacheService = injector.get($TEMPLATE_CACHE);
|
||||
const httpBackend: angular.IHttpBackendService = injector.get($HTTP_BACKEND);
|
||||
const $controller: angular.IControllerService = injector.get($CONTROLLER);
|
||||
for (const name in exportedComponents) {
|
||||
if ((<any>exportedComponents).hasOwnProperty(name)) {
|
||||
const exportedComponent = exportedComponents[name];
|
||||
exportedComponent.directive = exportedComponent.extractDirective(injector);
|
||||
exportedComponent.$controller = $controller;
|
||||
exportedComponent.extractBindings();
|
||||
const promise: Promise<angular.ILinkFn> =
|
||||
exportedComponent.compileTemplate(compile, templateCache, httpBackend);
|
||||
if (promise) promises.push(promise);
|
||||
}
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
}
|
||||
|
||||
class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
|
||||
private controllerInstance: IControllerInstance = null;
|
||||
destinationObj: IBindingDestination = null;
|
||||
checkLastValues: any[] = [];
|
||||
componentScope: angular.IScope;
|
||||
element: Element;
|
||||
$element: any = null;
|
||||
|
||||
constructor(
|
||||
private linkFn: angular.ILinkFn, scope: angular.IScope, private directive: angular.IDirective,
|
||||
elementRef: ElementRef, private $controller: angular.IControllerService,
|
||||
private inputs: string[], private outputs: string[], private propOuts: string[],
|
||||
private checkProperties: string[], private propertyMap: {[key: string]: string}) {
|
||||
this.element = elementRef.nativeElement;
|
||||
this.componentScope = scope.$new(!!directive.scope);
|
||||
this.$element = angular.element(this.element);
|
||||
const controllerType = directive.controller;
|
||||
if (directive.bindToController && controllerType) {
|
||||
this.controllerInstance = this.buildController(controllerType);
|
||||
this.destinationObj = this.controllerInstance;
|
||||
} else {
|
||||
this.destinationObj = this.componentScope;
|
||||
}
|
||||
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
(this as any /** TODO #9100 */)[inputs[i]] = null;
|
||||
}
|
||||
for (let j = 0; j < outputs.length; j++) {
|
||||
const emitter = (this as any /** TODO #9100 */)[outputs[j]] = new EventEmitter();
|
||||
this.setComponentProperty(
|
||||
outputs[j], ((emitter: any /** TODO #9100 */) => (value: any /** TODO #9100 */) =>
|
||||
emitter.emit(value))(emitter));
|
||||
}
|
||||
for (let k = 0; k < propOuts.length; k++) {
|
||||
(this as any /** TODO #9100 */)[propOuts[k]] = new EventEmitter();
|
||||
this.checkLastValues.push(INITIAL_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (!this.directive.bindToController && this.directive.controller) {
|
||||
this.controllerInstance = this.buildController(this.directive.controller);
|
||||
}
|
||||
|
||||
if (this.controllerInstance && isFunction(this.controllerInstance.$onInit)) {
|
||||
this.controllerInstance.$onInit();
|
||||
}
|
||||
|
||||
let link = this.directive.link;
|
||||
if (typeof link == 'object') link = (<angular.IDirectivePrePost>link).pre;
|
||||
if (link) {
|
||||
const attrs: angular.IAttributes = NOT_SUPPORTED;
|
||||
const transcludeFn: angular.ITranscludeFunction = NOT_SUPPORTED;
|
||||
const linkController = this.resolveRequired(this.$element, this.directive.require);
|
||||
(<angular.IDirectiveLinkFn>this.directive.link)(
|
||||
this.componentScope, this.$element, attrs, linkController, transcludeFn);
|
||||
}
|
||||
|
||||
const childNodes: Node[] = [];
|
||||
let childNode: any /** TODO #9100 */;
|
||||
while (childNode = this.element.firstChild) {
|
||||
this.element.removeChild(childNode);
|
||||
childNodes.push(childNode);
|
||||
}
|
||||
this.linkFn(this.componentScope, (clonedElement, scope) => {
|
||||
for (let i = 0, ii = clonedElement.length; i < ii; i++) {
|
||||
this.element.appendChild(clonedElement[i]);
|
||||
}
|
||||
}, {
|
||||
parentBoundTranscludeFn: (scope: any /** TODO #9100 */,
|
||||
cloneAttach: any /** TODO #9100 */) => { cloneAttach(childNodes); }
|
||||
});
|
||||
|
||||
if (this.controllerInstance && isFunction(this.controllerInstance.$postLink)) {
|
||||
this.controllerInstance.$postLink();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
const ng1Changes: any = {};
|
||||
Object.keys(changes).forEach(name => {
|
||||
const change: SimpleChange = changes[name];
|
||||
this.setComponentProperty(name, change.currentValue);
|
||||
ng1Changes[this.propertyMap[name]] = change;
|
||||
});
|
||||
|
||||
if (isFunction(this.destinationObj.$onChanges)) {
|
||||
this.destinationObj.$onChanges(ng1Changes);
|
||||
}
|
||||
}
|
||||
|
||||
ngDoCheck() {
|
||||
const destinationObj = this.destinationObj;
|
||||
const lastValues = this.checkLastValues;
|
||||
const checkProperties = this.checkProperties;
|
||||
for (let i = 0; i < checkProperties.length; i++) {
|
||||
const value = destinationObj[checkProperties[i]];
|
||||
const last = lastValues[i];
|
||||
if (value !== last) {
|
||||
if (typeof value == 'number' && isNaN(value) && typeof last == 'number' && isNaN(last)) {
|
||||
// ignore because NaN != NaN
|
||||
} else {
|
||||
const eventEmitter: EventEmitter<any> = (this as any /** TODO #9100 */)[this.propOuts[i]];
|
||||
eventEmitter.emit(lastValues[i] = value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.controllerInstance && isFunction(this.controllerInstance.$doCheck)) {
|
||||
this.controllerInstance.$doCheck();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.controllerInstance && isFunction(this.controllerInstance.$onDestroy)) {
|
||||
this.controllerInstance.$onDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
setComponentProperty(name: string, value: any) {
|
||||
this.destinationObj[this.propertyMap[name]] = value;
|
||||
}
|
||||
|
||||
private buildController(controllerType: any /** TODO #9100 */) {
|
||||
const locals = {$scope: this.componentScope, $element: this.$element};
|
||||
const controller: any =
|
||||
this.$controller(controllerType, locals, null, this.directive.controllerAs);
|
||||
this.$element.data(controllerKey(this.directive.name), controller);
|
||||
return controller;
|
||||
}
|
||||
|
||||
private resolveRequired(
|
||||
$element: angular.IAugmentedJQuery, require: angular.DirectiveRequireProperty): any {
|
||||
if (!require) {
|
||||
return undefined;
|
||||
} else if (typeof require == 'string') {
|
||||
let name: string = <string>require;
|
||||
let isOptional = false;
|
||||
let startParent = false;
|
||||
let searchParents = false;
|
||||
if (name.charAt(0) == '?') {
|
||||
isOptional = true;
|
||||
name = name.substr(1);
|
||||
}
|
||||
if (name.charAt(0) == '^') {
|
||||
searchParents = true;
|
||||
name = name.substr(1);
|
||||
}
|
||||
if (name.charAt(0) == '^') {
|
||||
startParent = true;
|
||||
name = name.substr(1);
|
||||
}
|
||||
|
||||
const key = controllerKey(name);
|
||||
if (startParent) $element = $element.parent();
|
||||
const dep = searchParents ? $element.inheritedData(key) : $element.data(key);
|
||||
if (!dep && !isOptional) {
|
||||
throw new Error(`Can not locate '${require}' in '${this.directive.name}'.`);
|
||||
}
|
||||
return dep;
|
||||
} else if (require instanceof Array) {
|
||||
const deps: any[] = [];
|
||||
for (let i = 0; i < require.length; i++) {
|
||||
deps.push(this.resolveRequired($element, require[i]));
|
||||
}
|
||||
return deps;
|
||||
}
|
||||
throw new Error(
|
||||
`Directive '${this.directive.name}' require syntax unrecognized: ${this.directive.require}`);
|
||||
}
|
||||
}
|
||||
|
||||
function isFunction(value: any): value is Function {
|
||||
return typeof value === 'function';
|
||||
}
|
46
packages/upgrade/src/static/angular1_providers.ts
Normal file
46
packages/upgrade/src/static/angular1_providers.ts
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as angular from '../common/angular1';
|
||||
|
||||
// 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']}
|
||||
];
|
479
packages/upgrade/src/static/upgrade_component.ts
Normal file
479
packages/upgrade/src/static/upgrade_component.ts
Normal file
@ -0,0 +1,479 @@
|
||||
/**
|
||||
* @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, OnDestroy, OnInit, SimpleChanges, ɵlooseIdentical as looseIdentical} from '@angular/core';
|
||||
import * as angular from '../common/angular1';
|
||||
import {$COMPILE, $CONTROLLER, $HTTP_BACKEND, $INJECTOR, $SCOPE, $TEMPLATE_CACHE} from '../common/constants';
|
||||
import {controllerKey} from '../common/util';
|
||||
|
||||
const REQUIRE_PREFIX_RE = /^(\^\^?)?(\?)?(\^\^?)?/;
|
||||
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 {
|
||||
$doCheck?: () => void;
|
||||
$onDestroy?: () => void;
|
||||
$onInit?: () => void;
|
||||
$postLink?: () => void;
|
||||
}
|
||||
|
||||
type LifecycleHook = '$doCheck' | '$onChanges' | '$onDestroy' | '$onInit' | '$postLink';
|
||||
|
||||
/**
|
||||
* @whatItDoes
|
||||
*
|
||||
* *Part of the [upgrade/static](/docs/ts/latest/api/#!?query=upgrade%2Fstatic)
|
||||
* library for hybrid upgrade apps that support AoT compilation*
|
||||
*
|
||||
* Allows an AngularJS component to be used from Angular.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* Let's assume that you have an AngularJS component called `ng1Hero` that needs
|
||||
* to be made available in Angular templates.
|
||||
*
|
||||
* {@example upgrade/static/ts/module.ts region="ng1-hero"}
|
||||
*
|
||||
* We must create a {@link Directive} that will make this AngularJS component
|
||||
* available inside Angular templates.
|
||||
*
|
||||
* {@example upgrade/static/ts/module.ts region="ng1-hero-wrapper"}
|
||||
*
|
||||
* In this example you can see that we must derive from the {@link UpgradeComponent}
|
||||
* base class but also provide an {@link Directive `@Directive`} decorator. This is
|
||||
* because the AoT compiler requires that this information is statically available at
|
||||
* compile time.
|
||||
*
|
||||
* Note that we must do the following:
|
||||
* * specify the directive's selector (`ng1-hero`)
|
||||
* * specify all inputs and outputs that the AngularJS component expects
|
||||
* * derive from `UpgradeComponent`
|
||||
* * call the base class from the constructor, passing
|
||||
* * the AngularJS name of the component (`ng1Hero`)
|
||||
* * the {@link ElementRef} and {@link Injector} for the component wrapper
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* A helper class that should be used as a base class for creating Angular directives
|
||||
* that wrap AngularJS components that need to be "upgraded".
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
|
||||
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 controllerInstance: IControllerInstance;
|
||||
private bindingDestination: IBindingDestination;
|
||||
|
||||
// We will be instantiating the controller in the `ngOnInit` hook, when the first `ngOnChanges`
|
||||
// will have been already triggered. We store the `SimpleChanges` and "play them back" later.
|
||||
private pendingChanges: SimpleChanges;
|
||||
|
||||
private unregisterDoCheckWatcher: Function;
|
||||
|
||||
/**
|
||||
* Create a new `UpgradeComponent` instance. You should not normally need to do this.
|
||||
* Instead you should derive a new class from this one and call the super constructor
|
||||
* from the base class.
|
||||
*
|
||||
* {@example upgrade/static/ts/module.ts region="ng1-hero-wrapper" }
|
||||
*
|
||||
* * The `name` parameter should be the name of the AngularJS directive.
|
||||
* * The `elementRef` and `injector` parameters should be acquired from Angular by dependency
|
||||
* injection into the base class constructor.
|
||||
*
|
||||
* Note that we must manually implement lifecycle hooks that call through to the super class.
|
||||
* This is because, at the moment, the AoT compiler is not able to tell that the
|
||||
* `UpgradeComponent`
|
||||
* already implements them and so does not wire up calls to them at runtime.
|
||||
*/
|
||||
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);
|
||||
|
||||
// We ask for the AngularJS scope from the Angular 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);
|
||||
|
||||
this.initializeOutputs();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// Collect contents, insert and compile template
|
||||
const contentChildNodes = this.extractChildNodes(this.element);
|
||||
const linkFn = this.compileTemplate(this.directive);
|
||||
|
||||
// Instantiate controller
|
||||
const controllerType = this.directive.controller;
|
||||
const bindToController = this.directive.bindToController;
|
||||
if (controllerType) {
|
||||
this.controllerInstance = this.buildController(
|
||||
controllerType, this.$componentScope, this.$element, this.directive.controllerAs);
|
||||
} else if (bindToController) {
|
||||
throw new Error(
|
||||
`Upgraded directive '${this.directive.name}' specifies 'bindToController' but no controller.`);
|
||||
}
|
||||
|
||||
// Set up outputs
|
||||
this.bindingDestination = bindToController ? this.controllerInstance : this.$componentScope;
|
||||
this.bindOutputs();
|
||||
|
||||
// Require other controllers
|
||||
const directiveRequire = this.getDirectiveRequire(this.directive);
|
||||
const requiredControllers =
|
||||
this.resolveRequire(this.directive.name, this.$element, directiveRequire);
|
||||
|
||||
if (this.directive.bindToController && isMap(directiveRequire)) {
|
||||
const requiredControllersMap = requiredControllers as{[key: string]: IControllerInstance};
|
||||
Object.keys(requiredControllersMap).forEach(key => {
|
||||
this.controllerInstance[key] = requiredControllersMap[key];
|
||||
});
|
||||
}
|
||||
|
||||
// Hook: $onChanges
|
||||
if (this.pendingChanges) {
|
||||
this.forwardChanges(this.pendingChanges);
|
||||
this.pendingChanges = null;
|
||||
}
|
||||
|
||||
// Hook: $onInit
|
||||
if (this.controllerInstance && isFunction(this.controllerInstance.$onInit)) {
|
||||
this.controllerInstance.$onInit();
|
||||
}
|
||||
|
||||
// Hook: $doCheck
|
||||
if (this.controllerInstance && isFunction(this.controllerInstance.$doCheck)) {
|
||||
const callDoCheck = () => this.controllerInstance.$doCheck();
|
||||
|
||||
this.unregisterDoCheckWatcher = this.$componentScope.$parent.$watch(callDoCheck);
|
||||
callDoCheck();
|
||||
}
|
||||
|
||||
// Linking
|
||||
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;
|
||||
const attrs: angular.IAttributes = NOT_SUPPORTED;
|
||||
const transcludeFn: angular.ITranscludeFunction = NOT_SUPPORTED;
|
||||
if (preLink) {
|
||||
preLink(this.$componentScope, this.$element, attrs, requiredControllers, transcludeFn);
|
||||
}
|
||||
|
||||
const attachChildNodes: angular.ILinkFn = (scope, cloneAttach) =>
|
||||
cloneAttach(contentChildNodes);
|
||||
linkFn(this.$componentScope, null, {parentBoundTranscludeFn: attachChildNodes});
|
||||
|
||||
if (postLink) {
|
||||
postLink(this.$componentScope, this.$element, attrs, requiredControllers, transcludeFn);
|
||||
}
|
||||
|
||||
// Hook: $postLink
|
||||
if (this.controllerInstance && isFunction(this.controllerInstance.$postLink)) {
|
||||
this.controllerInstance.$postLink();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (!this.bindingDestination) {
|
||||
this.pendingChanges = changes;
|
||||
} else {
|
||||
this.forwardChanges(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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (isFunction(this.unregisterDoCheckWatcher)) {
|
||||
this.unregisterDoCheckWatcher();
|
||||
}
|
||||
if (this.controllerInstance && isFunction(this.controllerInstance.$onDestroy)) {
|
||||
this.controllerInstance.$onDestroy();
|
||||
}
|
||||
this.$componentScope.$destroy();
|
||||
}
|
||||
|
||||
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 getDirectiveRequire(directive: angular.IDirective): angular.DirectiveRequireProperty {
|
||||
const require = directive.require || (directive.controller && directive.name);
|
||||
|
||||
if (isMap(require)) {
|
||||
Object.keys(require).forEach(key => {
|
||||
const value = require[key];
|
||||
const match = value.match(REQUIRE_PREFIX_RE);
|
||||
const name = value.substring(match[0].length);
|
||||
|
||||
if (!name) {
|
||||
require[key] = match[0] + key;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return require;
|
||||
}
|
||||
|
||||
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:
|
||||
let json = JSON.stringify(context);
|
||||
throw new Error(
|
||||
`Unexpected mapping '${bindingType}' in '${json}' in '${this.name}' directive.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return bindings;
|
||||
}
|
||||
|
||||
private extractChildNodes(element: Element): Node[] {
|
||||
const childNodes: Node[] = [];
|
||||
let childNode: Node;
|
||||
|
||||
while (childNode = element.firstChild) {
|
||||
element.removeChild(childNode);
|
||||
childNodes.push(childNode);
|
||||
}
|
||||
|
||||
return childNodes;
|
||||
}
|
||||
|
||||
private compileTemplate(directive: angular.IDirective): angular.ILinkFn {
|
||||
if (this.directive.template !== undefined) {
|
||||
return this.compileHtml(getOrCall(this.directive.template));
|
||||
} else if (this.directive.templateUrl) {
|
||||
const url = getOrCall(this.directive.templateUrl);
|
||||
const 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) {
|
||||
// TODO: Document that we do not pre-assign bindings on the controller instance
|
||||
// Quoted properties below so that this code can be optimized with Closure Compiler.
|
||||
const locals = {'$scope': $scope, '$element': $element};
|
||||
const controller = this.$controller(controllerType, locals, null, controllerAs);
|
||||
$element.data(controllerKey(this.directive.name), controller);
|
||||
return controller;
|
||||
}
|
||||
|
||||
private resolveRequire(
|
||||
directiveName: string, $element: angular.IAugmentedJQuery,
|
||||
require: angular.DirectiveRequireProperty): angular.SingleOrListOrMap<IControllerInstance> {
|
||||
if (!require) {
|
||||
return null;
|
||||
} else if (Array.isArray(require)) {
|
||||
return require.map(req => this.resolveRequire(directiveName, $element, req));
|
||||
} else if (typeof require === 'object') {
|
||||
const value: {[key: string]: IControllerInstance} = {};
|
||||
|
||||
Object.keys(require).forEach(
|
||||
key => value[key] = this.resolveRequire(directiveName, $element, require[key]));
|
||||
|
||||
return value;
|
||||
} else if (typeof require === 'string') {
|
||||
const match = require.match(REQUIRE_PREFIX_RE);
|
||||
const inheritType = match[1] || match[3];
|
||||
|
||||
const name = require.substring(match[0].length);
|
||||
const isOptional = !!match[2];
|
||||
const searchParents = !!inheritType;
|
||||
const startOnParent = inheritType === '^^';
|
||||
|
||||
const ctrlKey = controllerKey(name);
|
||||
|
||||
if (startOnParent) {
|
||||
$element = $element.parent();
|
||||
}
|
||||
|
||||
const value = searchParents ? $element.inheritedData(ctrlKey) : $element.data(ctrlKey);
|
||||
|
||||
if (!value && !isOptional) {
|
||||
throw new Error(
|
||||
`Unable to find required '${require}' in upgraded directive '${directiveName}'.`);
|
||||
}
|
||||
|
||||
return value;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unrecognized require syntax on upgraded directive '${directiveName}': ${require}`);
|
||||
}
|
||||
}
|
||||
|
||||
private initializeOutputs() {
|
||||
// Initialize the outputs for `=` and `&` bindings
|
||||
this.bindings.twoWayBoundProperties.concat(this.bindings.expressionBoundProperties)
|
||||
.forEach(propName => {
|
||||
const outputName = this.bindings.propertyToOutputMap[propName];
|
||||
(this as any)[outputName] = new EventEmitter();
|
||||
});
|
||||
}
|
||||
|
||||
private bindOutputs() {
|
||||
// Bind `&` bindings to the corresponding outputs
|
||||
this.bindings.expressionBoundProperties.forEach(propName => {
|
||||
const outputName = this.bindings.propertyToOutputMap[propName];
|
||||
const emitter = (this as any)[outputName];
|
||||
|
||||
this.bindingDestination[propName] = (value: any) => emitter.emit(value);
|
||||
});
|
||||
}
|
||||
|
||||
private forwardChanges(changes: SimpleChanges) {
|
||||
// Forward input changes to `bindingDestination`
|
||||
Object.keys(changes).forEach(
|
||||
propName => this.bindingDestination[propName] = changes[propName].currentValue);
|
||||
|
||||
if (isFunction(this.bindingDestination.$onChanges)) {
|
||||
this.bindingDestination.$onChanges(changes);
|
||||
}
|
||||
}
|
||||
|
||||
private notSupported(feature: string) {
|
||||
throw new Error(
|
||||
`Upgraded directive '${this.name}' contains unsupported feature: '${feature}'.`);
|
||||
}
|
||||
|
||||
private compileHtml(html: string): angular.ILinkFn {
|
||||
this.element.innerHTML = html;
|
||||
return this.$compile(this.element.childNodes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getOrCall<T>(property: Function | T): T {
|
||||
return isFunction(property) ? property() : property;
|
||||
}
|
||||
|
||||
function isFunction(value: any): value is Function {
|
||||
return typeof value === 'function';
|
||||
}
|
||||
|
||||
// NOTE: Only works for `typeof T !== 'object'`.
|
||||
function isMap<T>(value: angular.SingleOrListOrMap<T>): value is {[key: string]: T} {
|
||||
return value && !Array.isArray(value) && typeof value === 'object';
|
||||
}
|
237
packages/upgrade/src/static/upgrade_module.ts
Normal file
237
packages/upgrade/src/static/upgrade_module.ts
Normal file
@ -0,0 +1,237 @@
|
||||
/**
|
||||
* @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, Testability} from '@angular/core';
|
||||
|
||||
import * as angular from '../common/angular1';
|
||||
import {$$TESTABILITY, $DELEGATE, $INJECTOR, $PROVIDE, $ROOT_SCOPE, INJECTOR_KEY, UPGRADE_MODULE_NAME} from '../common/constants';
|
||||
import {ContentProjectionHelper} from '../common/content_projection_helper';
|
||||
import {controllerKey} from '../common/util';
|
||||
|
||||
import {angular1Providers, setTempInjectorRef} from './angular1_providers';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @whatItDoes
|
||||
*
|
||||
* *Part of the [upgrade/static](/docs/ts/latest/api/#!?query=upgrade%2Fstatic)
|
||||
* library for hybrid upgrade apps that support AoT compilation*
|
||||
*
|
||||
* Allows AngularJS and Angular components to be used together inside a hybrid upgrade
|
||||
* application, which supports AoT compilation.
|
||||
*
|
||||
* Specifically, the classes and functions in the `upgrade/static` module allow the following:
|
||||
* 1. Creation of an Angular directive that wraps and exposes an AngularJS component so
|
||||
* that it can be used in an Angular template. See {@link UpgradeComponent}.
|
||||
* 2. Creation of an AngularJS directive that wraps and exposes an Angular component so
|
||||
* that it can be used in an AngularJS template. See {@link downgradeComponent}.
|
||||
* 3. Creation of an Angular root injector provider that wraps and exposes an AngularJS
|
||||
* service so that it can be injected into an Angular context. See
|
||||
* {@link UpgradeModule#upgrading-an-angular-1-service Upgrading an AngularJS service} below.
|
||||
* 4. Creation of an AngularJS service that wraps and exposes an Angular injectable
|
||||
* so that it can be injected into an AngularJS context. See {@link downgradeInjectable}.
|
||||
* 3. Bootstrapping of a hybrid Angular application which contains both of the frameworks
|
||||
* coexisting in a single application. See the
|
||||
* {@link UpgradeModule#example example} below.
|
||||
*
|
||||
* ## Mental Model
|
||||
*
|
||||
* When reasoning about how a hybrid application works it is useful to have a mental model which
|
||||
* describes what is happening and explains what is happening at the lowest level.
|
||||
*
|
||||
* 1. There are two independent frameworks running in a single application, each framework treats
|
||||
* the other as a black box.
|
||||
* 2. Each DOM element on the page is owned exactly by one framework. Whichever framework
|
||||
* instantiated the element is the owner. Each framework only updates/interacts with its own
|
||||
* DOM elements and ignores others.
|
||||
* 3. AngularJS directives always execute inside the AngularJS framework codebase regardless of
|
||||
* where they are instantiated.
|
||||
* 4. Angular components always execute inside the Angular framework codebase regardless of
|
||||
* where they are instantiated.
|
||||
* 5. An AngularJS component can be "upgraded"" to an Angular component. This is achieved by
|
||||
* defining an Angular directive, which bootstraps the AngularJS component at its location
|
||||
* in the DOM. See {@link UpgradeComponent}.
|
||||
* 6. An Angular component can be "downgraded"" to an AngularJS component. This is achieved by
|
||||
* defining an AngularJS directive, which bootstraps the Angular component at its location
|
||||
* in the DOM. See {@link downgradeComponent}.
|
||||
* 7. Whenever an "upgraded"/"downgraded" component is instantiated the host element is owned by
|
||||
* the framework doing the instantiation. The other framework then instantiates and owns the
|
||||
* view for that component.
|
||||
* a. This implies that the component bindings will always follow the semantics of the
|
||||
* instantiation framework.
|
||||
* b. The DOM attributes are parsed by the framework that owns the current template. So
|
||||
* attributes
|
||||
* in AngularJS templates must use kebab-case, while AngularJS templates must use camelCase.
|
||||
* c. However the template binding syntax will always use the Angular style, e.g. square
|
||||
* brackets (`[...]`) for property binding.
|
||||
* 8. AngularJS is always bootstrapped first and owns the root component.
|
||||
* 9. The new application is running in an Angular zone, and therefore it no longer needs calls
|
||||
* to
|
||||
* `$apply()`.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* `import {UpgradeModule} from '@angular/upgrade/static';`
|
||||
*
|
||||
* ## Example
|
||||
* Import the {@link UpgradeModule} into your top level {@link NgModule Angular `NgModule`}.
|
||||
*
|
||||
* {@example upgrade/static/ts/module.ts region='ng2-module'}
|
||||
*
|
||||
* Then bootstrap the hybrid upgrade app's module, get hold of the {@link UpgradeModule} instance
|
||||
* and use it to bootstrap the top level [AngularJS
|
||||
* module](https://docs.angularjs.org/api/ng/type/angular.Module).
|
||||
*
|
||||
* {@example upgrade/static/ts/module.ts region='bootstrap'}
|
||||
*
|
||||
*
|
||||
* ## Upgrading an AngularJS service
|
||||
*
|
||||
* There is no specific API for upgrading an AngularJS service. Instead you should just follow the
|
||||
* following recipe:
|
||||
*
|
||||
* Let's say you have an AngularJS service:
|
||||
*
|
||||
* {@example upgrade/static/ts/module.ts region="ng1-title-case-service"}
|
||||
*
|
||||
* Then you should define an Angular provider to be included in your {@link NgModule} `providers`
|
||||
* property.
|
||||
*
|
||||
* {@example upgrade/static/ts/module.ts region="upgrade-ng1-service"}
|
||||
*
|
||||
* Then you can use the "upgraded" AngularJS service by injecting it into an Angular component
|
||||
* or service.
|
||||
*
|
||||
* {@example upgrade/static/ts/module.ts region="use-ng1-upgraded-service"}
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* This class is an `NgModule`, which you import to provide AngularJS core services,
|
||||
* and has an instance method used to bootstrap the hybrid upgrade application.
|
||||
*
|
||||
* ## Core AngularJS services
|
||||
* Importing this {@link NgModule} will add providers for the core
|
||||
* [AngularJS services](https://docs.angularjs.org/api/ng/service) to the root injector.
|
||||
*
|
||||
* ## Bootstrap
|
||||
* The runtime instance of this class contains a {@link UpgradeModule#bootstrap `bootstrap()`}
|
||||
* method, which you use to bootstrap the top level AngularJS module onto an element in the
|
||||
* DOM for the hybrid upgrade app.
|
||||
*
|
||||
* It also contains properties to access the {@link UpgradeModule#injector root injector}, the
|
||||
* bootstrap {@link NgZone} and the
|
||||
* [AngularJS $injector](https://docs.angularjs.org/api/auto/service/$injector).
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
@NgModule({providers: [angular1Providers, ContentProjectionHelper]})
|
||||
export class UpgradeModule {
|
||||
/**
|
||||
* The AngularJS `$injector` for the upgrade application.
|
||||
*/
|
||||
public $injector: any /*angular.IInjectorService*/;
|
||||
|
||||
constructor(
|
||||
/** The root {@link Injector} for the upgrade application. */
|
||||
public injector: Injector,
|
||||
/** The bootstrap zone for the upgrade application */
|
||||
public ngZone: NgZone) {}
|
||||
|
||||
/**
|
||||
* Bootstrap an AngularJS application from this NgModule
|
||||
* @param element the element on which to bootstrap the AngularJS application
|
||||
* @param [modules] the AngularJS modules to bootstrap for this application
|
||||
* @param [config] optional extra AngularJS bootstrap configuration
|
||||
*/
|
||||
bootstrap(
|
||||
element: Element, modules: string[] = [], config?: any /*angular.IAngularBootstrapConfig*/) {
|
||||
const INIT_MODULE_NAME = UPGRADE_MODULE_NAME + '.init';
|
||||
|
||||
// Create an ng1 module to bootstrap
|
||||
const initModule =
|
||||
angular
|
||||
.module(INIT_MODULE_NAME, [])
|
||||
|
||||
.value(INJECTOR_KEY, this.injector)
|
||||
|
||||
.config([
|
||||
$PROVIDE, $INJECTOR,
|
||||
($provide: angular.IProvideService, $injector: angular.IInjectorService) => {
|
||||
if ($injector.has($$TESTABILITY)) {
|
||||
$provide.decorator($$TESTABILITY, [
|
||||
$DELEGATE,
|
||||
(testabilityDelegate: angular.ITestabilityService) => {
|
||||
const originalWhenStable: Function = testabilityDelegate.whenStable;
|
||||
const injector = this.injector;
|
||||
// Cannot use arrow function below because we need the context
|
||||
const newWhenStable = function(callback: Function) {
|
||||
originalWhenStable.call(testabilityDelegate, function() {
|
||||
const ng2Testability: Testability = injector.get(Testability);
|
||||
if (ng2Testability.isStable()) {
|
||||
callback();
|
||||
} else {
|
||||
ng2Testability.whenStable(
|
||||
newWhenStable.bind(testabilityDelegate, callback));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
testabilityDelegate.whenStable = newWhenStable;
|
||||
return testabilityDelegate;
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
.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
|
||||
// We need to do this in the next tick so that we don't prevent the bootup
|
||||
// stabilizing
|
||||
setTimeout(() => {
|
||||
const $rootScope = $injector.get('$rootScope');
|
||||
const subscription =
|
||||
this.ngZone.onMicrotaskEmpty.subscribe(() => $rootScope.$digest());
|
||||
$rootScope.$on('$destroy', () => { subscription.unsubscribe(); });
|
||||
}, 0);
|
||||
}
|
||||
]);
|
||||
|
||||
const upgradeModule = angular.module(UPGRADE_MODULE_NAME, [INIT_MODULE_NAME].concat(modules));
|
||||
|
||||
// Make sure resumeBootstrap() only exists if the current bootstrap is deferred
|
||||
const windowAngular = (window as any /** TODO #???? */)['angular'];
|
||||
windowAngular.resumeBootstrap = undefined;
|
||||
|
||||
// Bootstrap the AngularJS application inside our zone
|
||||
this.ngZone.run(() => { angular.bootstrap(element, [upgradeModule.name], config); });
|
||||
|
||||
// Patch resumeBootstrap() to run inside the ngZone
|
||||
if (windowAngular.resumeBootstrap) {
|
||||
const originalResumeBootstrap: () => void = windowAngular.resumeBootstrap;
|
||||
const ngZone = this.ngZone;
|
||||
windowAngular.resumeBootstrap = function() {
|
||||
let args = arguments;
|
||||
windowAngular.resumeBootstrap = originalResumeBootstrap;
|
||||
ngZone.run(() => { windowAngular.resumeBootstrap.apply(this, args); });
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
14
packages/upgrade/static.ts
Normal file
14
packages/upgrade/static.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
// This file is not used to build this module. It is only used during editing
|
||||
// by the TypeScript language service and during build for verification. `ngc`
|
||||
// replaces this file with production static.ts when it rewrites private symbol
|
||||
// names.
|
||||
|
||||
export * from './public_api_static';
|
21
packages/upgrade/static/tsconfig-build.json
Normal file
21
packages/upgrade/static/tsconfig-build.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "./tsconfig-build",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/packages-dist/upgrade/static",
|
||||
"target": "es5",
|
||||
"paths": {
|
||||
"@angular/core": ["../../../dist/packages-dist/core"],
|
||||
"@angular/common": ["../../../dist/packages-dist/common"],
|
||||
"@angular/platform-browser": ["../../../dist/packages-dist/platform-browser"],
|
||||
"@angular/platform-browser-dynamic": ["../../../dist/packages-dist/platform-browser-dynamic"]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"public_api_static.ts",
|
||||
"../../../node_modules/zone.js/dist/zone.js.d.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"flatModuleOutFile": "static.js",
|
||||
"flatModuleId": "@angular/upgrade/static"
|
||||
}
|
||||
}
|
52
packages/upgrade/test/common/component_info_spec.ts
Normal file
52
packages/upgrade/test/common/component_info_spec.ts
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {PropertyBinding} from '@angular/upgrade/src/common/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');
|
||||
});
|
||||
});
|
||||
}
|
25
packages/upgrade/test/common/downgrade_injectable_spec.ts
Normal file
25
packages/upgrade/test/common/downgrade_injectable_spec.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @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/common/constants';
|
||||
import {downgradeInjectable} from '@angular/upgrade/src/common/downgrade_injectable';
|
||||
|
||||
export function main() {
|
||||
describe('downgradeInjectable', () => {
|
||||
it('should return an AngularJS annotated factory for the token', () => {
|
||||
const factory = downgradeInjectable('someToken');
|
||||
expect(factory).toEqual(jasmine.any(Function));
|
||||
expect((factory as any).$inject).toEqual([INJECTOR_KEY]);
|
||||
|
||||
const injector = {get: jasmine.createSpy('get').and.returnValue('service value')};
|
||||
const value = factory(injector);
|
||||
expect(injector.get).toHaveBeenCalledWith('someToken');
|
||||
expect(value).toEqual('service value');
|
||||
});
|
||||
});
|
||||
}
|
25
packages/upgrade/test/common/test_helpers.ts
Normal file
25
packages/upgrade/test/common/test_helpers.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @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 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();
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* @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 {DynamicContentProjectionHelper} from '@angular/upgrade/src/dynamic/content_projection_helper';
|
||||
import {nodes} from './test_helpers';
|
||||
|
||||
|
||||
export function main() {
|
||||
describe('groupNodesBySelector', () => {
|
||||
let groupNodesBySelector: (ngContentSelectors: string[], nodes: Node[]) => Node[][];
|
||||
|
||||
beforeEach(() => {
|
||||
const projectionHelper = new DynamicContentProjectionHelper();
|
||||
groupNodesBySelector = projectionHelper.groupNodesBySelector.bind(projectionHelper);
|
||||
});
|
||||
|
||||
|
||||
it('should return an array of node collections for each selector', () => {
|
||||
const contentNodes = nodes(
|
||||
'<div class="x"><span>div-1 content</span></div>' +
|
||||
'<input type="number" name="myNum">' +
|
||||
'<input type="date" name="myDate">' +
|
||||
'<span>span content</span>' +
|
||||
'<div class="x"><span>div-2 content</span></div>');
|
||||
|
||||
const selectors = ['input[type=date]', 'span', '.x'];
|
||||
const projectableNodes = groupNodesBySelector(selectors, contentNodes);
|
||||
|
||||
expect(projectableNodes[0]).toEqual(nodes('<input type="date" name="myDate">'));
|
||||
expect(projectableNodes[1]).toEqual(nodes('<span>span content</span>'));
|
||||
expect(projectableNodes[2])
|
||||
.toEqual(nodes(
|
||||
'<div class="x"><span>div-1 content</span></div>' +
|
||||
'<div class="x"><span>div-2 content</span></div>'));
|
||||
});
|
||||
|
||||
it('should collect up unmatched nodes for the wildcard selector', () => {
|
||||
const contentNodes = nodes(
|
||||
'<div class="x"><span>div-1 content</span></div>' +
|
||||
'<input type="number" name="myNum">' +
|
||||
'<input type="date" name="myDate">' +
|
||||
'<span>span content</span>' +
|
||||
'<div class="x"><span>div-2 content</span></div>');
|
||||
|
||||
const selectors = ['.x', '*', 'input[type=date]'];
|
||||
const projectableNodes = groupNodesBySelector(selectors, contentNodes);
|
||||
|
||||
expect(projectableNodes[0])
|
||||
.toEqual(nodes(
|
||||
'<div class="x"><span>div-1 content</span></div>' +
|
||||
'<div class="x"><span>div-2 content</span></div>'));
|
||||
expect(projectableNodes[1])
|
||||
.toEqual(nodes(
|
||||
'<input type="number" name="myNum">' +
|
||||
'<span>span content</span>'));
|
||||
expect(projectableNodes[2]).toEqual(nodes('<input type="date" name="myDate">'));
|
||||
});
|
||||
|
||||
it('should return an array of empty arrays if there are no nodes passed in', () => {
|
||||
const selectors = ['.x', '*', 'input[type=date]'];
|
||||
const projectableNodes = groupNodesBySelector(selectors, []);
|
||||
expect(projectableNodes).toEqual([[], [], []]);
|
||||
});
|
||||
|
||||
it('should return an empty array for each selector that does not match', () => {
|
||||
const contentNodes = nodes(
|
||||
'<div class="x"><span>div-1 content</span></div>' +
|
||||
'<input type="number" name="myNum">' +
|
||||
'<input type="date" name="myDate">' +
|
||||
'<span>span content</span>' +
|
||||
'<div class="x"><span>div-2 content</span></div>');
|
||||
|
||||
const noSelectorNodes = groupNodesBySelector([], contentNodes);
|
||||
expect(noSelectorNodes).toEqual([]);
|
||||
|
||||
const noMatchSelectorNodes = groupNodesBySelector(['.not-there'], contentNodes);
|
||||
expect(noMatchSelectorNodes).toEqual([[]]);
|
||||
});
|
||||
});
|
||||
}
|
15
packages/upgrade/test/dynamic/test_helpers.ts
Normal file
15
packages/upgrade/test/dynamic/test_helpers.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @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 * from '../common/test_helpers';
|
||||
|
||||
export function nodes(html: string) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html.trim();
|
||||
return Array.prototype.slice.call(div.childNodes);
|
||||
}
|
1922
packages/upgrade/test/dynamic/upgrade_spec.ts
Normal file
1922
packages/upgrade/test/dynamic/upgrade_spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
58
packages/upgrade/test/static/angular1_providers_spec.ts
Normal file
58
packages/upgrade/test/static/angular1_providers_spec.ts
Normal file
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Ng1Token} from '@angular/upgrade/src/common/angular1';
|
||||
import {compileFactory, injectorFactory, parseFactory, rootScopeFactory, setTempInjectorRef} from '@angular/upgrade/src/static/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,161 @@
|
||||
/**
|
||||
* @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, NgZone, SimpleChange, 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 * as angular from '@angular/upgrade/src/common/angular1';
|
||||
import {UpgradeComponent, UpgradeModule, downgradeComponent} from '@angular/upgrade/static';
|
||||
|
||||
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']);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should propagate changes to a downgraded component inside the ngZone', async(() => {
|
||||
let appComponent: AppComponent;
|
||||
|
||||
@Component({selector: 'my-app', template: '<my-child [value]="value"></my-child>'})
|
||||
class AppComponent {
|
||||
value: number;
|
||||
constructor() { appComponent = this; }
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-child',
|
||||
template: '<div>{{valueFromPromise}}',
|
||||
})
|
||||
class ChildComponent {
|
||||
valueFromPromise: number;
|
||||
@Input()
|
||||
set value(v: number) { expect(NgZone.isInAngularZone()).toBe(true); }
|
||||
|
||||
constructor(private zone: NgZone) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes['value'].isFirstChange()) return;
|
||||
|
||||
this.zone.onMicrotaskEmpty.subscribe(
|
||||
() => { expect(element.textContent).toEqual('5'); });
|
||||
|
||||
Promise.resolve().then(() => this.valueFromPromise = changes['value'].currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent, ChildComponent],
|
||||
entryComponents: [AppComponent],
|
||||
imports: [BrowserModule, UpgradeModule]
|
||||
})
|
||||
class Ng2Module {
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
||||
const ng1Module = angular.module('ng1', []).directive(
|
||||
'myApp', downgradeComponent({component: AppComponent}));
|
||||
|
||||
|
||||
const element = html('<my-app></my-app>');
|
||||
|
||||
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
|
||||
appComponent.value = 5;
|
||||
});
|
||||
}));
|
||||
|
||||
// This test demonstrates https://github.com/angular/angular/issues/6385
|
||||
// which was invalidly fixed by https://github.com/angular/angular/pull/6386
|
||||
// it('should not trigger $digest from an async operation in a watcher', async(() => {
|
||||
// @Component({selector: 'my-app', template: ''})
|
||||
// class AppComponent {
|
||||
// }
|
||||
|
||||
// @NgModule({declarations: [AppComponent], imports: [BrowserModule]})
|
||||
// class Ng2Module {
|
||||
// }
|
||||
|
||||
// const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
||||
// const ng1Module = angular.module('ng1', []).directive(
|
||||
// 'myApp', adapter.downgradeNg2Component(AppComponent));
|
||||
|
||||
// const element = html('<my-app></my-app>');
|
||||
|
||||
// adapter.bootstrap(element, ['ng1']).ready((ref) => {
|
||||
// let doTimeout = false;
|
||||
// let timeoutId: number;
|
||||
// ref.ng1RootScope.$watch(() => {
|
||||
// if (doTimeout && !timeoutId) {
|
||||
// timeoutId = window.setTimeout(function() {
|
||||
// timeoutId = null;
|
||||
// }, 10);
|
||||
// }
|
||||
// });
|
||||
// doTimeout = true;
|
||||
// });
|
||||
// }));
|
||||
});
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
/**
|
||||
* @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 * as angular from '@angular/upgrade/src/common/angular1';
|
||||
import {UpgradeComponent, UpgradeModule, downgradeComponent} from '@angular/upgrade/static';
|
||||
|
||||
import {bootstrap, html, multiTrim} 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: `{{ prop }}(<ng-content></ng-content>)`})
|
||||
class Ng2Component {
|
||||
prop = 'NG2';
|
||||
ngContent = 'ng2-content';
|
||||
}
|
||||
|
||||
// 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}))
|
||||
.run(($rootScope: angular.IRootScopeService) => {
|
||||
$rootScope['prop'] = 'NG1';
|
||||
$rootScope['ngContent'] = 'ng1-content';
|
||||
});
|
||||
|
||||
const element = html('<div>{{ \'ng1[\' }}<ng2>~{{ ngContent }}~</ng2>{{ \']\' }}</div>');
|
||||
|
||||
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
|
||||
expect(document.body.textContent).toEqual('ng1[NG2(~ng1-content~)]');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should correctly project structural directives', async(() => {
|
||||
@Component({selector: 'ng2', template: 'ng2-{{ itemId }}(<ng-content></ng-content>)'})
|
||||
class Ng2Component {
|
||||
@Input() itemId: string;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [BrowserModule, UpgradeModule],
|
||||
declarations: [Ng2Component],
|
||||
entryComponents: [Ng2Component]
|
||||
})
|
||||
class Ng2Module {
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
||||
const ng1Module =
|
||||
angular.module('ng1', [])
|
||||
.directive(
|
||||
'ng2', downgradeComponent({component: Ng2Component, inputs: ['itemId']}))
|
||||
.run(($rootScope: angular.IRootScopeService) => {
|
||||
$rootScope['items'] = [
|
||||
{id: 'a', subitems: [1, 2, 3]}, {id: 'b', subitems: [4, 5, 6]},
|
||||
{id: 'c', subitems: [7, 8, 9]}
|
||||
];
|
||||
});
|
||||
|
||||
const element = html(`
|
||||
<ng2 ng-repeat="item in items" [item-id]="item.id">
|
||||
<div ng-repeat="subitem in item.subitems">{{ subitem }}</div>
|
||||
</ng2>
|
||||
`);
|
||||
|
||||
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
|
||||
expect(multiTrim(document.body.textContent))
|
||||
.toBe('ng2-a( 123 )ng2-b( 456 )ng2-c( 789 )');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should instantiate ng1 in ng2 template and project content', async(() => {
|
||||
|
||||
@Component({
|
||||
selector: 'ng2',
|
||||
template: `{{ 'ng2(' }}<ng1>{{ transclude }}</ng1>{{ ')' }}`,
|
||||
})
|
||||
class Ng2Component {
|
||||
prop = 'ng2';
|
||||
transclude = 'ng2-transclude';
|
||||
}
|
||||
|
||||
@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', () => ({
|
||||
transclude: true,
|
||||
template: '{{ prop }}(<ng-transclude></ng-transclude>)'
|
||||
}))
|
||||
.directive('ng2', downgradeComponent({component: Ng2Component}))
|
||||
.run(($rootScope: angular.IRootScopeService) => {
|
||||
$rootScope['prop'] = 'ng1';
|
||||
$rootScope['transclude'] = 'ng1-transclude';
|
||||
});
|
||||
|
||||
const element = html('<div>{{ \'ng1(\' }}<ng2></ng2>{{ \')\' }}</div>');
|
||||
|
||||
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
|
||||
expect(document.body.textContent).toEqual('ng1(ng2(ng1(ng2-transclude)))');
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
@ -0,0 +1,371 @@
|
||||
/**
|
||||
* @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 * as angular from '@angular/upgrade/src/common/angular1';
|
||||
import {UpgradeModule, downgradeComponent} from '@angular/upgrade/static';
|
||||
|
||||
import {$apply, 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', []).value('$exceptionHandler', (err: any) => {
|
||||
throw err;
|
||||
}).run(($rootScope: angular.IScope) => {
|
||||
$rootScope['name'] = 'world';
|
||||
$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');
|
||||
assertChange('twoWayB', 'newB');
|
||||
break;
|
||||
case 2:
|
||||
assertChange('interpolate', 'Hello everyone');
|
||||
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 {{name}}"
|
||||
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;');
|
||||
|
||||
$apply(upgrade, 'name = "everyone"');
|
||||
expect(multiTrim(document.body.textContent))
|
||||
.toEqual(
|
||||
'ignore: -; ' +
|
||||
'literal: Text; interpolate: Hello everyone; ' +
|
||||
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' +
|
||||
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should bind to ng-model', async(() => {
|
||||
const ng1Module = angular.module('ng1', []).run(
|
||||
($rootScope: angular.IScope) => { $rootScope['modelA'] = 'A'; });
|
||||
|
||||
let ng2Instance: Ng2;
|
||||
@Component({selector: 'ng2', template: '<span>{{_value}}</span>'})
|
||||
class Ng2 {
|
||||
private _value: any = '';
|
||||
private _onChangeCallback: (_: any) => void = () => {};
|
||||
constructor() { ng2Instance = this; }
|
||||
writeValue(value: any) { this._value = value; }
|
||||
registerOnChange(fn: any) { this._onChangeCallback = fn; }
|
||||
doChange(newValue: string) {
|
||||
this._value = newValue;
|
||||
this._onChangeCallback(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
ng1Module.directive('ng2', downgradeComponent({component: Ng2}));
|
||||
|
||||
const element = html(`<div><ng2 ng-model="modelA"></ng2> | {{modelA}}</div>`);
|
||||
|
||||
@NgModule(
|
||||
{declarations: [Ng2], entryComponents: [Ng2], imports: [BrowserModule, UpgradeModule]})
|
||||
class Ng2Module {
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(Ng2Module).then((ref) => {
|
||||
const adapter = ref.injector.get(UpgradeModule) as UpgradeModule;
|
||||
adapter.bootstrap(element, [ng1Module.name]);
|
||||
const $rootScope = adapter.$injector.get('$rootScope');
|
||||
|
||||
expect(multiTrim(document.body.textContent)).toEqual('A | A');
|
||||
|
||||
$rootScope.modelA = 'B';
|
||||
$rootScope.$apply();
|
||||
expect(multiTrim(document.body.textContent)).toEqual('B | B');
|
||||
|
||||
ng2Instance.doChange('C');
|
||||
expect($rootScope.modelA).toBe('C');
|
||||
expect(multiTrim(document.body.textContent)).toEqual('C | C');
|
||||
});
|
||||
}));
|
||||
|
||||
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 downgraded components', async(() => {
|
||||
@Component({selector: '[itWorks]', template: 'It works'})
|
||||
class WorksComponent {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [WorksComponent],
|
||||
entryComponents: [WorksComponent],
|
||||
imports: [BrowserModule, UpgradeModule]
|
||||
})
|
||||
class Ng2Module {
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
||||
const ng1Module = angular.module('ng1', []).directive(
|
||||
'worksComponent', downgradeComponent({component: WorksComponent}));
|
||||
|
||||
const element = html('<works-component></works-component>');
|
||||
|
||||
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
|
||||
expect(multiTrim(document.body.textContent)).toBe('It works');
|
||||
});
|
||||
}));
|
||||
|
||||
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!');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should respect hierarchical dependency injection for ng2', async(() => {
|
||||
@Component({selector: 'parent', template: 'parent(<ng-content></ng-content>)'})
|
||||
class ParentComponent {
|
||||
}
|
||||
|
||||
@Component({selector: 'child', template: 'child'})
|
||||
class ChildComponent {
|
||||
constructor(parent: ParentComponent) {}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [ParentComponent, ChildComponent],
|
||||
entryComponents: [ParentComponent, ChildComponent],
|
||||
imports: [BrowserModule, UpgradeModule]
|
||||
})
|
||||
class Ng2Module {
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
||||
const ng1Module =
|
||||
angular.module('ng1', [])
|
||||
.directive('parent', downgradeComponent({component: ParentComponent}))
|
||||
.directive('child', downgradeComponent({component: ChildComponent}));
|
||||
|
||||
const element = html('<parent><child></child></parent>');
|
||||
|
||||
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
|
||||
expect(multiTrim(document.body.textContent)).toBe('parent(child)');
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
89
packages/upgrade/test/static/integration/examples_spec.ts
Normal file
89
packages/upgrade/test/static/integration/examples_spec.ts
Normal 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 * as angular from '@angular/upgrade/src/common/angular1';
|
||||
import {UpgradeComponent, UpgradeModule, downgradeComponent} from '@angular/upgrade/static';
|
||||
|
||||
import {bootstrap, html, multiTrim} from '../test_helpers';
|
||||
|
||||
export function main() {
|
||||
describe('examples', () => {
|
||||
|
||||
beforeEach(() => destroyPlatform());
|
||||
afterEach(() => destroyPlatform());
|
||||
|
||||
it('should have AngularJS loaded', () => expect(angular.version.major).toBe(1));
|
||||
|
||||
it('should verify UpgradeAdapter example', async(() => {
|
||||
|
||||
// This is wrapping (upgrading) an AngularJS component to be used in an Angular
|
||||
// component
|
||||
@Directive({selector: 'ng1'})
|
||||
class Ng1Component extends UpgradeComponent {
|
||||
@Input() title: string;
|
||||
|
||||
constructor(elementRef: ElementRef, injector: Injector) {
|
||||
super('ng1', elementRef, injector);
|
||||
}
|
||||
}
|
||||
|
||||
// This is an Angular 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 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 AngularJS pieces of the application
|
||||
const ng1Module =
|
||||
angular
|
||||
.module('myExample', [])
|
||||
// This is an AngularJS 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 component to be used in AngularJS
|
||||
.directive(
|
||||
'ng2',
|
||||
downgradeComponent({component: Ng2Component, inputs: ['nameProp: name']}));
|
||||
|
||||
// This is the (AngularJS) application bootstrap element
|
||||
// Notice that it is actually a downgraded Angular 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)');
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
103
packages/upgrade/test/static/integration/injection_spec.ts
Normal file
103
packages/upgrade/test/static/integration/injection_spec.ts
Normal file
@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @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 {InjectionToken, 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 * as angular from '@angular/upgrade/src/common/angular1';
|
||||
import {$INJECTOR, INJECTOR_KEY} from '@angular/upgrade/src/common/constants';
|
||||
import {UpgradeModule, downgradeInjectable} from '@angular/upgrade/static';
|
||||
|
||||
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 InjectionToken('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 InjectionToken('ng1-service');
|
||||
|
||||
// Sample ng1 NgModule for tests
|
||||
@NgModule({
|
||||
imports: [BrowserModule, UpgradeModule],
|
||||
providers: [
|
||||
// the following line is the "upgrade" of an AngularJS 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) => {
|
||||
const ng2Injector = upgrade.injector;
|
||||
expect(ng2Injector.get(Ng1Service)).toBe('ng1 service value');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should initialize the upgraded injector before application run blocks are executed',
|
||||
async(() => {
|
||||
let runBlockTriggered = false;
|
||||
|
||||
const ng1Module = angular.module('ng1Module', []).run([
|
||||
INJECTOR_KEY,
|
||||
function(injector: Injector) {
|
||||
runBlockTriggered = true;
|
||||
expect(injector.get($INJECTOR)).toBeDefined();
|
||||
}
|
||||
]);
|
||||
|
||||
@NgModule({imports: [BrowserModule, UpgradeModule]})
|
||||
class Ng2Module {
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
||||
bootstrap(platformBrowserDynamic(), Ng2Module, html('<div>'), ng1Module).then(() => {
|
||||
expect(runBlockTriggered).toBeTruthy();
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
77
packages/upgrade/test/static/integration/testability_spec.ts
Normal file
77
packages/upgrade/test/static/integration/testability_spec.ts
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @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 {NgZone} from '@angular/core/src/zone/ng_zone';
|
||||
import {fakeAsync, tick} from '@angular/core/testing';
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||
import * as angular from '@angular/upgrade/src/common/angular1';
|
||||
import {UpgradeModule} from '@angular/upgrade/static';
|
||||
|
||||
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;
|
||||
let stayedInTheZone: boolean;
|
||||
const ng1Module = angular.module('ng1', []).run(() => {
|
||||
applicationRunning = true;
|
||||
stayedInTheZone = NgZone.isInAngularZone();
|
||||
});
|
||||
|
||||
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);
|
||||
expect(stayedInTheZone).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);
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
3263
packages/upgrade/test/static/integration/upgrade_component_spec.ts
Normal file
3263
packages/upgrade/test/static/integration/upgrade_component_spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
35
packages/upgrade/test/static/test_helpers.ts
Normal file
35
packages/upgrade/test/static/test_helpers.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @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 * as angular from '@angular/upgrade/src/common/angular1';
|
||||
import {$ROOT_SCOPE} from '@angular/upgrade/src/common/constants';
|
||||
import {UpgradeModule} from '@angular/upgrade/static';
|
||||
|
||||
export * from '../common/test_helpers';
|
||||
|
||||
export function bootstrap(
|
||||
platform: PlatformRef, Ng2Module: Type<{}>, element: Element, ng1Module: angular.IModule) {
|
||||
// We bootstrap the Angular module first; then when it is ready (async)
|
||||
// We bootstrap the AngularJS module on the bootstrap element
|
||||
return platform.bootstrapModule(Ng2Module).then(ref => {
|
||||
const upgrade = ref.injector.get(UpgradeModule) as UpgradeModule;
|
||||
upgrade.bootstrap(element, [ng1Module.name]);
|
||||
return upgrade;
|
||||
});
|
||||
}
|
||||
|
||||
export function $apply(adapter: UpgradeModule, exp: angular.Ng1Expression) {
|
||||
const $rootScope = adapter.$injector.get($ROOT_SCOPE) as angular.IRootScopeService;
|
||||
$rootScope.$apply(exp);
|
||||
}
|
||||
|
||||
export function $digest(adapter: UpgradeModule) {
|
||||
const $rootScope = adapter.$injector.get($ROOT_SCOPE) as angular.IRootScopeService;
|
||||
$rootScope.$digest();
|
||||
}
|
36
packages/upgrade/tsconfig-build.json
Normal file
36
packages/upgrade/tsconfig-build.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"declaration": true,
|
||||
"stripInternal": true,
|
||||
"experimentalDecorators": true,
|
||||
"module": "es2015",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "../../../dist/packages-dist/upgrade",
|
||||
"paths": {
|
||||
"@angular/core": ["../../../dist/packages-dist/core"],
|
||||
"@angular/common": ["../../../dist/packages-dist/common"],
|
||||
"@angular/compiler": ["../../../dist/packages-dist/compiler"],
|
||||
"@angular/platform-browser": ["../../../dist/packages-dist/platform-browser"],
|
||||
"@angular/platform-browser-dynamic": ["../../../dist/packages-dist/platform-browser-dynamic"]
|
||||
},
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"target": "es2015",
|
||||
"skipLibCheck": true,
|
||||
"lib": [ "es2015", "dom" ],
|
||||
// don't auto-discover @types/node, it results in a ///<reference in the .d.ts output
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"public_api.ts",
|
||||
"../../../node_modules/zone.js/dist/zone.js.d.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"annotateForClosureCompiler": true,
|
||||
"strictMetadataEmit": true,
|
||||
"flatModuleOutFile": "index.js",
|
||||
"flatModuleId": "@angular/upgrade"
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user