WordPress / gutenberg

The Block Editor project for WordPress and beyond. Plugin is available from the official repository.
https://wordpress.org/gutenberg/
Other
10.45k stars 4.17k forks source link

[FR] Provide a onRender prop in ServerSideRender #35294

Open pryley opened 3 years ago

pryley commented 3 years ago

What problem does this address?

Sometimes you need to trigger some javascript after a block is rendered with ServerSideRender.

Previously, the solution was to extend the ServerSideRender class. However with WordPress 5.8 this is no longer easily possible due to the changes made to allow Blocks to work outside of the post editor (i.e. sidebars and widgets).

See also:

What is your proposed solution?

My suggestion is to include a onRender prop to ServerSideRender and trigger it with the useEffect hook:

export default function ServerSideRender (props) {
    const {
        attributes,
        block,
        className,
        httpMethod = 'GET',
        onRender, // <- Here is the new "onRender" prop
        urlQueryArgs,
        EmptyResponsePlaceholder = DefaultEmptyResponsePlaceholder,
        ErrorResponsePlaceholder = DefaultErrorResponsePlaceholder,
        LoadingResponsePlaceholder = DefaultLoadingResponsePlaceholder,
    } = props;

    // ... 

    // Here is how the "onRender" prop is used
    useEffect(() => {
        if (onRender) {
            // Here we pass the response, the block, and attributes used for the response
            onRender(response, block, attributes);
        }
    }, [response]);

    // ... 

    return <RawHTML className={ className }>{ response }</RawHTML>;
}

And this is how you would use it in your block:


const onRender = (response, block, attributes) => {
    if (!_.isEmpty(response) && !response.error) {
        console.log('the block has been rendered!');
    }
}

<ServerSideRender block="my/block" attributes={ attributes } onRender={ onRender }></ServerSideRender>
pryley commented 3 years ago

@jasmussen since you worked on https://github.com/WordPress/gutenberg/issues/35027, perhaps you'd be open to looking at this since it's related to the SSR?

I'm currently using a custom ServerSideRender class which is identical to the latest commit with the loading indicator changes except that it also includes the change above...but I'd love to see this or something similar in core.

talldan commented 3 years ago

@pryley What's the use case for such a callback?

pryley commented 3 years ago

@talldan In my case I use it to initialise a carousel layout, and to transform a SELECT into a custom rating control in a form. Additionally, I use it to add a direction (i.e. ltr, rtl) class which determines how the block's CSS layout is displayed.

kevin940726 commented 3 years ago

As mentioned in the description, the same thing had been brought up in https://github.com/WordPress/gutenberg/issues/7346. I don't see why not adding this feature, but maybe @gziolo has some more insights?

As for the actual implementation, I'm not sure onRender is the right API to use here. I would probably create a useServerSideRender hook instead and use that to implement <SeverSideRender> under the hood. So if you want to run some side effects between renders, use the hook instead of the component for more controls.

const html = useServerSideRender( block, attributes );

useEffect( () => {
  // Do your things.
}, [ html ] );

return <RawHTML>{ html }</RawHTML>;
talldan commented 3 years ago

Thanks for sharing @pryley. I like @kevin940726's idea.

pryley commented 3 years ago

@kevin940726 @talldan Thanks, that sounds promising! However, I don't quite understand how the useServerSideRender hook would be implemented as described.

This is my current implementation:

const edit = props => {
    const { attributes: { /* ... */ }, className, setAttributes } = props;
    const inspectorControls = { /* ... */ }
    const inspectorAdvancedControls = { /* ... */ }
    return [
        <InspectorControls>
            <PanelBody title={ _x('Settings', 'admin-text', 'site-reviews')}>
                { Object.values(wp.hooks.applyFilters('site-reviews.form.InspectorControls', inspectorControls, props)) }
            </PanelBody>
        </InspectorControls>,
        <InspectorAdvancedControls>
            { Object.values(wp.hooks.applyFilters('site-reviews.form.InspectorAdvancedControls', inspectorAdvancedControls, props)) }
        </InspectorAdvancedControls>,
        <ServerSideRender block={ blockName } attributes={ props.attributes }></ServerSideRender>
    ];
};

This is what I tried which obviously doesn't work since the hook is fired before render instead of before/after render:

const edit = props => {
    const { attributes: { /* ... */ }, className, setAttributes } = props;
    const inspectorControls = { /* ... */ }
    const inspectorAdvancedControls = { /* ... */ }

    const rendered = (<ServerSideRender block={ blockName } attributes={ props.attributes }></ServerSideRender>)

    useEffect(() => {
        console.log(rendered);
    }, [rendered]);

    return [
        <InspectorControls>
            <PanelBody title={ _x('Settings', 'admin-text', 'site-reviews')}>
                { Object.values(wp.hooks.applyFilters('site-reviews.form.InspectorControls', inspectorControls, props)) }
            </PanelBody>
        </InspectorControls>,
        <InspectorAdvancedControls>
            { Object.values(wp.hooks.applyFilters('site-reviews.form.InspectorAdvancedControls', inspectorAdvancedControls, props)) }
        </InspectorAdvancedControls>,
        <RawHTML>{ rendered }</RawHTML>
    ];
};

Would you be able to provide some more details on the implementation as described?

Although perhaps I have misunderstood, and when referring to a useServerSideRender hook you mean the actual implementation inside ServerSideRender?

talldan commented 3 years ago

I think @kevin940726 was referring to a new API that would be implemented in Gutenberg (as an alternative to having a callback prop).

jonathan-dejong commented 1 year ago

So this is still not a thing right? I tried finding any info on this or something equal for an hour no but nothing.

How would one go about triggering custom JS functionality after a successful serversiderender? Do I have to use componentDidUpdate()?

pryley commented 1 year ago

@jonathan-dejong

I assume this isn't a priority since the Gutenberg focus has been to move away from server-side rendering.

I get around this by using a custom ServerSideRender with the following code added:

useEffect(() => {
    if (props.onRender) {
        props.onRender(response, block, attributes);
    }
}, [response]);

The onRender (GLSR.Event is the event handler from my plugin Site Reviews):

const onRender = (response, block, attributes) => {
    GLSR.Event.trigger(block, response, attributes);
}

export default onRender;

Usage:

<ServerSideRender block={ blockName } attributes={ props.attributes } onRender={ onRender }></ServerSideRender>
jonathan-dejong commented 1 year ago

Thank you for the response @pryley

My impression is that most agencies build only or mostly dynamic blocks so that's a bit off the mark to do imo 😅 Custom static blocks are a horror to maintain and causes all sorts of problems with other things too so we try to build dynamic as well.

what does your ServerSideRender look like?

pryley commented 1 year ago

@jonathan-dejong

In my case, I'm waiting on upgrading to dynamic blocks until the next major version of the plugin. The reason being that dynamic blocks would likely not work with the template system that the plugin uses (i.e. copying plugin template files to your theme to customize the HTML) so it would be a breaking change.

You can see the current (somewhat outdated) implementation here:

https://github.com/pryley/site-reviews/blob/23e6dccb04c800228b1d03fcf1c4e154746a9ad2/%2B/scripts/blocks/server-side-render.js#L156-L160

https://github.com/pryley/site-reviews/blob/23e6dccb04c800228b1d03fcf1c4e154746a9ad2/%2B/scripts/blocks/on-render.js#L1-L5

https://github.com/pryley/site-reviews/blob/23e6dccb04c800228b1d03fcf1c4e154746a9ad2/%2B/scripts/blocks/block-form.js#L126-L127

jonathan-dejong commented 1 year ago

that's a nice simple approach yeah.

I don't see why that couldn't be added to the component already.

yalogica commented 3 months ago

I'm hoping it might help someone. I am using MutationObserver in my code for such thing, it signals me that I need to update the data in the block using js. Below is the code.

const Edit = ( props ) => {
   const { attributes, setAttributes } = props;
   ...
   ...
   const memoizedSSR = useMemo( () => {
      return (
         <ServerSideRender
               block = { block.name }
               attributes = { attributes }
               className = 'myblock'
         />
      )
   }, [ attributes ] );

   const ref = useRefEffect( ( block ) => {
      const observer = new MutationObserver((mutations, observer) => {
         for(let mutation of mutations) {
            for (let node of mutation.addedNodes) {
               if (node.tagName == 'DIV' && node.classList.contains('myblock')) {
                  console.log('do things with pure js library...');
               }
            }
         }
      });
      observer.observe(block, { childList: true, subtree: false } );

      return () => {
         observer.disconnect();
      }
   }, [] );
   ...
   ...
   return (
      <>
         <InspectorControls...>
         <BlockControls...>
            <div { ...useBlockProps( { ref } ) }>
               { memoizedSSR }
            </div>
      </>
   );
}

export default Edit;
christianwach commented 2 months ago

@yalogica Your approach works perfectly! Thank you for sharing 🤩