Reputation: 13172
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?
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>
Upvotes: 3
Views: 3186
Reputation: 135762
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">
.
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>
Upvotes: 2