Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «ViewChild та ContentChild в Angular». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`@ViewChild` та `@ContentChild`** - це декоратори Angular для доступу до елементів шаблону. `@ViewChild` читає з власного шаблону компонента; `@ContentChild` читає контент, переданий батьківським компонентом через `<ng-content>`. ```typescript @ViewChild('input') input!: ElementRef; // власний шаблон @ContentChild('title') title!: ElementRef; // проектований контент батька ``` **Ключове:** ViewChild доступний після `ngAfterViewInit`; ContentChild після `ngAfterContentInit`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`@ViewChild` та `@ContentChild`** - це декоратори Angular, що дають прямий доступ до дочірніх елементів шаблону. Де саме живе елемент визначає, який з них використовувати. ## Теорія ### TL;DR - `@ViewChild` звертається до власного шаблону компонента; `@ContentChild` до контенту, який батько передав через `<ng-content>` - Обидва повертають один збіг; для кількох елементів використовуй `@ViewChildren` / `@ContentChildren` - `@ViewChild` доступний після `ngAfterViewInit`; `@ContentChild` після `ngAfterContentInit` - Правило вибору: якщо елемент у твоєму шаблоні - ViewChild. Якщо батько його передав - ContentChild - Angular 17+ має сигнальні варіанти: `viewChild()` та `contentChild()` ### Швидкий приклад ```typescript // Батько передає контент у картку: // <app-card> // <h2 #title>Мій заголовок</h2> <-- проектований контент // </app-card> @Component({ selector: 'app-card', template: `<ng-content></ng-content>` }) export class CardComponent implements AfterContentInit { @ContentChild('title') titleRef!: ElementRef; ngAfterContentInit() { console.log(this.titleRef.nativeElement.textContent); // "Мій заголовок" } } ``` `h2` живе в шаблоні батьківського компонента, а не всередині `CardComponent`. Тому тут правильний `@ContentChild`, а `@ViewChild` повернув би `undefined`. ### Головна різниця `@ViewChild` шукає всередині власного шаблону компонента. `@ContentChild` шукає серед того, що батько передав у слот `<ng-content>`. Тайминг доступності відповідає цьому поділу: ViewChild готовий у `ngAfterViewInit`, ContentChild у `ngAfterContentInit`. Неправильний декоратор повертає `undefined` без жодного корисного повідомлення про помилку. ### Коли що використовувати - `@ViewChild`: зфокусувати поле форми, викликати метод дочірнього компонента, прочитати DOM-елемент зі свого шаблону - `@ContentChild`: будуєш обгортку на зразок картки, модального вікна або акордеону, якій потрібно взаємодіяти з тим, що всередині передали - `@ViewChildren`: кілька екземплярів через `*ngFor`, групові операції над дочірніми компонентами - `@ContentChildren`: вкладки, панелі акордеону, будь-який повторюваний проектований контент - Якщо потрібно лише передати дані вниз, `@Input()` простіший і жодного запиту не треба ### Таблиця порівняння | Аспект | @ViewChild | @ContentChild | |--------|------------|---------------| | Що запитує | Власний шаблон компонента | Контент через `<ng-content>` | | Доступний після | `ngAfterViewInit()` | `ngAfterContentInit()` | | Кілька елементів | `@ViewChildren` | `@ContentChildren` | | Хто визначає елемент | Сам компонент | Батьківський компонент | | Типовий сценарій | Поля форм, методи дочірнього компонента | Заголовки карток, панелі вкладок | ### Як Angular розв'язує запити Під час ініціалізації Angular сканує шаблон (або проектований контент) у пошуку елементів за вказаним селектором. Селектор буває двох видів: рядок, що відповідає змінній шаблону (`#ref`), або тип, що знаходить перший екземпляр компонента чи директиви цього класу. Для `@ViewChild` сканування відбувається після того, як DOM компонента відрендерився. Для `@ContentChild` - після того, як батько вставив контент у слот. Опція `static` змінює тайминг. `{ static: true }` розв'язує запит до запуску визначення змін, тому результат доступний вже в `ngOnInit`. Використовуй це лише для елементів, які завжди присутні в шаблоні і ніколи не обгорнені в `*ngIf` чи `*ngFor`. На практиці `static: true` - одна з найбільш занедбаних деталей Angular на співбесідах, а стандартний `{ static: false }` підходить для переважної більшості ситуацій. ### Сигнальні запити (Angular 17+) Angular 17 додав функціональні сигнальні запити як сучасну альтернативу декораторам. ```typescript @Component({ /* ... */ }) export class MyComponent { // Не потрібен lifecycle hook для доступу nameInput = viewChild.required<ElementRef>('nameInput'); chart = viewChild(ChartComponent); tabs = viewChildren(TabComponent); // Запити до проектованого контенту title = contentChild<ElementRef>('title'); buttons = contentChildren(ButtonComponent); ngOnInit() { // Сигнали реактивні, ngAfterViewInit не потрібен console.log(this.nameInput().nativeElement); } } ``` Сигнальні запити реактивні за замовчуванням. Значення оновлюється автоматично при зміні шаблону, без підписки на `QueryList.changes`. ### Типові помилки **1. Звернення до ViewChild в ngOnInit** Представлення компонента ще не відрендерилось на цьому етапі. Результат завжди `undefined`. ```typescript // Неправильно ngOnInit() { console.log(this.input.nativeElement); // TypeError } // Правильно ngAfterViewInit() { console.log(this.input.nativeElement); // працює } ``` **2. Використання @ViewChild для проектованого контенту** ```typescript // Неправильно - повертає undefined для елементів, переданих батьком @ViewChild('title') title!: ElementRef; // Правильно @ContentChild('title') title!: ElementRef; ``` **3. Сприйняття QueryList як статичного знімка** `QueryList` живий. Закеш довжину раз, і вона застаріє щойно елементи додадуться або прибережуться умовно. ```typescript // Неправильно - не реагує на динамічні зміни const count = this.items.length; // Правильно this.items.changes.subscribe(() => { console.log('Поточна кількість:', this.items.length); }); ``` **4. Відсутність опції `read` для директив** Коли директива та компонент знаходяться на одному елементі, Angular за замовчуванням повертає екземпляр компонента. Використовуй `read`, щоб отримати директиву. ```typescript // Неправильно - повертає компонент, а не директиву @ViewChild(MyDirective) dir!: MyComponent; // Правильно @ViewChild(MyDirective, { read: MyDirective }) dir!: MyDirective; ``` **5. Оновлення даних шаблону всередині ngAfterViewInit** Зміна властивості, пов'язаної з шаблоном, у `ngAfterViewInit` викликає `ExpressionChangedAfterItHasBeenCheckedError`, бо визначення змін вже завершилось. ```typescript // Неправильно ngAfterViewInit() { this.title = 'нове значення'; // помилка, якщо title прив'язаний у шаблоні } // Правильно ngAfterViewInit() { this.title = 'нове значення'; this.cdr.detectChanges(); // запустити ще один прохід } ``` ### Де зустрічається в реальних проектах - Angular Material: `MatTabGroup` використовує `@ContentChildren(MatTab)` для збору панелей вкладок від батьківського компонента - Angular Forms: `FormGroupDirective` використовує `@ViewChildren(FormControlName)` для відстеження всіх зареєстрованих контролів - PrimeNG: `p-dropdown` використовує `@ContentChild(PrimeTemplate)` для підтримки кастомних шаблонів елементів - Кастомні модальні вікна: `@ViewChild('closeBtn')` для переміщення фокусу на кнопку закриття при відкритті - Таблиці даних: `@ViewChildren(TableRowComponent)` для групового оновлення рядків після зміни фільтру ### Питання для практики **Q:** Яка різниця між `@ViewChild('ref')` та `@ViewChild(MyComponent)`? **A:** Рядкові референси збігаються зі змінними шаблону (`#ref`). Типові референси знаходять перший екземпляр цього класу. Типові запити типобезпечні і простіші для рефакторингу при перейменуванні компонента. **Q:** Навіщо `@ContentChild`, якщо є `@Input()`? **A:** `@Input()` передає дані. `@ContentChild` дає доступ до фактичного DOM-елемента або екземпляра компонента, щоб можна було викликати методи, читати властивості або застосовувати стилі напряму. Це два різні інструменти для різних завдань. **Q:** Чи працює `@ViewChild` з `*ngIf`? **A:** Так, але результат `undefined` коли умова хибна. Завжди перевіряй на наявність перед зверненням. Використовуй `{ static: false }` (стандарт) щоб Angular чекав появи елемента перед розв'язанням запиту. **Q:** Що відбувається, якщо кілька елементів мають однакову змінну шаблону? **A:** `@ViewChild` повертає лише перший збіг. Це поширена причина багів у циклах. Для отримання всіх елементів використовуй `@ViewChildren`. **Q (senior):** Як побудувати багаторазову обгортку для валідації форми, яка не залежить від того, які поля всередині? **A:** Створи `FormGroupComponent`, що використовує `@ContentChildren(FormControlName)` для знаходження контролів у `<ng-content>`. Обгортка обробляє логіку валідації та відображення помилок сама. Батьківський компонент просто огортає поля і отримує валідацію без будь-якої залежності від внутрішньої реалізації обгортки. ## Приклади ### Базовий: керування нативним полем введення через @ViewChild ```typescript @Component({ selector: 'app-search', template: ` <input #searchInput type="text" placeholder="Пошук..."> <button (click)="focusSearch()">Фокус</button> ` }) export class SearchComponent implements AfterViewInit { @ViewChild('searchInput') searchInput!: ElementRef<HTMLInputElement>; ngAfterViewInit() { // DOM готовий саме тут this.searchInput.nativeElement.focus(); } focusSearch() { this.searchInput.nativeElement.focus(); } } ``` `@ViewChild` розв'язує `#searchInput` після рендерингу компонента. Виклик `.focus()` у `ngOnInit` кинув би помилку, бо елемент ще не існує на тому етапі. ### Середній: @ContentChild у обгортці форми ```typescript // Обгортка не знає, які поля всередині неї @Component({ selector: 'app-form-group', template: `<div class="form-group"><ng-content></ng-content></div>` }) export class FormGroupComponent implements AfterContentInit { @ContentChild('password') passwordField!: ElementRef; ngAfterContentInit() { // Поле визначено в батьку і проектоване сюди console.log('Поле готове:', this.passwordField.nativeElement); } } // Батько проектує поле в обгортку @Component({ selector: 'app-login', template: ` <app-form-group> <input #password type="password" placeholder="Пароль"> </app-form-group> ` }) export class LoginComponent {} ``` Поле `#password` живе в шаблоні `LoginComponent`. `FormGroupComponent` отримує до нього доступ через `@ContentChild`, бо елемент був проектований всередину, а не визначений у самій обгортці. ### Просунутий: жива система вкладок через @ContentChildren ```typescript @Component({ selector: 'app-tabs', template: ` <div class="tab-bar"> <button *ngFor="let tab of tabs; let i = index" (click)="selectTab(i)"> {{ tab.label }} </button> </div> <ng-content></ng-content> ` }) export class TabsComponent implements AfterContentInit { @ContentChildren('tabPanel') tabPanels!: QueryList<ElementRef>; tabs: { label: string }[] = []; ngAfterContentInit() { this.buildTabs(); // QueryList живий - оновлюється при додаванні або видаленні панелей this.tabPanels.changes.subscribe(() => this.buildTabs()); } buildTabs() { this.tabs = this.tabPanels.map((_, i) => ({ label: `Вкладка ${i + 1}` })); } selectTab(index: number) { this.tabPanels.forEach((panel, i) => { panel.nativeElement.hidden = i !== index; }); } } ``` Використання: ```html <app-tabs> <div #tabPanel>Контент 1</div> <div #tabPanel>Контент 2</div> <div #tabPanel>Контент 3</div> </app-tabs> ``` `QueryList.changes` тримає панель вкладок синхронізованою при динамічних змінах. Без цієї підписки кількість вкладок залишалась би замороженою на початковому значенні.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.