Skip to content

Commit

Permalink
Legacy Widget: Improve backwards compatibility (#30709)
Browse files Browse the repository at this point in the history
* Legacy Widget: Improve backwards compatibility

- Fire 'widget-added' and 'widget-updated' events when the widget form
  is initialized or when it changes. This is what most widgets' scripts
  use to initialize and/or attach event listeners.
- Include hidden inputs that most widgets expect to exist in the DOM.

* Fix Legacy Widget E2E test

* Add onSave to deps

Co-authored-by: Kai Hao <kevin830726@gmail.com>

* Add setFormData to deps

Co-authored-by: Kai Hao <kevin830726@gmail.com>

* Import jquery instead of accessing from window

* Remove unnecessary useCallback

* Widgets E2E tests: Query by label instead of id

* Replace querySelector with extra ref

* Revert "Import jquery instead of accessing from window"

This reverts commit 4d591c5.

* Add fallback if jQuery does not exist

Co-authored-by: Kai Hao <kevin830726@gmail.com>
  • Loading branch information
noisysocks and kevin940726 authored Apr 15, 2021
1 parent 9e6ee0a commit bf5432c
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 60 deletions.
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(
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

0 comments on commit bf5432c

Please sign in to comment.