From a20c2a94fb7131f2a4f4abb374cc29ed88a1d5c9 Mon Sep 17 00:00:00 2001 From: Felipe Marcos Date: Wed, 5 Nov 2025 18:55:23 -0300 Subject: [PATCH 1/3] feat: add fields custom validation --- form-actions/readme.md | 147 -------------- forms/creating-custom-actions.md | 288 ++++++++++++++++++++++++++ forms/fields-custom-validation.md | 323 ++++++++++++++++++++++++++++++ 3 files changed, 611 insertions(+), 147 deletions(-) delete mode 100644 form-actions/readme.md create mode 100644 forms/creating-custom-actions.md create mode 100644 forms/fields-custom-validation.md diff --git a/form-actions/readme.md b/form-actions/readme.md deleted file mode 100644 index 8187d3e..0000000 --- a/form-actions/readme.md +++ /dev/null @@ -1,147 +0,0 @@ -# Form Actions API - -Creating a Breakdance Form Action involves two steps - -1. Create an Action class to represent your field -2. Register the action class with Breakdance - -# Creating an action class - -To get started create a new PHP class. Your class will need to extend the Breakdance base Action class `Breakdance\Forms\Actions\Action` - -## Required Methods - -There are three mandatory methods that must be implemented in your action class - -**name** - -The name method takes no arguments and returns a string that will be used to identify your action in the Form Builder Actions dropdown menu - -```php -/** - * @return string - */ -public function name() { - return 'My Action'; -} -``` - -**slug** - -The slug method takes no arguments and returns a string to identify the form action. This should be unique across all form actions, so it is recommended to prefix the slug appropriately. - -```php -/** - * @return string -*/ -public function slug() -{ - return 'my_plugin_form_action'; -} -``` - -**run** - -The run method accepts three arguments, `$form, $settings, $extra` and is called when the form has been submitted - -```php -/** -* Log the form submission to a file -* -* @param array $form -* @param array $settings -* @param array $extra -* @return array success or error message -*/ -public function run($form, $settings, $extra) -{ - try { - $this->writeToFile($extra['formId'], $extra['fields']); - } catch(Exception $e) { - return ['type' => 'error', 'message' => $e->getMessage()]; - } - - return ['type' => 'success', 'message' => 'Submission logged to file']; -} - -``` - -### Run Arguments - -$**form** - -The form argument contains all the form fields, their builder settings and the selected values - -- type: the field type -- name: the field name -- options: an array of available options for checkbox, radio or select inputs -- value: the submitted value of the field -- originalValue: the default/original value of the field - -**$settings** - -The settings argument contains an array of the configured form settings from the Breakdance builder - -**$extra** - -The extra argument contains additional data - -- files: An array of uploaded files -- fields: the submitted form fields in an `$id ⇒ $value` style array -- formId: The ID of the form -- postId: The ID of the post the form was submitted from -- ip: the submitters IP address -- referer: The form submitters referrer URL -- userAgent: The form submitters user agent string -- userId: The form submitters user ID (if applicable) - -### Responses - -The response should be an array that contains a `type` and `message` key. - -- type: either `error` or `success` -- message: a string message that will be displayed to admins with the submission - -**Success** - -```php -public function run($form, $settings, $extra) -{ - ... - return ['type' => 'success', 'message' => 'Submission logged to file']; -} - -``` - -**Error** - -```php -public function run($form, $settings, $extra) -{ - ... - return ['type' => 'error', 'message' => 'Could not write to file']; -} -``` - -## Register The Action - -Register the action by calling the registerAction helper and passing an instance of your action class - -**Note:** To prevent file loading race conditions, it is recommended to call the register helper from inside a WordPress action, e.g init. - -```php - -// register-actions.php included by your plugin - -add_action('init', function() { - // fail if Breakdance is not installed and available - if (!function_exists('\Breakdance\Forms\Actions\registerAction') || !class_exists('\Breakdance\Forms\Actions\Action')) { - return; - } - - require_once('my-action.php'); - - \Breakdance\Forms\Actions\registerAction(new MyAction()); - -}); -``` diff --git a/forms/creating-custom-actions.md b/forms/creating-custom-actions.md new file mode 100644 index 0000000..de1fd7b --- /dev/null +++ b/forms/creating-custom-actions.md @@ -0,0 +1,288 @@ +# Form Actions API + +Creating a Breakdance Form Action involves two steps + +1. Create an Action class to represent your field +2. Register the action class with Breakdance + +# Creating an action class + +To get started create a new PHP class. Your class will need to extend the Breakdance base Action class `Breakdance\Forms\Actions\Action` + +## Required Methods + +There are three mandatory methods that must be implemented in your action class + +**name** + +The name method takes no arguments and returns a string that will be used to identify your action in the Form Builder Actions dropdown menu + +```php +/** + * @return string + */ +public function name() { + return 'My Action'; +} +``` + +**slug** + +The slug method takes no arguments and returns a string to identify the form action. This should be unique across all form actions, so it is recommended to prefix the slug appropriately. + +```php +/** + * @return string +*/ +public function slug() +{ + return 'my_plugin_form_action'; +} +``` + +**run** + +The run method accepts three arguments, `$form, $settings, $extra` and is called when the form has been submitted + +```php +/** +* Log the form submission to a file +* +* @param array $form +* @param array $settings +* @param array $extra +* @return array success or error message +*/ +public function run($form, $settings, $extra) +{ + try { + $this->writeToFile($extra['formId'], $extra['fields']); + } catch(Exception $e) { + return ['type' => 'error', 'message' => $e->getMessage()]; + } + + return ['type' => 'success', 'message' => 'Submission logged to file']; +} + +``` + +### Run Arguments + +$**form** + +The form argument contains all the form fields, their builder settings and the selected values + +- type: the field type +- name: the field name +- options: an array of available options for checkbox, radio or select inputs +- value: the submitted value of the field +- originalValue: the default/original value of the field + +**$settings** + +The settings argument contains an array of the configured form settings from the Breakdance builder + +**$extra** + +The extra argument contains additional data + +- files: An array of uploaded files +- fields: the submitted form fields in an `$id ⇒ $value` style array +- formId: The ID of the form +- postId: The ID of the post the form was submitted from +- ip: the submitters IP address +- referer: The form submitters referrer URL +- userAgent: The form submitters user agent string +- userId: The form submitters user ID (if applicable) + +### Responses + +The response should be an array that contains a `type` and `message` key. + +- type: either `error` or `success` +- message: a string message that will be displayed to admins with the submission + +**Success** + +```php +public function run($form, $settings, $extra) +{ + ... + return ['type' => 'success', 'message' => 'Submission logged to file']; +} + +``` + +**Error** + +```php +public function run($form, $settings, $extra) +{ + ... + return ['type' => 'error', 'message' => 'Could not write to file']; +} +``` + +## Register The Action + +Register the action by calling the registerAction helper and passing an instance of your action class + +**Note:** To prevent file loading race conditions, it is recommended to call the register helper from inside a WordPress action, e.g init. + +```php + +// register-actions.php included by your plugin + +add_action('init', function() { + // fail if Breakdance is not installed and available + if (!function_exists('\Breakdance\Forms\Actions\registerAction') || !class_exists('\Breakdance\Forms\Actions\Action')) { + return; + } + + require_once('my-action.php'); + + \Breakdance\Forms\Actions\registerAction(new MyAction()); +}); +``` + +## Full Example + +Here's a complete example of a custom form action that automatically publishes recipe submissions to your blog: + +```php + 'recipes']); + $category_id = is_wp_error($result) ? 1 : $result['term_id']; + } else { + $category_id = $category->term_id; + } + + // Build the post content with recipe details + $content = ''; + + // Add description if available + if (!empty($fields['description'])) { + $content .= '

' . wp_kses_post($fields['description']) . '

'; + } + + // Add ingredients section + if (!empty($fields['ingredients'])) { + $content .= '

Ingredients

'; + $content .= '
'; + $content .= wpautop(wp_kses_post($fields['ingredients'])); + $content .= '
'; + } + + // Add instructions section + if (!empty($fields['instructions'])) { + $content .= '

Instructions

'; + $content .= '
'; + $content .= wpautop(wp_kses_post($fields['instructions'])); + $content .= '
'; + } + + // Add cooking time if available + if (!empty($fields['cooking_time'])) { + $content .= '

Cooking Time: ' . esc_html($fields['cooking_time']) . '

'; + } + + // Add submitter name to content + if (!empty($fields['your_name'])) { + $content .= '

Recipe submitted by: ' . esc_html($fields['your_name']) . '

'; + } + + // Create the post + $post_id = wp_insert_post([ + 'post_type' => 'post', + 'post_title' => sanitize_text_field($fields['recipe_name']), + 'post_content' => $content, + 'post_status' => 'publish', + 'post_category' => [$category_id], + ]); + + // Check for errors + if (is_wp_error($post_id)) { + return [ + 'type' => 'error', + 'message' => 'Failed to publish recipe: ' . $post_id->get_error_message() + ]; + } + + return [ + 'type' => 'success', + 'message' => 'Thank you! Your recipe has been published.' + ]; + } +} +``` + +**Registration file (register-recipe-action.php):** + +```php + 'text|email|tel|textarea|file|etc', + 'value' => 'sanitized user submitted value', + 'originalValue' => 'original submitted value (may be array)', + 'label' => 'Field Label', + 'advanced' => [ + 'id' => 'field_id', + 'required' => true|false, + 'conditional' => true|false, + // ... other advanced settings + ], + // ... other field properties based on field type +] +``` + +## Usage Examples + +### Basic Validation + +```php +add_filter('breakdance_form_validate_field', function($fieldErrors, $field, $formId, $postId) { + // Validate phone number format + if ($field['type'] === 'phone' && !empty($field['value'])) { + if (!preg_match('/^\d{10}$/', $field['value'])) { + $fieldErrors->add( + 'invalid_phone', + 'Please enter a valid 10-digit phone number.' + ); + } + } + + return $fieldErrors; +}, 10, 4); +``` + +### Validate Specific Field by ID + +```php +add_filter('breakdance_form_validate_field', function($fieldErrors, $field, $formId, $postId) { + $fieldId = $field['advanced']['id'] ?? ''; + + // Validate a specific field + if ($fieldId === 'custom_zip_code' && !empty($field['value'])) { + if (!preg_match('/^\d{5}(-\d{4})?$/', $field['value'])) { + $fieldErrors->add( + 'invalid_zip', + 'Please enter a valid ZIP code (12345 or 12345-6789).' + ); + } + } + + return $fieldErrors; +}, 10, 4); +``` + +### Validate Based on Form ID + +```php +add_filter('breakdance_form_validate_field', function($fieldErrors, $field, $formId, $postId) { + // Only validate for specific forms + if ($formId === 123) { + $fieldId = $field['advanced']['id'] ?? ''; + + if ($fieldId === 'age' && !empty($field['value'])) { + $age = intval($field['value']); + if ($age < 18) { + $fieldErrors->add( + 'age_restriction', + 'You must be 18 or older to submit this form.' + ); + } + } + } + + return $fieldErrors; +}, 10, 4); +``` + +### Advanced Validation with Multiple Conditions + +```php +add_filter('breakdance_form_validate_field', function($fieldErrors, $field, $formId, $postId) { + $fieldId = $field['advanced']['id'] ?? ''; + + // Custom email domain validation + if ($field['type'] === 'email' && !empty($field['value'])) { + $allowedDomains = ['company.com', 'example.org']; + $email = $field['value']; + $domain = substr(strrchr($email, "@"), 1); + + if (!in_array($domain, $allowedDomains)) { + $fieldErrors->add( + 'invalid_domain', + sprintf( + 'Email must be from one of these domains: %s', + implode(', ', $allowedDomains) + ) + ); + } + } + + // Password strength validation + if ($fieldId === 'password' && !empty($field['value'])) { + $password = $field['value']; + + if (strlen($password) < 8) { + $fieldErrors->add('password_length', 'Password must be at least 8 characters long.'); + } + + if (!preg_match('/[A-Z]/', $password)) { + $fieldErrors->add('password_uppercase', 'Password must contain at least one uppercase letter.'); + } + + if (!preg_match('/[0-9]/', $password)) { + $fieldErrors->add('password_number', 'Password must contain at least one number.'); + } + } + + return $fieldErrors; +}, 10, 4); +``` + +### API Validation + +```php +add_filter('breakdance_form_validate_field', function($fieldErrors, $field, $formId, $postId) { + $fieldId = $field['advanced']['id'] ?? ''; + + // Validate coupon code against external API + if ($fieldId === 'coupon_code' && !empty($field['value'])) { + $couponCode = $field['value']; + + // Call external API to validate coupon + $response = wp_remote_get("https://api.example.com/validate-coupon?code={$couponCode}"); + + if (is_wp_error($response)) { + $fieldErrors->add('coupon_error', 'Unable to validate coupon code. Please try again.'); + } else { + $body = json_decode(wp_remote_retrieve_body($response), true); + + if (!$body['valid']) { + $fieldErrors->add('invalid_coupon', 'This coupon code is not valid.'); + } + } + } + + return $fieldErrors; +}, 10, 4); +``` + +### Database Validation + +```php +add_filter('breakdance_form_validate_field', function($fieldErrors, $field, $formId, $postId) { + $fieldId = $field['advanced']['id'] ?? ''; + + // Check if username already exists + if ($fieldId === 'username' && !empty($field['value'])) { + $username = sanitize_user($field['value']); + + if (username_exists($username)) { + $fieldErrors->add('username_exists', 'This username is already taken.'); + } + } + + // Check if email already exists + if ($field['type'] === 'email' && $fieldId === 'user_email' && !empty($field['value'])) { + if (email_exists($field['value'])) { + $fieldErrors->add('email_exists', 'An account with this email already exists.'); + } + } + + return $fieldErrors; +}, 10, 4); +``` + +## Best Practices + +### 1. Always Check Field Type or ID + +Only validate fields that are relevant to your custom validation logic: + +```php +if ($field['type'] === 'email' && !empty($field['value'])) { + // Your validation logic +} +``` + +### 2. Check for Empty Values + +Don't validate empty values unless you need to enforce a custom required field rule: + +```php +if (!empty($field['value'])) { + // Your validation logic +} +``` + +### 3. Return the Error Object + +Always return the `$fieldErrors` object, even if you didn't add any errors: + +```php +return $fieldErrors; +``` + +### 4. Use Descriptive Error Codes and Messages + +Make error codes unique and messages user-friendly: + +```php +$fieldErrors->add('invalid_phone_format', 'Phone number must be in format: (123) 456-7890'); +``` + +### 5. Consider Performance + +Avoid heavy operations (like API calls) unless necessary. Consider caching results: + +```php +$cacheKey = 'validated_' . md5($field['value']); +$cached = get_transient($cacheKey); + +if ($cached !== false) { + if (!$cached) { + $fieldErrors->add('validation_error', 'Invalid value.'); + } +} else { + // Perform expensive validation + $isValid = expensive_validation($field['value']); + set_transient($cacheKey, $isValid, HOUR_IN_SECONDS); + + if (!$isValid) { + $fieldErrors->add('validation_error', 'Invalid value.'); + } +} +``` + +### 6. Sanitize User Input + +Always sanitize field values before using them in comparisons or database queries: + +```php +$fieldValue = sanitize_text_field($field['value']); +``` + +### 7. Use Priority Wisely + +Set appropriate priority if your validation depends on other validations: + +```php +// Run after other validations +add_filter('breakdance_form_validate_field', 'my_validation', 20, 4); + +// Run before other validations +add_filter('breakdance_form_validate_field', 'my_validation', 5, 4); +``` + +## Hook Execution Flow + +1. Form is submitted +2. For each field in the form: + - A fresh `WP_Error` object is created + - The `breakdance_form_validate_field` filter is called + - All hooked functions receive the same error object + - Functions can add errors using `$fieldErrors->add()` + - If the returned error object has errors, they are merged into the main validation bag +3. If the main bag has errors, form submission fails and errors are displayed +4. If no errors, form submission continues + +## Error Display + +Errors added through this hook will be displayed to users along with other form validation errors. The exact display depends on the form's error message settings. + +## Related Hooks + +- `breakdance_form_run_action_{action_slug}` - Control whether a specific form action should run +- `breakdance_form_honeypot_triggered` - Triggered when honeypot spam protection is triggered + +## Changelog + +- **v1.0** - Hook introduced + +## Support + +For more information about Breakdance forms and available hooks, visit the [Breakdance Documentation](https://breakdance.com/documentation/). + From fac4da1a2822f319d20b01f9e92b4acf0b6488e5 Mon Sep 17 00:00:00 2001 From: Felipe Marcos Date: Wed, 5 Nov 2025 19:09:46 -0300 Subject: [PATCH 2/3] fix: wrong field type --- forms/fields-custom-validation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forms/fields-custom-validation.md b/forms/fields-custom-validation.md index 6cda73d..b8fd769 100644 --- a/forms/fields-custom-validation.md +++ b/forms/fields-custom-validation.md @@ -57,7 +57,7 @@ The `$field` parameter contains: ```php add_filter('breakdance_form_validate_field', function($fieldErrors, $field, $formId, $postId) { // Validate phone number format - if ($field['type'] === 'phone' && !empty($field['value'])) { + if ($field['type'] === 'tel' && !empty($field['value'])) { if (!preg_match('/^\d{10}$/', $field['value'])) { $fieldErrors->add( 'invalid_phone', From 841de91beb60234b1075c64a5e87a8bab4c7de45 Mon Sep 17 00:00:00 2001 From: Felipe Marcos Date: Sun, 19 Apr 2026 12:07:07 -0300 Subject: [PATCH 3/3] Update Breakdance version and clean up documentation Updated the required Breakdance version in documentation and removed the changelog section. --- forms/fields-custom-validation.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/forms/fields-custom-validation.md b/forms/fields-custom-validation.md index b8fd769..27e3529 100644 --- a/forms/fields-custom-validation.md +++ b/forms/fields-custom-validation.md @@ -2,7 +2,7 @@ The `breakdance_form_validate_field` filter hook allows third-party developers to add custom validation logic to Breakdance form fields. This hook is called for each field during form submission validation, enabling you to implement custom validation rules beyond the built-in validation. -**Note**: Requires Breakdance X.X+. +**Note**: Requires Breakdance 2.8+. ## Location @@ -313,10 +313,6 @@ Errors added through this hook will be displayed to users along with other form - `breakdance_form_run_action_{action_slug}` - Control whether a specific form action should run - `breakdance_form_honeypot_triggered` - Triggered when honeypot spam protection is triggered -## Changelog - -- **v1.0** - Hook introduced - ## Support For more information about Breakdance forms and available hooks, visit the [Breakdance Documentation](https://breakdance.com/documentation/).