Angular 2 @ViewChild анотація повертається невизначеною


242

Я намагаюся вивчити Кутовий 2.

Я хотів би отримати доступ до дочірнього компонента з батьківського компонента за допомогою анотації @ViewChild .

Ось кілька рядків коду:

У BodyContent.ts у мене є:

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

перебуваючи на 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(){};
 }

Нарешті тут шаблони (як запропоновано в коментарях):

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>

Шаблон FilterTiles.html правильно завантажений у тег ico-filter- tile (я дійсно вмію бачити заголовок).

Примітка: клас BodyContent вводиться всередину іншого шаблону (Body) за допомогою DynamicComponetLoader: dcl.loadAsRoot (BodyContent, '# ico-bodyContent', інжектор):

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

   } 
}

Проблема полягає в тому, що коли я намагаюся записати ftв журнал консолі, я отримую undefined, і, звичайно, отримую виняток, коли намагаюся натиснути щось всередині масиву "плитки": "немає плиток властивостей для" невизначеного "' .

Ще одне: компонент FilterTiles здається правильно завантаженим, оскільки я можу побачити HTML-шаблон для нього.

Будь-яка пропозиція? Дякую


Виглядає правильно. Можливо, щось із шаблоном, але він не включений у ваше запитання.
Günter Zöchbauer

1
Домовились з Гюнтером. Я створив plunkr з вашим кодом та простими асоційованими шаблонами, і він працює. Дивіться це посилання: plnkr.co/edit/KpHp5Dlmppzo1LXcutPV?p=preview . Нам потрібні шаблони ;-)
Thierry Templier

1
ftне буде встановлено в конструкторі, але в обробці подій клацання це буде вже встановлено.
Günter Zöchbauer

5
Ви використовуєте loadAsRoot, яка має відому проблему з виявленням змін. Просто, щоб переконатися, що спробуйте loadNextToLocationабо loadIntoLocation.
Ерік Мартінес

1
Проблема була loadAsRoot. Як тільки я замінив loadIntoLocationпроблему було вирішено. Якщо ви зробите свій коментар як відповідь, я можу відзначити його прийнятим
Андреа Іаленті

Відповіді:


374

У мене була подібна проблема, і я думав, що опублікую, якщо хтось зробив таку ж помилку. По-перше, слід врахувати одне AfterViewInit; Вам потрібно дочекатися ініціалізації подання, перш ніж Ви зможете отримати доступ до свого @ViewChild. Однак мій @ViewChildвсе одно повертався недійсним. Проблема була моя *ngIf. *ngIfДиректива вбивали моє управління компонентами , так що я не міг посилатися на нього.

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

Сподіваюся, що це допомагає.

EDIT
Як згадував @Ashg нижче , рішення - використовувати @ViewChildrenзамість @ViewChild.


9
@kenecaswell Отже, ти знайшов кращий спосіб вирішити проблему. Я також стикаюся з тим же питанням. У мене є багато * ngIf, щоб цей елемент був єдиним зрештою істинним, але мені потрібна посилання на елемент. Будь-який спосіб вирішити це>
monica

4
Я виявив, що дочірній компонент у ngAfterViewInit () є "невизначеним", якщо використовується ngIf. Я намагався поставити довгі тайм-аути, але все одно ніякого ефекту. Однак дочірній компонент доступний пізніше (тобто у відповідь на події натискання тощо). Якщо я не використовую ngIf і він визначений, як очікувалося в ngAfterViewInit (). Більше про спілкування між
Меттью Хегарті

3
Я використовував bootstrap ngClass+ hiddenclass замість ngIf. Це спрацювало. Дякую!
Rahmathullah M

9
Це не вирішує проблему, використовуйте наведене нижче рішення, використовуючи @ViewChildren, перейдіть і отримайте посилання на контроль над дітьми, як тільки він стане доступним
Ешг,

20
Це просто доводить "питання", правда? Він не публікує рішення.
Мігель Рібейро

146

Як згадувалося раніше, проблема полягає в тому, ngIfщо спричиняє думку не визначеною. Відповідь - використовувати ViewChildrenзамість ViewChild. У мене була подібна проблема, коли я не хотів показувати сітку, поки не були завантажені всі довідкові дані.

html:

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

Код компонента

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


    }
}

Тут ми використовуємо, ViewChildrenна якому ви можете слухати зміни. У цьому випадку будь-які діти з посиланням #searchGrid. Сподіваюсь, це допомагає.


4
Я хотів би додати, що в деяких випадках, коли ви намагаєтеся змінити, наприклад. this.SearchGridвластивості, якими ви повинні використовувати синтаксис, як setTimeout(()=>{ ///your code here }, 1); уникнути винятку: вираз змінився після його перевірки
rafalkasa

3
Як це зробити, якщо ви хочете розмістити тег #searchGrid на звичайному HTML-елементі замість елемента Angular2? (Наприклад, <div #searchGrid> </div>, а це всередині блоку * ngIf?
Верн Дженсен,

1
це правильна відповідь для мого випадку використання! Дякую, мені потрібно отримати доступ до компонента, оскільки він доступний через ngIf =
Frozen_byte

1
це ідеально підходить для відповідей на ajax, тепер це *ngIfпрацює, і після візуалізації ми можемо зберегти ElementRef від компонентів динаміка.
elporfirio

4
Також не забудьте призначити його підписці, а потім скасуйте підписку на неї
tam.teixeira

64

Ви можете використовувати сетер для @ViewChild()

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

Якщо у вас є ngIf обгортка, сетер буде викликаний з невизначеним, а потім знову з посиланням, як тільки ngIf дозволить йому відтворити.

Але моє питання було чимось іншим. Я не включив модуль, що містить мої "FilterTiles", в мої програми app.modules. Шаблон не видав помилку, але посилання завжди було невизначено.


3
Це не працює для мене - я отримую перший невизначений, але другий дзвінок не отримую з посиланням. Додаток - це ng2 ... це функція ng4 +?
Джей Каммінс

@ Джей, я вважаю, це тому, що ви не зареєстрували компонент у Angular, в цьому випадку FilterTiles. З цією причиною я раніше стикався з цим питанням.
парламент

1
Працює для Angular 8, використовуючи #paginator на html-елементі та примітках, як@ViewChild('paginator', {static: false})
Qiteq

1
Це зворотний виклик для змін ViewChild?
Ясер Насіменто

24

Це працювало для мене.

Наприклад, мій компонент з назвою "мій компонент" відображався за допомогою * ngIf = "showMe" так:

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

Отже, коли компонент ініціалізований, компонент ще не відображається, поки "showMe" не відповідає дійсності. Таким чином, усі мої посилання на @ViewChild були не визначені.

Тут я використав @ViewChildren та QueryList, який він повертає. Дивіться углову статтю про QueryList та демонстрацію використання @ViewChildren .

Ви можете використовувати QueryList, що @ViewChildren повертається та підписується на будь-які зміни посилань, використовуючи rxjs, як показано нижче. @ViewChild не має цієї можливості.

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

Сподіваюся, це допоможе комусь заощадити час і розчарування.


3
Дійсно, рішення ви, здається, є найкращим підходом до цих пір. ПРИМІТКА Ми повинні мати на увазі , що топ 73 рішення тепер DEPRECATED ... так як директива: [...] Оголошення більше не підтримується в кутових 4. IOW він не буде працювати в Кутове 4 сценарії
PeteZaria

5
Не забудьте скасувати підписку або використати .take(1).subscribe(), але чудова відповідь, велике спасибі!
Блер Конноллі

2
Відмінне рішення. Я підписався на зміни змін у ngAfterViewInit (), а не на ngOnChanges (). Але мені довелося додати setTimeout, щоб позбутися помилки ExpressionChangedAfterChecked
Josf

Це слід позначити як фактичне рішення. Дуже дякую!
платжерш

10

Моє вирішення було використовувати [style.display]="getControlsOnStyleDisplay()"замість цього *ngIf="controlsOn". Блок є, але він не відображається.

@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";
    }
  }
....

Майте сторінку, на якій список елементів відображається в таблиці або відображається елемент редагування, виходячи зі значення змінної showList. Позбувся набридливої ​​помилки консолі, використовуючи [style.display] = "! ShowList" у поєднанні з * ngIf = "! ShowList".
razvanone

9

Що вирішило мою проблему, було переконатися, що це staticбуло встановлено false.

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

При staticвимкненому режимі @ViewChildпосилання оновлюється Angular при *ngIfзміні директиви.


1
Це майже ідеальний відповідь, просто наголосити - це гарний порядок також перевірити наявність змінних значень, тому ми закінчимо щось подібне: @ViewChild (ClrForm, {static: false}) встановити clrForm (clrForm: ClrForm) {якщо (clrForm) {this.clrForm = clrForm; }};
Клаус Кляйн

Я спробував так багато речей і, нарешті, виявив цю річ винуватцем.
Manish Sharma

8

Моє рішення для цього було замінити *ngIf на [hidden]. Даунсайд - всі дочірні компоненти були присутніми в коді DOM. Але працював на мої вимоги.


5

У моєму випадку у мене був сеттер змінної вхідного сигналу, що використовує директиву ViewChild, і знаходиться ViewChildвсередині *ngIfдирективи, тому сеттер намагався отримати доступ до неї перед *ngIfвинесеним (він би працював нормально без *ngIf, але не працював, якби він завжди був встановлений на правда з *ngIf="true").

Щоб вирішити, я використовував Rxjs, щоб переконатися, що будь-яке посилання на очікуване, ViewChildдоки перегляд не буде розпочато. Спочатку створіть тему, яка завершується після перегляду init.

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

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

Потім створіть функцію, яка приймає та виконує лямбда після завершення теми.

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

Нарешті, переконайтеся, що посилання на ViewChild використовують цю функцію.

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

@ViewChild('viewChildRefName', {read: MyViewChildComponent}) viewChild: MyViewChildComponent;

1
Це набагато краще рішення, ніж усі безглуздість у встановленому таймі
Ліам

4

Це має працювати.

Але, як сказав Гюнтер Зехбауер, у шаблоні повинна бути якась інша проблема. Я створив свого роду відповідне-Plunkr-відповідь . Просимо перевірити консоль браузера.

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

Це працює як шарм. Будь ласка, двічі перевірте свої теги та посилання.

Дякую...


1
Якщо проблема така ж, як і у мене, для дублювання вам потрібно буде поставити * ngIf в шаблон навколо <filter> </filter> .. Мабуть, якщо ngIf повернеться неправдиво, ViewChild не буде підключений та повернеться null
Dan Чейз

2

Це працює для мене, див. Приклад нижче.

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

}


2

У мене була аналогічна проблема, коли ViewChildвнутрішня частина switchпункту, що не завантажувала елемент viewChild до його посилання. Я вирішив це напівакітним способом, але загорнувши ViewChildпосилання у setTimeoutте, що виконується негайно (тобто 0 мс)


1

Моє рішення для цього полягала в тому, щоб перемістити ngIf з-під дочірнього компонента в внутрішню частину дочірнього компонента на діві, який обгорнув весь розділ html. Таким чином, він все ще приховувався, коли це було потрібно, але зміг завантажити компонент, і я міг посилатись на нього у батьків.


Але для цього, як ви потрапили до своєї "видимої" змінної, яка була у батьків?
Ден Чейз

1

Я виправляю це лише додаючи SetTimeout після встановлення видимого компонента

Мій HTML:

<input #txtBus *ngIf[show]>

Мій компонент 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);
  }
}

1

У моєму випадку я знав, що дочірній компонент завжди буде присутній, але хотів змінити стан до ініціалізації дитини, щоб зберегти роботу.

Я вибираю тестування на дитину до тих пір, поки вона не з’явилася, і внесу зміни негайно, що врятувало мені цикл змін на дочірньому компоненті.

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

Попереджувально, ви можете додати максимум кількості спроб або додати кілька затримок мс до setTimeout. setTimeoutефективно перекидає функцію внизу списку очікуваних операцій.


0

Різний загальний підхід:

Ви можете створити метод, який буде чекати, поки 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
  })

0

Для мене проблема полягала в тому, що я посилався на елемент на ID.

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

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

Замість цього:

@ViewChild('surveyForm') slides:IonSlides;

<div #surveyForm></div>

0

Якщо ви використовуєте Ionic, вам потрібно буде використовувати ionViewDidEnter()гачок життєвого циклу. Ionic запускає деякі додаткові матеріали (в основному пов'язані з анімацією), які, як правило, викликають несподівані помилки на кшталт цієї, отже, необхідність у тому, що працює після тощо ngOnInit , ngAfterContentInitтощо.


-1

Ось щось працювало на мене.

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

Тому я в основному встановлюю перевірку щосекунди, поки не *ngIfстане правдою, а потім я роблю свої речі, пов'язані з ElementRef.


-3

Для мене справою було додати директиву в декларації в app.module.ts

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.