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 включен!Property Binding [property]="expression" 📎
Заголовок раздела «Property Binding [property]="expression" 📎»Квадратные скобки говорят 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 Binding — управление CSS классами 🎨
Заголовок раздела «Class Binding — управление CSS классами 🎨»<!-- Один класс: [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 Binding — динамические стили 💅
Заголовок раздела «Style Binding — динамические стили 💅»<!-- Один стиль: [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>Attribute Binding — когда нет DOM свойства 🏷️
Заголовок раздела «Attribute Binding — когда нет DOM свойства 🏷️»Иногда нужно установить 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>Event Binding (event)="handler()" 👂
Заголовок раздела «Event Binding (event)="handler()" 👂»Круглые скобки говорят: “слушай это событие и вызывай обработчик”:
@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(); }}Custom Events через @Output + EventEmitter 📡
Заголовок раздела «Custom Events через @Output + EventEmitter 📡»@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>Практика
Заголовок раздела «Практика»Попробуйте концепцию в интерактивном редакторе: