typescript - update - viewchild undefined angular 6
La anotaciĆ³n angular 2 @ViewChild devuelve indefinida (17)
Estoy tratando de aprender Angular 2.
Me gustaría acceder a un componente secundario desde un componente primario utilizando la Anotación @ViewChild .
Aquí algunas líneas de código:
En BodyContent.ts tengo:
import {ViewChild, Component, Injectable} from ''angular2/core'';
import {FilterTiles} from ''../Components/FilterTiles/FilterTiles'';
@Component({
selector: ''ico-body-content''
, templateUrl: ''App/Pages/Filters/BodyContent/BodyContent.html''
, directives: [FilterTiles]
})
export class BodyContent {
@ViewChild(FilterTiles) ft:FilterTiles;
public onClickSidebar(clickedElement: string) {
console.log(this.ft);
var startingFilter = {
title: ''cognomi'',
values: [
''griffin''
, ''simpson''
]}
this.ft.tiles.push(startingFilter);
}
}
mientras que en FilterTiles.ts :
import {Component} from ''angular2/core'';
@Component({
selector: ''ico-filter-tiles''
,templateUrl: ''App/Pages/Filters/Components/FilterTiles/FilterTiles.html''
})
export class FilterTiles {
public tiles = [];
public constructor(){};
}
Finalmente aquí las plantillas (como se sugiere en los comentarios):
BodyContent.html
<div (click)="onClickSidebar()" class="row" style="height:200px; background-color:red;">
<ico-filter-tiles></ico-filter-tiles>
</div>
FilterTiles.html
<h1>Tiles loaded</h1>
<div *ngFor="#tile of tiles" class="col-md-4">
... stuff ...
</div>
La plantilla FilterTiles.html está cargada correctamente en la etiqueta ico-filter-tiles (de hecho, puedo ver el encabezado).
Nota: la clase BodyContent se inyecta dentro de otra plantilla (Body) usando DynamicComponetLoader: dcl.loadAsRoot (BodyContent, ''# ico-bodyContent'', inyector):
import {ViewChild, Component, DynamicComponentLoader, Injector} from ''angular2/core'';
import {Body} from ''../../Layout/Dashboard/Body/Body'';
import {BodyContent} from ''./BodyContent/BodyContent'';
@Component({
selector: ''filters''
, templateUrl: ''App/Pages/Filters/Filters.html''
, directives: [Body, Sidebar, Navbar]
})
export class Filters {
constructor(dcl: DynamicComponentLoader, injector: Injector) {
dcl.loadAsRoot(BodyContent, ''#ico-bodyContent'', injector);
dcl.loadAsRoot(SidebarContent, ''#ico-sidebarContent'', injector);
}
}
El problema es que cuando intento escribir
ft
en el registro de la consola, no estoy
undefined
, y por supuesto obtengo una excepción cuando intento insertar algo dentro de la matriz de "mosaicos":
''no hay mosaicos de propiedades para "indefinidos"''
.
Una cosa más: el componente FilterTiles parece estar cargado correctamente, ya que puedo ver la plantilla html para él.
¿Cualquier sugerencia? Gracias
Aquí hay algo que funcionó para mí.
@ViewChild(''mapSearch'', { read: ElementRef }) mapInput: ElementRef;
ngAfterViewInit() {
interval(1000).pipe(
switchMap(() => of(this.mapInput)),
filter(response => response instanceof ElementRef),
take(1))
.subscribe((input: ElementRef) => {
//do stuff
});
}
Así que básicamente configuro una verificación cada segundo hasta que
*ngIf
vuelve verdadero y luego hago mis cosas relacionadas con
ElementRef
.
Debe funcionar
Pero como dijo Günter Zöchbauer , debe haber algún otro problema en la plantilla. He creado un poco Relevant-Plunkr-Answer . Por favor, compruebe la consola del navegador.
boot.ts
@Component({
selector: ''my-app''
, template: `<div> <h1> BodyContent </h1></div>
<filter></filter>
<button (click)="onClickSidebar()">Click Me</button>
`
, directives: [FilterTiles]
})
export class BodyContent {
@ViewChild(FilterTiles) ft:FilterTiles;
public onClickSidebar() {
console.log(this.ft);
this.ft.tiles.push("entered");
}
}
filterTiles.ts
@Component({
selector: ''filter'',
template: ''<div> <h4>Filter tiles </h4></div>''
})
export class FilterTiles {
public tiles = [];
public constructor(){};
}
Funciona a las mil maravillas. Por favor revise sus etiquetas y referencias.
Gracias...
El problema como se mencionó anteriormente es el
ngIf
que está causando que la vista sea indefinida.
La respuesta es usar
ViewChildren
lugar de
ViewChild
.
Tuve un problema similar en el que no quería que se mostrara una cuadrícula hasta que se hayan cargado todos los datos de referencia.
html:
<section class="well" *ngIf="LookupData != null">
<h4 class="ra-well-title">Results</h4>
<kendo-grid #searchGrid> </kendo-grid>
</section>
Código de componente
import { Component, ViewChildren, OnInit, AfterViewInit, QueryList } from ''@angular/core'';
import { GridComponent } from ''@progress/kendo-angular-grid'';
export class SearchComponent implements OnInit, AfterViewInit
{
//other code emitted for clarity
@ViewChildren("searchGrid")
public Grids: QueryList<GridComponent>
private SearchGrid: GridComponent
public ngAfterViewInit(): void
{
this.Grids.changes.subscribe((comps: QueryList <GridComponent>) =>
{
this.SearchGrid = comps.first;
});
}
}
Aquí estamos utilizando
ViewChildren
en el que puede escuchar los cambios.
En este caso, cualquier elemento
#searchGrid
con la referencia
#searchGrid
.
Espero que esto ayude.
En mi caso, tenía un setter de variables de entrada usando
ViewChild
, y
ViewChild
estaba dentro de una directiva
*ngIf
, por lo que el setter intentaba acceder a él antes de que se
*ngIf
(funcionaría bien sin
*ngIf
, pero no funciona si siempre se estableció en verdadero con
*ngIf="true"
.
Para resolverlo, utilicé Rxjs para asegurarme de que cualquier referencia a
ViewChild
esperara hasta que se iniciara la vista.
Primero, cree un Asunto que se complete después de ver init.
export class MyComponent implements AfterViewInit {
private _viewInitWaiter$ = new Subject();
ngAfterViewInit(): void {
this._viewInitWaiter$.complete();
}
}
Luego, cree una función que ejecute una lambda después de que el sujeto se complete.
private _executeAfterViewInit(func: () => any): any {
this._viewInitWaiter$.subscribe(null, null, () => {
return func();
})
}
Finalmente, asegúrese de que las referencias a ViewChild usen esta función.
@Input()
set myInput(val: any) {
this.executeAfterViewInit(() => {
const viewChildProperty = this.viewChild.someProperty;
...
});
}
@ViewChild(''viewChildRefName'', {read: MyViewChildComponent}) viewChild: MyViewChildComponent;
Esto funcionó para mí.
Mi componente llamado ''my-component'', por ejemplo, se mostró usando * ngIf = "showMe" así:
<my-component [showMe]="showMe" *ngIf="showMe"></my-component>
Entonces, cuando el componente se inicializa, el componente aún no se muestra hasta que "showMe" sea verdadero. Por lo tanto, mis referencias @ViewChild no estaban definidas.
Aquí es donde utilicé @ViewChildren y la QueryList que devuelve. Vea el artículo angular en QueryList y una demostración de uso de @ViewChildren .
Puede usar la Lista de consultas que @ViewChildren devuelve y suscribirse a cualquier cambio en los elementos referenciados utilizando rxjs como se ve a continuación. @ViewChild no tiene esta capacidad.
import { Component, ViewChildren, ElementRef, OnChanges, QueryList, Input } from ''@angular/core'';
import ''rxjs/Rx'';
@Component({
selector: ''my-component'',
templateUrl: ''./my-component.component.html'',
styleUrls: [''./my-component.component.css'']
})
export class MyComponent implements OnChanges {
@ViewChildren(''ref'') ref: QueryList<any>; // this reference is just pointing to a template reference variable in the component html file (i.e. <div #ref></div> )
@Input() showMe; // this is passed into my component from the parent as a
ngOnChanges () { // ngOnChanges is a component LifeCycle Hook that should run the following code when there is a change to the components view (like when the child elements appear in the DOM for example)
if(showMe) // this if statement checks to see if the component has appeared becuase ngOnChanges may fire for other reasons
this.ref.changes.subscribe( // subscribe to any changes to the ref which should change from undefined to an actual value once showMe is switched to true (which triggers *ngIf to show the component)
(result) => {
// console.log(result.first[''_results''][0].nativeElement);
console.log(result.first.nativeElement);
// Do Stuff with referenced element here...
}
); // end subscribe
} // end if
} // end onChanges
} // end Class
Espero que esto ayude a alguien a ahorrar algo de tiempo y frustración.
Esto funciona para mí, vea el ejemplo a continuación.
import {Component, ViewChild, ElementRef} from ''angular2/core'';
@Component({
selector: ''app'',
template: `
<a (click)="toggle($event)">Toggle</a>
<div *ngIf="visible">
<input #control name="value" [(ngModel)]="value" type="text" />
</div>
`,
})
export class AppComponent {
private elementRef: ElementRef;
@ViewChild(''control'') set controlElRef(elementRef: ElementRef) {
this.elementRef = elementRef;
}
visible:boolean;
toggle($event: Event) {
this.visible = !this.visible;
if(this.visible) {
setTimeout(() => { this.elementRef.nativeElement.focus(); });
}
}
}
La solución que funcionó para mí fue agregar la directiva en las declaraciones en app.module.ts
Lo arreglo simplemente agregando SetTimeout después de configurar el componente visible
Mi HTML:
<input #txtBus *ngIf[show]>
My Component JS
@Component({
selector: "app-topbar",
templateUrl: "./topbar.component.html",
styleUrls: ["./topbar.component.scss"]
})
export class TopbarComponent implements OnInit {
public show:boolean=false;
@ViewChild("txtBus") private inputBusRef: ElementRef;
constructor() {
}
ngOnInit() {}
ngOnDestroy(): void {
}
showInput() {
this.show = true;
setTimeout(()=>{
this.inputBusRef.nativeElement.focus();
},500);
}
}
Mi solución a esto fue mover el ngIf desde el exterior del componente secundario al interior del componente secundario en un div que envolvió toda la sección de html. De esa manera, todavía se ocultaba cuando era necesario, pero podía cargar el componente y podía hacer referencia a él en el padre.
Mi solución a esto fue reemplazar
*ngIf
con
[hidden]
.
Lo malo era que todos los componentes secundarios estaban presentes en el código DOM.
Pero funcionó para mis requerimientos.
Mi solución era usar
[style.display]="getControlsOnStyleDisplay()"
lugar de
*ngIf="controlsOn"
.
El bloque está ahí pero no se muestra.
@Component({
selector: ''app'',
template: `
<controls [style.display]="getControlsOnStyleDisplay()"></controls>
...
export class AppComponent {
@ViewChild(ControlsComponent) controls:ControlsComponent;
controlsOn:boolean = false;
getControlsOnStyleDisplay() {
if(this.controlsOn) {
return "block";
} else {
return "none";
}
}
....
Para mí, el problema era que hacía referencia a la ID del elemento.
@ViewChild(''survey-form'') slides:IonSlides;
<div id="survey-form"></div>
En lugar de esto:
@ViewChild(''surveyForm'') slides:IonSlides;
<div #surveyForm></div>
Podrías usar un setter para
@ViewChild()
@ViewChild(FilterTiles) set ft(tiles: FilterTiles) {
console.log(tiles);
};
Si tiene un contenedor ngIf, se llamará al setter con undefined, y luego nuevamente con una referencia una vez que ngIf le permita renderizar.
Sin embargo, mi problema era otra cosa. No había incluido el módulo que contenía mis "FilterTiles" en mi app.modules. La plantilla no arrojó un error, pero la referencia siempre fue indefinida.
Tuve un problema similar y pensé en publicar en caso de que alguien más cometiera el mismo error.
Primero, una cosa a considerar es
AfterViewInit
;
debe esperar a que se inicialice la vista antes de poder acceder a su
@ViewChild
.
Sin embargo, mi
@ViewChild
seguía volviendo nulo.
El problema era mi
*ngIf
.
La directiva
*ngIf
estaba matando mi componente de controles, por lo que no pude hacer referencia a él.
import {Component, ViewChild, OnInit, AfterViewInit} from ''angular2/core'';
import {ControlsComponent} from ''./controls/controls.component'';
import {SlideshowComponent} from ''./slideshow/slideshow.component'';
@Component({
selector: ''app'',
template: `
<controls *ngIf="controlsOn"></controls>
<slideshow (mousemove)="onMouseMove()"></slideshow>
`,
directives: [SlideshowComponent, ControlsComponent]
})
export class AppComponent {
@ViewChild(ControlsComponent) controls:ControlsComponent;
controlsOn:boolean = false;
ngOnInit() {
console.log(''on init'', this.controls);
// this returns undefined
}
ngAfterViewInit() {
console.log(''on after view init'', this.controls);
// this returns null
}
onMouseMove(event) {
this.controls.show();
// throws an error because controls is null
}
}
Espero que ayude.
EDITAR
Como se menciona en
@Ashg a continuación
, una solución es usar
@ViewChildren
lugar de
@ViewChild
.
Tuve un problema similar, donde
ViewChild
estaba dentro de una cláusula de
switch
que no estaba cargando el elemento viewChild antes de que fuera referenciado.
Lo resolví de una manera semi-hacky pero envolviendo la referencia
ViewChild
en un
setTimeout
que se ejecutó de inmediato (es decir, 0 ms)
Un tipo de enfoque genérico:
Puede crear un método que esperará hasta que
ViewChild
esté listo
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;
}
Uso:
// Now you can do it in any place of your code
waitWhileViewChildIsReady(this, ''yourViewChildName'').subscribe(() =>{
// your logic here
})
En mi caso, sabía que el componente hijo siempre estaría presente, pero quería alterar el estado antes de que el niño se inicializara para ahorrar trabajo.
Elijo probar el niño hasta que apareció y hacer cambios de inmediato, lo que me ahorró un ciclo de cambio en el componente hijo.
export class GroupResultsReportComponent implements OnInit {
@ViewChild(ChildComponent) childComp: ChildComponent;
ngOnInit(): void {
this.WhenReady(() => this.childComp, () => { this.childComp.showBar = true; });
}
/**
* Executes the work, once the test returns truthy
* @param test a function that will return truthy once the work function is able to execute
* @param work a function that will execute after the test function returns truthy
*/
private WhenReady(test: Function, work: Function) {
if (test()) work();
else setTimeout(this.WhenReady.bind(window, test, work));
}
}
Alertnativamente, puede agregar un número máximo de intentos o agregar algunos ms de retraso al
setTimeout
.
setTimeout
efectivamente lanza la función al final de la lista de operaciones pendientes.