Have you ever found yourself seeking for a nice checkout component? Discover in this blog post a lazy-proof way of building a clean checkout process with the finest CSS framework (Tailwind CSS) and the best JS Framework (Vue.js).

Requirements

  • Node & NPM
  • Vue CLI

Result:

Screenshot_2020-05-20 tailwind-checkout.png

Or

Screenshot_2020-05-20 tailwind-checkout(1).png

If you are going to clone the project

git clone https://github.com/Light-it-labs/tailwind-vue-checkout-form.git

npm install

npm run serve

If you are going to do it from scratch

Step 1: Create the project and Install Tailwind CSS

vue create tailwind-checkout

npm install tailwindcss

//Create a css folder inside assets directory. Then main.css file
cd src/assets && mkdir css && cd css && touch main.css

//Back to the root of the project
../../../

npx tailwindcss init

Inside the main.css file, copy and paste this:

@tailwind base;
@tailwind components;
select {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
  outline: none;
}

input {
  outline: none;
}

@tailwind utilities;
main.css

Import the main.css inside the main.js file.

import Vue from 'vue'
import App from './App.vue'
import "./assets/css/main.css";

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')
main.js

Then create the Post CSS configuration file.

$ touch postcss.config.js

Inside the postcss.config.js, copy and paste this:

const autoprefixer = require('autoprefixer');
const tailwindcss = require('tailwindcss');

module.exports = {
  plugins: [
    tailwindcss,
    autoprefixer,
  ],
};
postcss.config.js

Step 2: Run the application

$ npm run serve

Step 3: Clean the default project files

Delete the HelloWorld.vue and all the styles & content in app.vue.

Your app.vue should look like this:

<template>
  <div id="app">

  </div>
</template>

<script>
export default {
  name: 'App',
}
</script>
app.vue

Step 4: Create the CheckoutPage.vue.

First create a pages directory inside src.

Then, create the CheckoutPage.vue and import it inside your app.vue

<template>
  <div id="app" class="bg-gray-200">
    <CheckoutPage/>
  </div>
</template>

<script>
import CheckoutPage from "./pages/CheckoutPage.vue";
export default {
  name: "App",
  components: {
    CheckoutPage
  }
};
</script>
app.vue

Inside CheckoutPage.vue, copy and paste this:

<template>
  <div :class="isCard ? '' : 'lg:h-screen'" class="container mx-auto p-6 grid grid-cols-1 row-gap-12 lg:grid-cols-10 lg:col-gap-10 lg:pt-12">
    <Payment @handle-card="handleCard" @change-parent="handleAlert" :total="total"></Payment>
    <Summary :items="items"></Summary>
    <Alert :visible="alertVisible" position="top-right" color="success" title="Success" description="Your payment has been successfully processed." />
  </div>
</template>

<script>
import Payment from "../components/Payment";
import Summary from "../components/Summary";
import Alert from "../components/Alert";

export default {
  name: "CheckoutPage",
  components: {
    Payment,
    Summary,
    Alert
  },
  data() {
    return {
      items: [
        {
          title: "Title 1",
          description: "lorem impsu liwe",
          price: 550
        },
        {
          title: "Title 2",
          description: "lorem impsu liwe",
          price: 250
        },
        {
          title: "Title 3",
          description: "lorem impsu liwe",
          price: 150
        }
      ],
      alertVisible: false,
      total: 0,
      isCard: false
    };
  },
  mounted() {
    this.getTotal(this.items);
  },
  methods: {
    getTotal(items) {
      items.forEach(item => {
        this.total += item.price;
      });
    },
    handleAlert() {
      this.alertVisible = true;
      setTimeout(() => {
        this.alertVisible = false;
      }, 4000);
    },
    handleCard() {
      this.isCard = true;
    }
  }
};
</script>
CheckoutPage.vue

Step 5: Create the Payment.vue,  Summary.vue, Alert.vue and Item.vue

Make the 3 components inside the component directory and import it here.

Inside Payment.vue, copy and paste this:

<template>
 <div class="col-span-1 lg:col-span-6">
  <h4 class="text-3xl text-gray-700 mb-5">Payment information</h4>
  <div class="p-10 rounded-md shadow-md bg-white">
   <div class="mb-6">
    <label class="block mb-3 text-gray-600" for="">Name on card</label>
    <input type="text" class="border border-gray-500 rounded-md inline-block py-2 px-3 w-full text-gray-600 tracking-wider"/>
   </div>
   <div class="mb-6">
    <label class="block mb-3 text-gray-600" for="">Card number</label>
    <input
     type="tel" class="border border-gray-500 rounded-md inline-block py-2 px-3 w-full text-gray-600 tracking-widest"/>
   </div>
   <div class="mb-6 flex flex-wrap -mx-3w-full">
    <div class="w-2/3 px-3">
     <label class="block mb-3 text-gray-600" for="">Expiraion date</label>
     <div class="flex">
      <select class="border border-gray-500 rounded-md inline-block py-2 px-3 w-full text-gray-600 tracking-widest mr-6">
       <option>Month</option>
      </select>
      <select class="border border-gray-500 rounded-md inline-block py-2 px-3 w-full text-gray-600 tracking-widest">
       <option>Year</option>
      </select>
     </div>
    </div>
    <div class="w-1/3 px-3">
     <label class="block mb-3 text-gray-600" for="">CVC</label>
     <input type="tel" class="border border-gray-500 rounded-md inline-block py-2 px-3 w-full text-gray-600 tracking-widest"/>
    </div>
   </div>
   <div class="mb-6 text-right">
    <span class="text-right font-bold">{{ total }}USD</span>
   </div>
   <div>
    <button @click="finishPayment" class="w-full text-ceenter px-4 py-3 bg-blue-500 rounded-md shadow-md text-white font-semibold">
     Confirm payment
    </button>
   </div>
  </div>
 </div>
</template>
<script>
export default {
 name: "Payment",
 props:{
  total:Number
 },
 methods:{
 finishPayment(){
  this.$emit('change-parent');
  }
 }
};
</script>
Payment.vue

Inside Summary.vue, copy and paste this:

<template>
 <div class="col-span-1 lg:col-span-4 order-first lg:order-last">
  <h4 class="text-3xl text-gray-700 mb-5">Order Summary</h4>
  <div class="p-10 rounded-md shadow-md bg-white">
   <item :key="i" v-for="(item, i) in items" :item="item"  />
  </div>
 </div>
</template>

<script>
import Item from "./Item";
export default {
  name: "Summary",
  props: {
    items: Object
  },
  components: {
    Item
  },
  data() {
    return {

    };
  }
};
</script>
Summary.vue

Inside Item.vue, copy and paste this (the item receives 1 item object as a prop):

<template>
  <div class="grid grid-cols-8 overflow-hidden col-gap-6 mb-5">
    <img class="block col-span-3 w-full h-20 rounded bg-gray-600" src alt />
    <div class="col-span-5">
      <div class="flex">
        <h6 class="text-sm font-semibold text-gray-800 flex-grow">{{ item.title }}</h6>
        <button>
          <svg class="w-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
            <path
              d="M10 8.586L2.929 1.515 1.515 2.929 8.586 10l-7.071 7.071 1.414 1.414L10 11.414l7.071 7.071 1.414-1.414L11.414 10l7.071-7.071-1.414-1.414L10 8.586z"
            />
          </svg>
        </button>
      </div>
      <p
        class="text-xs truncate text-gray-600"
      >{{item.description}}</p>
      <span class="text-sm text-gray-800">{{ item.price }} USD</span>
    </div>
  </div>
</template>

<script>
export default {
  name: "Item",
  props: {
    item: Object
  }
};
</script>
Item.vue

Inside Alert.vue:

You can customize the color, position, title and description for this component.

color: "success || error || warnning".

position: "top-right || bottom-right || top-left || bottom-left".

title: "whatever you want".

description:"Lorem ipsum".

For this component you also need to install animate.css

https://animate.style/

$ npm install animate.css --save
<template>
  <div v-if="visible" :class="[userPosition, userColor]" class="w-full lg:w-3/12 border-l-4 p-4" role="alert">
    <p class="font-bold">{{ title }}</p>
    <p>{{ description }}</p>
  </div>
</template>

<script>
import "animate.css";
export default {
  name: "Alert",
  props: {
    visible: Boolean,
    position: String,
    title: String,
    description: String,
    color: String
  },
  data() {
    return {
      userPosition: "",
      userColor: ""
    };
  },
  watch: {
    visible() {
      switch (this.color) {
        case "success":
          this.userColor = this.success;
          break;
        case "error":
          this.userColor = this.error;
          break;
        case "warnning":
          this.userColor = this.warnning;
          break;
        default:
          this.userColor = this.success;
      }
      switch (this.position) {
        case "top-right":
          this.userPosition = this.topRight;
          break;
        case "bottom-right":
          this.userPosition = this.bottomRight;
          break;
        case "top-left":
          this.userPosition = this.topLeft;
          break;
        case "bottom-left":
          this.userPosition = this.bottomLeft;
          break;
        default:
          this.userPosition = this.topRight;
      }
    }
  },
  computed: {
    topRight() {
      return "top-0 right-0 mt-5 mr-5 lg:absolute animate__animated animate__fadeInRight";
    },
    topLeft() {
      return "top-0 left-0 mt-5 ml-5 lg:absolute animate__animated animate__fadeInLeft";
    },
    bottomRight() {
      return "bottom-0 right-0 mb-5 mr-5 lg:absolute animate__animated animate__fadeInRight";
    },
    bottomLeft() {
      return "bottom-0 left-0 mb-5 ml-5 lg:absolute animate__animated animate__fadeInLeft";
    },
    success() {
      return "bg-green-100 border-green-500 text-green-700";
    },
    error() {
      return "bg-red-100 border-red-500 text-red-700";
    },
    warnning() {
      return "bg-yellow-100 border-yellow-500 text-yellow-700";
    }
  }
};
</script>
Alert.vue

That's it!

Thank you for reading this post! Please let us know if you like it with your feedback and support. If there's any bug or changes you would implement, feel free to make a PR!

Payment.vue file (Optional)

If you want your Payment.vue to look like this, here's how to do it!

CardComponent created by Muhammed Erdem

screencapture-localhost-8080-2020-05-18-15_23_08.png

Step 1: Install extra dependencies

$ npm install sass-loader --save-dev

$ npm install --save-dev node-sass

Step 2: Import the styles

Make a scss directory inside assets. Then create card-component.scss file inside.

Copy the styles from HERE

Step 3: Create the Card.vue component

Inside the components directory, create the Card.vue component

Copy and paste this:

<template>
  <div class="card-item" :class="{ '-active' : isCardFlipped }">
    <div class="card-item__side -front">
      <div
        class="card-item__focus"
        :class="{'-active' : focusElementStyle }"
        :style="focusElementStyle"
        ref="focusElement"
      ></div>
      <div class="card-item__cover">
        <img
          v-if="currentCardBackground"
          :src="currentCardBackground"
          class="card-item__bg"
        />
      </div>
      <div class="card-item__wrapper">
        <div class="card-item__top">
          <img
            src="https://raw.githubusercontent.com/muhammederdem/credit-card-form/master/src/assets/images/chip.png"
            class="card-item__chip"
          />
          <div class="card-item__type">
            <transition name="slide-fade-up">
              <img
                :src="'https://raw.githubusercontent.com/muhammederdem/credit-card-form/master/src/assets/images/' + cardType + '.png'"
                v-if="cardType"
                :key="cardType"
                alt
                class="card-item__typeImg"
              />
            </transition>
          </div>
        </div>
        <label :for="fields.cardNumber" class="card-item__number" :ref="fields.cardNumber">
          <template>
            <span v-for="(n, $index) in currentPlaceholder" :key="$index">
              <transition name="slide-fade-up">
                <div class="card-item__numberItem" v-if="getIsNumberMasked($index, n)">*</div>
                <div
                  class="card-item__numberItem"
                  :class="{ '-active' : n.trim() === '' }"
                  :key="currentPlaceholder"
                  v-else-if="labels.cardNumber.length > $index"
                >{{labels.cardNumber[$index]}}</div>
                <div
                  class="card-item__numberItem"
                  :class="{ '-active' : n.trim() === '' }"
                  v-else
                  :key="currentPlaceholder + 1"
                >{{n}}</div>
              </transition>
            </span>
          </template>
        </label>
        <div class="card-item__content">
          <label :for="fields.cardName" class="card-item__info" :ref="fields.cardName">
            <div class="card-item__holder">Card Holder</div>
            <transition name="slide-fade-up">
              <div class="card-item__name" v-if="labels.cardName.length" key="1">
                <transition-group name="slide-fade-right">
                  <span
                    class="card-item__nameItem"
                    v-for="(n, $index) in labels.cardName.replace(/\s\s+/g, ' ')"
                    :key="$index + 1"
                  >{{n}}</span>
                </transition-group>
              </div>
              <div class="card-item__name" v-else key="2">Full Name</div>
            </transition>
          </label>
          <div class="card-item__date" ref="cardDate">
            <label :for="fields.cardMonth" class="card-item__dateTitle">Expires</label>
            <label :for="fields.cardMonth" class="card-item__dateItem">
              <transition name="slide-fade-up">
                <span v-if="labels.cardMonth" :key="labels.cardMonth">{{labels.cardMonth}}</span>
                <span v-else key="2">MM</span>
              </transition>
            </label>
            /
            <label for="cardYear" class="card-item__dateItem">
              <transition name="slide-fade-up">
                <span v-if="labels.cardYear" :key="labels.cardYear">{{String(labels.cardYear).slice(2,4)}}</span>
                <span v-else key="2">YY</span>
              </transition>
            </label>
          </div>
        </div>
      </div>
    </div>
    <div class="card-item__side -back">
      <div class="card-item__cover">
        <img
          v-if="currentCardBackground"
          :src="currentCardBackground"
          class="card-item__bg"
        />
      </div>
      <div class="card-item__band"></div>
      <div class="card-item__cvv">
        <div class="card-item__cvvTitle">CVV</div>
        <div class="card-item__cvvBand">
          <span v-for="(n, $index) in labels.cardCvv" :key="$index">*</span>
        </div>
        <div class="card-item__type">
          <img
            :src="'https://raw.githubusercontent.com/muhammederdem/credit-card-form/master/src/assets/images/' + cardType + '.png'"
            v-if="cardType"
            class="card-item__typeImg"
          />
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "Card",
  props: {
    labels: Object,
    fields: Object,
    isCardNumberMasked: Boolean,
    randomBackgrounds: {
      type: Boolean,
      default: true
    },
    backgroundImage: [String, Object]
  },
  data() {
    return {
      focusElementStyle: null,
      currentFocus: null,
      isFocused: false,
      isCardFlipped: false,
      amexCardPlaceholder: "#### ###### #####",
      dinersCardPlaceholder: "#### ###### ####",
      defaultCardPlaceholder: "#### #### #### ####",
      currentPlaceholder: ""
    };
  },
  watch: {
    currentFocus() {
      if (this.currentFocus) {
        this.changeFocus();
      } else {
        this.focusElementStyle = null;
      }
    },
    cardType() {
      this.changePlaceholder();
    }
  },
  mounted() {
    this.changePlaceholder();

    let self = this;
    let fields = document.querySelectorAll("[data-card-field]");
    fields.forEach(element => {
      element.addEventListener("focus", () => {
        this.isFocused = true;
        if (
          element.id === this.fields.cardYear ||
          element.id === this.fields.cardMonth
        ) {
          this.currentFocus = "cardDate";
        } else {
          this.currentFocus = element.id;
        }
        this.isCardFlipped = element.id === this.fields.cardCvv;
      });
      element.addEventListener("blur", () => {
        this.isCardFlipped = !element.id === this.fields.cardCvv;
        setTimeout(() => {
          if (!self.isFocused) {
            self.currentFocus = null;
          }
        }, 300);
        self.isFocused = false;
      });
    });
  },
  computed: {
    cardType() {
      let number = this.labels.cardNumber;
      let re = new RegExp("^4");
      if (number.match(re) != null) return "visa";

      re = new RegExp("^(34|37)");
      if (number.match(re) != null) return "amex";

      re = new RegExp("^5[1-5]");
      if (number.match(re) != null) return "mastercard";

      re = new RegExp("^6011");
      if (number.match(re) != null) return "discover";

      re = new RegExp("^62");
      if (number.match(re) != null) return "unionpay";

      re = new RegExp("^9792");
      if (number.match(re) != null) return "troy";

      re = new RegExp("^3(?:0([0-5]|9)|[689]\\d?)\\d{0,11}");
      if (number.match(re) != null) return "dinersclub";

      re = new RegExp("^35(2[89]|[3-8])");
      if (number.match(re) != null) return "jcb";

      return ""; // default type
    },
    currentCardBackground() {
      if (this.randomBackgrounds && !this.backgroundImage) {
        // TODO will be optimized
        let random = Math.floor(Math.random() * 25 + 1);
        return `https://raw.githubusercontent.com/muhammederdem/credit-card-form/master/src/assets/images/${random}.jpeg`;
      } else if (this.backgroundImage) {
        return this.backgroundImage;
      } else {
        return null;
      }
    }
  },
  methods: {
    changeFocus() {
      let target = this.$refs[this.currentFocus];
      this.focusElementStyle = target
        ? {
            width: `${target.offsetWidth}px`,
            height: `${target.offsetHeight}px`,
            transform: `translateX(${target.offsetLeft}px) translateY(${target.offsetTop}px)`
          }
        : null;
    },
    getIsNumberMasked(index, n) {
      return (
        index > 4 &&
        index < 14 &&
        this.labels.cardNumber.length > index &&
        n.trim() !== "" &&
        this.isCardNumberMasked
      );
    },
    changePlaceholder() {
      if (this.cardType === "amex") {
        this.currentPlaceholder = this.amexCardPlaceholder;
      } else if (this.cardType === "dinersclub") {
        this.currentPlaceholder = this.dinersCardPlaceholder;
      } else {
        this.currentPlaceholder = this.defaultCardPlaceholder;
      }
      this.$nextTick(() => {
        this.changeFocus();
      });
    }
  }
};
</script>
Card.vue

Step 4: Payment.vue needs to look like this

Replace the Payment component for this:

<template>
 <div class="col-span-1 lg:col-span-6">
  <h4 class="text-3xl text-gray-700 mb-5">Payment information</h4>
  <div class="p-10 rounded-md shadow-md bg-white">
   <div v-if="cardComponent" class="mb-6">
    <Card
     :fields="fields"
     :labels="formData"
     :isCardNumberMasked="isCardNumberMasked"
     :randomBackgrounds="randomBackgrounds"
     :backgroundImage="backgroundImage"
    />
   </div>
   <div class="mb-6">
    <label class="block mb-3 text-gray-600" for="">Name on card</label>
    <input
     type="text"
     :id="fields.cardName"
     v-letter-only
     @input="changeName"
     :value="formData.cardName"
     data-card-field
     autocomplete="off"
     class="border border-gray-500 rounded-md inline-block py-2 px-3 w-full text-gray-600 tracking-wider"
    />
   </div>
   <div class="mb-6">
    <label class="block mb-3 text-gray-600" for="">Card number</label>
    <input
     type="tel"
     :id="fields.cardNumber"
     @input="changeNumber"
     @focus="focusCardNumber"
     @blur="blurCardNumber"
     :value="formData.cardNumber"
     :maxlength="cardNumberMaxLength"
     data-card-field
     autocomplete="off"
     class="border border-gray-500 rounded-md inline-block py-2 px-3 w-full text-gray-600 tracking-widest"
    />
   </div>
   <div class="mb-6 flex flex-wrap -mx-3w-full">
    <div class="w-2/3 px-3">
     <label class="block mb-3 text-gray-600" for="">Expiraion date</label>
     <div class="flex">
      <select
       class="border border-gray-500 rounded-md inline-block py-2 px-3 w-full text-gray-600 tracking-widest mr-6"
       :id="fields.cardMonth"
       v-model="formData.cardMonth"
       @change="changeMonth"
       data-card-field
      >
       <option value disabled selected>Month</option>
       <option
        v-bind:value="n < 10 ? '0' + n : n"
        v-for="n in 12"
        v-bind:disabled="n < minCardMonth"
        v-bind:key="n"
        >{{ generateMonthValue(n) }}</option
       >
      </select>
      <select
       class="border border-gray-500 rounded-md inline-block py-2 px-3 w-full text-gray-600 tracking-widest"
       :id="fields.cardYear"
       v-model="formData.cardYear"
       @change="changeYear"
       data-card-field
      >
       <option value disabled selected>Year</option>
       <option
        v-bind:value="$index + minCardYear"
        v-for="(n, $index) in 12"
        v-bind:key="n"
        >{{ $index + minCardYear }}</option
       >
      </select>
     </div>
    </div>
    <div class="w-1/3 px-3">
     <label class="block mb-3 text-gray-600" for="">CVC</label>
     <input
      type="tel"
      v-number-only
      :id="fields.cardCvv"
      maxlength="4"
      :value="formData.cardCvv"
      @input="changeCvv"
      data-card-field
      autocomplete="off"
      class="border border-gray-500 rounded-md inline-block py-2 px-3 w-full text-gray-600 tracking-widest"
     />
    </div>
   </div>
   <div class="mb-6 text-right">
    <span class="text-right font-bold">{{ total }} USD</span>
   </div>
   <div>
    <button @click="finishPayment"
     class="w-full text-ceenter px-4 py-3 bg-blue-500 rounded-md shadow-md text-white font-semibold"
    >
     Confirm payment
    </button>
   </div>
  </div>
 </div>
</template>
<script>
import Card from "./Card";
export default {
  name: "Payment",
  directives: {
    "number-only": {
      bind(el) {
        function checkValue(event) {
          event.target.value = event.target.value.replace(/[^0-9]/g, "");
          if (event.charCode >= 48 && event.charCode <= 57) {
            return true;
          }
          event.preventDefault();
        }
        el.addEventListener("keypress", checkValue);
      }
    },
    "letter-only": {
      bind(el) {
        function checkValue(event) {
          if (event.charCode >= 48 && event.charCode <= 57) {
            event.preventDefault();
          }
          return true;
        }
        el.addEventListener("keypress", checkValue);
      }
    }
  },
  props: {
    total: Number,
    formData: {
      type: Object,
      default: () => {
        return {
          cardName: "",
          cardNumber: "",
          cardMonth: "",
          cardYear: "",
          cardCvv: ""
        };
      }
    },
    backgroundImage: [String, Object],
    randomBackgrounds: {
      type: Boolean,
      default: true
    }
  },
  components: {
    Card
  },
  data() {
    return {
      cardComponent: true,
      fields: {
        cardNumber: "v-card-number",
        cardName: "v-card-name",
        cardMonth: "v-card-month",
        cardYear: "v-card-year",
        cardCvv: "v-card-cvv"
      },
      minCardYear: new Date().getFullYear(),
      isCardNumberMasked: true,
      mainCardNumber: this.cardNumber,
      cardNumberMaxLength: 19
    };
  },
  computed: {
    minCardMonth() {
      if (this.formData.cardYear === this.minCardYear)
        return new Date().getMonth() + 1;
      return 1;
    }
  },
  watch: {
    cardYear() {
      if (this.formData.cardMonth < this.minCardMonth) {
        this.formData.cardMonth = "";
      }
    }
  },
  mounted() {
    this.maskCardNumber();
    if (this.cardComponent === true) {
      this.$emit("handle-card");
    }
  },
  methods: {
    generateMonthValue(n) {
      return n < 10 ? `0${n}` : n;
    },
    changeName(e) {
      this.formData.cardName = e.target.value;
      this.$emit("input-card-name", this.formData.cardName);
    },
    changeNumber(e) {
      this.formData.cardNumber = e.target.value;
      let value = this.formData.cardNumber.replace(/\D/g, "");
      // american express, 15 digits
      if (/^3[47]\d{0,13}$/.test(value)) {
        this.formData.cardNumber = value
          .replace(/(\d{4})/, "$1 ")
          .replace(/(\d{4}) (\d{6})/, "$1 $2 ");
        this.cardNumberMaxLength = 17;
      } else if (/^3(?:0[0-5]|[68]\d)\d{0,11}$/.test(value)) {
        // diner's club, 14 digits
        this.formData.cardNumber = value
          .replace(/(\d{4})/, "$1 ")
          .replace(/(\d{4}) (\d{6})/, "$1 $2 ");
        this.cardNumberMaxLength = 16;
      } else if (/^\d{0,16}$/.test(value)) {
        // regular cc number, 16 digits
        this.formData.cardNumber = value
          .replace(/(\d{4})/, "$1 ")
          .replace(/(\d{4}) (\d{4})/, "$1 $2 ")
          .replace(/(\d{4}) (\d{4}) (\d{4})/, "$1 $2 $3 ");
        this.cardNumberMaxLength = 19;
      }
      // eslint-disable-next-line eqeqeq
      if (e.inputType == "deleteContentBackward") {
        let lastChar = this.formData.cardNumber.substring(
          this.formData.cardNumber.length,
          this.formData.cardNumber.length - 1
        );
        // eslint-disable-next-line eqeqeq
        if (lastChar == " ") {
          this.formData.cardNumber = this.formData.cardNumber.substring(
            0,
            this.formData.cardNumber.length - 1
          );
        }
      }
      this.$emit("input-card-number", this.formData.cardNumber);
    },
    changeMonth() {
      this.$emit("input-card-month", this.formData.cardMonth);
    },
    changeYear() {
      this.$emit("input-card-year", this.formData.cardYear);
    },
    changeCvv(e) {
      this.formData.cardCvv = e.target.value;
      this.$emit("input-card-cvv", this.formData.cardCvv);
    },
    invaildCard() {
      let number = this.formData.cardNumber;
      let sum = 0;
      let isOdd = true;
      for (let i = number.length - 1; i >= 0; i--) {
        let num = number.charAt(i);
        if (isOdd) {
          sum += num;
        } else {
          num = num * 2;
          if (num > 9) {
            num = num
              .toString()
              .split("")
              .join("+");
          }
          sum += num;
        }
        isOdd = !isOdd;
      }
      if (sum % 10 !== 0) {
        alert("invaild card number");
      }
    },
    blurCardNumber() {
      if (this.isCardNumberMasked) {
        this.maskCardNumber();
      }
    },
    maskCardNumber() {
      this.mainCardNumber = this.formData.cardNumber;
      let arr = this.formData.cardNumber.split("");
      arr.forEach((element, index) => {
        if (index > 4 && index < 14 && element.trim() !== "") {
          arr[index] = "*";
        }
      });
      this.formData.cardNumber = arr.join("");
    },
    unMaskCardNumber() {
      this.formData.cardNumber = this.mainCardNumber;
    },
    focusCardNumber() {
      this.unMaskCardNumber();
    },
    toggleMask() {
      this.isCardNumberMasked = !this.isCardNumberMasked;
      if (this.isCardNumberMasked) {
        this.maskCardNumber();
      } else {
        this.unMaskCardNumber();
      }
    },
    finishPayment() {
      this.$emit("change-parent");
    }
  }
};
</script>

<style lang="scss">
@import "../assets/scss/card-component.scss";
</style>
Payment.vue

We're Software Experts!

Do you have a project and need development services or an extra hand? Let's partner up!

At Light-it, we offer Product Discovery, UX/UI, Development, & Staff Augmentation Services. Let's create amazing software people love <3 to use!

Contact us