Interpolate dynamic components within a v-for loop - vue.js

I currently have a v-for which creates a list of components depending on an array of name values.
Depending on the step === 'name' check, I'm displaying the relevant component. This method requires that I list every single possible component with a v-if and I think there should be a cleaner way to accomplish this.
<div v-for="(step, idx) in steps" :key="idx">
<div v-if="step === 'url'">
<Goto #removeStep="removeStep(idx)" />
</div>
<div v-if="step === 'click'">
<Click #removeStep="removeStep(idx)" />
</div>
<div v-if="step === 'search'">
<Search #removeStep="removeStep(idx)" />
</div>
...
</div>
I'd rather interpolate the component directly in my for loop.
For example, something like...
<div v-for="(step, idx) in steps" :key="idx">
<[step.component] #removeStep="removeStep(idx)" :title="step.title" />
</div>
My array of dynamic components would then look like:
steps: [
{
component: 'Goto',
title: 'Go to Url'
},
{
component: 'Click',
title: 'Click Something'
}
]
I do not want to have all component options in a single file component, and prefer them to remain as separate components as I have currently.
Could I accomplish the above using either some form of interpolation or a computed prop perhaps?

Vue supports dynamic components with <component is="COMPONENT_NAME">. You could register your components locally, and then bind <component>.is to the component names in your steps array:
<template>
<div>
<component
:is="step.component"
v-for="(step, idx) in steps"
:key="step.component"
:title="step.title"
#remove-step="removeStep(idx)"
/>
</div>
</template>
<script>
import Goto from './components/Goto'
import Click from './components/Click'
import Search from './components/Search'
export default {
components: {
Goto,
Click,
Search,
},
data() {
return {
steps: [
{
component: 'Goto',
title: 'Go to Url',
},
{
component: 'Click',
title: 'Click Something',
},
{
component: 'Search',
title: 'Search the Internet',
},
],
}
},
methods: {
removeStep(idx) {
console.log('remove step', idx)
},
},
}
</script>
demo

Related

How do have unique variables for each dynamically created buttons/text fields?

I'm trying to create buttons and vue element inputs for each item on the page. I'm iterating through the items and rendering them with v-for and so I decided to expand on that and do it for both the rest as well. The problem i'm having is that I need to to bind textInput as well as displayTextbox to each one and i'm not sure how to achieve that.
currently all the input text in the el-inputs are bound to the same variable, and clicking to display the inputs will display them all at once.
<template>
<div class="container">
<div v-for="(item, index) in items" :key="index">
<icon #click="showTextbox"/>
<el-input v-if="displayTextbox" v-model="textInput" />
<el-button v-if="displayTextbox" type="primary" #click="confirm" />
<ItemDisplay :data-id="item.id" />
</div>
</div>
</template>
<script>
import ItemDisplay from '#/components/ItemDisplay';
export default {
name: 'ItemList',
components: {
ItemDisplay,
},
props: {
items: {
type: Array,
required: true,
},
}
data() {
displayTextbox = false,
textInput = '',
},
methods: {
confirm() {
// todo send request here
this.displayTextbox = false;
},
showTextbox() {
this.displayTextbox = true;
}
}
}
</script>
EDIT: with the help of #kissu here's the updated and working version
<template>
<div class="container">
<div v-for="(item, index) in itemDataList" :key="itemDataList.id">
<icon #click="showTextbox(item.id)"/>
<El-Input v-if="item.displayTextbox" v-model="item.textInput" />
<El-Button v-if="item.displayTextbox" type="primary" #click="confirm(item.id)" />
<ItemDisplay :data-id="item.item.uuid" />
</div>
</div>
</template>
<script>
import ItemDisplay from '#/components/ItemDisplay';
export default {
name: 'ItemList',
components: {
ItemDisplay,
},
props: {
items: {
type: Array,
required: true,
},
}
data() {
itemDataList = [],
},
methods: {
confirm(id) {
const selected = this.itemDataList.find(
(item) => item.id === id,
)
selected.displayTextbox = false;
console.log(selected.textInput);
// todo send request here
},
showTextbox(id) {
this.itemDataList.find(
(item) => item.id === id,
).displayTextbox = true;
},
populateItemData() {
this.items.forEach((item, index) => {
this.itemDataList.push({
id: item.uuid + index,
displayTextbox: false,
textInput: '',
item: item,
});
});
}
},
created() {
// items prop is obtained from parent component vuex
// generate itemDataList before DOM is rendered so we can render it correctly
this.populateItemData();
},
}
</script>
[assuming you're using Vue2]
If you want to interact with multiple displayTextbox + textInput state, you will need to have an array of objects with a specific key tied to each one of them like in this example.
As of right now, you do have only 1 state for them all, meaning that as you can see: you can toggle it for all or none only.
You'll need to refactor it with an object as in my above example to allow a case-per-case iteration on each state individually.
PS: :key="index" is not a valid solution, you should never use the index of a v-for as explained here.
PS2: please follow the conventions in terms of component naming in your template.
Also, I'm not sure how deep you were planning to go with your components since we don't know the internals of <ItemDisplay :data-id="item.id" />.
But if you also want to manage the labels for each of your inputs, you can do that with nanoid, that way you will be able to have unique UUIDs for each one of your inputs, quite useful.
Use an array to store the values, like this:
<template>
<div v-for="(item, index) in items" :key="index">
<el-input v-model="textInputs[index]" />
</div>
<template>
<script>
export default {
props: {
items: {
type: Array,
required: true,
},
},
data() {
textInputs: []
}
}
</script>

Remove dynamically-added Vue component

I'm updating a form built with Vue that lets you dynamically add and remove blocks of fields. Currently, it's set up so that the add and remove buttons are at the bottom of the interface and the remove button removes blocks in the reverse order of how they're added.
Now I need to update it so that each block or row has its own remove button so that you can remove any block. Because at the moment, if you add 10 blocks, and then decide you want to remove the first one, you have to remove all others first.
I've created a Vue Sandbox to illustrate. At the moment, even though I've got remove buttons per row, they're still only removing the rows in reverse order as before. That's because the remove function is just reducing the index by 1 each time.
But I'm not sure what I need to update though so that it removes the component matching the id of the button you click on.
Parent component
<template>
<div>
<component
:is="fieldType"
v-for="i in index"
:key="i"
:index="i"
#add="add"
#remove="remove"
/>
<div>
<button type="button" #click="add">Add {{ label }}</button>
</div>
</div>
</template>
<script>
import textField from "./textField";
export default {
components: {
textField,
},
props: {
fieldType: {
type: String,
required: true,
},
label: {
type: String,
default: "",
}
},
data: () => ({
index: 0,
}),
methods: {
add() {
this.index += 1;
},
remove() {
this.index -= 1;
},
},
};
</script>
Child component
<template>
<div>
<label :for="'text-' + index" v-text="'Text' + index"></label>
<input :id="'text-' + index" type="text" name="" />
<button v-if="index" type="button" #click="$emit('remove', index)">
Remove
</button>
</div>
</template>
<script>
export default {
props: {
index: {
type: Number,
required: true,
},
},
};
</script>

Vue.js don't know how to pass props to map() function and looping components

I am beginner at Vue and i am trying to convert this React piece of code in to my Vue App and i am struggling to fix it. I'm looping an object with map() function and passing props to it.
React:
<div className='directory-menu'>
{this.state.sections.map(({ id, ...otherSectionProps }) => (
<MenuItem key={id} {...otherSectionProps} />
))}
</div>
And i have this state object:
this.state = {
sections: [
{
title: 'hats',
imageUrl: 'https://i.ibb.co/cvpntL1/hats.png',
id: 1,
linkUrl: 'hats'
}, // the other elements continue
And in Vue I want to do the same thing:
<template>
<div class="directory-menu">
{{
sections.map(() => (
<MenuItem :key="sections.id" v-bind="sections" />
))
}}
</div>
</template>
Data where props are coming from:
<script>
import MenuItem from '../Menu-Item/MenuItem.vue';
export default {
components:{
MenuItem
},
data(){
return{
sections: [
{
title: 'hats',
imageUrl: 'https://i.ibb.co/cvpntL1/hats.png',
id: 1,
linkUrl: 'hats'
}, // the other elements continue
I don't know if i am failing to spread the props with spread operator v-bind or something else that i am missing.
You should use v-for in vuejs to render a list :
<div class="directory-menu">
<MenuItem v-for="section in sections" :key="section.id"/>
</div>

How can I set custom template for item in list and than use it inside `v-for` loop?

What I want to achieve is something like:
<li v-for="(item, index) in items" :key="index>
<div v-if="item.Component">
<item.Component :value="item.value" />
</div>
<div v-else>{{ item.value }}</div>
</li>
But anyway I don't like at all this solution. The idea of defining Component key for an item in items list is hard to maintain since at least it is hard to write it in template-style way (usually we are talking about too long HTML inside). Also I don't like to wrap item.Component inside div.
data() {
return {
list: [{
value: 'abc',
Component: {
props: ['value'],
template: `123 {{ value }} 312`
}
}]
};
}
Does anyone know the best-practice solution for this and where Vue describes such case in their docs?
You can use Vue's <component/> tag to dynamically set your component in your list.
<li v-for="(item, index) in items" :key="index>
<component v-if="item.Component" :is="item.Component" :value="item.value"></component>
<div v-else>{{ item.value }}</div>
</li>
<script>
...,
data: () => ({
list: [{
value: 'abc',
Component: {
props: ['value'],
template: `<div>123 {{ value }} 312</div>` // must be enclosed in a element.
}
}]
})
</script>
You can also import a component too so you can create a new file and put your templates and scripts there.
Parent.vue
<script>
import SomeComponent from "#/components/SomeComponent.vue"; //import your component here.
export default {
data() {
return {
list: [
{
value: "abc",
Component: SomeComponent // define your imported component here.
},
]
};
}
};
</script>
SomeComponent.vue
<template>
<div>123 {{ value }} 312</div>
</template>
<script>
export default {
name: "SomeComponent",
props: ["value"]
};
</script>
Here's a demo.

Vue.js - Inject el elements to html

I have website for online tests.
One of the question that i have created on the test its topic "Fill in the blank", which means fill in the blank spaces words.
The question comes from the server as a string like that "Today is a [1] day, and i should [2] today".
What i want to do is to get that string and replace all the [] with el-input.
I have done something like that
<template>
<div class="d-flex flex-column mg-t-20 pd-10">
<h6 class="tx-gray-800">Fill in the blank areas the missing words</h6>
<div class="mg-t-20" v-html="generateFillBlankQuestion(question.question)" />
</div>
</template>
<script>
export default {
name: 'FillBlank',
directives: {},
props: [ 'question' ],
components: {
},
computed: {},
data() {
return {
input: ''
}
},
filters: {},
created() {
},
methods: {
generateFillBlankQuestion(question) {
var matches = question.match((/\[\d\]/g))
console.log(matches)
matches.forEach((element) => {
console.log(element)
question = question.replace(element, '<el-input />')
})
console.log(question)
return question
}
}
}
On this line question = question.replace(element, '<el-input />') I'm replacing the [] to input.
For some reason when i try to replace it to <el-input> it doesn't render it.
But if i use <input type='text'> it renders it.
Is it possible to inject el elements?
If you are not using the Vue run-time template compiler you can not render Vue components inside v-html. You should do something like this:
<template>
<div class="d-flex flex-column mg-t-20 pd-10">
<h6 class="tx-gray-800">Fill in the blank areas the missing words</h6>
<div class="mg-t-20">
<template v-for="(word,idx) in wordList">
<el-input v-if="word.blank" v-model="word.value" :key="idx" />
<template v-else>{{ word.text }}</template>
</template>
</div>
</div>
</template>
<script>
export default
{
name: 'FillBlank',
props:
{
question:
{
type: String,
default: ''
}
},
computed:
{
wordList()
{
const words = this.question.split(' ');
return words.map(word =>
({
value: '',
text: word,
blank: /^\[\d+\]$/.test(word),
}));
}
}
}