I have one strange problem by uploading files using React JS.
The main component is as follows:
class Test extends React.Component {
constructor(props){
super(props);
this.state = {
files: [
{file_type: registrationFormFields.ID_CARD.key, values: {active: false, fileName: language.btnIdCard, file: null, base64: ""}},
{file_type: registrationFormFields.STATUTES.key, values: {active: false, fileName: language.btnStatut, file: null, base64: ""}},
{file_type: registrationFormFields.BLANK_LETTER_HEAD.key, values: {active: false, fileName: language.btnBlanco, file: null, base64: ""}},
{file_type: registrationFormFields.COMPANY_PHOTO.key, values: {active: false, fileName: language.btnCompanyPhoto, file: null, base64: ""}}
]
};
}
handleFiles(file_type, event){
event.preventDefault();
let self = this;
let fileValue = event.target.value;
let files = "";
let file = event.target.files[0];
let filename = file.name.substr(0, file.name.lastIndexOf('.')) || file.name;
const size_in_mb = file ? file.size/1000000 : null;
if(this.fileIsValid(file_type, size_in_mb)){
if(fileValue){
this.getBase64(file, function(e){
let base64 = e.target.result;
let nState = self.state.files;
if(self.state.files.some(i => i.file_type === file_type))
nState = self.state.files.filter(f => f.file_type !== file_type);
files = [...nState, {file_type: file_type, values: {active: true, fileName: filename, file, base64: base64}}];
self.setState({files: files});
});
}
}
}
removeFile = (file_type, file_name) => {
let nState = this.state.files;
let nFiles = this.state.files;
if(this.state.files.some(i => i.file_type === file_type)){
nState = this.state.files.filter(f => f.file_type !== file_type);
nFiles = [...nState, {file_type: file_type, values: {active: false, fileName: file_name, file: null, base64: ""}}];
}
this.setState({files: nFiles});
};
render(){
return (
<FileInputButton pictureIcon="credit-card"
onFileChange={this.handleFiles.bind(this, registrationFormFields.ID_CARD.key)}
btnName="idCard"
extraClass="col-lg-offset-2 col-md-offset-2 col-sm-offset-2"
fileType={registrationFormFields.ID_CARD.key}
fileName={language.btnIdCard}
removeFile={this.removeFile}
errors={this.state.errors[registrationFormFields.ID_CARD.key]}
values={this.getValues(registrationFormFields.ID_CARD.key)}/>
)
}
}
React.render(<Test />, document.getElementById('container'));
There is more code but this is the important one.
And FileInputButton component is as follows:
const FileInputButton = ({extraClass, pictureIcon, btnName, onFileChange, values, removeFile, fileType, fileName, errors}) => {
const name = values.fileName === fileName ? `Add ${fileName}` : `Added ${fileName}`;
const icon = values.active ? "check" : "upload";
let borderClass = "";
if(values.active) borderClass = "fileSuccess";
if(errors) borderClass = "fileError";
let removeButton = "";
if(values.active){
removeButton = <div className="remove-file" onClick={() => removeFile(fileType, fileName)}><Ionicon icon="ios-close" fontSize="22px" color={styles.errorColor}/> Remove</div>
}
let added_file = "";
if(values.active){
added_file = <div className="added-file-name"><Ionicon icon="ios-checkmark" fontSize="22px" color={styles.successColor}/>{values.file.name}</div>
}
return (
<div className={`col-lg-4 col-md-4 col-sm-4 ${extraClass}`}>
<div className="picture-container" style={{marginLeft: 18}}>
<div className={`picture ${borderClass}`}>
<FontAwesome name={pictureIcon} size="3x" className="picture-src"/>
</div>
</div>
<div className="uploadButtonBox" style={{width: '100%', textAlign: 'center', marginTop: 20}}>
<label className={`fileUploadButton ${borderClass}`}>
<FontAwesome name={icon} className="faIcon" style={{height: '39px !important'}}/>
<span className={`btnFileText ${btnName}`}>{name}</span>
<input id="btnFile" type="file" style={{display: 'none'}} onChange={onFileChange} name={name}/>
</label>
</div>
{added_file}
{removeButton}
<div className="errorBox">{errors}</div>
</div>
);
};
Everything works fine. There is only one scenario when it doesn't work as expected.
If the file is chosen little remove button (the removeButton from FileInputButton component) is shown. If I click on it the removeFilefunction from main component gets called, and I change the state (find that file in state, remove it, and add a new object with initial values for that file and then update the state).
The file is removed and everything works fine. If I new add a new file, that works also as expected. The problem is if I try to add the same file again, then it doesn't work. The handleFiles function isn't called at all.
If I add first file and then remove it, then add new second file then remove the second file and then again add the first file it works fine.
Only problem is if I add first file, then remove it and then try to add it again it doesn't work. I do not get any errors in console.
Any idea what I do wrong?
You have onChange handler on your file input, however when you clear file from your state, you are not clearing it from your input. So when you are trying to upload the same file again, onChange event is not triggered therefore your handler is not called.
Put your input file value to '' to reset it.
Lets say that your delete handler would look like this.
handleDelete(){
document.querySelector('input[type=file]').value = '';
this.props.removeFile(this.props.fileType, this.props.fileName);
}
This code may be not entirely in the spirit of your code, but you will get the point.
Related
I have a snippet below which is essentially my entire code block at this point, and essentially it creates a div and when you click "add another zone" it will clone that div. This allows the user to enter multiple lines of info and each have their own result and image.
The issue is that I'm successfully cloning everything with it's own unique identity thanks to my card setup. However, dropzone is not replicating. The first file dropzone form will work perfectly, but when I clone the div and have 2 or more dropzone insnstances on the page they don't work (they don't show the upload image text or anything)
How can I successfully apply my same logic to the dropzone instance here?
new Vue({
components: {},
el: "#commonNameDiv",
data() {
return {
searchString: [''],
results: [],
savedAttributes: [],
cards: [],
showList: false,
zoneNumber:[],
imageZoneNames: [] }
},
methods: {
autoComplete(ev, card) {
this.results = [];
console.log(this.searchString);
if (ev.target.value.length > 2) {
axios.get('/product/parts/components/search', {
params: {
searchString: ev.target.value
}
}).then(response => {
card.results = response.data;
this.showList = true;
console.log(this.results);
console.log(this.searchString);
});
}
},
saveAttribute(result, card) {
card.value = result.attribute_value;
card.results = [];
card.zone = this.zoneNumber;
this.showList = false;
},
addCard: function() {
this.cards.push({
index: "",
value: "",
zoneNumber: "",
results: [],
componentImage:""
});
console.log(this.cards);
},
hideDropdown() {
this.showList = false;
},
},
created() {
this.addCard();
let instance = this;
Dropzone.options = {
maxFilesize: 12,
renameFile: function (file) {
var dt = new Date();
var time = dt.getTime();
return time + file.name;
},
acceptedFiles: ".jpeg,.jpg,.png,.gif",
addRemoveLinks: true,
timeout: 50000,
removedfile: function (file) {
console.log(file.upload.filename);
var name = file.upload.filename;
var fileRef;
return (fileRef = file.previewElement) != null ?
fileRef.parentNode.removeChild(file.previewElement) : void 0;
},
init: function() {
this.on("addedfile",
function(file) {
instance.imageZoneNames.push({name: file.upload.filename, desc: 'Line Drawing'});
console.log(file);
console.log(instance.imageZoneNames);
});
}
};
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.5.0/dropzone.js"></script>
<div id="commonNameDiv">
<div class="uk-grid" v-for="(card, i) in cards" :key="i">
<div class="uk-width-1-10" >
<input v-model=" card.zoneNumber" size="4" type="text" name="mapNumber">
</div>
<div class="uk-width-6-10">
<input
style="width:100%"
placeholder="what are you looking for?"
v-model="card.value"
v-on:keyup="autoComplete($event, card)"
>
<div v-if="showList" class="panel-footer componentList" v-if="card.results.length">
<ul>
<li v-for="(result, i) in card.results" :key="i">
<a v-on:click="saveAttribute(result, card)">#{{ result.attribute_value }}</a>
</li>
</ul>
</div>
</div>
<div class="uk-width-3-10">
<form method="post" action="{{url('product/parts/upload/store')}}" enctype="multipart/form-data"
class="dropzone">
</form>
</div>
</div>
<div style="height: 35px;">
</div>
<div>
<a v-on:click="addCard">Add another zone</a>
</div>
</div>
When you instantiate the Dropzone class, it automatically looks for elements to transform in dropzones (by default, elements with the .dropzone class).
It looks like you want to dynamically add elements that are dropzones. Then you need to trigger the dropzone transformation yourself.
I would suggest you disable the autoDiscover option, and manually designates each element you want to transform into dropzones :
addCard() {
this.cards.push({
...
});
let cardIndex = this.cards.length - 1;
// Waiting for the element #dropzone-X to exist in DOM
Vue.nextTick(function () {
new Dropzone("#dropzone-"+cardIndex, {
...
});
});
},
created() {
...
Dropzone.autoDiscover = false
// no new Dropzone()
...
// Starting setup
this.addCard();
},
<form ... class="dropzone" v-bind:id="'dropzone-'+i">
Working jsbin
There are several ways to select the element to transform ($refs, ids, classes), here I'm suggesting ids.
See the doc on programmatically creating dropzones
Actually it is being created, but the Dropzone is not being reconstructed.
I think you have to create a new instance of the Dropzone.
if you try to insert:
created() {
this.addCard();
var myDropzone = new Dropzone('.dropzone')
let instance = this;
Dropzone.options.myDropzone = {
or even add the options to the addCard method or set a setupDropzones method and add it to the addCard method.
I have a defined a file upload field but the issue here is I am able to submit the form even if I have not selected a file. Please help me figure out how to through an error that no file is selected on clicking on the submit button if no file is selected. I am using vuetify version 1.0.
<template>
<v-form :model='agency' ref='AgencyForm'>
<div class="vue-file-wrapper">
<input
type="file"
ref="file"
name="vue-file-input"
required
:rules='uploadDocument'
#change="onFileSelected"
>
</div>
<v-btn #click.prevent='submit'>Save</v-btn>
</v-form>
</template>
<script>
export default {
props: ['agency'],
data: function () {
return {
filename: '',
uploadDocument: [
value => !!value || 'Please upload document'
],
}
}
methods: {
onFileSelected(event) {
var files = event.target.files || event.dataTransfer.files;
if (!files.length) {
return;
}
this.createImage(files[0]);
},
createImage(file) {
var fileReader = new FileReader(),
that = this;
fileReader.onload = function(event) {
that.agency.document = event.target.result;
that.agency.filename = file.name;
that.filename = file.name;
};
fileReader.readAsDataURL(file);
},
submit() {
if (this.$refs.AgencyForm.validate()) {
this.$axios.put('/agency.json', { agency: this.agency })
}
</script>
I can see some issues with your current implementation. Firstly, you are directly mutating a prop agency, which isn't a good practice. Also, you aren't waiting for your axios request to complete on submission.
However, for your current situation of not having an error thrown when no file is selected on clicking on the submit button, I think the issue is a syntax problem.
You currently have
<div class="vue-file-wrapper">
<input
type="file"
ref="file"
name="vue-file-input"
required
:rules='uploadDocument'
#change="onFileSelected"
>
According to the documentation, it should be
<v-file-input
:rules="uploadDocument"
#change="onFileSelected"
>
</v-file-input>
You can then leave the data property as it was
data: function () {
return {
filename: '',
uploadDocument: [
value => !!value || 'Please upload document'
],
}
}
EDIT -- since in Vuetify 1.0.5, there's no support for v-file-input, from this Github issue, you can do this
<template>
<div>
<v-text-field prepend-icon="attach_file" single-line
v-model="filename" :label="label" :required="required"
#click.native="onFocus"
:disabled="disabled" ref="fileTextField"></v-text-field>
<input type="file" :accept="accept" :multiple="false"
ref="file" #change="onFileSelected">
</div>
</template>
Your data property now becomes
data: function () {
return {
filename: '',
uploadDocument: [
value => !!value || 'Please upload document'
],
errors: {
file: ''
}
}
}
You can then style the text field using SCSS/CSS to be below the file input field or something.
One thing is for sure, the rules prop will not work on a input element because it's reserved for vuetify specific elements.
It won't be triggered by this.$refs.AgencyForm.validate() for that very reason. You will have to write custom validation
Maybe something along the lines of
methods: {
validateFile(file) {
if (!file.name) {
errors.file = 'Please select a file';
} else {
errors.file = '';
}
}
atLeastOneErrorExists(errors) {
return Object.values(errors).some(error => error.length > 0)
}
onFileSelected(event) {
var files = event.target.files || event.dataTransfer.files;
if (!files.length) {
return;
}
var file = files[0];
this.filename = file.name;
this.createImage(files[0]);
},
submit() {
this.validateFile(this.filename);
if (this.atLeastOneErrorExists(this.errors)) {
this.$axios.put('/agency.json', { agency: this.agency })
}
}
}
In your template, you can simulate the error message of Vuetify by styling a p tag that looks similar to Vuetify error messages
Something like
<div class="vue-file-wrapper">
...
<input
...
>
<p class="custom-error-class">{{errors.file}}</p> // custom error message
</div>
I wrote a function that saves an image as blob:
render() {
...
return (
...
<input
accept="image/*"
onChange={this.handleUploadImage.bind(this)}
id="contained-button-file"
multiple
type="file"
/>
)}
and this is the function called in onChange:
handleUploadImage(event) {
const that = this;
const file = event.target.files[0];
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onloadend = () => {
that.setState({
image: URL.createObjectURL(file),
userImage: reader.result,
});
};
}
I think this works fine because it saves in DB because the document has a field called image which looks like this: blob:http://localhost:3000/80953c91-68fe-4d2a-8f5e-d9dd437c1f94
this object can be accessed like this.props.product, to access the image it is this.props.product.image
The problem is when I want to show the image, I don't know how to do this.
I tried to put it in render like:
{
this.props.product.image ? (
<img alt="" src={this.props.product.image} />
) : (
null
);
}
it throws this error:
and the header:
any suggestions?
You should try URL.createObjectURL(blob)
https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
myImage.src = URL.createObjectURL(blob);
I try to preview a image before upload, I want to dispaly a small one, and on click to be full size, but till now it didn't work. I am using vue-upload-component, file.thumb from them also didn't work.
data() {
return {
url:null,
}
}
watch: {
files: {
handler: function(){
this.files.forEach((file,index) =>{
const file = e.target.file[0];
this.url = URL.createObjectURL(file);
console.log('Files:',this.files);
})
},
}
}
//First try found on stack
<div id="preview">
<img v-if="url" :src="url" />
</div>
//This is from them , but is not working.
<img v-if="file.thumb" :src="file.thumb" width="40" height="auto" />
<span v-else>No Image</span>
UPDATE
Now the small image it works, I just need to add some things form that library.
Now I just need to preview full size img hen click.
<img v-if="file.thumb" :src="file.thumb" width="40" height="auto" />
You are using the this from the forEach scope, try initializing the global ViewModel to be able to access the data params, like this :
watch: {
files: {
handler: function(){
let vm = this;
this.files.forEach((file,index) =>{
const file = e.target.file[0];
vm.url = URL.createObjectURL(file);
console.log('Files:',vm.files);
})
},
}
}
Can you try this?
handler: function(){
this.files.forEach((file,index) =>{
const file = event.target.files[0];
if (file) {
let reader = new FileReader();
reader.onload = (e: any) => {
this.url = e.target.result; //Base64 string
}
}
})
}
I have spent the better part of a full day looking through this site and the rest of the inter webs to piece together something that is probably a no-brainer for all of you top dogs. There is nothing I found that was all encompassing and overall most of the samples are missing some level of clarity.
SO I wanted to trying and accomplish an MVVM pattern and simply take JSON results from a webservice and populate a list view :)
The webservice returns this
[{"total_bulls":"651","GenericName":"Aripiprazole","brandName":"Abilify","drugCat":"Atypical Antipsychotic","bullID":2793,"fastURL":"http:\/\/got*****.com\/drug-bulletin\/abilify\/","litAlertLvl":"High"},{"total_bulls":"651","GenericName":"Zafirlukast","brandName":"Accolate","drugCat":"Leukotriene Antagonist","bullID":2794,"fastURL":"http:\/\/got****.com\/drug-bulletin\/accolate\/","litAlertLvl":"Withdrawn"},{"total_bulls":"651","GenericName":"Albuterol Sulfate Inhalation Solution","brandName":"AccuNeb","drugCat":"Bronchodilator","bullID":2855,"fastURL":"http:\/\/go***.com\/drug-bulletin\/accuneb\/","litAlertLvl":"Low"},{"total_bulls":"651","GenericName":"Quinapril Hydrochloride","brandName":"Accupril","drugCat":"ACE Inhibitor","bullID":2661,"fastURL":"http:\/\/go****.com\/drug-bulletin\/accupril\/","litAlertLvl":"Low"},{"total_bulls":"651","GenericName":"Quinapril HCl\/Hydrochlorothiazide","brandName":"Accuretic","drugCat":"ACE Inhibitor\/Thiazide Diuretic","bullID":2813,"fastURL":"http:\/\/got****.com\/drug-bulletin\/accuretic\/","litAlertLvl":"High"}]
I want the ListView to display the proper data and trigger a click action. The problems i ran into surrounded getting the results from the call to the webservice to populate the listview.
I can manually populate the model like this:
const viewModel = observableModule.fromObject({
bulletins: []
// Setting the listview binding source
/*
bulletins: [
{
"total_bulls": "651",
"GenericName": "Aripiprazole",
"brandName": "Abilify",
"drugCat": "Atypical Antipsychotic",
"bullID": 2793,
"fastURL": "http://g****s.com/drug-bulletin/abilify/",
"litAlertLvl": "High"
}, {
"total_bulls": "651",
"GenericName": "Zafirlukast",
"brandName": "Accolate",
"drugCat": "Leukotriene Antagonist",
"bullID": 2794,
"fastURL": "http://g****.com/drug-bulletin/accolate/",
"litAlertLvl": "Withdrawn"
}, {
"total_bulls": "651",
"GenericName": "Albuterol Sulfate Inhalation Solution",
"brandName": "AccuNeb",
"drugCat": "Bronchodilator",
"bullID": 2855,
"fastURL": "http://go****.com/drug-bulletin/accuneb/",
"litAlertLvl": "Low"
}
]
*/
});
However trying to do this with the JSON results from the call proved to be challenging.
After many hours of trial and error I came to this working pattern. Improvements on this are welcome.
Spin up a vanilla JS Core Nativescript 'Drawer Navigation' template project from either Sidekick or from here https://market.nativescript.org/plugins/tns-template-drawer-navigation and add these scripts (I put the first 3 in a folder named "bulletins" and the last one in a folder named "services").
I also added the list-view plugin.
bulletins-page.xml
<Page class="page" navigatingTo="onNavigatingTo"
xmlns="http://schemas.nativescript.org/tns.xsd">
<ActionBar class="action-bar">
<!--
Use the NavigationButton as a side-drawer button in Android
because ActionItems are shown on the right side of the ActionBar
-->
<NavigationButton ios:visibility="collapsed" icon="res://menu" tap="onDrawerButtonTap"></NavigationButton>
<!--
Use the ActionItem for IOS with position set to left. Using the
NavigationButton as a side-drawer button in iOS is not possible,
because its function is to always navigate back in the application.
-->
<ActionItem icon="res://navigation/menu" android:visibility="collapsed" tap="onDrawerButtonTap" ios.position="left">
</ActionItem>
<Label class="action-bar-title" text="Bulletins"></Label>
</ActionBar>
<GridLayout class="page-content">
<Label class="page-icon fa" text=""></Label>
<Label class="page-placeholder" text="<!-- Page content goes here -->"></Label>
</GridLayout>
<ScrollView>
<StackLayout>
<ListView items="{{ bulletins }}" itemTap="onItemTap" loaded="{{ onListViewLoaded }}"
separatorColor="orangered" rowHeight="100" height="500" class="list-group" id="listView" row="2">
<ListView.itemTemplate>
<!-- The item template can only have a single root view container (e.g. GriLayout, StackLayout, etc.) -->
<StackLayout class="list-group-item">
<Label text="{{ GenericName || 'Downloading...' }}" textWrap="true" class="title" />
<Label text="{{ brandName || 'Downloading...' }}" textWrap="true" class="title" />
</StackLayout>
</ListView.itemTemplate>
</ListView>>
</StackLayout>
</ScrollView>
</Page>
bulletins-page.js
const app = require("tns-core-modules/application");
const BulletinsViewModel = require("./bulletins-view-model");
const listViewModule = require("tns-core-modules/ui/list-view");
function onNavigatingTo(args) {
const page = args.object;
//bind the page to this the viewModel Function
page.bindingContext = new BulletinsViewModel();
//now call the function that GETS the data from the API AFTER the model is declared
BulletinsViewModel.showBulletins()
}
exports.onNavigatingTo = onNavigatingTo;
function onListViewLoaded(args) {
const listView = args.object;
}
exports.onListViewLoaded = onListViewLoaded;
function onItemTap(args) {
const index = args.index;
console.log(`Second ListView item tap ${index}`);
}
exports.onItemTap = onItemTap;
function onDrawerButtonTap(args) {
const sideDrawer = app.getRootView();
sideDrawer.showDrawer();
}
exports.onDrawerButtonTap = onDrawerButtonTap;
bulletins-view-model.js
const observableModule = require("tns-core-modules/data/observable");
const SelectedPageService = require("../shared/selected-page-service");
const bulletinService = require("~/services/bulletin-service");
function BulletinsViewModel() {
SelectedPageService.getInstance().updateSelectedPage("Bulletins");
//declare the viewmodel
const viewModel = observableModule.fromObject({
//declare the properties of this viewmodel
bulletins: []
});
//declare a function to be called LATER during the navigation to the view
BulletinsViewModel.showBulletins = function () {
//call the fetch function and pass it the users info
bulletinService.allBulletins({
user: 'admin',
password: this.password
}).then((r) => {
console.log(r);
//pass the response to the properties of the model
viewModel.bulletins = r;
})
.catch((e) => {
console.log(e);
alert("Unfortunately we could not find any bulletins");
});
}
return viewModel;
}
module.exports = BulletinsViewModel;
bulletin-service.js
exports.allBulletins = function () {
return new Promise((resolve, reject) => {
fetch("https://got****.com/wp-admin/admin-ajax.php?action=all-bulletins-paged")
.then((response) => response.json())
.then((r) => {
if (r.total_bulls == 0) {
//console.log('No Bulletins Found' + r.total_bulls);
reject(r);
}
//console.log('JSON Bulletins Found' + JSON.stringify(r));
resolve(r);
}).catch((err) => {
console.log(err);
reject(err);
});
});
};