feat(view): add onChange implementation to view.

This commit is contained in:
Rado Kirov 2014-10-27 23:16:31 -07:00
parent e1c84e02f8
commit b0c9d05ea7
7 changed files with 162 additions and 52 deletions

View File

@ -8,7 +8,7 @@ export function run () {
var appInjector = new Injector([]);
var bindings = [A, B, C];
var proto = new ProtoElementInjector(null, bindings, []);
var proto = new ProtoElementInjector(null, bindings, [], false);
for (var i = 0; i < ITERATIONS; ++i) {
var ei = proto.instantiate({view:null});
ei.instantiateDirectives(appInjector);
@ -31,4 +31,4 @@ class C {
constructor(a:A, b:B) {
count++;
}
}
}

View File

@ -16,7 +16,7 @@ export function run () {
], false)];
var proto = new ProtoElementInjector(null, bindings, []);
var proto = new ProtoElementInjector(null, bindings, [], false);
for (var i = 0; i < ITERATIONS; ++i) {
var ei = proto.instantiate({view:null});
ei.instantiateDirectives(appInjector);

View File

@ -8,7 +8,7 @@ export function run () {
var appInjector = new Injector([]);
var bindings = [A, B, C];
var proto = new ProtoElementInjector(null, bindings, []);
var proto = new ProtoElementInjector(null, bindings, [], false);
var ei = proto.instantiate({view:null});
for (var i = 0; i < ITERATIONS; ++i) {

View File

@ -118,7 +118,6 @@ export class ProtoElementInjector extends TreeNode {
query_keyId1:int;
textNodes:List<int>;
hasProperties:boolean;
events:Map<string, Expression>;
elementInjector:ElementInjector;
@ -144,8 +143,10 @@ export class ProtoElementInjector extends TreeNode {
@FIELD('_key7:int')
@FIELD('_key8:int')
@FIELD('_key9:int')
@FIELD('textNodes:List<int>')
constructor(parent:ProtoElementInjector, bindings:List, textNodes:List) {
@FIELD('textNodeIndices:List<int>')
@FIELD('hasElementPropertyBindings:bool')
constructor(parent:ProtoElementInjector, bindings:List, textNodeIndices:List,
hasElementPropertyBindings:boolean) {
super(parent);
this._elementInjector = null;
@ -177,10 +178,8 @@ export class ProtoElementInjector extends TreeNode {
throw 'Maximum number of directives per element has been reached.';
}
this.textNodes = textNodes;
// dummy fields to make analyzer happy
this.hasProperties = false;
this.textNodeIndices = textNodeIndices;
this.hasElementPropertyBindings = hasElementPropertyBindings;
}
instantiate({view}):ElementInjector {
@ -418,5 +417,28 @@ export class ElementInjector extends TreeNode {
if (p._keyId9 === keyId) return this._obj9;
return _undefined;
}
getAtIndex(index:int) {
if (index == 0) return this._obj0;
if (index == 1) return this._obj1;
if (index == 2) return this._obj2;
if (index == 3) return this._obj3;
if (index == 4) return this._obj4;
if (index == 5) return this._obj5;
if (index == 6) return this._obj6;
if (index == 7) return this._obj7;
if (index == 8) return this._obj8;
if (index == 9) return this._obj9;
throw new OutOfBoundsAccess(index);
}
}
class OutOfBoundsAccess extends Error {
constructor(index) {
this.message = `Index ${index} is out-of-bounds.`;
}
toString() {
return this.message;
}
}

View File

@ -23,14 +23,15 @@ export class View {
/// to keep track of the nodes.
@FIELD('final nodes:List<Node>')
@FIELD('final onChangeDispatcher:OnChangeDispatcher')
constructor(fragment:DocumentFragment, elementInjector:List, rootElementInjectors:List, textNodes:List) {
constructor(fragment:DocumentFragment, elementInjector:List,
rootElementInjectors:List, textNodes:List, bindElements:List) {
this.fragment = fragment;
this.nodes = ListWrapper.clone(fragment.childNodes);
this.elementInjectors = elementInjector;
this.rootElementInjectors = rootElementInjectors;
this.onChangeDispatcher = null;
this.textNodes = textNodes;
this.bindElements = null;
this.bindElements = bindElements;
}
onRecordChange(record:Record, target) {
@ -39,7 +40,7 @@ export class View {
// we know that it is DirectivePropertyMemento
var directiveMemento:DirectivePropertyMemento = target;
directiveMemento.invoke(record, this.elementInjectors);
} else if (target instanceof ElementPropertyMemento) {
} else if (target instanceof ElementPropertyMemento) {
var elementMemento:ElementPropertyMemento = target;
elementMemento.invoke(record, this.bindElements);
} else {
@ -83,8 +84,10 @@ export class ProtoView {
var elementInjectors = ProtoView._createElementInjectors(elements, protos);
var rootElementInjectors = ProtoView._rootElementInjectors(elementInjectors);
var textNodes = ProtoView._textNodes(elements, protos);
var bindElements = ProtoView._bindElements(elements, protos);
return new View(fragment, elementInjectors, rootElementInjectors, textNodes);
return new View(fragment, elementInjectors, rootElementInjectors, textNodes,
bindElements);
}
static _createElementInjectors(elements, protos) {
@ -92,7 +95,11 @@ export class ProtoView {
for (var i = 0; i < protos.length; ++i) {
injectors[i] = ProtoView._createElementInjector(elements[i], protos[i]);
}
ListWrapper.forEach(protos, p => p.clearElementInjector());
// Cannot be rolled into loop above, because parentInjector pointers need
// to be set on the children.
for (var i = 0; i < protos.length; ++i) {
protos[i].clearElementInjector();
}
return injectors;
}
@ -108,16 +115,26 @@ export class ProtoView {
static _textNodes(elements, protos) {
var textNodes = [];
for (var i = 0; i < protos.length; ++i) {
ProtoView._collectTextNodes(textNodes, elements[i], protos[i]);
ProtoView._collectTextNodes(textNodes, elements[i],
protos[i].textNodeIndices);
}
return textNodes;
}
static _collectTextNodes(allTextNodes, element, proto) {
static _bindElements(elements, protos):List<Element> {
var bindElements = [];
for (var i = 0; i < protos.length; ++i) {
if (protos[i].hasElementPropertyBindings) ListWrapper.push(
bindElements, elements[i]);
}
return bindElements;
}
static _collectTextNodes(allTextNodes, element, indices) {
var childNodes = DOM.childNodes(element);
ListWrapper.forEach(proto.textNodes, (i) => {
ListWrapper.push(allTextNodes, childNodes[i]);
});
for (var i = 0; i < indices.length; ++i) {
ListWrapper.push(allTextNodes, childNodes[indices[i]]);
}
}
}
@ -129,8 +146,8 @@ export class ElementPropertyMemento {
this._propertyName = propertyName;
}
invoke(record:Record, elementInjectors:List<Element>) {
var element:Element = elementInjectors[this._elementIndex];
invoke(record:Record, bindElements:List<Element>) {
var element:Element = bindElements[this._elementIndex];
DOM.setProperty(element, this._propertyName, record.currentValue);
}
}
@ -138,14 +155,13 @@ export class ElementPropertyMemento {
export class DirectivePropertyMemento {
@FIELD('final _elementInjectorIndex:int')
@FIELD('final _directiveIndex:int')
@FIELD('final _setterName:String')
@FIELD('final _setterName:string')
@FIELD('final _setter:SetterFn')
constructor(
elementInjectorIndex:number,
directiveIndex:number,
setterName:String,
setter:SetterFn)
{
setterName:string,
setter:SetterFn) {
this._elementInjectorIndex = elementInjectorIndex;
this._directiveIndex = directiveIndex;
this._setterName = setterName;
@ -154,7 +170,7 @@ export class DirectivePropertyMemento {
invoke(record:Record, elementInjectors:List<ElementInjector>) {
var elementInjector:ElementInjector = elementInjectors[this._elementInjectorIndex];
var directive = elementInjectors[this._directiveIndex];
var directive = elementInjector.getAtIndex(this._directiveIndex);
this._setter(directive, record.currentValue);
}
}

View File

@ -62,7 +62,7 @@ export function main() {
if (isBlank(appInjector)) appInjector = new Injector([]);
if (isBlank(props)) props = {};
var proto = new ProtoElementInjector(null, bindings, []);
var proto = new ProtoElementInjector(null, bindings, [], false);
var inj = proto.instantiate({view: props["view"]});
inj.instantiateDirectives(appInjector);
return inj;
@ -71,11 +71,12 @@ export function main() {
function parentChildInjectors(parentBindings, childBindings) {
var inj = new Injector([]);
var protoParent = new ProtoElementInjector(null, parentBindings, []);
var protoParent = new ProtoElementInjector(null, parentBindings, [], false);
var parent = protoParent.instantiate({view: null});
parent.instantiateDirectives(inj);
var protoChild = new ProtoElementInjector(protoParent, childBindings, []);
var protoChild = new ProtoElementInjector(protoParent, childBindings, [],
false);
var child = protoChild.instantiate({view: null});
child.instantiateDirectives(inj);
@ -85,9 +86,9 @@ export function main() {
describe("ElementInjector", function () {
describe("proto injectors", function () {
it("should construct a proto tree", function () {
var p = new ProtoElementInjector(null, [], []);
var c1 = new ProtoElementInjector(p, [], []);
var c2 = new ProtoElementInjector(p, [], []);
var p = new ProtoElementInjector(null, [], [], false);
var c1 = new ProtoElementInjector(p, [], [], false);
var c2 = new ProtoElementInjector(p, [], [], false);
expect(humanize(p, [
[p, 'parent'],
@ -99,9 +100,9 @@ export function main() {
describe("instantiate", function () {
it("should create an element injector", function () {
var protoParent = new ProtoElementInjector(null, [], []);
var protoChild1 = new ProtoElementInjector(protoParent, [], []);
var protoChild2 = new ProtoElementInjector(protoParent, [], []);
var protoParent = new ProtoElementInjector(null, [], [], false);
var protoChild1 = new ProtoElementInjector(protoParent, [], [], false);
var protoChild2 = new ProtoElementInjector(protoParent, [], [], false);
var p = protoParent.instantiate({view: null});
var c1 = protoChild1.instantiate({view: null});
@ -117,12 +118,12 @@ export function main() {
describe("hasBindings", function () {
it("should be true when there are bindings", function () {
var p = new ProtoElementInjector(null, [Directive], []);
var p = new ProtoElementInjector(null, [Directive], [], false);
expect(p.hasBindings).toBeTruthy();
});
it("should be false otherwise", function () {
var p = new ProtoElementInjector(null, [], []);
var p = new ProtoElementInjector(null, [], [], false);
expect(p.hasBindings).toBeFalsy();
});
});
@ -196,6 +197,15 @@ export function main() {
var inj = injector([bind(Directive).toClass(Directive)]);
expect(inj.get(Directive)).toBeAnInstanceOf(Directive);
});
it("should allow for direct access using getAtIndex", function () {
var inj = injector([bind(Directive).toClass(Directive)]);
expect(inj.getAtIndex(0)).toBeAnInstanceOf(Directive);
expect(() => inj.getAtIndex(-1)).toThrowError(
'Index -1 is out-of-bounds.');
expect(() => inj.getAtIndex(10)).toThrowError(
'Index 10 is out-of-bounds.');
});
});
describe("special objects", function () {
@ -207,4 +217,4 @@ export function main() {
});
});
});
}
}

View File

@ -1,28 +1,36 @@
import {describe, xit, it, expect} from 'test_lib/test_lib';
import {ProtoView} from 'core/compiler/view';
import {describe, xit, it, expect, beforeEach} from 'test_lib/test_lib';
import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from 'core/compiler/view';
import {Record} from 'change_detection/record';
import {ProtoElementInjector, ElementInjector} from 'core/compiler/element_injector';
import {DOM, Element} from 'facade/dom';
import {FIELD} from 'facade/lang';
class Directive {
@FIELD('prop')
constructor() {
this.prop = 'foo';
}
}
export function main() {
describe('view', function() {
describe('ProtoView', function() {
it('should create view instance and locate basic parts', function() {
var template = DOM.createTemplate(
var tempalteWithThreeTypesOfBindings =
'<section class="ng-binding">' +
'Hello {}!' +
'<div directive class="ng-binding">' +
'<span class="ng-binding">don\'t show me</span>' +
'<span class="ng-binding" [hidden]="exp">don\'t show me</span>' +
'</div>' +
'</section>');
'</section>';
describe('ProtoView', function() {
it('should create view instance and locate basic parts', function() {
var template = DOM.createTemplate(tempalteWithThreeTypesOfBindings);
var diBindings = [];
var sectionPI = new ProtoElementInjector(null, [], [0]);
var divPI = new ProtoElementInjector(sectionPI, [Directive], []);
var spanPI = new ProtoElementInjector(divPI, [], []);
var sectionPI = new ProtoElementInjector(null, [], [0], false);
var divPI = new ProtoElementInjector(sectionPI, [Directive], [], false);
var spanPI = new ProtoElementInjector(divPI, [], [], true);
var protoElementInjectors = [sectionPI, divPI, spanPI];
var protoWatchGroup = null;
@ -42,6 +50,7 @@ export function main() {
expect(view.elementInjectors[2]).toBeNull();
expect(view.textNodes.length).toEqual(1);
expect(view.bindElements.length).toEqual(1);
expect(view.textNodes[0].nodeValue).toEqual('Hello {}!');
});
@ -51,14 +60,67 @@ export function main() {
'<div directive class="ng-binding"></div>' +
'</section>');
var sectionPI = new ProtoElementInjector(null, [Directive], []);
var divPI = new ProtoElementInjector(sectionPI, [Directive], []);
var sectionPI = new ProtoElementInjector(null, [Directive], [], false);
var divPI = new ProtoElementInjector(sectionPI, [Directive], [], false);
var pv = new ProtoView(template, [], [sectionPI, divPI], null, false);
var view = pv.instantiate();
expect(view.rootElementInjectors.length).toEqual(1);
});
describe('react to watch group changes', function() {
var view;
beforeEach(() => {
var template = DOM.createTemplate(tempalteWithThreeTypesOfBindings);
var diBindings = [];
var sectionPI = new ProtoElementInjector(null, [], [0], false);
var divPI = new ProtoElementInjector(sectionPI, [Directive], [], false);
var spanPI = new ProtoElementInjector(divPI, [], [], true);
var protoElementInjectors = [sectionPI, divPI, spanPI];
var protoWatchGroup = null;
var hasSingleRoot = false;
var pv = new ProtoView(template, diBindings, protoElementInjectors,
protoWatchGroup, hasSingleRoot);
view = pv.instantiate();
});
it('should consume text node changes', () => {
var record = new Record(null, null);
record.currentValue = 'Hello World!';
view.onRecordChange(record , 0);
expect(view.textNodes[0].nodeValue).toEqual('Hello World!');
});
it('should consume element binding changes', () => {
var elementWithBinding = view.bindElements[0];
expect(elementWithBinding.hidden).toEqual(false);
var record = new Record(null, null);
var memento = new ElementPropertyMemento(0, 'hidden');
record.currentValue = true;
view.onRecordChange(record, memento);
expect(elementWithBinding.hidden).toEqual(true);
});
it('should consume directive watch expression change.', () => {
var elInj = view.elementInjectors[1];
// TODO(rado): hook-up instantiateDirectives in implementation and
// remove from here.
elInj.instantiateDirectives(null);
expect(elInj.get(Directive).prop).toEqual('foo');
var record = new Record(null, null);
var memento = new DirectivePropertyMemento(1, 0, 'prop',
(o, v) => o.prop = v);
record.currentValue = 'bar';
view.onRecordChange(record, memento);
expect(elInj.get(Directive).prop).toEqual('bar');
});
});
});
});
}