fix(upgrade): fix downgrade content projection and injector inheritance

- Full support for content projection in downgraded Angular 2
  components. In particular, this enables multi-slot projection and
  other features on <ng-content>.
- Correctly wire up hierarchical injectors for downgraded Angular 2
  components: downgraded components inherit the injector of the first
  other downgraded Angular 2 component they find up the DOM tree.

Closes #6629, #7727, #8729, #9643, #9649, #12675
This commit is contained in:
Eudes Petonnet-Vincent
2016-12-14 13:02:27 -08:00
committed by Victor Berchet
parent d6e5e9283c
commit d91a86aac6
10 changed files with 470 additions and 184 deletions
@@ -70,6 +70,14 @@ export class JitCompiler implements Compiler {
return this._compileModuleAndAllComponents(moduleType, false).asyncResult;
}
getNgContentSelectors(component: Type<any>): string[] {
const template = this._compiledTemplateCache.get(component);
if (!template) {
throw new Error(`The component ${stringify(component)} is not yet compiled!`);
}
return template.compMeta.template.ngContentSelectors;
}
private _compileModuleAndComponents<T>(moduleType: Type<T>, isSync: boolean):
SyncAsyncResult<NgModuleFactory<T>> {
const loadingPromise = this._loadModules(moduleType, isSync);
@@ -408,6 +416,11 @@ class ModuleBoundCompiler implements Compiler {
return this._delegate.compileModuleAndAllComponentsAsync(moduleType);
}
getNgContentSelectors(component: Type<any>): string[] {
return this._delegate.getNgContentSelectors(component);
}
/**
* Clears all caches
*/
@@ -70,6 +70,10 @@ export class TestingCompilerImpl implements TestingCompiler {
return this._compiler.compileModuleAndAllComponentsAsync(moduleType);
}
getNgContentSelectors(component: Type<any>): string[] {
return this._compiler.getNgContentSelectors(component);
}
overrideModule(ngModule: Type<any>, override: MetadataOverride<NgModule>): void {
const oldMetadata = this._moduleResolver.resolve(ngModule, false);
this._moduleResolver.setNgModule(
@@ -82,6 +82,14 @@ export class Compiler {
throw _throwError();
}
/**
* Exposes the CSS-style selectors that have been used in `ngContent` directives within
* the template of the given component.
* This is used by the `upgrade` library to compile the appropriate transclude content
* in the Angular 1 wrapper component.
*/
getNgContentSelectors(component: Type<any>): string[] { throw _throwError(); }
/**
* Clears all caches.
*/
+1 -1
View File
@@ -27,7 +27,7 @@ export interface IModule {
run(a: IInjectable): IModule;
}
export interface ICompileService {
(element: Element|NodeList|string, transclude?: Function): ILinkFn;
(element: Element|NodeList|Node[]|string, transclude?: Function): ILinkFn;
}
export interface ILinkFn {
(scope: IScope, cloneAttachFn?: ICloneAttachFunction, options?: ILinkFnOptions): IAugmentedJQuery;
+1 -1
View File
@@ -21,4 +21,4 @@ export const NG1_INJECTOR = '$injector';
export const NG1_PARSE = '$parse';
export const NG1_TEMPLATE_CACHE = '$templateCache';
export const NG1_TESTABILITY = '$$testability';
export const REQUIRE_INJECTOR = '?^' + NG2_INJECTOR;
export const REQUIRE_INJECTOR = '?^^' + NG2_INJECTOR;
@@ -23,26 +23,21 @@ export class DowngradeNg2ComponentAdapter {
componentRef: ComponentRef<any> = null;
changeDetector: ChangeDetectorRef = null;
componentScope: angular.IScope;
childNodes: Node[];
contentInsertionPoint: Node = null;
constructor(
private id: string, private info: ComponentInfo, private element: angular.IAugmentedJQuery,
private info: ComponentInfo, private element: angular.IAugmentedJQuery,
private attrs: angular.IAttributes, private scope: angular.IScope,
private parentInjector: Injector, private parse: angular.IParseService,
private componentFactory: ComponentFactory<any>) {
(<any>this.element[0]).id = id;
this.componentScope = scope.$new();
this.childNodes = <Node[]><any>element.contents();
}
bootstrapNg2() {
bootstrapNg2(projectableNodes: Node[][]) {
const childInjector = ReflectiveInjector.resolveAndCreate(
[{provide: NG1_SCOPE, useValue: this.componentScope}], this.parentInjector);
this.contentInsertionPoint = document.createComment('ng1 insertion point');
this.componentRef = this.componentFactory.create(
childInjector, [[this.contentInsertionPoint]], this.element[0]);
this.componentRef =
this.componentFactory.create(childInjector, projectableNodes, this.element[0]);
this.changeDetector = this.componentRef.changeDetectorRef;
this.component = this.componentRef.instance;
}
@@ -103,16 +98,6 @@ export class DowngradeNg2ComponentAdapter {
this.componentScope.$watch(() => this.changeDetector && this.changeDetector.detectChanges());
}
projectContent() {
const childNodes = this.childNodes;
const parent = this.contentInsertionPoint.parentNode;
if (parent) {
for (let i = 0, ii = childNodes.length; i < ii; i++) {
parent.insertBefore(childNodes[i], this.contentInsertionPoint);
}
}
}
setupOutputs() {
const attrs = this.attrs;
const outputs = this.info.outputs || [];
File diff suppressed because it is too large Load Diff
+14 -3
View File
@@ -6,8 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
export function onError(e: any) {
// TODO: (misko): We seem to not have a stack trace here!
if (console.error) {
@@ -23,6 +21,19 @@ export function controllerKey(name: string): string {
return '$' + name + 'Controller';
}
export function getAttributesAsArray(node: Node): string[][] {
const attributes = node.attributes;
let asArray: string[][];
if (attributes) {
let attrLen = attributes.length;
asArray = new Array(attrLen);
for (let i = 0; i < attrLen; i++) {
asArray[i] = [attributes[i].nodeName, attributes[i].nodeValue];
}
}
return asArray || [];
}
export class Deferred<R> {
promise: Promise<R>;
resolve: (value?: R|PromiseLike<R>) => void;
@@ -34,4 +45,4 @@ export class Deferred<R> {
this.reject = rej;
});
}
}
}
+126 -2
View File
@@ -10,8 +10,8 @@ import {ChangeDetectorRef, Class, Component, EventEmitter, NO_ERRORS_SCHEMA, NgM
import {async, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {UpgradeAdapter} from '@angular/upgrade';
import * as angular from '@angular/upgrade/src/angular_js';
import {UpgradeAdapter, sortProjectableNodes} from '@angular/upgrade/src/upgrade_adapter';
export function main() {
describe('adapter: ng1 to ng2', () => {
@@ -178,7 +178,7 @@ export function main() {
adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(document.body.textContent).toEqual('1A;2A;ng1a;2B;ng1b;2C;1C;');
// https://github.com/angular/angular.js/issues/12983
expect(log).toEqual(['1A', '1B', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']);
expect(log).toEqual(['1A', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']);
ref.dispose();
});
}));
@@ -359,6 +359,33 @@ export function main() {
ref.dispose();
});
}));
it('should support multi-slot projection', async(() => {
const ng1Module = angular.module('ng1', []);
const Ng2 = Component({
selector: 'ng2',
template: '2a(<ng-content select=".ng1a"></ng-content>)' +
'2b(<ng-content select=".ng1b"></ng-content>)'
}).Class({constructor: function() {}});
const Ng2Module = NgModule({declarations: [Ng2], imports: [BrowserModule]}).Class({
constructor: function() {}
});
// The ng-if on one of the projected children is here to make sure
// the correct slot is targeted even with structural directives in play.
const element = html(
'<ng2><div ng-if="true" class="ng1a">1a</div><div' +
' class="ng1b">1b</div></ng2>');
const adapter: UpgradeAdapter = new UpgradeAdapter(Ng2Module);
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(document.body.textContent).toEqual('2a(1a)2b(1b)');
ref.dispose();
});
}));
});
describe('upgrade ng1 component', () => {
@@ -1128,6 +1155,33 @@ export function main() {
ref.dispose();
});
}));
it('should respect hierarchical dependency injection for ng2', async(() => {
const ng1Module = angular.module('ng1', []);
const Ng2Parent = Component({
selector: 'ng2-parent',
template: `ng2-parent(<ng-content></ng-content>)`
}).Class({constructor: function() {}});
const Ng2Child = Component({selector: 'ng2-child', template: `ng2-child`}).Class({
constructor: [Ng2Parent, function(parent: any) {}]
});
const Ng2Module =
NgModule({declarations: [Ng2Parent, Ng2Child], imports: [BrowserModule]}).Class({
constructor: function() {}
});
const element = html('<ng2-parent><ng2-child></ng2-child></ng2-parent>');
const adapter: UpgradeAdapter = new UpgradeAdapter(Ng2Module);
ng1Module.directive('ng2Parent', adapter.downgradeNg2Component(Ng2Parent))
.directive('ng2Child', adapter.downgradeNg2Component(Ng2Child));
adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(document.body.textContent).toEqual('ng2-parent(ng2-child)');
ref.dispose();
});
}));
});
describe('testability', () => {
@@ -1241,6 +1295,70 @@ export function main() {
}));
});
});
describe('sortProjectableNodes', () => {
it('should return an array of node collections for each selector', () => {
const contentNodes = nodes(
'<div class="x"><span>div-1 content</span></div>' +
'<input type="number" name="myNum">' +
'<input type="date" name="myDate">' +
'<span>span content</span>' +
'<div class="x"><span>div-2 content</span></div>');
const selectors = ['input[type=date]', 'span', '.x'];
const projectableNodes = sortProjectableNodes(selectors, contentNodes);
expect(projectableNodes[0]).toEqual(nodes('<input type="date" name="myDate">'));
expect(projectableNodes[1]).toEqual(nodes('<span>span content</span>'));
expect(projectableNodes[2])
.toEqual(nodes(
'<div class="x"><span>div-1 content</span></div>' +
'<div class="x"><span>div-2 content</span></div>'));
});
it('should collect up unmatched nodes for the wildcard selector', () => {
const contentNodes = nodes(
'<div class="x"><span>div-1 content</span></div>' +
'<input type="number" name="myNum">' +
'<input type="date" name="myDate">' +
'<span>span content</span>' +
'<div class="x"><span>div-2 content</span></div>');
const selectors = ['.x', '*', 'input[type=date]'];
const projectableNodes = sortProjectableNodes(selectors, contentNodes);
expect(projectableNodes[0])
.toEqual(nodes(
'<div class="x"><span>div-1 content</span></div>' +
'<div class="x"><span>div-2 content</span></div>'));
expect(projectableNodes[1])
.toEqual(nodes(
'<input type="number" name="myNum">' +
'<span>span content</span>'));
expect(projectableNodes[2]).toEqual(nodes('<input type="date" name="myDate">'));
});
it('should return an array of empty arrays if there are no nodes passed in', () => {
const selectors = ['.x', '*', 'input[type=date]'];
const projectableNodes = sortProjectableNodes(selectors, []);
expect(projectableNodes).toEqual([[], [], []]);
});
it('should return an empty array for each selector that does not match', () => {
const contentNodes = nodes(
'<div class="x"><span>div-1 content</span></div>' +
'<input type="number" name="myNum">' +
'<input type="date" name="myDate">' +
'<span>span content</span>' +
'<div class="x"><span>div-2 content</span></div>');
const noSelectorNodes = sortProjectableNodes([], contentNodes);
expect(noSelectorNodes).toEqual([]);
const noMatchSelectorNodes = sortProjectableNodes(['.not-there'], contentNodes);
expect(noMatchSelectorNodes).toEqual([[]]);
});
});
}
function multiTrim(text: string): string {
@@ -1257,3 +1375,9 @@ function html(html: string): Element {
return body;
}
function nodes(html: string) {
const element = document.createElement('div');
element.innerHTML = html;
return Array.prototype.slice.call(element.childNodes);
}
+1
View File
@@ -219,6 +219,7 @@ export declare class Compiler {
compileModuleAndAllComponentsSync<T>(moduleType: Type<T>): ModuleWithComponentFactories<T>;
compileModuleAsync<T>(moduleType: Type<T>): Promise<NgModuleFactory<T>>;
compileModuleSync<T>(moduleType: Type<T>): NgModuleFactory<T>;
getNgContentSelectors(component: Type<any>): string[];
}
/** @experimental */