7. Two-Way Binding
Two-Way Data Binding — банан в коробке 🍌📦
Заголовок раздела «Two-Way Data Binding — банан в коробке 🍌📦»Двустороннее связывание — это когда данные синхронизируются в обе стороны одновременно: изменил в компоненте → обновился DOM, изменил в DOM (пользователь ввёл текст) → обновился компонент. Магия? Нет — просто синтаксический сахар! 🪄
Синтаксис [()] — “Banana in a Box”
Заголовок раздела «Синтаксис [()] — “Banana in a Box”»Angular разработчики называют [()] “банан в коробке” — потому что:
()— банан (EventEmitter, событие вверх)[]— коробка (property binding, данные вниз)[()]— банан в коробке = оба направления вместе!
<!-- Двустороннее связывание — "banana in a box" 🍌📦 --><input [(ngModel)]="username" />
<!-- Это ПОЛНОСТЬЮ эквивалентно: --><input [ngModel]="username" (ngModelChange)="username = $event">@Component({ template: ` <input [(ngModel)]="name" /> <p>Привет, {{ name }}!</p> `})export class GreetingComponent { name = 'Яша'; // Меняем name → обновляется input // Пишем в input → обновляется name // Всё синхронно! ✨}FormsModule — обязательный ингредиент 📦
Заголовок раздела «FormsModule — обязательный ингредиент 📦»ngModel живёт в FormsModule. Без него — ошибка:
// app.module.ts (NgModule подход)import { FormsModule } from '@angular/forms';
@NgModule({ imports: [ FormsModule, // ← обязательно! // ... ]})export class AppModule {}
// Или для standalone компонентов (Angular 14+):@Component({ standalone: true, imports: [FormsModule], // ← импортируем прямо в компонент template: `<input [(ngModel)]="name" />`})export class MyComponent { name = '';}Как ngModel работает под капотом 🔧
Заголовок раздела «Как ngModel работает под капотом 🔧»ngModel — это директива, которая реализует два механизма:
// Что делает Angular когда видит [(ngModel)]="username":// 1. Смотрит на ControlValueAccessor — адаптер для разных типов input// 2. Устанавливает [ngModel]="username" — данные В input// 3. Подписывается на (ngModelChange) — данные ИЗ input
// Всё это = одна строка:// [(ngModel)]="username"
// В деталях:@Directive({ selector: '[ngModel]' })class NgModel implements OnInit { @Input() ngModel: any; // принимает значение @Output() ngModelChange = new EventEmitter(); // отдаёт изменения
// Когда пользователь вводит текст: onChange(value: any) { this.ngModelChange.emit(value); // отправляет событие вверх }}Создаём собственный двусторонний биндинг 🛠️
Заголовок раздела «Создаём собственный двусторонний биндинг 🛠️»Ты можешь создать компонент с двусторонним связыванием! Правило: если есть @Input() value, то @Output() должен называться valueChange:
@Component({ selector: 'app-counter', template: ` <div class="counter"> <button (click)="decrement()">−</button> <span>{{ value }}</span> <button (click)="increment()">+</button> </div> `})export class CounterComponent { @Input() value = 0; @Output() valueChange = new EventEmitter<number>(); // ← valueChange = имя + "Change"!
increment() { this.valueChange.emit(this.value + 1); // НЕ меняем value напрямую! }
decrement() { this.valueChange.emit(this.value - 1); }}
// Использование:// [(value)]="counter" — банан в коробке для нашего компонента!// Или раздельно: [value]="counter" (valueChange)="counter = $event"<!-- В родителе --><app-counter [(value)]="quantity"></app-counter><p>Количество: {{ quantity }}</p>Соглашение об именах для Custom Two-Way Binding
Заголовок раздела «Соглашение об именах для Custom Two-Way Binding»// Правило: @Input() name + @Output() nameChange@Component({ selector: 'app-toggle' })export class ToggleComponent { @Input() checked = false; @Output() checkedChange = new EventEmitter<boolean>(); // Использование: [(checked)]="isActive"}
@Component({ selector: 'app-slider' })export class SliderComponent { @Input() position = 0; @Output() positionChange = new EventEmitter<number>(); // Использование: [(position)]="sliderValue"}
@Component({ selector: 'app-color-picker' })export class ColorPickerComponent { @Input() color = '#000000'; @Output() colorChange = new EventEmitter<string>(); // Использование: [(color)]="selectedColor"}Template-Driven Forms с ngModel 📋
Заголовок раздела «Template-Driven Forms с ngModel 📋»ngModel особенно мощен в формах. Он даёт доступ к состоянию каждого поля:
@Component({ template: ` <form #registrationForm="ngForm" (ngSubmit)="onSubmit(registrationForm)">
<!-- #nameField — ссылка на директиву NgModel --> <div class="field"> <label>Имя</label> <input name="name" [(ngModel)]="user.name" #nameField="ngModel" required minlength="2" [class.error]="nameField.invalid && nameField.touched">
@if (nameField.errors?.['required'] && nameField.touched) { <span class="error-msg">Имя обязательно</span> } @if (nameField.errors?.['minlength'] && nameField.touched) { <span class="error-msg">Минимум 2 символа</span> } </div>
<!-- Email с валидацией --> <div class="field"> <label>Email</label> <input name="email" type="email" [(ngModel)]="user.email" #emailField="ngModel" required email>
@if (emailField.invalid && emailField.dirty) { <span class="error-msg">Некорректный email</span> } </div>
<!-- Кнопка отправки — активна только если форма валидна --> <button type="submit" [disabled]="registrationForm.invalid || isLoading"> {{ isLoading ? 'Регистрация...' : 'Зарегистрироваться' }} </button>
</form>
<!-- Предпросмотр данных формы --> <pre>{{ user | json }}</pre> `})export class RegistrationComponent { user = { name: '', email: '', age: 18, };
isLoading = false;
onSubmit(form: NgForm) { if (form.valid) { this.isLoading = true; console.log('Отправляем:', this.user); // HTTP запрос... } }}Состояния NgModel — отслеживаем взаимодействие 🔍
Заголовок раздела «Состояния NgModel — отслеживаем взаимодействие 🔍»Состояние Класс CSS Значение──────────────────────────────────────────────Не тронуто: .ng-untouched touched = falseТронуто: .ng-touched touched = trueЧистое: .ng-pristine dirty = falseИзменённое: .ng-dirty dirty = trueВалидное: .ng-valid valid = trueНевалидное: .ng-invalid valid = false@Component({ template: ` <input name="email" [(ngModel)]="email" #emailRef="ngModel" required email>
<!-- Разные сообщения в зависимости от состояния --> <div class="hints"> <span [class.active]="emailRef.untouched">📝 Введите email</span> <span [class.active]="emailRef.touched && emailRef.valid">✅ Email корректен</span> <span [class.active]="emailRef.touched && emailRef.invalid">❌ Ошибка в email</span> <span [class.active]="emailRef.dirty">✏️ Поле изменено</span> </div>
<!-- Отладка состояния --> <small> valid: {{ emailRef.valid }}, dirty: {{ emailRef.dirty }}, touched: {{ emailRef.touched }} </small> `})ngModel vs Reactive Forms — когда что использовать?
Заголовок раздела «ngModel vs Reactive Forms — когда что использовать?»| Template-Driven (ngModel) | Reactive Forms | |
|---|---|---|
| Импорт | FormsModule | ReactiveFormsModule |
| Логика | В шаблоне | В TypeScript |
| Тестирование | Сложнее | Легче |
| Асинхронная валидация | Сложнее | Нативна |
| Динамические поля | Сложно | Легко |
| Лучше для | Простых форм | Сложных форм |
// Reactive Forms — альтернатива ngModel для сложных случаев:import { FormBuilder, Validators } from '@angular/forms';
@Component({ template: ` <form [formGroup]="form" (ngSubmit)="onSubmit()"> <input formControlName="name" /> <input formControlName="email" /> <button [disabled]="form.invalid">Отправить</button> </form> `})export class ReactiveFormComponent { form = this.fb.group({ name: ['', [Validators.required, Validators.minLength(2)]], email: ['', [Validators.required, Validators.email]], });
constructor(private fb: FormBuilder) {}
onSubmit() { console.log(this.form.value); }}Практика
Заголовок раздела «Практика»Попробуйте концепцию в интерактивном редакторе: