برنامه نویسیفرانت اند

ViewChild در Angular: کامپوننت‌ها، Directiveها و عناصر DOM

مقدمه

@ViewChild یک Decorator مربوط به Angular است که Template کامپوننت را Query کرده و اولین مورد مطابقت‌یافته از Instance کامپوننت فرزند، Instance Directive یا عنصر نیتیو DOM را برمی‌گرداند. به کلاس کامپوننت والد، دسترسی برنامه‌ای مستقیم به فرزندان View خودش را می‌دهد؛ بدون اتکا به Query کردن DOM خام یا پاس دادن دستی رویدادها.

این آموزش پوشش می‌دهد چطور با @ViewChild به کامپوننت فرزند، Directive و عنصر DOM از کلاس کامپوننت والد دسترسی پیدا کنید. هم‌چنین گزینه static، گزینه read، @ViewChildren با QueryList و View Queryهای مبتنی بر Signal—معرفی‌شده در Angular 17—را پوشش می‌دهد.

نکات کلیدی

  • @ViewChild اولین کامپوننت فرزند، Directive یا عنصر DOM مطابقت‌یافته از Template خودِ کامپوننت را برمی‌گرداند.
  • نتیجه به‌طور پیش‌فرض در ngAfterViewInit در دسترس است. از static: true فقط برای عناصری استفاده کنید که هرگز داخل Directive ساختاری (Structural Directive) نیستند و لازم است در ngOnInit در دسترس باشند.
  • @ViewChildren یک QueryList از همه عناصر مطابقت‌یافته را برمی‌گرداند. در QueryList.changes مشترک شوید تا به افزودن‌ها و حذف‌های داینامیک واکنش نشان دهید.
  • گزینه read نوع بازگشتی را Override می‌کند؛ که به شما اجازه می‌دهد یک ElementRef یا Instance Directive خاصی را از عنصر مطابقت‌یافته بازیابی کنید.
  • viewChild() و viewChildren() مبتنی بر Signal، جایگزین مدرنِ معرفی‌شده در Angular 17 هستند. در پروژه‌های جدید با هدف Angular 17 یا بعدتر آن‌ها را ترجیح دهید.
  • از دستکاری مستقیم DOM از طریق ElementRef.nativeElement بپرهیزید؛ هر جا Bindingهای Angular یا Renderer2 می‌توانند همان نتیجه را به دست آورند.

پیش‌نیازها

قبل از دنبال کردن این آموزش، مطمئن شوید محیط‌تان این موارد را دارد:

  • Node.js نسخه 18.x یا جدیدتر و npm نصب‌شده
  • Angular CLI نصب‌شده به‌صورت سراسری:
npm install -g @angular/cli
  • آشنایی با کامپوننت‌های Angular و Decoratorهای TypeScript

این آموزش با @angular/core نسخه 17 و @angular/cli نسخه 17 اعتبارسنجی شده است.

@ViewChild چیست و چرا وجود دارد

کامپوننت‌های Angular در بیشتر موارد از طریق Bindingهای @Input و @Output ارتباط برقرار می‌کنند. اما برخی سناریوها نیازمند آنند که کامپوننت والدی متدی را روی کامپوننت فرزند صدا بزند، ویژگی‌ای را از Directiveای که روی عنصر Template اعمال شده بخواند یا مستقیم به Node نیتیو DOM ارجاع بدهد. @ViewChild این موارد را پوشش می‌دهد.

بدون @ViewChild، لازم بود DOM را با document.querySelector جستجو کنید؛ که از تشخیص تغییر Angular عبور کرده و Server-Side Rendering را می‌شکند. @ViewChild با چرخه مقداردهی اولیه View Angular یکپارچه شده و ارجاع Type-شده‌ای را برمی‌گرداند؛ که از دسترسی ناامن DOM اجتناب می‌کند.

سیستم View Query مربوط به Angular چگونه کار می‌کند

Angular کامپوننتی را در دو فاز می‌سازد. در فاز اول، کلاس کامپوننت را Instantiate کرده و ngOnInit را اجرا می‌کند؛ اما عناصر فرزندِ تعریف‌شده در Template هنوز ساخته نشده‌اند. در فاز دوم، Angular View را مقداردهی اولیه کرده؛ کامپوننت‌های فرزند را می‌سازد، Directiveها را اعمال و Nodeهای DOM را درج می‌کند. ngAfterViewInit در انتهای این فاز دوم شلیک می‌شود؛ که دلیلش این است که نتایج @ViewChild آنجا—and نه در ngOnInit—در دسترس‌اند.

وقتی static: false (پیش‌فرض) است، Angular قبل از Resolve کردن Query، منتظر این فاز دوم می‌ماند. وقتی static: true است، Angular Query را بعد از اولین اجرای تشخیص تغییر—که قبل از ngAfterViewInit اما همچنان بعد از کامپایل Template است—به‌صورت همگام Resolve می‌کند. گزینه static در بخش اختصاصی پایین با مثال‌های عملی پوشش داده شده است.

راه‌اندازی پروژه نمونه

ساخت اپلیکیشن Angular

یک Workspace جدید Angular بسازید:

ng new viewchild-demo --no-standalone --routing=false --style=css
Output
CREATE viewchild-demo/src/app/app.component.ts (219 bytes)
CREATE viewchild-demo/src/app/app.module.ts (314 bytes)
CREATE viewchild-demo/src/app/app.component.html (23115 bytes)
...

به دایرکتوری پروژه بروید:

cd viewchild-demo

ساخت کامپوننت فرزند برای مثال‌ها

کامپوننت فرزندی بسازید که مثال‌ها Query‌اش کنند:

ng generate component pup --flat --skip-tests
Output
CREATE src/app/pup.component.css (0 bytes)
CREATE src/app/pup.component.html (19 bytes)
CREATE src/app/pup.component.ts (188 bytes)
UPDATE src/app/app.module.ts (467 bytes)

استفاده از ViewChild با Directiveها

وقتی Directiveای روی عنصر Template اعمال می‌شود، کامپوننت والد به‌طور پیش‌فرض هیچ ارجاع مستقیمی به آن Instance Directive ندارد. @ViewChild این را با اجازه Query کردن کلاس Directive و پس‌گرفتن ارجاع Type-شده به Instanceِ اعمال‌شده در View فعلی حل می‌کند.

مثال زیر SharkDirectiveای می‌سازد که اتریبیوت appShark را می‌خواند و کلمه «Shark» را قبل از متن عنصر Host می‌گذارد. سپس کامپوننت والد با @ViewChild ویژگی‌ای را از آن Instance Directive می‌خواند.

از @angular/cli برای ساخت Directive استفاده کنید:

ng generate directive shark --skip-tests

این دستور فایل shark.directive.ts را می‌سازد و Directive را در app.module.ts ثبت می‌کند:

app.module.ts

import { SharkDirective } from './shark.directive';
...
@NgModule({
  declarations: [
    AppComponent,
    SharkDirective
  ],
  ...
})

سپس، از ElementRef و Renderer2 برای بازنویسی متن استفاده کنید. محتوای shark.directive.ts را با این جایگزین کنید:

shark.directive.ts

import {
  Directive,
  ElementRef,
  Renderer2
} from '@angular/core';

@Directive(
  { selector: '[appShark]' }  // با هر عنصری با اتریبیوت appShark تطبیق می‌یابد
)
export class SharkDirective {
  creature = 'Dolphin';  // متغیر Instance قابل دسترسی از طریق @ViewChild

  constructor(elem: ElementRef, renderer: Renderer2) {
    let shark = renderer.createText('Shark ');  // ساخت یک text node
    renderer.appendChild(elem.nativeElement, shark);  // الصاق به عنصر host
  }
}

بعد، اتریبیوت appShark را به spanی حاوی متن در Template کامپوننت اضافه کنید. محتوای app.component.html را با این جایگزین کنید:

app.component.html

<span appShark>Fin!</span>

هنگام دیدن اپلیکیشن در مرورگر، کلمه «Shark» را قبل از محتوای عنصر رندر می‌کند:

Output
Shark Fin!

حالا، می‌توانید به متغیر Instance مربوط به creature از SharkDirective هم دسترسی پیدا کرده و متغیر Instanceِ extraCreature را با مقدارش تنظیم کنید. محتوای app.component.ts را با این جایگزین کنید:

app.component.ts

import {
  Component,
  ViewChild,
  AfterViewInit
} from '@angular/core';
import { SharkDirective } from './shark.directive';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit {
  extraCreature!: string;

  @ViewChild(SharkDirective)
  set appShark(directive: SharkDirective) {
    // setter وقتی Angular کوئری @ViewChild را resolve می‌کند شلیک می‌شود
    this.extraCreature = directive.creature;
  };

  ngAfterViewInit() {
    // @ViewChild اینجا تضمین شده resolve شده است
    console.log(this.extraCreature); // Dolphin
  }
}

این کد از Setter برای تنظیم متغیر extraCreature استفاده کرد. دقت کنید که برای دسترسی به متغیر منتظر Hook چرخه حیات AfterViewInit می‌ماند؛ چون این زمانی است که کامپوننت‌های فرزند و Directiveها در دسترس می‌شوند. الگوی Setter اینجا به‌جای تعریف مستقیم ویژگی استفاده شده؛ چون هر وقت Angular نتیجه Query را تخصیص می‌دهد به‌صورت همگام شلیک می‌شود—including اگر Instance Directive در زمان اجرا جایگزین شود.

هنگام دیدن اپلیکیشن در مرورگر، همچنان پیام «Shark Fin!» را می‌بینید. اما در Console Log، این نمایش داده می‌شود:

Output
Dolphin

استفاده از ViewChild با عناصر DOM

برای Query کردن عنصر نیتیو DOM با @ViewChild، باید اول در Template با متغیر ارجاع Template—نام محلی‌ای با پیشوند #—علامتش بزنید. Angular از آن نام به‌عنوان Selector استفاده می‌کند. بدون آن، @ViewChild راهی برای شناسایی اینکه کدام عنصر از Template برگردانده شود ندارد.

مثال زیر <input>ای را با #someInput علامت می‌زند؛ سپس از کلاس کامپوننت والدش می‌خواندش تا مقدارش را برنامه‌ای تنظیم کند:

app.component.html

<input #someInput placeholder="Your favorite sea creature">

حالا، می‌توانید به <input> با ViewChild دسترسی پیدا کرده و مقدارش را تنظیم کنید. محتوای app.component.ts را با این جایگزین کنید:

app.component.ts

import {
  Component,
  ViewChild,
  AfterViewInit,
  ElementRef
} from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit {
  @ViewChild('someInput') someInput!: ElementRef<HTMLInputElement>;


  ngAfterViewInit() {
    // nativeElement عنصر HTMLInputElement زیرین را افشا می‌کند
    this.someInput.nativeElement.value = 'Whale!';
  }
}

نکته: دسترسی مستقیم به nativeElement از مدل امنیتی Angular عبور کرده و Server-Side Rendering را می‌شکند. برای عملیات‌های DOM پروداکشن، Renderer2 را Inject و از متدهایش استفاده کنید. برای Binding ویژگی ساده، Bindingهای Template Angular مانند [value] یا [style] را ترجیح دهید.

وقتی ngAfterViewInit شلیک می‌شود، مقدار <input> روی این تنظیم می‌شود:

Output
Whale!

استفاده از ViewChild با کامپوننت‌های فرزند

@ViewChild به کامپوننت والد ارجاع Type-شده‌ای به Instance کامپوننت فرزند می‌دهد. این به شما اجازه می‌دهد متدهای عمومی را روی فرزند صدا بزنید یا ویژگی‌های عمومی‌اش را مستقیم از کلاس والد بخوانید؛ بدون نیاز به Event Emitter یا سرویس مشترک. از این الگو وقتی استفاده کنید که تعامل کاملاً والد-به-فرزند و برنامه‌ای Trigger می‌شود؛ نه توسط رویداد کاربر.

این بخش از PupComponent استفاده می‌کند که در بخش «راه‌اندازی پروژه نمونه» ساخته شد. اگر آن بخش را رد کردید، الان بسازیدش:

ng generate component pup --flat --skip-tests

دستور pup.component.ts، pup.component.css و pup.component.html را می‌سازد و کامپوننت را در app.module.ts ثبت می‌کند:

app.module.ts

import { PupComponent } from './pup.component';
...
@NgModule({
  declarations: [
    AppComponent,
    PupComponent
  ],
  ...
})

سپس، متد whoAmI را به PupComponent اضافه کنید که پیامی برمی‌گرداند:

pup.component.ts

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-pup',
  templateUrl: './pup.component.html',
  styleUrls: ['./pup.component.css']
})
export class PupComponent implements OnInit {

  constructor() { }

  whoAmI() {
    return 'I am a pup component!';
  }

  ngOnInit(): void {
  }

}

بعد، کامپوننت فرزند را در Template اپ ارجاع بدهید. محتوای app.component.html را با این جایگزین کنید:

app.component.html

<app-pup>pup works!</app-pup>

حالا، می‌توانید متد whoAmI را از درون کلاس کامپوننت والد با ViewChild صدا بزنید. محتوای app.component.ts را با این جایگزین کنید:

app.component.ts

import {
  Component,
  ViewChild,
  AfterViewInit
} from '@angular/core';
import { PupComponent } from './pup.component';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements AfterViewInit {
  // ارجاع type-شده به Instance کامپوننت فرزند
  @ViewChild(PupComponent) pup!: PupComponent;

  ngAfterViewInit() {
    // صدا زدن مستقیم متد روی Instance کامپوننت فرزند
    console.log(this.pup.whoAmI()); // I am a pup component!
  }
}

هنگام دیدن اپلیکیشن در مرورگر، Console Log این را نمایش می‌دهد:

Output
I am a pup component!

درک گزینه static

static: true در مقابل static: false

گزینه static کنترل می‌کند Angular کوئری @ViewChild را نسبت به تشخیص تغییر کِی Resolve کند:

گزینهResolve می‌شوددر دسترس درچه زمانی استفاده
static: false (پیش‌فرض)بعد از اولین تشخیص تغییرngAfterViewInitعنصر داخل *ngIf، *ngFor یا هر Directive ساختاری است
static: trueقبل از اولین تشخیص تغییرngOnInitعنصر همیشه موجود و هرگز داخل Directive ساختاری نیست

مثال عملی نمایش تفاوت

با static: false (پیش‌فرض)، به نتیجه Query در ngAfterViewInit دسترسی پیدا کنید:

// app.component.ts
@ViewChild('myElement') myElement!: ElementRef;  // static: false پیش‌فرض است

ngOnInit() {
  console.log(this.myElement); // undefined: کوئری هنوز resolve نشده
}

ngAfterViewInit() {
  console.log(this.myElement); // ElementRef: بعد از مقداردهی اولیه view resolve شده
}

با static: true، نتیجه در ngOnInit در دسترس است:

// app.component.ts
@ViewChild('myElement', { static: true }) myElement!: ElementRef;

ngOnInit() {
  console.log(this.myElement); // ElementRef: قبل از تشخیص تغییر resolve شده
}

از static: true روی عنصری داخل Directive ساختاری استفاده نکنید. Angular نمی‌تواند Query را قبل از تشخیص تغییر Resolve کند اگر عنصر ممکن است هنوز در DOM وجود نداشته باشد.

استفاده از گزینه read برای تغییر نوع بازگشتی

نوع بازگشتی پیش‌فرض کوئری @ViewChild توسط چیزی که Angular در عنصر مطابقت‌یافته پیدا می‌کند تعیین می‌شود: Instance کامپوننت اگر Selector با کامپوننتی تطبیق یابد؛ Instance Directive اگر با Directiveای تطبیق یابد. گزینه read این را Override می‌کند. موارد رایجی که لازمش دارید این‌هاست: گرفتن ElementRef خام برای عنصر Host حتی وقتی کامپوننتی رویش اعمال شده؛ یا انتخاب Directive خاصی از عنصری که بیش از یک Directive رویش اعمال شده.

خواندن ElementRef از Selector کامپوننت

از این وقتی استفاده کنید که عنصر DOM مربوط به Hostِ کامپوننت را لازم دارید؛ نه خود Instance کامپوننت را—مثلاً برای اندازه‌گیری ابعادش یا پاس دادنش به کتابخانه شخص ثالثی که Node خام DOM انتظار دارد:

// app.component.ts
// بدون read، این یک Instance از PupComponent برمی‌گرداند.
// با read: ElementRef، به‌جایش عنصر DOM مربوط به host را برمی‌گرداند.
@ViewChild(PupComponent, { read: ElementRef }) pupElement!: ElementRef;

ngAfterViewInit() {
  console.log(this.pupElement.nativeElement.tagName); // APP-PUP
}

خواندن Directive از یک عنصر

اگر چند Directive روی همان عنصر اعمال شده‌اند، از read برای تعیین اینکه کدام برگردد استفاده کنید:

#myRef و appShark را به همان عنصر در app.component.html اضافه کنید:

app.component.html

<span #myRef appShark>Fin!</span>

سپس Instance مربوط به SharkDirective اعمال‌شده روی آن عنصر را Query کنید:

// app.component.ts
@ViewChild('myRef', { read: SharkDirective }) sharkDir!: SharkDirective;

ngAfterViewInit() {
  console.log(this.sharkDir.creature); // Dolphin
}

دسترسی به چند عنصر با @ViewChildren و QueryList

@ViewChild فقط اولین عنصر مطابقت‌یافته را برمی‌گرداند. از @ViewChildren استفاده کنید وقتی چند عنصر همان Selector را شریک‌اند و به ارجاع همه‌شان نیاز دارید.

تعریف کوئری @ViewChildren

دو Instance از <app-pup> به Template اضافه و آن‌ها را با هم Query کنید:

app.component.html

<app-pup></app-pup>
<app-pup></app-pup>

app.component.ts

import {
  Component,
  ViewChildren,
  AfterViewInit,
  QueryList
} from '@angular/core';
import { PupComponent } from './pup.component';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit {
  // همه Instanceهای PupComponent در Template را برمی‌گرداند
  @ViewChildren(PupComponent) pups!: QueryList<PupComponent>;

  ngAfterViewInit() {
    console.log(this.pups.length); // 2
  }
}

Iterate کردن روی QueryList

از .forEach() برای دسترسی همگام به هر Instance مطابقت‌یافته در نقطه مقداردهی اولیه استفاده کنید؛ مثلاً برای صدا زدن متد Setup یا خواندن مقدار ویژگی اولیه از هر فرزند:

ngAfterViewInit() {
  this.pups.forEach((pup, index) => {
    console.log(`Pup ${index}:`, pup.whoAmI());
  });
}

اشتراک در تغییرات QueryList

QueryList یک Observable مربوط به changes را افشا می‌کند که هر وقت عناصری به مجموعه مطابقت‌یافته اضافه یا از آن حذف می‌شوند—مثلاً وقتی شرط *ngIf کامپوننت فرزندی را به داخل یا خارج View می‌برد—QueryList جدیدی منتشر می‌کند. رندر اولیه changes را Trigger نمی‌کند؛ فقط در به‌روزرسانی‌های بعدی شلیک می‌شود:

ngAfterViewInit() {
  // هر وقت PupComponentای به view اضافه یا از آن حذف شود شلیک می‌شود
  this.pups.changes.subscribe((list: QueryList<PupComponent>) => {
    console.log('Pup count:', list.length);
  });
}

اگر کامپوننت می‌تواند حین فعال بودن اشتراک Destroy شود، اشتراک را ذخیره و در ngOnDestroy از اشتراک خارج شوید تا از نشت حافظه اجتناب شود:

import { Subscription } from 'rxjs';

private pupSub!: Subscription;

ngAfterViewInit() {
  this.pupSub = this.pups.changes.subscribe((list: QueryList<PupComponent>) => {
    console.log('Pup count:', list.length);
  });
}

ngOnDestroy() {
  this.pupSub.unsubscribe();
}

View Queryهای مبتنی بر Signal در Angular 17+

Angular 17 توابع viewChild() و viewChildren() را به‌عنوان جایگزین‌های مبتنی بر Signal برای @ViewChild و @ViewChildren معرفی کرد.

viewChild() در مقابل @ViewChild

ویژگی@ViewChildviewChild()
نوع بازگشتیارجاع مستقیمSignal<T | undefined>
ngAfterViewInit لازم داردبلهخیر
با اپ‌های Zoneless کار می‌کندمحدودبله
موجود ازAngular 2Angular 17

@ViewChild در اپلیکیشن‌های Zoneless کار می‌کند؛ اما نیازمند اعلان دستی تشخیص تغییر از طریق ChangeDetectorRef.markForCheck() است وقتی ارجاع Query-شده به‌روز می‌شود؛ چون Zone.jsای نیست که تشخیص خودکار را Trigger کند. viewChild() مبتنی بر Signal مستقیماً با گراف Reactive Angular یکپارچه شده و بدون مداخله دستی به‌روز می‌شود.

مثال کوئری Signal پایه

مثال زیر از کامپوننت Standalone استفاده می‌کند. API مبتنی بر Signal برای مدل کامپوننت Standalone طراحی شده و نباید در app.module.ts تعریف شود.

مثال از afterNextRender استفاده می‌کند؛ تابع چرخه حیاتی که در Angular 17 معرفی شد و Callbackای را یک‌بار بعد از تکمیل چرخه رندر DOM بعدی اجرا می‌کند. در زمینه‌هایی که چرخه حیات مبتنی بر کلاس نیست—مانند کامپوننت‌های Standalone مبتنی بر Signal—جایگزین ngAfterViewInit می‌شود. از afterNextRender وقتی استفاده کنید که لازم است بعد از رندر اولیه یک‌بار از DOM بخوانید یا در آن بنویسید. روی سرور حین Server-Side Rendering اجرا نمی‌شود:

app.component.ts

import { Component, viewChild, ElementRef, afterNextRender } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<input #nameInput placeholder="Enter name">`,
  standalone: true
})
export class AppComponent {
  // viewChild() مقدار Signal<ElementRef | undefined> برمی‌گرداند
  nameInput = viewChild<ElementRef>('nameInput');

  constructor() {
    afterNextRender(() => {
      // مقدار signal را بعد از رندر view بخوان
      console.log(this.nameInput()?.nativeElement.value);
    });
  }
}

برای عنصری که همیشه موجود است، از viewChild.required() برای حذف Union مربوط به undefined استفاده کنید:

// Signal<ElementRef>: اگر کوئری موردی پیدا نکند پرتاب می‌کند
nameInput = viewChild.required<ElementRef>('nameInput');

چه زمانی کوئری‌های Signal را ترجیح دهیم

از viewChild() و viewChildren() در پروژه‌های جدید با هدف Angular 17 یا بعدتر استفاده کنید؛ به‌ویژه هنگام پذیرش تشخیص تغییر Zoneless یا مدل کامپوننت Standalone. برای پروژه‌های روی Angular 16 یا قدیمی‌تر، از @ViewChild و @ViewChildren استفاده کنید.

برای کامل بودن، اینجا نحوه بازگرداندن viewChildren() سیگنالی روی چند عنصر مطابقت‌یافته آمده. این در کامپوننت‌های مبتنی بر Signal جایگزین @ViewChildren می‌شود. برای این مثال، قبل از اجرای کد یک ChildComponent با ng generate component child --flat --skip-tests --standalone بسازید:

app.component.ts

import { Component, viewChildren, afterNextRender } from '@angular/core';
import { ChildComponent } from './child.component';

@Component({
  selector: 'app-root',
  template: `
    <app-child></app-child>
    <app-child></app-child>
  `,
  standalone: true,
  imports: [ChildComponent]
})
export class AppComponent {
  // viewChildren() مقدار Signal<ReadonlyArray<ChildComponent>> برمی‌گرداند
  children = viewChildren(ChildComponent);

  constructor() {
    afterNextRender(() => {
      console.log(this.children().length); // 2
    });
  }
}

@ViewChild در مقابل @ContentChild در مقابل @ViewChildren

Decorator / تابعQuery می‌کندبرمی‌گرداندچندگانگیدر دسترس در
@ViewChildTemplate خودِ کامپوننتاولین موردمنفردngAfterViewInit (یا ngOnInit با static: true)
@ViewChildrenTemplate خودِ کامپوننتQueryListچندتاییngAfterViewInit
@ContentChildمحتوای Project-شده از طریقاولین موردمنفردngAfterContentInit
viewChild()Template خودِ کامپوننتSignalمنفردهنگام خواندن (Angular 17+)
viewChildren()Template خودِ کامپوننتSignal<ReadonlyArray>چندتاییهنگام خواندن (Angular 17+)

از @ContentChild هنگام ساخت کامپوننت‌های Wrapper قابل استفاده مجدد که محتوای Project-شده می‌پذیرند استفاده کنید. از @ViewChild یا viewChild() برای عناصر در Template خودِ کامپوننت.

اشتباهات رایج و نحوه اجتناب از آن‌ها

دسترسی به @ViewChild قبل از ngAfterViewInit

دسترسی به ویژگی @ViewChild در ngOnInit مقدار undefined برمی‌گرداند؛ چون Angular هنوز View را مقداردهی اولیه نکرده است.

نادرست:

// app.component.ts
ngOnInit() {
  // TypeError: cannot read properties of undefined
  console.log(this.someInput.nativeElement.value);
}

درست:

// app.component.ts
ngAfterViewInit() {
  console.log(this.someInput.nativeElement.value); // طبق انتظار کار می‌کند
}

منطق دسترسی را به ngAfterViewInit منتقل کنید؛ جایی که Angular تضمین می‌کند View مقداردهی اولیه شده و Query Resolve شده است.

Query کردن عناصر داخل *ngIf یا *ngFor

تنظیم static: true روی کوئریِ هدف‌گیرنده عنصری داخل Directive ساختاری، باعث می‌شود Angular قبل از اینکه عنصر ممکن است وجود داشته باشد، تلاش به Resolve کند.

نادرست:

// app.component.ts
@ViewChild('conditionalEl', { static: true }) el!: ElementRef;
// static: true نمی‌تواند عنصری را resolve کند که *ngIf ممکن است هنوز رندرش نکرده باشد

درست:

// app.component.ts
@ViewChild('conditionalEl') el: ElementRef | undefined;
// static: false (پیش‌فرض) بعد از تشخیص تغییر resolve می‌شود

ngAfterViewInit() {
  if (this.el) {
    this.el.nativeElement.focus();
  }
}

استفاده از static: false و Guard کردن در برابر undefined تضمین می‌کند کوئری فقط وقتی اجرا شود که عنصر در DOM موجود است.

استفاده بیش از حد از ElementRef برای چیزی که @Input/@Output می‌تواند مدیریت کند

نفوذ به کامپوننت فرزند از طریق read: ElementRef برای دستکاری ظاهرش، وقتی Binding @Input می‌تواند همان نتیجه را به دست آورد، غیرضروری است.

نادرست:

// app.component.ts
// از read: ElementRef برای رسیدن به عنصر host و تنظیم مستقیم استایل استفاده می‌کند.
@ViewChild(PupComponent, { read: ElementRef }) pupEl!: ElementRef;

ngAfterViewInit() {
  this.pupEl.nativeElement.style.color = 'red';
}

درست:

// pup.component.ts
import { Input } from '@angular/core';

export class PupComponent {

  @Input() highlightColor: string = '';

}
// pup.component.html
// <span [style.color]="highlightColor">{{ message }}</span>

<!-- app.component.html -->
<app-pup [highlightColor]="'red'"></app-pup>

پاس دادن داده از طریق @Input والد و فرزند را غیرزوج‌شده نگه می‌دارد و مدل تشخیص تغییر و امنیت Angular را حفظ می‌کند.

سوالات متداول

تفاوت @ViewChild و @ContentChild در Angular چیست؟

@ViewChild عناصر تعریف‌شده در Template خودِ کامپوننت را Query می‌کند. @ContentChild عناصر Project-شده به کامپوننت از طریق <ng-content> را Query می‌کند. از @ContentChild هنگام ساخت کامپوننت‌های Wrapper قابل استفاده مجدد که محتوای Project-شده از والدی می‌پذیرند استفاده کنید.

چرا ویژگی @ViewChild من در ngOnInit مقدار undefined دارد؟

به‌طور پیش‌فرض، @ViewChild بعد از مقداردهی اولیه View—which بعد از ngOnInit رخ می‌دهد—Resolve می‌شود. منطق دسترسی‌تان را به ngAfterViewInit منتقل کنید. اگر دسترسی در ngOnInit لازم دارید، static: true را تنظیم کنید؛ اما فقط وقتی عنصر Query-شده داخل Directive ساختاری نیست.

گزینه static در @ViewChild چه می‌کند؟

static: true به Angular می‌گوید Query را قبل از اجرای تشخیص تغییر Resolve کند؛ که نتیجه را در ngOnInit در دسترس می‌سازد. static: false (پیش‌فرض) Query را بعد از تشخیص تغییر Resolve می‌کند؛ که در ngAfterViewInit در دسترسش می‌سازد. از static: false برای عناصر داخل *ngIf یا *ngFor استفاده کنید.

چطور به چند عنصر فرزند با همان Selector دسترسی پیدا کنم؟

به‌جای @ViewChild از @ViewChildren استفاده کنید. QueryListای حاوی همه عناصر مطابقت‌یافته را برمی‌گرداند. با .forEach() Iterate کنید یا در .changes مشترک شوید تا وقتی فهرست به‌روز می‌شود واکنش نشان دهید.

آیا دستکاری مستقیم DOM با ElementRef از @ViewChild امن است؟

دستکاری مستقیم DOM از طریق ElementRef.nativeElement از مدل امنیتی Angular عبور کرده و Server-Side Rendering را می‌شکند. از سرویس Renderer2 Angular برای عملیات‌های DOM استفاده کنید؛ یا Bindingهای Angular مانند [style]، [class] یا @HostBinding را تا جای ممکن ترجیح دهید.

جایگزین مبتنی بر Signal برای @ViewChild در Angular 17+ چیست؟

Angular 17 تابع viewChild() را به‌عنوان جایگزین Reactive معرفی کرد. سیگنالی را برمی‌گرداند که وقتی عنصر Query-شده تغییر می‌کند خودکار به‌روز می‌شود؛ و با مدل Reactivity مبتنی بر Signal Angular یکپارچه است. در پروژه‌های جدید با هدف Angular 17 یا بعدتر استفاده‌اش کنید.

*آیا @ViewChild می‌تواند عنصری داخل بلاک ngIf را Query کند؟

بله؛ اما فقط با static: false (پیش‌فرض). وقتی شرط *ngIf نادرست است و عنصر رندر نمی‌شود، ویژگی @ViewChild مقدار undefined دارد. همیشه قبل از دسترسی به ارجاع Query-شده، undefined را چک کنید.

گزینه read در @ViewChild برای چیست؟

گزینه read به Angular می‌گوید چه نوعی برای عنصر مطابقت‌یافته Inject شود. مثلاً @ViewChild('myRef', { read: ElementRef }) حتی اگر Selector با کامپوننتی تطبیق یابد، ElementRef برمی‌گرداند. @ViewChild('myRef', { read: MyDirective }) Instance Directiveِ اعمال‌شده روی آن عنصر را برمی‌گرداند.

نتیجه‌گیری

این آموزش پوشش داد چطور با @ViewChild به کامپوننت فرزند، Directive و عنصر نیتیو DOM از کلاس کامپوننت والد دسترسی پیدا کنید. هم‌چنین گزینه‌های static و read، Query کردن چند عنصر با @ViewChildren و QueryList و API مبتنی بر Signal یعنی viewChild() موجود در Angular 17 را پوشش داد.

حالا می‌توانید ارجاع‌های Type-شده به هر عنصری در Template کامپوننت را Query کنید، کنترل کنید کوئری‌ها نسبت به چرخه مقداردهی اولیه Angular کِی Resolve شوند؛ و بین APIهای Query مبتنی بر Decorator و مبتنی بر Signal بر اساس نسخه Angular پروژه‌تان انتخاب کنید.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا