In an Angular template, what does [(value)]="volume" expand to, and why must the parentheses sit inside the square brackets?
answer
- two bindings from one attribute
- the Change suffix
- banana inside the box
- reversed brackets make an event name
basics
~10 s[(value)]="volume" is shorthand for [value]="volume" plus (valueChange)="volume = $event". The parentheses go inside the brackets because ([value]) parses as an event binding to an event literally named [value].
solid answer
~40 sAngular's two-way syntax is pure template sugar. `[(value)]="volume"` becomes a property binding `[value]="volume"` that pushes the parent's value down, plus an event binding on `valueChange` that writes `$event` back into `volume`. The event name is always the property name with `Change` appended, so any target with an input `x` and an output `xChange` supports `[(x)]`: a `model()` input, a hand-written input and output pair, or a directive such as `NgModel`. The order is fixed as "banana in a box": `[( )]`. Written the other way, `([value])` is an event binding whose event name is `[value]`, which nothing ever emits, so the binding silently does nothing. With `strictTemplates`, the extended diagnostic NG8101 (`invalidBananaInBox`) flags it.
code
html · 8 lines<!-- correct: banana in a box -->
<app-volume-slider [(value)]="volume" />
<!-- equivalent longhand for a WritableSignal named volume -->
<app-volume-slider [value]="volume()" (valueChange)="volume.set($event)" />
<!-- wrong: an event binding to an event named [value]; flagged by NG8101 -->
<app-volume-slider ([value])="volume" />go deeper
Recall the expansion into [value] plus (valueChange) and the banana-in-a-box order.
Explain what the reversed form actually parses as and why the target must be an assignable expression.
Mention the strictTemplates diagnostic that catches the reversed form and how you would enforce it in CI.
Relate the naming convention to API design: every two-way capable component commits to an x and xChange pair.
## Two bindings from one attribute Angular templates have two one-way bindings: `[prop]="expr"` sends a value **down** into an element or child component, and `(event)="statement"` sends a signal **up** when something happens. **Two-way binding** is not a third mechanism; it is shorthand that writes both at once: ```html <!-- what you write --> <app-volume-slider [(value)]="volume" /> <!-- what the compiler treats it as (for a plain property) --> <app-volume-slider [value]="volume" (valueChange)="volume = $event" /> ``` The rules the compiler applies: 1. The name inside `[( )]` becomes a **property binding** with the same name. 2. The same name with **`Change` appended** becomes an **event binding**. 3. The event half **assigns `$event` back** to the bound expression. If the expression is a `WritableSignal`, it calls `set($event)` instead. Because the naming convention is the whole contract, anything with a matching input and output pair can be the target: | Target | Input half | Event half | |---|---|---| | A component with `value = model(0)` | `value` | `valueChange`, created automatically by `model()` | | A component with `value = input(0)` and `valueChange = output<number>()` | `value` | `valueChange` | | `NgModel` from `FormsModule` | `ngModel` | `ngModelChange` | ## Why the order of the brackets matters The mnemonic is **"banana in a box"**: the banana-shaped parentheses go inside the box-shaped brackets, `[( )]`. The parser recognises the two-way form only by that opening `[(`. The reversed form, `([value])="volume"`, is not a syntax error. The parser sees an attribute that starts with `(`, reads it as an **event binding**, and takes the event name to be the text inside the parentheses: `[value]`. No DOM element or output ever emits an event called `[value]`, so: - the child never receives the parent's value (there is no property half at all), and - the parent never receives updates (the listener never fires). Nothing fails at runtime; the slider just does not stay in sync. With `strictTemplates` enabled, the compiler's extended diagnostic **NG8101, `invalidBananaInBox`**, reports this pattern, which is a good reason to keep strict templates on. ## What `$event` and assignment mean here The event half is an ordinary event binding, so `$event` is whatever the child's `valueChange` output emitted, typically the new number. Only the property half's expression is written to, so it must be something assignable: a field (`volume`), a nested field (`settings.volume`) or a keyed read (`levels[channel]`). A method call such as `getVolume()` or a signal read like `volume()` is rejected with *Unsupported expression in a two-way binding*, because there is nothing to assign to. ## How the compiler emits it The generated code makes the "two bindings" reading literal. For `[(value)]` the compiler emits a **two-way property instruction** in the update block and a **two-way listener** in the creation block. The property instruction behaves like any property binding: on each change detection pass it compares the new value with the one it pushed last time and only writes the child's input when they differ. The listener is registered exactly like any `(event)` listener, which means it is subject to the same rules: it marks the parent's view dirty when it runs, it is removed when the view is destroyed, and it sits in template order next to any other listener on the same element. Knowing that the pieces are ordinary bindings is what makes the edge cases predictable; nothing about `[( )]` is magic beyond the naming convention and the write-back. ## A worked example: a volume slider ```ts @Component({ selector: 'app-player', imports: [VolumeSlider], template: ` <app-volume-slider [(value)]="volume" /> <p>Volume: {{ volume() }}</p> `, }) export class Player { volume = signal(40); } ``` On each change detection pass the property half pushes `40` into the slider. When the user drags the thumb and the slider emits `valueChange` with `55`, the event half sets `volume` to `55`, and the next pass pushes `55` back down. Because the pushed value equals what the slider already shows, nothing flips back and forth. ## Common mistakes - **`([value])`**: reversed brackets; silently does nothing without strict templates. - **Expecting `[(value)]` on an element or component with no `valueChange` output**: the event half has nothing to listen to (see the input and output pair rules). - **Binding `volume()` instead of `volume`**: the template must receive the signal itself so the event half can call `set()`. - **Believing two-way binding shares one object between parent and child**: it is two one-way flows that happen to form a loop.
- In Angular, can you use [(x)] on a component that has no model() inputs?Yes. The syntax only needs an input named `x` and an output named `xChange` on the same component or directive. A hand-written `input()` plus `output()` pair, or `@Input()` plus `@Output()`, works exactly like a `model()` input from the parent's point of view.
- What does Angular's NG8101 extended diagnostic report?`invalidBananaInBox`: a binding written `([x])` instead of `[(x)]`. It needs `strictTemplates`. The reversed form is legal syntax for an event binding named `[x]`, so without the diagnostic it compiles and silently does nothing.
Two-way binding is a thermostat and a boiler on two separate wires: one wire sends the set temperature down, the other reports the reading back, and plugging the wires into the wrong sockets means neither message arrives.
saying these in an interview costs you the question
- ([value]) and [(value)] are two accepted spellings of the same binding
- Two-way binding shares one reference between parent and child
- The event half is named valueChanged or onValueChange
- [(x)] only works on components that declare model()
- A reversed banana-in-a-box throws an error at runtime