3

I have two component

My first component (parent component) like this :

<template>
    <div>
        ...
            <form-input id="name" name="name" v-model="name">Name</form-input>
            <form-input id="birth-date" name="birth_date" type="date" v-model="birthDate">Date of Birth</form-input>
            <form-input id="avatar" name="avatar" type="file" v-on:triggerChange="onFileChange($event)">Avatar</form-input>
            <form-input id="mobile-number" name="mobile_number" type="number" v-model="mobileNumber">Mobile Number</form-input>
        ...
    </div>
</template>
<script>
    export default {
        data() {
            return {
                name: null,
                birthDate:  null,
                mobileNumber: null
            }
        },
        methods: {
            onFileChange(e) {
                let self = this
                this.validate(e.target.files[0])
                .then(function(res) {
                    let files = e.target.files,
                    reader = new FileReader()
                    // if any values
                    if (files.length) {
                        self.removeErrorMessageUpload()
                        self.files = files[0]
                        reader.onload = (e) => {
                            self.updateProfileAvatar(e.target.result)
                        }
                        reader.readAsDataURL(files[0])
                    }
                })
                .catch(function() {
                    // do something in the case where the image is not valid
                    self.displayErrorMessageUpload()
                })
            },
            validate(image) {
                let self = this
                return new Promise(function(resolve, reject) {
                    // validation file type
                    if (!self.allowableTypes.includes(image.name.split(".").pop().toLowerCase())) {
                        reject()
                    }
                    // validation file size
                    if (image.size > self.maximumSize) {
                        reject()
                    }
                    // validation image resolution
                    let img = new Image()
                    img.src = window.URL.createObjectURL(image)
                    img.onload = function() {
                        let width = img.naturalWidth,
                            height = img.naturalHeight

                        window.URL.revokeObjectURL(img.src)

                        if (width != 100 && height != 100) {
                            reject()
                        } 
                        else {
                            resolve()
                        }
                    }
                })         
            },
        }
    }
</script>

From the parent component, it will call child component (form input component)

My child component are input type text, input type date, input type file and input type number. I combine all of them into 1 component

The child component like this :

<template>
    <div class="form-group">
        <label :for="id" class="col-sm-3 control-label"><slot></slot></label>
        <div class="col-sm-9">
            <input :type="type" :name="name" :id="id" class="form-control" :value="value" v-on:change="applySelected($event)" @input="$emit('input', $event.target.value)">
        </div>
    </div>
</template>

<script>
    export default {
        name: "form-input",
        props: {
            'id': String,
            'name': String,
            'isRequired': {
                type: Boolean,
                default: true
            },
            'type': {
                type: String,
                default() {
                    if(this.type == 'number')
                        return 'number'
                    return 'text'
                }
            },
            'value': {
                type: [String, Number]
            }
        },
        methods: {
            applySelected(e) {
                this.$emit('triggerChange', e)
            }
        }
    }
</script>

Because I merge into 1 komponent, I get a new problem

If I input the input type file, the value of file will show in the input type file

But if I input in the input type text, the value of input type file missing

Why the value of input type file missing?

Demo:

Vue.component('form-input', {
  template: "#form-input-tpl",
  name: "form-input",
  props: {
    'id': String,
    'name': String,
    'isRequired': {type: Boolean, default: true},
    'type': { type: String, default () {if (this.type == 'number') {return 'number'} else {return 'text'}}},
    'value': { type: [String, Number] }
  },
  methods: {
    applySelected(e) { this.$emit('triggerChange', e) }
  }
});

new Vue({
  el: '#app',
  data: {
    name: null,
    birthDate: null,
    mobileNumber: null
  },
  methods: {
    onFileChange(e) {
      // ...
    }
  }
})
<script src="https://unpkg.com/vue"></script>

<template id="form-input-tpl">
    <div class="form-group">
        <label :for="id" class="col-sm-3 control-label"><slot></slot></label>
        <div class="col-sm-9">
            <input :type="type" :name="name" :id="id" class="form-control" :value="value" v-on:change="applySelected($event)" @input="$emit('input', $event.target.value)">
        </div>
    </div>
</template>

<div id="app">
  <h3>Select a file, then type a name. The file will be reset.</h3>
  <div>
    <form-input id="name" name="name" v-model="name">Name</form-input>
    <form-input id="birth-date" name="birth_date" type="date" v-model="birthDate">Date of Birth</form-input>
    <form-input id="avatar" name="avatar" type="file" v-on:triggerChange="onFileChange($event)">Avatar</form-input>
    <form-input id="mobile-number" name="mobile_number" type="number" v-model="mobileNumber">Mobile Number</form-input>
  </div>
</div>

6
  • "Why the value of input type file missing?" conflicts with prior statements, which is is? Commented Mar 17, 2018 at 14:24
  • @Mark Schultheiss What do you mean? I do not understand Commented Mar 17, 2018 at 15:00
  • You say "If I input the input type file, the value of file will show in the input type file But if I input in the input type text, the value of input type file missing Why the value of input type file missing?" but the sentence "But if I input in the input type text, the value of input type file missing" What does that mean? The text type field is impacting the file type field? That makes no sense that it happens that one field impacts the other indicating there might be something else at play here. Note the default html input type is type="text" if unspecified. Commented Mar 17, 2018 at 15:08
  • Perhaps show the rendered HTML as an additional bit of information. Commented Mar 17, 2018 at 15:12
  • @MarkSchultheiss This apparently occurs because v-on:change="applySelected($event)" or @input="$emit('input', $event.target.value)". It's run on the input type text, input type file, input type date and input type number. Because I combine it into 1 component Commented Mar 17, 2018 at 15:46

1 Answer 1

2

So the problem is:

After you have chosen a file in the <form-input type="file">, if you type something in the <form-input type="type">, the <form-input type="file"> erases. Why is that?

This happens because when you edit <form-input type="text">, Vue will "repaints" the components.

And when it repaints the <form-input type="file">, it will go back to "Nothing selected" because it is a new <input type="file">.


Solution: Keeping the files' values

As Kaiido points in the comments, in latest versions of browsers, you can set the files of a <input type="file"> in a standard way.

So this is what the code below does. It watches for the value property (that comes when the parent uses v-model and sets its value to the .files property of the <input type="file">.

We have to use two <input> (with v-if/v-else) because when it is a <input type="file">, the :value property can be set, the event handler should be different (@change="$emit('input', $event.target.files)") and we want to keep a ref so we can set the files.

Full working demo below.

Vue.component('form-input', {
  template: "#form-input-tpl",
  name: "form-input",
  props: {
    'id': String,
    'name': String,
    'isRequired': {type: Boolean, default: true},
    'type': {type: String, default: 'text'},
    'value': {type: [String, Number, FileList, DataTransfer]}
  },
  mounted() {
    // set files upon creation or update if parent's value changes
    this.$watch('value', () => {
      if (this.type === "file") { this.$refs.inputFile.files = this.value; }
    }, { immediate: true });
  }
});

new Vue({
  el: '#app',
  data: {
    name: null,
    birthDate: null,
    mobileNumber: null,
    files: null
  }
})
<script src="https://unpkg.com/vue"></script>

<template id="form-input-tpl">
    <div class="form-group">
        <label :for="id" class="col-sm-3 control-label"><slot></slot></label>
        <div class="col-sm-9">
           <input v-if="type !== 'file'" :type="type" :name="name" :id="id" class="form-control" :value="value" @input="$emit('input', $event.target.value)">
            
           <input v-else :type="type" :name="name" :id="id" class="form-control" @change="$emit('input', $event.target.files)" ref="inputFile">
        </div>
    </div>
</template>

<div id="app">
  <div>
    <form-input id="name" name="name" v-model="name">Name</form-input>
    <form-input id="birth-date" name="birth_date" type="date" v-model="birthDate">Date of Birth</form-input>
    <form-input id="avatar" name="avatar" type="file" v-model="files">Avatar</form-input>
    <form-input id="mobile-number" name="mobile_number" type="number" v-model="mobileNumber">Mobile Number</form-input>
  </div>
</div>

Using your file-change event and validate function:

Vue.component('form-input', {
  template: "#form-input-tpl",
  name: "form-input",
  props: {
    'id': String,
    'name': String,
    'isRequired': {type: Boolean, default: true},
    'type': {type: String, default: 'text'},
    'value': {type: [String, Number, FileList, DataTransfer]}
  },
  mounted() {
    // set files upon creation or update if parent's value changes
    this.$watch('value', () => {
      if (this.type === "file") { this.$refs.inputFile.files = this.value; }
    }, { immediate: true });
  }
});

new Vue({
  el: '#app',
  data: {
    name: null,
    birthDate: null,
    mobileNumber: null,
    filesVModel: null,
    allowableTypes: ['jpg', 'jpeg', 'png'],
    maximumSize: 1000,
    files: null
  },
  methods: {
    onFileChange(e) {
      console.log('onfilechange!');
      let self = this
      this.validate(e.target.files[0])
        .then(function(res) {
          let files = e.target.files,
            reader = new FileReader()
          // if any values
          if (files.length) {
            self.removeErrorMessageUpload()
            self.files = files[0]
            reader.onload = (e) => {
              self.updateProfileAvatar(e.target.result)
            }
            reader.readAsDataURL(files[0])
          }
        })
        .catch(function(err) {
          // do something in the case where the image is not valid
          self.displayErrorMessageUpload(err)
        })
    },
    validate(image) {
      let self = this
      return new Promise(function(resolve, reject) {
        // validation file type
        if (!self.allowableTypes.includes(image.name.split(".").pop().toLowerCase())) {
          reject("Type " + image.name.split(".").pop().toLowerCase() + " is not allowed.")
        }
        // validation file size
        if (image.size > self.maximumSize) {
          reject("Image size " + image.size + " is larger than allowed " + self.maximumSize + ".")
        }
        // validation image resolution
        let img = new Image()
        img.src = window.URL.createObjectURL(image)
        img.onload = function() {
          let width = img.naturalWidth,
            height = img.naturalHeight

          window.URL.revokeObjectURL(img.src)

          if (width != 100  && height != 100) {
            reject("Width and height are " + width + " and " + height + " and not both 100")
          } else {
            resolve()
          }
        }
      })
    },
    displayErrorMessageUpload(msg) {
      console.log('displayErrorMessageUpload', msg);
    },
    removeErrorMessageUpload() {
      console.log('removeErrorMessageUpload');
    },
    updateProfileAvatar(result) {
      console.log('updateProfileAvatar', result);
    }
  }
})
<script src="https://unpkg.com/vue"></script>

<template id="form-input-tpl">
    <div class="form-group">
        <label :for="id" class="col-sm-3 control-label"><slot></slot></label>
        <div class="col-sm-9">
           <input v-if="type !== 'file'" :type="type" :name="name" :id="id" class="form-control" :value="value" @input="$emit('input', $event.target.value)">
            
           <input v-else :type="type" :name="name" :id="id" class="form-control" @change="$emit('input', $event.target.files)" ref="inputFile" v-on:change="$emit('file-change', $event)">
        </div>
    </div>
</template>

<div id="app">
  <div>
    <form-input id="name" name="name" v-model="name">Name</form-input>
    <form-input id="birth-date" name="birth_date" type="date" v-model="birthDate">Date of Birth</form-input>
    <form-input id="avatar" name="avatar" type="file" v-model="filesVModel" @file-change="onFileChange">Avatar</form-input>
    <form-input id="mobile-number" name="mobile_number" type="number" v-model="mobileNumber">Mobile Number</form-input>
  </div>
</div>

Sign up to request clarification or add additional context in comments.

16 Comments

You can (in latest browsers) set the files property of an input[type=file] to an other FileList. I don't know vue.js so wouldn't be able to catch the rendering event myself nor to access the new input, but just to let you know that it's actually doable.
@Kaiido humm, I actually knew it was doable through a glitch or something. Since it was non standard (at least not supported by v-model) I thought it wasn’t worth pursuing. But thanks for letting us know, having a standard way of doing it in newer browsers is great news!
@acdcjunior Is there no other way? I am using required html5. If using your way, it will always show validation html5. And whether I can send the file selected to server side using your way?
Hey, guys, thanks for the push :) I ended up researching a bit more and I think the final version is quite clean! It sets the .files using standard interface! No hacks! I learned a lot in the process! Thanks a lot! @ SuccessMan @Kaiido
@acdcjunior You are welcome. I will try it. Btw, what do you mean by immediate: true?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.