feat(view): add support for instantiation of nested component views.
Include shadowDom creation and integration tests for nested components. Fix accidentally clobbered modules/core/test/compiler/view_spec.js by previous commit.
This commit is contained in:
parent
b07ea6b90e
commit
be4cb2db3a
|
@ -15,7 +15,7 @@ export class Directive {
|
||||||
bind:Object,
|
bind:Object,
|
||||||
lightDomServices:List,
|
lightDomServices:List,
|
||||||
implementsTypes:List
|
implementsTypes:List
|
||||||
})
|
}={})
|
||||||
{
|
{
|
||||||
this.selector = selector;
|
this.selector = selector;
|
||||||
this.lightDomServices = lightDomServices;
|
this.lightDomServices = lightDomServices;
|
||||||
|
|
|
@ -129,6 +129,7 @@ export class ProtoElementInjector {
|
||||||
@FIELD('_key9:int')
|
@FIELD('_key9:int')
|
||||||
@FIELD('final parent:ProtoElementInjector')
|
@FIELD('final parent:ProtoElementInjector')
|
||||||
@FIELD('final index:int')
|
@FIELD('final index:int')
|
||||||
|
@FIELD('view:View')
|
||||||
constructor(parent:ProtoElementInjector, index:int, bindings:List, firstBindingIsComponent:boolean = false) {
|
constructor(parent:ProtoElementInjector, index:int, bindings:List, firstBindingIsComponent:boolean = false) {
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.index = index;
|
this.index = index;
|
||||||
|
|
|
@ -30,6 +30,7 @@ export class View {
|
||||||
/// to keep track of the nodes.
|
/// to keep track of the nodes.
|
||||||
@FIELD('final nodes:List<Node>')
|
@FIELD('final nodes:List<Node>')
|
||||||
@FIELD('final onChangeDispatcher:OnChangeDispatcher')
|
@FIELD('final onChangeDispatcher:OnChangeDispatcher')
|
||||||
|
@FIELD('childViews: List<View>')
|
||||||
constructor(nodes:List<Node>, elementInjectors:List,
|
constructor(nodes:List<Node>, elementInjectors:List,
|
||||||
rootElementInjectors:List, textNodes:List, bindElements:List,
|
rootElementInjectors:List, textNodes:List, bindElements:List,
|
||||||
protoWatchGroup:ProtoWatchGroup, context) {
|
protoWatchGroup:ProtoWatchGroup, context) {
|
||||||
|
@ -41,6 +42,9 @@ export class View {
|
||||||
this.bindElements = bindElements;
|
this.bindElements = bindElements;
|
||||||
this.watchGroup = protoWatchGroup.instantiate(this, MapWrapper.create());
|
this.watchGroup = protoWatchGroup.instantiate(this, MapWrapper.create());
|
||||||
this.watchGroup.setContext(context);
|
this.watchGroup.setContext(context);
|
||||||
|
// TODO(rado): Since this is only used in tests for now, investigate whether
|
||||||
|
// we can remove it.
|
||||||
|
this.childViews = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
onRecordChange(record:Record, target) {
|
onRecordChange(record:Record, target) {
|
||||||
|
@ -58,6 +62,10 @@ export class View {
|
||||||
DOM.setText(this.textNodes[textNodeIndex], record.currentValue);
|
DOM.setText(this.textNodes[textNodeIndex], record.currentValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addChild(childView: View) {
|
||||||
|
ListWrapper.push(this.childViews, childView);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProtoView {
|
export class ProtoView {
|
||||||
|
@ -74,7 +82,7 @@ export class ProtoView {
|
||||||
this.elementsWithBindingCount = 0;
|
this.elementsWithBindingCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
instantiate(context, appInjector:Injector):View {
|
instantiate(context, lightDomAppInjector:Injector, hostElementInjector: ElementInjector):View {
|
||||||
var clone = DOM.clone(this.element);
|
var clone = DOM.clone(this.element);
|
||||||
var elements;
|
var elements;
|
||||||
if (clone instanceof TemplateElement) {
|
if (clone instanceof TemplateElement) {
|
||||||
|
@ -89,14 +97,15 @@ export class ProtoView {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: vsavkin: benchmark
|
* TODO: vsavkin: benchmark
|
||||||
* If this performs poorly, the five loops can be collapsed into one.
|
* If this performs poorly, the seven loops can be collapsed into one.
|
||||||
*/
|
*/
|
||||||
var elementInjectors = ProtoView._createElementInjectors(elements, binders);
|
var elementInjectors = ProtoView._createElementInjectors(elements, binders, hostElementInjector);
|
||||||
var rootElementInjectors = ProtoView._rootElementInjectors(elementInjectors);
|
var rootElementInjectors = ProtoView._rootElementInjectors(elementInjectors);
|
||||||
var textNodes = ProtoView._textNodes(elements, binders);
|
var textNodes = ProtoView._textNodes(elements, binders);
|
||||||
var bindElements = ProtoView._bindElements(elements, binders);
|
var bindElements = ProtoView._bindElements(elements, binders);
|
||||||
|
var shadowAppInjectors = ProtoView._createShadowAppInjectors(binders, lightDomAppInjector);
|
||||||
var viewNodes;
|
var viewNodes;
|
||||||
|
|
||||||
if (clone instanceof TemplateElement) {
|
if (clone instanceof TemplateElement) {
|
||||||
viewNodes = ListWrapper.clone(clone.content.childNodes);
|
viewNodes = ListWrapper.clone(clone.content.childNodes);
|
||||||
} else {
|
} else {
|
||||||
|
@ -105,7 +114,10 @@ export class ProtoView {
|
||||||
var view = new View(viewNodes, elementInjectors, rootElementInjectors, textNodes,
|
var view = new View(viewNodes, elementInjectors, rootElementInjectors, textNodes,
|
||||||
bindElements, this.protoWatchGroup, context);
|
bindElements, this.protoWatchGroup, context);
|
||||||
|
|
||||||
ProtoView._instantiateDirectives(view, elements, elementInjectors, appInjector);
|
ProtoView._instantiateDirectives(
|
||||||
|
view, elements, elementInjectors, lightDomAppInjector, shadowAppInjectors);
|
||||||
|
ProtoView._instantiateChildComponentViews(
|
||||||
|
elements, binders, elementInjectors, shadowAppInjectors, view);
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
@ -162,13 +174,13 @@ export class ProtoView {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static _createElementInjectors(elements, binders) {
|
static _createElementInjectors(elements, binders, hostElementInjector) {
|
||||||
var injectors = ListWrapper.createFixedSize(binders.length);
|
var injectors = ListWrapper.createFixedSize(binders.length);
|
||||||
for (var i = 0; i < binders.length; ++i) {
|
for (var i = 0; i < binders.length; ++i) {
|
||||||
var proto = binders[i].protoElementInjector;
|
var proto = binders[i].protoElementInjector;
|
||||||
if (isPresent(proto)) {
|
if (isPresent(proto)) {
|
||||||
var parentElementInjector = isPresent(proto.parent) ? injectors[proto.parent.index] : null;
|
var parentElementInjector = isPresent(proto.parent) ? injectors[proto.parent.index] : null;
|
||||||
injectors[i] = ProtoView._createElementInjector(elements[i], parentElementInjector, proto);
|
injectors[i] = proto.instantiate(parentElementInjector, hostElementInjector);
|
||||||
} else {
|
} else {
|
||||||
injectors[i] = null;
|
injectors[i] = null;
|
||||||
}
|
}
|
||||||
|
@ -177,17 +189,15 @@ export class ProtoView {
|
||||||
}
|
}
|
||||||
|
|
||||||
static _instantiateDirectives(
|
static _instantiateDirectives(
|
||||||
view: View, elements:List, injectors:List<ElementInjectors>, appInjector:Injector) {
|
view: View, elements:List, injectors:List<ElementInjectors>, lightDomAppInjector: Injector,
|
||||||
|
shadowDomAppInjectors:List<Injectors>) {
|
||||||
for (var i = 0; i < injectors.length; ++i) {
|
for (var i = 0; i < injectors.length; ++i) {
|
||||||
var preBuiltObjs = new PreBuiltObjects(view, new NgElement(elements[i]));
|
var preBuiltObjs = new PreBuiltObjects(view, new NgElement(elements[i]));
|
||||||
if (injectors[i] != null) injectors[i].instantiateDirectives(appInjector, null, preBuiltObjs);
|
if (injectors[i] != null) injectors[i].instantiateDirectives(
|
||||||
|
lightDomAppInjector, shadowDomAppInjectors[i], preBuiltObjs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static _createElementInjector(element, parent:ElementInjector, proto:ProtoElementInjector) {
|
|
||||||
return proto.instantiate(parent, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
static _rootElementInjectors(injectors) {
|
static _rootElementInjectors(injectors) {
|
||||||
return ListWrapper.filter(injectors, inj => isPresent(inj) && isBlank(inj.parent));
|
return ListWrapper.filter(injectors, inj => isPresent(inj) && isBlank(inj.parent));
|
||||||
}
|
}
|
||||||
|
@ -216,6 +226,39 @@ export class ProtoView {
|
||||||
ListWrapper.push(allTextNodes, childNodes[indices[i]]);
|
ListWrapper.push(allTextNodes, childNodes[indices[i]]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static _instantiateChildComponentViews(elements, binders, injectors,
|
||||||
|
shadowDomAppInjectors: List<Injector>, view: View) {
|
||||||
|
for (var i = 0; i < binders.length; ++i) {
|
||||||
|
var binder = binders[i];
|
||||||
|
if (isPresent(binder.componentDirective)) {
|
||||||
|
var injector = injectors[i];
|
||||||
|
var childView = binder.nestedProtoView.instantiate(
|
||||||
|
injector.getComponent(), shadowDomAppInjectors[i], injector);
|
||||||
|
view.addChild(childView);
|
||||||
|
var shadowRoot = elements[i].createShadowRoot();
|
||||||
|
// TODO(rado): reuse utility from ViewPort/View.
|
||||||
|
for (var j = 0; j < childView.nodes.length; ++j) {
|
||||||
|
DOM.appendChild(shadowRoot, childView.nodes[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static _createShadowAppInjectors(binders: List<ElementBinders>, lightDomAppInjector: Injector): List<Injectors> {
|
||||||
|
var injectors = ListWrapper.createFixedSize(binders.length);
|
||||||
|
for (var i = 0; i < binders.length; ++i) {
|
||||||
|
var componentDirective = binders[i].componentDirective;
|
||||||
|
if (isPresent(componentDirective)) {
|
||||||
|
var services = componentDirective.annotation.componentServices;
|
||||||
|
injectors[i] = isPresent(services) ?
|
||||||
|
lightDomAppInjector.createChild(services) : lightDomAppInjector;
|
||||||
|
} else {
|
||||||
|
injectors[i] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return injectors;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ElementPropertyMemento {
|
export class ElementPropertyMemento {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {describe, xit, it, expect, beforeEach, ddescribe, iit} from 'test_lib/te
|
||||||
|
|
||||||
import {DOM} from 'facade/dom';
|
import {DOM} from 'facade/dom';
|
||||||
|
|
||||||
|
import {Injector} from 'di/di';
|
||||||
import {ChangeDetector} from 'change_detection/change_detector';
|
import {ChangeDetector} from 'change_detection/change_detector';
|
||||||
import {Parser} from 'change_detection/parser/parser';
|
import {Parser} from 'change_detection/parser/parser';
|
||||||
import {ClosureMap} from 'change_detection/parser/closure_map';
|
import {ClosureMap} from 'change_detection/parser/closure_map';
|
||||||
|
@ -27,7 +28,7 @@ export function main() {
|
||||||
var view, ctx, cd;
|
var view, ctx, cd;
|
||||||
function createView(pv) {
|
function createView(pv) {
|
||||||
ctx = new MyComp();
|
ctx = new MyComp();
|
||||||
view = pv.instantiate(ctx, null);
|
view = pv.instantiate(ctx, new Injector([]), null);
|
||||||
cd = new ChangeDetector(view.watchGroup);
|
cd = new ChangeDetector(view.watchGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,6 +67,21 @@ export function main() {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support nested components.', (done) => {
|
||||||
|
compiler.compile(MyComp, createElement('<child-cmp></child-cmp>')).then((pv) => {
|
||||||
|
createView(pv);
|
||||||
|
|
||||||
|
cd.detectChanges();
|
||||||
|
|
||||||
|
// TODO(rado): this should be removed once watchgroups addChild is implemented.
|
||||||
|
var childWatchGroup = view.childViews[0].watchGroup;
|
||||||
|
new ChangeDetector(childWatchGroup).detectChanges();
|
||||||
|
|
||||||
|
expect(view.nodes[0].shadowRoot.childNodes[0].nodeValue).toEqual('hello');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -82,7 +98,7 @@ class MyDir {
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: new TemplateConfig({
|
template: new TemplateConfig({
|
||||||
directives: [MyDir]
|
directives: [MyDir, ChildComp]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
class MyComp {
|
class MyComp {
|
||||||
|
@ -91,6 +107,26 @@ class MyComp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'child-cmp',
|
||||||
|
componentServices: [MyService],
|
||||||
|
template: new TemplateConfig({
|
||||||
|
directives: [MyDir],
|
||||||
|
inline: '{{ctxProp}}'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
class ChildComp {
|
||||||
|
constructor(service: MyService) {
|
||||||
|
this.ctxProp = service.greeting;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyService {
|
||||||
|
constructor() {
|
||||||
|
this.greeting = 'hello';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function createElement(html) {
|
function createElement(html) {
|
||||||
return DOM.createTemplate(html).content.firstChild;
|
return DOM.createTemplate(html).content.firstChild;
|
||||||
|
|
|
@ -21,12 +21,14 @@ import {Parser} from 'change_detection/parser/parser';
|
||||||
import {Lexer} from 'change_detection/parser/lexer';
|
import {Lexer} from 'change_detection/parser/lexer';
|
||||||
import {ClosureMap} from 'change_detection/parser/closure_map';
|
import {ClosureMap} from 'change_detection/parser/closure_map';
|
||||||
import {ChangeDetector} from 'change_detection/change_detector';
|
import {ChangeDetector} from 'change_detection/change_detector';
|
||||||
|
import {Injector} from 'di/di';
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('ElementBinderBuilder', () => {
|
describe('ElementBinderBuilder', () => {
|
||||||
var evalContext, view, changeDetector;
|
var evalContext, view, changeDetector;
|
||||||
|
|
||||||
function createPipeline({textNodeBindings, propertyBindings, directives, protoElementInjector}={}) {
|
function createPipeline({textNodeBindings, propertyBindings, directives, protoElementInjector
|
||||||
|
}={}) {
|
||||||
var reflector = new Reflector();
|
var reflector = new Reflector();
|
||||||
var closureMap = new ClosureMap();
|
var closureMap = new ClosureMap();
|
||||||
return new CompilePipeline([
|
return new CompilePipeline([
|
||||||
|
@ -69,7 +71,7 @@ export function main() {
|
||||||
|
|
||||||
function instantiateView(protoView) {
|
function instantiateView(protoView) {
|
||||||
evalContext = new Context();
|
evalContext = new Context();
|
||||||
view = protoView.instantiate(evalContext, null);
|
view = protoView.instantiate(evalContext, new Injector([]), null);
|
||||||
changeDetector = new ChangeDetector(view.watchGroup);
|
changeDetector = new ChangeDetector(view.watchGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,7 +176,7 @@ export function main() {
|
||||||
'boundprop3': 'prop3'
|
'boundprop3': 'prop3'
|
||||||
});
|
});
|
||||||
var directives = [SomeDecoratorDirectiveWithBinding, SomeTemplateDirectiveWithBinding, SomeComponentDirectiveWithBinding];
|
var directives = [SomeDecoratorDirectiveWithBinding, SomeTemplateDirectiveWithBinding, SomeComponentDirectiveWithBinding];
|
||||||
var protoElementInjector = new ProtoElementInjector(null, 0, directives);
|
var protoElementInjector = new ProtoElementInjector(null, 0, directives, true);
|
||||||
var pipeline = createPipeline({
|
var pipeline = createPipeline({
|
||||||
propertyBindings: propertyBindings,
|
propertyBindings: propertyBindings,
|
||||||
directives: directives,
|
directives: directives,
|
||||||
|
@ -182,6 +184,8 @@ export function main() {
|
||||||
});
|
});
|
||||||
var results = pipeline.process(createElement('<div viewroot prop-binding directives></div>'));
|
var results = pipeline.process(createElement('<div viewroot prop-binding directives></div>'));
|
||||||
var pv = results[0].inheritedProtoView;
|
var pv = results[0].inheritedProtoView;
|
||||||
|
results[0].inheritedElementBinder.nestedProtoView = new ProtoView(
|
||||||
|
createElement('<div></div>'), new ProtoWatchGroup());
|
||||||
|
|
||||||
instantiateView(pv);
|
instantiateView(pv);
|
||||||
evalContext.prop1 = 'a';
|
evalContext.prop1 = 'a';
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
import {describe, xit, it, expect, beforeEach} from 'test_lib/test_lib';
|
import {describe, xit, it, expect, beforeEach, ddescribe, iit} from 'test_lib/test_lib';
|
||||||
import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from 'core/compiler/view';
|
import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from 'core/compiler/view';
|
||||||
import {Record, ProtoRecord} from 'change_detection/record';
|
|
||||||
import {ProtoElementInjector, ElementInjector} from 'core/compiler/element_injector';
|
import {ProtoElementInjector, ElementInjector} from 'core/compiler/element_injector';
|
||||||
|
import {Reflector} from 'core/compiler/reflector';
|
||||||
|
import {Component} from 'core/annotations/component';
|
||||||
|
import {Decorator} from 'core/annotations/decorator';
|
||||||
import {ProtoWatchGroup} from 'change_detection/watch_group';
|
import {ProtoWatchGroup} from 'change_detection/watch_group';
|
||||||
import {ChangeDetector} from 'change_detection/change_detector';
|
import {ChangeDetector} from 'change_detection/change_detector';
|
||||||
|
import {TemplateConfig} from 'core/annotations/template_config';
|
||||||
import {Parser} from 'change_detection/parser/parser';
|
import {Parser} from 'change_detection/parser/parser';
|
||||||
import {ClosureMap} from 'change_detection/parser/closure_map';
|
import {ClosureMap} from 'change_detection/parser/closure_map';
|
||||||
import {Lexer} from 'change_detection/parser/lexer';
|
import {Lexer} from 'change_detection/parser/lexer';
|
||||||
import {DOM, Element} from 'facade/dom';
|
import {DOM, Element} from 'facade/dom';
|
||||||
import {FIELD} from 'facade/lang';
|
import {FIELD} from 'facade/lang';
|
||||||
|
import {Injector} from 'di/di';
|
||||||
|
import {View} from 'core/compiler/view';
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('view', function() {
|
describe('view', function() {
|
||||||
|
@ -21,120 +26,185 @@ export function main() {
|
||||||
|
|
||||||
describe('ProtoView.instantiate', function() {
|
describe('ProtoView.instantiate', function() {
|
||||||
|
|
||||||
describe('collect root nodes', () => {
|
function createCollectDomNodesTestCases(useTemplateElement:boolean) {
|
||||||
|
|
||||||
it('should use the ProtoView element if it is no TemplateElement', () => {
|
function templateAwareCreateElement(html) {
|
||||||
var pv = new ProtoView(createElement('<div id="1"></div>'), new ProtoWatchGroup());
|
return createElement(useTemplateElement ? `<template>${html}</template>` : html);
|
||||||
var view = pv.instantiate(null, null);
|
|
||||||
expect(view.nodes.length).toBe(1);
|
|
||||||
expect(view.nodes[0].getAttribute('id')).toEqual('1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use the ProtoView elements children if it is a TemplateElement', () => {
|
|
||||||
var pv = new ProtoView(createElement('<template><div id="1"></div></template>'),
|
|
||||||
new ProtoWatchGroup());
|
|
||||||
var view = pv.instantiate(null, null);
|
|
||||||
expect(view.nodes.length).toBe(1);
|
|
||||||
expect(view.nodes[0].getAttribute('id')).toEqual('1');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('collect elements with property bindings', () => {
|
|
||||||
|
|
||||||
it('should collect property bindings on the root element if it has the ng-binding class', () => {
|
|
||||||
var pv = new ProtoView(createElement('<div [prop]="a" class="ng-binding"></div>'), new ProtoWatchGroup());
|
|
||||||
pv.bindElement(null);
|
|
||||||
pv.bindElementProperty('prop', parser.parseBinding('a'));
|
|
||||||
|
|
||||||
var view = pv.instantiate(null, null);
|
|
||||||
expect(view.bindElements.length).toEqual(1);
|
|
||||||
expect(view.bindElements[0]).toBe(view.nodes[0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should collect property bindings on child elements with ng-binding class', () => {
|
|
||||||
var pv = new ProtoView(createElement('<div><span></span><span class="ng-binding"></span></div>'),
|
|
||||||
new ProtoWatchGroup());
|
|
||||||
pv.bindElement(null);
|
|
||||||
pv.bindElementProperty('a', parser.parseBinding('b'));
|
|
||||||
|
|
||||||
var view = pv.instantiate(null, null);
|
|
||||||
expect(view.bindElements.length).toEqual(1);
|
|
||||||
expect(view.bindElements[0]).toBe(view.nodes[0].childNodes[1]);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('collect text nodes with bindings', () => {
|
|
||||||
|
|
||||||
it('should collect text nodes under the root element', () => {
|
|
||||||
var pv = new ProtoView(createElement('<div class="ng-binding">{{}}<span></span>{{}}</div>'), new ProtoWatchGroup());
|
|
||||||
pv.bindElement(null);
|
|
||||||
pv.bindTextNode(0, parser.parseBinding('a'));
|
|
||||||
pv.bindTextNode(2, parser.parseBinding('b'));
|
|
||||||
|
|
||||||
var view = pv.instantiate(null, null);
|
|
||||||
expect(view.textNodes.length).toEqual(2);
|
|
||||||
expect(view.textNodes[0]).toBe(view.nodes[0].childNodes[0]);
|
|
||||||
expect(view.textNodes[1]).toBe(view.nodes[0].childNodes[2]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should collect text nodes with bindings on child elements with ng-binding class', () => {
|
|
||||||
var pv = new ProtoView(createElement('<div><span> </span><span class="ng-binding">{{}}</span></div>'),
|
|
||||||
new ProtoWatchGroup());
|
|
||||||
pv.bindElement(null);
|
|
||||||
pv.bindTextNode(0, parser.parseBinding('b'));
|
|
||||||
|
|
||||||
var view = pv.instantiate(null, null);
|
|
||||||
expect(view.textNodes.length).toEqual(1);
|
|
||||||
expect(view.textNodes[0]).toBe(view.nodes[0].childNodes[1].childNodes[0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('react to watch group changes', function() {
|
|
||||||
var ctx, view, cd;
|
|
||||||
|
|
||||||
function createView(protoView) {
|
|
||||||
ctx = new MyEvaluationContext();
|
|
||||||
view = protoView.instantiate(ctx, null);
|
|
||||||
cd = new ChangeDetector(view.watchGroup);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should consume text node changes', () => {
|
it('should collect the root node in the ProtoView element', () => {
|
||||||
var pv = new ProtoView(createElement('<div class="ng-binding">{{}}</div>'),
|
var pv = new ProtoView(templateAwareCreateElement('<div id="1"></div>'), new ProtoWatchGroup());
|
||||||
new ProtoWatchGroup());
|
var view = pv.instantiate(null, null, null);
|
||||||
pv.bindElement(null);
|
expect(view.nodes.length).toBe(1);
|
||||||
pv.bindTextNode(0, parser.parseBinding('foo'));
|
expect(view.nodes[0].getAttribute('id')).toEqual('1');
|
||||||
createView(pv);
|
|
||||||
|
|
||||||
ctx.foo = 'buz';
|
|
||||||
cd.detectChanges();
|
|
||||||
expect(view.textNodes[0].nodeValue).toEqual('buz');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should consume element binding changes', () => {
|
describe('collect elements with property bindings', () => {
|
||||||
var pv = new ProtoView(createElement('<div class="ng-binding"></div>'),
|
|
||||||
new ProtoWatchGroup());
|
it('should collect property bindings on the root element if it has the ng-binding class', () => {
|
||||||
pv.bindElement(null);
|
var pv = new ProtoView(templateAwareCreateElement('<div [prop]="a" class="ng-binding"></div>'), new ProtoWatchGroup());
|
||||||
pv.bindElementProperty('id', parser.parseBinding('foo'));
|
pv.bindElement(null);
|
||||||
createView(pv);
|
pv.bindElementProperty('prop', parser.parseBinding('a'));
|
||||||
|
|
||||||
|
var view = pv.instantiate(null, null, null);
|
||||||
|
expect(view.bindElements.length).toEqual(1);
|
||||||
|
expect(view.bindElements[0]).toBe(view.nodes[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should collect property bindings on child elements with ng-binding class', () => {
|
||||||
|
var pv = new ProtoView(templateAwareCreateElement('<div><span></span><span class="ng-binding"></span></div>'),
|
||||||
|
new ProtoWatchGroup());
|
||||||
|
pv.bindElement(null);
|
||||||
|
pv.bindElementProperty('a', parser.parseBinding('b'));
|
||||||
|
|
||||||
|
var view = pv.instantiate(null, null, null);
|
||||||
|
expect(view.bindElements.length).toEqual(1);
|
||||||
|
expect(view.bindElements[0]).toBe(view.nodes[0].childNodes[1]);
|
||||||
|
});
|
||||||
|
|
||||||
ctx.foo = 'buz';
|
|
||||||
cd.detectChanges();
|
|
||||||
expect(view.bindElements[0].id).toEqual('buz');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should consume directive watch expression change.', () => {
|
describe('collect text nodes with bindings', () => {
|
||||||
var pv = new ProtoView(createElement('<div class="ng-binding"></div>'),
|
|
||||||
new ProtoWatchGroup());
|
|
||||||
pv.bindElement(new ProtoElementInjector(null, 0, [Directive]));
|
|
||||||
pv.bindDirectiveProperty( 0, parser.parseBinding('foo'), 'prop', closureMap.setter('prop'));
|
|
||||||
createView(pv);
|
|
||||||
|
|
||||||
ctx.foo = 'buz';
|
it('should collect text nodes under the root element', () => {
|
||||||
cd.detectChanges();
|
var pv = new ProtoView(templateAwareCreateElement('<div class="ng-binding">{{}}<span></span>{{}}</div>'), new ProtoWatchGroup());
|
||||||
expect(view.elementInjectors[0].get(Directive).prop).toEqual('buz');
|
pv.bindElement(null);
|
||||||
|
pv.bindTextNode(0, parser.parseBinding('a'));
|
||||||
|
pv.bindTextNode(2, parser.parseBinding('b'));
|
||||||
|
|
||||||
|
var view = pv.instantiate(null, null, null);
|
||||||
|
expect(view.textNodes.length).toEqual(2);
|
||||||
|
expect(view.textNodes[0]).toBe(view.nodes[0].childNodes[0]);
|
||||||
|
expect(view.textNodes[1]).toBe(view.nodes[0].childNodes[2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should collect text nodes with bindings on child elements with ng-binding class', () => {
|
||||||
|
var pv = new ProtoView(templateAwareCreateElement('<div><span> </span><span class="ng-binding">{{}}</span></div>'),
|
||||||
|
new ProtoWatchGroup());
|
||||||
|
pv.bindElement(null);
|
||||||
|
pv.bindTextNode(0, parser.parseBinding('b'));
|
||||||
|
|
||||||
|
var view = pv.instantiate(null, null, null);
|
||||||
|
expect(view.textNodes.length).toEqual(1);
|
||||||
|
expect(view.textNodes[0]).toBe(view.nodes[0].childNodes[1].childNodes[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('collect dom nodes with a regular element as root', () => {
|
||||||
|
createCollectDomNodesTestCases(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('collect dom nodes with a template element as root', () => {
|
||||||
|
createCollectDomNodesTestCases(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create ElementInjectors', () => {
|
||||||
|
it('should use the directives of the ProtoElementInjector', () => {
|
||||||
|
var pv = new ProtoView(createElement('<div class="ng-binding"></div>'), new ProtoWatchGroup());
|
||||||
|
pv.bindElement(new ProtoElementInjector(null, 1, [SomeDirective]));
|
||||||
|
|
||||||
|
var view = pv.instantiate(null, null, null);
|
||||||
|
expect(view.elementInjectors.length).toBe(1);
|
||||||
|
expect(view.elementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the correct parent', () => {
|
||||||
|
var pv = new ProtoView(createElement('<div class="ng-binding"><span class="ng-binding"></span></div>'),
|
||||||
|
new ProtoWatchGroup());
|
||||||
|
var protoParent = new ProtoElementInjector(null, 0, [SomeDirective]);
|
||||||
|
pv.bindElement(protoParent);
|
||||||
|
pv.bindElement(new ProtoElementInjector(protoParent, 1, [AnotherDirective]));
|
||||||
|
|
||||||
|
var view = pv.instantiate(null, null, null);
|
||||||
|
expect(view.elementInjectors.length).toBe(2);
|
||||||
|
expect(view.elementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true);
|
||||||
|
expect(view.elementInjectors[1].parent).toBe(view.elementInjectors[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('collect root element injectors', () => {
|
||||||
|
|
||||||
|
it('should collect a single root element injector', () => {
|
||||||
|
var pv = new ProtoView(createElement('<div class="ng-binding"><span class="ng-binding"></span></div>'),
|
||||||
|
new ProtoWatchGroup());
|
||||||
|
var protoParent = new ProtoElementInjector(null, 0, [SomeDirective]);
|
||||||
|
pv.bindElement(protoParent);
|
||||||
|
pv.bindElement(new ProtoElementInjector(protoParent, 1, [AnotherDirective]));
|
||||||
|
|
||||||
|
var view = pv.instantiate(null, null, null);
|
||||||
|
expect(view.rootElementInjectors.length).toBe(1);
|
||||||
|
expect(view.rootElementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should collect multiple root element injectors', () => {
|
||||||
|
var pv = new ProtoView(createElement('<div><span class="ng-binding"></span><span class="ng-binding"></span></div>'),
|
||||||
|
new ProtoWatchGroup());
|
||||||
|
pv.bindElement(new ProtoElementInjector(null, 1, [SomeDirective]));
|
||||||
|
pv.bindElement(new ProtoElementInjector(null, 2, [AnotherDirective]));
|
||||||
|
|
||||||
|
var view = pv.instantiate(null, null, null);
|
||||||
|
expect(view.rootElementInjectors.length).toBe(2)
|
||||||
|
expect(view.rootElementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true);
|
||||||
|
expect(view.rootElementInjectors[1].get(AnotherDirective) instanceof AnotherDirective).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('recurse over child component views', () => {
|
||||||
|
var view, ctx;
|
||||||
|
|
||||||
|
function createComponentWithSubPV(subProtoView) {
|
||||||
|
var pv = new ProtoView(createElement('<cmp class="ng-binding"></cmp>'), new ProtoWatchGroup());
|
||||||
|
var binder = pv.bindElement(new ProtoElementInjector(null, 0, [SomeComponent], true));
|
||||||
|
binder.componentDirective = new Reflector().annotatedType(SomeComponent);
|
||||||
|
binder.nestedProtoView = subProtoView;
|
||||||
|
return pv;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNestedView(protoView) {
|
||||||
|
ctx = new MyEvaluationContext();
|
||||||
|
return protoView.instantiate(ctx, new Injector([]), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should create shadow dom', () => {
|
||||||
|
var subpv = new ProtoView(createElement('<span>hello shadow dom</span>'), new ProtoWatchGroup());
|
||||||
|
var pv = createComponentWithSubPV(subpv);
|
||||||
|
|
||||||
|
var view = createNestedView(pv);
|
||||||
|
|
||||||
|
expect(view.nodes[0].shadowRoot.childNodes[0].childNodes[0].nodeValue).toEqual('hello shadow dom');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should expose component services to the component', () => {
|
||||||
|
var subpv = new ProtoView(createElement('<span></span>'), new ProtoWatchGroup());
|
||||||
|
var pv = createComponentWithSubPV(subpv);
|
||||||
|
|
||||||
|
var view = createNestedView(pv);
|
||||||
|
|
||||||
|
var comp = view.rootElementInjectors[0].get(SomeComponent);
|
||||||
|
expect(comp.service).toBeAnInstanceOf(SomeService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should expose component services and component instance to directives in the shadow Dom',
|
||||||
|
() => {
|
||||||
|
var subpv = new ProtoView(
|
||||||
|
createElement('<div dec class="ng-binding">hello shadow dom</div>'), new ProtoWatchGroup());
|
||||||
|
var subBinder = subpv.bindElement(
|
||||||
|
new ProtoElementInjector(null, 0, [ServiceDependentDecorator]));
|
||||||
|
var pv = createComponentWithSubPV(subpv);
|
||||||
|
|
||||||
|
var view = createNestedView(pv);
|
||||||
|
|
||||||
|
var subView = view.childViews[0];
|
||||||
|
var subInj = subView.rootElementInjectors[0];
|
||||||
|
var subDecorator = subInj.get(ServiceDependentDecorator);
|
||||||
|
var comp = view.rootElementInjectors[0].get(SomeComponent);
|
||||||
|
|
||||||
|
expect(subDecorator).toBeAnInstanceOf(ServiceDependentDecorator);
|
||||||
|
expect(subDecorator.service).toBe(comp.service);
|
||||||
|
expect(subDecorator.component).toBe(comp);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -143,7 +213,7 @@ export function main() {
|
||||||
|
|
||||||
function createView(protoView) {
|
function createView(protoView) {
|
||||||
ctx = new MyEvaluationContext();
|
ctx = new MyEvaluationContext();
|
||||||
view = protoView.instantiate(ctx, null);
|
view = protoView.instantiate(ctx, null, null);
|
||||||
cd = new ChangeDetector(view.watchGroup);
|
cd = new ChangeDetector(view.watchGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,26 +244,48 @@ export function main() {
|
||||||
it('should consume directive watch expression change.', () => {
|
it('should consume directive watch expression change.', () => {
|
||||||
var pv = new ProtoView(createElement('<div class="ng-binding"></div>'),
|
var pv = new ProtoView(createElement('<div class="ng-binding"></div>'),
|
||||||
new ProtoWatchGroup());
|
new ProtoWatchGroup());
|
||||||
pv.bindElement(new ProtoElementInjector(null, 0, [Directive]));
|
pv.bindElement(new ProtoElementInjector(null, 0, [SomeDirective]));
|
||||||
pv.bindDirectiveProperty( 0, parser.parseBinding('foo'), 'prop', closureMap.setter('prop'));
|
pv.bindDirectiveProperty( 0, parser.parseBinding('foo'), 'prop', closureMap.setter('prop'));
|
||||||
createView(pv);
|
createView(pv);
|
||||||
|
|
||||||
ctx.foo = 'buz';
|
ctx.foo = 'buz';
|
||||||
cd.detectChanges();
|
cd.detectChanges();
|
||||||
expect(view.elementInjectors[0].get(Directive).prop).toEqual('buz');
|
expect(view.elementInjectors[0].get(SomeDirective).prop).toEqual('buz');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class Directive {
|
class SomeDirective {
|
||||||
@FIELD('prop')
|
@FIELD('prop')
|
||||||
constructor() {
|
constructor() {
|
||||||
this.prop = 'foo';
|
this.prop = 'foo';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SomeService {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
componentServices: [SomeService]
|
||||||
|
})
|
||||||
|
class SomeComponent {
|
||||||
|
constructor(service: SomeService) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decorator({
|
||||||
|
selector: '[dec]'
|
||||||
|
})
|
||||||
|
class ServiceDependentDecorator {
|
||||||
|
constructor(component: SomeComponent, service: SomeService) {
|
||||||
|
this.component = component;
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class AnotherDirective {
|
class AnotherDirective {
|
||||||
@FIELD('prop')
|
@FIELD('prop')
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
Loading…
Reference in New Issue