Nicholas
Nicholas

Reputation: 1980

Vue SPA Single File Component Element binding with Stripe Elements

Looking to see if I can utilise Stripe Elements on a Vue SPA single file component. However, I am challenged by the following error:

IntegrationError: Missing argument. Make sure to call mount() with a valid DOM element or selector.
    at new t (https://js.stripe.com/v3/:1:10765)
    at t.<anonymous> (https://js.stripe.com/v3/:1:97008)
    at t.<anonymous> (https://js.stripe.com/v3/:1:26038)
    at VueComponent.createCardElement (webpack-internal:///./node_modules/cache-loader/dist/cjs.js?!./node_modules/babel-loader/lib/index.js!./node_modules/cache-loader/dist/cjs.js?!./node_modules/vue-loader/lib/index.js?!./src/components/stripe/card-modal.vue?vue&type=script&lang=js&:143:17)
    at VueComponent.stripePubKey (webpack-internal:///./node_modules/cache-loader/dist/cjs.js?!./node_modules/babel-loader/lib/index.js!./node_modules/cache-loader/dist/cjs.js?!./node_modules/vue-loader/lib/index.js?!./src/components/stripe/card-modal.vue?vue&type=script&lang=js&:177:14)
    at Watcher.run (webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:4562:19)
    at flushSchedulerQueue (webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:4304:13)
    at Array.eval (webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:1979:12)
    at flushCallbacks (webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:1905:14)

The code executes, failing right at the .mount() function, whining about an inexistent DOM element/selector.

Below are the following methods I've attempted:

  1. The code is executed post-mounted of the component's lifecycle (My component is a Buefy modal and the stripe element mount logic executes when the modal is visible to the user).
  2. The code is executed immediately according to Stripe's official Vue example
  3. The code is executed immediately after the property 'stripePubKey' is not null (Notice that the 'stripePubKey' is updated at the mounted segment of the Vue lifecycle).

Here's the component as of scenario 3

<template>
    <div>
        <b-button class="button is-info is-rounded"
                  size="is-medium"
                  icon-left="credit-card"
                  :loading="isLoading" 
                  @click="isModalActive = true" v-if="!cardId">
            Add a card
        </b-button>
        <button class="button is-warning"
                :loading="isLoading"
                @click="isModalActive = true" v-else>
            Edit
        </button>

        <b-modal has-modal-card trap-focus :active.sync="isModalActive">
            <b-loading :active.sync="isModalLoading" :can-cancel="false" />
            <!--https://stackoverflow.com/questions/48028718/using-event-modifier-prevent-in-vue-to-submit-form-without-redirection-->
            <form v-on:submit.prevent="create()" class="has-text-justified">
                <div class="modal-card">
                    <header class="modal-card-head">
                        <p class="modal-card-title" v-if="!cardId">Add a card</p>
                        <p class="modal-card-title" v-else>Edit a card</p>
                    </header>
                    <section class="modal-card-body">
                        <div ref="cardo"></div>
                        <p v-show="elementsError" id="card-errors" v-text="elementsError" />
                    </section>

                    <footer class="modal-card-foot">
                        <button class="button" type="button" @click="isModalActive = false">Close</button>
                        <button class="button is-primary" type="submit" :disabled="!complete">Add</button>
                    </footer>
                </div>
            </form>
        </b-modal>
    </div>
</template>

<script>
    import {mapActions, mapGetters} from 'vuex';
    import {NotificationProgrammatic as Notification} from 'buefy';
    import PaymentService from "@/services/auth/PaymentService";

    export default {
        name: "stripe-card-modal",
        props: {
            currentRoute: window.location.href, // https://forum.vuejs.org/t/how-to-get-path-from-route-instance/26934/2
            cardId: {
                type: String,
                default: null
            }
        },
        computed: {
            ...mapGetters('oidcStore', [
                'oidcUser'
            ])
        },
        data: function () {
            return {
                isLoading: true,
                isModalActive: false,
                isModalLoading: false,
                complete: false,
                // Stripe variables
                card: null,
                elementsError: null,
                paymentMethod: 'card',
                stripe: null,
                stripePubKey: ''
            }
        },
        methods: {
            ...mapActions('oidcStore', ['authenticateOidc', 'signOutOidc']),
            createCardElement: function() {
                let self = this;
                
                // Check if stripe is up, else set it up
                if (!self.stripe && self.stripePubKey) {
                    self.stripe = Stripe(self.stripePubKey);
                } else {
                    return;
                }
                
                // Get stripe elements up
                const elements = self.stripe.elements({
                    // Use Roboto from Google Fonts
                    fonts: [
                        {
                            cssSrc: 'https://fonts.googleapis.com/css?family=Roboto',
                        },
                    ],
                    // Detect the locale automatically
                    locale: 'auto',
                });
                // Define CSS styles for Elements
                const style = {
                    base: {
                        fontSize: '15px',
                        fontFamily: 'Roboto',
                        fontSmoothing: 'antialiased',
                        color: '#525f7f',
                        '::placeholder': {
                            color: '#AAB7C4',
                        },
                    },
                    // Styles when the Element has invalid input
                    invalid: {
                        color: '#cc5b7a',
                        iconColor: '#cc5b7a'
                    }
                };
                // Create the card element, then attach it to the DOM
                self.card = elements.create('card', {style});
                self.card.mount(this.$refs.cardo);
                // Add an event listener: check for error messages as we type
                self.card.addEventListener('change', ({error}) => {
                    if (error) {
                        self.elementsError = error.message;
                    } else {
                        self.elementsError = '';
                    }
                });
            },
            create: function () {
                this.isModalLoading = true;

                let self = this;
                
            },
        },
        mounted: function() {
            let self = this;

            PaymentService.getStripePubKey()
                .then(function(res) {
                    self.stripePubKey = res.data;
                })
            .finally(function() {
                self.isLoading = false;
            });
        },
        watch: {
            stripePubKey(newVal, oldVal) {
                let self = this;
                
                if (newVal && !oldVal) { // If the modal is up
                    self.createCardElement();
                }
            }
        },
    }
</script>

Additionally, the component file given above attempts to mount the card element via $refs while the official Stripe VueJS sample uses '#card-element' to do so. That was already attempted as well!

Here's my index.html for my SPA just in case (I have also attempted to place it at the end of the tag.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <link href="https://fonts.googleapis.com/css?family=Roboto:100:300,400,500,700,900|Material+Icons" rel="stylesheet">
    <title>Nozomi</title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but Nozomi doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
  <script src="https://js.stripe.com/v3/"></script>
</html>

Upvotes: 0

Views: 1623

Answers (1)

Trenton Trama
Trenton Trama

Reputation: 4930

What's happening is that the buefy model doesn't actually exist in the dom until after it's been opened and rendered.

It's a really easy fix - instead of directly setting isModalActive to true, create a method to open it and instantiate the stripe element in the next tick. This will ensure the dom element has been rendered.

methods: {
    openModal() {
        this.isModalActive = true;
        this.$nextTick(function () {
            this.createCardElement()
        })
    }
}

Upvotes: 1

Related Questions