Remove dynamically-added Vue component - vue.js

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>

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>

How to change a prop value in a generated vue components for single instance or for all instances?

Trying to create a simple blog style page. Every post has a like button, that increments when clicked. I generate 10 of these components with a v-for loop, taking data from a vuex store. However, I'd like there to be a button on the home page that resets all of the like counters.
By googling I seem to find and get working solutions that do either one or the other, not together. Yet to get anything working at all except singular counters.
How can I add a button that resets all the PostEntity counter props? Or how should I restructure it? I've thought about somehow doing in with states.
This is my post component, that gets looped in the main view .vue object:
<template>
<div class="post">
<div class="postheader">
<img :src="profilePic" alt="profilepic" class="profilepic" />
<p>{{ postDate }}</p>
</div>
<div class="postbody">
<img :src="postImage" />
<p>{{ postParagraph }}</p>
</div>
<div class="postfooter">
<!--<img :src="require('#/assets/' +nation.drapeau)"/> -->
<img
:src="require('#/assets/like.png')"
class="likepilt"
#click.prevent="increment"
/>
<p>Number of likes: {{ count }}</p>
</div>
</div>
</template>
<script>
export default {
name: 'PostEntity',
props: {
postDate: String,
postImage: String,
profilePic: String,
postParagraph: String
},
data: function () {
return {
count: 0
};
},
methods: {
increment() {
this.count++;
}
}
};
</script>
This is how I retrieve info from my VueX store:
getters: {
postListStuff: state => {
const postListStuff = state.postList.map(post => {
return {
id: post.id,
img: post.img,
profilepic: post.profilepic,
date: post.date,
paragraph: post.paragraph
};
});
return postListStuff;
}
}
This is how I display the components and generate the posts:
<template>
<HeaderBox title-text="Homepage" />
<div v-for="post in postListStuff" :key="post.id" class="posts">
<PostEntity
:post-date="post.date"
:profile-pic="post.profilepic"
:post-image="post.img"
:post-paragraph="post.paragraph"
></PostEntity>
</div>
<FooterBox />
<HelloWorld />
</template>
<script>
import HelloWorld from './components/HelloWorld.vue';
import HeaderBox from '#/components/Header';
import FooterBox from '#/components/Footer';
import PostEntity from '#/components/Post';
export default {
name: 'App',
components: {
FooterBox,
HeaderBox,
HelloWorld,
PostEntity
},
computed: {
postListStuff() {
return this.$store.getters.postListStuff;
}
}
};
</script>
There are multiple possible ways to go about doing this, but the simplest way I can think of with least amount of code would be:
Add a reset method to the PostEntity component that sets count to 0.
methods: {
increment() {
this.count++;
},
reset() {
this.count = 0;
}
}
Then in the parent component add a ref to the PostEntity components inside the v-for loop, then add a new button with onclick method resetCounters:
<div v-for="post in postListStuff" :key="post.id" class="posts">
<PostEntity
ref="post"
:post-date="post.date"
:profile-pic="post.profilepic"
:post-image="post.img"
:post-paragraph="post.paragraph"
></PostEntity>
</div>
<button #click="resetCounters">Reset</button>
resetCounters will loop through the array of PostEntity refs and call the reset method on each of them.
methods: {
resetCounters() {
this.$refs.post.forEach(p => p.reset());
}
}

How to register an array of objects with v-model? Vue 2

The first time that I dive into making this type of form with Vue, the issue is that I can't think of how to save the data inside the foreach that I generate with axios.
Where I would like to save the ID and the option selected with the input select as an object in order to make faster the match in the backend logic.
<template>
<div class="row" v-else>
<div class=" col-sm-6 col-md-6 col-lg-6" v-for="(project, index) in projects" :key="index">
<fieldset class="border p-2 col-11">
<legend class="w-auto col-12">Proyecto: {{project.name}}</legend>
<b-form-group
id="user_id"
label="Reemplazante"
>
<b-form-select
v-model="formProject[index].us"
:options="project.users"
value-field="replacement_user_id"
text-field="replacement_user_name"
#change="addReemplacemet($event,project.id)"
>
<template v-slot:first>
<b-form-select-option value="All">Seleccione</b-form-select-option>
</template>
</b-form-select>
<input type="hidden" name="project" v-model="formProject[index].proj">
</b-form-group>
</fieldset>
</div>
</div>
</template>
<script>
import skeleton from './skeleton/ProjectUserSkeleton.vue'
export default {
name: 'ProjectsUser',
components: { skeleton },
props:{
user: { type: String },
},
data() {
return {
user_id: null,
showProject: false,
projects: [],
loadingProjects: true,
formProject: [
{
us: 'All',
proj: null
}
]
}
},
watch: {
user: function() {
this.viewProjects(this.user)
}
},
methods: {
async getProjects(salesman){
this.loadingProjects = true
await axios.get(route('users.getProjects'),{
params: {
filter_user: salesman
}
})
.then((res)=>{
this.projects = res.data.data
setTimeout(() => {
this.loadingProjects = false
}, 800);
})
},
This is the form:
This is the message error:
At first glance, this looks like an issue with data. Your error suggests that you're trying to read an undefined variable.
It's often undesirable to use an index from iterating one array on another. In your Vue code, the index is projects, but you use it to access the variable formProject. This is usually undesirable because you can no longer expect that index to reference a defined variable (unless you're referencing the array you are currently iterating).
The easiest solution, for now, is to make sure the arrays are of the same length. Then utilizing v-if or other methods to not render the snippet if the variable is not defined.
A more complicated but better solution is restructuring your data such that formProject exists in projects

Interpolate dynamic components within a v-for loop

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

vuejs semantic ui - drop down not displaying on arrow click

I'm faced with an issue where my semantic drop down in my vue project won't activate when clicking on the arrow icon but works when I click on the rest of the element. The drop down also works when I set the dropdown to activate on hover, but just not on click. Solutions I've tried:
tested if the dynamic id are at fault
tested if the back ticks are confusing things
placed the values directly into the semantic drop down
Aside from the dropdown not activating, the code below works as intended and brings back the selected value to the parent component and can be displayed.
Dropdown.vue:
<template>
<div class="ui selection dropdown" :id="`drop_${dropDownId}`">
<input type="hidden" name="gender" v-model="selected">
<i class="dropdown icon"></i>
<div class="default text">Gender</div>
<div class="menu">
<div class="item" v-for="option in options" v-bind:data-value="option.value">
{{ option.text }}
</div>
</div>
</div>
</template>
<script>
export default {
data: function () {
return {
selected: {}
}
},
watch: {
selected: function (){
this.$emit("dropDownChanged", this.selected)
}
},
props: {
options: Array, //[{text, value}]
dropDownId: String
},
mounted () {
let vm = this;
$(`#drop_${vm.dropDownId}`).dropdown({
onChange: function (value, text, $selectedItem) {
vm.selected = value;
},
forceSelection: false,
selectOnKeydown: false,
showOnFocus: false,
on: "click"
});
}
}
</script>
The component usage:
<vue-drop-down :options="dropDownOptions" dropDownId="drop1" #dropDownChanged="dropDownSelectedValue = $event"></vue-drop-down>
The data in the parent:
dropDownOptions: [
{ text: 'One', value: 'A' },
{ text: 'Two', value: 'B' },
{ text: 'Three', value: 'C' }
],
dropDownSelectedValue: ""
Here is a fiddle of the above but simplified to use a flatter project. However the problem doesn't reproduce :(
https://jsfiddle.net/eywraw8t/210520/
I'm not sure what is causing your issue (as the examples on the Semantic Ui website look similar), but there is a workaround. For you arrow icon:
<i #click="toggleDropDownVisibility" class="dropdown icon"></i>
And then in the methods section of your Vue component:
methods: {
toggleDropDownVisibility () {
$(`#drop_${this.dropDownId}`)
.dropdown('toggle');
}
},