Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(useStepper): new function #1679

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/contributors.json
Expand Up @@ -155,6 +155,7 @@
"josephfh",
"joaoeudes7",
"xvaara",
"DrJume",
"Julien-Martin",
"kidonng",
"kirklin",
Expand Down
1 change: 1 addition & 0 deletions packages/core/index.ts
Expand Up @@ -120,6 +120,7 @@ export * from './useWebWorkerFn'
export * from './useWindowFocus'
export * from './useWindowScroll'
export * from './useWindowSize'
export * from './useStepper'
export * from './types'
export * from '@vueuse/shared'
export * from './ssr-handlers'
111 changes: 111 additions & 0 deletions packages/core/useStepper/demo.vue
@@ -0,0 +1,111 @@
<script setup lang="ts">
import { useStepper } from '@vueuse/core'
import type { UnwrapRef } from 'vue'
import { reactive } from 'vue'

const { index, current, goToNext, goTo, steps, currentStepIs, isFirst, isLast, isStepBefore: todo, isStepAfter: completed, nextStep, previousStep } = useStepper([
'Billing address',
'Terms',
'Payment',
] as const)

function backTo(step: UnwrapRef<typeof current>) {
if (completed(step))
goTo(step)
}

const form = reactive({
billingAddress: '',
contract: false,
carbonOffsetting: false,
payment: 'credit-card' as 'paypal' | 'credit-card',
})

const wizard = reactive({
index,
current,
steps,
isFirst,
isLast,
nextStep,
previousStep,
})

function canGoNext() {
if (currentStepIs('Billing address') && form.billingAddress === '')
return false

if (currentStepIs('Terms') && !form.contract)
return false

return true
}

function submit() {
if (!canGoNext())
return

goToNext()
}
</script>

<template>
<div>
<div class="flex gap-2 justify-center">
<div v-for="step in steps" :key="step" class="">
<button :disabled="todo(step)" @click="backTo(step)" v-text="step" />
</div>
</div>

<form class="mt-10" @submit.prevent="submit">
<span class="text-lg font-bold" v-text="current" />
<div class="flex gap-4 items-center">
<div v-if="currentStepIs('Billing address')">
<input v-model="form.billingAddress" type="text">
</div>

<div v-if="currentStepIs('Terms')">
<div>
<input id="carbon-offsetting" v-model="form.carbonOffsetting" type="checkbox" class="mr-2">
<label for="carbon-offsetting">I accept to deposit a carbon offsetting fee</label>
</div>
<div>
<input id="contract" v-model="form.contract" type="checkbox" class="mr-2">
<label for="contract">I accept the terms of the contract</label>
</div>
</div>

<div v-if="currentStepIs('Payment')">
<div>
<input id="credit-card" v-model="form.payment" type="radio" class="mr-2" value="credit-card">
<label for="credit-card">Credit card</label>
</div>
<div>
<input id="paypal" v-model="form.payment" type="radio" class="mr-2" value="paypal">
<label for="paypal">PayPal</label>
</div>
</div>

<button v-if="!isLast" :disabled="!canGoNext()">
Next
</button>

<button v-if="isLast" :disabled="!canGoNext()">
Submit
</button>
</div>
</form>

<div class="flex gap-4 mt-12">
<div class="px-4 py-2 rounded border border-main space-y-2">
<span class="font-bold">Form</span>
<pre v-text="form" />
</div>

<div class="px-4 py-2 rounded border border-main space-y-2">
<span class="font-bold">Wizard</span>
<pre v-text="wizard" />
</div>
</div>
</div>
</template>
15 changes: 15 additions & 0 deletions packages/core/useStepper/index.md
@@ -0,0 +1,15 @@
---
category: Utilities
---

# useStepper

Provides helpers for building a multi-step wizard interface.

## Usage

```js
import { useStepper } from '@vueuse/core'

const { current, previous, next, steps, isFirst, isLast } = useStepper()
```
66 changes: 66 additions & 0 deletions packages/core/useStepper/index.test.ts
@@ -0,0 +1,66 @@
import { useStepper } from '.'

describe('useStepper', () => {
it('should be defined', () => {
expect(useStepper).toBeDefined()
})

it('can navigate through steps', () => {
const { isFirst, isLast, goToNext, goToPrevious, nextStep, previousStep, current, index, goTo, isPrevious, isNext, currentStepIs, isStepAfter, isStepBefore } = useStepper([
'First step',
'Second step',
'Last step',
] as const)

expect(index.value).toBe(0)
expect(current.value).toBe('First step')
expect(isFirst.value).toBe(true)
expect(nextStep.value).toBe('Second step')
expect(previousStep.value).toBeUndefined()
expect(currentStepIs('First step')).toBe(true)
expect(currentStepIs('Second step')).toBe(false)
expect(isStepAfter('Second step')).toBe(false)

goToNext()
expect(index.value).toBe(1)
expect(current.value).toBe('Second step')
expect(nextStep.value).toBe('Last step')
expect(previousStep.value).toBe('First step')
expect(isPrevious('First step')).toBe(true)
expect(isNext('Last step')).toBe(true)
expect(isStepAfter('First step')).toBe(true)
expect(isStepBefore('First step')).toBe(false)
expect(isStepBefore('Last step')).toBe(true)

goToPrevious()
expect(index.value).toBe(0)
expect(current.value).toBe('First step')

goTo('Last step') // we can force a step
expect(nextStep.value).toBeUndefined()
expect(previousStep.value).toBe('Second step')
expect(index.value).toBe(2)
expect(current.value).toBe('Last step')
expect(isLast.value).toBe(true)
})

it('can use objects as steps', () => {
const { goToNext, index, current } = useStepper([
{ id: 1, label: 'First step' },
{ id: 2, label: 'Second step' },
{ id: 3, label: 'Third step' },
])

expect(index.value).toBe(0)
expect(current.value).toMatchObject({ id: 1, label: 'First step' })

goToNext()
expect(index.value).toBe(1)
expect(current.value).toMatchObject({ id: 2, label: 'Second step' })

goToNext()
goToNext()
expect(index.value).toBe(2)
expect(current.value).toMatchObject({ id: 3, label: 'Third step' })
})
})
81 changes: 81 additions & 0 deletions packages/core/useStepper/index.ts
@@ -0,0 +1,81 @@
import { computed, ref } from 'vue-demi'

export function useStepper<T>(steps: readonly T[], initial?: T) {
const index = ref(steps.indexOf(initial ?? steps[0]))
const current = computed(() => steps[index.value])
const isFirst = computed(() => index.value === 0)
const isLast = computed(() => index.value === steps.length - 1)
const nextStep = computed<T | undefined>(() => steps[index.value + 1])
const previousStep = computed<T | undefined>(() => steps[index.value - 1])

function goTo(step: T) {
index.value = steps.indexOf(step)
}

function goToNext() {
if (isLast.value)
return

index.value++
}

function goToPrevious() {
if (isFirst.value)
return

index.value--
}

/**
* Checks if the given step is the step just after the current step.
*/
function isNext(step: T) {
return steps.indexOf(step) === index.value + 1
}

/**
* Checks if the given step is the step just before the current step.
*/
function isPrevious(step: T) {
return steps.indexOf(step) === index.value - 1
}

/**
* Checks if the current step is the same as the given step.
*/
function currentStepIs(step: T) {
return steps.indexOf(step) === index.value
}

/**
* Checks if the current step is after given step.
*/
function isStepAfter(step: T) {
return index.value > steps.indexOf(step)
}

/**
* Checks if the current step is before given step.
*/
function isStepBefore(step: T) {
return index.value < steps.indexOf(step)
}

return {
goTo,
current,
index,
goToNext,
goToPrevious,
steps,
currentStepIs,
isStepAfter,
isStepBefore,
isFirst,
isLast,
isNext,
isPrevious,
nextStep,
previousStep,
}
}