Basics
Multi-step form
{}
vue
<template>
<div class="bg-white rounded-lg p-10 max-w-xl shadow-box-circle dark:bg-dark-1000">
<form @submit="handleSubmit">
<!-- Defining Form Steps -->
<FormSteps>
<!-- 1st step - 'Customer information' -->
<FormStep
name="customer_information"
label="Customer information"
:elements="['contact_information', 'shipping_address']"
:labels="{
next: 'Continue to shipping method',
}"
/>
<!-- 2nd step - 'Shipping method' -->
<FormStep
name="shipping_method"
label="Shipping method"
:elements="['summary', 'shipping_method']"
:labels="{
next: 'Continue to payment',
previous: 'Back'
}"
/>
<!-- 3rd step - 'Payment method' -->
<FormStep
name="payment_method"
label="Payment method"
:elements="['summary', 'email', 'payment_method', 'billing_address', 'remember_me', 'terms']"
:labels="{
finish: 'Complete order',
previous: 'Back'
}"
/>
</FormSteps>
<!-- Defining form elements -->
<FormElements>
<!-- 'Contact information' (email) -->
<GroupElement name="contact_information">
<template #label>
<div class="text-lg leading-tight mb-4 mt-4">Contact information</div>
</template>
<TextElement
name="email"
default="john@doe.com"
placeholder="Email"
rules="required|email:debounce=300"
/>
<ToggleElement name="updates">
Keep me up to date on news and exclusive offers
</ToggleElement>
</GroupElement>
<!-- 'Shipping address' (name, company, address fields, phone) -->
<GroupElement name="shipping_address">
<template #label>
<div class="text-lg leading-tight mb-4 mt-4">Shipping address</div>
</template>
<TextElement
name="firstname"
default="John"
placeholder="First name (optional)"
:columns="6"
/>
<TextElement
name="lastname"
default="Doe"
placeholder="Last name"
rules="required"
:columns="6"
/>
<TextElement
name="company"
default="Google Inc."
placeholder="Company (optional)"
/>
<TextElement
name="address"
default="Amphitheatre Parkway 1600"
placeholder="Address"
rules="required"
/>
<TextElement
name="address2"
placeholder="Apartment, suite, etc. (optional)"
/>
<TextElement
name="city"
default="Mountain View"
placeholder="City"
rules="required"
/>
<SelectElement
name="country"
default="US"
placeholder="Country"
autocomplete="new-country"
input-type="search"
rules="required"
:items="countries"
:search="true"
:can-clear="false"
:columns="countryColumn"
/>
<SelectElement
name="state"
default="CA"
placeholder="State"
autocomplete="new-state"
input-type="search"
rules="required"
:items="states"
:search="true"
:resolve-on-load="true"
:can-clear="false"
:columns="4"
:conditions="[
['shipping_address.country', 'US']
]"
/>
<TextElement
name="zip_code"
default="94043"
placeholder="ZIP code"
rules="required"
:columns="4"
/>
<TextElement
name="phone"
default="(516)-793-8668"
placeholder="Phone"
rules="required"
/>
</GroupElement>
<!-- Shipping summary block with custom ContactComponent -->
<StaticElement name="summary">
<ContactComponent />
</StaticElement>
<!-- 'Shipping method' - using custom template for checkboxes -->
<RadiogroupElement
name="shipping_method"
rules="required"
:items="[
{ value: 'usps', label: '<b>$66.46</b> - USPS Priority Mail Express', description: '1 business days' },
{ value: 'fedex', label: '<b>$66.98</b> - FedEx Home Delivery', description: '1 to 5 business days' },
{ value: 'ups', label: '<b>$120.82</b> - UPS Second Day Air', description: '2 business days' },
]"
view="blocks"
>
<template #label>
<div class="text-lg leading-tight mt-2">Shipping method</div>
</template>
<template #before>
<div class="text-gray-700 mb-4 dark:text-dark-300">
Please Note - Orders will be ship the next business day. Please add one shipping day to all estimates.
</div>
</template>
</RadiogroupElement>
<!-- 'Payment method' block -->
<GroupElement name="payment_method">
<template #label>
<div class="text-lg leading-tight mt-2">Payment method</div>
</template>
<template #before>
<div class="text-gray-700 mb-4 dark:text-dark-300">
All transactions are secure and encrypted.
</div>
</template>
<!-- 'Credit card' option -->
<RadioElement
name="credit_card"
radio-name="payment_method"
:add-class="{ wrapper: 'rounded-t' }"
>
<div class="flex justify-between items-center ml-0.5">
<div>Credit card</div>
<div class="flex items-center">
<img src="/images/card-logos.png" class="h-6" />
</div>
</div>
</RadioElement>
<!-- Credit card details (if selected) -->
<GroupElement
name="card_details"
add-class="bg-gray-100 p-6 -mt-4 border-l border-r border-gray-300 dark:bg-dark-900 dark:border-dark-700"
:conditions="[
['payment_method.credit_card', 1]
]"
>
<TextElement
name="card_number"
placeholder="Card number (do not enter actual card number)"
/>
<TextElement
name="card_name"
placeholder="Cardholder name"
:columns="6"
/>
<TextElement
name="card_date"
placeholder="MM / YY"
:columns="3"
/>
<TextElement
name="card_cvv"
placeholder="CVV"
:columns="3"
/>
</GroupElement>
<!-- 'Paypal' option -->
<RadioElement
name="paypal"
radio-name="payment_method"
:add-class="{
container: '-mt-4 relative -top-px',
wrapper: data.paypal ? '' : 'rounded-b',
}"
:rules="[{
required: ['credit_card', null]
}]"
:messages="{
required: 'Please choose a payment method.'
}"
>
<div class="flex justify-between items-center ml-0.5">
<img src="/images/paypal.png" class="h-6" />
<div class="flex items-center">
<img src="/images/card-logos.png" class="object-cover object-left w-30 h-6" />
</div>
</div>
</RadioElement>
<!-- Paypal info window (if selected) -->
<StaticElement
name="paypal_info"
:conditions="[
['payment_method.paypal', 1]
]"
>
<div class="bg-gray-100 py-6 border border-gray-300 rounded-b -mt-4 relative -top-0.5 dark:bg-dark-900 dark:border-dark-700">
<div class="w-72 mx-auto text-md text-center">
<img src="/images/paypal-redirect.svg" class="mx-auto mb-4" />
After clicking "Complete order", you will be redirected to PayPal to complete your purchase securely.
</div>
</div>
</StaticElement>
</GroupElement>
<!-- 'Billing address' block -->
<ObjectElement name="billing_address">
<template #label>
<div class="text-lg leading-tight mt-2 mb-2">Billing address</div>
</template>
<!-- 'Same' button -->
<RadioElement
name="same"
radio-name="billing"
:add-class="{ wrapper: 'rounded-t' }"
>
<div class="ml-0.5">
Same as billing address
</div>
</RadioElement>
<!-- 'Different' button -->
<RadioElement
name="different"
radio-name="billing"
:add-class="{
container: '-mt-4 relative -top-px',
wrapper: data.billing_address && data.billing_address.different ? '' : 'rounded-b',
}"
:rules="[{
required: ['billing_address.same', null]
}]"
:messages="{
required: 'A billing address option must be selected.'
}"
>
<div class="ml-0.5">
Use a different billing address
</div>
</RadioElement>
<!-- Billing info block (if 'use different is selected') -->
<GroupElement
name="billing_info"
add-class="bg-gray-100 p-6 -mt-4 border border-gray-300 rounded-b relative -top-0.5 dark:bg-dark-900 dark:border-dark-700"
:conditions="[
['billing_address.different', 1]
]"
>
<TextElement
name="firstname"
default="John"
placeholder="First name (optional)"
:columns="6"
/>
<TextElement
name="lastname"
default="Doe"
placeholder="Last name"
rules="required"
:columns="6"
/>
<TextElement
name="company"
default="Google Inc."
placeholder="Company (optional)"
/>
<TextElement
name="address"
default="Amphitheatre Parkway 1600"
placeholder="Address"
rules="required"
/>
<TextElement
name="address2"
placeholder="Apartment, suite, etc. (optional)"
/>
<TextElement
name="city"
default="Mountain View"
placeholder="City"
rules="required"
/>
<SelectElement
name="country"
default="US"
placeholder="Country"
autocomplete="new-country"
input-type="search"
rules="required"
:items="countries"
:search="true"
:can-clear="false"
:columns="countryColumn"
/>
<SelectElement
name="state"
default="CA"
placeholder="State"
autocomplete="new-state"
input-type="search"
rules="required"
:items="states"
:search="true"
:resolve-on-load="true"
:can-clear="false"
:columns="4"
:conditions="[
['shipping_address.country', 'US']
]"
/>
<TextElement
name="zip_code"
default="94043"
placeholder="ZIP code"
rules="required"
:columns="4"
/>
</GroupElement>
</ObjectElement>
<!-- 'Remember me' block -->
<GroupElement name="remember_me">
<template #label>
<div class="text-lg leading-tight mt-2 mb-2">Remember me</div>
</template>
<!-- 'Remember me' checkbox -->
<CheckboxElement
name="remember"
:add-class="{
wrapper: `rounded-t ${data.remember ? '' : 'rounded-b'}`,
}"
>
Save my information for faster checkout
</CheckboxElement>
<!-- 'Mobile phone number' input (if 'Remember me' is checked) -->
<TextElement
name="mobile_number"
placeholder="Mobile phone number"
rules="required"
add-class="bg-gray-100 p-6 -mt-4 border border-gray-300 rounded-b relative -top-px dark:bg-dark-900 dark:border-dark-700"
:conditions="[
['remember_me.remember', 1]
]"
>
<template #addon-before>
<fa :icon="['fas', 'mobile-alt']"></fa>
</template>
<template #after>
<div class="mt-2">
Next time you check out here or other stores powered by Shop, Shop will send you an authorization code by SMS to securely purchase with Shop Pay.
</div>
</template>
</TextElement>
</GroupElement>
<StaticElement name="terms" add-class="text-sm text-gray-500 dark:text-dark-300">
By continuing, you agree to Shop Pay’s <a href="" class="underline">Privacy Policy</a> and <a href="" class="underline">Terms of Service</a>.
</StaticElement>
</FormElements>
<FormStepsControls />
</form>
</div>
</template>
<script>
import { Vueform, useVueform } from '@vueform/vueform'
import ContactComponent from "./partials/ContactComponent.vue";
export default {
mixins: [ Vueform ],
setup: useVueform,
components: {
ContactComponent,
},
data: () => ({
vueform: {
size: 'lg',
validateOn: 'change|step',
overrideClasses: {
RadioElement: {
wrapper: 'flex border border-gray-300 py-4 px-4 items-center cursor-pointer dark:border-dark-700',
text: 'w-full items-center',
},
CheckboxElement: {
wrapper: 'flex border border-gray-300 py-4 px-4 items-center cursor-pointer dark:border-dark-700',
text: 'w-full items-center',
},
},
addClasses: {
RadioElement: {
input: 'mb-1',
},
CheckboxElement: {
input: 'mb-1',
},
}
},
countries: {},
states: {},
}),
computed: {
// Dynamically calculating column size for country
// (narrower when states are also visible)
countryColumn() {
return this.data.country === 'US' ? 4 : 8
},
},
mounted() {
// Setting `countries` and `states`
['countries', 'states'].map((param) => {
fetch(`/json/${param}.json`)
.then(response => response.json())
.then(data => this[param] = data)
})
}
}
</script>
<style>
.w-30 {
width: 7.5rem;
}
</style>
vue
<template>
<div class="bg-white border border-gray-300 rounded p-4 mt-4 dark:bg-dark-800 dark:border-dark-800">
<!-- Contact details -->
<div class="pb-4 flex justify-between">
<div class="flex items-start mr-6">
<span class="text-gray-500 w-20 flex-grow-0 flex-shrink-0 dark:text-dark-400">Contact</span>
<span>{{ contact }}</span>
</div>
<div class="text-sm text-primary-500"><a href="" @click.prevent="handleChangeData">Change</a></div>
</div>
<!-- Shipping address -->
<div class="pt-4 border-t border-gray-200 flex justify-between dark:border-dark-600">
<div class="flex items-start mr-6">
<span class="text-gray-500 w-20 flex-grow-0 flex-shrink-0 dark:text-dark-400">Ship to</span>
<span>{{ shipTo }}</span>
</div>
<div class="text-sm text-primary-500"><a href="" @click.prevent="handleChangeData">Change</a></div>
</div>
<!-- Payment method -->
<div v-if="isAtLastStep" class="pt-4 mt-4 border-t border-gray-200 flex justify-between dark:border-dark-600">
<div class="flex items-start mr-6">
<span class="text-gray-500 w-20 flex-grow-0 flex-shrink-0 dark:text-dark-400">Method</span>
<span v-html="method"></span>
</div>
<div class="text-sm text-primary-500"><a href="" @click.prevent="handleChangeMethod">Change</a></div>
</div>
</div>
</template>
<script>
import { inject, computed } from 'vue'
export default {
setup(props, context) {
const form$ = inject('form$')
// `Contact details` data
const contact = computed(() => {
return form$.value.data.email
})
// `Shipping address` data
const shipTo = computed(() => {
const data = form$.value.data
const parts = ['address', ',', 'address2', ',', 'city', 'state', 'zip_code', ',', 'country']
return parts.map((part, i) => {
if (part === ',') {
return data[parts[i-1]] ? part : ''
}
let value = data[part]
if (part === 'country') {
value = form$.value.countries[value]
}
return value && i > 0 ? ' ' + value : value
}).join('')
})
// `Payment method` data
const method = computed(() => {
const shippingMethod = form$.value.data.shipping_method
const shippingMethodItems = form$.value.el$('shipping_method')?.resolvedOptions
return shippingMethod
? shippingMethodItems.find(i => i.value === shippingMethod).label
: undefined
})
const isAtLastStep = computed(() => {
return form$.value.steps$.isAtLastStep
})
const handleChangeData = () => {
form$.value.steps$.goTo('customer_information')
}
const handleChangeMethod = () => {
form$.value.steps$.goTo('shipping_method')
}
return {
contact,
shipTo,
method,
isAtLastStep,
handleChangeData,
handleChangeMethod,
}
},
}
</script>