Created
June 29, 2025 06:04
-
-
Save skysan87/8590c603dee05870f9769cb020ba3812 to your computer and use it in GitHub Desktop.
[Vue.js] ツリー形式のチェックボックスグループを操作するサンプル
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="ja"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>tree-style-checkbox-group</title> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "Vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js" | |
| } | |
| } | |
| </script> | |
| </head> | |
| <body> | |
| <div id="app"> | |
| <div v-for="(row) in rows" :key="row.id"> | |
| <label :style="`padding-left:${row.level * 20}px;`"> | |
| <input type="checkbox" :checked="row.checked" :value="row.value" @change="handleCheck(row.id)" /> | |
| {{ row.title }} | |
| </label> | |
| </div> | |
| </div> | |
| </body> | |
| <!-- vue.js --> | |
| <script type="module"> | |
| import { createApp, ref, watch } from 'Vue' | |
| /** | |
| * @typedef {object} treeItem 一次元化したツリー要素 | |
| * @property {string} id ツリー内の識別番号 | |
| * @property {string} parentId 親要素Id | |
| * @property {string} title 表示ラベル | |
| * @property {string} value 値 | |
| * @property {boolean} checked チェック状態 | |
| * @property {number} level ツリーの深さ | |
| * @property {boolean} hasChildren 子要素有無 | |
| */ | |
| const TREE_DATA = [ | |
| { | |
| title: 'group1', | |
| children: [ | |
| { title: 'title1-1', value: 'value1_1' }, | |
| { title: 'title1-2', value: 'value1_2' } | |
| ] | |
| }, | |
| { | |
| title: 'group2', | |
| children: [ | |
| { title: 'title2-1', value: 'value2_1' }, | |
| { | |
| title: 'sub-group-2-2', children: [ | |
| { title: 'title2-2-1', value: 'value2_2_1' }, | |
| { title: 'title2-2-2', value: 'value2_2_2' }, | |
| ] | |
| }, | |
| { title: 'title2-3', value: 'value2_3' }, | |
| ] | |
| }, | |
| { | |
| title: 'titleA', value: 'valueA' | |
| } | |
| ] | |
| createApp({ | |
| setup () { | |
| /** @type {ref<treeItem[]>} */ | |
| const rows = ref([]) | |
| /** | |
| * 再起的にツリー構造を一次元化 | |
| * @param {any[]} treeData | |
| * @param {number} parentIds | |
| * @returns {treeItem[]} | |
| */ | |
| const flattenTree = (treeData, parentIds = []) => { | |
| if (!Array.isArray(treeData)) return [] | |
| return treeData.reduce( | |
| /** | |
| * @param {treeItem[]} acc | |
| * @param {any} item | |
| * @param {number} index | |
| */ | |
| (acc, item, index) => { | |
| const hasChildren = item.children && item.children.length > 0 | |
| acc.push({ | |
| id: [...parentIds, index].join('_'), | |
| parentId: parentIds.join('_') ?? '', | |
| title: item.title, | |
| value: item.value ?? '', | |
| checked: false, | |
| level: parentIds.length, | |
| hasChildren: hasChildren | |
| }) | |
| if (hasChildren) { | |
| acc.push(...flattenTree(item.children, [...parentIds, index])) | |
| } | |
| return acc | |
| }, []) | |
| } | |
| /** | |
| * 再帰的に子要素を全てチェックする | |
| * @param {treeItem} row | |
| */ | |
| const checkChildren = (row) => { | |
| /** @type {treeItem[]} */ | |
| const children = rows.value.filter(r => r.parentId === row.id) | |
| children.forEach(r => { | |
| r.checked = true | |
| checkChildren(r) | |
| }) | |
| } | |
| /** | |
| * 再帰的に同階層がすべてチェックされていれば、親要素をチェックする | |
| * @param {treeItem} row | |
| */ | |
| const checkAncestor = (row) => { | |
| /** @type {treeItem[]} */ | |
| const samegroup = rows.value.filter(r => r.parentId === row.parentId) | |
| if (samegroup.every(r => r.checked)) { | |
| /** @type {treeItem} */ | |
| const parent = rows.value.find(r => r.id === row.parentId) | |
| if (parent) { | |
| parent.checked = true | |
| checkAncestor(parent) | |
| } | |
| } | |
| } | |
| /** | |
| * 再帰的に子要素を全てチェック解除する | |
| * @param {treeItem} row | |
| */ | |
| const uncheckChildren = (row) => { | |
| /** @type {treeItem[]} */ | |
| const children = rows.value.filter(r => r.parentId === row.id) | |
| children.forEach(r => { | |
| r.checked = false | |
| uncheckChildren(r) | |
| }) | |
| } | |
| /** | |
| * 再帰的に親要素のチェックを解除する | |
| * @param {treeItem} row | |
| */ | |
| const uncheckAncestor = (row) => { | |
| /** @type {treeItem} */ | |
| const parent = rows.value.find(r => r.id === row.parentId) | |
| if (parent) { | |
| parent.checked = false | |
| uncheckAncestor(parent) | |
| } | |
| } | |
| /** | |
| * チェックイベント処理 | |
| * @param {string} id | |
| */ | |
| const handleCheck = (id) => { | |
| /** @type {treeItem} */ | |
| const row = rows.value.find(row => row.id === id) | |
| if (!row) return | |
| row.checked = !row.checked | |
| // チェックされた要素に合わせて、子要素と親要素も連動してチェック状態を更新する | |
| if (row.checked) { | |
| checkChildren(row) | |
| checkAncestor(row) | |
| } else { | |
| uncheckChildren(row) | |
| uncheckAncestor(row) | |
| } | |
| } | |
| rows.value = flattenTree(TREE_DATA) | |
| return { | |
| rows, handleCheck | |
| } | |
| } | |
| }).mount('#app') | |
| </script> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment