Work process for data passing in VueJS 2 application - vue.js

I am pretty new to VueJS 2, so wanted to see if I am working in the correct way. I have a system where someone uploads a file that contains data, which will then be used to create charts. So I display the uploaded files to them
<tr v-for="file in files.data" :key="file.id">
//file information
<td>
<router-link :to="{ name: file.chart, params: { fileName: file.name }}"
tag="a" exact> View Results
</router-link>
</td>
</tr>
So you can see I have a link in the table, that directs them to the chart page for the file they uploaded. It includes the params for the file name to be loaded.
On the chart page, I get the params within the created method. I then pass these to the component for the chart to be displayed
<template>
<div>
//some information
<div class="row">
<div class="col-12" id="parentDiv">
<barchart :file-name = this.fileName></barchart>
</div>
</div>
</div>
</template>
<script>
import Barchart from '../charts/Barchart';
export default {
components: {
'barchart': Barchart
},
data() {
return {
fileName: ''
}
},
created() {
this.fileName = this.$route.params.fileName;
}
}
</script>
Finally, I have the Barchart component. This is what creates the chart based on the file uploaded data.
<script>
import * as d3 from 'd3';
export default {
props: {
fileName: {
type: String,
required: true
}
},
methods: {
createBarChart() {
//d3 to create chart using the file that was uploaded
}
},
created() {
let vm = this;
d3.json('storage/' + this.fileName)
.then(function (data) {
vm.createBarChart(data);
}).catch(function (error) {
// handle error
});
}
};
</script>
To me, there seems to be a lot of passing of data from one component to the next. I pass it from the files display component (which displays all uploaded files), then to the page for the chart, which then passes it to the chart component.
The other issue is, if I am on the charts page, and I refresh the page, then the chart no longer has the filename prop and therefore the chart does not render. How would I handle this
situation?
Any advice appreciated

The reason that you are losing the chart on refresh is due to the use of the created method.
In your chart component remove the entire created method and reference the route param directly in your barchart reference, like so:
<template>
<div>
//some information
<div class="row">
<div class="col-12" id="parentDiv">
<barchart :file-name="$route.params.fileName"></barchart>
</div>
</div>
</div>
</template>
<script>
import Barchart from '../charts/Barchart';
export default {
components: {
'barchart': Barchart
},
data() {
return {
}
}
}
</script>

You may want to look into vuex to manage the data passing from parent to some deeply nested child.
Before you decide you want to persist the file in the nested component, you may want to consider if this is good UX (does it make sense that when the user refreshes the page, the old file they had uploaded is still cached?) You can look into using localStorage to store things locally so that upon refresh, the data is still there without needing the user to re-enter it.

Related

passing object to component using v-for

I am trying to send a series of objects that are in an array to a child component using v-for, but when I try to access them from the child component, it tells me that the props are not defined.
Im using Quasar Framework actually
This is how I pass the data:
<div class="row justify-center">
<foo
v-for="brand in brands"
:key="brand.id"
:brand="brand"
></foo>
</div>
<script>
import foo from "src/components/foo.vue";
export default {
components: {
foo
},
data() {
return {
brands: []
};
},
methods: {
async getData() {
let x = await get.getData();
this.brands = x.data;
console.log(this.brands);
}
},
mounted() {
this.getData();
}
};
</script>
brands is an array that obtains two objects from a request made to a local database, which I have already verified that it receives the data correctly
And this is the component file and how I try to get the properties:
<q-card class="my-card" flat bordered>
<q-img
:src="require(`../assets/${brand.img}`)"
:alt="brand.img + ' Logo'"
/>
<div class="text-h5 q-mt-sm q-mb-xs">{{ brand.name }}</div>
<div class="text-caption text-grey">
<p>
{{ brand.price }}
</p>
</div>
<script>
export default {
name: "foo",
props: ["brand"],
data() {
return {
expanded: false
};
},
};
</script>
but when I try to execute the code it gives me the following error:
Error in render: "Error: Cannot find module './undefined'
I know one way to make it work, and it is by creating a property for each of the object's values, for example:
<component
v-for="brand in brands"
:key="brand.id"
:name="brand.name"
:price="brand.price"
></component>
But I dont think thats the correct way to do this....
try to change
import component from "src/components/component.vue";
to
import foo from "src/components/component.vue";
on your components section you just call foo instead of foo:component
I am not sure, but:
Looks like ${brand} is empty. Your function GetData() is async, so the <foo> is created before the GetData() has its data set/returned.
You can change
<foo v-for="brand in brands" :key="brand.id" :brand="brand"></foo>
To
<foo v-if="brands.length> 0" v-for="brand in brands" :key="brand.id" :brand="brand"></foo>
To make sure that the element is renderd after the data if set.
Note: v-if is when the html is rendered, v-show is just a css display hide, but the html is always renderd

<b-form-file> does not keep the filename visible when switched to other components

I am new to Vue and using Bootstrap view for my form.
I am using b-form-file for uploading a file in component say Component2.vue and I am able to browse my local filesystem and select the file. The filename is seen in the b-form-file after the selection.
When I visit another component(Component1.vue) via the UI and come back to Component2.vue, the filename is not visible in the browse anymore. Although I have used v-model to bind it to the file and the bind is happening but the filename is not displayed in the b-form-file.
I want to keep the filename visible in the b-form-file as it is binded to 'file' throughout the session.
I am using b-form-file like this:
<form>
<b-form-file
id="form-model"
v-model="file"
required
placeholder="Choose a model..."
></b-form-file>
<form>
<script>
export default {
data() {
return { file: null }
}
}
</script>
well I was using tab view to display <b-form-file> and encountered the same behavior. As I was not using <router-view> so the <keep-alive> solution did not worked for me. I found a nice little trick by dynamically setting the placeholder prop of the <b-form-file> component to display the filename. The following code is pretty self-explanatory I think.
<template>
<b-form-file
v-model="file"
id="file"
:placeholder="fileName"
drop-placeholder="Drop file here..."
accept=".xlsx, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
/>
</template>
<script>
import {
BFormFile,
} from "bootstrap-vue";
export default {
components: {
BFormFile,
},
data() {
return {
file: null,
fileName: "Choose a .xlsx file or drop it here...",
};
},
watch: {
file(newVal, _) {
this.fileName = newVal.name;
},
},
};
</script>

Mounted method called twice when used component in page

I have a modal code in one component which i am using in the page, everything works fine but if I refresh the page than mounted hook called twice which is breaking my code. I want it to called only once.
import axios from '~/plugins/axios';
export default {
data() {
return {}
};
},
components: {
LoginSignupModal
},
mounted() {
console.log('mounted called......');
},
<template>
<div>
<p>
<LoginSignupModal :isModalOpen='isModalOpen' v-on:onModalClose="onModalClose($event)"></LoginSignupModal>
</p>
</div>
</template>
You cannot paste div into paragraph.
Remove <p> and it should work.

Vue component not rendered on second visit

I have a Vue component that lists a bunch of clickable tags. When you click on a tag, it takes you to another page with a list of objects containing that tag.
The relevant parts of the component code are:
<template>
<div>
<h2>All Tags</h2>
<TagList v-bind:tags="tags"/>
</div>
</template>
...
<script>
import TagList from './TagList'
export default {
name: 'AllTags',
components: {
TagList
},
data () {
return {
tags: []
}
},
mounted () {
tags = // array loaded from a database
}
}
</script>
This all works fine when I initially view the page. However if I browse away from this list, e.g. by clicking on a single tag, and then browse back, I only see the <h2>All Tags</h2> header. Using the Vue debugger in the browser, I can see that the data are still there.
I'm using <router-view :key="$route.fullPath"> to control the overall app and suspect the problem lies with the keys somehow.
Can someone point me in the right direction here? How can I get the TagList component to render every time I visit that page of the app?
EDIT: Here's the code of the TagList component:
<template>
<div class="tags">
<Tag v-for="tag in tags" v-bind:tag="tag" v-bind:key="tag" />
</div>
</template>
<script>
import Tag from './Tag'
export default {
name: 'TagList',
props: ['tags'],
components: {
Tag
}
}
</script>
You can try removing v-bind all thought its not required to use, I've checked your code it seems to work fine after visiting a tag and going back, all tags are still rendered. You can take a look at this working sample .
https://codesandbox.io/s/vue-template-3tcs4?fontsize=14

Dynamic rendering of images with v-for?

I want to render images from a local folder with v-for, but how to make it 100% dynamic?
I tried the solutions offered in this thread. When I tried the most useful solution, I just get a blank page, unless I fill the array with the name of the images.
<template>
<div class="comp__cardroster">
<div class="container__cards" >
<div v-for="image in images" :key="image" class="tile--outer">
<img class="tile--inner" :src="selectImage(image)" :alt="image"></div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
images: []
}
}
methods: {
selectImage(image) {
return require('#/assets/card-images/' + image + ".jpg")
}
}
}
</script>
The code above gives me a blank page. But when I fill the array with values, like below, I do get a result. But I don't want this obviously.
data() {
return {
images: [1,2,3,4,5,6,7,8,9,10]
}
}
I would want the code to render the images dynamically, no matter how many images I do have in my "assets/card-images" folder and without having to manually add values in the array each time I add a new image to the folder.
What am I doing wrong? I thank you for any advise.
UPDATE
Things I tried;
moving the "images" array from data to computed
moving "selectImage" method from methods to computed
moving both "images" array and "selectImage" method into computed
Either I get a blank page, or I get the same result as before
I don't think I was clear enough with my comments, so I'll explain what I meant with an example; you should be able to apply this to your use case with minimal effort. My src file structure and a screenshot of the result are also at the bottom of this answer.
The template I've made is basically the same as yours (without the extra divs with classes):
<template>
<div>
<div v-for="image in images" :key="image">
<img :src="selectImage(image)" :alt="image" />
</div>
</div>
</template>
Here's my script. I'll go through it all below:
<script>
export default {
name: 'app',
computed: {
images: function() {
const x = require.context('#/assets/card-images/', true, /\.png$/)
return this.importAll(x)
}
},
methods: {
importAll(r) {
return r.keys().map(x =>
x.substring(2, x.length) // remove "./" from file names
)
},
selectImage(image) {
return require('#/assets/card-images/' + image)
}
}
}
</script>
computed
The computed section is where you define your dynamically generated or computed values. Since you want your images to be dynamically generated, I've made images a computed function (can probably just be a value, you can play around with that).
All that images does is it uses require.context to get a list of all of the .png images in my #/assets/card-images/ folder, and trims the first couple of characters from them.
methods
importAll just retrieves and trims the image names. I've done this because otherwise, it'll think the images are at #/assets/card-images/./xxxxx.png - there's probably a better way of doing this but it works well enough.
selectImage gets an image from the file name you pass in (if it exists). If the image name doesn't exist, this will break but that shouldn't happen with how this is implemented.
Note: You can technically shorten the v-for loop by putting it directly on the img tag if you really want to, though I'd argue this is less readable:
<template>
<div>
<img v-for="image in images"
:key="image"
:src="selectImage(image)"
:alt="image" />
</div>
</template>
Here is my src folder structure. It doesn't matter what the images are called, as long as they have the same extension as you're using in your script tag:
Here is what the code prints out (all of the images are just copies of the Vue logo):
EDIT
If you want to keep your initial images array, you can move the computed stuff into the lifecycle method mounted or created (depending on your use-case). Read more about lifecycle methods here or here. Here's what my component would look like with the calculations in mounted:
<template>
<div>
<div v-for="image in images" :key="image">
<img :src="selectImage(image)" :alt="image" />
</div>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
images: []
}
},
mounted() {
const x = require.context('#/assets/card-images/', true, /\.png$/)
this.images = this.importAll(x)
},
methods: {
importAll(r) {
return r.keys().map(x =>
x.substring(2, x.length) // remove "./" from file names
)
},
selectImage(image) {
return require('#/assets/card-images/' + image)
}
}
}
</script>
Use a requireAll method to get an array, and your count. Or a custom loader.
How to load all files in a directory using webpack without require statements
node.js require all files in a folder?