Ionic/Angular中如何重置超时器与动画?
问题描述
预期通知展示行为
- 点击按钮后,通知滑下显示文本
- 通知自动滑回;若有多条消息,每2秒切换显示下一条
重复点击的期望行为
- 点击按钮,通知滑下显示文本
- 再次点击时,立即停止当前动画,重置计时器与消息队列
- 显示新消息并重新启动计时器,最后通知滑回
遇到的错误
[ng] 11: Cannot find namespace 'NodeJS',对应代码行:endAnimationTimeout: NodeJS.Timeout | undefined;[ng] 10: Cannot find namespace 'NodeJS',对应代码行:startAnimationTimeout: NodeJS.Timeout | undefined;
注意:消息组件被其他组件调用
相关代码
消息组件TS代码
import { Component, OnInit } from '@angular/core'; import { ValidatorsService } from '../../../services/validators.service'; @Component({ selector: 'app-message', templateUrl: './message.component.html', styleUrls: ['./message.component.scss'], }) export class MessageComponent implements OnInit { startAnimationTimeout: NodeJS.Timeout | undefined; endAnimationTimeout: NodeJS.Timeout | undefined; constructor(public validatorsService: ValidatorsService) { } ngOnInit() { let card = document.querySelector('.message-card') as HTMLDivElement; let timeBar = document.querySelector('.time-bar') as HTMLDivElement; this.showMessage(card, timeBar); } messageAnimation(card: HTMLDivElement, timeBar: HTMLDivElement) { if (this.validatorsService.messageIndex <= this.validatorsService.messages.length - 1) { timeBar.classList.remove('slideToLeft') } clearTimeout(this.startAnimationTimeout); this.startAnimationTimeout = setTimeout(() => { timeBar.classList.add('slideToLeft'); this.endAnimationTimeout = setTimeout(() => { if (this.validatorsService.messageIndex >= this.validatorsService.messages.length - 1) { card.classList.replace('slideDown', 'slideUp'); setTimeout(() => { card.classList.replace('slideUp', 'hide'); timeBar.classList.remove('slideToLeft') }, 750); } }, 2000); }, 500); } showMessage(card: HTMLDivElement, timeBar: HTMLDivElement) { if (this.validatorsService.messages.length === 1) { clearTimeout(this.startAnimationTimeout); clearTimeout(this.endAnimationTimeout); this.messageAnimation(card, timeBar); return; } if (this.validatorsService.messageIndex >= this.validatorsService.messages.length) { clearTimeout(this.startAnimationTimeout); return; } this.messageAnimation(card, timeBar); setTimeout(() => { this.validatorsService.messageIndex++; this.messageAnimation(card, timeBar); this.showMessage(card, timeBar); }, 2600); return; }; }
消息组件HTML代码
<div #card class="message-card slideDown"> <p class="message" [ngClass]="{'validMessage': validatorsService.messageType === 'success', 'warningMessage': validatorsService.messageType === 'warning'}"> <span *ngIf="validatorsService.messageType === 'error'">Error {{validatorsService.messageIndex + 1}} of {{validatorsService.messages.length}}:</span> {{validatorsService.messages[validatorsService.messageIndex]}}</p> <div #timeBar class="time-bar" [ngClass]="{'valid': validatorsService.messageType === 'success', 'warning': validatorsService.messageType === 'warning'}"> </div> </div>
ValidatorsService及按钮点击函数
import { Injectable } from '@angular/core'; import { AbstractControl } from '@angular/forms'; @Injectable({ providedIn: 'root' }) export class ValidatorsService { messages: string[] = []; messageIndex: number = 0; messageType: string = 'error'; showMessage: boolean = false; constructor() { } // Form validations noSpaceAllowed(control: AbstractControl) { let parentControl: any = control.parent; let controlName; if (parentControl) { controlName = Object.keys(parentControl.controls).find(key => parentControl.controls[key] === control); } if (control.value !== null && control.value.indexOf(' ') !== -1) { return { error: `Your ${controlName} can't contain spaces.` }; } return null; } noEmptyAllowed(control: AbstractControl) { let parentControl: any = control.parent; let controlName; if (parentControl) { controlName = Object.keys(parentControl.controls).find(key => parentControl.controls[key] === control); } if (control.value !== null && control.value === '') { return { error: `Your ${controlName} can't be empty.` }; } return null; } lengthRangeAllowed(control: AbstractControl) { let parentControl: any = control.parent; let controlName; if (parentControl) { controlName = Object.keys(parentControl.controls).find(key => parentControl.controls[key] === control); } if (controlName === 'email' && control.value !== null && control.value.length > 254) { return { error: `Your ${controlName} must have less then 254 characters.` }; } if (controlName === 'username' && control.value !== null && control.value.length > 20) { return { error: `Your ${controlName} must have less then 20 characters.` }; } if (controlName === 'password' && control.value !== null && control.value.length < 6) { return { error: `Your ${controlName} must contain a minimum of 6 characters.` }; } if (controlName === 'password' && control.value !== null && control.value.length > 24) { return { error: `Your ${controlName} must have less then 24 characters.` }; } return null; } noSpaceOnlyAllowed(control: AbstractControl) { let parentControl: any = control.parent; let controlName; if (parentControl) { controlName = Object.keys(parentControl.controls).find(key => parentControl.controls[key] === control); } if (control.value !== null && control.value !== '' && /^\s*$/.test(control.value)) { return { error: `Your ${controlName} can't contain just spaces.` }; } return null; } noConsecutiveSpacesAllowed(control: AbstractControl) { let parentControl: any = control.parent; let controlName; if (parentControl) { controlName = Object.keys(parentControl.controls).find(key => parentControl.controls[key] === control); } if (control.value !== null && control.value !== '' && /\s{2,}/.test(control.value)) { return { error: `Your ${controlName} can't contain consecutive spaces.` }; } return null; } noStartNorEndSpacesAllowed(control: AbstractControl) { let parentControl: any = control.parent; let controlName; if (parentControl) { controlName = Object.keys(parentControl.controls).find(key => parentControl.controls[key] === control); } if (control.value !== null && control.value !== '' && control.value.startsWith(" ")) { return { error: `Your ${controlName} can't start with a space.` }; } if (control.value !== null && control.value !== '' && control.value.endsWith(" ")) { return { error: `Your ${controlName} can't end with a space.` }; } return null; } acceptToSValidation(control: AbstractControl) { if (control.value !== null && control.value === false) { return { error: 'You Must Agree to the Terms & Conditions.' }; } return null; } }
formValidation() { this.validatorsService.messages = []; this.validatorsService.messageIndex = 0; Object.keys(this.form.controls).forEach(key => { let control: any = this.form.controls[key]; if (control.errors) { Object.keys(control.errors).forEach(error => { this.validatorsService.messages.push(control.errors[error]); }); }; }); this.validatorsService.showMessage = true; } onSubmit() { this.validatorsService.showMessage = false Object.values(this.form.controls).forEach(control => { control.markAsTouched(); }); if (this.form.valid === false) { this.validatorsService.messageType = 'error'; this.formValidation(); return; } this.validatorsService.messageType = 'success'; console.log(this.form.value); }
父组件引用消息组件的HTML
<app-message *ngIf="this.validatorsService.showMessage"></app-message>
解决方案
1. 修复NodeJS命名空间错误
Angular浏览器环境无需使用NodeJS.Timeout,直接用ReturnType<typeof setTimeout>替代(类型更安全):
startAnimationTimeout: ReturnType<typeof setTimeout> | undefined; endAnimationTimeout: ReturnType<typeof setTimeout> | undefined;
若确实需要NodeJS类型支持,可安装依赖并配置:
npm install @types/node --save-dev
修改tsconfig.json:
{ "compilerOptions": { "types": ["node"] } }
2. 实现重复点击重置逻辑
步骤1:添加重置方法到消息组件
resetNotifications(card: HTMLDivElement, timeBar: HTMLDivElement) { // 清除所有计时器 clearTimeout(this.startAnimationTimeout); clearTimeout(this.endAnimationTimeout); // 重置动画状态 card.classList.remove('slideUp', 'hide'); card.classList.add('slideDown'); timeBar.classList.remove('slideToLeft'); // 重置消息索引 this.validatorsService.messageIndex = 0; }
步骤2:监听消息数组变化
修改ValidatorsService,用BehaviorSubject实现消息数组的可观察性:
import { BehaviorSubject } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class ValidatorsService { messages$ = new BehaviorSubject<string[]>([]); messageIndex: number = 0; messageType: string = 'error'; showMessage: boolean = false; // ... 其他验证方法保持不变 }
更新formValidation方法:
formValidation() { const newMessages: string[] = []; Object.keys(this.form.controls).forEach(key => { let control: any = this.form.controls[key]; if (control.errors) { Object.keys(control.errors).forEach(error => { newMessages.push(control.errors[error]); }); }; }); this.validatorsService.messages$.next(newMessages); this.validatorsService.showMessage = true; }
在消息组件中订阅变化并重置:
import { Subscription } from 'rxjs'; export class MessageComponent implements OnInit, OnDestroy { startAnimationTimeout: ReturnType<typeof setTimeout> | undefined; endAnimationTimeout: ReturnType<typeof setTimeout> | undefined; private messagesSubscription!: Subscription; constructor(public validatorsService: ValidatorsService) { } ngOnInit() { const card = document.querySelector('.message-card') as HTMLDivElement; const timeBar = document.querySelector('.time-bar') as HTMLDivElement; this.messagesSubscription = this.validatorsService.messages$.subscribe(() => { this.resetNotifications(card, timeBar); this.showMessage(card, timeBar); }); this.showMessage(card, timeBar); } ngOnDestroy() { this.messagesSubscription.unsubscribe(); clearTimeout(this.startAnimationTimeout); clearTimeout(this.endAnimationTimeout); } // ... 其他方法保持不变 }
步骤3:优化消息展示逻辑
替换递归调用为可控的计时器链,避免内存泄漏:
showMessage(card: HTMLDivElement, timeBar: HTMLDivElement) { const messages = this.validatorsService.messages$.value; if (messages.length === 0) return; const displayNext = () => { if (this.validatorsService.messageIndex >= messages.length) { // 所有消息展示完毕,隐藏卡片 card.classList.replace('slideDown', 'slideUp'); setTimeout(() => { card.classList.replace('slideUp', 'hide'); timeBar.classList.remove('slideToLeft'); }, 750); return; } this.messageAnimation(card, timeBar); this.validatorsService.messageIndex++; setTimeout(displayNext, 2600); }; displayNext(); }
内容的提问来源于stack exchange,提问作者Mateus Oliveira
相关产品推荐
相关产品推荐

