Skip to content

Commit 6dbfe0d

Browse files
committed
feat(vars): assignment of component or element instance to vars.
1 parent ab733bd commit 6dbfe0d

File tree

10 files changed

+224
-21
lines changed

10 files changed

+224
-21
lines changed

modules/angular2/src/core/compiler/element_injector.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,10 +211,22 @@ export class ProtoElementInjector {
211211
index:int;
212212
view:View;
213213
distanceToParent:number;
214+
215+
/** Whether the element is exported as $implicit. */
216+
exportElement:boolean;
217+
218+
/** Whether the component instance is exported as $implicit. */
219+
exportComponent:boolean;
220+
221+
/** The variable name that will be set to $implicit for the element. */
222+
exportImplicitName:string;
223+
214224
constructor(parent:ProtoElementInjector, index:int, bindings:List, firstBindingIsComponent:boolean = false, distanceToParent:number = 0) {
215225
this.parent = parent;
216226
this.index = index;
217227
this.distanceToParent = distanceToParent;
228+
this.exportComponent = false;
229+
this.exportElement = false;
218230

219231
this._binding0IsComponent = firstBindingIsComponent;
220232
this._binding0 = null; this._keyId0 = null;
@@ -405,6 +417,11 @@ export class ElementInjector extends TreeNode {
405417
return this._preBuiltObjects.element.domElement === el;
406418
}
407419

420+
/** Gets the NgElement associated with this ElementInjector */
421+
getNgElement() {
422+
return this._preBuiltObjects.element;
423+
}
424+
408425
getComponent() {
409426
if (this._proto._binding0IsComponent) {
410427
return this._obj0;
@@ -603,6 +620,21 @@ export class ElementInjector extends TreeNode {
603620
hasEventEmitter(eventName: string) {
604621
return this._proto.hasEventEmitter(eventName);
605622
}
623+
624+
/** Gets whether this element is exporting a component instance as $implicit. */
625+
isExportingComponent() {
626+
return this._proto.exportComponent;
627+
}
628+
629+
/** Gets whether this element is exporting its element as $implicit. */
630+
isExportingElement() {
631+
return this._proto.exportElement;
632+
}
633+
634+
/** Get the name to which this element's $implicit is to be assigned. */
635+
getExportImplicitName() {
636+
return this._proto.exportImplicitName;
637+
}
606638
}
607639

608640
class OutOfBoundsAccess extends Error {

modules/angular2/src/core/compiler/pipeline/compile_element.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,17 @@ export class CompileElement {
106106
MapWrapper.set(this.propertyBindings, property, expression);
107107
}
108108

109-
addVariableBinding(directiveName:string, templateName:string) {
109+
addVariableBinding(variableName:string, variableValue:string) {
110110
if (isBlank(this.variableBindings)) {
111111
this.variableBindings = MapWrapper.create();
112112
}
113-
MapWrapper.set(this.variableBindings, templateName, directiveName);
113+
114+
// Store the variable map from value to variable, reflecting how it will be used later by
115+
// View. When a local is set to the view, a lookup for the variable name will take place keyed
116+
// by the "value", or exported identifier. For example, ng-repeat sets a view local of "index".
117+
// When this occurs, a lookup keyed by "index" must occur to find if there is a var referencing
118+
// it.
119+
MapWrapper.set(this.variableBindings, variableValue, variableName);
114120
}
115121

116122
addEventBinding(eventName:string, expression:AST) {

modules/angular2/src/core/compiler/pipeline/property_binding_parser.js

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,16 @@ import {CompileElement} from './compile_element';
99
import {CompileControl} from './compile_control';
1010

1111
// TODO(tbosch): Cannot make this const/final right now because of the transpiler...
12-
var BIND_NAME_REGEXP = RegExpWrapper.create('^(?:(?:(bind)|(var)|(on))-(.+))|\\[([^\\]]+)\\]|\\(([^\\)]+)\\)');
12+
// Group 1 = "bind"
13+
// Group 2 = "var"
14+
// Group 3 = "on"
15+
// Group 4 = the identifier after "bind", "var", or "on"
16+
// Group 5 = idenitifer inside square braces
17+
// Group 6 = identifier inside parenthesis
18+
// Group 7 = "#"
19+
// Group 8 = identifier after "#"
20+
var BIND_NAME_REGEXP = RegExpWrapper.create(
21+
'^(?:(?:(bind)|(var)|(on))-(.+))|\\[([^\\]]+)\\]|\\(([^\\)]+)\\)|(#)(.+)');
1322

1423
/**
1524
* Parses the property bindings on a single element.
@@ -35,14 +44,12 @@ export class PropertyBindingParser extends CompileStep {
3544
if (isPresent(bindParts[1])) {
3645
// match: bind-prop
3746
current.addPropertyBinding(bindParts[4], this._parseBinding(attrValue));
38-
} else if (isPresent(bindParts[2])) {
39-
// match: let-prop
40-
// Note: We assume that the ViewSplitter already did its work, i.e. template directive should
41-
// only be present on <template> elements any more!
42-
if (!(current.element instanceof TemplateElement)) {
43-
throw new BaseException('var-* is only allowed on <template> elements!');
44-
}
45-
current.addVariableBinding(bindParts[4], attrValue);
47+
} else if (isPresent(bindParts[2]) || isPresent(bindParts[7])) {
48+
// match: var-name / var-name="iden" / #name / #name="iden"
49+
var identifier = (isPresent(bindParts[4]) && bindParts[4] !== '') ?
50+
bindParts[4] : bindParts[8];
51+
var value = attrValue == '' ? '\$implicit' : attrValue;
52+
current.addVariableBinding(identifier, value);
4653
} else if (isPresent(bindParts[3])) {
4754
// match: on-prop
4855
current.addEventBinding(bindParts[4], this._parseAction(attrValue));

modules/angular2/src/core/compiler/pipeline/proto_element_injector_builder.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {isPresent, isBlank} from 'angular2/src/facade/lang';
2-
import {ListWrapper} from 'angular2/src/facade/collection';
2+
import {ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
33

44
import {Key} from 'angular2/di';
55
import {ProtoElementInjector, ComponentKeyMetaData, DirectiveBinding} from '../element_injector';
@@ -39,7 +39,10 @@ export class ProtoElementInjectorBuilder extends CompileStep {
3939
// but after the directives as we rely on that order
4040
// in the element_binder_builder.
4141

42-
if (injectorBindings.length > 0) {
42+
// Create a protoElementInjector for any element that either has bindings *or* has one
43+
// or more var- defined. Elements with a var- defined need a their own element injector
44+
// so that, when hydrating, $implicit can be set to the element.
45+
if (injectorBindings.length > 0 || isPresent(current.variableBindings)) {
4346
var protoView = current.inheritedProtoView;
4447
var hasComponent = isPresent(current.componentDirective);
4548

@@ -49,6 +52,18 @@ export class ProtoElementInjectorBuilder extends CompileStep {
4952
);
5053
current.distanceToParentInjector = 0;
5154

55+
// Template directives are treated differently than other element with var- definitions.
56+
if (isPresent(current.variableBindings) && !isPresent(current.templateDirective)) {
57+
current.inheritedProtoElementInjector.exportComponent = hasComponent;
58+
current.inheritedProtoElementInjector.exportElement = !hasComponent;
59+
60+
// experiment
61+
var exportImplicitName = MapWrapper.get(current.variableBindings, '\$implicit');
62+
if (isPresent(exportImplicitName)) {
63+
current.inheritedProtoElementInjector.exportImplicitName = exportImplicitName;
64+
}
65+
}
66+
5267
} else {
5368
current.inheritedProtoElementInjector = parentProtoElementInjector;
5469
current.distanceToParentInjector = distanceToParentInjector;

modules/angular2/src/core/compiler/pipeline/proto_view_builder.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export class ProtoViewBuilder extends CompileStep {
4040
throw new BaseException('Only one nested view per element is allowed');
4141
}
4242
parent.inheritedElementBinder.nestedProtoView = inheritedProtoView;
43+
44+
// When current is a view root, the variable bindings are set to the *nested* proto view.
45+
// The root view conceptually signifies a new "block scope" (the nested view), to which
46+
// the variables are bound.
4347
if (isPresent(parent.variableBindings)) {
4448
MapWrapper.forEach(parent.variableBindings, (mappedName, varName) => {
4549
inheritedProtoView.bindVariable(varName, mappedName);
@@ -49,6 +53,17 @@ export class ProtoViewBuilder extends CompileStep {
4953
} else if (isPresent(parent)) {
5054
inheritedProtoView = parent.inheritedProtoView;
5155
}
56+
57+
// The view's contextWithLocals needs to have a full set of variable names at construction time
58+
// in order to prevent new variables from being set later in the lifecycle. Since we don't want
59+
// to actually create variable bindings for the $implicit bindings, add to the
60+
// protoContextLocals manually.
61+
if (isPresent(current.variableBindings)) {
62+
MapWrapper.forEach(current.variableBindings, (mappedName, varName) => {
63+
MapWrapper.set(inheritedProtoView.protoContextLocals, mappedName, null);
64+
});
65+
}
66+
5267
current.inheritedProtoView = inheritedProtoView;
5368
}
5469
}

modules/angular2/src/core/compiler/view.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,16 @@ export class View {
150150
var elementInjector = this.elementInjectors[i];
151151
if (isPresent(elementInjector)) {
152152
elementInjector.instantiateDirectives(appInjector, shadowDomAppInjector, this.preBuiltObjects[i]);
153+
154+
// The exporting of $implicit is a special case. Since multiple elements will all export
155+
// the different values as $implicit, directly assign $implicit bindings to the variable
156+
// name.
157+
var exportImplicitName = elementInjector.getExportImplicitName();
158+
if (elementInjector.isExportingComponent()) {
159+
this.context.set(exportImplicitName, elementInjector.getComponent());
160+
} else if (elementInjector.isExportingElement()) {
161+
this.context.set(exportImplicitName, elementInjector.getNgElement().domElement);
162+
}
153163
}
154164

155165
if (isPresent(componentDirective)) {

modules/angular2/test/core/compiler/integration_spec.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,62 @@ export function main() {
133133
done();
134134
});
135135
});
136+
137+
it('should assign the component instance to a var-', (done) => {
138+
compiler.compile(MyComp, el('<p><child-cmp var-alice></child-cmp></p>')).then((pv) => {
139+
createView(pv);
140+
141+
expect(view.contextWithLocals).not.toBe(null);
142+
expect(view.contextWithLocals.get('alice')).toBeAnInstanceOf(ChildComp);
143+
144+
done();
145+
})
146+
});
147+
148+
it('should assign two component instances each with a var-', (done) => {
149+
var element = el('<p><child-cmp var-alice></child-cmp><child-cmp var-bob></p>');
150+
151+
compiler.compile(MyComp, element).then((pv) => {
152+
createView(pv);
153+
154+
expect(view.contextWithLocals).not.toBe(null);
155+
expect(view.contextWithLocals.get('alice')).toBeAnInstanceOf(ChildComp);
156+
expect(view.contextWithLocals.get('bob')).toBeAnInstanceOf(ChildComp);
157+
expect(view.contextWithLocals.get('alice')).not.toBe(view.contextWithLocals.get('bob'));
158+
159+
done();
160+
})
161+
});
162+
163+
it('should assign the component instance to a var- with shorthand syntax', (done) => {
164+
compiler.compile(MyComp, el('<child-cmp #alice></child-cmp>')).then((pv) => {
165+
createView(pv);
166+
167+
expect(view.contextWithLocals).not.toBe(null);
168+
expect(view.contextWithLocals.get('alice')).toBeAnInstanceOf(ChildComp);
169+
170+
done();
171+
})
172+
});
173+
174+
it('should assign the element instance to a user-defined variable', (done) => {
175+
// How is this supposed to work?
176+
var element = el('<p></p>');
177+
var div = el('<div var-alice></div>');
178+
DOM.appendChild(div, el('<i>Hello</i>'));
179+
DOM.appendChild(element, div);
180+
181+
compiler.compile(MyComp, element).then((pv) => {
182+
createView(pv);
183+
expect(view.contextWithLocals).not.toBe(null);
184+
185+
var value = view.contextWithLocals.get('alice');
186+
expect(value).not.toBe(null);
187+
expect(value.tagName).toEqual('DIV');
188+
189+
done();
190+
})
191+
});
136192
});
137193
});
138194
}

modules/angular2/test/core/compiler/pipeline/property_binding_parser_spec.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,24 @@ export function main() {
3434
expect(MapWrapper.get(results[0].variableBindings, 'b')).toEqual('a');
3535
});
3636

37-
it('should not allow var- syntax on non template elements', () => {
38-
expect( () => {
39-
createPipeline().process(el('<div var-a="b"></div>'))
40-
}).toThrowError('var-* is only allowed on <template> elements!');
37+
it('should store variable binding for a non-template element', () => {
38+
var results = createPipeline().process(el('<p var-george="washington"></p>'));
39+
expect(MapWrapper.get(results[0].variableBindings, 'washington')).toEqual('george');
40+
});
41+
42+
it('should store variable binding for a non-template element using shorthand syntax', () => {
43+
var results = createPipeline().process(el('<p #george="washington"></p>'));
44+
expect(MapWrapper.get(results[0].variableBindings, 'washington')).toEqual('george');
45+
});
46+
47+
it('should store a variable binding with an implicit value', () => {
48+
var results = createPipeline().process(el('<p var-george></p>'));
49+
expect(MapWrapper.get(results[0].variableBindings, '\$implicit')).toEqual('george');
50+
});
51+
52+
it('should store a variable binding with an implicit value using shorthand syntax', () => {
53+
var results = createPipeline().process(el('<p #george></p>'));
54+
expect(MapWrapper.get(results[0].variableBindings, '\$implicit')).toEqual('george');
4155
});
4256

4357
it('should detect () syntax', () => {

modules/angular2/test/core/compiler/pipeline/proto_element_injector_builder_spec.js

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {describe, beforeEach, it, expect, iit, ddescribe, el} from 'angular2/test_lib';
22
import {isPresent, isBlank} from 'angular2/src/facade/lang';
33
import {DOM} from 'angular2/src/facade/dom';
4-
import {List, ListWrapper} from 'angular2/src/facade/collection';
4+
import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
55

66
import {ProtoElementInjectorBuilder} from 'angular2/src/core/compiler/pipeline/proto_element_injector_builder';
77
import {CompilePipeline} from 'angular2/src/core/compiler/pipeline/compile_pipeline';
@@ -21,6 +21,11 @@ export function main() {
2121
protoView = new ProtoView(null, null, null);
2222
});
2323

24+
// Create consts for an elements with a var- so that we can fake parsing the var into
25+
// the CompileElement's variableBindings without actually doing any parsing.
26+
var ELEMENT_WITH_VAR = el('<div var-name></div>');
27+
var DIRECTIVE_ELEMENT_WITH_VAR = el('<div var-name directives></div>');
28+
2429
function createPipeline(directives = null) {
2530
if (isBlank(directives)) {
2631
directives = [];
@@ -30,12 +35,20 @@ export function main() {
3035
if (isPresent(current.element.getAttribute('viewroot'))) {
3136
current.isViewRoot = true;
3237
}
38+
3339
if (isPresent(current.element.getAttribute('directives'))) {
3440
for (var i=0; i<directives.length; i++) {
3541
var dirMetadata = reader.read(directives[i]);
3642
current.addDirective(dirMetadata);
3743
}
3844
}
45+
46+
// Check only for the hard-coded var- attribute from ELEMENT_WITH_VAR test element.
47+
if (isPresent(current.element.getAttribute('var-name'))) {
48+
current.variableBindings = MapWrapper.create();
49+
MapWrapper.set(current.variableBindings, '\$implicit', 'name');
50+
}
51+
3952
current.inheritedProtoView = protoView;
4053
}), protoElementInjectorBuilder]);
4154
}
@@ -44,11 +57,16 @@ export function main() {
4457
return protoElementInjectorBuilder.findArgsFor(protoElementInjector);
4558
}
4659

47-
it('should not create a ProtoElementInjector for elements without directives', () => {
60+
it('should not create a ProtoElementInjector for elements without directives or vars', () => {
4861
var results = createPipeline().process(el('<div></div>'));
4962
expect(results[0].inheritedProtoElementInjector).toBe(null);
5063
});
5164

65+
it('should create a ProtoElementInjector for elements with a variable binding', () => {
66+
var results = createPipeline().process(ELEMENT_WITH_VAR);
67+
expect(results[0].inheritedProtoElementInjector).toBeAnInstanceOf(ProtoElementInjector);
68+
});
69+
5270
it('should create a ProtoElementInjector for elements directives', () => {
5371
var directives = [SomeComponentDirective, SomeTemplateDirective, SomeDecoratorDirective];
5472
var results = createPipeline(directives).process(el('<div directives></div>'));
@@ -57,7 +75,22 @@ export function main() {
5775
expect(boundDirectives).toEqual(directives);
5876
});
5977

60-
it('should mark ProtoElementInjector for elements with component directives and use the ComponentDirective as first binding', () => {
78+
it('should flag the ProtoElementInjector for exporting the component instance when a' +
79+
'component has a var- declaration', () => {
80+
var results = createPipeline([SomeComponentDirective]).process(DIRECTIVE_ELEMENT_WITH_VAR);
81+
expect(results[0].inheritedProtoElementInjector.exportComponent).toBe(true);
82+
expect(results[0].inheritedProtoElementInjector.exportElement).toBe(false);
83+
});
84+
85+
it('should flag the ProtoElementInjector for exporting the element when a' +
86+
'non-component element has a var- declaration', () => {
87+
var results = createPipeline([SomeComponentDirective]).process(ELEMENT_WITH_VAR);
88+
expect(results[0].inheritedProtoElementInjector.exportComponent).toBe(false);
89+
expect(results[0].inheritedProtoElementInjector.exportElement).toBe(true);
90+
});
91+
92+
it('should mark ProtoElementInjector for elements with component directives and use the ' +
93+
'ComponentDirective as first binding', () => {
6194
var directives = [SomeDecoratorDirective, SomeComponentDirective];
6295
var results = createPipeline(directives).process(el('<div directives></div>'));
6396
var creationArgs = getCreationArgs(results[0].inheritedProtoElementInjector);

0 commit comments

Comments
 (0)