TypeScriptでWordPressのブロックを開発する「参考記事リンクブロック」パート2
参考サイト
ソースファイル
index.tsx
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps } from '@wordpress/block-editor';
import { TextControl, Button } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { EntityRecord } from '@wordpress/core-data';
import { store as coreStore } from '@wordpress/core-data';
import React, { useState, useEffect } from 'react';
// Post 型の定義: WordPress の投稿データ構造を表現
type Post = EntityRecord & {
id: number;
title: {
rendered: string;
};
};
// WordPress データストアの型定義を拡張
declare module '@wordpress/data' {
export function select(store: typeof coreStore): {
// 複数のエンティティレコードを取得するための関数
getEntityRecords<T extends EntityRecord>(
kind: 'postType',
name: string,
queryArgs?: Record<string, any>
): T[] | null;
// 単一のエンティティレコードを取得するための関数
getEntityRecord<T extends EntityRecord>(
kind: 'postType',
name: string,
id: number
): T | undefined;
// タクソノミーのエンティティレコードを取得するための関数
getEntityRecords<T extends EntityRecord>(
kind: 'taxonomy',
name: string,
queryArgs?: Record<string, any>
): T[] | null;
};
}
// ブロックの属性を定義するインターフェース
interface PostSelectorAttributes {
selectedPostId: number;
selectedCategoryId: number;
searchText: string;
}
// カスタムブロックの登録
registerBlockType<PostSelectorAttributes>('customtheme/sample04', {
title: __('Post Selector', 'my-customtheme'),
icon: 'admin-post',
category: 'widgets',
// ブロックの属性を定義
attributes: {
selectedPostId: {
type: 'number',
default: 0,
},
selectedCategoryId: {
type: 'number',
default: 0,
},
searchText: {
type: 'string',
default: '',
},
},
// ブロックのエディタ表示を定義
edit: ({ attributes, setAttributes }) => {
const blockProps = useBlockProps();
// 検索テキストの状態を管理
const [searchTerm, setSearchTerm] = useState(attributes.searchText);
// カテゴリーの一覧を取得
const categories = useSelect((select) => {
return select(coreStore).getEntityRecords('taxonomy', 'category', { per_page: -1 });
}, []);
// 投稿一覧を取得(検索条件とカテゴリーでフィルタリング)
const posts = useSelect((select) => {
const queryArgs: Record<string, any> = {
per_page: 10, // パフォーマンスのため、表示する投稿数を制限
search: searchTerm,
};
if (attributes.selectedCategoryId !== 0) {
queryArgs.categories = attributes.selectedCategoryId;
}
return select(coreStore).getEntityRecords<Post>('postType', 'post', queryArgs);
}, [attributes.selectedCategoryId, searchTerm]);
// 選択された投稿の詳細を取得
const selectedPost = useSelect((select) => {
return attributes.selectedPostId
? select(coreStore).getEntityRecord<Post>('postType', 'post', attributes.selectedPostId)
: undefined;
}, [attributes.selectedPostId]);
// 投稿選択時の処理
const onSelectPost = (postId: number) => {
setAttributes({ selectedPostId: postId });
};
// カテゴリー選択時の処理
const onSelectCategory = (categoryId: number) => {
setAttributes({ selectedCategoryId: categoryId, selectedPostId: 0 });
};
// 検索テキスト変更時の処理
const onSearchTextChange = (text: string) => {
setSearchTerm(text);
};
// 検索テキストが変更されたら属性を更新
useEffect(() => {
setAttributes({ searchText: searchTerm });
}, [searchTerm]);
// ブロックのUI構造を返す
return (
<div {...blockProps} style={{
border: '1px solid skyblue',
padding: '1rem',
marginTop: '4rem',
position: 'relative'
}}>
{/* ブロックのタイトル */}
<span style={{
fontSize: '1rem',
color: '#ffffff',
position: 'absolute',
lineHeight: '2rem',
top: '-2rem',
left: '-1px',
backgroundColor: 'skyblue',
padding: '0 1rem',
}}>
参考記事
</span>
<div style={{ marginBottom: '1rem' }}>
{/* 検索入力フィールド */}
<TextControl
label={__('記事を検索', 'my-customtheme')}
value={searchTerm}
onChange={onSearchTextChange}
// プレイスホルダーテキストを追加して、ユーザーに入力すべき内容を示唆
placeholder={__('タイトルまたは本文を入力', 'my-customtheme')}
style={{ marginBottom: '0.5rem' }}
/>
{/* カテゴリー選択ボタン群 */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', marginBottom: '1rem' }}>
<Button
isPrimary={attributes.selectedCategoryId === 0}
onClick={() => onSelectCategory(0)}
>
{__('全てのカテゴリー', 'my-customtheme')}
</Button>
{categories?.map((category) => (
<Button
key={category.id}
isPrimary={attributes.selectedCategoryId === category.id}
onClick={() => onSelectCategory(category.id)}
>
{category.name}
</Button>
))}
</div>
{/* 投稿リスト */}
<div style={{ maxHeight: '200px', overflowY: 'auto', border: '1px solid #ccc', padding: '0.5rem' }}>
{posts?.map((post) => (
<div
key={post.id}
style={{
padding: '0.5rem',
cursor: 'pointer',
backgroundColor: attributes.selectedPostId === post.id ? '#e0e0e0' : 'transparent',
}}
onClick={() => onSelectPost(post.id)}
>
{post.title.rendered}
</div>
))}
{(!posts || posts.length === 0) && (
<p style={{color: '#888', fontStyle: 'italic'}}>{__('該当する投稿がありません', 'my-customtheme')}</p>
)}
</div>
</div>
{/* 選択された投稿の表示 */}
{selectedPost && (
<p style={{
padding: '0rem 1rem 0rem 2rem',
position: 'relative',
margin: 0,
}}>
<span style={{
content: '"\u25B6"',
color: 'skyblue',
position: 'absolute',
top: '50%',
left: 0,
transform: 'translateY(-50%)'
}}>
▶
</span>
{selectedPost.title.rendered}
</p>
)}
{!selectedPost && (
<p style={{color: '#888', fontStyle: 'italic'}}>{__('投稿が選択されていません', 'my-customtheme')}</p>
)}
</div>
);
},
// フロントエンド表示はPHPで処理するため、save関数は不要
save: () => null,
});
functions.php
// カスタムブロックの登録
function my_theme_custom_block_init() {
// sample04 ブロックの登録
register_block_type('customtheme/sample04', array(
'editor_script' => 'sample04-block-script',
'render_callback' => 'render_sample04_block',
'attributes' => array(
'selectedPostId' => array(
'type' => 'number',
'default' => 0,
),
),
));
// ブロックスクリプトの登録
$asset_file_sample04 = include(get_template_directory() . '/build/sample04.asset.php');
wp_register_script(
'sample04-block-script',
get_template_directory_uri() . '/build/sample04.js',
$asset_file_sample04['dependencies'],
$asset_file_sample04['version']
);
}
add_action('init', 'my_theme_custom_block_init');
// sample04ブロックのレンダリングコールバック関数
function render_sample04_block($attributes) {
$post_id = $attributes['selectedPostId'];
if (!$post_id) {
return '<p>' . esc_html__('No post selected', 'my-customtheme') . '</p>';
}
$post = get_post($post_id);
if (!$post) {
return '<p>' . esc_html__('Selected post not found', 'my-custometheme') . '</p>';
}
$post_title = esc_html($post->post_title);
$post_url = esc_url(get_permalink($post));
return sprintf(
'<p><a href="%s">%s</a></p>',
$post_url,
$post_title
);
}
webpack.config.js
const defaultConfig = require('@wordpress/scripts/config/webpack.config');
const path = require('path');
module.exports = {
...defaultConfig,
// エントリーポイントの設定
entry: {
sample04: path.resolve(__dirname, 'wp-content/themes/customtheme/src/blocks/sample04selectpost2/index.tsx'),
},
// 出力の設定
output: {
path: path.resolve(__dirname, 'wp-content/themes/customtheme/build'),
filename: '[name].js', // [name] には entry で指定したキーが入る
},
};
package.json
{
"name": "1007_coconala",
"version": "1.0.0",
"main": "build/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "wp-scripts build",
"start": "wp-scripts start"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@types/wordpress__core-data": "^2.4.5",
"@wordpress/block-editor": "^14.4.0",
"@wordpress/blocks": "^13.9.0",
"@wordpress/components": "^28.9.0",
"@wordpress/core-data": "^7.9.0",
"@wordpress/data": "^10.9.0",
"@wordpress/i18n": "^5.9.0",
"@wordpress/scripts": "^30.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/wordpress__block-editor": "^11.5.15",
"@types/wordpress__blocks": "^12.5.14",
"@types/wordpress__components": "^23.0.12"
}
}