Прив'язка даних в Angular
Прив'язка даних (data binding) в Angular автоматично синхронізує TypeScript-властивості компонента з HTML-шаблоном. Руками DOM чіпати не потрібно.
Теорія
TL;DR
- Як живе табло: компонент передає дані в шаблон, а дії користувача повертають події назад
- Чотири типи: інтерполяція
{{ }}, прив'язка властивостей[prop], прив'язка подій(event), двостороннє зв'язування[(ngModel)] - За замовчуванням напрямок односторонній (компонент до шаблону), так легше відслідковувати стан
[(ngModel)]використовуй тільки для полів форм, не як загальний підхід
Швидкий приклад
@Component({
template: `
<p>{{ title }}</p> // інтерполяція: відображає текст
<img [src]="imageUrl"> // прив'язка властивості: встановлює DOM-властивість
<button (click)="onClick()">Go</button> // прив'язка події: викликає метод при кліку
<input [(ngModel)]="name"> // двостороннє: поле і властивість синхронізовані
<p>Ти написав: {{ name }}</p>
`
})
export class AppComponent {
title = 'Hello';
imageUrl = 'https://example.com/logo.png';
name = '';
onClick() { console.log('Clicked!'); }
}Шаблон читає безпосередньо з властивостей компонента. Змінився title у класі - <p> оновиться при наступному циклі change detection.
Чотири типи прив'язки
Інтерполяція {{ вираз }} перетворює значення на рядок і вставляє його в DOM. Підходить для текстового вмісту, але не для DOM-властивостей.
Прив'язка властивостей [property]="value" встановлює DOM-властивість напряму, а не HTML-атрибут. Різниця важлива: [src]="imageUrl" встановлює img.src (DOM-властивість). src="{{ imageUrl }}" встановлює HTML-атрибут і чекає, поки браузер синхронізує його. Для простих рядків обидва варіанти схожі, але [disabled]="isLoading" правильно працює тільки як прив'язка властивостей.
Прив'язка подій (event)="handler($event)" слухає DOM-події і запускає метод компонента. Об'єкт $event дає доступ до нативної події. Ніякого addEventListener в TypeScript, ніякого inline-JS у шаблоні.
Двостороннє зв'язування [(ngModel)]="prop" - скорочення для [ngModel]="prop" (ngModelChange)="prop = $event". Квадратні дужки встановлюють значення, круглі слухають зміни. Потребує FormsModule в імпортах модуля.
Коли що використовувати
| Ситуація | Тип прив'язки |
|---|---|
| Відобразити текст або обчислене значення | {{ value }} |
Встановити атрибути, класи, стан disabled | [property]="value" |
| Обробити кліки, введення, submit форми | (event)="method()" |
| Поле форми з двостороньою синхронізацією | [(ngModel)]="prop" |
Для складного стану відмовся від [(ngModel)] і використовуй прив'язку подій разом з прив'язкою властивостей окремо. Дані течуть в одному напрямку і легше відстежуються.
Як Angular запускає це
Компілятор Angular (AOT) перетворює шаблон на інструкції для change detection під час збірки. Zone.js патчить браузерні API типу addEventListener і setTimeout. Коли спрацьовує подія, Angular проходить по дереву компонентів і оновлює тільки ті прив'язки, що змінились. Зі стратегією OnPush пропускаються компоненти, чиї inputs не змінились, що прискорює роботу у великих деревах.
Типові помилки
Помилка 1: Інтерполяція замість прив'язки властивостей
<!-- Встановлює HTML-атрибут, а не DOM-властивість -->
<img src="{{ imageUrl }}">
<!-- Встановлює img.src напряму -->
<img [src]="imageUrl">Для динамічних значень, які не є простими рядками, варіант через атрибут тихо ламається. Для DOM-властивостей завжди використовуй прив'язку властивостей.
Помилка 2: Забути FormsModule
Error: Can't bind to 'ngModel' since it isn't a known property of 'input'.
Додай FormsModule до масиву imports в @NgModule. У standalone-компонентах - імпортуй безпосередньо в декоратор компонента.
Помилка 3: Виклик методів в шаблоні
<!-- Викликається при кожному циклі change detection -->
<div>{{ getUserName() }}</div>
<!-- Прив'яжи до властивості -->
<div>{{ userName }}</div>Це, мабуть, найпоширеніша проблема продуктивності, яку я бачу в Angular-проектах. Angular запускає change detection багато разів за секунду під час активної взаємодії. Метод у шаблоні виконується при кожному циклі.
Помилка 4: Двостороннє зв'язування на не-input елементах
<!-- ngModel працює тільки з елементами форм -->
<div [(ngModel)]="data">Контент</div>Для кастомних елементів використовуй явний патерн: [value]="data" (input)="data = $event.target.value".
Де зустрічається в реальних проектах
- Angular Material:
[disabled]="isLoading"на кнопках під час запиту до API - Форми авторизації:
[(ngModel)]для простих полів,[formControl]для реактивних форм - Таблиці:
(selectionChange)="onSelect($event)"на компонентах вибору - Динамічні стилі:
[class.active]="isSelected",[style.color]="textColor"
Питання на співбесіді
Q: В чому різниця між [src] і [attr.src]?
A: [src] встановлює DOM-властивість. [attr.src] встановлює HTML-атрибут. За замовчуванням використовуй прив'язку властивостей. Прив'язка атрибутів потрібна для ARIA-атрибутів типу [attr.aria-label], у яких немає відповідної DOM-властивості.
Q: Як [(ngModel)] працює всередині?
A: Розгортається в [ngModel]="value" (ngModelChange)="value = $event". Частина [] встановлює властивість, частина () слухає зміни. Цей самий патерн можна використовувати для побудови двостороннього зв'язування у власних компонентах.
Q: Чому виклик методу в шаблоні шкодить продуктивності?
A: Change detection запускається при кожній асинхронній події. Виклик методу в шаблоні виконується при кожному циклі. Обчислюй значення в компоненті і прив'язуй готовий результат.
Q: Чому уникають двостороннього зв'язування у великих застосунках?
A: Воно створює двонаправлений потік даних, який важко відстежити. Кращий патерн - однонаправлений: дані вниз через прив'язку властивостей, події вгору через прив'язку подій. Саме так влаштовані NgRx та інші менеджери стану.
Приклади
Базовий: Одностороннє відображення даних
@Component({
template: `<h2>{{ hero.name }} (ID: {{ hero.id }})</h2>`
})
export class HeroComponent {
hero = { id: 1, name: 'Windstorm' };
// Відображає: Windstorm (ID: 1)
}Інтерполяція читає з об'єкта hero при кожному циклі change detection. Зміни в hero.name відображаться в шаблоні автоматично.
Середній: Todo-список з усіма чотирма типами прив'язки
@Component({
template: `
<ul>
<li *ngFor="let todo of todos"
[class.completed]="todo.done"
(click)="toggle(todo)">
{{ todo.text }}
</li>
</ul>
<input [(ngModel)]="newTodo" (keyup.enter)="add()">
`
})
export class TodoComponent {
todos = [{ text: 'Купити молоко', done: false }];
newTodo = '';
add() {
this.todos.push({ text: this.newTodo, done: false });
this.newTodo = '';
}
toggle(todo: { text: string; done: boolean }) {
todo.done = !todo.done;
}
}Усі чотири типи в одному компоненті. {{ todo.text }} відображає текст. [class.completed] встановлює CSS-клас на основі булевого значення. (click) обробляє перемикання. [(ngModel)] тримає поле в синхронізації з newTodo. Не забудь додати FormsModule до імпортів модуля.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.