角度2 - ngIf的可观察性使元素不可接受

时间:2017-02-07 19:16:10

标签: angular

我正在尝试在observable解析时运行一些DOM操作代码,因此在视图中解析了ngIf条件,但每次我尝试访问它时,它都会返回undefined。这是我的代码的快照:

查看:

<div *ngIf="data?.data_child" id="data-element">
    ... data here
</div>

控制器:

public data;
constructor(private _async: AsyncService){}

ngOnInit() {
    this._async.subscribe( result => {
        if( result ) {
            // data contains data_child
            this.data = result;

            // run DOM manipulation on the element
            if( this.data.data_child ) {

               // error is thrown here as the element is undefined
               $('#data-element').scrollTop($('#data-element')[0].scrollHeight);
            }
        }
    }
}

我想提一下,我已经尝试在ngAfterViewInit中创建另一个订阅并在那里调用但仍然发生同样的错误。

唯一有用的是在加载数据时发出一个事件,然后我订阅并触发DOM操作方法。但是,它仅在第一次触发ngOnInit时起作用,因为进一步路由到该组件不再触发事件(没有reuseComponent功能)。

我还尝试使用@ViewChild并设置一个模板变量,然后在控制器中使用它,但它仍然会抛出相同的错误。

有什么想法吗?谢谢!

3 个答案:

答案 0 :(得分:1)

使用ngAfterViewInit生命周期钩子来操作DOM元素。你是否使用jQuery来获取元素的高度?您可以使用原生Angular代码执行此操作:this.elementRef.nativeElement.offsetHeight

jQuery应该谨慎使用,但如果你必须使用它,你可以制定一个使用#data-element作为其选择器的指令:

import { Directive, ElementRef, AfterViewInit } from '@angular/core';

declare var $: any

@Directive({
  selector: '#data-element'
})
export class DataElementDirective implements AfterViewInit {

  constructor(private el: ElementRef) {
  }

  ngAfterViewInit() {
    $(this.el.nativeElement).scrollTop($('#data-element')[0].scrollHeight);
  }

}

但是再一次没有必要诉诸jQuery来做这件事。

答案 1 :(得分:1)

您必须为更改检测提供先检测某些内容并进行渲染的机会。

当您设置this.data = result;时,您的更改检测将启动并将命令重新呈现受影响的视图(/ -child)。这将花费更长的时间来评估您的下一个if案例,这意味着在您调用jQuery函数时,DOM Element尚无法访问。

快速而肮脏的解决方案是使用setTimeout

this.data = result;

if( this.data.data_child ) {
    setTimeout(() => {
         $('#data-element').scrollTop($('#data-element')[0].scrollHeight);
    });
}

你可以在你的例子中使用它,但我应该提到这不是一个好习惯。

更好的方法可能是使用Observable,它会在您的数据被分配时通知您或通过其他类型的逻辑通知您,例如设置您想要滚动到的固定锚点,这不会依赖于动态添加的元素并转而使用它。

另一方面,要模仿诸如scrollTo之类的事件,如果你想以编程方式触发它们并想要以“角度”方式进行操作,那么动态生成的元素上的焦点,模糊通常很棘手。说到这个,作为数据容器的指令可能是另一种在不使用超时的情况下实现这样的事情的好方法。

答案 2 :(得分:0)

首先,您绝对应该使用ViewChild,而不是JQuery。

<div #data-element>
    ... data here
</div>

在模板中

@ViewChild('data-element') dataElement;

在后面的代码中。 Angular是一个全面的框架。您真的很需要拥抱它,这是做事的方式。当然,这里有一条学习曲线,但这很值得,而且JQuery可以添加很多东西。

别忘了将'ViewChild'添加到您的导入指令中。例如

import { Component, Input, Output, OnInit, OnChanges, ViewChild, ElementRef, Renderer2, HostListener, EventEmitter, AfterViewInit} from '@angular/core';

但是,由于使用ngIf切换了元素,您仍然会遇到相同的错误,因为在初始化组件时该元素不存在,因此使用未定义的ElementRef创建了子视图。

有一些解决方案。

如果您运行的是Angular 8,则可以简单地告诉它元素不是静态的,并且当它存在时,视图子级将收到ElementRef:

@ViewChild('data-element', { static: false }) dataElement: ElementRef;

在Angular 8之前,最好的方法是将条件元素放在子组件中,并将所有必需的数据作为输入参数传递:

<app-data-element *ngIf="data?.data_child"> </app-data-element>

,子组件呈现您的数据。您可以通过@input参数将数据传递到其中:

@input() data: DataStructure;
@input() dob: Date;

您还可以通过@Output事件传回数据。

@Output() dataChanged: EventEmitter<DataStructure> = new EventEmitter<DataStructure>();
this.dataChanged.emit(data);

该元素始终存在于子组件中,因此ElementRef将始终有效。

最后,您也可以只切换样式显示属性。该组件始终存在于DOM中,并且视图子级将始终具有有效的ElementRef。显然,它总是使用资源并导致负载开销,无论您是否显示它:

<div [style.display] = "displayData()" id="data-element">
    ... data here
</div>

代码如下:

@ViewChild('data-element') dataElement;

public displayData() {
    if (data?.data_child) {
       return 'block';
    }
    return 'none';
}

(这也将与JQuery选择器一起使用,但实际上不应该。)