Search code examples
javascripttypescriptvue.jsvuejs3primevue

Why does Input not storing inputted data in modelValue Prop


I have read several posts on Vue 3 regarding v-model and :value. But none of those really helped me in this case.

I want to store value in modelValue prop and wanna update it whenever it gets updated or inputted by user.

This code runs without any errors but shows modelValue: undefined in Vue 3 Dev Console.

My Vue code:

    <template>
  <span :class="wrapperClass">
    <i :class="iconLeft" v-if="iconLeft" />
    <InputText
      :type="type"
      :value="modelValue"
      :class="{ 'p-filled': filled }"
      @input="onInput"
    />
    <label for="username">{{ label }}</label>
    <i :class="iconRight" v-if="iconRight" />
  </span>
</template>
<script lang="ts">
import InputText from "primevue/inputtext"
import { defineComponent } from "vue"
export default defineComponent({
  name: "FormInput",
  emits: ["update:modelValue"],
  components: { InputText },
  props: {
    iconLeft: {
      type: String,
      default: "",
    },
    iconRight: {
      type: String,
      default: "",
    },
    error: {
      type: String,
      default: "",
    },
    label: {
      type: String,
      default: "",
    },
    type: {
      type: String,
    },
    modelValue: {
      type: String,
    },
  },
  computed: {
    wrapperClass(): any {
      return {
        "p-float-label": true,
        "p-input-icon-left": this.iconLeft,
        "p-input-icon-right": this.iconRight,
      }
    },
    filled(): any {
      return this.modelValue != null && this.modelValue.toString().length > 0
    },
  },
  methods: {
    onInput(event: any): any {
      this.$emit("update:modelValue", event.target.value)
    },
  },
})
</script>

Parent Component:

<template>
  <div class="home">
    <Example :example="{ heading: 'fii' }" />
    <Button label="Test" />
    <hr />
    <img alt="Vue logo" src="../assets/logo.png" />
    <hr />
    <FormInput v-bind="args" />

    <hr />

    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue"
import HelloWorld from "../components/HelloWorld.vue" // @ is an alias to /src
import Example from "../components/Example/Example.vue" // @ is an alias to /src
import FormInput from "../components/Form/FormInput.vue"

export default defineComponent({
  name: "Home",
  components: {
    HelloWorld,
    Example,
    FormInput,
  },
  setup() {
    return {
      args: {
        label: "test",
        type: "text",
        value: "",
      },
    }
  },
})
</script>

Please see this screenshot

Vue dev console

Parent Component in Dev Console

enter image description here


Solution

  • There are few things to fix:

    1. PrimeVue component InputText has no prop value but modelValue (to support v-model) - InputText Docs + read Using v-model on Components to learn a difference between v-model on native inputs vs custom components

    2. InputText is not a native input element, so there is no point to listen for a input event. The component is emitting update:modelValue event. Also event payload is not native event so event.target.value makes no sense, just use value - source

    3. There is no point in duplicating the p-filled as InputText does this already (see source above)

    // FormInput.vue
    
    <template>
      <span :class="wrapperClass">
        <i :class="iconLeft" v-if="iconLeft" />
        <InputText
          :type="type"
          :modelValue="modelValue"
          @update:model-value="onInput"
        />
        <label for="username">{{ label }}</label>
        <i :class="iconRight" v-if="iconRight" />
      </span>
    </template>
    <script lang="ts">
    import InputText from "primevue/inputtext"
    import { defineComponent } from "vue"
    
    export default defineComponent({
      name: "FormInput",
      emits: ["update:modelValue"],
      components: { InputText },
      props: {
        iconLeft: {
          type: String,
          default: "",
        },
        iconRight: {
          type: String,
          default: "",
        },
        error: {
          type: String,
          default: "",
        },
        label: {
          type: String,
          default: "",
        },
        type: {
          type: String,
          default: "text"
        },
        modelValue: {
          type: String,
          required: true
        },
      },
      computed: {
        wrapperClass(): any {
          return {
            "p-float-label": true,
            "p-input-icon-left": this.iconLeft,
            "p-input-icon-right": this.iconRight,
          }
        },
      },
      methods: {
        onInput(value: any): any {
          this.$emit("update:modelValue", value)
        },
      },
    })
    </script>
    

    Usage:

    <FormInput v-model="data" />
    <!-- OR -->
    <FormInput :modelValue="data" @update:model-value="data = $event" />
    

    Demo:

    const app = Vue.createApp({
      data() {
        return {
          text: "default value",
        }
      },
    });
    
    app.component('FormInput', {
      emits: ["update:modelValue"],
      components: {
        'inputtext': primevue.inputtext
      },
      template: `
      <span :class="wrapperClass">
        <i :class="iconLeft" v-if="iconLeft" />
        <inputtext
          :type="type"
          :modelValue="modelValue"
          @update:model-value="onInput"
        ></inputtext>
        <label for="username">{{ label }}</label>
        <i :class="iconRight" v-if="iconRight" />
      </span>
      `,
      props: {
        iconLeft: {
          type: String,
          default: "",
        },
        iconRight: {
          type: String,
          default: "",
        },
        error: {
          type: String,
          default: "",
        },
        label: {
          type: String,
          default: "",
        },
        type: {
          type: String,
          default: "text"
        },
        modelValue: {
          type: String,
          required: true
        },
      },
      computed: {
        wrapperClass() {
          return {
            "p-float-label": true,
            "p-input-icon-left": this.iconLeft,
            "p-input-icon-right": this.iconRight,
          }
        },
      },
      methods: {
        onInput(value) {
          this.$emit("update:modelValue", value)
        },
      },
    });
    
    app.mount('#app');
    <link href="https://unpkg.com/primevue/resources/themes/saga-blue/theme.css" rel="stylesheet">
    <link href="https://unpkg.com/primevue/resources/primevue.min.css" rel="stylesheet">
    <link href="https://unpkg.com/primeicons/primeicons.css" rel="stylesheet">
    
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/primevue/inputtext/inputtext.min.js"></script>
    
    <div id="app">
      <form-input v-model="text"></form-input>
      <pre>
        {{ text }}
      </pre>
    </div>