admin管理员组

文章数量:1122846

I have this specific block

than when I insert in the Editor, gives me this error:

and the error states:

Content generated by save function:

<div class="data-props" style="display:none;"><!--{"styleMode":"light","ctaTarget":"false","isImage":"true","isCardLeft":"true","mediaSrc":"","previewImage":"","className":"my-block light-mode"}--></div><div class="my-block light-mode" data-props="prev-sibling"><div class="my-block my-block light-mode"><div class="my-block"><div class="cnt-light cnt-maxwidth card-on-left"><div class="my-block__card"><div data-innerblocks="1"></div></div></div></div></div></div>

Content retrieved from post body:

<div class="my-block light-mode"><div class="data-props" style="display:none;"><!--{"ctaTarget":"false","isImage":"true","styleMode":"light","isCardLeft":"true","mediaSrc":"","previewImage":""}--></div><div class="my-block" data-props="prev-sibling"><div class="my-block"><img class="background__image" id="image-bg-desktop" src="/wp-content/themes/myTheme/assets/media/image.png" alt="my-block"/><div class="cnt-light cnt-maxwidth card-on-left"><img class="background__image" id="image-bg-mobile" src="/wp-content/themes/myTheme/assets/media/image.png" alt="my-block"/><div class="my-block__card"><div data-innerblocks="1"></div></div></div></div></div></div>

The React Component's code:

import { HydratedInnerBlock } from '../../../src/components/HydratedInnerBlock';
import { DEFAULT_IMAGE } from '../../../src/constants';
import { useStyleModeClassName } from '../../../src/hooks/useStyleModeClassName';
import { cssClassName } from '../../../src/util/cssClassName';

export const MyComponent = ( props ) => {
    const { mediaSrc, isImage, styleMode, isCardLeft, innerBlockProps } = props;
    const classNames = useStyleModeClassName( styleMode );
    const [ baseClassName ] = classNames;

    return (
        <div className="my-block">
            { isImage && (
                <img
                    className="background__image"
                    id="image-bg-desktop"
                    src={ mediaSrc || DEFAULT_IMAGE }
                    alt="my-block"
                />
            ) }
            { ! isImage && (
                <video
                    autoPlay
                    loop
                    muted
                    src={ mediaSrc }
                    className="background__video"
                />
            ) }
            <div
                className={ `cnt-${ baseClassName } ${ cssClassName(
                    'cnt-maxwidth ',
                    {
                        'card-on-left': isCardLeft,
                        'card-on-right': ! isCardLeft,
                    }
                ) }` }
            >
                { isImage && (
                    <img
                        className="background__image"
                        id="image-bg-mobile"
                        src={
                            mediaSrc || DEFAULT_IMAGE
                        }
                        alt="my-block"
                    />
                ) }
                { ! isImage && (
                    <video
                        autoPlay
                        loop
                        muted
                        src={ mediaSrc }
                        className="background__video"
                    />
                ) }
                <div className="my-block__card">
                    <HydratedInnerBlock innerBlockProps={ innerBlockProps } />
                </div>
            </div>
        </div>
    );
};

Here is the HydratedInnerBlock component:

export function HydratedInnerBlock( {
    innerBlockProps,
    onRef,
    ...otherDirectProps
} ) {
    return (
        <div
            data-innerblocks="1"
            dangerouslySetInnerHTML={ {
                __html:
                    innerBlockProps?.children?.props?.children ||
                    innerBlockProps,
            } }
            ref={ onRef }
            { ...otherDirectProps }
        />
    );
}

The block's Editor's code:

import { useEffect } from '@wordpress/element';
import {
    useBlockProps,
    InspectorControls,
    MediaUpload,
    useInnerBlocksProps,
} from '@wordpress/block-editor';
import { Panel, PanelBody, ToggleControl } from '@wordpress/components';

import { DEFAULT_IMAGE } from '../../src/constants';
import { EditorFileSelector } from '../../src/components/EditorFileSelector';

import './Editor.scss';
import { cssClassName } from '../../src/util/cssClassName';
import { useStyleModeClassName } from '../../src/hooks/useStyleModeClassName';
import { ColorModeSelector } from '../../src/components/ColorModeSelector';

/**
 * The edit function describes the structure of your block in the context of the
 * editor. This represents what the editor will render when the block is used.
 * Note this is a client-side component, so you can use client-side functionality
 * (hooks, events, ajax calls, etc)
 */

const TEMPLATE = [
    [
        'core/heading',
        {
            content:
                'Content 1',
            level: 2,
            className: 'my-block__title',
        },
    ],
    [
        'core/heading',
        {
            content: 'Content2',
            level: 5,
            className: 'my-block__subtitle',
        },
    ],
    [
        'core/paragraph',
        {
            content:
                'Content3',
            className: 'my-block__description',
        },
    ],
    [
        'core/buttons',
        {},
        [
            [
                'core/button',
                {
                    text: 'Get Started',
                    className: 'btn btn-dark',
                },
            ],
        ],
    ],
];
export const Editor = ( {
    attributes: {
        styleMode,
        mediaSrc,
        ctaTarget,
        isImage,
        isCardLeft,
        previewImage,
    },
    setAttributes,
} ) => {
    const blockProps = useBlockProps();
    const classNames = useStyleModeClassName( styleMode );
    const [ baseClassName, invertedClassName ] = classNames;
    const innerBlockProps = useInnerBlocksProps( undefined, {
        template: TEMPLATE,
    } );

    useEffect( () => {
        const button = document.querySelector( '.btn' );
        if ( button ) {
            button.className = '';
            button.classList.add( 'btn', `btn-${ invertedClassName }` );
        }
    }, [ invertedClassName ] );

    const handleMediaUpload = ( value ) => {
        setAttributes( { mediaSrc: value.url } );
    };

    const handleLinkTab = ( value ) => {
        setAttributes( { ctaTarget: value } );
    };

    const handleMediaTypeChange = ( value ) => {
        setAttributes( { isImage: value } );
    };

    const handleCardPosition = ( value ) => {
        setAttributes( { isCardLeft: value } );
    };

    const handleSelectStyleMode = ( value ) => {
        setAttributes( { styleMode: value } );
    };

    if ( previewImage?.length > 0 ) {
        return (
            <img
                src={ previewImage }
                alt="Preview"
                style={ { width: '470px' } }
            />
        );
    }

    return (
        <div
            { ...blockProps }
            className="my-block"
        >
            <div className="my-block">
                { isImage && (
                    <img
                        className="background__image"
                        id="image-bg-desktop"
                        src={
                            mediaSrc || DEFAULT_IMAGE
                        }
                        alt="my-block"
                    />
                ) }
                { ! isImage && (
                    <video
                        autoPlay
                        loop
                        muted
                        src={ mediaSrc }
                        className="background__video"
                    />
                ) }
                <div
                    className={ `cnt-${ baseClassName } ${ cssClassName(
                        'cnt-maxwidth ',
                        {
                            'card-on-left': isCardLeft,
                            'card-on-right': ! isCardLeft,
                        }
                    ) }` }
                >
                    { isImage && (
                        <img
                            className="background__image"
                            id="image-bg-mobile"
                            src={
                                mediaSrc ||
                                DEFAULT_IMAGE
                            }
                            alt="my-block"
                        />
                    ) }
                    { ! isImage && (
                        <video
                            autoPlay
                            loop
                            muted
                            src={ mediaSrc }
                            className="background__video"
                        />
                    ) }
                    <div className="my-block__card">
                        <div { ...innerBlockProps } />
                    </div>
                </div>

                <InspectorControls>
                    <Panel>
                        <PanelBody
                            title="Component Settings"
                            icon="admin-plugins"
                        >
                            <ColorModeSelector
                                value={ styleMode }
                                onChange={ handleSelectStyleMode }
                                useDefaultColors={ true }
                            />
                            <ToggleControl
                                label="Position card to the left"
                                checked={ isCardLeft }
                                onChange={ handleCardPosition }
                            />
                            <ToggleControl
                                label="Open Cta Link in a new tab"
                                checked={ ctaTarget }
                                onChange={ handleLinkTab }
                            />
                            <ToggleControl
                                label="Background is an image"
                                checked={ isImage }
                                onChange={ handleMediaTypeChange }
                            />
                            <MediaUpload
                                onSelect={ handleMediaUpload }
                                render={ ( { open } ) => (
                                    <EditorFileSelector
                                        open={ open }
                                        label="Upload Media"
                                        buttonLabel="Select / Upload Media"
                                    />
                                ) }
                                allowedTypes={ [ 'image', 'video' ] }
                            />
                        </PanelBody>
                    </Panel>
                </InspectorControls>
            </div>
        </div>
    );
};

And its Save.js:

/**
 * The 'Save' component of the block. This renders on server-side only
 * We separate this initializer from the 'Form' component because
 * the 'Form' component would also be used after hydration.
 *
 */
export const Save = ( { attributes } ) => {
    const blockProps = useBlockProps.save();
    const innerBlockProps = useInnerBlocksProps.save();
    const { isDarkMode } = attributes;

    const blockState = JSON.stringify( {
        ...attributes,
    } )
        // remove html comments
        .replace( /<\!--.+?-->/g, '' );

    return (
        <>
            <div
                className="data-props"
                style="display:none;"
                dangerouslySetInnerHTML={ {
                    __html: `<!--${ blockState }-->`,
                } }
            />
            <div
                className={ cssClassName(
                    `my-block `,
                    {
                        'dark-mode': isDarkMode,
                        'light-mode': ! isDarkMode,
                    }
                ) }
                data-props="prev-sibling"
            >
                <div { ...blockProps }>
                    <MyComponent
                        { ...attributes }
                        innerBlockProps={ innerBlockProps }
                    />
                </div>
            </div>
        </>
    );
};

The Save.js has that structure because I rehydrate the block after selecting custom attributes (theme dark/light) from custom metaboxes from the Editor's panel:

/**
 * Initializes the client-side component.
 * Hydrates the server-side rendered component and initializes client-side functionality.
 *
 */
const MyBlock = (props) => {
  const { styleMode } = props;
  const classNames = useStyleModeClassName(styleMode);
  const [baseClassName, invertedClassName] = classNames;

  useEffect(() => {
    const button = document.querySelector(".btn");
    if (button) {
      button.className = "";
      button.classList.add("btn", `btn-${invertedClassName}`);
    }
  }, [baseClassName, invertedClassName]);

  return <MyComponent {...props} />;
};

/**
 * If we have any blocks hydrate the server-side renderered HTML
 */
hydrate(
  ".my-block",
  MyBlock,
  true
);

Here is my hydrate function, if it helps:

import { createRoot } from '@wordpress/element';

/**
 * Helper function to hydrate any custom block following our approach for hydration
 *
 * @param {string}   selector         indicates the identifier for the container of the block to hydrate
 * @param {Function} Component        indicates the react component to hydrate with
 * @param {boolean}  isUseInnerBlocks indicates the react component uses inner blocks, to hydrate them recursively
 */
export function hydrate( selector, Component, isUseInnerBlocks ) {
    // mutation observer
    const mutationObserverConfig = {
        attributes: true,
        childList: true,
        subtree: isUseInnerBlocks ? true : false,
    };

    const initializeBlocks = () => {
        const elements = document.querySelectorAll( selector );

        if ( elements.length ) {
            elements.forEach( ( element ) => {
                if ( element.attributes[ 'data-hydrated' ]?.value === '1' ) {
                    // already hydrated, ignore
                    return;
                }

                // read hydration data
                const closestParent = element.closest( '[data-props]' );

                if ( ! closestParent ) {
                    // no props, nothing to hydrate
                    return;
                }

                let attributeValue =
                    closestParent.attributes[ 'data-props' ].value;

                if ( attributeValue === 'prev-sibling' ) {
                    const indexOf = Array.prototype.indexOf.call(
                        closestParent.parentNode.childNodes,
                        closestParent
                    );

                    attributeValue =
                        closestParent.parentNode.childNodes[ indexOf - 1 ]
                            .childNodes[ 0 ].textContent;
                }

                const data = JSON.parse( attributeValue );
                const innerBlockPropsStr = isUseInnerBlocks
                    ? closestParent
                            .querySelector( '[data-innerblocks]' )
                            ?.innerHTML?.replace( /data-hydrated="1"/gi, '' )
                    : undefined;

                // render new instance replacing the existing element (hydrate)
                const root = createRoot( element );

                root.render(
                    <Component
                        { ...data }
                        innerBlockProps={ innerBlockPropsStr }
                    />
                );

                element.setAttribute( 'data-hydrated', '1' );
            } );
        }
    };

    // eslint-disable-next-line no-undef
    const observer = new MutationObserver( initializeBlocks );

    // Start observing the target node for configured mutations
    observer.observe( document.body, mutationObserverConfig );
}

I changed the structure of the Save.js, fixing the WordPress error, but that breaks the hydrate function, and results in keeping the default theme (dark) and doesn't update its colors when changing to light mode.

Thanks in advance for your help!

本文标签: errorsBlock validation failed for React Component block that rehydrates after taking effect