Перейти к содержимому

7. Two-Way Binding

Двустороннее связывание — это когда данные синхронизируются в обе стороны одновременно: изменил в компоненте → обновился DOM, изменил в DOM (пользователь ввёл текст) → обновился компонент. Магия? Нет — просто синтаксический сахар! 🪄


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
// Всё синхронно! ✨
}

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 — это директива, которая реализует два механизма:

// Что делает 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:

counter.component.ts
@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>

// Правило: @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"
}

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>
`
})

Template-Driven (ngModel)Reactive Forms
ИмпортFormsModuleReactiveFormsModule
ЛогикаВ шаблонеВ 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);
}
}

Попробуйте концепцию в интерактивном редакторе: