Tabs
WTabs takes WTabsItem children and renders a header of tab buttons above the active tab's content. Each item needs a unique name; title, icon and count go on its button, the count as a badge. divider draws a line under the buttons. The first tab opens unless initTab, initTabIndex or an item's init says otherwise.
Password, two-factor authentication and active sessions.
Tabs keep the height of the tallest one opened so far, so the page below doesn't jump.
Current tab: security
<template>
<WTabs
:init-tab="current"
divider
@update:current="current = $event"
>
<WTabsItem
name="profile"
title="Profile"
:icon="markRaw(IconUser)"
>
<p>Name, avatar and contact details.</p>
</WTabsItem>
<WTabsItem
name="security"
title="Security"
:icon="markRaw(IconLock)"
>
<p>Password, two-factor authentication and active sessions.</p>
<p>Tabs keep the height of the tallest one opened so far, so the page below doesn't jump.</p>
</WTabsItem>
<WTabsItem
name="notifications"
title="Notifications"
:icon="markRaw(IconAlarm)"
:count="3"
>
<p>Three unread notifications.</p>
</WTabsItem>
<WTabsItem
name="billing"
title="Billing"
disabled
>
<p>Not available on the free plan.</p>
</WTabsItem>
</WTabs>
<p class="text-description mt-4 text-sm">
Current tab: {{ current }}
</p>
</template>
<script lang="ts" setup>
import {markRaw, ref} from 'vue'
import WTabs from 'eco-vue-js/dist/components/Tabs/WTabs.vue'
import WTabsItem from 'eco-vue-js/dist/components/Tabs/WTabsItem.vue'
import IconAlarm from 'eco-vue-js/dist/assets/icons/IconAlarm'
import IconLock from 'eco-vue-js/dist/assets/icons/IconLock'
import IconUser from 'eco-vue-js/dist/assets/icons/IconUser'
const current = ref('security')
</script>Switching slides the content in from the side of the new tab, and the content area keeps the height of the tallest tab opened so far, so what's below it doesn't jump — disableMinHeight turns that off, lessTransitions fades instead of sliding.
The slide moves content its full width plus --inner-margin past the edge of the tabs, and WTabs doesn't clip it: the container decides where it shows. Spanning the page, the content slides in from the screen edge. Inside a card, a modal or a column, give the container overflow-x-clip — unlike overflow-hidden, it keeps sticky elements inside working. A header wider than its space scrolls sideways and keeps the active tab in view.
update:current and update:current-index emit the active tab, and update:current-title its title — for putting it in a page header when the tabs' own header is hidden with noHeader. To switch from code, call updateCurrent(name) or updateIndex(index) on a template ref. The first update:current comes during setup, so when the parent shows the current tab, seed it and pass it as initTab, as the demo does — otherwise a server-rendered page shows a different value than the first client render.
Tabs as routes
Give each WTabsItem a to, and the route opens the tab: the one whose location has the current route's path, whatever the query and hash. The tab buttons become links that replace the history entry, so Back leaves the page instead of stepping through tabs, and they open in a new browser tab on a middle click. Anything else that links to a tab's location — a summary card above the tabs, say, with filters in the query — switches to it, and updateCurrent navigates. initTab is only used when no tab matches the route.
<WTabs>
<WTabsItem name="findings" :to="{name: 'asset', params: {assetId, tab: 'findings'}}" />
<WTabsItem name="settings" :to="{name: 'asset', params: {assetId, tab: 'settings'}}" />
</WTabs>Closable and added tabs
A tab with a close listener gets a close button. Children that aren't WTabsItem render in the header after the tab buttons, which is where an add button goes; switchToNew opens a tab as soon as it is added. When the active tab is removed, the one before it opens.
Contents of Query 1.
<template>
<WTabs switch-to-new>
<WTabsItem
v-for="tab in tabs"
:key="tab.id"
:name="String(tab.id)"
:title="tab.title"
v-bind="tabs.length > 1 ? {onClose: () => remove(tab.id)} : {}"
>
<p>Contents of {{ tab.title }}.</p>
</WTabsItem>
<WButton
:semantic-type="SemanticType.SECONDARY"
class="ml-2 self-center"
@click="add"
>
<IconAdd class="square-4" />
</WButton>
</WTabs>
</template>
<script lang="ts" setup>
import {ref} from 'vue'
import {SemanticType} from 'eco-vue-js/dist/utils/SemanticType'
import WButton from 'eco-vue-js/dist/components/Button/WButton.vue'
import WTabs from 'eco-vue-js/dist/components/Tabs/WTabs.vue'
import WTabsItem from 'eco-vue-js/dist/components/Tabs/WTabsItem.vue'
import IconAdd from 'eco-vue-js/dist/assets/icons/IconAdd'
let nextId = 3
const tabs = ref([
{id: 1, title: 'Query 1'},
{id: 2, title: 'Query 2'},
])
const add = () => {
tabs.value.push({id: nextId, title: `Query ${ nextId }`})
nextId++
}
const remove = (id: number) => {
tabs.value = tabs.value.filter(tab => tab.id !== id)
}
</script>Side layout
side puts the tab buttons in a column to the left of the content, w-tabs-side-width-* sets the column's maximum width. On phones the column and the content become two full-width screens side by side, and picking a tab scrolls to its content.
Workspace name, language and time zone.
<template>
<WTabs
side
class="w-tabs-side-width-48"
>
<WTabsItem
name="general"
title="General"
:icon="markRaw(IconSettings)"
>
<p>Workspace name, language and time zone.</p>
</WTabsItem>
<WTabsItem
name="members"
title="Members"
:icon="markRaw(IconUser)"
:count="12"
>
<p>Twelve members, two of them admins.</p>
</WTabsItem>
<WTabsItem
name="integrations"
title="Integrations"
:icon="markRaw(IconLink)"
>
<p>Connected services and API tokens.</p>
</WTabsItem>
</WTabs>
</template>
<script lang="ts" setup>
import {markRaw} from 'vue'
import WTabs from 'eco-vue-js/dist/components/Tabs/WTabs.vue'
import WTabsItem from 'eco-vue-js/dist/components/Tabs/WTabsItem.vue'
import IconLink from 'eco-vue-js/dist/assets/icons/IconLink'
import IconSettings from 'eco-vue-js/dist/assets/icons/IconSettings'
import IconUser from 'eco-vue-js/dist/assets/icons/IconUser'
</script>WButtonTab is the tab button on its own, for a button among the tab buttons that isn't a tab — such as "New chat" at the top of a side column of chats. With side it lines up with the side column's buttons; title, icon and count show as on a tab, and it emits click.
WTabsColumns shows its WTabsItem items as tabs on phones and as columns side by side from sm up, e.g. for the two halves of a form in a modal.
Tabs in forms
An item's hasError, hasChanges and hasValue mark its button — statusIcon shows them as icons, showHasValue marks tabs with a value. When a tab gets an error while another is open, the tabs switch to it; noSwitchOnInvalid turns that off. Inside a Uniform form the tab items pick these states up from their fields on their own.
stepper turns the tabs into steps: the tabs after the first one with hasValue set to false are disabled, and next(), previous() and jump(name) on a template ref move between steps. Before next() and jump(name) leave a step, validate on its item runs, then the Uniform fields inside it are checked: invalid fields show their errors, a warning lists them, and the step stays open. update:progress, update:first and update:last emit where the stepper is, for the buttons around it.
stepperControls lets the stepper bring those buttons itself: Back, or on the first step in an overlay Close — Cancel once something is changed — and Next, or the submit on the last, named by submitText. submitting shows a spinner in it while a submit on submit runs, and disabledNext disables it. The texts come from setTexts (Getting started). The submit checks the last step and submits the Uniform form with api-method around it, or emits submit. In an overlay frame, or in a WModalWrapper on a page, the step's title, a progress line and the buttons go to the frame's title, subtitle and actions — the form's own title, if it has one, stays instead of the step's. A frame shows one stepper: the first one to mount takes it, and another one, such as a stepper inside a step, keeps its progress and buttons in itself.
API
WTabs
import WTabs from 'eco-vue-js/dist/components/Tabs/WTabs.vue'Props
| Prop | Type | Default | Description |
|---|---|---|---|
customSlots | VNode[] | — | Tab items rendered instead of the default slot. |
lessTransitions | boolean | — | Fades between tabs instead of sliding. |
initTab | string | — | Name of the tab opened first. |
initTabIndex | number | — | Index of the tab opened first, when initTab is not set. |
side | boolean | — | Puts the tab buttons in a column beside the content. On small screens the buttons and the content become two swipeable screens, and picking a tab scrolls to its content. |
disableMinHeight | boolean | — | Lets the content shrink to the current tab's height. By default it keeps the height of the tallest tab shown so far. |
noHeader | boolean | — | Hides the tab buttons. Switch tabs through the exposed methods. |
headerClass | string | — | Classes for the row of tab buttons. |
switchToNew | boolean | — | Switches to a tab when it is added. |
stepper | boolean | — | Numbers the tab titles and disables every tab after the first one with hasValue false. The exposed next and jump check the Uniform fields of the tab they leave and stay on it if one is invalid. Enables update:progress, update:first and update:last. |
stepperControls | boolean | — | With stepper, brings its own controls: the open step's title, a progress line, and Back, Next and submit buttons — Close on the first step in an overlay. In a frame, such as a modal or a page WModalWrapper, they go to its title, subtitle and actions, under a title of the form's own if there is one; the first stepper in the frame takes it. The submit checks the last step and submits the enclosing form with api-method, or emits submit. |
submitText | string | — | Text of the submit button on the last step with stepperControls. Defaults to the submit text of setTexts. |
submitting | boolean | — | With stepperControls, shows a spinner in the Next or submit button and disables Back and Close, such as while a submit on submit runs. A form with api-method around the stepper does it on its own. |
disabledNext | boolean | — | With stepperControls, disables the Next or submit button, such as until something is picked on the step. |
showHasValue | boolean | — | Colors the titles of tabs that have a value. |
noSwitchOnInvalid | boolean | — | Stays on the current tab when another one gets an error. By default the first tab with an error is opened. |
wrap | boolean | — | Wraps the tab buttons onto new lines instead of scrolling sideways. |
divider | boolean | — | Draws a line under the row of tab buttons, which the open tab's underline covers. |
statusIcon | boolean | — | Shows a value and error status icon next to each title. |
flat | boolean | — | Renders all tabs one after another, each under its title, without the buttons. |
indicator | boolean | — | Shows a large status circle on each tab button — error, has value or empty. |
Events
| Event | Payload | Description |
|---|---|---|
update:current | (value: string) | Name of the open tab. Also emitted on mount. |
update:current-index | (value: number) | Index of the open tab. Also emitted on mount. |
update:has-changes | (value: boolean) | Whether any tab has unsaved changes. |
update:current-title | (value: string | undefined) | Title of the open tab, also when the title itself changes. |
update:tabs-length | (value: number) | Number of tabs. Also emitted on mount. |
update:progress | (value: number) | With stepper, the share of steps reached, in percent. |
update:first | (value: boolean) | With stepper, whether the first tab is open. |
update:last | (value: boolean) | With stepper, whether the last tab is open. |
submit | — | With stepperControls, the submit button was clicked on the last step and the step is valid. Not emitted inside a form with api-method, which is submitted instead. |
Slots
| Slot | Props | Description |
|---|---|---|
default | — | WTabsItem elements. Anything else is rendered among the tab buttons. |
WTabsItem
import WTabsItem from 'eco-vue-js/dist/components/Tabs/WTabsItem.vue'Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | — | Title of the tab button. The title slot replaces it. |
name | string | required | Unique key of the tab, used by initTab, update:current and the exposed methods. |
icon | SVGComponent | — | Icon before the title. |
disabled | boolean | — | Disables the tab button. |
removable | boolean | — | Unmounts the content while the tab is not open, instead of hiding it. |
init | boolean | — | Opens this tab first, when WTabs has no initTab or initTabIndex. |
hasValue | boolean | null | — | Marks the tab as filled in. By default it is read from the forms inside the tab. |
hasError | boolean | — | Marks the tab as invalid. By default it is read from the forms inside the tab. |
hasChanges | boolean | — | Shows the unsaved changes dot. By default it is read from the forms inside the tab. |
validate | (() => string | undefined) | — | Checks the tab before the exposed next and jump leave it. A returned error message is shown as a warning and the tab stays open. |
requireSave | boolean | — | Submits the enclosing stepper form before moving forward past this tab, and stays on it if the submit fails. |
count | number | — | Number shown in a badge after the title, tinted while the tab is open. |
to | RouteLocationRaw | — | Router location of the tab. The tab button becomes a link that replaces the history entry. While any tab has one, the route decides the open tab — the one whose location has the current route's path, whatever the query and hash — and the exposed methods navigate to the tab instead of opening it. |
Events
| Event | Payload | Description |
|---|---|---|
close | — | The close button was clicked. Listening to it adds the button to the tab. |
Slots
| Slot | Props | Description |
|---|---|---|
default | — | Content of the tab. |
title | StateScope | Replaces the content of the tab button. |
suffix | StateScope | Content after the title, inside the tab button. |
right | StateScope | Content after the tab button. |
WButtonTab
import WButtonTab from 'eco-vue-js/dist/components/Button/WButtonTab.vue'Props
| Prop | Type | Default | Description |
|---|---|---|---|
active | boolean | — | Marks the button as the open tab: primary text and an underline. |
hasError | boolean | — | Colors the button red, over the other states. |
hasValue | boolean | — | Marks the tab as filled in, for showHasValue, statusIcon and indicator. |
hasChanges | boolean | — | Shows the unsaved changes dot. |
disabled | boolean | — | Grays the button out and ignores clicks. |
icon | SVGComponent | — | Icon before the title. |
title | string | — | Text of the button. The title slot replaces it. |
indicator | boolean | — | Shows a large status circle — error, has value or empty — before the title. |
side | boolean | — | Aligns the title to the start, for a column of tab buttons. |
statusIcon | boolean | — | Shows a value and error status icon after the title. |
showHasValue | boolean | — | Colors the title green when hasValue is set and the tab isn't open. |
enableOverflow | boolean | — | Scrolls a title that doesn't fit into view on hover. |
count | number | — | Number shown in a badge after the title, tinted while the tab is open. |
to | RouteLocationRaw | — | Router location — renders a link that replaces the current history entry, unless disabled. |
Events
| Event | Payload | Description |
|---|---|---|
click | (value: MouseEvent) | The button was clicked, unless it is disabled. |
Slots
| Slot | Props | Description |
|---|---|---|
title | { hasChanges?: boolean | undefined; hasError?: boolean | undefined; hasValue?: boolean | undefined; } | Content of the button, replacing the title, icon, count and status icon. |
suffix | { hasChanges?: boolean | undefined; hasError?: boolean | undefined; hasValue?: boolean | undefined; } | Content after the title, such as a close button. |
WTabsColumns
import WTabsColumns from 'eco-vue-js/dist/components/Tabs/WTabsColumns.vue'Props
No props.
Slots
| Slot | Props | Description |
|---|---|---|
default | — | WTabsItem items: tabs on phones, and columns side by side from sm up. |