`
- parseTemplateBindings(tplKey: string): TemplateBindingParseResult {
- let firstBinding = true;
+ /**
+ * Parse microsyntax template expression and return a list of bindings or
+ * parsing errors in case the given expression is invalid.
+ *
+ * For example,
+ * ```
+ *
+ * ```
+ * contains five bindings:
+ * 1. ngFor -> null
+ * 2. item -> NgForOfContext.$implicit
+ * 3. ngForOf -> items
+ * 4. i -> NgForOfContext.index
+ * 5. ngForTrackBy -> func
+ *
+ * For a full description of the microsyntax grammar, see
+ * https://gist.github.com/mhevery/d3530294cff2e4a1b3fe15ff75d08855
+ *
+ * @param templateKey name of the microsyntax directive, like ngIf, ngFor, without the *
+ */
+ parseTemplateBindings(templateKey: string): TemplateBindingParseResult {
const bindings: TemplateBinding[] = [];
- const warnings: string[] = [];
- do {
- const start = this.inputIndex;
- let rawKey: string;
- let key: string;
- let isVar: boolean = false;
- if (firstBinding) {
- rawKey = key = tplKey;
- firstBinding = false;
+
+ // The first binding is for the template key itself
+ // In *ngFor="let item of items", key = "ngFor", value = null
+ // In *ngIf="cond | pipe", key = "ngIf", value = "cond | pipe"
+ bindings.push(...this.parseDirectiveKeywordBindings(
+ templateKey, new ParseSpan(0, templateKey.length), this.absoluteOffset));
+
+ while (this.index < this.tokens.length) {
+ // If it starts with 'let', then this must be variable declaration
+ const letBinding = this.parseLetBinding();
+ if (letBinding) {
+ bindings.push(letBinding);
} else {
- isVar = this.peekKeywordLet();
- if (isVar) this.advance();
- rawKey = this.expectTemplateBindingKey();
- key = isVar ? rawKey : tplKey + rawKey[0].toUpperCase() + rawKey.substring(1);
- this.optionalCharacter(chars.$COLON);
- }
-
- let name: string = null !;
- let expression: ASTWithSource|null = null;
- if (isVar) {
- if (this.optionalOperator('=')) {
- name = this.expectTemplateBindingKey();
+ // Two possible cases here, either `value "as" key` or
+ // "directive-keyword expression". We don't know which case, but both
+ // "value" and "directive-keyword" are template binding key, so consume
+ // the key first.
+ const {key, keySpan} = this.expectTemplateBindingKey();
+ // Peek at the next token, if it is "as" then this must be variable
+ // declaration.
+ const binding = this.parseAsBinding(key, keySpan, this.absoluteOffset);
+ if (binding) {
+ bindings.push(binding);
} else {
- name = '\$implicit';
+ // Otherwise the key must be a directive keyword, like "of". Transform
+ // the key to actual key. Eg. of -> ngForOf, trackBy -> ngForTrackBy
+ const actualKey = templateKey + key[0].toUpperCase() + key.substring(1);
+ bindings.push(
+ ...this.parseDirectiveKeywordBindings(actualKey, keySpan, this.absoluteOffset));
}
- } else if (this.peekKeywordAs()) {
- this.advance(); // consume `as`
- name = rawKey;
- key = this.expectTemplateBindingKey(); // read local var name
- isVar = true;
- } else if (this.next !== EOF && !this.peekKeywordLet()) {
- const start = this.inputIndex;
- const ast = this.parsePipe();
- const source = this.input.substring(start - this.offset, this.inputIndex - this.offset);
- expression =
- new ASTWithSource(ast, source, this.location, this.absoluteOffset + start, this.errors);
}
+ this.consumeStatementTerminator();
+ }
- bindings.push(new TemplateBinding(
- this.span(start), this.sourceSpan(start), key, isVar, name, expression));
- if (this.peekKeywordAs() && !isVar) {
- const letStart = this.inputIndex;
- this.advance(); // consume `as`
- const letName = this.expectTemplateBindingKey(); // read local var name
- bindings.push(new TemplateBinding(
- this.span(letStart), this.sourceSpan(letStart), letName, true, key, null !));
- }
- if (!this.optionalCharacter(chars.$SEMICOLON)) {
- this.optionalCharacter(chars.$COMMA);
- }
- } while (this.index < this.tokens.length);
+ return new TemplateBindingParseResult(bindings, [] /* warnings */, this.errors);
+ }
- return new TemplateBindingParseResult(bindings, warnings, this.errors);
+ /**
+ * Parse a directive keyword, followed by a mandatory expression.
+ * For example, "of items", "trackBy: func".
+ * The bindings are: ngForOf -> items, ngForTrackBy -> func
+ * There could be an optional "as" binding that follows the expression.
+ * For example,
+ * ```
+ * *ngFor="let item of items | slice:0:1 as collection".`
+ * ^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^
+ * keyword bound target optional 'as' binding
+ * ```
+ *
+ * @param key binding key, for example, ngFor, ngIf, ngForOf
+ * @param keySpan span of the key in the expression. keySpan might be different
+ * from `key.length`. For example, the span for key "ngForOf" is "of".
+ * @param absoluteOffset absolute offset of the attribute value
+ */
+ private parseDirectiveKeywordBindings(key: string, keySpan: ParseSpan, absoluteOffset: number):
+ TemplateBinding[] {
+ const bindings: TemplateBinding[] = [];
+ this.optionalCharacter(chars.$COLON); // trackBy: trackByFunction
+ const valueExpr = this.getDirectiveBoundTarget();
+ const span = new ParseSpan(keySpan.start, this.inputIndex);
+ bindings.push(new TemplateBinding(
+ span, span.toAbsolute(absoluteOffset), key, false /* keyIsVar */, valueExpr?.source || '', valueExpr));
+ // The binding could optionally be followed by "as". For example,
+ // *ngIf="cond | pipe as x". In this case, the key in the "as" binding
+ // is "x" and the value is the template key itself ("ngIf"). Note that the
+ // 'key' in the current context now becomes the "value" in the next binding.
+ const asBinding = this.parseAsBinding(key, keySpan, absoluteOffset);
+ if (asBinding) {
+ bindings.push(asBinding);
+ }
+ this.consumeStatementTerminator();
+ return bindings;
+ }
+
+ /**
+ * Return the expression AST for the bound target of a directive keyword
+ * binding. For example,
+ * ```
+ * *ngIf="condition | pipe".
+ * ^^^^^^^^^^^^^^^^ bound target for "ngIf"
+ * *ngFor="let item of items"
+ * ^^^^^ bound target for "ngForOf"
+ * ```
+ */
+ private getDirectiveBoundTarget(): ASTWithSource|null {
+ if (this.next === EOF || this.peekKeywordAs() || this.peekKeywordLet()) {
+ return null;
+ }
+ const ast = this.parsePipe(); // example: "condition | async"
+ const {start, end} = ast.span;
+ const value = this.input.substring(start, end);
+ return new ASTWithSource(ast, value, this.location, this.absoluteOffset + start, this.errors);
+ }
+
+ /**
+ * Return the binding for a variable declared using `as`. Note that the order
+ * of the key-value pair in this declaration is reversed. For example,
+ * ```
+ * *ngFor="let item of items; index as i"
+ * ^^^^^ ^
+ * value key
+ * ```
+ *
+ * @param value name of the value in the declaration, "ngIf" in the example above
+ * @param valueSpan span of the value in the declaration
+ * @param absoluteOffset absolute offset of `value`
+ */
+ private parseAsBinding(value: string, valueSpan: ParseSpan, absoluteOffset: number):
+ TemplateBinding|null {
+ if (!this.peekKeywordAs()) {
+ return null;
+ }
+ this.advance(); // consume the 'as' keyword
+ const {key} = this.expectTemplateBindingKey();
+ const valueAst = new AST(valueSpan, valueSpan.toAbsolute(absoluteOffset));
+ const valueExpr = new ASTWithSource(
+ valueAst, value, this.location, absoluteOffset + valueSpan.start, this.errors);
+ const span = new ParseSpan(valueSpan.start, this.inputIndex);
+ return new TemplateBinding(
+ span, span.toAbsolute(absoluteOffset), key, true /* keyIsVar */, value, valueExpr);
+ }
+
+ /**
+ * Return the binding for a variable declared using `let`. For example,
+ * ```
+ * *ngFor="let item of items; let i=index;"
+ * ^^^^^^^^ ^^^^^^^^^^^
+ * ```
+ * In the first binding, `item` is bound to `NgForOfContext.$implicit`.
+ * In the second binding, `i` is bound to `NgForOfContext.index`.
+ */
+ private parseLetBinding(): TemplateBinding|null {
+ if (!this.peekKeywordLet()) {
+ return null;
+ }
+ const spanStart = this.inputIndex;
+ this.advance(); // consume the 'let' keyword
+ const {key} = this.expectTemplateBindingKey();
+ let valueExpr: ASTWithSource|null = null;
+ if (this.optionalOperator('=')) {
+ const {key: value, keySpan: valueSpan} = this.expectTemplateBindingKey();
+ const ast = new AST(valueSpan, valueSpan.toAbsolute(this.absoluteOffset));
+ valueExpr = new ASTWithSource(
+ ast, value, this.location, this.absoluteOffset + valueSpan.start, this.errors);
+ }
+ const spanEnd = this.inputIndex;
+ const span = new ParseSpan(spanStart, spanEnd);
+ return new TemplateBinding(
+ span, span.toAbsolute(this.absoluteOffset), key, true /* keyIsVar */, valueExpr?.source || '$implicit', valueExpr);
+ }
+
+ /**
+ * Consume the optional statement terminator: semicolon or comma.
+ */
+ private consumeStatementTerminator() {
+ this.optionalCharacter(chars.$SEMICOLON) || this.optionalCharacter(chars.$COMMA);
}
error(message: string, index: number|null = null) {
@@ -896,4 +1046,4 @@ class IvySimpleExpressionChecker extends SimpleExpressionChecker {
}
visitPrefixNot(ast: PrefixNot, context: any) { ast.expression.visit(this); }
-}
\ No newline at end of file
+}
diff --git a/packages/compiler/test/expression_parser/parser_spec.ts b/packages/compiler/test/expression_parser/parser_spec.ts
index fb5ac23530..5c4adba422 100644
--- a/packages/compiler/test/expression_parser/parser_spec.ts
+++ b/packages/compiler/test/expression_parser/parser_spec.ts
@@ -270,6 +270,14 @@ describe('parser', () => {
binding => binding.expression != null ? binding.expression.source : null);
}
+ function humanize(bindings: TemplateBinding[]): Array<[string, string | null, boolean]> {
+ return bindings.map(binding => {
+ const {key, expression, name, keyIsVar} = binding;
+ const value = keyIsVar ? name : (expression ? expression.source : expression);
+ return [key, value, keyIsVar];
+ });
+ }
+
it('should parse a key without a value',
() => { expect(keys(parseTemplateBindings('a', ''))).toEqual(['a']); });
@@ -317,6 +325,44 @@ describe('parser', () => {
expect(bindings[0].expression !.location).toEqual('location');
});
+ it('should support common usage of ngIf', () => {
+ const bindings = parseTemplateBindings('ngIf', 'cond | pipe as foo, let x; ngIf as y');
+ expect(humanize(bindings)).toEqual([
+ // [ key, value, keyIsVar ]
+ ['ngIf', 'cond | pipe ', false],
+ ['foo', 'ngIf', true],
+ ['x', '$implicit', true],
+ ['y', 'ngIf', true],
+ ]);
+ });
+
+ it('should support common usage of ngFor', () => {
+ let bindings: TemplateBinding[];
+ bindings = parseTemplateBindings(
+ 'ngFor', 'let item; of items | slice:0:1 as collection, trackBy: func; index as i');
+ expect(humanize(bindings)).toEqual([
+ // [ key, value, keyIsVar ]
+ ['ngFor', null, false],
+ ['item', '$implicit', true],
+ ['ngForOf', 'items | slice:0:1 ', false],
+ ['collection', 'ngForOf', true],
+ ['ngForTrackBy', 'func', false],
+ ['i', 'index', true],
+ ]);
+
+ bindings = parseTemplateBindings(
+ 'ngFor', 'let item, of: [1,2,3] | pipe as items; let i=index, count as len');
+ expect(humanize(bindings)).toEqual([
+ // [ key, value, keyIsVar ]
+ ['ngFor', null, false],
+ ['item', '$implicit', true],
+ ['ngForOf', '[1,2,3] | pipe ', false],
+ ['items', 'ngForOf', true],
+ ['i', 'index', true],
+ ['len', 'count', true],
+ ]);
+ });
+
it('should support let notation', () => {
let bindings = parseTemplateBindings('key', 'let i');
expect(keyValues(bindings)).toEqual(['key', 'let i=$implicit']);