HTML
JavaScript

JavaScriptの連想配列をツリーメニューに変換し表示・展開する


これはなに?

下記のような、イベントハンドラとなる関数が登録された連想配列を、ツリーメニューに変換し表示・展開できるようにします。

const treeMenu = {}

treeMenu['menu1'] = (event) => outputToLog('menu1')
treeMenu['menu2'] = {}
treeMenu['menu2']['menu2-1'] = (event) => outputToLog('menu2-1')
treeMenu['menu2']['menu2-2'] = (event) => outputToLog('menu2-2')
treeMenu['menu2']['menu2-3'] = (event) => outputToLog('menu2-3')
treeMenu['menu3'] = (event) => outputToLog('menu3')

(event) => outputToLog()は、暫定的なイベントハンドラです。実際に使用するときは、各メニュー選択時に実行したい関数を登録してください。

実際にツリーメニューが展開されると、下記のように<ul class='menu'>を使ったHTMLが展開されます。したがって、実用時はこれをCSSで装飾して見栄えを整える必要があると思います。

<!-- '≡'をクリックするまでは折り畳まれている -->

<ul class='menu'>
<li>menu1</li>
<li>menu2</li>
<!-- 'menu2'をクリックするまでは折り畳まれている -->
<ul class='menu'>
<li>menu2-1</li>
<li>menu2-2</li>
<li>menu2-3</li>
</ul>
<li>menu3</li>
</ul>


サンプル


See the Pen
easy_tree_menu
by THANKS-JP (@thanks-jp)
on CodePen.


作り方

上記プログラムの作り方です。


ツリーメニュー表示用のモジュールを作る

ツリーメニューの要件は以下のように定義しました。


  • ≡をクリックすると、最上階層のメニューが表示される

  • イベントハンドラが登録された項目をクリックすると、イベントハンドラが実行される

  • サブメニューの連想配列が登録された項目をクリックすると、サブメニューが展開される

  • 展開されたメニューの親項目をクリックすると、そのメニューは収納される

上記要件を実現するために、次のようなモジュールを作ります。


menu.js

"use strict"

/**
* ツリー型メニューを開くイベントハンドラを作成する関数を作成します。
*
* @param 'string' className (required) ULタグのクラス名
* @return 'function'
*/

const makeMakerOfOpenMenuHandler = (className) => {

/**
* クリックされた要素の子メニューを閉じるイベントハンドラを作成します。
*
* @param 'function' handler (required) 子メニューを閉じた後、再び開くためのイベントハンドラ
* @return 'function'
*/

const makeCloseMenuHandler = (handler) => {
const closeMenu = (event) => {
const element = event.currentTarget
element.parentNode.removeChild(element.nextSibling)
element.removeEventListener('click', closeMenu)
element.addEventListener('click', handler)
}
return closeMenu
}

/**
* クリックされた要素のイベントハンドラを実行して、ツリーメニュー全体を閉じるイベントハンドラを作成します。
*
* @param 'function' handler (required) クリックされた要素のイベントハンドラ
*/

const makeExecuteElementHandler = (handler) => (event) => {
const element = event.currentTarget
let parentNode = element.parentNode
while (parentNode) {
const nextNode = parentNode.parentNode
if (parentNode.nodeName === 'UL' && parentNode.getAttribute('class') === className) {
parentNode.previousSibling.click()
}
parentNode = nextNode
}
handler(event)
}

/**
* ULタグに、イベントハンドラを持つ項目を追加します。
*
* @param 'HTMLElement' ul (required) 項目を追加するULタグ
* @param 'string' name (required) 項目の表示名
* @param 'function' handler (required) 項目に対応したイベントハンドラ
*/

const addItem = (ul, name, handler) => {
const li = document.createElement('li')
li.innerHTML = name
li.addEventListener('click', makeExecuteElementHandler(handler))
ul.appendChild(li)
}

/**
* ULタグに、サブメニューを持つ項目を追加します。
*
* @param 'HTMLElement' ul (required) 項目を追加するULタグ
* @param 'string' name (required) 項目の表示名
* @param 'object' menuStruct (required) 項目に対応したサブメニュー構造連想配列
*/

const addMenuItem = (ul, name, menuStruct) => {
const li = document.createElement('li')
li.innerHTML = name
li.addEventListener('click', makeOpenMenuHandler(menuStruct))
ul.appendChild(li)
}

/**
* 指定したHTMLElement直後に、指定したメニュー構造を持つツリーメニューを展開します。
*
* @param 'HTMLELement' element (required) メニューを展開する直前のHTML要素
* @param 'object' menuStruct (required) 展開するメニューの構造を表す連想配列
*/

const openMenu = (element, menuStruct) => {
const ul = document.createElement('ul')
ul.setAttribute('class', className)
for (const menu in menuStruct) {
const item = menuStruct[menu]
if (typeof item === 'function') {
addItem(ul, menu, item)
} else if (typeof item === 'object') {
addMenuItem(ul, menu, item)
}
}
element.parentNode.insertBefore(ul, element.nextSibling)
}

/**
* 指定したメニュー構造を持つツリーメニューを展開するイベントハンドラを作成します。
*
* @param 'object' menuStruct (required) 展開されるメニューの構造を表す連想配列
* @return 'function'
*/

const makeOpenMenuHandler = (menuStruct) => {
const openMenuHandler = (event) => {
const element = event.currentTarget
element.removeEventListener('click', openMenuHandler)
element.addEventListener('click', makeCloseMenuHandler(openMenuHandler))
openMenu(element, menuStruct)
}
return openMenuHandler
}

return makeOpenMenuHandler
}

// makeMakerOfOpenMenuHandlerをエクスポートします。
export default makeMakerOfOpenMenuHandler



モジュールの使い方

makeMakerOfOpenMenuHandler()でクラス名を指定して、ツリーメニューを展開するためのイベントハンドラを作成する関数を作成します。ここで指定したクラス名は、<ul class='menu'>'menu'部分に反映されるので、好きな名前を設定してください。

import makeMakerOfOpenMenuHandler from 'menu.js'

const makeOpenMenuHandler = makeMakerOfOpenMenuHandler('menu')

そして、makeOpenMenuHandler()で展開するツリーメニューの構造を表す連想配列を指定して、ツリーメニューを展開するためのイベントハンドラを作成します。

const openMenu = makeOpenMenuHandler(treeMenu)

そして、作成したイベントハンドラを、<p id='menu'>'click'イベントに登録すると、<p id='menu'>をクリックしたときに、treeMenuで指定した構造を持つツリーメニューが展開されるようになります。

const menu = document.getElementById('menu')

menu.addEventListener('click', openMenu)


テストページを作る

では、実際に上記モジュールを使用してテストページを作ってみましょう。menu.jsは、ツリーメニューを実際に表示するためのモジュールです。


test.html

<!DOCTYPE html>

<html>
<head>
<meta charset="utf-8">
<title>テスト</title>
<script type='module'>

// テキストエリアに文字列を挿入する暫定的なイベントハンドラ
const outputToLog = (text) => {
const log = document.getElementById('log')
log.innerHTML += text + '\n'
}

// ツリーメニューの構造を表す暫定的な連想配列
const treeMenu = {}
treeMenu['menu1'] = (event) => outputToLog('menu1')
treeMenu['menu2'] = {}
treeMenu['menu2']['menu2-1'] = (event) => outputToLog('menu2-1')
treeMenu['menu2']['menu2-2'] = (event) => outputToLog('menu2-2')
treeMenu['menu2']['menu2-3'] = (event) => outputToLog('menu2-3')
treeMenu['menu3'] = (event) => outputToLog('menu3')

// menu.jsをインポートし、クラス名'menu'で
// ツリーメニューを展開するイベントハンドラを作成する関数を作成
import makeMakerOfOpenMenuHandler from 'menu.js'
const makeOpenMenuHandler = makeMakerOfOpenMenuHandler('menu')

// DOMの読み込み完了時、ツリーメニューを展開するイベントハンドラを作成し、
// id='menu'の'click'イベントに追加する
document.addEventListener('DOMContentLoaded', (event) => {
const openMenu = makeOpenMenuHandler(treeMenu)
const menu = document.getElementById('menu')
menu.addEventListener('click', openMenu)
})
</script>
</head>
<body>

<!-- メニューを展開する元となる要素 -->
<p id='menu'></p>

<!-- ログを出力するテキストエリア -->
<textarea id='log'></textarea>
</body>
</html>



余談

JavaScriptは楽しい! え? こんなの〇〇っていうフレームワークを使えば簡単にできるって? いいのさ! 楽しければ!