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

6. Property и Event Binding

Property и Event Binding — однонаправленный поток данных ⬇️⬆️

Заголовок раздела «Property и Event Binding — однонаправленный поток данных ⬇️⬆️»

Angular строится на однонаправленном потоке данных: данные текут из компонента в DOM (property binding), события летят из DOM в компонент (event binding). Это делает приложение предсказуемым и легким для отладки! 🎯


Представь реку: вода течёт только в одну сторону. Так же и данные в Angular:

Компонент (TypeScript) ──[property binding]──▶ DOM (HTML)
Компонент (TypeScript) ◀──(event binding)────── DOM (HTML)
@Component({
template: `
<!-- Данные ВНИЗ: [disabled]="isLoading" -->
<button [disabled]="isLoading">
{{ isLoading ? 'Загрузка...' : 'Отправить' }}
</button>
<!-- Событие ВВЕРХ: (click)="onSubmit()" -->
<button (click)="onSubmit()">Готово</button>
`
})
export class FormComponent {
isLoading = false;
onSubmit() {
this.isLoading = true;
// делаем HTTP запрос...
}
}

HTML Атрибуты vs DOM Свойства — КЛЮЧЕВОЕ отличие! 🔑

Заголовок раздела «HTML Атрибуты vs DOM Свойства — КЛЮЧЕВОЕ отличие! 🔑»

Это самая частая путаница у новичков. Атрибуты и свойства — разные вещи:

HTML АтрибутыDOM Свойства
Определены вHTML спецификацииDOM (JavaScript объект)
Меняются ли?Нет (статичны)Да (динамичны)
ТипВсегда строкаЛюбой тип JS
<!-- HTML атрибут — начальное значение, строка -->
<input value="Hello">
<!-- DOM свойство — текущее значение, динамическое -->
<!-- После того как пользователь изменил input:
getAttribute('value') → "Hello" (атрибут не менялся)
input.value → "World" (свойство обновилось!) -->
// ✅ Property binding — привязываемся к DOM свойству
<input [value]="userName" />
// ❌ Это просто HTML атрибут — статичная строка, не реактивная
<input value="{{ userName }}" />
// Пример разницы: disabled
<button [disabled]="!isValid">Кнопка</button> // ✅ bool
<button disabled="{{ !isValid }}">Кнопка</button> // ❌ всегда disabled!
// disabled="false" — атрибут присутствует = disabled включен!

Квадратные скобки говорят Angular: “вычисли выражение и присвой DOM свойству”:

@Component({
template: `
<!-- Базовый property binding -->
<img [src]="product.imageUrl" [alt]="product.name" />
<!-- Boolean свойства -->
<button [disabled]="isLoading || !formValid">Отправить</button>
<input [readonly]="!isEditMode" [value]="username" />
<!-- Числовые свойства -->
<progress [value]="uploadPercent" [max]="100"></progress>
<input type="range" [min]="minValue" [max]="maxValue" [step]="step" />
<!-- Работа с объектами -->
<app-user-card [user]="currentUser" [config]="cardConfig"></app-user-card>
<!-- Привязка к нестандартным свойствам компонентов -->
<app-chart [data]="chartData" [options]="chartOptions"></app-chart>
`
})
export class ProductComponent {
product = { name: 'Ноутбук', imageUrl: '/laptop.jpg' };
isLoading = false;
formValid = true;
uploadPercent = 65;
currentUser = { id: 1, name: 'Яша', role: 'admin' };
}

<!-- Один класс: [class.className]="boolean" -->
<div [class.active]="isActive">Меню</div>
<button [class.loading]="isLoading" [class.error]="hasError">
Кнопка
</button>
<!-- Несколько классов через объект: [class]="object" -->
<div [class]="{
'active': isActive,
'disabled': !isEnabled,
'highlighted': isNew,
'large': size === 'lg'
}">Блок</div>
<!-- Несколько классов через строку или массив -->
<div [class]="'base-class ' + (isActive ? 'active' : '')">...</div>
<div [ngClass]="['btn', 'btn-primary', isLarge ? 'btn-lg' : 'btn-sm']">
Кнопка
</div>
// Вычисляем классы в TypeScript — чище!
get buttonClasses(): Record<string, boolean> {
return {
'btn': true,
'btn-primary': this.variant === 'primary',
'btn-danger': this.variant === 'danger',
'btn-lg': this.size === 'large',
'disabled': this.disabled,
};
}

<!-- Один стиль: [style.property]="value" -->
<div [style.color]="textColor">Текст</div>
<div [style.fontSize.px]="fontSize">Большой текст</div>
<div [style.width.%]="progressPercent">Прогресс</div>
<div [style.opacity]="isVisible ? 1 : 0">Элемент</div>
<!-- Несколько стилей через объект: [style]="object" -->
<div [style]="{
color: textColor,
fontSize: fontSize + 'px',
backgroundColor: theme === 'dark' ? '#1e293b' : '#ffffff',
transform: 'rotate(' + rotation + 'deg)'
}">Стилизованный блок</div>
<!-- [ngStyle] — старый способ, но тоже работает -->
<div [ngStyle]="{ 'font-size': fontSize + 'px', 'color': color }">
Текст
</div>

Иногда нужно установить HTML атрибут, у которого нет DOM свойства:

<!-- ARIA атрибуты — нет DOM свойства, только атрибут -->
<button
[attr.aria-label]="'Удалить ' + item.name"
[attr.aria-expanded]="isMenuOpen"
[attr.aria-disabled]="isDisabled">
🗑️
</button>
<!-- SVG атрибуты -->
<svg>
<circle
[attr.cx]="centerX"
[attr.cy]="centerY"
[attr.r]="radius">
</circle>
</svg>
<!-- colspan, rowspan в таблицах -->
<td [attr.colspan]="colSpan">Заголовок</td>
<!-- data-* атрибуты -->
<div [attr.data-testid]="'product-' + product.id">...</div>

Круглые скобки говорят: “слушай это событие и вызывай обработчик”:

@Component({
template: `
<!-- Базовые события -->
<button (click)="onClick()">Нажми меня</button>
<input (input)="onInput($event)" (blur)="onBlur()" (focus)="onFocus()" />
<!-- Клавиатурные события -->
<input
(keyup)="onKeyUp($event)"
(keyup.enter)="onSearch()"
(keyup.escape)="clearSearch()"
(keydown.ctrl.s)="onSave()">
<!-- Объект $event — оригинальное DOM событие -->
<div (click)="onDivClick($event)">
<button (click)="onButtonClick($event)">Кнопка</button>
</div>
<!-- Предотвращаем всплытие -->
<button (click)="onDelete($event); $event.stopPropagation()">
Удалить
</button>
`
})
export class EventComponent {
onClick() {
console.log('Клик!');
}
onInput(event: Event) {
const value = (event.target as HTMLInputElement).value;
console.log('Ввод:', value);
}
onDivClick(event: MouseEvent) {
console.log('Координаты:', event.clientX, event.clientY);
}
onButtonClick(event: MouseEvent) {
event.stopPropagation(); // не даём событию всплыть до div
console.log('Кнопка нажата!');
}
onSave(event: KeyboardEvent) {
event.preventDefault(); // предотвращаем стандартное поведение Ctrl+S
this.saveDocument();
}
}

@Component({
selector: 'app-search-bar',
template: `
<div class="search">
<input
#searchInput
[value]="query"
(input)="query = searchInput.value"
(keyup.enter)="onSearch()"
placeholder="Поиск...">
<button (click)="onSearch()" [disabled]="query.length < 2">
🔍
</button>
<button *ngIf="query" (click)="onClear()">✕</button>
</div>
`
})
export class SearchBarComponent {
@Input() initialQuery = '';
@Output() searched = new EventEmitter<string>();
@Output() cleared = new EventEmitter<void>();
query = this.initialQuery;
onSearch() {
if (this.query.length >= 2) {
this.searched.emit(this.query);
}
}
onClear() {
this.query = '';
this.cleared.emit();
}
}
// Использование:
// <app-search-bar
// [initialQuery]="searchQuery"
// (searched)="onSearch($event)"
// (cleared)="onClear()">
// </app-search-bar>

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