WordPress Logo WordPress スライダーを表示するブロックの作成

WordPress のブロックエディタ Gutenberg でスライダーのプラグイン Swiper を使ってスライダーを表示するカスタムブロックの作り方についての覚書です。

以下は(旧)ブロックの作成 チュートリアル(削除予定)などの古い情報を参考にしているため部分的に古い情報になっています。

更新日:2024年01月21日

作成日:2020年10月11日

ブロックの基本的な作成方法や環境の構築方法については以下を御覧ください。

関連ページ

Node.js がインストールされていて、WordPress のローカル環境があることを前提にしています。

概要

スライダープラグイン Swiper を使って、MediaUpload コンポーネントで選択した複数の画像をスライダーで表示するカスタムブロックを作成します。

create-block を使って環境構築及び初期ファイルを作成し、まず MediaUpload コンポーネントを使って複数の画像を挿入できるブロックを作成します。

そして Swiper のファイルを読み込み、save 関数でレンダリングをスライダー用に変更してスライダーで表示できるようにします。

ファイルのセットアップ

ブロックの作成に必要なファイルとスライダープラグイン Swiper のファイルを用意します。

ブロックの作成では JSX を使用するので JSX をコンパイルする環境が必要ですが、create-block を使用すると簡単に環境の構築と初期ファイルが作成できます。

関連ページ:Gutenberg ブロック開発の環境構築

この例ではプラグインとしてブロックを作成するので、ターミナルでプラグインディレクトリ wp-content/plugins に移動します。パスは環境に合わせて指定します。

$ cd /path/to/wp-content/plugins return //プラグインディレクトリへ移動

続いて、create-block のコマンドを実行して環境の構築と初期ファイルを生成します。

以下は、--namespace オプションで名前空間(namespace)を wdl に指定し、slug(作成するプラグインのフォルダ名)に my-slider を指定して create-block のコマンドを実行しています。

この例では slug に my-slider を指定してコマンドを実行したので、my-slider というフォルダがプラグインディレクトリ(wp-content/plugins )に作成され、その中に以下のようなファイルが出力されます。

my-slider //作成されたプラグインのディレクトリ
├── block.json
├── build //ビルドで出力されるファイル(本番環境で使用するファイル)のディレクトリ
│   ├── index.asset.php
│   ├── index.css
│   ├── index.js
│   └── style-index.css
├── node_modules
├── my-slider.php  //PHP 側でブロックを登録するプラグインファイル
├── package-lock.json
├── package.json
├── readme.txt
└── src  //開発用ディレクトリ(この中のファイルを編集)
    ├── edit.js  //edit 関数を記述するファイル
    ├── editor.scss  //エディター用スタイル
    ├── index.js  //ブロック用スクリプト(エントリーポイント)
    ├── save.js  //save 関数を記述するファイル
    └── style.scss  //フロントエンド及びエディターに適用するスタイル

開発モード

ターミナルで作成されたプラグインのディレクトリ(この例では my-slider)に移動して npm start を実行して開発モードで作業をしていきます。

開発モードの場合、ファイルの変更が自動的に検知され、ビルドが実行されるので、毎回ビルドコマンドを実行する必要がありません。何らかの理由でビルドが失敗するとターミナル及びコンソールにエラーが表示されます。

開発モードを終了するには control + c を押します。

$ cd my-slider //作成されたプラグインのディレクトリに移動

$ npm start  //開発モード

開発が完了して procuction ビルドするには npm run build を実行します。

MediaUpload を使ってブロックを作成

生成されたファイルを使って複数の画像をアップロードできるブロックを作成します。

スライダーには複数の画像を表示するので、複数の画像をアップロードして、画像の追加や削除、順番なども入れ替えられるようにする必要があります。

そのためには MediaUpload コンポーネントの multiple と gallery プロパティを true にします。

MediaUpload コンポーネントを使って複数の画像を挿入する方法の詳細については以下を御覧ください。

以下がブロックのコードです。コードの詳細は上記リンクを参照ください。

以下はエントリーポイントのファイルで、edit 関数と save 関数はインポートしています。

src/index.js
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import save from './save';

registerBlockType( 'wdl/my-slider', {
  title: 'My Slider',
  description: 'Example block written with ESNext standard and JSX support',
  category: 'widgets',
  icon: 'smiley',
  supports: {
    html: false,
  },
  //属性を設定
  attributes: {
    //属性 mediaID(メディア ID の配列)
    mediaID: {
      type: 'array',
      default: []
    },
    //img の src に指定する URL
    imageUrl: {
      type: 'array',
      default: []
    },
    //img の alt 属性の値
    imageAlt: {
      type: 'array',
      default: []
    },
  },
  edit: Edit,
  save,
} );

以下は edit 関数が記述された Edit コンポーネントの JavaScript ファイルです。

src/edit.js
import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
import './editor.scss';

export default function Edit( props ) {
  //分割代入を使って props 経由でプロパティを変数に代入
  const { className, attributes, setAttributes} = props;

  //選択された画像の情報を更新する関数(media は画像オブジェクトの配列)
  const onSelectImage = ( media ) => {
    // media から map で id プロパティの配列を生成
    const media_ID = media.map((image) => image.id);
    // media から map で url プロパティの配列を生成
    const imageUrl = media.map((image) => image.url);
    // media から map で alt プロパティの配列を生成
    const imageAlt = media.map((image) => image.alt);

    setAttributes( {
      mediaID: media_ID,  //メディア ID の配列
      imageUrl: imageUrl,  // URL の配列
      imageAlt: imageAlt,  // alt 属性の配列
    } );
  };

  //URL の配列から画像を生成
  const getImages = ( urls ) => {
    let imagesArray = urls.map(( url ) => {
      return (
        <img
          src={ url }
          className="image"
          alt="アップロード画像"
        />
      );
    });
    return imagesArray;
  }

  //メディアライブラリを開くボタンをレンダリングする関数(上記関数を使って画像をレンダリング)
  const getImageButton = (open) => {
    if(attributes.imageUrl.length > 0 ) {
      return (
        <div onClick={ open } className="block-container">
         { getImages( attributes.imageUrl ) }
        </div>
      )
    }
    else {
      return (
        <div className="button-container">
          <Button
            onClick={ open }
            className="button button-large"
          >
            画像をアップロード
          </Button>
        </div>
      );
    }
  }

  //画像を削除する(メディアをリセットする)関数
  const removeMedia = () => {
    setAttributes({
      mediaID: [],
      imageUrl: [],
      imageAlt: [],
    });
  }

  return (
    <div className={ className }>
      <MediaUploadCheck>
        <MediaUpload
          multiple={ true }
          gallery={ true }
          onSelect={ onSelectImage }
          allowedTypes={ ['image'] }
          value={ attributes.mediaID }
          render={ ({ open }) => getImageButton( open ) }
        />
      </MediaUploadCheck>
      { attributes.imageUrl.length != 0  &&   // imageUrl(配列の長さ)で判定
        <MediaUploadCheck>
          <Button
            onClick={removeMedia}
            isLink
            isDestructive
            className="removeImage">画像を削除
          </Button>
        </MediaUploadCheck>
      }
    </div>
  );
}

以下は save 関数が記述された JavaScript ファイルです。

src/save.js
import { Fragment } from '@wordpress/element';
export default function save( { attributes } ) {
  //画像をレンダリングする関数
  const getImagesSave = ( url, alt ) => {
    let image_elem;
    let imagesArray = [];

    for( let i = 0 ; i < url.length; i ++ ) {
      if( url.length === 0 ) {
        image_elem = null;
      }else{
        if( alt[i] ) {
          image_elem =  (
            <img
              className="card_image"
              src={ url[i] }
              alt={ alt[i] }
            />
          );
        }else{
          image_elem = (
            <img
              className="card_image"
              src={ url[i] }
              alt=""
              aria-hidden="true"
            />
          );
        }
      }
      imagesArray.push( image_elem ) ;
    }
    return imagesArray;
  }

  return (
    <div className="block-container">
     { getImagesSave( attributes.imageUrl, attributes.imageAlt ) }
    </div>
  );
}

以下はフロントエンド及びエディターのスタイルを設定する Sass ファイルです。この時点では挿入した画像は最大幅を設定し Flexbox で表示しています(後で変更)。

src/style.scss
.wp-block-wdl-my-slider {
  background-color: #fefefe;
  color: #666;
  padding: 10px;
  border: 1px solid #ccc;
}

.block-container {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
}

.block-container img {
  width: 100%;
  max-width: 160px;
  margin: 10px;
}

以下はエディターのスタイルを設定する Sass ファイルです。

src/editor.scss
.image {
  cursor: pointer;
}

プラグインの有効化(確認)

プラグインページでブロックのプラグインを有効化して確認します。

投稿にブロックを挿入するとエディタ側では以下のように表示されます。

「画像をアップロード」をクリックするとモーダルが表示され、画像を選択することができます。

「ギャラリーを編集」では画像を追加したり、順番を並べ替えることができます。選択した画像をブロックに挿入するには「ギャラリーを挿入」をクリックします。

編集画面に挿入された画像は画像をクリックすれば追加や編集ができます。「画像を削除」をクリックすると全ての画像が削除されます。

投稿を保存すると、フロントエンド側では以下のような表示になります。

スライダープラグイン Swiper

この例では、スライダーの表示には Swiper という JavaScript のプラグインを使用します。

Swiper の使い方の詳細は以下を御覧ください。

この例では Swiper のダウンロードページの swiper-bundle.min.js と swiper-bundle.min.css をコピーして使用しています。この時点の最新版は 6.3.2 ですが、右上のセレクトメニューでバージョンを指定することもできます。

ファイル ソースコード タイプ この例でのファイル名
swiper-bundle.min.js RAW JavaScript swiper.js
swiper-bundle.min.css RAW CSS swiper.css

ブロックのプラグインのフォルダ(my-slider)の中に assets というフォルダを作成し、その中にコピーしたファイルとスライダーを初期化するための JavaScript ファイル(init-swiper.js)を保存します。

ファイル名は上記表に記載されている「この例でのファイル名」に変更しています。

Swiper を初期化するためのファイル

以下は Swiper を初期化するためのファイル init-swiper.js の例です。

init-swiper.js
let mySwiper = new Swiper ('.swiper-container', {

  //スライダーを自動再生する場合は以下のコメントを外す
  /*autoplay: {
    delay: 4000, //4秒間隔でスライドを自動的に実行
  },*/

  //最後に達したら先頭に戻る
  loop: true,

  //ページネーション表示の設定
  pagination: {
    el: '.swiper-pagination', //ページネーションの要素
    type: 'bullets', //ページネーションの種類
    clickable: true, //クリックに反応させる
  },

  //ナビゲーションボタン(矢印)表示の設定
  navigation: {
    nextEl: '.swiper-button-next', //「次へボタン」要素の指定
    prevEl: '.swiper-button-prev', //「前へボタン」要素の指定
  },

  //スクロールバー表示の設定
  scrollbar: {
    el: '.swiper-scrollbar', //要素の指定
  },
})

以下は Swiper のサイトからコピーした JavaScript と CSS ファイルの冒頭部分です。

swiper.js(swiper-bundle.min.js)
/**
 * Swiper 6.3.2
 * Most modern mobile touch slider and framework with hardware accelerated transitions
 * http://swiperjs.com
 *
 * Copyright 2014-2020 Vladimir Kharlampidi
 *
 * Released under the MIT License
 *
 * Released on: September 28, 2020
 */

!function(e,t){"object"==typeof ・・・以下省略・・・
swiper.css(swiper-bundle.min.css)
@charset "UTF-8"; /* 追加 */
/**
 * Swiper 6.3.2
 * Most modern mobile touch slider and framework with hardware accelerated transitions
 * http://swiperjs.com
 *
 * Copyright 2014-2020 Vladimir Kharlampidi
 *
 * Released under the MIT License
 *
 * Released on: September 28, 2020
 */

@font-face{font-family:swiper-icons; ・・・以下省略・・・

スライダーのスクリプトの読み込み

assets フォルダに保存したスライダーのスクリプトとスタイルを PHP でブロックを登録するファイル(my-slider.php)で wp_enqueue_scriptwp_enqueue_style を使って、enqueue_block_assets アクションで読み込みます。

以下を my-slider.php に追加します。

この例の場合、エディター側ではスライダーは表示しないので ! is_admin() で判定してフロントエンド側でのみ読み込むようにします。

function add_my_slider_scripts_and_styles() {
  $dir = dirname( __FILE__ );

  //管理画面以外(フロントエンド側でのみ読み込む)
  if(! is_admin()) {
    //Swiper の JavaScript ファイルの読み込み(エンキュー)
    wp_enqueue_script(
      'swiper-slider',
      plugins_url( '/assets/swiper.js', __FILE__ ),
      array(),
      filemtime( "$dir/assets/swiper.js" ),
      true
    );

    //Swiper を初期化するためのファイルの読み込み(エンキュー)
    wp_enqueue_script(
      'swiper-slider-init',
      plugins_url( '/assets/init-swiper.js', __FILE__ ),
      //依存ファイルに上記 Swiper の JavaScript を指定
      array('swiper-slider'),
      filemtime( "$dir/assets/init-swiper.js" ),
      true
    );

    //Swiper の CSS ファイルの読み込み(エンキュー)
    wp_enqueue_style(
      'swipe-style',
      plugins_url( '/assets/swiper.css', __FILE__ ),
      array(),
      filemtime( "$dir/assets/swiper.css"  )
    );
  }

}
add_action('enqueue_block_assets', 'add_my_slider_scripts_and_styles');
my-slider.php
<?php
/**
 * Plugin Name:     My Slider
 * Description:     Example block written with ESNext standard and JSX support – build step required.
 * Version:         0.1.0
 * Author:          The WordPress Contributors
 * License:         GPL-2.0-or-later
 * License URI:     https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:     my-slider
 *
 * @package         wdl
 */

function wdl_my_slider_block_init() {
  $dir = dirname( __FILE__ );

  $script_asset_path = "$dir/build/index.asset.php";
  if ( ! file_exists( $script_asset_path ) ) {
    throw new Error(
      'You need to run `npm start` or `npm run build` for the "wdl/my-slider" block first.'
    );
  }
  $index_js     = 'build/index.js';
  $script_asset = require( $script_asset_path );
  wp_register_script(
    'wdl-my-slider-block-editor',
    plugins_url( $index_js, __FILE__ ),
    $script_asset['dependencies'],
    $script_asset['version']
  );

  $editor_css = 'build/index.css';
  wp_register_style(
    'wdl-my-slider-block-editor',
    plugins_url( $editor_css, __FILE__ ),
    array(),
    filemtime( "$dir/$editor_css" )
  );

  $style_css = 'build/style-index.css';
  wp_register_style(
    'wdl-my-slider-block',
    plugins_url( $style_css, __FILE__ ),
    array(),
    filemtime( "$dir/$style_css" )
  );

  register_block_type( 'wdl/my-slider', array(
    'editor_script' => 'wdl-my-slider-block-editor',
    'editor_style'  => 'wdl-my-slider-block-editor',
    'style'         => 'wdl-my-slider-block',
  ) );
}
add_action( 'init', 'wdl_my_slider_block_init' );

function add_my_slider_scripts_and_styles() {
  $dir = dirname( __FILE__ );

  //Swiper の JavaScript ファイルの読み込み(エンキュー)
  wp_enqueue_script(
    'swiper-slider',
    plugins_url( '/assets/swiper.js', __FILE__ ),
    array(),
    filemtime( "$dir/assets/swiper.js" ),
    true
  );

  //Swiper を初期化するためのファイルの読み込み(エンキュー)
  wp_enqueue_script(
    'swiper-slider-init',
    plugins_url( '/assets/init-swiper.js', __FILE__ ),
    //依存ファイルに上記 Swiper の JavaScript を指定
    array('swiper-slider'),
    filemtime( "$dir/assets/init-swiper.js" ),
    true
  );

  //Swiper の CSS ファイルの読み込み(エンキュー)
  wp_enqueue_style(
    'swipe-style',
    plugins_url( '/assets/swiper.css', __FILE__ ),
    array(),
    filemtime( "$dir/assets/swiper.css"  )
  );

}
add_action('enqueue_block_assets', 'add_my_slider_scripts_and_styles');

Swiper のマークアップ

以下は Swiper でスライダーを表示するための基本的なマークアップの例です。

save 関数でレンダリングする際に、以下の構造を記述すればスライダーが表示されます。

<!-- スライダーのメインのコンテナー -->
<div class="swiper-container">

  <!-- スライダーのラッパー -->
  <div class="swiper-wrapper">
    <!-- スライド .swiper-slide の中に画像を配置 -->
    <div class="swiper-slide"><img src="images/sample_01.jpg" alt=""></div>
    <div class="swiper-slide"><img src="images/sample_02.jpg" alt=""></div>
    <div class="swiper-slide"><img src="images/sample_03.jpg" alt=""></div>
  </div>

  <!-- ページネーションの表示 -->
  <div class="swiper-pagination"></div>

  <!-- ナビゲーションボタンの表示 -->
  <div class="swiper-button-prev"></div>
  <div class="swiper-button-next"></div>

  <!-- スクロールバーの表示 -->
  <div class="swiper-scrollbar"></div>
</div>

save 関数

save 関数(save.js)を Swiper のマークアップで書き換えます。

画像をレンダリングする関数 getImagesSave() では Swiper のマークアップに合わせて img をレンダリングする際に、swiper-slide クラスを指定した div でラップします。

そして return ステートメントでは Swiper の構造でマークアップします。但し、class は className とする必要があります。

src/save.js
export default function save( { attributes } ) {
  //画像をレンダリングする関数
  const getImagesSave = ( url, alt ) => {
    let image_elem;
    let imagesArray = [];

    for( let i = 0 ; i < url.length; i ++ ) {
      if( url.length === 0 ) {
        image_elem = null;
      }else{
        if( alt[i] ) {
          image_elem =  (
            <div className="swiper-slide">
              <img
                className="card_image"
                src={ url[i] }
                alt={ alt[i] }
              />
            </div>
          );
        }else{
          image_elem = (
            <div className="swiper-slide">
              <img
                className="card_image"
                src={ url[i] }
                alt=""
                aria-hidden="true"
              />
            </div>
          );
        }
      }
      imagesArray.push( image_elem ) ;
    }
    return imagesArray;
  }

  return (
    <div className="slider-container">
      <div className="swiper-container">
        <div className="swiper-wrapper">
          { getImagesSave( attributes.imageUrl, attributes.imageAlt ) }
        </div>
        <div className="swiper-pagination"></div>
        <div className="swiper-button-prev"></div>
        <div className="swiper-button-next"></div>
        <div className="swiper-scrollbar"></div>
      </div>
    </div>
  );
}

スタイルを設定

今まで設定していたスタイルは、複数の画像を Flexbox で表示するためのものだったので、フロントエンド側ではスライダーを表示するように変更します。

style.scss は以下のように Flexbox の設定を削除します。.wp-block-wdl-my-slider は 自動的にブロックに付与されるクラス です。

src/style.scss
.wp-block-wdl-my-slider {
  background-color: #fefefe;
  color: #666;
  padding: 10px;
  border: 1px solid #ccc;
}

また、ナビゲーションボタンやページネーションの色を設定するには以下を追加します。以下はナビゲーションボタンとページネーションの色を白(#fff)にする例です。

src/style.scss
:root {
  --swiper-navigation-color: #fff;
  --swiper-pagination-color: #fff;
}

editor.scss ではエディタ側では画像を Flexbox で表示するようにします。

src/editor.scss
.wp-block-wdl-my-slider .slider-block-container {
  display: flex;
  flex-wrap: wrap;
}

.wp-block-wdl-my-slider .slider-block-container img {
  width: 100%;
  max-width: 160px;
  margin: 10px;
}

.image {
  cursor: pointer;
}

これでフロントエンド側ではスライダーが表示されるようになります。

妥当性検証プロセス

save 関数を書き換えてビルドして、投稿のページで再読込すると「このブロックには、想定されていないか無効なコンテンツが含まれています」と表示され、コンソールにはエラーが表示されます。

これは、エディターが現在定義しているものとは異なる save 関数の出力を検出するために発生しますが、表示されるボタンをクリックして「ブロックを削除」を選択し、現在のブロックを一度削除してから再度ブロックを挿入すれば問題ありません。

再度ブロックを挿入して、画像をブロックに挿入して投稿を保存するとフロントエンド側ではスライダーが表示されます。以下は style.scss でビゲーションボタンとページネーションの色を変更してあります。

エディター側の表示は変更はありません。

画像をクリックして、スライダーに表示する画像をギャラリー機能を使って追加・削除したり、順番を並べ替えたりすることができます。

スライダーのオプションの追加

インスペクターにトグルボタンなどを配置して、スライダーのナビゲーションボタンやページネーションなどの表示・非表示を制御することができます。

以下は、スライダー Swiper のナビゲーションボタン、ページネーション、スクロールバーの表示・非表示を切り替えるトグルボタンをインスペクターに表示する例です。

attributes の追加

ナビゲーションボタン、ページネーション、スクロールバーの表示・非表示の状態を保持する属性を registerBlockType に追加します。

src/index.js
registerBlockType( 'wdl/my-slider', {
  title: 'My Slider',
  ・・・中略・・・
  attributes: {
    ・・・中略・・・
    //ナビゲーションボタンの表示・非表示
    showNavigationButton: {
      type: 'boolean',
      default: true
    },
    //ページネーションの表示・非表示
    showPagination: {
      type: 'boolean',
      default: true
    },
    //ページネーションの表示・非表示
    showScrollbar: {
      type: 'boolean',
      default: true
    }
  },
  edit: Edit,
  save,
} );

インスペクターの追加

Edit コンポーネント(edit.js)にインスペクターの記述を追加します。

インスペクターの設定の詳細は「インスペクター」を御覧ください。

この例ではインスペクターを追加する関数を作成してナビゲーションボタン、ページネーション、スクロールバーの表示・非表示を選択するトグルボタンを設置します。

InspectorControls、PanelBody、PanelRow、ToggleControl コンポーネントをインポートします。

import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, PanelRow, ToggleControl } from '@wordpress/components';

ToggleControl コンポーネントの onChange プロパティのコールバック関数は checked の値(真偽値)を引数に取るので、選択状態が変更されたら setAttributes を使って属性の値を更新します。

//インスペクターを追加する関数
const getInspectorControls = () => {
  return (
    <InspectorControls>
      <PanelBody
        title='Slider Settings'
        initialOpen={true}
      >
        <PanelRow>
          <ToggleControl
            label="ナビゲーションボタン"
            checked={attributes.showNavigationButton}
            onChange={(val) => setAttributes({ showNavigationButton: val })}
          />
        </PanelRow>
        <PanelRow>
          <ToggleControl
            label="ページネーション"
            checked={attributes.showPagination}
            onChange={(val) => setAttributes({ showPagination: val })}
          />
        </PanelRow>
        <PanelRow>
          <ToggleControl
            label="スクロールバー"
            checked={attributes.showScrollbar}
            onChange={(val) => setAttributes({ showScrollbar: val })}
          />
        </PanelRow>
      </PanelBody>
    </InspectorControls>
  );
}

return ステートメントの書き換え

return ステートメントでは配列を使ってインスペクターと MediaUpload コンポーネントをレンダリングします。

return (
  [
    getInspectorControls(),  //インスペクター
    <div className={ className }>
    <MediaUploadCheck>
      <MediaUpload
        multiple={ true }
        gallery={ true }
        onSelect={ onSelectImage }
        allowedTypes={ ['image'] }
        value={ attributes.mediaID }
        render={ ({ open }) => getImageButton( open ) }
      />
    </MediaUploadCheck>
    { attributes.imageUrl.length != 0  &&   // imageUrl(配列の長さ)で判定
      <MediaUploadCheck>
        <Button
          onClick={removeMedia}
          isLink
          isDestructive
          className="removeImage">画像を削除
        </Button>
      </MediaUploadCheck>
    }
  </div>
  ]
);
src/edit.js
import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, PanelRow, ToggleControl } from '@wordpress/components';
import './editor.scss';

export default function Edit( props ) {
  //分割代入を使って props 経由でプロパティを変数に代入
  const { className, attributes, setAttributes} = props;

  //選択された画像の情報を更新する関数
  const onSelectImage = ( media ) => {
    // media から map で id プロパティの配列を生成
    const media_ID = media.map((image) => image.id);
    // media から map で url プロパティの配列を生成
    const imageUrl = media.map((image) => image.url);
    // media から map で alt プロパティの配列を生成
    const imageAlt = media.map((image) => image.alt);

    setAttributes( {
      mediaID: media_ID,  //メディア ID の配列
      imageUrl: imageUrl,  // URL の配列
      imageAlt: imageAlt,  // alt 属性の配列
    } );
  };

  //URL の配列から画像を生成
  const getImages = ( urls ) => {
    let imagesArray = urls.map(( url ) => {
      return (
        <img
          src={ url }
          className="image"
          alt="アップロード画像"
        />
      );
    });
    return imagesArray;
  }

  //メディアライブラリを開くボタンをレンダリングする関数(上記関数を使って画像をレンダリング)
  const getImageButton = (open) => {
    if(attributes.imageUrl.length > 0 ) {
      return (
        <div onClick={ open } className="block-container">
         { getImages( attributes.imageUrl ) }
        </div>
      )
    }
    else {
      return (
        <div className="button-container">
          <Button
            onClick={ open }
            className="button button-large"
          >
            画像をアップロード
          </Button>
        </div>
      );
    }
  }

  //画像を削除する(メディアをリセットする)関数
  const removeMedia = () => {
    setAttributes({
      mediaID: [],
      imageUrl: [],
      imageAlt: [],
    });
  }

  //インスペクターを追加する関数
  const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody
          title='Slider Settings'
          initialOpen={true}
        >
          <PanelRow>
            <ToggleControl
              label="ナビゲーションボタン"
              checked={attributes.showNavigationButton}
              onChange={(val) => setAttributes({ showNavigationButton: val })}
            />
          </PanelRow>
          <PanelRow>
            <ToggleControl
              label="ページネーション"
              checked={attributes.showPagination}
              onChange={(val) => setAttributes({ showPagination: val })}
            />
          </PanelRow>
          <PanelRow>
            <ToggleControl
              label="スクロールバー"
              checked={attributes.showScrollbar}
              onChange={(val) => setAttributes({ showScrollbar: val })}
            />
          </PanelRow>
        </PanelBody>
      </InspectorControls>
    );
  }

  return (
    [
      getInspectorControls(),  //インスペクター
      <div className={ className }>
      <MediaUploadCheck>
        <MediaUpload
          multiple={ true }
          gallery={ true }
          onSelect={ onSelectImage }
          allowedTypes={ ['image'] }
          value={ attributes.mediaID }
          render={ ({ open }) => getImageButton( open ) }
        />
      </MediaUploadCheck>
      { attributes.imageUrl.length != 0  &&   // imageUrl(配列の長さ)で判定
        <MediaUploadCheck>
          <Button
            onClick={removeMedia}
            isLink
            isDestructive
            className="removeImage">画像を削除
          </Button>
        </MediaUploadCheck>
      }
    </div>
    ]
  );
}

save 関数の書き換え

save 関数ではトグルボタンの状態(属性の値)により、return ステートメントでナビゲーションボタンなどをレンダリングするかどうかを判定します。

それぞれの属性の値は真偽値なので、以下のようにその値が true の場合にレンダリングするようにします。

return (
  <div className="slider-container">
    <div className="swiper-container">
      <div className="swiper-wrapper">
        { getImagesSave( attributes.imageUrl, attributes.imageAlt ) }
      </div>
      { attributes.showPagination &&
        <div className="swiper-pagination"></div>
      }
      { attributes.showNavigationButton &&
        <Fragment>
          <div className="swiper-button-prev"></div>
          <div className="swiper-button-next"></div>
        </Fragment>
      }
      { attributes.showScrollbar &&
        <div className="swiper-scrollbar"></div>
      }
    </div>
  </div>
);

上記 12〜13行目の2つの div 要素は Fragment でラップする必要があるので、このファイルの先頭でインポートします。

import { Fragment } from '@wordpress/element';
src/save.js
import { Fragment } from '@wordpress/element';

export default function save( { attributes } ) {
  //画像をレンダリングする関数
  const getImagesSave = ( url, alt ) => {
    let image_elem;
    let imagesArray = [];

    for( let i = 0 ; i < url.length; i ++ ) {
      if( url.length === 0 ) {
        image_elem = null;
      }else{
        if( alt[i] ) {
          image_elem =  (
            <div className="swiper-slide">
              <img
                className="card_image"
                src={ url[i] }
                alt={ alt[i] }
              />
            </div>
          );
        }else{
          image_elem = (
            <div className="swiper-slide">
              <img
                className="card_image"
                src={ url[i] }
                alt=""
                aria-hidden="true"
              />
            </div>
          );
        }
      }
      imagesArray.push( image_elem ) ;
    }
    return imagesArray;
  }

  return (
    <div className="slider-container">
      <div className="swiper-container">
        <div className="swiper-wrapper">
          { getImagesSave( attributes.imageUrl, attributes.imageAlt ) }
        </div>
        { attributes.showPagination &&
          <div className="swiper-pagination"></div>
        }
        { attributes.showNavigationButton &&
          <Fragment>
            <div className="swiper-button-prev"></div>
            <div className="swiper-button-next"></div>
          </Fragment>
        }
        { attributes.showScrollbar &&
          <div className="swiper-scrollbar"></div>
        }
      </div>
    </div>
  );
}

これでエディター画面のインスペクターのグルボタンでナビゲーションボタンなどの表示・非表示を切り替えることができます。

キャプションの追加

画像にキャプションが設定されていれば、スライダーにキャプションを表示する例です。

画像のキャプションの値を保存する属性 imageCaption を registerBlockType に追加します。また、キャプションの表示・非表示もインスペクターのトグルボタンで切り替えられるようにするので属性 showCaption も追加します。

//img の キャプションの値
imageCaption: {
  type: 'array',
  default: []
},
//キャプションの表示・非表示
showCaption: {
  type: 'boolean',
  default: true
},
src/index.js
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import save from './save';

registerBlockType( 'wdl/my-slider', {
  title: 'My Slider',
  description: 'Example block written with ESNext standard and JSX support',
  category: 'widgets',
  icon: 'smiley',
  supports: {
    html: false,
  },
  //属性を設定
  attributes: {
    //属性 mediaID(メディア ID の配列)
    mediaID: {
      type: 'array',
      default: []
    },
    //img の src に指定する URL
    imageUrl: {
      type: 'array',
      default: []
    },
    //img の alt 属性の値
    imageAlt: {
      type: 'array',
      default: []
    },
    //ナビゲーションボタンの表示・非表示
    showNavigationButton: {
      type: 'boolean',
      default: true
    },
    //ページネーションの表示・非表示
    showPagination: {
      type: 'boolean',
      default: true
    },
    //ページネーションの表示・非表示
    showScrollbar: {
      type: 'boolean',
      default: true
    },
    //img の キャプションの値
    imageCaption: {
      type: 'array',
      default: []
    },
    //キャプションの表示・非表示
    showCaption: {
      type: 'boolean',
      default: true
    },
  },
  edit: Edit,
  save,
} );

Edit コンポーネント(edit.js)で属性 imageCaption の値を更新する記述を追加します。

//選択された画像の情報を更新する関数
const onSelectImage = ( media ) => {
  const media_ID = media.map((image) => image.id);
  const imageUrl = media.map((image) => image.url);
  const imageAlt = media.map((image) => image.alt);
  // media から map で caption プロパティの配列を生成
  const imageCaption = media.map((image) => image.caption);

  setAttributes( {
    mediaID: media_ID,
    imageUrl: imageUrl,
    imageAlt: imageAlt,
    imageCaption: imageCaption,  // キャプションの配列
  } );
};

インスペクターをレンダリングする関数にキャプションのトグルボタンを追加します。

//インスペクターを追加する関数
const getInspectorControls = () => {
  return (
    <InspectorControls>
      <PanelBody
        title='Slider Settings'
        initialOpen={true}
      >
        ・・・中略・・・
        <PanelRow>
          <ToggleControl
            label="キャプション"
            checked={attributes.showCaption}
            onChange={(val) => setAttributes({ showCaption: val })}
          />
        </PanelRow>
      </PanelBody>
    </InspectorControls>
  );
}

figure と figcaption を使って画像とキャプションを生成する関数 getImagesWithCaption() を追加します。

//URL とキャプションの配列から画像をキャプション付きで生成
const getImagesWithCaption = ( url, caption ) => {
  let imagesArray = [];
  for(let i = 0 ; i < url.length; i ++) {
    imagesArray.push (
      <figure>
        <img
          src={ url[i] }
          className="image"
          alt="アップロード画像"
        />
        <figcaption className="block-image-caption">
          { caption[i] ? caption[i] : '' }
        </figcaption>
      </figure>
    );
  }
  return imagesArray;
}

メディアライブラリを開くボタンをレンダリングする関数 getImageButton() の画像をレンダリングする部分を、画像のみかキャプション付きの画像をレンダリングするかを属性 showCaption の値で判定して切り替えます。

const getImageButton = (open) => {
  if(attributes.imageUrl.length > 0 ) {
    return (
      <div onClick={ open } className="slider-block-container">
       { attributes.showCaption ?
           getImagesWithCaption( attributes.imageUrl, attributes.imageCaption ) :
           getImages( attributes.imageUrl ) }
      </div>
    )
  }
  else {
    return (
      <div className="button-container">
        <Button
          onClick={ open }
          className="button button-large"
        >
          画像をアップロード
        </Button>
      </div>
    );
  }
};

画像を削除する(メディアをリセットする)関数にキャプションの属性 imageCaption をリセットする記述を追加します。

//画像を削除する(メディアをリセットする)関数
const removeMedia = () => {
  setAttributes({
    mediaID: [],
    imageUrl: [],
    imageAlt: [],
    imageCaption: [],
  });
}
src/edit.js
import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, PanelRow, ToggleControl } from '@wordpress/components';
import './editor.scss';

export default function Edit( props ) {
  //分割代入を使って props 経由でプロパティを変数に代入
  const { className, attributes, setAttributes} = props;

  //選択された画像の情報を更新する関数
  const onSelectImage = ( media ) => {
    // media から map で id プロパティの配列を生成
    const media_ID = media.map((image) => image.id);
    // media から map で url プロパティの配列を生成
    const imageUrl = media.map((image) => image.url);
    // media から map で alt プロパティの配列を生成
    const imageAlt = media.map((image) => image.alt);
    // media から map で caption プロパティの配列を生成
    const imageCaption = media.map((image) => image.caption);

    setAttributes( {
      mediaID: media_ID,  //メディア ID の配列
      imageUrl: imageUrl,  // URL の配列
      imageAlt: imageAlt,  // alt 属性の配列
      imageCaption: imageCaption,  // キャプションの配列
    } );
  };

  //URL の配列から画像を生成
  const getImages = ( urls ) => {
    let imagesArray = urls.map(( url ) => {
      return (
        <img
          src={ url }
          className="image"
          alt="アップロード画像"
        />
      );
    });
    return imagesArray;
  }

  //URL とキャプションの配列から画像をキャプション付きで生成(for 文に変更)
  const getImagesWithCaption = ( url, caption ) => {
    let imagesArray = [];
    for(let i = 0 ; i < url.length; i ++) {
      imagesArray.push (
        <figure>
          <img
            src={ url[i] }
            className="image"
            alt="アップロード画像"
          />
          <figcaption className="block-image-caption">
            { caption[i] ? caption[i] : '' }
          </figcaption>
        </figure>
      );
    }
    return imagesArray;
  }

  //メディアライブラリを開くボタンをレンダリングする関数(上記関数を使って画像をレンダリング)
  const getImageButton = (open) => {
    if(attributes.imageUrl.length > 0 ) {
      return (
        <div onClick={ open } className="slider-block-container">
         { attributes.showCaption ?
             getImagesWithCaption( attributes.imageUrl, attributes.imageCaption ) :
             getImages( attributes.imageUrl ) }
        </div>
      )
    }
    else {
      return (
        <div className="button-container">
          <Button
            onClick={ open }
            className="button button-large"
          >
            画像をアップロード
          </Button>
        </div>
      );
    }
  };

  //画像を削除する(メディアをリセットする)関数
  const removeMedia = () => {
    setAttributes({
      mediaID: [],
      imageUrl: [],
      imageAlt: [],
      imageCaption: [],
    });
  }

  //インスペクターを追加する関数
  const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody
          title='Slider Settings'
          initialOpen={true}
        >
          <PanelRow>
            <ToggleControl
              label="ナビゲーションボタン"
              checked={attributes.showNavigationButton}
              onChange={(val) => setAttributes({ showNavigationButton: val })}
            />
          </PanelRow>
          <PanelRow>
            <ToggleControl
              label="ページネーション"
              checked={attributes.showPagination}
              onChange={(val) => setAttributes({ showPagination: val })}
            />
          </PanelRow>
          <PanelRow>
            <ToggleControl
              label="スクロールバー"
              checked={attributes.showScrollbar}
              onChange={(val) => setAttributes({ showScrollbar: val })}
            />
          </PanelRow>
          <PanelRow>
            <ToggleControl
              label="キャプション"
              checked={attributes.showCaption}
              onChange={(val) => setAttributes({ showCaption: val })}
            />
          </PanelRow>
        </PanelBody>
      </InspectorControls>
    );
  }

  return (
    [
      getInspectorControls(),  //インスペクター
      <div className={ className }>
      <MediaUploadCheck>
        <MediaUpload
          multiple={ true }
          gallery={ true }
          onSelect={ onSelectImage }
          allowedTypes={ ['image'] }
          value={ attributes.mediaID }
          render={ ({ open }) => getImageButton( open ) }
        />
      </MediaUploadCheck>
      { attributes.imageUrl.length != 0  &&   // imageUrl(配列の長さ)で判定
        <MediaUploadCheck>
          <Button
            onClick={removeMedia}
            isLink
            isDestructive
            className="removeImage">画像を削除
          </Button>
        </MediaUploadCheck>
      }
    </div>
    ]
  );
}

save 関数の画像をレンダリングする関数 getImagesSave() で属性 showCaption の値が true ならキャプションのレンダリングを追加します。

return ステートメントの getImagesSave() の引数にキャプションの属性(attributes.imageCaption)を追加します。

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

export default function save( { attributes } ) {
  //画像をレンダリングする関数
  const getImagesSave = ( url, alt, caption ) => {
    let image_elem;
    let imagesArray = [];

    for( let i = 0 ; i < url.length; i ++ ) {
      if( url.length === 0 ) {
        image_elem = null;
      }else{
        if( alt[i] ) {
          image_elem =  (
            <div className="swiper-slide">
              <img
                className="card_image"
                src={ url[i] }
                alt={ alt[i] }
              />
              { attributes.showCaption &&
                <div class="caption">{ caption[i] ? caption[i] : "" }</div>
              }
            </div>
          );
        }else{
          image_elem = (
            <div className="swiper-slide">
              <img
                className="card_image"
                src={ url[i] }
                alt=""
                aria-hidden="true"
              />
              { attributes.showCaption &&
                <div class="caption">{ caption[i] ? caption[i] : "" }</div>
              }
            </div>
          );
        }
      }
      imagesArray.push( image_elem ) ;
    }
    return imagesArray;
  }

  return (
    <div className="slider-container">
      <div className="swiper-container">
        <div className="swiper-wrapper">
          { getImagesSave( attributes.imageUrl, attributes.imageAlt, attributes.imageCaption ) }
        </div>
        { attributes.showPagination &&
          <div className="swiper-pagination"></div>
        }
        { attributes.showNavigationButton &&
          <Fragment>
            <div className="swiper-button-prev"></div>
            <div className="swiper-button-next"></div>
          </Fragment>
        }
        { attributes.showScrollbar &&
          <div className="swiper-scrollbar"></div>
        }
      </div>
    </div>
  );
}

キャプションをオンにした場合の表示

キャプションをオフにした場合の表示

エディター画面のインスペクターでキャプションをオンにするとフロントエンド側ではキャプションを表示します。キャプションの位置などのスタイルは style.scss で設定することができます。

PHP でレンダリング

以下はブロックをダイナミックブロックに変更して PHP でレンダリングする例です。

PHP でレンダリングする場合、PHP 側の出力(マークアップ)を変更しても妥当性検証プロセスは行われないので、マークアップを変更する際に発生するブロックを一度削除してから再度ブロックを挿入しなければならない問題を回避できます。

そのため、例えばブロックにタイトルを追加すれば、既存のブロック全てにタイトルが追加されます。また、その際、JavaScript 側の再コンパイルは不要です。

edit.js には変更はありません。

関連ページ:ダイナミックブロックの作り方

save 関数で null を返す

ダイナミックブロックでは save 関数が null を返すようにします。

src/index.js 抜粋
registerBlockType( 'wdl/my-slider', {
  title: 'My Slider',
  description: 'Example block written with ESNext standard and JSX support',
  category: 'widgets',
  icon: 'smiley',
  supports: {
    html: false,
  },
  attributes: {
    ・・・中略・・・
  },
  edit: Edit,
  //save 関数で null を返す
  save: () => { return null }
} );
src/index.js
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import save from './save';

registerBlockType( 'wdl/my-slider', {
  title: 'My Slider',
  description: 'Example block written with ESNext standard and JSX support',
  category: 'widgets',
  icon: 'smiley',
  supports: {
    html: false,
  },
  //属性を設定
  attributes: {
    //属性 mediaID(メディア ID の配列)
    mediaID: {
      type: 'array',
      default: []
    },
    //img の src に指定する URL
    imageUrl: {
      type: 'array',
      default: []
    },
    //img の alt 属性の値
    imageAlt: {
      type: 'array',
      default: []
    },
    //ナビゲーションボタンの表示・非表示
    showNavigationButton: {
      type: 'boolean',
      default: true
    },
    //ページネーションの表示・非表示
    showPagination: {
      type: 'boolean',
      default: true
    },
    //ページネーションの表示・非表示
    showScrollbar: {
      type: 'boolean',
      default: true
    },
    //img の キャプションの値
    imageCaption: {
      type: 'array',
      default: []
    },
    //キャプションの表示・非表示
    showCaption: {
      type: 'boolean',
      default: true
    },
  },
  edit: Edit,
  //save 関数で null を返す
  save: () => { return null }
} );

register_block_type に attributes を追加

register_block_type 関数の第2パラメータの配列に attributes キーを追加して、ブロックから抽出したいすべての属性を設定することができます。register_block_type に attributes を設定する場合は、オブジェクトではなく連想配列で設定する必要があります。

my-slider.php 抜粋
register_block_type( 'wdl/my-slider', array(
  'editor_script' => 'wdl-my-slider-block-editor',
  'editor_style'  => 'wdl-my-slider-block-editor',
  'style'         => 'wdl-my-slider-block',
  //属性を追加
  'attributes' => [
    //属性 mediaID(メディア ID の配列)
    'mediaID' => [
      'type' => 'array',
      'default' => []
    ],
    //属性 imageUrl(URL の配列)
    'imageUrl' => [
      'type' => 'array',
      'default' => []
    ],
    //属性 imageAlt(alt 属性の配列)
    'imageAlt' => [
      'type' => 'array',
      'default' => []
    ],
    //属性 imageCaption(キャプションの配列)
    'imageCaption' => [
      'type' => 'array',
      'default' => []
    ],
    //ナビゲーションボタンの表示・非表示
    'showNavigationButton' => [
      'type' => 'boolean',
      'default' => true
    ],
    //ページネーションの表示・非表示
    'showPagination' => [
      'type' => 'boolean',
      'default' => true
    ],
    //スクロールバーンの表示・非表示
    'showScrollbar' => [
      'type' => 'boolean',
      'default' => true
    ],
    //キャプションの表示・非表示
    'showCaption' => [
      'type' => 'boolean',
      'default' => true
    ],
  ]
) );

render_callback を設定

ブロックの出力を PHP でレンダリングするには、register_block_type 関数の第2パラメータの配列に render_callback キーを追加し、値にコールバック関数(PHP でレンダリングする関数)を指定します。

my-slider.php 抜粋
register_block_type( 'wdl/my-slider', array(
  'editor_script' => 'wdl-my-slider-block-editor',
  'editor_style'  => 'wdl-my-slider-block-editor',
  'style'         => 'wdl-my-slider-block',
  //render_callback を追加
  'render_callback' => 'my_slider_render',
  'attributes' => [
    'mediaID' => [
      'type' => 'array',
      'default' => []
    ],
    ・・・中略・・・
  ]
) );

PHP でスライダーをレンダリングする関数を定義します。

以下はスライダーを PHP でレンダリングするコールバック関数の例です。属性の showCaption が true でキャプションが設定されていればキャプションも表示するようにしています。

render_callback の関数はブロックの出力を return する必要があるので、文字列(HTML)を作成して最後に return します。

外側の div 要素には save 関数では自動的にブロックに付与されるクラス(この例の場合は wp-block-wdl-my-slider)を指定しています。

属性 imageUrl(画像の URL の配列)に含まれる URL を元に for 文でスライダーのマークアップを生成しています。URL、キャプション、alt 属性の値はそれぞれエスケープ処理しています。

my-slider.php 抜粋
function my_slider_render($attributes, $content) {

  //属性 imageUrl が空なら何も表示しない
  if (empty($attributes['imageUrl'])) {
    return '';
  }

  //属性 imageUrl が空でなければスライダーのマークアップを組み立てる
  $output = '<div class="wp-block-wdl-my-slider">';
  $output .= '<div class="swiper-container" '. $slider_options .'>';
  $output .= '<div class="swiper-wrapper">';

  $imageUrl = $attributes['imageUrl'];
  $imageCaption = $attributes['imageCaption'];
  $imageAlt = $attributes['imageAlt'];

  for($i = 0; $i < count($imageUrl); $i ++) {
    $img_url = esc_url($imageUrl[$i]);
    $img_caption = $imageCaption[$i] ? esc_html($imageCaption[$i]): '';
    $img_alt = $imageAlt[$i] ? esc_attr($imageAlt[$i]): '';
    if($img_alt !=="") {
      $output .= '<div class="swiper-slide"><img className="card_image" src="'
                 . $img_url . '" alt="' . $img_alt . '" />';
    }else {
      $output .= '<div class="swiper-slide"><img className="card_image" src="'
                 . $img_url . '" alt="" aria-hidden="true" />';
    }
    //キャプションを表示する場合
    if($attributes['showCaption']) {
      if($img_caption) {
        $output .= '<div class="caption">'. $img_caption . '</div>';
      }
    }
    $output .= '</div>';
  }

  $output .= '</div>';
  //ページネーションを表示する場合
  if($attributes['showPagination']) {
    $output .= '<div class="swiper-pagination"></div>';
  }
  //ナビゲーションボタンを表示する場合
  if($attributes['showNavigationButton']) {
    $output .= '<div class="swiper-button-prev"></div><div class="swiper-button-next"></div>';
  }
  //スクロールバーを表示する場合
  if($attributes['showScrollbar']) {
    $output .= '<div class="swiper-scrollbar"></div>';
  }
  $output .= '</div></div>';
  return $output;
}
my-slider.php
<?php
/**
 * Plugin Name:     My Slider
 * Description:     Example block written with ESNext standard and JSX support – build step required.
 * Version:         0.1.0
 * Author:          The WordPress Contributors
 * License:         GPL-2.0-or-later
 * License URI:     https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:     my-slider
 *
 * @package         wdl
 */

function wdl_my_slider_block_init() {
  $dir = dirname( __FILE__ );

  $script_asset_path = "$dir/build/index.asset.php";
  if ( ! file_exists( $script_asset_path ) ) {
    throw new Error(
      'You need to run `npm start` or `npm run build` for the "wdl/my-slider" block first.'
    );
  }
  $index_js     = 'build/index.js';
  $script_asset = require( $script_asset_path );
  wp_register_script(
    'wdl-my-slider-block-editor',
    plugins_url( $index_js, __FILE__ ),
    $script_asset['dependencies'],
    $script_asset['version']
  );

  $editor_css = 'build/index.css';
  wp_register_style(
    'wdl-my-slider-block-editor',
    plugins_url( $editor_css, __FILE__ ),
    array(),
    filemtime( "$dir/$editor_css" )
  );

  $style_css = 'build/style-index.css';
  wp_register_style(
    'wdl-my-slider-block',
    plugins_url( $style_css, __FILE__ ),
    array(),
    filemtime( "$dir/$style_css" )
  );

  register_block_type( 'wdl/my-slider', array(
    'editor_script' => 'wdl-my-slider-block-editor',
    'editor_style'  => 'wdl-my-slider-block-editor',
    'style'         => 'wdl-my-slider-block',
    'render_callback' => 'my_slider_render',
    //属性を追加
    'attributes' => [
      //属性 mediaID(メディア ID の配列)
      'mediaID' => [
        'type' => 'array',
        'default' => []
      ],
      //属性 imageUrl(URL の配列)
      'imageUrl' => [
        'type' => 'array',
        'default' => []
      ],
      //属性 imageAlt(alt 属性の配列)
      'imageAlt' => [
        'type' => 'array',
        'default' => []
      ],
      //属性 imageCaption(キャプションの配列)
      'imageCaption' => [
        'type' => 'array',
        'default' => []
      ],
      //ナビゲーションボタンの表示・非表示
      'showNavigationButton' => [
        'type' => 'boolean',
        'default' => true
      ],
      //ページネーションの表示・非表示
      'showPagination' => [
        'type' => 'boolean',
        'default' => true
      ],
      //スクロールバーンの表示・非表示
      'showScrollbar' => [
        'type' => 'boolean',
        'default' => true
      ],
      //キャプションの表示・非表示
      'showCaption' => [
        'type' => 'boolean',
        'default' => true
      ],
    ]
  ) );
}
add_action( 'init', 'wdl_my_slider_block_init' );

function my_slider_render($attributes, $content) {

  //属性 imageUrl が空なら何も表示しない
  if (empty($attributes['imageUrl'])) {
    return '';
  }

  //属性 imageUrl が空でなければスライダーのマークアップを組み立てる
  $output = '<div class="wp-block-wdl-my-slider">';
  $output .= '<div class="swiper-container" '. $slider_options .'>';
  $output .= '<div class="swiper-wrapper">';

  $imageUrl = $attributes['imageUrl'];
  $imageCaption = $attributes['imageCaption'];
  $imageAlt = $attributes['imageAlt'];

  for($i = 0; $i < count($imageUrl); $i ++) {
    $img_url = esc_url($imageUrl[$i]);
    $img_caption = $imageCaption[$i] ? esc_html($imageCaption[$i]): '';
    $img_alt = $imageAlt[$i] ? esc_attr($imageAlt[$i]): '';
    if($img_alt !=="") {
      $output .= '<div class="swiper-slide"><img className="card_image" src="'
                 . $img_url . '" alt="' . $img_alt . '" />';
    }else {
      $output .= '<div class="swiper-slide"><img className="card_image" src="'
                 . $img_url . '" alt="" aria-hidden="true" />';
    }
    //キャプションを表示する場合
    if($attributes['showCaption']) {
      if($img_caption) {
        $output .= '<div class="caption">'. $img_caption . '</div>';
      }
    }
    $output .= '</div>';
  }

  $output .= '</div>';
  //ページネーションを表示する場合
  if($attributes['showPagination']) {
    $output .= '<div class="swiper-pagination"></div>';
  }
  //ナビゲーションボタンを表示する場合
  if($attributes['showNavigationButton']) {
    $output .= '<div class="swiper-button-prev"></div><div class="swiper-button-next"></div>';
  }
  //スクロールバーを表示する場合
  if($attributes['showScrollbar']) {
    $output .= '<div class="swiper-scrollbar"></div>';
  }
  $output .= '</div></div>';
  return $output;
}

function add_my_slider_scripts_and_styles() {
  $dir = dirname( __FILE__ );

  //Swiper の JavaScript ファイルの読み込み(エンキュー)
  wp_enqueue_script(
    'swiper-slider',
    plugins_url( '/assets/swiper.js', __FILE__ ),
    array(),
    filemtime( "$dir/assets/swiper.js" ),
    true
  );

  //Swiper を初期化するためのファイルの読み込み(エンキュー)
  wp_enqueue_script(
    'swiper-slider-init',
    plugins_url( '/assets/init-swiper.js', __FILE__ ),
    //依存ファイルに上記 Swiper の JavaScript を指定
    array('swiper-slider'),
    filemtime( "$dir/assets/init-swiper.js" ),
    true
  );

  //Swiper の CSS ファイルの読み込み(エンキュー)
  wp_enqueue_style(
    'swipe-style',
    plugins_url( '/assets/swiper.css', __FILE__ ),
    array(),
    filemtime( "$dir/assets/swiper.css"  )
  );

}
add_action('enqueue_block_assets', 'add_my_slider_scripts_and_styles');

プレビューボタンの追加

エディター画面でスライダーを表示するようにツールバーにプレビューボタンを追加する例です。

ブロックを選択するとプレビューボタンが表示されます。プレビューボタンまたは「スライダーをプレビュー」のリンクをクリックするとスライダーが表示されます(プレビューモード)。

ツールバーの鉛筆のアイコンをクリックすると編集画面(編集モード)に戻ります。

但し、以下のような問題があるためあまり実用的ではありません。

  • 複数のブロックで同時にスライダーをプレビューすると動きがおかしくなってしまう場合があります。
  • プレビュー中にインスペクターのトグルボタン(ナビゲーションなどのオン・オフ)を操作するとスライダーの動きがおかしくなる場合があります。

スライダーのスクリプトとスタイルをエディター画面でも読み込むように my-slider.php を変更します。

但し、Swiper の初期化は edit 関数に記述するので、init-swiper.js はエディター画面では読み込まないようにしています。

my-slider.php 抜粋
function add_my_slider_scripts_and_styles() {
  $dir = dirname( __FILE__ );

  //Swiper の JavaScript ファイルの読み込み(エンキュー)
  wp_enqueue_script(
    'swiper-slider',
    plugins_url( '/assets/swiper.js', __FILE__ ),
    array(),
    filemtime( "$dir/assets/swiper.js" ),
    true
  );

  //フロントエンド側でのみ読み込む
  if(! is_admin()) {
    //Swiper を初期化するためのファイルの読み込み(エンキュー)
    wp_enqueue_script(
      'swiper-slider-init',
      plugins_url( '/assets/init-swiper.js', __FILE__ ),
      //依存ファイルに上記 Swiper の JavaScript を指定
      array('swiper-slider'),
      filemtime( "$dir/assets/init-swiper.js" ),
      true
    );
  }

  //Swiper の CSS ファイルの読み込み(エンキュー)
  wp_enqueue_style(
    'swipe-style',
    plugins_url( '/assets/swiper.css', __FILE__ ),
    array(),
    filemtime( "$dir/assets/swiper.css"  )
  );

}
add_action('enqueue_block_assets', 'add_my_slider_scripts_and_styles');
my-slider.php
<?php
/**
 * Plugin Name:     My Slider
 * Description:     Example block written with ESNext standard and JSX support – build step required.
 * Version:         0.1.0
 * Author:          The WordPress Contributors
 * License:         GPL-2.0-or-later
 * License URI:     https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:     my-slider
 *
 * @package         wdl
 */

function wdl_my_slider_block_init() {
  $dir = dirname( __FILE__ );

  $script_asset_path = "$dir/build/index.asset.php";
  if ( ! file_exists( $script_asset_path ) ) {
    throw new Error(
      'You need to run `npm start` or `npm run build` for the "wdl/my-slider" block first.'
    );
  }
  $index_js     = 'build/index.js';
  $script_asset = require( $script_asset_path );
  wp_register_script(
    'wdl-my-slider-block-editor',
    plugins_url( $index_js, __FILE__ ),
    $script_asset['dependencies'],
    $script_asset['version']
  );

  $editor_css = 'build/index.css';
  wp_register_style(
    'wdl-my-slider-block-editor',
    plugins_url( $editor_css, __FILE__ ),
    array(),
    filemtime( "$dir/$editor_css" )
  );

  $style_css = 'build/style-index.css';
  wp_register_style(
    'wdl-my-slider-block',
    plugins_url( $style_css, __FILE__ ),
    array(),
    filemtime( "$dir/$style_css" )
  );

  register_block_type( 'wdl/my-slider', array(
    'editor_script' => 'wdl-my-slider-block-editor',
    'editor_style'  => 'wdl-my-slider-block-editor',
    'style'         => 'wdl-my-slider-block',
    'render_callback' => 'my_slider_render',
    //属性を追加
    'attributes' => [
      //属性 mediaID(メディア ID の配列)
      'mediaID' => [
        'type' => 'array',
        'default' => []
      ],
      //属性 imageUrl(URL の配列)
      'imageUrl' => [
        'type' => 'array',
        'default' => []
      ],
      //属性 imageAlt(alt 属性の配列)
      'imageAlt' => [
        'type' => 'array',
        'default' => []
      ],
      //属性 imageCaption(キャプションの配列)
      'imageCaption' => [
        'type' => 'array',
        'default' => []
      ],
      //ナビゲーションボタンの表示・非表示
      'showNavigationButton' => [
        'type' => 'boolean',
        'default' => true
      ],
      //ページネーションの表示・非表示
      'showPagination' => [
        'type' => 'boolean',
        'default' => true
      ],
      //スクロールバーンの表示・非表示
      'showScrollbar' => [
        'type' => 'boolean',
        'default' => true
      ],
      //キャプションの表示・非表示
      'showCaption' => [
        'type' => 'boolean',
        'default' => true
      ],
    ]
  ) );
}
add_action( 'init', 'wdl_my_slider_block_init' );

function my_slider_render($attributes, $content) {

  //属性 imageUrl が空なら何も表示しない
  if (empty($attributes['imageUrl'])) {
    return '';
  }

  //属性 imageUrl が空でなければスライダーのマークアップを組み立てる
  $output = '<div class="wp-block-wdl-my-slider">';
  $output .= '<div class="swiper-container" '. $slider_options .'>';
  $output .= '<div class="swiper-wrapper">';

  $imageUrl = $attributes['imageUrl'];
  $imageCaption = $attributes['imageCaption'];
  $imageAlt = $attributes['imageAlt'];

  for($i = 0; $i < count($imageUrl); $i ++) {
    $img_url = esc_url($imageUrl[$i]);
    $img_caption = $imageCaption[$i] ? esc_html($imageCaption[$i]): '';
    $img_alt = $imageAlt[$i] ? esc_attr($imageAlt[$i]): '';
    if($img_alt !=="") {
      $output .= '<div class="swiper-slide"><img className="card_image" src="'
                 . $img_url . '" alt="' . $img_alt . '" />';
    }else {
      $output .= '<div class="swiper-slide"><img className="card_image" src="'
                 . $img_url . '" alt="" aria-hidden="true" />';
    }
    //キャプションを表示する場合
    if($attributes['showCaption']) {
      if($img_caption) {
        $output .= '<div class="caption">'. $img_caption . '</div>';
      }
    }
    $output .= '</div>';
  }

  $output .= '</div>';
  //ページネーションを表示する場合
  if($attributes['showPagination']) {
    $output .= '<div class="swiper-pagination"></div>';
  }
  //ナビゲーションボタンを表示する場合
  if($attributes['showNavigationButton']) {
    $output .= '<div class="swiper-button-prev"></div><div class="swiper-button-next"></div>';
  }
  //スクロールバーを表示する場合
  if($attributes['showScrollbar']) {
    $output .= '<div class="swiper-scrollbar"></div>';
  }
  $output .= '</div></div>';
  return $output;
}

function add_my_slider_scripts_and_styles() {
  $dir = dirname( __FILE__ );

  //Swiper の JavaScript ファイルの読み込み(エンキュー)
  wp_enqueue_script(
    'swiper-slider',
    plugins_url( '/assets/swiper.js', __FILE__ ),
    array(),
    filemtime( "$dir/assets/swiper.js" ),
    true
  );

  if(! is_admin()) {
    //Swiper を初期化するためのファイルの読み込み(エンキュー)
    wp_enqueue_script(
      'swiper-slider-init',
      plugins_url( '/assets/init-swiper.js', __FILE__ ),
      //依存ファイルに上記 Swiper の JavaScript を指定
      array('swiper-slider'),
      filemtime( "$dir/assets/init-swiper.js" ),
      true
    );
  }

  //Swiper の CSS ファイルの読み込み(エンキュー)
  wp_enqueue_style(
    'swipe-style',
    plugins_url( '/assets/swiper.css', __FILE__ ),
    array(),
    filemtime( "$dir/assets/swiper.css"  )
  );

}
add_action('enqueue_block_assets', 'add_my_slider_scripts_and_styles');

registerBlockType() の attributes に以下の属性を追加します(isEditMode が true の場合は編集モードになります)

src/index.js
//isEditMode を属性として追加
isEditMode: {
  type: 'boolean',
  default: true
},
src/index.js
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import save from './save';

registerBlockType( 'wdl/my-slider', {
  title: 'My Slider',
  description: 'Example block written with ESNext standard and JSX support',
  category: 'widgets',
  icon: 'smiley',
  supports: {
    html: false,
  },
  //属性を設定
  attributes: {
    //属性 mediaID(メディア ID の配列)
    mediaID: {
      type: 'array',
      default: []
    },
    //img の src に指定する URL
    imageUrl: {
      type: 'array',
      default: []
    },
    //img の alt 属性の値
    imageAlt: {
      type: 'array',
      default: []
    },
    //ナビゲーションボタンの表示・非表示
    showNavigationButton: {
      type: 'boolean',
      default: true
    },
    //ページネーションの表示・非表示
    showPagination: {
      type: 'boolean',
      default: true
    },
    //ページネーションの表示・非表示
    showScrollbar: {
      type: 'boolean',
      default: true
    },
    //img の キャプションの値
    imageCaption: {
      type: 'array',
      default: []
    },
    //キャプションの表示・非表示
    showCaption: {
      type: 'boolean',
      default: true
    },
    //isEditMode を属性として追加
    isEditMode: {
      type: 'boolean',
      default: true
    },
  },
  edit: Edit,
  //save 関数で null を返す
  save: () => { return null }
} );

edit.js ではツールバーに編集モードとプレビューモードを切り替えるボタンを表示する関数を追加し、属性 isEditMode が false の場合はプレビューモードと判定し、スライダーの構造をレンダリングするようにしています。

また、ツールバーはブロックを選択しないと表示されないため使い勝手良くないので、「スライダーをプレビュー」というリンクを return ステートメント内に追加してリンクを直接クリックしてもプレビューできるようにします。

ツールバーに必要な BlockControls と Toolbar、スライダーのレンダリングに必要な Fragment を追加でインポートします。

スライダーを初期化するコード(関数)をコンポーネントがレンダリングされた直後に実行するため、useEffect フックをインポートします。

src/edit.js
import { BlockControls, MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
import { InspectorControls } from '@wordpress/block-editor';
import { Toolbar, PanelBody, PanelRow, ToggleControl } from '@wordpress/components';
import { Fragment } from '@wordpress/element';
import { useEffect } from '@wordpress/element';  

以下はツールバーに編集モードとプレビューモードを切り替えるボタンを表示する関数です。

const getBlockControls = () => {
  return (
    <BlockControls>
      <Toolbar>
        <Button
          //属性 isEditMode の値により表示するラベルを切り替え
          label={ attributes.isEditMode ? "Preview" : "Edit" }
          //属性 isEditMode の値により表示するアイコンを切り替え
          icon={ attributes.isEditMode ? "format-image" : "edit" }
          className="my-custom-button"
          //setAttributes を使って属性の値を更新(真偽値を反転)
          onClick={() => setAttributes({ isEditMode: !attributes.isEditMode })}
        />
      </Toolbar>
    </BlockControls>
  );
}

swiper-slide というクラスを付与した div 要素で img をラップしたスライダー表示用の画像を生成する関数を追加します。

//URL の配列から画像を生成(スライダー表示用)
const getSliderImages = ( url, caption ) => {
  let imagesArray = [];
  for(let i = 0 ; i < url.length; i ++) {
    let image = '';
    if(attributes.showCaption) {
      image = (
        <div className="swiper-slide">
          <img
            src={ url[i] }
            className="image"
            alt="アップロード画像"
          />
          <div class="caption">
             { caption[i] ? caption[i] : '' }
          </div>
        </div>
      )
    }else{
      image = (
        <div className="swiper-slide">
          <img
            src={ url[i] }
            className="image"
            alt="アップロード画像"
          />
        </div>
      )
    }

    imagesArray.push ( image );
  }
  return imagesArray;
}

Swiper のスライダーを初期化する関数 initSwiper を追加します。

const initSwiper = () => {
  let editorSwiper = new Swiper ('.swiper-container', {
    //自動的にスライダーを開始する場合は以下のコメントアウトを削除
    /*autoplay: {
      delay: 4000, //4秒間隔でスライドを自動的に実行
    },*/

    //最後に達したら先頭に戻る
    loop: true,

    //ページネーション表示の設定
    pagination: {
      el: '.swiper-pagination', //ページネーションの要素
      type: 'bullets', //ページネーションの種類
      clickable: true, //クリックに反応させる
    },

    //ナビゲーションボタン(矢印)表示の設定
    navigation: {
      nextEl: '.swiper-button-next', //「次へボタン」要素の指定
      prevEl: '.swiper-button-prev', //「前へボタン」要素の指定
    },

    //スクロールバー表示の設定
    scrollbar: {
      el: '.swiper-scrollbar', //要素の指定
    },
  })
}

メディアライブラリを開くボタンをレンダリングする getImageButton 関数で、isEditMode が false なら getSliderImages 関数を使ってスライダーを表示します。

const getImageButton = (open) => {
  if(attributes.imageUrl.length > 0 ) {
    if(attributes.isEditMode) {
      return (
        <div onClick={ open } className="slider-block-container">
         { getImages( attributes.imageUrl ) }
        </div>
      )
    }else{
      //isEditMode が false ならスライダーを表示
      return (
        <div className="slider-container">
          <div className="swiper-container">
            <div className="swiper-wrapper">
              { getSliderImages(attributes.imageUrl, attributes.imageCaption) }
            </div>
            { attributes.showPagination &&
              <div className="swiper-pagination"></div>
            }
            { attributes.showNavigationButton &&
              <Fragment>
                <div className="swiper-button-prev"></div>
                <div className="swiper-button-next"></div>
              </Fragment>
            }
            { attributes.showScrollbar &&
              <div className="swiper-scrollbar"></div>
            }
          </div>
        </div>
      )
    }
  }
  else {
    return (
      <div className="button-container">
        <Button
          onClick={ open }
          className="button button-large"
        >
          画像をアップロード
        </Button>
      </div>
    );
  }
};

useEffect フック

スライダーを初期化する関数(initSwiper)をコンポーネントがレンダリングされた直後に実行するため、useEffect フックを使ってスライダーの初期化を実行します。

useEffect はレンダーの完了時に毎回実行されますが、第2引数を指定することによって、実行される条件を制御することができます。この例の場合は isEditMode の値が変更された場合にのみ実行すれば良いので、第2引数に attributes.isEditMode を指定しています。

この第2引数を指定しないと、例えばインスペクターのトグルボタンを操作しても React によるレンダリングが発生するので useEffect が実行されてしまいます。

//useEffect フックを使ってスライダーの初期化を実行
useEffect(() => {
  initSwiper();
}, [attributes.isEditMode]);

return ステートメントではプレビューボタンを関数を使ってレンダリングします。プレビューモードの場合は削除ボタンを表示しないように attributes.isEditMode を判定に追加しています(16行目)。

また、「スライダーをプレビュー」というリンクを Button コンポーネントに isLink プロパティを指定して追加します。onClick プロパティではプレビューボタン同様 setAttributes を使って属性 isEditMode の値を反転させます。

src/edit.js
return (
  [
    getBlockControls(), //プレビューボタン
    getInspectorControls(),  //インスペクター
    <div className={ className }>
    <MediaUploadCheck>
      <MediaUpload
        multiple={ true }
        gallery={ true }
        onSelect={ onSelectImage }
        allowedTypes={ ['image'] }
        value={ attributes.mediaID }
        render={ ({ open }) => getImageButton( open ) }
      />
    </MediaUploadCheck>
    { attributes.imageUrl.length != 0  && attributes.isEditMode &&
      <MediaUploadCheck>
        <Button
          onClick={removeMedia}
          isLink
          isDestructive
          className="removeImage">画像を削除
        </Button>
        <Button
          onClick={() => setAttributes({ isEditMode: !attributes.isEditMode })}
          isLink
          className="removeImage">スライダーをプレビュー
        </Button>
      </MediaUploadCheck>
    }
  </div>
  ]
);    
src/edit.js
import { BlockControls, MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
import { InspectorControls } from '@wordpress/block-editor';
import { Toolbar, PanelBody, PanelRow, ToggleControl } from '@wordpress/components';
import { Fragment } from '@wordpress/element';
import { useEffect } from '@wordpress/element';
import './editor.scss';

export default function Edit( props ) {
  //分割代入を使って props 経由でプロパティを変数に代入
  const { className, attributes, setAttributes} = props;

  //選択された画像の情報を更新する関数
  const onSelectImage = ( media ) => {
    // media から map で id プロパティの配列を生成
    const media_ID = media.map((image) => image.id);
    // media から map で url プロパティの配列を生成
    const imageUrl = media.map((image) => image.url);
    // media から map で alt プロパティの配列を生成
    const imageAlt = media.map((image) => image.alt);
    // media から map で caption プロパティの配列を生成
    const imageCaption = media.map((image) => image.caption);

    setAttributes( {
      mediaID: media_ID,  //メディア ID の配列
      imageUrl: imageUrl,  // URL の配列
      imageAlt: imageAlt,  // alt 属性の配列
      imageCaption: imageCaption,  // キャプションの配列
    } );
  };

  //URL の配列から画像を生成
  const getImages = ( urls ) => {
    let imagesArray = urls.map(( url ) => {
      return (
        <img
          src={ url }
          className="image"
          alt="アップロード画像"
        />
      );
    });
    return imagesArray;
  }

  //URL とキャプションの配列から画像をキャプション付きで生成(for 文に変更)
  const getImagesWithCaption = ( url, caption ) => {
    let imagesArray = [];
    for(let i = 0 ; i < url.length; i ++) {
      imagesArray.push (
        <figure>
          <img
            src={ url[i] }
            className="image"
            alt="アップロード画像"
          />
          <figcaption className="block-image-caption">
            { caption[i] ? caption[i] : '' }
          </figcaption>
        </figure>
      );
    }
    return imagesArray;
  }

  //URL の配列から画像を生成(スライダー表示用)
  const getSliderImages = ( url, caption ) => {
    let imagesArray = [];
    for(let i = 0 ; i < url.length; i ++) {
      let image = '';
      if(attributes.showCaption) {
        image = (
          <div className="swiper-slide">
            <img
              src={ url[i] }
              className="image"
              alt="アップロード画像"
            />
            <div class="caption">
               { caption[i] ? caption[i] : '' }
            </div>
          </div>
        )
      }else{
        image = (
          <div className="swiper-slide">
            <img
              src={ url[i] }
              className="image"
              alt="アップロード画像"
            />
          </div>
        )
      }

      imagesArray.push ( image );
    }
    return imagesArray;
  }

  const initSwiper = () => {
    let editorSwiper = new Swiper ('.swiper-container', {
      //自動的にスライダーを開始する場合は以下のコメントアウトを削除
      /*autoplay: {
        delay: 4000, //4秒間隔でスライドを自動的に実行
      },*/

      //最後に達したら先頭に戻る
      loop: true,

      //ページネーション表示の設定
      pagination: {
        el: '.swiper-pagination', //ページネーションの要素
        type: 'bullets', //ページネーションの種類
        clickable: true, //クリックに反応させる
      },

      //ナビゲーションボタン(矢印)表示の設定
      navigation: {
        nextEl: '.swiper-button-next', //「次へボタン」要素の指定
        prevEl: '.swiper-button-prev', //「前へボタン」要素の指定
      },

      //スクロールバー表示の設定
      scrollbar: {
        el: '.swiper-scrollbar', //要素の指定
      },
    })
  }

  const getImageButton = (open) => {
    if(attributes.imageUrl.length > 0 ) {
      if(attributes.isEditMode) {
        return (
          <div onClick={ open } className="slider-block-container">
           { attributes.showCaption ? getImagesWithCaption( attributes.imageUrl, attributes.imageCaption ) : getImages( attributes.imageUrl ) }
          </div>
        )
      }else{
        //isEditMode が false ならスライダーを表示
        return (
          <div className="slider-container">
            <div className="swiper-container">
              <div className="swiper-wrapper">
                { getSliderImages(attributes.imageUrl, attributes.imageCaption) }
              </div>
              { attributes.showPagination &&
                <div className="swiper-pagination"></div>
              }
              { attributes.showNavigationButton &&
                <Fragment>
                  <div className="swiper-button-prev"></div>
                  <div className="swiper-button-next"></div>
                </Fragment>
              }
              { attributes.showScrollbar &&
                <div className="swiper-scrollbar"></div>
              }
            </div>
          </div>
        )
      }
    }
    else {
      return (
        <div className="button-container">
          <Button
            onClick={ open }
            className="button button-large"
          >
            画像をアップロード
          </Button>
        </div>
      );
    }
  };

  //画像を削除する(メディアをリセットする)関数
  const removeMedia = () => {
    setAttributes({
      mediaID: [],
      imageUrl: [],
      imageAlt: [],
      imageCaption: [],
    });
  }

  //インスペクターを追加する関数
  const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody
          title='Slider Settings'
          initialOpen={true}
        >
          <PanelRow>
            <ToggleControl
              label="ナビゲーションボタン"
              checked={attributes.showNavigationButton}
              onChange={(val) => setAttributes({ showNavigationButton: val })}
            />
          </PanelRow>
          <PanelRow>
            <ToggleControl
              label="ページネーション"
              checked={attributes.showPagination}
              onChange={(val) => setAttributes({ showPagination: val })}
            />
          </PanelRow>
          <PanelRow>
            <ToggleControl
              label="スクロールバー"
              checked={attributes.showScrollbar}
              onChange={(val) => setAttributes({ showScrollbar: val })}
            />
          </PanelRow>
          <PanelRow>
            <ToggleControl
              label="キャプション"
              checked={attributes.showCaption}
              onChange={(val) => setAttributes({ showCaption: val })}
            />
          </PanelRow>
        </PanelBody>
      </InspectorControls>
    );
  }

  const getBlockControls = () => {
    return (
      <BlockControls>
        <Toolbar>
          <Button
            //属性 isEditMode の値により表示するラベルを切り替え
            label={ attributes.isEditMode ? "Preview" : "Edit" }
            //属性 isEditMode の値により表示するアイコンを切り替え
            icon={ attributes.isEditMode ? "format-image" : "edit" }
            className="my-custom-button"
            //setAttributes を使って属性の値を更新(真偽値を反転)
            onClick={() => setAttributes({ isEditMode: !attributes.isEditMode })}
          />
        </Toolbar>
      </BlockControls>
    );
  }

  //useEffect フックを使ってスライダーを初期化
  useEffect(() => {
    initSwiper();
  }, [attributes.isEditMode]);

  return (
    [
      getBlockControls(), //プレビューボタン
      getInspectorControls(),  //インスペクター
      <div className={ className }>
        <MediaUploadCheck>
          <MediaUpload
            multiple={ true }
            gallery={ true }
            onSelect={ onSelectImage }
            allowedTypes={ ['image'] }
            value={ attributes.mediaID }
            render={ ({ open }) => getImageButton( open ) }
          />
        </MediaUploadCheck>
        { attributes.imageUrl.length != 0  && attributes.isEditMode &&
          <MediaUploadCheck>
            <Button
              onClick={removeMedia}
              isLink
              isDestructive
              className="removeImage">画像を削除
            </Button>
            <Button
              onClick={() => setAttributes({ isEditMode: !attributes.isEditMode })}
              isLink
              className="removeImage">スライダーをプレビュー
            </Button>
          </MediaUploadCheck>
        }
      </div>
    ]
  );
}

useEffect フックは save 関数では使えない

useEffect フックを save 関数で使用するとエラーになり利用できませんでした。

以下の場合、edit 関数では問題なくコンソールに「edit effect」と出力されます。

import { registerBlockType } from '@wordpress/blocks';
import { useEffect } from '@wordpress/element';
registerBlockType( 'wdl/my-effect-test', {
  title: 'my-effect-test',
  description: 'Example block.',
  category: 'widgets',
  icon: 'smiley',
  edit: () => {
    //問題なし
    useEffect(() => {
      console.log('edit effect');
    });
    return <div>Hello Editor!</div>
  },
  save: () => {
    return <div>Hello Front End!</div>
  },
});

但し、以下の場合、save 関数ではエラーになります(コンパイルはされます)。

registerBlockType( 'wdl/my-effect-test', {
  title: 'my-effect-test',
  description: 'Example block.',
  category: 'widgets',
  icon: 'smiley',
  edit: () => {
    return <div>Hello Editor!</div>
  },
  save: () => {
    //エラーになる
    useEffect(() => {
      console.log('save effect');
    });
    return <div>Hello Front End!</div>
  },
});

コンソールに表示されるリンクをクリックすると React のエラーの詳細が表示されます。

以下のように「Invalid hook call. Hooks can only be called inside of the body of a function component.(フックは関数コンポーネント内でのみ呼び出せます)」というエラーが確認できます。

Using hooks in a functional component which is inside the block's save function throws a React error.

WordPress Gutenberg 公式ドキュメントの save 関数の部分に以下の記述があります。useEffect フックは save 関数では使えないようです。

注意: save 関数は、呼び出し時に使用された属性にのみ依存する純粋関数(pure function)でなければなりません。どのようなサイドイフェクト(side effect)も与えられず、別のソースからの情報も取得できません。

ServerSideRender

ServerSideRender を使ってスライダーのプレビューができるかを試しましたがうまくできませんでした。以下は個人的なメモ(失敗例)になります(何かが間違っている)。

プレビューモードにすると、一枚目の画像は表示されますが、スライダーが初期化されておらず、スライダー以外の部分(投稿のタイトルやインスペクターなど)をクリックするとスライダーが初期化され動くようにはなりますが、使い物になりません。

また、useEffect の第2パラメータに attributes.isEditMode を指定すると機能しません。

//useEffect フックを使ってスライダーの初期化を実行(この使い方が正しくない?)
useEffect(() => {
  initSwiper();
});

return (
  [
    getBlockControls(), //プレビューボタン
    getInspectorControls(),  //インスペクター
    <div className={ className }>
      { attributes.isEditMode &&  // isEditMode が true の場合(編集 モード)
        <Fragment>
          <MediaUploadCheck>
            <MediaUpload
              multiple={ true }
              gallery={ true }
              onSelect={ onSelectImage }
              allowedTypes={ ['image'] }
              value={ attributes.mediaID }
              render={ ({ open }) => getImageButton( open ) }
            />
          </MediaUploadCheck>
          { attributes.imageUrl.length != 0  &&
            <MediaUploadCheck>
              <Button
                onClick={removeMedia}
                isLink
                isDestructive
                className="removeImage">画像を削除
              </Button>
              <Button
                onClick={() => setAttributes({ isEditMode: !attributes.isEditMode })}
                isLink
                className="removeImage">スライダーをプレビュー
              </Button>
            </MediaUploadCheck>
          }
        </Fragment>
      }
      { !attributes.isEditMode &&   // isEditMode が false の場合(プレビュー モード)
        <ServerSideRender
          block={props.name}
          attributes={{
            mediaID: attributes.mediaID,
            imageUrl: attributes.imageUrl,
            imageCaption: attributes.imageCaption,
          }}
        />
      }
    </div>
  ]
);

useEffect を使わずに以下のように記述したのとほぼ同じ結果でした(40行目:スクリプトタグで初期化のコードを無理やり挿入して実行)。

return (
  [
    getBlockControls(), //プレビューボタン
    getInspectorControls(),  //インスペクター
    <div className={ className }>
      { attributes.isEditMode &&  // isEditMode が true の場合(編集 モード)
        <Fragment>
          <MediaUploadCheck>
            <MediaUpload
              multiple={ true }
              gallery={ true }
              onSelect={ onSelectImage }
              allowedTypes={ ['image'] }
              value={ attributes.mediaID }
              render={ ({ open }) => getImageButton( open ) }
            />
          </MediaUploadCheck>
          { attributes.imageUrl.length != 0  &&
            <MediaUploadCheck>
              <Button
                onClick={removeMedia}
                isLink
                isDestructive
                className="removeImage">画像を削除
              </Button>
              <Button
                onClick={() => setAttributes({ isEditMode: !attributes.isEditMode })}
                isLink
                className="removeImage">スライダーをプレビュー
              </Button>
            </MediaUploadCheck>
          }
        </Fragment>
      }
      { !attributes.isEditMode &&   // isEditMode が false の場合(プレビュー モード)
        <Fragment>
          <ServerSideRender
            block={props.name}
            attributes={{
              mediaID: attributes.mediaID,
              imageUrl: attributes.imageUrl,
              imageCaption: attributes.imageCaption,
            }}
          />
          <script>{ initSwiper() }</script>
        </Fragment>
      }
    </div>
  ]
);
注意:このコードには問題があり、機能しません。
import { BlockControls, MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
import { InspectorControls } from '@wordpress/block-editor';
import {  Toolbar, PanelBody, PanelRow, ToggleControl } from '@wordpress/components';
import { Fragment } from '@wordpress/element';
import { useEffect } from '@wordpress/element';
import ServerSideRender from '@wordpress/server-side-render';
//または import { ServerSideRender } from '@wordpress/editor';
import './editor.scss';

export default function Edit( props ) {
  //分割代入を使って props 経由でプロパティを変数に代入
  const { className, attributes, setAttributes} = props;

  //選択された画像の情報を更新する関数
  const onSelectImage = ( media ) => {
    const media_ID = media.map((image) => image.id);
    const imageUrl = media.map((image) => image.url);
    const imageAlt = media.map((image) => image.alt);
    const imageCaption = media.map((image) => image.caption);

    setAttributes( {
      mediaID: media_ID,  //メディア ID の配列
      imageUrl: imageUrl,  // URL の配列
      imageAlt: imageAlt,  // alt 属性の配列
      imageCaption: imageCaption,  // キャプションの配列
    } );
  };

  //URL の配列から画像を生成
  const getImages = ( urls ) => {
    let imagesArray = urls.map(( url ) => {
      return (
        <img
          src={ url }
          className="image"
          alt="アップロード画像"
        />
      );
    });
    return imagesArray;
  }

  //URL とキャプションの配列から画像をキャプション付きで生成(for 文に変更)
  const getImagesWithCaption = ( url, caption ) => {
    let imagesArray = [];
    for(let i = 0 ; i < url.length; i ++) {
      imagesArray.push (
        <figure>
          <img
            src={ url[i] }
            className="image"
            alt="アップロード画像"
          />
          <figcaption className="block-image-caption">
            { caption[i] ? caption[i] : '' }
          </figcaption>
        </figure>
      );
    }
    return imagesArray;
  }

  //メディアライブラリを開くボタンをレンダリングする関数(上記関数を使って画像をレンダリング)
  const getImageButton = (open) => {
    if(attributes.imageUrl.length > 0 ) {
      return (
        <div onClick={ open } className="slider-block-container">
         { attributes.showCaption ?
             getImagesWithCaption( attributes.imageUrl, attributes.imageCaption ) :
             getImages( attributes.imageUrl ) }
        </div>
      )
    }
    else {
      return (
        <div className="button-container">
          <Button
            onClick={ open }
            className="button button-large"
          >
            画像をアップロード
          </Button>
        </div>
      );
    }
  };

  //画像を削除する(メディアをリセットする)関数
  const removeMedia = () => {
    setAttributes({
      mediaID: [],
      imageUrl: [],
      imageAlt: [],
      imageCaption: [],
    });
  }

  //インスペクターを追加する関数
  const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody
          title='Slider Settings'
          initialOpen={true}
        >
          <PanelRow>
            <ToggleControl
              label="ナビゲーションボタン"
              checked={attributes.showNavigationButton}
              onChange={(val) => setAttributes({ showNavigationButton: val })}
            />
          </PanelRow>
          <PanelRow>
            <ToggleControl
              label="ページネーション"
              checked={attributes.showPagination}
              onChange={(val) => setAttributes({ showPagination: val })}
            />
          </PanelRow>
          <PanelRow>
            <ToggleControl
              label="スクロールバー"
              checked={attributes.showScrollbar}
              onChange={(val) => setAttributes({ showScrollbar: val })}
            />
          </PanelRow>
          <PanelRow>
            <ToggleControl
              label="キャプション"
              checked={attributes.showCaption}
              onChange={(val) => setAttributes({ showCaption: val })}
            />
          </PanelRow>
        </PanelBody>
      </InspectorControls>
    );
  }

  //ツールバーに編集モードとプレビューモードを切り替えるボタンを表示する関数
  const getBlockControls = () => {
    return (
      <BlockControls>
        <Toolbar>
          <Button
            //属性 isEditMode の値により表示するラベルを切り替え
            label={ attributes.isEditMode ? "Preview" : "Edit" }
            //属性 isEditMode の値により表示するアイコンを切り替え
            icon={ attributes.isEditMode ? "format-image" : "edit" }
            className="my-custom-button"
            //setAttributes を使って属性の値を更新(真偽値を反転)
            onClick={() => setAttributes({ isEditMode: !attributes.isEditMode })}
          />
        </Toolbar>
      </BlockControls>
    );
  }

  //Swiper のスライダーを初期化する関数
  const initSwiper = () => {
    let editorSwiper = new Swiper ('.swiper-container', {
      //自動的にスライダーを開始する場合は以下のコメントアウトを削除
      /*autoplay: {
        delay: 4000, //4秒間隔でスライドを自動的に実行
      },*/

      //最後に達したら先頭に戻る
      loop: true,

      //ページネーション表示の設定
      pagination: {
        el: '.swiper-pagination', //ページネーションの要素
        type: 'bullets', //ページネーションの種類
        clickable: true, //クリックに反応させる
      },

      //ナビゲーションボタン(矢印)表示の設定
      navigation: {
        nextEl: '.swiper-button-next', //「次へボタン」要素の指定
        prevEl: '.swiper-button-prev', //「前へボタン」要素の指定
      },

      //スクロールバー表示の設定
      scrollbar: {
        el: '.swiper-scrollbar', //要素の指定
      },
    })
  }

  //useEffect フックを使ってスライダーの初期化を実行
  useEffect(() => {
    initSwiper();
  });

  return (
    [
      getBlockControls(), //プレビューボタン
      getInspectorControls(),  //インスペクター
      <div className={ className }>
        { attributes.isEditMode &&  // isEditMode が true の場合(編集 モード)
          <Fragment>
            <MediaUploadCheck>
              <MediaUpload
                multiple={ true }
                gallery={ true }
                onSelect={ onSelectImage }
                allowedTypes={ ['image'] }
                value={ attributes.mediaID }
                render={ ({ open }) => getImageButton( open ) }
              />
            </MediaUploadCheck>
            { attributes.imageUrl.length != 0  &&   // imageUrl(配列の長さ)で判定
              <MediaUploadCheck>
                <Button
                  onClick={removeMedia}
                  isLink
                  isDestructive
                  className="removeImage">画像を削除
                </Button>
                <Button
                  onClick={() => setAttributes({ isEditMode: !attributes.isEditMode })}
                  isLink
                  className="removeImage">スライダーをプレビュー
                </Button>
              </MediaUploadCheck>
            }
          </Fragment>
        }
        { !attributes.isEditMode &&   // isEditMode が false の場合(プレビュー モード)
          <ServerSideRender
            block={props.name}
            attributes={{
              mediaID: attributes.mediaID,
              imageUrl: attributes.imageUrl,
              imageCaption: attributes.imageCaption,
            }}
          />
        }
      </div>
    ]
  );
}

また、ServerSideRender を別コンポーネントとしても同様の結果で、プレビューボタンをクリックしてスライダー以外の部分(投稿のタイトルやインスペクターなど)をクリックしないとスライダーが動きません。

server-side-slider.js(ServerSideRender をレンダリングするコンポーネント)
import { useEffect } from '@wordpress/element';
import ServerSideRender from '@wordpress/server-side-render';

export default function ServerSideSlider( props ) {

  //Swiper のスライダーを初期化する関数
  const initSwiper = () => {
    let editorSwiper = new Swiper ('.swiper-container', {
      autoplay: {
        delay: 4000,
      },
      loop: true,
      pagination: {
        el: '.swiper-pagination',
        type: 'bullets',
        clickable: true,
      },
      navigation: {
        nextEl: '.swiper-button-next',
        prevEl: '.swiper-button-prev',
      },
      scrollbar: {
        el: '.swiper-scrollbar',
      },
    })
  }

  //useEffect フックを使ってスライダーの初期化を実行(期待通りに機能しない)
  useEffect(() => {
    initSwiper();
  });

  // ServerSideRender をレンダリング
  return (
    <ServerSideRender
      block={props.block}
      attributes={{
        mediaID: props.mediaID,
        imageUrl: props.imageUrl,
        imageCaption: props.imageCaption,
      }}
    />
  );
}
edit.js 抜粋(うまく機能しない例)
import ServerSideSlider from './server-side-slider';
・・・中略・・・

return (
  [
    getBlockControls(),
    getInspectorControls(),
    <div className={ className }>
      { attributes.isEditMode &&  // isEditMode が true の場合(編集 モード)
        <Fragment>
          <MediaUploadCheck>
            <MediaUpload
              multiple={ true }
              gallery={ true }
              onSelect={ onSelectImage }
              allowedTypes={ ['image'] }
              value={ attributes.mediaID }
              render={ ({ open }) => getImageButton( open ) }
            />
          </MediaUploadCheck>
          { attributes.imageUrl.length != 0  &&
            <MediaUploadCheck>
              <Button
                onClick={removeMedia}
                isLink
                isDestructive
                className="removeImage">画像を削除
              </Button>
              <Button
                onClick={() => setAttributes({ isEditMode: !attributes.isEditMode })}
                isLink
                className="removeImage">スライダーをプレビュー
              </Button>
            </MediaUploadCheck>
          }
        </Fragment>
      }
      { !attributes.isEditMode && //プレビューモードでは ServerSideSlider をレンダリング
        <ServerSideSlider
          block={props.name}
          mediaID={attributes.mediaID}
          imageUrl={attributes.imageUrl}
          imageCaption={attributes.imageCaption}
        />
      }
    </div>
  ]
);

自動再生オプションなどの追加

スライダーの自動再生やエフェクトなどのオプションをインスペクターに追加する例です。この例では初期状態ではこれらの設定は非表示にしてあります。

パネルのボタンをクリックすると設定を表示します。選択した画像が多くなると、ブロックが大きくなってしまいますが、ブロックに max-height を指定してスクロールバーなどを表示するようにスタイルで設定することが可能です。

複数のスライダーを設定する際に、異なるパラメータを設定することができます。但し、エディター画面でのプレビューは1つずつ確認する必要があります(プレビューボタンをクリックするとそのブロックの設定が他のプレビューモードで表示しているスライダーにも反映されてしまいます)。

スライダーの初期化ファイルの変更

スライダーの自動再生やエフェクトなどのオプションのパラメータは Swiper を初期化するためのファイルに記述しているので動的に変更するのは難しいです(と思います)。

それらのパラメータを動的に変更できるようにするにはスライダーのマークアップに data 属性を設定してそれらの値を attributes を使って変更することができます。

例えば、スライダーのループを有効にして効果(エフェクト)を cube で表示し、スライドのスピードを1秒にするには以下のように data-xxxx 属性を使ってマークアップします。

<div class="swiper-container" data-loop="true" data-effect="cube" data-speed="1000">
  <div class="swiper-wrapper">
    <div class="swiper-slide"><img src="images/swiper_img_01.jpg" alt=""></div>
    <div class="swiper-slide"><img src="images/swiper_img_02.jpg" alt=""></div>
    <div class="swiper-slide"><img src="images/swiper_img_03.jpg" alt=""></div>
  </div>
  <div class="swiper-pagination"></div>
  <div class="swiper-button-prev"></div>
  <div class="swiper-button-next"></div>
  <div class="swiper-scrollbar"></div>
</div>

上記のようなマークアップでパラメータを変更できるように、今まで使用していた Swiper を初期化するファイル(フロントエンド側で読み込むファイル)を以下のように変更し、ファイル名も my-swiper-init.js に変更します。

内容は、フロントエンド側で表示されるスライダーの要素を取得して、それらのマークアップの data 属性からパラメータの値を取得し、それらの値を使ってスライダーを初期化しています。

このブロック以外でも Swiper を使っている可能性を考慮して、querySelectorAll() でブロックに自動的に付与されるクラス wp-block-wdl-my-slider 内の swiper-container クラスの要素を取得します。

詳細は「スライダープラグイン Swiper(v5)の使い方」を参照ください。

assets/my-swiper-init.js
//そのページにあるスライダーの要素を変数 sliderElems に取得
const sliderElems = document.querySelectorAll('.wp-block-wdl-my-slider .swiper-container');

for ( let element of sliderElems ) {
  //data 属性の値をパラメータの値として使用する変数に格納
  let elementSpeed = element.getAttribute('data-speed'),
      elementDirection = element.getAttribute('data-direction'),
      elementAutoPlay = element.getAttribute('data-autoplay'),
      elementLoop = element.getAttribute('data-loop'),
      elementEffect = element.getAttribute('data-effect'),
      elementSlidesPerView = element.getAttribute('data-slidesPerView'),
      elementCenteredSlides = element.getAttribute('data-centeredSlides');

  //data 属性が設定されていない場合は初期値(デフォルト)を設定及び型を変換
  if (!elementSpeed) {
    elementSpeed = 300;
  }
  if (!elementDirection) {
    elementDirection = 'horizontal';
  }
  //data-autoplay が設定されていれば値を数値に変換し、設定されていなければ大きな値を設定
  if (elementAutoPlay) {
    elementAutoPlay = parseInt(elementAutoPlay);
  } else {
    elementAutoPlay = 999999999;
  }
  //真偽値の場合は文字列から真偽値に変換
  if (elementLoop == 'true') {
    elementLoop = true;
  } else {
    elementLoop = false;
  }
  if (!elementEffect) {
    elementEffect = 'slide';
  }
  if (!elementSlidesPerView) {
    elementSlidesPerView = 1;
  }
  if (elementCenteredSlides == 'true') {
    elementCenteredSlides = true;
  } else {
    elementCenteredSlides = false;
  }

  //上記パラメータを使って Swiper を初期化
  let swiperSlider = new Swiper(element, {
    direction: elementDirection,
    speed: parseInt(elementSpeed),
    autoplay: {
      delay: elementAutoPlay
    },
    loop: elementLoop,
    effect: elementEffect,
    slidesPerView: parseInt(elementSlidesPerView),
    centeredSlides: elementCenteredSlides,
    pagination: {
      el: '.swiper-pagination',
      type: 'bullets',
      clickable: true,
    },
    navigation: {
      nextEl: '.swiper-button-next',
      prevEl: '.swiper-button-prev',
    },
    scrollbar: {
      el: '.swiper-scrollbar',
    },
  });
}

ファイル名を変更したので、ファイルの読み込みの記述を変更します。

my-slider.php 抜粋
function add_my_slider_scripts_and_styles() {
  $dir = dirname( __FILE__ );

  ・・・中略・・・

  if(! is_admin()) {
    //Swiper を初期化するためのファイルの読み込みを変更
    wp_enqueue_script(
      'swiper-slider-init',
      plugins_url( '/assets/my-swiper-init.js', __FILE__ ),
      array('swiper-slider'),
      filemtime( "$dir/assets/my-swiper-init.js" ),
      true
    );
  }

  ・・・中略・・・
}
add_action('enqueue_block_assets', 'add_my_slider_scripts_and_styles');

パラメータの attributes を追加

この例では PHP でレンダリングしていますが、JavaScript 側の registerBlockType() の attributes にもスライダーのパラメータに使用する属性を追加します(何故か設定しないとインスペクターに初期値が反映されないため。どこかがおかしい?)。

src/index.js
attributes: {
  mediaID: {
    type: 'array',
    default: []
  },

  ・・・中略・・・

  //スライダー自動再生
  slideAutoPlay: {
    type: 'number',
    default: 0
  },
  //スライダースピード
  slideSpeed: {
    type: 'number',
    default: 300
  },
  //スライダーのループ設定
  slideLoopEnable: {
    type: 'boolean',
    default: true
  },
  //スライダーのエフェクト
  slideEffect: {
    type: 'string',
    default: 'slide'
  },
  //スライダーの画像表示枚数
  slidesPerView: {
    type: 'number',
    default: 1
  },
  //スライダー画像の中央寄せ
  slideCentered: {
    type: 'boolean',
    default: false
  },
},

同様に PHP 側の register_block_type() の attributes にもスライダーのパラメータに使用する属性を追加します。

my-slider.php 抜粋
'attributes' => [
  'mediaID' => [
    'type' => 'array',
    'default' => []
  ],

  ・・・中略・・・

  //スライダー自動再生
  'slideAutoPlay' => [
    'type' => 'number',
    'default' => 0
  ],
  //スライダースピード
  'slideSpeed' => [
    'type' => 'number',
    'default' => 300
  ],
  //スライダーのループ設定
  'slideLoopEnable' => [
    'type' => 'boolean',
    'default' => true
  ],
  //スライダーのエフェクト
  'slideEffect' => [
    'type' => 'string',
    'default' => 'slide'
  ],
  //スライダーの画像表示枚数
  'slidesPerView' => [
    'type' => 'number',
    'default' => 1
  ],
  //スライダー画像の中央寄せ
  'slideCentered' => [
    'type' => 'boolean',
    'default' => false
  ],
]

PHP でのレンダリングの変更

ブロックの出力を PHP でレンダリングする render_callback 関数を以下のように変更します。

8〜28行目でスライダーのパラメータに使用する data 属性を作成します。 data 属性の値には attributes に設定した属性を指定します。作成したオプション(data 属性)を .swiper-container の div 要素に追加します。

my-slider.php 抜粋
function my_slider_render($attributes, $content) {

  if (empty($attributes['imageUrl'])) {
    return '';
  }

  //スライダーのオプション(attributes の値により data 属性を追加)
  $slider_options = '';
  //自動再生
  if($attributes['slideAutoPlay'] !== 0) {
    $slider_options .= 'data-autoplay="' .$attributes['slideAutoPlay'] . '"';
  }
  //スライドスピード
  $slider_options .=  ' data-speed="' .$attributes['slideSpeed'] . '"';
  //ループ
  if($attributes['slideLoopEnable']) {
    $slider_options .=  ' data-loop="true"';
  }
  //スライドエフェクト
  if($attributes['slideEffect'] !== 'slide') {
    $slider_options .= 'data-effect="' .$attributes['slideEffect'] . '"';
  }
  //スライダーに一度に表示する画像の数
  $slider_options .=  ' data-slidesPerView="' .$attributes['slidesPerView'] . '"';
  //画像の中央配置
  if($attributes['slideCentered']) {
    $slider_options .=  ' data-centeredSlides="true"';
  }

  $output = '<div class="wp-block-wdl-my-slider">';
  //上記で作成したオプション(data 属性)を追加
  $output .= '<div class="swiper-container" '. $slider_options .'>';
  $output .= '<div class="swiper-wrapper">';

  $imageUrl = $attributes['imageUrl'];
  $imageCaption = $attributes['imageCaption'];
  $imageAlt = $attributes['imageAlt'];

  for($i = 0; $i < count($imageUrl); $i ++) {
    $img_url = esc_url($imageUrl[$i]);
    $img_caption = $imageCaption[$i] ? esc_html($imageCaption[$i]): '';
    $img_alt = $imageAlt[$i] ? esc_attr($imageAlt[$i]): '';
    if($img_alt !=="") {
      $output .= '<div class="swiper-slide" ><img className="card_image" src="'
                 . $img_url . '" alt="' . $img_alt . '" />';
    }else {
      $output .= '<div class="swiper-slide" ><img className="card_image" src="'
                 . $img_url . '" alt="" aria-hidden="true" />';
    }
    //キャプションを表示する場合
    if($attributes['showCaption']) {
      if($img_caption) {
        $output .= '<div class="caption">'. $img_caption . '</div>';
      }
    }
    $output .= '</div>';
  }

  $output .= '</div>';
  if($attributes['showPagination']) {
    $output .= '<div class="swiper-pagination"></div>';
  }
  if($attributes['showNavigationButton']) {
    $output .= '<div class="swiper-button-prev"></div><div class="swiper-button-next"></div>';
  }
  if($attributes['showScrollbar']) {
    $output .= '<div class="swiper-scrollbar"></div>';
  }
  $output .= '</div></div>';
  return $output;
}

オプションをインスペクターに追加

スライダーのパラメータを設定するオプション(項目)をインスペクターに追加します。

すでにあるインスペクターを表示する関数 getInspectorControls に PanelBody コンポーネントを追加してその中に RangeControlSelectControlCheckboxControl コンポーネントを配置します。追加する PanelBody の initialOpen プロパティには false を指定して初期状態では閉じた状態で表示します。

自動再生では0を指定すると自動生成しないことにして、最大8000ミリ秒まで100ミリ秒単位で設定できるようにしています。

const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody
          title='スライダー表示設定'
          initialOpen={true}
        >

          ・・・中略・・・

        </PanelBody>
        <PanelBody
          title='スライダー詳細設定'
          initialOpen={false}
        >
          <PanelRow>
            <RangeControl
              label='自動再生'
              value={attributes.slideAutoPlay}
              onChange={(val) => setAttributes({ slideAutoPlay: val })}
              min={0}
              max={8000}
              step={100}
              help="自動生成しない場合は0を指定"
            />
          </PanelRow>
          <PanelRow>
            <RangeControl
              label='スライドスピード'
              value={attributes.slideSpeed}
              onChange={(val) => setAttributes({ slideSpeed: val })}
              min={100}
              max={1000}
              step={100}
            />
          </PanelRow>
          <PanelRow>
            <SelectControl
              label="エフェクト"
              value={attributes.slideEffect}
              options={[
                {label: "Slide", value: 'slide'},
                {label: "Fade", value: 'fade'},
                {label: "Cube", value: 'cube'},
                {label: "Coverflow", value: 'coverflow'},
                {label: "Flip", value: 'flip'},
              ]}
              onChange={(val) => setAttributes({ slideEffect: val })}
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label="ループ"
              checked={attributes.slideLoopEnable}
              onChange={(val) => setAttributes({ slideLoopEnable: val })}
            />
          </PanelRow>
          <PanelRow>
            <RangeControl
              label='表示枚数'
              value={attributes.slidesPerView}
              onChange={(val) => setAttributes({ slidesPerView: val })}
              min={1}
              max={5}
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label="中央配置"
              checked={attributes.slideCentered}
              onChange={(val) => setAttributes({ slideCentered: val })}
            />
          </PanelRow>
        </PanelBody>
      </InspectorControls>
    );
  }

プレビューでのスライダーの初期化の変更

プレビューで表示するスライダーに設定されたパラメータが反映されるように、スライダーを初期化する関数で属性の値をパラメータに指定して動的にオプションを設定するように変更します。

この関数は、プレビューモードがレンダリングされる際(属性 isEditMode が変更された際)に、useEffect フックで実行されます。

Swiper では自動生成のパラメータ autoplay の delay に0を指定するとスライドが自動生成されてしまうので、ユーザがレンジコントロールで0を指定した場合は、delay の値を 999999999 にして実質的に自動再生されないようにしています。

また、この例のスライダーのパラメータの値を保存する属性はそのブロックの値を保存していますが、複数のブロックがプレビューモードで表示されている場合、個々のブロックとその属性の関連付けがないため、対象のブロックのプレビューボタンを押すと、その他のブロックのプレビューモードになっているスライダーにもそのブロックの設定が反映されてしまいます。保存されている設定やフロントエンド側には影響はありませんが、ブロックのプレビューは1つずつ行わないとおかしなことになってしまいます。

const initSwiper = () => {
  let elementSpeed = attributes.slideSpeed,
    elementAutoPlay = attributes.slideAutoPlay,
    elementLoop = attributes.slideLoopEnable,
    elementEffect = attributes.slideEffect,
    elementSlidesPerView = attributes.slidesPerView,
    elementCenteredSlides = attributes.slideCentered;

  //自動再生はインスペクターで 0 の場合は、大きな値を指定して実質的に自動再生しないようにする
  if (elementAutoPlay != 0) {
    elementAutoPlay = parseInt(elementAutoPlay);
  } else {
    elementAutoPlay = 999999999;
  }

  let swiperSlider = new Swiper('.swiper-container', {
    speed: parseInt(elementSpeed),
    autoplay: {
      delay: elementAutoPlay
    },
    loop: elementLoop,
    effect: elementEffect,
    slidesPerView: parseInt(elementSlidesPerView),
    centeredSlides: elementCenteredSlides,
    pagination: {
      el: '.swiper-pagination', //ページネーションの要素
      type: 'bullets', //ページネーションの種類
      clickable: true, //クリックに反応させる
    },
    navigation: {
      nextEl: '.swiper-button-next', //「次へボタン」要素の指定
      prevEl: '.swiper-button-prev', //「前へボタン」要素の指定
    },
    scrollbar: {
      el: '.swiper-scrollbar', //要素の指定
    },
  });
}

サンプルファイル

以下は上記で作成したスライダーを表示するブロックのサンプルです(実用的なものではありません)。

実際には Smart Slider 3WordPress Slider Block Gutenslider のようなスライダーのプラグインが色々あるのでそれらを利用するのが簡単です。

編集モード

プレビューモード

以下はサンプルのファイル構成とソースコードです。実際にブロックを試すには src フォルダのファイルをコンパイルする必要があります。

my-slider
├── assets
│   ├── my-swiper-init.js //ファイル変更
│   ├── swiper.css
│   └── swiper.js
├── block.json
├── build
│   ├── index.asset.php
│   ├── index.css
│   ├── index.js
│   └── style-index.css
├── node_modules
├── my-slider.php
├── package-lock.json
├── package.json
├── readme.txt
└── src
    ├── edit.js
    ├── editor.scss
    ├── index.js
    ├── save.js //PHP でレンダリングする場合は使用しない
    └── style.scss  
const sliderElems = document.querySelectorAll('.wp-block-wdl-my-slider .swiper-container');

for ( let element of sliderElems ) {
  let elementSpeed = element.getAttribute('data-speed'),
      elementDirection = element.getAttribute('data-direction'),
      elementAutoPlay = element.getAttribute('data-autoplay'),
      elementLoop = element.getAttribute('data-loop'),
      elementEffect = element.getAttribute('data-effect'),
      elementSlidesPerView = element.getAttribute('data-slidesPerView'),
      elementCenteredSlides = element.getAttribute('data-centeredSlides');

  if (!elementSpeed) {
    elementSpeed = 300;
  }
  if (!elementDirection) {
    elementDirection = 'horizontal';
  }
  if (elementAutoPlay) {
    elementAutoPlay = parseInt(elementAutoPlay);
  } else {
    elementAutoPlay = 999999999;
  }
  if (elementLoop == 'true') {
    elementLoop = true;
  } else {
    elementLoop = false;
  }
  if (!elementEffect) {
    elementEffect = 'slide';
  }
  if (!elementSlidesPerView) {
    elementSlidesPerView = 1;
  }
  if (elementCenteredSlides == 'true') {
    elementCenteredSlides = true;
  } else {
    elementCenteredSlides = false;
  }

  let swiperSlider = new Swiper(element, {
    direction: elementDirection,
    speed: parseInt(elementSpeed),
    autoplay: {
      delay: elementAutoPlay
    },
    loop: elementLoop,
    effect: elementEffect,
    slidesPerView: parseInt(elementSlidesPerView),
    centeredSlides: elementCenteredSlides,
    pagination: {
      el: '.swiper-pagination',
      type: 'bullets',
      clickable: true,
    },
    navigation: {
      nextEl: '.swiper-button-next',
      prevEl: '.swiper-button-prev',
    },
    scrollbar: {
      el: '.swiper-scrollbar',
    },
  });
}

swiper.css(https://unpkg.com/swiper@6.3.2/swiper-bundle.min.css)

swiper.js(https://unpkg.com/swiper@6.3.2/swiper-bundle.min.js)

my-slider.php
<?php
/**
 * Plugin Name:     My Slider
 * Description:     Example block written with ESNext standard and JSX support – build step required.
 * Version:         0.1.0
 * Author:          The WordPress Contributors
 * License:         GPL-2.0-or-later
 * License URI:     https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:     my-slider
 *
 * @package         wdl
 */

function wdl_my_slider_block_init() {
  $dir = dirname( __FILE__ );

  $script_asset_path = "$dir/build/index.asset.php";
  if ( ! file_exists( $script_asset_path ) ) {
    throw new Error(
      'You need to run `npm start` or `npm run build` for the "wdl/my-slider" block first.'
    );
  }
  $index_js     = 'build/index.js';
  $script_asset = require( $script_asset_path );
  wp_register_script(
    'wdl-my-slider-block-editor',
    plugins_url( $index_js, __FILE__ ),
    $script_asset['dependencies'],
    $script_asset['version']
  );

  $editor_css = 'build/index.css';
  wp_register_style(
    'wdl-my-slider-block-editor',
    plugins_url( $editor_css, __FILE__ ),
    array(),
    filemtime( "$dir/$editor_css" )
  );

  $style_css = 'build/style-index.css';
  wp_register_style(
    'wdl-my-slider-block',
    plugins_url( $style_css, __FILE__ ),
    array(),
    filemtime( "$dir/$style_css" )
  );

  register_block_type( 'wdl/my-slider', array(
    'editor_script' => 'wdl-my-slider-block-editor',
    'editor_style'  => 'wdl-my-slider-block-editor',
    'style'         => 'wdl-my-slider-block',
    'render_callback' => 'my_slider_render',
    //属性を追加
    'attributes' => [
      //属性 mediaID(メディア ID の配列)
      'mediaID' => [
        'type' => 'array',
        'default' => []
      ],
      //属性 imageUrl(URL の配列)
      'imageUrl' => [
        'type' => 'array',
        'default' => []
      ],
      //属性 imageAlt(alt 属性の配列)
      'imageAlt' => [
        'type' => 'array',
        'default' => []
      ],
      //属性 imageCaption(キャプションの配列)
      'imageCaption' => [
        'type' => 'array',
        'default' => []
      ],
      //ナビゲーションボタンの表示・非表示
      'showNavigationButton' => [
        'type' => 'boolean',
        'default' => true
      ],
      //ページネーションの表示・非表示
      'showPagination' => [
        'type' => 'boolean',
        'default' => true
      ],
      //スクロールバーの表示・非表示
      'showScrollbar' => [
        'type' => 'boolean',
        'default' => true
      ],
      //キャプションの表示・非表示
      'showCaption' => [
        'type' => 'boolean',
        'default' => true
      ],
      //スライダー自動再生
      'slideAutoPlay' => [
        'type' => 'number',
        'default' => 0
      ],
      //スライダースピード
      'slideSpeed' => [
        'type' => 'number',
        'default' => 300
      ],
      //スライダーのループ設定
      'slideLoopEnable' => [
        'type' => 'boolean',
        'default' => true
      ],
      //スライダーのエフェクト
      'slideEffect' => [
        'type' => 'string',
        'default' => 'slide'
      ],
      //スライダーの画像表示枚数
      'slidesPerView' => [
        'type' => 'number',
        'default' => 1
      ],
      //スライダー画像の中央寄せ
      'slideCentered' => [
        'type' => 'boolean',
        'default' => false
      ],
    ]
  ) );
}
add_action( 'init', 'wdl_my_slider_block_init' );

function my_slider_render($attributes, $content) {
  //属性 imageUrl が空なら何も表示しない
  if (empty($attributes['imageUrl'])) {
    return '';
  }

  //スライダーのオプション(attributes の値により data 属性を追加)
  $slider_options = '';

  if($attributes['slideAutoPlay'] !== 0) {
    $slider_options .= 'data-autoplay="' .$attributes['slideAutoPlay'] . '"';
  }

  $slider_options .=  ' data-speed="' .$attributes['slideSpeed'] . '"';

  if($attributes['slideLoopEnable']) {
    $slider_options .=  ' data-loop="true"';
  }

  if($attributes['slideEffect'] !== 'slide') {
    $slider_options .= 'data-effect="' .$attributes['slideEffect'] . '"';
  }

  $slider_options .=  ' data-slidesPerView="' .$attributes['slidesPerView'] . '"';

  if($attributes['slideCentered']) {
    $slider_options .=  ' data-centeredSlides="true"';
  }

  //属性 imageUrl が空でなければスライダーのマークアップを組み立てる
  $output = '<div class="wp-block-wdl-my-slider">';
  $output .= '<div class="swiper-container" '. $slider_options .'>';
  $output .= '<div class="swiper-wrapper">';

  $imageUrl = $attributes['imageUrl'];
  $imageCaption = $attributes['imageCaption'];
  $imageAlt = $attributes['imageAlt'];

  for($i = 0; $i < count($imageUrl); $i ++) {
    $img_url = esc_url($imageUrl[$i]);
    $img_caption = $imageCaption[$i] ? esc_html($imageCaption[$i]): '';
    $img_alt = $imageAlt[$i] ? esc_attr($imageAlt[$i]): '';
    if($img_alt !=="") {
      $output .= '<div class="swiper-slide" ><img className="card_image" src="'
                 . $img_url . '" alt="' . $img_alt . '" />';
    }else {
      $output .= '<div class="swiper-slide" ><img className="card_image" src="'
                 . $img_url . '" alt="" aria-hidden="true" />';
    }
    //キャプションを表示する場合
    if($attributes['showCaption']) {
      if($img_caption) {
        $output .= '<div class="caption">'. $img_caption . '</div>';
      }
    }
    $output .= '</div>';
  }

  $output .= '</div>';
  //ページネーションを表示する場合
  if($attributes['showPagination']) {
    $output .= '<div class="swiper-pagination"></div>';
  }
  //ナビゲーションボタンを表示する場合
  if($attributes['showNavigationButton']) {
    $output .= '<div class="swiper-button-prev"></div><div class="swiper-button-next"></div>';
  }
  //スクロールバーを表示する場合
  if($attributes['showScrollbar']) {
    $output .= '<div class="swiper-scrollbar"></div>';
  }
  $output .= '</div></div>';
  return $output;
}

function add_my_slider_scripts_and_styles() {
  $dir = dirname( __FILE__ );

  //Swiper の JavaScript ファイルの読み込み(エンキュー)
  wp_enqueue_script(
    'swiper-slider',
    plugins_url( '/assets/swiper.js', __FILE__ ),
    array(),
    filemtime( "$dir/assets/swiper.js" ),
    true
  );

  if(! is_admin()) {
    //Swiper を初期化するためのファイルの読み込み(エンキュー)
    wp_enqueue_script(
      'swiper-slider-init',
      plugins_url( '/assets/my-swiper-init.js', __FILE__ ),
      //依存ファイルに上記 Swiper の JavaScript を指定
      array('swiper-slider'),
      filemtime( "$dir/assets/my-swiper-init.js" ),
      true
    );
  }

  //Swiper の CSS ファイルの読み込み(エンキュー)
  wp_enqueue_style(
    'swipe-style',
    plugins_url( '/assets/swiper.css', __FILE__ ),
    array(),
    filemtime( "$dir/assets/swiper.css"  )
  );

}
add_action('enqueue_block_assets', 'add_my_slider_scripts_and_styles');
edit.js
import { BlockControls, MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
import { InspectorControls } from '@wordpress/block-editor';
import { Toolbar, PanelBody, PanelRow, ToggleControl, RangeControl, SelectControl, CheckboxControl } from '@wordpress/components';
import { Fragment } from '@wordpress/element';
import { useEffect } from '@wordpress/element';
import './editor.scss';

export default function Edit( props ) {
  //分割代入を使って props 経由でプロパティを変数に代入
  const { className, attributes, setAttributes} = props;

  //選択された画像の情報を更新する関数
  const onSelectImage = ( media ) => {
    const media_ID = media.map((image) => image.id);
    const imageUrl = media.map((image) => image.url);
    const imageAlt = media.map((image) => image.alt);
    const imageCaption = media.map((image) => image.caption);

    setAttributes( {
      mediaID: media_ID,  //メディア ID の配列
      imageUrl: imageUrl,  // URL の配列
      imageAlt: imageAlt,  // alt 属性の配列
      imageCaption: imageCaption,  // キャプションの配列
    } );
  };

  //URL の配列から画像を生成
  const getImages = ( urls ) => {
    let imagesArray = urls.map(( url ) => {
      return (
        <img
          src={ url }
          className="image"
          alt="アップロード画像"
        />
      );
    });
    return imagesArray;
  }

  //URL とキャプションの配列から画像をキャプション付きで生成
  const getImagesWithCaption = ( url, caption ) => {
    let imagesArray = [];
    for(let i = 0 ; i < url.length; i ++) {
      imagesArray.push (
        <figure>
          <img
            src={ url[i] }
            className="image"
            alt="アップロード画像"
          />
          <figcaption className="block-image-caption">
            { caption[i] ? caption[i] : '' }
          </figcaption>
        </figure>
      );
    }
    return imagesArray;
  }

  //URL の配列から画像を生成(スライダー表示用)
  const getSliderImages = ( url, caption ) => {
    let imagesArray = [];
    for(let i = 0 ; i < url.length; i ++) {
      let image = '';
      if(attributes.showCaption) {
        image = (
          <div className="swiper-slide">
            <img
              src={ url[i] }
              className="image"
              alt="アップロード画像"
            />
            <div class="caption">
               { caption[i] ? caption[i] : '' }
            </div>
          </div>
        )
      }else{
        image = (
          <div className="swiper-slide">
            <img
              src={ url[i] }
              className="image"
              alt="アップロード画像"
            />
          </div>
        )
      }

      imagesArray.push ( image );
    }
    return imagesArray;
  }

  //スライダーを初期化する関数(属性の値をパラメータに指定して動的にオプションを設定)
  const initSwiper = () => {

    let elementSpeed = attributes.slideSpeed,
      elementAutoPlay = attributes.slideAutoPlay,
      elementLoop = attributes.slideLoopEnable,
      elementEffect = attributes.slideEffect,
      elementSlidesPerView = attributes.slidesPerView,
      elementCenteredSlides = attributes.slideCentered;

    //自動再生はインスペクターで 0 の場合は、大きな値を指定して実質的に自動再生しないようにする
    if (elementAutoPlay != 0) {
      elementAutoPlay = parseInt(elementAutoPlay);
    } else {
      elementAutoPlay = 999999999;
    }

    let swiperSlider = new Swiper('.swiper-container', {
      speed: parseInt(elementSpeed),
      autoplay: {
        delay: elementAutoPlay
      },
      loop: elementLoop,
      effect: elementEffect,
      slidesPerView: parseInt(elementSlidesPerView),
      centeredSlides: elementCenteredSlides,
      pagination: {
        el: '.swiper-pagination', //ページネーションの要素
        type: 'bullets', //ページネーションの種類
        clickable: true, //クリックに反応させる
      },
      navigation: {
        nextEl: '.swiper-button-next', //「次へボタン」要素の指定
        prevEl: '.swiper-button-prev', //「前へボタン」要素の指定
      },
      scrollbar: {
        el: '.swiper-scrollbar', //要素の指定
      },
    });
  }

  const getImageButton = (open) => {
    if(attributes.imageUrl.length > 0 ) {
      if(attributes.isEditMode) {
        return (
          <div onClick={ open } className="slider-block-container">
           { attributes.showCaption ? getImagesWithCaption( attributes.imageUrl, attributes.imageCaption ) : getImages( attributes.imageUrl ) }
          </div>
        )
      }else{
        //isEditMode が false ならスライダーを表示
        return (
          <div className="slider-container">
            <div className="swiper-container">
              <div className="swiper-wrapper">
                { getSliderImages(attributes.imageUrl, attributes.imageCaption) }
              </div>
              { attributes.showPagination &&
                <div className="swiper-pagination"></div>
              }
              { attributes.showNavigationButton &&
                <Fragment>
                  <div className="swiper-button-prev"></div>
                  <div className="swiper-button-next"></div>
                </Fragment>
              }
              { attributes.showScrollbar &&
                <div className="swiper-scrollbar"></div>
              }
            </div>
          </div>
        )
      }
    }
    else {
      return (
        <div className="button-container">
          <Button
            onClick={ open }
            className="button button-large"
          >
            画像をアップロード
          </Button>
        </div>
      );
    }
  };

  //画像を削除する(メディアをリセットする)関数
  const removeMedia = () => {
    setAttributes({
      mediaID: [],
      imageUrl: [],
      imageAlt: [],
      imageCaption: [],
    });
  }

  //インスペクターを追加する関数
  const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody
          title='スライダー表示設定'
          initialOpen={true}
        >
          <PanelRow>
            <ToggleControl
              label="ナビゲーションボタン"
              checked={attributes.showNavigationButton}
              onChange={(val) => setAttributes({ showNavigationButton: val })}
            />
          </PanelRow>
          <PanelRow>
            <ToggleControl
              label="ページネーション"
              checked={attributes.showPagination}
              onChange={(val) => setAttributes({ showPagination: val })}
            />
          </PanelRow>
          <PanelRow>
            <ToggleControl
              label="スクロールバー"
              checked={attributes.showScrollbar}
              onChange={(val) => setAttributes({ showScrollbar: val })}
            />
          </PanelRow>
          <PanelRow>
            <ToggleControl
              label="キャプション"
              checked={attributes.showCaption}
              onChange={(val) => setAttributes({ showCaption: val })}
            />
          </PanelRow>
        </PanelBody>
        <PanelBody
          title='スライダー詳細設定'
          initialOpen={false}
        >
          <PanelRow>
            <RangeControl
              label='自動再生'
              value={attributes.slideAutoPlay}
              onChange={(val) => setAttributes({ slideAutoPlay: val })}
              min={0}
              max={8000}
              step={100}
              help="自動生成しない場合は0を指定"
            />
          </PanelRow>
          <PanelRow>
            <RangeControl
              label='スライドスピード'
              value={attributes.slideSpeed}
              onChange={(val) => setAttributes({ slideSpeed: val })}
              min={100}
              max={1000}
              step={100}
            />
          </PanelRow>
          <PanelRow>
            <SelectControl
              label="エフェクト"
              value={attributes.slideEffect}
              options={[
                {label: "Slide", value: 'slide'},
                {label: "Fade", value: 'fade'},
                {label: "Cube", value: 'cube'},
                {label: "Coverflow", value: 'coverflow'},
                {label: "Flip", value: 'flip'},
              ]}
              onChange={(val) => setAttributes({ slideEffect: val })}
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label="ループ"
              checked={attributes.slideLoopEnable}
              onChange={(val) => setAttributes({ slideLoopEnable: val })}
            />
          </PanelRow>
          <PanelRow>
            <RangeControl
              label='表示枚数'
              value={attributes.slidesPerView}
              onChange={(val) => setAttributes({ slidesPerView: val })}
              min={1}
              max={5}
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label="中央配置"
              checked={attributes.slideCentered}
              onChange={(val) => setAttributes({ slideCentered: val })}
            />
          </PanelRow>
        </PanelBody>
      </InspectorControls>
    );
  }

  const getBlockControls = () => {
    return (
      <BlockControls>
        <Toolbar>
          <Button
            //属性 isEditMode の値により表示するラベルを切り替え
            label={ attributes.isEditMode ? "Preview" : "Edit" }
            //属性 isEditMode の値により表示するアイコンを切り替え
            icon={ attributes.isEditMode ? "format-image" : "edit" }
            className="my-custom-button"
            //setAttributes を使って属性の値を更新(真偽値を反転)
            onClick={() => setAttributes({ isEditMode: !attributes.isEditMode })}
          />
        </Toolbar>
      </BlockControls>
    );
  }

  //useEffect フックを使ってスライダーを初期化
  useEffect(() => {
    initSwiper();
  }, [attributes.isEditMode] );

  return (
    [
      getBlockControls(), //プレビューボタン
      getInspectorControls(),  //インスペクター
      <div className={ className }>
      <MediaUploadCheck>
        <MediaUpload
          multiple={ true }
          gallery={ true }
          onSelect={ onSelectImage }
          allowedTypes={ ['image'] }
          value={ attributes.mediaID }
          render={ ({ open }) => getImageButton( open ) }
        />
      </MediaUploadCheck>
      { attributes.imageUrl.length != 0  && attributes.isEditMode &&
        <MediaUploadCheck>
          <Button
            onClick={removeMedia}
            isLink
            isDestructive
            className="removeImage">画像を削除
          </Button>
          <Button
            onClick={() => setAttributes({ isEditMode: !attributes.isEditMode })}
            isLink
            className="removeImage">スライダーをプレビュー
          </Button>
        </MediaUploadCheck>
      }
    </div>
    ]
  );
}
index.js
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import save from './save';

registerBlockType( 'wdl/my-slider', {
  title: 'My Slider',
  description: 'Example block written with ESNext standard and JSX support',
  category: 'widgets',
  icon: 'smiley',
  supports: {
    html: false,
  },
  //属性を設定
  attributes: {
    //属性 mediaID(メディア ID の配列)
    mediaID: {
      type: 'array',
      default: []
    },
    //img の src に指定する URL
    imageUrl: {
      type: 'array',
      default: []
    },
    //img の alt 属性の値
    imageAlt: {
      type: 'array',
      default: []
    },
    //ナビゲーションボタンの表示・非表示
    showNavigationButton: {
      type: 'boolean',
      default: true
    },
    //ページネーションの表示・非表示
    showPagination: {
      type: 'boolean',
      default: true
    },
    //ページネーションの表示・非表示
    showScrollbar: {
      type: 'boolean',
      default: true
    },
    //img の キャプションの値
    imageCaption: {
      type: 'array',
      default: []
    },
    //キャプションの表示・非表示
    showCaption: {
      type: 'boolean',
      default: true
    },
    //isEditMode を属性として追加
    isEditMode: {
      type: 'boolean',
      default: true
    },
    //スライダー自動再生
    slideAutoPlay: {
      type: 'number',
      default: 0
    },
    //スライダースピード
    slideSpeed: {
      type: 'number',
      default: 300
    },
    //スライダーのループ設定
    slideLoopEnable: {
      type: 'boolean',
      default: true
    },
    //スライダーのエフェクト
    slideEffect: {
      type: 'string',
      default: 'slide'
    },
    //スライダーの画像表示枚数
    slidesPerView: {
      type: 'number',
      default: 1
    },
    //スライダー画像の中央寄せ
    slideCentered: {
      type: 'boolean',
      default: false
    },
  },
  edit: Edit,
  //save 関数で null を返す
  save: () => { return null }
} );
editor.scss
.wp-block-wdl-my-slider .slider-block-container {
  display: flex;
  flex-wrap: wrap;
  margin: 20px;
}

.wp-block-wdl-my-slider .slider-block-container img {
  width: 100%;
  max-width: 160px;
  margin: 10px;
}

.image {
  cursor: pointer;
}

.wp-block-wdl-my-slider figcaption.block-image-caption {
  text-align: center;
  margin-top: 0;
}
style.scss
$gray: #cccccc;
$off-white: #f1f1f1;

.wp-block-wdl-my-slider {
  background-color: #fefefe;
  color: #666;
  padding: 10px;
  border: 1px solid #ccc;
}

.button-container {
  text-align: center;
  padding: 22% 0;
  background: $off-white;
  border: 1px solid $gray;
  border-radius: 2px;
  margin: 0 0 1.2rem 0;
}

:root {
  --swiper-navigation-color: #B8DCF5;
  --swiper-pagination-color: #B8DCF5;
}

.swiper-container .caption {
  background-color: rgba(0,0,0,0.7);
  color: #fefefe;
  text-align: center;
  position: absolute;
  bottom: 0px;
  width: 100%;
}

.swiper-container .swiper-pagination.swiper-pagination-bullets {
  bottom: 30px;
}
package.json
{
  "name": "my-slider",
  "version": "0.1.0",
  "description": "Example block written with ESNext standard and JSX support – build step required.",
  "author": "The WordPress Contributors",
  "license": "GPL-2.0-or-later",
  "main": "build/index.js",
  "scripts": {
    "build": "wp-scripts build",
    "format:js": "wp-scripts format-js",
    "lint:css": "wp-scripts lint-style",
    "lint:js": "wp-scripts lint-js",
    "start": "wp-scripts start",
    "packages-update": "wp-scripts packages-update"
  },
  "devDependencies": {
    "@wordpress/scripts": "^12.3.0"
  }
}

save 関数を使ってレンダリングする場合

以下は PHP でのレンダリングではなく、save 関数を使ってレンダリングする場合のサンプルです。

index.js では save 関数の記述を null から変更します。

index.js
edit: Edit,
save,
//上記を追加して、以下を削除
//save: () => { return null }

また、attributes の source プロパティは PHP でレンダリングされるブロックではサポートされていませんが、save 関数を使ってレンダリングする場合は使用することができます。

source プロパティを指定しない場合、属性はブロックのコメントデリミタに JSON 形式で保存されます。

以下は source プロパティを指定しない場合のコードエディターでの表示です。

この例の場合、自動再生などのパラメータの値は data 属性に設定しているので、save 関数を使ってレンダリングする場合は source プロパティに attribute を指定して attribute プロパティで data 属性を指定することができます。関連:attributes

slideAutoPlay: {
  type: 'number',
  default: 0,
  source: 'attribute',
  selector: '.swiper-container',
  attribute: 'data-autoplay',
},

以下は source プロパティを指定した場合のコードエディターでの表示で、自動再生のオプションの slideAutoPlay などはコメントデリミタからなくなっているのが確認できます。

この例の場合、source プロパティを指定する必要性はありませんが、以下は attributes の source プロパティを指定した場合の例です(PHP でレンダリングする場合は、source プロパティを削除する必要があります)。

import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import save from './save';

registerBlockType( 'wdl/my-slider', {
  title: 'My Slider',
  description: 'Example block written with ESNext standard and JSX support',
  category: 'widgets',
  icon: 'smiley',
  supports: {
    html: false,
  },
  //属性を設定
  attributes: {
    //属性 mediaID(メディア ID の配列)
    mediaID: {
      type: 'array',
      default: []
    },
    //img の src に指定する URL
    imageUrl: {
      type: 'array',
      default: []
    },
    //img の alt 属性の値
    imageAlt: {
      type: 'array',
      default: []
    },
    //ナビゲーションボタンの表示・非表示
    showNavigationButton: {
      type: 'boolean',
      default: true
    },
    //ページネーションの表示・非表示
    showPagination: {
      type: 'boolean',
      default: true
    },
    //ページネーションの表示・非表示
    showScrollbar: {
      type: 'boolean',
      default: true
    },
    //img の キャプションの値
    imageCaption: {
      type: 'array',
      default: []
    },
    //キャプションの表示・非表示
    showCaption: {
      type: 'boolean',
      default: true
    },
    //isEditMode を属性として追加
    isEditMode: {
      type: 'boolean',
      default: true
    },
    //スライダー自動再生
    slideAutoPlay: {
      type: 'number',
      default: 0,
      source: 'attribute',
      selector: '.swiper-container',
      attribute: 'data-autoplay',
    },
    //スライダースピード
    slideSpeed: {
      type: 'number',
      default: 300,
      source: 'attribute',
      selector: '.swiper-container',
      attribute: 'data-speed',
    },
    //スライダーのループ設定
    slideLoopEnable: {
      type: 'boolean',
      default: true,
      source: 'attribute',
      selector: '.swiper-container',
      attribute: 'data-loop',
    },
    //スライダーのエフェクト
    slideEffect: {
      type: 'string',
      default: 'slide',
      source: 'attribute',
      selector: '.swiper-effect',
      attribute: 'data-autoplay',
    },
    //スライダーの画像表示枚数
    slidesPerView: {
      type: 'number',
      default: 1,
      source: 'attribute',
      selector: '.swiper-container',
      attribute: 'data-slidesPerView',
    },
    //スライダー画像の中央寄せ
    slideCentered: {
      type: 'boolean',
      default: false,
      source: 'attribute',
      selector: '.swiper-container',
      attribute: 'data-centeredSlides',
    },
    /* PHP でレンダリングする場合は source プロパティは使えない
    //スライダー自動再生
    slideAutoPlay: {
      type: 'number',
      default: 0
    },
    //スライダースピード
    slideSpeed: {
      type: 'number',
      default: 300
    },
    //スライダーのループ設定
    slideLoopEnable: {
      type: 'boolean',
      default: true
    },
    //スライダーのエフェクト
    slideEffect: {
      type: 'string',
      default: 'slide'
    },
    //スライダーの画像表示枚数
    slidesPerView: {
      type: 'number',
      default: 1
    },
    //スライダー画像の中央寄せ
    slideCentered: {
      type: 'boolean',
      default: false
    }, */
  },
  edit: Edit,
  save,
} );

my-slider.php では attributes と render_callback キーを削除します。

my-slider.php
<?php
/**
 * Plugin Name:     My Slider
 * Description:     Example block written with ESNext standard and JSX support – build step required.
 * Version:         0.1.0
 * Author:          The WordPress Contributors
 * License:         GPL-2.0-or-later
 * License URI:     https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:     my-slider
 *
 * @package         wdl
 */

function wdl_my_slider_block_init() {
  $dir = dirname( __FILE__ );

  $script_asset_path = "$dir/build/index.asset.php";
  if ( ! file_exists( $script_asset_path ) ) {
    throw new Error(
      'You need to run `npm start` or `npm run build` for the "wdl/my-slider" block first.'
    );
  }
  $index_js     = 'build/index.js';
  $script_asset = require( $script_asset_path );
  wp_register_script(
    'wdl-my-slider-block-editor',
    plugins_url( $index_js, __FILE__ ),
    $script_asset['dependencies'],
    $script_asset['version']
  );

  $editor_css = 'build/index.css';
  wp_register_style(
    'wdl-my-slider-block-editor',
    plugins_url( $editor_css, __FILE__ ),
    array(),
    filemtime( "$dir/$editor_css" )
  );

  $style_css = 'build/style-index.css';
  wp_register_style(
    'wdl-my-slider-block',
    plugins_url( $style_css, __FILE__ ),
    array(),
    filemtime( "$dir/$style_css" )
  );

  register_block_type( 'wdl/my-slider', array(
    'editor_script' => 'wdl-my-slider-block-editor',
    'editor_style'  => 'wdl-my-slider-block-editor',
    'style'         => 'wdl-my-slider-block',
  ) );
}
add_action( 'init', 'wdl_my_slider_block_init' );

function add_my_slider_scripts_and_styles() {
  $dir = dirname( __FILE__ );

  //Swiper の JavaScript ファイルの読み込み(エンキュー)
  wp_enqueue_script(
    'swiper-slider',
    plugins_url( '/assets/swiper.js', __FILE__ ),
    array(),
    filemtime( "$dir/assets/swiper.js" ),
    true
  );

  if(! is_admin()) {
    //Swiper を初期化するためのファイルの読み込み(エンキュー)
    wp_enqueue_script(
      'swiper-slider-init',
      plugins_url( '/assets/my-swiper-init.js', __FILE__ ),
      //依存ファイルに上記 Swiper の JavaScript を指定
      array('swiper-slider'),
      filemtime( "$dir/assets/my-swiper-init.js" ),
      true
    );
  }

  //Swiper の CSS ファイルの読み込み(エンキュー)
  wp_enqueue_style(
    'swipe-style',
    plugins_url( '/assets/swiper.css', __FILE__ ),
    array(),
    filemtime( "$dir/assets/swiper.css"  )
  );

}
add_action('enqueue_block_assets', 'add_my_slider_scripts_and_styles');
save.js
import { Fragment } from '@wordpress/element';

export default function save( { attributes } ) {
  //画像をレンダリングする関数
  const getImagesSave = ( url, alt, caption ) => {
    let image_elem;
    let imagesArray = [];

    for( let i = 0 ; i < url.length; i ++ ) {
      if( url.length === 0 ) {
        image_elem = null;
      }else{
        if( alt[i] ) {
          image_elem =  (
            <div className="swiper-slide">
              <img
                className="card_image"
                src={ url[i] }
                alt={ alt[i] }
              />
              { attributes.showCaption &&
                <div class="caption">{ caption[i] ? caption[i] : "" }</div>
              }
            </div>
          );
        }else{
          image_elem = (
            <div className="swiper-slide">
              <img
                className="card_image"
                src={ url[i] }
                alt=""
                aria-hidden="true"
              />
              { attributes.showCaption &&
                <div class="caption">{ caption[i] ? caption[i] : "" }</div>
              }
            </div>
          );
        }
      }
      imagesArray.push( image_elem ) ;
    }
    return imagesArray;
  }

  //attributes から data 属性に指定する値を取得
  let elementSpeed = parseInt(attributes.slideSpeed),
      elementAutoPlay = parseInt(attributes.slideAutoPlay),
      elementLoop = attributes.slideLoopEnable ? "true" : "false",
      elementEffect = attributes.slideEffect,
      elementSlidesPerView = parseInt(attributes.slidesPerView),
      elementCenteredSlides = attributes.slideCentered ? "true" : "false";

  //自動再生はインスペクターで 0 の場合は、大きな値を指定して実質的に自動再生しないようにする
  if (elementAutoPlay === 0) {
    elementAutoPlay = 999999999;
  }

  return (
    <div className="slider-container">
      <div
        className="swiper-container"
        data-speed={elementSpeed}
        data-autoplay={elementAutoPlay}
        data-loop={elementLoop}
        data-effect={elementEffect}
        data-slidesPerView={elementSlidesPerView}
        data-centeredSlides={elementCenteredSlides}
      >
        <div className="swiper-wrapper">
          { getImagesSave( attributes.imageUrl, attributes.imageAlt, attributes.imageCaption ) }
        </div>
        { attributes.showPagination &&
          <div className="swiper-pagination"></div>
        }
        { attributes.showNavigationButton &&
          <Fragment>
            <div className="swiper-button-prev"></div>
            <div className="swiper-button-next"></div>
          </Fragment>
        }
        { attributes.showScrollbar &&
          <div className="swiper-scrollbar"></div>
        }
      </div>
    </div>
  );
}

save 関数を書き換えて投稿のページで再読込すると「このブロックには、想定されていないか無効なコンテンツが含まれています」と表示されるので、表示されるボタンをクリックして「ブロックを削除」を選択し、現在のブロックを一度削除してから再度ブロックを挿入する必要があります。