JavaScriptでフラットなオブジェクト配列をツリー構造に変換する方法
はじめに
Web開発では、カテゴリ一覧やフォルダ構成、組織図など、階層構造をもつデータを画面に表示したい場面がよくあります。一方で、データベースやAPIから取得したデータは、idとparentIdを持つフラット(一次元)な配列として渡されることがほとんどです。
本記事では、こうしたフラットな配列をもとに、子要素を親オブジェクトへリンクさせたツリー構造を組み立て、ネストされたリスト形式で画面に表示するまでの手順を、HTML・CSSのコード付きでわかりやすく解説します。
元データとなるフラットな配列
まず、変換対象となるデータを確認しましょう。各オブジェクトは、自身の一意な識別子であるid、表示名のname、そして親要素を指すparentId(ルート要素の場合はnull)というプロパティを持っています。
const arr = [
{ id: '1', name: 'name 1', parentId: null },
{ id: '2', name: 'name 2', parentId: null },
{ id: '2_1', name: 'name 2_1', parentId: '2' },
{ id: '2_2', name: 'name 2_2', parentId: '2' },
{ id: '3', name: 'name 3', parentId: null },
{ id: '4', name: 'name 4', parentId: null },
{ id: '5', name: 'name 5', parentId: null },
{ id: '6', name: 'name 6', parentId: null },
{ id: '7', name: 'name 7', parentId: null },
{ id: '1_1', name: 'name 1_1', parentId: '1' },
{ id: '1_2', name: 'name 1_2', parentId: '1' },
{ id: '1_3', name: 'name 1_3', parentId: '1' },
{ id: '1_4', name: 'name 1_4', parentId: '1' },
{ id: '1_5', name: 'name 1_5', parentId: '1' },
{ id: '2_1_1', name: 'name 2_1_1', parentId: '2_1' },
{ id: '2_1_2', name: 'name 2_1_2', parentId: '2_1' },
{ id: '2_1_3', name: 'name 2_1_3', parentId: '2_1' },
{ id: '2_1_4', name: 'name 2_1_4', parentId: '2_1' },
{ id: '2_1_5', name: 'name 2_1_5', parentId: '2_1' },
];この配列では、たとえばidが2_1の要素はparentIdが'2'であるため、id2の要素の子になります。さらに2_1_1のように孫にあたる要素も存在しており、最大で3階層のツリーが構築されることになります。
目標:ツリー構造への変換とネスト表示
今回作成するのは、次のような処理を実現するコードです。
- 配列内の各オブジェクトを走査し、
parentIdがnullの要素をルートノードとして配置する parentIdを持つ要素は、対応する親要素の下にネストして配置する- 親ノードをクリックすると、子要素の表示・非表示を切り替えられるインタラクティブなUIを実装する
実装コード
HTML
ツリーを描画するためのコンテナを用意します。
<html>
<head></head>
<body>
<div id = "tree">
</div>
</body>
</html>JavaScript
続いて、メインとなるロジックです。
const arr = [
{ id: '1', name: 'name 1', parentId: null },
{ id: '2', name: 'name 2', parentId: null },
{ id: '2_1', name: 'name 2_1', parentId: '2' },
{ id: '2_2', name: 'name 2_2', parentId: '2' },
{ id: '3', name: 'name 3', parentId: null },
{ id: '4', name: 'name 4', parentId: null },
{ id: '5', name: 'name 5', parentId: null },
{ id: '6', name: 'name 6', parentId: null },
{ id: '7', name: 'name 7', parentId: null },
{ id: '1_1', name: 'name 1_1', parentId: '1' },
{ id: '1_2', name: 'name 1_2', parentId: '1' },
{ id: '1_3', name: 'name 1_3', parentId: '1' },
{ id: '1_4', name: 'name 1_4', parentId: '1' },
{ id: '1_5', name: 'name 1_5', parentId: '1' },
{ id: '2_1_1', name: 'name 2_1_1', parentId: '2_1' },
{ id: '2_1_2', name: 'name 2_1_2', parentId: '2_1' },
{ id: '2_1_3', name: 'name 2_1_3', parentId: '2_1' },
{ id: '2_1_4', name: 'name 2_1_4', parentId: '2_1' },
{ id: '2_1_5', name: 'name 2_1_5', parentId: '2_1' },
]
const tree = document.getElementById("tree")
arr.forEach(a => {
const div = document.createElement("div")
if (a.parentId === null) {
div.classList.add("parent")
div.setAttribute("id", `id${a.id}`)
div.innerText = a.name
tree.appendChild(div)
}
else {
const parent = document.getElementById(`id${a.parentId}`)
if (!parent.classList.contains("parent")) {
parent.classList.add("parent")
}
console.log(`${a.id} `, parent)
let childContainer = null
if (parent.childElementCount === 0) {
childContainer = document.createElement("div")
childContainer.classList.add("childContainer")
parent.appendChild(childContainer)
childContainer.classList.add("hidden")
}
else {
childContainer = document.querySelector(`#id${a.parentId}
.childContainer`)
}
div.classList.add("child")
div.setAttribute("id", `id${a.id}`)
div.innerText = a.name
childContainer.appendChild(div)
}
})
const parents =
Array.from(document.getElementsByClassName("parent")).filter(p =>
p.childElementCount !== 0)
parents.forEach(p => {
p.addEventListener("click", function(e) {
e.preventDefault()
const container = this.getElementsByClassName("childContainer")[0]
container.classList.toggle("visible")
e.stopPropagation()
})
})処理の流れを整理すると、次のようになっています。
- ルート要素の配置:
parentId === nullの要素にはparentクラスを付与し、そのまま#treeコンテナに追加します。 - 子要素の配置:親要素のDOMを
document.getElementById()で取得し、まだ子要素を格納するコンテナ(.childContainer)が存在しない場合は新しく作成します。その後、自分自身にchildクラスと一意なIDを付けて、その子コンテナへ追加します。 - 開閉機能の実装:子要素を1つでも持つ親ノードすべてにクリックイベントを登録し、クリックのたびに
visibleクラスをトグルすることで、子要素の表示・非表示を切り替えます。
CSS
最後に、見た目を整えるためのスタイルです。
.parent, .child {
cursor: pointer;
}
.parent {
margin: 10px 0;
font-size: 1.3rem;
}
.parent::before {
content: "\25BA";
margin-right: 10px;
}
.childContainer {
margin-left: 20px;
display: none;
}
.childContainer.visible {
display: block;
}
.child {
font-size: 1rem;
}
.child::before {
content: "\25BA";
}ポイントは次の2点です。
.parent::beforeと.child::beforeのcontentプロパティによって、▶(右向き三角)アイコンを表示し、ツリーらしい見た目を演出しています。.childContainerは初期状態でdisplay: noneにより非表示になっており、visibleクラスが付与されたときだけdisplay: blockで展開されます。
実行結果
ブラウザで表示すると、ルートノードが縦に並び、それぞれをクリックすると子ノードがアコーディオン式に開閉するツリー構造が完成します。

-
JavaScriptのオブジェクト配列に配列メソッドを適用する方法
JavaScriptでは、オブジェクトが格納された配列に対しても、通常の配列と同じようにpop()、push()、splice()などの標準的な配列メソッドをそのまま使用できます。オブジェクト配列はあくまで「配列」であるため、要素としてオブジェクトが入っていても配列操作のAPIは共通で動作します。 コード例 以下は、JavaScriptオブジェクトの配列に対して配列メソッドを使用するサンプルコードです。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8
-
JavaScriptで配列内オブジェクトのプロパティ名を置き換える方法
JavaScriptでオブジェクト配列のプロパティ名を置き換える方法 JavaScriptでは、map()メソッドを活用することで、配列に含まれる各オブジェクトのプロパティ名を別の名前に簡単に置き換えることができます。map()は元の配列を変更せずに新しい配列を生成するため、安全かつ効率的なデータ変換処理に適しています。 以下は、オブジェクト配列のプロパティ名 name を fullName に置き換えるサンプルコードです。 サンプルコード <!DOCTYPE html> <html lang="en"> <head> <meta