Vue and "vuedraggable", update data in subcomponents? - vue.js

I have a list which is draggable and I use there "vuedraggable" component.
Inside this list I have 3 subcomponents.
1 input and 2 selectboxes, I have there #change to update informations.
If I do the order of "tasks", it works perfectly and I send to backend correct data, but in frontend in subcomponents is the data not updated. In selectbox and input box I see the data on the same place.
My vuejs example:
<draggable v-model="tasks" draggable=".task">
<div v-for="task in tasks" class="task">
<task-title-update :task="task"></task-title-update>
<task-assignee :task="task"></task-assignee>
<task-priority :task="task"></task-priority>
</div>
</draggable>
Is there possible to keep this subcomponents and update the data correctly?

You need to listen on the event with one of the events that vue-draggable provides, try using #end or #start, then you can put the logic from the #change into those events.

Related

Send template to grandchild component (nested template)

There is this TablePackage component that we use in our project. I saw that to customize how values in a column render, one can pass template named with the specific column name (in this example status) and use slot prop row.
<!--Code Snippet 1-->
<!--In each row of status columns, we will see status buttons instead of plain status text-->
<TablePackage>
<template #status="{ row }">
<button>{{ row.status }}</button>
</template>
</TablePackage>
Now there are two more components, Parent and Dialogue.
Parent uses Dialogue, Dialogue uses TablePackage.
Parent component
<Dialogue title="dialogueTitle">
</Dialogue>
Dialogue component
{{ title }}
<TablePackage rows="rowList" columns="columnObj">
</TablePackage>
Now I want to be able to custom render values in a column as in 'Code Snippet 1' which means I want to render a button not a text in 'status' column. How can I achieve this from Parent component level? I tried nested template but it was not allowed. With the constraint that I can edit Parent and Dialogue components but not TablePackage, what's the recommended away to achieve this?

Prevent list display filter from resetting v-list selection

Version info: Vuetify 2.6.3, Vue 2.6.14, Nuxt 2.15.8
I'm making a custom component that is supposed to be somewhat similar to v-autocomplete, except that it's rendered as bottom sheet. If user enters a display filter into v-text-field, the option list (v-list) is supposed to display only those options that match the filter.
In overall it works fine except one use case: let say the list has 5 items (aa, bb, cc, dd, ee) and user selected bb and cc from the list. Now, v-list-item-group's model selectedItems contains the 2 selected items bb and cc, perfect! However, when user enters b into display filter, the already selected item cc will be auto deleted from selectedItems. I can't tell if selectedItems change is caused by filter or by user selection. Is there a way to maintain the selection in model?
I'm considering a hack - if an item is selected, keep it in filteredChoices even if it does not match the filter. This behaviour is bearable but UX wise not as intuitive as the filter of v-autocomplete.
The simplified structure looks like the below:
<template>
<v-bottom-sheet scrollable>
<v-card>
<v-card-text>
<v-list>
<v-list-item-group
v-model="selectedItems"
:mandatory="!optional"
:multiple="multiple"
>
<v-list-item
v-for="item in filteredChoices"
:key="item.value"
:value="item"
>
</v-list-item>
</v-list-item-group>
</v-list>
</v-card-text>
<v-text-field
v-model="filterInput"
placeholder="filter choices..."
hide-details
></v-text-field>
</v-card>
</v-bottom-sheet>
</template>
<script>
...
filteredChoices() {
if (this.filterInput == null) {
return this.allItems
}
return this.allItems.filter((item) => {
return item.label
.toLocaleLowerCase()
.includes(String(this.filterInput).toLocaleLowerCase())
})
},
...
</script>
How I reach the solution:
I was quite new to front-end stuff. Previously, when I was learning how to implement v-model support for custom component, all web resources I came across say the same thing - bind inner component's value props to custom component's value props. However I just discovered that this is merely one of the ways rather than a must. With that new learning, more possibilities pop up in my mind and one of them lead me to below solution.
Solution:
Decouple the custom component value from bottomsheet's list
Bind the model of inner v-autocomplete to an array data, say internalValue. (this inner component is not included in question's template for simplification)
Bind the model of inner v-list-item-group (in bottomsheet) to a separate data, say bottomSheetSelections.
Update the custom component value based on user actions in bottomsheet
Add a watcher to bottomSheetSelections array:
if the array grows, it means the user has selected more item. We should push the additional item to internalValue.
if the array shrinks:
if the missing item is still there in filteredChoices, the removal is triggered by user de-selection. We should remove this item from internalValue.
else, we consider the removal is triggered by list filter. No action is needed.
Restore user selection in bottomsheet on clearing filter
Add a watcher for filteredChoices. Whenever the array grows, if the additional choice exist in internalValue, we should push it to bottomSheetSelections.
Summary
Strictly speaking, this doesn't solve the ask of question's title - the list selection in bottomsheet (bound to v-list-item-group) is still getting reset. However, at least we're able to restore it in bottomsheet on clearing filter.
More importantly, this solution achieved the objective mentioned in question details - retain the user selection value. We kept it in a separate data internalValue.
I haven't test this solution with long list of data. My guts feeling is that there could be more efficient solution. Please share it if you have a better solution.

vue3 array value problem between components

i have a strange problem with arrays in vue3
When loading the components, a start array is transferred, in the components the values ​​are taken over with a watcher if the start array changes. that works wonderfully. the whole thing is a little game, the user moves a cone on a certain route.
I switch to another component by changing a page number. the component with the game should actually be completely from the dom, I don't see anything in the tree either.
<transition name="fadepage" mode="out-in" >
<div v-if="getPage()==3">
<VueGameIntro :content="content" :level="level" :kontrastswitch="kontrastSwitch" :typoswitch="typoSwitch" :levelcolor="levelColor" #mqttMessage="changeMqttMessage"></VueGameIntro>
</div>
</transition>
<transition name="fadepage" mode="out-in" >
<div v-if="getPage()==4">
<VueGame :content="content" :level="level" :playerlist="playerList" :startpointlist="startPointList" :levelendstate="levelEndState" :activeplayerlist="activePlayerList" :kegelmoved="kegelMoved" :kontrastswitch="kontrastSwitch" :id="id" #mqttMessage="changeMqttMessage" #changeCurrPoint="changeCurrPoint"></VueGame>
</div>
</transition>
<transition name="fadepage" mode="out-in">
<div v-if="getPage()==5">
<VueGameOutro :content="content" :level="level" :levelendstate="levelEndState" :kontrastswitch="kontrastSwitch" :typoswitch="typoSwitch" #mqttMessage="changeMqttMessage"></VueGameOutro>
</div>
ok, when the first level has been played it changes to another component VueGameOutro, then it goes back to the actual game components with a new level. Suddenly I have the feeling that this array exists twice.
in the console i see different values ​​than in the output on the screen: {{ currPointList }} shows something different than this.currPointList. in the vue dev plugin, however, are the values ​​​​as well as the screen output. but the component definitely has a different starting point than the one in the currPointList.
the value in the console seems to have been the latest from the previous level.
Is that possible? how can I be sure that a component is completely gone?
are there arrays that have different values ​​for string or number?
##########################################################
edited
that's the console output:
currPointList unmounted 25,0,0,0,
currPointList mounted 8,0,0,0,
GAME_MOVE currPointList #move,1,3,1 25,0,0,0,
currPointList before move 25,0,0,0,
currPointList move point updated 3,0,0,0,
as you can see, the 25 is in front of the unmount, then the component is reloaded and gets the 8 once via a property form the parent comp
then a move command arrives via websocket, an mqtt component receives the message and sends it to the game via MITT emitter. there's no other place to change currPointList. the first value jumps back to 25.
the problem occurred because the emitter.on("eventname" ...) was not deleted when removing the components. I also changed the emitter.on to an arrow function.

Bootstrap Table row-clicked event

I'm using a <b-table> component in a Vue project and I need to define a function to be triggered when a row is clicked. I heard that there is a #row-clicked property in b-table so I tried to use it and I can put simple functions inside of it, I mean I can get console prints etc but nothing more complicated...
I want two functions to be executed when a row is clicked. The functions are now bound to a button inside of every row, but I dont want use it like this, I just need to click the related row to make them work. Here is the current version of the b-table component;
<b-table
:items="displayByType"
:fields="displayColumn"
responsive="sm"
:per-page="perPage"
:current-page="currentPage"
:sort-by.sync="sortBy"
:sort-desc.sync="sortDesc"
:filter="filter"
:filter-included-fields="filterOn"
#filtered="onFiltered"
hover
#row-clicked="test"
>
And here is the two functions that I want to be triggered when a row is clicked...
toggleRightBar(){
document.body.classList.toggle("right-bar-enabled");
}
changeRightBarContent(row){
this.$store.dispatch("rightbar/changeRightBarInfo", tableData[row]);
},
There is a JavaScript folder that I export column names and row data. I also a have Rightbar, when a row is clicked, I want to enable the rightbar so that the user can see detailed information about that specific row.
At last, here is the button that I use to make these two functions work;
<template #cell(detail)="row">
<button #click="toggleRightBar(); changeRightBarContent(row.index);" class="btn btn-outline-primary toggle-right">
<i class="bx bx-detail toggle-right"></i>
</button>
</template>
Basically, I dont want to use this button, instead, I want to click the row itself. Every row's rightbar information will be uniqe, my function does that. I just dont know how to use these 2 functions inside #row-clicked
Thanks in advance.
The row-clicked event will pass 3 parameters to your function.
The first being the item specific to the row that was clicked. The second will be the index of the row, and the third is a native click event.
This means you can use the arguments keyword to get the index and pass it to your function.
#row-clicked="toggleRightBar(); changeRightBarContent(arguments[1])".
Alternatively you can create a third method which will call the other two.
<b-table #row-clicked="onRowClicked" />
{
onRowClicked(item, index, event) {
this.toggleRightBar();
this.changeRightBarContent(index);
}
}
You can read more about the #row-clicked event on the docs component reference section.
https://bootstrap-vue.org/docs/components/table#comp-ref-b-table-events

How to use `v-if` and `v-for` on the same element?

Hi I'm trying to figure out how to use v-if on a iterated element which also uses v-for. I need to check if the current element has any of a series of classes, which are numbers.
so the classes of each article would be:
<article class="post-item post-guide 12 22 19 30 55">...
this is the HTML that renders all:
<article v-if="showIfHasClass" :class="'post-item post-guide ' + guide.categories.toString().replace(/,/g, ' ')"
v-for="(guide, index) in guides" :key="index">
<header>
<h1 class="post-title">
{{ guide.title.rendered}}
</h1>
</header>
</article>
I have tried with methods that check the class of all elements, that works, but i'm trying to use a clean Vue built-in solution with v-if without success, i'm not able to retrieve the class of this in a successful way.
Should showIfHasClass be a computed property? I have tried with that too... but it seems, I'm missing something along the way.
my data I have to check against is an array:
data:{
guides: [...]
selectedCategories: [1, 22, 33, 100, 30];
}
or maybe it is better to directly loop over the guides and check if they have the selectedCategory or not, then remove the element from the guides data array?
What is more effective?
Besides the option to create an additional filtered computed (effectively eliminating the need to use v-for and v-if on the same element), you also have a template level way of dealing with such edge-cases: the <template> tag.
The <template> tag allows you to use arbitrary template logic without actually rendering an extra element. Just remember that, because it doesn't render any element, you have to place the keys from the v-for on the actual elements, like this:
<template v-for="(guide, index) in guides">
<article v-if="isGuideVisible(guide)"
:key="index"
class="post-item post-guide"
:class="[guide.categories.toString().replace(/,/g, ' ')]">
<header>
<h1 v-text="guide.title.rendered" />
</header>
</article>
</template>
isGuideVisible should be a method returning whether the item is rendered, so you don't have to write that logic inside your markup. One advantage of this method is that you can follow your v-if element with a fallback v-else element, should you want to replace the missing items with fallback content. Just remember to also :key="index" the fallback element as well.
Apart from the above use-case, <template> tags come in handy when rendering additional wrapper elements is not an option (would result in invalid HTML markup) (i.e: table > tr > td relations or ol/ul > li relations).
It's mentioned here as "invisible wrapper", but it doesn't have a dedicated section in the docs.
Side note: since you haven't actually shown what's inside guide.categories, I can't advise on it, but there's probably a cleaner way to deal with it than .toString().replace(). If guide.categories is an array of strings, you could simply go: :class="guide.categories".
I think the most Vue way is to create a computed property with filtered items from selected categories, then use that in v-for loop (the idea is to move the business logic away from template).
computed: {
filteredItems(){
return this.guides.filter(e => this.selectedCategories.includes(e.category))
}
}
Also, as a note, it is not recommended to use v-if and v-for on the same element, as it may interfere with the rendering and ordering of loop elements. If you don't want to add another level of nesting, you can loop on a 'template' element.
<template v-for="item in items">
// Note the key is defined on real element, and not on template
<item-element v-if='condition' :key="item.key"></item-element>
</template>