You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何确认表单步骤渲染完成后再使用元素?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元素。

我的疑问

  1. 创建Stripe令牌时,怎么调试确认元素是否被移除、重置或隐藏?
  2. 如何确保第二步完全渲染到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>
解决方案

一、调试确认元素状态的方法

  1. 直接检查DOM元素的存在与连接状态
    在调用createToken()前,用原生JS查询DOM节点并验证其是否在文档树中:
const cardNumberEl = document.getElementById('floatingDebitCardNumber');
console.log('DOM元素是否存在:', !!cardNumberEl);
console.log('元素是否在文档树中:', cardNumberEl?.isConnected);

如果isConnected为false,说明元素已被移除出DOM,这就是错误的直接原因。

  1. 结合Stripe的isMounted()与DOM检查
    Stripe的isMounted()可能因元素引用未更新导致误判,需同时验证DOM状态:
const isStripeMounted = this.debitCardNumberElement?.isMounted();
const isDomConnected = !!document.getElementById('floatingDebitCardNumber')?.isConnected;
console.log('Stripe挂载状态:', isStripeMounted, 'DOM连接状态:', isDomConnected);

只有两者都为true,元素才处于正常可用状态。

  1. 用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 });

二、确保第二步渲染完成后再挂载元素

  1. 利用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');
    // ... 其他元素的创建与挂载逻辑
  });
}
  1. 动态挂载/卸载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;
}
  1. 强制触发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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 06:33:08