Dynamic rendering of images with v-for? - vue.js

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?

Related

Bootstrap image asset not processed for props style attribute on Vue Component (Nuxt.js)

I have been trying to import an image asset relative path to a banner component. The following works just fine.
<b-img src="~/static/images/carousel1.jpg" alt="Samyojya Consultants banner"/>
On html, I see it rendered as this
<div class="card-body"><img src="/_nuxt/static/images/carousel1.jpg"...
But the v-bind style representation like this does not bundle the image
<b-img :src="imgSrc" :alt="title+'banner'"/>
I can see on the html that imgSrc value is passing on but not compiled by asset processor
<div class="card-body"><img src="~/static/images/carousel1.jpg" ...
Is there a way we can explicitly trigger this compilation? require doesn't seem to work too.
<b-img :src="require(imgSrc)" :alt="require(title)+'banner'"/>
This dynamic style is needed for my use-case.
Create a computed prop (or method, or similar) to resolve (require) the relative path:
export default {
data() {
return {
title: 'Image title'
}
},
computed: {
imgSrc() {
// Relative to component directory
return require('./image.png')
}
}
}
And then reference that in your template:
<b-img :src="imgSrc" :alt="title+' banner'"/>
On the calling (parent) template, I used this
<banner :imgSrc="imgSrc" ...
And the data export in parent like this.
export default {
data: function(){
return {
imgSrc:require('../static/images/carousel2.jpg')
}
},
...
In the child component where the banner is drawn.
<b-img :src="imgSrc"...
Note: require needs a relative path (../static) from components/pages while without require we can use absolute (~/static).
<b-img :src="require('../static/images/carousel1.jpg')" alt="Samyojya Consultants banner"/>

How do I create new instances of a Vue component, and subsuqently destroy them, with methods?

I'm trying to add components to the DOM dynamically on user input. I effectively have a situation with ±200 buttons/triggers which, when clicked, need to create/show an instance of childComponent (which is a sort of infowindow/modal).
I would also then need to be able to remove/hide them later when the user 'closes' the component.
I'm imagining something like this?
<template>
<div ref="container">
<button #click="createComponent(1)" />
...
<button #click="createComponent(n)" />
<childComponent ref="cc53" :num="53" v-on:kill="destroyComponent" />
...
<childComponent ref="ccn" :num="n" v-on:kill="destroyComponent"/>
</div>
</template>
<script>
import childComponent from '#/components/ChildComponent'
export default {
components: {childComponent},
methods: {
createComponent (num) {
// How do I create an instance of childComponent with prop 'num' and add it to this.$refs.container?
},
destroyComponent (vRef) {
// How do I destroy an instance of childComponent?
this.vRef.$destroy();
}
}
}
</script>
The number of possible childComponent instances required is finite, immutable and known before render, so I could loop and v-show them, but your typical user will probably only need to look at a few, and certainly only a few simultaneously.
My questions:
Firstly, given there are ±200 of them, is there any performance benefit to only creating instances dynamically as and when required, vs. v-for looping childComponents and let Vue manage the DOM?
Secondly, even if v-for is the way to go for this particular case, how would one handle this if the total number of possible childComponents is not known or dynamic? Is this a job for Render Functions and JSX?
If I understand, you want to display a list of the same component that take :num as a prop.
First, you have to keep in mind that Vue is a "Data driven application", wich means that you need to represent your list as Data in an array or an object, in your case you can use a myList array and v-for loop to display your child components list in the template.
The add and remove operations must be donne on the myList array it self, once done, it will be automatically applied on your template.
To add a new instance just use myList.push(n)
To remove an instance use myLsit.splice(myLsit.indexOf(n), 1);
The result should look like this :
<template>
<input v-model="inputId" />
<button #click="addItem(inputId)">Add Item</button>
<childComponent
v-for="itemId in myList"
:key="itemId"
:ref="'cc' + itemId"
:num="itemId"
#kill="removeItem(itemId)"
/>
</template>
<script>
data(){
return{
inputId : 0,
myList : []
}
},
methods:{
addItem(id){
this.myList.push(id)
},
removeItem(id){
this.myLsit.splice(this.myLsit.indexOf(id), 1)
}
}
</script>
Ps :
Didn't test the code, if there is any error just tell me
#kill method must be emitted by the childComponent, $emit('kill', this.num)
Here is an excellent tutorial to better understand v-for
Performance Penalties
As there is only a limited possibility of ±200 elements, I highly doubt that it can cause any performance issue, and for further fine-tuning, instead of using v-show, you can use v-if it'll reduce the total memory footprint, but increases the render time if you're going to change the items constantly.
Other Approaches
If there weren't limited possibilities of x elements, it'd be still and v-for having items which contain the v-if directive.
But if the user could only see one item (or multiple but limited items) at the same time, instead of v-for, It'd much better to directly bind the properties to the childComponent.
For example, if the child component is a modal that'll be shown by the application when a user clicked on the edit button for a row of a table. Instead of having x number of modals, each having editable contents of a row and showing the modal related to the edit button, we can have one modal and bind form properties to it. This approach usually implemented by having a state management library like vuex.
Finally, This is an implementation based on vuex, that can be used, if the user could only see one childComponent at the same time, it can be easily extended to support multiple childComponent viewed at the same time.
store.js
export Store {
state: {
childComponentVisible: false,
childComponentNumber: 0
},
mutations: {
setChildComponentNumber(state, value) {
if(typeof value !== 'number')
return false;
state.childComponentNumber = value;
},
setChildComponentVisibility(state, value) {
if(typeof value !== 'boolean')
return false;
state.childComponentVisible = value;
}
}
}
child-component.vue
<template>
<p>
{{ componentNumber }}
<span #click="close()">Close</span>
</p>
</template>
<script>
export default {
methods: {
close() {
this.$store.commit('setChildComponentVisibility', false);
}
}
computed: {
componentNumber() {
return this.$store.state.childComponentNumber;
}
}
}
</script>
list-component.vue
<template>
<div class="list-component">
<button v-for="n in [1,2,3,4,5]" #click="triggerChildComponent(n)">
{{ n }}
</button>
<childComponent v-if="childComponentVisible"/>
</div>
</template>
<script>
export default {
methods: {
triggerChildComponent(n) {
this.$store.commit('setChildComponentNumber', n);
this.$store.commit('setChildComponentVisibility', true);
}
},
computed: {
childComponentVisible() {
return this.$store.state.childComponentVisible;
}
}
}
</script>
Note: The code written above is abstract only and isn't tested, you might need to change it a little bit to make it work for your own situation.
For more information on vuex check out its documentation here.

Work process for data passing in VueJS 2 application

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.

How to use the contents of a Vue js component as are initially loaded and then bind them to a variable

We are looking to get the contents from the html (for example using a slot ) but we later (after mounting) want to bind this to a variable as one would do with a v-html.
In other words we want on initialization to have the contents being loaded as they appear in the slot and later this entire piece of content to be controlled by a binding variable like v-html (because it will be html content).
How to achieve this? without following any ugly solutions such as needing to pass the entire initial html content inside v-html attribute!
You can try like this
<template>
<custom-component>
<div v-if="html_variable" v-html="html_variable"></div>
<template v-else>
<!-- default content here -->
</template>
</custom-component>
</template>
<script>
export default
{
data()
{
return {html_variable: null};
}
}
</script>
How using v-html is ugly? Anyway just use a non-visible element to define your initial html. a <script ref="initHTML" type="text/html"> ... </script> might be a good option.
Define a data entry that should hold your HTML init:
data () {
return {
dHTML: '',
}
},
On created() update your initial html state using the ref
created() {
this.dHTML = this.$refs.initHTML.innerHTML;
},
Now you can have a pretty v-html="dHTML"

Can't load images from relative path with Vue V-Lazy-Image Plugin

I'm using this plugin to create a portfolio gallery on a single page components application but I'm stuck loading the images from a relative path, it works fine if I use a full URL path.
When I use the full URL the image Loads and loose the blur as expected, instead when I use a relative path the image load but it keeps blurred and it doesn't load the class .v-lazy-image-loaded into the image tag. Any Ideas? here is the code.
LazyImage.vue
<template>
<v-lazy-image :src="src" />
</template>
<script>
import VLazyImage from 'v-lazy-image'
export default {
props: {
// HERE I TRIED TO USE A FUNCTION ALSO WITH NO LUCK
// src: Function
src: String
},
components: {
VLazyImage
},
// UPDATE: I added a method to test here.
methods: {
consoleSuccess (msg) {
console.log(msg)
}
}
}
</script>
Portfolio.vue
<template>
<section id="portfolio">
// I'M TRYING THIS TWO OPTIONS
// UPDATED: I Added here the events calls and its just calling the intersect but the load.
<v-lazy-image src="../assets/img/innovation.jpg" alt="alternate text" #load="consoleSuccess('LOADED!')" #intersect="consoleSuccess('INTERSECS!')"></v-lazy-image> <-- this just work with full url ex. http://cdn...
<v-lazy-image :src="require('../assets/img/innovation.jpg')" alt="alternate text"></v-lazy-image> <-- this loads the image but remains blured
</section>
</template>
<script>
import VLazyImage from '#/components/lazyImage'
export default {
components: {
VLazyImage
}
}
</script>
UPDATE
I added a method to test and realized that just the #intersect event it's called and the #load won't fire at all with relative paths.
I noticed this was caused by bug in the component code:
if (this.$el.src === this.src) {
should be replaced by
if (this.$el.getAttribute('src') === this.src) {
I've sent pull request for this.