diff --git a/packages/ui/src/components/va-carousel/VaCarousel.vue b/packages/ui/src/components/va-carousel/VaCarousel.vue
index e59c7d0e63..7afab0f8b8 100644
--- a/packages/ui/src/components/va-carousel/VaCarousel.vue
+++ b/packages/ui/src/components/va-carousel/VaCarousel.vue
@@ -166,7 +166,7 @@ export default defineComponent({
emits: [...useStatefulEmits],
setup (props, { emit }) {
- const { valueComputed: currentSlide } = useStateful(props, emit, 'modelValue', { defaultValue: 0 })
+ const { valueComputed: currentSlide } = useStateful(props, emit, 'modelValue')
const {
goTo, next, prev,
diff --git a/packages/ui/src/components/va-form/VaForm.stories.ts b/packages/ui/src/components/va-form/VaForm.stories.ts
index c2dc959226..695db8fa44 100644
--- a/packages/ui/src/components/va-form/VaForm.stories.ts
+++ b/packages/ui/src/components/va-form/VaForm.stories.ts
@@ -23,6 +23,7 @@ import {
VaRadio,
} from '../'
import { sleep } from '../../utils/sleep'
+import { useForm } from '../../composables'
export default {
title: 'VaForm',
@@ -475,3 +476,66 @@ addText(
'This is old demo resqued to have visual tests, but we want to rewrite it eventually.',
'stale',
)
+
+export const FormDataInitialValue = () => ({
+ components: {
+ VaForm,
+ VaInput,
+ VaSelect,
+ VaDateInput,
+ VaTimeInput,
+ VaOptionList,
+ VaButton,
+ },
+ data: () => ({
+ input: 'value',
+ checkbox: true,
+ date: new Date(0),
+ time: new Date(0),
+ options: OPTIONS,
+ select: OPTIONS[0],
+ validationRules: [false],
+ }),
+ setup () {
+ const form = useForm('formEl')
+
+ return {
+ form,
+ }
+ },
+ template: `
+ [form data]:
{{ form.formData }}
+
+
+
+
+
+
+
+
+
+ Reset form
+
+ `,
+})
diff --git a/packages/ui/src/components/va-input/VaInput.stories.ts b/packages/ui/src/components/va-input/VaInput.stories.ts
index 9b1fa67267..ccd9908842 100644
--- a/packages/ui/src/components/va-input/VaInput.stories.ts
+++ b/packages/ui/src/components/va-input/VaInput.stories.ts
@@ -1,3 +1,4 @@
+import { ref } from 'vue'
import VaInputDemo from './VaInput.demo.vue'
import VaInput from './VaInput.vue'
import { expect } from '@storybook/jest'
@@ -19,6 +20,11 @@ export const Loading = () => ({
template: '',
})
+export const Stateful = () => ({
+ components: { VaInput },
+ template: '',
+})
+
export const Clearable = () => ({
components: { VaInput },
data () {
diff --git a/packages/ui/src/components/va-input/VaInput.vue b/packages/ui/src/components/va-input/VaInput.vue
index 693c5a689b..f1403ccb4a 100644
--- a/packages/ui/src/components/va-input/VaInput.vue
+++ b/packages/ui/src/components/va-input/VaInput.vue
@@ -103,7 +103,7 @@ export default defineComponent({
// input
placeholder: { type: String, default: '' },
tabindex: { type: [String, Number], default: 0 },
- modelValue: { type: [String, Number] },
+ modelValue: { type: [Number, String], default: '' },
type: { type: String as AnyStringPropType<'text' | 'password'>, default: 'text' },
inputClass: { type: String, default: '' },
pattern: { type: String },
@@ -132,7 +132,7 @@ export default defineComponent({
const input = shallowRef()
- const { valueComputed } = useStateful(props, emit, 'modelValue', { defaultValue: '' })
+ const { valueComputed } = useStateful(props, emit, 'modelValue')
const reset = () => withoutValidation(() => {
emit('update:modelValue', props.clearValue)
diff --git a/packages/ui/src/components/va-select/VaSelect.stories.ts b/packages/ui/src/components/va-select/VaSelect.stories.ts
index 85818eb690..3a337011d6 100644
--- a/packages/ui/src/components/va-select/VaSelect.stories.ts
+++ b/packages/ui/src/components/va-select/VaSelect.stories.ts
@@ -29,7 +29,7 @@ export const Validation: StoryFn = () => ({
components: { VaSelect },
data () {
- return { value: '', options: ['one', 'two', 'tree'], rules: [(v: string) => (v && v === 'one') || 'Must be one'] }
+ return { value: '', options: ['one', 'two', 'three'], rules: [(v: string) => (v && v === 'one') || 'Must be one'] }
},
template: '',
@@ -46,7 +46,7 @@ export const ImmediateValidation: StoryFn = () => ({
components: { VaSelect },
data () {
- return { value: '', options: ['one', 'two', 'tree'], rules: [(v: string) => (v && v === 'one') || 'Must be one'] }
+ return { value: '', options: ['one', 'two', 'three'], rules: [(v: string) => (v && v === 'one') || 'Must be one'] }
},
template: '',
@@ -63,7 +63,7 @@ export const DirtyValidation: StoryFn = () => ({
components: { Component: VaSelect },
data () {
- return { value: '', dirty: false, haveError: false, options: ['one', 'two', 'tree'], rules: [(v: string) => (v && v === 'one') || 'Must be one'] }
+ return { value: '', dirty: false, haveError: false, options: ['one', 'two', 'three'], rules: [(v: string) => (v && v === 'one') || 'Must be one'] }
},
template: `
@@ -104,7 +104,7 @@ export const DirtyImmediateValidation: StoryFn = () => ({
components: { Component: VaSelect },
data () {
- return { value: '', dirty: false, haveError: false, options: ['one', 'two', 'tree'], rules: [(v: string) => (v && v === 'one') || 'Must be one'] }
+ return { value: '', dirty: false, haveError: false, options: ['one', 'two', 'three'], rules: [(v: string) => (v && v === 'one') || 'Must be one'] }
},
template: `
@@ -124,3 +124,24 @@ DirtyImmediateValidation.play = async ({ canvasElement, step }) => {
expect(error).not.toBeNull()
})
}
+
+export const Autocomplete: StoryFn = () => ({
+ components: { VaSelect },
+
+ data () {
+ // Test if initial value is correctly set
+ return { value: 'one', options: ['one', 'two', 'three'] }
+ },
+
+ template: '',
+})
+
+export const AutocompleteMultiple: StoryFn = () => ({
+ components: { VaSelect },
+
+ data () {
+ return { value: ['one', 'two'], options: ['one', 'two', 'three'] }
+ },
+
+ template: '',
+})
diff --git a/packages/ui/src/components/va-select/hooks/useAutocomplete.ts b/packages/ui/src/components/va-select/hooks/useAutocomplete.ts
index 0e9f981044..2fa9dc71eb 100644
--- a/packages/ui/src/components/va-select/hooks/useAutocomplete.ts
+++ b/packages/ui/src/components/va-select/hooks/useAutocomplete.ts
@@ -17,6 +17,11 @@ export const useAutocomplete = (
) => {
const getLastOptionText = (v: SelectOption[]) => v?.length ? getText(v.at(-1)!) : ''
+ if (props.autocomplete && !props.multiple) {
+ // Set current value as autocomplete value text
+ autocompleteValue.value = getLastOptionText(value.value)
+ }
+
watch(value, (newValue, oldValue) => {
if (!props.autocomplete) { return }
diff --git a/packages/ui/src/components/va-stepper/VaStepper.vue b/packages/ui/src/components/va-stepper/VaStepper.vue
index fd79ae74e2..28d6710324 100644
--- a/packages/ui/src/components/va-stepper/VaStepper.vue
+++ b/packages/ui/src/components/va-stepper/VaStepper.vue
@@ -116,7 +116,7 @@ export default defineComponent({
emits: ['update:modelValue', 'finish', 'update:steps'],
setup (props, { emit }) {
const stepperNavigation = shallowRef()
- const { valueComputed: modelValue }: { valueComputed: Ref } = useStateful(props, emit, 'modelValue', { defaultValue: 0 })
+ const { valueComputed: modelValue }: { valueComputed: Ref } = useStateful(props, emit, 'modelValue')
const focusedStep = ref({ trigger: false, stepIndex: props.navigationDisabled ? -1 : props.modelValue })
diff --git a/packages/ui/src/composables/tests/useStateful.spec.ts b/packages/ui/src/composables/tests/useStateful.spec.ts
index ae8b4df209..673f3e08b9 100644
--- a/packages/ui/src/composables/tests/useStateful.spec.ts
+++ b/packages/ui/src/composables/tests/useStateful.spec.ts
@@ -30,8 +30,8 @@ describe('useStateful', () => {
[ true, true, true ],
[ false, true, undefined ],
/* eslint-enable */
- ])('stateful %s', async (stateful: boolean, valueToSet: boolean, internalValue?: true) => {
- const wrapper = mount(TestComponentRich, { props: { stateful } })
+ ])('stateful %s', async (stateful: boolean, valueToSet: boolean, internalValue?: boolean) => {
+ const wrapper = mount(TestComponentRich, { props: { stateful } as any })
wrapper.vm.valueComputed = valueToSet
expect(wrapper.emitted()['update:modelValue']).toBeTruthy()
expect(wrapper.vm.valueComputed).toBe(internalValue)
@@ -39,4 +39,9 @@ describe('useStateful', () => {
await wrapper.setProps({ modelValue: false })
expect(wrapper.vm.valueComputed).toBe(false)
})
+
+ it('should react to prop change', async () => {
+ const wrapper = mount(TestComponentRich, { props: { stateful: true, modelValue: 'Hello!' } })
+ expect(wrapper.vm.valueComputed).toBe('Hello!')
+ })
})
diff --git a/packages/ui/src/composables/tests/useUserProvidedProp.spec.ts b/packages/ui/src/composables/tests/useUserProvidedProp.spec.ts
new file mode 100644
index 0000000000..da7de5586f
--- /dev/null
+++ b/packages/ui/src/composables/tests/useUserProvidedProp.spec.ts
@@ -0,0 +1,27 @@
+import { mount } from '@vue/test-utils'
+import { describe, it, expect } from 'vitest'
+import { defineComponent } from 'vue'
+import { NOT_PROVIDED, useUserProvidedProp } from '../useUserProvidedProp'
+
+const TestComponentRich = defineComponent({
+ template: '',
+ props: { modelValue: { type: String } },
+ setup (props) {
+ const providedProp = useUserProvidedProp('modelValue', props)
+
+ return {
+ providedProp,
+ }
+ },
+ emits: ['update:modelValue'],
+})
+
+describe('useUserProvidedProp', () => {
+ it('should react to prop change', async () => {
+ const wrapper = mount(TestComponentRich, { props: { stateful: true } } as any)
+ expect(wrapper.vm.providedProp).toBe(NOT_PROVIDED)
+
+ await wrapper.setProps({ modelValue: 'Hello' })
+ expect(wrapper.vm.providedProp).toBe('Hello')
+ })
+})
diff --git a/packages/ui/src/composables/useSelectable.ts b/packages/ui/src/composables/useSelectable.ts
index 708981eea3..af28500b4d 100644
--- a/packages/ui/src/composables/useSelectable.ts
+++ b/packages/ui/src/composables/useSelectable.ts
@@ -15,6 +15,7 @@ export type SelectableProps = StatefulProps & LoadingProps & ExtractPro
indeterminateValue: V | null,
disabled: boolean,
readonly: boolean,
+ modelValue: unknown
}
export type Elements = {
diff --git a/packages/ui/src/composables/useStateful.ts b/packages/ui/src/composables/useStateful.ts
index c57096b2be..a69a53f86e 100644
--- a/packages/ui/src/composables/useStateful.ts
+++ b/packages/ui/src/composables/useStateful.ts
@@ -1,4 +1,5 @@
-import { ref, computed, watch, PropType, Ref } from 'vue'
+import { ref, computed, watch, PropType, Ref, getCurrentInstance, watchEffect } from 'vue'
+import { NOT_PROVIDED, useUserProvidedProp } from './useUserProvidedProp'
export type StatefulProps = {
stateful: boolean
@@ -6,7 +7,7 @@ export type StatefulProps = {
export type StatefulOptions = {
eventName?: string
- /** @deprecated set default value for prop, not here */
+ /** Prefer to set default value for prop, not here. */
defaultValue?: T
}
@@ -41,18 +42,21 @@ export const useStateful = <
D extends any,
O extends StatefulOptions,
Key extends string = 'modelValue',
- P extends StatefulProps & { [key in Key]?: T } = StatefulProps & { [key in Key]?: T },
+ P extends StatefulProps & Record = StatefulProps & Record
>(
props: P,
emit: (name: `update:${Key}`, ...args: any[]) => void,
key: Key = 'modelValue' as Key,
options: O = {} as O,
) => {
- const { defaultValue, eventName } = options
+ const { eventName, defaultValue } = options
const event = (eventName || `update:${key.toString()}`) as `update:${Key}`
- const valueState = ref(defaultValue === undefined ? props[key] : defaultValue) as Ref
- let unwatchModelValue: Function
+ const passedProp = useUserProvidedProp(key, props)
+
+ const valueState = ref(passedProp.value === NOT_PROVIDED ? defaultValue || props[key] : passedProp) as Ref
+
+ let unwatchModelValue: ReturnType
const watchModelValue = () => {
unwatchModelValue = watch(() => props[key], (modelValue) => {
valueState.value = modelValue
@@ -63,7 +67,7 @@ export const useStateful = <
stateful ? watchModelValue() : unwatchModelValue?.()
}, { immediate: true })
- const valueComputed = computed>({
+ const valueComputed = computed({
get: () => {
if (props.stateful) { return valueState.value }
diff --git a/packages/ui/src/composables/useUserProvidedProp.ts b/packages/ui/src/composables/useUserProvidedProp.ts
new file mode 100644
index 0000000000..8ccba997d5
--- /dev/null
+++ b/packages/ui/src/composables/useUserProvidedProp.ts
@@ -0,0 +1,14 @@
+import { computed, getCurrentInstance } from 'vue'
+
+export const NOT_PROVIDED = Symbol('NOT_PROVIDED')
+
+export const useUserProvidedProp = >(propName: Name, props: Props) => {
+ const vm = getCurrentInstance()!
+
+ return computed(() => {
+ if (!vm?.vnode.props) { return null }
+ const originalProp = props[propName]
+ // If vnode doesn't have this prop it mean default value is used
+ return propName in vm.vnode.props ? originalProp as Props[Name] : NOT_PROVIDED
+ })
+}