@ViewChild в * ngIf


215

Питання

Який найелегантніший спосіб отримати @ViewChildвідповідний елемент у шаблоні?

Нижче наведено приклад. Також Plunker доступний.

Шаблон:

<div id="layout" *ngIf="display">
    <div #contentPlaceholder></div>
</div>

Компонент:

export class AppComponent {

    display = false;
    @ViewChild('contentPlaceholder', {read: ViewContainerRef}) viewContainerRef;

    show() {
        this.display = true;
        console.log(this.viewContainerRef); // undefined
        setTimeout(()=> {
            console.log(this.viewContainerRef); // OK
        }, 1);
    }
}

У мене є компонент з його вмістом за замовчуванням прихований. Коли хтось викликає show()метод, він стає видимим. Однак перед тим, як виявити зміни Angular 2, я не можу посилатися на це viewContainerRef. Зазвичай я вкладаю всі необхідні дії, setTimeout(()=>{},1)як показано вище. Чи є більш правильний спосіб?

Я знаю, що є варіант з ngAfterViewChecked, але це викликає занадто багато марних дзвінків.

ВІДПОВІДЬ (Plunker)


3
ви намагалися використовувати атрибут [hidden] замість * ngIf? Це працювало для мене у подібній ситуації.
Шардул

Відповіді:


335

Використовуйте сетер для ViewChild:

 private contentPlaceholder: ElementRef;

 @ViewChild('contentPlaceholder') set content(content: ElementRef) {
    if(content) { // initially setter gets called with undefined
        this.contentPlaceholder = content;
    }
 }

Оператор викликається, коли елемент *ngIfстає посиланням на елемент true.

Зауважте, для Angular 8 ви повинні переконатися, що це встановлено { static: false }, що є налаштуванням за замовчуванням в інших версіях Agnular:

 @ViewChild('contentPlaceholder', { static: false })

Примітка: якщо contentPlaceholder є компонентом, ви можете змінити ElementRef на ваш клас компонентів:

  private contentPlaceholder: MyCustomComponent;
  @ViewChild('contentPlaceholder') set content(content: MyCustomComponent) {
     if(content) { // initially setter gets called with undefined
          this.contentPlaceholder = content;
     }
  }

27
зауважте, що цей сеттер називається спочатку з невизначеним вмістом, тому перевіряйте на нуль, чи робите щось у сетері
отримуйте

1
Гарна відповідь, але contentPlaceholderце ElementRefне так ViewContainerRef.
developer033

6
Як ви називаєте сетера?
Леандро Кусак

2
@LeandroCusack його називають автоматично, коли кутовий знаходить <div #contentPlaceholder></div>. Технічно ви можете назвати це вручну, як і будь-який інший сетер, this.content = someElementRefале я не розумію, чому ви хочете це зробити.
парламент

3
Просто корисна примітка для тих, хто зараз стикається з цим - вам потрібно мати @ViewChild ('myComponent', {static: false}), де ключовим бітом є статичний: false, який дозволяє йому приймати різні дані.
nospamthanks

107

Альтернативою для подолання цього є запуск детектора змін вручну.

Ви спочатку вводите ChangeDetectorRef:

constructor(private changeDetector : ChangeDetectorRef) {}

Потім ви викликаєте це після оновлення змінної, яка керує * ngIf

show() {
        this.display = true;
        this.changeDetector.detectChanges();
    }

1
Дякую! Я використовував прийняту відповідь, але вона все ще спричиняла помилку, оскільки діти все ще не визначалися, коли я намагався їх використати десь після цього onInit(), тому я додав функцію detectChangesдо виклику будь-якої дочірньої функції, і це виправити. (Я використав і прийняту відповідь, і цю відповідь)
Minyc510

Супер корисно! Дякую!
AppDreamer

55

Кутовий 8+

Ви повинні додати { static: false }як другий варіант для @ViewChild. Це призводить до вирішення результатів запиту після запуску виявлення змін, що дозволяє @ViewChildоновлюватись після зміни значення.

Приклад:

export class AppComponent {
    @ViewChild('contentPlaceholder', { static: false }) contentPlaceholder: ElementRef;

    display = false;

    constructor(private changeDetectorRef: ChangeDetectorRef) {
    }

    show() {
        this.display = true;
        this.changeDetectorRef.detectChanges(); // not required
        console.log(this.contentPlaceholder);
    }
}

Приклад Stackblitz: https://stackblitz.com/edit/angular-d8ezsn


3
Дякую Святославу. Випробував усе вище, але тільки ваше рішення спрацювало.
Пітер Дрінан

Це також працювало для мене (як і хитрість глядачів). Цей більш інтуїтивний і легший для кутових 8.
Олексій

2
Працював як шарм :)
Sandeep K Nair

1
Це має бути прийнятою відповіддю для останньої версії.
Кришна Прашатт

Я використовую <mat-horizontal-stepper *ngIf="viewMode === DialogModeEnum['New']" linear #stepper, @ViewChild('stepper', {static: true}) private stepper: MatStepper;і this.changeDetector.detectChanges();та він по- , як і раніше не працює.
Пол Струпейкіс

21

Наведені вище відповіді не спрацювали для мене, оскільки в моєму проекті ngIf знаходиться на вхідному елементі. Мені потрібен був доступ до атрибута nativeElement, щоб зосередитись на вході, коли ngIf вірно. Здається, немає атрибута nativeElement у ViewContainerRef. Ось що я зробив (дотримуючись документації @ViewChild ):

<button (click)='showAsset()'>Add Asset</button>
<div *ngIf='showAssetInput'>
    <input #assetInput />
</div>

...

private assetInputElRef:ElementRef;
@ViewChild('assetInput') set assetInput(elRef: ElementRef) {
    this.assetInputElRef = elRef;
}

...

showAsset() {
    this.showAssetInput = true;
    setTimeout(() => { this.assetInputElRef.nativeElement.focus(); });
}

Перед фокусуванням я використовував setTimeout, тому що ViewChild призначає секунду. Інакше це було б не визначено.


2
Для мене працював setTimeout () з 0. Мій елемент, прихований моїм ngIf був правильно пов’язаний після setTimeout, без необхідності функція встановленого активу Input () посередині.
Буде Шавер

Ви можете виявити зміни в showAsset () і не потрібно використовувати час очікування.
WrksOnMyMachine

Як це відповідь? ОП вже згадувалося за допомогою setTimeout? I usually wrap all required actions into setTimeout(()=>{},1) as shown above. Is there a more correct way?
Хуан Мендес

12

Як вже згадували інші, найшвидше і швидке рішення - використовувати [приховано] замість * ngIf, таким чином компонент буде створений, але не видно, там ви можете мати доступ до нього, хоча це може бути не найефективнішим шлях.


1
Ви повинні зауважити, що використання "[hidden]" може не працювати, якщо елемент не є "display: block". краще використовувати [style.display] = "умова? '':" жоден ""
Фелікс Брунет

10

Це може спрацювати, але я не знаю, чи зручно це для вашого випадку:

@ViewChildren('contentPlaceholder', {read: ViewContainerRef}) viewContainerRefs: QueryList;

ngAfterViewInit() {
 this.viewContainerRefs.changes.subscribe(item => {
   if(this.viewContainerRefs.toArray().length) {
     // shown
   }
 })
}

1
Ви можете спробувати ngAfterViewInit()замість цього ngOnInit(). Я припускав, що viewContainerRefsце вже ініціалізовано, але ще не містить елементів. Здається, я пам’ятав це неправильно.
Günter Zöchbauer

Вибачте, я помилився AfterViewInitнасправді працює. Я видалив усі свої коментарі, щоб не бентежити людей. Ось робочий Plunker: plnkr.co/edit/myu7qXonmpA2hxxU3SLB?p=preview
sinedsem

1
Це насправді хороша відповідь. Це працює, і я зараз цим користуюся. Дякую!
Костянтин

1
Це працювало для мене після оновлення з кутового 7 до 8. З якоїсь причини оновлення призвело до того, що компонент не буде визначений у AfterViewInit, навіть із використанням static: false для нового синтаксису ViewChild, коли компонент був загорнутий у ngIf. Також зауважте, що QueryList потребує такого типу, як цей QueryList <YourComponentType>;
Алекс

Можливо, зміни будуть пов'язані з constпараметромViewChild
Günter Zöchbauer

9

Ще один швидкий "трюк" (просте рішення) - просто використовувати тег [прихований] замість * ngIf, важливо знати, що в цьому випадку Angular будує об'єкт і малює його під класом: приховано, тому ViewChild працює без проблем . Тому важливо пам’ятати, що не слід використовувати приховані на важких або дорогих предметах, які можуть спричинити проблеми з продуктивністю

  <div class="addTable" [hidden]="CONDITION">

Якщо це приховане знаходиться всередині іншого, якщо тоді потрібно змінити багато речей
VIKAS KOHLI

6

Моя мета полягала в тому, щоб уникнути будь-яких хакітівських методів, які передбачають щось (наприклад, setTimeout), і я в кінцевому підсумку реалізував прийняте рішення з трохи смаком RxJS зверху:

  private ngUnsubscribe = new Subject();
  private tabSetInitialized = new Subject();
  public tabSet: TabsetComponent;
  @ViewChild('tabSet') set setTabSet(tabset: TabsetComponent) {
    if (!!tabSet) {
      this.tabSet = tabSet;
      this.tabSetInitialized.next();
    }
  }

  ngOnInit() {
    combineLatest(
      this.route.queryParams,
      this.tabSetInitialized
    ).pipe(
      takeUntil(this.ngUnsubscribe)
    ).subscribe(([queryParams, isTabSetInitialized]) => {
      let tab = [undefined, 'translate', 'versions'].indexOf(queryParams['view']);
      this.tabSet.tabs[tab > -1 ? tab : 0].active = true;
    });
  }

Мій сценарій: я хотів запустити дію на @ViewChildелемент залежно від маршрутизатора queryParams. Через те, що обгортання *ngIfє помилковим, поки запит HTTP не поверне дані, ініціалізація @ViewChildелемента відбувається із запізненням.

Як це працює: combineLatest вперше видає значення лише тоді, коли кожна із наданих Оглядових даних випромінює перше значення з моменту combineLatestпідписки. tabSetInitializedПід час встановлення @ViewChildелемента My Subject видає значення . Тим самим я затягую виконання коду під тим, subscribeпоки *ngIfобороти не будуть позитивними та не @ViewChildбуде ініціалізовано.

Звичайно, не забудьте скасувати підписку на ngOnDestroy, я це роблю за допомогою ngUnsubscribeSubject:

  ngOnDestroy() {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
  }

велике спасибі, у мене була та сама проблема, з tabSet & ngIf ваш метод врятував мені багато часу і головний біль. Ура m8;)
Exitl0l

3

У спрощеній версії, у мене була подібна проблема з цим, коли використовувався JS SDK Google Maps.

Моє рішення полягало в тому, щоб витягнути divі ViewChildмати свій дочірній компонент, який при використанні в батьківському компоненті можна було приховати / відобразити за допомогою *ngIf.

До цього

HomePageComponent Шаблон

<div *ngIf="showMap">
  <div #map id="map" class="map-container"></div>
</div>

HomePageComponent Компонент

@ViewChild('map') public mapElement: ElementRef; 

public ionViewDidLoad() {
    this.loadMap();
});

private loadMap() {

  const latLng = new google.maps.LatLng(-1234, 4567);
  const mapOptions = {
    center: latLng,
    zoom: 15,
    mapTypeId: google.maps.MapTypeId.ROADMAP,
  };
   this.map = new google.maps.Map(this.mapElement.nativeElement, mapOptions);
}

public toggleMap() {
  this.showMap = !this.showMap;
 }

Після

MapComponent Шаблон

 <div>
  <div #map id="map" class="map-container"></div>
</div>

MapComponent Компонент

@ViewChild('map') public mapElement: ElementRef; 

public ngOnInit() {
    this.loadMap();
});

private loadMap() {

  const latLng = new google.maps.LatLng(-1234, 4567);
  const mapOptions = {
    center: latLng,
    zoom: 15,
    mapTypeId: google.maps.MapTypeId.ROADMAP,
  };
   this.map = new google.maps.Map(this.mapElement.nativeElement, mapOptions);
}

HomePageComponent Шаблон

<map *ngIf="showMap"></map>

HomePageComponent Компонент

public toggleMap() {
  this.showMap = !this.showMap;
 }

1

У моєму випадку мені потрібно було завантажувати цілий модуль лише тоді, коли в шаблоні існував div, тобто розетка знаходилася всередині ngif. Таким чином, кожен раз, коли кут виявляв елемент #geolocalisationOutlet, він створював компонент всередині нього. Модуль також завантажується лише один раз.

constructor(
    public wlService: WhitelabelService,
    public lmService: LeftMenuService,
    private loader: NgModuleFactoryLoader,
    private injector: Injector
) {
}

@ViewChild('geolocalisationOutlet', {read: ViewContainerRef}) set geolocalisation(geolocalisationOutlet: ViewContainerRef) {
    const path = 'src/app/components/engine/sections/geolocalisation/geolocalisation.module#GeolocalisationModule';
    this.loader.load(path).then((moduleFactory: NgModuleFactory<any>) => {
        const moduleRef = moduleFactory.create(this.injector);
        const compFactory = moduleRef.componentFactoryResolver
            .resolveComponentFactory(GeolocalisationComponent);
        if (geolocalisationOutlet && geolocalisationOutlet.length === 0) {
            geolocalisationOutlet.createComponent(compFactory);
        }
    });
}

<div *ngIf="section === 'geolocalisation'" id="geolocalisation">
     <div #geolocalisationOutlet></div>
</div>


0

Робота над Angular 8 Не потрібно імпортувати ChangeDector

ngIf дозволяє не завантажувати елемент і уникати додаткового навантаження на додаток. Ось як я запустив його без ChangeDetector

elem: ElementRef;

@ViewChild('elemOnHTML', {static: false}) set elemOnHTML(elemOnHTML: ElementRef) {
    if (!!elemOnHTML) {
      this.elem = elemOnHTML;
    }
}

Тоді, коли я змінюю своє ngIf значення на truthy, я б використовував setTimeout, як це, щоб він чекав лише наступного циклу змін:

  this.showElem = true;
  console.log(this.elem); // undefined here
  setTimeout(() => {
    console.log(this.elem); // back here through ViewChild set
    this.elem.do();
  });

Це також дозволило мені уникати використання будь-яких додаткових бібліотек чи імпорту.


0

для Angular 8 - суміш перевірки нуля і @ViewChild static: falseхакерства

для управління пейджингом, що чекає даних про асинхронізацію

@ViewChild(MatPaginator, { static: false }) set paginator(paginator: MatPaginator) {
  if(!paginator) return;
  paginator.page.pipe(untilDestroyed(this)).subscribe(pageEvent => {
    const updated: TSearchRequest = {
      pageRef: pageEvent.pageIndex,
      pageSize: pageEvent.pageSize
    } as any;
    this.dataGridStateService.alterSearchRequest(updated);
  });
}

0

Це працює для мене, якщо я використовую ChangeDetectorRef у Angular 9

@ViewChild('search', {static: false})
public searchElementRef: ElementRef;

constructor(private changeDetector: ChangeDetectorRef) {}

//then call this when this.display = true;
show() {
   this.display = true;
   this.changeDetector.detectChanges();
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.