feat(Picker): support cascade columns (#4247)

This commit is contained in:
陈嘉涵 2020-01-26 15:35:53 +08:00
parent a49d239d69
commit c04697cc42
8 changed files with 355 additions and 105 deletions

View File

@ -80,41 +80,36 @@ export default {
};
```
### With Popup
### Cascade
```html
<van-field
readonly
clickable
label="City"
:value="value"
placeholder="Choose City"
@click="showPicker = true"
/>
<van-popup v-model="showPicker" position="bottom">
<van-picker
show-toolbar
:columns="columns"
@cancel="showPicker = false"
@confirm="onConfirm"
/>
</van-popup>
<van-picker show-toolbar title="Title" :columns="columns" />
```
```js
export default {
data() {
return {
value: '',
showPicker: false,
columns: ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine']
}
},
methods: {
onConfirm(value) {
this.value = value;
this.showPicker = false;
}
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' }]
}]
}]
};
}
};
```
@ -183,6 +178,45 @@ When Picker columns data is acquired asynchronously, use `loading` prop to show
<van-picker :columns="columns" loading />
```
### With Popup
```html
<van-field
readonly
clickable
label="City"
:value="value"
placeholder="Choose City"
@click="showPicker = true"
/>
<van-popup v-model="showPicker" position="bottom">
<van-picker
show-toolbar
:columns="columns"
@cancel="showPicker = false"
@confirm="onConfirm"
/>
</van-popup>
```
```js
export default {
data() {
return {
value: '',
showPicker: false,
columns: ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine']
}
},
methods: {
onConfirm(value) {
this.value = value;
this.showPicker = false;
}
}
};
```
## API
### Props

View File

@ -2,7 +2,7 @@
### 介绍
选择器组件通常与 [弹出层](#/zh-CN/popup) 组件配合使用
提供多个选项集合供用户选择,支持单列选择和多列级联,通常与 [弹出层](#/zh-CN/popup) 组件配合使用
### 引入
@ -17,7 +17,7 @@ Vue.use(Picker);
### 基础用法
对于单列选择器,传入数值格式的 columns 即可,同时可以监听选项改变的 change 事件
Picker 组件通过`columns`属性配置选项数据,`columns`是一个包含字符串或对象的数组
```html
<van-picker :columns="columns" @change="onChange" />
@ -42,19 +42,15 @@ export default {
### 默认选中项
单列选择器可以直接通过`default-index`属性设置初始选中项的索引
单列选择时,可以通过`default-index`属性设置初始选中项的索引
```html
<van-picker
:columns="columns"
:default-index="2"
@change="onChange"
/>
<van-picker :columns="columns" :default-index="2" />
```
### 展示顶部栏
通常选择器组件会传入`show-toolbar`属性以展示顶部操作栏,并可以监听对应的`confirm``cancel`事件
设置`show-toolbar`属性后会展示顶部操作栏,点击确认按钮触发`confirm`事件,点击取消按钮触发`cancel`事件
```html
<van-picker
@ -86,41 +82,38 @@ export default {
};
```
### 搭配弹出层使用
### 级联选择
使用`columns``children`字段可以实现选项级联的效果(从 2.4.5 版本开始支持)
```html
<van-field
readonly
clickable
label="城市"
:value="value"
placeholder="选择城市"
@click="showPicker = true"
/>
<van-popup v-model="showPicker" position="bottom">
<van-picker
show-toolbar
:columns="columns"
@cancel="showPicker = false"
@confirm="onConfirm"
/>
</van-popup>
<van-picker show-toolbar title="标题" :columns="columns" />
```
```js
export default {
data() {
return {
value: '',
showPicker: false,
columns: ['杭州', '宁波', '温州', '嘉兴', '湖州']
}
},
methods: {
onConfirm(value) {
this.value = value;
this.showPicker = false;
}
columns: [{
text: '浙江',
children: [{
text: '杭州',
children: [{ text: '西湖区' }, { text: '余杭区' }]
}, {
text: '温州',
children: [{ text: '鹿城区' }, { text: '瓯海区' }]
}]
}, {
text: '福建',
children: [{
text: '福州',
children: [{ text: '鼓楼区' }, { text: '台江区' }]
}, {
text: '厦门',
children: [{ text: '思明区' }, { text: '海沧区' }]
}]
}]
};
}
};
```
@ -191,6 +184,47 @@ export default {
<van-picker :columns="columns" loading />
```
### 搭配弹出层使用
在实际场景中Picker 通常作为用于辅助表单填写,可以搭配 Popup 和 Field 实现该效果
```html
<van-field
readonly
clickable
label="城市"
:value="value"
placeholder="选择城市"
@click="showPicker = true"
/>
<van-popup v-model="showPicker" position="bottom">
<van-picker
show-toolbar
:columns="columns"
@cancel="showPicker = false"
@confirm="onConfirm"
/>
</van-popup>
```
```js
export default {
data() {
return {
value: '',
showPicker: false,
columns: ['杭州', '宁波', '温州', '嘉兴', '湖州']
}
},
methods: {
onConfirm(value) {
this.value = value;
this.showPicker = false;
}
}
};
```
## API
### Props

58
src/picker/demo/data.js Normal file
View File

@ -0,0 +1,58 @@
export const cascadeColumns = {
'zh-CN': [
{
text: '浙江',
children: [
{
text: '杭州',
children: [{ text: '西湖区' }, { text: '余杭区' }],
},
{
text: '温州',
children: [{ text: '鹿城区' }, { text: '瓯海区' }],
},
],
},
{
text: '福建',
children: [
{
text: '福州',
children: [{ text: '鼓楼区' }, { text: '台江区' }],
},
{
text: '厦门',
children: [{ text: '思明区' }, { text: '海沧区' }],
},
],
},
],
'en-US': [
{
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' }],
},
],
},
],
};

View File

@ -12,7 +12,7 @@
/>
</demo-block>
<demo-block :title="$t('title3')">
<demo-block :title="$t('showToolbar')">
<van-picker
show-toolbar
:title="$t('title')"
@ -22,6 +22,28 @@
/>
</demo-block>
<demo-block :title="$t('cascade')">
<van-picker
show-toolbar
:title="$t('title')"
:columns="$t('cascadeColumns')"
@cancel="onCancel"
@confirm="onConfirm"
/>
</demo-block>
<demo-block :title="$t('disableOption')">
<van-picker :columns="$t('column2')" />
</demo-block>
<demo-block :title="$t('title4')">
<van-picker :columns="columns" @change="onChange2" />
</demo-block>
<demo-block :title="$t('loadingStatus')">
<van-picker loading :columns="columns" />
</demo-block>
<demo-block :title="$t('withPopup')">
<van-field
readonly
@ -40,32 +62,24 @@
/>
</van-popup>
</demo-block>
<demo-block :title="$t('title2')">
<van-picker :columns="$t('column2')" />
</demo-block>
<demo-block :title="$t('title4')">
<van-picker :columns="columns" @change="onChange2" />
</demo-block>
<demo-block :title="$t('loadingStatus')">
<van-picker loading :columns="columns" />
</demo-block>
</demo-section>
</template>
<script>
import { cascadeColumns } from './data';
export default {
i18n: {
'zh-CN': {
city: '城市',
title2: '禁用选项',
title3: '展示顶部栏',
title4: '多列联动',
defaultIndex: '默认选中项',
cascade: '级联选择',
withPopup: '搭配弹出层使用',
chooseCity: '选择城市',
showToolbar: '展示顶部栏',
defaultIndex: '默认选中项',
disableOption: '禁用选项',
cascadeColumns: cascadeColumns['zh-CN'],
column1: ['杭州', '宁波', '温州', '嘉兴', '湖州'],
column2: [
{ text: '杭州', disabled: true },
@ -80,12 +94,14 @@ export default {
},
'en-US': {
city: 'City',
title2: 'Disable Option',
title3: 'Show Toolbar',
title4: 'Multi Columns',
defaultIndex: 'Default Index',
cascade: 'Cascade',
withPopup: 'With Popup',
chooseCity: 'Choose City',
showToolbar: 'Show Toolbar',
defaultIndex: 'Default Index',
disableOption: 'Disable Option',
cascadeColumns: cascadeColumns['en-US'],
column1: ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine'],
column2: [
{ text: 'Delaware', disabled: true },

View File

@ -1,7 +1,6 @@
// Utils
import { createNamespace } from '../utils';
import { preventDefault } from '../utils/dom/event';
import { deepClone } from '../utils/deep-clone';
import { BORDER_TOP_BOTTOM, BORDER_UNSET_TOP_BOTTOM } from '../utils/constant';
import { pickerProps } from './shared';
@ -35,37 +34,101 @@ export default createComponent({
data() {
return {
children: [],
formattedColumns: [],
};
},
computed: {
simple() {
return this.columns.length && !this.columns[0].values;
dataType() {
const { columns } = this;
const firstColumn = columns[0] || {};
if (firstColumn.children) {
return 'cascade';
}
if (firstColumn.values) {
return 'object';
}
return 'text';
},
},
watch: {
columns: 'setColumns',
columns: {
handler: 'format',
immediate: true,
},
},
methods: {
setColumns() {
const columns = this.simple ? [{ values: this.columns }] : this.columns;
columns.forEach((column, index) => {
this.setColumnValues(index, deepClone(column.values));
});
format() {
const { columns, dataType } = this;
if (dataType === 'text') {
this.formattedColumns = [{ values: columns }];
} else if (dataType === 'cascade') {
this.formatCascade();
} else {
this.formattedColumns = columns;
}
},
formatCascade() {
const formatted = [];
let cursor = { children: this.columns };
while (cursor && cursor.children) {
const defaultIndex = cursor.defaultIndex || this.defaultIndex;
formatted.push({
values: cursor.children.map(item => item[this.valueKey]),
className: cursor.className,
defaultIndex,
});
cursor = cursor.children[defaultIndex];
}
this.formattedColumns = formatted;
},
emit(event) {
if (this.simple) {
if (this.dataType === 'text') {
this.$emit(event, this.getColumnValue(0), this.getColumnIndex(0));
} else {
this.$emit(event, this.getValues(), this.getIndexes());
}
},
onCascadeChange(columnIndex) {
let cursor = { children: this.columns };
const indexes = this.getIndexes();
for (let i = 0; i <= columnIndex; i++) {
cursor = cursor.children[indexes[i]];
}
while (cursor.children) {
columnIndex++;
this.setColumnValues(
columnIndex,
cursor.children.map(item => item[this.valueKey])
);
cursor = cursor.children[cursor.defaultIndex || 0];
}
},
onChange(columnIndex) {
if (this.simple) {
if (this.dataType === 'cascade') {
this.onCascadeChange(columnIndex);
}
if (this.dataType === 'text') {
this.$emit(
'change',
this,
@ -201,9 +264,7 @@ export default createComponent({
},
genColumns() {
const columns = this.simple ? [this.columns] : this.columns;
return columns.map((item, index) => (
return this.formattedColumns.map((item, columnIndex) => (
<PickerColumn
valueKey={this.valueKey}
allowHtml={this.allowHtml}
@ -212,9 +273,9 @@ export default createComponent({
defaultIndex={item.defaultIndex || this.defaultIndex}
swipeDuration={this.swipeDuration}
visibleItemCount={this.visibleItemCount}
initialOptions={this.simple ? item : item.values}
initialOptions={item.values}
onChange={() => {
this.onChange(index);
this.onChange(columnIndex);
}}
/>
));

View File

@ -63,13 +63,35 @@ exports[`renders demo correctly 1`] = `
</div>
</div>
<div>
<div role="button" tabindex="0" class="van-cell van-cell--clickable van-field">
<div class="van-cell__title van-field__label"><span>城市</span></div>
<div class="van-cell__value">
<div class="van-field__body"><input type="text" readonly="readonly" placeholder="选择城市" class="van-field__control"></div>
<div class="van-picker">
<div class="van-hairline--top-bottom van-picker__toolbar"><button type="button" class="van-picker__cancel">取消</button>
<div class="van-ellipsis van-picker__title">标题</div><button type="button" class="van-picker__confirm">确认</button>
</div>
<!---->
<div class="van-picker__columns" style="height: 220px;">
<div class="van-picker-column">
<ul class="van-picker-column__wrapper" style="transform: translate3d(0, 88px, 0); transition-duration: 0ms; transition-property: none; line-height: 44px;">
<li role="button" tabindex="0" class="van-ellipsis van-picker-column__item van-picker-column__item--selected" style="height: 44px;">浙江</li>
<li role="button" tabindex="0" class="van-ellipsis van-picker-column__item" style="height: 44px;">福建</li>
</ul>
</div>
<div class="van-picker-column">
<ul class="van-picker-column__wrapper" style="transform: translate3d(0, 88px, 0); transition-duration: 0ms; transition-property: none; line-height: 44px;">
<li role="button" tabindex="0" class="van-ellipsis van-picker-column__item van-picker-column__item--selected" style="height: 44px;">杭州</li>
<li role="button" tabindex="0" class="van-ellipsis van-picker-column__item" style="height: 44px;">温州</li>
</ul>
</div>
<div class="van-picker-column">
<ul class="van-picker-column__wrapper" style="transform: translate3d(0, 88px, 0); transition-duration: 0ms; transition-property: none; line-height: 44px;">
<li role="button" tabindex="0" class="van-ellipsis van-picker-column__item van-picker-column__item--selected" style="height: 44px;">西湖区</li>
<li role="button" tabindex="0" class="van-ellipsis van-picker-column__item" style="height: 44px;">余杭区</li>
</ul>
</div>
<div class="van-picker__mask" style="background-size: 100% 88px;"></div>
<div class="van-hairline-unset--top-bottom van-picker__frame" style="height: 44px;"></div>
</div>
<!---->
</div>
<!---->
</div>
<div>
<div class="van-picker">
@ -138,5 +160,14 @@ exports[`renders demo correctly 1`] = `
<!---->
</div>
</div>
<div>
<div role="button" tabindex="0" class="van-cell van-cell--clickable van-field">
<div class="van-cell__title van-field__label"><span>城市</span></div>
<div class="van-cell__value">
<div class="van-field__body"><input type="text" readonly="readonly" placeholder="选择城市" class="van-field__control"></div>
</div>
</div>
<!---->
</div>
</div>
`;

View File

@ -32,6 +32,9 @@ exports[`columns-top、columns-bottom prop 1`] = `
<div class="van-picker">
<div class="van-hairline--top-bottom van-picker__toolbar"><button type="button" class="van-picker__cancel">取消</button><button type="button" class="van-picker__confirm">确认</button></div>
<!---->Custom Columns Top<div class="van-picker__columns" style="height: 220px;">
<div class="van-picker-column">
<ul class="van-picker-column__wrapper" style="transform: translate3d(0, 88px, 0); transition-duration: 0ms; transition-property: none; line-height: 44px;"></ul>
</div>
<div class="van-picker__mask" style="background-size: 100% 88px;"></div>
<div class="van-hairline-unset--top-bottom van-picker__frame" style="height: 44px;"></div>
</div>Custom Columns Bottom
@ -60,6 +63,9 @@ exports[`render title slot 1`] = `
<div class="van-hairline--top-bottom van-picker__toolbar"><button type="button" class="van-picker__cancel">取消</button>Custom title<button type="button" class="van-picker__confirm">确认</button></div>
<!---->
<div class="van-picker__columns" style="height: 220px;">
<div class="van-picker-column">
<ul class="van-picker-column__wrapper" style="transform: translate3d(0, 88px, 0); transition-duration: 0ms; transition-property: none; line-height: 44px;"></ul>
</div>
<div class="van-picker__mask" style="background-size: 100% 88px;"></div>
<div class="van-hairline-unset--top-bottom van-picker__frame" style="height: 44px;"></div>
</div>
@ -72,6 +78,9 @@ exports[`toolbar-position prop 1`] = `
<!---->
<!---->
<div class="van-picker__columns" style="height: 220px;">
<div class="van-picker-column">
<ul class="van-picker-column__wrapper" style="transform: translate3d(0, 88px, 0); transition-duration: 0ms; transition-property: none; line-height: 44px;"></ul>
</div>
<div class="van-picker__mask" style="background-size: 100% 88px;"></div>
<div class="van-hairline-unset--top-bottom van-picker__frame" style="height: 44px;"></div>
</div>

View File

@ -33,13 +33,20 @@ test('multiple columns confirm & cancel event', () => {
const wrapper = mount(Picker, {
propsData: {
showToolbar: true,
columns,
},
});
wrapper.find('.van-picker__confirm').trigger('click');
wrapper.find('.van-picker__cancel').trigger('click');
expect(wrapper.emitted('confirm')[0]).toEqual([[], []]);
expect(wrapper.emitted('cancel')[0]).toEqual([[], []]);
const params = [
['vip', '1990'],
[0, 0],
];
expect(wrapper.emitted('confirm')[0]).toEqual(params);
expect(wrapper.emitted('cancel')[0]).toEqual(params);
});
test('set picker values', () => {