Skip to main content

JavaScript Customization Guide

Form Plant can also be extended flexibly on the client side ( JavaScript ).

  • Custom validation: Add your own per-field validation through a callback API
  • Lifecycle events: Hook custom logic into each step of the form with custom events
Choosing between client-side and server-side

JavaScript validation exists purely to improve UX ( instant feedback ). Validation that involves security ( restricting to a company domain, integrating with an external API, etc. ) must always also be implemented with the Validation Hooks ( PHP ), since JavaScript can easily be bypassed with DevTools and similar tools.

Supported Scope​

Embed methodCustom validation APILifecycle events
Page ( shortcode )SupportedSupported
iframe embedNot supportedSupported ( with some limitations )
JavaScript embedNot supportedSupported ( with some limitations )

Forms placed directly on a page support all features. With iframe / JavaScript embeds, the fplant:validateField event and fplant.addValidator() are not available. See Limitations at the end of this page for details.


How to Load Custom JS​

Load your custom JS file with wp_enqueue_script(), specifying fplant-form as a dependency.

// functions.php or a plugin file
function my_fplant_custom_scripts() {
wp_enqueue_script(
'my-fplant-custom',
get_stylesheet_directory_uri() . '/js/my-fplant-custom.js',
array( 'fplant-form' ), // load after form.js
'1.0.0',
true
);
}
add_action( 'wp_enqueue_scripts', 'my_fplant_custom_scripts' );

By specifying fplant-form as a dependency, your custom JS runs only when the window.fplant API is available.


Custom Validation API​

fplant.addValidator(fieldName, callback)​

Registers a custom validator for a field.

window.fplant.addValidator('email', function (value, fieldName, formData) {
// value: current value of the field
// fieldName: field name (e.g. 'email')
// formData: data object containing all fields

if (value && !value.endsWith('@example.com')) {
return 'Only the @example.com domain is allowed'; // return an error message
}
return ''; // an empty string means validation passed
});

Arguments:

ArgumentTypeDescription
fieldNamestringThe name attribute value of the target field
callbackfunctionThe validation function

Callback function:

ArgumentTypeDescription
valuestring / Array / File / nullCurrent value of the field ( see below )
fieldNamestringField name
formDataobjectData for the whole form ( { fieldName: value, ... } )

Type of value by field type:

Field typeType of valueContents
Text / Email / URL, etc.stringInput value
TextareastringInput value
Checkboxstring[]Array of checked values
Radio buttonstring / nullSelected value, or null when nothing is selected
FileFile / nullSelected file object, or null when no file is selected

Return value:

  • Returning an error message string makes the field invalid
  • Returning an empty string or a falsy value ( '', null, undefined ) means validation passed

fplant.removeValidator(fieldName, callback)​

Removes a registered validator.

// Remove a specific validator
var myValidator = function (value) { /* ... */ };
fplant.addValidator('email', myValidator);
fplant.removeValidator('email', myValidator);

// Remove all validators for a field ( omit callback )
fplant.removeValidator('email');

Example: Cross-Field Validation​

// Check that the password confirmation matches
fplant.addValidator('password_confirm', function (value, fieldName, formData) {
if (value && formData.password && value !== formData.password) {
return 'The passwords do not match';
}
return '';
});

Example: Conditionally Required Check​

// Require the detail field only when "Other" is selected
fplant.addValidator('other_detail', function (value, fieldName, formData) {
if (formData.category === 'other' && (!value || value.trim() === '')) {
return 'Please enter details when "Other" is selected';
}
return '';
});

Example: Character Limit​

fplant.addValidator('message', function (value) {
if (value && value.length > 500) {
return 'The message must be 500 characters or fewer ( currently: ' + value.length + ' characters )';
}
return '';
});

fplant.validateField(formId, fieldName)​

Runs the client-side validation of one field ( added in v1.5.2 ) . It is exactly what happens when the field loses focus: the required check, the format check and every validator registered with addValidator() all run, and the field's error message is shown or cleared as usual.

Return valueMeaning
trueThe value is valid
falseThere is an error ( its message is now on screen )
nullNo form with that ID was found
// Validate the first three fields in place when a "Next" button is clicked
document.getElementById('my-next-button').addEventListener('click', function () {
var ok = ['your_name', 'email', 'tel'].every(function (name) {
return fplant.validateField('12', name) === true;
});
if (ok) {
// move on to the next step
}
});
Browser-side validation only

validateField() does not contact the server, so server-side checks ( a duplicate email address, for example ) are not included. The submission result is still the final word.


Field value API​

Scripts outside the form can read the normalized value of any field — the same shape the server receives ( added in v1.4.0 ).

fplant.getFieldValue(formId, fieldName)​

Returns the current value of a field. Composite fields such as date ( dropdown ) or name ( parts ) return the combined value ( e.g. "2026-07-19", "Taro Yamada" ). Checkboxes return an array. Returns null when the form or field is not found.

var value = fplant.getFieldValue('12', 'your_name'); // → "Taro Yamada"

fplant.getFormData(formId)​

Returns all field values as a { fieldName: value } object.

var data = fplant.getFormData('12');
// → { your_name: "Taro Yamada", email: "a@example.com", topics: ["Brochure"] }

formId can be a number or a string. Combined with the fplant:fieldChange event below, this makes it easy to react to input values ( e.g. toggling visibility ).

Structured values from extension field types​

Extension field types ( e.g. from add-ons ) may use bracketed names such as name="items[0][price]". Inputs in this format are automatically collected as nested values on submit (v1.4.0), and getFormData() returns the same structure.

fplant.getFormData('12');
// → {
// your_name: "Taro Yamada",
// items: [ { item: "Apple", price: "100" }, { item: "Orange", price: "80" } ], // items[row][subName]
// contact: { tel: "03-0000-0000", time: "Morning" } // contact[subName]
// }
Behavior as of 1.4.0
  • Passing a parent name ( e.g. items ) to getFieldValue() returns null. Use getFormData() to read structured values
  • Changing a nested sub-input does not fire fplant:fieldChange

Lifecycle Events Overview​

A CustomEvent fires at each step of the form. Register listeners on the form element ( .fplant-form ).

Form initialization ──── fplant:init
│
Value change ─────────── fplant:fieldChange
│
Input / validation ───── fplant:validateField (cancelable)
│
Submit button click
│
Validation
├── on error ──── fplant:error / fplant:validationError
│
Before submit ───────── fplant:beforeSubmit (cancelable)
│
Loading ─────────────── fplant:loading { loading: true }
│
Confirmation shown ──── fplant:confirmationShow
│ ├── go back ──── fplant:confirmationHide
│
Final submit
│
Loading ─────────────── fplant:loading { loading: false }
│
├── success ──────── fplant:success
└── error ────────── fplant:submitError

Event List​

Event namecancelableWhen it fires
fplant:initfalseWhen form initialization completes
fplant:fieldChangefalseWhen a field's value changes ( v1.4.0 )
fplant:validateFieldtrueAfter each field is validated ( only when there is no error )
fplant:beforeSubmittrueAfter validation succeeds, just before the submit processing
fplant:errorfalseWhen a validation error is displayed ( only when .fplant-errors exists )
fplant:validationErrorfalseWhen a validation error is displayed ( always fires; v1.5.2 )
fplant:loadingfalseWhen the loading state changes
fplant:confirmationShowfalseWhen the confirmation screen is shown
fplant:confirmationHidefalseWhen returning from the confirmation screen to the input screen
fplant:successfalseWhen the form is submitted successfully
fplant:submitErrorfalseOn an error after the server submission

About cancelable events: For events with cancelable: true, you can interrupt the processing by calling e.preventDefault().


Details and Examples for Each Event​

fplant:init​

Fires when the form finishes initializing.

Property ( detail )TypeDescription
formIdstringForm ID
formHTMLElementForm element
document.addEventListener('fplant:init', function (e) {
console.log('Form #' + e.detail.formId + ' has been initialized');

var form = e.detail.form;
form.querySelector('.my-custom-field')?.classList.add('initialized');
});

fplant:validateField​

Fires after each field is validated ( only when neither the built-in validation nor the custom validators produced an error ). Because it is cancelable: true, you can call preventDefault() to display your own error.

Property ( detail )TypeDescription
fieldNamestringField name
valueanyCurrent value of the field
fieldHTMLElementField element
groupHTMLElementField group element
errorMessagestring / nullError message ( settable from the listener )

Displaying a custom error with preventDefault():

var form = document.querySelector('.fplant-form');
form.addEventListener('fplant:validateField', function (e) {
if (e.detail.fieldName === 'email') {
var value = e.detail.value;
if (value && value.indexOf('@company.co.jp') === -1) {
e.detail.errorMessage = 'Please use your company email address (@company.co.jp)';
e.preventDefault(); // mark as a validation error
}
}
});
Key point

Set the error message on e.detail.errorMessage and call e.preventDefault(). Both are required. If you do not set errorMessage, the default error message is shown.

Choosing between this and addValidator():

  • addValidator() — best for simple validation; you just pass a callback function
  • fplant:validateField — convenient when you need access to DOM elements, or when the decision depends on a combination of multiple fields

fplant:fieldChange​

Fires when a field's value changes ( added in v1.4.0 ). detail.value carries the same normalized value as the field value API. Composite fields such as date ( dropdown ) or name ( parts ) fire with the combined value each time it is re-assembled.

Property ( detail )TypeDescription
formIdstringForm ID
fieldNamestringField name
valuestring / array / nullThe normalized current value ( arrays for checkboxes )
// Show a free-text field only when "Other" is selected
var form = document.querySelector('.fplant-form');
form.addEventListener('fplant:fieldChange', function (e) {
if (e.detail.fieldName !== 'category') {
return;
}
var other = form.querySelector('.fplant-field-group[data-field-name="category_other"]');
if (other) {
other.style.display = e.detail.value === 'Other' ? '' : 'none';
}
});
Event granularity

Text fields fire on blur ( change ), checkboxes / radios / selects on selection change, and composite fields on every input ( each re-assembly ). The event does not fire per keystroke.

fplant:beforeSubmit​

Fires after validation succeeds, just before the actual submit processing. Because it is cancelable: true, you can call preventDefault() to abort the submission.

Property ( detail )TypeDescription
formIdstringForm ID
formDataobjectThe form data to be submitted
// Track the start of a form submission in GA4
var form = document.querySelector('.fplant-form');
form.addEventListener('fplant:beforeSubmit', function (e) {
gtag('event', 'form_submit_start', {
form_id: e.detail.formId,
});
});
// Show a confirmation dialog before submitting
var form = document.querySelector('.fplant-form');
form.addEventListener('fplant:beforeSubmit', function (e) {
if (!confirm('Are you sure you want to submit this content ?')) {
e.preventDefault(); // abort the submission
}
});

fplant:error​

Fires when a validation error is displayed.

Property ( detail )TypeDescription
fieldErrorsobjectPer-field error messages ( { fieldName: 'error text', ... } )
messagestringThe overall error message
// Shake animation on error
var form = document.querySelector('.fplant-form');
form.addEventListener('fplant:error', function (e) {
Object.keys(e.detail.fieldErrors).forEach(function (fieldName) {
var group = document.querySelector(
'.fplant-field-group[data-field-name="' + fieldName + '"]'
);
if (group) {
group.classList.add('shake');
setTimeout(function () { group.classList.remove('shake'); }, 600);
}
});
});
@keyframes shake {
0%, 100% { transform: translateX(0); }
20%, 60% { transform: translateX(-5px); }
40%, 80% { transform: translateX(5px); }
}
.shake {
animation: shake 0.6s ease;
}

fplant:validationError​

Fires when a validation error is displayed ( added in v1.5.2 ) . Unlike fplant:error, it fires even when the form contains no [fplant_errors] ( the form-wide error list ) . A custom HTML template without [fplant_errors] never fires fplant:error, so use this event when you need to catch every validation error.

Property (detail)TypeDescription
formIdstringForm ID
errorsobjectPer-field error messages ({ fieldName: 'error text', ... })
sourcestring'client': browser-side validation / 'server': the server-side check made on the way to the confirmation screen
var form = document.querySelector('.fplant-form');
form.addEventListener('fplant:validationError', function (e) {
// Scroll to the first field that failed
var first = Object.keys(e.detail.errors)[0];
var group = document.querySelector(
'.fplant-field-group[data-field-name="' + first + '"]'
);
if (group) {
group.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
Errors returned after submission are a different event

An error returned by the server after the form was submitted is still fplant:submitError, which has always fired unconditionally.

fplant:loading​

Fires when the loading state changes. It fires twice: when the submission starts ( true ) and when it completes ( false ).

Property ( detail )TypeDescription
loadingbooleantrue: loading started, false: loading finished
// Custom loading overlay
var form = document.querySelector('.fplant-form');
form.addEventListener('fplant:loading', function (e) {
var overlay = document.getElementById('my-loading-overlay');
if (overlay) {
overlay.style.display = e.detail.loading ? 'flex' : 'none';
}
});

fplant:confirmationShow​

Fires when the confirmation screen is shown.

Property ( detail )TypeDescription
formIdstringForm ID
confirmationElHTMLElementThe confirmation screen element
// Fade-in animation for the confirmation screen
var form = document.querySelector('.fplant-form');
form.addEventListener('fplant:confirmationShow', function (e) {
var confirmation = e.detail.confirmationEl;
confirmation.style.opacity = '0';
confirmation.style.transition = 'opacity 0.3s ease';
requestAnimationFrame(function () {
confirmation.style.opacity = '1';
});
});
// Scroll to the top of the page when the confirmation screen is shown
var form = document.querySelector('.fplant-form');
form.addEventListener('fplant:confirmationShow', function (e) {
window.scrollTo({ top: 0, behavior: 'smooth' });
});

fplant:confirmationHide​

Fires when returning from the confirmation screen to the input screen.

Property ( detail )TypeDescription
formIdstringForm ID
var form = document.querySelector('.fplant-form');
form.addEventListener('fplant:confirmationHide', function (e) {
form.style.opacity = '0';
form.style.transition = 'opacity 0.3s ease';
requestAnimationFrame(function () {
form.style.opacity = '1';
});
});

fplant:success​

Fires when the form is submitted successfully. The response data from the server is included directly in detail. Since v1.4.0 it bubbles like the other events, so listeners registered on document also receive it.

Property ( detail )TypeDescription
action_typestringPost-submission action ( 'message' / 'redirect' / 'custom_page' )
messagestringSuccess message
redirect_urlstringRedirect destination URL ( when action_type is 'redirect' )
success_page_htmlstringCustom completion screen HTML ( when action_type is 'custom_page' )
// Conversion tracking in GA4
var form = document.querySelector('.fplant-form');
form.addEventListener('fplant:success', function (e) {
gtag('event', 'form_submit_success', {
action_type: e.detail.action_type,
});
});
// Run custom logic after a successful submission
var form = document.querySelector('.fplant-form');
form.addEventListener('fplant:success', function (e) {
setTimeout(function () {
window.location.href = '/thank-you/';
}, 3000);
});

fplant:submitError​

Fires when an error is returned after submitting to the server. Unlike a validation error ( fplant:error ), this is an error that occurs after communicating with the server.

Property ( detail )TypeDescription
messagestringError message
errorsobjectPer-field errors ( { fieldName: 'error text', ... } )
var form = document.querySelector('.fplant-form');
form.addEventListener('fplant:submitError', function (e) {
console.error('Form submission error:', e.detail.message);

// Send to Sentry or similar
if (typeof Sentry !== 'undefined') {
Sentry.captureMessage('Form submission error: ' + e.detail.message);
}
});

Notes​

JS Validation Is Not a Security Measure​

JavaScript validation is intended to improve UX ( instant feedback ). Perform security-related input validation with the Validation Hooks ( PHP ). Because JavaScript can easily be bypassed with DevTools and similar tools, server-side validation always runs independently.

Error Messages Are Handled Safely​

Error messages set by custom validators or fplant:validateField are displayed via textContent. HTML tags are not interpreted and are shown as plain text, so there is no XSS risk.

Error Handling in Validators​

If an exception is thrown inside a callback function registered with addValidator(), the error is automatically ignored and does not affect the form's behavior. For easier debugging during development, we recommend handling errors appropriately within your callbacks.

When to Register Event Listeners​

Except for fplant:init, event listeners can be registered at any time as long as the form's DOM exists. Registering them after DOMContentLoaded is the safe approach.

document.addEventListener('DOMContentLoaded', function () {
var form = document.querySelector('.fplant-form');
if (form) {
form.addEventListener('fplant:beforeSubmit', function (e) {
// ...
});
}
});

When There Are Multiple Forms​

When there are multiple forms on the same page, either register a listener on each form element individually, or use e.detail.formId to identify the target form. Because events fire with bubbles: true, you can also listen at the document level.

document.addEventListener('fplant:beforeSubmit', function (e) {
if (e.detail.formId === '145') {
// process only for form #145
}
});

iframe / JavaScript Embed Limitations​

With iframe embeds and JavaScript embeds ( embed.js ), the following limitations apply.

  • The fplant:validateField event does not fire ( there is no client-side validation )
  • window.fplant.addValidator() / removeValidator() are not available
  • The detail.formData of fplant:beforeSubmit is not included ( only formId )
  • The detail.confirmationEl of fplant:confirmationShow is not included ( only formId )