Angular 9+组件DOM增删时生命周期钩子触发及展开面板动画异常求解
问题根因分析
- 子组件生命周期未触发的核心原因:父级组件(sky-accordion/filter-panel)大概率内部使用了模板缓存逻辑(比如
ng-template复用、虚拟滚动复用实例),没有真实销毁重建sky-expansion-panel实例,所以ngOnInit/ngOnDestroy只会在页面首次加载时触发一次。 - 动画异常根因:
- 之前的状态绑定写法存在语法错误:
[@expand]="isClosed ? closed : open"里的closed/open未加引号,被识别为组件实例变量而非状态字符串,导致动画状态匹配失败 - 父组件销毁时触发了子组件的
:leave动画,给内容元素插入了关闭状态的行内样式,父组件重建后动画模块未正确重置该样式,导致和实际isClosed状态不匹配 *ngIf和状态动画混用存在队列冲突:父组件销毁优先级高于子组件动画状态重置逻辑,导致状态同步失败
- 之前的状态绑定写法存在语法错误:
解决方案
第一步:修正动画配置与绑定语法
首先简化状态驱动动画的配置,移除不必要的void状态过渡:
animations: [ trigger('expand', [ state('closed', [ style({ height: '0', opacity: 0, overflow: 'hidden' }), ]), state('open', [ style({ height: '*', opacity: 1, overflow: 'visible' }) ]), transition('closed <=> open', [ animate( '500ms cubic-bezier(1, .14, .69, .05)') ]) ]) ]
同时修正模板中的状态绑定,给状态值加单引号,并且移除*ngIf避免和动画逻辑冲突:
<div class="content" [@expand]="isClosed ? 'closed' : 'open'"> <div class="content-wrapper"> <ng-content></ng-content> </div> </div>
第二步:监听输入属性变化同步状态
因为组件实例被复用不会触发ngOnInit,改用ngOnChanges监听isClosed输入属性的变化,主动触发动画状态更新:
import { Component, Input, OnChanges, SimpleChanges, ChangeDetectorRef } from '@angular/core'; export class SkyExpansionPanelComponent implements OnChanges { @Input() header: string; @Input() isClosed = true; constructor(private cdr: ChangeDetectorRef) {} ngOnChanges(changes: SimpleChanges): void { // 每次isClosed输入值变化时主动触发状态更新 if (changes['isClosed']) { this.cdr.markForCheck(); } } public toggleSelf(): void { this.isClosed = !this.isClosed; } }
第三步:补充默认样式兜底
给内容元素加默认样式,避免动画行内样式丢失时出现显示异常:
.content { height: auto; opacity: 1; overflow: visible; // 保留原有其他样式 }
可选方案:保留父组件*ngIf的同时保留leave动画
如果需要保留父组件的*ngIf销毁逻辑,避免父组件隐藏时触发不必要的子组件leave动画,可以调整显隐逻辑:
- 给filter-panel声明单独的显隐控制变量,不要直接用
*ngIf="toggleLogic()" - 当需要隐藏filter-panel时,先把所有子展开面板的
isClosed设为true,监听子组件的动画完成事件,等所有收起动画跑完后再把filter-panel的显隐变量设为false,触发DOM移除。
组件插入/移除DOM的监听方案
如果确实需要每次组件插入DOM时执行自定义逻辑,可以通过监听元素连接状态实现:
import { ElementRef } from '@angular/core'; private prevInDomStatus = false; constructor(private elementRef: ElementRef<HTMLElement>) {} ngDoCheck(): void { const isInDom = this.elementRef.nativeElement.isConnected; if (isInDom && !this.prevInDomStatus) { // 组件刚被插入DOM,执行状态重置逻辑 this.prevInDomStatus = true; this.cdr.markForCheck(); } else if (!isInDom && this.prevInDomStatus) { // 组件刚被移除DOM,执行清理逻辑 this.prevInDomStatus = false; } }
内容的提问来源于stack exchange,提问作者Brett Parsons
相关产品推荐
相关产品推荐

