Here’s an example of how you can create a multi-step checkout form using Alpine.js. In this example, we’ll create a simple 5 steps form where the user enters their shipping information and submit the form in the final steps.
What is Alpine.js?
Alpine.js is a lightweight JavaScript framework that empowers developers to add interactive behaviors to web pages with minimal overhead. Unlike larger frameworks, such as Vue.js or React, Alpine.js focuses on simplicity and ease of use, making it an excellent choice for projects that don’t require the complexity of a full-scale framework.
At its core, Alpine.js provides a declarative syntax for enhancing HTML elements with dynamic behavior. By leveraging the power of directives directly in your HTML markup, Alpine.js allows you to effortlessly build interactive components and user interfaces. This distinctive approach eliminates the need for writing separate JavaScript files and promotes a tighter integration between the HTML and JavaScript parts of your application.
Advantages of Using Alpine.js:
- Minimal Learning Curve: Alpine.js is designed with simplicity in mind. Its intuitive syntax and minimal setup make it accessible to developers of all skill levels. Even if you’re new to JavaScript frameworks, you can quickly grasp the concepts and start building interactive features.
- Lightweight: Alpine.js has an incredibly small footprint, weighing in at around 10KB when minified and gzipped. This means faster loading times and a smaller impact on your website’s performance. It’s ideal for projects where optimizing load times is crucial.
- Direct HTML Integration: The framework’s directives are directly applied to your HTML elements as attributes. This “in-place” approach makes it easy to understand the interactivity of a component just by looking at its markup. This tight coupling between HTML and JavaScript streamlines development and debugging.
- No Build Step Required: Alpine.js doesn’t require a build step or a compilation process. You can include it directly in your HTML using a script tag and start using its features immediately. This simplicity is especially beneficial for prototyping, small projects, or situations where complex build setups are unnecessary.
- Component-Focused: Alpine.js enables you to create self-contained components with their own logic and state. This promotes modularization and code reusability, allowing you to create interactive elements that can be easily reused across your project.
- Vue.js-like Syntax: If you’re familiar with Vue.js, you’ll find some similarities in the syntax and concepts of Alpine.js. This can be advantageous if you’re transitioning from Vue.js or if you want a lightweight alternative that aligns with similar design patterns.
- Interactivity Without Sacrificing Performance: Alpine.js helps you achieve dynamic and interactive interfaces without the performance overhead associated with larger frameworks. It’s particularly useful when you need to add specific features to your application without bogging it down with unnecessary complexity.
OK. Let’s find out how we can build a multi steps form using Alpine.js!
1. Setting Up HTML
<!doctype html>
<html lang="en" data-bs-theme="auto">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<title>Multi-Step Form With Alpine JS | onlyWebPro Web Dev Blog</title>
<!-- For styles purpose -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<main class="w-100 mx-auto my-4">
</main>
<script src="https://unpkg.com/alpinejs@3.12.3" defer></script>
</body>
</html>
- Line 10: Load Bootstrap CSS framework from jsdelivr cdn.
- Line 16: Load AlpineJs v3.12.3 from unpkg cdn.
2. Setting Up Logic
Next, place following JavaScript after Line 16 as mentioned above:
<script>
const BTN_TEXT = [
'Back',
'Next',
'Confirm & Submit',
'Return to Homepage'
];
const ERROR_MSG = [
'Error occured. Please make sure all fields are filled in correctly.',
'Error occured. Please make sure insert a valid format of zipcode.',
'Error occured. Please make sure insert a valid phone number.',
'Error occured. Please select your delivery date.'
];
const SUCCESS_MSG = [
'Thank You',
'Your form has been received and is being processed. You should receive a confirmation email shortly.'
];
document.addEventListener('alpine:init', () => {
Alpine.data('appData', () => ({
isError: false,
isLoading: false,
errorMsg: '',
successMsgTitle: SUCCESS_MSG[0],
successMsg: SUCCESS_MSG[1],
step: 1,
minStep: 1,
maxStep: 5,
progressBar: { width: '0%' },
primaryBtnText: BTN_TEXT[1],
secondaryBtnText: BTN_TEXT[0],
formData: {
recipient: '',
address: '',
postcode: '',
phone: '',
receipt: '',
date: ''
},
receiptPreview: null,
handleBackClick() {
this.step--
this.isError = false
},
handleClick() {
if (this.step === 1) {
if (this.formData.recipient.length === 0) {
this.isError = true;
this.errorMsg = ERROR_MSG[0]
} else {
this.isError = false;
}
} else if (this.step === 2) {
if (this.formData.address.length === 0 || this.formData.postcode.length === 0 || this.formData.phone.length === 0 || this.formData.receipt.length === 0) {
this.isError = true;
this.errorMsg = ERROR_MSG[0]
} else {
if (this.validateZipCode(this.formData.postcode) == false) {
this.isError = true;
this.errorMsg = ERROR_MSG[1]
return false;
}
if (this.validatePhone(this.formData.phone) == false) {
this.isError = true;
this.errorMsg = ERROR_MSG[2]
return false;
}
this.isError = false;
}
} else if (this.step === 3) {
if (this.formData.date.length === 0) {
this.isError = true;
this.errorMsg = ERROR_MSG[3]
} else {
this.isError = false
}
} else if (this.step === 4) {
// ajax submit your form here
this.isLoading = true;
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: JSON.stringify(this.formData),
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
})
.then(() => {
this.isLoading = false
})
.catch(() => {
this.isError = true;
console.log('something wrong');
return false;
});
} else if (this.step === this.maxStep) {
this.resetData();
return false;
}
if (this.isError == false) {
this.step++;
}
},
previewFile() {
this.isLoading = true;
let file = this.$refs.myFile.files[0];
if (!file || file.type.indexOf('image/') === -1) return;
this.receiptPreview = null;
let reader = new FileReader();
reader.onload = e => {
this.receiptPreview = e.target.result;
}
reader.readAsDataURL(file);
this.isLoading = false;
},
resetData() {
this.errorMsg = '';
this.step = 1;
this.formData = {
recipient: '',
address: '',
postcode: '',
phone: '',
receipt: '',
date: ''
}
this.receiptPreview = null;
},
validateZipCode(str) {
/* support for
* 43000
*/
return /^\d{5}?$/.test(str);
},
validatePhone(str) {
/* support for
* (123) 456-7890
* (123)456-7890
* 123-456-7890
* 1234567890
*/
return /^\(?(\d{3})\)?[- ]?(\d{3})[- ]?(\d{4})$/.test(str)
},
/******
* Handling the button's text
**/
handleStep(step) {
switch (step) {
case 1:
case 2:
case 3:
this.primaryBtnText = BTN_TEXT[1]
break;
case 4:
this.primaryBtnText = BTN_TEXT[2]
break;
case this.maxStep:
this.primaryBtnText = BTN_TEXT[3]
break;
}
}
}))
})
</script>
- Line 2 – 7: An array BTN_TEXT contains primary & secondary button’s text for different scenarios.
- Line 8 – 13: An array ERROR_MSG contains multiple error message for different error scenarios.
- Line 14 – 17: An array SUCCESS_MSG contains multiple success message for successful scenarios.
- Line 19: Initialize Alpine.js component, and provide reactive data “appData” for that component to reference.
- Line 41 & 45: Function that handles click for next & back button.
- Line 106: Function that handles the upload image thumbnail preview interactivity.
- Line 121: Function that reset everything back to original state.
- Line 134: Function that handles ZIP code validation.
- Line 140: Function that handles Phone number validation.
- Line 152: Function that handles switching primary button’s text according to the scenario, example: displaying “Return to Homepage” when users at the end of the checkout process.
3. Creating UI
Next, place following HTML code into the <main> tag:
<form class="card px-3 py-4 rounded-4 border-0" x-data="appData"
x-init="$watch('step', step => handleStep(step))" @submit.prevent="handleClick">
<div class="progress mb-3">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar"
aria-label="Basic example" :style="'width: '+ parseInt(step / maxStep * 100) +'%'" aria-valuemin="0"
aria-valuemax="100"></div>
</div>
<div id="step1" x-show="step === 1">
<h1 class="h5 mb-3">Delivery Details</h1>
<div class="mb-4">
<label for="recipient" class="mb-2">Your Name</label>
<input type="text" class="form-control" id="recipient" x-model="formData.recipient" />
</div>
</div>
<div id="step2" x-show="step === 2">
<h1 class="h5 mb-3">Confirmed Delivery Product</h1>
<div class="d-flex mb-3">
<img src="http://bitly.ws/RM6t" class="img-thumbnail" width="100">
<p class="mx-3 h6">Samsung Smart TV 42"</p>
</div>
<div>
<div class="mb-3">
<label for="address" class="form-label">Address</label>
<input type="text" class="form-control" id="address" x-model="formData.address">
</div>
<div class="mb-3">
<label for="postcode" class="form-label">Postcode (ex: 13000)</label>
<input type="text" pattern="\d{5}" maxlength="5" class="form-control" id="postcode"
placeholder="4****" x-model="formData.postcode">
</div>
<div class="mb-3">
<label for="phone" class="form-label">Mobile Phone Number (ex: 012-456-7890)</label>
<input type="tel" class="form-control" id="phone" x-model="formData.phone"
placeholder="01*-*******">
</div>
<div class="mb-3">
<label for="formFile" class="form-label">Receipt</label>
<input type="file" id="imgSelect" accept="image/*" x-ref="myFile" @change="previewFile"
x-model="formData.receipt">
<template x-if="receiptPreview">
<img :src="receiptPreview" class="imgPreview" width="100">
</template>
</div>
</div>
</div>
<div id="step3" x-show="step === 3">
<div>
<div class="mb-3">
<label for="date" class="form-label">Select Preferred Delivery Date</label>
<input type="date" id="date" name="date" class="form-control" id="date"
pattern="\d{4}-\d{2}-\d{2}" x-model="formData.date">
</div>
</div>
</div>
<div id="step4" x-show="step === 4">
<div>
<div class="mb-3">
<label for="name" class="form-label">Recipient Name</label>
<div class="h6" x-text="formData.recipient"></div>
</div>
<div class="mb-3">
<label for="address" class="form-label">Address</label>
<div class="h6" x-text="formData.address"></div>
</div>
<div class="mb-3">
<label for="postcode" class="form-label">Postcode</label>
<div class="h6" x-text="formData.postcode"></div>
</div>
<div class="mb-3">
<label for="phone" class="form-label">Mobile Phone Number</label>
<div class="h6" x-text="formData.phone"></div>
</div>
<div class="mb-3">
<label for="formFile" class="form-label">Receipt</label>
<div class="d-flex mb-3">
<template x-if="receiptPreview">
<img :src="receiptPreview" class="imgPreview" width="100">
</template>
<p class="mx-3 h6" x-text="formData.receipt"></p>
</div>
</div>
<div class="mb-3">
<label for="phone" class="form-label">Preferred Delivery Date (dd/mm/yy)</label>
<div class="h6" x-text="formData.date"></div>
</div>
</div>
</div>
<div id="step5" x-show="step === 5">
<h1 class="h5 mb-3" x-text="successMsgTitle"></h1>
<div class="mb-3" x-text="successMsg"></div>
</div>
<div x-show="isError" class="mb-4 text-danger" x-text="errorMsg"></div>
<button class="btn btn-primary w-100 py-2 h6" type="submit" x-text="primaryBtnText"></button>
<button class="btn btn-outline-primary w-100 py-2" type="button" x-on:click="handleBackClick()"
x-show="step != minStep && step != maxStep" x-text="secondaryBtnText"></button>
<div id="loading" x-show="isLoading == true">
<div class="modal fade show" tabindex="-1" role="dialog" style="display: block;">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-body">
<p>Loading...</p>
</div>
</div>
</div>
</div>
<div class="modal-backdrop fade show"></div>
</div>
</form>
- Line 1: Provide reactive data “appData” that we defined earlier via x-data directive; Hook the form component into the initialization phase via x-init directive; To listen for a form submission but prevent the browser from submitting a form request using @submit.prevent event.
- Line 6: Bind styles for progress bar via x-bind directive shorthand syntax – “:style“.
- Line 9, 16, 47, 56, 89, 98: Display different form elements for different steps on screen via x-show directive.
- Line 13, 25, 29, 34, 40, 52: Bind the value of an input element to Alpine data via x-model directive.
- Line 60, 64, 68, 72, 80, 85: Sets the text content of an element to the result of a given expression via x-text directive.
Done!
