@ViewChild in * ngIf

216

Вопрос

Какой самый элегантный способ получить @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, но она вызывает слишком много бесполезных звонков.

ОТВЕТ (Плункер)

sinedsem
источник
3
Вы пытались использовать атрибут [hidden] вместо * ngIf? Это сработало для меня в аналогичной ситуации.
Шардул

Ответы:

336

Используйте сеттер для 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
обратите внимание, что этот установщик изначально вызывается с неопределенным содержимым, поэтому проверьте наличие нуля, если что-то делаете в установщике
Recep
1
Хороший ответ, но contentPlaceholderэто ElementRefне так ViewContainerRef.
developer033
6
Как вы называете сеттер?
Леандро Кьюсак
2
@LeandroCusack вызывается автоматически, когда Angular находит <div #contentPlaceholder></div>. Технически вы можете вызвать это вручную, как любой другой сеттер, this.content = someElementRefно я не понимаю, почему вы захотите это сделать.
парламент
3
Просто полезное примечание для тех, кто сталкивается с этим сейчас - вам нужно иметь @ViewChild ('myComponent', {static: false}), где бит ключа является static: false, что позволяет ему принимать различные входные данные.
nospamth спасибо
108

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

Вы сначала вводите ChangeDetectorRef:

constructor(private changeDetector : ChangeDetectorRef) {}

Затем вы вызываете его после обновления переменной, управляющей * ngIf

show() {
        this.display = true;
        this.changeDetector.detectChanges();
    }
Джефферсон Лима
источник
1
Спасибо! Я использовал принятый ответ, но он все еще вызывал ошибку, потому что дети все еще не были определены, когда я пытался использовать их некоторое время спустя onInit(), поэтому я добавил detectChangesперед вызовом любую дочернюю функцию, и это исправило ее. (Я использовал как принятый ответ, так и этот ответ)
Minyc510
Супер полезно! Спасибо!
AppDreamer
57

Угловой 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 равен true. Кажется, нет никакого атрибута 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 требуется секунда, чтобы быть назначенным. В противном случае это было бы неопределенным.

zebraco
источник
2
SetTimeout () 0 работал для меня. Мой элемент, скрытый моим ngIf, был правильно связан после setTimeout, без необходимости устанавливать функцию assetInput () в середине.
Будет ли бритва
Вы можете обнаружить изменения в showAsset () и не использовать таймаут.
WrksOnMyMachine
Как это ответ? ОП уже упоминалось с помощью setTimeout? I usually wrap all required actions into setTimeout(()=>{},1) as shown above. Is there a more correct way?
Хуан Мендес
12

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

user3728728
источник
1
Вы должны заметить, что использование «[hidden]» может не работать, если элемент не имеет «display: block». лучше использовать [style.display] = "condition? '': 'none'"
Феликс Брюне
10

Это может сработать, но я не знаю, удобно ли это для вашего случая:

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

ngAfterViewInit() {
 this.viewContainerRefs.changes.subscribe(item => {
   if(this.viewContainerRefs.toArray().length) {
     // shown
   }
 })
}
Гюнтер Цохбауэр
источник
1
Можете ли вы попробовать ngAfterViewInit()вместо ngOnInit(). Я предположил, что viewContainerRefsон уже инициализирован, но еще не содержит элементов. Кажется, я запомнил это неправильно.
Гюнтер Цохбауэр
Извините, я был не прав. AfterViewInitна самом деле работает. Я удалил все свои комментарии, чтобы не запутать людей. Вот работающий Плункер: plnkr.co/edit/myu7qXonmpA2hxxU3SLB?p=preview
sinedsem
1
На самом деле это хороший ответ. Это работает, и я использую это сейчас. Спасибо!
Константин
1
Это сработало для меня после обновления с углового 7 до 8. По какой-то причине обновление вызвало неопределенность компонента в afterViewInit даже с использованием static: false в соответствии с новым синтаксисом ViewChild, когда компонент был обернут в ngIf. Также обратите внимание, что для QueryList теперь требуется тип, подобный этому QueryList <YourComponentType>;
Алекс
Может быть изменение, связанное с constпараметромViewChild
Гюнтер Цохбауэр
9

Другой быстрый «трюк» (простое решение) - просто использовать тег [hidden] вместо * ngIf, просто важно знать, что в этом случае Angular создает объект и рисует его в классе: hidden, поэтому ViewChild работает без проблем. , Поэтому важно помнить, что вы не должны использовать скрытые тяжелые или дорогие предметы, которые могут вызвать проблемы с производительностью

  <div class="addTable" [hidden]="CONDITION">
Или Яаков
источник
Если то, что скрыто, находится внутри другого, тогда нужно много чего менять
ВИКАС КОЛИ
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 выдает значение в первый раз, только когда каждый из предоставленных Observables выдает первое значение с момента combineLatestподписки. Мой предметtabSetInitialized испускает значение, когда @ViewChildэлемент устанавливается. При этом я откладываю выполнение кода subscribeдо *ngIfположительного поворота и @ViewChildинициализации.

Конечно, не забудьте отменить подписку на ngOnDestroy, я делаю это, используя ngUnsubscribeтему:

  ngOnDestroy() {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
  }
Филипп Юнку
источник
Большое спасибо, у меня была такая же проблема, с tabSet & ngIf, ваш метод сэкономил мне много времени и головной боли. Приветствия m8;)
Выход 10
3

Упрощенная версия, у меня была похожая проблема при использовании Google Maps JS SDK.

Моим решением было извлечь 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. Таким образом, каждый раз, когда angular обнаруживает элемент #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>
Gabb1995
источник
1

Я думаю, что использование defer от lodash имеет большой смысл, особенно в моем случае, когда мой @ViewChild()был внутри asyncтрубы

Timo
источник
0

Работа на Angular 8 Нет необходимости импортировать ChangeDector

ngIf позволяет вам не загружать элемент и избегать дополнительной нагрузки на ваше приложение. Вот как я запустил его без ChangeDetector

elem: ElementRef;

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

Затем, когда я изменю свое значение ngIf на истинное, я буду использовать 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);
  });
}
Jenson кнопки-событие
источник
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();
}
Иван Сим
источник