An Angular component holds three languages, and only one of them is TypeScript
Open a component file and you are reading a decorated class, an HTML template with Angular syntax layered over it, and a block of CSS, all inside one pair of braces. Feed the whole thing to a JavaScript beautifier and the class comes out clean while the template stays welded into a single line, because the beautifier sees a string and leaves strings alone. This page splits the file first, then hands each piece to rules written for that piece.
Decorators, members and lifecycle hooks go through a JavaScript pass tuned for Angular. Generic type arguments are masked before the pass runs, so Observable<User | null> does not come back split into a row of comparison operators.
Markup inside the template backticks, or a whole standalone file, is parsed into a tree of elements, text and control flow blocks. Bindings, structural directives and template references survive character for character.
Rules in the styles array are formatted as CSS, then pushed back in at the indent of the property holding them, rather than flattened against the left margin the way a single pass leaves them.
What a plain JavaScript beautifier does to your template
The difference shows up on the first component you paste. A beautifier walks tokens, and a template literal is one token to it. Everything between the backticks is copied through untouched, so a template written on one line stays on one line no matter how you set the options.
template: `<div class="row" *ngFor="let m of members"><span>{{m.name}}</span></div>`template: `
<div class="row" *ngFor="let m of members"><span>{{ m.name }}</span></div>`The template printer, rule by rule
- Each element opens on its own line and its children indent one level. A leaf element wrapping nothing but text stays on a single line while the finished line fits inside your wrap column.
- Interpolation is padded to
{{ value }}and pipe operators are spaced tovalue | date. A logical||inside an expression is left alone, and so is a pipe character sitting inside a quoted string. - Binding values collapse to single spaces. Plain HTML attribute values are copied byte for byte, because a
titleor acontentattribute holds prose where spacing carries meaning. - Content inside
preandtextareapasses through with every space and newline preserved, including the indentation you gave it. - Implied end tags close the way a browser closes them. Write
<li>one<li>twoand you get two siblings back, not one list item nested inside the other. - A quote character inside an attribute value switches the surrounding quotes instead of producing broken markup.
- Comments keep their text and move to the indent of the node beside them.
Control flow blocks are parsed, not treated as loose text
Angular 17 moved branching out of directives and into template syntax. A formatter with no knowledge of the blocks reads @if (open) { as a run of text and the matching brace as more text, so both land wherever they started. Here the blocks build real nodes, children indent under them, and a closing brace followed by a continuation joins onto one line.
@if (user()) {<p>Hi</p>} @else {<a routerLink="/login">Sign in</a>}@if (user()) {<p>Hi</p>} @else {<a routerLink="/login">Sign in</a>}The same treatment covers @for with its @empty branch, @switch with @case and @default, and @defer with @placeholder, @loading and @error. Braces belonging to interpolation are recognised first, so an expression sitting inside a block never gets mistaken for the end of it.
Tags wrap by measurement, not by attribute count
Angular tags run long. One button carries a class list, a disabled binding, a click handler and a test id before anybody calls the code messy. The formatter builds the opening tag, measures the finished line against your wrap column of 80, 100 or 120 characters, and splits one attribute per line only when the line runs past it. A tag with a single attribute never splits, since <div class="card"> spread over three lines helps nobody.
When a tag splits, the > rides on the last attribute line rather than dropping to a line of its own. Both styles are in wide use. This one keeps the vertical cost of wrapping down, which matters on a template where half the elements carry four bindings.
The list beside the output reads the tree you pasted
Formatting and reviewing are separate jobs, so nothing in this list edits your code. While the parser walks the template it records patterns worth a second look, groups repeats, and counts them:
- A
@forblock with notrackexpression. The compiler refuses to build this, so the failure arrives before formatting matters. - Two structural directives on one element. Also a compiler error, and the fix is an
ng-containerwrapper around one of them. ngIforngForwritten without the leading star. Angular treats the result as an ordinary attribute and renders the element every time, with no error to tell you.*ngForwith notrackBy, which rebuilds every row whenever the array reference changes.- An
[innerHTML]binding, worth tracing back to its source before release. - A native
onclickattribute, which skips Angular event handling entirely. - A click handler on a
divorspancarrying notabindex, so keyboard users never reach it. - An
imgwith noaltattribute. - An inline template past forty lines, where
templateUrlstarts paying for itself.
Nothing leaves the tab
Parsing, formatting and rendering all run in your browser. Disconnect after the page loads and every button keeps working. Angular components pasted into web tools carry internal API paths, feature flag names and route structures for software that has not shipped yet, so a round trip through somebody else's server buys nothing worth the exposure.
Where this formatter stops
- No compiler runs. The page builds a text tree, not the Angular AST. A template with a genuine syntax error still comes back formatted rather than rejected, so build the result before you commit it.
- Expression bodies are left alone. Only the outer padding and pipe spacing change inside interpolation. Spacing around
?and:stays as you typed it, because a colon also separates pipe arguments indate:'short'and guessing wrong there rewrites working code. - Prose does not reflow. A long paragraph of text between tags stays on one line. Breaking it would move whitespace that shows up in the rendered page.
- Attributes keep their order. Sorting bindings ahead of plain attributes is a house style, and imposing one would produce a diff touching every element in the file.
- Element tags are never converted. A self-closing
<app-card />and a paired<app-card></app-card>both come back in the form you wrote. - Inline templates end at the first unescaped backtick. A nested backtick inside a
${}expression closes the literal early and the rest of the file falls back to a plain TypeScript pass. - Only inline styles are formatted. Files named in
styleUrlsare never opened, and the styles array is treated as CSS rather than Sass. - This is not Prettier. Prettier parses Angular templates properly, wraps text and enforces one style across a repository under CI. Install it for the project, and use this page for the component somebody pasted into a chat thread.
- Around 2 MB is the ceiling. Browser editors crawl past that size, and a component that large has other problems.
