WordPress Logo WordPress スライダーでアイキャッチ画像を表示するブロックを作成

WordPress のブロックエディタ Gutenberg で特定のカテゴリーのアイキャッチ画像をスライダーで表示するブロックを作成する方法の覚書です。スライダーの表示はダイナミックブロックを作成して PHP でレンダリングします。

スライダーは Swiper という JavaScript のプラグインを使用しています。

更新日:2020年10月29日

作成日:2020年10月14日

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

関連ページ

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

概要

以下は特定のカテゴリーの投稿に設定されているアイキャッチ画像をスライダーで表示するブロックの例です。ブロックはダイナミックブロックで作成します。

スライダーに表示する画像の数やナビゲーション、ページネーション、キャプションなどの表示・非表示はインスペクターで設定が可能です。

withSelect と getEntityRecords を使って全てのカテゴリーを取得してそのリストを表示し、ユーザーが選択したカテゴリーの投稿のアイキャッチ画像を表示します。

以下のサイトを参考にさせていただきました。

Tutorial: Create a Slider as a Dynamic Gutenberg Block

環境構築とファイルの作成

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

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

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

create-block の実行

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

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

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

以下は、--namespace オプションで namespace を wdl に指定し、slug に my-dynamic-slider を指定して create-block のコマンドを実行しています。

この例では対話モードで slug に my-dynamic-slider を指定して create-block を実行したので、以下のようなファイルが出力されます。

my-dynamic-slider //作成されたプラグインのディレクトリ
├── block.json  //ブロックディレクトリの登録で使用されるファイル(?)
├── build //ビルドで出力されるファイルのディレクトリ
│   ├── index.asset.php //依存情報とファイルバージョンが記載されるファイル(自動生成)
│   ├── index.css  //editor.scss がビルドで変換された CSS
│   ├── index.js  //ビルドされたブロック用のスクリプト
│   └── style-index.css // style.scss がビルドで変換された CSS
├── node_modules //関連パッケージのディレクトリ
├── my-dynamic-slider.php  //メインのプラグインファイル
├── package-lock.json
├── package.json //パッケージの設定ファイル
├── readme.txt
└── src  //開発用ディレクトリ(この中のファイルを編集)
    ├── edit.js
    ├── editor.scss
    ├── index.js  //ブロック用のスクリプト
    ├── save.js
    └── style.scss

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

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

$ cd my-dynamic-slider  return  //プラグインのディレクトリに移動
  
$ npm start  return  //開発モード

procuction ビルドするには npm run build を実行します。

初期ファイルの作成

この例では create-block を実行して生成されたファイルを利用します。

src フォルダ内に生成されたブロックの作成に使用するファイルにはコメントや翻訳関連の関数などが記述されていますが、コメントは削除し、この例では翻訳関数は使わないのでその部分なども変更しています。

以下が変更後のファイルです。

src/edit.js

edit.js はこの時点では p 要素をレンダリングするだけです。

src/edit.js(edit プロパティに指定する Edit コンポーネント)
import './editor.scss';

export default function Edit( { className } ) {
  return (
    <p className={ className }>My Dynamic Slider – hello from the editor!</p>
  );
}
save 関数で null を return

ダイナミックブロックを作成するので src/index.js では save 関数が null を返すように変更して、save.js は削除します。

src/index.js(ブロックを登録・定義する registerBlockType が記述されたファイル)
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';

registerBlockType( 'wdl/my-dynamic-slider', {
  title: 'My Dynamic Slider',
  description:'Example block written with ESNext standard and JSX support',
  category: 'widgets',
  icon: 'smiley',
  supports: {
    html: false,
  },
  edit: Edit,
  // save 関数の定義を直接記述して null を返す
  save: () => { return null }
} );
render_callback の追加

ブロックを PHP でレンダリングするには、PHP 側でブロックを登録する register_block_type 関数の第2パラメータの配列に render_callback を追加して、ブロックをレンダリングする関数を定義します。

my-dynamic-slider.php
register_block_type( 'wdl/my-dynamic-slider', array(
  'editor_script' => 'wdl-my-dynamic-slider-block-editor',
  'editor_style'  => 'wdl-my-dynamic-slider-block-editor',
  'style'         => 'wdl-my-dynamic-slider-block',
  //render_callback を追加
  'render_callback' => 'my_dynamic_slider_render',
) );

この時点では以下のような単に p 要素を返すだけの render_callback 関数を定義しておきます。

function my_dynamic_slider_render($attributes, $content) {
  return '<p>スライダーのレンダリングをここに記述</p>';
}
<?php
/**
 * Plugin Name:     My Dynamic 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-dynamic-slider
 *
 * @package         wdl
 */

/**
 * Registers all block assets so that they can be enqueued through the block editor
 * in the corresponding context.
 *
 * @see https://developer.wordpress.org/block-editor/tutorials/block-tutorial/applying-styles-with-stylesheets/
 */
function wdl_my_dynamic_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-dynamic-slider" block first.'
    );
  }
  $index_js     = 'build/index.js';
  $script_asset = require( $script_asset_path );
  wp_register_script(
    'wdl-my-dynamic-slider-block-editor',
    plugins_url( $index_js, __FILE__ ),
    $script_asset['dependencies'],
    $script_asset['version']
  );
  wp_set_script_translations( 'wdl-my-dynamic-slider-block-editor', 'my-dynamic-slider' );

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

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

  register_block_type( 'wdl/my-dynamic-slider', array(
    'editor_script' => 'wdl-my-dynamic-slider-block-editor',
    'editor_style'  => 'wdl-my-dynamic-slider-block-editor',
    'style'         => 'wdl-my-dynamic-slider-block',
    //render_callback を追加
    'render_callback' => 'my_dynamic_slider_render',
  ) );
}
add_action( 'init', 'wdl_my_dynamic_slider_block_init' );

function my_dynamic_slider_render($attributes, $content) {
  return '<p>スライダーのレンダリングをここに記述</p>';
}

この時点で、プラグインページで作成したプラグインを有効化し、投稿に挿入すると以下のように表示されます。

以下はフロントエンド側の表示例です。

属性の追加

ユーザが選択した値を保持するためにこの例では以下のような属性(attributes)を設定します。

  • termId :表示するアイキャッチ画像のカテゴリーの ID
  • numSlides:スライダーに表示する画像の枚数
  • showNavigationButton:ナビゲーションボタンの表示・非表示
  • showPagination:ページネーションの表示・非表示
  • showScrollbar:スクロールバーの表示・非表示
  • showCaption:キャプションの表示・非表示

この例の場合、ダイナミックブロックなので PHP 側の register_block_type で属性を設定します。

register_block_type に attributes を設定する場合は連想配列で設定します。

my-dynamic-slider.php
register_block_type( 'wdl/my-dynamic-slider', array(
  'editor_script' => 'wdl-my-dynamic-slider-block-editor',
  'editor_style'  => 'wdl-my-dynamic-slider-block-editor',
  'style'         => 'wdl-my-dynamic-slider-block',
  //render_callback を追加
  'render_callback' => 'my_dynamic_slider_render',
  //属性を追加
  'attributes' => [
    //アイキャッチ画像のカテゴリーのID
    'termId' => [
      'type' => 'number', 
      'default' => 0
    ],
    //スライダーに表示する画像の数
    'numSlides' => [
      'type' => 'number', 
      'default' => 3
    ],
    //ナビゲーションボタンの表示・非表示
    'showNavigationButton' => [
      'type' => 'boolean', 
      'default' => true
    ],
    //ページネーションの表示・非表示
    'showPagination' => [
      'type' => 'boolean', 
      'default' => true
    ],
    //スクロールバーンの表示・非表示
    'showScrollbar' => [
      'type' => 'boolean', 
      'default' => true
    ],
    //キャプションの表示・非表示
    'showCaption' => [
      'type' => 'boolean', 
      'default' => true
    ],
  ]
));

インスペクターの追加

インスペクター(設定サイドバー)に、表示する画像の数を指定できるスライドバー(RangeControl)及びナビゲーションボタンなどの表示・非表示を選択するトグルボタン(ToggleControl)を設置します

InspectorControls コンポーネントの中に PanelBody、PanelRow コンポーネントを配置し、その中に RangeControl や ToggleControl を配置します。

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

それぞれのコンポーネントをパッケージからインポートします。

パッケージ コンポーネント
block-editor InspectorControls
components Placeholder, PanelBody, PanelRow, RangeControl, ToggleControl
src/edit.js
import { InspectorControls } from '@wordpress/block-editor';
import { Placeholder, PanelBody, PanelRow, RangeControl, ToggleControl } from '@wordpress/components';

インスペクターは return ステートメントに直接記述することもできますが、この例ではインスペクターを追加する以下のような関数を作成します。

src/edit.js
//インスペクターを表示する関数
const getInspectorControls = () => {
  return (
    <InspectorControls>
      <PanelBody
        title='Slider Settings'
        initialOpen={true}
      >
        <PanelRow>   
          <RangeControl
            label='Number of slides'
            value={attributes.numSlides}
            onChange={(val) => setAttributes({ numSlides: val })}
            min={1}
            max={10}
          />
        </PanelRow>
        <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>
  );
}

RangeControl コンポーネントには以下のようなプロパティを指定することができます。

onChange プロパティでは値が変更された際に setAttributes で属性 numSlides の値を更新し、その値が val プロパティの値になります。

RangeControl のプロパティ(一部抜粋)
プロパティ 説明
label 指定すると label 要素でラベルを出力します。
help 指定するとヘルプテキストを出力します。
beforeIcon DashIcon を指定するとスライダーの前にアイコンを表示できます。
afterIcon DashIcon を指定するとスライダーの後ろにアイコンを表示できます。
allowReset このプロパティに true を指定するとデフォルトにリセットするボタンを表示します。
onChange 値が変更された際に新しい値を受け取るコールバック関数を指定します。
min 指定できる値の最小値
max 指定できる値の最大値
step 最小値(min)と最大値(max)の間のステップ間隔
value 現在のレンジスライダーの値

以下は ToggleControl のプロパティです。トグルボタンの値(checked)は真偽値になります。

ToggleControl
プロパティ 説明
label 指定すると label 要素でラベルを出力します。
help 指定するとヘルプテキストを出力します。
checked トグルボタンの状態を表す真偽値。トグルボタンがオンの場合、checked の値は true になり、false の場合はトグルボタンはオフ(チェックされていない状態)です。
onChange checked の値(真偽値)を引数に取る関数

Placeholder の追加

この例では編集画面では実際のスライダーは表示せず、スライダーを表示する部分の領域に Placeholder コンポーネントを配置し、その中に後で MenuItem コンポーネントを使ったアイキャッチ画像のカテゴリーを選択するためのリストを表示します。

Placeholder コンポーネントには以下のようなプロパティを指定することができます。

プロパティ 説明
label プレースホルダーのラベルを div 要素で出力します。
icon DashIcon を指定するとラベルの前にアイコンを出力します。
instructions 指定するとインストラクション用のテキストをラベルの下に表示します。
isColumnLayout true を指定すると Placeholder コンポーネントの子要素のレイアウトを flex-row から flex-column に変更します。

この時点では instructions と icon プロパティとダミーの文字列「スライダーのプレースホルダー」を表示しておきます。

return ステートメント内に前述のインスペクターの関数と共にプレースホルダーのレンダリングを記述します。その際にはレンダリングを配列で記述します。

src/edit.js
return (
  [
    getInspectorControls(),  //インスペクター
    <div className={className}>
      <Placeholder
        label='スライダーカテゴリー'
        icon='format-image'
        instructions='スライダーに表示するアイキャッチ画像のカテゴリーを選択'
      >
        スライダーのプレースホルダー
      </Placeholder>
    </div>
  ]
);
import './editor.scss';
import { InspectorControls } from '@wordpress/block-editor';
import { Placeholder, PanelBody, PanelRow, RangeControl, ToggleControl } from '@wordpress/components';
 
export default function Edit( props ) {
  //props 経由でプロパティを取得して分割代入で変数を設定
  const {className, attributes, setAttributes} = props;
  
  //インスペクターを表示する関数
  const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody
          title='Slider Settings'
          initialOpen={true}
        >
          <PanelRow>   
            <RangeControl
              label='Number of slides'
              value={attributes.numSlides}
              onChange={(val) => setAttributes({ numSlides: val })}
              min={1}
              max={10}
            />
          </PanelRow>
          <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}>
        <Placeholder
          label='スライダーカテゴリー'
          icon='format-image'
          instructions='スライダーに表示するアイキャッチ画像のカテゴリーを選択'
        >
          スライダーのプレースホルダー
        </Placeholder>
      </div>
    ]
  );
}

上記を記述して保存するとエディター画面は以下のような表示になります。

カテゴリーを選択するコンポーネントの作成

このスライダーでは指定された投稿のカテゴリーのアイキャッチ画像を表示するので、投稿のカテゴリーを WordPress から取得する必要があります。

投稿の情報(この場合はカテゴリー)を取得するには withSelect を使います。

withSelect

withSelect を使うには、data モジュールから withSelectselect をインポートします。

import { withSelect, select } from '@wordpress/data';

withSelect の中で select() と getEntityRecords() を使って投稿の情報(オブジェクト)を取得することができます。例えば、3件の投稿を取得するには以下のように記述します。

select('core').getEntityRecords('postType', 'post',  { per_page: 3 })

セレクター select('core') で Core Data にアクセスしてセレクターの関数 getEntityRecords() でパラメータで指定したオブジェクトを取得します。全てのカテゴリーを取得するには以下のように記述します。

select('core').getEntityRecords('taxonomy', 'category', {per_page: -1})

この例ではカテゴリーを選択する CategorySelect コンポーネントを別ファイル(category-select.js)に記述します。

以下では withSelect と定義した CategorySelect コンポーネントの組み合わせをデフォルトエクスポートしています(withSelect の戻り値は CategorySelect が拡張されたコンポーネント)。

withSelect 内では、select() で取得したカテゴリーを値に指定したオブジェクト terms を return しています。terms は withSelect に渡されるコンポーネント(CategorySelect)への props になります。

つまり、CategorySelect コンポーネントでは props 経由で terms を取得できます。言い換えると CategorySelect コンポーネントの props に terms がマージされます。

この時点では、CategorySelect コンポーネントは props 経由で取得した terms(カテゴリーオブジェクトの配列)をコンソールに出力し、「Category Select」と表示する div 要素をレンダリングします。

category-select.js
import { withSelect } from '@wordpress/data'; 
 
const CategorySelect = (props) => {
  console.log(props.terms);  //カテゴリーをコンソールに出力

  return(
    <div>Category Select</div>
  );
}
 
export default withSelect((select, props) => {
  return {
    terms: select('core').getEntityRecords('taxonomy', 'category', {per_page: -1})
  }
})(CategorySelect);

作成した CategorySelect コンポーネントを src/edit.js でインポートして使用します。

src/edit.js
import CategorySelect from './category-select';

return ステートメント内の Placeholder 内に CategorySelect を配置(ネスト)します。

return (
  [
    getInspectorControls(),
    <div className={className}>
      <Placeholder
        label='スライダーカテゴリー'
        icon='format-image'
        instructions='スライダーに表示するアイキャッチ画像のカテゴリーを選択'
      >
        <CategorySelect 
        />
      </Placeholder>
    </div>
  ]
);

ブロックを挿入した投稿のエディター画面のコンソールにはカテゴリーオブジェクトの配列が出力されます。コンソールには最初に空の配列や null が出力されますが、これは getEntityRecords() が非同期で行われるためで、値がまだ受信されていない(投稿が読み込まれていない)場合は null を返します。

カテゴリーリストのレンダリング

MenuGroupMenuItem コンポーネントを使って選択するカテゴリーのリストをレンダリングします。

メニュー(リスト)に表示する項目(MenuItem)を MenuGroup でラップします。

<MenuGroup label="Settings">
  <MenuItem>Setting 1</MenuItem>
  <MenuItem>Setting 2</MenuItem>
</MenuGroup>

CategorySelect コンポーネントで、取得したカテゴリーを MenuGroup と MenuItem でレンダリングします。

MenuGroup と MenuItem を components パッケージからインポートします。

import { MenuGroup, MenuItem } from '@wordpress/components'; 

props 経由で取得した terms はカテゴリーオブジェクトの配列なので map() を使ってループして各オブジェクトの名前(name プロパティ)を取得して MenuItem に出力します。

MenuItem の role プロパティには menuitemradio を指定して、1つの項目を選択できるようにしています。

category-select.js
const CategorySelect = (props) => {
  // props 経由で terms(カテゴリーオブジェクトの配列)を取得
  const { terms } = props;
  
  //terms を map() でループして name プロパティを MenuItem に出力
  return(
    <MenuGroup
      className="my-category-select"
    >
      {terms && (
        terms.map((item) => (
          <MenuItem
            role="menuitemradio"
          >
            {item.name}
          </MenuItem>
        ))
      )}
    </MenuGroup>
  );
}

MenuItem には以下のようなプロパティがあります。

プロパティ 説明
children button 要素の子要素としてレンダリングする要素を指定
info ボタン(button)のテキストの説明として使用するテキスト
icon 項目の前に表示するアイコン(DashIcon)。例 icon='smiley'
isSelected 現在この項目が選択されているかどうかの真偽値
role ARIA の role 属性。選択可能なメニュー項目が必要な場合は、単一選択には menuitemradio を使用し、複数選択には menuitemcheckbox を使用します。
import { withSelect } from '@wordpress/data'; 
import { MenuGroup, MenuItem } from '@wordpress/components'; 
 
const CategorySelect = (props) => {
  // props 経由で terms(カテゴリーオブジェクト)を取得
  const { terms } = props;
  
  //terms(カテゴリーオブジェクトの配列)を map() でループして name プロパティを MenuItem に出力
  return(
    <MenuGroup
      className="my-category-select"
    >
      {terms && (
        terms.map((item) => (
          <MenuItem
            role="menuitemradio"
          >
            {item.name}
          </MenuItem>
        ))
      )}
    </MenuGroup>
  );
}
 
export default withSelect((select, props) => {
  return {
    terms: select('core').getEntityRecords('taxonomy', 'category', {per_page: -1})
  }
})(CategorySelect);

カテゴリーリストのスタイルの設定

カテゴリーが多い場合、カテゴリーリストは長くなってしまうので、max-height を設定してそれ以上の高さになる場合はスクロールバーを表示するようにします。

MenuGroup に指定したクラス my-category-select を使ってスタイルを指定します。以下を editor.scss に記述します。

src/editor.scss
 .my-category-select {
  max-height: 200px;
  overflow: hidden scroll;
  border: 1px solid #C0BCBC;
  background: #fcfcfc;
}
props の追加

props はコンポーネントに渡される任意のデータで、props を使ってデータをコンポーネントに渡すことができます。(関連項目:React props

現在選択されているカテゴリーのID(属性 termId の値)を selectedTermId、選択が変更された場合にその値(カテゴリーID)を更新する関数を selectTerm として CategorySelect コンポーネントの props に追加します。

コンポーネント内では、selectedTermId と各 id を比較して MenuItem の isSelected プロパティ及び icon プロパティの値を設定しています。そして onClick プロパティのハンドラ(クリックされたら呼び出される関数)に selectTerm を指定しています。

category-select.js
const CategorySelect = (props) => {
  //  selectedTermId  と selectTerm を追加
  const { terms, selectedTermId, selectTerm } = props;
  
  return(
    <MenuGroup
      className="my-category-select"
    >
      {terms && (
        terms.map((item) => (
          <MenuItem
            role="menuitemradio"
            //selectedTermId と id を比較して真偽値を設定
            isSelected={item.id == selectedTermId}
            //selectedTermId と id の比較結果により表示するアイコンを設定
            icon={item.id == selectedTermId ? 'yes' : 'marker'}
            //ハンドラで選択されたIDを属性に設定して更新
            onClick={() => selectTerm(item.id)}
          >
            {item.name}
          </MenuItem>
        ))
      )}
    </MenuGroup>
  );
}
category-select.js
import { withSelect } from '@wordpress/data'; 
import { MenuGroup, MenuItem } from '@wordpress/components'; 
 
const CategorySelect = (props) => {
  // props 経由で terms(カテゴリーオブジェクト)を取得
  // selectedTermId は選択されているカテゴリーID の値のプロパティ
  // selectTerm は選択されているカテゴリーIDを更新する関数のプロパティ
  const { terms, selectedTermId, selectTerm } = props;
  
  //terms(カテゴリーオブジェクトの配列)をループして各 name プロパティを MenuItem に出力
  return(
    <MenuGroup
      className="my-category-select"
    >
      {terms && (
        terms.map((item) => (
          <MenuItem
            role="menuitemradio"
            //selectedTermId と id を比較して真偽値を設定
            isSelected={item.id == selectedTermId}
            //selectedTermId と id の比較結果により表示するアイコンを設定
            icon={item.id == selectedTermId ? 'yes' : 'marker'}
            //ハンドラで選択されたIDを属性に設定して更新
            onClick={() => selectTerm(item.id)}
          >
            {item.name}
          </MenuItem>
        ))
      )}
    </MenuGroup>
  );
}
 
export default withSelect((select, props) => {
  return {
    terms: select('core').getEntityRecords('taxonomy', 'category', {per_page: -1})
  }
})(CategorySelect);

edit 関数では CategorySelect コンポーネントの selectTerm プロパティに指定する関数を定義し(5〜7行目)、selectTerm プロパティに定義した同じ名前の関数 selectTerm を指定します(30行目)。

また、selectedTermId プロパティに選択されたカテゴリー ID(属性 termId の値)を設定します。

src/edit.js
export default function Edit( props ) {
  const { className, attributes, setAttributes } = props;
  
  // 属性 termId を更新する関数 selectTerm を実装
  const selectTerm = ( termId ) => {
    setAttributes({ termId: termId });
  }
  
  const getInspectorControls = () => {
    return (
      <InspectorControls>
        ・・・中略・・・
      </InspectorControls>
    );
  }
  
  return (
    [
      getInspectorControls(),  //インスペクター
      <div className={ className }>
        <Placeholder
          label='スライダーカテゴリー'
          icon='format-image'
          instructions='スライダーに表示するアイキャッチ画像のカテゴリーを選択'
        >
          <CategorySelect 
            // プロパティ selectedTermId に属性 termId を指定
            selectedTermId={ attributes.termId }
            // プロパティ selectedTerm に5行目で実装した関数 selectTerm を指定
            selectTerm={ selectTerm }
          />
        </Placeholder>
      </div>
    ]
  );
}
src/edit.js
import './editor.scss';
import { InspectorControls } from '@wordpress/block-editor';
import { Placeholder, PanelBody, PanelRow, RangeControl, ToggleControl } from '@wordpress/components';
import CategorySelect from './category-select';
 
export default function Edit( props ) {
  //props 経由でプロパティを取得して分割代入で変数を設定
  const {className, attributes, setAttributes} = props;
  
  // 属性 termId を更新する関数
  const selectTerm = (termId) => {
    setAttributes({ termId: termId });
  }
  
  //インスペクターを表示する関数
  const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody
          title='Slider Settings'
          initialOpen={true}
        >
          <PanelRow>   
            <RangeControl
              label='Number of slides'
              value={attributes.numSlides}
              onChange={(val) => setAttributes({ numSlides: val })}
              min={1}
              max={10}
            />
          </PanelRow>
          <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}>
        <Placeholder
          label='スライダーカテゴリー'
          icon='format-image'
          instructions='スライダーに表示するアイキャッチ画像のカテゴリーを選択'
        >
          <CategorySelect 
            selectedTermId={attributes.termId}
            selectTerm={selectTerm}
          />
        </Placeholder>
      </div>
    ]
  );
}

この時点でブロックの挿入された投稿を再読込して確認すると、エディター画面では以下のようにカテゴリーのリストが表示され、選択されたカテゴリーにはチェックマークが表示されます。

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

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

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

以下は 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.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 フォルダに保存したスライダーのスクリプトとスタイルをプラグインのファイル my-dynamic-slider.php で wp_enqueue_scriptwp_enqueue_style を使って、enqueue_block_assets アクションで読み込みます。

この例の場合、エディター側ではスライダーは表示しないのでフロントエンド側でのみ読み込むようにします。そのため以下では ! is_admin() で判定してスクリプトとスタイルをエンキューしています。

my-dynamic-slider.php 抜粋
function add_my_dynamic_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_dynamic_slider_scripts_and_styles');

Swiper のマークアップ

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

<!-- スライダーのメインのコンテナー -->
<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>

PHP でレンダリング

register_block_type 関数の第2パラメータに追加した render_callback に指定した関数を使ってスライダーをレンダリングします。

render_callback の関数はブロックの出力を return する必要があるので、文字列(HTML)を作成して最後に return します。この例の場合、投稿からアイキャッチ画像を取得して Swiper の構造のマークアップを作成して return します。

以下では、属性 termId の値によりカテゴリーが選択されているかを判定し、何も選択されていなければ空文字列を返し何も表示しません。

カテゴリーが選択されている場合は、属性 termId と numSlides の値をクエリパラメータに指定した WP_Query を使ったサブループで投稿を取得し、投稿のアイキャッチ画像が存在すればスライダーの画像のマークアップを作成します。

アイキャッチ画像は get_post_thumbnail_id() でアイキャッチ画像の ID を取得し、それを元にアイキャッチ画像のオブジェクトを取得して URL とキャプションを取得しています。alt 属性の値は get_post_meta() を使って取得しています。

キャプションやページネーション、ナビゲーションボタン、スクロールバーの表示は属性の値を元に出力しています。

function my_dynamic_slider_render($attributes, $content) {
  //カテゴリーが選択されていない場合は空文字列を返す(何も表示しない)
  if ($attributes['termId'] === 0) {
    return '';
  }

  // WP_Query を使ったサブループ
  $my_Query = new WP_Query([
    //属性の値を使って取得する投稿の条件を指定
    'posts_per_page' => $attributes['numSlides'],
    'cat' => $attributes['termId']
  ]);

  if ($my_Query->have_posts()) {
    //該当する投稿があればスライダーのマークアップを組み立てる
    $output = '<div class="swiper-container wp-block-wdl-my-dynamic-slider">';
    $output .= '<div class="swiper-wrapper">';
    while ($my_Query->have_posts()) {
      $my_Query->the_post();
      //アイキャッチ画像があれば
      if (has_post_thumbnail()) {
        //アイキャッチ画像の ID を取得
        $post_thumbnail_id = get_post_thumbnail_id();
        //アイキャッチ画像のオブジェクトを取得
        $my_thumbnail = get_post($post_thumbnail_id);
        $img_url = esc_url($my_thumbnail -> guid);
        $img_caption = esc_html($my_thumbnail -> post_excerpt);
        //get_post_meta() でカスタムフィールドの内部的な値である alt 属性を取得
        $my_thumbnail_meta = get_post_meta($post_thumbnail_id);
        $img_alt = $my_thumbnail_meta["_wp_attachment_image_alt"][0];
        $output .= '<div class="swiper-slide"><img src="' . $img_url . '" alt="' . $img_alt .'" />';   
        //キャプションを表示する場合
        if($attributes['showCaption']) {
          if($img_caption) {
            $output .= '<div class="caption">'. $img_caption . '</div>';
          } 
        }
        $output .= '</div>';
      } 
    }
    wp_reset_postdata(); //グローバル変数 $post を復元

    $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>';
    return $output;
  } else {
    return '';
  }
}

ブロックの外側の要素はスライダーの外側のクラス swiper-container と WordPress が自動的にブロックに付与するクラス wp-block-wdl-my-dynamic-slider を指定した div 要素でマークアップしています。

my-dynamic-slider.php
<?php
/**
 * Plugin Name:     My Dynamic 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-dynamic-slider
 *
 * @package         wdl
 */

/**
 * Registers all block assets so that they can be enqueued through the block editor
 * in the corresponding context.
 *
 * @see https://developer.wordpress.org/block-editor/tutorials/block-tutorial/applying-styles-with-stylesheets/
 */
function wdl_my_dynamic_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-dynamic-slider" block first.'
    );
  }
  $index_js     = 'build/index.js';
  $script_asset = require( $script_asset_path );
  wp_register_script(
    'wdl-my-dynamic-slider-block-editor',
    plugins_url( $index_js, __FILE__ ),
    $script_asset['dependencies'],
    $script_asset['version']
  );
  wp_set_script_translations( 'wdl-my-dynamic-slider-block-editor', 'my-dynamic-slider' );

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

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

  register_block_type( 'wdl/my-dynamic-slider', array(
    'editor_script' => 'wdl-my-dynamic-slider-block-editor',
    'editor_style'  => 'wdl-my-dynamic-slider-block-editor',
    'style'         => 'wdl-my-dynamic-slider-block',
    //render_callback を追加
    'render_callback' => 'my_dynamic_slider_render',
    //属性を追加
    'attributes' => [
      //アイキャッチ画像のカテゴリーのID
      'termId' => [
        'type' => 'number', 
        'default' => 0
      ],
      //スライダーに表示する画像の数
      'numSlides' => [
        'type' => 'number', 
        'default' => 3
      ],
      //ナビゲーションボタンの表示・非表示
      '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_dynamic_slider_block_init' );

function my_dynamic_slider_render($attributes, $content) {
  //カテゴリーが選択されていない場合は空文字列を返す(何も表示しない)
  if ($attributes['termId'] === 0) {
    return '';
  }

  // WP_Query を使ったサブループ
  $my_Query = new WP_Query([
    //属性の値を使って取得する投稿の条件を指定
    'posts_per_page' => $attributes['numSlides'],
    'cat' => $attributes['termId']
  ]);

  if ($my_Query->have_posts()) {
    //該当する投稿があればスライダーのマークアップを組み立てる
    $output = '<div class="swiper-container wp-block-wdl-my-dynamic-slider">';
    $output .= '<div class="swiper-wrapper">';
    while ($my_Query->have_posts()) {
      $my_Query->the_post();
      //アイキャッチ画像があれば
      if (has_post_thumbnail()) {
        //アイキャッチ画像の ID を取得
        $post_thumbnail_id = get_post_thumbnail_id();
        //アイキャッチ画像のオブジェクトを取得
        $my_thumbnail = get_post($post_thumbnail_id);
        $img_url = esc_url($my_thumbnail -> guid);
        $img_caption = esc_html($my_thumbnail -> post_excerpt);
        //get_post_meta() でカスタムフィールドの内部的な値である alt 属性を取得
        $my_thumbnail_meta = get_post_meta($post_thumbnail_id);
        $img_alt = $my_thumbnail_meta["_wp_attachment_image_alt"][0];
        $output .= '<div class="swiper-slide"><img src="' . $img_url . '" alt="' . $img_alt .'" />';   
        //キャプションを表示する場合
        if($attributes['showCaption']) {
          if($img_caption) {
            $output .= '<div class="caption">'. $img_caption . '</div>';
          } 
        }
        $output .= '</div>';
      } 
    }
    wp_reset_postdata(); //グローバル変数 $post を復元

    $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>';
    return $output;
  } else {
    return '';
  }
}


function add_my_dynamic_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_dynamic_slider_scripts_and_styles');

スライダーのスタイル

表示するスライダーのスタイルは src/style.scss で設定することができます。

以下ではスライダーのマークアップで指定したクラス swiper-container などを使ってキャプションとページネーションのスタイルを指定しています。

src/style.scss
.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;
}

以下はフロントエンド側の表示例です。

ここまでのコードのサンプル

以下はここまでのコードのファイル構成とファイルです。

npm run build でビルドを実行すると、以下のような構成になります。

本番環境で必要なのは、assets と build 内の全てのファイルと my-dynamic-slider.php になります。

my-dynamic-slider 
├── assets 
│   ├── init-swiper.js
│   ├── swiper.css  
│   └── swiper.js 
├── block.json 
├── build 
│   ├── index.asset.php 
│   ├── index.css  
│   ├── index.js  
│   └── style-index.css 
├── node_modules 
├── my-dynamic-slider.php  
├── package-lock.json
├── package.json 
├── readme.txt
└── src  
    ├── category-select.js
    ├── edit.js
    ├── editor.scss
    ├── index.js      
    └── style.scss
src/category-select.js
import { withSelect } from '@wordpress/data'; 
import { MenuGroup, MenuItem } from '@wordpress/components'; 
 
const CategorySelect = (props) => {
  const { terms, selectedTermId, selectTerm } = props;
  
  return(
    <MenuGroup
      className="my-category-select"
    >
      {terms && (
        terms.map((item) => (
          <MenuItem
            role="menuitemradio"
            isSelected={item.id == selectedTermId}
            icon={item.id == selectedTermId ? 'yes' : 'marker'}
            onClick={() => selectTerm(item.id)}
          >
            {item.name}
          </MenuItem>
        ))
      )}
    </MenuGroup>
  );
}
 
export default withSelect((select, props) => {
  return {
    terms: select('core').getEntityRecords('taxonomy', 'category', {per_page: -1})
  }
})(CategorySelect);
src/edit.js
import './editor.scss';
import { InspectorControls } from '@wordpress/block-editor';
import { Placeholder, PanelBody, PanelRow, RangeControl, ToggleControl } from '@wordpress/components';
import CategorySelect from './category-select';
 
export default function Edit( props ) {
  const {className, attributes, setAttributes} = props;
  
  const selectTerm = (termId) => {
    setAttributes({ termId: termId });
  }
  
  const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody
          title='Slider Settings'
          initialOpen={true}
        >
          <PanelRow>   
            <RangeControl
              label='Number of slides'
              value={attributes.numSlides}
              onChange={(val) => setAttributes({ numSlides: val })}
              min={1}
              max={10}
            />
          </PanelRow>
          <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}>
        <Placeholder
          label='スライダーカテゴリー'
          icon='format-image'
          instructions='スライダーに表示するアイキャッチ画像のカテゴリーを選択'
        >
          <CategorySelect 
            selectedTermId={attributes.termId}
            selectTerm={selectTerm}
          />
        </Placeholder>
      </div>
    ]
  );
}
src/index.js
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';

registerBlockType( 'wdl/my-dynamic-slider', {
  title: 'My Dynamic Slider',
  description:'Example block written with ESNext standard and JSX support',
  category: 'widgets',
  icon: 'smiley',
  supports: {
    html: false,
  },
  edit: Edit,
  save: () => { return null }
} );
src/editor.scss
 .my-category-select {
  max-height: 200px;
  overflow: hidden scroll;
  border: 1px solid #C0BCBC;
  background: #fcfcfc;
}
src/style.scss
.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;
}
my-dynamic-slider.php
<?php
/**
 * Plugin Name:     My Dynamic 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-dynamic-slider
 *
 * @package         wdl
 */

/**
 * Registers all block assets so that they can be enqueued through the block editor
 * in the corresponding context.
 *
 * @see https://developer.wordpress.org/block-editor/tutorials/block-tutorial/applying-styles-with-stylesheets/
 */
function wdl_my_dynamic_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-dynamic-slider" block first.'
    );
  }
  $index_js     = 'build/index.js';
  $script_asset = require( $script_asset_path );
  wp_register_script(
    'wdl-my-dynamic-slider-block-editor',
    plugins_url( $index_js, __FILE__ ),
    $script_asset['dependencies'],
    $script_asset['version']
  );
  wp_set_script_translations( 'wdl-my-dynamic-slider-block-editor', 'my-dynamic-slider' );

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

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

  register_block_type( 'wdl/my-dynamic-slider', array(
    'editor_script' => 'wdl-my-dynamic-slider-block-editor',
    'editor_style'  => 'wdl-my-dynamic-slider-block-editor',
    'style'         => 'wdl-my-dynamic-slider-block',
    //render_callback を追加
    'render_callback' => 'my_dynamic_slider_render',
    //属性を追加
    'attributes' => [
      //アイキャッチ画像のカテゴリーのID
      'termId' => [
        'type' => 'number', 
        'default' => 0
      ],
      //スライダーに表示する画像の数
      'numSlides' => [
        'type' => 'number', 
        'default' => 3
      ],
      //ナビゲーションボタンの表示・非表示
      '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_dynamic_slider_block_init' );

function my_dynamic_slider_render($attributes, $content) {
  //カテゴリーが選択されていない場合は空文字列を返す(何も表示しない)
  if ($attributes['termId'] === 0) {
    return '';
  }

  // WP_Query を使ったサブループ
  $my_Query = new WP_Query([
    //属性の値を使って取得する投稿の条件を指定
    'posts_per_page' => $attributes['numSlides'],
    'cat' => $attributes['termId']
  ]);

  if ($my_Query->have_posts()) {
    //該当する投稿があればスライダーのマークアップを組み立てる
    $output = '<div class="swiper-container wp-block-wdl-my-dynamic-slider">';
    $output .= '<div class="swiper-wrapper">';
    while ($my_Query->have_posts()) {
      $my_Query->the_post();
      //アイキャッチ画像があれば
      if (has_post_thumbnail()) {
        //アイキャッチ画像の ID を取得
        $post_thumbnail_id = get_post_thumbnail_id();
        //アイキャッチ画像のオブジェクトを取得
        $my_thumbnail = get_post($post_thumbnail_id);
        $img_url = esc_url($my_thumbnail -> guid);
        $img_caption = esc_html($my_thumbnail -> post_excerpt);
        //get_post_meta() でカスタムフィールドの内部的な値である alt 属性を取得
        $my_thumbnail_meta = get_post_meta($post_thumbnail_id);
        $img_alt = $my_thumbnail_meta["_wp_attachment_image_alt"][0];
        $output .= '<div class="swiper-slide"><img src="' . $img_url . '" alt="' . $img_alt .'" />';   
        //キャプションを表示する場合
        if($attributes['showCaption']) {
          if($img_caption) {
            $output .= '<div class="caption">'. $img_caption . '</div>';
          } 
        }
        $output .= '</div>';
      } 
    }
    wp_reset_postdata(); //グローバル変数 $post を復元

    $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>';
    return $output;
  } else {
    return '';
  }
}


function add_my_dynamic_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_dynamic_slider_scripts_and_styles');
init-swiper.js
let mySwiper = 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', //要素の指定
  },
})

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)

プレビューボタンの追加

(2020年10月28日追加)

以下はプレビューボタンを追加してエディター画面でスライダーを表示する例です。

スライダーに表示する画像は withSelect を使って選択されたカテゴリーの投稿を取得してそのアイキャッチ画像の ID を取得し、更に withSelect を使って選択されたカテゴリーの画像を取得して、アイキャッチ画像のみを表示するようにしています(もっと効率の良い方法があるかも知れません)。

スライダーのスクリプトとスタイルをエディター画面でも読み込むように my-dynamic-slider.php を変更します。但し、Swiper の初期化は新たに追加するコンポーネントに記述するので、init-swiper.js はエディター画面では読み込まないようにしています。

my-dynamic-slider.php
function add_my_dynamic_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_dynamic_slider_scripts_and_styles');

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

register_block_type( 'wdl/my-dynamic-slider', array(
    'editor_script' => 'wdl-my-dynamic-slider-block-editor',
    'editor_style'  => 'wdl-my-dynamic-slider-block-editor',
    'style'         => 'wdl-my-dynamic-slider-block',
    'render_callback' => 'my_dynamic_slider_render',
    'attributes' => [
      'termId' => [
        'type' => 'number', 
        'default' => 0
      ],
      
      ・・・中略・・・
      
      //isEditMode を属性として追加
      'isEditMode' => [
        'type' => 'boolean', 
        'default' => true
      ],
    ]
  ));

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

edit.js
//ツールバーにモードを切り替えるボタンを表示する関数を追加
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>
  );
}

isEditMode が false の場合は、スライダーのマークアップを記述して表示するようにします。また、その際に「編集モード」というプレビューモードから編集モードへの切り替えるためのリンクを表示します。

FeaturedMediaImages(28〜31行目)は withSelect を使ってアイキャッチ画像を取得してレンダリングするコンポーネントです。

edit.js
return (
  [
    getBlockControls(), //プレビューボタン
    getInspectorControls(),  //インスペクター
    <div className={className}>
      { attributes.isEditMode &&  // isEditMode が true の場合
      <Fragment>   
        <Placeholder
          label='スライダーカテゴリー'
          icon='format-image'
          instructions='スライダーに表示するアイキャッチ画像のカテゴリーを選択'
        >
          <CategorySelect 
            selectedTermId={attributes.termId}
            selectTerm={selectTerm}
          />
        </Placeholder>
      </Fragment>
      }
      { !attributes.isEditMode &&    // isEditMode が false の場合(プレビュー モード)
        <div className="slider-container">
          <Button 
            onClick={() => setAttributes({ isEditMode: true })}
            isLink >編集モード
          </Button>
          <div className="swiper-container">
            <div className="swiper-wrapper"> 
              <FeaturedMediaImages //画像をレンダリングするコンポーネント
                catid = {attributes.termId}
                showCaption= {attributes.showCaption}
              />
            </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>
      }
    </div>
  ]
);

以下がスライダーの画像をレンダリングするコンポーネント FeaturedMediaImages です。

withSelect と getEntityRecords を使って選択されたカテゴリーの投稿(posts)と画像(images)を取得し、FeaturedMediaImages に渡します。

getEntityRecords のクエリパラメータに指定する選択されたカテゴリー(props.catid)は、このコンポーネントが edit.js で呼び出されるときに props 経由で渡します。

FeaturedMediaImages では props 経由で取得した投稿の配列(posts)からアイキャッチ画像の ID の配列を取得し変数 featured_media_ids に格納します。

続いて props 経由で取得した画像の配列(images)を filter() でループして includes() を使い各画像がアイキャッチ画像かどうかを判定して、アイキャッチ画像のみを featured_media_obj に格納します。

そして return ステートメントで images が true の場合(withSelect による画像や投稿の取得は非同期で行われるため)、featured_media_obj をループして img 要素とキャプションの div 要素を生成してレンダリングします。

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

featured-media-images.js
import { withSelect } from '@wordpress/data'; 
import { useEffect, Fragment } from '@wordpress/element';

const FeaturedMediaImages = (props) => {

  const { posts, images, showCaption } = props;
  
  // アイキャッチ画像の ID を格納する配列の初期化
  let featured_media_ids = [];
  // アイキャッチ画像の投稿を格納する配列の初期化
  let featured_media_obj = [];
  
  if(posts) {
    //アイキャッチ画像の ID の配列を取得
    featured_media_ids = posts.map(post => post.featured_media);
  }
  
  if(images) {    
    featured_media_obj = images.filter(image => {
      if(featured_media_ids.includes(image.id)) {
         return  image;
      }
    }); 
  }

  //Swiper のスライダーを初期化する関数
  const initSwiper = () => {
    let editorSwiper = new Swiper ('.swiper-container', {

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

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

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

      //スクロールバー表示の設定
      scrollbar: { 
        el: '.swiper-scrollbar', //要素の指定
      },
    })
  }
  
  //useEffect フックを使ってスライダーを初期化する initSwiper を実行
  useEffect(() => {
    initSwiper();
  });
 
  return(
    <Fragment>
      {images && 
        featured_media_obj.map(image => (
         <div className="swiper-slide">
          <img 
             src={image.source_url}
             alt={image.alt_text}
          />
          { showCaption && // props 経由で attributes.showCaption を取得
            <div class="caption">{ image.caption.raw }</div>
          }
         </div>
        ))    
      }
    </Fragment>
  );
}
 
export default withSelect((select, props) => {
  return {
    // catid は edit.js で props 経由で取得した attributes.termId 
    // posts は取得した選択されたカテゴリーの投稿
    posts: select('core').getEntityRecords('postType', 'post', { categories: props.catid }),
    // images は取得した選択されたカテゴリーの添付ファイル(画像)
    images: select('core').getEntityRecords('postType', 'attachment', { categories: props.catid })
  }
})(FeaturedMediaImages);

img 要素の src 属性は画像オブジェクトの source_url プロパティ、alt 属性は alt_text プロパティで取得できます。また、キャプションは画像オブジェクトの caption.raw で取得できます。

どのようなプロパティがあるかは例えば、console.log(images); で確認することができます。

また、withSelect での getEntityRecords による画像の取得は非同期で行われるため console.log(images) の出力では最初に何度か null が表示されているのが確認できます。

useEffect

スライダーを初期化する関数 initSwiper はスライダーのマークアップを返す edit.js で useEffect を使って実行しても良さそうですが、withSelect での getEntityRecords を使った画像の取得は非同期で行われるため FeaturedMediaImages コンポーネントがレンダリングされたら実行するようにする必要があります。

もし、edit.js で useEffect を使っスライダーを初期化する関数 initSwiper を実行すると、最初に edit.js をレンダリングした時点ではまだ画像が取得できていないため期待通りの動作になりません。

例えば、edit.js に以下のように initSwiper と useEffect を記述すると、まだ画像が取得されていない時点で useEffect が実行されているのがわかります。

//Swiper のスライダーを初期化する関数
const initSwiper = () => {
  let editorSwiper = new Swiper ('.swiper-container', {

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

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

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

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

//useEffect フックを使ってスライダーを初期化する initSwiper を実行
useEffect(() => {
  initSwiper();
  //useEffect フックが実行されたらコンソールに useEffect と出力
  console.log('useEffect');
}, [attributes.isEditMode]);

以下はプレビューボタンを追加したファイル構成とサンプルファイルです。

my-dynamic-slider 
├── assets 
│   ├── init-swiper.js
│   ├── swiper.css  
│   └── swiper.js 
├── block.json 
├── build 
│   ├── index.asset.php 
│   ├── index.css  
│   ├── index.js  
│   └── style-index.css 
├── node_modules 
├── my-dynamic-slider.php  
├── package-lock.json
├── package.json 
├── readme.txt
└── src  
    ├── category-select.js
    ├── edit.js
    ├── editor.scss
    ├── featured-media-images.js  //追加
    ├── index.js
    └── style.scss
my-dynamic-slider.php
<?php
/**
 * Plugin Name:     My Dynamic 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-dynamic-slider
 *
 * @package         wdl
 */
 
/**
 * Registers all block assets so that they can be enqueued through the block editor
 * in the corresponding context.
 *
 * @see https://developer.wordpress.org/block-editor/tutorials/block-tutorial/applying-styles-with-stylesheets/
 */
function wdl_my_dynamic_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-dynamic-slider" block first.'
    );
  }
  $index_js     = 'build/index.js';
  $script_asset = require( $script_asset_path );
  wp_register_script(
    'wdl-my-dynamic-slider-block-editor',
    plugins_url( $index_js, __FILE__ ),
    $script_asset['dependencies'],
    $script_asset['version']
  );
  wp_set_script_translations( 'wdl-my-dynamic-slider-block-editor', 'my-dynamic-slider' );
 
  $editor_css = 'build/index.css';
  wp_register_style(
    'wdl-my-dynamic-slider-block-editor',
    plugins_url( $editor_css, __FILE__ ),
    array(),
    filemtime( "$dir/$editor_css" )
  );
 
  $style_css = 'build/style-index.css';
  wp_register_style(
    'wdl-my-dynamic-slider-block',
    plugins_url( $style_css, __FILE__ ),
    array(),
    filemtime( "$dir/$style_css" )
  );
 
  register_block_type( 'wdl/my-dynamic-slider', array(
    'editor_script' => 'wdl-my-dynamic-slider-block-editor',
    'editor_style'  => 'wdl-my-dynamic-slider-block-editor',
    'style'         => 'wdl-my-dynamic-slider-block',
    //render_callback を追加
    'render_callback' => 'my_dynamic_slider_render',
    //属性を追加
    'attributes' => [
      //アイキャッチ画像のカテゴリーのID
      'termId' => [
        'type' => 'number', 
        'default' => 0
      ],
      //スライダーに表示する画像の数
      'numSlides' => [
        'type' => 'number', 
        'default' => 3
      ],
      //ナビゲーションボタンの表示・非表示
      'showNavigationButton' => [
        'type' => 'boolean', 
        'default' => true
      ],
      //ページネーションの表示・非表示
      'showPagination' => [
        'type' => 'boolean', 
        'default' => true
      ],
      //スクロールバーンの表示・非表示
      'showScrollbar' => [
        'type' => 'boolean', 
        'default' => true
      ],
      //キャプションの表示・非表示
      'showCaption' => [
        'type' => 'boolean', 
        'default' => true
      ],
      //isEditMode を属性として追加
      'isEditMode' => [
        'type' => 'boolean', 
        'default' => true
      ],
    ]
  ));
}
add_action( 'init', 'wdl_my_dynamic_slider_block_init' );
 
function my_dynamic_slider_render($attributes, $content) {
  //カテゴリーが選択されていない場合は空文字列を返す(何も表示しない)
  if ($attributes['termId'] === 0) {
    return '';
  }
 
  // WP_Query を使ったサブループ
  $my_Query = new WP_Query([
    //属性の値を使って取得する投稿の条件を指定
    'posts_per_page' => $attributes['numSlides'],
    'cat' => $attributes['termId']
  ]);
 
  if ($my_Query->have_posts()) {
    //該当する投稿があればスライダーのマークアップを組み立てる
    $output = '<div class="swiper-container wp-block-wdl-my-dynamic-slider">';
    $output .= '<div class="swiper-wrapper">';
    while ($my_Query->have_posts()) {
      $my_Query->the_post();
      //アイキャッチ画像があれば
      if (has_post_thumbnail()) {
        //アイキャッチ画像の ID を取得
        $post_thumbnail_id = get_post_thumbnail_id();
        //アイキャッチ画像のオブジェクトを取得
        $my_thumbnail = get_post($post_thumbnail_id);
        $img_url = esc_url($my_thumbnail -> guid);
        $img_caption = esc_html($my_thumbnail -> post_excerpt);
        //get_post_meta() でカスタムフィールドの内部的な値である alt 属性を取得
        $my_thumbnail_meta = get_post_meta($post_thumbnail_id);
        $img_alt = $my_thumbnail_meta["_wp_attachment_image_alt"][0];
        $output .= '<div class="swiper-slide"><img src="' . $img_url . '" alt="' . $img_alt .'" />';   
        //キャプションを表示する場合
        if($attributes['showCaption']) {
          if($img_caption) {
            $output .= '<div class="caption">'. $img_caption . '</div>';
          } 
        }
        $output .= '</div>';
      } 
    }
    wp_reset_postdata(); //グローバル変数 $post を復元
 
    $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>';
    return $output;
  } else {
    return '';
  }
}
 
 
function add_my_dynamic_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_dynamic_slider_scripts_and_styles');
edit.js
import './editor.scss';
import { BlockControls, InspectorControls } from '@wordpress/block-editor';
import { Toolbar, Button, Placeholder, PanelBody, PanelRow, RangeControl, ToggleControl } from '@wordpress/components';
import CategorySelect from './category-select';
import FeaturedMediaImages from './featured-media-images';


import { Fragment } from '@wordpress/element'; 
 
export default function Edit( props ) {
  const {className, attributes, setAttributes} = props;
  
  const selectTerm = (termId) => {
    setAttributes({ termId: termId });
  }
  
  const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody
          title='Slider Settings'
          initialOpen={true}
        >
          <PanelRow>   
            <RangeControl
              label='Number of slides'
              value={attributes.numSlides}
              onChange={(val) => setAttributes({ numSlides: val })}
              min={1}
              max={10}
            />
          </PanelRow>
          <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>
    );
  }
  
  return (
    [
      getBlockControls(), //プレビューボタン
      getInspectorControls(),  //インスペクター
      <div className={className}>
        { attributes.isEditMode &&  // isEditMode が true の場合
        <Fragment>   
          <Placeholder
            label='スライダーカテゴリー'
            icon='format-image'
            instructions='スライダーに表示するアイキャッチ画像のカテゴリーを選択'
          >
            <CategorySelect 
              selectedTermId={attributes.termId}
              selectTerm={selectTerm}
            />
          </Placeholder>
        </Fragment>
        }
        { !attributes.isEditMode && //isEditMode が false の場合(プレビュー モード)
          <div className="slider-container">
            <Button 
              onClick={() => setAttributes({ isEditMode: true })}
              isLink >編集モード
            </Button>
            <div className="swiper-container">
              <div className="swiper-wrapper"> 
                <FeaturedMediaImages
                  catid = {attributes.termId}
                  showCaption= {attributes.showCaption}
                />
              </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>
        }
      </div>
    ]
  );
}
featured-media-images.js
import { withSelect } from '@wordpress/data'; 
import { useEffect, Fragment } from '@wordpress/element';

const FeaturedMediaImages = (props) => {

  const { posts, images, showCaption } = props;
  
  // アイキャッチ画像の ID を格納する配列の初期化
  let featured_media_ids = [];
  // アイキャッチ画像の投稿を格納する配列の初期化
  let featured_media_obj = [];
  
  if(posts) {
    //アイキャッチ画像の ID の配列を取得
    featured_media_ids = posts.map(post => post.featured_media);
  }
  
  if(images) {    
    featured_media_obj = images.filter(image => {
      if(featured_media_ids.includes(image.id)) {
         return  image;
      }
    }); 
  }

  //console.log(images);
  
  //Swiper のスライダーを初期化する関数
  const initSwiper = () => {
    let editorSwiper = new Swiper ('.swiper-container', {

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

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

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

      //スクロールバー表示の設定
      scrollbar: { 
        el: '.swiper-scrollbar', //要素の指定
      },
    })
  }
  
  //useEffect フックを使ってスライダーを初期化する initSwiper を実行
  useEffect(() => {
    initSwiper();
    //console.log('useEffect');
  });
 
  return(
    <Fragment>
      {images && 
        featured_media_obj.map(image => (
         <div className="swiper-slide">
          <img 
             src={image.source_url}
             alt={image.alt_text}
          />
          { showCaption && // showCaption は props 経由で取得
            <div class="caption">{ image.caption.raw }</div>
          }
         </div>
        ))    
      }
    </Fragment>
  );
}
 
export default withSelect((select, props) => {
  return {
    // catid は edit.js で props 経由で取得した attributes.termId 
    // posts は取得した選択されたカテゴリーの投稿
    posts: select('core').getEntityRecords('postType', 'post', { categories: props.catid }),
    // images は取得した選択されたカテゴリーの添付ファイル(画像)
    images: select('core').getEntityRecords('postType', 'attachment', { categories: props.catid })
  }
})(FeaturedMediaImages);
index.js
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
 
registerBlockType( 'wdl/my-dynamic-slider', {
  title: 'My Dynamic Slider',
  description:'Example block written with ESNext standard and JSX support',
  category: 'widgets',
  icon: 'smiley',
  supports: {
    html: false,
  },
  edit: Edit,
  save: () => { return null }
} );
src/category-select.js
import { withSelect } from '@wordpress/data'; 
import { MenuGroup, MenuItem } from '@wordpress/components'; 
 
const CategorySelect = (props) => {
  // props 経由で terms(カテゴリーオブジェクト)を取得
  // selectedTermId は選択されているカテゴリーID の値
  // selectTerm は選択されているカテゴリーIDを更新する関数
  const { terms, selectedTermId, selectTerm } = props;
  
  //terms(カテゴリーオブジェクトの配列)をループして各 name プロパティを MenuItem に出力
  return(
    <MenuGroup
      className="my-category-select"
    >
      {terms && (
        terms.map((item) => (
          <MenuItem
            role="menuitemradio"
            //selectedTermId と id を比較して真偽値を設定
            isSelected={item.id == selectedTermId}
            //selectedTermId と id の比較結果により表示するアイコンを設定
            icon={item.id == selectedTermId ? 'yes' : 'marker'}
            //ハンドラで選択されたIDを属性に設定して更新
            onClick={() => selectTerm(item.id)}
          >
            {item.name}
          </MenuItem>
        ))
      )}
    </MenuGroup>
  );
}
 
export default withSelect((select, props) => {
  return {
    terms: select('core').getEntityRecords('taxonomy', 'category', {per_page: -1})
  }
})(CategorySelect);
editor.scss
.my-category-select {
  max-height: 200px;
  overflow: hidden scroll;
  border: 1px solid #C0BCBC;
  background: #fcfcfc;
}

.wp-block-wdl-my-dynamic-slider figcaption.block-image-caption {
  text-align: center;
  margin-top: 0;
}

.swiper-container .caption {
  background-color: rgba(0,0,0,0.7);
  color: #fefefe;
  text-align: center;
  position: absolute;
  bottom: 10px;
  width: 100%;
}
style.scss
.swiper-container.wp-block-wdl-my-dynamic-slider .caption {
  background-color: rgba(0,0,0,0.4);
  color: #fefefe;
  text-align: center;
  position: absolute;
  bottom: 0px;
  width: 100%;
}

.swiper-container .swiper-pagination.swiper-pagination-bullets {
  bottom: 30px;
}

/* ナビゲーションとページネーションの色を設定 */
:root {
  --swiper-navigation-color: #B8DCF5;
  --swiper-pagination-color: #B8DCF5;
}