├── screenshot.PNG
├── webflow.html
├── LICENSE
├── dist
└── msf.js
└── README.md
/screenshot.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brotame/multi-step-form/HEAD/screenshot.PNG
--------------------------------------------------------------------------------
/webflow.html:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Alex Iglesias
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/dist/msf.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Multi Step Form functionality for Webflow
3 | * MIT License © Alex Iglesias - https://brota.me.
4 | */
5 |
6 | class MSF {
7 | constructor(data) {
8 | this.currentStep = 0;
9 | this.form = document.getElementById(data.formID);
10 | this.next = document.getElementById(data.nextButtonID);
11 | this.back = document.getElementById(data.backButtonID);
12 | this.submitButton = this.form.querySelector('input[type="submit"]');
13 | this.mask = this.form.querySelector('.w-slider-mask');
14 | this.steps = this.form.querySelectorAll('.w-slide');
15 | this.rightArrow = this.form.querySelector('.w-slider-arrow-right');
16 | this.leftArrow = this.form.querySelector('.w-slider-arrow-left');
17 | this.nextText = data.nextButtonText;
18 | this.submitText = data.submitButtonText;
19 | this.warningClass = data.warningClass;
20 | this.alertText = data.alertText;
21 | if (data.alertElementID) {
22 | this.alertElement = document.getElementById(data.alertElementID);
23 | }
24 | if (data.hiddenFormID) {
25 | this.hiddenForm = document.getElementById(data.hiddenFormID);
26 | this.hiddenSubmitButton = this.hiddenForm.querySelector(
27 | 'input[type="submit"]'
28 | );
29 | }
30 | }
31 |
32 | getInputs(index) {
33 | const inputs = this.steps[index].querySelectorAll(
34 | 'input, select, textarea'
35 | );
36 | return Array.from(inputs);
37 | }
38 |
39 | setMaskHeight() {
40 | this.mask.style.height = '';
41 | this.mask.style.height = `${this.steps[this.currentStep].offsetHeight}px`;
42 | }
43 |
44 | setNextButtonText() {
45 | if (this.currentStep === this.steps.length - 1)
46 | this.next.textContent = this.submitText;
47 | if (this.currentStep === this.steps.length - 2)
48 | this.next.textContent = this.nextText;
49 | }
50 |
51 | goNext() {
52 | this.rightArrow.click();
53 | }
54 |
55 | goBack() {
56 | this.leftArrow.click();
57 | }
58 |
59 | submitForm() {
60 | this.submitButton.click();
61 | }
62 |
63 | submitHiddenForm(index) {
64 | const inputs = this.getInputs(index);
65 |
66 | inputs.forEach((input) => {
67 | const hiddenInput = document.getElementById(`hidden-${input.id}`);
68 |
69 | if (hiddenInput) hiddenInput.value = input.value;
70 | });
71 |
72 | this.hiddenSubmitButton.click();
73 | }
74 |
75 | hideButtons() {
76 | this.next.style.display = 'none';
77 | this.back.style.display = 'none';
78 | }
79 |
80 | addWarningClass(target) {
81 | target.classList.add(this.warningClass);
82 | }
83 |
84 | removeWarningClass(target) {
85 | target.classList.remove(this.warningClass);
86 | }
87 |
88 | showAlert() {
89 | if (this.alertText) alert(this.alertText);
90 | if (this.alertElement) this.alertElement.classList.remove('hidden');
91 | }
92 |
93 | hideAlert() {
94 | if (this.alertElement) this.alertElement.classList.add('hidden');
95 | }
96 |
97 | setConfirmValues() {
98 | const inputs = this.getInputs(this.currentStep);
99 |
100 | inputs.forEach((input) => {
101 | let value, confirmElement;
102 |
103 | if (input.type === 'radio') {
104 | const radioGroup = input.getAttribute('name');
105 | const isChecked = document.querySelector(
106 | `input[name="${radioGroup}"]:checked`
107 | );
108 |
109 | if (isChecked) {
110 | value = isChecked.value;
111 | confirmElement = document.getElementById(`${radioGroup}-value`);
112 | }
113 | } else {
114 | value = input.value;
115 | confirmElement = document.getElementById(`${input.id}-value`);
116 | }
117 |
118 | if (!confirmElement) return;
119 |
120 | confirmElement.textContent = value ? value : '-';
121 | });
122 | }
123 | }
124 |
125 | const msfController = {
126 | init: (msf) => {
127 | const start = () => {
128 | setEventListeners();
129 | msf.setMaskHeight(0);
130 | };
131 |
132 | const setEventListeners = () => {
133 | msf.next.addEventListener('click', nextClick);
134 | msf.back.addEventListener('click', backClick);
135 | if (msf.hiddenForm) {
136 | msf.rightArrow.addEventListener(
137 | 'click',
138 | () => {
139 | msf.submitHiddenForm(0);
140 | },
141 | { once: true }
142 | );
143 | }
144 | };
145 |
146 | const nextClick = () => {
147 | const filledFields = checkRequiredInputs(msf.currentStep);
148 |
149 | if (!filledFields) {
150 | msf.showAlert();
151 | return;
152 | }
153 |
154 | msf.setConfirmValues();
155 | msf.currentStep++;
156 |
157 | if (msf.currentStep === msf.steps.length) {
158 | msf.submitForm();
159 | msf.hideButtons();
160 | } else {
161 | msf.goNext();
162 | msf.setMaskHeight();
163 | msf.setNextButtonText();
164 | }
165 |
166 | msf.hideAlert();
167 | };
168 |
169 | const backClick = () => {
170 | const previousStep = msf.currentStep - 1;
171 |
172 | if (previousStep >= 0) {
173 | msf.goBack();
174 | msf.currentStep = previousStep;
175 | msf.setMaskHeight();
176 | msf.setNextButtonText();
177 | msf.hideAlert();
178 | }
179 | };
180 |
181 | const checkRequiredInputs = (index) => {
182 | const inputs = msf.getInputs(index);
183 | const requiredInputs = inputs.filter((input) => input.required);
184 | const requiredCheckboxes = requiredInputs.filter(
185 | (input) => input.type === 'checkbox'
186 | );
187 | const requiredRadios = requiredInputs.filter(
188 | (input) => input.type === 'radio'
189 | );
190 | let filledInputs = 0;
191 |
192 | requiredInputs.forEach((input) => {
193 | if (!input.value) {
194 | msf.addWarningClass(input);
195 | return;
196 | }
197 |
198 | if (input.type === 'email') {
199 | const correctEmail = validateEmail(input.value);
200 | if (!correctEmail) {
201 | msf.addWarningClass(input);
202 | return;
203 | }
204 |
205 | msf.removeWarningClass(input);
206 | filledInputs++;
207 | return;
208 | }
209 |
210 | msf.removeWarningClass(input);
211 | filledInputs++;
212 | });
213 |
214 | requiredCheckboxes.forEach((input) => {
215 | const checkbox = input.parentNode.querySelector('.w-checkbox-input');
216 |
217 | if (!input.checked) {
218 | if (checkbox) msf.addWarningClass(checkbox);
219 | return;
220 | }
221 |
222 | if (checkbox) msf.removeWarningClass(checkbox);
223 | filledInputs++;
224 | });
225 |
226 | requiredRadios.forEach((input) => {
227 | const radio = input.parentNode.querySelector('.w-radio-input');
228 | const radioGroup = input.getAttribute('name');
229 | const isChecked = document.querySelector(
230 | `input[name="${radioGroup}"]:checked`
231 | );
232 |
233 | if (isChecked) {
234 | msf.removeWarningClass(radio);
235 | filledInputs++;
236 | } else {
237 | msf.addWarningClass(radio);
238 | }
239 | });
240 |
241 | return filledInputs ===
242 | requiredInputs.length +
243 | requiredCheckboxes.length +
244 | requiredRadios.length
245 | ? true
246 | : false;
247 | };
248 |
249 | const validateEmail = (email) => {
250 | const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
251 | return re.test(String(email).toLowerCase());
252 | };
253 |
254 | start();
255 | },
256 | };
257 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Multi Step Form for Webflow
2 |
3 | A custom multi step form made for Webflow websites. You can check the cloneable project [here](https://webflow.com/website/Multi-Step-Form-with-Input-Validation).
4 |
5 |
6 |
7 | # How to use it
8 |
9 | In order to make the form work as intended, you will need to:
10 |
11 | 1. [Set up some components in Webflow](#1-webflow-setup)
12 | 2. [Add the custom code](#2-custom-code)
13 |
14 | ## 1. Webflow setup
15 |
16 | If you don't want to do this manually, you will find an already built [starter form](https://brota-msf.webflow.io/starter-cloneable) in the cloneable project.
17 |
18 | ### Form and slider
19 |
20 | Place a slider inside the form that you are using. Inside each slide, you can put all the inputs you want.
21 | Make sure that:
22 |
23 | - The form has a submit button placed anywhere inside it. **Hide it** as the _Next_ button will replace its functionality.
24 | - The form has a unique ID. `I.E: #form`
25 | - The slider has the _Swipe Gestures_ and _Auto Play Slides_ options disabled.
26 |
27 | > Note: Make sure that you give the ID to the **Form** element and not to the _Form Block_ element.
28 |
29 | ### Step change buttons
30 |
31 | You can hide the slider arrows and navigation as you won't use them. Instead, place two buttons **anywhere you want** and give them a unique ID.
32 |
33 | > I.E: place a button with **#next** ID for the _Next Step_ functionality, and a button with **#back** ID for the _Previous Step_ functionality.
34 |
35 | It is recommended to hide the Back button in the first slide using Webflow interactions to avoid confusing users.
36 |
37 | ### _Optional_: Warning class
38 |
39 | When an input is not filled, the script adds a CSS class to it. You can create the CSS class using Webflow itself or via custom code. I.E:
40 |
41 | ```html
42 |
47 | ```
48 |
49 | If you want to apply the class to the _Checkboxes_ and _Radio_ inputs, make sure to set the style to **Custom** inside the element settings in the Webflow designer.
50 |
51 | ### _Optional_: Alert
52 |
53 | Aside from the warning CSS class that is applied to the inputs, you can also alert the user. You have two options:
54 |
55 | 1. Show an alert window with a message: check the [initialize script section](#initialize-the-script) for more info.
56 | 2. Display an element in the form (text block, div, image, etc). To do so, you must place the element anywhere you want and:
57 | - Give the element a unique ID. `I.E: #alert`.
58 | - Give the element a combo CSS class of **.hidden** which sets the element to _display:none_. This is necessary as the script adds or removes the **.hidden** class when the input has to be displayed or not.
59 |
60 | ### _Optional_: Inputs confirm
61 |
62 | If you want to display the value of the inputs that the user provided, you must:
63 |
64 | 1. Give the inputs that you want to display a unique ID.
65 | 2. Place a text block or a paragraph anywhere you want with the following ID:
66 | - `InputID + "-value"` for fields and checkboxes.
67 | - `GroupName + "-value"` for radio inputs.
68 |
69 | > I.E: to display the value of an input that has a **#name** ID, just place a text block with **#name-value** as ID.
70 |
71 | > I.E: to display the value of the selected radio input in the group named **variants**, just place a text block with **#variants-value** as ID.
72 |
73 | ### _Optional_: Submit an additional form on the first step
74 |
75 | You can collect the data from the 1st step into a hidden form and submit it when the user moves to the 2nd step.
76 | In order to do so, you must:
77 |
78 | 1. Place a hidden form anywhere on the page and give it a unique ID. `I.E: #hidden-form`
79 | 2. In the form, place the same inputs that you want to collect and give them the following ID: `"hidden-" + InputID`.
80 |
81 | > I.E: to collect the email field that has **#email** ID, you must place in the hidden form an email field with **#hidden-email** as ID.
82 |
83 | ## 2. Custom Code
84 |
85 | In order to make the form work, you must setup the script and initialize it:
86 |
87 | ### Setup the script
88 |
89 | Include the script tag below in the **before <\/body> tag** section of your page:
90 |
91 | ```html
92 |
93 | ```
94 |
95 | If you don't want to use CDN delivery, you can take the code inside the `/dist/msf.js` file and put it in your project.
96 |
97 | ### Initialize the script
98 |
99 | Place the script tag below in the **before <\/body> tag** section of your page after the main script.
100 |
101 | ```html
102 |
119 | ```
120 |
121 | Replace the following keys (delete the optional ones that you will be not using:
122 |
123 | | Key | Required | Description | Example |
124 | | ------------------ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
125 | | `formID` | `Yes` | The ID of the Form element. | `formID: "form"` |
126 | | `nextButtonID` | `Yes` | The ID of the Next button. | `nextButtonID: next` |
127 | | `backButtonID` | `Yes` | The ID of the Back button. | `backButtonID: "back"` |
128 | | `nextButtonText` | `Yes` | The text inside the Next button. This is required because the script changes the text of the Next button when the user reaches the last step. | `nextButtonText: "Next Step"` |
129 | | `submitButtonText` | `Yes` | The text that you want to display when the user reaches the last step. | `submitButtonText: "Submit"` |
130 | | `warningClass` | `Optional` | The CSS class that you want to add to the inputs that are not filled. | `warningClass: "warning"` |
131 | | `alertText` | `Optional` | The text that you want to show in an alert window when some inputs are not filled. | `alertText: "Please, fill all the required fields."` |
132 | | `alertElementID` | `Optional` | The element that you want to show when some inputs are not filled. | `alertElementID: "alert"` |
133 | | `hiddenFormID` | `Optional` | The ID of the Hidden Form element. | `hiddenFormID: "hidden-form"` |
134 |
135 | #### Initialize examples
136 |
137 | Form that doesn't use the hidden form functionality and shows an element when a required input is not filled:
138 |
139 | ```html
140 |
155 | ```
156 |
157 | Form that uses the hidden form functionality and shows an alert window when a required input is not filled:
158 |
159 | ```html
160 |
176 | ```
177 |
--------------------------------------------------------------------------------