feat(compiler): attach components and project light dom during compilation.
Closes #2529 BREAKING CHANGES: - shadow dom emulation no longer supports the `<content>` tag. Use the new `<ng-content>` instead (works with all shadow dom strategies). - removed `DomRenderer.setViewRootNodes` and `AppViewManager.getComponentView` -> use `DomRenderer.getNativeElementSync(elementRef)` and change shadow dom directly - the `Renderer` interface has changed: * `createView` now also has to support sub views * the notion of a container has been removed. Instead, the renderer has to implement methods to attach views next to elements or other views. * a RenderView now contains multiple RenderFragments. Fragments are used to move DOM nodes around. Internal changes / design changes: - Introduce notion of view fragments on render side - DomProtoViews and DomViews on render side are merged, AppProtoViews are not merged, AppViews are partially merged (they share arrays with the other merged AppViews but we keep individual AppView instances for now). - DomProtoViews always have a `<template>` element as root * needed for storing subviews * we have less chunks of DOM to clone now - remove fake ElementBinder / Bound element for root text bindings and model them explicitly. This removes a lot of special cases we had! - AppView shares data with nested component views - some methods in AppViewManager (create, hydrate, dehydrate) are iterative now * now possible as we have all child AppViews / ElementRefs already in an array!
This commit is contained in:
@ -16,10 +16,11 @@ import {
|
||||
import {MapWrapper} from 'angular2/src/facade/collection';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
|
||||
import {DomTestbed, TestView, elRef} from './dom_testbed';
|
||||
import {DomTestbed, TestRootView, elRef} from './dom_testbed';
|
||||
|
||||
import {ViewDefinition, DirectiveMetadata, RenderViewRef} from 'angular2/src/render/api';
|
||||
import {DOM_REFLECT_PROPERTIES_AS_ATTRIBUTES} from 'angular2/src/render/dom/dom_renderer';
|
||||
import {ShadowDomStrategy, NativeShadowDomStrategy} from 'angular2/render';
|
||||
import {bind} from 'angular2/di';
|
||||
|
||||
export function main() {
|
||||
@ -27,101 +28,69 @@ export function main() {
|
||||
beforeEachBindings(() => [DomTestbed]);
|
||||
|
||||
it('should create and destroy root host views while using the given elements in place',
|
||||
inject([AsyncTestCompleter, DomTestbed], (async, tb) => {
|
||||
inject([AsyncTestCompleter, DomTestbed], (async, tb: DomTestbed) => {
|
||||
tb.compiler.compileHost(someComponent)
|
||||
.then((hostProtoViewDto) => {
|
||||
var view =
|
||||
new TestView(tb.renderer.createRootHostView(hostProtoViewDto.render, '#root'));
|
||||
expect(view.rawView.rootNodes[0]).toEqual(tb.rootEl);
|
||||
|
||||
tb.renderer.destroyView(view.viewRef);
|
||||
// destroying a root view should not disconnect it!
|
||||
var view = new TestRootView(
|
||||
tb.renderer.createRootHostView(hostProtoViewDto.render, 0, '#root'));
|
||||
expect(tb.rootEl.parentNode).toBeTruthy();
|
||||
expect(view.hostElement).toEqual(tb.rootEl);
|
||||
|
||||
tb.renderer.detachFragment(view.fragments[0]);
|
||||
tb.renderer.destroyView(view.viewRef);
|
||||
expect(tb.rootEl.parentNode).toBeFalsy();
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should attach and detach component views',
|
||||
inject([AsyncTestCompleter, DomTestbed], (async, tb) => {
|
||||
tb.compileAll([
|
||||
someComponent,
|
||||
new ViewDefinition({componentId: 'someComponent', template: 'hello', directives: []})
|
||||
])
|
||||
.then((protoViewDtos) => {
|
||||
var rootView = tb.createRootView(protoViewDtos[0]);
|
||||
var cmpView = tb.createComponentView(rootView.viewRef, 0, protoViewDtos[1]);
|
||||
expect(tb.rootEl).toHaveText('hello');
|
||||
tb.destroyComponentView(rootView.viewRef, 0, cmpView.viewRef);
|
||||
expect(tb.rootEl).toHaveText('');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
it('should update text nodes',
|
||||
inject([AsyncTestCompleter, DomTestbed], (async, tb: DomTestbed) => {
|
||||
tb.compileAndMerge(
|
||||
someComponent,
|
||||
[
|
||||
new ViewDefinition(
|
||||
{componentId: 'someComponent', template: '{{a}}', directives: []})
|
||||
])
|
||||
.then((protoViewMergeMappings) => {
|
||||
var rootView = tb.createView(protoViewMergeMappings[0]);
|
||||
|
||||
it('should not create LightDom instances if the host element is empty',
|
||||
inject([AsyncTestCompleter, DomTestbed], (async, tb) => {
|
||||
tb.compileAll([
|
||||
someComponent,
|
||||
new ViewDefinition({
|
||||
componentId: 'someComponent',
|
||||
template: '<some-comp> <!-- comment -->\n </some-comp>',
|
||||
directives: [someComponent]
|
||||
})
|
||||
])
|
||||
.then((protoViewDtos) => {
|
||||
var rootView = tb.createRootView(protoViewDtos[0]);
|
||||
var cmpView = tb.createComponentView(rootView.viewRef, 0, protoViewDtos[1]);
|
||||
expect(cmpView.rawView.proto.elementBinders[0].componentId).toBe('someComponent');
|
||||
expect(cmpView.rawView.boundElements[0].lightDom).toBe(null);
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should update text nodes', inject([AsyncTestCompleter, DomTestbed], (async, tb) => {
|
||||
tb.compileAll([
|
||||
someComponent,
|
||||
new ViewDefinition({componentId: 'someComponent', template: '{{a}}', directives: []})
|
||||
])
|
||||
.then((protoViewDtos) => {
|
||||
var rootView = tb.createRootView(protoViewDtos[0]);
|
||||
var cmpView = tb.createComponentView(rootView.viewRef, 0, protoViewDtos[1]);
|
||||
|
||||
tb.renderer.setText(cmpView.viewRef, 0, 'hello');
|
||||
expect(tb.rootEl).toHaveText('hello');
|
||||
tb.renderer.setText(rootView.viewRef, 0, 'hello');
|
||||
expect(rootView.hostElement).toHaveText('hello');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should update any element property/attributes/class/style independent of the compilation',
|
||||
inject([AsyncTestCompleter, DomTestbed], (async, tb) => {
|
||||
tb.compileAll([
|
||||
someComponent,
|
||||
new ViewDefinition({
|
||||
componentId: 'someComponent',
|
||||
template: '<input [title]="y" style="position:absolute">',
|
||||
directives: []
|
||||
})
|
||||
])
|
||||
.then((protoViewDtos) => {
|
||||
var rootView = tb.createRootView(protoViewDtos[0]);
|
||||
var cmpView = tb.createComponentView(rootView.viewRef, 0, protoViewDtos[1]);
|
||||
inject([AsyncTestCompleter, DomTestbed], (async, tb: DomTestbed) => {
|
||||
tb.compileAndMerge(someComponent,
|
||||
[
|
||||
new ViewDefinition({
|
||||
componentId: 'someComponent',
|
||||
template: '<input [title]="y" style="position:absolute">',
|
||||
directives: []
|
||||
})
|
||||
])
|
||||
.then((protoViewMergeMappings) => {
|
||||
var rootView = tb.createView(protoViewMergeMappings[0]);
|
||||
|
||||
var el = DOM.childNodes(tb.rootEl)[0];
|
||||
tb.renderer.setElementProperty(elRef(cmpView.viewRef, 0), 'value', 'hello');
|
||||
var elr = elRef(rootView.viewRef, 1);
|
||||
var el = DOM.childNodes(rootView.hostElement)[0];
|
||||
tb.renderer.setElementProperty(elr, 'value', 'hello');
|
||||
expect((<HTMLInputElement>el).value).toEqual('hello');
|
||||
|
||||
tb.renderer.setElementClass(elRef(cmpView.viewRef, 0), 'a', true);
|
||||
expect((<HTMLInputElement>DOM.childNodes(tb.rootEl)[0]).value).toEqual('hello');
|
||||
tb.renderer.setElementClass(elRef(cmpView.viewRef, 0), 'a', false);
|
||||
tb.renderer.setElementClass(elr, 'a', true);
|
||||
expect((<HTMLInputElement>DOM.childNodes(rootView.hostElement)[0]).value)
|
||||
.toEqual('hello');
|
||||
tb.renderer.setElementClass(elr, 'a', false);
|
||||
expect(DOM.hasClass(el, 'a')).toBe(false);
|
||||
|
||||
tb.renderer.setElementStyle(elRef(cmpView.viewRef, 0), 'width', '10px');
|
||||
tb.renderer.setElementStyle(elr, 'width', '10px');
|
||||
expect(DOM.getStyle(el, 'width')).toEqual('10px');
|
||||
tb.renderer.setElementStyle(elRef(cmpView.viewRef, 0), 'width', null);
|
||||
tb.renderer.setElementStyle(elr, 'width', null);
|
||||
expect(DOM.getStyle(el, 'width')).toEqual('');
|
||||
|
||||
tb.renderer.setElementAttribute(elRef(cmpView.viewRef, 0), 'someAttr', 'someValue');
|
||||
tb.renderer.setElementAttribute(elr, 'someAttr', 'someValue');
|
||||
expect(DOM.getAttribute(el, 'some-attr')).toEqual('someValue');
|
||||
|
||||
async.done();
|
||||
@ -131,16 +100,18 @@ export function main() {
|
||||
|
||||
it('should NOT reflect property values as attributes if flag is NOT set',
|
||||
inject([AsyncTestCompleter, DomTestbed], (async, tb) => {
|
||||
tb.compileAll([
|
||||
someComponent,
|
||||
new ViewDefinition(
|
||||
{componentId: 'someComponent', template: '<input [title]="y">', directives: []})
|
||||
])
|
||||
.then((protoViewDtos) => {
|
||||
var rootView = tb.createRootView(protoViewDtos[0]);
|
||||
var cmpView = tb.createComponentView(rootView.viewRef, 0, protoViewDtos[1]);
|
||||
var el = DOM.childNodes(tb.rootEl)[0];
|
||||
tb.renderer.setElementProperty(elRef(cmpView.viewRef, 0), 'maxLength', '20');
|
||||
tb.compileAndMerge(someComponent,
|
||||
[
|
||||
new ViewDefinition({
|
||||
componentId: 'someComponent',
|
||||
template: '<input [title]="y">',
|
||||
directives: []
|
||||
})
|
||||
])
|
||||
.then((protoViewMergeMappings) => {
|
||||
var rootView = tb.createView(protoViewMergeMappings[0]);
|
||||
var el = DOM.childNodes(rootView.hostElement)[0];
|
||||
tb.renderer.setElementProperty(elRef(rootView.viewRef, 1), 'maxLength', '20');
|
||||
expect(DOM.getAttribute(<HTMLInputElement>el, 'ng-reflect-max-length'))
|
||||
.toEqual(null);
|
||||
|
||||
@ -150,21 +121,21 @@ export function main() {
|
||||
|
||||
describe('reflection', () => {
|
||||
beforeEachBindings(() => [bind(DOM_REFLECT_PROPERTIES_AS_ATTRIBUTES).toValue(true)]);
|
||||
|
||||
it('should reflect property values as attributes if flag is set',
|
||||
inject([AsyncTestCompleter, DomTestbed], (async, tb) => {
|
||||
tb.compileAll([
|
||||
someComponent,
|
||||
new ViewDefinition({
|
||||
componentId: 'someComponent',
|
||||
template: '<input [title]="y">',
|
||||
directives: []
|
||||
})
|
||||
])
|
||||
.then((protoViewDtos) => {
|
||||
var rootView = tb.createRootView(protoViewDtos[0]);
|
||||
var cmpView = tb.createComponentView(rootView.viewRef, 0, protoViewDtos[1]);
|
||||
var el = DOM.childNodes(tb.rootEl)[0];
|
||||
tb.renderer.setElementProperty(elRef(cmpView.viewRef, 0), 'maxLength', '20');
|
||||
tb.compileAndMerge(someComponent,
|
||||
[
|
||||
new ViewDefinition({
|
||||
componentId: 'someComponent',
|
||||
template: '<input [title]="y">',
|
||||
directives: []
|
||||
})
|
||||
])
|
||||
.then((protoViewMergeMappings) => {
|
||||
var rootView = tb.createView(protoViewMergeMappings[0]);
|
||||
var el = DOM.childNodes(rootView.hostElement)[0];
|
||||
tb.renderer.setElementProperty(elRef(rootView.viewRef, 1), 'maxLength', '20');
|
||||
expect(DOM.getAttribute(<HTMLInputElement>el, 'ng-reflect-max-length'))
|
||||
.toEqual('20');
|
||||
async.done();
|
||||
@ -174,70 +145,69 @@ export function main() {
|
||||
|
||||
if (DOM.supportsDOMEvents()) {
|
||||
it('should call actions on the element independent of the compilation',
|
||||
inject([AsyncTestCompleter, DomTestbed], (async, tb) => {
|
||||
tb.compileAll([
|
||||
someComponent,
|
||||
new ViewDefinition({
|
||||
componentId: 'someComponent',
|
||||
template: '<input [title]="y"></input>',
|
||||
directives: []
|
||||
})
|
||||
])
|
||||
.then((protoViewDtos) => {
|
||||
var views = tb.createRootViews(protoViewDtos);
|
||||
var componentView = views[1];
|
||||
inject([AsyncTestCompleter, DomTestbed], (async, tb: DomTestbed) => {
|
||||
tb.compileAndMerge(someComponent,
|
||||
[
|
||||
new ViewDefinition({
|
||||
componentId: 'someComponent',
|
||||
template: '<input [title]="y"></input>',
|
||||
directives: []
|
||||
})
|
||||
])
|
||||
.then((protoViewMergeMappings) => {
|
||||
var rootView = tb.createView(protoViewMergeMappings[0]);
|
||||
|
||||
tb.renderer.invokeElementMethod(elRef(componentView.viewRef, 0), 'setAttribute',
|
||||
tb.renderer.invokeElementMethod(elRef(rootView.viewRef, 1), 'setAttribute',
|
||||
['a', 'b']);
|
||||
|
||||
expect(DOM.getAttribute(DOM.childNodes(tb.rootEl)[0], 'a')).toEqual('b');
|
||||
expect(DOM.getAttribute(DOM.childNodes(rootView.hostElement)[0], 'a'))
|
||||
.toEqual('b');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
it('should add and remove views to and from containers',
|
||||
inject([AsyncTestCompleter, DomTestbed], (async, tb) => {
|
||||
tb.compileAll([
|
||||
someComponent,
|
||||
new ViewDefinition({
|
||||
componentId: 'someComponent',
|
||||
template: '<template>hello</template>',
|
||||
directives: []
|
||||
})
|
||||
])
|
||||
.then((protoViewDtos) => {
|
||||
var rootView = tb.createRootView(protoViewDtos[0]);
|
||||
var cmpView = tb.createComponentView(rootView.viewRef, 0, protoViewDtos[1]);
|
||||
it('should add and remove fragments',
|
||||
inject([AsyncTestCompleter, DomTestbed], (async, tb: DomTestbed) => {
|
||||
tb.compileAndMerge(someComponent,
|
||||
[
|
||||
new ViewDefinition({
|
||||
componentId: 'someComponent',
|
||||
template: '<template>hello</template>',
|
||||
directives: []
|
||||
})
|
||||
])
|
||||
.then((protoViewMergeMappings) => {
|
||||
var rootView = tb.createView(protoViewMergeMappings[0]);
|
||||
|
||||
var childProto = protoViewDtos[1].elementBinders[0].nestedProtoView;
|
||||
expect(tb.rootEl).toHaveText('');
|
||||
var childView = tb.createViewInContainer(cmpView.viewRef, 0, 0, childProto);
|
||||
expect(tb.rootEl).toHaveText('hello');
|
||||
tb.destroyViewInContainer(cmpView.viewRef, 0, 0, childView.viewRef);
|
||||
expect(tb.rootEl).toHaveText('');
|
||||
var elr = elRef(rootView.viewRef, 1);
|
||||
expect(rootView.hostElement).toHaveText('');
|
||||
var fragment = rootView.fragments[1];
|
||||
tb.renderer.attachFragmentAfterElement(elr, fragment);
|
||||
expect(rootView.hostElement).toHaveText('hello');
|
||||
tb.renderer.detachFragment(fragment);
|
||||
expect(rootView.hostElement).toHaveText('');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should handle events', inject([AsyncTestCompleter, DomTestbed], (async, tb: DomTestbed) => {
|
||||
tb.compileAll([
|
||||
someComponent,
|
||||
new ViewDefinition({
|
||||
componentId: 'someComponent',
|
||||
template: '<input (change)="doSomething()">',
|
||||
directives: []
|
||||
})
|
||||
])
|
||||
tb.compileAndMerge(someComponent,
|
||||
[
|
||||
new ViewDefinition({
|
||||
componentId: 'someComponent',
|
||||
template: '<input (change)="doSomething()">',
|
||||
directives: []
|
||||
})
|
||||
])
|
||||
.then((protoViewDtos) => {
|
||||
var rootView = tb.createRootView(protoViewDtos[0]);
|
||||
var cmpView = tb.createComponentView(rootView.viewRef, 0, protoViewDtos[1]);
|
||||
var rootView = tb.createView(protoViewDtos[0]);
|
||||
|
||||
tb.triggerEvent(cmpView.viewRef, 0, 'change');
|
||||
var eventEntry = cmpView.events[0];
|
||||
tb.triggerEvent(elRef(rootView.viewRef, 1), 'change');
|
||||
var eventEntry = rootView.events[0];
|
||||
// bound element index
|
||||
expect(eventEntry[0]).toEqual(0);
|
||||
expect(eventEntry[0]).toEqual(1);
|
||||
// event type
|
||||
expect(eventEntry[1]).toEqual('change');
|
||||
// actual event
|
||||
@ -247,6 +217,30 @@ export function main() {
|
||||
|
||||
}));
|
||||
|
||||
if (DOM.supportsNativeShadowDOM()) {
|
||||
describe('native shadow dom support', () => {
|
||||
beforeEachBindings(
|
||||
() => { return [bind(ShadowDomStrategy).toValue(new NativeShadowDomStrategy())]; });
|
||||
|
||||
it('should support shadow dom components',
|
||||
inject([AsyncTestCompleter, DomTestbed], (async, tb: DomTestbed) => {
|
||||
tb.compileAndMerge(
|
||||
someComponent,
|
||||
[
|
||||
new ViewDefinition(
|
||||
{componentId: 'someComponent', template: 'hello', directives: []})
|
||||
])
|
||||
.then((protoViewMergeMappings) => {
|
||||
var rootView = tb.createView(protoViewMergeMappings[0]);
|
||||
expect(DOM.getShadowRoot(rootView.hostElement)).toHaveText('hello');
|
||||
async.done();
|
||||
});
|
||||
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user