Skip to content
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

Legacy Widget: Improve backwards compatibility #30709

Merged
merged 10 commits into from
Apr 15, 2021
184 changes: 127 additions & 57 deletions packages/block-library/src/legacy-widget/edit/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,60 +14,32 @@ import {
useRef,
useState,
useCallback,
forwardRef,
RawHTML,
} from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { Button } from '@wordpress/components';
import { useInstanceId } from '@wordpress/compose';

export default function Form( { id, idBase, instance, setInstance } ) {
const ref = useRef();

const { html, setFormData } = useForm( {
id,
idBase,
instance,
setInstance,
} );

const onSubmit = useCallback(
( event ) => {
event.preventDefault();
if ( id ) {
setFormData( serializeForm( ref.current ) );
}
},
[ id ]
);

const onChange = useCallback(
debounce( () => {
if ( idBase ) {
setFormData( serializeForm( ref.current ) );
}
}, 300 ),
[ idBase ]
);
const setFormDataDebounced = useCallback( debounce( setFormData, 300 ), [
setFormData,
] );

return (
<div className="widget open">
<div className="widget-inside">
<ObservableForm
ref={ ref }
className="form"
method="post"
onSubmit={ onSubmit }
onChange={ onChange }
>
<RawHTML className="widget-content">{ html }</RawHTML>
{ id && (
<Button type="submit" isPrimary>
{ __( 'Save' ) }
</Button>
) }
</ObservableForm>
</div>
</div>
<Control
id={ id }
idBase={ idBase }
html={ html }
onChange={ setFormDataDebounced }
onSave={ setFormData }
/>
);
}

Expand Down Expand Up @@ -154,26 +126,124 @@ function useForm( { id, idBase, instance, setInstance } ) {
return { html, setFormData };
}

function serializeForm( form ) {
return new window.URLSearchParams(
Array.from( new window.FormData( form ) )
).toString();
}
function Control( { id, idBase, html, onChange, onSave } ) {
const controlRef = useRef();
const formRef = useRef();

const ObservableForm = forwardRef( ( { onChange, ...props }, ref ) => {
// React won't call the form's onChange handler because it doesn't know
// about the <input>s that we add using dangerouslySetInnerHTML. We work
// around this by not using React's event system.
// Trigger 'widget-added' when widget is ready and 'widget-updated' when
// widget changes. This event is what widgets' scripts use to initialize,
// attach events, etc. The event must be fired using jQuery's event bus as
// this is what widget scripts expect. If jQuery is not loaded, do nothing -
// some widgets will still work regardless.
const hasBeenAdded = useRef( false );
useEffect( () => {
if ( ! window.jQuery ) {
return;
}

const { jQuery: $ } = window;

if ( html ) {
$( document ).trigger(
noisysocks marked this conversation as resolved.
Show resolved Hide resolved
hasBeenAdded.current ? 'widget-updated' : 'widget-added',
[ $( controlRef.current ) ]
);
hasBeenAdded.current = true;
}
}, [
html,
// Include id and idBase in the deps so that widget-updated is triggered
// if they change.
id,
idBase,
] );

// Prefer jQuery 'change' event instead of the native 'change' event because
// many widgets use jQuery's event bus to trigger an update.
useEffect( () => {
const handler = () => onChange( ref.current );
ref.current.addEventListener( 'change', handler );
ref.current.addEventListener( 'input', handler );
return () => {
ref.current.removeEventListener( 'change', handler );
ref.current.removeEventListener( 'input', handler );
};
const handler = () => onChange( serializeForm( formRef.current ) );

if ( window.jQuery ) {
const { jQuery: $ } = window;
$( formRef.current ).on( 'change', null, handler );
return () => $( formRef.current ).off( 'change', null, handler );
}

formRef.current.addEventListener( 'change', handler );
return () => formRef.current.removeEventListener( 'change', handler );
}, [ onChange ] );

return <form ref={ ref } { ...props } />;
} );
// Non-multi widgets can be saved via a Save button.
const handleSubmit = ( event ) => {
event.preventDefault();
onSave( serializeForm( event.target ) );
};

// We can't use the real widget number as this is calculated by the server
// and we may not ever *actually* save this widget. Instead, use a fake but
// unique number.
const number = useInstanceId( Control );

return (
<div ref={ controlRef } className="widget open">
<div className="widget-inside">
<form
ref={ formRef }
className="form"
method="post"
onSubmit={ handleSubmit }
>
{ /* Many widgets expect these hidden inputs to exist in the DOM. */ }
<input
type="hidden"
name="widget-id"
className="widget-id"
value={ id ?? `${ idBase }-${ number }` }
/>
<input
type="hidden"
name="id_base"
className="id_base"
value={ idBase ?? id }
/>
<input
type="hidden"
name="widget-width"
className="widget-width"
value="250"
/>
<input
type="hidden"
name="widget-height"
className="widget-height"
value="200"
/>
<input
type="hidden"
name="widget_number"
className="widget_number"
value={ idBase ? number : '' }
/>
<input
type="hidden"
name="add_new"
className="add_new"
value=""
/>
<RawHTML className="widget-content">{ html }</RawHTML>
{ id && (
<Button type="submit" isPrimary>
{ __( 'Save' ) }
</Button>
) }
</form>
</div>
</div>
);
}

function serializeForm( form ) {
return new window.URLSearchParams(
Array.from( new window.FormData( form ) )
).toString();
}
16 changes: 13 additions & 3 deletions packages/e2e-tests/specs/widgets/adding-widgets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,12 @@ describe( 'Widgets screen', () => {
'[aria-label="Block: Widget Area"][role="group"]'
);

const legacyWidget = await page.waitForSelector(
// Wait for the widget's form to load.
await page.waitForSelector(
'[data-block][data-type="core/legacy-widget"] input'
);

const legacyWidget = await page.$(
'[data-block][data-type="core/legacy-widget"]'
);

Expand Down Expand Up @@ -491,8 +496,13 @@ describe( 'Widgets screen', () => {
);
await editButton.click();

// TODO: Should query this with role and label.
const titleInput = await legacyWidget.$( 'input' );
const [ titleLabel ] = await legacyWidget.$x(
'//label[contains(text(), "Title")]'
);
const titleInputId = await titleLabel.evaluate( ( node ) =>
node.getAttribute( 'for' )
);
const titleInput = await page.$( `#${ titleInputId }` );
await titleInput.type( 'Search Title' );

// Trigger the toolbar to appear.
Expand Down