Skip to content

Instantly share code, notes, and snippets.

@mcaskill
Last active June 5, 2023 12:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mcaskill/c9cd47a5b98cfc50ce2e20c1aaf3d691 to your computer and use it in GitHub Desktop.
Save mcaskill/c9cd47a5b98cfc50ce2e20c1aaf3d691 to your computer and use it in GitHub Desktop.
WP / NF: Support for custom, complex, remote field validation for Ninja Forms. See README below.
/**
* Custom Ninja Forms Field Validation
*
* This controller is used to for client-side custom (asynchronous) sanitization/validation
* of user-submitted data.
*/
( function ( $, Backbone, Marionette, undefined ) {
'use strict';
const DEBUG_CONTROLLER = false;
const DEBUG_BLOCK_LAST_ACTION = false;
/**
* @var {string} VALIDATING_FIELD - The Ninja Forms error code for a field in progress of validation.
* @var {string} INVALID_FIELD - The Ninja Forms error code for a validated field.
*/
const VALIDATING_FIELD = 'custom_validation_validating';
const INVALID_FIELD = 'custom_validation_field_invalid';
const nfRadio = Backbone.Radio;
const formChannel = nfRadio.channel( 'form' );
const fieldsChannel = nfRadio.channel( 'fields' );
const submitChannel = nfRadio.channel( 'submit' );
/**
* Manages the validation state of a form.
*
* @typedef {Object} FormState
*
* @property {?FormModel} model - The Ninja Forms form model.
* @property {?string} async - The name of the validation method
* in the process of asynchronous validating a field value.
* @property {?string} busy - The name of the validation method
* in the process of (synchronous) validating a field value.
* @property {?function} lastAction - A callback to retry the last form
* action attempted, either form submission or multi-part breadcrumb
* and pagination navigation.
*/
class FormState {
#model = null;
#async = null;
#busy = null;
#lastAction = null;
/**
* @param {FormModel} model
*/
constructor( model ) {
if (
! ( model instanceof Backbone.Model ) ||
! model.has( 'formContentData' )
) {
throw new TypeError(
'Form Validation expected model to be a Ninja Forms FormModel'
);
}
this.#model = model;
}
get hasAction() {
return ( this.#lastAction != null );
}
get isAsync() {
return ( this.#async != null );
}
get isBusy() {
return ( this.#busy != null );
}
get model() {
return this.#model;
}
set lastAction( callback ) {
if ( typeof callback !== 'function' ) {
throw new TypeError(
'Form Validation expected action to be a function'
);
}
this.#lastAction = callback;
}
/**
* If name matches, enable BUSY and maybe ASYNC.
*
* @param {string} name - The name of the busy method.
* @param {?boolean} [isAsync] - Whether the busy method is async.
*/
maybeBusy( name, isAsync = null ) {
if ( this.#busy != null && this.#busy !== name ) {
return;
}
this.#busy = name;
if ( typeof isAsync === 'boolean' ) {
this.#async = isAsync ? name : null;
}
}
/**
* If name matches, disable BUSY and ASYNC.
*
* @param {string} name - The name of the busy method.
* @param {boolean} [retry] - Whether to retry the last action.
*/
maybeIdle( name, retry = false ) {
if ( this.#busy !== name ) {
return;
}
if ( retry && typeof this.#lastAction === 'function' ) {
if ( DEBUG_BLOCK_LAST_ACTION ) {
DEBUG_CONTROLLER && console.log( 'Ignoring Last Action' );
} else {
DEBUG_CONTROLLER && console.log( 'Attempting Last Action' );
this.#lastAction();
}
this.#lastAction = null;
}
this.#async = null;
this.#busy = null;
}
}
/**
* Stores the validation state of all forms.
*
* @typedef {Object<FormID, FormState>} FieldValidationState
*/
const FieldValidationState = {};
const FieldValidationController = Marionette.Object.extend( {
initialize: function () {
DEBUG_CONTROLLER && console.group( 'FieldValidationController.initialize' );
this.listenTo( formChannel, 'render:view', this.onRenderView );
this.listenTo( fieldsChannel, 'change:modelValue', this.validateModelData );
this.listenTo( submitChannel, 'validate:field', this.validateModelData );
DEBUG_CONTROLLER && console.groupEnd();
},
/**
* Prepares the listeners for each form.
*
* @param {MainLayoutView} mainLayoutView
*/
onRenderView: function ( mainLayoutView ) {
DEBUG_CONTROLLER && console.group( 'FieldValidationController.onRenderView' );
DEBUG_CONTROLLER && console.log( 'MainLayoutView:', mainLayoutView );
if ( mainLayoutView?.model ) {
const formModel = mainLayoutView.model;
const formID = formModel.get( 'id' );
DEBUG_CONTROLLER && console.log( 'FormModel:', formModel );
FieldValidationState[ formID ] = new FormState( formModel );
this.listenTo( nfRadio.channel( `form-${formID}` ), 'before:submit', this.onNFBeforeSubmit );
if ( mainLayoutView?.$( '.nf-mp-body' )?.length ) {
const formContentData = formModel.get( 'formContentData' );
if ( mainLayoutView?.$el?.length ) {
mainLayoutView.$el
.on( 'click', '.nf-breadcrumb', ( event ) => this.onNFMPClickPart( event, formModel ) )
.on( 'click', '.nf-next', ( event ) => this.onNFMPClickPart( event, formModel ) )
.on( 'click', '.nf-previous', ( event ) => this.onNFMPClickPart( event, formModel ) );
}
}
}
DEBUG_CONTROLLER && console.groupEnd();
},
/**
* @param {ninja-forms:FormModel} formModel - The Ninja Forms form model.
*/
onNFBeforeSubmit: function ( formModel ) {
DEBUG_CONTROLLER && console.group( 'FieldValidationController.onNFBeforeSubmit' );
DEBUG_CONTROLLER && console.log( 'Form:', formModel );
const formID = formModel.get( 'id' );
const validitationState = formID && FieldValidationState[ formID ];
if (
validitationState &&
validitationState.isBusy &&
validitationState.isAsync
) {
DEBUG_CONTROLLER && console.log( 'Interrupted Form Submit' );
validitationState.lastAction = () => nfRadio.channel( `form-${formID}` ).request( 'submit', formModel );
}
DEBUG_CONTROLLER && console.groupEnd();
},
/**
* @param {jQuery.Event} event - The jQuery click event.
* @param {ninja-forms:FormModel} formModel - The Ninja Forms form model.
*/
onNFMPClickPart: function ( event, formModel ) {
DEBUG_CONTROLLER && console.group( 'FieldValidationController.onNFMPClickPart' );
DEBUG_CONTROLLER && console.log( 'Event:', event );
DEBUG_CONTROLLER && console.log( 'Form:', formModel );
const formID = formModel.get( 'id' );
const validitationState = formID && FieldValidationState[ formID ];
if (
validitationState &&
validitationState.isBusy &&
validitationState.isAsync
) {
const target = event.target;
const formContentData = formModel.get( 'formContentData' );
if ( target?.classList && formContentData ) {
if ( target.classList.contains( 'nf-breadcrumb' ) && 'index' in target.dataset ) {
DEBUG_CONTROLLER && console.log( 'Interrupted Breadcrumb Part Change' );
validitationState.lastAction = () => formContentData.setElement( formContentData.getVisibleParts()[ target.dataset.index ] );
} else if ( target.classList.contains( 'nf-next' ) ) {
DEBUG_CONTROLLER && console.log( 'Interrupted Next Part Change' );
validitationState.lastAction = () => formContentData.next();
} else if ( target.classList.contains( 'nf-previous' ) ) {
DEBUG_CONTROLLER && console.log( 'Interrupted Previous Part Change' );
validitationState.lastAction = () => formContentData.previous();
}
}
}
DEBUG_CONTROLLER && console.groupEnd();
},
/**
* Validate field value.
*
* @param {mixed} value
* @param {NFFieldModel} fieldModel
*/
validateField: function ( value, fieldModel ) {
DEBUG_CONTROLLER && console.group( 'FieldValidationController.validateField' );
DEBUG_CONTROLLER && console.log( 'Field:', fieldModel );
const formID = fieldModel.get( 'formID' );
const validitationState = formID && FieldValidationState[ formID ];
if (
validitationState &&
validitationState.isBusy
) {
DEBUG_CONTROLLER && console.log( `${fieldModel.get( 'label' )}:`, 'Skipping; Busy' );
DEBUG_CONTROLLER && console.groupEnd();
return;
}
const last_value = fieldModel.get( 'last_value' );
if ( last_value != null && value === last_value ) {
DEBUG_CONTROLLER && console.log( `${fieldModel.get( 'label' )}:`, 'Skipping; Same Value' );
DEBUG_CONTROLLER && console.groupEnd();
return;
}
if ( value === '' && fieldModel.get( 'required' ) ) {
fieldsChannel.request(
'remove:error',
fieldModel.get( 'id' ),
INVALID_FIELD
);
DEBUG_CONTROLLER && console.log( `${fieldModel.get( 'label' )}:`, 'Skipping; Empty Value' );
DEBUG_CONTROLLER && console.groupEnd();
return;
}
fieldModel.set( 'last_value', value );
const validator = fieldModel.get( 'custom_validation' );
if ( validator ) {
const validator_fn = `validate_${validator}_field`;
if ( 'function' === typeof FieldValidators[ validator_fn ] ) {
fieldsChannel.request(
'remove:error',
fieldModel.get( 'id' ),
INVALID_FIELD
);
FieldValidators[ validator_fn ]( value, fieldModel );
DEBUG_CONTROLLER && console.groupEnd();
return;
}
}
DEBUG_CONTROLLER && console.log( `${fieldModel.get( 'label' )}:`, 'Not Validatable' );
DEBUG_CONTROLLER && console.groupEnd();
},
/**
* Validate field model data.
*
* @param {NFFieldModel} fieldModel
*/
validateModelData: function ( fieldModel ) {
DEBUG_CONTROLLER && console.group( 'FieldValidationController.validateModelData' );
if ( ! fieldModel.get( 'custom_validation' ) || ! fieldModel.get( 'visible' ) || fieldModel.get( 'clean' ) ) {
DEBUG_CONTROLLER && console.log( `${fieldModel.get( 'label' )}:`, 'Not Validatable' );
DEBUG_CONTROLLER && console.groupEnd();
return;
}
const value = fieldModel.get( 'value' );
this.validateField( value, fieldModel );
DEBUG_CONTROLLER && console.groupEnd();
},
} );
const FieldValidators = {
/**
* Returns an error message from the given error constant.
*
* @param {?list<string>|object<string, mixed>} [codes] - The error codes to retrieve.
* If NULL, return all messages.
* @param {object<string, mixed>} formSettings - The form settings.
* @return {object<string, string>} - A plain object of messages keyed
* by their error constant values.
*/
get_error_messages: function ( codes, formSettings ) {
if ( codes == null ) {
return {};
}
if (
formSettings == null &&
typeof formSettings !== 'object'
) {
return {};
}
if ( codes == null ) {
return formSettings;
}
const filtered = {};
if ( Array.isArray( codes ) ) {
for ( const code of codes ) {
if ( code in formSettings ) {
filtered[ code ] = formSettings[ code ];
}
}
} else if ( typeof codes === 'object' ) {
for ( const code in codes ) {
const details = codes[ code ];
if (
null == details ||
false === details ||
! ( code in formSettings )
) {
continue;
}
filtered[ code ] = this.format_message(
messages[ code ],
details
);
}
}
return filtered;
},
/**
* Returns a map of error strings from the given error
* constants.
*
* @param {string} code - The error code.
* @param {object} details - The error details.
* @return {string} - An error message.
*/
format_message: function ( message, details ) {
if (
null == details ||
typeof details !== 'object'
) {
return message;
}
for ( const pattern in details ) {
let replacement = details[ pattern ];
if ( Array.isArray( replacement ) ) {
replacement = replacement.join();
}
message = message.replace( pattern, replacement );
}
return message;
}
/**
* Performs the expensive/remote validation.
*
* @async
* @param {mixed} value
* @param {NFFieldModel} fieldModel
* @param {object<string, bool>} [options]
* @return {Promise<boolean>}
*/
validate_example_field: async function ( value, fieldModel, options = {} ) {
DEBUG_CONTROLLER && console.group( 'FieldValidators.validate_example_field' );
const formID = fieldModel.get('formID');
const validitationState = formID && FieldValidationState[ formID ];
validitationState?.maybeBusy( 'validate_example_field', true );
const formSettings = validitationState?.model?.get('settings');
fieldsChannel.request(
'add:error',
fieldModel.get('id'),
VALIDATING_FIELD,
formSettings[ VALIDATING_FIELD ]
);
const results = {};
/** EXPENSIVE/REMOTE VALIDATION HERE */
const valid = await fetch( 'https://example.com/', { body: value } );
fieldsChannel.request(
'remove:error',
fieldModel.get( 'id' ),
VALIDATING_FIELD
);
if ( valid ) {
if ( DEBUG_CONTROLLER ) {
if ( Object.keys( results ).length ) {
console.log( 'Valid:', results );
} else {
console.log( 'Valid' );
}
DEBUG_CONTROLLER && console.groupEnd();
}
validitationState?.maybeIdle( 'validate_example_field', valid );
return true;
}
const hasResults = ( Object.keys( results ).length > 0 );
if ( DEBUG_CONTROLLER ) {
if ( hasResults ) {
console.log( 'Invalid:', results );
} else {
console.log( 'Invalid' );
}
}
const messages = this.get_error_messages( results, formSettings );
const message = (
Object.values( messages )[0] ??
formSettings[ INVALID_FIELD ]
);
fieldsChannel.request(
'add:error',
fieldModel.get('id'),
INVALID_FIELD,
message
);
DEBUG_CONTROLLER && console.groupEnd();
validitationState?.maybeIdle( 'validate_example_field', valid );
return false;
}
};
if ( DEBUG_CONTROLLER ) {
if ( DEBUG_BLOCK_LAST_ACTION ) {
console.warn( 'Debugging Custom Form Validation; Disabled Last Action Retry' );
} else {
console.warn( 'Debugging Custom Form Validation' );
}
window.NFFieldValidationController = FieldValidationController;
window.NFFieldValidationState = FieldValidationState;
}
$( function () {
new FieldValidationController();
} );
} )( jQuery, Backbone, Marionette );
<?php
/**
* Custom Ninja Forms Field Validation
*
* This controller is used to for server-side custom sanitization/validation of
* user-submitted data.
*
* @psalm-type NFFieldError array{slug: string, message: string}|string
*/
class FieldValidationController
{
public const VALIDATING_FIELD = 'custom_validation_validating';
public const INVALID_FIELD = 'custom_validation_field_invalid';
public function boot() : void
{
add_filter( 'ninja_forms/custom_validation/submit_data/validate_field', [ $this, 'filter_field_validation' ], 10, 3 );
add_filter( 'ninja_forms_display_form_settings', [ $this, 'filter_ninja_forms_display_form_settings' ], 10, 2 );
add_filter( 'ninja_forms_field_settings', [ $this, 'filter_ninja_forms_field_settings' ] );
add_filter( 'ninja_forms_field_load_settings', [ $this, 'filter_ninja_forms_field_load_settings' ], 10, 3 );
add_filter( 'ninja_forms_form_display_settings', [ $this, 'filter_ninja_forms_form_display_settings' ] );
add_filter( 'ninja_forms_submit_data', [ $this, 'filter_ninja_forms_submit_data' ] );
add_action( 'init', [ $this, 'action_init' ] );
add_action( 'nf_display_enqueue_scripts', [ $this, 'enqueue_front_assets' ] );
}
/**
* @listens action:init
*/
public function action_init() : void {
$this->register_front_assets();
}
/**
* Enqueues the front-end styles and scripts.
*
* @listens action:nf_display_enqueue_scripts
*/
public function enqueue_front_assets() : void {
wp_enqueue_script( 'nf-custom-validation' );
}
/**
* Adds custom field settings to the pool of all field settings in Ninja Forms.
*
* @listens filter:ninja_forms_field_settings
*
* @param array<string, array<string, mixed>> $settings The field settings.
* @return array<string, array<string, mixed>>
*/
public function filter_ninja_forms_field_settings( array $settings ) : array {
return $settings + $this->get_validation_field_settings();
}
/**
* Adds custom field settings to a given field in Ninja Forms.
*
* @listens filter:ninja_forms_field_load_settings
*
* @param array<string, array<string, mixed>> $settings The field settings.
* @param string $field_type The field type.
* @param string $parent_type The parent field type.
* @return array<string, array<string, mixed>>
*/
public function filter_ninja_forms_field_load_settings( array $settings, string $field_type, string $parent_type ) : array {
if ( ! in_array( $field_type, [ 'textbox' ], true ) ) {
return $settings;
}
return $settings + $this->get_validation_field_settings();
}
/**
* Adds custom labeling settings to the pool of all form settings in Ninja Forms.
*
* @listens filter:ninja_forms_form_display_settings
*
* @param array<string, array<string, mixed>> $settings The form settings.
* @return array<string, array<string, mixed>>
*/
public function filter_ninja_forms_form_display_settings( array $settings ) : array {
return $settings + $this->get_validation_form_settings();
}
/**
* Adds any custom labels to form settings in Ninja Forms.
*
* @listens filter:ninja_forms_display_form_settings
*
* @param array<string, array<string, mixed>> $settings The form settings.
* @param int|string $form_id The form ID.
* @return array<string, array<string, mixed>>
*/
public function filter_ninja_forms_display_form_settings( array $settings, $form_id ) : array {
foreach ( $this->retrieve_error_messages() as $name => $label ) {
if ( empty( $settings[ $name ] ) ) {
$settings[ $name ] = $label;
}
}
return $settings;
}
/**
* Adds a field validation hooks for submitted data in Ninja Forms.
*
* Portions of this method are copied from {@see \NF_AJAX_Controllers_Submission::process()}
* and {@see \NF_AJAX_Controllers_Submission::submit()} since there no convenient field hooks
* nor proper handling of form validation clean-up (renewing the nonce).
*
* The extra hook is needed to avoid duplicating the iteration and avoid multiple
* iterations of the fields.
*
* @listens filter:ninja_forms_submit_data
* @fires filter:ninja_forms/custom_validation/submit_data/validate_field
*
* @param array<string, array<string, mixed>> $form_data The form submission data.
* @return array<string, array<string, mixed>>
*/
public function filter_ninja_forms_submit_data( array $form_data ) : array {
if ( empty( $form_data['id'] ) || ! is_numeric( $form_data['id'] ) ) {
return $form_data;
}
// $form = Ninja_Forms()->form( $form_data['id'] )->get();
$form_fields = Ninja_Forms()->form( $form_data['id'] )->get_fields();
if ( ! is_iterable( $form_fields ) ) {
return $form_data;
}
foreach ( $form_fields as $field_id => $field ) {
if ( is_object( $field ) ) {
$field_data = [
'id' => $field->get_id(),
'settings' => $field->get_settings(),
];
}
$field_data['settings']['id'] = $field_id;
$field_data['settings']['value'] = ( $form_data['fields'][ $field_id ]['value'] ?? '' );
$field_data = array_merge( $field_data, $field_data['settings'] );
/**
* Filters the field validation error array.
*
* @event filter:ninja_forms/custom_validation/submit_data/validate_field
*
* @param NFFieldError|null $field_errors The field errors.
* @param array<string, mixed> $field_data The field settings.
* @param array<string, mixed> $form_data The form submission data.
*/
$field_errors = apply_filters( 'ninja_forms/custom_validation/submit_data/validate_field', null, $field_data, $form_data );
if ( $field_errors ) {
$form_data['errors']['fields'][ $field_id ] = $field_errors;
}
}
return $form_data;
}
/**
* Validates the submitted field value against any custom field settings in Ninja Forms.
*
* @listens filter:ninja_forms/custom_validation/submit_data/validate_field
*
* @param NFFieldError|null $field_errors The field errors.
* @param array<string, mixed> $field_data The field settings.
* @param array<string, mixed> $form_data The form submission data.
* @return NFFieldError|null
*/
public function filter_field_validation( array|string|null $field_errors, array $field_data, array $form_data ) : array|string|null {
if ( empty( $field_data['settings']['custom_validation'] ) ) {
return $field_errors;
}
$validator = $field_data['settings']['custom_validation'];
$validator = "validate_{$validator}_field";
if ( ! method_exists( $this, $validator ) ) {
return $field_errors;
}
$options = [];
$errors = $this->{$validator}( $field_data, $form_data, $options );
if ( ! $errors ) {
return $field_errors;
}
return $errors;
}
/**
* Returns an associative array of error strings from the given error
* constants.
*
* @param ?(list<string>|array<string, mixed>) $codes The error codes to retrieve.
* If NULL, return all messages.
* @return array<string, string> An array of messages keyed
* by their error constant values.
*/
public function get_error_messages( ?array $codes = null ) : array {
/**
* Map of error codes and friendly messages.
*/
static $errors = null;
$errors ??= $this->retrieve_error_messages();
if ( is_null( $codes ) ) {
return $errors;
}
$messages = [];
if ( array_is_list( $codes ) ) {
foreach ( $codes as $code ) {
if ( isset( $errors[ $code ] ) ) {
$messages[ $code ] = $errors[ $code ];
}
}
} else {
foreach ( $codes as $code => $details ) {
if (
null === $details ||
false === $details ||
! isset( $errors[ $code ] )
) {
continue;
}
if ( is_array( $details ) ) {
$details = array_map(
fn ( $detail ) => ( is_array( $detail )
? implode( __( ', ' ), $detail )
: (string) $detail ),
$details
);
$messages[ $code ] = strtr( $errors[ $code ], $details );
} else {
$messages[ $code ] = $errors[ $code ];
}
}
}
return $messages;
}
/**
* Retrieves custom validation field settings to Ninja Forms.
*
* @return array<string, array<string, mixed>>
*/
public function get_validation_field_settings() : array {
return [
'custom_validation' => [
'name' => 'custom_validation',
'type' => 'select',
'label' => esc_html__( 'Input Validation', 'nf-custom-validation' ),
'width' => 'one-half',
'group' => 'restrictions',
'value' => '',
'options' => [
[
'label' => '-- ' . esc_html__( 'None', 'nf-custom-validation' ) . ' --',
'value' => '',
],
[
'label' => esc_html_x( 'Example', 'validation type', 'nf-custom-validation' ),
'value' => 'example',
],
],
],
];
}
/**
* Retrieves custom validation form settings to Ninja Forms.
*
* @return array<string, array<string, mixed>>
*/
public function get_validation_form_settings() : array {
$custom_settings = [];
foreach ( $this->retrieve_error_messages() as $name => $label ) {
$label_setting = [
'name' => $name,
'type' => 'textbox',
'label' => esc_html( $label ),
'width' => 'full',
];
$custom_settings[] = $label_setting;
}
return [
'custom_validation_messages' => [
'name' => 'custom_validation_messages',
'type' => 'fieldset',
'label' => esc_html__( 'Custom Validation Labels', 'nf-custom-validation' ),
'width' => 'full',
'group' => 'advanced',
'settings' => $custom_settings,
],
];
}
/**
* Registers the front-end styles and scripts.
*/
public function register_front_assets() : void {
wp_register_script(
'nf-custom-validation',
plugin_dir_url( __FILE__ ) . 'resources/scripts/nf-validation.js',
[
'nf-front-end-deps',
]
);
}
/**
* Returns an associative array of error strings from the given error
* constants.
*
* @return array<string, string>
*/
public function retrieve_error_messages() : array {
$messages = [
static::VALIDATING_FIELD => _x( 'Validating…', 'form validation', 'nf-custom-validation' ),
static::INVALID_FIELD => _x( 'Please enter a valid value.', 'form validation', 'nf-custom-validation' ),
];
return $messages;
}
/**
* Performs the expensive/remote validation.
*
* @param array<string, mixed> $field_data The field to validate.
* @param array<string, mixed> $form_data The form submission data.
* @param array<string, bool> $options Validation options.
* @return NFFieldError|null
*/
public function validate_example_field( array $field_data, array $form_data, array $options = [] ) : array|string|null {
/**
* EXPENSIVE/REMOTE VALIDATION DONE HERE
*
* THIS EXAMPLE IS INCOMPLETE
*/
$valid = $response = wp_remote_get( 'https://example.com/', [
'body' => $field_data['value'],
] );
if ( ! $valid ) {
if ( ! $errors ) {
$messages = $this->get_error_messages();
return [
'slug' => static::INVALID_FIELD,
'message' => $messages[ static::INVALID_FIELD ],
];
}
$messages = $this->get_error_messages( array_slice( $errors, 0, 1, true ) );
return [
'slug' => static::INVALID_FIELD,
'message' => $messages[ array_key_first( $messages ) ],
];
}
return null;
}
}

README: Custom Ninja Forms Field Validation

The accompanying PHP and JS files contain stripped-down controllers that provide support for custom validation logic along with support for asynchronous validation to handle slow/complex or remote/confirmation routines.

For the client-side, this is accomplished by adding a temporary custom_validation_validating error to interrupt any attempt to submit the form or move to another part of a multi-part form. This temporary error displays a customizable "Validating…" error message. When the custom validation is complete, if the field is invalid the temporary error is replaced with a corresponding error message.

If the user attempted to submit the form or move to the next part of form, and asynchronous validation is in progress, it will re-attempt the user's action when the asynchronous validation is complete.

Ideally, Ninja Forms would implement replace its synchronous queue of callbacks with a promise-based solution to allow for controllers to take their time with whatever needs to be processed.


THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment