@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
, но она вызывает слишком много бесполезных вызовов.
Ответы
Ответ 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);