Подтвердить что ты не робот

@ViewChild in * ngIf

Вопрос

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

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

4b9b3361

Ответ 1

Принятый ответ с использованием QueryList не работал у меня. Однако то, что работала с помощью setter для ViewChild:

 private contentPlaceholder: ElementRef;

 @ViewChild('contentPlaceholder') set content(content: ElementRef) {
    this.contentPlaceholder = content;
 }

Сетчатель вызывается один раз * ngIf становится истинным.

Ответ 2

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

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

constructor(private changeDetector : ChangeDetectorRef) {}

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

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

Ответ 3

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

Ответ 4

Angular 8

Вы должны добавить { static: false } в качестве второй опции для @ViewChild

Пример:

export class AppComponent  {

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

  display = false;

  constructor(private changeDetectorRef: ChangeDetectorRef) {

  }

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

Пример Stackblitz: https://stackblitz.com/edit/angular-d8ezsn

Ответ 5

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

Ответ 6

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

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

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

Ответ 7

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

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

Ответ 8

Моя цель состояла в том, чтобы избежать любых хакерских методов, которые предполагают что-то (например, 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 элемента @ViewChild. При этом я откладываю выполнение кода с subscribe до тех пор, пока *ngIf станет положительным и инициализация @ViewChild не @ViewChild.

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

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

Ответ 9

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

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

До

HomePageComponent Template

<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 Template

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

HomePageComponent компонент

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

Ответ 10

Некий общий подход:

Вы можете создать метод, который будет ждать, пока ViewChild будет готов

function waitWhileViewChildIsReady(parent: any, viewChildName: string, refreshRateSec: number = 50, maxWaitTime: number = 3000): Observable<any> {
  return interval(refreshRateSec)
    .pipe(
      takeWhile(() => !isDefined(parent[viewChildName])),
      filter(x => x === undefined),
      takeUntil(timer(maxWaitTime)),
      endWith(parent[viewChildName]),
      flatMap(v => {
        if (!parent[viewChildName]) throw new Error('ViewChild "${viewChildName}" is never ready');
        return of(!parent[viewChildName]);
      })
    );
}


function isDefined<T>(value: T | undefined | null): value is T {
  return <T>value !== undefined && <T>value !== null;
}

Использование:

  // Now you can do it in any place of your code
  waitWhileViewChildIsReady(this, 'yourViewChildName').subscribe(() =>{
      // your logic here
  })

Ответ 11

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

Ответ 12

In my case, its still not working. Can someone help here ?

HTML Code

    <div *ngIf="someCondition()">
    <div class="row top-container">
        <div class="col-md-6">
          <h1 class="text-center">Registered </h1>
        </div>

        <div class="col-md-6">
          <div class="pull-right header-buttons-container">
            <button #registerPartnerButton id="registerPartner" class="btn btn-primary" type="button"  (click)="registerUser()">Register</button>
          </div>
        </div>
      </div>
    </div>

TS Code

    registerButton: ElementRef;
    @ViewChildren('registerPartnerButton') set someDummyName(content: ElementRef) {
        this.registerPartnerButton = content;
      };
    constructor(changeDetector: ChangeDetectorRef) {}

Issue:
Ever after applying above solutions, i'm getting registerPartnerButton as undefined. but When i checked the logs, it shows me something like below


  [1]: https://i.stack.imgur.com/mpb16.png

Ответ 13

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

Ответ 14

Работа на 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();
  });

Это также позволило мне избежать использования каких-либо дополнительных библиотек или импорта.

Ответ 15

Это работало на меня. Просто примените некоторую задержку после привязки данных к форме.

 setTimeout(() => { this.changeDetector.detectChanges(); }, 500);