🦊

ReactでXML editorを実装する

2024/02/23に公開

react-xml-editorを使ってXML editorを実装する

完成イメージ

Reactにこんな感じのXMLエディターを実装します。
上段が読み込んだXMLを階層別に表示して、適宜属性等を編集できる領域
その下のボタンは上段で編集した階層構造をXMLとして下段に出力するもの

react-xml-editor

今回はreact-xml-editorというパッケージを使っていきます。

リポジトリ
https://github.com/captain-igloo/react-xml-editor/tree/master

インストール

npm install --save react-xml-editor

Reactで実装

react-xml-editorはドキュメントらしいものはなく、基本的にDemoフォルダを見てくれよなというものなのでそちらを参考に実装

モジュールのインポート

まずは必要なモジュールをインポートします。
ここはDemoとパス等が違うので、調整しました。

// react-xml-editorから必要なモジュールをインポートします。
import { Builder, Util, XmlEditor } from 'react-xml-editor';
// DocSpecの型定義をインポートします。
import { DocSpec } from 'react-xml-editor/lib/src/types';
// XMLエディタのスタイルシートをインポートします。
import 'react-xml-editor/css/xonomy.css';

XMLドキュメントの仕様定義

ここで仕様を決めることで各種編集がGUIで可能になります。
属性名等は変数にして汎用性を高める等工夫が必要です。
ここでは決め打ちでitemとかにしてます。

// XMLドキュメントの仕様を定義します。これによりXMLエディタの振る舞いをカスタマイズできます。
const docSpec: DocSpec = {
  elements: {
    item: {
      attributes: {
        label: {
          asker: Util.askString,
          menu: [
            {
              action: Util.deleteAttribute,
              caption: 'Delete attribute',
            },
          ],
        },
        type: {
          asker: Util.askPicklist([
            {
              value: 'short',
              caption: 'short',
            },
            {
              value: 'medium',
              caption: 'medium',
            },
            'long',
          ]),
        },
      },
      menu: [
        {
          action: Util.newElementChild('<child />'),
          caption: 'Append child <child />',
        },
        {
          action: Util.newAttribute({
            name: 'label',
            value: 'default value',
          }),
          caption: 'Add attribute @label',
          hideIf: (xml, id) => {
            const element = Util.getXmlNode(xml, id);
            return (
              element && element.$ && typeof element.$.label !== 'undefined'
            );
          },
        },
        {
          action: Util.deleteElement,
          caption: 'Delete this <item />',
          icon: 'exclamation.png',
        },
        {
          action: Util.newElementBefore('<item />'),
          caption: 'New <item /> before this',
        },
        {
          action: Util.newElementAfter('<item />'),
          caption: 'New <item /> after this',
        },
        {
          action: Util.duplicateElement,
          caption: 'Copy <item />',
        },
        {
          action: Util.moveElementUp,
          caption: 'Move <item /> up',
          hideIf: (xml, id) => !Util.canMoveElementUp(xml, id),
        },
        {
          action: Util.moveElementDown,
          caption: 'Move <item /> down',
          hideIf: (xml, id) => !Util.canMoveElementDown(xml, id),
        },
      ],
    },
  },
};

コンポーネントの実装

あとは淡々とコンポーネントを実装するだけです。
ここはパッケージのDemoと大きく変更しています。
Demoの方ではClassで定義していますが、別に使い回す予定もないのでFunctionで定義しました。


// 初期XML文字列を定義します。
const xml =
'<list><item label="one" type="short">text 1</item><item label="two">text 2</item><!-- ABC --></list>';
// Appコンポーネントを定義します。
export default function App() {
  // XmlEditorコンポーネントへの参照と、エディタから取得したXMLの状態を管理するためのフック
  const ref = useRef<XmlEditor>(null);
  const [xmlState, setXmlState] = useState('');

  // Harvestボタンがクリックされた時に実行される関数
  const onClickHarvest = () => {
    if (ref.current) {
      const builder = new Builder({});
      const xml = ref.current.getXml();
      if (xml) {
        setXmlState(builder.buildObject(xml)); // 取得したXMLを状態に設定
      }
    }
  };

  // コンポーネントのUIをレンダリング
  return (
    <>
      <div>
        <XmlEditor docSpec={docSpec} ref={ref} xml={xml} /> {/* XMLエディタ */}
      </div>
      <div>
        <button onClick={onClickHarvest}>Harvest</button> {/* Harvestボタン */}
      </div>

      <div>
        <pre>{xmlState}</pre> {/* 編集後のXMLを表示 */}
      </div>
    </>
  );
}

コード全文

Reactと言いつつNext.jsで実装したので余計なものも含まれていますがご容赦ください

// 'use client'指令は、このコンポーネントがクライアントサイドでのみ実行されることを示します。
'use client';

// Reactとそのフック、useStateとuseRefをインポートします。
import React, { useState, useRef } from 'react';
// react-xml-editorから必要なモジュールをインポートします。
import { Builder, Util, XmlEditor } from 'react-xml-editor';
// DocSpecの型定義をインポートします。
import { DocSpec } from 'react-xml-editor/lib/src/types';
// XMLエディタのスタイルシートをインポートします。
import 'react-xml-editor/css/xonomy.css';


// XMLドキュメントの仕様を定義します。これによりXMLエディタの振る舞いをカスタマイズできます。
const docSpec: DocSpec = {
  elements: {
    item: {
      attributes: {
        label: {
          asker: Util.askString,
          menu: [
            {
              action: Util.deleteAttribute,
              caption: 'Delete attribute',
            },
          ],
        },
        type: {
          asker: Util.askPicklist([
            {
              value: 'short',
              caption: 'short',
            },
            {
              value: 'medium',
              caption: 'medium',
            },
            'long',
          ]),
        },
      },
      menu: [
        {
          action: Util.newElementChild('<child />'),
          caption: 'Append child <child />',
        },
        {
          action: Util.newAttribute({
            name: 'label',
            value: 'default value',
          }),
          caption: 'Add attribute @label',
          hideIf: (xml, id) => {
            const element = Util.getXmlNode(xml, id);
            return (
              element && element.$ && typeof element.$.label !== 'undefined'
            );
          },
        },
        {
          action: Util.deleteElement,
          caption: 'Delete this <item />',
          icon: 'exclamation.png',
        },
        {
          action: Util.newElementBefore('<item />'),
          caption: 'New <item /> before this',
        },
        {
          action: Util.newElementAfter('<item />'),
          caption: 'New <item /> after this',
        },
        {
          action: Util.duplicateElement,
          caption: 'Copy <item />',
        },
        {
          action: Util.moveElementUp,
          caption: 'Move <item /> up',
          hideIf: (xml, id) => !Util.canMoveElementUp(xml, id),
        },
        {
          action: Util.moveElementDown,
          caption: 'Move <item /> down',
          hideIf: (xml, id) => !Util.canMoveElementDown(xml, id),
        },
      ],
    },
  },
};

// 初期XML文字列を定義します。
const xml =
'<list><item label="one" type="short">text 1</item><item label="two">text 2</item><!-- ABC --></list>';
// Appコンポーネントを定義します。
export default function App() {
  // XmlEditorコンポーネントへの参照と、エディタから取得したXMLの状態を管理するためのフック
  const ref = useRef<XmlEditor>(null);
  const [xmlState, setXmlState] = useState('');

  // Harvestボタンがクリックされた時に実行される関数
  const onClickHarvest = () => {
    if (ref.current) {
      const builder = new Builder({});
      const xml = ref.current.getXml();
      if (xml) {
        setXmlState(builder.buildObject(xml)); // 取得したXMLを状態に設定
      }
    }
  };

  // コンポーネントのUIをレンダリング
  return (
    <>
      <div>
        <XmlEditor docSpec={docSpec} ref={ref} xml={xml} /> {/* XMLエディタ */}
      </div>
      <div>
        <button onClick={onClickHarvest}>Harvest</button> {/* Harvestボタン */}
      </div>

      <div>
        <pre>{xmlState}</pre> {/* 編集後のXMLを表示 */}
      </div>
    </>
  );
}

Discussion