feat(view): hook watch group instantiation in the view.

This commit is contained in:
Rado Kirov 2014-10-29 15:41:50 -07:00
parent 01e6c7b70c
commit 91f50b67b7
3 changed files with 111 additions and 42 deletions

View File

@ -39,10 +39,12 @@ export class ProtoWatchGroup {
this.tailRecord = tail; this.tailRecord = tail;
} }
instantiate(dispatcher:WatchGroupDispatcher):WatchGroup { // TODO(rado): the type annotation should be dispatcher:WatchGroupDispatcher.
// but @Implements is not ready yet.
instantiate(dispatcher):WatchGroup {
var watchGroup:WatchGroup = new WatchGroup(this, dispatcher); var watchGroup:WatchGroup = new WatchGroup(this, dispatcher);
var tail:Record = null; var tail:Record = null;
var proto:ProtoRecord; var proto:ProtoRecord = null;
var prevRecord:Record = null; var prevRecord:Record = null;
if (this.headRecord !== null) { if (this.headRecord !== null) {
@ -70,7 +72,9 @@ export class WatchGroup {
@FIELD('final dispatcher:WatchGroupDispatcher') @FIELD('final dispatcher:WatchGroupDispatcher')
@FIELD('final headRecord:Record') @FIELD('final headRecord:Record')
@FIELD('final tailRecord:Record') @FIELD('final tailRecord:Record')
constructor(protoWatchGroup:ProtoWatchGroup, dispatcher:WatchGroupDispatcher) { // TODO(rado): the type annotation should be dispatcher:WatchGroupDispatcher.
// but @Implements is not ready yet.
constructor(protoWatchGroup:ProtoWatchGroup, dispatcher) {
this.protoWatchGroup = protoWatchGroup; this.protoWatchGroup = protoWatchGroup;
this.dispatcher = dispatcher; this.dispatcher = dispatcher;
this.headRecord = null; this.headRecord = null;

View File

@ -6,6 +6,7 @@ import {ProtoElementInjector, ElementInjector} from './element_injector';
import {SetterFn} from 'change_detection/facade'; import {SetterFn} from 'change_detection/facade';
import {FIELD, IMPLEMENTS, int, isPresent, isBlank} from 'facade/lang'; import {FIELD, IMPLEMENTS, int, isPresent, isBlank} from 'facade/lang';
import {List} from 'facade/collection'; import {List} from 'facade/collection';
import {Injector} from 'di/di';
/*** /***
* Const of making objects: http://jsperf.com/instantiate-size-of-object * Const of making objects: http://jsperf.com/instantiate-size-of-object
@ -24,7 +25,8 @@ export class View {
@FIELD('final nodes:List<Node>') @FIELD('final nodes:List<Node>')
@FIELD('final onChangeDispatcher:OnChangeDispatcher') @FIELD('final onChangeDispatcher:OnChangeDispatcher')
constructor(fragment:DocumentFragment, elementInjector:List, constructor(fragment:DocumentFragment, elementInjector:List,
rootElementInjectors:List, textNodes:List, bindElements:List) { rootElementInjectors:List, textNodes:List, bindElements:List,
protoWatchGroup:ProtoWatchGroup, context) {
this.fragment = fragment; this.fragment = fragment;
this.nodes = ListWrapper.clone(fragment.childNodes); this.nodes = ListWrapper.clone(fragment.childNodes);
this.elementInjectors = elementInjector; this.elementInjectors = elementInjector;
@ -32,6 +34,8 @@ export class View {
this.onChangeDispatcher = null; this.onChangeDispatcher = null;
this.textNodes = textNodes; this.textNodes = textNodes;
this.bindElements = bindElements; this.bindElements = bindElements;
this.watchGroup = protoWatchGroup.instantiate(this);
this.watchGroup.setContext(context);
} }
onRecordChange(record:Record, target) { onRecordChange(record:Record, target) {
@ -66,28 +70,29 @@ export class ProtoView {
this._template = template; this._template = template;
this._bindings = bindings; this._bindings = bindings;
this._protoElementInjectors = protoElementInjectors; this._protoElementInjectors = protoElementInjectors;
this._protoWatchGroup = protoWatchGroup;
// not implemented // not implemented
this._protoWatchGroup = protoWatchGroup;
this._useRootElement = useRootElement; this._useRootElement = useRootElement;
} }
instantiate():View { instantiate(context, appInjector:Injector):View {
var fragment = DOM.clone(this._template.content); var fragment = DOM.clone(this._template.content);
var elements = DOM.querySelectorAll(fragment, ".ng-binding"); var elements = DOM.querySelectorAll(fragment, ".ng-binding");
var protos = this._protoElementInjectors; var protos = this._protoElementInjectors;
/** /**
* TODO: vsavkin: benchmark * TODO: vsavkin: benchmark
* If this performs poorly, the three loops can be collapsed into one. * If this performs poorly, the five loops can be collapsed into one.
*/ */
var elementInjectors = ProtoView._createElementInjectors(elements, protos); var elementInjectors = ProtoView._createElementInjectors(elements, protos);
var rootElementInjectors = ProtoView._rootElementInjectors(elementInjectors); var rootElementInjectors = ProtoView._rootElementInjectors(elementInjectors);
var textNodes = ProtoView._textNodes(elements, protos); var textNodes = ProtoView._textNodes(elements, protos);
var bindElements = ProtoView._bindElements(elements, protos); var bindElements = ProtoView._bindElements(elements, protos);
ProtoView._instantiateDirectives(elementInjectors, appInjector);
return new View(fragment, elementInjectors, rootElementInjectors, textNodes, return new View(fragment, elementInjectors, rootElementInjectors, textNodes,
bindElements); bindElements, this._protoWatchGroup, context);
} }
static _createElementInjectors(elements, protos) { static _createElementInjectors(elements, protos) {
@ -103,6 +108,13 @@ export class ProtoView {
return injectors; return injectors;
} }
static _instantiateDirectives(
injectors:List<ElementInjectors>, appInjector:Injector) {
for (var i = 0; i < injectors.length; ++i) {
if (injectors[i] != null) injectors[i].instantiateDirectives(appInjector);
}
}
static _createElementInjector(element, proto) { static _createElementInjector(element, proto) {
//TODO: vsavkin: pass element to `proto.instantiate()` once https://github.com/angular/angular/pull/98 is merged //TODO: vsavkin: pass element to `proto.instantiate()` once https://github.com/angular/angular/pull/98 is merged
return proto.hasBindings ? proto.instantiate({view:null}) : null; return proto.hasBindings ? proto.instantiate({view:null}) : null;

View File

@ -2,8 +2,12 @@ import {describe, xit, it, expect, beforeEach} from 'test_lib/test_lib';
import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from 'core/compiler/view'; import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from 'core/compiler/view';
import {Record} from 'change_detection/record'; import {Record} from 'change_detection/record';
import {ProtoElementInjector, ElementInjector} from 'core/compiler/element_injector'; import {ProtoElementInjector, ElementInjector} from 'core/compiler/element_injector';
import {ProtoWatchGroup} from 'change_detection/watch_group';
import {ChangeDetector} from 'change_detection/change_detector';
import {DOM, Element} from 'facade/dom'; import {DOM, Element} from 'facade/dom';
import {FIELD} from 'facade/lang'; import {FIELD} from 'facade/lang';
import {ImplicitReceiver, FieldRead} from 'change_detection/parser/ast';
import {ClosureMap} from 'change_detection/parser/closure_map';
class Directive { class Directive {
@FIELD('prop') @FIELD('prop')
@ -13,32 +17,36 @@ class Directive {
} }
export function main() { export function main() {
var oneFieldAst = (fieldName) =>
new FieldRead(new ImplicitReceiver(), fieldName,
(new ClosureMap()).getter(fieldName));
describe('view', function() { describe('view', function() {
var tempalteWithThreeTypesOfBindings = var tempalteWithThreeTypesOfBindings =
'<section class="ng-binding">' + '<section class="ng-binding">' +
'Hello {}!' + 'Hello {}!' +
'<div directive class="ng-binding">' + '<div directive class="ng-binding">' +
'<span class="ng-binding" [hidden]="exp">don\'t show me</span>' + '<span class="ng-binding" [id]="exp">don\'t show me</span>' +
'</div>' + '</div>' +
'</section>'; '</section>';
function templateElInj() {
var sectionPI = new ProtoElementInjector(null, [], [0], false);
var divPI = new ProtoElementInjector(sectionPI, [Directive], [], false);
var spanPI = new ProtoElementInjector(divPI, [], [], true);
return [sectionPI, divPI, spanPI];
}
describe('ProtoView', function() { describe('ProtoView', function() {
it('should create view instance and locate basic parts', function() { it('should create view instance and locate basic parts', function() {
var template = DOM.createTemplate(tempalteWithThreeTypesOfBindings); var template = DOM.createTemplate(tempalteWithThreeTypesOfBindings);
var diBindings = []; 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 hasSingleRoot = false;
var pv = new ProtoView(template, diBindings, protoElementInjectors, var pv = new ProtoView(template, diBindings, templateElInj(),
protoWatchGroup, hasSingleRoot); new ProtoWatchGroup(), hasSingleRoot);
var view = pv.instantiate(); var view = pv.instantiate(null, null);
var section = DOM.firstChild(template.content); var section = DOM.firstChild(template.content);
@ -63,8 +71,9 @@ export function main() {
var sectionPI = new ProtoElementInjector(null, [Directive], [], false); var sectionPI = new ProtoElementInjector(null, [Directive], [], false);
var divPI = new ProtoElementInjector(sectionPI, [Directive], [], false); var divPI = new ProtoElementInjector(sectionPI, [Directive], [], false);
var pv = new ProtoView(template, [], [sectionPI, divPI], null, false); var pv = new ProtoView(template, [], [sectionPI, divPI],
var view = pv.instantiate(); new ProtoWatchGroup(), false);
var view = pv.instantiate(null, null);
expect(view.rootElementInjectors.length).toEqual(1); expect(view.rootElementInjectors.length).toEqual(1);
}); });
@ -73,20 +82,9 @@ export function main() {
var view; var view;
beforeEach(() => { beforeEach(() => {
var template = DOM.createTemplate(tempalteWithThreeTypesOfBindings); var template = DOM.createTemplate(tempalteWithThreeTypesOfBindings);
var pv = new ProtoView(template, [], templateElInj(),
var diBindings = []; new ProtoWatchGroup(), false);
view = pv.instantiate(null, null);
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', () => { it('should consume text node changes', () => {
@ -98,20 +96,17 @@ export function main() {
it('should consume element binding changes', () => { it('should consume element binding changes', () => {
var elementWithBinding = view.bindElements[0]; var elementWithBinding = view.bindElements[0];
expect(elementWithBinding.hidden).toEqual(false); expect(elementWithBinding.id).toEqual('');
var record = new Record(null, null); var record = new Record(null, null);
var memento = new ElementPropertyMemento(0, 'hidden'); var memento = new ElementPropertyMemento(0, 'id');
record.currentValue = true; record.currentValue = 'foo';
view.onRecordChange(record, memento); view.onRecordChange(record, memento);
expect(elementWithBinding.hidden).toEqual(true); expect(elementWithBinding.id).toEqual('foo');
}); });
it('should consume directive watch expression change.', () => { it('should consume directive watch expression change.', () => {
var elInj = view.elementInjectors[1]; 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'); expect(elInj.get(Directive).prop).toEqual('foo');
var record = new Record(null, null); var record = new Record(null, null);
var memento = new DirectivePropertyMemento(1, 0, 'prop', var memento = new DirectivePropertyMemento(1, 0, 'prop',
@ -121,6 +116,64 @@ export function main() {
expect(elInj.get(Directive).prop).toEqual('bar'); expect(elInj.get(Directive).prop).toEqual('bar');
}); });
}); });
describe('integration view update with change detector', () => {
var view, cd, ctx;
function setUp(memento) {
var template = DOM.createTemplate(tempalteWithThreeTypesOfBindings);
var protoWatchGroup = new ProtoWatchGroup();
protoWatchGroup.watch(oneFieldAst('foo'), memento);
var pv = new ProtoView(template, [], templateElInj(),
protoWatchGroup, false);
ctx = new MyEvaluationContext();
view = pv.instantiate(ctx, null);
cd = new ChangeDetector(view.watchGroup);
}
it('should consume text node changes', () => {
setUp(0);
ctx.foo = 'buz';
cd.detectChanges();
expect(view.textNodes[0].nodeValue).toEqual('buz');
});
it('should consume element binding changes', () => {
setUp(new ElementPropertyMemento(0, 'id'));
var elementWithBinding = view.bindElements[0];
expect(elementWithBinding.id).toEqual('');
ctx.foo = 'buz';
cd.detectChanges();
expect(elementWithBinding.id).toEqual('buz');
});
it('should consume directive watch expression change.', () => {
var memento = new DirectivePropertyMemento(1, 0, 'prop',
(o, v) => o.prop = v);
setUp(memento);
var elInj = view.elementInjectors[1];
expect(elInj.get(Directive).prop).toEqual('foo');
ctx.foo = 'buz';
cd.detectChanges();
expect(elInj.get(Directive).prop).toEqual('buz');
});
});
}); });
}); });
} }
class MyEvaluationContext {
@FIELD('foo')
constructor() {
this.foo = 'bar';
};
}