refactor(DropdownItem): refactor with composition api

This commit is contained in:
chenjiahan 2020-09-08 20:19:36 +08:00
parent 60eef853db
commit cd5f5bb655
2 changed files with 198 additions and 203 deletions

View File

@ -1,11 +1,12 @@
import { Teleport } from 'vue'; import { reactive, Teleport } from 'vue';
// Utils // Utils
import { createNamespace } from '../utils'; import { createNamespace } from '../utils';
import { on, off } from '../utils/dom/event'; import { DROPDOWN_KEY } from '../dropdown-menu';
// Mixins // Composition
import { ChildrenMixin } from '../mixins/relation'; import { useParent } from '../composition/use-relation';
import { usePublicApi } from '../composition/use-public-api';
// Components // Components
import Cell from '../cell'; import Cell from '../cell';
@ -15,8 +16,6 @@ import Popup from '../popup';
const [createComponent, bem] = createNamespace('dropdown-item'); const [createComponent, bem] = createNamespace('dropdown-item');
export default createComponent({ export default createComponent({
mixins: [ChildrenMixin('vanDropdownMenu')],
props: { props: {
title: String, title: String,
disabled: Boolean, disabled: Boolean,
@ -35,93 +34,79 @@ export default createComponent({
emits: ['open', 'opened', 'close', 'closed', 'change', 'update:modelValue'], emits: ['open', 'opened', 'close', 'closed', 'change', 'update:modelValue'],
data() { setup(props, { emit, slots }) {
return { const state = reactive({
transition: true,
showPopup: false, showPopup: false,
transition: true,
showWrapper: false, showWrapper: false,
}; });
},
computed: { const renderTitle = () => {
displayTitle() { if (slots.title) {
if (this.title) { return slots.title();
return this.title;
} }
const match = this.options.filter( if (props.title) {
(option) => option.value === this.modelValue return props.title;
}
const match = props.options.filter(
(option) => option.value === props.modelValue
); );
return match.length ? match[0].text : ''; return match.length ? match[0].text : '';
}, };
},
watch: { const toggle = (show = !state.showPopup, options = {}) => {
showPopup(val) { if (show === state.showPopup) {
this.bindScroll(val);
},
},
beforeCreate() {
const createEmitter = (eventName) => () => this.$emit(eventName);
this.onOpen = createEmitter('open');
this.onClose = createEmitter('close');
this.onOpened = createEmitter('opened');
},
methods: {
// @exposed-api
toggle(show = !this.showPopup, options = {}) {
if (show === this.showPopup) {
return; return;
} }
this.transition = !options.immediate; state.showPopup = show;
this.showPopup = show; state.transition = !options.immediate;
if (show) { if (show) {
this.parent.updateOffset(); state.showWrapper = true;
this.showWrapper = true;
} }
}, };
bindScroll(bind) { const { parent } = useParent(DROPDOWN_KEY, {
const { scroller } = this.parent; props,
const action = bind ? on : off; state,
action(scroller, 'scroll', this.onScroll, true); toggle,
}, renderTitle,
});
onScroll() { const createEmitter = (eventName) => () => emit(eventName);
this.parent.updateOffset(); const onOpen = createEmitter('open');
}, const onClose = createEmitter('close');
const onOpened = createEmitter('opened');
onClosed() { const onClosed = () => {
this.showWrapper = false; state.showWrapper = false;
this.$emit('closed'); emit('closed');
}, };
onClickWrapper(event) { const onClickWrapper = (event) => {
// prevent being identified as clicking outside and closed when using teleport // prevent being identified as clicking outside and closed when using teleport
if (this.teleport) { if (props.teleport) {
event.stopPropagation(); event.stopPropagation();
} }
}, };
},
render() { const renderOption = (option) => {
const { const { activeColor } = parent.props;
zIndex, const active = option.value === props.modelValue;
offset,
overlay, const onClick = () => {
duration, state.showPopup = false;
direction,
activeColor, if (option.value !== props.modelValue) {
closeOnClickOverlay, emit('update:modelValue', option.value);
} = this.parent; emit('change', option.value);
}
};
const Options = this.options.map((option) => {
const active = option.value === this.modelValue;
return ( return (
<Cell <Cell
clickable clickable
@ -130,61 +115,67 @@ export default createComponent({
title={option.text} title={option.text}
class={bem('option', { active })} class={bem('option', { active })}
style={{ color: active ? activeColor : '' }} style={{ color: active ? activeColor : '' }}
onClick={() => { onClick={onClick}
this.showPopup = false;
if (option.value !== this.modelValue) {
this.$emit('update:modelValue', option.value);
this.$emit('change', option.value);
}
}}
> >
{active && ( {active && (
<Icon class={bem('icon')} color={activeColor} name="success" /> <Icon class={bem('icon')} color={activeColor} name="success" />
)} )}
</Cell> </Cell>
); );
}); };
const style = { zIndex }; const renderContent = () => {
if (direction === 'down') { const { offset } = parent;
style.top = `${offset}px`; const {
} else { zIndex,
style.bottom = `${offset}px`; overlay,
} duration,
direction,
closeOnClickOverlay,
} = parent.props;
const Content = ( const style = { zIndex };
<div if (direction === 'down') {
vShow={this.showWrapper} style.top = `${offset.value}px`;
ref="wrapper" } else {
style={style} style.bottom = `${offset.value}px`;
class={bem([direction])} }
onClick={this.onClickWrapper}
> return (
<Popup <div
vModel={[this.showPopup, 'show']} vShow={state.showWrapper}
overlay={overlay} style={style}
class={bem('content')} class={bem([direction])}
position={direction === 'down' ? 'top' : 'bottom'} onClick={onClickWrapper}
duration={this.transition ? duration : 0}
lazyRender={this.lazyRender}
overlayStyle={{ position: 'absolute' }}
closeOnClickOverlay={closeOnClickOverlay}
onOpen={this.onOpen}
onClose={this.onClose}
onOpened={this.onOpened}
onClosed={this.onClosed}
> >
{Options} <Popup
{this.$slots.default?.()} vModel={[state.showPopup, 'show']}
</Popup> class={bem('content')}
</div> overlay={overlay}
); position={direction === 'down' ? 'top' : 'bottom'}
duration={state.transition ? duration : 0}
lazyRender={props.lazyRender}
overlayStyle={{ position: 'absolute' }}
closeOnClickOverlay={closeOnClickOverlay}
onOpen={onOpen}
onClose={onClose}
onOpened={onOpened}
onClosed={onClosed}
>
{props.options.map(renderOption)}
{slots.default?.()}
</Popup>
</div>
);
};
if (this.teleport) { usePublicApi({ toggle });
return <Teleport to={this.teleport}>{Content}</Teleport>;
}
return Content; return () => {
if (props.teleport) {
return <Teleport to={props.teleport}>{renderContent()}</Teleport>;
}
return renderContent();
};
}, },
}); });

View File

@ -1,22 +1,19 @@
import { ref, provide, reactive, computed } from 'vue';
// Utils // Utils
import { createNamespace, isDef } from '../utils'; import { createNamespace, isDef } from '../utils';
import { getScroller } from '../utils/dom/scroll';
// Mixins // Composition
import { ParentMixin } from '../mixins/relation'; import { useRect } from '../composition/use-rect';
import { ClickOutsideMixin } from '../mixins/click-outside'; import { useScroller } from '../composition/use-scroller';
import { useGlobalEvent } from '../composition/use-global-event';
import { useClickOutside } from '../composition/use-click-outside';
const [createComponent, bem] = createNamespace('dropdown-menu'); const [createComponent, bem] = createNamespace('dropdown-menu');
export default createComponent({ export const DROPDOWN_KEY = 'vanDropdownMenu';
mixins: [
ParentMixin('vanDropdownMenu'),
ClickOutsideMixin({
event: 'click',
method: 'onClickOutside',
}),
],
export default createComponent({
props: { props: {
zIndex: [Number, String], zIndex: [Number, String],
activeColor: String, activeColor: String,
@ -38,101 +35,108 @@ export default createComponent({
}, },
}, },
data() { setup(props, { slots }) {
return { const offset = ref(0);
offset: 0, const barRef = ref();
}; const rootRef = ref();
}, const children = reactive([]);
computed: { const scroller = useScroller(rootRef);
scroller() {
return getScroller(this.$el);
},
opened() { const opened = computed(() =>
return this.children.some((item) => item.showWrapper); children.some((item) => item.state.showWrapper)
}, );
barStyle() { const barStyle = computed(() => {
if (this.opened && isDef(this.zIndex)) { if (opened.value && isDef(props.zIndex)) {
return { return {
zIndex: 1 + this.zIndex, zIndex: 1 + props.zIndex,
}; };
} }
}, });
},
methods: { const onClickOutside = () => {
updateOffset() { children.forEach((item) => {
if (!this.$refs.bar) { item.toggle(false);
return; });
};
const updateOffset = () => {
if (barRef.value) {
const rect = useRect(barRef);
if (props.direction === 'down') {
offset.value = rect.bottom;
} else {
offset.value = window.innerHeight - rect.top;
}
} }
};
const rect = this.$refs.bar.getBoundingClientRect(); const toggleItem = (active) => {
children.forEach((item, index) => {
if (this.direction === 'down') {
this.offset = rect.bottom;
} else {
this.offset = window.innerHeight - rect.top;
}
},
toggleItem(active) {
this.children.forEach((item, index) => {
if (index === active) { if (index === active) {
updateOffset();
item.toggle(); item.toggle();
} else if (item.showPopup) { } else if (item.state.showPopup) {
item.toggle(false, { immediate: true }); item.toggle(false, { immediate: true });
} }
}); });
}, };
onClickOutside() { const renderTitle = (item, index) => {
this.children.forEach((item) => { const { showPopup } = item.state;
item.toggle(false); const { disabled, titleClass } = item.props;
});
},
},
render() { return (
const Titles = this.children.map((item, index) => (
<div
role="button"
tabindex={item.disabled ? -1 : 0}
class={bem('item', { disabled: item.disabled })}
onClick={() => {
if (!item.disabled) {
this.toggleItem(index);
}
}}
>
<span
class={[
bem('title', {
active: item.showPopup,
down: item.showPopup === (this.direction === 'down'),
}),
item.titleClass,
]}
style={{ color: item.showPopup ? this.activeColor : '' }}
>
<div class="van-ellipsis">
{item.$slots.title ? item.$slots.title() : item.displayTitle}
</div>
</span>
</div>
));
return (
<div class={bem()}>
<div <div
ref="bar" role="button"
style={this.barStyle} tabindex={disabled ? -1 : 0}
class={bem('bar', { opened: this.opened })} class={bem('item', { disabled })}
onClick={() => {
if (!disabled) {
toggleItem(index);
}
}}
> >
{Titles} <span
class={[
bem('title', {
down: showPopup === (props.direction === 'down'),
active: showPopup,
}),
titleClass,
]}
style={{ color: showPopup ? props.activeColor : '' }}
>
<div class="van-ellipsis">{item.renderTitle()}</div>
</span>
</div> </div>
{this.$slots.default?.()} );
};
provide(DROPDOWN_KEY, { props, offset, children });
useClickOutside({
element: rootRef,
callback: onClickOutside,
});
useGlobalEvent(scroller, 'scroll', () => {
if (opened.value) {
updateOffset();
}
});
return () => (
<div ref={rootRef} class={bem()}>
<div
ref={barRef}
style={barStyle.value}
class={bem('bar', { opened: opened.value })}
>
{children.map(renderTitle)}
</div>
{slots.default?.()}
</div> </div>
); );
}, },