fix(upgrade): bring the dynamic version closer to the static one

(#17971)

This commit changes the dynamic version of ngUpgrade to use `UpgradeHelper`,
thus bringing its behavior (wrt upgraded components) much closer to
`upgrade/static`. Fixes/features include:

- Fix template compilation: Now takes place in the correct DOM context, instead
  of in a detached node (thus has access to required ancestors etc).
- Fix support for the `$onInit()` lifecycle hook.
- Fix single-slot transclusion (including optional transclusion and fallback
  content).
- Add support for multi-slot transclusion (inclusing optional slots and fallback
  content).
- Add support for binding required controllers to the directive's controller
  (and make the `require` behavior more consistent with AngularJS).
- Add support for pre-/post-linking functions.

(This also ports the fixes from #16627 to the dynamic version.)

Fixes #11044
This commit is contained in:
Georgios Kalpakas 2017-05-29 20:55:41 +03:00 committed by Jason Aden
parent 0193be7c9b
commit 11db3bd85e
5 changed files with 828 additions and 292 deletions

View File

@ -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<string> {
if (directive.template !== undefined) {
return getOrCall<string>(directive.template);
} else if (directive.templateUrl) {
const $templateCache = $injector.get($TEMPLATE_CACHE) as angular.ITemplateCacheService;
const url = getOrCall<string>(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<string>(this.directive.template));
} else if (this.directive.templateUrl) {
const url = this.getOrCall<string>(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.`);
}
compileTemplate(template?: string): angular.ILinkFn {
if (template === undefined) {
template = UpgradeHelper.getTemplate(this.$injector, this.directive) as string;
}
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}`);
}
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<IControllerInstance>|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<T>(property: T|Function): T {
function getOrCall<T>(property: T | Function): T {
return isFunction(property) ? property() : property;
}
private notSupported(feature: string) {
throw new Error(
`Upgraded directive '${this.name}' contains unsupported feature: '${feature}'.`);
// 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';
}
function notSupported(name: string, feature: string) {
throw new Error(`Upgraded directive '${name}' contains unsupported feature: '${feature}'.`);
}

View File

@ -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: [

View File

@ -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,25 +30,24 @@ 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({
this.type =
Directive({selector: selector, inputs: this.inputsRename, outputs: this.outputsRename})
.Class({
constructor: [
new Inject($SCOPE), ElementRef,
function(scope: angular.IScope, elementRef: ElementRef) {
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(
self.linkFn !, scope, self.directive !, elementRef, self.$controller !, self.inputs,
self.outputs, self.propertyOutputs, self.checkProperties, self.propertyMap);
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 */ },
@ -72,25 +57,6 @@ export class UpgradeNg1ComponentAdapterBuilder {
});
}
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) {
@ -148,66 +114,22 @@ export class UpgradeNg1ComponentAdapterBuilder {
}
}
compileTemplate(
compile: angular.ICompileService, templateCache: angular.ITemplateCacheService,
httpBackend: angular.IHttpBackendService): Promise<angular.ILinkFn>|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<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)) {
$injector: angular.IInjectorService): Promise<string[]> {
const promises = Object.keys(exportedComponents).map(name => {
const exportedComponent = exportedComponents[name];
exportedComponent.directive = exportedComponent.extractDirective(injector);
exportedComponent.$controller = $controller;
exportedComponent.directive = UpgradeHelper.getDirective($injector, name);
exportedComponent.extractBindings();
const promise: Promise<angular.ILinkFn> =
exportedComponent.compileTemplate(compile, templateCache, httpBackend) !;
if (promise) promises.push(promise);
}
}
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<any>();
@ -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 = (<angular.IDirectivePrePost>link).pre;
if (link) {
// 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;
const linkController = this.resolveRequired(this.$element, this.directive.require !);
(<angular.IDirectiveLinkFn>this.directive.link)(
this.componentScope, this.$element, attrs, linkController, transcludeFn);
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 = <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

@ -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<T>(value: angular.SingleOrListOrMap<T>): value is {[key: string]: T} {
return value && !Array.isArray(value) && typeof value === 'object';
}

View File

@ -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: '<ng1></ng1>'})
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(`<ng2></ng2>`);
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: '<ng1-b></ng1-b>',
link: {pre: () => log.push('ng1A-pre')}
};
const ng1DirectiveB: angular.IDirective = {link: () => log.push('ng1B-post')};
// Define `Ng2Component`
@Component({selector: 'ng2', template: '<ng1-a></ng1-a>'})
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(`<ng2></ng2>`);
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: '<ng1-b></ng1-b>',
link: {post: () => log.push('ng1A-post')}
};
const ng1DirectiveB: angular.IDirective = {link: () => log.push('ng1B-post')};
// Define `Ng2Component`
@Component({selector: 'ng2', template: '<ng1-a></ng1-a>'})
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(`<ng2></ng2>`);
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: '<ng1-b></ng1-b>',
link: () => log.push('ng1A-post')
};
const ng1DirectiveB: angular.IDirective = {link: () => log.push('ng1B-post')};
// Define `Ng2Component`
@Component({selector: 'ng2', template: '<ng1-a></ng1-a>'})
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(`<ng2></ng2>`);
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: '<ng1></ng1>'})
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(`<ng2></ng2>`);
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(<div ng-transclude></div>)',
transclude: true
};
// Define `Ng2Component`
@Component({
selector: 'ng2A',
template: 'ng2A(<ng1>{{ value }} | <ng2B *ngIf="showB"></ng2B></ng1>)'
})
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(`<ng2-a></ng2-a>`);
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(<div ng-transclude>{{ $ctrl.value }}</div>)',
transclude: true,
controller: class {
value = 'from-ng1'; constructor() { ng1ControllerInstances.push(this); }
}
};
// Define `Ng2Component`
@Component({
selector: 'ng2',
template: `
ng2(
<ng1><div>{{ value }}</div></ng1> |
<!-- Interpolation-only content should still be detected as transcluded content. -->
<ng1>{{ value }}</ng1> |
<ng1></ng1>
)`
})
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(`<ng2></ng2>`);
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(<div ng-transclude="slotX"></div>) | y(<div ng-transclude="slotY"></div>))',
transclude: {slotX: 'contentX', slotY: 'contentY'}
};
// Define `Ng2Component`
@Component({
selector: 'ng2',
template: `
ng2(
<ng1>
<content-x>{{ x }}1</content-x>
<content-y>{{ y }}1</content-y>
<content-x>{{ x }}2</content-x>
<content-y>{{ y }}2</content-y>
</ng1>
)`
})
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(`<ng2></ng2>`);
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(<div ng-transclude="">fallback-{{ $ctrl.value }}</div>))',
transclude: {slotX: 'contentX', slotY: 'contentY'},
controller:
class {value = 'ng1'; constructor() { ng1ControllerInstances.push(this); }}
};
// Define `Ng2Component`
@Component({
selector: 'ng2',
template: `
ng2(
<ng1>
({{ x }})
<content-x>ignored x</content-x>
{{ x }}-<span>{{ y }}</span>
<content-y>ignored y</content-y>
<span>({{ y }})</span>
</ng1> |
<!--
Remove any whitespace, because in AngularJS versions prior to 1.6
even whitespace counts as transcluded content.
-->
<ng1><content-x>ignored x</content-x><content-y>ignored y</content-y></ng1> |
<!--
Interpolation-only content should still be detected as transcluded content.
-->
<ng1>{{ x }}<content-x>ignored x</content-x>{{ y + x }}<content-y>ignored y</content-y>{{ y }}</ng1>
)`
})
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(`<ng2></ng2>`);
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(<div ng-transclude="slotX">{{ $ctrl.x }}</div>) |
y(<div ng-transclude="slotY">{{ $ctrl.y }}</div>)
)`,
transclude: {slotX: '?contentX', slotY: '?contentY'},
controller: class {
x = 'ng1X'; y = 'ng1Y'; constructor() { ng1ControllerInstances.push(this); }
}
};
// Define `Ng2Component`
@Component({
selector: 'ng2',
template: `
ng2(
<ng1><content-x>{{ x }}</content-x></ng1> |
<ng1><content-y>{{ y }}</content-y></ng1>
)`
})
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(`<ng2></ng2>`);
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: '<ng1></ng1>'})
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(`<ng2></ng2>`);
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(<div ng-transclude="slotX"></div>) | default(<div ng-transclude=""></div>))',
transclude: {slotX: 'contentX'}
};
// Define `Ng2Component`
@Component({
selector: 'ng2',
template: `
ng2(
<ng1>
<content-x><div *ngIf="show">{{ x }}1</div></content-x>
<div *ngIf="!show">{{ y }}1</div>
<content-x><div *ngIf="!show">{{ x }}2</div></content-x>
<div *ngIf="show">{{ y }}2</div>
</ng1>
)`
})
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(`<ng2></ng2>`);
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', []);