Adnotacja Angular 2 @ViewChild zwraca wartość niezdefiniowaną

242

Próbuję nauczyć się Angular 2.

Chciałbym uzyskać dostęp do komponentu podrzędnego z komponentu nadrzędnego za pomocą adnotacji @ViewChild .

Oto kilka wierszy kodu:

W BodyContent.ts mam:

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);
    } 
}

podczas gdy w 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(){};
 }

Wreszcie tutaj szablony (zgodnie z sugestiami w komentarzach):

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>

Szablon FilterTiles.html jest poprawnie załadowany do tagu ico-filter-tiles (rzeczywiście jestem w stanie zobaczyć nagłówek).

Uwaga: klasa BodyContent jest wstrzykiwana do innego szablonu (Body) przy użyciu DynamicComponetLoader: dcl.loadAsRoot (BodyContent, '# ico-bodyContent', inżektor):

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);

   } 
}

Problem polega na tym, że kiedy próbuję pisać ftdo dziennika konsoli, dostaję undefinedi oczywiście dostaję wyjątek, gdy próbuję wepchnąć coś do tablicy „kafelków”: „brak kafelków właściwości dla„ niezdefiniowany ”” .

Jeszcze jedno: składnik FilterTiles wydaje się być poprawnie załadowany, ponieważ widzę dla niego szablon HTML.

Jakieś sugestie? Dzięki

Andrea Ialenti
źródło
Wygląda poprawnie. Może coś z szablonem, ale nie ma go w pytaniu.
Günter Zöchbauer
1
Uzgodniony z Günter. Stworzyłem plunkr z twoim kodem i prostymi powiązanymi szablonami i działa. Zobacz ten link: plnkr.co/edit/KpHp5Dlmppzo1LXcutPV?p=preview . Potrzebujemy szablonów ;-)
Thierry Templier
1
ftnie byłby ustawiony w konstruktorze, ale w module obsługi zdarzeń kliknięcia byłby już ustawiony.
Günter Zöchbauer
5
Używasz loadAsRoot, który ma znany problem z wykrywaniem zmian. Tylko dla pewności spróbuj użyć loadNextToLocationlub loadIntoLocation.
Eric Martinez
1
Problem polegał na tym loadAsRoot. Po zastąpieniu loadIntoLocationproblem został rozwiązany. Jeśli podasz komentarz jako odpowiedź,
oznaczę

Odpowiedzi:

374

Miałem podobny problem i pomyślałem, że opublikuję na wypadek, gdyby ktoś popełnił ten sam błąd. Po pierwsze, jedną rzeczą do rozważenia jest AfterViewInit; musisz poczekać na zainicjowanie widoku, zanim będziesz mieć dostęp do swojego @ViewChild. Jednak mój @ViewChildwciąż zwracał wartość zero. Problem był mój *ngIf. *ngIfDyrektywa zabija moje komponencie kontroli, więc nie mogłem go odwołać.

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

Mam nadzieję, że to pomaga.

EDYCJA
Jak wspomniano poniżej przez @Ashg , rozwiązaniem jest użycie @ViewChildrenzamiast @ViewChild.

Kenecaswell
źródło
9
@kenecaswell Czy znalazłeś lepszy sposób na rozwiązanie problemu? Mam również ten sam problem. Mam wiele * ngIf, więc ten element będzie w końcu tylko prawdziwy, ale potrzebuję odwołania do elementu. Jakikolwiek sposób, aby to rozwiązać>
monica
4
Odkryłem, że komponent potomny jest „niezdefiniowany” w ngAfterViewInit (), jeśli używasz ngIf. Próbowałem stawiać długie limity czasu, ale nadal nie miałem efektu. Jednak komponent potomny jest dostępny później (tj. W odpowiedzi na zdarzenia kliknięcia itp.). Jeśli nie używam ngIf i jest to zdefiniowane zgodnie z oczekiwaniami w ngAfterViewInit (). Więcej informacji na temat komunikacji rodzic / dziecko można znaleźć
Matthew Hegarty
3
Zamiast tego użyłem bootstrap ngClass+ hiddenklasy ngIf. To się udało. Dzięki!
Rahmathullah M
9
To nie rozwiązuje problemu, skorzystaj z poniższego rozwiązania za pomocą @ViewChildren idź po odniesienie do kontroli dzieci, gdy tylko będzie dostępna
Ashg
20
To tylko dowodzi „problemu”, prawda? Nie publikuje rozwiązania.
Miguel Ribeiro,
146

Jak już wspomniano, problemem jest ngIfniezdefiniowanie widoku. Odpowiedź brzmi: użyj ViewChildrenzamiast ViewChild. Miałem podobny problem, w którym nie chciałem pokazywać siatki, dopóki wszystkie dane referencyjne nie zostaną załadowane.

HTML:

   <section class="well" *ngIf="LookupData != null">
       <h4 class="ra-well-title">Results</h4>
       <kendo-grid #searchGrid> </kendo-grid>
   </section>

Kod części

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;
        });


    }
}

Tutaj używamy, ViewChildrenna których można nasłuchiwać zmian. W tym przypadku wszystkie dzieci z referencją #searchGrid. Mam nadzieję że to pomoże.

Ashg
źródło
4
Chciałbym dodać to w niektórych przypadkach, gdy próbujesz zmienić np. this.SearchGridwłaściwości, których należy używać, takich jak setTimeout(()=>{ ///your code here }, 1); unikanie Wyjątek: Wyrażenie zmieniło się po sprawdzeniu
rafalkasa
3
Jak to zrobić, jeśli chcesz umieścić tag #searchGrid na normalnym elemencie HTML zamiast elementu Angular2? (Na przykład <div #searchGrid> </div> i to jest w bloku * ngIf?
Vern Jensen
1
to jest poprawna odpowiedź dla mojego przypadku użycia! Dzięki, muszę uzyskać dostęp do komponentu, który jest dostępny poprzez ngIf =
Frozen_byte
1
działa to idealnie w odpowiedziach ajax, teraz *ngIfdziała, a po renderowaniu możemy zapisać ElementRef z komponentów dynamicznych.
elporfirio
4
Nie zapomnij także przypisać go do subskrypcji, a następnie wypisać się z subskrypcji
tam.teixeira,
64

Możesz użyć setera dla @ViewChild()

@ViewChild(FilterTiles) set ft(tiles: FilterTiles) {
    console.log(tiles);
};

Jeśli masz otoki ngIf, setter zostanie wywołany z niezdefiniowanym, a następnie ponownie z referencją, gdy ngIf pozwoli na renderowanie.

Mój problem był jednak inny. W moim app.modules nie zawarłem modułu zawierającego moje „FilterTiles”. Szablon nie zgłosił błędu, ale odwołanie zawsze było niezdefiniowane.

parlament
źródło
3
To nie działa dla mnie - otrzymuję pierwsze niezdefiniowane, ale nie otrzymuję drugiego połączenia z referencją. Aplikacja jest ng2 ... czy ta funkcja ng4 +?
Jay Cummins,
@Jay Myślę, że dzieje się tak, ponieważ w tym przypadku nie zarejestrowałeś komponentu w Angular FilterTiles. Z tego powodu napotkałem już wcześniej ten problem.
parlament
1
Działa dla Angulara 8 przy użyciu #paginator na elemencie HTML i adnotacji typu@ViewChild('paginator', {static: false})
Qiteq,
1
Czy jest to wywołanie zwrotne dla zmian ViewChild?
Yasser Nascimento
24

To zadziałało dla mnie.

Na przykład mój komponent o nazwie „mój-komponent” został wyświetlony za pomocą * ngIf = „showMe” w następujący sposób:

<my-component [showMe]="showMe" *ngIf="showMe"></my-component>

Tak więc po zainicjowaniu komponentu komponent nie jest jeszcze wyświetlany, dopóki „showMe” nie będzie prawdziwe. Tak więc wszystkie moje referencje @ViewChild były niezdefiniowane.

To tutaj użyłem @ViewChildren i zwróconej listy QueryList. Zobacz ciekawy artykuł na temat QueryList i demo użytkowania @ViewChildren .

Możesz użyć QueryList, którą zwraca @ViewChildren i subskrybować wszelkie zmiany w odnośnych elementach za pomocą rxjs, jak pokazano poniżej. @ViewChild nie ma tej zdolności.

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

Mam nadzieję, że to pomoże komuś zaoszczędzić trochę czasu i frustracji.

Joshua Dyck
źródło
3
Rzeczywiście, wydaje się, że twoje rozwiązanie jest najlepszym z wymienionych do tej pory. UWAGA Musimy pamiętać, że najlepsze 73 rozwiązanie jest teraz DEPRECATED ... od czasu dyrektywy: [...] deklaracja NIE jest już obsługiwana w Angular 4. RÓWNIEŻ NIE
DZIAŁA w
5
Nie zapomnij zrezygnować z subskrypcji lub użyć .take(1).subscribe(), ale doskonała odpowiedź, dziękuję bardzo!
Blair Connolly,
2
Doskonałe rozwiązanie Zapisałem się na zmiany referencji w ngAfterViewInit () zamiast ngOnChanges (). Ale musiałem dodać setTimeout, aby pozbyć się błędu ExpressionChangedAfterChecked
Josf
Należy to zaznaczyć jako faktyczne rozwiązanie. Wielkie dzięki!
platzhersh
10

Moim obejściem było użycie [style.display]="getControlsOnStyleDisplay()"zamiast *ngIf="controlsOn". Blok jest tam, ale nie jest wyświetlany.

@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";
    }
  }
....
Calderas
źródło
Mieć stronę, na której w tabeli wyświetlana jest lista elementów lub wyświetlany jest element edycji na podstawie wartości zmiennej showList. Pozbyłem się irytującego błędu konsoli, używając [style.display] = "! ShowList", w połączeniu z * ngIf = "! ShowList".
razvanone
9

Tym, co rozwiązało mój problem, było upewnienie się, że staticzostało ustawione false.

@ViewChild(ClrForm, {static: false}) clrForm;

Po staticwyłączeniu @ViewChildodniesienie jest aktualizowane przez Angular po *ngIfzmianie dyrektywy.

Paul LeBeau
źródło
1
Jest to niemal idealna odpowiedź, wystarczy wskazać, że warto sprawdzić wartości zerowalne, więc otrzymujemy coś takiego: @ViewChild (ClrForm, {static: false}) ustaw clrForm (clrForm: ClrForm) {if (clrForm) {this.clrForm = clrForm; }};
Klaus Klein
Próbowałem tak wielu rzeczy i wreszcie znalazłem winowajcę.
Manish Sharma
8

Moje rozwiązanie to było zastąpienie *ngIf z [hidden]. Minusem było to, że wszystkie elementy potomne były obecne w kodzie DOM. Ale pracował dla moich wymagań.

Suzanne Suzanne
źródło
5

W moim przypadku miałem ustawiający zmienną wejściową za pomocą ViewChild, i ViewChildznajdował się w *ngIfdyrektywie, więc ustawiający próbował uzyskać do niego dostęp przed *ngIfrenderowanym (działałby dobrze bez *ngIf, ale nie działałby, gdyby zawsze był ustawiony na prawda z *ngIf="true").

Aby rozwiązać, użyłem Rxjs, aby upewnić się, że wszelkie odniesienia do ViewChildoczekiwania czekały na zainicjowanie widoku. Najpierw utwórz Temat, który zostanie ukończony, gdy zostanie wyświetlony init.

export class MyComponent implements AfterViewInit {
  private _viewInitWaiter$ = new Subject();

  ngAfterViewInit(): void {
    this._viewInitWaiter$.complete();
  }
}

Następnie utwórz funkcję, która pobiera i wykonuje lambda po zakończeniu tematu.

private _executeAfterViewInit(func: () => any): any {
  this._viewInitWaiter$.subscribe(null, null, () => {
    return func();
  })
}

Na koniec upewnij się, że odwołania do ViewChild korzystają z tej funkcji.

@Input()
set myInput(val: any) {
    this._executeAfterViewInit(() => {
        const viewChildProperty = this.viewChild.someProperty;
        ...
    });
}

@ViewChild('viewChildRefName', {read: MyViewChildComponent}) viewChild: MyViewChildComponent;
nikojpapa
źródło
1
Jest to znacznie lepsze rozwiązanie niż wszystkie bezsensowne ustalenia
Liam
4

To musi działać.

Ale jak powiedział Günter Zöchbauer , w szablonie musi być jakiś inny problem. Stworzyłem coś w rodzaju Relevant-Plunkr-Answer . Proszę sprawdzić konsolę przeglądarki.

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(){};
 }

To działa jak urok. Sprawdź dokładnie swoje tagi i referencje.

Dzięki...

mikroniki
źródło
1
Jeśli problem jest taki sam jak mój, aby go zduplikować, musisz umieścić * ngIf w szablonie wokół <filter> </filter> .. Najwyraźniej jeśli ngIf zwróci false, ViewChild nie zostanie podłączony i zwróci wartość null
Dan Ścigaj
2

To działa dla mnie, patrz przykład poniżej.

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(); });
      }
    }

}

NiZa
źródło
2

Miałem podobny problem, w którym ViewChildznajdowała się w switchklauzuli, która nie ładowała elementu viewChild przed odwołaniem się do niego. Rozwiązałem go w pół-hacky sposób, ale zawijając ViewChildodwołanie w setTimeoutwykonywanym natychmiast (tj. 0ms)

Nikola Jankovic
źródło
1

Moim rozwiązaniem tego było przeniesienie ngIf z zewnątrz komponentu potomnego do wnętrza komponentu potomnego na div, który owinął całą sekcję HTML. W ten sposób wciąż się ukrywał, kiedy trzeba, ale był w stanie załadować komponent i mogłem odwoływać się do niego w rodzicu.

harmonickey
źródło
Ale w tym celu, w jaki sposób doszedłeś do swojej „widocznej” zmiennej, która była w rodzicu?
Dan Chase,
1

Naprawiam to po prostu dodając SetTimeout po ustawieniu widocznego komponentu

Mój 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);
  }
}
Osvaldo Leiva
źródło
1

W moim przypadku wiedziałem, że element potomny będzie zawsze obecny, ale chciałem zmienić stan przed zainicjowaniem go przez dziecko w celu zaoszczędzenia pracy.

Wybieram testowanie dziecka, aż się pojawi, i natychmiast wprowadzam zmiany, co zapisało mi cykl zmian w elemencie potomnym.

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));
    }
}

Ostrzegawczo, możesz dodać maksymalną liczbę prób lub dodać kilka ms opóźnienia do setTimeout. setTimeoutskutecznie rzuca funkcję na dół listy oczekujących operacji.

N-zjadł
źródło
0

Rodzaj ogólnego podejścia:

Możesz utworzyć metodę, która będzie czekać, aż ViewChildbędzie gotowa

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;
}

Stosowanie:

  // Now you can do it in any place of your code
  waitWhileViewChildIsReady(this, 'yourViewChildName').subscribe(() =>{
      // your logic here
  })
Siergiej Panfilow
źródło
0

Dla mnie problem polegał na tym, że odwoływałem się do identyfikatora elementu.

@ViewChild('survey-form') slides:IonSlides;

<div id="survey-form"></div>

Zamiast tego:

@ViewChild('surveyForm') slides:IonSlides;

<div #surveyForm></div>
Jeremy Quick
źródło
0

Jeśli używasz Ionic, musisz użyć ionViewDidEnter()haka cyklu życia. Ionic biegnie kilka dodatkowych rzeczy (głównie związane animacji), które zazwyczaj powoduje nieoczekiwane błędy jak ten, stąd potrzeba czegoś, co biegnie po ngOnInit , ngAfterContentIniti tak dalej.

Jai
źródło
-1

Oto coś, co zadziałało dla mnie.

@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
        });
}

Zasadniczo więc sprawdzam co sekundę, aż stanie *ngIfsię to prawdą, a następnie robię swoje rzeczy związane z ElementRef.

Anjil Dhamala
źródło
-3

Rozwiązaniem, które działało dla mnie, było dodanie dyrektywy do deklaracji w app.module.ts

noro5
źródło