diff --git a/packages/upgrade/src/common/upgrade_helper.ts b/packages/upgrade/src/common/upgrade_helper.ts index a58975e15c..0b9c6a8022 100644 --- a/packages/upgrade/src/common/upgrade_helper.ts +++ b/packages/upgrade/src/common/upgrade_helper.ts @@ -14,7 +14,7 @@ import {controllerKey, directiveNormalize, isFunction} from './util'; // Constants -export const REQUIRE_PREFIX_RE = /^(\^\^?)?(\?)?(\^\^?)?/; +const REQUIRE_PREFIX_RE = /^(\^\^?)?(\?)?(\^\^?)?/; // Interfaces export interface IBindingDestination { @@ -38,18 +38,66 @@ export class UpgradeHelper { private readonly $compile: angular.ICompileService; private readonly $controller: angular.IControllerService; - private readonly $templateCache: angular.ITemplateCacheService; - constructor(private injector: Injector, private name: string, elementRef: ElementRef) { + constructor( + private injector: Injector, private name: string, elementRef: ElementRef, + directive?: angular.IDirective) { this.$injector = injector.get($INJECTOR); this.$compile = this.$injector.get($COMPILE); this.$controller = this.$injector.get($CONTROLLER); - this.$templateCache = this.$injector.get($TEMPLATE_CACHE); this.element = elementRef.nativeElement; this.$element = angular.element(this.element); - this.directive = this.getDirective(); + this.directive = directive || UpgradeHelper.getDirective(this.$injector, name); + } + + static getDirective($injector: angular.IInjectorService, name: string): angular.IDirective { + const directives: angular.IDirective[] = $injector.get(name + 'Directive'); + if (directives.length > 1) { + throw new Error(`Only support single directive definition for: ${name}`); + } + + const directive = directives[0]; + + // AngularJS will transform `link: xyz` to `compile: () => xyz`. So we can only tell there was a + // user-defined `compile` if there is no `link`. In other cases, we will just ignore `compile`. + if (directive.compile && !directive.link) notSupported(name, 'compile'); + if (directive.replace) notSupported(name, 'replace'); + if (directive.terminal) notSupported(name, 'terminal'); + + return directive; + } + + static getTemplate( + $injector: angular.IInjectorService, directive: angular.IDirective, + fetchRemoteTemplate = false): string|Promise { + if (directive.template !== undefined) { + return getOrCall(directive.template); + } else if (directive.templateUrl) { + const $templateCache = $injector.get($TEMPLATE_CACHE) as angular.ITemplateCacheService; + const url = getOrCall(directive.templateUrl); + const template = $templateCache.get(url); + + if (template !== undefined) { + return template; + } else if (!fetchRemoteTemplate) { + throw new Error('loading directive templates asynchronously is not supported'); + } + + return new Promise((resolve, reject) => { + const $httpBackend = $injector.get($HTTP_BACKEND) as angular.IHttpBackendService; + $httpBackend('GET', url, null, (status: number, response: string) => { + if (status === 200) { + resolve($templateCache.put(url, response)); + } else { + reject(`GET component template from '${url}' returned '${status}: ${response}'`); + } + }); + }); + } else { + throw new Error(`Directive '${directive.name}' is not a component, it is missing template.`); + } } buildController(controllerType: angular.IController, $scope: angular.IScope) { @@ -63,34 +111,12 @@ export class UpgradeHelper { return controller; } - compileTemplate(): angular.ILinkFn { - if (this.directive.template !== undefined) { - return this.compileHtml(this.getOrCall(this.directive.template)); - } else if (this.directive.templateUrl) { - const url = this.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'); - } - } else { - throw new Error(`Directive '${this.name}' is not a component, it is missing template.`); - } - } - - getDirective(): angular.IDirective { - const directives: angular.IDirective[] = this.$injector.get(this.name + 'Directive'); - if (directives.length > 1) { - throw new Error(`Only support single directive definition for: ${this.name}`); + compileTemplate(template?: string): angular.ILinkFn { + if (template === undefined) { + template = UpgradeHelper.getTemplate(this.$injector, this.directive) as string; } - const directive = directives[0]; - if (directive.replace) this.notSupported('replace'); - if (directive.terminal) this.notSupported('terminal'); - if (directive.compile) this.notSupported('compile'); - - return directive; + return this.compileHtml(template); } prepareTransclusion(): angular.ILinkFn|undefined { @@ -169,7 +195,56 @@ export class UpgradeHelper { return attachChildrenFn; } - resolveRequire(require: angular.DirectiveRequireProperty): + resolveAndBindRequiredControllers(controllerInstance: IControllerInstance|null) { + const directiveRequire = this.getDirectiveRequire(); + const requiredControllers = this.resolveRequire(directiveRequire); + + if (controllerInstance && this.directive.bindToController && isMap(directiveRequire)) { + const requiredControllersMap = requiredControllers as{[key: string]: IControllerInstance}; + Object.keys(requiredControllersMap).forEach(key => { + controllerInstance[key] = requiredControllersMap[key]; + }); + } + + return requiredControllers; + } + + private compileHtml(html: string): angular.ILinkFn { + this.element.innerHTML = html; + return this.$compile(this.element.childNodes); + } + + private extractChildNodes(): Node[] { + const childNodes: Node[] = []; + let childNode: Node|null; + + while (childNode = this.element.firstChild) { + this.element.removeChild(childNode); + childNodes.push(childNode); + } + + return childNodes; + } + + private getDirectiveRequire(): angular.DirectiveRequireProperty { + const require = this.directive.require || (this.directive.controller && this.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 resolveRequire(require: angular.DirectiveRequireProperty, controllerInstance?: any): angular.SingleOrListOrMap|null { if (!require) { return null; @@ -203,30 +278,17 @@ export class UpgradeHelper { `Unrecognized 'require' syntax on upgraded directive '${this.name}': ${require}`); } } - - private compileHtml(html: string): angular.ILinkFn { - this.element.innerHTML = html; - return this.$compile(this.element.childNodes); - } - - private extractChildNodes(): Node[] { - const childNodes: Node[] = []; - let childNode: Node|null; - - while (childNode = this.element.firstChild) { - this.element.removeChild(childNode); - childNodes.push(childNode); - } - - return childNodes; - } - - private getOrCall(property: T|Function): T { - return isFunction(property) ? property() : property; - } - - private notSupported(feature: string) { - throw new Error( - `Upgraded directive '${this.name}' contains unsupported feature: '${feature}'.`); - } +} + +function getOrCall(property: T | Function): T { + return isFunction(property) ? property() : property; +} + +// NOTE: Only works for `typeof T !== 'object'`. +function isMap(value: angular.SingleOrListOrMap): value is {[key: string]: T} { + return value && !Array.isArray(value) && typeof value === 'object'; +} + +function notSupported(name: string, feature: string) { + throw new Error(`Upgraded directive '${name}' contains unsupported feature: '${feature}'.`); } diff --git a/packages/upgrade/src/dynamic/upgrade_adapter.ts b/packages/upgrade/src/dynamic/upgrade_adapter.ts index 346954cd8a..fbddc22f35 100644 --- a/packages/upgrade/src/dynamic/upgrade_adapter.ts +++ b/packages/upgrade/src/dynamic/upgrade_adapter.ts @@ -546,8 +546,8 @@ export class UpgradeAdapter { (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. + // At this point we have ng1 injector and we have prepared + // ng1 components to be upgraded, we now can bootstrap ng2. const DynamicNgUpgradeModule = NgModule({ providers: [ diff --git a/packages/upgrade/src/dynamic/upgrade_ng1_adapter.ts b/packages/upgrade/src/dynamic/upgrade_ng1_adapter.ts index 38980799d4..366fd82f0b 100644 --- a/packages/upgrade/src/dynamic/upgrade_ng1_adapter.ts +++ b/packages/upgrade/src/dynamic/upgrade_ng1_adapter.ts @@ -6,26 +6,12 @@ * 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 {Directive, DoCheck, ElementRef, EventEmitter, Inject, Injector, 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, strictEquals} 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'; +import {$SCOPE} from '../common/constants'; +import {IBindingDestination, IControllerInstance, UpgradeHelper} from '../common/upgrade_helper'; +import {isFunction, strictEquals} from '../common/util'; const CAMEL_CASE = /([A-Z])/g; @@ -44,51 +30,31 @@ export class UpgradeNg1ComponentAdapterBuilder { propertyOutputs: string[] = []; checkProperties: string[] = []; propertyMap: {[name: string]: string} = {}; - linkFn: angular.ILinkFn|null = null; directive: angular.IDirective|null = null; - $controller: angular.IControllerService|null = null; + template: string; constructor(public name: string) { - const selector = name.replace( - CAMEL_CASE, (all: any /** TODO #9100 */, next: string) => '-' + next.toLowerCase()); + const selector = + name.replace(CAMEL_CASE, (all: string, 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 ((link).post) this.notSupported('link.post'); - } - return directive; - } - - private notSupported(feature: string) { - throw new Error(`Upgraded directive '${this.name}' does not support '${feature}'.`); + this.type = + Directive({selector: selector, inputs: this.inputsRename, outputs: this.outputsRename}) + .Class({ + constructor: [ + new Inject($SCOPE), Injector, ElementRef, + function(scope: angular.IScope, injector: Injector, elementRef: ElementRef) { + const helper = new UpgradeHelper(injector, name, elementRef, this.directive); + return new UpgradeNg1ComponentAdapter( + helper, scope, self.template, 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 */ }, + }); } extractBindings() { @@ -148,66 +114,22 @@ export class UpgradeNg1ComponentAdapterBuilder { } } - compileTemplate( - compile: angular.ICompileService, templateCache: angular.ITemplateCacheService, - httpBackend: angular.IHttpBackendService): Promise|null { - if (this.directive !.template !== undefined) { - this.linkFn = compileHtml( - isFunction(this.directive !.template) ? (this.directive !.template as Function)() : - this.directive !.template); - } else if (this.directive !.templateUrl) { - const url = isFunction(this.directive !.templateUrl) ? - (this.directive !.templateUrl as Function)() : - 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 { - const promises: Promise[] = []; - 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 ((exportedComponents).hasOwnProperty(name)) { - const exportedComponent = exportedComponents[name]; - exportedComponent.directive = exportedComponent.extractDirective(injector); - exportedComponent.$controller = $controller; - exportedComponent.extractBindings(); - const promise: Promise = - exportedComponent.compileTemplate(compile, templateCache, httpBackend) !; - if (promise) promises.push(promise); - } - } + $injector: angular.IInjectorService): Promise { + const promises = Object.keys(exportedComponents).map(name => { + const exportedComponent = exportedComponents[name]; + exportedComponent.directive = UpgradeHelper.getDirective($injector, name); + exportedComponent.extractBindings(); + + return Promise + .resolve(UpgradeHelper.getTemplate($injector, exportedComponent.directive, true)) + .then(template => exportedComponent.template = template); + }); + return Promise.all(promises); } } @@ -216,28 +138,31 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck { private controllerInstance: IControllerInstance|null = null; destinationObj: IBindingDestination|null = null; checkLastValues: any[] = []; - componentScope: angular.IScope; + private directive: angular.IDirective; element: Element; $element: any = null; + componentScope: angular.IScope; constructor( - private linkFn: angular.ILinkFn, scope: angular.IScope, private directive: angular.IDirective, - elementRef: ElementRef, private $controller: angular.IControllerService, + private helper: UpgradeHelper, scope: angular.IScope, private template: string, 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.directive = helper.directive; + this.element = helper.element; + this.$element = helper.$element; + this.componentScope = scope.$new(!!this.directive.scope); + + const controllerType = this.directive.controller; + + if (this.directive.bindToController && controllerType) { + this.controllerInstance = this.helper.buildController(controllerType, this.componentScope); 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; + (this as any)[inputs[i]] = null; } for (let j = 0; j < outputs.length; j++) { const emitter = (this as any)[outputs[j]] = new EventEmitter(); @@ -250,39 +175,43 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck { } ngOnInit() { - if (!this.directive.bindToController && this.directive.controller) { - this.controllerInstance = this.buildController(this.directive.controller); + // Collect contents, insert and compile template + const attachChildNodes: angular.ILinkFn|undefined = this.helper.prepareTransclusion(); + const linkFn = this.helper.compileTemplate(this.template); + + // Instantiate controller (if not already done so) + const controllerType = this.directive.controller; + const bindToController = this.directive.bindToController; + if (controllerType && !bindToController) { + this.controllerInstance = this.helper.buildController(controllerType, this.componentScope); } + // Require other controllers + const requiredControllers = + this.helper.resolveAndBindRequiredControllers(this.controllerInstance); + + // Hook: $onInit if (this.controllerInstance && isFunction(this.controllerInstance.$onInit)) { this.controllerInstance.$onInit(); } - let link = this.directive.link; - if (typeof link == 'object') link = (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 !); - (this.directive.link)( - this.componentScope, this.$element, attrs, linkController, transcludeFn); + // 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 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); } - }); + 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(); } @@ -329,56 +258,4 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck { 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 = 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'; } diff --git a/packages/upgrade/src/static/upgrade_component.ts b/packages/upgrade/src/static/upgrade_component.ts index d4702aba8f..06908f7595 100644 --- a/packages/upgrade/src/static/upgrade_component.ts +++ b/packages/upgrade/src/static/upgrade_component.ts @@ -9,7 +9,7 @@ import {DoCheck, ElementRef, EventEmitter, Injector, OnChanges, OnDestroy, OnInit, SimpleChanges, ɵlooseIdentical as looseIdentical} from '@angular/core'; import * as angular from '../common/angular1'; import {$SCOPE} from '../common/constants'; -import {IBindingDestination, IControllerInstance, REQUIRE_PREFIX_RE, UpgradeHelper} from '../common/upgrade_helper'; +import {IBindingDestination, IControllerInstance, UpgradeHelper} from '../common/upgrade_helper'; import {isFunction} from '../common/util'; const NOT_SUPPORTED: any = 'NOT_SUPPORTED'; @@ -144,15 +144,8 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy { this.bindOutputs(); // Require other controllers - const directiveRequire = this.getDirectiveRequire(this.directive); - const requiredControllers = this.helper.resolveRequire(directiveRequire); - - if (this.directive.bindToController && isMap(directiveRequire)) { - const requiredControllersMap = requiredControllers as{[key: string]: IControllerInstance}; - Object.keys(requiredControllersMap).forEach(key => { - this.controllerInstance[key] = requiredControllersMap[key]; - }); - } + const requiredControllers = + this.helper.resolveAndBindRequiredControllers(this.controllerInstance); // Hook: $onChanges if (this.pendingChanges) { @@ -232,24 +225,6 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy { this.$componentScope.$destroy(); } - 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) { @@ -323,8 +298,3 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy { } } } - -// NOTE: Only works for `typeof T !== 'object'`. -function isMap(value: angular.SingleOrListOrMap): value is {[key: string]: T} { - return value && !Array.isArray(value) && typeof value === 'object'; -} diff --git a/packages/upgrade/test/dynamic/upgrade_spec.ts b/packages/upgrade/test/dynamic/upgrade_spec.ts index c9cc675375..86837839c0 100644 --- a/packages/upgrade/test/dynamic/upgrade_spec.ts +++ b/packages/upgrade/test/dynamic/upgrade_spec.ts @@ -1974,6 +1974,633 @@ export function main() { })); }); + describe('linking', () => { + it('should run the pre-linking after instantiating the controller', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const log: string[] = []; + + // Define `ng1Directive` + const ng1Directive: angular.IDirective = { + template: '', + link: {pre: () => log.push('ng1-pre')}, + controller: class {constructor() { log.push('ng1-ctrl'); }} + }; + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ''}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1', []) + .directive('ng1', () => ng1Directive) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule], + declarations: [adapter.upgradeNg1Component('ng1'), Ng2Component] + }) + class Ng2Module { + } + + // Bootstrap + const element = html(``); + + adapter.bootstrap(element, ['ng1']).ready(() => { + expect(log).toEqual(['ng1-ctrl', 'ng1-pre']); + }); + })); + + it('should run the pre-linking function before linking', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const log: string[] = []; + + // Define `ng1Directive` + const ng1DirectiveA: angular.IDirective = { + template: '', + link: {pre: () => log.push('ng1A-pre')} + }; + + const ng1DirectiveB: angular.IDirective = {link: () => log.push('ng1B-post')}; + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ''}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1', []) + .directive('ng1A', () => ng1DirectiveA) + .directive('ng1B', () => ng1DirectiveB) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule], + declarations: [adapter.upgradeNg1Component('ng1A'), Ng2Component], + schemas: [NO_ERRORS_SCHEMA] + }) + class Ng2Module { + } + + // Bootstrap + const element = html(``); + + adapter.bootstrap(element, ['ng1']).ready(() => { + expect(log).toEqual(['ng1A-pre', 'ng1B-post']); + }); + })); + + it('should run the post-linking function after linking (link: object)', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const log: string[] = []; + + // Define `ng1Directive` + const ng1DirectiveA: angular.IDirective = { + template: '', + link: {post: () => log.push('ng1A-post')} + }; + + const ng1DirectiveB: angular.IDirective = {link: () => log.push('ng1B-post')}; + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ''}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1', []) + .directive('ng1A', () => ng1DirectiveA) + .directive('ng1B', () => ng1DirectiveB) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule], + declarations: [adapter.upgradeNg1Component('ng1A'), Ng2Component], + schemas: [NO_ERRORS_SCHEMA] + }) + class Ng2Module { + } + + // Bootstrap + const element = html(``); + + adapter.bootstrap(element, ['ng1']).ready(() => { + expect(log).toEqual(['ng1B-post', 'ng1A-post']); + }); + })); + + it('should run the post-linking function after linking (link: function)', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const log: string[] = []; + + // Define `ng1Directive` + const ng1DirectiveA: angular.IDirective = { + template: '', + link: () => log.push('ng1A-post') + }; + + const ng1DirectiveB: angular.IDirective = {link: () => log.push('ng1B-post')}; + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ''}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1', []) + .directive('ng1A', () => ng1DirectiveA) + .directive('ng1B', () => ng1DirectiveB) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule], + declarations: [adapter.upgradeNg1Component('ng1A'), Ng2Component], + schemas: [NO_ERRORS_SCHEMA] + }) + class Ng2Module { + } + + // Bootstrap + const element = html(``); + + adapter.bootstrap(element, ['ng1']).ready(() => { + expect(log).toEqual(['ng1B-post', 'ng1A-post']); + }); + })); + + it('should run the post-linking function before `$postLink`', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const log: string[] = []; + + // Define `ng1Directive` + const ng1Directive: angular.IDirective = { + template: '', + link: () => log.push('ng1-post'), + controller: class {$postLink() { log.push('ng1-$post'); }} + }; + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ''}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1', []) + .directive('ng1', () => ng1Directive) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule], + declarations: [adapter.upgradeNg1Component('ng1'), Ng2Component] + }) + class Ng2Module { + } + + // Bootstrap + const element = html(``); + + adapter.bootstrap(element, ['ng1']).ready(() => { + expect(log).toEqual(['ng1-post', 'ng1-$post']); + }); + })); + }); + + describe('transclusion', () => { + it('should support single-slot transclusion', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + let ng2ComponentAInstance: Ng2ComponentA; + let ng2ComponentBInstance: Ng2ComponentB; + + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: 'ng1(
)', + transclude: true + }; + + // Define `Ng2Component` + @Component({ + selector: 'ng2A', + template: 'ng2A({{ value }} | )' + }) + class Ng2ComponentA { + value = 'foo'; + showB = false; + constructor() { ng2ComponentAInstance = this; } + } + + @Component({selector: 'ng2B', template: 'ng2B({{ value }})'}) + class Ng2ComponentB { + value = 'bar'; + constructor() { ng2ComponentBInstance = this; } + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2A', adapter.downgradeNg2Component(Ng2ComponentA)); + + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule], + declarations: [adapter.upgradeNg1Component('ng1'), Ng2ComponentA, Ng2ComponentB] + }) + class Ng2Module { + } + + // Bootstrap + const element = html(``); + + adapter.bootstrap(element, ['ng1Module']).ready((ref) => { + expect(multiTrim(element.textContent)).toBe('ng2A(ng1(foo | ))'); + + ng2ComponentAInstance.value = 'baz'; + ng2ComponentAInstance.showB = true; + $digest(ref); + + expect(multiTrim(element.textContent)).toBe('ng2A(ng1(baz | ng2B(bar)))'); + + ng2ComponentBInstance.value = 'qux'; + $digest(ref); + + expect(multiTrim(element.textContent)).toBe('ng2A(ng1(baz | ng2B(qux)))'); + }); + })); + + it('should support single-slot transclusion with fallback content', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + let ng1ControllerInstances: any[] = []; + let ng2ComponentInstance: Ng2Component; + + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: 'ng1(
{{ $ctrl.value }}
)', + transclude: true, + controller: class { + value = 'from-ng1'; constructor() { ng1ControllerInstances.push(this); } + } + }; + + // Define `Ng2Component` + @Component({ + selector: 'ng2', + template: ` + ng2( +
{{ value }}
| + + + {{ value }} | + + + )` + }) + class Ng2Component { + value = 'from-ng2'; + constructor() { ng2ComponentInstance = this; } + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule], + declarations: [adapter.upgradeNg1Component('ng1'), Ng2Component] + }) + class Ng2Module { + } + + // Bootstrap + const element = html(``); + + adapter.bootstrap(element, ['ng1Module']).ready(ref => { + expect(multiTrim(element.textContent, true)) + .toBe('ng2(ng1(from-ng2)|ng1(from-ng2)|ng1(from-ng1))'); + + ng1ControllerInstances.forEach(ctrl => ctrl.value = 'ng1-foo'); + ng2ComponentInstance.value = 'ng2-bar'; + $digest(ref); + + expect(multiTrim(element.textContent, true)) + .toBe('ng2(ng1(ng2-bar)|ng1(ng2-bar)|ng1(ng1-foo))'); + }); + })); + + it('should support multi-slot transclusion', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + let ng2ComponentInstance: Ng2Component; + + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: + 'ng1(x(
) | y(
))', + transclude: {slotX: 'contentX', slotY: 'contentY'} + }; + + // Define `Ng2Component` + @Component({ + selector: 'ng2', + template: ` + ng2( + + {{ x }}1 + {{ y }}1 + {{ x }}2 + {{ y }}2 + + )` + }) + class Ng2Component { + x = 'foo'; + y = 'bar'; + constructor() { ng2ComponentInstance = this; } + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule], + declarations: [adapter.upgradeNg1Component('ng1'), Ng2Component], + schemas: [NO_ERRORS_SCHEMA] + }) + class Ng2Module { + } + + // Bootstrap + const element = html(``); + + adapter.bootstrap(element, ['ng1Module']).ready(ref => { + expect(multiTrim(element.textContent, true)) + .toBe('ng2(ng1(x(foo1foo2)|y(bar1bar2)))'); + + ng2ComponentInstance.x = 'baz'; + ng2ComponentInstance.y = 'qux'; + $digest(ref); + + expect(multiTrim(element.textContent, true)) + .toBe('ng2(ng1(x(baz1baz2)|y(qux1qux2)))'); + }); + })); + + it('should support default slot (with fallback content)', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + let ng1ControllerInstances: any[] = []; + let ng2ComponentInstance: Ng2Component; + + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: 'ng1(default(
fallback-{{ $ctrl.value }}
))', + transclude: {slotX: 'contentX', slotY: 'contentY'}, + controller: + class {value = 'ng1'; constructor() { ng1ControllerInstances.push(this); }} + }; + + // Define `Ng2Component` + @Component({ + selector: 'ng2', + template: ` + ng2( + + ({{ x }}) + ignored x + {{ x }}-{{ y }} + ignored y + ({{ y }}) + | + + + ignored xignored y | + + + {{ x }}ignored x{{ y + x }}ignored y{{ y }} + )` + }) + class Ng2Component { + x = 'foo'; + y = 'bar'; + constructor() { ng2ComponentInstance = this; } + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule], + declarations: [adapter.upgradeNg1Component('ng1'), Ng2Component], + schemas: [NO_ERRORS_SCHEMA] + }) + class Ng2Module { + } + + // Bootstrap + const element = html(``); + + adapter.bootstrap(element, ['ng1Module']).ready(ref => { + expect(multiTrim(element.textContent, true)) + .toBe( + 'ng2(ng1(default((foo)foo-bar(bar)))|ng1(default(fallback-ng1))|ng1(default(foobarfoobar)))'); + + ng1ControllerInstances.forEach(ctrl => ctrl.value = 'ng1-plus'); + ng2ComponentInstance.x = 'baz'; + ng2ComponentInstance.y = 'qux'; + $digest(ref); + + expect(multiTrim(element.textContent, true)) + .toBe( + 'ng2(ng1(default((baz)baz-qux(qux)))|ng1(default(fallback-ng1-plus))|ng1(default(bazquxbazqux)))'); + }); + })); + + it('should support optional transclusion slots (with fallback content)', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + let ng1ControllerInstances: any[] = []; + let ng2ComponentInstance: Ng2Component; + + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: ` + ng1( + x(
{{ $ctrl.x }}
) | + y(
{{ $ctrl.y }}
) + )`, + transclude: {slotX: '?contentX', slotY: '?contentY'}, + controller: class { + x = 'ng1X'; y = 'ng1Y'; constructor() { ng1ControllerInstances.push(this); } + } + }; + + // Define `Ng2Component` + @Component({ + selector: 'ng2', + template: ` + ng2( + {{ x }} | + {{ y }} + )` + }) + class Ng2Component { + x = 'ng2X'; + y = 'ng2Y'; + constructor() { ng2ComponentInstance = this; } + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule], + declarations: [adapter.upgradeNg1Component('ng1'), Ng2Component], + schemas: [NO_ERRORS_SCHEMA] + }) + class Ng2Module { + } + + // Bootstrap + const element = html(``); + + adapter.bootstrap(element, ['ng1Module']).ready(ref => { + expect(multiTrim(element.textContent, true)) + .toBe('ng2(ng1(x(ng2X)|y(ng1Y))|ng1(x(ng1X)|y(ng2Y)))'); + + ng1ControllerInstances.forEach(ctrl => { + ctrl.x = 'ng1X-foo'; + ctrl.y = 'ng1Y-bar'; + }); + ng2ComponentInstance.x = 'ng2X-baz'; + ng2ComponentInstance.y = 'ng2Y-qux'; + $digest(ref); + + expect(multiTrim(element.textContent, true)) + .toBe('ng2(ng1(x(ng2X-baz)|y(ng1Y-bar))|ng1(x(ng1X-foo)|y(ng2Y-qux)))'); + }); + })); + + it('should throw if a non-optional slot is not filled', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + let errorMessage: string; + + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: '', + transclude: {slotX: '?contentX', slotY: 'contentY'} + }; + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ''}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = + angular.module('ng1Module', []) + .value('$exceptionHandler', (error: Error) => errorMessage = error.message) + .component('ng1', ng1Component) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule], + declarations: [adapter.upgradeNg1Component('ng1'), Ng2Component] + }) + class Ng2Module { + } + + // Bootstrap + const element = html(``); + + adapter.bootstrap(element, ['ng1Module']).ready(ref => { + expect(errorMessage) + .toContain('Required transclusion slot \'slotY\' on directive: ng1'); + }); + })); + + it('should support structural directives in transcluded content', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + let ng2ComponentInstance: Ng2Component; + + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: + 'ng1(x(
) | default(
))', + transclude: {slotX: 'contentX'} + }; + + // Define `Ng2Component` + @Component({ + selector: 'ng2', + template: ` + ng2( + +
{{ x }}1
+
{{ y }}1
+
{{ x }}2
+
{{ y }}2
+
+ )` + }) + class Ng2Component { + x = 'foo'; + y = 'bar'; + show = true; + constructor() { ng2ComponentInstance = this; } + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule], + declarations: [adapter.upgradeNg1Component('ng1'), Ng2Component], + schemas: [NO_ERRORS_SCHEMA] + }) + class Ng2Module { + } + + // Bootstrap + const element = html(``); + + adapter.bootstrap(element, ['ng1Module']).ready(ref => { + expect(multiTrim(element.textContent, true)).toBe('ng2(ng1(x(foo1)|default(bar2)))'); + + ng2ComponentInstance.x = 'baz'; + ng2ComponentInstance.y = 'qux'; + ng2ComponentInstance.show = false; + $digest(ref); + + expect(multiTrim(element.textContent, true)).toBe('ng2(ng1(x(baz2)|default(qux1)))'); + + ng2ComponentInstance.show = true; + $digest(ref); + + expect(multiTrim(element.textContent, true)).toBe('ng2(ng1(x(baz1)|default(qux2)))'); + }); + })); + }); + it('should bind input properties (<) of components', async(() => { const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const ng1Module = angular.module('ng1', []);