例えば次の様な木構造のデータがあります。これは PHP のフレームワークである Laravel 内の一部分のファイルとディレクトリです。
Mail
├── Attachment.php
├── Events
│ ├── MessageSending.php
│ └── MessageSent.php
├── LICENSE.md
├── MailManager.php
├── MailServiceProvider.php
├── Mailable.php
├── Mailer.php
├── Markdown.php
├── Message.php
├── PendingMail.php
├── SendQueuedMailable.php
├── SentMessage.php
├── Transport
│ ├── ArrayTransport.php
│ ├── LogTransport.php
│ └── SesTransport.php
├── composer.json
└── resources
└── views
├── html
│ ├── button.blade.php
│ ├── footer.blade.php
│ ├── header.blade.php
│ ├── layout.blade.php
│ ├── message.blade.php
│ ├── panel.blade.php
│ ├── subcopy.blade.php
│ ├── table.blade.php
│ └── themes
│ └── default.css
└── text
├── button.blade.php
├── footer.blade.php
├── header.blade.php
├── layout.blade.php
├── message.blade.php
├── panel.blade.php
├── subcopy.blade.php
└── table.blade.php
この木構造を HTML 上で表現することを考えます。あるディレクトリの中にはファイル名、もしくはディレクトリが任意に並ぶ感じです。TypeScriptの型にするなら次の様に表せます。
type File = {
name: string;
};
type Directory = {
name: string;
children: (File | Directory)[];
};
まずはこれを素の HTML と CSS で表示することを考えます。これは例えば次の様にできます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ディレクトリ</title>
<style>
.directory, .file {
border: solid 1px black;
}
.name {
margin: 0 5px;
}
.directory {
display: grid;
width: fit-content;
width: -moz-fit-content;
grid-template-columns: fit-content(100%) 1fr;
}
</style>
</head>
<body>
<div id="demo-plane-html-root">
<div class="directory">
<span class="name">Mail</span>
<div class="children">
<div class="file">
<span class="name">Attachment.php</span>
</div>
<div class="directory">
<span class="name">Events</span>
<div class="children">
<div class="file">
<span class="name">MessageSending.php</span>
</div>
<div class="file">
<span class="name">MessageSent.php</span>
</div>
</div>
</div>
<div class="file">
<span class="name">LICENSE.md</span>
</div>
</div>
</div>
</div>
</body>
</html>
XMLでしばしば見る階層構造そのままのHTMLです。これをデータ部である name と更なるネスト部である children に分けて、それぞれについてグリッドの幅を指定することで木構造らしい見た目にしています。
実際にプログラム中でデータを取り扱う時は定数的でなく、都度解釈して適切にデザインを割り当てる必要があります。これを React 上で行うと次の様になります。
ソースコード
.directory, .file {
border: solid 1px black;
}
.name {
margin: 0 5px;
}
.directory {
display: grid;
width: fit-content;
width: -moz-fit-content;
grid-template-columns: fit-content(100%) 1fr;
}
import React from 'react';
import ReactDOM from 'react-dom';
import './styles.css';
// 今回取り扱うデータ構造のまとめ
type File = {
name: string;
};
type Directory = {
name: string;
children: (File | Directory)[];
};
const isDir = (fd: File | Directory): fd is Directory => 'children' in fd;
/** 木構造データ、実際はAPIなどから取得することが多い */
const MailDirectory: Directory = {
name: 'Mail',
children: [
{ name: 'Attachment.php' },
{
name: 'Events',
children: [{ name: 'MessageSending.php' }, { name: 'MessageSent.php' }],
},
{ name: 'LICENSE.md' },
{ name: 'MailManager.php' },
{ name: 'MailServiceProvider.php' },
{ name: 'Mailable.php' },
{ name: 'Mailer.php' },
{ name: 'Markdown.php' },
{ name: 'Message.php' },
{ name: 'PendingMail.php' },
{ name: 'SendQueuedMailable.php' },
{ name: 'SentMessage.php' },
{
name: 'Transport',
children: [{ name: 'ArrayTransport.php' }, { name: 'LogTransport.php' }, { name: 'SesTransport.php' }],
},
{ name: 'composer.json' },
{
name: 'resources',
children: [
{
name: 'views',
children: [
{
name: 'html',
children: [
{ name: 'button.blade.php' },
{ name: 'footer.blade.php' },
{ name: 'header.blade.php' },
{ name: 'layout.blade.php' },
{ name: 'message.blade.php' },
{ name: 'panel.blade.php' },
{ name: 'subcopy.blade.php' },
{ name: 'table.blade.php' },
{ name: 'themes', children: [{ name: 'default.css' }] },
],
},
{
name: 'html',
children: [
{ name: 'button.blade.php' },
{ name: 'footer.blade.php' },
{ name: 'header.blade.php' },
{ name: 'layout.blade.php' },
{ name: 'message.blade.php' },
{ name: 'panel.blade.php' },
{ name: 'subcopy.blade.php' },
{ name: 'table.blade.php' },
],
},
],
},
],
},
],
};
/** ファイル表示用コンポーネント。単に名前のみを表示する */
const FileDisplay: React.FC<{ file: File }> = ({ file }) => {
return (
<div className="file">
<span className="name">{file.name}</span>
</div>
);
};
/** ディレクトリ表示用コンポーネント。名前のみとディレクトリ内にあるファイルやディレクトリを表示する */
const DirectoryDisplay: React.FC<{ directory: Directory }> = ({ directory }) => {
return (
<div className="directory">
{/* 名前の表示 */}
<span className="name">{directory.name}</span>
<div className="children">
{/* ディレクトリ内にあるファイルやディレクトリを一通り回す */}
{directory.children.map((fOrD) =>
isDir(fOrD) ? (
// ディレクトリ内のディレクトリで更に続く場合、再帰する
<DirectoryDisplay key={fOrD.name} directory={fOrD} />
) : (
// ファイルならファイルの方を表示
<FileDisplay key={fOrD.name} file={fOrD} />
)
)}
</div>
</div>
);
};
const App = () => {
return <DirectoryDisplay directory={MailDirectory} />;
};
ReactDOM.render(<App />, document.getElementById('root'));
再帰で自身をコンポーネントとして呼び出すのが肝です。木構造なので正しく辿れていれば特に工夫もなくループはストップします。これによりコードの全長は短く済み、再帰がわかれば理解も早く済みます。