-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
Changes from 2 commits
0d07c59
2b7eeeb
c0c2437
10c39b2
a610322
f55a1da
febeb2c
4cd4be2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
<script setup lang="ts"> | ||
import { useStepper } from '@vueuse/core' | ||
import { reactive } from 'vue' | ||
|
||
const { index, current, next, steps, backTo, currentStepIs, isFirst, isLast, todo } = useStepper([ | ||
'Billing address', | ||
'Terms', | ||
'Payment', | ||
] as const) | ||
|
||
const form = reactive({ | ||
billingAddress: '', | ||
contract: false, | ||
carbonOffsetting: false, | ||
payment: 'credit-card' as 'paypal' | 'credit-card', | ||
}) | ||
|
||
const wizard = reactive({ | ||
index, | ||
current, | ||
steps, | ||
isFirst, | ||
isLast, | ||
}) | ||
|
||
function canGoNext() { | ||
if (currentStepIs('Billing address') && form.billingAddress === '') | ||
return false | ||
|
||
if (currentStepIs('Terms') && !form.contract) | ||
return false | ||
|
||
return true | ||
} | ||
|
||
function submit() { | ||
if (!canGoNext()) | ||
return | ||
|
||
next() | ||
} | ||
</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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { useStepper } from '.' | ||
|
||
describe('useStepper', () => { | ||
it('should be defined', () => { | ||
expect(useStepper).toBeDefined() | ||
}) | ||
|
||
it('can navigate through steps', () => { | ||
const { isFirst, isLast, next, previous, current, index, goTo, backTo, isPrevious, isNext, currentStepIs, currentStepIsAfter, currentStepIsBefore } = 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(currentStepIs('First step')).toBe(true) | ||
expect(currentStepIs('Second step')).toBe(false) | ||
expect(currentStepIsAfter('Second step')).toBe(false) | ||
|
||
next() | ||
expect(index.value).toBe(1) | ||
expect(current.value).toBe('Second step') | ||
expect(isPrevious('First step')).toBe(true) | ||
expect(isNext('Last step')).toBe(true) | ||
expect(currentStepIsAfter('First step')).toBe(true) | ||
expect(currentStepIsBefore('First step')).toBe(false) | ||
expect(currentStepIsBefore('Last step')).toBe(true) | ||
|
||
previous() | ||
expect(index.value).toBe(0) | ||
expect(current.value).toBe('First step') | ||
|
||
backTo('Last step') // we can't go back to a future step | ||
expect(index.value).toBe(0) | ||
expect(current.value).toBe('First step') | ||
|
||
goTo('Last step') // we can force a step | ||
expect(index.value).toBe(2) | ||
expect(current.value).toBe('Last step') | ||
expect(isLast.value).toBe(true) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { computed, ref } from 'vue-demi' | ||
|
||
export function useStepper<T extends string>(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) | ||
|
||
function backTo(step: T) { | ||
if (currentStepIsAfter(step)) | ||
goTo(step) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer to have this check on usage. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm sorry, I don't understand. On usage of what? To clarify, I needed this feature when trying to allow to click on the step names, but only when it has been done, as you can see on the demo: <button :disabled="todo(step)" @click="backTo(step)" v-text="step" /> There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would feel a bit weird to me that: const s = useStepper([1,2,3,4])
s.goTo(2)
// ...
s.backTo(3)
// and s stays on 2 In your case, the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But If you think it's weird maybe you just need to use Maybe we can rename There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, what I mean is the guard should be done on userland when calling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand - I removed it. That being said, I still think this function is very useful, since I used it in all my wizards without exception - and I updated the demo code to show you how an userland But I respect your decision if you still think it's not needed! |
||
|
||
function goTo(step: T) { | ||
index.value = steps.indexOf(step) | ||
} | ||
|
||
function next() { | ||
if (isLast.value) | ||
return | ||
|
||
index.value++ | ||
} | ||
|
||
function previous() { | ||
if (isFirst.value) | ||
return | ||
|
||
index.value-- | ||
} | ||
innocenzi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
function isNext(step: T) { | ||
return steps.indexOf(step) === index.value + 1 | ||
} | ||
|
||
function isPrevious(step: T) { | ||
return steps.indexOf(step) === index.value - 1 | ||
} | ||
antfu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
function currentStepIs(step: T) { | ||
return steps.indexOf(step) === index.value | ||
} | ||
|
||
function currentStepIsAfter(step: T) { | ||
return index.value > steps.indexOf(step) | ||
} | ||
|
||
function currentStepIsBefore(step: T) { | ||
return index.value < steps.indexOf(step) | ||
} | ||
innocenzi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
function completed(step: T) { | ||
return currentStepIsAfter(step) | ||
} | ||
|
||
function active(step: T) { | ||
return currentStepIs(step) | ||
} | ||
|
||
function todo(step: T) { | ||
return currentStepIsBefore(step) | ||
} | ||
antfu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return { | ||
backTo, | ||
goTo, | ||
current, | ||
index, | ||
next, | ||
previous, | ||
steps, | ||
currentStepIs, | ||
currentStepIsAfter, | ||
currentStepIsBefore, | ||
completed, | ||
active, | ||
todo, | ||
isFirst, | ||
isLast, | ||
isNext, | ||
isPrevious, | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we have it arbitrary? I might want to use objects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I don't see a reason why it should only be strings. I use that mostly but you're right that objects can be useful. Also added basic tests for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@antfu Do you have a suggestion of how to use
currentStepIs
and other functions with objects? Partial match?