Skip to content

Commit

Permalink
[#724][3.0] Tabs 컴포넌트 개발
Browse files Browse the repository at this point in the history
###############################
- Tab의 리스트와 선택된 탭의 value값을 Tabs 태그에서 양방향 바인딩 v-model을 걸어서 렌더링하도록 로직을 변경
- Docs > Tabs 페이지에 탭 추가, 삭제하는 예제 추가
- 컴포넌트가 포함되어있는 배열을 shallowRef로 감싸고 이벤트가 발생한 경우 triggerRef를 실행하여 반응형을 트리거 시키도록 하는 예제 추가 -> defineAsyncComponent 관련 예제로직을 참고하여 다른 방식이 있는지 확인 중
- 탭 목록에서 하나가 삭제된 경우 onBeforeUpdate 훅에서 현재 렌더링된 배열의 메뉴들과 이미 삭제된 탭리스트 간의 비교를 통해 현재 선택된 탭이 삭제되는 경우에 맨 첫번째 탭은 그 다음탭으로, 그 외에는 삭제되는 탭 바로 앞의 인덱스를 선택하도록 함
- 불필요한 코드 삭제 및 모듈 리펙토링
  • Loading branch information
kdeun1 committed Nov 26, 2020
1 parent 2972a17 commit f1a141f
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 60 deletions.
58 changes: 42 additions & 16 deletions docs/views/tab/example/Default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
<p class="case-title">Common</p>
<div class="tab-wrapper">
<ev-tabs
v-model="activeName1"
v-model="selectedValue1"
v-model:panels="tabPanels1"
>
<ev-tab-panel
v-for="(item, idx) in tabPanels1"
Expand All @@ -22,15 +23,29 @@
>
addItem
</button>
&nbsp;&nbsp;&nbsp;
<button
class="btn"
@click="popItem1"
>
popItem
</button>
&nbsp;&nbsp;&nbsp;
<button
class="btn"
@click="spliceItem1"
>
spliceItem1
</button>
</div>
</div>
<div class="case">
<p class="case-title">Common</p>
<div class="tab-wrapper">
<ev-tabs
v-model="activeName2"
v-model="selectedValue2"
v-model:panels="tabPanels2"
>
tabPanels2 : {{ tabPanels2 }}
<ev-tab-panel
v-for="(item, idx) in tabPanels2"
:key="`${item.name}_${idx}`"
Expand All @@ -57,11 +72,11 @@
</template>

<script>
import { ref, shallowRef, triggerRef, watchEffect, defineAsyncComponent } from 'vue';
import { ref, shallowRef, triggerRef, defineAsyncComponent } from 'vue';
export default {
setup() {
const activeName1 = ref('tabName1');
const selectedValue1 = ref('tabName2');
const tabPanels1 = ref([
{
text: 'LABEL1LABEL1LABEL1LABEL1LABEL1',
Expand Down Expand Up @@ -110,16 +125,29 @@ export default {
// content: 'content9',
// },
]);
const idx = ref(tabPanels1.value.length + 1);
const addItem1 = () => {
tabPanels1.value.push({
text: 'NEW TEXT',
value: `value${tabPanels1.value.length + 1}`,
content: `content${tabPanels1.value.length + 1}`,
value: `value${idx.value}`,
content: `content${idx.value}`,
});
idx.value++;
};
const activeName2 = ref('comp1');
const popItem1 = () => {
if (tabPanels1.value.length > 1) {
tabPanels1.value.pop();
}
};
const spliceItem1 = () => {
if (tabPanels1.value.length > 1) {
tabPanels1.value.splice(0, 1);
}
};
const selectedValue2 = ref('comp1');
const tabPanels2 = shallowRef([
{
text: 'LABEL1LABEL1LABEL1LABEL1LABEL1',
Expand All @@ -138,30 +166,28 @@ export default {
},
]);
watchEffect(() => {
console.log(tabPanels2.value);
});
const toggleComp4 = () => {
if (!tabPanels2.value.find(v => v.value === 'comp4')) {
const comp4Idx = tabPanels2.value.findIndex(v => v.value === 'comp4');
if (comp4Idx < 0) {
tabPanels2.value.push({
text: 'LABEL4',
value: 'comp4',
component: defineAsyncComponent(() => import('./Comp4.vue')),
});
} else {
const comp4Idx = tabPanels2.value.findIndex(v => v.value === 'comp4');
tabPanels2.value.splice(comp4Idx, 1);
}
triggerRef(tabPanels2);
};
return {
activeName1,
selectedValue1,
tabPanels1,
addItem1,
popItem1,
spliceItem1,
activeName2,
selectedValue2,
tabPanels2,
toggleComp4,
};
Expand Down
10 changes: 1 addition & 9 deletions src/components/tabPanel/TabPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</template>

<script>
import { reactive, computed, inject } from 'vue';
import { computed, inject } from 'vue';
export default {
name: 'EvTabPanel',
Expand All @@ -33,16 +33,8 @@ export default {
emits: {
},
setup(props) {
const tabInfo = reactive({
text: props.text,
value: props.value,
disabled: props.disabled,
});
const evTabs = inject('evTabs', null);
const mv = computed(() => evTabs.ctx.mv);
const addTab = evTabs.ctx.addTab;
addTab(tabInfo);
const isSelected = computed(() => props.value === mv.value);
return {
Expand Down
132 changes: 97 additions & 35 deletions src/components/tabs/Tabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@
<section
class="ev-tabs"
:class="{
closable,
closable: true,
}"
>
<div
class="ev-tabs-header"
>
<div class="ev-tabs-header">
<div
class="ev-tabs-nav-wrapper"
:class="{
Expand Down Expand Up @@ -61,16 +59,20 @@
</div>
</div>
</div>
<div class="ev-tabs-body">
<div
class="ev-tabs-body"
>
<slot />
</div>
</section>
</template>

<script>
import { ref, reactive, computed, watch,
provide, getCurrentInstance, nextTick } from 'vue';
import {
ref, computed,
provide, getCurrentInstance,
onBeforeUpdate, onUpdated,
} from 'vue';
export default {
name: 'EvTabs',
Expand All @@ -81,24 +83,45 @@ export default {
type: [String, Number],
default: null,
},
closable: {
type: Boolean,
default: true,
panels: {
type: Array,
default: () => [],
validator: (list) => {
const valueList = list.map(v => v.value);
const setList = [...new Set(valueList)];
if (list.length !== setList.length) {
console.warn('[EVUI][Tabs] TabPanel \'value\' attribute is duplicate values.');
return false;
}
if (!list.every(v => Object.hasOwnProperty.call(v, 'value'))) {
console.warn('[EVUI][Tabs] TabPanel \'value\' attribute is essential.');
return false;
}
return true;
},
},
},
emits: {
'update:modelValue': [String, Number],
change: null,
'update:panels': [Array],
change: [String, Number],
},
setup(props, { emit }) {
const instance = getCurrentInstance();
provide('evTabs', instance);
const mv = computed({
get: () => props.modelValue,
set: val => emit('update:modelValue', val),
set: (val) => {
emit('update:modelValue', val);
emit('change', val);
},
});
const tabList = computed({
get: () => props.panels,
set: val => emit('update:panels', val),
});
const tabList = reactive([]);
let tabElValueList = tabList.value.map(v => v.value);
const listWrapperRef = ref(null);
const listRef = ref(null);
Expand All @@ -117,45 +140,84 @@ export default {
hasScroll.value = listWrapperWidth < listWidth;
};
watch(
() => tabList,
curr => console.log(curr),
);
onBeforeUpdate(() => {
// 삭제된 탭이 선택된 경우 탭선택인덱스를 변경하는 로직
if (tabElValueList.length === tabList.value.length + 1) {
let longList;
let shortList;
if (tabElValueList.length > tabList.value.length) {
longList = tabElValueList;
shortList = tabList.value.map(v => v.value);
} else {
longList = tabList.value.map(v => v.value);
shortList = tabElValueList;
}
const removeValue = longList.filter(v => !shortList.includes(v))[0];
if (mv.value === removeValue) {
const selectedIdx = tabElValueList.findIndex(v => v === removeValue);
if (selectedIdx === 0) {
mv.value = tabList.value[0].value;
} else {
mv.value = tabList.value[selectedIdx - 1].value;
}
}
}
});
onUpdated(() => {
observeListEl();
tabElValueList = tabList.value.map(v => v.value);
});
const addTab = async (val) => {
if (!val.value) {
/**
* 탭 추가 로직
*/
const addTab = (info) => {
if (!info.value) {
console.warn('[EVUI][Tabs] TabPanel \'value\' attribute is essential.');
return;
}
if (tabList.some(v => v.value === val.value)) {
if (mv.value.some(v => v.value === info.value)) {
console.warn('[EVUI][Tabs] TabPanel \'value\' attribute is duplicate values.');
return;
}
tabList.push(val);
mv.value.push(info);
};
await nextTick();
observeListEl();
/**
* 탭 클릭 로직
*/
const clickTab = (val) => {
mv.value = val;
};
const clickTab = (val) => { mv.value = val; };
const removeTab = async (val) => {
if (tabList.length < 2) {
/**
* 탭 삭제 로직
*/
const removeTab = (val) => {
if (tabList.value.length < 2) {
return;
}
const selectedIdx = tabList.value.findIndex(v => v.value === val);
if (selectedIdx < 0) {
mv.value = tabList.value[0].value;
return;
}
const selectedIdx = tabList.findIndex(v => v.value === val);
if (val === mv.value) {
if (selectedIdx === 0) {
mv.value = tabList[1].value;
mv.value = tabList.value[1].value;
} else {
mv.value = tabList[selectedIdx - 1].value;
mv.value = tabList.value[selectedIdx - 1].value;
}
}
tabList.splice(selectedIdx, 1);
await nextTick();
observeListEl();
tabList.value.splice(selectedIdx, 1);
};
/**
* tab nav위에서 마우스 휠 동작
* @param type - {'next'|'prev'}
*/
const scrollTab = (type) => {
console.log(type);
if (type === 'next') {
listRefTranslateX.value -= 100;
} else {
Expand Down

0 comments on commit f1a141f

Please sign in to comment.