Skip to content

Selectorless template parsing#60724

Closed
crisbeto wants to merge 10 commits intoangular:mainfrom
crisbeto:selectorless-ast
Closed

Selectorless template parsing#60724
crisbeto wants to merge 10 commits intoangular:mainfrom
crisbeto:selectorless-ast

Conversation

@crisbeto
Copy link
Copy Markdown
Member

@crisbeto crisbeto commented Apr 3, 2025

⚠️ Disclaimer ⚠️ this PR implements syntax that is still being designed and discussed. It's an initial prototype that will allow us experiment with the runtime and run user studies.

These changes are an initial implementation of the template parsing for selectorless templates. Here's an example showing most of the syntax where we create a MatButton component as a link, we apply the HasRipple directive without any inputs and set a tooltip that's only enabled if the user doesn't have permissions to go to the admin page:

<MatButton:a href="/admin" @HasRipple @Tooltip(message="Cannot navigate" [disabled]="hasPermissions")>Admin</MatButton:a>

@crisbeto crisbeto added action: review The PR is still awaiting reviews from at least one requested reviewer target: major This PR is targeted for the next major release labels Apr 3, 2025
@angular-robot angular-robot bot added the area: compiler Issues related to `ngc`, Angular's template compiler label Apr 3, 2025
@ngbot ngbot bot added this to the Backlog milestone Apr 3, 2025
@crisbeto crisbeto marked this pull request as ready for review April 3, 2025 07:53
@kbrilla
Copy link
Copy Markdown
Contributor

kbrilla commented Apr 3, 2025

question: Does that mean that now we can have component and 2 directives that have the same input name and still apply diffrent values to them?

Like:

<MatButton:a href="https://github.com/admin" [disabled]="false" @HasRipple([disabled]="reducedMotion") @Tooltip(message="Cannot navigate" [disabled]="!hasPermissions")>Admin</MatButton:a>

@e-oz
Copy link
Copy Markdown

e-oz commented Apr 3, 2025

<MatButton:a

What element will be added to the DOM, <a>?
If MatButton is a component, not a directive, does it mean we can finally create components without a wrapping custom element?

For example, to convert this:

<ul>
  <Example:li>Content</Example:li>
</ul>

into this:

<ul>
  <li>Content</li>
</ul>

@crisbeto
Copy link
Copy Markdown
Member Author

crisbeto commented Apr 3, 2025

question: Does that mean that now we can have component and 2 directives that have the same input name and still apply diffrent values to them?

Yes, all the bindings are targeted so you can pass different values to each of them.

What element will be added to the DOM, ? If MatButton is a component, not a directive, does it mean we can finally create components without a wrapping custom element?

Yes, it will create the MatButton component on an anchor node. We're still thinking if the component should control what node types it can be applied to.

@kbrilla
Copy link
Copy Markdown
Contributor

kbrilla commented Apr 3, 2025

I supposse there will be a RFC but any thoughts about this syntax:

<a href="https://github.com/admin" [disabled]="false" [test-id]="123" @@MatButton([variant]='ghost') @HasRipple([disabled]="reducedMotion") @Tooltip(message="Cannot navigate" [disabled]="!hasPermissions")>Admin</MatButton:a>

this way we won't need to write [attr.test-id] and it will be clear what is html attribute and what are component properties , and mayby even more like

<a (click)="nativeEventHandler()"  @@MatButton((click)="matButtonClickOutputHandler()" @Directive((click)="directiveClickOutputHandler")> 

so again clear distinction between what is html event and angular output

also mayby Generic types?

@Directive()
export class SomeDirecctive<T> {
test = input<T>(); 
}

@SomeDirective<string>( [test]=2]) //error!

@crisbeto crisbeto force-pushed the selectorless-ast branch 2 times, most recently from eb3db92 to 25fd131 Compare April 3, 2025 09:41
@muuvmuuv
Copy link
Copy Markdown

muuvmuuv commented Apr 3, 2025

It think these are two features here.

Selecterless would get a thumb up from me. Really useful to keep in line with web standard tags without nested weird tag names.

The attribute style thing is sonething that needs discussion.

@Jordan-Hall
Copy link
Copy Markdown
Contributor

It think these are two features here.

Selecterless would get a thumb up from me. Really useful to keep in line with web standard tags without nested weird tag names.

The attribute style thing is sonething that needs discussion.

I agree. Selectorlews been lovely using a lite version for a year now. But don't like the @ i much rather use the use: prefix makes it cleaner in my view. Take a look at solidjs

@Deku-nattsu
Copy link
Copy Markdown

I really like the distinction between html attributes and directives because right now when a directive isn't imported the language service thinks it is a html attribute, and targeted bindings is NEEDED since it solves the overlap when combining directives that have similar inputs/output. good stuff

@splincode
Copy link
Copy Markdown
Contributor

splincode commented Apr 3, 2025

😅

<div @Dir(...props)></div>

@crisbeto
Copy link
Copy Markdown
Member Author

crisbeto commented Apr 3, 2025

Passing TGP

@waterplea
Copy link
Copy Markdown
Contributor

What element will be added to the DOM, <a>? If MatButton is a component, not a directive, does it mean we can finally create components without a wrapping custom element?

You could always do that with attribute component selectors: a[MatButton], no wrapping custom elements.

@EricPoul
Copy link
Copy Markdown

EricPoul commented Apr 3, 2025

We apply styles to taiga.ui(but can be any lib) component/directives or add functionality that we need anytime we use libs Comp/Dir just applying our directive to the same selector as libs Comp/Dir. With the selectorless approach, I don't see a way to do so without manually adding our directives whenever we use libs Comp/Dir, even though we consider it a default state.

@crisbeto crisbeto force-pushed the selectorless-ast branch 2 times, most recently from 11b96df to 426ca0f Compare April 3, 2025 15:31
@e-oz
Copy link
Copy Markdown

e-oz commented Apr 3, 2025

We're still thinking if the component should control what node types it can be applied to.

Please don't limit this; at least make such a limitation optional. The only issue I can see right now is that someone might apply it to a void element, but I think the compiler can easily report an error about that.

@Guilhermeasper
Copy link
Copy Markdown

What element will be added to the DOM, <a>? If MatButton is a component, not a directive, does it mean we can finally create components without a wrapping custom element?

You could always do that with attribute component selectors: a[MatButton], no wrapping custom elements.

With the exception of some elements like input and textarea. Will this new syntax support using input as well?

@e-oz
Copy link
Copy Markdown

e-oz commented Apr 3, 2025

You could always do that with attribute component selectors: a[MatButton], no wrapping custom elements.

You are right, @waterplea, I confused it with this:

<ul>
 <example>Content</example>
</ul>

@Component({
   template: `<li>Some: <ng-content></ng-content></li>`
})
class Example {}

and result should be:

<ul>
 <li>Some: Content</li>
</ul>

I think selectorless will not allow it either.

Will this new syntax support using input as well?

input is a void element, it can not have content. Applying components is pointless.

@pti4life
Copy link
Copy Markdown

pti4life commented Apr 3, 2025

😅

<div @Dir(...props)></div>

Is this going to be possible? :D This my biggest pain point regarding this framework 😄

@d-koppenhagen
Copy link
Copy Markdown
Contributor

Will the newly proposed Template Syntax for selectorless components/directives also apply to existing components/directives that have a selector specified? In other words, will it be backward-compatible?

I am considering reusing components/directives from older libraries or Angular versions. My idea is to ignore the selector definition when using the new approach. Is that possible?

@Martinspire
Copy link
Copy Markdown

Martinspire commented Apr 3, 2025

So why is it <MatButton:a and not <a:MatButton. From a developer perspective, it seems more logical to still go element->directive than the other way around. Especially with the a link its very easy to miss what will be the item in the DOM. Overall I'm not entirely against it, but I don't think this is easy to read compared to what we have now.

And why is it MatButton:a but also @HasRipple and @Tooltip. If we're combining directives, wouldn't it make sense to put the a tag first and just have @MatButton ? And if the use case is that is going to be replaced with why don't we use another kind of input for saying what element we want to have?

Also, something like <#a @MatButton @Tooltip seems more logical to me than the current one. And if you really want MatButton first, than even using <MatButton#a is easier to recognize and scan in a template than using :. But if directives decide that they can replace, it honestly should just be a component instead. If the underlying thought is that the : is referring to its Type, than I think it should follow the entire tag, not just the opening entry.

I can see why you'd want to replace the host with an actual element one can use but I'm not sure if this is the right direction to go for. We could have the parent decide what the element should be, but also have the component itself decide (or even give multiple variants as part of its configuration where the parent can give input to what variant you want. In cases where you'd want a button over a a link. As a common use case right now is to have a button component that replaces both of them.

Sidenote: from the Flow Syntax, I preferred it when you gave the community multiple options and didn't start with a PR but rather a discussion and made clear why you'd want it and what limitations you can think of that decide the boundaries in which the community can provide feedback.

Comment on lines 768 to 772
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain this more? I'm not really following how this happens or what the special case is. Wouldn't @Dir(someAttr) have openParens===1?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll elaborate the comment a bit more, but the explanation is that the @Dir( already got captured as other tokens (directive name and directive open) so at this point we're parsing someAttr). It'll keep parsing until it hits a termination character for an attribute name (whitespace, equals etc.). We can't just ignore all parens in attribute names, because it handles things like (click).

I'll also move the code around a bit since this is used both for attribute names and tag names. It should apply only for attributes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we update the error message? It seems like it could be more helpful. Like we know that we're expecting :button after MyComp so we could ask if they meant MyComp:button?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I added some logic to suggest the opening tag name if it's currently inside an unclosed component.

@waterplea
Copy link
Copy Markdown
Contributor

waterplea commented Apr 4, 2025

With the exception of some elements like input and textarea. Will this new syntax support using input as well?

What do you mean? You could use it on inputs too. I do that all the time.

@e-oz while it's true that input is a void element and cannot have content it can still have template as a component where you can define ng-template or structural directives to use later, for example you can have some template shown as a dropdown in some scenarios. It's a pretty neat trick that allows you to bake some additional logic in.

Please don't limit this; at least make such a limitation optional.

Limiting tags that components can be applied to absolutely makes sense. You can have a component that is applied to buttons only, because it takes care of disabled state and a version of it for links to look like buttons, for example, but it cannot be applied to random span/div.

We often have the same component twice say my-list for flat lists and my-list[labels] for grouped lists, these are 2 different components that look the same to the end user but are matched by selector. Such things should definitely still be possible, selectors are very powerful. I suppose this whole endeavor is necessary for incremental compilation or something, I just want to make sure it does not take away from our DX/UX.

So why is it <MatButton:a and not <a:MatButton. From a developer perspective, it seems more logical to still go element->directive than the other way around.

💯 @Martinspire — it definitely should be the other way around, especially keeping in mind my argument above. I want to type a and then see autocomplete for all components applicable to a link tag.

And why is it MatButton:a but also @HasRipple and @ToolTip.

Because MatButton is a component and HasRipple/Tooltip are directives. It's not "directives deciding what they should replace", it's literally just a[MatButton] component written in other syntax (and that new syntax supposedly unlocks some benefits).

We could have the parent decide what the element should be, but also have the component itself decide (or even give multiple variants as part of its configuration where the parent can give input to what variant you want. In cases where you'd want a button over a a link. As a common use case right now is to have a button component that replaces both of them.

I want to reiterate, because scanning through this discussion it seems like people are missing this: you can do selector: 'a[myButton], button[myButton]' right now and have your components applied to native tags and whatever tags you want with no intermediary custom element, you do not have to do my-button and then internally decide to use a or button tags, no need for a "button component that replaces both", it can be both.

crisbeto added 2 commits April 4, 2025 09:28
Sets up the tokenization for the new experimental selectorless components as a first step towards producing an AST.
Sets up tokenization for the new experimental directive syntax.
crisbeto added 4 commits April 4, 2025 10:30
Adds `startSourceSpan` and `endSourceSpan` to the `Content` AST node so it's consistent with the rest of the element-like nodes.
… tag name

Currently it's required to pass in the tag name when determining the security context, however with selectorless we might not have a tag name. These changes update the logic to account for it.
Adds the initial logic to produce the `Component` and `Directive` AST nodes from their equivalents in the HTML AST.
Updates the various visitors to add placeholders for the new AST nodes.
Copy link
Copy Markdown
Member Author

@crisbeto crisbeto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback has been addressed.

Comment on lines 768 to 772
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll elaborate the comment a bit more, but the explanation is that the @Dir( already got captured as other tokens (directive name and directive open) so at this point we're parsing someAttr). It'll keep parsing until it hits a termination character for an attribute name (whitespace, equals etc.). We can't just ignore all parens in attribute names, because it handles things like (click).

I'll also move the code around a bit since this is used both for attribute names and tag names. It should apply only for attributes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I added some logic to suggest the opening tag name if it's currently inside an unclosed component.

@muuvmuuv
Copy link
Copy Markdown

muuvmuuv commented Apr 4, 2025

So why is it <MatButton:a and not <a:MatButton. From a developer perspective, it seems more logical to still go element->directive than the other way around. Especially with the a link its very easy to miss what will be the item in the DOM. Overall I'm not entirely against it, but I don't think this is easy to read compared to what we have now.

I like that even more, so we can have <input:date ... /> (date beeing the component) and the component handles the other attributes, this makes it a lot more logical. No more need for ViewContainerRef.createComponent and ng-container magic and I still can inject inner contents :) love it.

@waterplea
Copy link
Copy Markdown
Contributor

@muuvmuuv what issues with VCR.createComponent and ng-container are you talking about? This changes nothing from writing <input date .../> right now.

@e-oz
Copy link
Copy Markdown

e-oz commented Apr 4, 2025

Limiting tags that components can be applied to absolutely makes sense. You can have a component that is applied to buttons only, because it takes care of disabled state and a version of it for links to look like buttons, for example, but it cannot be applied to random span/div.

I understand this, but if a user applies something like MatButton to a textarea, then the issue is with the user, not the component. I only ask to make it optional, so by default, a component could be applied to any element, but with an option to explicitly declare the set of supported elements.

@Jordan-Hall
Copy link
Copy Markdown
Contributor

One thing i cant see in the inital PR comments and reading through the code. What happening about pipes. Surely same support should be added here. If not is the idea to drop pipes for signals computed values/function calls?

@crisbeto crisbeto added action: merge The PR is ready for merge by the caretaker and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Apr 4, 2025
@alxhub
Copy link
Copy Markdown
Member

alxhub commented Apr 4, 2025

So why is it <MatButton:a and not <a:MatButton>

This would be a good topic for the RFC :) None of the syntax here is set in stone, but we need to start somewhere.

@atscott
Copy link
Copy Markdown
Contributor

atscott commented Apr 4, 2025

This PR was merged into the repository by commit 92c4123.

The changes were merged into the following branches: main

@atscott atscott closed this in e8dbc36 Apr 4, 2025
atscott pushed a commit that referenced this pull request Apr 4, 2025
Sets up tokenization for the new experimental directive syntax.

PR Close #60724
atscott pushed a commit that referenced this pull request Apr 4, 2025
…60724)

Updates the HTML AST to add nodes for components and directives. Also adds a `directives` field to `Element`.

PR Close #60724
atscott pushed a commit that referenced this pull request Apr 4, 2025
Updates the HTML parser to produce AST nodes from the tokens produced that were added to the lexer in the previous commit.

PR Close #60724
atscott pushed a commit that referenced this pull request Apr 4, 2025
Integrates the `Component` and `Directive` nodes into the various visitors.

PR Close #60724
atscott pushed a commit that referenced this pull request Apr 4, 2025
…ves (#60724)

Sets up the AST nodes for components and directives in the R3 AST.

PR Close #60724
atscott pushed a commit that referenced this pull request Apr 4, 2025
Adds `startSourceSpan` and `endSourceSpan` to the `Content` AST node so it's consistent with the rest of the element-like nodes.

PR Close #60724
atscott pushed a commit that referenced this pull request Apr 4, 2025
… tag name (#60724)

Currently it's required to pass in the tag name when determining the security context, however with selectorless we might not have a tag name. These changes update the logic to account for it.

PR Close #60724
atscott pushed a commit that referenced this pull request Apr 4, 2025
Adds the initial logic to produce the `Component` and `Directive` AST nodes from their equivalents in the HTML AST.

PR Close #60724
atscott pushed a commit that referenced this pull request Apr 4, 2025
Updates the various visitors to add placeholders for the new AST nodes.

PR Close #60724
@emiliodeg
Copy link
Copy Markdown

I like the new selector less syntax, also the host element is great and powerful, but I'm not sure about the @directive syntax. we're using @ for control flow statements. Maybe & like saying "and"

<MatButton:a href="/admin" 
&HasRipple 
&Tooltip(message="Cannot navigate" [disabled]="hasPermissions")
>Admin</MatButton:a>

So we could read it as "MatButton hosted with tag, and HasRipple, and Tooltip".

@angular-automatic-lock-bot
Copy link
Copy Markdown

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators May 8, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

action: merge The PR is ready for merge by the caretaker area: compiler Issues related to `ngc`, Angular's template compiler target: major This PR is targeted for the next major release

Projects

None yet

Development

Successfully merging this pull request may close these issues.