Repository: https://github.com/Light-it-labs/tailwind-vue-checkout-form
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 Result :
Or
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
$ 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 Developers with Expertise in Tailwind.css and Vue.jse're 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!