<div>
@if (user.name as userName) {
{{ userName }}
}
</div>
<div>
@if (user.name) {
{{ user.name }}
}
</div>
I am wondering what is the difference between this two approaches? just a syntax? or something else? For example when using as
does it creates variable? What is happening under the hood?
I don't know how deep you want such an explaination to go.
if you really want to go down to each line of execution, I'm sorry.. I will not put hours into this question.
Basically this happens:
First of all, you have to keep in mind, what you are writing there, is a dialect that does resemble js, but isn't.
So to not go into too much depth and keep this somewhat reasonable, let's start at the point, where the parser will encounter a @
symbol:
https://github.com/angular/angular/blob/c213a4e15a594ff141cf312ad301128e7ed4127c/packages/compiler/src/ml_parser/lexer.ts#L206-L207
} else if (this._tokenizeBlocks && this._attemptCharCode(chars.$AT)) {
this._consumeBlockStart(start);
basically what you see there, is that when encountering an @
, it will consume the continuing data as block.
the consumption of that block will parse as follows:
https://github.com/angular/angular/blob/c213a4e15a594ff141cf312ad301128e7ed4127c/packages/compiler/src/ml_parser/lexer.ts#L245-L277
private _consumeBlockStart(start: CharacterCursor) {
this._beginToken(TokenType.BLOCK_OPEN_START, start);
const startToken = this._endToken([this._getBlockName()]);
if (this._cursor.peek() === chars.$LPAREN) {
// Advance past the opening paren.
this._cursor.advance();
// Capture the parameters.
this._consumeBlockParameters();
// Allow spaces before the closing paren.
this._attemptCharCodeUntilFn(isNotWhitespace);
if (this._attemptCharCode(chars.$RPAREN)) {
// Allow spaces after the paren.
this._attemptCharCodeUntilFn(isNotWhitespace);
} else {
startToken.type = TokenType.INCOMPLETE_BLOCK_OPEN;
return;
}
}
if (this._attemptCharCode(chars.$LBRACE)) {
this._beginToken(TokenType.BLOCK_OPEN_END);
this._endToken([]);
} else {
startToken.type = TokenType.INCOMPLETE_BLOCK_OPEN;
}
}
private _consumeBlockEnd(start: CharacterCursor) {
this._beginToken(TokenType.BLOCK_CLOSE, start);
this._endToken([]);
}
after all the blocks of your code are parsed, it will be thrown into angulars expression parser. (since @
refers to an expression)
this will also go through, and parse the as
bindings.
https://github.com/angular/angular/blob/c213a4e15a594ff141cf312ad301128e7ed4127c/packages/compiler/src/expression_parser/parser.ts#L1145-L1180
/**
* 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, along with its
* absolute span.
*/
private parseDirectiveKeywordBindings(key: TemplateBindingIdentifier): TemplateBinding[] {
const bindings: TemplateBinding[] = [];
this.consumeOptionalCharacter(chars.$COLON); // trackBy: trackByFunction
const value = this.getDirectiveBoundTarget();
let spanEnd = this.currentAbsoluteOffset;
// 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);
if (!asBinding) {
this.consumeStatementTerminator();
spanEnd = this.currentAbsoluteOffset;
}
const sourceSpan = new AbsoluteSourceSpan(key.span.start, spanEnd);
bindings.push(new ExpressionBinding(sourceSpan, key, value));
if (asBinding) {
bindings.push(asBinding);
}
return bindings;
}
which will then go there:
https://github.com/angular/angular/blob/c213a4e15a594ff141cf312ad301128e7ed4127c/packages/compiler/src/expression_parser/parser.ts#L1202-L1223
/**
* 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, along with its absolute span.
*/
private parseAsBinding(value: TemplateBindingIdentifier): TemplateBinding|null {
if (!this.peekKeywordAs()) {
return null;
}
this.advance(); // consume the 'as' keyword
const key = this.expectTemplateBindingKey();
this.consumeStatementTerminator();
const sourceSpan = new AbsoluteSourceSpan(value.span.start, this.currentAbsoluteOffset);
return new VariableBinding(sourceSpan, key, value);
}
and will then do a variable binding as seen here:
https://github.com/angular/angular/blob/c213a4e15a594ff141cf312ad301128e7ed4127c/packages/compiler/src/expression_parser/ast.ts#L343-L374
/**
* TemplateBinding refers to a particular key-value pair in a microsyntax
* expression. A few examples are:
*
* |---------------------|--------------|---------|--------------|
* | expression | key | value | binding type |
* |---------------------|--------------|---------|--------------|
* | 1. let item | item | null | variable |
* | 2. of items | ngForOf | items | expression |
* | 3. let x = y | x | y | variable |
* | 4. index as i | i | index | variable |
* | 5. trackBy: func | ngForTrackBy | func | expression |
* | 6. *ngIf="cond" | ngIf | cond | expression |
* |---------------------|--------------|---------|--------------|
*
* (6) is a notable exception because it is a binding from the template key in
* the LHS of a HTML attribute to the expression in the RHS. All other bindings
* in the example above are derived solely from the RHS.
*/
export type TemplateBinding = VariableBinding|ExpressionBinding;
export class VariableBinding {
/**
* @param sourceSpan entire span of the binding.
* @param key name of the LHS along with its span.
* @param value optional value for the RHS along with its span.
*/
constructor(
public readonly sourceSpan: AbsoluteSourceSpan,
public readonly key: TemplateBindingIdentifier,
public readonly value: TemplateBindingIdentifier|null) {}
}
afterwards the bindings and other entities that drop out the AST compiler will be rendered here:
https://github.com/angular/angular/blob/c213a4e15a594ff141cf312ad301128e7ed4127c/packages/compiler/src/render3/view/template.ts#L2875-L2876
const {nodes, errors, styleUrls, styles, ngContentSelectors, commentNodes} = htmlAstToRender3Ast(
rootNodes, bindingParser, {collectCommentNodes: !!options.collectCommentNodes});
if you then go further into that function there, called htmlAstToRender3Ast
, you will encounter a call to HtmlAstToIvyAst
which then goes further into roughly 600 more lines of code that do yet another AST to AST convertion..
I could go further ofc, but as I said at the start, going deep will take a long time.
I hope you get a gist of how this somewhat works now.
essentially it parses this as
expression as a variable expansion, like let [foo, bar] = [1, 5]
in vanilla js. except in this case it's more like [1, 5][0] as foo
also a small reminder: all this code is ran every time you compile a template.