Skip to content

feat(compiler): allow self-closing tags on custom elements #48535

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from

Conversation

crisbeto
Copy link
Member

@crisbeto crisbeto commented Dec 19, 2022

Allows for self-closing tags to be used for non-native tag names, e.g. <foo [input]="bar"></foo> can now be written as <foo [input]="bar"/>. Native tag names still have to have closing tags.

Fixes #39525.

@angular-robot angular-robot bot added the detected: feature PR contains a feature commit label Dec 19, 2022
@e-oz
Copy link

e-oz commented Dec 19, 2022

I’m sorry if the answer to my question is in the source code: will the compiler expand self-closed tags to regular tags to don't break the specification for custom elements?

@crisbeto
Copy link
Member Author

Yes, <foo/> will be treated the same as <foo></foo>.

@crisbeto crisbeto added action: review The PR is still awaiting reviews from at least one requested reviewer area: compiler Issues related to `ngc`, Angular's template compiler target: minor This PR is targeted for the next minor release labels Dec 19, 2022
@ngbot ngbot bot modified the milestone: Backlog Dec 19, 2022
@crisbeto crisbeto requested review from alxhub and jelbourn December 19, 2022 13:59
@crisbeto crisbeto marked this pull request as ready for review December 19, 2022 13:59
@Yogu
Copy link
Contributor

Yogu commented Dec 19, 2022

In the issue you linked, @IgorMinar explained why this feature could not "just be added" and outlined that there would be an impact to the ecosystem.

There are also additional considerations around template editing, syntax highlighting, autocompletion, linting etc - all of these tools nowadays work with Angular because they already support standard html. Once we move away from the spec, there is a possibility that common developer workflows could be affected or outright broken.

As far as I understand, supporting self-closing tags (that do not exist in HTML) would mean that it would no longer be possible to use HTML parsers like the DOM to parse Angular templates.

Did you evaluate the impact of this change and think about how it could be rolled out smoothly?

@e-oz
Copy link

e-oz commented Dec 19, 2022

@Yogu from the other side, developers are not forced to use self-closed tags only. While tools are being updated to this change, we can still use our existing code and write regular tags. If Angular will not make a step in this direction, tools will never follow.

@crisbeto
Copy link
Member Author

Angular templates aren't being parsed by the DOM. We have our own parser and compiler that turn them into JavaScript function calls.

@TillFProtzek
Copy link

TillFProtzek commented Dec 19, 2022

Angular templates aren't being parsed by the DOM. We have our own parser and compiler that turn them into JavaScript function calls.

Sorry, but this full on ignores the main point. This introduces the first HTML non-compliant change. That means that all existing tooling that depends on HTML compliance of Angular templates needs to be changed/rewritten to support this feature.
You are willingly sacrifice the Angular HTML compliance of templates, developers could count on till now, for a little bit of syntactic sugar?

I cite from the official Angular documentation itself:

Template syntax
In Angular, a template is a chunk of HTML

@e-oz
Copy link

e-oz commented Dec 19, 2022

@TillFProtzek current capabilities of tooling should not stop Angular evolution. Tools should adapt to changes and evolution, that's the reality. Somehow they adapted to standalone components, directive selectors, (event) [binding] syntax - they weren't stopped by the fact that it's not part of HTML.

And existing code will not be converted to "self-closing tags only" overnight.

@kroeder
Copy link
Contributor

kroeder commented Dec 19, 2022

What parser should people use when they write update schematics? We currently use parse5 for that. Is it possible to use the angular parser for that, does it have public api?

@TillFProtzek
Copy link

@e-oz it's not the point if something was part or not part of HTML. All syntax extension of Angular are/were HTML syntax compliant. All points you mentioned are HTML syntax compliant. Templates until now could be parsed by any HTML-parser.

It seems that the full scope of that change is missed in regards of reward/risk. Formatters, Linters, sophisticated dev-tools - all have to change if the developers start to use the feature or adapt a template using it.

@samclaus the originator of the feature-request got convinced in the discussion there that it is perhaps not a good idea. So convinced that the only mention of the feature request afterwards, before this pull request out of the blue, is from himself. Pointing out again to what he learned about the risk of changing HTML compliance of the templates.

@alfaproject
Copy link
Contributor

I'm also not sure where this PR is coming from? Angular HTML template files are HTML-spec compliant and I believe that was the point of even using HTML to begin with as opposed to JSX or something else, right?

@alxhub
Copy link
Member

alxhub commented Dec 19, 2022

Hello all! I'm super excited for this feature and I love to see the discussion here.

Yes, this is a departure from templates being strictly HTML-compliant. We're fully aware of that - it's an intentional choice on our part, after thoughtful analysis of the tradeoffs involved. We're not making this decision purely for the syntactic sugar of <my-cmp />, either - there are good reasons to treat Angular templates as their own HTML-derived language.

It enables some bigger DX improvements in templates

Syntactic sugar for self-closing tags is a small DX improvement of its own, but it's not the main motivation here. We're looking at a number of other improvements we can make to improve the template authoring experience. Things like improving the ergonomics of control flow, resolving longstanding inconsistencies in some of the binding syntax, and improving support for TypeScript as well as other tooling such as linters - all of which can benefit from native syntax in templates.

These are early thoughts, and will be bringing these kinds of changes to RFC as we nail down the details (although these things take time).

Templates already are their own language

Today, Angular templates are syntactically HTML, but semantically distinct. Even if [input]="expression" is a valid syntax for an element attribute, it means something different in Angular than it does in HTML. The same goes for {{ 'interpolations' }} - there's a whole sub-language inside of these syntaxes that's invisible to any tool which treats the template as plain HTML. It's true that simply parsing templates as HTML doesn't cause any problems, but it's easy for tooling to get into trouble when interpreting the parsed template with HTML semantics - that can lead to subtle bugs.

Consider an HTML formatter running on the template expression {{ data.replace(' ', '-') }} (replacing a sequence of 4 spaces in some string with a dash). HTML formatters aren't aware of the switch in language context in what they consider to be plain text. In HTML, multiple spaces in a row can be collapsed into one, so the formatter may choose to "improve" this template by removing the extraneous spaces, unaware that it is altering the meaning of the template to Angular in the process. This may not seem like a huge problem, but it's an example of how treating Angular templates as plain HTML in tooling is fundamentally unsound today.

In practice, we've seen some tools avoid this problem by using Angular's own template parser to parse templates. The popular angular-eslint plugin does this, for example. In the future, we want to make it easier for other tools to do the same, and give the ecosystem APIs to work with Angular templates both syntactically and semantically.

This specific change is minimally impactful

Self-closing tags are an optional language feature. If a project wants to enforce that its templates are parseable as plain HTML, it can do so (likely via a lint rule).

All tooling and parsers we've tested has been perfectly usable with self-closing component tags. In fact, they're often tripped up by other aspects of Angular syntax (case sensitivity, [property] and (event) bindings, etc).

Summary

To be clear, we like HTML. Angular templates are HTML-flavored, and we have no plans to change that - writing templates should feel like writing HTML. But Angular templates are special already, and the ergonomic benefits of relaxing strict compliance with the HTML spec from a syntactic standpoint are significant.

Copy link
Member

@jelbourn jelbourn left a comment

Choose a reason for hiding this comment

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

LGTM

@alxhub already mentioned most of what I would have said here.

The idea of "valid HTML templates" has historically been an important one to make sure that HTML documents rendered by the browser render as intended. Angular templates, though, are never rendered directly by the browser. An Angular template is always compiled into JavaScript instructions that manually create a DOM structure (via underlying APIs like document.createElement). While some developers may appreciate the concept of concept/experience of authoring spec-valid HTML, it does not actually have any bearing whatsoever on what's ultimately rendered into the DOM.

This feature in particular is completely optional, and people can continue using closing tags for custom elements, so we don't see this as breaking.

While we do expect to make changes to the template syntax over the years, we will always pay close attention to the stability and backwards of the ecosystem.

@Yogu
Copy link
Contributor

Yogu commented Dec 19, 2022

Thank you for these detailed responses! I see that you're not making this decision lightheartedly. I just would like to draw attention to the question asked by @kroeder again:

What parser should people use when they write update schematics? We currently use parse5 for that. Is it possible to use the angular parser for that, does it have public api?

If library and tool authors are expected to support the new template syntax, it would be very helpful to provide a stable parser (and modification / printer) API. This is especially relevant when writing schematics that update templates which previously could use standard HTML parsers and printers. While a team developing an app can just decide not to use this new feature, library and tool authors are not in this luxurious position - if they want to claim say Angular 16 compatibility, they need to support the new template syntax.

Edit: Sorry, I somehow missed that @alxhub already addressed this point and provided angular-eslint as an example. The package @angular/compiler currently does not have a public API

All compiler apis are currently considered experimental and private!

If it's already planned to make some of these APIs public, maybe it would make sense to align the template syntax changes with the release of these public APIs? (Or keep the new template syntax behind an experimental compiler options so libraries and tools could still claim Angular compatibility while they don't support it yet)

All tooling and parsers we've tested has been perfectly usable with self-closing component tags.

The html parser parse5 is not (demonstration) and it would not be HTML-compliant if it was.

@crisbeto crisbeto changed the title feat(compiler): allow self-closing tags feat(compiler): allow self-closing tags on custom elements Dec 20, 2022
@@ -138,9 +143,15 @@ export function getHtmlTagDefinition(tagName: string): HtmlTagDefinition {
'textarea': new HtmlTagDefinition(
{contentType: TagContentType.ESCAPABLE_RAW_TEXT, ignoreFirstLf: true}),
};

new DomElementSchemaRegistry().allKnownElementNames().forEach(knownTagName => {
Copy link
Member Author

Choose a reason for hiding this comment

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

The change here and to the DEFAULT_TAG_DEFINITION are the key ones. Now we mark any element from the DOM schema, that hasn't been added to the tag definition already, as not being able to self-close, whereas all unknown elements are allowed to self-close.

@@ -172,22 +170,6 @@ export class CssSelector {
this.element = element;
}

/** Gets a template string for an element that matches the selector. */
Copy link
Member Author

Choose a reason for hiding this comment

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

The context here is that this would've caused a circular import with my latest changes. I removed the code, because it wasn't being referenced anywhere except the unit tests for the method.

Copy link
Member

Choose a reason for hiding this comment

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

👍

@crisbeto
Copy link
Member Author

crisbeto commented Dec 20, 2022

@alxhub I've reworked the PR so that only non-native elements allow self-closing.

@crisbeto crisbeto force-pushed the self-closing-tags branch 2 times, most recently from d5b7294 to ddf2559 Compare December 20, 2022 06:05
Allows for self-closing tags to be used for non-native tag names, e.g. `<foo [input]="bar"></foo>` can now be written as `<foo [input]="bar"/>`. Native tag names still have to have closing tags.

Fixes angular#39525.
@@ -172,22 +170,6 @@ export class CssSelector {
this.element = element;
}

/** Gets a template string for an element that matches the selector. */
Copy link
Member

Choose a reason for hiding this comment

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

👍

@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 Dec 27, 2022
@bob-watson
Copy link
Contributor

Is there any (new or update to existing) documentation to accompany this new feature?
I don't see any in this PR.

@AndrewKushnir
Copy link
Contributor

@crisbeto quick question: it looks like this change covers DOM elements, should we allow <ng-content> and <ng-container> to be self-closing as well (it's less applicable to <ng-template>)? I think it'd be great to allow this, especially for <ng-content> that doesn't have any child elements. This is not a blocker for this PR, we can create a followup PR if needed.

@crisbeto
Copy link
Member Author

@AndrewKushnir unless I'm missing something, this should allow ng-content and ng-container to be self-closing already. The change marks all known node types as NOT allowing self closing while all unknown ones (such as ng-content) can be self-closing.

@AndrewKushnir
Copy link
Contributor

@crisbeto yeah, you are right, thanks for the reply 👍

@e-oz
Copy link

e-oz commented Dec 29, 2022

I hope "path" is not in the list of known tags - it would help SVG components.

@alxhub
Copy link
Member

alxhub commented Jan 4, 2023

This PR was merged into the repository by commit a532d71.

@alxhub alxhub closed this in a532d71 Jan 4, 2023
crapStone pushed a commit to Calciumdibromid/CaBr2 that referenced this pull request Jan 12, 2023
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@angular/animations](https://github.com/angular/angular) | dependencies | minor | [`15.0.4` -> `15.1.0`](https://renovatebot.com/diffs/npm/@angular%2fanimations/15.0.4/15.1.0) |
| [@angular/common](https://github.com/angular/angular) | dependencies | minor | [`15.0.4` -> `15.1.0`](https://renovatebot.com/diffs/npm/@angular%2fcommon/15.0.4/15.1.0) |
| [@angular/compiler](https://github.com/angular/angular) | dependencies | minor | [`15.0.4` -> `15.1.0`](https://renovatebot.com/diffs/npm/@angular%2fcompiler/15.0.4/15.1.0) |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/main/packages/compiler-cli) ([source](https://github.com/angular/angular)) | devDependencies | minor | [`15.0.4` -> `15.1.0`](https://renovatebot.com/diffs/npm/@angular%2fcompiler-cli/15.0.4/15.1.0) |
| [@angular/core](https://github.com/angular/angular) | dependencies | minor | [`15.0.4` -> `15.1.0`](https://renovatebot.com/diffs/npm/@angular%2fcore/15.0.4/15.1.0) |
| [@angular/forms](https://github.com/angular/angular) | dependencies | minor | [`15.0.4` -> `15.1.0`](https://renovatebot.com/diffs/npm/@angular%2fforms/15.0.4/15.1.0) |
| [@angular/platform-browser](https://github.com/angular/angular) | dependencies | minor | [`15.0.4` -> `15.1.0`](https://renovatebot.com/diffs/npm/@angular%2fplatform-browser/15.0.4/15.1.0) |
| [@angular/platform-browser-dynamic](https://github.com/angular/angular) | dependencies | minor | [`15.0.4` -> `15.1.0`](https://renovatebot.com/diffs/npm/@angular%2fplatform-browser-dynamic/15.0.4/15.1.0) |

---

### Release Notes

<details>
<summary>angular/angular</summary>

### [`v15.1.0`](https://github.com/angular/angular/blob/HEAD/CHANGELOG.md#&#8203;1510-2023-01-10)

[Compare Source](angular/angular@15.0.4...15.1.0)

#### Deprecations

##### router

-   CanLoad guards in the Router are deprecated. Use CanMatch
    instead.
-   router writable properties

    The following strategies are meant to be configured by registering the
    application strategy in DI via the `providers` in the root `NgModule` or
    `bootstrapApplication`:

    -   `routeReuseStrategy`
    -   `titleStrategy`
    -   `urlHandlingStrategy`

    The following options are meant to be configured using the options
    available in `RouterModule.forRoot` or `provideRouter`.

    -   `onSameUrlNavigation`
    -   `paramsInheritanceStrategy`
    -   `urlUpdateStrategy`
    -   `canceledNavigationResolution`

    The following options are available in `RouterModule.forRoot` but not
    available in `provideRouter`:

    -   `malformedUriErrorHandler` - This was found to not be used anywhere
        internally.
    -   `errorHandler` - Developers can instead subscribe to `Router.events`
        and filter for `NavigationError`.

##### common

| Commit | Type | Description |
| -- | -- | -- |
| [fe50813664](angular/angular@fe50813) | feat | Add BrowserPlatformLocation to the public API ([#&#8203;48488](angular/angular#48488)) |
| [2f4f0638c7](angular/angular@2f4f063) | fix | Add data attribtue to NgOptimizedImage ([#&#8203;48497](angular/angular#48497)) |

##### compiler

| Commit | Type | Description |
| -- | -- | -- |
| [a532d71975](angular/angular@a532d71) | feat | allow self-closing tags on custom elements ([#&#8203;48535](angular/angular#48535)) |
| [caf7228f8a](angular/angular@caf7228) | fix | resolve deprecation warning ([#&#8203;48652](angular/angular#48652)) |
| [33f35b04ef](angular/angular@33f35b0) | fix | type-only symbols incorrectly retained when downlevelling custom decorators ([#&#8203;48638](angular/angular#48638)) |

##### compiler-cli

| Commit | Type | Description |
| -- | -- | -- |
| [caedef0f5b](angular/angular@caedef0) | fix | update `@babel/core` dependency and lock version ([#&#8203;48634](angular/angular#48634)) |

##### core

| Commit | Type | Description |
| -- | -- | -- |
| [6acae1477a](angular/angular@6acae14) | feat | Add `TestBed.runInInjectionContext` to help test functions which use `inject` ([#&#8203;47955](angular/angular#47955)) |
| [38421578a2](angular/angular@3842157) | feat | Make the `isStandalone()` function available in public API ([#&#8203;48114](angular/angular#48114)) |
| [dd42974b07](angular/angular@dd42974) | feat | support TypeScript 4.9 ([#&#8203;48005](angular/angular#48005)) |

##### forms

| Commit | Type | Description |
| -- | -- | -- |
| [8aa8b4b77c](angular/angular@8aa8b4b) | fix | Form provider FormsModule.withConfig return a FormsModule ([#&#8203;48526](angular/angular#48526)) |

##### language-service

| Commit | Type | Description |
| -- | -- | -- |
| [5f0b53c735](angular/angular@5f0b53c) | feat | Allow auto-imports to suggest multiple possible imports. ([#&#8203;47787](angular/angular#47787)) |
| [6a8ea29a04](angular/angular@6a8ea29) | fix | expose `package.json` for vscode extension resolution ([#&#8203;48678](angular/angular#48678)) |
| [ce8160ecb2](angular/angular@ce8160e) | fix | Prevent crashes on unemitable references ([#&#8203;47938](angular/angular#47938)) |
| [e615b598ba](angular/angular@e615b59) | fix | ship `/api` entry-point ([#&#8203;48670](angular/angular#48670)) |
| [6ce7d76a0e](angular/angular@6ce7d76) | fix | update packages/language-service/build.sh script to work with vscode-ng-language-service's new Bazel build ([#&#8203;48663](angular/angular#48663)) |

##### localize

| Commit | Type | Description |
| -- | -- | -- |
| [a1a8e91eca](angular/angular@a1a8e91) | fix | add triple slash type reference on `@angular/localize` on \`ng  add ([#&#8203;48502](angular/angular#48502)) |

##### migrations

| Commit | Type | Description |
| -- | -- | -- |
| [cc284afbbc](angular/angular@cc284af) | fix | combine newly-added imports in import manager ([#&#8203;48620](angular/angular#48620)) |

##### router

| Commit | Type | Description |
| -- | -- | -- |
| [228e992db7](angular/angular@228e992) | docs | Deprecate canLoad guards in favor of canMatch ([#&#8203;48180](angular/angular#48180)) |
| [0a8b8a66cd](angular/angular@0a8b8a6) | docs | Deprecate public members of Router that are meant to be configured elsewhere ([#&#8203;48006](angular/angular#48006)) |
| [332461bd0c](angular/angular@332461b) | feat | Add ability to override `onSameUrlNavigation` default per-navigation ([#&#8203;48050](angular/angular#48050)) |
| [f58ad86e51](angular/angular@f58ad86) | feat | Add feature provider for enabling hash navigation ([#&#8203;48301](angular/angular#48301)) |
| [73f03ad2d2](angular/angular@73f03ad) | feat | Add new NavigationSkipped event for ignored navigations ([#&#8203;48024](angular/angular#48024)) |
| [3fe75710d9](angular/angular@3fe7571) | fix | page refresh should not destroy history state ([#&#8203;48540](angular/angular#48540)) |

#### Special Thanks

Alan Agius, Alex Castle, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Bob Watson, Charles Lyding, Derek Cormier, Doug Parker, Dylan Hunn, George Kalpakas, Greg Magolan, Jessica Janiuk, JiaLiPassion, Joey Perrott, Kristiyan Kostadinov, Matthieu Riegler, Paul Gschwendtner, Pawel Kozlowski, Renan Ferro, Tim Gates, Vadim, Virginia Dooley, ced, mgechev, piyush132000, robertIsaac and sr5434

<!-- CHANGELOG SPLIT MARKER -->

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNC45Ny41IiwidXBkYXRlZEluVmVyIjoiMzQuOTkuMiJ9-->

Co-authored-by: cabr2-bot <cabr2.help@gmail.com>
Reviewed-on: https://codeberg.org/Calciumdibromid/CaBr2/pulls/1719
Reviewed-by: Epsilon_02 <epsilon_02@noreply.codeberg.org>
Co-authored-by: Calciumdibromid Bot <cabr2_bot@noreply.codeberg.org>
Co-committed-by: Calciumdibromid Bot <cabr2_bot@noreply.codeberg.org>
@maximilian27
Copy link

Nice, but how does this improve Angular in any way?

@kroeder
Copy link
Contributor

kroeder commented Jan 19, 2023

Nice, but how does this improve Angular in any way?

@maximilian27 See #48535 (comment)

Syntactic sugar for self-closing tags is a small DX improvement of its own, but it's not the main motivation here. We're looking at a number of other improvements we can make to improve the template authoring experience.

trekladyone pushed a commit to trekladyone/angular that referenced this pull request Feb 1, 2023
…8535)

Allows for self-closing tags to be used for non-native tag names, e.g. `<foo [input]="bar"></foo>` can now be written as `<foo [input]="bar"/>`. Native tag names still have to have closing tags.

Fixes angular#39525.

PR Close angular#48535
@angular-automatic-lock-bot
Copy link

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 Feb 19, 2023
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 detected: feature PR contains a feature commit target: minor This PR is targeted for the next minor release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Self-closing component tags (<my-comp/> instead of <my-comp></my-comp>)?