3

I'm developing a custom Gutenberg block and would like to use the same JavaScript code on frontend and backend.

In the save function of the block I generate the following html code (abridged):

return (
    <div {...blockProps}>
        <table className="my-plugin-table" data-rows="50">
            <thead>
                <th>Col 1</th>
                <th>Col 2</th>
            </thead>
        </table>
    </div>
)

The frontend code reads the data attribute and creates the tbody element with 50 rows that are fetched from an API via fetch(), like so:

const tables = document.querySelectorAll('.my-plugin-table')
tables.forEach(table => {
    const tbody = document.createElement('tbody')
    table.appendChild(tbody)
    const rows = table.dataset.row
    const apiURL = 'https://xxxxxxx?rows=' + rows
    fetch(apiRUL)
        .then(response => response.json())
        .then(data => {
            const trs = []
            data.elements.forEach(element => {
                const tr = document.createElement('tr')
                const td1 = document.createElement('td')
                const td2 = document.createElement('td')
                td1.innerHTML = element.key
                td2.innerHTML = element.label
                tr.replaceChildren(td1, td2)
                trs.push(tr)
            })
            tbody.replaceChildren(trs)
        })
})

In the edit function of the block I use the identical html code:

return (
    <div {...blockProps}>
        <table className="my-plugin-table" data-rows="50">
            <thead>
                <th>Col 1</th>
                <th>Col 2</th>
            </thead>
        </table>
    </div>
)

What is the best practice here to use the frontend JavaScript in the backend as well? I need to access the dom element inside the editor. I tried it with wp.data.select( 'core/block-editor' ).getBlocks() but to no avail.

Can you maybe give me a hint what would be the best solution in the Gutenberg and WordPress way?

2 Answers 2

2

I need to access the dom element inside the editor.

useRef can be used for that, but actually, instead of doing that, I would just apply the approach shown below to my edit() function :)

https://reactjs.org/docs/faq-ajax.html

So using a function component, you can achieve the same thing your front-end script does like so:

Remember to load useState and useEffect, e.g. add import { useState, useEffect } from '@wordpress/element'; at the top in your file.

  1. Use useState to add states which will be used to determine whether we should display an error, a loading message or the items fetched from the remote API.

    const [ error, setError ]       = useState( null );
    const [ isLoaded, setIsLoaded ] = useState( false );
    const [ items, setItems ]       = useState( [] );
    
  2. Use useEffect to make the AJAX calls and then update the above states. And note that useEffect runs after the block is rendered in the editor (i.e. attached to the DOM).

    useEffect( () => {
        fetch( 'https://xxxxxxx?rows=50' )
            .then( res => res.json() )
            .then(
                result => {
                    setIsLoaded( true );
    
                    // * Change this accordingly, e.g. if `result` is not an array
                    // and instead an object with keys like 'items' or 'products'.
                    setItems( result );
                },
                error => {
                    setIsLoaded( false );
                    setError( error );
                }
            );
    }, [] );
    
  3. Now display the fetched items, or a loading message, or an error:

    return (
        <div { ...useBlockProps() }>
            <table className="my-plugin-table">
                <thead>
                    <th>Key</th>
                    <th>Label</th>
                </thead>
                <tbody>
                    { error && (
                        <tr><td colspan="2">Error: { error.message }</td></tr>
                    ) }
                    { ! error && ! isLoaded && (
                        <tr><td colspan="2">Loading items..</td></tr>
                    ) }
                    { isLoaded && items.map( item => (
                        <tr key={ 'item-' + item.key }>
                            <td>{ item.key }</td>
                            <td>{ item.label }</td>
                        </tr>
                    ) ) }
                </tbody>
            </table>
        </div>
    );
    

Additional Notes

  1. The key in the above <tr> tag is a special property that should always be specified in a list. More details here.

  2. If you're making a request to a custom WordPress REST API endpoint, you can use apiFetch() instead of fetch(). See my answer here for an example

4
  • 1
    Hi Sally, thank you very much for your very detailed reply and nice approach to my problem. Sometimes one one cannot see the wood for the trees - I was so infested from my frontend approach... Now I'll see how the rest of my frontend code will fit in here. The answers from you and @bosco are so detailed and great, I don't know which one to mark as the accepted answer... ;) Jul 20, 2022 at 10:59
  • I actually only intended to add my answer as a secondary answer (an alternate solution), and refs can be helpful, but I personally thought that in your case, using states is preferable than mutating the element like what your front-end script does. Moreover, that script was pretty simple, hence it's not hard to create an equivalent one for the block editor.. ;) Anyway, I'm glad my answer helped 🙂
    – Sally CJ
    Jul 20, 2022 at 14:26
  • The script is heavily shortened... :) So it's actually pretty hard to replicate. Still thinking about leveraging refs... Jul 20, 2022 at 19:42
  • I see.. 🤭 and in that case, maybe using refs is easier.. particularly if it could be like as simple as wrapping your front-end script in a function and calling it via useEffect()... you could also try mixing them - replicate the ones that can be replicated and use refs for the rest.. Or another option you might want to try or check is listening to the DOM changes using the MutationObserver API. And BTW, regarding the "accepted answer", I will understand it if you changed your mind :)
    – Sally CJ
    Jul 21, 2022 at 10:59
2

The only semi-canonical solution which I am aware of would be to write the block to be dynamic, rendering that portion of the block with PHP function and using the Server Side Render component to render it in the editor. But WordPress recommends against using the SSR component for new functionality, and the solution precludes the use/reuse of JS on the frontend.

Outside of that I'm not aware of any best practice or "WordPress way" to accomplish this. But I think the easiest way would be to genericize your shared functionality so it can operate on a single reference to a table element, then use a React ref in order to obtain the reference within the editor, along the lines of the process described in Integrating React with Other Libraries.

// Shared functionality

function populateMyTable( table ) {
  const tbody = document.createElement('tbody');
  table.appendChild(tbody);
  
  // ...
}
// Frontend initializer

window.addEventListener( 'DOMContentLoaded', () => {
  document.querySelectorAll('.my-plugin-table').forEach( populateMyTable );
} );
// Editor component

export default function Edit() {
  const table = useRef( null );

  useEffect(
    () => {
      if( table.current )
        populateMyTable( table.current );

      return () => {
        for( const tr of [...table.current.tBodies[0].children] )
          tr.remove();
      }
    }, 
    [ table.current ]
  );

  return (
    <div {...useBlockProps()}>
      <table ref={ table } className="my-plugin-table" data-rows="50">
        <thead>
          <tr><th>Col 1</th><th>Col 2</th></tr>
        </thead>
      </table>
    </div>
  );
}

A few notes here:

  • If you intend to incorporate the uncontrolled DOM elements into React/Block Editor functionality beyond simple display, it's probably not worth it to try and share that code between the editor and the frontend; things can get very messy very quickly!
  • If there's any chance that the ref could change (for instance any modification to the <table> component or it's props), you should use a callback ref instead to avoid possible breaks in functionality and memory leaks.
  • The function returned from the useEffect() callback performs cleanup of the added elements in order to mitigate potential memory leaks which might occur if the component should unmount after we've modified the DOM, particularly in the case that any event handlers have been attached to the uncontrolled elements.
  • I've revised the <thead> structure for standards compliance.
  • You could probably package the frontend initializer along with the shared functionality if you perform some check to determine whether or not the code is executing within the editor environment.
2
  • Hi @bosco, thank you for your very detailed answer and especially your helpful notes! The reffunctionality was exactly what I was looking for! Together with the answer from @sally-cj I now have something to play with. :) Jul 20, 2022 at 10:51
  • @bosco, I stumbled upon this answer and I was wondering if you could tell me which syntax must be used to import the populateMyTable function in your example into the editor component. Thanks!
    – leemon
    May 9, 2023 at 11:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.