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
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/block-library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@wordpress/viewport": "file:../viewport",
"classnames": "^2.2.5",
"fast-average-color": "4.3.0",
"jquery": "^3.5.1",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Off topic: I always think the version here is misleading, I think it even came up once during my coding test interview 😅. Maybe we should just pin it as * to make it more clear that it's not the version we are using at all.

However, that would become really weird for direct consumer of the npm package itself. Speaking of that, does it mean we're requiring jquery for this package now? I wonder what we could do about it 🤔. Maybe a fallback as you mentioned?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I... do not know 😅

@gziolo: Is this OK? Or is it better to access jQuery via the window global? We need to use it for backwards compatibility.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we did a lot of work in the past to avoid jQuery as a dependency in our packages, So I think since this is specifically for BC, we should use the global (if present).

Makes me wonder whether the legacy widget block should be in the block library package or in edit-widgets

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good question to ask whether the Legacy Widget block would be useful outside of the widgets screen and the Customizer? I wouldn't mind it was moved to the edit-widgets package. It's already the case for the Widget Area block:
https://github.com/WordPress/gutenberg/tree/trunk/packages/edit-widgets/src/blocks/widget-area

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the legacy widget block should be available anywhere else but the block editor that edits widget areas. The legacy widget block is tied to the way widgets are stored and it would be a bad idea to bring this all over the place.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes me wonder whether the legacy widget block should be in the block library package or in edit-widgets

It was in edit-widgets but I moved it back to block-library in #29960 for two reasons:

  • It means that customize-widgets can use the block.
  • It ensures that we are not relying on anything in edit-widgets to implement the block. I think this is important as it gives us the option to, in the future, allow Legacy Widget to be used in the site editor or wherever else we may want to allow it. I could see a case for block-based themes wanting to use a widget when there is no block equivalent.

The legacy widget block is tied to the way widgets are stored

This is not true as of #29960. Care has been taken to make Legacy Widget work like any other block that relies on the REST API: Latest Posts, Latest Comments, etc.

"lodash": "^4.17.19",
"memize": "^1.1.0",
"moment": "^2.22.1",
Expand Down
170 changes: 113 additions & 57 deletions packages/block-library/src/legacy-widget/edit/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* External dependencies
*/
import { debounce } from 'lodash';
import $ from 'jquery';

/**
* WordPress dependencies
Expand All @@ -14,60 +15,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 +127,109 @@ 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.
const hasBeenAdded = useRef( false );
useEffect( () => {
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,
] );

// Use jQuery change event instead of the native change event because many
// widgets use jQuery's trigger() 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 ) );
$( formRef.current ).on( 'change', null, handler );
return () => $( formRef.current ).off( 'change', null, 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