refactor: move angular source to /packages rather than modules/@angular
This commit is contained in:
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';
|
||||
}
|
Reference in New Issue
Block a user