編集して保存（saveとedit、ブロック検証について）

以下のページを読んでみて、実際にちょっと動かしてみたのをきっかけに、自分なりに内容をかみ砕いてみました。
翻訳未満という感じにまとめてみます。
https://developer.wordpress.org/block-editor/developers/block-api/block-edit-save/
(全ページ訳して書き溜めておいてから公開したかったのですが、時代に取り残される前に公開だけしておこうというわけで中途半端にも公開しておこうと思ったらすでに5月です)


この記事を書いた時点でのWordPressのバージョンは5.3です。

# 編集して保存
ブロック登録の時に使用するeditおよびsave関数の説明です。

# edit(編集)
editには、管理画面で使用するブロックの構造を記述します
edit関数はオブジェクト引数を介して、次のプロパティを受け取ります。

## attributes(属性)
使用可能なすべての属性と、対応する値を表示します。
ブロック登録中にcontent属性を定義したと仮定すると、編集機能でその値を受け取って使用することができます。

実際にattributesを受け取って使用してみました。

```
const { registerBlockType } = wp.blocks;

registerBlockType( 'my-gutenblock/message', {
	title: 'message',
	category: 'common',
	attributes: {
		content: {
			type: 'string',
			default: 'Thank you, world',
		},
	},
	edit: ( { attributes } ) => {
		return <div>{ attributes.content }</div> 
	},
} );
```

attributesにてcontentという属性を定義しました。文字列でデフォルト値は「Thank you, world」という文字列です。
このcontentをeditで参照すると、デフォルト値である「Thank you, world」が表示されます。
![edit_attributes.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404526/4fe0bd34-595b-6931-16fa-117a8e15d4c0.png)

## className
ラッパー要素のクラス名を返す。
デフォルトだとwp-block-your-block-nameというクラス名ですね。

## isSelected
ブロックが現在選択されているか。サンプルでは選択されているときのみspan要素を出力しています。
カーソルを外すとspan要素は消えます。

## setAttributes
attributesの値を更新できます。
例えばテキストボックスの文字列を保持する属性があったとしたら、文字列が変更されるごとに属性も更新します。

サンプルで載っていたコードはボタンクリックごとにattributesのmySettingの値を変更させるようだったので、書いてみました。
mySettingをboolean型にして、ボタンが押されるごとにtrue/falseを変更するようにしています。

```
const { registerBlockType } = wp.blocks;

registerBlockType( 'my-gutenblock/message', {
	title: 'message',
	category: 'common',
	attributes: {
		content: {
			type: 'string',
			default: 'Thank you, world',
		},
		mySetting : {
			type: 'boolean',
			default: true,
		}
	},
	edit: ( { attributes, setAttributes, className, isSelected } ) => {
		const { content, mySetting } = attributes;
		const toggleSetting = () => setAttributes( { mySetting: !mySetting } );
		return (
			<div className={ className }>
				{ content }
				{ isSelected && 
						<button onClick={ toggleSetting }>ToggleSetting:{ mySetting ? 'True' : 'False' }</button>
				}
			</div>
		);
	},
} );
```

# save(保存)
save関数では、属性をマークアップに結合する方法を定義します。
最終的にマークアップされた内容はpost_contentsに保存されます。

```
const { registerBlockType } = wp.blocks;

registerBlockType( 'my-gutenblock/message', {
	title: 'message',
	category: 'common',
	edit: () => {
		return <div>Your block.</div>;
	},
	save: () => {
		return <div>Your block.</div>;
	},
} );
```

上記ブロックを1つ設置して保存した時のwp_postsのpost_contentの中身です。

```
<!-- wp:my-gutenblock/message -->
<div class="wp-block-my-gutenblock-message">Your block.</div>
<!-- /wp:my-gutenblock/message -->
```

saveの戻り値はサイトでどのようにブロックが表示されるかを表すWordPressElementである必要があります。
https://developer.wordpress.org/block-editor/packages/packages-element/

WordpressElementではなく、単純に文字列だけを返すという実装もできます。
例えば、こんな風にHTMLを文字列として返すということもできます。

```
save: () => {
	return '<div>Your block.</div>';
},
```

しかし、この場合は文字列として処理されるので、文字はエスケープされてタグとして認識されません。（文字列の扱い）
DBの値をみると、エスケープされているのが分かります。
![save_string.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404526/35abf01a-c598-f69f-b8aa-ce993bbb7c79.png)


```
<!-- wp:my-gutenblock/message -->
&lt;div>Your block.&lt;/div>
<!-- /wp:my-gutenblock/message -->
```

どうしてもHTMLを返したい場合は、wp.element.RawHTMLを使って返します

```
const { RawHTML } = wp.element;
const { registerBlockType } = wp.blocks;

registerBlockType( 'my-gutenblock/message', {
	title: 'message',
	category: 'common',
	save: () => {
		const my_html='<div>Your <b>block</b>.</div>';

		return <RawHTML>{ my_html }</RawHTML>;
	},
} );
```
![save_rawhtml.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404526/a5a4d48e-24b6-26b9-217e-561f64b475eb.png)


こちらもDBの値をみてみました、エスケープされていません。

```
<!-- wp:my-gutenblock/message -->
<div class="wp-block-my-gutenblock-message"><div>Your <b>block</b>.</div></div>
<!-- /wp:my-gutenblock/message -->
```

ただ、入力された値をそのままRawHTMLで保存/出力すると、クロスサイトスクリプティングが発生するのでお勧めはしないとのことです…。

## 注意書き
save関数には制約があります。
原文を日本語訳したものをそのまま書くと
「保存関数は、呼び出しに使用される属性のみに依存する純粋な関数である必要があります」
のようになります。
例えばページ内のどこかの要素から値を取得して表示、などは値が変わった時に整合性がとれなくなるので
使えないということです。
どうしても使いたいというときは以下の2つの方法を検討します。
1.動的ブロックを使用して、サーバ上の必要な情報を動的に取得
2.外部値を属性として保存し、変更が発生するとブロックのedit機能で動的に更新する

1は動的ブロックの説明をよんでもらうのが早いです。
https://developer.wordpress.org/block-editor/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks/
サーバでレンダリングすることで、save関数には何も持たせないようにする方法です。
2はどこからの要素から値を取得した場合は一旦属性値として保存しておくという方法です。

## attributes(属性)
editと同じくsave関数は、オブジェクト引数も受け取ります。

属性にてソースが指定されていないものはコメントの後ろに追記される形で保存されます。
こちらも実験してみました。

```
const { registerBlockType } = wp.blocks;

registerBlockType( 'my-gutenblock/message', {
	title: 'message',
	category: 'common',
	attributes: {
		content: {
			type: 'string',
		}
	},
	edit: ( { attributes, setAttributes } ) => {
		const { content } = attributes;
		setAttributes({content: 'Thank you, world'});

		return <div>{ attributes.content }</div>
	},
	save: ( { attributes } ) => {
		return <div>{ attributes.content }</div>
	},
} );
```

attributesにsourceを指定せずにcontentの値を変更します。
こちらのブロックを作成して保存した場合のDB値です。
ブロックのコメントの後ろにcontentが保存されていますね。

```
<!-- wp:my-gutenblock/message {"content":"Thank you, world"} -->
<div class="wp-block-my-gutenblock-message">Thank you, world</div>
<!-- /wp:my-gutenblock/message -->
```

例
子要素に属性を保存します
（pタグに値を保存するということかな…）

```
const { registerBlockType } = wp.blocks;
const { TextControl } = wp.components;

registerBlockType( 'my-gutenblock/message', {
	title: 'message',
	category: 'common',
	attributes: {
		content: {
			type: 'string',
			source: 'html',
			selector: 'p',
		}
	},
	edit: ( { attributes, setAttributes } ) => {
		const updateFieldValue = ( val ) => {
			setAttributes( { content: val } );
		}
		return <TextControl
				label='My Text Field'
				value={ attributes.content }
				onChange={ updateFieldValue }
			/>;
	},
	save: ( { attributes } ) => {
		return <p>{ attributes.content }</p>
	},
} );
```

※PHPにてwp-componentsを読み込む必要あり

## シリアル化による属性の保存
saveでも少しふれたとおりにソースが指定されていないものはコメントに追記される形でシリアル化されて保存されます。
理想的には属性はマークアップに含める必要がある…とのことですが、どうしてもという場合はこの保存方法を使うのが良いかと思います。
例として載っていたサンプルコードは「最新の記事」ウィジェットのブロックと同じような動作のものです。
動的ブロックはsave関数にてnullを返し、内容をコメントに保存しているので、この説明の例としてあがっています。
ですが、ちょっと動きがどうなっているかがつかみにくかったので、最新の記事ウィジェットのコードを見ながら似たようなものを作成してみました。
「最新の記事」ウィジェットはサイドバーでパラメータが設定できますが、今回はテキストボックスで表示件数を入力するだけとしています。

```
const { registerBlockType } = wp.blocks;
const { TextControl } = wp.components;

registerBlockType( 'my-gutenblock/message', {
	title: 'message',
	category: 'common',
	attributes: {
		postsToShow: {
			type: 'number',
		}
	},
	edit: ( { attributes, setAttributes } ) => {
		return <TextControl
				label='Number Posts to Show'
				value={ attributes.postsToShow }
				onChange={ ( val ) => {
					setAttributes( { postsToShow: parseInt( val ) } );
				}}
		/>
	},
	save: () => {
		return null;
	},
} );
```

※PHP側（プラグインとしてブロックを登録）

```
function myguten_block_example_01_register_block()
{
    wp_register_script(
        'myguten-test-block-01',
        plugins_url('index.js',  __FILE__ ),
        ['wp-blocks','wp-element','wp-components']
    );

    register_block_type('my-gutenblock/message', [
        'editor_script' => 'myguten-test-block-01',
        'render_callback' => 'my_render_block_core_latest_posts',
    ] );

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

function my_render_block_core_latest_posts($attributes)
{
	$args = [
		'post_type' => 'post',
		'posts_per_page' => $attributes['postsToShow'],
		'post_status' => 'publish',	// 投稿ステータス
		'orderby' => 'date',	// 日付
		'order' => 'DESC',	// 降順
		'suppress_filters' => false,	// 現在の言語の投稿のみ表示
	];

	$recent_posts = get_posts($args);

	$html = '';
	foreach ($recent_posts as $recent_post) 
	{
		$html .= '<div>';
		$html .= '<p><a href="'.get_permalink($recent_post->ID).'">'.$recent_post->post_title.'</a></p>';
		$html .= '<p>'.get_the_date('Y年m月d日',$recent_post->ID).'</p>';
		$html .= '</div>';
	}
	return $html;
}
```

管理画面で指定した件数分、フロントでは最新の記事のタイトルと投稿日を表示するようにしています。
フロント
![save_example_serialize1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404526/fb01ad71-a40b-b274-e57f-1ca8fc0393b6.png)


管理画面
![save_example_serialize2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404526/dd71a3d2-c7ae-94c0-97f6-708be1ad8664.png)


この場合、表示件数の値はコメントに追記される形で保存されています。

```
<!-- wp:my-gutenblock/message {"postsToShow":3} /-->
```

# 検証
エディターが読み込まれると、投稿コンテンツ内のすべてのブロックが検証されます。
DBに保存されている値とブロックの内容があっているかという検証です。
エディターが読み込まれると、DBに保存されている内容から属性を解析してブロックを再生成します。
しかし、再生成したマークアップが保存した内容と異なる場合はブロックが無効になります。
（どういう場合に無効になるか…というのは検証FAQに書いてあったので後述します）

ブロックが無効となった場合は以下のように表示されます。
（ここはドキュメントと私の環境で画像が違います…。自分の環境ベースでどうなるかを見ていきます）
![validation_type2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404526/356ffc2d-596f-4ca8-fad8-aaf3f3f5c05c.png)

解決をクリックすると、「HTMLに変換」か「ブロックへ変換」を迫られます。
しかし、どちらを押してもカスタムHTMLブロックになります…。
（「ブロックへ変換」はブロック内でtransformsを設定すると任意のブロックに変換できるのかと思って実験したのですが、うまくいかずにクラシックブロックに変更されました）
![validation_recover_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404526/25bb5138-cc4f-f97e-fe97-529828cd0605.png)


右のオプションをクリックすると、「クラシックブロックに変換」または「ブロックのリカバリーを試行」を選択できます。
![validation_recover_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404526/fc3d5783-71a9-da87-db6e-0bce74b6f34d.png)

ブロックのリカバリーを試行を選択すると、ブロック作成後の入力画面が表示されます。
内容は反映されていないので…また内容を入力してください、ということでしょうか…。
![validation_recover_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404526/d456d074-dac3-316e-6d74-5aa70e60e8bf.png)

# 検証FAQ
ブロック無効についてのFAQの項目です。

どういうときにブロックが無効扱いになるのか、これが発生するのは大きく分けて2パターンのようです。
1.ブロックのコードに欠陥がある
2.ユーザまたは外部エディターがブロックのHTMLマークアップを変更して、ブロックが正しいとみなされないようにした

字面だけでは想像しづらいので、ちょっと実際に書いてみます。

実験のため、テキストボックスの値を表示するブロックを作成します。
出力イメージは以下の通りです。

```
<div><p>Danke, Welt</p></div>
```

まずは1の項目を実験します。
attributesをうっかり間違えて、ブロック無効となるかを確認します。
selectorにdivを指定していますが、本来はpです。

```
const { registerBlockType } = wp.blocks;
const { TextControl } = wp.components;

registerBlockType( 'my-gutenblock/message', {
	title: 'message',
	category: 'common',
	attributes: {
		content: {
			type: 'string',
			source: 'html',
			selector: 'div',
		}
	},
	edit: ( { attributes, setAttributes } ) => {
		return <TextControl
				label='input your message.'
				value={ attributes.content }
				onChange={ ( val ) => {
					setAttributes( { content: val } );
				}}
		/>
	},
	save: ( { attributes } ) => {
		return <div><p>{ attributes.content }</p></div>
	},
} );
```

任意のテキストを入力して、保存します。
![validation_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404526/9bae72f6-0a8b-badf-b308-5a4a0a4ccbbc.png)

保存後のDBの値はこちらです。

```
<!-- wp:my-gutenblock/message -->
<div class="wp-block-my-gutenblock-message"><p>Danke, Welt</p></div>
<!-- /wp:my-gutenblock/message -->
```

再び記事を表示すると、（意図した通りに）ブロック無効となっています。
![validation_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404526/b4f75481-7058-7ce4-2b83-ae3a835f6c4b.png)

コンソールを見るとエラーメッセージが出ています。（Firefoxを使用しています。）

```
Block validation: Block validation failed for `my-gutenblock/message` ( 
Object { name: "my-gutenblock/message", icon: {…}, attributes: {…}, keywords: [], save: save(), title: "message", category: "common", edit: edit() }
 ).

Content generated by `save` function:

<div class="wp-block-my-gutenblock-message"><p>&lt;p>Danke, Welt&lt;/p></p></div>

Content retrieved from post body:

<div class="wp-block-my-gutenblock-message"><p>Danke, Welt</p></div> 
```

エラーログを見て、selectorをdivからpに修正すると…テキストが読み込まれます。


では、次は2について実験します。
マークアップの変更とありますね…。これが発生するのは、ユーザーから「このブロックで作成するテキストは全部太字にしたい」という要望があるような場合…でしょうか。
（他にも、「divタグにクラスを追加してほしい」とかいう要望は結構ありそうですね。）

「太字にしたい」という要望をかなえるために、フロントのマークアップをこのように変更します。

```
<div><p><b>Danke, Welt</b></p></div>
```

コードも変更ですね。
\<b\>タグを入れて、selectorもbを見るようにします。

```
const { registerBlockType } = wp.blocks;
const { TextControl } = wp.components;

registerBlockType( 'my-gutenblock/message', {
	title: 'message',
	category: 'common',
	attributes: {
		content: {
			type: 'string',
			source: 'html',
			selector: 'b',
		}
	},
	edit: ( { attributes, setAttributes } ) => {
		return <TextControl
				label='input your message.'
				value={ attributes.content }
				onChange={ ( val ) => {
					setAttributes( { content: val } );
				}}
		/>
	},
	save: ( { attributes } ) => {
		return <div><p><b>{ attributes.content }</b></p></div>
	},
} );
```

保存しておいたブロックをエディターで開くと、ブロック無効になります。

…この動作からわかるように、ブロックを使用後にマークアップの変更は難しいようですね。
とはいえ、けっこうよくある修正だと思うのですが…。
こちらに関してはQの最後でもあげられており、
回答としては以下のようになっています。

意図的なマークアップの変更で、レガシーコンテンツに対応する方法の詳細については、非推奨ブロックのガイドを参照してくださいhttps://developer.wordpress.org/block-editor/developers/block-api/block-deprecation/

（私まだちらっとしか見てない…）

# 終わり
今回は以上です。