#app
script#multi-selection(type="text/x-template")
.multi-selection
template(v-for="option in cmpOptions")
.multi-selection__item(
:class="{\
'-selected': option.isSelected,\
'-selecting': option.isSelecting,\
'-delete': option.isDelete,\
}"
@click="onClick(option)"
)
| {{ option.text }}
script#vue-app(type="text/x-template")
div
div 複数選択の状態管理
.block
.block__title MultiSelection1
.block__content
div
button(@click="multiSelection.onCancel") cancel
button(@click="multiSelection.onConfirm") confirm
MultiSelection(
:selectedIds="multiSelection.state.selectedIds"
:selectingIds="multiSelection.state.selectingIds"
:deleteIds="multiSelection.state.deleteIds"
:options="OPTIONS"
@select="multiSelection.onSelect"
)
div selectedIds: {{ multiSelection.state.selectedIds }}
div selectingIds: {{ multiSelection.state.selectingIds }}
div deleteIds: {{ multiSelection.state.deleteIds }}
.block
.block__title MultiSelection2
.block__content
div
button(@click="multiSelection2.onCancel") cancel
button(@click="multiSelection2.onConfirm") confirm
MultiSelection(
:selectedIds="multiSelection2.state.selectedIds"
:selectingIds="multiSelection2.state.selectingIds"
:deleteIds="multiSelection2.state.deleteIds"
:options="OPTIONS"
@select="multiSelection2.onSelect"
)
div selectedIds: {{ multiSelection2.state.selectedIds }}
div selectingIds: {{ multiSelection2.state.selectingIds }}
div deleteIds: {{ multiSelection2.state.deleteIds }}
View Compiled
* {
box-sizing: border-box;
}
.block {
padding: 10px;
border: solid 1px #000;
& + & {
margin-top: 10px;
}
&__title {
font-weight: bold;
}
&__content {
margin-top: 5px;
}
}
.multi-selection {
display: flex;
padding: 5px 0;
&__item {
border: solid 1px #ccc;
padding: 5px 15px;
border-radius: 5px;
cursor: pointer;
&.-selected {
background-color: #cfc;
}
&.-selecting {
color: #fff;
background-color: #090;
}
&.-delete {
background-color: #eee;
border-style: dashed;
}
& + & {
margin-left: 10px;
}
}
}
View Compiled
const { ref, reactive, computed } = Vue;
const OPTIONS = [
{ id: 'A', text: 'A' },
{ id: 'B', text: 'B' },
{ id: 'C', text: 'C' },
{ id: 'D', text: 'D' },
{ id: 'E', text: 'E' }
];
/**
* 複数選択の状態を管理するモジュール
*/
function useMultiSelection(initialSelectedIds = []) {
const state = reactive({
selectedIds: initialSelectedIds,
selectingIds: [],
deleteIds: [],
});
return {
state,
onSelect: (option) => {
{
// 選択中の場合は選択を外す
const index = state.selectingIds.findIndex((id) => id === option.id);
if (index !== -1) {
state.selectingIds.splice(index, 1);
return;
}
}
{
// 削除中の場合は削除リストから外す
const index = state.deleteIds.findIndex((id) => id === option.id);
if (index !== -1) {
state.deleteIds.splice(index, 1);
return;
}
}
{
// 選択済みの項目なら削除リストに追加する
const index = state.selectedIds.findIndex((id) => id === option.id);
if (index !== -1) {
state.deleteIds.push(option.id);
return;
}
}
// それ以外は選択中として登録
state.selectingIds.push(option.id);
},
onCancel: () => {
state.selectingIds = [];
state.deleteIds = [];
},
onConfirm: () => {
const addedIds = _.union(state.selectedIds, state.selectingIds);
const excludedIds = _.difference(addedIds, state.deleteIds);
state.selectedIds = excludedIds;
state.selectingIds = [];
state.deleteIds = [];
},
};
}
const app = Vue.createApp({
template: '#vue-app',
setup() {
const multiSelection = useMultiSelection();
const multiSelection2 = useMultiSelection([OPTIONS[0].id, OPTIONS[1].id]);
return {
OPTIONS,
multiSelection,
multiSelection2,
};
},
});
app.component('MultiSelection', {
template: '#multi-selection',
emits: {
select: (option) => {
return option != null;
},
},
props: {
selectedIds: { type: Array },
selectingIds: { type: Array },
deleteIds: { type: Array },
options: { type: Array },
},
setup(props, context) {
const cmpOptions = computed(() => {
return props.options.map((option) => {
const isSelected = props.selectedIds.includes(option.id);
const isSelecting = props.selectingIds.includes(option.id);
const isDelete = props.deleteIds.includes(option.id);
return {
option,
isSelected: !isDelete && isSelected,
isSelecting,
isDelete,
};
});
});
return {
cmpOptions,
onClick: (option) => {
context.emit('select', option);
},
};
},
});
app.mount('#app');
This Pen doesn't use any external CSS resources.