3

I have a custom gutenberg block with arbitrary InnerBlocks. For reasons of a custom javascript library I am using I cannot use the <InnerBlocks> component but want to iterate over all blocks and display the blocks with their InnerBlocks in my block's edit function.

I want to write a slider block with the swiper library. My efforts to really make the InnerBlocks (with Toolbars and other UI) work did not work out well. First, swiper needs a special DOM structure, that I could not get with the InnerBlocks component as it has a predifined wrapper and div structure. Second, swiper clones slides when you loop it, which eventually caused react errors when the whole gutenberg block ui was cloned. So what I figured out to work best is to have a button in my block, that switches between "edit mode" and "preview mode", where you see the InnerBlocks with all the UI for adding, moving in edit, and where you see the slider in preview mode.

I am using the code (simplified):

{ innerBlocks.map( ( block ) => {
    return (
        <div
            key={ block.clientId }
            dangerouslySetInnerHTML={ { __html: wp.blocks.getBlockContent( block ) } }
        >
        </div>
    );
} ) }

This works for most innerBlocks like core/heading, core/paragraph, ... But for embeds e.g. it just displays the embed url but not the embedded media content, e.g. from Youtube. I found wp.blocks.serialize and wp.blocks.rawHandler but cannot get it to work either. What am I doing wrong.

I am looking for answers that make embeds work without <InnerBlocks> or for a solution that works with swiper (loop enabled) and <InnerBlocks>.

11
  • 1
    This won't work as those blocks won't be interactive or have their hooks fire. You'll also get their saved content rather than their edit components. This approach is a dead end. Can you share your original problem? There's a good chance that your problem can be either solved, or sidestepped, or that a misunderstanding has been made and you can achieve your goal by other means. Additionally, expand your code example to include the entire component – Tom J Nowell Jan 8 at 20:11
  • @TomJNowell thanks for the answer, I extended my question. I do not mind that being a "dead end" in that way the user cannot edit blocks anymore, because as I described I have that edit mode available. My problem is really just the hooks(?) not being fired if that is what makes embeds work and I wonder how that would be possible. It is also a "dynamic block" with a php frontend, but I figured it was way faster and more performant to not use a <ServerSideRender> in edit, or would that be the only way? – user9 Jan 9 at 11:39
  • I do not believe that is true, that library has react support swiperjs.com/react all you need to do is wrap the innerblocks component in a <Swiper> component, then wrap the children in <SwiperSlide> components. You can do this easily with a Swiper block and a Swiper Slide block, much like how core has a columns and a column block. Do that and your problem and all the workarounds and hacks disappear into thin air, it even provides a nice way to add new slides as you can make sure an inserter is always showing, and your inner blocks are always editable live – Tom J Nowell Jan 9 at 23:34
  • @TomJNowell I tried that and alot of other approaches. The Problem with what you suggest is, that if I do <Swiper><InnerBlocks></Swiper> where all inner blocks are wrapped in <SwiperSlide>, the innerblocks add two wrapper divs which make swiper unusable. Also I tried the new useInnerBlocksProps hook, to get rid of the wrapper divs however then the parent <Swiper> does not recognize it SwiperSlides. For me it ends up in trying it the way I am asking for above. – user9 Jan 10 at 16:02
  • If each inner block is a slide then that is not the appropriate method of doing that. You cannot have just a swiper block, you need a swiper slide block, as well as a prop to restrict the inner blocks to only be swiper slide blocks, in the same way that the columns block doesn't just wrap the contents, it instead has individual column blocks. This can not and should not be done with a single block – Tom J Nowell Jan 10 at 17:29
5
+200

What you asked for is not possible for blocks that rely on frontend javascript, this includes embeds.


It's also not possible using the Swiper react library. The Swiper library made the questionable choice of manually inspecting child components for the SwiperSlide component name, preventing it from ever being used with child blocks.

Edit: Actually maybe it is! Since writing this, a PR was merged that may fix this and allow the code below to work as desired


It isn't possible using jQuery either with inner blocks as it conflicts with the way React works, and will cause virtual DOM problems.

The Closest You Can Get

Instead, if we use the React Swiper library, useBlockProps, and useInnerBlocksProps, we can implement this, and have editable slides, while keeping the slider.

However, Swiper will not see the specific arrangement of components it needs and will place them after the slider container instead of inside the wrapper.

The important thing to note, is this snippet:

        const blockProps = useBlockProps();
        const innerBlocksProps = useInnerBlocksProps( blockProps, {
            allowedBlocks: [ 'tomjn/swiper-slide' ],
            template: TEMPLATE,
        });
        const params = {...};
        return (
            <div { ...innerBlocksProps }>
                <Swiper { ...params }>
                    { innerBlocksProps.children }
                </Swiper>
            </div>
        );

By using innerBlocksProps.children we eliminate the 2 containing <div> tags.

Here's my demo:

src/index.js:

import { registerBlockType } from '@wordpress/blocks';
import {
    InnerBlocks,
    useBlockProps,
    __experimentalUseInnerBlocksProps as useInnerBlocksProps
} from '@wordpress/block-editor';

import SwiperCore, { Navigation, Pagination, Scrollbar, A11y } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';

// Import Swiper styles
import 'swiper/swiper-bundle.css';

// install Swiper components
SwiperCore.use([Navigation, Pagination, Scrollbar, A11y]);

const TEMPLATE = [
    [ 'tomjn/swiper-slide', {}, [
        [ 'core/paragraph', { placeholder: 'Enter slide content...' } ],
    ] ],
    [ 'tomjn/swiper-slide', {}, [
        [ 'core/paragraph', { placeholder: 'Enter slide content...' } ],
    ] ]
];

registerBlockType( 'tomjn/swiper', {
    apiVersion: 2,
    title: 'Swiper',
    category: 'layout',
    edit: () => {
        const blockProps = useBlockProps();
        const innerBlocksProps = useInnerBlocksProps( blockProps, {
            allowedBlocks: [ 'tomjn/swiper-slide' ],
            template: TEMPLATE,
        });
        const params = {
            init: true,
            paceBetween: 50,
            slidesPerView: 2,
            loop: true,
            navigation: {
                clickable: true,
            },
            spaceBetween: 30,
            pagination: {
                clickable: true,
            }
        };
        return (
            <div { ...innerBlocksProps }>
                <Swiper { ...params }>
                    { innerBlocksProps.children }
                </Swiper>
            </div>
        );
    },
    save: () => {
        return (
            <div className="swiper-container">
                <div className="swiper-wrapper">
                    <InnerBlocks.Content />
                </div>
                <div className="swiper-pagination"></div>
                <div className="swiper-button-prev"></div>
                <div className="swiper-button-next"></div>
            </div>
        );
    },
} );

registerBlockType( 'tomjn/swiper-slide', {
    apiVersion: 2,
    title: 'Swiper Slide',
    category: 'layout',
    edit: () => {
        const blockProps = useBlockProps();
        return (
            <SwiperSlide>
                <div {...blockProps}>
                    <InnerBlocks />
                </div>
            </SwiperSlide>
        );
    },
    save: () => {
        return (
            <div class="swiper-slide">
                <InnerBlocks.Content />
            </div>
        );
    },
} );

package.json:

{
    "dependencies": {
        "swiper": "^6.4.5"
    },
    "devDependencies": {
        "@wordpress/scripts": "^12.6.1"
    },
    "scripts": {
        "build": "wp-scripts build",
        "check-engines": "wp-scripts check-engines",
        "check-licenses": "wp-scripts check-licenses",
        "format:js": "wp-scripts format-js",
        "lint:css": "wp-scripts lint-style",
        "lint:js": "wp-scripts lint-js",
        "lint:md:docs": "wp-scripts lint-md-docs",
        "lint:md:js": "wp-scripts lint-md-js",
        "lint:pkg-json": "wp-scripts lint-pkg-json",
        "packages-update": "wp-scripts packages-update",
        "start": "wp-scripts start",
        "test:e2e": "wp-scripts test-e2e",
        "test:unit": "wp-scripts test-unit-js"
    }
}

plugin.php:

<?php
/**
 * Plugin Name: Toms Swiper Plugin
 * Plugin URI: https://tomjn.com
 * Description: Swiper no swiping.
 * Requires at least: 5.5
 * Requires PHP: 7.3
 * Version: 1.0.0
 * Author: Tom J Nowell
 */

defined( 'ABSPATH' ) || exit;

add_action(
    'enqueue_block_editor_assets',
    function() {
        $asset = include plugin_dir_path( __FILE__ ) . 'build/index.asset.php';

        wp_register_script(
            'tomjn-swiper-block-js',
            plugins_url( 'build/index.js', __FILE__ ),
            $asset['dependencies'],
            $asset['version'],
        );
        wp_register_style(
            'tomjn-swiper-block-css',
            plugins_url( 'build/index.css', __FILE__ ),
            [],
            $asset['version'],
        );
    }
);

/**
 * Registers all block assets so that they can be enqueued through Gutenberg in
 * the corresponding context.
 */
function tomjn_swiper_register_block() {

    if ( ! function_exists( 'register_block_type' ) ) {
        // Gutenberg is not active.
        return;
    }

    register_block_type(
        'tomjn/swiper',
        [
            'editor_script' => 'tomjn-swiper-block-js',
            'editor_style'  => 'tomjn-swiper-block-css',
        ]
    );
    register_block_type(
        'tomjn/swiperslide',
        [
            'editor_script' => 'tomjn-swiper-block-js',
            'editor_style'  => 'tomjn-swiper-block-css',
        ]
    );

}
add_action( 'init', 'tomjn_swiper_register_block' );

12
  • A bit more digging shows that the swiper carousels are... awful. It's directly manipulating child components which is a terrible thing to do in order to get the slides in an array, even though there are far better ways like react context to do this. Ironically, what I need to figure this out is the very part of your question you did not add to try and be helpful – Tom J Nowell Jan 11 at 14:52
  • So I figured that out and it didn't lead anywhere good. React Swiper will not accept it as a slide, and there is no way to get the final HTML that appears on the frontend because that HTML is not what the user sees once JS has finished running ( embeds for example are converted into their final form on the frontend ). The fundamental problem is the choice f this library and its poor design choices in its React component. Specifically github.com/nolimits4web/swiper/blob/master/src/react/… – Tom J Nowell Jan 11 at 17:45
  • As for the original method that was asked about, it is not possible for blocks that have a frontend JS component, such as embeds. You can show their edit components but then you run into issues leading you to the code that's in my answer ( individual edit components would not be possible as they would no longer be attached to the document ) – Tom J Nowell Jan 11 at 17:46
  • Thanks Tom, for all the effort you already put in this. I also looked in that swiper file you posted above already and altered it, so that it "always" pushes to the slides array. This got me somewhere at least. However still having other issue on that path. innerBlocksProps really seems to get me as close as possible. My question really was if there was a way with the save output of the blocks and it is at least satisfiying to find it is a dead end (and not put more time in it). I think I somewhere read about using innerBlockProps in class components but cannot find it. Do you know? – user9 Jan 11 at 20:23
  • innerBlockProps is meant to let you put the block component props on to your edit components markup rather than it being wrapped, the goal in Gutenberg core is to make styling blocks in the admin closer to the frontend, and to reduce the <div> spam to improve performance. I don't believe there's a class based version but its internals can be reused. I did look to see if the get children function could be monkey-patched, and I raised a ticket suggesting they use React context instead github.com/nolimits4web/swiper/issues/4084 – Tom J Nowell Jan 11 at 20:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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