feat(ngUpgrade): support for content project from ng1->ng2

This commit is contained in:
Misko Hevery 2015-10-09 14:53:04 -07:00
parent 867c08ac84
commit cd90e6ed8f
9 changed files with 93 additions and 28 deletions

View File

@ -50,6 +50,8 @@ export abstract class DomRenderer extends Renderer implements NodeFactory<Node>
abstract destroyView(viewRef: RenderViewRef); abstract destroyView(viewRef: RenderViewRef);
abstract createRootContentInsertionPoint();
getNativeElementSync(location: RenderElementRef): any { getNativeElementSync(location: RenderElementRef): any {
return resolveInternalDomView(location.renderView).boundElements[location.boundElementIndex]; return resolveInternalDomView(location.renderView).boundElements[location.boundElementIndex];
} }
@ -282,6 +284,9 @@ export class DomRenderer_ extends DomRenderer {
DOM.setAttribute(node, attrNameAndValues[attrIdx], attrNameAndValues[attrIdx + 1]); DOM.setAttribute(node, attrNameAndValues[attrIdx], attrNameAndValues[attrIdx + 1]);
} }
} }
createRootContentInsertionPoint(): Node {
return DOM.createComment('root-content-insertion-point');
}
createShadowRoot(host: Node, templateId: number): Node { createShadowRoot(host: Node, templateId: number): Node {
var sr = DOM.createShadowRoot(host); var sr = DOM.createShadowRoot(host);
var styles = this._nativeShadowStyles.get(templateId); var styles = this._nativeShadowStyles.get(templateId);

View File

@ -25,7 +25,7 @@ export class DefaultRenderView<N> extends RenderViewRef {
constructor(public fragments: DefaultRenderFragmentRef<N>[], public boundTextNodes: N[], constructor(public fragments: DefaultRenderFragmentRef<N>[], public boundTextNodes: N[],
public boundElements: N[], public nativeShadowRoots: N[], public boundElements: N[], public nativeShadowRoots: N[],
public globalEventAdders: Function[]) { public globalEventAdders: Function[], public rootContentInsertionPoints: N[]) {
super(); super();
} }

View File

@ -23,7 +23,8 @@ export function createRenderView(fragmentCmds: RenderTemplateCmd[], inplaceEleme
fragments.push(new DefaultRenderFragmentRef(context.fragments[i])); fragments.push(new DefaultRenderFragmentRef(context.fragments[i]));
} }
view = new DefaultRenderView<any>(fragments, context.boundTextNodes, context.boundElements, view = new DefaultRenderView<any>(fragments, context.boundTextNodes, context.boundElements,
context.nativeShadowRoots, context.globalEventAdders); context.nativeShadowRoots, context.globalEventAdders,
context.rootContentInsertionPoints);
return view; return view;
} }
@ -31,6 +32,7 @@ export interface NodeFactory<N> {
resolveComponentTemplate(templateId: number): RenderTemplateCmd[]; resolveComponentTemplate(templateId: number): RenderTemplateCmd[];
createTemplateAnchor(attrNameAndValues: string[]): N; createTemplateAnchor(attrNameAndValues: string[]): N;
createElement(name: string, attrNameAndValues: string[]): N; createElement(name: string, attrNameAndValues: string[]): N;
createRootContentInsertionPoint(): N;
mergeElement(existing: N, attrNameAndValues: string[]); mergeElement(existing: N, attrNameAndValues: string[]);
createShadowRoot(host: N, templateId: number): N; createShadowRoot(host: N, templateId: number): N;
createText(value: string): N; createText(value: string): N;
@ -41,7 +43,9 @@ export interface NodeFactory<N> {
class BuildContext<N> { class BuildContext<N> {
constructor(private _eventDispatcher: Function, public factory: NodeFactory<N>, constructor(private _eventDispatcher: Function, public factory: NodeFactory<N>,
private _inplaceElement: N) {} private _inplaceElement: N) {
this.isHost = isPresent((_inplaceElement));
}
private _builders: RenderViewBuilder<N>[] = []; private _builders: RenderViewBuilder<N>[] = [];
globalEventAdders: Function[] = []; globalEventAdders: Function[] = [];
@ -49,6 +53,9 @@ class BuildContext<N> {
boundTextNodes: N[] = []; boundTextNodes: N[] = [];
nativeShadowRoots: N[] = []; nativeShadowRoots: N[] = [];
fragments: N[][] = []; fragments: N[][] = [];
rootContentInsertionPoints: N[] = [];
componentCount: number = 0;
isHost: boolean;
build(fragmentCmds: RenderTemplateCmd[]) { build(fragmentCmds: RenderTemplateCmd[]) {
this.enqueueFragmentBuilder(null, fragmentCmds); this.enqueueFragmentBuilder(null, fragmentCmds);
@ -65,6 +72,7 @@ class BuildContext<N> {
} }
enqueueComponentBuilder(component: Component<N>) { enqueueComponentBuilder(component: Component<N>) {
this.componentCount++;
this._builders.push(new RenderViewBuilder<N>( this._builders.push(new RenderViewBuilder<N>(
component, null, this.factory.resolveComponentTemplate(component.cmd.templateId))); component, null, this.factory.resolveComponentTemplate(component.cmd.templateId)));
} }
@ -131,10 +139,20 @@ class RenderViewBuilder<N> implements RenderCommandVisitor {
} }
visitNgContent(cmd: RenderNgContentCmd, context: BuildContext<N>): any { visitNgContent(cmd: RenderNgContentCmd, context: BuildContext<N>): any {
if (isPresent(this.parentComponent)) { if (isPresent(this.parentComponent)) {
var projectedNodes = this.parentComponent.project(cmd.index); if (this.parentComponent.isRoot) {
for (var i = 0; i < projectedNodes.length; i++) { var insertionPoint = context.factory.createRootContentInsertionPoint();
var node = projectedNodes[i]; if (this.parent instanceof Component) {
this._addChild(node, cmd.ngContentIndex, context); context.factory.appendChild((<Component<N>>this.parent).shadowRoot, insertionPoint);
} else {
context.factory.appendChild(<N>this.parent, insertionPoint);
}
context.rootContentInsertionPoints.push(insertionPoint);
} else {
var projectedNodes = this.parentComponent.project(cmd.index);
for (var i = 0; i < projectedNodes.length; i++) {
var node = projectedNodes[i];
this._addChild(node, cmd.ngContentIndex, context);
}
} }
} }
return null; return null;
@ -154,7 +172,8 @@ class RenderViewBuilder<N> implements RenderCommandVisitor {
root = context.factory.createShadowRoot(el, cmd.templateId); root = context.factory.createShadowRoot(el, cmd.templateId);
context.nativeShadowRoots.push(root); context.nativeShadowRoots.push(root);
} }
var component = new Component(el, root, cmd); var isRoot = context.componentCount === 0 && context.isHost;
var component = new Component(el, root, cmd, isRoot);
context.enqueueComponentBuilder(component); context.enqueueComponentBuilder(component);
this.parentStack.push(component); this.parentStack.push(component);
return null; return null;
@ -213,7 +232,8 @@ class RenderViewBuilder<N> implements RenderCommandVisitor {
class Component<N> { class Component<N> {
private contentNodesByNgContentIndex: N[][] = []; private contentNodesByNgContentIndex: N[][] = [];
constructor(public hostElement: N, public shadowRoot: N, public cmd: RenderBeginComponentCmd) {} constructor(public hostElement: N, public shadowRoot: N, public cmd: RenderBeginComponentCmd,
public isRoot: boolean) {}
addContentNode(ngContentIndex: number, node: N, context: BuildContext<N>) { addContentNode(ngContentIndex: number, node: N, context: BuildContext<N>) {
if (isBlank(ngContentIndex)) { if (isBlank(ngContentIndex)) {
if (this.cmd.nativeShadow) { if (this.cmd.nativeShadow) {

View File

@ -517,6 +517,16 @@ export function main() {
expect(stringifyFragment(view.fragments[0].nodes)) expect(stringifyFragment(view.fragments[0].nodes))
.toEqual('<a-comp><b-comp>(hello)</b-comp></a-comp>'); .toEqual('<a-comp><b-comp>(hello)</b-comp></a-comp>');
}); });
it('should store content injection points for root component in a view', () => {
componentTemplates.set(0, [ngContent(0, null)]);
var view =
createRenderView([beginComponent('a-comp', [], [], false, null, 0), endComponent()],
DOM.createElement('root'), nodeFactory);
expect(stringifyFragment(view.rootContentInsertionPoints))
.toEqual('<root-content-insertion-point></root-content-insertion-point>');
});
}); });
}); });
} }
@ -571,6 +581,9 @@ class DomNodeFactory implements NodeFactory<Node> {
return root; return root;
} }
createText(value: string): Node { return DOM.createTextNode(isPresent(value) ? value : ''); } createText(value: string): Node { return DOM.createTextNode(isPresent(value) ? value : ''); }
createRootContentInsertionPoint(): Node {
return DOM.createElement('root-content-insertion-point');
}
appendChild(parent: Node, child: Node) { DOM.appendChild(parent, child); } appendChild(parent: Node, child: Node) { DOM.appendChild(parent, child); }
on(element: Node, eventName: string, callback: Function) { on(element: Node, eventName: string, callback: Function) {
this._localEventListeners.push(new LocalEventListener(element, eventName, callback)); this._localEventListeners.push(new LocalEventListener(element, eventName, callback));

View File

@ -18,7 +18,7 @@ export function main() {
it('should register global event listeners', () => { it('should register global event listeners', () => {
var addCount = 0; var addCount = 0;
var adder = () => { addCount++ }; var adder = () => { addCount++ };
var view = new DefaultRenderView<Node>([], [], [], [], [adder]); var view = new DefaultRenderView<Node>([], [], [], [], [adder], []);
view.hydrate(); view.hydrate();
expect(addCount).toBe(1); expect(addCount).toBe(1);
}); });
@ -28,7 +28,7 @@ export function main() {
it('should deregister global event listeners', () => { it('should deregister global event listeners', () => {
var removeCount = 0; var removeCount = 0;
var adder = () => () => { removeCount++ }; var adder = () => () => { removeCount++ };
var view = new DefaultRenderView<Node>([], [], [], [], [adder]); var view = new DefaultRenderView<Node>([], [], [], [], [adder], []);
view.hydrate(); view.hydrate();
view.dehydrate(); view.dehydrate();
expect(removeCount).toBe(1); expect(removeCount).toBe(1);

View File

@ -25,7 +25,7 @@ declare namespace angular {
require?: string; require?: string;
restrict?: string; restrict?: string;
scope?: {[key: string]: string}; scope?: {[key: string]: string};
link?: Function; link?: {pre?: Function, post?: Function};
} }
interface IAttributes { interface IAttributes {
$observe(attr: string, fn: (v: string) => void); $observe(attr: string, fn: (v: string) => void);
@ -33,6 +33,10 @@ declare namespace angular {
interface ITranscludeFunction {} interface ITranscludeFunction {}
interface IAugmentedJQuery { interface IAugmentedJQuery {
bind(name: string, fn: () => void); bind(name: string, fn: () => void);
data(name: string, value?: any);
contents(): IAugmentedJQuery;
length: number;
[index: number]: Node;
} }
interface IParseService { interface IParseService {
(expression: string): ICompiledExpression; (expression: string): ICompiledExpression;
@ -40,7 +44,7 @@ declare namespace angular {
interface ICompiledExpression { interface ICompiledExpression {
assign(context: any, value: any): any; assign(context: any, value: any): any;
} }
function element(e: Element); function element(e: Element): IAugmentedJQuery;
function bootstrap(e: Element, modules: IModule[], config: IAngularBootstrapConfig); function bootstrap(e: Element, modules: IModule[], config: IAngularBootstrapConfig);
namespace auto { namespace auto {

View File

@ -5,10 +5,13 @@ import {
HostViewRef, HostViewRef,
Injector, Injector,
ProtoViewRef, ProtoViewRef,
SimpleChange SimpleChange,
ViewRef
} from 'angular2/angular2'; } from 'angular2/angular2';
import {NG1_SCOPE} from './constants'; import {NG1_SCOPE} from './constants';
import {ComponentInfo} from './metadata'; import {ComponentInfo} from './metadata';
import {ViewRef_} from "../../angular2/src/core/linker/view_ref";
import Element = protractor.Element;
const INITIAL_VALUE = { const INITIAL_VALUE = {
__UNINITIALIZED__: true __UNINITIALIZED__: true
@ -21,13 +24,17 @@ export class Ng2ComponentFacade {
hostViewRef: HostViewRef = null; hostViewRef: HostViewRef = null;
changeDetector: ChangeDetectorRef = null; changeDetector: ChangeDetectorRef = null;
componentScope: angular.IScope; componentScope: angular.IScope;
childNodes: Node[];
contentInserctionPoint: Node = null;
constructor(private id: string, private info: ComponentInfo, constructor(private id: string, private info: ComponentInfo,
private element: angular.IAugmentedJQuery, private attrs: angular.IAttributes, private element: angular.IAugmentedJQuery, private attrs: angular.IAttributes,
private scope: angular.IScope, private parentInjector: Injector, private scope: angular.IScope, private parentInjector: Injector,
private parse: angular.IParseService, private viewManager: AppViewManager, private parse: angular.IParseService, private viewManager: AppViewManager,
private protoView: ProtoViewRef) { private protoView: ProtoViewRef) {
(<any>this.element[0]).id = id;
this.componentScope = scope.$new(); this.componentScope = scope.$new();
this.childNodes = <Node[]><any>element.contents();
} }
bootstrapNg2() { bootstrapNg2() {
@ -35,9 +42,11 @@ export class Ng2ComponentFacade {
this.parentInjector.resolveAndCreateChild([bind(NG1_SCOPE).toValue(this.componentScope)]); this.parentInjector.resolveAndCreateChild([bind(NG1_SCOPE).toValue(this.componentScope)]);
this.hostViewRef = this.hostViewRef =
this.viewManager.createRootHostView(this.protoView, '#' + this.id, childInjector); this.viewManager.createRootHostView(this.protoView, '#' + this.id, childInjector);
var renderer: any = (<any>this.hostViewRef).render;
var hostElement = this.viewManager.getHostElement(this.hostViewRef); var hostElement = this.viewManager.getHostElement(this.hostViewRef);
this.changeDetector = this.hostViewRef.changeDetectorRef; this.changeDetector = this.hostViewRef.changeDetectorRef;
this.component = this.viewManager.getComponent(hostElement); this.component = this.viewManager.getComponent(hostElement);
this.contentInserctionPoint = renderer.rootContentInsertionPoints[0];
} }
setupInputs() { setupInputs() {
@ -94,6 +103,16 @@ export class Ng2ComponentFacade {
this.componentScope.$watch(() => this.changeDetector.detectChanges()); this.componentScope.$watch(() => this.changeDetector.detectChanges());
} }
projectContent() {
var childNodes = this.childNodes;
if (this.contentInserctionPoint) {
var parent = this.contentInserctionPoint.parentNode;
for (var i = 0, ii = childNodes.length; i < ii; i++) {
parent.insertBefore(childNodes[i], this.contentInserctionPoint);
}
}
}
setupOutputs() { setupOutputs() {
var attrs = this.attrs; var attrs = this.attrs;
var outputs = this.info.outputs; var outputs = this.info.outputs;

View File

@ -169,16 +169,19 @@ function ng1ComponentDirective(info: ComponentInfo, idPrefix: string): Function
return { return {
restrict: 'E', restrict: 'E',
require: REQUIRE_INJECTOR, require: REQUIRE_INJECTOR,
link: (scope: angular.IScope, element: angular.IAugmentedJQuery, attrs: angular.IAttributes, link: {
parentInjector: any, transclude: angular.ITranscludeFunction): void => { post: (scope: angular.IScope, element: angular.IAugmentedJQuery, attrs: angular.IAttributes,
var facade = parentInjector: any, transclude: angular.ITranscludeFunction): void => {
new Ng2ComponentFacade(element[0].id = idPrefix + (idCount++), info, element, attrs, var domElement = <any>element[0];
scope, <Injector>parentInjector, parse, viewManager, protoView); var facade =
new Ng2ComponentFacade(idPrefix + (idCount++), info, element, attrs, scope,
facade.setupInputs(); <Injector>parentInjector, parse, viewManager, protoView);
facade.bootstrapNg2(); facade.setupInputs();
facade.setupOutputs(); facade.bootstrapNg2();
facade.registerCleanup(); facade.projectContent();
facade.setupOutputs();
facade.registerCleanup();
}
} }
}; };
} }

View File

@ -18,17 +18,18 @@ export function main() {
describe('upgrade: ng1 to ng2', () => { describe('upgrade: ng1 to ng2', () => {
it('should have angular 1 loaded', () => expect(angular.version.major).toBe(1)); it('should have angular 1 loaded', () => expect(angular.version.major).toBe(1));
it('should instantiate ng2 in ng1 template', inject([AsyncTestCompleter], (async) => { it('should instantiate ng2 in ng1 template and project content',
inject([AsyncTestCompleter], (async) => {
var Ng2 = Component({selector: 'ng2'}) var Ng2 = Component({selector: 'ng2'})
.View({template: `{{ 'NG2' }}`}) .View({template: `{{ 'NG2' }}(<ng-content></ng-content>)`})
.Class({constructor: function() {}}); .Class({constructor: function() {}});
var element = html("<div>{{ 'ng1-' }}<ng2>~~</ng2>{{ '-ng1' }}</div>"); var element = html("<div>{{ 'ng1[' }}<ng2>~{{ 'ng-content' }}~</ng2>{{ ']' }}</div>");
var upgradeModule: UpgradeModule = createUpgradeModule(); var upgradeModule: UpgradeModule = createUpgradeModule();
upgradeModule.importNg2Component(Ng2); upgradeModule.importNg2Component(Ng2);
upgradeModule.bootstrap(element).ready(() => { upgradeModule.bootstrap(element).ready(() => {
expect(document.body.textContent).toEqual("ng1-NG2-ng1"); expect(document.body.textContent).toEqual("ng1[NG2(~ng-content~)]");
async.done(); async.done();
}); });
})); }));