Skip to content

Commit a9be2eb

Browse files
committed
feat: add support for the safe navigation (aka Elvis) operator
fixes angular#791
1 parent ec2d8cc commit a9be2eb

File tree

11 files changed

+153
-18
lines changed

11 files changed

+153
-18
lines changed

modules/angular2/src/change_detection/change_detection_jit_generator.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import {
1717
RECORD_TYPE_KEYED_ACCESS,
1818
RECORD_TYPE_PIPE,
1919
RECORD_TYPE_BINDING_PIPE,
20-
RECORD_TYPE_INTERPOLATE
20+
RECORD_TYPE_INTERPOLATE,
21+
RECORD_TYPE_SAFE_PROPERTY,
22+
RECORD_TYPE_SAFE_INVOKE_METHOD
2123
} from './proto_record';
2224

2325

@@ -295,6 +297,10 @@ export class ChangeDetectorJITGenerator {
295297
rhs = `${context}.${r.name}`;
296298
break;
297299

300+
case RECORD_TYPE_SAFE_PROPERTY:
301+
rhs = `${UTIL}.isValueBlank(${context}) ? null : ${context}.${r.name}`;
302+
break;
303+
298304
case RECORD_TYPE_LOCAL:
299305
rhs = `${LOCALS_ACCESSOR}.get('${r.name}')`;
300306
break;
@@ -303,6 +309,10 @@ export class ChangeDetectorJITGenerator {
303309
rhs = `${context}.${r.name}(${argString})`;
304310
break;
305311

312+
case RECORD_TYPE_SAFE_INVOKE_METHOD:
313+
rhs = `${UTIL}.isValueBlank(${context}) ? null : ${context}.${r.name}(${argString})`;
314+
break;
315+
306316
case RECORD_TYPE_INVOKE_CLOSURE:
307317
rhs = `${context}(${argString})`;
308318
break;

modules/angular2/src/change_detection/change_detection_util.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,6 @@ export class ChangeDetectionUtil {
142142
changes[propertyName] = change;
143143
return changes;
144144
}
145+
146+
static isValueBlank(value: any): boolean { return isBlank(value); }
145147
}

modules/angular2/src/change_detection/dynamic_change_detector.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import {
1919
RECORD_TYPE_KEYED_ACCESS,
2020
RECORD_TYPE_PIPE,
2121
RECORD_TYPE_BINDING_PIPE,
22-
RECORD_TYPE_INTERPOLATE
22+
RECORD_TYPE_INTERPOLATE,
23+
RECORD_TYPE_SAFE_PROPERTY,
24+
RECORD_TYPE_SAFE_INVOKE_METHOD
2325
} from './proto_record';
2426

2527
import {ExpressionChangedAfterItHasBeenChecked, ChangeDetectionError} from './exceptions';
@@ -192,6 +194,10 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
192194
var context = this._readContext(proto);
193195
return proto.funcOrValue(context);
194196

197+
case RECORD_TYPE_SAFE_PROPERTY:
198+
var context = this._readContext(proto);
199+
return isBlank(context) ? null : proto.funcOrValue(context);
200+
195201
case RECORD_TYPE_LOCAL:
196202
return this.locals.get(proto.name);
197203

@@ -200,6 +206,14 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
200206
var args = this._readArgs(proto);
201207
return proto.funcOrValue(context, args);
202208

209+
case RECORD_TYPE_SAFE_INVOKE_METHOD:
210+
var context = this._readContext(proto);
211+
if (isBlank(context)) {
212+
return null;
213+
}
214+
var args = this._readArgs(proto);
215+
return proto.funcOrValue(context, args);
216+
203217
case RECORD_TYPE_KEYED_ACCESS:
204218
var arg = this._readArgs(proto)[0];
205219
return this._readContext(proto)[arg];

modules/angular2/src/change_detection/parser/ast.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,20 @@ export class AccessMember extends AST {
9191
visit(visitor) { return visitor.visitAccessMember(this); }
9292
}
9393

94+
export class SafeAccessMember extends AST {
95+
constructor(public receiver: AST, public name: string, public getter: Function,
96+
public setter: Function) {
97+
super();
98+
}
99+
100+
eval(context, locals) {
101+
var evaluatedReceiver = this.receiver.eval(context, locals);
102+
return isBlank(evaluatedReceiver) ? null : this.getter(evaluatedReceiver);
103+
}
104+
105+
visit(visitor) { return visitor.visitSafeAccessMember(this); }
106+
}
107+
94108
export class KeyedAccess extends AST {
95109
constructor(public obj: AST, public key: AST) { super(); }
96110

@@ -251,6 +265,22 @@ export class MethodCall extends AST {
251265
visit(visitor) { return visitor.visitMethodCall(this); }
252266
}
253267

268+
export class SafeMethodCall extends AST {
269+
constructor(public receiver: AST, public name: string, public fn: Function,
270+
public args: List<any>) {
271+
super();
272+
}
273+
274+
eval(context, locals) {
275+
var evaluatedReceiver = this.receiver.eval(context, locals);
276+
if (isBlank(evaluatedReceiver)) return null;
277+
var evaluatedArgs = evalList(context, locals, this.args);
278+
return this.fn(evaluatedReceiver, evaluatedArgs);
279+
}
280+
281+
visit(visitor) { return visitor.visitSafeMethodCall(this); }
282+
}
283+
254284
export class FunctionCall extends AST {
255285
constructor(public target: AST, public args: List<any>) { super(); }
256286

@@ -300,6 +330,8 @@ export class AstVisitor {
300330
visitLiteralPrimitive(ast: LiteralPrimitive) {}
301331
visitMethodCall(ast: MethodCall) {}
302332
visitPrefixNot(ast: PrefixNot) {}
333+
visitSafeAccessMember(ast: SafeAccessMember) {}
334+
visitSafeMethodCall(ast: SafeMethodCall) {}
303335
}
304336

305337
export class AstTransformer {
@@ -315,10 +347,18 @@ export class AstTransformer {
315347
return new AccessMember(ast.receiver.visit(this), ast.name, ast.getter, ast.setter);
316348
}
317349

350+
visitSafeAccessMember(ast: SafeAccessMember) {
351+
return new SafeAccessMember(ast.receiver.visit(this), ast.name, ast.getter, ast.setter);
352+
}
353+
318354
visitMethodCall(ast: MethodCall) {
319355
return new MethodCall(ast.receiver.visit(this), ast.name, ast.fn, this.visitAll(ast.args));
320356
}
321357

358+
visitSafeMethodCall(ast: SafeMethodCall) {
359+
return new SafeMethodCall(ast.receiver.visit(this), ast.name, ast.fn, this.visitAll(ast.args));
360+
}
361+
322362
visitFunctionCall(ast: FunctionCall) {
323363
return new FunctionCall(ast.target.visit(this), this.visitAll(ast.args));
324364
}

modules/angular2/src/change_detection/parser/lexer.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,15 +219,15 @@ class _Scanner {
219219
case $DQ:
220220
return this.scanString();
221221
case $HASH:
222-
return this.scanOperator(start, StringWrapper.fromCharCode(peek));
223222
case $PLUS:
224223
case $MINUS:
225224
case $STAR:
226225
case $SLASH:
227226
case $PERCENT:
228227
case $CARET:
229-
case $QUESTION:
230228
return this.scanOperator(start, StringWrapper.fromCharCode(peek));
229+
case $QUESTION:
230+
return this.scanComplexOperator(start, $PERIOD, '?', '.');
231231
case $LT:
232232
case $GT:
233233
case $BANG:
@@ -434,7 +434,8 @@ var OPERATORS = SetWrapper.createFromList([
434434
'|',
435435
'!',
436436
'?',
437-
'#'
437+
'#',
438+
'?.'
438439
]);
439440

440441

modules/angular2/src/change_detection/parser/parser.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
EmptyExpr,
2929
ImplicitReceiver,
3030
AccessMember,
31+
SafeAccessMember,
3132
LiteralPrimitive,
3233
Binary,
3334
PrefixNot,
@@ -40,6 +41,7 @@ import {
4041
LiteralMap,
4142
Interpolation,
4243
MethodCall,
44+
SafeMethodCall,
4345
FunctionCall,
4446
TemplateBinding,
4547
ASTWithSource
@@ -360,7 +362,10 @@ class _ParseAST {
360362
var result = this.parsePrimary();
361363
while (true) {
362364
if (this.optionalCharacter($PERIOD)) {
363-
result = this.parseAccessMemberOrMethodCall(result);
365+
result = this.parseAccessMemberOrMethodCall(result, false);
366+
367+
} else if (this.optionalOperator('?.')) {
368+
result = this.parseAccessMemberOrMethodCall(result, true);
364369

365370
} else if (this.optionalCharacter($LBRACKET)) {
366371
var key = this.parseExpression();
@@ -405,7 +410,7 @@ class _ParseAST {
405410
return this.parseLiteralMap();
406411

407412
} else if (this.next.isIdentifier()) {
408-
return this.parseAccessMemberOrMethodCall(_implicitReceiver);
413+
return this.parseAccessMemberOrMethodCall(_implicitReceiver, false);
409414

410415
} else if (this.next.isNumber()) {
411416
var value = this.next.toNumber();
@@ -451,19 +456,21 @@ class _ParseAST {
451456
return new LiteralMap(keys, values);
452457
}
453458

454-
parseAccessMemberOrMethodCall(receiver): AST {
459+
parseAccessMemberOrMethodCall(receiver, isSafe: boolean = false): AST {
455460
var id = this.expectIdentifierOrKeyword();
456461

457462
if (this.optionalCharacter($LPAREN)) {
458463
var args = this.parseCallArguments();
459464
this.expectCharacter($RPAREN);
460465
var fn = this.reflector.method(id);
461-
return new MethodCall(receiver, id, fn, args);
466+
return isSafe ? new SafeMethodCall(receiver, id, fn, args) :
467+
new MethodCall(receiver, id, fn, args);
462468

463469
} else {
464470
var getter = this.reflector.getter(id);
465471
var setter = this.reflector.setter(id);
466-
var am = new AccessMember(receiver, id, getter, setter);
472+
var am = isSafe ? new SafeAccessMember(receiver, id, getter, setter) :
473+
new AccessMember(receiver, id, getter, setter);
467474

468475
if (this.optionalOperator("|")) {
469476
return this.parseInlinedPipe(am);

modules/angular2/src/change_detection/proto_change_detector.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import {
1919
LiteralMap,
2020
LiteralPrimitive,
2121
MethodCall,
22-
PrefixNot
22+
PrefixNot,
23+
SafeAccessMember,
24+
SafeMethodCall
2325
} from './parser/ast';
2426

2527
import {
@@ -49,7 +51,9 @@ import {
4951
RECORD_TYPE_KEYED_ACCESS,
5052
RECORD_TYPE_PIPE,
5153
RECORD_TYPE_BINDING_PIPE,
52-
RECORD_TYPE_INTERPOLATE
54+
RECORD_TYPE_INTERPOLATE,
55+
RECORD_TYPE_SAFE_PROPERTY,
56+
RECORD_TYPE_SAFE_INVOKE_METHOD
5357
} from './proto_record';
5458

5559
export class DynamicProtoChangeDetector extends ProtoChangeDetector {
@@ -149,6 +153,11 @@ class _ConvertAstIntoProtoRecords {
149153
}
150154
}
151155

156+
visitSafeAccessMember(ast: SafeAccessMember) {
157+
var receiver = ast.receiver.visit(this);
158+
return this._addRecord(RECORD_TYPE_SAFE_PROPERTY, ast.name, ast.getter, [], null, receiver);
159+
}
160+
152161
visitMethodCall(ast: MethodCall) {
153162
var receiver = ast.receiver.visit(this);
154163
var args = this._visitAll(ast.args);
@@ -160,6 +169,12 @@ class _ConvertAstIntoProtoRecords {
160169
}
161170
}
162171

172+
visitSafeMethodCall(ast: SafeMethodCall) {
173+
var receiver = ast.receiver.visit(this);
174+
var args = this._visitAll(ast.args);
175+
return this._addRecord(RECORD_TYPE_SAFE_INVOKE_METHOD, ast.name, ast.fn, args, null, receiver);
176+
}
177+
163178
visitFunctionCall(ast: FunctionCall) {
164179
var target = ast.target.visit(this);
165180
var args = this._visitAll(ast.args);

modules/angular2/src/change_detection/proto_record.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export const RECORD_TYPE_KEYED_ACCESS = 7;
1313
export const RECORD_TYPE_PIPE = 8;
1414
export const RECORD_TYPE_BINDING_PIPE = 9;
1515
export const RECORD_TYPE_INTERPOLATE = 10;
16+
export const RECORD_TYPE_SAFE_PROPERTY = 11;
17+
export const RECORD_TYPE_SAFE_INVOKE_METHOD = 12;
1618

1719
export class ProtoRecord {
1820
constructor(public mode: number, public name: string, public funcOrValue, public args: List<any>,

modules/angular2/test/change_detection/change_detector_spec.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,14 +151,26 @@ export function main() {
151151
expect(executeWatch('const', '"a\n\nb"')).toEqual(['const=a\n\nb']);
152152
});
153153

154-
it('simple chained property access', () => {
154+
it('should support simple chained property access', () => {
155155
var address = new Address('Grenoble');
156156
var person = new Person('Victor', address);
157157

158158
expect(executeWatch('address.city', 'address.city', person))
159159
.toEqual(['address.city=Grenoble']);
160160
});
161161

162+
it('should support the safe navigation operator', () => {
163+
var person = new Person('Victor', null);
164+
165+
expect(executeWatch('city', 'address?.city', person)).toEqual(['city=null']);
166+
expect(executeWatch('city', 'address?.toString()', person)).toEqual(['city=null']);
167+
168+
person.address = new Address('MTV');
169+
170+
expect(executeWatch('city', 'address?.city', person)).toEqual(['city=MTV']);
171+
expect(executeWatch('city', 'address?.toString()', person)).toEqual(['city=MTV']);
172+
});
173+
162174
it("should support method calls", () => {
163175
var person = new Person('Victor');
164176
expect(executeWatch('m', 'sayHi("Jim")', person)).toEqual(['m=Hi, Jim']);
@@ -976,7 +988,7 @@ class Address {
976988
city: string;
977989
constructor(city: string) { this.city = city; }
978990

979-
toString(): string { return this.city; }
991+
toString(): string { return isBlank(this.city) ? '-' : this.city }
980992
}
981993

982994
class Uninitialized {

modules/angular2/test/change_detection/parser/lexer_spec.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,14 @@ export function main() {
126126
expectIdentifierToken(tokens[1], 8, 'b');
127127
});
128128

129-
it('should tokenize quoted string', function() {
129+
it('should tokenize quoted string', () => {
130130
var str = "['\\'', \"\\\"\"]";
131131
var tokens: List<Token> = lex(str);
132132
expectStringToken(tokens[1], 1, "'");
133133
expectStringToken(tokens[3], 7, '"');
134134
});
135135

136-
it('should tokenize escaped quoted string', function() {
136+
it('should tokenize escaped quoted string', () => {
137137
var str = '"\\"\\n\\f\\r\\t\\v\\u00A0"';
138138
var tokens: List<Token> = lex(str);
139139
expect(tokens.length).toEqual(1);
@@ -203,7 +203,7 @@ export function main() {
203203
});
204204

205205
// NOTE(deboer): NOT A LEXER TEST
206-
// it('should tokenize negative number', function() {
206+
// it('should tokenize negative number', () => {
207207
// var tokens:List<Token> = lex("-0.5");
208208
// expectNumberToken(tokens[0], 0, -0.5);
209209
// });
@@ -240,6 +240,11 @@ export function main() {
240240
expectOperatorToken(tokens[0], 0, '#');
241241
});
242242

243+
it('should tokenize ?. as operator', () => {
244+
var tokens: List<Token> = lex('?.');
245+
expectOperatorToken(tokens[0], 0, '?.');
246+
});
247+
243248
});
244249
});
245250
}

0 commit comments

Comments
 (0)