unovue / shadcn-vue

Vue port of shadcn-ui
https://www.shadcn-vue.com/
MIT License
5.33k stars 318 forks source link

[Question]: v-model not updating input value in Shadcn-vue Input component inside a Form #771

Closed SAntoineS closed 2 months ago

SAntoineS commented 2 months ago

Reproduction

https://stackblitz.com/edit/1d3tzs-zd6u8m?file=src%2FApp.vue

Describe the bug

Description:

I am experiencing an issue with the v-model binding not updating the input value in the Input component from Shadcn-vue inside a Form. Even though the value changes in the parent component, the input field in the child component doesn't reflect the updated value.

Steps to reproduce:

  1. Parent component:
  1. Child component (Input.vue):

Expected behavior:

The input field should update its value when uniteLocale.ide is modified programmatically.

Actual behavior:

The value in the input field does not update even though the value of uniteLocale.ide changes.

What I’ve tried:

Despite these changes, the input field does not reflect the updated value.

Additional context:

Thanks !

System Info

System:
    OS: Windows 10 10.0.19045
    CPU: (12) x64 13th Gen Intel(R) Core(TM) i5-1345U
    Memory: 20.89 GB / 31.64 GB
  Binaries:
    Node: 20.17.0 - C:\Program Files\nodejs\node.EXE
    npm: 10.8.2 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Edge: Chromium (127.0.2651.98)
    Internet Explorer: 11.0.19041.4355
    @vueuse/core: ^11.0.3 => 11.0.3
    radix-vue: ^1.9.5 => 1.9.5
    vue: ^3.5.4 => 3.5.4

Contributes

SAntoineS commented 2 months ago

It seems to work if I bind the v-model on the FormField and not directly on the Input.

sadeghbarati commented 2 months ago

Don't create new ref for values, componentField slotProp already have model-value and @update:model-value (v-model).

Instead use initialValues (useForm) option or :initial-values (Form) prop

https://stackblitz.com/edit/1d3tzs-8cmpb1?file=src%2FApp.vue

SAntoineS commented 2 months ago

Don't create new ref for values, componentField slotProp already have model-value and @update:model-value (v-model).

Instead use initialValues (useForm) option or :initial-values (Form) prop

https://stackblitz.com/edit/1d3tzs-8cmpb1?file=src%2FApp.vue

Oooh ok cool !

So I can access to the values via values.uniteLocale.idea, like :

watch(() => values.uniteLocale.ide,
  (newValue) => {
    console.log('New Value: ', newValue);
  }
);

And it works!

Thanks a lot !

Another question

How can I pass the values to child component, it's like a ref ?

Because in my context I have a stepper (with two steps) inside the form and each steps is a component.

It's a form application with fields into the steps.

sadeghbarati commented 2 months ago

In Stepper it's better to use Form component instead of useForm composable just like this link

https://www.shadcn-vue.com/docs/components/stepper.html#form

So you could access the Form context wherever you want with passing props or using inject or using vee-validate utilities

SAntoineS commented 2 months ago

In Stepper it's better to use Form component instead of useForm composable just like this link

https://www.shadcn-vue.com/docs/components/stepper.html#form

So you could access the Form context wherever you want with passing props or using inject or using vee-validate utilities

I am sorry but I can't understand how to do this...

This is my Form :

<Form v-slot="{ meta, values, validate }" as="" keep-values
              :validation-schema="toTypedSchema(formSchema[stepIndex - 1])">
          <Stepper v-slot="{ isNextDisabled, isPrevDisabled, nextStep, prevStep }" v-model="stepIndex"
                   class="block w-full">
            <form @submit.prevent="HandleSubmit(meta, values, validate)">
              <div class="flex w-full flex-start gap-2">
                <StepperItem
                    v-for="step in steps"
                    :key="step.step"
                    v-slot="{ state }"
                    class="relative flex w-full flex-col items-center justify-center"
                    :step="step.step">
                  <StepperSeparator
                      v-if="step.step !== steps[steps.length - 1].step"
                      class="absolute left-[calc(50%+20px)] right-[calc(-50%+10px)] top-5 block h-0.5 shrink-0 rounded-full bg-muted group-data-[state=completed]:bg-primary"/>

                  <StepperTrigger as-child>
                    <Button
                        :variant="state === 'completed' || state === 'active' ? 'default' : 'outline'"
                        size="icon"
                        class="z-10 rounded-full shrink-0"
                        :class="[state === 'active' && 'ring-2 ring-ring ring-offset-2 ring-offset-background']"
                        :disabled="state !== 'completed' && !meta.valid">
                      <Check v-if="state === 'completed'" class="size-5"/>
                      <component v-if="state === 'active' || state === 'inactive'" :is="step.icon" class="w-4 h-4"/>
                    </Button>
                  </StepperTrigger>

                  <div class="mt-5 flex flex-col items-center text-center">
                    <StepperTitle
                        :class="[state === 'active' && 'text-primary']"
                        class="text-sm font-semibold transition lg:text-base">
                      {{ step.title }}
                    </StepperTitle>
                    <StepperDescription
                        :class="[state === 'active' && 'text-primary']"
                        class="sr-only text-xs text-muted-foreground transition md:not-sr-only lg:text-sm">
                      {{ step.description }}
                    </StepperDescription>
                  </div>
                </StepperItem>
              </div>

              <div class="flex flex-col gap-4 mt-20">
                <step-inscription v-if="stepIndex === 1"/>
                <step-responsable v-if="stepIndex === 2"/>
              </div>

              <!-- Step Actions -->
              <div class="flex items-center justify-between mt-4">
                <Button :disabled="isPrevDisabled" variant="ghost" size="sm" @click="prevStep()">
                  Précédent
                </Button>
                <!-- Next or Submit Actions -->
                <div class="flex items-center gap-3">
                  <Button variant="informationTonal" v-if="stepIndex !== 2" :type="meta.valid ? 'button' : 'submit'"
                          :disabled="isNextDisabled"
                          size="sm" @click="meta.valid && nextStep()">
                    Suivant
                  </Button>
                  <Button variant="successTonal"
                          v-if="stepIndex === 2" size="sm" type="submit">
                    Soumettre
                  </Button>
                </div>
              </div>
            </form>
          </Stepper>
        </Form>

How can I use inject from vue with values (Inside the Form)

I want to modify / access (like watch()) values in my child components.

I can't pass with this :

<step-inscription v-model="values" v-if="stepIndex === 1"/> cause Vue don't want v-slot scope variable in v-model. Like this error : Internal server error: v-model cannot be used on v-for or v-slot scope variables because they are not writable.

sadeghbarati commented 2 months ago

Can you share a minimal demo of your Stepper component

I can't help in this way

SAntoineS commented 2 months ago

Can you share a minimal demo of your Stepper component

I can't help in this way

Sure !

There is a stackblitz of a minimal demo of my project.

I'm currently using v-model on FormField and provide (then inject in child) my object like this in main.vue :

const uniteLocale = ref<any>({});
provide('uniteLocale', uniteLocale);

I saw in the Vee Validate documentation that we can use v-model . I don't know if there is a cleaner method to access to Form values from outside the Form.

sadeghbarati commented 2 months ago

Inside <Form> component you can use useFormValues vee-validate composable

Outside <Form> component

  1. you can get values on form submit and do whatever you want to them like emit them to parent
  2. or use ref on the form component and access the values in script section like formRef.value.values or in template like formRef.values

Even though the vee-validate docs says you can use v-model on FormField it is better not to do it

SAntoineS commented 2 months ago

Thanks for the solution ! But...

I tried with ref like :

const uniteLocale = ref<any>({});
provide('uniteLocale', uniteLocale);

----------------------------------------------

<Form v-slot="{ meta, validate }" :initial-values="formValuesUniteLocale" keep-values
 :validation-schema="toTypedSchema(formSchema[stepIndex - 1])" ref="uniteLocale">

In my child component I have a watch() that works :

const uniteLocale = inject<Ref>("uniteLocale");

----------------------------------------------

watch(() => uniteLocale.value.values?.descriptionActiEconomiques, (newValue) => {
  isDescriptionFilled.value = !!newValue;
});

The only issue I have left is that I can't modify the value directly from the code like this :

function importIDE(uniteLegal) {
  if (uniteLegal) {
    try {
      const { value } = useField('ide');
      value.value = "test1"
      uniteLocale.value.values.ide = "test2"
      toast({
        title: 'Entreprise importée avec succès ! ✅',
        variant: 'default',
      });
    } catch (e) {
      console.error("Erreur lors de l'importation UniteLegal -", e)
      toast({
        title: "Oops ! Erreur lors de l'importation ❌",
        variant: 'default',
      });
    }
  }
  console.log("UniteLocale.ide: ", uniteLocale.value.values.ide);
}

I tried with directly changing the variable and tried with useField method of vee-validate.

No one works...

But I have this vue-warn : [Vue warn] Set operation on key "ide" failed: target is readonly.

Do you know why ? I have to contact directly vee-validate ?

Sorry for the deep discussion : /

Thanks a lot !

sadeghbarati commented 2 months ago

The only issue I have left is that I can't modify the value directly from the code like this :

You need to use

uniteLocale.value.setFieldValue('idle or object.idle', 'new value')

//or 

uniteLocale.value.setValues({
  idle: 'new value',

  // or

  'object.idle': 'new value'
})
SAntoineS commented 2 months ago

uniteLocale.value.setFieldValue('idle or object.idle', 'new value')

It works fine !

Thanks a lot for all your help !

bjorne84 commented 2 months ago

can somebody explain whats the point of adding these extra step instead of binding a ref to the input field? what benefit is there to not being able to bind to the input field?

sadeghbarati commented 2 months ago

can somebody explain whats the point of adding these extra step instead of binding a ref to the input field? what benefit is there to not being able to bind to the input field?

VeeValidate FormField slotProps already have model-value and @update:model-value (componentField slotProp), which is why it's better to use initialValues with componentField slotProp so we could also get a better resetForm functionality and get right types