如何确认表单步骤渲染完成后再使用元素?Angular2-wizard+Stripe问题
问题描述
我在用angular2-wizard搭建银行账户信息填写流程,第二步需要收集卡号、CVC、有效期信息,这些输入元素通过Stripe.js结合ngx-stripe库手动挂载。但点击下一步调用createToken()创建Stripe令牌时,出现以下错误:
IntegrationError: We could not retrieve data from the specified Element. Please make sure the Element you are attempting to use is still mounted.
我推测问题原因是:angular2-wizard切换步骤时移除了DOM元素,或是我在第二步完全渲染完成前就尝试挂载了Stripe元素。
我的疑问
- 创建Stripe令牌时,怎么调试确认元素是否被移除、重置或隐藏?
- 如何确保第二步完全渲染到DOM后再挂载Stripe元素?
已尝试的方法
- 调用Stripe的
this.debitCardNumberElement.isMounted()方法,在createToken()前检查元素挂载状态,变量有值但不确定判断是否准确。 - 查找步骤库的渲染完成通知发射器,未找到,想了解其他检查方式。
示例代码
组件逻辑代码
showAccountFieldsAndMountElements($event) { // 挂载三个Stripe元素 this.stripeService.elements(this.stripeElementsOptions).subscribe(elements => { this.elements = elements; // 只在第一次挂载元素 if (!this.debitCardNumberElement) { this.debitCardNumberElement = elements.create('cardNumber', this.debitCardNumberElementOptions); this.debitCardNumberElement.mount('#floatingDebitCardNumber'); this.debitCardNumberElement.on('change', (event) => { this.cardNumberErrors = event.error ? event.error.message : null; this.cardNumberValid = event.complete; }) } if (!this.debitCardExpiryElement) { this.debitCardExpiryElement = elements.create('cardExpiry', this.debitCardExpiryElementOptions); this.debitCardExpiryElement.mount('#floatingDebitCardExpiry'); this.debitCardExpiryElement.on('change', (event) => { this.cardExpiryErrors = event.error ? event.error.message : null; this.cardExpiryValid = event.complete; }) } if (!this.debitCardCvcElement) { this.debitCardCvcElement = elements.create('cardCvc', this.debitCardCvcElementOptions); this.debitCardCvcElement.mount('#floatingDebitCardCvc'); this.debitCardCvcElement.on('change', (event) => { this.cardCvcErrors = event.error ? event.error.message : null; this.cardCvcValid = event.complete; }) } }); } createTokenAndShowFinalStep($event) { this.stripeService.createToken(this.debitCardNumberElement, data).subscribe((result) => { this.loadingAddress = false; if (result.token) { this.externalToken = result.token.id; this.initNext(); } else if (result.error) { // 创建令牌出错 console.log(result.error); this.alertService.danger(result.error.message); } }); }
模板代码
<form-wizard> <wizard-step [title]="'Personal Info'" (onNext)="showAccountFieldsAndMountElements($event)"> <h1>Step1</h1> </wizard-step> <wizard-step [title]="'Account Info'" (onNext)="createTokenAndShowFinalStep($event)"> <h1>Step2</h1> <div class="form-floating mb-3"> <div class="form-control stripe-form-control" id="floatingDebitCardNumber" placeholder="ex. 1111 1111 1111 1111"></div> <label for="floatingDebitCardNumber">Card Number</label> <div *ngIf="cardNumberErrors" class="invalid-feedback d-block" role="alert"> {{cardNumberErrors}} </div> </div> <div class="row mb-md-3"> <div class="col-md mb-3 mb-md-0"> <div class="form-floating"> <div class="form-control" id="floatingDebitCardExpiry" placeholder="ex. 11/23"></div> <label for="floatingDebitCardExpiry">Card Expiration</label> <ng-container *ngIf="cardExpiryErrors"> <span class="text-danger">{{cardExpiryErrors}}</span> </ng-container> </div> </div> <div class="col-md"> <div class="form-floating"> <div class="form-control" id="floatingDebitCardCvc" placeholder="ex. 123"></div> <label for="floatingDebitCardCvc">Card CVC</label> <ng-container *ngIf="cardCvcErrors"> <span class="text-danger">{{cardCvcErrors}}</span> </ng-container> </div> </div> </div> </wizard-step> </form-wizard>
解决方案
一、调试确认元素状态的方法
- 直接检查DOM元素的存在与连接状态
在调用createToken()前,用原生JS查询DOM节点并验证其是否在文档树中:
const cardNumberEl = document.getElementById('floatingDebitCardNumber'); console.log('DOM元素是否存在:', !!cardNumberEl); console.log('元素是否在文档树中:', cardNumberEl?.isConnected);
如果isConnected为false,说明元素已被移除出DOM,这就是错误的直接原因。
- 结合Stripe的
isMounted()与DOM检查
Stripe的isMounted()可能因元素引用未更新导致误判,需同时验证DOM状态:
const isStripeMounted = this.debitCardNumberElement?.isMounted(); const isDomConnected = !!document.getElementById('floatingDebitCardNumber')?.isConnected; console.log('Stripe挂载状态:', isStripeMounted, 'DOM连接状态:', isDomConnected);
只有两者都为true,元素才处于正常可用状态。
- 用MutationObserver监听DOM变化
监听第二步容器的DOM变化,查看元素是否被移除:
const step2Container = document.querySelector('wizard-step[title="Account Info"]'); const observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { if (mutation.type === 'childList') { console.log('第二步DOM移除的节点:', mutation.removedNodes); } }); }); observer.observe(step2Container, { childList: true, subtree: true });
二、确保第二步渲染完成后再挂载元素
- 利用Angular视图生命周期+步骤激活状态判断
给第二步添加模板引用变量,监听其激活状态,等待Angular完成视图渲染后再挂载:
模板修改:
<wizard-step #accountStep [title]="'Account Info'" (onNext)="createTokenAndShowFinalStep($event)"> <!-- 原有内容不变 --> </wizard-step>
组件逻辑补充:
import { ViewChild, AfterViewInit } from '@angular/core'; // ... @ViewChild('accountStep') accountStep: any; ngAfterViewInit() { const checkStepActive = () => { if (this.accountStep?.active) { // 等待浏览器完成DOM渲染 requestAnimationFrame(() => { this.mountStripeElements(); // 把挂载逻辑抽成独立方法 }); } else { setTimeout(checkStepActive, 100); } }; checkStepActive(); } mountStripeElements() { this.stripeService.elements(this.stripeElementsOptions).subscribe(elements => { // 步骤切换时DOM可能被销毁,需重新创建元素 this.debitCardNumberElement = elements.create('cardNumber', this.debitCardNumberElementOptions); this.debitCardNumberElement.mount('#floatingDebitCardNumber'); // ... 其他元素的创建与挂载逻辑 }); }
- 动态挂载/卸载Stripe元素
每次进入第二步时重新挂载,离开时卸载并清空引用,避免DOM销毁后引用失效:
// 第一步点击下一步后延迟挂载,确保第二步DOM已渲染 showAccountFieldsAndMountElements($event) { setTimeout(() => { this.mountStripeElements(); }, 500); } // 离开第二步时卸载元素(如果wizard支持onLeave事件) onStep2Leave($event) { this.debitCardNumberElement?.unmount(); this.debitCardExpiryElement?.unmount(); this.debitCardCvcElement?.unmount(); // 清空引用,下次进入重新创建 this.debitCardNumberElement = null; this.debitCardExpiryElement = null; this.debitCardCvcElement = null; }
- 强制触发Angular变更检测
如果DOM已渲染但Angular未更新视图,可在挂载前强制触发变更检测:
import { ChangeDetectorRef } from '@angular/core'; // ... constructor(private cdr: ChangeDetectorRef) {} mountStripeElements() { this.cdr.detectChanges(); // 强制Angular更新视图 this.stripeService.elements(this.stripeElementsOptions).subscribe(elements => { // 挂载逻辑 }); }
内容的提问来源于stack exchange,提问作者chuckd
相关产品推荐
相关产品推荐

