当父级div中由 @if渲染的子内容发生变化时,如何重新渲染该父级div?
我有一个悬浮在光标上方的提示框元素。提示框的大小显然取决于其内容。我还需要确保提示框在视口内完全可见,因此我把提示框的位置设定为与其大小以及光标的位置相关。
以下是模板:
@if (cursorLocation()) {
<div #tooltipContainer
[style.top]="'max(' + (tooltipContainer.offsetHeight + 40) + 'px,' + cursorLocation()!.top + 'px)'"
[style.left]="'min(100vw - ' + (10 + tooltipContainer.offsetWidth / 2) + 'px, max(' + (10 + tooltipContainer.offsetWidth / 2) + 'px,' + cursorLocation()!.left + 'px))'">
<div class="mat-mdc-dialog-content">
@if (dataHandler.selectedWord()) {
{{dataHandler.selectedWord()?.translation}}
} @else {
<wb-loading-screen></wb-loading-screen>
}
</div>
</div>
}
整体要复杂一些,但重要的信息在于 #tooltipContainer 和 @if 内容部分。
以下是组件中的要点:
export class BaseTooltipComponent {
public readonly cursorLocation: ModelSignal<{ top: number, left: number } | undefined> = model.required();
}
export class WordTooltipComponent extends BaseTooltipComponent {
public readonly dataHandler: WordHandler = inject(WordHandler);
public readonly ids: InputSignal<{ languageId: string,
wordId: string } | undefined> = input.required();
constructor() {
super();
effect(this._initWords.bind(this), { allowSignalWrites: true });
}
private _initWords(): void {
if (!this.ids()) { return; }
this.dataHandler.read(this.ids()!.wordId, this.ids()!.languageId);
}
}
如果要显示提示框,它会获取光标的当前位置信息。然后,在针对某个单词的特定提示框情形下,它还会接收两个ID,一个用于语言,一个用于单词,然后请数据处理器读取这些信息,这些信息要么来自其内部缓存,要么向数据库请求。一旦完成,dataHandler会更新其信号属性 selectedWord。这随后调用以更新提示框中的内容,即 @if 内部的内容。一旦完成,就会抛出以下错误:
ERROR RuntimeError: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value for 'top': 'max(166px, 442px)'. Current value: 'max(154px, 442px)'
据我理解,这个问题是指只有在 selectedWord 更新时,提示框的内容才会重新渲染。但是,内容的变化会改变 #tooltipContainer 的大小,这随后会更新 [style.top] 和 [style.left] 的值,超出了相应的Angular生命周期步骤,因为容器本身并没有被重新渲染,这显然会出现报错(尽管提示框仍然在正确的位置渲染)。
我不理解的是,为什么这只在 offsetHeight 时才会发生。如果我把它从 [style.top] 绑定中移除,尽管 offsetWidth 仍然存在并在更新,似乎就不会抛出异常。我也尝试了顶/左参数顺序的各种组合,尝试将绑定在top和 left之间交换,使left包含 offsetHeight。出于某种原因,这个问题只是出现在 offsetHeight。
因此,我其实也不确定自己对这个问题的判断是否正确,否则它也应该对offsetWidth同样发生不是吗?不过如果确实如此,是否有办法让Angular在设置 selectedWord 时,在容器内容发生改变时重新渲染父容器组件,而不需要把整个容器包裹在 @if-语句中?
解决方案
更新:
如果 afterRenderEffect 不可用,你可以尝试在ngAfterViewInit内添加该效应,同时确保你也传入注入器。
export class BaseTooltipComponent {
public readonly cursorLocation: ModelSignal<{ top: number, left: number } | undefined> = model.required();
}
export class WordTooltipComponent extends BaseTooltipComponent {
private injector = inject(Injector);
public readonly dataHandler: WordHandler = inject(WordHandler);
public readonly ids: InputSignal<{ languageId: string,
wordId: string } | undefined> = input.required();
constructor() {
super();
}
ngAfterViewInit() {
effect(this._initWords.bind(this), {
injector: this.injector,
allowSignalWrites: true
});
}
private _initWords(): void {
if (!this.ids()) { return; }
this.dataHandler.read(this.ids()!.wordId, this.ids()!.languageId);
}
}
这也许只会在 offsetHeight 上发生,因为那是变更检测运行前后实际值不同的计算。
要了解更多关于此错误,请访问 Error Encyclopedia - Expression Changed After Checked | Angular.dev
Angular在变更检测完成后再次修改表达式的值时会抛出ExpressionChangedAfterItHasBeenCheckedError。只有在开发模式下才会抛出此错误。 在开发模式下,Angular会在每次变更检测后进行额外的检查,以确保绑定没有改变。这可以捕捉到视图处于不一致状态时的错误。例如,如果某个方法或getter每次调用返回不同的值,或者子组件改变了父组件的值。如果发生任意一种情况,就说明变更检测尚未稳定。Angular抛出该错误,确保数据始终在视图中正确体现,从而防止异常UI行为或可能的无限循环。
要解决这个问题,我们可以使用 afterRenderEffect,它将:
注册一个在应用完成渲染时,在mixedReadWrite阶段被触发执行的效应。
因此,计算在渲染之后完成,在开发模式下值不会改变。
export class BaseTooltipComponent {
public readonly cursorLocation: ModelSignal<{ top: number, left: number } | undefined> = model.required();
}
export class WordTooltipComponent extends BaseTooltipComponent {
public readonly dataHandler: WordHandler = inject(WordHandler);
public readonly ids: InputSignal<{ languageId: string,
wordId: string } | undefined> = input.required();
constructor() {
super();
afterRenderEffect(this._initWords.bind(this), { allowSignalWrites: true });
}
private _initWords(): void {
if (!this.ids()) { return; }
this.dataHandler.read(this.ids()!.wordId, this.ids()!.languageId);
}
}
唯一能确定的是 dataHandler 在某种程度上改变了用于计算 top 和 left 样式的信号状态。