🦊
ReactでXML editorを実装する
react-xml-editorを使ってXML editorを実装する
完成イメージ
Reactにこんな感じのXMLエディターを実装します。
上段が読み込んだXMLを階層別に表示して、適宜属性等を編集できる領域
その下のボタンは上段で編集した階層構造をXMLとして下段に出力するもの
react-xml-editor
今回はreact-xml-editorというパッケージを使っていきます。
リポジトリ
インストール
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