-
Notifications
You must be signed in to change notification settings - Fork 42
Fix pro issue 6392 (Properly replace errors without error classes when submitting with AJAX) #3339
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d8bc835
9b50444
9a3db84
6e19af5
4820566
1094736
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -311,9 +311,91 @@ private function replace_error_shortcode() { | |||||
| $this->html = str_replace( 'role="alert"', '', $this->html ); | ||||||
| } | ||||||
|
|
||||||
| $this->add_data_frm_error_attribute(); | ||||||
|
|
||||||
| FrmShortcodeHelper::remove_inline_conditions( true, 'error', $error, $this->html ); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Tag every top-level element in the [if error] block with a data-frm-error | ||||||
| * attribute, so js/formidable.js's removeFieldError()/removeAllErrors() can find | ||||||
| * and remove it on revalidation even when a custom field template's error markup | ||||||
| * carries no frm_error class or id (e.g. `[if error]<div>[error]</div>[/if error]`). | ||||||
| * Mirrors insertErrorHtml() tagging every top-level element client-side. | ||||||
| * | ||||||
| * @since x.x | ||||||
| * | ||||||
| * @return void | ||||||
| */ | ||||||
| private function add_data_frm_error_attribute() { | ||||||
| $error_body = self::get_error_body( $this->html ); | ||||||
|
|
||||||
| if ( ! is_string( $error_body ) || '' === trim( $error_body ) ) { | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| $tagged_body = self::tag_top_level_elements( $error_body ); | ||||||
|
|
||||||
| if ( $tagged_body === $error_body ) { | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| $this->html = str_replace( '[if error]' . $error_body . '[/if error]', '[if error]' . $tagged_body . '[/if error]', $this->html ); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Add a data-frm-error attribute to every element at the top level of an HTML | ||||||
| * fragment (direct children only, not nested descendants) by tracking open tags | ||||||
| * on a stack through a single scan, rather than a full DOM parse. | ||||||
| * | ||||||
| * @since x.x | ||||||
| * | ||||||
| * @param string $html | ||||||
| * | ||||||
| * @return string | ||||||
| */ | ||||||
| private static function tag_top_level_elements( $html ) { | ||||||
| $void_elements = array( 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr' ); | ||||||
| $open_tags = array(); | ||||||
| $offset = 0; | ||||||
| $result = ''; | ||||||
|
|
||||||
| while ( preg_match( '/<(\/?)([a-zA-Z][a-zA-Z0-9-]*)([^>]*?)(\/?)>/', $html, $match, PREG_OFFSET_CAPTURE, $offset ) ) { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocking: this regex breaks on an ordinary attribute value containing
tag_top_level_elements( '<div title="a>b">[error]</div>' );
// => '<div title="a data-frm-error>b">[error]</div>'The Fix: make the attrs capture skip over quoted attribute values instead of stopping at the first
Suggested change
Re-ran the standalone repro with this variant: fixes the Also: zero test coverage for this new ~80-line function, despite a PHPUnit file dedicated to this exact class already existing ( |
||||||
| $full_tag = $match[0][0]; | ||||||
| $tag_start = $match[0][1]; | ||||||
| $is_closing = '' !== $match[1][0]; | ||||||
| $tag_name = strtolower( $match[2][0] ); | ||||||
| $attrs = $match[3][0]; | ||||||
| $self_close = '' !== $match[4][0] || in_array( $tag_name, $void_elements, true ); | ||||||
|
|
||||||
| $result .= substr( $html, $offset, $tag_start - $offset ); | ||||||
|
|
||||||
| if ( $is_closing ) { | ||||||
| if ( $open_tags ) { | ||||||
| array_pop( $open_tags ); | ||||||
| } | ||||||
|
|
||||||
| $result .= $full_tag; | ||||||
| } elseif ( ! $open_tags ) { | ||||||
| $result .= '<' . $match[2][0] . $attrs . ' data-frm-error' . ( $self_close ? ' />' : '>' ); | ||||||
|
|
||||||
| if ( ! $self_close ) { | ||||||
| $open_tags[] = $tag_name; | ||||||
| } | ||||||
| } else { | ||||||
| $result .= $full_tag; | ||||||
|
|
||||||
| if ( ! $self_close ) { | ||||||
| $open_tags[] = $tag_name; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| $offset = $tag_start + strlen( $full_tag ); | ||||||
| }//end while | ||||||
|
|
||||||
| return $result . substr( $html, $offset ); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Pull the HTML between [if error] and [/if error] shortcodes. | ||||||
| * | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1192,6 +1192,26 @@ function frmFrontFormJS() { | |
| return formEl.frmErrorConfigCache; | ||
| } | ||
|
|
||
| /** | ||
| * Inserts error HTML into a field's container, tagging every inserted top-level | ||
| * element with a data-frm-error attribute. removeFieldError()/removeAllErrors() rely | ||
| * on that attribute (rather than the frm_error class) to find and remove the visible | ||
| * error element again, since a site's own custom field HTML template can render the | ||
| * [error] placeholder without a frm_error class or id. This only covers the visible | ||
| * element — aria-describedby cleanup still depends on an id, which custom markup may | ||
| * not have. | ||
| * | ||
| * @param {HTMLElement} container | ||
| * @param {string} errorHtml | ||
| * @return {void} | ||
| */ | ||
| function insertErrorHtml( container, errorHtml ) { | ||
| const template = document.createElement( 'template' ); | ||
| template.innerHTML = errorHtml; | ||
| Array.from( template.content.children ).forEach( el => el.setAttribute( 'data-frm-error', '' ) ); | ||
| container.append( template.content ); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocking: this fix doesn't reach the server-rendered error path — #6392 can still reproduce.
Looked at Concretely: a non-AJAX submit (or any page reload with a sticky server-rendered error) on a custom-template field hits This needs the same identifying hook applied server-side — e.g. have PHP add
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 9a3db84 (rebased on top since, unchanged content) — added FrmFieldFormHtml::add_data_frm_error_attribute(), called from replace_error_shortcode() right before the [if error]/[/if error] markers get stripped. It tags every top-level element in the server-rendered error body with data-frm-error, the same as insertErrorHtml() does client-side, using a single-pass tag-depth scan (not a full DOM parse) so nested descendants are left untouched. Verified directly via Reflection against the exact repro body ([if error] [error] [/if error]) plus nested/multi-sibling/pre-existing-class cases.
|
||
| } | ||
|
|
||
| function addFieldError( $fieldCont, key, jsErrors ) { | ||
| const container = $fieldCont instanceof jQuery ? $fieldCont.get( 0 ) : $fieldCont; | ||
|
|
||
|
|
@@ -1216,7 +1236,7 @@ function frmFrontFormJS() { | |
| const roleString = config.includeAlertRole ? 'role="alert"' : ''; | ||
| errorHtml = `<div class="frm_error" ${ roleString } id="${ id }">${ jsErrors[ key ] }</div>`; | ||
| } | ||
| container.insertAdjacentHTML( 'beforeend', errorHtml ); | ||
| insertErrorHtml( container, errorHtml ); | ||
| inputs.forEach( input => { | ||
| describedBy = input.getAttribute( 'aria-describedby' ); | ||
| if ( ! describedBy ) { | ||
|
|
@@ -1275,7 +1295,7 @@ function frmFrontFormJS() { | |
| return; | ||
| } | ||
|
|
||
| const errorMessage = container.querySelector( '.frm_error' ); | ||
| const errorMessages = container.querySelectorAll( '.frm_error, [data-frm-error]' ); | ||
| const input = container.querySelector( 'input, select, textarea' ); | ||
|
|
||
| container.classList.remove( 'frm_blank_field', 'has-error' ); | ||
|
|
@@ -1291,10 +1311,10 @@ function frmFrontFormJS() { | |
| } | ||
| } | ||
|
|
||
| if ( errorMessage ) { | ||
| errorMessages.forEach( errorMessage => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-blocking, but worth a look before merge: Per the CSS attribute-selector spec, an empty-string substring match (
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Left as-is — this is pre-existing (the custom-HTML branch never set an id before this PR either), and fixing it properly means deciding whether to force an id onto a site own custom error markup, which is a bigger design call than this fix. Softened insertErrorHtml()s docblock instead so it does not overclaim: it now says the data-frm-error tagging is for visible-element removal, not full aria-describedby cleanup, so the gap you found is not implied fixed. Worth a follow-up issue if you think it is worth tracking separately. |
||
| removeElementFromInputDescribedBy( errorMessage ); | ||
| errorMessage.remove(); | ||
| } | ||
| } ); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -1325,7 +1345,7 @@ function frmFrontFormJS() { | |
| document.querySelectorAll( '.form-field' ).forEach( field => { | ||
| field.classList.remove( 'frm_blank_field', 'has-error' ); | ||
| } ); | ||
| document.querySelectorAll( '.form-field .frm_error' ).forEach( el => { | ||
| document.querySelectorAll( '.form-field .frm_error, .form-field [data-frm-error]' ).forEach( el => { | ||
| removeElementFromInputDescribedBy( el ); | ||
| el.remove(); | ||
| } ); | ||
|
|
@@ -1473,7 +1493,7 @@ function frmFrontFormJS() { | |
| } | ||
|
|
||
| function checkForErrorsAndMaybeSetFocus() { | ||
| const errors = document.querySelectorAll( '.frm_form_field .frm_error' ); | ||
| const errors = document.querySelectorAll( '.frm_form_field .frm_error, .frm_form_field [data-frm-error]' ); | ||
| if ( ! errors.length ) { | ||
| return; | ||
| } | ||
|
|
@@ -1497,6 +1517,9 @@ function frmFrontFormJS() { | |
| let timeoutCallback; | ||
| do { | ||
| element = element.previousSibling; | ||
| if ( ! element ) { | ||
| break; | ||
| } | ||
| if ( [ 'input', 'select', 'textarea' ].includes( element.nodeName.toLowerCase() ) ) { | ||
| focusInput( element ); | ||
| break; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: Strategy11/formidable-forms
Length of output: 41548
🏁 Script executed:
Repository: Strategy11/formidable-forms
Length of output: 45559
🏁 Script executed:
Repository: Strategy11/formidable-forms
Length of output: 42223
Tag each distinct
[if error]body.get_error_body()extracts only the first body.str_replace()then tags every occurrence of that exact body, but a later non-identical body remains untagged.remove_inline_conditions()removes all error wrappers, so custom later blocks without.frm_errorordata-frm-errorremain visible after client revalidation. Process every matched body before removing the wrappers.🤖 Prompt for AI Agents