Merge branch 'next' of github.com:youzan/vant into next

This commit is contained in:
chenjiahan 2022-01-21 16:52:51 +08:00
commit 3c3f58a7be
26 changed files with 1488 additions and 2294 deletions

View File

@ -4,22 +4,47 @@
本文档提供了从 Vant 3 到 Vant 4 的升级指南。
### 为什么会有 Vant 4.0
## API 调整
为了支持 **暗色模式**,我们对 Vant 中的 **样式变量** 进行了一些不兼容更新,因此发布了新的大版本。
### Picker 组件重构
如果你的项目没有使用主题定制,那样式变量的调整对你没有任何影响,只需要花几分钟去适配 API 调整,即可完成升级。
在之前的版本中Picker 组件的 API 设计存在较大问题,比如:
如果你的项目使用了主题定制,请完整阅读此文档,并进行迁移。
- columns 数据格式定义不合理,容易产生误解
- 数据流不清晰,暴露了过多的实例方法来对数据进行操作
### API 调整
为了解决上述问题,我们在 v4 版本中对 Picker 组件进行了重构。
4.0 版本对少量 API 进行了不兼容调整:
#### 主要变更
#### Picker
- 支持通过 `v-model` 绑定当前选中的值,移除 `default-index` 属性
- 重新定义了 `columns` 属性的结构
- 移除了操作内部数据的实例方法,仅保留 `confirm` 方法
- 调整了 `confirm``cancel``change` 事件的参数
- 重命名 `item-height` 属性为 `option-height`
- 重命名 `visible-item-count` 属性为 `visible-option-num`
- `default` 插槽重命名为 `toolbar`
- 移除了 `value-key` 属性,使用 `columnsFieldNames` 属性代替
详细用法请参见 [Picker 组件文档](#/zh-CN/picker)。
### Area 组件重构
Area 组件是基于 Picker 组件进行封装的,因此本次升级也对 Area 组件进行了内部逻辑的重构,并优化了部分 API 设计。
#### 主要变更
- 支持通过 `v-model` 绑定当前选中的值
- 移除 `reset` 方法,现在可以通过修改 `v-model` 来进行重置
- 移除 `is-oversea-code` 属性
- 调整所有事件的参数,与 Picker 组件保持一致
- 重命名 `value` 属性我 `modelValue`
- 重命名 `item-height` 属性为 `option-height`
- 重命名 `visible-item-count` 属性为 `visible-option-num`
详细用法请参见 [Area 组件文档](#/zh-CN/area)。
### 其他 API 调整
4.0 版本中,以下 API 进行了不兼容更新:
#### Tabs

View File

@ -2,16 +2,12 @@ import {
ref,
watch,
computed,
reactive,
nextTick,
onMounted,
defineComponent,
type PropType,
type ExtractPropTypes,
} from 'vue';
// Utils
import { deepClone } from '../utils/deep-clone';
import {
pick,
extend,
@ -20,52 +16,24 @@ import {
createNamespace,
} from '../utils';
import { pickerSharedProps } from '../picker/Picker';
// Composables
import { useExpose } from '../composables/use-expose';
import { INHERIT_PROPS, INHERIT_SLOTS, formatDataForCascade } from './utils';
// Components
import { Picker, PickerInstance } from '../picker';
import { Picker } from '../picker';
// Types
import type { AreaList, AreaColumnType, AreaColumnOption } from './types';
import type { AreaList } from './types';
const [name, bem] = createNamespace('area');
const EMPTY_CODE = '000000';
const INHERIT_SLOTS = [
'title',
'cancel',
'confirm',
'toolbar',
'columns-top',
'columns-bottom',
] as const;
const INHERIT_PROPS = [
'title',
'loading',
'readonly',
'itemHeight',
'swipeDuration',
'visibleItemCount',
'cancelButtonText',
'confirmButtonText',
] as const;
const isOverseaCode = (code: string) => code[0] === '9';
const areaProps = extend({}, pickerSharedProps, {
value: String,
modelValue: String,
columnsNum: makeNumericProp(3),
columnsPlaceholder: makeArrayProp<string>(),
areaList: {
type: Object as PropType<AreaList>,
default: () => ({}),
},
isOverseaCode: {
type: Function as PropType<(code: string) => boolean>,
default: isOverseaCode,
},
});
export type AreaProps = ExtractPropTypes<typeof areaProps>;
@ -75,262 +43,54 @@ export default defineComponent({
props: areaProps,
emits: ['change', 'confirm', 'cancel'],
emits: ['change', 'confirm', 'cancel', 'update:modelValue'],
setup(props, { emit, slots }) {
const pickerRef = ref<PickerInstance>();
const state = reactive({
code: props.value,
columns: [{ values: [] }, { values: [] }, { values: [] }],
});
const areaList = computed(() => {
const { areaList } = props;
return {
province: areaList.province_list || {},
city: areaList.city_list || {},
county: areaList.county_list || {},
};
});
const placeholderMap = computed(() => {
const { columnsPlaceholder } = props;
return {
province: columnsPlaceholder[0] || '',
city: columnsPlaceholder[1] || '',
county: columnsPlaceholder[2] || '',
};
});
const getDefaultCode = () => {
if (props.columnsPlaceholder.length) {
return EMPTY_CODE;
}
const { county, city } = areaList.value;
const countyCodes = Object.keys(county);
if (countyCodes[0]) {
return countyCodes[0];
}
const cityCodes = Object.keys(city);
if (cityCodes[0]) {
return cityCodes[0];
}
return '';
};
const getColumnValues = (type: AreaColumnType, code?: string) => {
let column: AreaColumnOption[] = [];
if (type !== 'province' && !code) {
return column;
}
const list = areaList.value[type];
column = Object.keys(list).map((listCode) => ({
code: listCode,
name: list[listCode],
}));
if (code) {
// oversea code
if (type === 'city' && props.isOverseaCode(code)) {
code = '9';
}
column = column.filter((item) => item.code.indexOf(code!) === 0);
}
if (placeholderMap.value[type] && column.length) {
// set columns placeholder
let codeFill = '';
if (type === 'city') {
codeFill = EMPTY_CODE.slice(2, 4);
} else if (type === 'county') {
codeFill = EMPTY_CODE.slice(4, 6);
}
column.unshift({
code: code + codeFill,
name: placeholderMap.value[type],
});
}
return column;
};
// get index by code
const getIndex = (type: AreaColumnType, code: string) => {
let compareNum = code.length;
if (type === 'province') {
compareNum = props.isOverseaCode(code) ? 1 : 2;
}
if (type === 'city') {
compareNum = 4;
}
code = code.slice(0, compareNum);
const list = getColumnValues(
type,
compareNum > 2 ? code.slice(0, compareNum - 2) : ''
);
for (let i = 0; i < list.length; i++) {
if (list[i].code.slice(0, compareNum) === code) {
return i;
}
}
return 0;
};
const setValues = () => {
const picker = pickerRef.value;
if (!picker) {
return;
}
let code = state.code || getDefaultCode();
const province = getColumnValues('province');
const city = getColumnValues('city', code.slice(0, 2));
picker.setColumnValues(0, province);
picker.setColumnValues(1, city);
if (
city.length &&
code.slice(2, 4) === '00' &&
!props.isOverseaCode(code)
) {
[{ code }] = city;
}
picker.setColumnValues(2, getColumnValues('county', code.slice(0, 4)));
picker.setIndexes([
getIndex('province', code),
getIndex('city', code),
getIndex('county', code),
]);
};
// parse output columns data
const parseValues = (values: AreaColumnOption[]) =>
values.map((value, index) => {
if (value) {
value = deepClone(value);
if (!value.code || value.name === props.columnsPlaceholder[index]) {
value.code = '';
value.name = '';
}
}
return value;
});
const getValues = () => {
if (pickerRef.value) {
const values = pickerRef.value
.getValues<AreaColumnOption>()
.filter(Boolean);
return parseValues(values);
}
return [];
};
const getArea = () => {
const values = getValues();
const area = {
code: '',
country: '',
province: '',
city: '',
county: '',
};
if (!values.length) {
return area;
}
const names = values.map((item) => item.name);
const validValues = values.filter((value) => value.code);
area.code = validValues.length
? validValues[validValues.length - 1].code
: '';
if (props.isOverseaCode(area.code)) {
area.country = names[1] || '';
area.province = names[2] || '';
} else {
area.province = names[0] || '';
area.city = names[1] || '';
area.county = names[2] || '';
}
return area;
};
const reset = (newCode = '') => {
state.code = newCode;
setValues();
};
const onChange = (values: AreaColumnOption[], index: number) => {
state.code = values[index].code;
setValues();
if (pickerRef.value) {
const parsedValues = parseValues(pickerRef.value.getValues());
emit('change', parsedValues, index);
}
};
const onConfirm = (values: AreaColumnOption[], index: number) => {
setValues();
emit('confirm', parseValues(values), index);
};
const codes = ref<string[]>([]);
const columns = computed(() => formatDataForCascade(props));
const onChange = (...args: unknown[]) => emit('change', ...args);
const onCancel = (...args: unknown[]) => emit('cancel', ...args);
onMounted(setValues);
const onConfirm = (...args: unknown[]) => emit('confirm', ...args);
watch(
() => props.value,
(value) => {
state.code = value;
setValues();
}
codes,
(newCodes) => {
const lastCode = newCodes.length ? newCodes[newCodes.length - 1] : '';
if (lastCode && lastCode !== props.modelValue) {
emit('update:modelValue', lastCode);
}
},
{ deep: true }
);
watch(() => props.areaList, setValues, { deep: true });
watch(
() => props.columnsNum,
() => nextTick(setValues)
() => props.modelValue,
(newCode) => {
const lastCode = codes.value.length
? codes.value[codes.value.length - 1]
: '';
if (newCode && newCode !== lastCode) {
codes.value = [
`${newCode.slice(0, 2)}0000`,
`${newCode.slice(0, 4)}00`,
newCode,
].slice(0, +props.columnsNum);
}
},
{ immediate: true }
);
useExpose({ reset, getArea, getValues });
return () => {
const columns = state.columns.slice(0, +props.columnsNum);
return (
<Picker
v-slots={pick(slots, INHERIT_SLOTS)}
ref={pickerRef}
class={bem()}
columns={columns}
columnsFieldNames={{ text: 'name' }}
onChange={onChange}
onCancel={onCancel}
onConfirm={onConfirm}
{...pick(props, INHERIT_PROPS)}
/>
);
};
return () => (
<Picker
v-model={codes.value}
v-slots={pick(slots, INHERIT_SLOTS)}
class={bem()}
columns={columns.value}
onChange={onChange}
onCancel={onCancel}
onConfirm={onConfirm}
{...pick(props, INHERIT_PROPS)}
/>
);
},
});

View File

@ -75,12 +75,23 @@ export default {
};
```
### Initial Value
### Model Value
To have a selected valuesimply pass the `code` of target area to `value` property.
Bind the currently selected area code via `v-model`.
```html
<van-area title="Title" :area-list="areaList" value="110101" />
<van-area v-model="value" title="Title" :area-list="areaList" />
```
```js
import { ref } from 'vue';
export default {
setup() {
const value = ref('330302');
return { value };
},
};
```
### Columns Number
@ -109,7 +120,7 @@ To have a selected valuesimply pass the `code` of target area to `value` prop
| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
| value | the `code` of selected area | _string_ | - |
| v-model | the `code` of selected area | _string_ | - |
| title | Toolbar title | _string_ | - |
| confirm-button-text | Text of confirm button | _string_ | `Confirm` |
| cancel-button-text | Text of cancel button | _string_ | `Cancel` |
@ -117,40 +128,18 @@ To have a selected valuesimply pass the `code` of target area to `value` prop
| columns-placeholder | Placeholder of columns | _string[]_ | `[]` |
| loading | Whether to show loading prompt | _boolean_ | `false` |
| readonly | Whether to be readonly | _boolean_ | `false` |
| item-height | Option height, supports `px` `vw` `vh` `rem` unit, default `px` | _number \| string_ | `44` |
| option-height | Option height, supports `px` `vw` `vh` `rem` unit, default `px` | _number \| string_ | `44` |
| columns-num | Level of picker | _number \| string_ | `3` |
| visible-item-count | Count of visible columns | _number \| string_ | `6` |
| visible-option-num | Count of visible columns | _number \| string_ | `6` |
| swipe-duration | Duration of the momentum animationunit `ms` | _number \| string_ | `1000` |
| is-oversea-code | The method to validate oversea code | _() => boolean_ | - |
### Events
| Event | Description | Arguments |
| --- | --- | --- |
| confirm | Emitted when the confirm button is clicked | _result: ConfirmResult_ |
| cancel | Emitted when the cancel button is clicked | - |
| change | Emitted when current option changed | current valuescolumn index |
### ConfirmResult
An array that contains selected area objects.
```js
[
{
code: '330000',
name: 'Zhejiang Province',
},
{
code: '330100',
name: 'Hangzhou',
},
{
code: '330105',
name: 'Xihu District',
},
];
```
| confirm | Emitted when the confirm button is clicked | _{ selectedValues, selectedOptions }_ |
| cancel | Emitted when the cancel button is clicked | _{ selectedValues, selectedOptions }_ |
| change | Emitted when current option is changed | _{ selectedValues, selectedOptions, columnIndex }_ |
### Slots
@ -163,14 +152,6 @@ An array that contains selected area objects.
| columns-top | Custom content above columns | - |
| columns-bottom | Custom content below columns | - |
### Methods
Use [ref](https://v3.vuejs.org/guide/component-template-refs.html) to get Area instance and call instance methods.
| Name | Description | Attribute | Return value |
| ----- | ------------------------- | --------------- | ------------ |
| reset | Reset all options by code | _code?: string_ | - |
### Types
The component exports the following type definitions:

View File

@ -77,12 +77,23 @@ export default {
};
```
### 选中省市区
### 控制选中项
如果想选中某个省市区,需要传入一个 `value` 属性,绑定对应的地区码。
通过 `v-model` 绑定当前选中的地区码。
```html
<van-area title="标题" :area-list="areaList" value="110101" />
<van-area v-model="value" title="标题" :area-list="areaList" />
```
```js
import { ref } from 'vue';
export default {
setup() {
const value = ref('330302');
return { value };
},
};
```
### 配置显示列
@ -111,7 +122,7 @@ export default {
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| value | 当前选中项对应的地区码 | _string_ | - |
| v-model | 当前选中项对应的地区码 | _string_ | - |
| title | 顶部栏标题 | _string_ | - |
| confirm-button-text | 确认按钮文字 | _string_ | `确认` |
| cancel-button-text | 取消按钮文字 | _string_ | `取消` |
@ -119,40 +130,18 @@ export default {
| columns-placeholder | 列占位提示文字 | _string[]_ | `[]` |
| loading | 是否显示加载状态 | _boolean_ | `false` |
| readonly | 是否为只读状态,只读状态下无法切换选项 | _boolean_ | `false` |
| item-height | 选项高度,支持 `px` `vw` `vh` `rem` 单位,默认 `px` | _number \| string_ | `44` |
| option-height | 选项高度,支持 `px` `vw` `vh` `rem` 单位,默认 `px` | _number \| string_ | `44` |
| columns-num | 显示列数3-省市区2-省市1-省 | _number \| string_ | `3` |
| visible-item-count | 可见的选项个数 | _number \| string_ | `6` |
| visible-option-num | 可见的选项个数 | _number \| string_ | `6` |
| swipe-duration | 快速滑动时惯性滚动的时长,单位 `ms` | _number \| string_ | `1000` |
| is-oversea-code | 根据地区码校验海外地址,海外地址会划分至单独的分类 | _() => boolean_ | - |
### Events
| 事件 | 说明 | 回调参数 |
| ------- | ------------------ | ------------------------------ |
| confirm | 点击完成按钮时触发 | _result: ConfirmResult_ |
| cancel | 点击取消按钮时触发 | - |
| change | 选项改变时触发 | 所有列选中值,当前列对应的索引 |
### ConfirmResult 格式
confirm 事件返回的数据整体为一个数组,数组每一项对应一列选项中被选中的数据。
```js
[
{
code: '110000',
name: '北京市',
},
{
code: '110100',
name: '北京市',
},
{
code: '110101',
name: '东城区',
},
];
```
| 事件 | 说明 | 回调参数 |
| --- | --- | --- |
| confirm | 点击完成按钮时触发 | _{ selectedValues, selectedOptions }_ |
| cancel | 点击取消按钮时触发 | _{ selectedValues, selectedOptions }_ |
| change | 选项改变时触发 | _{ selectedValues, selectedOptions, columnIndex }_ |
### Slots
@ -165,20 +154,12 @@ confirm 事件返回的数据整体为一个数组,数组每一项对应一列
| columns-top | 自定义选项上方内容 | - |
| columns-bottom | 自定义选项下方内容 | - |
### 方法
通过 ref 可以获取到 Area 实例并调用实例方法,详见[组件实例方法](#/zh-CN/advanced-usage#zu-jian-shi-li-fang-fa)。
| 方法名 | 说明 | 参数 | 返回值 |
| --- | --- | --- | --- |
| reset | 根据地区码重置所有选项,若不传地区码,则重置到第一项 | _code?: string_ | - |
### 类型定义
组件导出以下类型定义:
```ts
import type { AreaProps, AreaList, AreaInstance, AreaColumnOption } from 'vant';
import type { AreaProps, AreaList, AreaInstance } from 'vant';
```
`AreaInstance` 是组件实例的类型,用法如下:

View File

@ -1 +0,0 @@
// 已迁移至 https://github.com/youzan/vant/tree/dev/packages/vant-area-data

View File

@ -7,14 +7,14 @@ import { useTranslate } from '../../../docs/site/use-translate';
const t = useTranslate({
'zh-CN': {
title2: '选中省市区',
title2: '控制选中项',
title3: '配置显示列',
title4: '配置列占位提示文字',
columnsPlaceholder: ['请选择', '请选择', '请选择'],
areaList,
},
'en-US': {
title2: 'Initial Value',
title2: 'Model Value',
title3: 'Columns Number',
title4: 'Columns Placeholder',
columnsPlaceholder: ['Choose', 'Choose', 'Choose'],
@ -31,7 +31,7 @@ const value = ref('330302');
</demo-block>
<demo-block card :title="t('title2')">
<van-area :title="t('title')" :area-list="t('areaList')" :value="value" />
<van-area v-model="value" :title="t('title')" :area-list="t('areaList')" />
</demo-block>
<demo-block card :title="t('title3')">

View File

@ -4,7 +4,7 @@ import _Area from './Area';
export const Area = withInstall(_Area);
export default Area;
export type { AreaProps } from './Area';
export type { AreaList, AreaInstance, AreaColumnOption } from './types';
export type { AreaList, AreaInstance } from './types';
declare module 'vue' {
export interface GlobalComponents {

View File

@ -8,23 +8,4 @@ export type AreaList = {
province_list: Record<string, string>;
};
export type AreaColumnOption = {
name: string;
code: string;
};
export type AreaColumnType = 'province' | 'county' | 'city';
export type AreaExpose = {
reset: (newCode?: string) => void;
getArea: () => {
code: string;
country: string;
province: string;
city: string;
county: string;
};
getValues: () => AreaColumnOption[];
};
export type AreaInstance = ComponentPublicInstance<AreaProps, AreaExpose>;
export type AreaInstance = ComponentPublicInstance<AreaProps>;

View File

@ -0,0 +1,103 @@
import type { AreaProps } from '.';
import type { PickerOption } from '../picker';
const EMPTY_CODE = '000000';
export const INHERIT_SLOTS = [
'title',
'cancel',
'confirm',
'toolbar',
'columns-top',
'columns-bottom',
] as const;
export const INHERIT_PROPS = [
'title',
'loading',
'readonly',
'optionHeight',
'swipeDuration',
'visibleOptionNum',
'cancelButtonText',
'confirmButtonText',
] as const;
const makeOption = (
text = '',
value = EMPTY_CODE,
children?: PickerOption[]
): PickerOption => ({
text,
value,
children,
});
export function formatDataForCascade({
areaList,
columnsNum,
columnsPlaceholder: placeholder,
}: AreaProps) {
const {
city_list: city,
county_list: county,
province_list: province,
} = areaList;
const showCity = columnsNum > 1;
const showCounty = columnsNum > 2;
const getProvinceChildren = () => {
if (showCity) {
return placeholder.length
? [makeOption(placeholder[0], EMPTY_CODE, showCounty ? [] : undefined)]
: [];
}
};
const provinceMap = new Map<string, PickerOption>();
Object.keys(province).forEach((code) => {
provinceMap.set(
code.slice(0, 2),
makeOption(province[code], code, getProvinceChildren())
);
});
const cityMap = new Map<string, PickerOption>();
if (showCity) {
const getCityChildren = () => {
if (showCounty) {
return placeholder.length ? [makeOption(placeholder[1])] : [];
}
};
Object.keys(city).forEach((code) => {
const option = makeOption(city[code], code, getCityChildren());
cityMap.set(code.slice(0, 4), option);
const province = provinceMap.get(code.slice(0, 2));
if (province) {
province.children!.push(option);
}
});
}
if (showCounty) {
Object.keys(county).forEach((code) => {
const city = cityMap.get(code.slice(0, 4));
if (city) {
city.children!.push(makeOption(county[code], code));
}
});
}
const options = Array.from(provinceMap.values()) as PickerOption[];
if (placeholder.length) {
const county = showCounty ? [makeOption(placeholder[2])] : undefined;
const city = showCity
? [makeOption(placeholder[1], EMPTY_CODE, county)]
: undefined;
options.unshift(makeOption(placeholder[0], EMPTY_CODE, city));
}
return options;
}

View File

@ -291,8 +291,8 @@ export default {
| filter | Option filter | _(type: string, values: string[]) => string[]_ | - |
| formatter | Option text formatter | _(type: string, value: string) => string_ | - |
| columns-order | Array for ordering columns, where item can be set to<br> `year`, `month`, `day`, `hour` and `minute` | _string[]_ | - |
| item-height | Option height, supports `px` `vw` `vh` `rem` unit, default `px` | _number \| string_ | `44` |
| visible-item-count | Count of visible columns | _number \| string_ | `6` |
| option-height | Option height, supports `px` `vw` `vh` `rem` unit, default `px` | _number \| string_ | `44` |
| visible-option-num | Count of visible columns | _number \| string_ | `6` |
| swipe-duration | Duration of the momentum animationunit `ms` | _number \| string_ | `1000` |
### DatePicker Props

View File

@ -300,8 +300,8 @@ export default {
| filter | 选项过滤函数 | _(type: string, values: string[]) => string[]_ | - |
| formatter | 选项格式化函数 | _(type: string, value: string) => string_ | - |
| columns-order | 自定义列排序数组, 子项可选值为<br> `year``month``day``hour``minute` | _string[]_ | - |
| item-height | 选项高度,支持 `px` `vw` `vh` `rem` 单位,默认 `px` | _number \| string_ | `44` |
| visible-item-count | 可见的选项个数 | _number \| string_ | `6` |
| option-height | 选项高度,支持 `px` `vw` `vh` `rem` 单位,默认 `px` | _number \| string_ | `44` |
| visible-option-num | 可见的选项个数 | _number \| string_ | `6` |
| swipe-duration | 快速滑动时惯性滚动的时长,单位`ms` | _number \| string_ | `1000` |
### DatePicker Props

View File

@ -20,6 +20,14 @@ import {
HAPTICS_FEEDBACK,
BORDER_UNSET_TOP_BOTTOM,
} from '../utils';
import {
isValuesEqual,
getColumnsType,
findOptionByValue,
formatCascadeColumns,
getFirstEnabledOption,
assignDefaultFields,
} from './utils';
// Composables
import { useChildren } from '@vant/use';
@ -32,10 +40,9 @@ import Column, { PICKER_KEY } from './PickerColumn';
// Types
import type {
PickerColumn,
PickerOption,
PickerExpose,
PickerOption,
PickerFieldNames,
PickerObjectColumn,
PickerToolbarPosition,
} from './types';
@ -46,17 +53,17 @@ export const pickerSharedProps = {
loading: Boolean,
readonly: Boolean,
allowHtml: Boolean,
itemHeight: makeNumericProp(44),
optionHeight: makeNumericProp(44),
showToolbar: truthProp,
swipeDuration: makeNumericProp(1000),
visibleItemCount: makeNumericProp(6),
visibleOptionNum: makeNumericProp(6),
cancelButtonText: String,
confirmButtonText: String,
};
const pickerProps = extend({}, pickerSharedProps, {
columns: makeArrayProp<PickerOption | PickerColumn>(),
defaultIndex: makeNumericProp(0),
modelValue: makeArrayProp<number | string>(),
toolbarPosition: makeStringProp<PickerToolbarPosition>('top'),
columnsFieldNames: Object as PropType<PickerFieldNames>,
});
@ -68,213 +75,77 @@ export default defineComponent({
props: pickerProps,
emits: ['confirm', 'cancel', 'change'],
emits: ['confirm', 'cancel', 'change', 'update:modelValue'],
setup(props, { emit, slots }) {
const hasOptions = ref(false);
const formattedColumns = ref<PickerObjectColumn[]>([]);
const {
text: textKey,
values: valuesKey,
children: childrenKey,
} = extend(
{
text: 'text',
values: 'values',
children: 'children',
},
props.columnsFieldNames
);
const selectedValues = ref(props.modelValue);
const { children, linkChildren } = useChildren(PICKER_KEY);
linkChildren();
const itemHeight = computed(() => unitToPx(props.itemHeight));
const fields = computed(() => assignDefaultFields(props.columnsFieldNames));
const optionHeight = computed(() => unitToPx(props.optionHeight));
const columnsType = computed(() =>
getColumnsType(props.columns, fields.value)
);
const dataType = computed(() => {
const firstColumn = props.columns[0];
if (typeof firstColumn === 'object') {
if (childrenKey in firstColumn) {
return 'cascade';
}
if (valuesKey in firstColumn) {
return 'object';
}
const currentColumns = computed<PickerColumn[]>(() => {
const { columns } = props;
switch (columnsType.value) {
case 'multiple':
return columns as PickerColumn[];
case 'cascade':
return formatCascadeColumns(columns, fields.value, selectedValues);
default:
return [columns];
}
return 'plain';
});
const formatCascade = () => {
const formatted: PickerObjectColumn[] = [];
const hasOptions = computed(() =>
currentColumns.value.some((options) => options.length)
);
let cursor: PickerObjectColumn = {
[childrenKey]: props.columns,
};
const selectedOptions = computed(() =>
currentColumns.value.map((options, index) =>
findOptionByValue(options, selectedValues.value[index], fields.value)
)
);
while (cursor && cursor[childrenKey]) {
const children = cursor[childrenKey];
let defaultIndex = cursor.defaultIndex ?? +props.defaultIndex;
const onChange = (value: number | string, columnIndex: number) => {
selectedValues.value[columnIndex] = value;
while (children[defaultIndex] && children[defaultIndex].disabled) {
if (defaultIndex < children.length - 1) {
defaultIndex++;
} else {
defaultIndex = 0;
break;
if (columnsType.value === 'cascade') {
// reset values after cascading
selectedValues.value.forEach((value, index) => {
const options = currentColumns.value[index];
if (!options.find((option) => option[fields.value.value] === value)) {
selectedValues.value[index] = options.length
? options[0][fields.value.value]
: undefined;
}
}
formatted.push({
[valuesKey]: cursor[childrenKey],
className: cursor.className,
defaultIndex,
});
cursor = children[defaultIndex];
}
formattedColumns.value = formatted;
};
const format = () => {
const { columns } = props;
if (dataType.value === 'plain') {
formattedColumns.value = [{ [valuesKey]: columns }];
} else if (dataType.value === 'cascade') {
formatCascade();
} else {
formattedColumns.value = columns as PickerObjectColumn[];
}
hasOptions.value = formattedColumns.value.some(
(item) => item[valuesKey] && item[valuesKey].length !== 0
);
};
// get indexes of all columns
const getIndexes = () => children.map((child) => child.state.index);
// set options of column by index
const setColumnValues = (index: number, options: PickerOption[]) => {
const column = children[index];
if (column) {
column.setOptions(options);
hasOptions.value = true;
}
};
const onCascadeChange = (columnIndex: number) => {
let cursor: PickerObjectColumn = {
[childrenKey]: props.columns,
};
const indexes = getIndexes();
for (let i = 0; i <= columnIndex; i++) {
cursor = cursor[childrenKey][indexes[i]];
}
while (cursor && cursor[childrenKey]) {
columnIndex++;
setColumnValues(columnIndex, cursor[childrenKey]);
cursor = cursor[childrenKey][cursor.defaultIndex || 0];
}
};
// get column instance by index
const getChild = (index: number) => children[index];
// get column value by index
const getColumnValue = (index: number) => {
const column = getChild(index);
if (column) {
return column.getValue();
}
};
// set column value by index
const setColumnValue = (index: number, value: string) => {
const column = getChild(index);
if (column) {
column.setValue(value);
if (dataType.value === 'cascade') {
onCascadeChange(index);
}
}
};
// get column option index by column index
const getColumnIndex = (index: number) => {
const column = getChild(index);
if (column) {
return column.state.index;
}
};
// set column option index by column index
const setColumnIndex = (columnIndex: number, optionIndex: number) => {
const column = getChild(columnIndex);
if (column) {
column.setIndex(optionIndex);
if (dataType.value === 'cascade') {
onCascadeChange(columnIndex);
}
}
};
// get options of column by index
const getColumnValues = (index: number) => {
const column = getChild(index);
if (column) {
return column.state.options;
}
};
// get values of all columns
const getValues = () => children.map((child) => child.getValue());
// set values of all columns
const setValues = (values: string[]) => {
values.forEach((value, index) => {
setColumnValue(index, value);
emit('change', {
columnIndex,
selectedValues: selectedValues.value,
selectedOptions: selectedOptions.value,
});
};
// set indexes of all columns
const setIndexes = (indexes: number[]) => {
indexes.forEach((optionIndex, columnIndex) => {
setColumnIndex(columnIndex, optionIndex);
});
};
const emitAction = (event: 'confirm' | 'cancel') => {
if (dataType.value === 'plain') {
emit(event, getColumnValue(0), getColumnIndex(0));
} else {
emit(event, getValues(), getIndexes());
}
};
const onChange = (columnIndex: number) => {
if (dataType.value === 'cascade') {
onCascadeChange(columnIndex);
}
if (dataType.value === 'plain') {
emit('change', getColumnValue(0), getColumnIndex(0));
} else {
emit('change', getValues(), columnIndex);
}
};
const confirm = () => {
children.forEach((child) => child.stopMomentum());
emitAction('confirm');
emit('confirm', {
selectedValues: selectedValues.value,
selectedOptions: selectedOptions.value,
});
};
const cancel = () => emitAction('cancel');
const cancel = () =>
emit('cancel', {
selectedValues: selectedValues.value,
selectedOptions: selectedOptions.value,
});
const renderTitle = () => {
if (slots.title) {
@ -324,27 +195,26 @@ export default defineComponent({
};
const renderColumnItems = () =>
formattedColumns.value.map((item, columnIndex) => (
currentColumns.value.map((options, columnIndex) => (
<Column
v-slots={{ option: slots.option }}
textKey={textKey}
value={selectedValues.value[columnIndex]}
fields={fields.value}
options={options}
readonly={props.readonly}
allowHtml={props.allowHtml}
className={item.className}
itemHeight={itemHeight.value}
defaultIndex={item.defaultIndex ?? +props.defaultIndex}
optionHeight={optionHeight.value}
swipeDuration={props.swipeDuration}
initialOptions={item[valuesKey]}
visibleItemCount={props.visibleItemCount}
onChange={() => onChange(columnIndex)}
visibleOptionNum={props.visibleOptionNum}
onChange={(value: number | string) => onChange(value, columnIndex)}
/>
));
const renderMask = (wrapHeight: number) => {
if (hasOptions.value) {
const frameStyle = { height: `${itemHeight.value}px` };
const frameStyle = { height: `${optionHeight.value}px` };
const maskStyle = {
backgroundSize: `100% ${(wrapHeight - itemHeight.value) / 2}px`,
backgroundSize: `100% ${(wrapHeight - optionHeight.value) / 2}px`,
};
return [
<div class={bem('mask')} style={maskStyle} />,
@ -357,7 +227,7 @@ export default defineComponent({
};
const renderColumns = () => {
const wrapHeight = itemHeight.value * +props.visibleItemCount;
const wrapHeight = optionHeight.value * +props.visibleOptionNum;
const columnsStyle = { height: `${wrapHeight}px` };
return (
<div
@ -371,21 +241,39 @@ export default defineComponent({
);
};
watch(() => props.columns, format, { immediate: true });
watch(
currentColumns,
(columns) => {
columns.forEach((options, index) => {
if (selectedValues.value[index] === undefined && options.length) {
selectedValues.value[index] =
getFirstEnabledOption(options)[fields.value.value];
}
});
},
{ immediate: true }
);
useExpose<PickerExpose>({
confirm,
getValues,
setValues,
getIndexes,
setIndexes,
getColumnIndex,
setColumnIndex,
getColumnValue,
setColumnValue,
getColumnValues,
setColumnValues,
});
watch(
() => props.modelValue,
(newValues) => {
if (!isValuesEqual(newValues, selectedValues.value)) {
selectedValues.value = newValues;
}
},
{ deep: true }
);
watch(
selectedValues,
(newValues) => {
if (!isValuesEqual(newValues, props.modelValue)) {
emit('update:modelValue', selectedValues.value);
}
},
{ deep: true }
);
useExpose<PickerExpose>({ confirm });
return () => (
<div class={bem()}>

View File

@ -1,18 +1,21 @@
import { ref, watch, reactive, defineComponent, type InjectionKey } from 'vue';
import {
ref,
watchEffect,
defineComponent,
type PropType,
type InjectionKey,
} from 'vue';
// Utils
import { deepClone } from '../utils/deep-clone';
import {
clamp,
isObject,
unknownProp,
numericProp,
makeArrayProp,
makeNumberProp,
preventDefault,
createNamespace,
makeRequiredProp,
} from '../utils';
import { getElementTranslateY, findIndexOfEnabledOption } from './utils';
// Composables
import { useParent } from '@vant/use';
@ -20,42 +23,36 @@ import { useTouch } from '../composables/use-touch';
import { useExpose } from '../composables/use-expose';
// Types
import type { PickerOption, PickerColumnProvide } from './types';
import type {
PickerOption,
PickerFieldNames,
PickerColumnProvide,
} from './types';
const DEFAULT_DURATION = 200;
// 惯性滑动思路:
// 在手指离开屏幕时,如果和上一次 move 时的间隔小于 `MOMENTUM_LIMIT_TIME` 且 move
// 距离大于 `MOMENTUM_LIMIT_DISTANCE` 时,执行惯性滑动
const MOMENTUM_LIMIT_TIME = 300;
const MOMENTUM_LIMIT_DISTANCE = 15;
// 在手指离开屏幕时,如果和上一次 move 时的间隔小于 `MOMENTUM_TIME` 且 move
// 距离大于 `MOMENTUM_DISTANCE` 时,执行惯性滑动
const MOMENTUM_TIME = 300;
const MOMENTUM_DISTANCE = 15;
const [name, bem] = createNamespace('picker-column');
function getElementTranslateY(element: Element) {
const { transform } = window.getComputedStyle(element);
const translateY = transform.slice(7, transform.length - 1).split(', ')[5];
return Number(translateY);
}
export const PICKER_KEY: InjectionKey<PickerColumnProvide> = Symbol(name);
const isOptionDisabled = (option: PickerOption) =>
isObject(option) && option.disabled;
export default defineComponent({
name,
props: {
textKey: makeRequiredProp(String),
value: numericProp,
fields: makeRequiredProp(Object as PropType<Required<PickerFieldNames>>),
options: makeArrayProp<PickerOption>(),
readonly: Boolean,
allowHtml: Boolean,
className: unknownProp,
itemHeight: makeRequiredProp(Number),
defaultIndex: makeNumberProp(0),
optionHeight: makeRequiredProp(Number),
swipeDuration: makeRequiredProp(numericProp),
initialOptions: makeArrayProp<PickerOption>(),
visibleItemCount: makeRequiredProp(numericProp),
visibleOptionNum: makeRequiredProp(numericProp),
},
emits: ['change'],
@ -68,61 +65,34 @@ export default defineComponent({
let transitionEndTrigger: null | (() => void);
const wrapper = ref<HTMLElement>();
const state = reactive({
index: props.defaultIndex,
offset: 0,
duration: 0,
options: deepClone(props.initialOptions),
});
const currentOffset = ref(0);
const currentDuration = ref(0);
const touch = useTouch();
const count = () => state.options.length;
const count = () => props.options.length;
const baseOffset = () =>
(props.itemHeight * (+props.visibleItemCount - 1)) / 2;
(props.optionHeight * (+props.visibleOptionNum - 1)) / 2;
const adjustIndex = (index: number) => {
index = clamp(index, 0, count());
const updateValueByIndex = (index: number) => {
const enabledIndex = findIndexOfEnabledOption(props.options, index);
const offset = -enabledIndex * props.optionHeight;
for (let i = index; i < count(); i++) {
if (!isOptionDisabled(state.options[i])) return i;
}
for (let i = index - 1; i >= 0; i--) {
if (!isOptionDisabled(state.options[i])) return i;
}
};
const setIndex = (index: number, emitChange?: boolean) => {
index = adjustIndex(index) || 0;
const offset = -index * props.itemHeight;
const trigger = () => {
if (index !== state.index) {
state.index = index;
if (emitChange) {
emit('change', index);
}
const value = props.options[enabledIndex][props.fields.value];
if (value !== props.value) {
emit('change', value);
}
};
// trigger the change event after transitionend when moving
if (moving && offset !== state.offset) {
if (moving && offset !== currentOffset.value) {
transitionEndTrigger = trigger;
} else {
trigger();
}
state.offset = offset;
};
const setOptions = (options: PickerOption[]) => {
if (JSON.stringify(options) !== JSON.stringify(state.options)) {
state.options = deepClone(options);
setIndex(props.defaultIndex);
}
currentOffset.value = offset;
};
const onClickItem = (index: number) => {
@ -131,34 +101,28 @@ export default defineComponent({
}
transitionEndTrigger = null;
state.duration = DEFAULT_DURATION;
setIndex(index, true);
};
const getOptionText = (option: PickerOption) => {
if (isObject(option) && props.textKey in option) {
return option[props.textKey];
}
return option;
currentDuration.value = DEFAULT_DURATION;
updateValueByIndex(index);
};
const getIndexByOffset = (offset: number) =>
clamp(Math.round(-offset / props.itemHeight), 0, count() - 1);
clamp(Math.round(-offset / props.optionHeight), 0, count() - 1);
const momentum = (distance: number, duration: number) => {
const speed = Math.abs(distance / duration);
distance = state.offset + (speed / 0.003) * (distance < 0 ? -1 : 1);
distance =
currentOffset.value + (speed / 0.003) * (distance < 0 ? -1 : 1);
const index = getIndexByOffset(distance);
state.duration = +props.swipeDuration;
setIndex(index, true);
currentDuration.value = +props.swipeDuration;
updateValueByIndex(index);
};
const stopMomentum = () => {
moving = false;
state.duration = 0;
currentDuration.value = 0;
if (transitionEndTrigger) {
transitionEndTrigger();
@ -175,13 +139,11 @@ export default defineComponent({
if (moving) {
const translateY = getElementTranslateY(wrapper.value!);
state.offset = Math.min(0, translateY - baseOffset());
startOffset = state.offset;
} else {
startOffset = state.offset;
currentOffset.value = Math.min(0, translateY - baseOffset());
}
state.duration = 0;
currentDuration.value = 0;
startOffset = currentOffset.value;
touchStartTime = Date.now();
momentumOffset = startOffset;
transitionEndTrigger = null;
@ -199,16 +161,16 @@ export default defineComponent({
preventDefault(event, true);
}
state.offset = clamp(
currentOffset.value = clamp(
startOffset + touch.deltaY.value,
-(count() * props.itemHeight),
props.itemHeight
-(count() * props.optionHeight),
props.optionHeight
);
const now = Date.now();
if (now - touchStartTime > MOMENTUM_LIMIT_TIME) {
if (now - touchStartTime > MOMENTUM_TIME) {
touchStartTime = now;
momentumOffset = state.offset;
momentumOffset = currentOffset.value;
}
};
@ -217,23 +179,22 @@ export default defineComponent({
return;
}
const distance = state.offset - momentumOffset;
const distance = currentOffset.value - momentumOffset;
const duration = Date.now() - touchStartTime;
const allowMomentum =
duration < MOMENTUM_LIMIT_TIME &&
Math.abs(distance) > MOMENTUM_LIMIT_DISTANCE;
const startMomentum =
duration < MOMENTUM_TIME && Math.abs(distance) > MOMENTUM_DISTANCE;
if (allowMomentum) {
if (startMomentum) {
momentum(distance, duration);
return;
}
const index = getIndexByOffset(state.offset);
state.duration = DEFAULT_DURATION;
setIndex(index, true);
const index = getIndexByOffset(currentOffset.value);
currentDuration.value = DEFAULT_DURATION;
updateValueByIndex(index);
// compatible with desktop scenario
// use setTimeout to skip the click event Emitted after touchstart
// use setTimeout to skip the click event emitted after touchstart
setTimeout(() => {
moving = false;
}, 0);
@ -241,21 +202,24 @@ export default defineComponent({
const renderOptions = () => {
const optionStyle = {
height: `${props.itemHeight}px`,
height: `${props.optionHeight}px`,
};
return state.options.map((option, index: number) => {
const text = getOptionText(option);
const disabled = isOptionDisabled(option);
return props.options.map((option, index) => {
const text = option[props.fields.text];
const { disabled } = option;
const value: string | number = option[props.fields.value];
const data = {
role: 'button',
style: optionStyle,
tabindex: disabled ? -1 : 0,
class: bem('item', {
disabled,
selected: index === state.index,
}),
class: [
bem('item', {
disabled,
selected: value === props.value,
}),
option.className,
],
onClick: () => onClickItem(index),
};
@ -272,39 +236,21 @@ export default defineComponent({
});
};
const setValue = (value: string) => {
const { options } = state;
for (let i = 0; i < options.length; i++) {
if (getOptionText(options[i]) === value) {
return setIndex(i);
}
}
};
const getValue = (): PickerOption => state.options[state.index];
setIndex(state.index);
useParent(PICKER_KEY);
useExpose({
state,
setIndex,
getValue,
setValue,
setOptions,
stopMomentum,
useExpose({ stopMomentum });
watchEffect(() => {
const index = props.options.findIndex(
(option) => option[props.fields.value] === props.value
);
const enabledIndex = findIndexOfEnabledOption(props.options, index);
const offset = -enabledIndex * props.optionHeight;
currentOffset.value = offset;
});
watch(() => props.initialOptions, setOptions);
watch(
() => props.defaultIndex,
(value) => setIndex(value)
);
return () => (
<div
class={[bem(), props.className]}
class={bem()}
onTouchstart={onTouchStart}
onTouchmove={onTouchMove}
onTouchend={onTouchEnd}
@ -313,9 +259,11 @@ export default defineComponent({
<ul
ref={wrapper}
style={{
transform: `translate3d(0, ${state.offset + baseOffset()}px, 0)`,
transitionDuration: `${state.duration}ms`,
transitionProperty: state.duration ? 'all' : 'none',
transform: `translate3d(0, ${
currentOffset.value + baseOffset()
}px, 0)`,
transitionDuration: `${currentDuration.value}ms`,
transitionProperty: currentDuration.value ? 'all' : 'none',
}}
class={bem('wrapper')}
onTransitionend={stopMomentum}

View File

@ -35,13 +35,18 @@ import { Toast } from 'vant';
export default {
setup() {
const columns = ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine'];
const onConfirm = (value, index) => {
Toast(`Value: ${value}, Index: ${index}`);
const columns = [
{ text: 'Delaware', value: 'Delaware' },
{ text: 'Florida', value: 'Florida' },
{ text: 'Georqia', value: 'Georqia' },
{ text: 'Indiana', value: 'Indiana' },
{ text: 'Maine', value: 'Maine' },
];
const onConfirm = ({ selectedValues }) => {
Toast(`Value: ${selectedValues.join(',')}`);
};
const onChange = (value, index) => {
Toast(`Value: ${value}, Index: ${index}`);
const onChange = ({ selectedValues }) => {
Toast(`Value: ${selectedValues.join(',')}`);
};
const onCancel = () => Toast('Cancel');
@ -55,161 +60,6 @@ export default {
};
```
### Default Index
```html
<van-picker title="Title" :columns="columns" :default-index="2" />
```
### Multiple Columns
```html
<van-picker title="Title" :columns="columns" />
```
```js
export default {
setup() {
const columns = [
{
values: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
defaultIndex: 2,
},
{
values: ['Morning', 'Afternoon', 'Evening'],
defaultIndex: 1,
},
];
return { columns };
},
};
```
### Cascade
```html
<van-picker title="Title" :columns="columns" />
```
```js
export default {
setup() {
const columns = [
{
text: 'Zhejiang',
children: [
{
text: 'Hangzhou',
children: [{ text: 'Xihu' }, { text: 'Yuhang' }],
},
{
text: 'Wenzhou',
children: [{ text: 'Lucheng' }, { text: 'Ouhai' }],
},
],
},
{
text: 'Fujian',
children: [
{
text: 'Fuzhou',
children: [{ text: 'Gulou' }, { text: 'Taijiang' }],
},
{
text: 'Xiamen',
children: [{ text: 'Siming' }, { text: 'Haicang' }],
},
],
},
];
return { columns };
},
};
```
### Disable option
```html
<van-picker :columns="columns" />
```
```js
export default {
setup() {
const columns = [
{ text: 'Delaware', disabled: true },
{ text: 'Florida' },
{ text: 'Georqia' },
];
return { columns };
},
};
```
### Set Column Values
```html
<van-picker ref="picker" title="Title" :columns="columns" @change="onChange" />
```
```js
import { ref } from 'vue';
export default {
setup() {
const picker = ref(null);
const states = {
Group1: ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine'],
Group2: ['Alabama', 'Kansas', 'Louisiana', 'Texas'],
};
const columns = [
{ values: Object.keys(states) },
{ values: states.Group1 },
];
const onChange = (values) => {
picker.value.setColumnValues(1, states[values[0]]);
};
return {
picker,
columns,
onChange,
};
},
};
```
### Loading
When Picker columns data is acquired asynchronously, use `loading` prop to show loading prompt.
```html
<van-picker title="Title" :columns="columns" :loading="loading" />
```
```js
import { ref } from 'vue';
export default {
setup() {
const columns = ref([]);
const loading = ref(true);
setTimeout(() => {
columns.value = ['Option'];
loading.value = false;
}, 1000);
return { columns, loading };
},
};
```
### With Popup
```html
@ -236,13 +86,19 @@ import { ref } from 'vue';
export default {
setup() {
const columns = ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine'];
const columns = [
{ text: 'Delaware', value: 'Delaware' },
{ text: 'Florida', value: 'Florida' },
{ text: 'Georqia', value: 'Georqia' },
{ text: 'Indiana', value: 'Indiana' },
{ text: 'Maine', value: 'Maine' },
];
const result = ref('');
const showPicker = ref(false);
const onConfirm = (value) => {
result.value = value;
const onConfirm = ({ selectedOptions }) => {
showPicker.value = false;
fieldValue.value = selectedOptions[0].text;
};
return {
@ -255,6 +111,141 @@ export default {
};
```
### Multiple Columns
```html
<van-picker title="Title" :columns="columns" />
```
```js
export default {
setup() {
const columns = [
[
{ text: 'Monday', value: 'Monday' },
{ text: 'Tuesday', value: 'Tuesday' },
{ text: 'Wednesday', value: 'Wednesday' },
{ text: 'Thursday', value: 'Thursday' },
{ text: 'Friday', value: 'Friday' },
],
[
{ text: 'Morning', value: 'Morning' },
{ text: 'Afternoon', value: 'Afternoon' },
{ text: 'Evening', value: 'Evening' },
],
];
return { columns };
},
};
```
### Cascade
```html
<van-picker title="Title" :columns="columns" />
```
```js
export default {
setup() {
const columns = [
{
text: 'Zhejiang',
value: 'Zhejiang',
children: [
{
text: 'Hangzhou',
value: 'Hangzhou',
children: [
{ text: 'Xihu', value: 'Xihu' },
{ text: 'Yuhang', value: 'Yuhang' },
],
},
{
text: 'Wenzhou',
value: 'Wenzhou',
children: [
{ text: 'Lucheng', value: 'Lucheng' },
{ text: 'Ouhai', value: 'Ouhai' },
],
},
],
},
{
text: 'Fujian',
value: 'Fujian',
children: [
{
text: 'Fuzhou',
value: 'Fuzhou',
children: [
{ text: 'Gulou', value: 'Gulou' },
{ text: 'Taijiang', value: 'Taijiang' },
],
},
{
text: 'Xiamen',
value: 'Xiamen',
children: [
{ text: 'Siming', value: 'Siming' },
{ text: 'Haicang', value: 'Haicang' },
],
},
],
},
];
return { columns };
},
};
```
### Disable option
```html
<van-picker :columns="columns" />
```
```js
export default {
setup() {
const columns = [
{ text: 'Delaware', value: 'Delaware', disabled: true },
{ text: 'Florida', value: 'Florida' },
{ text: 'Georqia', value: 'Georqia' },
];
return { columns };
},
};
```
### Loading
When Picker columns data is acquired asynchronously, use `loading` prop to show loading prompt.
```html
<van-picker title="Title" :columns="columns" :loading="loading" />
```
```js
import { ref } from 'vue';
export default {
setup() {
const columns = ref([]);
const loading = ref(true);
setTimeout(() => {
columns.value = [{ text: 'Option', value: 'option' }];
loading.value = false;
}, 1000);
return { columns, loading };
},
};
```
### Custom Columns Field
```html
@ -299,6 +290,7 @@ export default {
const customFieldName = {
text: 'cityName',
value: 'cityName',
children: 'cities',
};
@ -316,8 +308,8 @@ export default {
| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
| columns | Columns data | _Column[]_ | `[]` |
| columns-field-names | custom columns field | _object_ | `{ text: 'text', values: 'values', children: 'children' }` |
| columns | Columns data | _PickerOption[] \| PickerOption[][]_ | `[]` |
| columns-field-names | custom columns field | _object_ | `{ text: 'text', value: 'value', children: 'children' }` |
| title | Toolbar title | _string_ | - |
| confirm-button-text | Text of confirm button | _string_ | `Confirm` |
| cancel-button-text | Text of cancel button | _string_ | `Cancel` |
@ -325,59 +317,47 @@ export default {
| loading | Whether to show loading prompt | _boolean_ | `false` |
| show-toolbar | Whether to show toolbar | _boolean_ | `true` |
| allow-html | Whether to allow HTML in option text | _boolean_ | `false` |
| default-index | Default value index of single column picker | _number \| string_ | `0` |
| item-height | Option height, supports `px` `vw` `vh` `rem` unit, default `px` | _number \| string_ | `44` |
| visible-item-count | Count of visible columns | _number \| string_ | `6` |
| option-height | Option height, supports `px` `vw` `vh` `rem` unit, default `px` | _number \| string_ | `44` |
| visible-option-num | Count of visible columns | _number \| string_ | `6` |
| swipe-duration | Duration of the momentum animationunit `ms` | _number \| string_ | `1000` |
### Events
Picker events will pass different parameters according to the columns are single or multiple
| Event | Description | Arguments |
| --- | --- | --- |
| confirm | Emitted when click confirm button | Single columncurrent valuecurrent index<br>Multiple columnscurrent valuescurrent indexes |
| cancel | Emitted when click cancel button | Single columncurrent valuecurrent index<br>Multiple columnscurrent valuescurrent indexes |
| change | Emitted when current option changed | Single columnPicker instance, current valuecurrent index<br>Multiple columnsPicker instance, current valuescolumn index |
| confirm | Emitted when the confirm button is clicked | _{ selectedValues, selectedOptions }_ |
| cancel | Emitted when the cancel button is clicked | _{ selectedValues, selectedOptions }_ |
| change | Emitted when current option is changed | _{ selectedValues, selectedOptions, columnIndex }_ |
### Slots
| Name | Description | SlotProps |
| --------------- | ---------------------------- | -------------------------- |
| toolbar `3.1.2` | Custom toolbar content | - |
| title | Custom title | - |
| confirm | Custom confirm button text | - |
| cancel | Custom cancel button text | - |
| option | Custom option content | _option: string \| object_ |
| columns-top | Custom content above columns | - |
| columns-bottom | Custom content below columns | - |
| Name | Description | SlotProps |
| --------------- | ---------------------------- | ---------------------- |
| toolbar `3.1.2` | Custom toolbar content | - |
| title | Custom title | - |
| confirm | Custom confirm button text | - |
| cancel | Custom cancel button text | - |
| option | Custom option content | _option: PickerOption_ |
| columns-top | Custom content above columns | - |
| columns-bottom | Custom content below columns | - |
### Data Structure of Column
### Data Structure of PickerOption
| Key | Description | Type |
| ------------ | ------------------------- | --------------------------- |
| values | Value of column | _Array<string \| number>_ |
| defaultIndex | Default value index | _number_ |
| className | ClassName for this column | _string \| Array \| object_ |
| children | Cascade children | _Column_ |
| Key | Description | Type |
| --------- | ------------------------- | --------------------------- |
| text | Text | _string \| number_ |
| value | Value of option | _string \| number_ |
| disabled | Whether to disable option | _boolean_ |
| children | Cascade children options | _PickerOption[]_ |
| className | ClassName for this option | _string \| Array \| object_ |
### Methods
Use [ref](https://v3.vuejs.org/guide/component-template-refs.html) to get Picker instance and call instance methods.
| Name | Description | Attribute | Return value |
| --- | --- | --- | --- |
| getValues | Get current values of all columns | - | values |
| setValues | Set current values of all columns | values | - |
| getIndexes | Get current indexes of all columns | - | indexes |
| setIndexes | Set current indexes of all columns | indexes | - |
| getColumnValue | Get current value of the column | columnIndex | value |
| setColumnValue | Set current value of the column | columnIndex, value | - |
| getColumnIndex | Get current index of the column | columnIndex | optionIndex |
| setColumnIndex | Set current index of the column | columnIndex, optionIndex | - |
| getColumnValues | Get columns data of the column | columnIndex | values |
| setColumnValues | Set columns data of the column | columnIndex, values | - |
| confirm | Stop scrolling and emit confirm event | - | - |
| Name | Description | Attribute | Return value |
| ------- | ------------------------------------- | --------- | ------------ |
| confirm | Stop scrolling and emit confirm event | - | - |
### Types
@ -390,9 +370,10 @@ import type {
PickerOption,
PickerInstance,
PickerFieldNames,
PickerObjectColumn,
PickerObjectOption,
PickerToolbarPosition,
PickerCancelEventParams,
PickerChangeEventParams,
PickerConfirmEventParams,
} from 'vant';
```

View File

@ -2,7 +2,7 @@
### 介绍
提供多个选项集合供用户选择,支持单列选择和多列级联,通常与[弹出层](#/zh-CN/popup)组件配合使用。
提供多个选项集合供用户选择,支持单列选择、多列选择和级联选择,通常与[弹出层](#/zh-CN/popup)组件配合使用。
### 引入
@ -43,13 +43,18 @@ import { Toast } from 'vant';
export default {
setup() {
const columns = ['杭州', '宁波', '温州', '绍兴', '湖州', '嘉兴', '金华'];
const onConfirm = (value, index) => {
Toast(`当前值: ${value}, 当前索引: ${index}`);
const columns = [
{ text: '杭州', value: 'Hangzhou' },
{ text: '宁波', value: 'Ningbo' },
{ text: '温州', value: 'Wenzhou' },
{ text: '绍兴', value: 'Shaoxing' },
{ text: '湖州', value: 'Huzhou' },
];
const onConfirm = ({ selectedValues }) => {
Toast(`当前值: ${selectedValues.join(',')}`);
};
const onChange = (value, index) => {
Toast(`当前值: ${value}, 当前索引: ${index}`);
const onChange = ({ selectedValues }) => {
Toast(`当前值: ${selectedValues.join(',')}`);
};
const onCancel = () => Toast('取消');
@ -63,175 +68,6 @@ export default {
};
```
### 默认选中项
单列选择时,可以通过 `default-index` 属性设置初始选中项的索引。
```html
<van-picker title="标题" :columns="columns" :default-index="2" />
```
### 多列选择
`columns` 属性可以通过对象数组的形式配置多列选择,对象中可以配置选项数据、初始选中项等,详细格式见[下方表格](#/zh-CN/picker#column-shu-ju-jie-gou)。
```html
<van-picker title="标题" :columns="columns" />
```
```js
export default {
setup() {
const columns = [
// 第一列
{
values: ['周一', '周二', '周三', '周四', '周五'],
defaultIndex: 2,
},
// 第二列
{
values: ['上午', '下午', '晚上'],
defaultIndex: 1,
},
];
return { columns };
},
};
```
### 级联选择
使用 `columns``children` 字段可以实现选项级联的效果。如果级联层级较多,推荐使用 [Cascader 级联选项组件](#/zh-CN/cascader)。
```html
<van-picker title="标题" :columns="columns" />
```
```js
export default {
setup() {
const columns = [
{
text: '浙江',
children: [
{
text: '杭州',
children: [{ text: '西湖区' }, { text: '余杭区' }],
},
{
text: '温州',
children: [{ text: '鹿城区' }, { text: '瓯海区' }],
},
],
},
{
text: '福建',
children: [
{
text: '福州',
children: [{ text: '鼓楼区' }, { text: '台江区' }],
},
{
text: '厦门',
children: [{ text: '思明区' }, { text: '海沧区' }],
},
],
},
];
return { columns };
},
};
```
> 级联选择的数据嵌套深度需要保持一致,如果部分选项没有子选项,可以使用空字符串进行占位。
### 禁用选项
选项可以为对象结构,通过设置 `disabled` 来禁用该选项。
```html
<van-picker :columns="columns" />
```
```js
export default {
setup() {
const columns = [
{ text: '杭州', disabled: true },
{ text: '宁波' },
{ text: '温州' },
];
return { columns };
},
};
```
### 动态设置选项
通过 Picker 上的实例方法可以更灵活地控制选择器,比如使用 `setColumnValues` 方法实现多列联动。
```html
<van-picker ref="picker" :columns="columns" @change="onChange" />
```
```js
import { ref } from 'vue';
export default {
setup() {
const picker = ref(null);
const cities = {
浙江: ['杭州', '宁波', '温州', '嘉兴', '湖州'],
福建: ['福州', '厦门', '莆田', '三明', '泉州'],
};
const columns = [
{ values: Object.keys(cities) },
{ values: cities['浙江'] },
];
const onChange = (values) => {
picker.value.setColumnValues(1, cities[values[0]]);
};
return {
picker,
columns,
onChange,
};
},
};
```
### 加载状态
若选择器数据是异步获取的,可以通过 `loading` 属性显示加载提示。
```html
<van-picker :columns="columns" :loading="loading" />
```
```js
import { ref } from 'vue';
export default {
setup() {
const columns = ref([]);
const loading = ref(true);
setTimeout(() => {
columns.value = ['选项'];
loading.value = false;
}, 1000);
return { columns, loading };
},
};
```
### 搭配弹出层使用
在实际场景中Picker 通常作为用于辅助表单填写,可以搭配 Popup 和 Field 实现该效果。
@ -259,13 +95,19 @@ import { ref } from 'vue';
export default {
setup() {
const columns = ['杭州', '宁波', '温州', '绍兴', '湖州', '嘉兴', '金华'];
const columns = [
{ text: '杭州', value: 'Hangzhou' },
{ text: '宁波', value: 'Ningbo' },
{ text: '温州', value: 'Wenzhou' },
{ text: '绍兴', value: 'Shaoxing' },
{ text: '湖州', value: 'Huzhou' },
];
const result = ref('');
const showPicker = ref(false);
const onConfirm = (value) => {
result.value = value;
const onConfirm = ({ selectedOptions }) => {
showPicker.value = false;
fieldValue.value = selectedOptions[0].text;
};
return {
@ -278,6 +120,151 @@ export default {
};
```
### 多列选择
`columns` 属性可以通过二维数组的形式配置多列选择。
```html
<van-picker title="标题" :columns="columns" />
```
```js
export default {
setup() {
const columns = [
// 第一列
[
{ text: '周一', value: 'Monday' },
{ text: '周二', value: 'Tuesday' },
{ text: '周三', value: 'Wednesday' },
{ text: '周四', value: 'Thursday' },
{ text: '周五', value: 'Friday' },
],
// 第二列
[
{ text: '上午', value: 'Morning' },
{ text: '下午', value: 'Afternoon' },
{ text: '晚上', value: 'Evening' },
],
];
return { columns };
},
};
```
### 级联选择
使用 `columns``children` 字段可以实现选项级联的效果。如果级联层级较多,推荐使用 [Cascader 级联选项组件](#/zh-CN/cascader)。
```html
<van-picker title="标题" :columns="columns" />
```
```js
export default {
setup() {
const columns = [
{
text: '浙江',
value: 'Zhejiang',
children: [
{
text: '杭州',
value: 'Hangzhou',
children: [
{ text: '西湖区', value: 'Xihu' },
{ text: '余杭区', value: 'Yuhang' },
],
},
{
text: '温州',
value: 'Wenzhou',
children: [
{ text: '鹿城区', value: 'Lucheng' },
{ text: '瓯海区', value: 'Ouhai' },
],
},
],
},
{
text: '福建',
value: 'Fujian',
children: [
{
text: '福州',
value: 'Fuzhou',
children: [
{ text: '鼓楼区', value: 'Gulou' },
{ text: '台江区', value: 'Taijiang' },
],
},
{
text: '厦门',
value: 'Xiamen',
children: [
{ text: '思明区', value: 'Siming' },
{ text: '海沧区', value: 'Haicang' },
],
},
],
},
];
return { columns };
},
};
```
> 级联选择的数据嵌套深度需要保持一致,如果部分选项没有子选项,可以使用空字符串进行占位。
### 禁用选项
选项可以为对象结构,通过设置 `disabled` 来禁用该选项。
```html
<van-picker :columns="columns" />
```
```js
export default {
setup() {
const columns = [
{ text: '杭州', value: 'Hangzhou', disabled: true },
{ text: '宁波', value: 'Ningbo' },
{ text: '温州', value: 'Wenzhou' },
];
return { columns };
},
};
```
### 加载状态
若选择器数据是异步获取的,可以通过 `loading` 属性显示加载提示。
```html
<van-picker :columns="columns" :loading="loading" />
```
```js
import { ref } from 'vue';
export default {
setup() {
const columns = ref([]);
const loading = ref(true);
setTimeout(() => {
columns.value = [{ text: '选项', value: 'option' }];
loading.value = false;
}, 1000);
return { columns, loading };
},
};
```
### 自定义 Columns 的结构
```html
@ -322,6 +309,7 @@ export default {
const customFieldName = {
text: 'cityName',
value: 'cityName',
children: 'cities',
};
@ -339,8 +327,8 @@ export default {
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| columns | 对象数组,配置每一列显示的数据 | _Column[]_ | `[]` |
| columns-field-names | 自定义 `columns` 结构中的字段 | _object_ | `{ text: 'text', values: 'values', children: 'children' }` |
| columns | 对象数组,配置每一列显示的数据 | _PickerOption[] \| PickerOption[][]_ | `[]` |
| columns-field-names | 自定义 `columns` 结构中的字段 | _object_ | `{ text: 'text', value: 'value', children: 'children' }` |
| title | 顶部栏标题 | _string_ | - |
| confirm-button-text | 确认按钮文字 | _string_ | `确认` |
| cancel-button-text | 取消按钮文字 | _string_ | `取消` |
@ -348,61 +336,47 @@ export default {
| loading | 是否显示加载状态 | _boolean_ | `false` |
| show-toolbar | 是否显示顶部栏 | _boolean_ | `true` |
| allow-html | 是否允许选项内容中渲染 HTML | _boolean_ | `false` |
| default-index | 单列选择时,默认选中项的索引 | _number \| string_ | `0` |
| item-height | 选项高度,支持 `px` `vw` `vh` `rem` 单位,默认 `px` | _number \| string_ | `44` |
| visible-item-count | 可见的选项个数 | _number \| string_ | `6` |
| option-height | 选项高度,支持 `px` `vw` `vh` `rem` 单位,默认 `px` | _number \| string_ | `44` |
| visible-option-num | 可见的选项个数 | _number \| string_ | `6` |
| swipe-duration | 快速滑动时惯性滚动的时长,单位 `ms` | _number \| string_ | `1000` |
### Events
当选择器有多列时,事件回调参数会返回数组。
| 事件名 | 说明 | 回调参数 |
| --- | --- | --- |
| confirm | 点击完成按钮时触发 | 单列:选中值,选中值对应的索引<br>多列:所有列选中值,所有列选中值对应的索引 |
| cancel | 点击取消按钮时触发 | 单列:选中值,选中值对应的索引<br>多列:所有列选中值,所有列选中值对应的索引 |
| change | 选项改变时触发 | 单列:选中值,选中值对应的索引<br>多列:所有列选中值,当前列对应的索引 |
| confirm | 点击完成按钮时触发 | _{ selectedValues, selectedOptions }_ |
| cancel | 点击取消按钮时触发 | _{ selectedValues, selectedOptions }_ |
| change | 选项改变时触发 | _{ selectedValues, selectedOptions, columnIndex }_ |
### Slots
| 名称 | 说明 | 参数 |
| ---------------- | ---------------------- | -------------------------- |
| toolbar `v3.1.2` | 自定义整个顶部栏的内容 | - |
| title | 自定义标题内容 | - |
| confirm | 自定义确认按钮内容 | - |
| cancel | 自定义取消按钮内容 | - |
| option | 自定义选项内容 | _option: string \| object_ |
| columns-top | 自定义选项上方内容 | - |
| columns-bottom | 自定义选项下方内容 | - |
| 名称 | 说明 | 参数 |
| ---------------- | ---------------------- | ---------------------- |
| toolbar `v3.1.2` | 自定义整个顶部栏的内容 | - |
| title | 自定义标题内容 | - |
| confirm | 自定义确认按钮内容 | - |
| cancel | 自定义取消按钮内容 | - |
| option | 自定义选项内容 | _option: PickerOption_ |
| columns-top | 自定义选项上方内容 | - |
| columns-bottom | 自定义选项下方内容 | - |
### Column 数据结构
### PickerOption 数据结构
当传入多列数据时,`columns` 为一个对象数组,数组中的每一个对象配置每一列,每一列有以下 `key`:
| 键名 | 说明 | 类型 |
| ------------ | -------------------------- | --------------------------- |
| values | 列中对应的备选值 | _Array<string \| number>_ |
| defaultIndex | 初始选中项的索引,默认为 0 | _number_ |
| className | 为对应列添加额外的类名 | _string \| Array \| object_ |
| children | 级联选项 | _Column_ |
| 键名 | 说明 | 类型 |
| --------- | ------------ | --------------------------- |
| text | 选项文字内容 | _string \| number_ |
| value | 选项对应的值 | _string \| number_ |
| disabled | 是否禁用选项 | _boolean_ |
| children | 级联选项 | _PickerOption[]_ |
| className | 选项额外类名 | _string \| Array \| object_ |
### 方法
通过 ref 可以获取到 Picker 实例并调用实例方法,详见[组件实例方法](#/zh-CN/advanced-usage#zu-jian-shi-li-fang-fa)。
| 方法名 | 说明 | 参数 | 返回值 |
| --- | --- | --- | --- |
| getValues | 获取所有列选中的值 | - | values |
| setValues | 设置所有列选中的值 | values | - |
| getIndexes | 获取所有列选中值对应的索引 | - | indexes |
| setIndexes | 设置所有列选中值对应的索引 | indexes | - |
| getColumnValue | 获取对应列选中的值 | columnIndex | value |
| setColumnValue | 设置对应列选中的值 | columnIndex, value | - |
| getColumnIndex | 获取对应列选中项的索引 | columnIndex | optionIndex |
| setColumnIndex | 设置对应列选中项的索引 | columnIndex, optionIndex | - |
| getColumnValues | 获取对应列中所有选项 | columnIndex | values |
| setColumnValues | 设置对应列中所有选项 | columnIndex, values | - |
| confirm | 停止惯性滚动并触发 confirm 事件 | - | - |
| 方法名 | 说明 | 参数 | 返回值 |
| ------- | --------------------------------- | ---- | ------ |
| confirm | 停止惯性滚动并触发 `confirm` 事件 | - | - |
### 类型定义
@ -415,9 +389,10 @@ import type {
PickerOption,
PickerInstance,
PickerFieldNames,
PickerObjectColumn,
PickerObjectOption,
PickerToolbarPosition,
PickerCancelEventParams,
PickerChangeEventParams,
PickerConfirmEventParams,
} from 'vant';
```

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import { ref } from 'vue';
import VanPicker from '..';
import VanField from '../../field';
import VanPopup from '../../popup';
import { basicColumns } from './data';
import { useTranslate } from '../../../docs/site/use-translate';
import type { PickerConfirmEventParams } from '../types';
const t = useTranslate({
'zh-CN': {
city: '城市',
withPopup: '搭配弹出层使用',
chooseCity: '选择城市',
basicColumns: basicColumns['zh-CN'],
},
'en-US': {
city: 'City',
withPopup: 'With Popup',
chooseCity: 'Choose City',
basicColumns: basicColumns['en-US'],
},
});
const showPicker = ref(false);
const fieldValue = ref('');
const onClickField = () => {
showPicker.value = true;
};
const onCancel = () => {
showPicker.value = false;
};
const onConfirm = ({ selectedOptions }: PickerConfirmEventParams) => {
showPicker.value = false;
fieldValue.value = selectedOptions[0].text as string;
};
</script>
<template>
<demo-block card :title="t('withPopup')">
<van-field
v-model="fieldValue"
is-link
readonly
:label="t('city')"
:placeholder="t('chooseCity')"
@click="onClickField"
/>
<van-popup v-model:show="showPicker" round position="bottom">
<van-picker
:title="t('title')"
:columns="t('basicColumns')"
@cancel="onCancel"
@confirm="onConfirm"
/>
</van-popup>
</demo-block>
</template>

View File

@ -1,23 +1,48 @@
export const dateColumns = {
export const basicColumns = {
'zh-CN': [
{
values: ['周一', '周二', '周三', '周四', '周五'],
defaultIndex: 2,
},
{
values: ['上午', '下午', '晚上'],
defaultIndex: 1,
},
{ text: '杭州', value: 'Hangzhou' },
{ text: '宁波', value: 'Ningbo' },
{ text: '温州', value: 'Wenzhou' },
{ text: '绍兴', value: 'Shaoxing' },
{ text: '湖州', value: 'Huzhou' },
],
'en-US': [
{
values: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
defaultIndex: 2,
},
{
values: ['Morning', 'Afternoon', 'Evening'],
defaultIndex: 1,
},
{ text: 'Delaware', value: 'Delaware' },
{ text: 'Florida', value: 'Florida' },
{ text: 'Georqia', value: 'Georqia' },
{ text: 'Indiana', value: 'Indiana' },
{ text: 'Maine', value: 'Maine' },
],
};
export const dateColumns = {
'zh-CN': [
[
{ text: '周一', value: 'Monday' },
{ text: '周二', value: 'Tuesday' },
{ text: '周三', value: 'Wednesday' },
{ text: '周四', value: 'Thursday' },
{ text: '周五', value: 'Friday' },
],
[
{ text: '上午', value: 'Morning' },
{ text: '下午', value: 'Afternoon' },
{ text: '晚上', value: 'Evening' },
],
],
'en-US': [
[
{ text: 'Monday', value: 'Monday' },
{ text: 'Tuesday', value: 'Tuesday' },
{ text: 'Wednesday', value: 'Wednesday' },
{ text: 'Thursday', value: 'Thursday' },
{ text: 'Friday', value: 'Friday' },
],
[
{ text: 'Morning', value: 'Morning' },
{ text: 'Afternoon', value: 'Afternoon' },
{ text: 'Evening', value: 'Evening' },
],
],
};
@ -25,27 +50,45 @@ export const cascadeColumns = {
'zh-CN': [
{
text: '浙江',
value: 'Zhejiang',
children: [
{
text: '杭州',
children: [{ text: '西湖区' }, { text: '余杭区' }],
value: 'Hangzhou',
children: [
{ text: '西湖区', value: 'Xihu' },
{ text: '余杭区', value: 'Yuhang' },
],
},
{
text: '温州',
children: [{ text: '鹿城区' }, { text: '瓯海区' }],
value: 'Wenzhou',
children: [
{ text: '鹿城区', value: 'Lucheng' },
{ text: '瓯海区', value: 'Ouhai' },
],
},
],
},
{
text: '福建',
value: 'Fujian',
children: [
{
text: '福州',
children: [{ text: '鼓楼区' }, { text: '台江区' }],
value: 'Fuzhou',
children: [
{ text: '鼓楼区', value: 'Gulou' },
{ text: '台江区', value: 'Taijiang' },
],
},
{
text: '厦门',
children: [{ text: '思明区' }, { text: '海沧区' }],
value: 'Xiamen',
children: [
{ text: '思明区', value: 'Siming' },
{ text: '海沧区', value: 'Haicang' },
],
},
],
},
@ -53,34 +96,52 @@ export const cascadeColumns = {
'en-US': [
{
text: 'Zhejiang',
value: 'Zhejiang',
children: [
{
text: 'Hangzhou',
children: [{ text: 'Xihu' }, { text: 'Yuhang' }],
value: 'Hangzhou',
children: [
{ text: 'Xihu', value: 'Xihu' },
{ text: 'Yuhang', value: 'Yuhang' },
],
},
{
text: 'Wenzhou',
children: [{ text: 'Lucheng' }, { text: 'Ouhai' }],
value: 'Wenzhou',
children: [
{ text: 'Lucheng', value: 'Lucheng' },
{ text: 'Ouhai', value: 'Ouhai' },
],
},
],
},
{
text: 'Fujian',
value: 'Fujian',
children: [
{
text: 'Fuzhou',
children: [{ text: 'Gulou' }, { text: 'Taijiang' }],
value: 'Fuzhou',
children: [
{ text: 'Gulou', value: 'Gulou' },
{ text: 'Taijiang', value: 'Taijiang' },
],
},
{
text: 'Xiamen',
children: [{ text: 'Siming' }, { text: 'Haicang' }],
value: 'Xiamen',
children: [
{ text: 'Siming', value: 'Siming' },
{ text: 'Haicang', value: 'Haicang' },
],
},
],
},
],
};
export const cascadeColumnsCustomKey = {
export const customKeyColumns = {
'zh-CN': [
{
cityName: '浙江',
@ -138,3 +199,16 @@ export const cascadeColumnsCustomKey = {
},
],
};
export const disabledColumns = {
'zh-CN': [
{ text: '杭州', value: 'Hangzhou', disabled: true },
{ text: '宁波', value: 'Ningbo' },
{ text: '温州', value: 'Wenzhou' },
],
'en-US': [
{ text: 'Delaware', value: 'Delaware', disabled: true },
{ text: 'Florida', value: 'Florida' },
{ text: 'Georqia', value: 'Georqia' },
],
};

View File

@ -1,146 +1,79 @@
<script setup lang="ts">
import VanPicker from '..';
import VanField from '../../field';
import VanPopup from '../../popup';
import { ref, computed } from 'vue';
import { dateColumns, cascadeColumns, cascadeColumnsCustomKey } from './data';
import { useTranslate } from '../../../docs/site/use-translate';
import WithPopup from './WithPopup.vue';
import VanPicker, {
PickerChangeEventParams,
PickerConfirmEventParams,
} from '..';
import {
dateColumns,
basicColumns,
cascadeColumns,
disabledColumns,
customKeyColumns,
} from './data';
import { Toast } from '../../toast';
import { useTranslate } from '../../../docs/site/use-translate';
const t = useTranslate({
'zh-CN': {
city: '城市',
cascade: '级联选择',
withPopup: '搭配弹出层使用',
chooseCity: '选择城市',
showToolbar: '展示顶部栏',
dateColumns: dateColumns['zh-CN'],
basicColumns: basicColumns['zh-CN'],
defaultIndex: '默认选中项',
disableOption: '禁用选项',
cascadeColumns: cascadeColumns['zh-CN'],
disabledColumns: disabledColumns['zh-CN'],
multipleColumns: '多列选择',
setColumnValues: '动态设置选项',
customChildrenKey: '自定义 Columns 结构',
customChildrenColumns: cascadeColumnsCustomKey['zh-CN'],
textColumns: [
'杭州',
'宁波',
'温州',
'绍兴',
'湖州',
'嘉兴',
'金华',
'衢州',
],
disabledColumns: [
{ text: '杭州', disabled: true },
{ text: '宁波' },
{ text: '温州' },
],
column3: {
浙江: ['杭州', '宁波', '温州', '嘉兴', '湖州'],
福建: ['福州', '厦门', '莆田', '三明', '泉州'],
},
toastContent: (value: string, index: number) =>
`当前值:${value}, 当前索引:${index}`,
customChildrenColumns: customKeyColumns['zh-CN'],
toastContent: (value: string) => `当前值:${value}`,
},
'en-US': {
city: 'City',
cascade: 'Cascade',
withPopup: 'With Popup',
chooseCity: 'Choose City',
showToolbar: 'Show Toolbar',
dateColumns: dateColumns['en-US'],
basicColumns: basicColumns['en-US'],
defaultIndex: 'Default Index',
disableOption: 'Disable Option',
cascadeColumns: cascadeColumns['en-US'],
disabledColumns: disabledColumns['en-US'],
multipleColumns: 'Multiple Columns',
setColumnValues: 'Set Column Values',
customChildrenKey: 'Custom Columns Fields',
customChildrenColumns: cascadeColumnsCustomKey['en-US'],
textColumns: ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine'],
disabledColumns: [
{ text: 'Delaware', disabled: true },
{ text: 'Florida' },
{ text: 'Georqia' },
],
column3: {
Group1: ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine'],
Group2: ['Alabama', 'Kansas', 'Louisiana', 'Texas'],
},
customChildrenColumns: customKeyColumns['en-US'],
toastContent: (value: string, index: number) =>
`Value: ${value}, Index${index}`,
},
});
const picker = ref();
const showPicker = ref(false);
const fieldValue = ref('');
const customFieldName = ref({
const customFieldName = {
text: 'cityName',
value: 'cityName',
children: 'cities',
});
const columns = computed(() => {
const column = t('column3');
return [
{
values: Object.keys(column),
className: 'column1',
},
{
values: column[Object.keys(column)[0]],
className: 'column2',
defaultIndex: 2,
},
];
});
const onChange1 = (value: string, index: number) => {
Toast(t('toastContent', value, index));
};
const onChange2 = (values: string[]) => {
picker.value.setColumnValues(1, t('column3')[values[0]]);
const onChange1 = ({ selectedValues }: PickerChangeEventParams) => {
Toast(t('toastContent', selectedValues.join(',')));
};
const onConfirm = (value: string, index: number) => {
Toast(t('toastContent', value, index));
const onConfirm = ({ selectedValues }: PickerConfirmEventParams) => {
Toast(t('toastContent', selectedValues.join(',')));
};
const onCancel = () => Toast(t('cancel'));
const onCancel2 = () => {
showPicker.value = false;
};
const onClickField = () => {
showPicker.value = true;
};
const onConfirm2 = (value: string) => {
showPicker.value = false;
fieldValue.value = value;
};
</script>
<template>
<demo-block card :title="t('basicUsage')">
<van-picker
:title="t('title')"
:columns="t('textColumns')"
:columns="t('basicColumns')"
@change="onChange1"
@confirm="onConfirm"
/>
</demo-block>
<demo-block card :title="t('defaultIndex')">
<van-picker
:title="t('title')"
:columns="t('textColumns')"
:default-index="2"
@change="onChange1"
/>
</demo-block>
<WithPopup />
<demo-block card :title="t('multipleColumns')">
<van-picker
@ -159,37 +92,10 @@ const onConfirm2 = (value: string) => {
<van-picker :title="t('title')" :columns="t('disabledColumns')" />
</demo-block>
<demo-block card :title="t('setColumnValues')">
<van-picker
ref="picker"
:title="t('title')"
:columns="columns"
@change="onChange2"
/>
</demo-block>
<demo-block card :title="t('loadingStatus')">
<van-picker loading :title="t('title')" :columns="columns" />
<van-picker loading :title="t('title')" />
</demo-block>
<demo-block card :title="t('withPopup')">
<van-field
v-model="fieldValue"
is-link
readonly
:label="t('city')"
:placeholder="t('chooseCity')"
@click="onClickField"
/>
<van-popup v-model:show="showPicker" round position="bottom">
<van-picker
:title="t('title')"
:columns="t('textColumns')"
@cancel="onCancel2"
@confirm="onConfirm2"
/>
</van-popup>
</demo-block>
<demo-block card :title="t('customChildrenKey')">
<van-picker
:title="t('title')"

View File

@ -9,9 +9,10 @@ export type {
PickerOption,
PickerInstance,
PickerFieldNames,
PickerObjectColumn,
PickerObjectOption,
PickerToolbarPosition,
PickerCancelEventParams,
PickerChangeEventParams,
PickerConfirmEventParams,
} from './types';
declare module 'vue' {

View File

@ -84,86 +84,35 @@ exports[`should render demo and match snapshot 1`] = `
</div>
</div>
<div>
<div class="van-picker">
<div class="van-picker__toolbar">
<button type="button"
class="van-picker__cancel van-haptics-feedback"
<div class="van-cell van-cell--clickable van-field"
role="button"
tabindex="0"
>
<div class="van-cell__title van-field__label">
<label id="van-field-label"
for="van-field-input"
>
Cancel
</button>
<div class="van-picker__title van-ellipsis">
Title
</div>
<button type="button"
class="van-picker__confirm van-haptics-feedback"
>
Confirm
</button>
City
</label>
</div>
<div class="van-picker__columns"
style="height: 264px;"
>
<div class="van-picker-column">
<ul style="transform: translate3d(0, 22px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
<div class="van-cell__value van-field__value">
<div class="van-field__body">
<input type="text"
id="van-field-input"
class="van-field__control"
readonly
placeholder="Choose City"
aria-labelledby="van-field-label"
>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
Delaware
</div>
</li>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
Florida
</div>
</li>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item van-picker-column__item--selected"
>
<div class="van-ellipsis">
Georqia
</div>
</li>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
Indiana
</div>
</li>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
Maine
</div>
</li>
</ul>
</div>
<div class="van-picker__mask"
style="background-size: 100% 110px;"
>
</div>
<div class="van-hairline-unset--top-bottom van-picker__frame"
style="height: 44px;"
>
</div>
</div>
<i class="van-badge__wrapper van-icon van-icon-arrow van-cell__right-icon">
</i>
</div>
<transition-stub>
</transition-stub>
<transition-stub>
</transition-stub>
</div>
<div>
<div class="van-picker">
@ -186,13 +135,13 @@ exports[`should render demo and match snapshot 1`] = `
style="height: 264px;"
>
<div class="van-picker-column">
<ul style="transform: translate3d(0, 22px, 0); transition-duration: 0ms; transition-property: none;"
<ul style="transform: translate3d(0, 110px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
class="van-picker-column__item van-picker-column__item--selected"
>
<div class="van-ellipsis">
Monday
@ -210,7 +159,7 @@ exports[`should render demo and match snapshot 1`] = `
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item van-picker-column__item--selected"
class="van-picker-column__item"
>
<div class="van-ellipsis">
Wednesday
@ -237,13 +186,13 @@ exports[`should render demo and match snapshot 1`] = `
</ul>
</div>
<div class="van-picker-column">
<ul style="transform: translate3d(0, 66px, 0); transition-duration: 0ms; transition-property: none;"
<ul style="transform: translate3d(0, 110px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
class="van-picker-column__item van-picker-column__item--selected"
>
<div class="van-ellipsis">
Morning
@ -252,7 +201,7 @@ exports[`should render demo and match snapshot 1`] = `
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item van-picker-column__item--selected"
class="van-picker-column__item"
>
<div class="van-ellipsis">
Afternoon
@ -447,112 +396,6 @@ exports[`should render demo and match snapshot 1`] = `
</div>
</div>
</div>
<div>
<div class="van-picker">
<div class="van-picker__toolbar">
<button type="button"
class="van-picker__cancel van-haptics-feedback"
>
Cancel
</button>
<div class="van-picker__title van-ellipsis">
Title
</div>
<button type="button"
class="van-picker__confirm van-haptics-feedback"
>
Confirm
</button>
</div>
<div class="van-picker__columns"
style="height: 264px;"
>
<div class="van-picker-column column1">
<ul style="transform: translate3d(0, 110px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item van-picker-column__item--selected"
>
<div class="van-ellipsis">
Group1
</div>
</li>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
Group2
</div>
</li>
</ul>
</div>
<div class="van-picker-column column2">
<ul style="transform: translate3d(0, 22px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
Delaware
</div>
</li>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
Florida
</div>
</li>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item van-picker-column__item--selected"
>
<div class="van-ellipsis">
Georqia
</div>
</li>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
Indiana
</div>
</li>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
Maine
</div>
</li>
</ul>
</div>
<div class="van-picker__mask"
style="background-size: 100% 110px;"
>
</div>
<div class="van-hairline-unset--top-bottom van-picker__frame"
style="height: 44px;"
>
</div>
</div>
</div>
</div>
<div>
<div class="van-picker">
<div class="van-picker__toolbar">
@ -587,123 +430,15 @@ exports[`should render demo and match snapshot 1`] = `
<div class="van-picker__columns"
style="height: 264px;"
>
<div class="van-picker-column column1">
<div class="van-picker-column">
<ul style="transform: translate3d(0, 110px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item van-picker-column__item--selected"
>
<div class="van-ellipsis">
Group1
</div>
</li>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
Group2
</div>
</li>
</ul>
</div>
<div class="van-picker-column column2">
<ul style="transform: translate3d(0, 22px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
Delaware
</div>
</li>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
Florida
</div>
</li>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item van-picker-column__item--selected"
>
<div class="van-ellipsis">
Georqia
</div>
</li>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
Indiana
</div>
</li>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
Maine
</div>
</li>
</ul>
</div>
<div class="van-picker__mask"
style="background-size: 100% 110px;"
>
</div>
<div class="van-hairline-unset--top-bottom van-picker__frame"
style="height: 44px;"
>
</div>
</div>
</div>
</div>
<div>
<div class="van-cell van-cell--clickable van-field"
role="button"
tabindex="0"
>
<div class="van-cell__title van-field__label">
<label id="van-field-label"
for="van-field-input"
>
City
</label>
</div>
<div class="van-cell__value van-field__value">
<div class="van-field__body">
<input type="text"
id="van-field-input"
class="van-field__control"
readonly
placeholder="Choose City"
aria-labelledby="van-field-label"
>
</div>
</div>
<i class="van-badge__wrapper van-icon van-icon-arrow van-cell__right-icon">
</i>
</div>
<transition-stub>
</transition-stub>
<transition-stub>
</transition-stub>
</div>
<div>
<div class="van-picker">
<div class="van-picker__toolbar">

View File

@ -1,179 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`column watch default index 1`] = `
<div class="van-picker-column">
<ul style="transform: translate3d(0, 50px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
<li role="button"
style="height: 50px;"
tabindex="-1"
class="van-picker-column__item van-picker-column__item--disabled"
>
<div class="van-ellipsis">
1
</div>
</li>
<li role="button"
style="height: 50px;"
tabindex="0"
class="van-picker-column__item van-picker-column__item--selected"
>
<div class="van-ellipsis">
1990
</div>
</li>
<li role="button"
style="height: 50px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
1991
</div>
</li>
<li role="button"
style="height: 50px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
1992
</div>
</li>
<li role="button"
style="height: 50px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
1993
</div>
</li>
<li role="button"
style="height: 50px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
1994
</div>
</li>
<li role="button"
style="height: 50px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
1995
</div>
</li>
</ul>
</div>
`;
exports[`column watch default index 2`] = `
<div class="van-picker-column">
<ul style="transform: translate3d(0, 0px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
<li role="button"
style="height: 50px;"
tabindex="-1"
class="van-picker-column__item van-picker-column__item--disabled"
>
<div class="van-ellipsis">
1
</div>
</li>
<li role="button"
style="height: 50px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
1990
</div>
</li>
<li role="button"
style="height: 50px;"
tabindex="0"
class="van-picker-column__item van-picker-column__item--selected"
>
<div class="van-ellipsis">
1991
</div>
</li>
<li role="button"
style="height: 50px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
1992
</div>
</li>
<li role="button"
style="height: 50px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
1993
</div>
</li>
<li role="button"
style="height: 50px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
1994
</div>
</li>
<li role="button"
style="height: 50px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
1995
</div>
</li>
</ul>
</div>
`;
exports[`columns-top、columns-bottom prop 1`] = `
<div class="van-picker">
<div class="van-picker__toolbar">
<button type="button"
class="van-picker__cancel van-haptics-feedback"
>
Cancel
</button>
<button type="button"
class="van-picker__confirm van-haptics-feedback"
>
Confirm
</button>
</div>
Custom Columns Top
<div class="van-picker__columns"
style="height: 264px;"
>
<div class="van-picker-column">
<ul style="transform: translate3d(0, 110px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
</ul>
</div>
</div>
Custom Columns Bottom
</div>
`;
exports[`not allow html 1`] = `
exports[`should not allow to render html text 1`] = `
<div class="van-picker">
<div class="van-picker__toolbar">
<button type="button"
@ -217,235 +44,7 @@ exports[`not allow html 1`] = `
</div>
`;
exports[`render option slot with object columns 1`] = `
<div class="van-picker">
<div class="van-picker__toolbar">
<button type="button"
class="van-picker__cancel van-haptics-feedback"
>
Cancel
</button>
<button type="button"
class="van-picker__confirm van-haptics-feedback"
>
Confirm
</button>
</div>
<div class="van-picker__columns"
style="height: 264px;"
>
<div class="van-picker-column">
<ul style="transform: translate3d(0, 110px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item van-picker-column__item--selected"
>
<div class="van-ellipsis">
foo
</div>
</li>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
bar
</div>
</li>
</ul>
</div>
<div class="van-picker__mask"
style="background-size: 100% 110px;"
>
</div>
<div class="van-hairline-unset--top-bottom van-picker__frame"
style="height: 44px;"
>
</div>
</div>
</div>
`;
exports[`render option slot with simple columns 1`] = `
<div class="van-picker">
<div class="van-picker__toolbar">
<button type="button"
class="van-picker__cancel van-haptics-feedback"
>
Cancel
</button>
<button type="button"
class="van-picker__confirm van-haptics-feedback"
>
Confirm
</button>
</div>
<div class="van-picker__columns"
style="height: 264px;"
>
<div class="van-picker-column">
<ul style="transform: translate3d(0, 110px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item van-picker-column__item--selected"
>
foo
</li>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
>
bar
</li>
</ul>
</div>
<div class="van-picker__mask"
style="background-size: 100% 110px;"
>
</div>
<div class="van-hairline-unset--top-bottom van-picker__frame"
style="height: 44px;"
>
</div>
</div>
</div>
`;
exports[`set rem item-height 1`] = `
<div class="van-picker">
<div class="van-picker__toolbar">
<button type="button"
class="van-picker__cancel van-haptics-feedback"
>
Cancel
</button>
<button type="button"
class="van-picker__confirm van-haptics-feedback"
>
Confirm
</button>
</div>
<div class="van-picker__columns"
style="height: 960px;"
>
<div class="van-picker-column">
<ul style="transform: translate3d(0, 400px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
<li role="button"
style="height: 160px;"
tabindex="0"
class="van-picker-column__item van-picker-column__item--selected"
>
<div class="van-ellipsis">
1990
</div>
</li>
<li role="button"
style="height: 160px;"
tabindex="0"
class="van-picker-column__item"
>
<div class="van-ellipsis">
1991
</div>
</li>
</ul>
</div>
<div class="van-picker__mask"
style="background-size: 100% 400px;"
>
</div>
<div class="van-hairline-unset--top-bottom van-picker__frame"
style="height: 160px;"
>
</div>
</div>
</div>
`;
exports[`should render confirm/cancel slot correctly 1`] = `
<div class="van-picker">
<div class="van-picker__toolbar">
<button type="button"
class="van-picker__cancel van-haptics-feedback"
>
Custom Cancel
</button>
<button type="button"
class="van-picker__confirm van-haptics-feedback"
>
Custom Confirm
</button>
</div>
<div class="van-picker__columns"
style="height: 264px;"
>
<div class="van-picker-column">
<ul style="transform: translate3d(0, 110px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
</ul>
</div>
</div>
</div>
`;
exports[`should render title slot correctly 1`] = `
<div class="van-picker">
<div class="van-picker__toolbar">
<button type="button"
class="van-picker__cancel van-haptics-feedback"
>
Cancel
</button>
Custom title
<button type="button"
class="van-picker__confirm van-haptics-feedback"
>
Confirm
</button>
</div>
<div class="van-picker__columns"
style="height: 264px;"
>
<div class="van-picker-column">
<ul style="transform: translate3d(0, 110px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
</ul>
</div>
</div>
</div>
`;
exports[`should render toolbar slot correctly 1`] = `
<div class="van-picker">
<div class="van-picker__toolbar">
Custom toolbar
</div>
<div class="van-picker__columns"
style="height: 264px;"
>
<div class="van-picker-column">
<ul style="transform: translate3d(0, 110px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
</ul>
</div>
</div>
</div>
`;
exports[`toolbar-position prop 1`] = `
exports[`should render bottom toolbar when toolbar-position is bottom 1`] = `
<div class="van-picker">
<div class="van-picker__columns"
style="height: 264px;"

View File

@ -0,0 +1,152 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render columns-top、columns-bottom slots correctly 1`] = `
<div class="van-picker">
<div class="van-picker__toolbar">
<button type="button"
class="van-picker__cancel van-haptics-feedback"
>
Cancel
</button>
<button type="button"
class="van-picker__confirm van-haptics-feedback"
>
Confirm
</button>
</div>
Custom Columns Top
<div class="van-picker__columns"
style="height: 264px;"
>
<div class="van-picker-column">
<ul style="transform: translate3d(0, 110px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
</ul>
</div>
</div>
Custom Columns Bottom
</div>
`;
exports[`should render confirm/cancel slot correctly 1`] = `
<div class="van-picker">
<div class="van-picker__toolbar">
<button type="button"
class="van-picker__cancel van-haptics-feedback"
>
Custom Cancel
</button>
<button type="button"
class="van-picker__confirm van-haptics-feedback"
>
Custom Confirm
</button>
</div>
<div class="van-picker__columns"
style="height: 264px;"
>
<div class="van-picker-column">
<ul style="transform: translate3d(0, 110px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
</ul>
</div>
</div>
</div>
`;
exports[`should render option slot correctly 1`] = `
<div class="van-picker">
<div class="van-picker__toolbar">
<button type="button"
class="van-picker__cancel van-haptics-feedback"
>
Cancel
</button>
<button type="button"
class="van-picker__confirm van-haptics-feedback"
>
Confirm
</button>
</div>
<div class="van-picker__columns"
style="height: 264px;"
>
<div class="van-picker-column">
<ul style="transform: translate3d(0, 110px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item van-picker-column__item--selected"
>
Custom 1990
</li>
<li role="button"
style="height: 44px;"
tabindex="0"
class="van-picker-column__item"
>
Custom 1991
</li>
</ul>
</div>
<div class="van-picker__mask"
style="background-size: 100% 110px;"
>
</div>
<div class="van-hairline-unset--top-bottom van-picker__frame"
style="height: 44px;"
>
</div>
</div>
</div>
`;
exports[`should render title slot correctly 1`] = `
<div class="van-picker">
<div class="van-picker__toolbar">
<button type="button"
class="van-picker__cancel van-haptics-feedback"
>
Cancel
</button>
Custom title
<button type="button"
class="van-picker__confirm van-haptics-feedback"
>
Confirm
</button>
</div>
<div class="van-picker__columns"
style="height: 264px;"
>
<div class="van-picker-column">
<ul style="transform: translate3d(0, 110px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
</ul>
</div>
</div>
</div>
`;
exports[`should render toolbar slot correctly 1`] = `
<div class="van-picker">
<div class="van-picker__toolbar">
Custom toolbar
</div>
<div class="van-picker__columns"
style="height: 264px;"
>
<div class="van-picker-column">
<ul style="transform: translate3d(0, 110px, 0); transition-duration: 0ms; transition-property: none;"
class="van-picker-column__wrapper"
>
</ul>
</div>
</div>
</div>
`;

View File

@ -1,20 +1,16 @@
import { later, mount, triggerDrag } from '../../../test';
import { Picker } from '..';
import PickerColumn from '../PickerColumn';
const simpleColumn = ['1990', '1991', '1992', '1993', '1994', '1995'];
const columns = [
{
values: ['vip', 'normal'],
className: 'column1',
},
{
values: simpleColumn,
className: 'column2',
},
const simpleColumn = [
{ text: '1990', value: '1990' },
{ text: '1991', value: '1991' },
{ text: '1992', value: '1992' },
{ text: '1993', value: '1993' },
{ text: '1994', value: '1994' },
{ text: '1995', value: '1995' },
];
test('simple columns confirm & cancel event', () => {
test('should emit confirm event after clicking the confirm button', () => {
const wrapper = mount(Picker, {
props: {
showToolbar: true,
@ -23,89 +19,32 @@ test('simple columns confirm & cancel event', () => {
});
wrapper.find('.van-picker__confirm').trigger('click');
wrapper.find('.van-picker__cancel').trigger('click');
expect(wrapper.emitted('confirm')![0]).toEqual(['1990', 0]);
expect(wrapper.emitted('cancel')![0]).toEqual(['1990', 0]);
wrapper.unmount();
});
test('multiple columns confirm & cancel event', () => {
const wrapper = mount(Picker, {
props: {
showToolbar: true,
columns,
expect(wrapper.emitted('confirm')![0]).toEqual([
{
selectedOptions: [{ text: '1990', value: '1990' }],
selectedValues: ['1990'],
},
});
wrapper.find('.van-picker__confirm').trigger('click');
wrapper.find('.van-picker__cancel').trigger('click');
const params = [
['vip', '1990'],
[0, 0],
];
expect(wrapper.emitted('confirm')![0]).toEqual(params);
expect(wrapper.emitted('cancel')![0]).toEqual(params);
});
test('set picker values', () => {
const wrapper = mount(Picker, {
props: {
columns,
},
});
const vm = wrapper.vm as Record<string, any>;
expect(vm.getColumnValues(-1)).toEqual(undefined);
expect(vm.getColumnValues(1)).toHaveLength(6);
expect(vm.getColumnValue(1)).toEqual('1990');
vm.setColumnValue(0, 'normal');
expect(vm.getColumnValue(0)).toEqual('normal');
vm.setColumnIndex(0, 0);
expect(vm.getColumnValue(0)).toEqual('vip');
vm.setColumnValue(1, '1991');
expect(vm.getColumnValue(1)).toEqual('1991');
vm.setColumnValues(0, ['vip', 'normal', 'other']);
expect(vm.getColumnValues(0)).toHaveLength(3);
expect(vm.getValues()).toHaveLength(2);
vm.setColumnValues(-1, []);
expect(vm.getValues()).toHaveLength(2);
vm.setValues(['vip', '1992']);
expect(vm.getColumnIndex(1)).toEqual(2);
expect(vm.getColumnIndex(2)).toEqual(undefined);
expect(vm.getIndexes(2)).toEqual([0, 2]);
vm.setIndexes([1, 4]);
expect(vm.getColumnValue(1)).toEqual('1994');
expect(vm.getColumnValue(2)).toEqual(undefined);
});
test('drag columns', () => {
const wrapper = mount(Picker, {
props: {
columns,
},
});
triggerDrag(wrapper.find('.van-picker-column'), 0, -100);
wrapper.find('.van-picker-column ul').trigger('transitionend');
// 由于在极短的时间(大约几毫秒)移动 `100px`,因此再计算惯性滚动的距离时,
// 会得到一个很大的值,导致会滚动到且选中列表的最后一项
expect(wrapper.emitted<[Array<string>, number]>('change')![0][0]).toEqual([
'normal',
'1990',
]);
});
test('drag simple columns', () => {
test('should emit cancel event after clicking the cancel button', () => {
const wrapper = mount(Picker, {
props: {
showToolbar: true,
columns: simpleColumn,
},
});
wrapper.find('.van-picker__cancel').trigger('click');
expect(wrapper.emitted('cancel')![0]).toEqual([
{
selectedOptions: [{ text: '1990', value: '1990' }],
selectedValues: ['1990'],
},
]);
});
test('should emit change event after draging the column', () => {
const wrapper = mount(Picker, {
props: {
columns: simpleColumn,
@ -115,121 +54,21 @@ test('drag simple columns', () => {
triggerDrag(wrapper.find('.van-picker-column'), 0, -100);
wrapper.find('.van-picker-column ul').trigger('transitionend');
// 由于在极短的时间(大约几毫秒)移动 `100px`,因此再计算惯性滚动的距离时,
// 会得到一个很大的值,导致会滚动到且选中列表的最后一项
expect(wrapper.emitted<[string, number]>('change')![0][0]).toEqual('1995');
expect(wrapper.emitted('change')).toEqual([
[
{
columnIndex: 0,
selectedOptions: [{ text: '1995', value: '1995' }],
selectedValues: ['1995'],
},
],
]);
});
test('column watch default index', async () => {
const disabled = { disabled: true, text: 1 };
const wrapper = mount(PickerColumn, {
props: {
initialOptions: [disabled, ...simpleColumn],
textKey: 'text',
itemHeight: 50,
visibleItemCount: 5,
swipeDuration: 1000,
},
} as any);
await later();
expect(wrapper.html()).toMatchSnapshot();
await wrapper.setProps({
defaultIndex: 2,
});
expect(wrapper.html()).toMatchSnapshot();
});
test('should render title slot correctly', () => {
const wrapper = mount(Picker, {
slots: {
title: () => 'Custom title',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
test('should render toolbar slot correctly', () => {
const wrapper = mount(Picker, {
slots: {
toolbar: () => 'Custom toolbar',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
test('should render confirm/cancel slot correctly', () => {
const wrapper = mount(Picker, {
slots: {
confirm: () => 'Custom Confirm',
cancel: () => 'Custom Cancel',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
test('render option slot with simple columns', () => {
const wrapper = mount(Picker, {
props: {
columns: ['foo', 'bar'],
showToolbar: true,
},
slots: {
option: (item) => item,
},
});
expect(wrapper.html()).toMatchSnapshot();
});
test('render option slot with object columns', () => {
const wrapper = mount(Picker, {
props: {
columns: [{ text: 'foo' }, { text: 'bar' }],
showToolbar: true,
},
slots: {
options: (item) => item,
},
});
expect(wrapper.html()).toMatchSnapshot();
});
test('simulation finger swipe again before transitionend', () => {
// mock getComputedStyle
// see: https://github.com/jsdom/jsdom/issues/2588
const originGetComputedStyle = window.getComputedStyle;
window.getComputedStyle = (ele) => {
const style = originGetComputedStyle(ele);
return {
...style,
transform: 'matrix(1, 0, 0, 1, 0, -5)',
};
};
const wrapper = mount(Picker, {
props: {
columns: simpleColumn,
},
});
triggerDrag(wrapper.find('.van-picker-column'), 0, -5);
triggerDrag(wrapper.find('.van-picker-column'), -5, -100);
wrapper.find('.van-picker-column ul').trigger('transitionend');
expect(wrapper.emitted<[string, number]>('change')![0][0]).toEqual('1995');
});
test('click column item', () => {
test('should emit change event when after clicking a option', async () => {
const columns = [
{ text: '杭州' },
{ text: '宁波' },
{ text: '温州', disabled: true },
{ text: '嘉兴', disabled: true },
{ text: 'A', value: 'A' },
{ text: 'B', value: 'B' },
];
const wrapper = mount(Picker, {
props: {
@ -237,13 +76,50 @@ test('click column item', () => {
},
});
wrapper.findAll('.van-picker-column__item')[3].trigger('click');
expect(wrapper.emitted<[string, number]>('change')![0][0]).toEqual(
columns[1]
);
await wrapper.findAll('.van-picker-column__item')[1].trigger('click');
expect(wrapper.emitted('change')).toEqual([
[
{
columnIndex: 0,
selectedOptions: [{ text: 'B', value: 'B' }],
selectedValues: ['B'],
},
],
]);
});
test('toolbar-position prop', () => {
test('should not emit change event if modelValue is not changed', async () => {
const columns = [
{ text: 'A', value: 'A' },
{ text: 'B', value: 'B' },
];
const wrapper = mount(Picker, {
props: {
modelValue: ['B'],
columns,
},
});
await wrapper.findAll('.van-picker-column__item')[1].trigger('click');
expect(wrapper.emitted('change')).toBeFalsy();
});
test('should not emit change event when after clicking a disabled option', async () => {
const columns = [
{ text: 'A', value: 'A' },
{ text: 'B', value: 'B', disabled: true },
];
const wrapper = mount(Picker, {
props: {
columns,
},
});
await wrapper.findAll('.van-picker-column__item')[1].trigger('click');
expect(wrapper.emitted<[string, number]>('change')).toBeFalsy();
});
test('should render bottom toolbar when toolbar-position is bottom', () => {
const wrapper = mount(Picker, {
props: {
showToolbar: true,
@ -254,66 +130,67 @@ test('toolbar-position prop', () => {
expect(wrapper.html()).toMatchSnapshot();
});
test('not allow html', () => {
test('should not allow to render html text', () => {
const wrapper = mount(Picker, {
props: {
allowHtml: false,
columns: ['<div>option</div>'],
columns: [{ text: '<div>option</div>' }],
},
});
expect(wrapper.html()).toMatchSnapshot();
});
test('columns-top、columns-bottom prop', () => {
test('should allow to update columns props dynamically', async () => {
const wrapper = mount(Picker, {
props: {
showToolbar: true,
},
slots: {
'columns-top': () => 'Custom Columns Top',
'columns-bottom': () => 'Custom Columns Bottom',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
test('watch columns change', async () => {
const wrapper = mount(Picker, {
props: {
showToolbar: true,
columns: ['1', '2'],
defaultIndex: 1,
modelValue: ['2'],
columns: [
{ text: '1', value: '1' },
{ text: '2', value: '2' },
],
},
});
await wrapper.setProps({
columns: ['2', '3'],
columns: [
{ text: '2', value: '2' },
{ text: '3', value: '3' },
],
});
wrapper.find('.van-picker__confirm').trigger('click');
expect(wrapper.emitted<[string, number]>('confirm')![0]).toEqual(['3', 1]);
expect(wrapper.emitted<[string, number]>('confirm')![0]).toEqual([
{ selectedOptions: [{ text: '2', value: '2' }], selectedValues: ['2'] },
]);
});
test('should not reset index when columns unchanged', async () => {
const wrapper = mount(Picker, {
props: {
modelValue: ['2'],
showToolbar: true,
columns: ['1', '2'],
columns: [
{ text: '1', value: '1' },
{ text: '2', value: '2' },
],
},
});
(wrapper.vm as Record<string, any>).setIndexes([1]);
await wrapper.setProps({
columns: ['1', '2'],
columns: [
{ text: '1', value: '1' },
{ text: '2', value: '2' },
],
});
wrapper.find('.van-picker__confirm').trigger('click');
expect(wrapper.emitted<[string, number]>('confirm')![0]).toEqual(['2', 1]);
await wrapper.find('.van-picker__confirm').trigger('click');
expect(wrapper.emitted<[string, number]>('confirm')![0]).toEqual([
{ selectedOptions: [{ text: '2', value: '2' }], selectedValues: ['2'] },
]);
});
test('set rem item-height', async () => {
test('should allow to set rem option height', async () => {
const originGetComputedStyle = window.getComputedStyle;
window.getComputedStyle = () => ({ fontSize: '16px' } as CSSStyleDeclaration);
@ -321,27 +198,29 @@ test('set rem item-height', async () => {
const wrapper = mount(Picker, {
props: {
columns: simpleColumn.slice(0, 2),
itemHeight: '10rem',
optionHeight: '10rem',
},
});
await later();
expect(wrapper.html()).toMatchSnapshot();
expect(wrapper.find('.van-picker-column__item').style.height).toEqual(
'160px'
);
window.getComputedStyle = originGetComputedStyle;
});
test('readonly prop', () => {
test('should not allow to change option when using readonly prop', async () => {
const wrapper = mount(Picker, {
props: {
columns,
columns: simpleColumn,
readonly: true,
},
});
triggerDrag(wrapper.find('.van-picker-column'), 0, -100);
wrapper.find('.van-picker-column ul').trigger('transitionend');
wrapper.findAll('.van-picker-column__item')[3].trigger('click');
await wrapper.find('.van-picker-column ul').trigger('transitionend');
await wrapper.findAll('.van-picker-column__item')[3].trigger('click');
expect(wrapper.emitted('change')).toBeFalsy();
});
@ -349,7 +228,7 @@ test('readonly prop', () => {
test('should not render mask and frame when options is empty', async () => {
const wrapper = mount(Picker, {
props: {
columns: [{ values: [] }],
columns: [[], []],
},
});
expect(wrapper.find('.van-picker__mask').exists()).toBeFalsy();

View File

@ -0,0 +1,64 @@
import { mount } from '../../../test';
import { Picker, PickerOption } from '..';
const simpleColumn = [
{ text: '1990', value: '1990' },
{ text: '1991', value: '1991' },
];
test('should render title slot correctly', () => {
const wrapper = mount(Picker, {
slots: {
title: () => 'Custom title',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
test('should render toolbar slot correctly', () => {
const wrapper = mount(Picker, {
slots: {
toolbar: () => 'Custom toolbar',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
test('should render confirm/cancel slot correctly', () => {
const wrapper = mount(Picker, {
slots: {
confirm: () => 'Custom Confirm',
cancel: () => 'Custom Cancel',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
test('should render option slot correctly', () => {
const wrapper = mount(Picker, {
props: {
columns: simpleColumn,
},
slots: {
option: (option: PickerOption) => `Custom ${option.text}`,
},
});
expect(wrapper.html()).toMatchSnapshot();
});
test('should render columns-top、columns-bottom slots correctly', () => {
const wrapper = mount(Picker, {
props: {
showToolbar: true,
},
slots: {
'columns-top': () => 'Custom Columns Top',
'columns-bottom': () => 'Custom Columns Bottom',
},
});
expect(wrapper.html()).toMatchSnapshot();
});

View File

@ -6,42 +6,24 @@ export type PickerToolbarPosition = 'top' | 'bottom';
export type PickerFieldNames = {
text?: string;
values?: string;
value?: string;
children?: string;
};
export type PickerObjectOption = {
export type PickerOption = {
text?: string | number;
value?: string | number;
disabled?: boolean;
// for custom filed names
[key: PropertyKey]: any;
};
export type PickerOption = string | number | PickerObjectOption;
export type PickerObjectColumn = {
values?: PickerOption[];
children?: PickerColumn;
className?: unknown;
defaultIndex?: number;
// for custom filed names
[key: PropertyKey]: any;
};
export type PickerColumn = PickerOption[] | PickerObjectColumn;
export type PickerColumn = PickerOption[];
export type PickerExpose = {
confirm: () => void;
getValues: <T = PickerOption>() => T[];
setValues: (values: string[]) => void;
getIndexes: () => number[];
setIndexes: (indexes: number[]) => void;
getColumnIndex: (index: number) => number;
setColumnIndex: (columnIndex: number, optionIndex: number) => void;
getColumnValue: <T = PickerOption>(index: number) => T;
setColumnValue: (index: number, value: string) => void;
getColumnValues: <T = PickerOption>(index: number) => T[];
setColumnValues: (index: number, options: PickerOption[]) => void;
};
export type PickerColumnProvide = {
@ -59,3 +41,14 @@ export type PickerColumnProvide = {
};
export type PickerInstance = ComponentPublicInstance<PickerProps, PickerExpose>;
export type PickerConfirmEventParams = {
selectedValues: Array<number | string>;
selectedOptions: PickerOption[];
};
export type PickerCancelEventParams = PickerConfirmEventParams;
export type PickerChangeEventParams = PickerConfirmEventParams & {
columnIndex: number;
};

View File

@ -0,0 +1,110 @@
import { isDef, clamp, extend } from '../utils';
import type { Ref } from 'vue';
import type { PickerOption, PickerColumn, PickerFieldNames } from './types';
export function getFirstEnabledOption(options: PickerOption[]) {
return options.find((option) => !option.disabled) || options[0];
}
export function getColumnsType(
columns: PickerColumn | PickerColumn[],
fields: Required<PickerFieldNames>
) {
const firstColumn = columns[0];
if (firstColumn) {
if (Array.isArray(firstColumn)) {
return 'multiple';
}
if (fields.children in firstColumn) {
return 'cascade';
}
}
return 'default';
}
export function findIndexOfEnabledOption(
options: PickerOption[],
index: number
) {
index = clamp(index, 0, options.length);
for (let i = index; i < options.length; i++) {
if (!options[i].disabled) return i;
}
for (let i = index - 1; i >= 0; i--) {
if (!options[i].disabled) return i;
}
return 0;
}
export function findOptionByValue(
options: PickerOption[],
value: number | string,
fields: Required<PickerFieldNames>
) {
const index = options.findIndex((option) => option[fields.value] === value);
const enabledIndex = findIndexOfEnabledOption(options, index);
return options[enabledIndex];
}
export function formatCascadeColumns(
columns: PickerColumn | PickerColumn[],
fields: Required<PickerFieldNames>,
selectedValues: Ref<Array<number | string>>
) {
const formatted: PickerColumn[] = [];
let cursor: PickerOption | undefined = {
[fields.children]: columns,
};
let columnIndex = 0;
while (cursor && cursor[fields.children]) {
const options: PickerOption[] = cursor[fields.children];
const value = selectedValues.value[columnIndex];
cursor = isDef(value)
? findOptionByValue(options, value, fields)
: undefined;
if (!cursor && options.length) {
const firstValue = getFirstEnabledOption(options)[fields.value];
cursor = findOptionByValue(options, firstValue, fields);
}
columnIndex++;
formatted.push(options);
}
return formatted;
}
export function getElementTranslateY(element: Element) {
const { transform } = window.getComputedStyle(element);
const translateY = transform.slice(7, transform.length - 1).split(', ')[5];
return Number(translateY);
}
export function isValuesEqual(
valuesA: Array<string | number>,
valuesB: Array<string | number>
) {
return (
valuesA.length === valuesB.length &&
valuesA.every((value, index) => value === valuesB[index])
);
}
export function assignDefaultFields(
fields: PickerFieldNames | undefined
): Required<PickerFieldNames> {
return extend(
{
text: 'text',
value: 'value',
children: 'children',
},
fields
);
}