fix(upgrade): correctly project content on downgraded components with structural directives (#14274)

Previously, downgraded component adapters were compiling and projecting the
contents of the template element instead of the link-element. This didn't make
any difference is most cases (as the elements are the same), but broke with
structural directives (e.g. `ngRepeat`), which compile the orginal (template)
element once and then create and link clones of it.

This commit fixes it by always compiling and projecting the contents of the
correct (link) element.

Fixes #14260
This commit is contained in:
George Kalpakas
2017-02-09 20:07:11 +02:00
committed by Miško Hevery
parent e90661aaee
commit 74cb575219
3 changed files with 117 additions and 51 deletions
+37 -45
View File
@@ -656,67 +656,59 @@ function ng1ComponentDirective(info: ComponentInfo, idPrefix: string): Function
restrict: 'E',
terminal: true,
require: REQUIRE_INJECTOR,
compile: (templateElement: angular.IAugmentedJQuery, templateAttributes: angular.IAttributes,
transclude: angular.ITranscludeFunction) => {
link: (scope: angular.IScope, element: angular.IAugmentedJQuery, attrs: angular.IAttributes,
parentInjector: Injector | ParentInjectorPromise): void => {
// We might have compile the contents lazily, because this might have been triggered by the
// UpgradeNg1ComponentAdapterBuilder, when the ng2 templates have not been compiled yet
return {
post: (scope: angular.IScope, element: angular.IAugmentedJQuery,
attrs: angular.IAttributes, parentInjector: Injector | ParentInjectorPromise,
transclude: angular.ITranscludeFunction): void => {
let id = idPrefix + (idCount++);
(<any>element[0]).id = id;
let injectorPromise = new ParentInjectorPromise(element);
let id = idPrefix + (idCount++);
(<any>element[0]).id = id;
const ng2Compiler = ng1Injector.get(NG2_COMPILER) as Compiler;
const ngContentSelectors = ng2Compiler.getNgContentSelectors(info.type);
const linkFns = compileProjectedNodes(templateElement, ngContentSelectors);
let injectorPromise = new ParentInjectorPromise(element);
const componentFactory: ComponentFactory<any> = componentFactoryRefMap[info.selector];
if (!componentFactory)
throw new Error('Expecting ComponentFactory for: ' + info.selector);
const ng2Compiler = ng1Injector.get(NG2_COMPILER) as Compiler;
const ngContentSelectors = ng2Compiler.getNgContentSelectors(info.type);
const linkFns = compileProjectedNodes(element, ngContentSelectors);
element.empty();
let projectableNodes = linkFns.map(link => {
let projectedClone: Node[];
link(scope, (clone: Node[]) => {
projectedClone = clone;
element.append(clone);
});
return projectedClone;
});
const componentFactory: ComponentFactory<any> = componentFactoryRefMap[info.selector];
if (!componentFactory) throw new Error('Expecting ComponentFactory for: ' + info.selector);
parentInjector = parentInjector || ng1Injector.get(NG2_INJECTOR);
element.empty();
let projectableNodes = linkFns.map(link => {
let projectedClone: Node[];
link(scope, (clone: Node[]) => {
projectedClone = clone;
element.append(clone);
});
return projectedClone;
});
if (parentInjector instanceof ParentInjectorPromise) {
parentInjector.then((resolvedInjector: Injector) => downgrade(resolvedInjector));
} else {
downgrade(parentInjector);
}
parentInjector = parentInjector || ng1Injector.get(NG2_INJECTOR);
function downgrade(injector: Injector) {
const facade = new DowngradeNg2ComponentAdapter(
info, element, attrs, scope, injector, parse, componentFactory);
facade.setupInputs();
facade.bootstrapNg2(projectableNodes);
facade.setupOutputs();
facade.registerCleanup();
injectorPromise.resolve(facade.componentRef.injector);
}
}
};
if (parentInjector instanceof ParentInjectorPromise) {
parentInjector.then((resolvedInjector: Injector) => downgrade(resolvedInjector));
} else {
downgrade(parentInjector);
}
function downgrade(injector: Injector) {
const facade = new DowngradeNg2ComponentAdapter(
info, element, attrs, scope, injector, parse, componentFactory);
facade.setupInputs();
facade.bootstrapNg2(projectableNodes);
facade.setupOutputs();
facade.registerCleanup();
injectorPromise.resolve(facade.componentRef.injector);
}
}
};
function compileProjectedNodes(
templateElement: angular.IAugmentedJQuery,
ngContentSelectors: string[]): angular.ILinkFn[] {
element: angular.IAugmentedJQuery, ngContentSelectors: string[]): angular.ILinkFn[] {
if (!ngContentSelectors)
throw new Error('Expecting ngContentSelectors for: ' + info.selector);
// We have to sort the projected content before we compile it, hence the terminal: true
let projectableTemplateNodes =
sortProjectableNodes(ngContentSelectors, templateElement.contents());
let projectableTemplateNodes = sortProjectableNodes(ngContentSelectors, element.contents());
return projectableTemplateNodes.map(nodes => ng1Compile(nodes));
}
}
@@ -6,14 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Directive, ElementRef, Injector, NgModule, destroyPlatform} from '@angular/core';
import {Component, Directive, ElementRef, Injector, Input, NgModule, destroyPlatform} from '@angular/core';
import {async} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import * as angular from '@angular/upgrade/src/angular_js';
import {UpgradeComponent, UpgradeModule, downgradeComponent} from '@angular/upgrade/static';
import {bootstrap, html} from '../test_helpers';
import {bootstrap, html, multiTrim} from '../test_helpers';
export function main() {
describe('content projection', () => {
@@ -52,6 +52,44 @@ export function main() {
});
}));
it('should correctly project structural directives', async(() => {
@Component({selector: 'ng2', template: 'ng2-{{ itemId }}(<ng-content></ng-content>)'})
class Ng2Component {
@Input() itemId: string;
}
@NgModule({
imports: [BrowserModule, UpgradeModule],
declarations: [Ng2Component],
entryComponents: [Ng2Component]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module =
angular.module('ng1', [])
.directive(
'ng2', downgradeComponent({component: Ng2Component, inputs: ['itemId']}))
.run(($rootScope: angular.IRootScopeService) => {
$rootScope['items'] = [
{id: 'a', subitems: [1, 2, 3]}, {id: 'b', subitems: [4, 5, 6]},
{id: 'c', subitems: [7, 8, 9]}
];
});
const element = html(`
<ng2 ng-repeat="item in items" [item-id]="item.id">
<div ng-repeat="subitem in item.subitems">{{ subitem }}</div>
</ng2>
`);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
expect(multiTrim(document.body.textContent))
.toBe('ng2-a( 123 )ng2-b( 456 )ng2-c( 789 )');
});
}));
it('should instantiate ng1 in ng2 template and project content', async(() => {
@Component({
+40 -4
View File
@@ -491,6 +491,39 @@ export function main() {
});
}));
it('should correctly project structural directives', async(() => {
@Component({selector: 'ng2', template: 'ng2-{{ itemId }}(<ng-content></ng-content>)'})
class Ng2Component {
@Input() itemId: string;
}
@NgModule({imports: [BrowserModule], declarations: [Ng2Component]})
class Ng2Module {
}
const adapter: UpgradeAdapter = new UpgradeAdapter(Ng2Module);
const ng1Module = angular.module('ng1', [])
.directive('ng2', adapter.downgradeNg2Component(Ng2Component))
.run(($rootScope: angular.IRootScopeService) => {
$rootScope['items'] = [
{id: 'a', subitems: [1, 2, 3]}, {id: 'b', subitems: [4, 5, 6]},
{id: 'c', subitems: [7, 8, 9]}
];
});
const element = html(`
<ng2 ng-repeat="item in items" [item-id]="item.id">
<div ng-repeat="subitem in item.subitems">{{ subitem }}</div>
</ng2>
`);
adapter.bootstrap(element, [ng1Module.name]).ready(ref => {
expect(multiTrim(document.body.textContent))
.toBe('ng2-a( 123 )ng2-b( 456 )ng2-c( 789 )');
ref.dispose();
});
}));
it('should allow attribute selectors for components in ng2', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => MyNg2Module));
const ng1Module = angular.module('myExample', []);
@@ -1901,14 +1934,17 @@ function multiTrim(text: string): string {
}
function html(html: string): Element {
// Don't return `body` itself, because using it as a `$rootElement` for ng1
// will attach `$injector` to it and that will affect subsequent tests.
const body = document.body;
body.innerHTML = html;
body.innerHTML = `<div>${html.trim()}</div>`;
const div = document.body.firstChild as Element;
if (body.childNodes.length == 1 && body.firstChild instanceof HTMLElement) {
return <Element>body.firstChild;
if (div.childNodes.length === 1 && div.firstChild instanceof HTMLElement) {
return div.firstChild;
}
return body;
return div;
}
function nodes(html: string) {