diff --git a/src/tab/README.md b/src/tab/README.md index 567c562ab..714912b34 100644 --- a/src/tab/README.md +++ b/src/tab/README.md @@ -126,7 +126,7 @@ export default { ### Sticky -In sticky mode, the tab will be fixed to top when scroll to top +In sticky mode, the tab will be fixed to top when scroll to top. ```html @@ -138,7 +138,7 @@ In sticky mode, the tab will be fixed to top when scroll to top ### Custom title -Use title slot to custom tab title +Use title slot to custom tab title. ```html @@ -151,7 +151,7 @@ Use title slot to custom tab title ### Switch Animation -Use `animated` props to change tabs with animation +Use `animated` props to change tabs with animation. ```html @@ -163,7 +163,7 @@ Use `animated` props to change tabs with animation ### Swipeable -In swipeable mode, you can switch tabs with swipe gestrue in the content +In swipeable mode, you can switch tabs with swipe gestrue in the content. ```html @@ -175,7 +175,7 @@ In swipeable mode, you can switch tabs with swipe gestrue in the content ### Scrollspy -In scrollspy mode, the list of content will be tiled +In scrollspy mode, the list of content will be tiled. ```html @@ -185,6 +185,34 @@ In scrollspy mode, the list of content will be tiled ``` +### Before Change + +```html + + + content {{ index }} + + +``` + +```js +export default { + methods: { + beforeChange(index) { + // prevent change + if (index === 1) { + return false; + } + + // async + return new Promise((resolve) => { + resolve(index !== 3); + }); + }, + }, +}; +``` + ## API ### Tabs Props @@ -209,6 +237,7 @@ In scrollspy mode, the list of content will be tiled | swipe-threshold | Set swipe tabs threshold | _number \| string_ | `4` | - | | title-active-color | Title active color | _string_ | - | | title-inactive-color | Title inactive color | _string_ | - | +| before-change `v2.9.3` | Callback function before changing tabs,return `false` to prevent change,support return Promise | _(name) => boolean \| Promise_ | - | ### Tab Props diff --git a/src/tab/README.zh-CN.md b/src/tab/README.zh-CN.md index 1c62490e3..a2ba20c5b 100644 --- a/src/tab/README.zh-CN.md +++ b/src/tab/README.zh-CN.md @@ -14,7 +14,7 @@ Vue.use(Tabs); ### 基础用法 -通过`v-model`绑定当前激活标签对应的索引值,默认情况下启用第一个标签 +通过 `v-model` 绑定当前激活标签对应的索引值,默认情况下启用第一个标签。 ```html @@ -37,7 +37,7 @@ export default { ### 通过名称匹配 -在标签指定`name`属性的情况下,`v-model`的值为当前标签的`name`(此时无法通过索引值来匹配标签) +在标签指定 `name` 属性的情况下,`v-model` 的值为当前标签的 `name`(此时无法通过索引值来匹配标签)。 ```html @@ -59,7 +59,7 @@ export default { ### 标签栏滚动 -标签数量超过 4 个时,标签栏可以在水平方向上滚动,切换时会自动将当前标签居中 +标签数量超过 4 个时,标签栏可以在水平方向上滚动,切换时会自动将当前标签居中。 ```html @@ -71,7 +71,7 @@ export default { ### 禁用标签 -设置`disabled`属性即可禁用标签。如果需要监听禁用标签的点击事件,可以在`van-tabs`上监听`disabled`事件 +设置 `disabled` 属性即可禁用标签,如果需要监听禁用标签的点击事件,可以在 `van-tabs` 上监听`disabled` 事件。 ```html @@ -95,7 +95,7 @@ export default { ### 样式风格 -`Tab`支持两种样式风格:`line`和`card`,默认为`line`样式,可以通过`type`属性修改样式风格 +`Tab` 支持两种样式风格:`line` 和`card`,默认为 `line` 样式,可以通过 `type` 属性切换样式风格。 ```html @@ -107,7 +107,7 @@ export default { ### 点击事件 -可以在`van-tabs`上绑定`click`事件,事件传参为标签对应的索引和标题 +可以在 `van-tabs` 上绑定 `click` 事件,事件传参为标签对应的标识符和标题。 ```html @@ -130,7 +130,7 @@ export default { ### 粘性布局 -通过`sticky`属性可以开启粘性布局,粘性布局下,当 Tab 滚动到顶部时会自动吸顶 +通过 `sticky` 属性可以开启粘性布局,粘性布局下,标签页滚动到顶部时会自动吸顶。 ```html @@ -142,7 +142,7 @@ export default { ### 自定义标签 -通过 title 插槽可以自定义标签内容 +通过 `title` 插槽可以自定义标签内容。 ```html @@ -155,7 +155,7 @@ export default { ### 切换动画 -通过`animated`属性可以开启切换标签内容时的转场动画 +通过 `animated` 属性可以开启切换标签内容时的转场动画。 ```html @@ -167,7 +167,7 @@ export default { ### 滑动切换 -通过`swipeable`属性可以开启滑动切换标签页 +通过 `swipeable` 属性可以开启滑动切换标签页。 ```html @@ -179,7 +179,7 @@ export default { ### 滚动导航 -通过`scrollspy`属性可以开启滚动导航模式,该模式下,内容将会平铺展示 +通过 `scrollspy` 属性可以开启滚动导航模式,该模式下,内容将会平铺展示。 ```html @@ -189,6 +189,37 @@ export default { ``` +### 异步切换 + +通过 `before-change` 属性可以在切换标签前执行特定的逻辑。 + +```html + + + 内容 {{ index }} + + +``` + +```js +export default { + methods: { + beforeChange(index) { + // 返回 false 表示阻止此次切换 + if (index === 1) { + return false; + } + + // 返回 Promise 来执行异步逻辑 + return new Promise((resolve) => { + // 在 resolve 函数中返回 true 或 false + resolve(index !== 3); + }); + }, + }, +}; +``` + ## API ### Tabs Props @@ -213,6 +244,7 @@ export default { | swipe-threshold | 滚动阈值,标签数量超过阈值时开始横向滚动 | _number \| string_ | `4` | | title-active-color | 标题选中态颜色 | _string_ | - | | title-inactive-color | 标题默认态颜色 | _string_ | - | +| before-change `v2.9.3` | 切换标签前的回调函数,返回 `false` 可阻止切换,支持返回 Promise | _(name) => boolean \| Promise_ | - | ### Tab Props diff --git a/src/tab/demo/index.vue b/src/tab/demo/index.vue index 6a72eea9e..9d24cac71 100644 --- a/src/tab/demo/index.vue +++ b/src/tab/demo/index.vue @@ -93,6 +93,14 @@ + + + + + {{ t('content') }} {{ index }} + + + @@ -112,6 +120,7 @@ export default { title10: '滚动导航', disabled: ' 已被禁用', matchByName: '通过名称匹配', + beforeChange: '异步切换', }, 'en-US': { tab: 'Tab ', @@ -127,6 +136,7 @@ export default { title10: 'Scrollspy', disabled: ' is disabled', matchByName: 'Match By Name', + beforeChange: 'Before Change', }, }, @@ -146,6 +156,16 @@ export default { onClick(index, title) { this.$toast(title); }, + + beforeChange(name) { + if (name === 1) { + return false; + } + + return new Promise((resolve) => { + resolve(name !== 3); + }); + }, }, }; diff --git a/src/tab/test/__snapshots__/demo.spec.js.snap b/src/tab/test/__snapshots__/demo.spec.js.snap index e8b7e703a..7349ac11c 100644 --- a/src/tab/test/__snapshots__/demo.spec.js.snap +++ b/src/tab/test/__snapshots__/demo.spec.js.snap @@ -318,5 +318,32 @@ exports[`renders demo correctly 1`] = ` +
+
+
+
+ + + + +
+
+
+
+
+ 内容 1 +
+ + + +
+
+
`; diff --git a/src/tab/test/index.spec.js b/src/tab/test/index.spec.js index cf1b6ec39..9eb51fba5 100644 --- a/src/tab/test/index.spec.js +++ b/src/tab/test/index.spec.js @@ -362,3 +362,51 @@ test('should not trigger rendered event when disable lazy-render', async () => { await later(); expect(onRendered).toHaveBeenCalledTimes(0); }); + +test('before-change prop', async () => { + const onChange = jest.fn(); + const wrapper = mount({ + template: ` + + Text + Text + Text + Text + Text + + `, + methods: { + onChange, + beforeChange(name) { + switch (name) { + case 1: + return false; + case 2: + return true; + case 3: + return Promise.resolve(false); + case 4: + return Promise.resolve(true); + } + }, + }, + }); + + await later(); + + const tabs = wrapper.findAll('.van-tab'); + tabs.at(1).trigger('click'); + expect(onChange).toHaveBeenCalledTimes(0); + + tabs.at(2).trigger('click'); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenLastCalledWith(2, 'title3'); + + tabs.at(3).trigger('click'); + expect(onChange).toHaveBeenCalledTimes(1); + + tabs.at(4).trigger('click'); + await later(); + expect(onChange).toHaveBeenCalledTimes(2); + expect(onChange).toHaveBeenLastCalledWith(4, 'title5'); +}); diff --git a/src/tabs/index.js b/src/tabs/index.js index d52d35eea..476024b43 100644 --- a/src/tabs/index.js +++ b/src/tabs/index.js @@ -1,5 +1,5 @@ // Utils -import { createNamespace, isDef, addUnit } from '../utils'; +import { createNamespace, isDef, addUnit, isPromise } from '../utils'; import { scrollLeftTo, scrollTopTo } from './utils'; import { route } from '../utils/router'; import { isHidden } from '../utils/dom/style'; @@ -53,6 +53,7 @@ export default createComponent({ background: String, lineWidth: [Number, String], lineHeight: [Number, String], + beforeChange: Function, titleActiveColor: String, titleInactiveColor: String, type: { @@ -266,14 +267,34 @@ export default createComponent({ } }, + callBeforeChange(name, done) { + if (this.beforeChange) { + const returnVal = this.beforeChange(name); + + if (isPromise(returnVal)) { + returnVal.then((value) => { + if (value) { + done(); + } + }); + } else if (returnVal) { + done(); + } + } else { + done(); + } + }, + // emit event when clicked onClick(item, index) { const { title, disabled, computedName } = this.children[index]; if (disabled) { this.$emit('disabled', computedName, title); } else { - this.setCurrentIndex(index); - this.scrollToCurrentContent(); + this.callBeforeChange(computedName, () => { + this.setCurrentIndex(index); + this.scrollToCurrentContent(); + }); this.$emit('click', computedName, title); route(item.$router, item); }