refactor(Collapse): use composition api

This commit is contained in:
chenjiahan 2020-08-24 21:06:40 +08:00
parent f82834886b
commit 84fe7db57d
2 changed files with 124 additions and 126 deletions

View File

@ -1,19 +1,20 @@
import { ref, watch, computed, nextTick } from 'vue';
// Utils // Utils
import { createNamespace, isDef } from '../utils'; import { createNamespace, isDef } from '../utils';
import { raf, doubleRaf } from '../utils/dom/raf'; import { raf, doubleRaf } from '../utils/dom/raf';
// Mixins // Composition
import { ChildrenMixin } from '../mixins/relation'; import { useParent } from '../api/use-relation';
// Components // Components
import Cell from '../cell'; import Cell from '../cell';
import { COLLAPSE_KEY } from '../collapse';
import { cellProps } from '../cell/shared'; import { cellProps } from '../cell/shared';
const [createComponent, bem] = createNamespace('collapse-item'); const [createComponent, bem] = createNamespace('collapse-item');
export default createComponent({ export default createComponent({
mixins: [ChildrenMixin('vanCollapse')],
props: { props: {
...cellProps, ...cellProps,
name: [Number, String], name: [Number, String],
@ -24,152 +25,119 @@ export default createComponent({
}, },
}, },
data() { setup(props, { slots }) {
return { const wrapper = ref(null);
show: null, const content = ref(null);
inited: null, const { parent, index } = useParent(COLLAPSE_KEY, ref());
const currentName = computed(() =>
isDef(props.name) ? props.name : index.value
);
const expanded = computed(() => {
if (parent) {
return parent.isExpanded(currentName.value);
}
return null;
});
const show = ref(expanded.value);
const inited = ref(expanded.value);
const onTransitionEnd = () => {
if (!expanded.value) {
show.value = false;
} else {
wrapper.value.style.height = '';
}
}; };
},
computed: { watch(expanded, (value, oldValue) => {
currentName() { if (oldValue === null) {
return isDef(this.name) ? this.name : this.index;
},
expanded() {
if (!this.parent) {
return null;
}
const { modelValue, accordion } = this.parent;
if (
process.env.NODE_ENV !== 'production' &&
!accordion &&
!Array.isArray(modelValue)
) {
console.error(
'[Vant] Collapse: type of prop "modelValue" should be Array'
);
return; return;
} }
return accordion if (value) {
? modelValue === this.currentName show.value = true;
: modelValue.some((name) => name === this.currentName); inited.value = true;
},
},
created() {
this.show = this.expanded;
this.inited = this.expanded;
},
watch: {
expanded(expanded, prev) {
if (prev === null) {
return;
}
if (expanded) {
this.show = true;
this.inited = true;
} }
// Use raf: flick when opened in safari // Use raf: flick when opened in safari
// Use nextTick: closing animation failed when set `user-select: none` // Use nextTick: closing animation failed when set `user-select: none`
const nextTick = expanded ? this.$nextTick : raf; const tick = value ? nextTick : raf;
nextTick(() => { tick(() => {
const { content, wrapper } = this.$refs; if (!content.value || !wrapper.value) {
if (!content || !wrapper) {
return; return;
} }
const { offsetHeight } = content; const { offsetHeight } = content.value;
if (offsetHeight) { if (offsetHeight) {
const contentHeight = `${offsetHeight}px`; const contentHeight = `${offsetHeight}px`;
wrapper.style.height = expanded ? 0 : contentHeight; wrapper.value.style.height = value ? 0 : contentHeight;
// use double raf to ensure animation can start // use double raf to ensure animation can start
doubleRaf(() => { doubleRaf(() => {
wrapper.style.height = expanded ? contentHeight : 0; wrapper.value.style.height = value ? contentHeight : 0;
}); });
} else { } else {
this.onTransitionEnd(); onTransitionEnd();
} }
}); });
}, });
},
methods: { const onClickTitle = () => {
onClick() { if (!props.disabled) {
if (this.disabled) { parent.toggle(currentName.value, !expanded.value);
return;
} }
};
const { parent, currentName } = this; const renderTitle = () => {
const close = parent.accordion && currentName === parent.modelValue; const { border, disabled } = props;
const name = close ? '' : currentName;
parent.switch(name, !this.expanded);
},
onTransitionEnd() {
if (!this.expanded) {
this.show = false;
} else {
this.$refs.wrapper.style.height = '';
}
},
genTitle() {
const { border, disabled, expanded } = this;
const slots = {
icon: this.$slots.icon,
title: this.$slots.title,
default: this.$slots.value,
'right-icon': this.$slots['right-icon'],
};
return ( return (
<Cell <Cell
v-slots={slots} v-slots={{
icon: slots.icon,
title: slots.title,
default: slots.value,
'right-icon': slots['right-icon'],
}}
role="button" role="button"
class={bem('title', { disabled, expanded, borderless: !border })} class={bem('title', {
onClick={this.onClick} disabled,
expanded: expanded.value,
borderless: !border,
})}
tabindex={disabled ? -1 : 0} tabindex={disabled ? -1 : 0}
aria-expanded={String(expanded)} aria-expanded={String(expanded.value)}
{...this.$props} onClick={onClickTitle}
{...props}
/> />
); );
}, };
genContent() { const renderContent = () => {
if (this.inited) { if (inited.value) {
return ( return (
<div <div
vShow={this.show} ref={wrapper}
ref="wrapper" vShow={show.value}
class={bem('wrapper')} class={bem('wrapper')}
onTransitionend={this.onTransitionEnd} onTransitionend={onTransitionEnd}
> >
<div ref="content" class={bem('content')}> <div ref={content} class={bem('content')}>
{this.$slots.default?.()} {slots.default?.()}
</div> </div>
</div> </div>
); );
} }
}, };
},
render() { return () => (
return ( <div class={[bem({ border: index.value && props.border })]}>
<div class={[bem({ border: this.index && this.border })]}> {renderTitle()}
{this.genTitle()} {renderContent()}
{this.genContent()}
</div> </div>
); );
}, },

View File

@ -1,12 +1,12 @@
import { ref, provide } from 'vue';
import { createNamespace } from '../utils'; import { createNamespace } from '../utils';
import { ParentMixin } from '../mixins/relation';
import { BORDER_TOP_BOTTOM } from '../utils/constant'; import { BORDER_TOP_BOTTOM } from '../utils/constant';
const [createComponent, bem] = createNamespace('collapse'); const [createComponent, bem] = createNamespace('collapse');
export default createComponent({ export const COLLAPSE_KEY = 'vanCollapse';
mixins: [ParentMixin('vanCollapse')],
export default createComponent({
props: { props: {
accordion: Boolean, accordion: Boolean,
modelValue: [String, Number, Array], modelValue: [String, Number, Array],
@ -18,22 +18,52 @@ export default createComponent({
emits: ['change', 'update:modelValue'], emits: ['change', 'update:modelValue'],
methods: { setup(props, { emit, slots }) {
switch(name, expanded) { const children = ref([]);
if (!this.accordion) {
name = expanded
? this.modelValue.concat(name)
: this.modelValue.filter((activeName) => activeName !== name);
}
this.$emit('change', name);
this.$emit('update:modelValue', name);
},
},
render() { const toggle = (name, expanded) => {
return ( const { accordion, modelValue } = props;
<div class={[bem(), { [BORDER_TOP_BOTTOM]: this.border }]}>
{this.$slots.default?.()} if (accordion) {
if (name === modelValue) {
name = '';
}
} else if (expanded) {
name = modelValue.concat(name);
} else {
name = modelValue.filter((activeName) => activeName !== name);
}
emit('change', name);
emit('update:modelValue', name);
};
const isExpanded = (name) => {
const { accordion, modelValue } = props;
if (
!accordion &&
!Array.isArray(modelValue) &&
process.env.NODE_ENV !== 'production'
) {
console.error(
'[Vant] Collapse: type of prop "modelValue" should be Array'
);
return;
}
return accordion ? modelValue === name : modelValue.indexOf(name) !== -1;
};
provide(COLLAPSE_KEY, {
toggle,
children,
isExpanded,
});
return () => (
<div class={[bem(), { [BORDER_TOP_BOTTOM]: props.border }]}>
{slots.default?.()}
</div> </div>
); );
}, },