I am facing a problem in deleting item from an array. Array splice supposed to work but its not working like I want. Its always delete the item from last. I am using Vue.js . I am pushing item dynamically to an array. But after click remove its delete from the last. why I am facing this. I am attaching the codes.
<template>
<div>
<span class="badge badge-pill mb-10 px-10 py-5 btn-add" :class="btnClass" #click="addBtn"><i class="fa fa-plus mr-5"></i>Button</span>
<div class="block-content block-content-full block-content-sm bg-body-light font-size-sm" v-if="buttons.length > 0">
<div v-for="(item, index) in buttons">
<div class="field-button">
<div class="delete_btn"><i #click="remove(index)" class="fa fa-trash-o"></i></div>
<flow-button v-model="item.title" :showLabel="false" className="btn btn-block min-width-125 mb-10 btn-border" mainWrapperClass="mb-0" wrapperClass="pt-0" placeholder="Button Title"></flow-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import flowButton from '../assets/flow-button'
export default {
name: "textArea",
props:{
index : Number
},
data() {
return {
buttons : [],
btnClass : 'badge-primary',
}
}
components : {
flowButton
},
methods : {
addBtn () {
if(this.buttons.length >= 2) {
this.btnClass = 'btn-secondary'
}
if(this.buttons.length < 3) {
this.buttons.push({
title : ''
});
}
},
remove(index) {
this.buttons.splice(index, 1)
}
}
}
</script>
This must be because of your flow-button I have tried to replicate your error but endup to this code. I just replaced the flow-button with input and it works. Try the code below.
Use v-bind:key="index", When Vue is updating a list of elements rendered with v-for, by default it uses an “in-place patch” strategy. If the order of the data items has changed, instead of moving the DOM elements to match the order of the items, Vue will patch each element in-place and make sure it reflects what should be rendered at that particular index. This is similar to the behavior of track-by="$index"
You have missing comma between data and components, I remove the component here it won't cause any error now, and more tips don't mixed double quotes with single qoutes.
<template>
<div>
<span class="badge badge-pill mb-10 px-10 py-5 btn-add" :class="btnClass" #click="addBtn"><i class="fa fa-plus mr-5"></i>Button</span>
<div class="block-content block-content-full block-content-sm bg-body-light font-size-sm" v-if="buttons.length > 0">
<div v-for="(item, index) in buttons" v-bind:key="index">
<div class="field-button">
<div class="delete_btn"><i #click="remove(index)" class="fa fa-trash-o">sdfsdff</i></div>
<input type="text" v-model="item.title" :showLabel="false" className="btn btn-block min-width-125 mb-10 btn-border" mainWrapperClass="mb-0" wrapperClass="pt-0" placeholder="Button Title"/>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'textArea',
props: {
index: Number
},
data () {
return {
buttons: [],
btnClass: 'badge-primary'
}
},
methods: {
addBtn () {
if (this.buttons.length >= 2) {
this.btnClass = 'btn-secondary'
}
if (this.buttons.length < 3) {
this.buttons.push({
title: ''
})
}
},
remove (index) {
this.buttons.splice(index, 1)
}
}
}
</script>
I think that you may be facing a conflict with the index prop of your component. Try to use a different name for the index of your v-for loop:
<div v-for="(item, ind) in buttons">
<div class="field-button">
<div class="delete_btn"><i #click="remove(ind)" class="fa fa-trash-o"></i></div>
<flow-button v-model="item.title" :showLabel="false" className="btn btn-block min-width-125 mb-10 btn-border" mainWrapperClass="mb-0" wrapperClass="pt-0" placeholder="Button Title"></flow-button>
</div>
</div>
Try this. Removing an item correctly using this.
<div v-for="(item, ind) in buttons" :key="JSON.stringify(item)">
Related
I have two components TheCollapsible and TheBuyProduct. The first should be a structural component, making it possible to pass some children and a slot and render multiple collapsible itens (parents), that can expand and show its children. The second (which uses the first), should be a reactive screen to make it possible for the user to add items of some product. The problem here is the quantity of the product is not updated when the add or subtract functions are called.
Notice I have made two trials on reactivity: the first was to add a new property to the prop passed for my second component, this property is my quantity, but it doesn't work; the second trial was to create an array, where the index is the product id and the value is the quantity (which starts at zero). Both fail to be reactive.
I wander if this is because of the way my slot works. In my TheCollapsible component, I need a list of objects that has a property which is a new list (parent has property containing children). But as I said this is merely structural, each children HTML is passed as a slot and every prop I need in the slot I get through the v-slot.
TheCollapsible.vue
<template>
<div class="ui-collapsible">
<div
class="ui-collapsible__parent"
:key="parent_index"
v-for="(parent, parent_index) in parents"
>
<div>
<p>{{ parent.title }}</p>
<button #click="openChildren(parent_index)">
<img
alt="Abrir"
:id="`collapsible-icon-${parent_index}`"
src="/img/icons/chevrons-up-down.svg"
/>
</button>
</div>
<ul
class="ui-collapsible__children"
:id="`collapsible-child-${parent_index}`"
>
<li :key="index" v-for="(child, index) in parent[child_key]">
<slot
:child="child"
:child_index="index"
:parent_index="parent_index"
/>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
props: ["child_key", "parents"],
methods: {
openChildren(index) {
this.changeIconForOpenOrClosed(index);
const open_child = document.getElementsByClassName(
"ui-collapsible__children--active"
)[0];
if (open_child) {
open_child.classList?.remove("ui-collapsible__children--active");
open_child.classList.add("ui-collapsible__children");
}
const child = document.getElementById(`collapsible-child-${index}`);
if (open_child?.id != child.id) {
child.classList.remove("ui-collapsible__children");
child.classList.add("ui-collapsible__children--active");
}
},
changeIconForOpenOrClosed(index) {
const opened_button = document.querySelector(
'[src$="chevrons-down-up.svg"]'
);
if (opened_button?.src) {
opened_button.src = "/img/icons/chevrons-up-down.svg";
}
const click_button = document.querySelector(`#collapsible-icon-${index}`);
if (click_button?.id != opened_button?.id) {
click_button.src = "/img/icons/chevrons-down-up.svg";
}
},
},
};
</script>
TheBuyProduct.vue
<template>
<div class="row">
<div class="col-6">
<img :alt="product.title" class="product__image" :src="product.image" />
<h1>{{ product.title }}</h1>
<div class="product__lead" v-html="product.lead"></div>
</div>
<div class="col-6">
<h1>Acompanhamentos</h1>
<the-collapsible
child_key="items"
:parents="product.accompaniment_categories"
v-slot="{ child, child_index, parent_index }"
>
<div class="row product__accompaniment">
<img :alt="child.title" class="col-2" :src="child.image" />
<span class="col-4">{{ child.title }}</span>
<div class="col-4 product__quantity">
<button #click="subtract(child_index, parent_index, child.id)">
<i class="fa fa-minus"></i>
</button>
<span>
{{
product.accompaniment_categories[parent_index].items[
child_index
].quantity
}}
-
{{ accompaniments[child.id] }}
</span>
<button #click="add(child_index, parent_index, child.id)">
<i class="fa fa-plus"></i>
</button>
</div>
<span class="col-2">R${{ child.price }}</span>
</div>
</the-collapsible>
<button class="ui-button product__button">Enviar pedido</button>
</div>
</div>
</template>
<script>
export default {
props: ["product"],
data() {
return {
accompaniments: [],
};
},
created() {
this.addPropToAccompaniments();
},
methods: {
addPropToAccompaniments() {
for (let i = 0; i < this.product.accompaniment_categories.length; i++) {
const category_items = this.product.accompaniment_categories[i].items;
for (let j = 0; j < category_items.length; j++) {
category_items[j].quantity = 0;
this.accompaniments[category_items[j].id] = 0;
}
}
},
add(i, j, id) {
this.product.accompaniment_categories[i].items[j].quantity += 1;
console.log("add:", this.accompaniments[id]);
this.accompaniments[id] += 1;
},
subtract(i, j, id) {
this.product.accompaniment_categories[i].items[j].quantity -= 1;
console.log("sub:", this.accompaniments[id]);
this.accompaniments[id] -= 1;
},
},
};
</script>
I am trying to make my vue component as flexible as possibile, So i have some button and a search i need to use on all my table component . For now it is hard coded, but i wnat to make it more flexible, everything work until now except the search . I see an error like this on console
ERROR: [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "query"
I know the problem is when i try to change the query input but does now know how to fix this problem. Does anyone know ?
//custom action buttons
import Button from './button.js';
export default {
components: {
'v-button': Button
},
props: {
query: {
type: String
},
selected: {
type: Array
},
url: {
type: String
}
},
// props: [
// 'selected',
// 'query',
// 'url'
// ],
methods: {
showModal() {
this.$emit('showModal')
},
massDeleteRecord() {
this.$emit('massDeleteRecord')
}
},
template: `
<div class="add-user py-4">
<div class="d-flex">
<div class="col-10">
<v-button type="success" #click.prevent="showModal" >
Add Record
</v-button>
<v-button type="primary" data-mdb-toggle="collapse" data-mdb-target="#collapseFilter" aria-expanded="false" aria-controls="collapseFilter" >
Filter
<i class="fa-solid fa-filter"></i>
</v-button>
<v-button v-show="selected.length>0" type="danger" class="dropdown dropdown-toggle" id="dropdownMenuButton" data-mdb-toggle="dropdown" aria-expanded="false" >
Action ({{selected.length}})
</v-button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<li>
<a :href="url" class="dropdown-item btn-warning text-center">
Export
<i class="fa-solid fa-file-excel"></i>
</a>
</li>
<li>
<v-button #click="massDeleteRecord()" type="danger" class="dropdown-item text-center">
Delete
<i class="fa-solid fa-trash"></i>
</v-button>
</li>
</ul>
</div>
<div class="col-2">
<input #input="$emit('updatedQuery', $event.target.value)"
:value="query"
type="search"
class="form-control border-0"
placeholder="Search"
/>
</div>
</div>
</div>
`,
}
User Table Component
<template>
<table-custom-action
#massDeleteRecord="massDeleteUser"
#showModal="showModal"
#updated-query="(newQuery) => query = newQuery"
:selected="selected"
:query="query"
:url="url"
></table-custom-action>
</template>
<script>
import TableCustomAction from './partials/modules/TableCustomAction.js';
export default {
components: {
TableCustomAction,
},
data(){
return {
....
query: '',
....
url: ''
}
},
watch: {
query: function(newQ, old) {
if (newQ === "") {
this.getRecord();
} else {
this.searchRecord();
}
},
},
</script>
Rather than using v-model on query, send back the update to the parent. It will update in the parent, be transferred through the props to your child and produce the same result. If you mutate it in the child, then it will at some point mutate in the parent, hence do twice the work.
Here is a nice article on the subject.
Also, you don't update props actually, they are read-only.
In the child
<input #input="$emit('updatedQuery', $event.target.value)"
:value="query"
type="search"
class="form-control border-0"
placeholder="Search"
/>
In the parent
<table-custom-action #updated-query="(newQuery) => query = newQuery">
</table-custom-action>
UPDATE: here is a working repro.
More details here in the doc.
In my Vue.js code below I'm trying to add a Show More button to my data coming from API so initially it should show 10 data and whenever clicked load more 10 and so on. I tried answer from:
Load more button in vuejs
but it's not working since I'm looping over an array it gives me the error below can't read property of question title. Is there a way to do it?
<div class="search-askbutton">
<b-row>
<div class="search-wrapper">
<input
type="text"
v-model="search"
placeholder="Search something..."
class="fas fa-search"
/>
</div>
<div class="container vue">
<div v-for="commentIndex in commentsToShow">
<div v-if="commentIndex <= commentsToShow">
<ul
class="container-question"
v-for="(question, index) in filteredList"
:key="index"
>
<div>{{question[commentIndex - 1].questionTitle}} says:</div>
<hr />
</ul>
</div>
</div>
<button #click="commentsToShow += 10">show more</button>
</div>
<script>
export default {
data() {
return { commentsToShow: 10,
search: '',
questions: [],}
},
computed: {
filteredList() {
return this.questions.filter((question) => {
return (
question.questionTitle
.toLowerCase()
.includes(this.search.toLowerCase()) ||
question.owner.username
.toLowerCase()
.includes(this.search.toLowerCase()) ||
question.questionTitle
.toUpperCase()
.includes(this.search.toUpperCase()) ||
question.owner.username
.toUpperCase()
.includes(this.search.toUpperCase())
);
});
},
},
mounted: function() {
questionService.getAllQuestions().then((response) => {
this.questions = response.data.response;}
}
</script>
The problem is your subtraction
<div>{{question[commentIndex - 1].questionTitle}} says:</div>
If commentIndex = 0 then you'll be saying 0-1 = -1 therefore it will not find the -1 index.
You could replace this line
<div v-if="commentIndex <= commentsToShow">
So that it can run only if the index is greater than 0
<div v-if="commentIndex > 0">
1)
v-for returns what's inside an array, not the array itself.
<div>{{question.questionTitle}} says:</div>
2)
also, you can remove the v-for loop.
note:- your reference question is also uses this way.
<div v-for="commentIndex in commentsToShow">
<div v-if="commentIndex <= commentsToShow">
<ul class="container-question">
<div>{{filteredList[commentIndex - 1].questionTitle}} says:</div>
<hr />
</ul>
</div>
</div>
What I have
I'm importing the same component twice. This component is used to display a dropdown with collections.
When an item in the dropdown is selected, an event is triggered.
The component is imported in list.js and in BulkActions.vue.
The problem
An event is fired when a collection in the dropdown is selected. It then triggers an event using $emit. Somehow this event is only catched in list.blade.php and not in BulkActions.vue.
For both dropdowns (loading from the same component) there should be a different behaviour.
I have no idea why this happens or why the event is only catched at my root.
What I've tried
I've tried to pass an additional prop in the HTML to have a "variable event name", but that didn't work. I've tried various ways of importing the component as well.
Does anyone know how to solve this issue?
The files
list.blade.php:
<div class="media-list-navbar mt-3 mb-3">
<shown-results :text="resultText"></shown-results>
<search-bar #update-filters="updateFilters"></search-bar>
<document-types #update-filters="updateFilters"></document-types>
<collection-dropdown eventname="update-filters"
#update-filters="updateFilters"></collection-dropdown>
<div class="clearfix"></div>
</div>
<bulk-actions #select-all="selectAll"
#deselect-all="deselectAll"
:items="items"
:multiselect="multiSelect"></bulk-actions>
BulkActions.vue
<template>
<div class="multiselect-list-navbar mt-3 mb-3" v-if="multiselect">
<div class="float-left">
<button type="button"
class="btn btn-outline-secondary"
#click="$emit('deselect-all')">
<i class="fas fa-fw fa-times"></i> {{ Lang.get('media/item.index.list.multi-select.deselect-all') }}
</button>
<button type="button"
class="btn btn-outline-secondary"
#click="$emit('select-all')">
<i class="fas fa-fw fa-check"></i> {{ Lang.get('media/item.index.list.multi-select.select-all') }}
</button>
</div>
<bulk-collection
#update-filters="doSomething"></bulk-collection>
<div class="clearfix"></div>
</div>
</template>
<script>
export default {
name: "Bulk",
props: {
multiselect: Boolean,
items: Array
},
components: {
'bulk-collection': () => import('./Collections')
},
methods: {
doSomething() {
console.log(this.items)
}
}
}
</script>
<style scoped>
</style>
list.js
import MediaItem from '../components/MediaItem';
import UploadModal from '../components/media/UploadModal';
import ItemDetail from '../components/media/ItemDetail';
import ShownResults from '../components/media/list/ShownResults';
import SearchBar from '../components/media/list/SearchBar';
import DocumentTypes from '../components/media/list/DocumentTypes';
import {default as CollectionDropdown} from '../components/media/list/Collections';
import Order from '../components/media/list/Order';
import BulkActions from '../components/media/list/BulkActions';
if (document.getElementById('media-list')) {
const mediaList = new Vue({
el: '#media-list',
components: {
MediaItem,
ShownResults,
SearchBar,
UploadModal,
ItemDetail,
DocumentTypes,
CollectionDropdown,
Order,
BulkActions
},
[...]
Collections.vue
<template>
<div class="dropdown float-left">
<button class="btn btn-secondary dropdown-toggle"
type="button"
data-toggle="dropdown">
{{ Lang.get('media/item.index.list.filters.collections.title') }}
</button>
<div class="dropdown-menu" ref="collectionDropdown">
<div class="dropdown-item no-pseudo">
<input type="search"
class="form-control"
name="search"
:placeholder="Lang.get('media/item.index.list.filters.collections.filter')"
v-model="query"
#keyup="search">
</div>
<div class="dropdown-item last-item no-pseudo">
<alert type="warning">
<template v-slot:body>
{{ Lang.get('media/item.index.list.filters.collections.none-filter') }}
</template>
</alert>
</div>
<div v-for="item in list"
class="dropdown-item"
v-if="!item.hidden">
<span class="custom-control custom-checkbox">
<input type="checkbox"
class="custom-control-input"
name="collection[]"
:checked="item.checked"
:id="item.slug"
:value="item.id"
#change="selectItem">
<label class="custom-control-label" :for="item.slug">
{{ item.name }}
</label>
</span>
</div>
</div>
</div>
</template>
<script>
import Alert from '../../partials/Alert';
export default {
name: "Collections",
components: {
Alert
},
data() {
return {
displayAmount: 10, // amount of items displayed without search
list: [],
query: ''
}
},
computed: {
/**
* Return an array of selected items only
*/
checked() {
return this.list.filter(item => {
return item.checked === true;
})
}
},
methods: {
/**
* Mark an item as selected
*/
selectItem(e) {
let selectedId = e.target.value;
this.markItem(selectedId);
},
/**
* Mark an item from the list as selected
*
* #param {Number} itemId
*/
markItem(itemId) {
this.list.forEach(item => {
if (item.id === parseInt(itemId)) {
item.checked = !item.checked;
}
});
this.$emit('update-filters', {
props: this.checked.map(item => item.id).join(',')
});
},
/**
* Search in the current URL for collection ids
*/
markItemsFromUrl() {
let urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('collection')) {
let urlFilters = urlParams.get('collection').split(','); // split url parameters
urlFilters.forEach(itemId => {
this.markItem(itemId);
});
}
},
},
mounted() {
this.fetchList();
this.markItemsFromUrl();
/**
* Prevent Bootstrap dropdown from closing after clicking inside it
*/
$(this.$refs.collectionDropdown).on('click.bs.dropdown', function (e) {
e.stopPropagation();
});
}
}
</script>
Below is a GIF animation to demonstrate the problem. The first dropdown (the one on the top) has normal behaviour. The second one (that appears later on) does not. When clicking the second one, an event of the first one is happening.
I'm working on a small project in vue.js connected to a lumen API (working).
I currently have a list of students ('Etudiants') in which I can click in the list to select one, and delete it via a button in a toolbar.
When a student is deleted I'm reloading the student list (since it's not up to date anymore), therefore I'm doing 2 api calls via axios.
DELETE http://www.url.com/etudiants (param: idEtudiant)
GET http://www.url.com/etudiants (param: page)
The problem is that my API calls are not done in the right order, as shown here on a screenshot of the calls (with watterfall):
This problem involves 3 vue files.
'Etudiants.vue' and its 2 childs: 'ListeEtudiants.vue' (student list) and 'BarreOutilsEtudiant.vue' (toolbar)
This simple image shows the hierarchy of the 3 files and the order of which everything should execute.
In my case (when it's not working) the action number 3, the axios DELETE, happens in last.
Here are the contents of my files:
Etudiants.vue:
<template>
<div id="etudiants" class="container-fluid h-100">
<div class="row">
<div class="col-3 borderR">
<ListeEtudiants ref="list" #idEtudiantChanged="updateIdEtudiant"/>
</div>
<div class="col-9 bg-light">
<BarreOutilsEtudiant v-if="idEtudiant != null" :idEtudiant="idEtudiant" #delEtudiant="delEtudiant"/>
<InfosEtudiant v-if="idEtudiant != null" :idEtudiant="idEtudiant"/>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import ListeEtudiants from '#/components/ListeEtudiants.vue'
import InfosEtudiant from '#/components/InfosEtudiant.vue'
import BarreOutilsEtudiant from '#/components/BarreOutilsEtudiant.vue'
export default {
components: {
ListeEtudiants,
InfosEtudiant,
BarreOutilsEtudiant
},
data: function(){
return {
idEtudiant: null
}
},
methods:{
updateIdEtudiant(idEtudiant){
this.idEtudiant=idEtudiant;
},
delEtudiant(){
axios
.delete('http://82ab2617.ngrok.io/etudiants', {params: {"idEtudiant" :this.idEtudiant}})
.then(this.$refs.list.loadList())
.catch(error => console.log(error));
}
}
}
</script>
ListeEtudiants.vue:
<template>
<div id="ListeEtudiants">
<div class="row bg-light">
<!-- Trigger Modal Ajout Etudiant -->
<button class="btn btn-light w-100" data-toggle="modal" data-target="#addModal">
<font-awesome-icon icon="plus" size="1x"/>
<span> Ajouter un Etudiant</span>
</button>
</div>
<ul v-if="etudiants != null" id="list" class="row flex-nowrap list-group list-group-flush pr-0">
<button v-for="etudiant in etudiants.data" v-on:click='$emit("idEtudiantChanged",etudiant.idEtudiant)' class="btn btn-light text-left list-group-item pl-5 py-1">{{ etudiant.nom }} {{ etudiant.prenom }}</button>
</ul>
<ul v-else class="row flex-nowrap list-group list-group-flush pr-0">
</ul>
<div class="row bg-light">
<button class="btn btn-light col-3" v-on:click="page -= 1" :disabled="page === 1 || disabled"><font-awesome-icon icon="chevron-left" size="1x"/></button>
<div class="align-middle col-6 my-auto">{{ page }} / {{ maxPage }}</div>
<button class="btn btn-light col-3" v-on:click="page += 1" :disabled="page === maxPage || disabled"><font-awesome-icon icon="chevron-right" size="1x"/></button>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: "ListeEtudiants",
data: function(){
return {
etudiants: null,
maxPage:1,
disabled:false,
page:1,
}
},
methods: {
parseAndDisplay: function(data){
this.etudiants = data;
this.maxPage = data.last_page;
this.page = data.current_page;
this.disabled = false;
},
loadList: function(){
this.disabled = true;
this.etudiants = null;
axios
.get('http://82ab2617.ngrok.io/etudiants', {params: {page:this.page}})
.then(response =>this.parseAndDisplay(response.data))
.catch(error => console.log(error));
}
},
watch: {
'page': function(newVal, oldVal){
if((newVal === 0 && oldVal === 1) || (newVal === this.maxPage+1 && oldVal === this.maxPage)){
this.page = oldVal;
}else{
if(oldVal !== 0 && oldVal !== this.maxPage+1) {
this.loadList();
}
}
}
}
,
mounted() {
this.loadList();
}
}
</script>
BarreOutilsEtudiant.vue:
<template>
<div class="row p-2 navbar-expand navbar-info bg-info">
<button class="btn btn-info mr-5" type="button"><font-awesome-icon icon="download" size="1x"/> Télécharger le Bulletin</button>
<button class="btn btn-info ml-auto" type="button"><font-awesome-icon icon="user-edit" size="1x"/></button>
<button class="btn btn-danger ml-4" v-on:click="$emit('delEtudiant')" type="button"><font-awesome-icon icon="trash-alt" size="1x"/></button>
</div>
</template>
<script>
export default {
name: "BarreOutilsEtudiant"
}
</script>
<style scoped>
</style>
Thank you very much for helping me.
I believe the problem is here:
.then(this.$refs.list.loadList())
That will call loadList immediately and pass the value it returns to then, which isn't what you want.
Instead it should be something like this, wrapping it in a function:
.then(() => this.$refs.list.loadList())