@ ngIf中的@ViewChild

时间:2016-09-07 10:08:52

标签: angular angular2-changedetection

问题

显示模板中相应元素后获取@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有一个选项,但它会导致太多无用的通话。

ANSWER (Plunker)

18 个答案:

答案 0 :(得分:224)

使用QueryList接受的答案对我不起作用。然而,有什么工作是使用ViewChild的setter:

 private contentPlaceholder: ElementRef;

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

一旦* ngIf变为真,就会调用setter。

答案 1 :(得分:84)

另一种解决方法是手动运行更换检测器。

首先注入ChangeDetectorRef

constructor(private changeDetector : ChangeDetectorRef) {}

然后在更新控制* ngIf

的变量后调用它
show() {
        this.display = true;
        this.changeDetector.detectChanges();
    }

答案 2 :(得分:21)

角度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

答案 3 :(得分:18)

上面的答案对我不起作用,因为在我的项目中,ngIf在输入元素上。我需要访问nativeElement属性,以便在ngIf为true时关注输入。 ViewContainerRef上似乎没有nativeElement属性。这是我做的(跟@ViewChild documentation之后):

<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 :(得分:9)

正如其他人所提到的,最快和最快的解决方案是使用[hidden]而不是* ngIf,这样组件将被创建但不可见,因为你可以访问它,尽管它可能不是最有效的方式。

答案 5 :(得分:6)

这可行,但我不知道你的情况是否方便:

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

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

答案 6 :(得分:3)

我的目标是避免任何假设某事的hacky方法(例如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;
    });
  }

我的方案:我想根据路由器@ViewChildqueryParams元素上触发操作。由于在HTTP请求返回数据之前包装*ngIf为假,因此@ViewChild元素的初始化会延迟。

它是如何工作的: combineLatest仅在每个提供的Observable从订阅combineLatest的那一刻起发出第一个值时才首次发出值。我的主题tabSetInitialized在设置@ViewChild元素时会发出一个值。因此,我延迟了subscribe下的代码执行,直到*ngIf变为正,@ViewChild被初始化。

当然不要忘记取消订阅ngOnDestroy,我使用ngUnsubscribe主题:

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

答案 7 :(得分:2)

另一个快速的“技巧”(简单的解决方案)只是使用[hidden]标签而不是* ngIf,重要的是要知道在这种情况下Angular会构建对象并将其绘制在类下:隐藏的原因是ViewChild可以正常工作的原因。 因此请务必记住,不要在可能导致性能问题的笨重或昂贵物品上使用隐藏物品

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

答案 8 :(得分:1)

一个简化的版本,在使用Google Maps JS SDK时遇到类似的问题。

我的解决方案是将divViewChild提取到其自己的子组件中,当在父组件中使用该子组件时,可以使用*ngIf隐藏/显示它。

之前

HomePageComponent模板

<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模板

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

HomePageComponent组件

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

答案 9 :(得分:1)

如果我在Angular 9中使用ChangeDetectorRef,这对我有用

<form method="POST" data-url="{% url 'home:post-create' %}" class="post-create-form" enctype="multipart/form-data">
    {% csrf_token %}
        <div class="modal-header text-center">
            <h5 class="modal-title col-12 text-center">Create a Post
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                <span aria-hidden="true">&times;</span>
            </button>
        </h5>
        </div>
    <div class="modal-body" style="height:400px;overflow-y: auto; margin:0;">
        {{ form|crispy }}
        <div class="pt-3" style="margin-bottom: -100px !important;">
            <input type="file" class="filepond" accept="image/*">
        </div>


    </div>
    <div class="modal-footer col-12" style="margin-top: -20px;">
        <button type="submit" class="btn btn-primary"  style="border-radius: 20px; width: 100%;">Post</button>
    </div>
</form>
<script>
    $.fn.filepond.registerPlugin(
        FilePondPluginImagePreview,
        FilePondPluginImageCrop,
        FilePondPluginImageEdit,
        FilePondPluginImageEdit,
        FilePondPluginFileValidateType,
        );
    $(function () {
        var csrf_token="{{ csrf_token }}";
        $('.filepond').filepond({
            allowMultiple: true,
            className: "d-flex",
            maxFiles: 4,
            allowFileTypeValidation: true,
            acceptedFileType:['image/png', 'image/jpeg'],
            dropOnPage: true,
            server: {
            process: {
                headers: {"X-CSRFToken":csrf_token,},
                url: '/home/post/create/',
                method: 'POST',
            },
            revert: {
                headers: {
                "X-CSRFToken":csrf_token,
                },
                url: '/home/post/create/',
                method: 'DELETE',
            },
            fetch: null,
            load: null,
            },
            labelIdle: '<span class="filepond--label-action">Add Photos</span>',
            imagePreviewHeight: 250,

        });
    })
</script>

答案 10 :(得分:0)

如果 setter 似乎无法与 @ViewChild 一起工作(根本没有被调用),请尝试使用 @ContentChild

答案 11 :(得分:0)

我自己也遇到了同样的问题,使用 Angular 10。

如果我尝试使用 [hidden]*ngIf,那么 @ViewChild 变量总是未定义

<p-calendar #calendar *ngIf="bShowCalendar" >
</p-calendar>

我通过没有从网页中删除它来修复它。
我使用 [ngClass] 使控件具有 opacity:0,并将其完全移开。

<style>
  .notVisible {
    opacity: 0;
    left: -1000px;
    position: absolute !important;
  }
</style>

<p-calendar #calendar [ngClass]="{'notVisible': bShowCalendar }" >
</p-calendar>

是的,我知道,它既愚蠢又丑陋,但它解决了问题。

我还必须使控件静态。我不明白为什么......但是,如果没有这个更改,它再次拒绝工作:

export class DatePickerCellRenderer {
    @ViewChild('calendar', {static: true }) calendar: Calendar;

答案 12 :(得分:0)

Angular 8 -空检查和#include <gtkmm.h> int main( int argc, char **argv) { Glib::RefPtr< Gtk::Application > app = Gtk::Application::create( "App1" ); Gtk::Window window; Gtk::MenuButton menuButton; menuButton.set_label("menu button"); Gtk::Menu menu; Gtk::Label label1("label1"); Gtk::Label label2("label2"); Gtk::MenuItem item1(label1); Gtk::MenuItem item2(label2); menu.append(item1); menu.append(item2); menu.show_all(); menuButton.set_popup(menu); window.add(menuButton); window.show_all(); return app->run(window); } @ViewChild黑客的混合物

用于等待异步数据的分页控件

static: false

答案 13 :(得分:0)

使用Angular 8无需导入ChangeDector

ngIf允许您不加载元素,并避免给应用程序增加压力。这是我如何在没有ChangeDetector的情况下运行它的方法

elem: ElementRef;

@ViewChild('elemOnHTML', {static: false}) set elemOnHTML(elemOnHTML: ElementRef) {
    if (!!elemOnHTML) {
      this.elem = elemOnHTML;
    }
}

然后,当我将ngIf值更改为true时,我将使用setTimeout来使其仅等待下一个更改周期:

  this.showElem = true;
  console.log(this.elem); // undefined here
  setTimeout(() => {
    console.log(this.elem); // back here through ViewChild set
    this.elem.do();
  });

这也使我避免使用任何其他库或导入。

答案 14 :(得分:0)

我认为使用defer from lodash非常有意义,尤其是在我的@ViewChild()位于async管道内的情况下

答案 15 :(得分:0)

就我而言,我仅在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>

答案 16 :(得分:-1)

只需确保将 static 选项设置为false

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

答案 17 :(得分:-3)

这对我有用。在绑定数据后形成一些延迟。

List<SelectListItem> Contracts = (from a in 
db.v_xxx_yyyy_aaaa_contracts.Where(m => m.GroupID.ToString() == 
@group).Distinct().OrderBy(a => a.ContractID).ToList()
                                              select new SelectListItem()
                                              {
                                                  Value = a.ContractID,
                                                  Text = a.ContractID
                                              }).ToList();

ViewBag.ContractID = Contracts;