Vuetify expansion panel, open state does not follow dataprovider - vuetify.js

When the data array changes order, the state of panels that are open does not follow their data positions.
<div id="app">
<v-app id="inspire">
<div>
<div class="text-center d-flex pb-4">
<v-btn #click="changeOrder">Change order</v-btn>
<v-btn #click="removeItem">Remove item</v-btn>
</div>
<v-expansion-panels
v-model="panel"
multiple
>
<v-expansion-panel
v-for="(item, i) in items">
<v-expansion-panel-header>{{ item.name }}</v-expansion-panel-header>
<v-expansion-panel-content>
Lorem ipsum dolor sit amet.
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</div>
</v-app>
</div>
new Vue({
el: '#app',
vuetify: new Vuetify(),
data () {
return {
panel: [],
items: [
{ id:1, name: 'James', },
{ id:2, name: 'Bernt', },
{ id:3, name: 'Julie', },
{ id:4, name: 'Veronica', },
],
}
},
methods: {
changeOrder () {
this.items = this.items.reverse();
},
removeItem () {
this.items.splice(0, 1);
},
},
})
https://codepen.io/Agint/pen/GRpmBxE
In the demo, open a panel and click the button, and you see the problem. Also same problem when you remove data from the list. If you have one panel open, and you remove it, the sibling is suddenly open.
How do I attack this problem?

Per the docs the value prop of the v-expansion-panels component:
Controls the opened/closed state of content in the expansion-panel. Corresponds to a zero-based index of the currently opened content. If the multiple prop (previously expand in 1.5.x) is used then it is an array of numbers where each entry corresponds to the index of the opened content. The index order is not relevant.
That means that which panel(s) are open has no connection to their content. If you reverse or change the order of the items array, you also need to update the panel array to adjust the open indexes accordingly:
methods: {
changeOrder () {
// reverse the elements in your items array
this.items = this.items.reverse()
// get the max index of elements in your items array
const maxIndex = this.items.length - 1
// set index of each open panel to its inverse
this.panel = this.panel.map(p => maxIndex - p)
},
removeItem () {
this.items.splice(0, 1)
// since the index of each item will be effectively reduced by 1
//
this.panel = this.panel.map(p => p - 1)
},
}

Related

How to style a data table td in Vuetify?

Good Afternoon.
I'm trying to build a stylized table with "v-data-table", without being used to it. Mainly put style into second or third cell (table, tr, td). I don't find the solution for my problem. Help me, please.
thanks.
You can use the item-class attributes to style every row
Property on supplied items that contains item’s row class or function that takes an item as an argument and returns the class of corresponding row
It works as the following :
It takes a function as argument that return a class depending on the row.
If you want to return a specific class depending on the item use it like this :
<template>
<v-datad-table :item="items" :item-class="getMyClass"></v-data-table>
</template>
<script>
methods: {
getMyClass(item){
// here define your logic
if (item.value === 1) return "myFirstClass"
else return "mySecondClass"
}
}
</script>
If you want to always give the same class you can just return the class you want to give (note that this is the same as stylized the td of the table using css)
<template>
<v-data-table :items="items" :item-class="() => 'myClass'"></v-data-table>
</template>
In your case, you can add an index to your data using a computed property and added a class based on the index
computed: {
myItemsWithIndex(){
retunr this.items.map((x, index) => {...x, index: index})
}
}
methods: {
getMyClass(item){
if(item.index === 2 || item.index === 3) return "myClass"
}
}
Working example
new Vue({
el: "#app",
vuetify: new Vuetify(),
data: () => {
return {
items: [
{name: "foo"},
{name: "bar"},
{name: "baz"},
{name: "qux"},
{name: "quux"},
{name: "corge"},
{name: "grault"},
],
headers: [{ text: 'Name', value: 'name'}],
}
},
computed: {
itemsWithIndex(){
return this.items.map((item, index) => ({ ...item, index:index }))
}
},
methods: {
getMyClass(item){
if(item.index === 2 || item.index === 3){
return "myClass"
} else return
}
}
})
.myClass {
background: red
}
<script src="https://unpkg.com/vue#2.x/dist/vue.js"></script>
<script src="https://unpkg.com/vuetify#2.6.4/dist/vuetify.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/vuetify#2.6.4/dist/vuetify.min.css" />
<div id="app" data-app>
<v-data-table :items="itemsWithIndex" :headers="headers" :item-class="getMyClass"></v-data-table>
</div>
I'd bet that what you're trying to achieve can be done using named slots
See this example from the docs. Basically, the template tag you see in the example will become whatever node is 'above it' (which it really isn't because it takes its place, but you get the point). For instance, in the case of data-tables, <template #item="{ item }">... represents every <td> of your table. Then you can use the destructured item and apply some logic to it to still of modify you table as you will.
Don't forget to upvote/validate the answer if it helped your to solve your issue, comment if you need more details and welcome to Stack!
There are also the possibility to use cellClass, which is part of the headers.
The image is from https://vuetifyjs.com/en/api/v-data-table/#props
As computed property i have:
headers() {
return [
{ text: this.$t('Name'), align: 'left', sortable: true, value: 'name', cellClass:'select' },
{ text: 'CVR', sortable: false, value: 'cvrno' },
{ text: this.$t('Updated At'), sortable: false, value: 'updatedAt' }
]
},
and by v-data-table tag looks like:
<v-data-table
v-model="selected"
:headers="headers"
:items="customerFiltered"
:loading="loadingCustomers"
:items-per-page="-1"
selected-key="id"
show-select
hide-default-footer
fixed-header
>

Increasing performance of v-data-table with custom cells and async data loading

I'm creating a page with v-data-table. Some content of this table is loading at mounted stage, but the data for one column should be loaded line-by-line in the background by async API calls after rendering the whole table. Table rows should also be colored based on data returned from API call.
I've already developed this page, but stuck into one issue - when the table contains composite cells that was redefined by item slot (by example, a cell with icons, tooltips or spans), table row update time significantly increases.
According to business logic, the page may contain a large amount of rows, but I can't use v-data-table pagination to reduce entries count at one page.
The question is - how can I update row (in fact, just its color and one cell value) with a little performance loss as possible?
There is a Codepen with this problem. The way of loading data into the page is completely preserved in this Codepen, but API calls was replaced by promises with fixed timeout.
The problem still exists in Codepen. By default all the requests for 100 items have passed in 12-13 seconds (there's a counter at the bottom of the page). When I comment out last td, they're passed just in 7-8 seconds. When I comment out another one td (second from the end), they're passed in 6 seconds. When I increase items count to 1000, row update time is also increases.
new Vue({
el: '#app',
vuetify: new Vuetify(),
data () {
return {
headers: [
{
text: 'Dessert (100g serving)',
value: 'name',
},
{ text: 'Second name', value: 'secondName' },
{ text: 'Fat (g)', value: 'fat' },
{ text: 'Carbs (g)', value: 'carbs' },
{ text: 'Protein (g)', value: 'protein' },
{ text: 'Max value', value: 'maxValue' },
{ text: 'Actions', value: 'id' },
],
desserts: [],
timerStart: null,
loadingTime: null,
}
},
created() {
this.generateDesserts();
},
mounted() {
this.countMaxValues(this.desserts).then(() => {
this.loadingTime = (Date.now() - this.timerStart) / 1000;
});
},
methods: {
generateDesserts() {
let dessertNames = [
'Frozen Yogurt ',
'Ice cream sandwich ',
'Eclair',
'Cupcake',
'Gingerbread',
'Jelly bean',
'Lollipop',
'Honeycomb',
'Donut',
'KitKat',
null
];
for (let i = 0; i < 100; i++) {
let dessert = {
id: i,
name: dessertNames[Math.floor(Math.random() * dessertNames.length)],
secondName: dessertNames[8 + Math.floor(Math.random() * (dessertNames.length - 8))],
fat: Math.random() * 100,
carbs: Math.floor(Math.random() * 100),
protein: Math.random() * 10
};
this.desserts.push(dessert);
}
},
async countMaxValues(array) {
this.timerStart = Date.now();
for (const item of array) {
await this.countMaxValue(item).catch(() => {
//Even when one request throws error we should not stop others
})
}
},
async countMaxValue(item) {
await new Promise(resolve => setTimeout(resolve, 50)).then(() => {
let maxVal = Math.random() * 100;
item.maxValue = maxVal < 20 ? null : maxVal;
this.desserts.splice(item.id, 1, item);
});
}
}
})
<div id="app">
<v-app id="inspire">
<v-data-table
:headers="headers"
:items="desserts"
:footer-props='{
itemsPerPageOptions: [-1],
prevIcon: null,
nextIcon: null,
}'
>
<template v-slot:item="props">
<tr :style="{
background: (props.item.maxValue !== null && (props.item.carbs < props.item.maxValue))
? '#ffcdd2'
: (
(props.item.maxValue !== null && (props.item.carbs > props.item.maxValue)
? '#ffee58'
: (
props.item.maxValue === null ? '#ef5350' : 'transparent'
)
)
)}">
<td>{{ props.item.name || '—' }}</td>
<td>{{ props.item.secondName || '—' }}</td>
<td>{{ props.item.fat }}</td>
<td>{{ props.item.carbs }}</td>
<td>{{ props.item.protein }}</td>
<td>
<span>
{{ props.item.maxValue || '—' }}
</span>
<v-btn v-if="props.item.name && props.item.maxValue" icon>
<v-icon small>mdi-refresh</v-icon>
</v-btn>
</td>
<td class="justify-center text-center" style="min-width: 100px">
<v-tooltip bottom v-if="props.item.name && props.item.secondName">
<template v-slot:activator="{ on }">
<v-icon v-on="on"
class="mr-2"
small
>
format_list_numbered_rtl
</v-icon>
</template>
<span>Some action tooltip</span>
</v-tooltip>
<v-tooltip bottom v-if="props.item.name && props.item.secondName">
<template v-slot:activator="{ on }">
<v-icon v-on="on"
class="mr-2"
small
>
edit
</v-icon>
</template>
<span>Edit action tooltip</span>
</v-tooltip>
<v-tooltip bottom v-if="props.item.name === 'KitKat'">
<template v-slot:activator="{ on }">
<v-icon v-on="on"
small
>
delete
</v-icon>
</template>
<span>Delete action tooltip</span>
</v-tooltip>
</td>
</tr>
</template>
</v-data-table>
<p>{{ "Page loading time (sec): " + (loadingTime || '...') }}</p>
</v-app>
</div>
It seems Vue can update DOM more efficient if it is wrap in component (Sorry, I don't know in detail why).
This is your original code in JSFiddle. It will use around 12-13 seconds.
Then I create a component which wrap your entire tr:
const Tr = {
props: {
item: Object
},
template: `
<tr>
... // change props.item to item
</tr>
`
}
new Vue({
el: '#app',
vuetify: new Vuetify(),
components: {
'tr-component': Tr // register Tr component
},
...
async countMaxValue(item) {
await new Promise(resolve => setTimeout(resolve, 50)).then(() => {
let maxVal = Math.random() * 100;
// update entire object instead of one property since we send it as object to Tr
let newItem = {
...item,
maxValue: maxVal < 20 ? null : maxVal
}
this.desserts.splice(newItem.id, 1, newItem);
});
}
})
And your html will looks like:
<v-data-table
:headers="headers"
:items="desserts"
:footer-props='{
itemsPerPageOptions: [-1],
prevIcon: null,
nextIcon: null,
}'>
<template v-slot:item="props">
<tr-component :item='props.item'/>
</template>
</v-data-table>
The result will use around 6-7 seconds which is only 1-2 seconds to update DOM.
Or if you find out that your function trigger very fast (In your example use 50ms which is too fast in my opinion) you could try throttle it to less update DOM.
...
methods: {
async countMaxValue(item) {
await new Promise(resolve => setTimeout(resolve, 50)).then(() => {
let maxVal = Math.random() * 100;
let newItem = {
...item,
maxValue: maxVal < 20 ? null : maxVal
}
this.changes.push(newItem) // keep newItem to change later
this.applyChanges() // try to apply changes if it already schedule it will do nothing
});
},
applyChanges () {
if (this.timeoutId) return
this.timeoutId = setTimeout(() => {
while (this.changes.length) {
let item = this.changes.pop()
this.desserts.splice(item.id, 1, item)
}
this.timeoutId = null
}, 1500)
}
}
The result will use around 5-6 seconds but as you can see it's not immediately update.
Or you may try to call your API in parallel such as 10 requests, you could reduce from to wait 100 * 50 ms to around 10 * 50 ms (mathematically).
I hope this help.

VueDraggable and Laravel

I'm confused as how to correctly use vueDraggable together with Laravel.
I can drag and sort the elements in the browser but the array is not changing (when I check in the console)/ it seems to me the changes aren't reflected in the array. Shouldn't the array index numbers change after moving items?
In the overview.blade.php I have the component:
<qm-draggable :list="{{ $mylaravelarray }}"></qm-draggable>
In the qm-draggable.vue I have:
<template>
<draggable group="fragenblatt" #start="drag=true" #end="endDrag" handle=".handle">
<li v-for="(item, index) in draggablearray" :key="item.index">
// list items here
</li>
</draggable>
</template>
<script>
data() {
return {
draggablearray:{},
};
},
props: {
list: Array,
},
mounted: function(){
this.draggablearray = this.list; // create a new array so I don't alter the prop directly.
},
[..]
</script>
In the documentation it says, one way to pass the array is:
value
Type: Array
Required: false
Default: null
Input array to draggable component. Typically same array as referenced by inner element v-for directive.
This is the preferred way to use Vue.draggable as it is compatible with Vuex.
It should not be used directly but only though the v-model directive:
<draggable v-model="myArray">
But where do I do that? in overview.blade.php or in the component (.vue), or both?
Try setting v-model on your draggable as that's what will update draggablearray.
Also if draggablearray is supposed to be an array, initialise it as one, so draggablearray:{} should be draggablearray:[].
new Vue({
el: '#app',
data: () => {
return {
drag: false,
draggablearray: [{
id: 1,
name: "1"
}, {
id: 2,
name: "2"
}, {
id: 3,
name: "3"
}]
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js">
</script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs#1.7.0/Sortable.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Vue.Draggable/2.16.0/vuedraggable.min.js"></script>
<div class="container">
<div id="app">
<draggable v-model="draggablearray" group="fragenblatt">
<li v-for="(item, index) in draggablearray">
{{item.name}}
</li>
</draggable>
{{draggablearray}}
</div>
</div>
<script type="text/x-template" id="tree-menu">
<div class="tree-menu">
<div class="label-wrapper">
<div :style="indent" :class="labelClasses" #click.stop="toggleChildren">
<i v-if="nodes" class="fa" :class="iconClasses"></i>
<input type="checkbox" :checked="selected" #input="tickChildren" #click.stop /> {{label}}
</div>
</div>
<draggable v-model="nodes" :options="{group:{ name:'g1'}}">
<tree-menu v-if="showChildren" v-for="node in nodes" :nodes="node.nodes" :label="node.label" :depth="depth + 1" :selected="node.selected" :key="node">
</tree-menu>
</draggable>
</div>
</script>
Ah, I solved it, now I get the altered array back, I achieved it with this:
Had to add v-model="draggablearray" in the component .vue file
Needed to change my 'draggablearray' in data to an Array, instead of
object.
It looks like this now:
In the overview.blade.php I have the component:
<qm-draggable :list="{{ $mylaravelarray }}"></qm-draggable>
In the qm-draggable.vue I have:
<template>
<draggable v-model="draggablearray" group="fragenblatt" #start="drag=true" #end="endDrag" handle=".handle">
<li v-for="(item, index) in draggablearray" :key="item.index">
// list items here
</li>
</draggable>
</template>
<script>
data() {
return {
draggablearray:[], //has to be an Array, was '{}' before
};
},
props: {
list: Array,
},
mounted: function(){
this.draggablearray = this.list; // create a new array so I don't alter the prop directly.
},
[..]
</script>

owl.carousel vue.js component not rendering in lavarel application

I've been hunting and trying for the last handful of days but with no success.
I am trying to render a list of quotes to be displayed as a carousal on a login page.
I need to pull a list of quotes from a database which i have done. I then need to loop through the quotes and display them in the owl.carousel.
If i manually add the div.elements, it renders without a problem. when i add the element in a v-for loop, it does not display. Can someone please advise or guide me into the correct direction?
<template>
<carousel class="crsl" :autoplay="true" :nav="false" :items="1">
<div v-for="(item, index) in quotes" :key="item.id" v-text="item.quote"></div>
</carousel>
import carousel from 'vue-owl-carousel';
export default {
components: { carousel },
mounted() {
console.log('Component mounted.')
axios.post('api/quotes', {})
.then(response => {
this.quotes = response.data;
});
},
data: function () {
return {
quotes: null,
}
},
}
found a solution here
https://github.com/93gaurav93/v-owl-carousel/issues/16
My final code is as follows
<template>
<div v-if="quotes.length > 0">
<carousel :items="1" :autoplay="true" :nav="false" :dots="false">
<div v-for="(item, index) in quotes">
<div v-text="item.quote"></div>
</div>
</carousel>
</div>
<script>
import carousel from 'vue-owl-carousel';
export default {
components: { carousel },
data() {
return {
quotes: [],
};
},
mounted() {
axios.post('/api/quotes')
.then(resp => {
this.quotes = resp.data;
});
},
}

Binding a v-data-table to a props property in a template

I have a vue component which calls a load method returning a multi-part json object. The template of this vue is made up of several sub-vue components where I assign :data="some_object".
This works in all templates except for the one with a v-data-table in that the v-for process (or the building/rendering of the v-data-table) seems to kick-in before the "data" property is loaded.
With an npm dev server if I make a subtle change to the project which triggers a refresh the data-table then loads the data as I expect.
Tried various events to try and assign a local property to the one passed in via "props[]". Interestingly if I do a dummy v-for to iterate through or simply access the data[...] property the subsequent v-data-table loads. But I need to bind in other rules based on columns in the same row and that doesn't work.
Parent/main vue component:
...
<v-flex xs6 class="my-2">
<ShipViaForm :data="freight"></ShipViaForm>
</v-flex>
<OrderHeaderForm :data="orderheader"></OrderHeaderForm>
<v-flex xs12>
<DetailsForm :data="orderdet" :onSubmit="submit"></DetailsForm>
</v-flex>
...
So in the above the :data property is assigned from the result below for each sub component.
...
methods: {
load(id) {
API.getPickingDetails(id).then((result) => {
this.picking = result.picking;
this.freight = this.picking.freight;
this.orderheader = this.picking.orderheader;
this.orderdet = this.picking.orderdet;
});
},
...
DetailsForm.vue
<template lang="html">
<v-card>
<v-card-title>
<!-- the next div is a dummy one to force the 'data' property to load before v-data-table -->
<div v-show="false">
<div class="hide" v-for='header in headers' v-bind:key='header.product_code'>
{{ data[0][header.value] }}
</div>
</div>
<v-data-table
:headers='headers'
:items='data'
disable-initial-sort
hide-actions
>
<template slot='items' slot-scope='props'>
<td v-for='header in headers' v-bind:key='header.product_code'>
<v-text-field v-if="header.input"
label=""
v-bind:type="header.type"
v-bind:max="props.item[header.max]"
v-model="props.item[header.value]">
</v-text-field>
<span v-else>{{ props.item[header.value] }}</span>
</td>
</template>
</v-data-table>
</v-card-title>
</v-card>
</template>
<script>
import API from '#/lib/API';
export default {
props: ['data'],
data() {
return {
valid: false,
order_id: '',
headers: [
{ text: 'Order Qty', value: 'ord_qty', input: false },
{ text: 'B/O Qty', value: 'bo_qty', input: false },
{ text: 'EDP Code', value: 'product_code', input: false },
{ text: 'Description', value: 'product_desc', input: false },
{ text: 'Location', value: 'location', input: false },
{ text: 'Pick Qty', value: 'pick_qty', input: true, type: 'number', max: ['ord_qty'] },
{ text: 'UM', value: 'unit_measure', input: false },
{ text: 'Net Price', value: 'net_price', input: false },
],
};
},
mounted() {
const { id } = this.$route.params;
this.order_id = id;
},
methods: {
submit() {
if (this.valid) {
API.updateOrder(this.order_id, this.data).then((result) => {
console.log(result);
this.$router.push({
name: 'Orders',
});
});
}
},
clear() {
this.$refs.form.reset();
},
},
};
</script>
Hopefully this will help someone else who can't see the forest for the trees...
When I declared the data() { ... } properties in the parent form I initialised orderdet as {} instead of [].

Resources