フロントエンドとバックエンドが連携するデータ管理プラグインを作る
これまでの例は純粋なクライアントサイド(ブロック、フィールド、操作)か、クライアント + シンプルなインターフェース(設定ページ)でした。この例ではより完全なシーンを示します。サーバーサイドでデータテーブルを定義し、クライアントで TableBlockModel を継承して完全なテーブル機能を取得し、さらにカスタムフィールドコンポーネントとカスタム操作ボタンを追加して、CRUD を備えたデータ管理プラグインを構成 します。
この例は、これまでに学んだブロック、フィールド、操作をまとめて、完全なプラグインの開発フローを示します。
前提知識
以下の内容を事前に理解しておくと、開発がスムーズになります:
最終的な効果
「ToDo」データ管理プラグインを作ります。以下の機能を含みます:
サーバーサイドで todoItems データテーブルを定義し、プラグインインストール時にサンプルデータを自動投入
クライアントで TableBlockModel を継承し、すぐに使えるテーブルブロック(フィールド列、ページネーション、操作バーなど)
カスタムフィールドコンポーネント — カラー Tag で priority フィールドをレンダリング
カスタム操作ボタン — 「新規 ToDo」ボタンをクリックするとダイアログでフォーム入力してレコードを作成
完全なソースコードは @nocobase-example/plugin-custom-table-block-resource を参照してください。ローカルで動作確認したい場合:
yarn pm enable @nocobase-example/plugin-custom-table-block-resource
以下、ゼロからこのプラグインを構築していきます。
ステップ1:プラグインスケルトンの作成
リポジトリのルートで実行します:
yarn pm create @my-project/plugin-custom-table-block-resource
詳しくははじめてのプラグインを書く をご覧ください。
続いて開発モードを起動すると、以降はコードを修正するだけでホットリロードされます。nb init で作成したプロジェクトはプロジェクトのルートディレクトリ(<app-path>)で nb source dev を、自分で clone したソースリポジトリはソースコードのルートディレクトリで yarn dev を 実行してください。
ステップ2:データテーブルの定義(サーバーサイド)
src/server/collections/todoItems.ts を新規作成します。NocoBase はこのディレクトリ配下の collection 定義を自動的に読み込みます:
// src/server/collections/todoItems.ts
import { defineCollection } from '@nocobase/database' ;
export default defineCollection ({
name : 'todoItems' ,
title : 'Todo Items' ,
fields : [
{ name : 'title' , type : 'string' , title : 'Title' } ,
{
name : 'completed' ,
type : 'boolean' ,
title : 'Completed' ,
defaultValue : false ,
} ,
{
name : 'priority' ,
type : 'string' ,
title : 'Priority' ,
defaultValue : 'medium' ,
} ,
] ,
});
設定ページの例とは異なり、ここでは手動で resource を登録する必要はありません。NocoBase が各 collection に対して標準的な CRUD インターフェース(list、get、create、update、destroy)を自動生成します。
ステップ3:権限設定とサンプルデータ(サーバーサイド)
src/server/plugin.ts を編集し、load() で ACL 権限を設定、install() でサンプルデータを投入します:
// src/server/plugin.ts
import { Plugin } from '@nocobase/server' ;
export class PluginDataBlockServer extends Plugin {
async load () {
// ログインユーザーが todoItems の CRUD を実行可能
this . app . acl .allow ( 'todoItems' , [ 'list' , 'get' , 'create' , 'update' , 'destroy' ] , 'loggedIn' );
}
async install () {
// プラグインの初回インストール時にサンプルデータを投入
const repo = this . db .getRepository ( 'todoItems' );
const count = await repo .count ();
if (count === 0 ) {
await repo .createMany ({
records : [
{ title : 'Learn NocoBase plugin development' , completed : true , priority : 'high' } ,
{ title : 'Build a custom block' , completed : false , priority : 'high' } ,
{ title : 'Write documentation' , completed : false , priority : 'medium' } ,
{ title : 'Add unit tests' , completed : false , priority : 'low' } ,
] ,
});
}
}
}
export default PluginDataBlockServer;
重要なポイント:
acl.allow() — ['list', 'get', 'create', 'update', 'destroy'] で完全な CRUD 権限を開放、'loggedIn' はログインユーザーがアクセス可能であることを意味
install() — プラグインの初回インストール時にのみ実行され、初期データの投入に適している
this.db.getRepository() — collection 名でデータ操作オブジェクトを取得
resourceManager.define() は不要 — NocoBase が collection に対して CRUD インターフェースを自動生成
ステップ4:ブロックモデルの作成(クライアント)
src/client-v2/models/TodoBlockModel.tsx を新規作成します。TableBlockModel を継承すると、完全なテーブルブロック機能がすぐに使えます。フィールド列、操作バー、ページネーション、ソートなどが含まれ、renderComponent を自分で書く必要はありません。
ヒント
実際のプラグイン開発では、TableBlockModel のカスタマイズが不要な場合、このブロックを継承・登録する必要はなく、ユーザーにブロック追加時に「テーブル」を選択してもらえば十分です。この記事ではブロックモデルの定義と登録のフローを示すために、TodoBlockModel で TableBlockModel を継承しています。TableBlockModel がその他すべて(フィールド列、操作バー、ページネーションなど)を処理します。
// src/client-v2/models/TodoBlockModel.tsx
import { TableBlockModel } from '@nocobase/client-v2' ;
import type { Collection } from '@nocobase/flow-engine' ;
import { tExpr } from '../locale' ;
export class TodoBlockModel extends TableBlockModel {
// todoItems データテーブルでのみ使用可能に制限
static filterCollection (collection : Collection ) {
return collection .name === 'todoItems' ;
}
}
TodoBlockModel .define ({
label : tExpr ( 'Todo block' ) ,
});
filterCollection でこのブロックを todoItems データテーブルでのみ使用可能に制限しています。ユーザーが「Todo block」を追加する際、データテーブル選択リストには todoItems のみが表示され、関係のないテーブルは表示されません。
ステップ5:カスタムフィールドコンポーネントの作成(クライアント)
src/client-v2/models/PriorityFieldModel.tsx を新規作成します。カラー Tag で priority フィールドをレンダリングし、プレーンテキストよりも直感的にします:
// src/client-v2/models/PriorityFieldModel.tsx
import React from 'react' ;
import { ClickableFieldModel } from '@nocobase/client-v2' ;
import { DisplayItemModel } from '@nocobase/flow-engine' ;
import { Tag } from 'antd' ;
import { tExpr } from '../locale' ;
const priorityColors : Record < string , string > = {
high : 'red' ,
medium : 'orange' ,
low : 'green' ,
};
export class PriorityFieldModel extends ClickableFieldModel {
public renderComponent (value : string ) {
if ( ! value) return < span >-</ span >;
return < Tag color = {priorityColors[value] || 'default' }>{value}</ Tag >;
}
}
PriorityFieldModel .define ({
label : tExpr ( 'Priority tag' ) ,
});
// input(単行テキスト)タイプのフィールドインターフェースにバインド
DisplayItemModel .bindModelToInterface ( 'PriorityFieldModel' , [ 'input' ]);
登録後、テーブルの priority 列の設定で、「フィールドコンポーネント」ドロップダウンから「Priority tag」に切り替えられます。
ステップ6:カスタム操作ボタンの作成(クライアント)
src/client-v2/models/NewTodoActionModel.tsx を新規作成します。「新規 ToDo」ボタンをクリックすると、ctx.viewer.dialog() でダイアログを開き、フォーム入力後にレコードを作成します:
// src/client-v2/models/NewTodoActionModel.tsx
import React from 'react' ;
import { ActionModel , ActionSceneEnum } from '@nocobase/client-v2' ;
import { MultiRecordResource , observable , observer } from '@nocobase/flow-engine' ;
import { Button , Form , Input , Select , Space , Switch } from 'antd' ;
import { ButtonProps } from 'antd' ;
import { tExpr } from '../locale' ;
// observable でローディング状態を管理。useState の代わり
const formState = observable ({
loading : false ,
});
// ダイアログ内のフォームコンポーネント。observer でラップして observable の変化に応答
const NewTodoForm = observer ( function NewTodoForm ({
onSubmit ,
onCancel ,
} : {
onSubmit : (values : any ) => Promise < void >;
onCancel : () => void ;
}) {
const [ form ] = Form .useForm ();
const handleSubmit = async () => {
const values = await form .validateFields ();
formState .loading = true ;
try {
await onSubmit (values);
} finally {
formState .loading = false ;
}
};
return (
< Form form = {form} layout = "vertical" initialValues = {{ priority : 'medium' , completed : false }}>
< Form.Item label = "Title" name = "title" rules = {[{ required : true , message : 'Please enter title' }]}>
< Input placeholder = "Enter todo title" />
</ Form.Item >
< Form.Item label = "Priority" name = "priority" >
< Select
options = {[
{ label : 'High' , value : 'high' } ,
{ label : 'Medium' , value : 'medium' } ,
{ label : 'Low' , value : 'low' } ,
]}
/>
</ Form.Item >
< Form.Item label = "Completed" name = "completed" valuePropName = "checked" >
< Switch />
</ Form.Item >
< Form.Item >
< Space >
< Button type = "primary" onClick = {handleSubmit} loading = { formState .loading}>
OK
</ Button >
< Button onClick = {onCancel}>Cancel</ Button >
</ Space >
</ Form.Item >
</ Form >
);
});
export class NewTodoActionModel extends ActionModel {
static scene = ActionSceneEnum .collection;
defaultProps : ButtonProps = {
type : 'primary' ,
children : tExpr ( 'New todo' ) ,
};
}
NewTodoActionModel .define ({
label : tExpr ( 'New todo' ) ,
});
NewTodoActionModel .registerFlow ({
key : 'newTodoFlow' ,
title : tExpr ( 'New todo' ) ,
on : 'click' , // ボタンクリックイベントをリッスン
steps : {
openForm : {
async handler (ctx) {
const resource = ctx . blockModel ?.resource as MultiRecordResource ;
if ( ! resource) return ;
// ctx.viewer.dialog でダイアログを開く
ctx . viewer .dialog ({
content : (view) => (
< NewTodoForm
onSubmit = { async (values) => {
await resource .create (values);
ctx . message .success ( ctx .t ( 'Created successfully' ));
view .close ();
}}
onCancel = {() => view .close ()}
/>
) ,
});
} ,
} ,
} ,
});
重要なポイント:
ActionSceneEnum.collection — ボタンがブロック上部の操作バーに表示
on: 'click' — registerFlow でボタンの click イベントをリッスン
ctx.viewer.dialog() — NocoBase 組み込みのダイアログ機能。content は関数を受け取り、引数 view から view.close() でダイアログを閉じることが可能
resource.create(values) — データテーブルの create インターフェースを呼び出してレコードを作成。作成後、テーブルが自動更新
observable + observer — flow-engine が提供するリアクティブ状態管理で useState を置き換え。コンポーネントが formState.loading の変化に自動的に応答
ステップ7:多言語ファイルの追加
プラグインの src/locale/ 配下の翻訳ファイルを編集します:
// src/locale/zh-CN.json
{
"Todo block" : "待办事项区块" ,
"Priority tag" : "优先级标签" ,
"New todo" : "新建待办" ,
"Todo form" : "待办表单" ,
"Title" : "标题" ,
"Priority" : "优先级" ,
"Completed" : "已完成" ,
"Created successfully" : "创建成功"
}
// src/locale/en-US.json
{
"Todo block" : "Todo block" ,
"Priority tag" : "Priority tag" ,
"New todo" : "New todo" ,
"Todo form" : "Todo form" ,
"Title" : "Title" ,
"Priority" : "Priority" ,
"Completed" : "Completed" ,
"Created successfully" : "Created successfully"
}
注意
初めて言語ファイルを追加した場合、アプリの再起動が必要です。
翻訳ファイルの書き方と tExpr() のその他の使い方については、i18n 国際化 をご覧ください。
ステップ8:プラグインへの登録(クライアント)
src/client-v2/plugin.tsx を編集します。2つのことが必要です:モデルの登録と、todoItems のクライアントデータソースへの登録。
注意
プラグインコード内で addCollection を使ってデータテーブルを手動登録するのは稀な方法 です。ここではフロントエンド・バックエンド連携の完全なフローをデモするためだけに行っています。実際のプロジェクトでは、データテーブルは通常 NocoBase の画面上で作成・設定するか、API / MCP などで管理するため、プラグインのクライアントコードで明示的に登録する必要はありません。
defineCollection で定義したテーブルはサーバー内部テーブルで、デフォルトではブロックのデータテーブル選択リストに表示されません。addCollection で手動登録すると、ユーザーがブロック追加時に todoItems を選択できるようになります。
// src/client-v2/plugin.tsx
import { Plugin } from '@nocobase/client-v2' ;
const todoItemsCollection = {
name : 'todoItems' ,
title : 'Todo Items' ,
// filterTargetKey は必須。設定しないと collection がブロックのデータテーブル選択リストに表示されない
filterTargetKey : 'id' ,
fields : [
{
type : 'bigInt' ,
name : 'id' ,
primaryKey : true ,
autoIncrement : true ,
interface : 'id' ,
} ,
{
type : 'string' ,
name : 'title' ,
interface : 'input' ,
uiSchema : { type : 'string' , title : 'Title' , 'x-component' : 'Input' } ,
} ,
{
type : 'boolean' ,
name : 'completed' ,
interface : 'checkbox' ,
uiSchema : { type : 'boolean' , title : 'Completed' , 'x-component' : 'Checkbox' } ,
} ,
{
type : 'string' ,
name : 'priority' ,
interface : 'input' ,
uiSchema : { type : 'string' , title : 'Priority' , 'x-component' : 'Input' } ,
} ,
] ,
};
export class PluginCustomTableBlockResourceClientV2 extends Plugin {
async load () {
// ブロック、フィールド、操作モデルの登録
this . flowEngine .registerModelLoaders ({
TodoBlockModel : {
loader : () => import ( './models/TodoBlockModel' ) ,
} ,
PriorityFieldModel : {
loader : () => import ( './models/PriorityFieldModel' ) ,
} ,
NewTodoActionModel : {
loader : () => import ( './models/NewTodoActionModel' ) ,
} ,
});
// todoItems をクライアントサイドのデータソースに登録。
// ensureLoaded() が load() の後に実行され、setCollections() で
// すべての collection をクリアして再設定するため、'dataSource:loaded' イベントを
// リッスンする必要がある。イベントコールバック内で再登録することで
// addCollection がリロード後も維持される。
const addTodoCollection = () => {
const mainDS = this . flowEngine . dataSourceManager .getDataSource ( 'main' );
if (mainDS && ! mainDS .getCollection ( 'todoItems' )) {
mainDS .addCollection (todoItemsCollection);
}
};
this . app . eventBus .addEventListener ( 'dataSource:loaded' , (event : Event ) => {
if ((event as CustomEvent ). detail ?.dataSourceKey === 'main' ) {
addTodoCollection ();
}
});
}
}
export default PluginCustomTableBlockResourceClientV2;
重要なポイント:
registerModelLoaders — 遅延読み込みで3つのモデル(ブロック、フィールド、操作)を登録
this.app.eventBus — アプリケーションレベルのイベントバス、ライフサイクルイベントの監視に使用
dataSource:loaded イベント — データソースの読み込み完了後にトリガー。このイベントのコールバック内で addCollection を呼び出す必要がある。ensureLoaded() が load() の後に実行され、すべての collection をクリアして再設定するため、load() 内で直接 addCollection を呼ぶと上書きされてしまう
addCollection() — collection をクライアントデータソースに登録。フィールドには interface と uiSchema プロパティが必要で、NocoBase がレンダリング方法を判断できるようにする
filterTargetKey: 'id' — 必須設定。レコードの一意識別に使うフィールド(通常は主キー)を指定。設定しないと collection がブロックのデータテーブル選択リストに表示されない
サーバーの defineCollection は物理テーブルと ORM マッ ピングの作成を担当し、クライアントの addCollection は UI にテーブルの存在を知らせる。両方が連携して初めてフロントエンド・バックエンド連携が完成する
ステップ9:プラグインの有効化
yarn pm enable @my-project/plugin-custom-table-block-resource
有効化後:
新しいページを作成し、「ブロックの追加」をクリックして「Todo block」を選択、todoItems データテーブルにバインド
テーブルが自動的にデータを読み込み、フィールド列、ページネーションなどを表示
「操作の設定」で「New todo」ボタンを追加し、クリックするとダイアログでフォーム入力してレコードを作成
priority 列の「フィールドコンポーネント」で「Priority tag」に切り替えると、priority がカラー Tag で表示
完全なソースコード
まとめ
この例で使用した機能:
関連リンク