为什么在 vue 组件上输入另一个输入时输入文件的值丢失?
Why the value of input file missing when I input the another input on the vue component?
我有两个组件
我的第一个组件(父组件)是这样的:
<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>
从父组件调用子组件(表单输入组件)
我的子组件是输入类型文本、输入类型日期、输入类型文件和输入类型数字。我将它们全部组合成 1 个组件
子组件是这样的:
<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>
因为我合并成1个组件,我遇到了一个新问题
如果我输入输入类型文件,输入类型文件中会显示文件的值
但是如果我在输入类型文本中输入,输入类型文件的值丢失
为什么输入类型文件的值丢失?
演示:
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>
所以问题是:
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?
发生这种情况是因为当您编辑 <form-input type="text">
时,Vue 将 "repaints" 组件。
并且当它重新绘制 <form-input type="file">
时,它将返回到 "Nothing selected",因为它是一个新的 <input type="file">
。
解决方案:保留文件的值
作为 Kaiido points in the , in latest versions of browsers, you can set 标准方式 <input type="file">
的文件。
这就是下面代码的作用。它监视 value
属性(当父级使用 v-model
并将其值设置为 <input type="file">
的 .files
属性 时出现。
我们必须使用两个 <input>
(与 v-if
/v-else
),因为当它是 <input type="file">
时,:value
属性 可以设置,事件处理程序应该不同 (@change="$emit('input', $event.target.files)"
) 并且我们希望保留 ref
因此我们可以设置 files
.
下面是完整的工作演示。
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>
使用您的 file-change
事件和 validate
函数:
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>
我有两个组件
我的第一个组件(父组件)是这样的:
<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>
从父组件调用子组件(表单输入组件)
我的子组件是输入类型文本、输入类型日期、输入类型文件和输入类型数字。我将它们全部组合成 1 个组件
子组件是这样的:
<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>
因为我合并成1个组件,我遇到了一个新问题
如果我输入输入类型文件,输入类型文件中会显示文件的值
但是如果我在输入类型文本中输入,输入类型文件的值丢失
为什么输入类型文件的值丢失?
演示:
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>
所以问题是:
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?
发生这种情况是因为当您编辑 <form-input type="text">
时,Vue 将 "repaints" 组件。
并且当它重新绘制 <form-input type="file">
时,它将返回到 "Nothing selected",因为它是一个新的 <input type="file">
。
解决方案:保留文件的值
作为 Kaiido points in the <input type="file">
的文件。
这就是下面代码的作用。它监视 value
属性(当父级使用 v-model
并将其值设置为 <input type="file">
的 .files
属性 时出现。
我们必须使用两个 <input>
(与 v-if
/v-else
),因为当它是 <input type="file">
时,:value
属性 可以设置,事件处理程序应该不同 (@change="$emit('input', $event.target.files)"
) 并且我们希望保留 ref
因此我们可以设置 files
.
下面是完整的工作演示。
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>
使用您的 file-change
事件和 validate
函数:
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>