wordpress Gutenberg ブロック作成 attributes の source と selector プロパティ

2020年9月2日

関連ページ:WordPress Gutenberg ブロックの作成

Gutenberg で編集可能なブロックを作成する際に、 例えば、複数の RichText コンポーネントを使う場合、attributes(属性)の source プロパティや selector プロパティ、type プロパティを適切に設定する必要があります。

適切に設定されていないと、一見問題ないように見えても、ページを再読み込みするとブロックが壊れて、コンソールを確認すると Block validation: Block validation failed のエラーが表示される場合があります。

以下は複数の RichText コンポーネントを使ったブロックを作成する際の、attributes(属性)やその source プロパティなどについての個人的な覚書です。ブロックの作成は、JSX は使わずに ES5 での記述になります。

取り敢えず、1つの RichText コンポーネントを使ったブロックをプラグインとして作成してみます。

wp-content
└── plugins
    └── my-blocks //プラグインのディレクトリ
        ├── my-first-block-rt.php  //プラグインファイル
        └── block_rt.js  //ブロック用のスクリプト

以下がブロックのスクリプトを読み込むプラグインファイルの例です。

my-first-block-rt.php

<?php
/*
Plugin Name: My First Block RichText
*/
defined( 'ABSPATH' ) || exit; 

function my_first_block_rt_enqueue() {
  //ブロック用のスクリプトを登録
  wp_register_script(
    'my-first-block-rt-script',
    plugins_url( 'block_rt.js', __FILE__ ),
    //依存スクリプト
    array( 'wp-blocks', 'wp-element', 'wp-block-editor') 
  );
  //ブロックタイプを登録
  register_block_type( 
    'my-first-block/sample-richtext',
    array(
      //エディター用スクリプトをブロックに関連付け
      'editor_script' => 'my-first-block-rt-script',   
    ) 
  );
}
add_action( 'init', 'my_first_block_rt_enqueue' );   

以下がブロックを登録するスクリプトです。文字を入力して編集、保存するだけの最低限の設定です。

入力された値を保存(保持)するために registerBlockType() の第2パラメータの attributes プロパティに myRichText という属性を定義し、必須の type と初期値 default を設定してています(13〜19行目)。

入力された値が変更されると、onChange ハンドラの props.setAttributes( { myRichText: newText } ); で myRichText の値が更新されます。

block_rt.js

( function( blocks, element, blockEditor ) {
  var el = element.createElement;
  //RickTest コンポーネントは wp.blockEditor パッケージにあります
  var RichText = blockEditor.RichText ;
  
  blocks.registerBlockType( 
    'my-first-block/sample-richtext', 
    {
      title: 'My First Block RichText',  //インサータに表示されるタイトル
      icon: 'smiley',  //インサータに表示されるアイコン
      category: 'layout',  //ブロックのカテゴリー
      example: {},  //プレビュー表示
      attributes: {
        //入力された値を保存する属性を設定
        myRichText: {
          type: 'string',
          default: '' //デフォルトの値(初期値)
        }
      },
      // edit 関数
      edit: function( props ) {
        //onChange ハンドラ
        function onChangeContent( newText ) {
          //myRichText の値を更新
          props.setAttributes( { myRichText: newText } );
        }
        return el(
          RichText,
          {
            //イベントハンドラを設定
            onChange: onChangeContent,
            //value プロパティ(値)
            value: props.attributes.myRichText
          }
        );
      },
      // save 関数
      save: function( props ) {
        return el( 
          //RichText のコンテンツを正しく保存するには RichText.Content を使用
          RichText.Content, 
          {
            //value プロパティに値を設定
            value: props.attributes.myRichText,
          } 
        );
      },
    }
  );
}(
  window.wp.blocks,
  window.wp.element,
  window.wp.blockEditor
) );

上記2つのファイルを保存し、作成したプラグイン(My First Block RichText)を有効化して、投稿の編集画面を開くと作成したブロックを挿入できるようになります。以下は作成したブロック(My First Block RichTex)を挿入して「サンプルテキストの文字です」と入力して保存したスクリーンショットです。

コードエディターで確認すると、ブロックのコメントブロックには、JSON の属性(attributes)の値が含まれています。属性の source プロパティを指定しない場合、属性はブロックのコメントデリミッターに保存されます。

RichText コンポーネントのプロパティを追加して、ブロックを div 要素として表示して自動生成されるクラス名を付与し、、Enter キーを押すと改行ではなく段落(p 要素)を作成するように変更してみます。

save 関数ではクラス名は自動的に出力されるので、tagName プロパティだけを追加して div 要素として表示するようにします。

block_rt.js(抜粋)

edit: function( props ) {
  function onChangeContent( newText ) {
    props.setAttributes( { myRichText: newText } );
  }
  return el(
    RichText,
    {
      onChange: onChangeContent,
      value: props.attributes.myRichText,
      // 自動生成されるクラスを追加
      className: props.className,
      // div 要素として表示
      tagName: 'div',
      //Enter キーで新しい段落(p 要素)を作成
      multiline: 'p'
    }
  );
},
save: function( props ) {
  return el( 
    RichText.Content, 
    {
      value: props.attributes.myRichText,
      // div 要素として表示
      tagName: 'div',
    } 
  );
},

上記のように変更して、編集画面で再読込すると既にこのブロックを追加した投稿に壊れたブロックが表示されます。これは、変更によってエディターが現在定義しているものとは異なる save 関数の出力を検出するために発生するもので問題ありません。

表示されるボタンをクリックして「ブロックを削除」を選択し、現在の壊れたブロックを一度削除してから再度ブロックを挿入すれば大丈夫です。

更新したブロックを再度挿入して「テキスト1」と入力後、エンターキーを押して「テキスト2」と入力すると2つの p 要素が作成され、コードエディターで確認すると以下のように表示されます。

投稿を保存して、フロント側の出力を確認すると以下のようになっています。

<div class="wp-block-my-first-block-sample-richtext">
  <p>テキスト1</p><p>テキスト2</p>
</div>

編集画面でページを再読み込みしても、特に問題は発生しません。

attributes(属性)

attributes(属性)はブロックが保持することができるデータで、registerBlockType() の第2パラメータに指定するプロパティの1つ(attributes プロパティ)として追加されます。

また、attributes は DOM から個々の属性のデータを抽出する方法をコンポーネントに知らせる(データの取得方法を定義するための)オブジェクトでもあり、以下のようなプロパティがあります。

  • selector: DOM セレクタ(タグ名やクラス名など)
  • source: 投稿コンテンツ(selector で指定した DOM)からどのようにブロックの属性値を取り出すか。以下を指定。
    • children(child node:子ノード)
    • html(内部の HTML)
    • text(内部のテキスト)
    • attribute(属性の値)
    • query(値の配列)
    • meta(投稿のメタ情報)

attributes に以下のように source と selector プロパティを追加してみます。

source を設定すると、ブロックのデータをコメントブロックからではなく、属性から取得することを意味します。以下の場合、selector で指定した div 要素の html(内部の HTML) から type で指定した string 型として取得して、myRichText という属性に保存するというような意味になるようです。

myRichText: {
  type: 'string',
  default: '',
  source: 'html',
  selector: 'div'
}

上記の source と selector プロパティを追加して、コードエディターで確認すると、JSON で記述されていた属性がなくなっているのが確認できます。

複数の RichText コンポーネントを使う

複数のコンポーネントを組み合わせてブロックを作成することもできます。その場合、それぞれのコンポーネントどとに独自の属性 attributes を設定します。

以下は見出しと段落を組み合わせた2つの RichText コンポーネントを使ったブロックの例です。

attributes には見出し用と段落用の属性をそれぞれ設定しています。

edit 関数と save 関数では、見出しと段落を div 要素でラップしています。element.createElement(el)の第3パラメータの children(子要素)に再度 element.createElement を使ってネストています。

[注意]以下のコードは属性に source と selector を設定していません。一応問題なく動作しますが、何らかの問題が発生する可能性があるかも知れません。

( function( blocks, element, blockEditor ) {
  var el = element.createElement;
  var RichText = blockEditor.RichText ;
  
  blocks.registerBlockType( 
    'my-first-block/sample-richtext', 
    {
      title: 'My First Block RichText',
      icon: 'smiley',
      category: 'layout',
      example: {},
      attributes: {
        //見出し用の属性
        myRichHeading: {
          type: 'string',
          default: ''
        },
        //段落用の属性
        myRichText: {
          type: 'string',
          default: ''
        }
      },
      
      edit: function( props ) {
        return el(
          //外側を div 要素で囲む
          'div', 
          {}, 
          [
            // 子要素に見出しと段落を配列で設定
            el(
              //見出し
              RichText, 
              {
                tagName: 'h3',
                placeholder: 'タイトル(h3)を入力',
                value: props.attributes.myRichHeading,
                onChange: function( newHeading ) {
                  props.setAttributes( { myRichHeading: newHeading } );
                },
              }
            ),
            el(
              //段落
              RichText, 
              {
                tagName: 'div',
                placeholder: '文章(段落)を入力',
                value: props.attributes.myRichText,
                multiline: 'p',
                onChange: function( newText ) {
                  props.setAttributes( { myRichText: newText } );
                }
              }
            )
          ]
        );
      },
      
      save: function( props ) {
        return el(
          'div', 
          {}, 
          [
            el(
              RichText.Content, 
              {
                tagName: 'h3',
                value: props.attributes.myRichHeading
              }
            ),
            el(
              RichText.Content, 
              {
                tagName: 'div',
                value: props.attributes.myRichText
              }
            )
          ]
        );
      },
    }
  );
}(
  window.wp.blocks,
  window.wp.element,
  window.wp.blockEditor
) );

上記ブロックを作成すると、編集画面で以下のような h3 要素と p 要素を入力できるブロックを挿入することができます。RichText コンポーネントを使っているので、文字をイタリックや太字にしたり、リンクやインライン画像を挿入できます。

上記の場合、source プロパティを設定していないので、属性の情報はコメントブロックに JSON 形式で保持されます。以下はコードエディターでの表示です。

うまく行かない source や selector の指定の例

例えば、属性(attributes)に以下のような source と selector を指定すると、うまく動作しません。

属性(attributes)部分抜粋

attributes: {
  //見出し用の属性
  myRichHeading: {
    type: 'string',
    default: '',
    //source と selector プロパティを追加
    source: 'children',
    // h3 タグを指定
    selector:'h3'
  },
  //段落用の属性
  myRichText: {
    type: 'array', //array に変更
    default: '',
    //source と selector プロパティを追加
    source: 'children',
    // p タグを指定
    selector:'p'
  }
},

属性を上記のように変更後、元あるブロックを削除して、新たにブロックを挿入してタイトルとテキストを入力してみると、エディタ画面では問題なく表示されます。

source を設定したので、ブロックのデータをコメントブロックからではなく属性から取得するため、コードエディタで確認すると属性値の JSON の記述はなくなっています。

入力した文字も保存でき、プレビューも見れるので、一見問題がないように見えますが、エディター画面でページを再読み込みすると以下のように「このブロックには、想定されていないか無効なコンテンツが含まれています」と表示されてしまいます。

コンソールを確認すると、以下のような「save 関数の出力と投稿コンテンツから取得した内容が異なる」というようなエラーが表示されています。

Block validation: Block validation failed for `my-first-block/sample-richtext` (Object).

Content generated by `save` function:

<div class="wp-block-my-first-block-sample-richtext"><h3></h3><div>テキスト</div></div>

Content retrieved from post body:

<div class="wp-block-my-first-block-sample-richtext"><h3>タイトル</h3><div><p>テキスト</p></div></div>

save 関数の出力と投稿コンテンツから取得した内容を確認すると、この例の場合は save 関数の出力にはタイトルの文字列が取得されていません。

これは source プロパティと selector プロパティの設定に問題があるようです。

source プロパティと selector プロパティ、及び type プロパティを使って属性のデータをどのように抽出するかを指定する必要があるのですが、要素がネストしたりしていると、例えば単に div 要素と指定しても、うまく抽出できない場合があるようです。

source や selector を適切に設定

source プロパティ、 selector プロパティ、及び type プロパティを使って投稿コンテンツからどのようにブロックの属性値を取り出すかを適切に指定する必要があります。作成するブロックにより、設定方法はいろいろとあると思います。

以下では save 及び edit 関数で h3 要素と div 要素にクラスを追加で設定し、attributes のそれぞれの属性の selsector プロパティでそれらのクラス名を使ってセレクタを指定しています。

また、この例の場合、h3 のタイトル用の属性では source に html を指定し、段落用の属性では source 属性に children(子ノード)を指定しています。

以下の場合、ページを再読み込みしても問題なく動作します。

( function( blocks, element, blockEditor ) {
  var el = element.createElement;
  //RickTest コンポーネントは wp.blockEditor パッケージにあります
  var RichText = blockEditor.RichText ;
  
  blocks.registerBlockType( 
    'my-first-block/sample-richtext', 
    {
      title: 'My First Block RichText',
      icon: 'smiley',
      category: 'layout',
      example: {},
      attributes: {
        //見出し用の属性
        myRichHeading: {
          //.myRichHeading クラスの h3 要素の HTML から string を抽出
          type: 'string',
          default: '',
          //内部の HTML
          source: 'html',
          //クラスを使ったセレクタを指定
          selector:'h3.myRichHeading'
        },
        //段落用の属性
        myRichText: {
          //.myRichText クラスの div 要素の子ノードから array を抽出
          type: 'array',
          default: '',
          //子ノード
          source: 'children',
          //クラスを使ったセレクタを指定
          selector:'div.myRichText'
        }
      },
      
      edit: function( props ) {
        return el(
          'div', 
          {}, 
          [
            el(
              RichText, 
              {
                tagName: 'h3',
                placeholder: 'タイトル(h3)を入力',
                //クラスを追加
                className: 'myRichHeading',
                value: props.attributes.myRichHeading,
                onChange: function( newHeading ) {
                  props.setAttributes( { myRichHeading: newHeading } );
                }
              }
            ),
            el(
              RichText, 
              {
                tagName: 'div',
                placeholder: '文章(段落)を入力',
                //クラスを追加
                className: 'myRichText',
                value: props.attributes.myRichText,
                multiline: 'p',
                onChange: function( newText ) {
                  props.setAttributes( { myRichText: newText } );
                }
              }
            )
          ]
        );
      },
      
      save: function( props ) {
        return el(
          'div', 
          {}, 
          [
            el(
              RichText.Content, 
              {
                tagName: 'h3',
                //クラスを追加
                className: 'myRichHeading',
                value: props.attributes.myRichHeading
              }
            ),
            el(
              RichText.Content, 
              {
                tagName: 'div',
                //クラスを追加
                className: 'myRichText',
                value: props.attributes.myRichText
              }
            )
          ]
        );
      },
    }
  );
}(
  window.wp.blocks,
  window.wp.element,
  window.wp.blockEditor
) );