Replies: 163 comments 479 replies
-
Happy to see we have improvements on the side of trackby. The previous implementation was so off putting ! I see this new synthax as a big DX improvement 🔥🔥 |
Beta Was this translation helpful? Give feedback.
-
About the syntax not being as HTML elements. ¿Have been there any other considerations about how this affects other IDEs outside VS Code language servers? I have no problem sometimes using a simple HTML editor to edit a template and have collapsible control structures in the editor because they were added to elements, now that they will look like any other CDATA on the HTML file, editors must be adapted to understand that a big {#if.... content can be collapsed. |
Beta Was this translation helpful? Give feedback.
{{title}}
{{editor}}'s edit
{{editor}}'s edit
-
Is there any chance we could see this applied to content projection ? Generally speaking, is there any plan to extend this structural directive remodeling to other Angular-specific matters ? |
Beta Was this translation helpful? Give feedback.
-
Great, amazing, awesome, and brilliant idea! The syntax is great!
No, please leave them optional.
There might be cases where objects are not simple structures where you could use a key to distinguish the objects. Sometimes the primary key is composite, sometimes the difference might be buried deep down in the nested structures. For example, if one object has an array of values as a value of a deeply nested key, and the second object has a different value in one of the items of that array, then two objects should be considered different, and I understand that it's a bit complicated example, but my point is: there should be some built-in way of declaring that if objects are not equal by reference, then they are not identical (a===b), even for zone-based components. Since Please: a) consider creating either a built-in way to re-render Once again: incredibly great initiative, thank you so much! |
Beta Was this translation helpful? Give feedback.
-
With the switch, could we get some way of getting errors when a case is left out. |
Beta Was this translation helpful? Give feedback.
{{title}}
{{editor}}'s edit
{{editor}}'s edit
-
1A: I think
would look better than
2A: I think it should be required, also to use const with the for loop
looks better than this
and the track part is a bit weird, can it be done in another line like for example?
so overall of 1A and 2A would look like
instead of
3A:
and how it will be automically migrated? 4A: no additional question, will the new syntax narrow down the Signal calls? |
Beta Was this translation helpful? Give feedback.
-
Looks great! In my opinion, it improve readability and it's easier to distinguish between the control flow parts and rest of the code.
If parentheses would be optional, then I would turn this option on. They might also increase readability of statements in some cases. |
Beta Was this translation helpful? Give feedback.
-
I think you need to remove |
Beta Was this translation helpful? Give feedback.
{{title}}
{{editor}}'s edit
{{editor}}'s edit
-
I'm very excited about this, especially
No, the fact that this matches Svelte is enough of a reason to go with this syntax imo.
I'd prefer them to be optional.
I've written a custom list implementation that tracks changes to itself, and a custom IterableDiffer that reads those changes instead of performing a real diff, similar to https://blog.mgechev.com/2017/11/14/angular-iterablediffer-keyvaluediffer-custom-differ-track-by-fn-performance/. I think what this RFC describes would allow me to replace that with an |
Beta Was this translation helpful? Give feedback.
-
1A: Curly braces feel familliar; |
Beta Was this translation helpful? Give feedback.
-
Syntax looks powerful, but with this I think we should use new template extension like .angular , so all IDEs can easily identify & format it. |
Beta Was this translation helpful? Give feedback.
-
Very cool to see some new ideas popping up! 🔥
From the short examples shown so far I think it's looking good. My only concern is readability in templates when you have more complex blocks with other bindings, example: {#for user of users(); track user.id}
<app-user-comp [class.is-you]="user.id === auth$.user().id" [user]="user">
<div class="custom-user-template">
{#if user.imgSrc}
<img [ngSrc]="user.imgSrc" loading="eager" />
{:else}
<img ngSrc="avatar.png" loading="eager" />
{/if}
</div>
</app-user-comp>
{:empty}
<app-card i18n>No users added yes :(</app-card>
{/for} But I must say, writing this syntax feels good, but remembering when to use
This one is hard. On one side you want examples and blog posts to be streamlined, but you also want the flexibility for developers. In this case, I think we should keep it simple and only go without parentheses.
I think in cases where you are dealing with small data amounts we shouldn't be forced to provide a Perhaps a special class provided by the Angular team, implementing
In an app with over 600 components, I've used it two times to overcome some complex problems. I think it can be solved in better ways today so I would say no.
Supporting things like for-await-of loops would be really cool to lazy load content from an api and yield faster load times. Very much an edge case but looping two-dimensional arrays, is that something which potentially could be simplified? {#for y of rows() & x of y.columns()}
<div [class.y]="y" [class.x]="x"></div>
{/for}
Regarding this. One of the very strong aspects of Angular imo is the reduced nesting with the help of structrual directives. This has really help me keeping components readable. In general I think avoiding nesting is a good thing for cleaner code and given that html is already a heavily nested language, this syntax might make it worse.
I think this area is super interesting and healthy for the Angular community. As soon as I read about the |
Beta Was this translation helpful? Give feedback.
{{title}}
{{editor}}'s edit
{{editor}}'s edit
-
I must start by saying that after working with angular ever since it was forged - i had to Think Long and hard about these suggestion/RFC I like that its a known syntax outside the angular ecosystem mustache/handlebars/svelte I feel so good about just jamming an *ngIf on my list item and not having to wrapping it an extra time - i dont know if its worth it - i would have to try both options Lets keep '{{}}' its just tapping the same button twice instead of having to reach for another button i Think it Will slow down development Could we have support for ng-content(and slots) and ng-template in the same syntax just for streamlining code and lower the overall complexity and things people have to learn Big fan of the suggested trackby syntax! Would it by any chance make sense to bring back the Old "ng-init" but in a new form factor? Okay im stopping here but i might extend with another comment later ✌️🔥 Love the angularMomentum! 🔥🔥 |
Beta Was this translation helpful? Give feedback.
-
Awesome changes recently and this for sure is another hit! 🔥 One question: |
Beta Was this translation helpful? Give feedback.
-
Will the new switch syntax enable multiple case evaluations for one block? Something like this:
|
Beta Was this translation helpful? Give feedback.
-
I like this. Indeed syntatically more cleaner,
However, for only one line in the if block, thought would be better to make braces optional. Such as
or even
|
Beta Was this translation helpful? Give feedback.
-
I like this. Indeed syntatically more cleaner,
However, for only one line in the if block, thought would be better to make braces optional. Such as
or even
|
Beta Was this translation helpful? Give feedback.
-
I also support the idea of built-in control flow, but big thumbs up for @ syntax. It is also close to the syntax used by razor pages. If an incentive is to
OR
|
Beta Was this translation helpful? Give feedback.
{{title}}
{{editor}}'s edit
{{editor}}'s edit
-
Using @ with brackets would also automatically solve the "one liner":
|
Beta Was this translation helpful? Give feedback.
{{title}}
-
Authors: @alxhub @pkozlowski-opensource @jelbourn
Area: Angular Framework
Posted: June 14, 2023
Status: Open
This RFC proposes a new control flow syntax for Angular, and represents a significant change to how we approach control flow in the framework. There are two questions which should be answered up front: why and why now? Let’s start with why:
A core goal of Angular's initial design is that template control flow would be expressed via composition of user-facing concepts only. The structural directive concept serves this purpose: a directive that sits on an
<ng-template>
declaration and determines when & where embedded views are created from that template. To achieve a familiar syntax for control flow similar to that of JavaScript (e.g.let user of users
), microsyntax in the template language provides an alternative binding syntax for configuring a structural directive and its inputs.Our review of developer experience pain points in Angular has highlighted microsyntax-based control flow as having significant weaknesses compared to syntaxes in other frameworks. The proposed built-in control flow syntax addresses these issues and significantly improves the developer experience, in addition to being a foundation for new features.
As for why now, as a part of our ongoing work on implementing the Signals RFC, we’ve known that the existing zone-based control flow directives would not be able to function in zoneless applications. We will need to implement reactive control flow for zoneless applications. We considered modifying the existing control flow directives to support zoneless applications as well, but decided against this option for a couple reasons:
Since every existing Angular application depends on these directives, even the slightest incidental change in behavior could be breaking.
Supporting both signal and zone based patterns in a single directive would greatly complicate the code, and it’s unlikely that we could properly tree-shake either pattern if it wasn’t used.
We knew we wanted to make bigger improvements to control flow anyway.
We also considered implementing new reactive control flow directives just for signal components, but rejected this idea as we didn’t want to create more differences between signal and zone components than necessary.
As a result, we decided to move forward with this design of built-in control flow for templates, to both support the zoneless/signal work as well as address the longstanding DX issues with structural directives.
Goals and Non-Goals
The new syntax for template control flow has several major goals:
if
vsfor
vsswitch
).It is useful to explicitly mention some non-goals of the new syntax:
Overview
Many templating languages in the open source space have control flow primitives, and we’re fortunate to be able to draw on those ideas and designs from the larger web community. The proposed new control flow syntax is heavily inspired by Svelte’s control flow, as well as the
mustache
templating language.The new control flow syntax is introduced in the form of blocks, a new syntactic structure in templates.
Conditional control flow uses blocks with the keywords
if
andelse
:Switch control flow uses blocks with the keywords
switch
,case
, anddefault
:Angular’s
switch
does not have fallthrough (there is nobreak
operation required).Loop control flow uses blocks with the keywords
for
andempty
:Detailed Design
Syntax
The primary syntactic element of the new control flow design is a named block group composed of one or more named blocks. Block groups are template nodes, occupying the same syntactic positions as elements, text nodes, and text bindings. Each block in a block group contains Angular template syntax within it, as a list of child template nodes (potentially including additional block groups).
Each block group begins with a tag of the form
{#name}
and ends with a matching closing tag{/name}
. Tags can contain specific syntax based on their name. As an example, the basicif
control flow structure looks like: \Every block group also defines a named block with the same name as the block group, known as the primary block. The above example thus represents a block group (
if
) with a single primary block (if
), which has the<child-cmp>
element as a child.This syntax is, of course, a blocking feature.
Discussion Question 1A: should we consider other syntax besides curly braces for the block tags, or besides
#
,/
and:
? E.g.[if cond]
?Tag syntax
Each named block group can customize the tag syntax following the tag name and a space. For example, the
if
block group supports an expression following theif
, which is used as the if condition.Different block groups may use this syntactic space in different ways.
Additional named blocks
In addition to the primary named block created by the block group tag itself, additional blocks may be defined within a block group. Additional blocks are specified with a tag that starts with
:
instead of#
. Blocks (including the primary block) follow a set of syntactic rules:For example, an
if
block group may define an optionalelse
block:The above syntax creates an
if
block group with two blocks: the primaryif
block created by the block group (containing the<true-cmp>
) and anelse
block containing the<false-cmp>
.Like the primary block tag, the syntactic space following the name of an additional block is entirely customizable depending on the block group type.
Optional parentheses
In JavaScript, control flow statements require parentheses (
if (cond)
). Because block tags are wrapped in braces already, they don't require parentheses. However, we support including parentheses if desired:If used, the parentheses wrap the entire body of the block tag.
Discussion Question 2A: should parentheses be required to maximize similarity to JavaScript/TypeScript?
Nesting
Block groups are "nodes" in templates and can be children of blocks in other block groups. Control flow structures can therefore be nested.
Why
#
at all?There are two main reasons why we chose to use a hieroglyphic (
#
) instead of bare single-curly statements (e.g.{if cond.expr}
):{...}
is used today for the ICU Message format which supports several i18n features (eg: pluralization).if
block - conditionalsThe
if
block replaces*ngIf
for expressing conditional parts of the UI.The primary
if
block includes an expression representing the condition under which this block will be displayed.{#if a > b} {{a}} is greater than {{b}} {/if}
if
block groups may also contain one or moreelse
blocks. One bareelse
block is allowed, as well as zero or moreelse
blocks with an additional condition (indicated by anif
keyword following theelse
name:Aliasing the conditional expression
The current
*ngIf
supports aliasing of the condition expression viaas
: \To enable direct automatic migration from
*ngIf
to the new built in conditional statement, aliasing will be supported inif
:Aliasing only supported for zone components
Because signals are synchronous and side effect free, there is no harm in reading their value in multiple places in the template. In the case of complex conditions or conditions that involve expensive calculations,
computed
can be leveraged for memoization and de-repetition. For that reason, we feel there isn't a strong use case for aliasing the value of the condition expression in signal components (that isn't better served bycomputed
).Why not
cond as name
, similarly to microsyntax?A long-term goal for future evolution of Angular's template syntax is to make expressions "just TypeScript" (or at least a subset of TypeScript). Because
as
is used in TypeScript to represent typecasts, we want to allow for expressions including typecasts in the future.for
block - repeatersThe
for
block replaces*ngFor
for iteration, and has several differences compared to its structural directive predecessor. A basicfor
block group looks like:track
keys for diffingThe
track
setting replacesNgFor
's concept of atrackBy
function. It determines the row key whichfor
will use to associate array items with the views it creates, moving them around as needed. Becausefor
is built-in, we can provide a better experience than passing atrackBy
function, and use an expression representing the key instead.Migrating from
trackBy
totrack
is possible by invoking thetrackBy
function:The new syntax helps the _run_time by offering you a track and field to run around your loops.
$index
and other variablesWithin
for
row views, there are several implicit variables which are always available:$index
$first
$last
$even
$odd
These variables are always available with these names, but can be aliased via a
let
segment:Aliasing is useful when nesting
for
loops, since the inner loop's implicit variables shadow those from the outer loop.track
is requiredIn our performance research, we've identified
NgFor
loops over immutable data withouttrackBy
as one of the most common causes for performance issues across Angular applications. Because of the potential for poor performance, we’re makingtrack
required forfor
loops.There is one exception to this requirement: for
for
usages in signal components, when the iterable expression is assignable toIterable<Signal<unknown>>
, thentrack
is not required (and in fact, not allowed). That's because when individual rows use signals for reactivity, we want the outer loop to track those signals by their identity and not depend on their values. This allows individual rows to update without triggering diffing of the list.Cross-country is not required though.
Discussion Question 3A: are there technical objections to requiring
track
, or other ways to solve performance issues withNgFor
that we may not have considered?empty
blockNew to
for
is support for a template to render when there are no items in a list. This is a common community feature request.IterableDiffers
Unlike its structural directive predecessor, the new
for
will not use Angular’s customizable diffing implementation (IterableDiffers
), but instead will use a new optimized algorithm (which would not be customizable).Discussion Question 4A: do you have a use case for customizing the diffing algorithm by customizing
IterableDiffers
today?for
in signal componentsIn signal-based components,
for
is reactive. The iteration variable and all of the special variables created for each row are signals instead of plain values, and must be unwrapped via their getter. This allows each row created byfor
to change detect independently of the view containingfor
, and allowsfor
to work in zoneless applications.switch
block - selectionThe syntax for
switch
is very similar toif
, and is inspired by the JavaScriptswitch
statement:#switch
has several major benefits overNgSwitch
:Note that the primary block within the
switch
group is unused. It will be an error to have any child nodes inside that block.switch
does not have fallthrough (there is nobreak
operation required).Migration Considerations
It should be possible to write an automated migration schematic which converts from
NgIf
,NgForOf
, andNgSwitch
to their equivalents in the new syntax, with a few exceptions which this section considers:Observation of structural directive presence
Existing code may look for the presence of
NgIf
,NgForOf
, orNgSwitch
directives explicitly. This could take a few forms:ViewChild
queries to find control flow directives.<ng-template>
.Iterable Differs
Angular makes it possible to customize the diffing algorithm used by
NgForOf
via theIterableDiffer
interface and associated DI token.Theoretically an application could customize diffing in a way that would break if migrated to use
for
. This could result in strange behavior when the iterable changes or, in the worst case, a complete failure to render.Future Opportunities
This section captures potential future improvements which aren't in scope for this design, but could be achieved on top of this syntax in the future.
for..in
and other loop stylesNot in scope for this design is support for other iteration flavors in JS, such as
for..in
loops, async iteration, etc. Such additions would theoretically be possible under the current syntax. So for now, Angular will see these other loop styles as…for-in.Discussion Question 5A: are there other flavors of looping which would benefit your applications today?
Virtual scrolling
We could extend the syntax of
for
to support some kind of abstraction for a viewport and scrolling controller, that would enable virtual scrolling on top of the built in syntax.Destructuring
We could support binding patterns (destructuring) as JS loops do.
Alternatives Considered
HTML-based control flow
Instead of the mustache-inspired syntax
{#if …}
, we considered using HTML syntax such as tags prefixed with theng
namespace:This concept was rejected for several reasons:
For example, the condition of
<ng:if>
would likely be expressed as<ng:if cond="expr">
which looks less like JS control flow.An if-else block would take the form:
This has a lot of visual overhead compared to the proposed block language.
Continuing with Microsyntax
Another option considered and not taken was to continue using Angular microsyntax to express control flow in templates. Our compiler could detect usages of
*ngIf
,*ngFor
, and*ngSwitch
and generate special code for them. We could use this mechanism to circumvent some of the limitations of the directive-based implementations of control flow, while not introducing new syntax to learn.We rejected this option because improving the DX of control flow beyond the microsyntax form is one of the main goals of introducing built-in control flow. Having built in implementations of the original structural directives would also introduce magic which might confuse advanced users, as these directives would no longer behave according to the mental model for regular structural directives.
The microsyntax model also does not support multiple associated sub-templates for a control flow statement, so we would not solve the problem of
*ngIf
'selse
case being extremely awkward to use.Frequently Asked Questions
Q: Will the existing structural directives (
NgIf
, etc) continue to work?Ideally, it will be possible to replace most usages of the existing structural directives from
@angular/common
with the new syntax, and we will have an automatic migration to do so. Given that, we plan to strongly encourage developers to switch to the new control flow syntax to take advantage of the developer experience improvements.That said, we're also aware that all existing Angular content (blogs, books, videos, etc) will continue to reference the existing directives for a long time. Currently the plan is to deprecate the existing directives but not remove them until the ecosystem (including community-authored content) has switched over to the new control flow. Ultimately, we will base this timeline on the feedback we receive from the community.
In the rare cases where automatic migration fails (e.g. for
NgForOf
cases discussed above) we could apply similar techniques that we’ve used before in CDK/Material, such as generating a clone of the structural directive in the user’s project.Q: Will the structural directive concept itself be removed?
No, structural directives are an essential feature for application architecture in Angular, and we have no plans to remove them.
Q: Will the new control flow be syntax highlighted?
Yes, the Angular Language Service will highlight the keywords and expressions within the new control flow blocks.
Q: Will the new control flow affect query results at all?
Query results will work the same way as with the existing
ngIf
,ngFor
, andngSwitch
Q: Will the new control flow still define & create embedded views?
Yes, the technical internals of the new control flow will use the same concepts as the structural directives, including
<ng-template>
s and view containers.Q: Will I still need to import the new control flow into my components?
No, it will be built into the template language and automatically available to all components.
Q: Will the new control flow be as tree-shakable as the previous version?
Yes, if an application doesn’t use a particular block group (e.g.
switch
), its code will not be included in the production bundle.Q: Will the new control flow have better performance?
There may be some marginal improvements, especially for
for
and diffing. We expect larger memory usage improvements.We may also be able to implement compiler optimizations in the future that would not have been possible with the previous user-land control flow. For example, we know that
if
’s embedded view is only created once, which might allow us to optimize some data structures.Q: Why are we planning more changes instead of delivering on signals?
The existing control flow directives are based on zones, and so changes to control flow (particularly
for
) are required to support fully zoneless applications. Given that we need to change control flow anyway, we’re taking the opportunity to update the design and address other commonly reported DX pain points as well.Note the section on
for
exposing its loop variable as a signal in signal-based components above. This is required to properly notify each child row’s view when its data changes (and it must be change detected).Q: Can libraries define their own block groups?
For now, no – block groups will be a template language builtin, and not a user extension point. We are interested in researching whether it makes sense to expose some kind of extensibility here, what use cases there might be, and what form such a feature might take.
Q: Can I add directives to the new control flow blocks?
No. This is another area where we’d like to research potential use cases.
Q: What about the CDK’s virtual scrolling functionality?
The CDK will continue to provide its existing structural directives for virtual scrolling and other use cases for the time being. We will be researching whether it makes sense to either convert some of the CDK’s structural directives to built-in syntax, or to add extension points to the existing syntax for the CDK to build on (for example, to support virtual scrolling in
for
).Q: Is this syntax valid HTML?
It is valid HTML, in the sense that to an HTML parser block groups are indistinguishable from plain text nodes (like Angular text bindings). We did explore using HTML pseudo-elements for control flow instead but rejected that design for several reasons (see the Alternatives Considered section above for details).
Q: Will there be other built-in blocks in the future?
Maybe 😉 we're
#defer
ring this decision.Beta Was this translation helpful? Give feedback.
All reactions