オリジナルユニットを作る


エントリー本文に、独自の編集画面と保存データを持つユニット(オリジナルユニット)を追加します。

対応バージョン: Ver. 3.2.38 以降

実験的機能

JavaScript 側の登録 API は、Ver. 3.2 の間は実験的機能として提供し、Ver. 3.3 で正式に提供する予定です。

  • 変わりうる範囲: JavaScript 側の登録 API(registerUnitDefinition)の名前と引数、ユニット定義に指定できる項目は、マイナーバージョンアップで変更される可能性があります。
  • 変わらない範囲: PHP 側のユニット登録(unit-registry)は、この実験的機能の対象外です。従来どおりの仕様として提供しています。
  • 公開 API の線引き: このページに掲載している登録 API とユニット定義の項目を公開 API として扱います。掲載外のものも利用できますが、告知なく変更されることがあります。
  • サポート対象外: 管理画面に追加した独自の JavaScript・CSS は、バージョンアップ後の動作を保証しません。これらを原因とする不具合は、サポートの対象外です。

カスタムユニットとオリジナルユニット

a-blog cms には、もともとカスタムユニットという標準の機能があります。このページで作るオリジナルユニットとは別のものです。

カスタムユニット(標準機能)オリジナルユニット(このページ)
作り方ユニットメニューに custom_ で始まるモードを追加し、編集画面用と表示用のテンプレートに入力欄と出力を書くJavaScript で編集画面を、PHP で保存と表示を実装する
独自の編集 UIテンプレートに書いた入力欄自由に作れる(React などで実装できる)
独自の保存形式カスタムフィールドとして保存される自由に決められる
PHP の実装不要必要
向いているケース入力欄の組み合わせで足りる専用の入力画面・独自の保存形式が必要

まずカスタムユニットで足りないかを検討してください。 カスタムユニットなら PHP の実装が不要で、運用も簡単です。入力欄の組み合わせで足りない場合に、オリジナルユニットを作ります。

標準のカスタムユニットの編集画面そのものを JavaScript で差し替えることもできますが、その差し替えはすべてのカスタムユニットに適用されます。特定のカスタムユニットだけを差し替えることはできないため、個別の編集画面が必要な場合は、オリジナルユニットを作ってください。

作業のチェックリスト

オリジナルユニットは、次の5つが揃わないと動きません。どれか1つでも欠けると、保存できない・表示されない・データが消えるといった状態になります。

  • JavaScript で編集画面を登録する
  • PHP でユニットを登録する
  • 編集設定でユニットメニューに追加する
  • ユニット設定で、メニューにユニットを割り当てる
  • テーマにユニットの表示テンプレートを書く

JavaScript で編集画面を登録する

次の例は、文字列を1つ入力できる「グラフ」ユニットの編集画面です。入力欄の name は、ユニットの種類名_項目名_ユニットID の形にします。PHP 側の保存処理がこの名前で値を受け取ります。

ACMS.adminReady(() => {
  const { createElement } = ACMS.Library.React;

  function ChartEdit({ editor, unit }) {
    return createElement(
      'div',
      { className: 'acms-admin-unit-content' },
      createElement('input', {
        type: 'text',
        name: `my-chart_value_${unit.id}`,
        className: 'acms-admin-form-width-full',
        'aria-label': 'グラフの値(カンマ区切りの数値)',
        placeholder: '10,20,30',
        value: unit.attributes.value ?? '',
        onChange: (event) => editor.commands.setUnitAttributes(unit.id, { value: event.target.value }),
      }),
      createElement(
        'button',
        { type: 'button', className: 'acms-admin-btn', onClick: () => editor.commands.moveUpUnit(unit.id) },
        '上へ'
      ),
      createElement(
        'button',
        { type: 'button', className: 'acms-admin-btn', onClick: () => editor.commands.moveDownUnit(unit.id) },
        '下へ'
      ),
      createElement(
        'button',
        { type: 'button', className: 'acms-admin-btn', onClick: () => editor.commands.removeUnit(unit.id) },
        '削除'
      )
    );
  }

  ACMS.plugins.register('acme-units', {
    setup(ctx) {
      ctx.unitEditor.registerUnitDefinition('my-chart', {
        type: 'my-chart',
        name: 'グラフ',
        icon: 'bar_chart',
        edit: ChartEdit,
        supports: {
          multiple: true,
          nested: true,
        },
      });
    },
  });
});

登録は ACMS.adminReady() の中で行います

ユニットの登録は、ほかのプラグインの登録と同じく ACMS.adminReady() の中で行います。管理画面を表示したあとに登録した場合も、表示済みのエントリー編集・ダイレクト編集・ユニット設定の画面に、そのユニットが反映されます。

プラグインの JS は、<script type="module"> で読み込みます(はじめてのプラグイン を参照)。これは、実行順を揃えるための指定です。

edit に渡されるのは、unit(このユニットのデータ)と editor(ユニットエディター)などです。unit.attributes が、PHP 側の getAttributes() で返した値です。editor.commands で、データの更新・移動・削除などの操作ができます。

この例では、ユニットの移動と削除のボタンを自分で用意しています。標準のユニットが持つツールバーは、オリジナルユニットでは自動では付きません。

項目説明
typeユニットの種類名(登録名と同じにします)
name管理画面に表示する名前
iconアイコン名(Material Symbols の名前)
edit編集画面を返す関数
config編集設定画面での設定 UI を返す関数(省略可)
inplaceEditダイレクト編集用の UI を返す関数(省略可)。省略すると、ダイレクト編集では「対応していません」と表示されます
supports複数配置・入れ子・複製・移動の可否(省略可)

この登録だけで、エントリー編集・ダイレクト編集・編集設定の3つの画面すべてに反映されます。

種類名にアンダースコア(_)を使わないでください

a-blog cms は _ より前の部分をユニットの種類として扱います。my_chart は my として解釈され、意図しない動作になります。ハイフン区切り(my-chart)を使ってください。入力欄の name の区切りに使う _ は、この制約とは関係ありません。

PHP でユニットを登録する

保存・読み込み・サイト表示側の出力を担当するクラスを登録します。プラグインの ServiceProvider の init() に、次のように書きます。

use Acms\Services\Facades\Application;
use Acms\Services\Unit\Registry;

Application::bootstrap('unit-registry', function (Registry $registry) {
    $registry->bind('my-chart', \Acms\Plugins\AcmeUnits\Unit\MyChart::class);
});

登録するクラスは、Acms\Services\Unit\Contracts\Model を継承します。次の例は、JavaScript の例に対応する最小の実装です。

<?php

namespace Acms\Plugins\AcmeUnits\Unit;

use Acms\Services\Unit\Contracts\Model;
use Template;

/**
 * @extends \Acms\Services\Unit\Contracts\Model<array{value: string}>
 */
class MyChart extends Model
{
    /**
     * @var array{value: string}
     */
    private $attributes = ['value' => ''];

    public static function getUnitType(): string
    {
        return 'my-chart';
    }

    public static function getUnitLabel(): string
    {
        return 'グラフ';
    }

    public function getAttributes()
    {
        return ['value' => $this->getField1()];
    }

    public function setAttributes($attributes): void
    {
        $this->attributes = $attributes;
    }

    public function setDefault(string $configKeyPrefix, int $configIndex): void
    {
        $this->setField1(config("{$configKeyPrefix}field_1", '', $configIndex));
    }

    public function extract(array $request): void
    {
        $id = $this->getId();
        if (is_null($id)) {
            throw new \LogicException('Unit ID must be set before calling extract');
        }
        $value = $request["my-chart_value_{$id}"] ?? '';
        $this->setField1(is_string($value) ? $value : '');
    }

    public function canSave(): bool
    {
        return $this->getField1() !== '';
    }

    public function handleDuplicate(): void
    {
    }

    public function handleRemove(): void
    {
    }

    public function getSearchText(): string
    {
        return $this->getField1();
    }

    public function getSummaryText(): array
    {
        return [$this->getField1()];
    }

    public function render(Template $tpl, array $vars, array $rootBlock): void
    {
        if ($this->getField1() === '') {
            return;
        }
        $vars += ['value' => $this->getField1()];
        $tpl->add(array_merge(['unit#' . $this->getType()], $rootBlock), $vars);
    }

    public function renderEdit(Template $tpl, array $vars, array $rootBlock): void
    {
    }

    protected function getLegacy(): array
    {
        return ['value' => $this->getField1()];
    }
}

値は、ユニットが共通で持つ field1 に保存しています。getUnitType() が返す種類名は、bind() に渡した名前と同じにします。getUnitLabel() の値は、編集設定の入力候補やユニットメニューでの名前に使われます。

この登録を忘れると、データが警告なく失われます

PHP 側に登録されていない種類のユニットは、保存時・読み込み時・表示時のすべてで、エラーもログも出さずに無視されます。「保存したはずのユニットが消えている」という形で表面化するため、原因が分かりにくい不具合になります。

オリジナルユニットを作る場合は、JavaScript の登録と PHP の登録を必ずセットで行ってください。

Application::bootstrap() に渡すコールバックは、unit-registry が使われるたびに実行されます。bind() のように何度実行しても同じ結果になる処理だけを書いてください(カウントアップや外部通信は書かないでください)。

編集設定でユニットメニューに追加する

  1. 管理画面の「編集設定」→「ユニットメニュー」を開きます。編集画面セットを使っているサイトでは、使っているセットの「編集設定」を開きます。
  2. 「モード」の入力欄に種類名(例: my-chart)を入力します。PHP に登録済みの種類は入力候補として表示されます。 「ラベル」「アイコン」も必要に応じて入力します。
  3. 保存します。

PHP 側の追加実装は必要ありません。 入力候補は登録済みのユニットから自動的に作られます。

ユニット設定で、メニューにユニットを割り当てる

メニューの項目を追加しただけでは、エントリー編集画面のユニットメニューに出ません。どのユニットを追加する項目なのかを、「ユニット設定」で指定します。

  1. 管理画面の「編集設定」の隣にある「ユニット設定」を開きます(編集画面セットを使っているサイトでは、同じセットのもの)。
  2. 追加した項目(例: 「グラフ」)の欄に、ユニットの種類のボタンが並びます。「グラフ」のボタンを押して、ユニットを1つ追加します。
  3. 保存します。

保存すると、エントリー編集画面の「ユニットを追加」に、「グラフ」が出ます。

テーマに表示テンプレートを書く

サイトに表示するときの HTML は、テーマのテンプレートに書きます。テーマの include/unit/extend.html に、unit#種類名 のブロックを追加します。render() で渡した値が、ブロックの中の変数になります。


<div class="my-chart" data-values="{value}">{value}</div>

テンプレートに書かれていない種類のユニットは、サイトに何も表示されません。

ツールバーにボタンを追加する

標準のツールバーを使うユニット(標準のユニットなど)には、UI の差し込み(Slot / Fill)でボタンを追加できます。次の例は、ツールバーに「ID をコピー」ボタンを追加します。

ACMS.adminReady(() => {
  const { createElement } = ACMS.Library.React;
  const { withFillProps } = ACMS.plugins;

  ACMS.plugins.register('acme-unit-toolbar', {
    setup(ctx) {
      ctx.ui.fill(
        'unit-editor.unit-toolbar',
        withFillProps(({ unit }) =>
          createElement(
            'button',
            {
              type: 'button',
              className: 'acms-admin-btn',
              onClick: () => navigator.clipboard.writeText(unit.id),
            },
            'ID をコピー'
          )
        )
      );
    },
  });
});

→ UI の差し込み(Slot / Fill)

関連ページ

変更履歴

バージョン変更内容
Ver. 3.2新規追加(実験的機能)