Vue Form Component
Set of components to handle your online form interaction.
You'll find most of the instructions below, but to have a more in depth understanding of how this set of components works, you might want to watch Advanced VueJs Form Component series, which shows how it's been built step by step and explains all design decisions.
Installation
npm i @ssdcode/vue-form --save-dev
Dependencies
This package makes use of the following packages
vue
@ssdcode/cms-partials
@ssdcode/vue-event-bus
@ssdcode/vue-focus-directive
which you will have to import and register
import Vue from 'vue'
import EventBus from '@ssdcode/vue-event-bus';
import { FocusDirective } from '@ssdcode/vue-focus-directive';
window.EventBus = window.EventBus || new EventBus;
import VueForm from '@ssdcode/vue-form';
Lastly you can register it with your vue instance
const app = new Vue({
el: '#app',
directives: {
FocusDirective
},
components: {
// ...
...VueForm
}
});
Styles
Package comes with 2 scss
files to style your components, but you can use your own styling if you've decided to use a different structure for form-validation
, top-dialog
. I recommend to check these files in case you need some of the styling for your particular case.
src/styles/components/_form.scss
src/styles/components/_top-dialog.scss
Main components
Top dialog
Include <top-dialog>
just before the closing div with id #app
(or whichever one serves as your app's wrapper)
<div id="app">
// ... page elements
<top-dialog></top-dialog>
</div>
Form wrapper
<form-wrapper
group="update-form"
action="/"
v-slot:default="{
group,
fields,
error,
isDisabled,
processing,
reset,
clear,
disableEvent,
enableEvent
}"
v-cloak
>
//... form inputs and buttons go here
</form-wrapper>
or if you are providing support for older browsers that do not support object destructuring:
<form-wrapper
group="update-form"
action="/"
v-slot:default="props"
v-cloak
>
//... form inputs and buttons go here
</form-wrapper>
Please make sure that in this case you will have to prepend props.*
to all properties you'll use within the rest of this documentation i.e. v-model="props.fields.last_name"
as opposed to just v-model="fields.last_name"
Input components
The package comes with component for most commonly used input types.
Most of them can render validation errors by just passing :validation
and :error
props.
To overwrite the way the validation is rendered, you can pass it between the opening and closing tag of the given input component.
Text input
Please note :group="group"
represents group associated with the form-wrapper
, hence after destructuring slot scope you can pass it in dynamically
<text-input
:group="group"
label="Last name: *"
name="last_name"
v-model="fields.last_name"
maxlength="30"
autocomplete="family-name"
:validation="{
'required': 'Please provide your last name',
'min:2': 'Minimum length 2 chars.',
'max:30': 'Maximum length 30 chars.',
}"
:error="error"
:disabled="isDisabled"
></text-input>
Email input
<email-input
:group="group"
label="Email: *"
name="email"
v-model="fields.email"
:validation="{
'required': 'Please provide a valid email address',
'email': 'Invalid email address'
}"
:error="error"
:disabled="isDisabled"
></email-input>
Password input
<password-input
:group="group"
name="password"
label="Password: *"
v-model="fields.password"
autocomplete="off"
:validation="{
'required': 'Please provide your password',
'password': 'Invalid password format',
}"
:error="error"
:disabled="isDisabled"
></password-input>
input
types
Other In addition to the three input components above there are also the following available:
date-input
time-input
datetime-input
hidden-input
number-input
float-input
Radio input
Because radio button renders multiple inputs, but expects a single value, validation needs to be rendered outside of the component.
When rendering validation outside of the component, you only need to pass the :validation
rules (without associated messages) in the form of the array - as shown in the example below.
<form-validation
id="city"
name="city"
:show="error.has('city')"
:validation="{
'required': 'Please select your city',
'in': 'Please select your city',
}"
:error="error"
css-class="block"
></form-validation>
<radio-input
:group="group"
name="city"
:options="[
{
name: 'London',
value: 1,
},
{
name: 'Paris',
value: 2,
},
{
name: 'Madrid',
value: 3,
},
{
name: 'Amsterdam',
value: 6,
}
]"
v-model="fields.city"
:validation="['required', 'in:1,2,3,6']"
:error="error"
:disabled="isDisabled"
></radio-input>
Single checkbox
You can specify what value should be returned for checked
and unchecked
status of the checkbox using true-value
(when checked) and false-value
(otherwise). The current-value
property allows you to specify value of the input when the form first loads - allowing you to have it automatically checked if it is the same as true-value
.
If you omit true/false-value
properties, by default boolean true/false
will be used.
<checkbox-input
:group="group"
name="share"
label="Please share my data with third party"
v-model="fields.share"
:validation="{
'required': 'Invalid selection',
'in:a': 'We have to share your data'
}"
:error="error"
current-value="a"
true-value="a"
false-value="b"
validation-css-class="block"
:disabled="isDisabled"
></checkbox-input>
If you would like to exclude the unchecked field from the list of inputs when sending the request, use the remove-when-false
property and set it to true
:
<checkbox-input
:group="group"
name="privacy"
label="I agree with the privacy policy"
v-model="fields.privacy"
:validation="{
'required': 'You have to agree with our privacy policy'
}"
:error="error"
:remove-when-false="true"
validation-css-class="block"
:disabled="isDisabled"
></checkbox-input>
Group of checkboxes (produces array)
This input produces array consisting of all checked values i.e. ['blue', 'green', 'orange']
.
Again, because it is a multi select, validation is being rendered outside of the input component.
<form-validation
id="colours"
name="colours"
:show="error.has('colours')"
:validation="{
'required': 'Please select exactly 2 items',
'min': 'Please select exactly 2 items',
'max': 'Please select exactly 2 items'
}"
:error="error"
css-class="block"
></form-validation>
<div class="checkbox-group">
<checkbox-group-input
:group="group"
name="colours"
:current-value="['blue']"
:options="[
{
name: 'Blue',
value: 'blue',
},
{
name: 'Green',
value: 'green',
},
{
name: 'Orange',
value: 'orange',
}
]"
v-model="fields.colours"
:validation="['required', 'min:2', 'max:2']"
:error="error"
:disabled="isDisabled"
></checkbox-group-input>
</div>
Single select
<single-select
:group="group"
name="title"
label="Title: *"
v-model="fields.title"
:options="[
{
name: 'Mr',
value: 1,
},
{
name: 'Mrs',
value: 2,
},
{
name: 'Ms',
value: 3,
},
{
name: 'Master',
value: 4,
},
{
name: 'Miss',
value: 5,
}
]"
placeholder="Please select one"
:focus="true"
:validation="{
'required': 'Please select your title',
'in:1,2,3,4,5': 'Invalid selection'
}"
:error="error"
:disabled="isDisabled"
></single-select>
Multi select (produces array)
With multi-select
you can specify array of items representing current-value
property to pre-select them when form first loads.
<multi-select
:group="group"
name="fruit"
v-model="fields.fruit"
:options="[
{
name: 'Apple',
value: 'apple',
},
{
name: 'Orange',
value: 'orange',
},
{
name: 'Grapefruit',
value: 'grapefruit',
},
{
name: 'Banana',
value: 'banana',
}
]"
:validation="{ 'required': 'Please select your fruits' }"
:error="error"
:current-value="['banana', 'apple']"
input-css-class="margin-bottom-0"
:disabled="isDisabled"
></multi-select>
Text area
To allow us pass validation as well as content to the text-area
component, we are using named slots with body
representing content for the input and validation
to replace default validation.
<text-area
:group="group"
name="excerpt"
label="Excerpt: *"
v-model="fields.excerpt"
:validation="{
'required': 'Please provide the excerpt',
'min:3': 'Minimum 3 characters',
'max:10': 'Maximum 10 characters',
}"
:error="error"
:disabled="isDisabled"
>
<div slot="body">Content goes here</div>
</text-area>
or with both, body
and validation
<text-area
:group="group"
name="excerpt"
label="Excerpt: *"
v-model="fields.excerpt"
:validation="['required', 'min:3', 'max:10']"
:error="error"
:disabled="isDisabled"
>
<template v-slot:body>Content goes here</template>
<template v-slot:validation>
<form-validation
id="colours"
name="colours"
:show="error.has('colours')"
:validation="{
'required': 'Please provide the excerpt',
'min': 'Minimum 3 characters',
'max': 'Maximum 10 characters',
}"
:error="error"
css-class="block"
></form-validation>
</template>
</text-area>
CKEDITOR
The wysiwyg-editor
component will render CKEditor window.
For this component to work, you have to make sure that the instance of CKEIDTOR
is globally available i.e. import it in the header of your document <script src="//cdn.ckeditor.com/4.11.2/full/ckeditor.js"></script>
.
The same way as text-area
, you can pass the content and validation using named slots body
and validation
.
Make sure that the value you pass to body
slot is encoded i.e. using htmlentities
.
This component takes additional 2 properties components-css
, which is the path pointing to the css file that should be styling wysiwyg content and general config
to pass any additional CKEDITOR specific configuration options. For more information on config
see CKEDITOR_config.
<wysiwyg-editor
:group="group"
name="body"
label="Body: *"
v-model="fields.body"
:validation="{
'required': 'Please provide the body'
}"
:error="error"
contents-css="./assets/editor.css"
:config="{ height: '30rem' }"
:disabled="isDisabled"
>
<template v-slot:body><h1>Body</h1></template>
</wysiwyg-editor>
Switch input
// todo
Triggers
To interact with the form, you can either use buttons within the form-wrapper
<form-wrapper
group="update-form"
action="/"
v-slot:default="{
group,
fields,
error,
isDisabled,
processing,
reset,
clear,
disableEvent,
enableEvent
}"
v-cloak
>
// ...
<button
type="submit"
class="expanded button"
v-show="!processing"
:disabled="isDisabled"
><i class="fas fa-check fa-fw"></i> SUBMIT</button>
<button
type="button"
disabled
class="button"
v-show="processing"
><i class="fas fa-spinner fa-spin fa-fw"></i> PROCESSING</button>
<button
type="button"
class="secondary button"
@click="disableEvent"
v-if="!isDisabled"
><i class="fas fa-power-off fa-fw"></i> DISABLE</button>
<button
type="button"
class="success button"
@click="enableEvent"
v-if="isDisabled"
><i class="fas fa-power-off fa-fw"></i> ENABLE</button>
<button
type="button"
class="alert button"
@click="reset"
:disabled="isDisabled"
><i class="fas fa-eraser fa-fw"></i> RESET</button>
<button
type="button"
class="warning button"
@click="clear"
:disabled="isDisabled"
><i class="fas fa-times fa-fw"></i> CLEAR</button>
</form-wrapper>
or outside of it. When using triggers outside of the form-wrapper
you have to make sure that they have group
property matching the form you're trying to bind them to.
You also need to specify what event should they fire
on click.
There are several events that a form-wrapper
is listening to:
-
submit
: submits the form -
reset
: resets the form input -
clear
: clears the form input -
disable-started
: sets isDisabled flag to true -
disable-ended
: sets isDisabled flag to false
<form-trigger
group="update-form"
fire="submit"
v-slot:default="{ isDisabled, trigger, processing }"
v-cloak
>
<span
class="expanded button"
:class="{ disabled: isDisabled }"
@click="trigger"
>
<span v-if="!processing">
<i class="fas fa-check fa-fw"></i> SUBMIT
</span>
<span v-else>
<i class="fas fa-spinner fa-spin fa-fw"></i> PROCESSING
</span>
</span>
</form-trigger>
<form-trigger
group="update-form"
fire="disable-started"
v-slot:default="{ isDisabled, trigger }"
v-cloak
>
<span
class="secondary button"
@click="trigger"
v-if="!isDisabled"
>
<i class="fas fa-power-off fa-fw"></i> DISABLE
</span>
</form-trigger>
<form-trigger
group="update-form"
fire="disable-ended"
:always-enabled="true"
v-slot:default="{ isDisabled, trigger }"
v-cloak
>
<span
class="success button"
@click="trigger"
v-if="isDisabled"
>
<i class="fas fa-power-off fa-fw"></i> ENABLE
</span>
</form-trigger>
<form-trigger
group="update-form"
fire="reset"
v-slot:default="{ isDisabled, trigger, processing }"
v-cloak
>
<span
class="alert button"
:class="{ disabled: isDisabled }"
@click="trigger"
>
<span v-if="!processing">
<i class="fas fa-eraser fa-fw"></i> RESET
</span>
<span v-else>
<i class="fas fa-spinner fa-spin fa-fw"></i> PROCESSING
</span>
</span>
</form-trigger>
<form-trigger
group="update-form"
fire="clear"
v-slot:default="{ isDisabled, trigger, processing }"
v-cloak
>
<span
class="warning button"
:class="{ disabled: isDisabled }"
@click="trigger"
>
<span v-if="!processing">
<i class="fas fa-times fa-fw"></i> CLEAR
</span>
<span v-else>
<i class="fas fa-spinner fa-spin fa-fw"></i> PROCESSING
</span>
</span>
</form-trigger>
Validation
// todo
Top Dialog
// todo
Master checkbox
// todo
Depending dropdowns
// todo
Additional attribute bindings
If you'd like to add some additional attribute bindings to your form input you can use :input-bindings
attribute.
Example would be adding step
, min
and max
attributes to the number-input
:
<number-input
:group="group"
label="Price: *"
name="price"
v-model="fields.price"
:validation="{
'required': 'Please provide the price',
}"
:error="error"
:disabled="isDisabled"
current-value="20.22"
:input-bindings="{ step: '.01', min: 0, max: 1000.00 }"
></number-input>
Mutating values for the request
Sometimes you might want to have a value represented in one format, but send it in another.
Example of such situation could be the price
where input uses float
format, but server expects integer
i.e. pounds
vs pence
/ dollars
vs cents
.
For this sort of scenarios you can use mutators
attribute on the form-wrapper
in the form of the object with keys representing the field to mutate and value - callback function to mutate the entered value.
<form-wrapper
group="update-form"
action="/"
behaviour="redirect"
v-slot:default="{
group,
fields,
error,
isDisabled,
processing,
reset,
clear,
disableEvent,
enableEvent
}"
:mutators="{ price: value => parseInt(value.replace('.', '')) }"
v-cloak
>
//...
<float-input
:group="group"
name="price"
label="Price: *"
v-model="fields.price"
current-value="2257"
:validation="{
'required': 'Please provide the price',
'integer': 'Invalid price format',
}"
:error="error"
:disabled="isDisabled"
></float-input>
//...
</form-wrapper>
The above form will send the request with value for price
index an integer of 2257
.
Please note that current-value
is also provided as integer
type - this will automatically be converted to a float
when the component is created and allows to pass the value directly from the database without necessity of manually converting it to the decimal point.
Float input has :decimals
property that determines the number of decimal places, for example :decimals="3"
would display 3 digits after the decimal point.
Bouncing
If you need the field to update with the values of another field when empty, you can use :bounce-of
property:
<text-input
:group="group"
label="Contact card: *"
name="contact_card"
v-model="fields.contact_card"
:validation="{
required: 'Please provide your contact card',
}"
:error="error"
:disabled="isDisabled"
:bounce-of="(fields.first_name + ' ' + fields.last_name + ' <' + fields.email + '>')"
></text-input>