Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 77 additions & 25 deletions assets/src/js/pro/blocks-v3/components/block-edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,22 @@ function deserializeGoogleMapValues( $form, serializedData, clientId ) {
} );
}

/**
* Serializes the block form into the JSON string stored as theSerializedAcfData.
*
* @param {Object} $form The block form jQuery element.
* @param {string} clientId The block client ID.
* @return {string|null} JSON string, or null when the form has no data.
*/
function serializeBlockForm( $form, clientId ) {
const serializedData = acf.serialize( $form, `acf-block_${ clientId }` );
if ( ! serializedData ) {
return null;
}
deserializeGoogleMapValues( $form, serializedData, clientId );
return JSON.stringify( serializedData );
}

/**
* Main BlockEdit component wrapper
* Manages block data fetching and initial setup
Expand Down Expand Up @@ -264,6 +280,27 @@ export const BlockEdit = ( props ) => {
const acfFormRef = useRef( null );
const previewRef = useRef( null );
const debounceRef = useRef( null );
// Serialized form data before the user changed anything.
const initialFormDataRef = useRef( null );
const hasWrittenFormDataRef = useRef( false );

/**
* Stores the form's current data as its loaded state, until the first real edit is written
*
* @param {jQuery} $form - The block form
*/
const captureInitialFormData = useCallback(
( $form ) => {
if ( hasWrittenFormDataRef.current ) {
return;
}
const serializedData = serializeBlockForm( $form, clientId );
if ( serializedData ) {
initialFormDataRef.current = serializedData;
}
},
[ clientId ]
);

// Initialize acf.blockEdit namespace for jsx-parser to use
if ( ! acf.blockEdit ) {
Expand Down Expand Up @@ -414,9 +451,10 @@ export const BlockEdit = ( props ) => {
* @returns {string} - MD5 hash of serialized attributes
*/
function generateAttributesHash( attrs, ctx ) {
delete attrs.hasAcfError;
attrs._acf_context = sortObjectKeys( ctx );
return md5( JSON.stringify( sortObjectKeys( attrs ) ) );
// Work on a copy, attrs can be the block editor store's own object.
const { hasAcfError, ...hashAttrs } = attrs;
hashAttrs._acf_context = sortObjectKeys( ctx );
return md5( JSON.stringify( sortObjectKeys( hashAttrs ) ) );
}

/**
Expand Down Expand Up @@ -579,10 +617,18 @@ export const BlockEdit = ( props ) => {

// Update hasAcfError attribute based on validation errors
useEffect( () => {
setAttributes(
validationErrors ? { hasAcfError: true } : { hasAcfError: false }
);
}, [ validationErrors, setAttributes ] );
const hasError = !! validationErrors;
if ( hasError === !! attributes.hasAcfError ) {
return;
}

// hasAcfError is not saved with the block, so this must not mark the post as changed.
// Send only this key, so the next real edit is not merged into this change.
wp.data
.dispatch( 'core/block-editor' )
.__unstableMarkNextChangeAsNotPersistent();
setAttributes( { hasAcfError: hasError } );
}, [ validationErrors, attributes.hasAcfError, setAttributes ] );

// Listen for validation error events from other blocks
useEffect( () => {
Expand Down Expand Up @@ -631,18 +677,29 @@ export const BlockEdit = ( props ) => {
} );
}

// Stored data is keyed by field name and the form by field key, so on load
// the form is compared with its own first state instead.
if (
theSerializedAcfData ===
JSON.stringify( attributesWithoutError.data )
JSON.stringify( attributesWithoutError.data ) ||
( ! hasWrittenFormDataRef.current &&
theSerializedAcfData === initialFormDataRef.current )
) {
// Send the form values even when they are not stored, so the server
// validates them. A new block has no stored data to validate.
return void fetchBlockData( {
theAttributes: attributesWithoutError,
theAttributes: {
...attributesWithoutError,
data: parsedData,
},
theClientId: clientId,
theContext: context,
isSelected: isSelected,
} );
}

hasWrittenFormDataRef.current = true;

// Use original attributes (with hasAcfError) when updating
const updatedAttributes = {
...attributes,
Expand Down Expand Up @@ -714,6 +771,7 @@ export const BlockEdit = ( props ) => {
showValidationErrors={ showValidationErrors }
theSerializedAcfData={ theSerializedAcfData }
setTheSerializedAcfData={ setTheSerializedAcfData }
captureInitialFormData={ captureInitialFormData }
acfFormRef={ acfFormRef }
blockFormHtml={ blockFormHtml }
blockPreviewHtml={ blockPreviewHtml }
Expand Down Expand Up @@ -770,6 +828,7 @@ function BlockEditInner( props ) {
showValidationErrors,
theSerializedAcfData,
setTheSerializedAcfData,
captureInitialFormData,
acfFormRef,
blockFormHtml,
blockPreviewHtml,
Expand Down Expand Up @@ -1005,14 +1064,12 @@ function BlockEditInner( props ) {
setContentEditableChangeInProgress( false );
fieldInput.value = content;

const $form = $( acfFormRef?.current );
const serializedData = acf.serialize(
$form,
`acf-block_${ clientId }`
const serializedData = serializeBlockForm(
$( acfFormRef?.current ),
clientId
);
if ( serializedData ) {
deserializeGoogleMapValues( $form, serializedData, clientId );
setTheSerializedAcfData( JSON.stringify( serializedData ) );
setTheSerializedAcfData( serializedData );
} else {
setUserHasInteractedWithForm( false );
}
Expand Down Expand Up @@ -1251,20 +1308,15 @@ function BlockEditInner( props ) {
$={ $ }
clientId={ clientId }
blockFormHtml={ blockFormHtml }
onMount={ captureInitialFormData }
onUntouchedChange={ captureInitialFormData }
onChange={ function ( $form ) {
const serializedData = acf.serialize(
const serializedData = serializeBlockForm(
$form,
`acf-block_${ clientId }`
clientId
);
if ( serializedData ) {
deserializeGoogleMapValues(
$form,
serializedData,
clientId
);
setTheSerializedAcfData(
JSON.stringify( serializedData )
);
setTheSerializedAcfData( serializedData );
}
} }
validationErrors={ validationErrors }
Expand Down
20 changes: 16 additions & 4 deletions assets/src/js/pro/blocks-v3/components/block-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { lockPostSaving, unlockPostSaving } from '../utils/post-locking';
* @param {string} props.blockFormHtml - HTML markup for the ACF form
* @param {Function} props.onMount - Callback when the form mounts
* @param {Function} props.onChange - Callback when form data changes
* @param {Function} props.onUntouchedChange - Callback when the form changes before any user interaction
* @param {Array} props.validationErrors - Array of validation error objects
* @param {boolean} props.showValidationErrors - Whether to display validation errors
* @param {React.RefObject} props.acfFormRef - Ref to the form container element
Expand All @@ -29,6 +30,7 @@ export const BlockForm = ( {
blockFormHtml,
onMount,
onChange,
onUntouchedChange,
validationErrors,
showValidationErrors,
acfFormRef,
Expand All @@ -39,24 +41,32 @@ export const BlockForm = ( {
const [ formHtml, setFormHtml ] = useState( blockFormHtml );
const [ pendingChange, setPendingChange ] = useState( false );
const debounceTimer = useRef( null );
const reportedUntouchedChange = useRef( null );
const [ userInteracted, setUserInteracted ] = useState( false );
const [ hasRemounted, setHasRemounted ] = useState( false );

// Trigger onChange when there's a pending change
useEffect( () => {
if (
pendingChange &&
( userHasInteractedWithForm || userInteracted )
) {
if ( ! pendingChange ) {
return;
}

if ( userHasInteractedWithForm || userInteracted ) {
onChange( pendingChange );
setPendingChange( false );
} else if ( reportedUntouchedChange.current !== pendingChange ) {
// No interaction yet, so this is the form setting itself up.
// Report it once, not on every render, so a later edit is never taken as setup.
reportedUntouchedChange.current = pendingChange;
onUntouchedChange?.( pendingChange );
}
}, [
pendingChange,
userHasInteractedWithForm,
userInteracted,
setPendingChange,
onChange,
onUntouchedChange,
] );

// Update form HTML when blockFormHtml prop changes
Expand Down Expand Up @@ -177,6 +187,8 @@ export const BlockForm = ( {
}

const handleChange = () => {
// Edits like a context menu paste fire no click or keydown first.
setUserInteracted( true );
onChange( $form );
};

Expand Down
Loading
Loading