refactor: move angular source to /packages rather than modules/@angular

This commit is contained in:
Jason Aden
2017-03-02 10:48:42 -08:00
parent 5ad5301a3e
commit 3e51a19983
1051 changed files with 18 additions and 18 deletions

View 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;

View 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}`;
}
}

View 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';

View 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];
}
}

View 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;
}
}

View 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;
}
}

View 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;
}

View 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));
}
}

View File

@ -0,0 +1,19 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* @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');

View 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;
}
}

View 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();
}
}

View 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';
}

View File

@ -0,0 +1,46 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as angular from '../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']}
];

View 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';
}

View 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); });
};
}
}
}