Vuetify Using datatable with external data from an API with Vuex - vue.js

I want to use the vuetify framework with Vuex , but there is limited documentation about using it with Vuex.
I want to:
Get data from an external API ( but only the data needed )
Then Save the data in state and edit or whatever
Then push any changes back to the api
I have tried some of the external pagination and sorting examples with vuetify , but I can't get it to show all record count unless I hard code it.
I am quite new to Vue and Vuetify , so maybe I am misunderstanding something.
<template>
<div>
<v-data-table
:headers='headers'
:items='items'
:length='pages'
:search='search'
:pagination.sync='pagination'
:total-items='totalItemCount'
class='elevation-1'
>
<template slot='items' slot-scope='props'>
<td class='text-xs-right'>{{ props.item.id }}</td>
<td class='text-xs-right'>{{ props.item.first_name }}</td>
<td class='text-xs-right'>{{ props.item.last_name }}</td>
<td class='text-xs-right'>{{ props.item.avatar }}</td>
</template>
</v-data-table>
</div>
</template>
<script>
import moment from 'moment'
import axios from 'axios'
export default {
name: 'test-table',
watch: {
pagination: {
async handler () {
const rowsPerPage = this.pagination.rowsPerPage
// const skip = (this.pagination.page - 1) * rowsPerPage
const pageNumber = this.pagination.page
const res = await axios.get(`https://reqres.in/api/users?page=${pageNumber}&per_page=${rowsPerPage}`)
this.items = res.data.data
this.$store.commit('saveTableData', this.items)
},
deep: true
}
},
computed: {
pages () {
return 171
},
totalItemCount () {
return 400
}
},
async mounted () {
const rowsPerPage = this.pagination.rowsPerPage
const skip = (this.pagination.page - 1) * rowsPerPage
const res = await axios.get(`https://reqres.in/api/users?page=${skip}&per_page=${rowsPerPage}`)
this.items = res.data.data
this.$store.commit('saveTableData', this.items)
},
methods: {
nzDate: function (dt) {
return moment(dt).format('DD/MM/YYYY')
}
},
data: () => ({
search: '',
// totalItems: 0,
items: [],
pagination: {
sortBy: 'Date'
},
headers: [
{ text: 'ID', value: 'id' },
{ text: 'First Name', value: 'first_name' },
{ text: 'Last Name', value: 'last_name' },
{ text: 'Avatar', value: 'avatar' }
]
})
}

This is my working setup:
<template>
<v-data-table
:total-items="pagination.totalItems"
:pagination.sync="pagination"
:items="rows"
:headers="columns">
<template slot="headers" slot-scope="props">
<tr :active="props.selected">
<th v-for="column in props.headers">
{{ column.value }}
</th>
</tr>
</template>
<template slot="items" slot-scope="props">
<tr>
<td v-for="cell in props.item.row">
<v-edit-dialog lazy>
{{ cell.value }}
<v-text-field
:value="cell.value"
single-line
counter>
</v-text-field>
</v-edit-dialog>
</td>
</tr>
</template>
</v-data-table>
</template>
<script>
export default {
data: () => ({
pagination: {
page: 1,
rowsPerPage: 10,
totalItems: 0
},
selected: []
}),
computed: {
columns: {
get () {
return this.$store.state.columns
}
},
rows: {
get () {
return this.$store.state.rows
}
}
},
methods: {
async getRowsHandler () {
try {
const {total} = await this.$store.dispatch('getRows', {
tableIdentifier: this.$route.params.tableIdentifier,
page: this.pagination.page,
size: this.pagination.rowsPerPage
})
this.pagination.totalItems = total
} catch (error) {
// Error
}
}
}
}
</script>
I didn't implement everything. If you miss a specific part ask again and I will update my example. One more tip: You should avoid watch deep wherever possible. It can result in heavy calculations.

Assuming this is Vuetify v1.5, the documentation on the total-items prop on data-tables states:
Caution: Binding this to a blank string or using in conjunction with
search will yield unexpected behaviours
If you remove the 'search' prop from your table the record count will show again. If you're doing external stuff anyway, you'll won't want the default search functionality.

Related

Vue composition API is not reactive inside v-for?

I am using Vue 2 and Vuetify (not Vue 3) to create a form builder website. I was going perfectly well until I found out that something is wrong. So here's the case. I rendered text fields (inputs) from a reactive array using the following code.
<template>
// ... some other unrelated code
<template v-if="answer.type !== 1">
<v-col
v-for="(_, i) in answer.options"
:key="`#question-${answer.id}-${i}`"
cols="12"
>
<div class="flex flex-row justify-between items-center">
<TextField
v-model="answer.options[i]"
class="ml-4"
label="Option"
underlined
hideLabel
/>
<v-btn #click="deleteOption(i)" icon>
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
</v-col>
<v-col>
<TextButton #click="addOption()" text="+ ADD OPTION" />
</v-col>
</template>
// ... some other unrelated code
</template>
<script>
import { reactive, ref, watch } from '#vue/composition-api'
import useInquiry from '#/composables/useInquiry'
import TextButton from '#/components/clickables/TextButton.vue'
import TextField from '#/components/inputs/TextField.vue'
export default {
components: { TextField, TextButton },
setup() {
const { answer, addOption, deleteOption } = useInquiry()
return { answer, addOption, deleteOption }
}
}
</script>
Here's my useInquiry composable logic
import { reactive, watch, se } from '#vue/composition-api'
import ID from '#/helpers/id'
export default () => {
const answer = reactive({
id: ID(), // this literally just generates an ID
type: 2,
options: ['', '']
})
const addOption = () => {
answer.options.push('')
}
const deleteOption = at => {
const temp = answer.options.filter((_, i) => i !== at)
answer.options = []
answer.options = temp
};
return { answer, addOption, deleteOption }
}
And finally, here's my TextField.vue
<template>
<v-text-field
v-model="inputValue"
:label="label"
:single-line="hideLabel"
:type="password ? 'password' : 'text'"
:outlined="!underlined"
:dense="!large"
hide-details="auto"
/>
</template>
<script>
import { ref, watch } from '#vue/composition-api'
export default {
model: {
props: 'value',
event: 'change'
},
props: {
label: String,
value: String,
password: Boolean,
underlined: Boolean,
large: Boolean,
hideLabel: Boolean
},
setup(props, context) {
const inputValue = ref(props.value)
watch(inputValue, (currInput, prevInput) => {
context.emit('change', currInput)
})
return { inputValue }
}
}
</script>
The problem is, everytime the delete button is clicked, the deleted input is always the last one on the array, even though I didn't click on last one. I tried to log my reactive array by watching it using Vue's composition watch method. Apparently, the data is correctly updated. The problem is the v-model looks un-synced and the last input is always the one that gets deleted.
Looks good to me ....except the TextField.vue
Problem is in TextField.vue. What you actually doing inside setup of TextField.vue is this (Vue 2 API):
data() {
return {
inputValue: this.value
}
}
...this is one time initialization of inputValue data member with value of value prop. So when one of the options is removed and components are reused (because that's what Vue does all the time - especially when index is used in :key) inputValue is not updated to a new value of value prop...
You don't need inputValue or model option at all, just remove it and use this template:
<v-text-field
:value="value"
#input="$emit('input', $event)"
:label="label"
:single-line="hideLabel"
:type="password ? 'password' : 'text'"
:outlined="!underlined"
:dense="!large"
hide-details="auto"
/>
NOTE that in my example I'm using $event.target.value instead of just $event because I'm working with native <input> element and not Vuetify's custom component input...
working example...
const {
reactive,
ref,
watch
} = VueCompositionAPI
Vue.use(VueCompositionAPI)
const BrokenInput = {
model: {
props: 'value',
event: 'change'
},
props: {
value: String,
},
setup(props, context) {
const inputValue = ref(props.value)
watch(inputValue, (currInput, prevInput) => {
context.emit('change', currInput)
})
return {
inputValue
}
},
template: `<input type="text" v-model="inputValue" />`
}
const FixedInput = {
props: {
value: String,
},
template: `<input type="text" :value="value" #input="$emit('input', $event.target.value)"/>`
}
const useInquiry = () => {
const answer = reactive({
id: 1,
type: 2,
options: ['1', '2', '3', '4', '5']
})
const addOption = () => {
answer.options.push('')
}
const deleteOption = at => {
const temp = answer.options.filter((_, i) => i !== at)
answer.options = temp
};
return {
answer,
addOption,
deleteOption
}
}
const app = new Vue({
components: { 'broken-input': BrokenInput, 'fixed-input': FixedInput },
setup() {
return useInquiry()
},
})
app.$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/#vue/composition-api#1.0.0-beta.18"></script>
<div id="app">
<table>
<tr>
<th>Option</th>
<th>Broken input</th>
<th>Model</th>
<th>Fixed input</th>
<th></th>
<tr>
<tr v-for="(_, i) in answer.options" :key="'#question-'+ answer.id + '-' + i">
<td>Option {{ i }}:</td>
<td>
<broken-input v-model="answer.options[i]" />
</td>
<td>
{{ answer.options[i] }}
</td>
<td>
<fixed-input v-model="answer.options[i]" />
</td>
<td><button #click="deleteOption(i)">Remove option</button></td>
</tr>
</table>
<button #click="addOption()">Add option</button>
</div>

Embeded template in vue

I would like to use a local component in VueJS:
My component file (cleaned up a bit):
<template id="heroValuePair">
<td class="inner label">{{label}}</td>
<td class="inner value">{{c}}</td>
<td class="inner value">
{{t}}
<span v-if="c < t" class="more">(+{{t-c}})</span>
<span v-if="c > t" class="less">({{t-c}})</span>
</td>
</template>
<template id="hero">
<table class="hero card" border="0" cellpadding="0" cellspacing="0">
<tr>
<td>other data...</td>
</tr>
<tr>
<hvp label="Label" v-bind:c="current.level" :t="target.level" :key="hero.id"/>
</tr>
</table>
</template>
<script>
var HeroValuePair = {
template: "#heroValuePair",
props: {
label : String,
c : Number,
t : Number
},
created() {
console.log("HVP: "+this.c+" "+this.t);
}
};
Vue.component("Hero", {
template: "#hero",
props: {
heroId : String
},
components: {
"hvp" : HeroValuePair
},
data: () => ({
hero: {},
current: {},
target: {}
}),
computed: {
},
created() {
fetch("/api/hero/"+this.heroId)
.then(res => res.json())
.then(res => {
this.hero = res.hero
this.current = res.current
this.target = res.target
})
}
});
</script>
<style>
</style>
This outer Hero template is used in a list iterator:
<template id="card-list">
<table>
Card list
<div id="">
<div v-for="card in cards" class="entry">
<Hero :hero-id="card.hero.id" :key="card.hero.id"/>
</div>
</div>
</table>
</template>
<script>
Vue.component("card-list", {
template: "#card-list",
data: () => ({
cards: [],
}),
created() {
fetch("/api/cards")
.then(res => res.json())
.then(res => {
this.cards = res.heroes
})
.catch((e) => alert("Error while fetching cards: "+e));
}
});
</script>
<style>
</style>
However, when I render the card list, it only produces the list of the first td in hvp template:
When I comment out the call of hpv the page is rendered correctly with all the HTML code from Hero template.
I tried to figure out what step I left out, but can't find the clue.
One last info: I used JavalinVue to support the server side, not nodejs-based Vue CLI. I don't know if it has any impact, but may be important.
UPDATE 1
After IVO GELOV spot the problem with multiple root tags, and because I can't move to Vue3, I tried to make it as a functional template, as he suggested. I removed the template and created the render function:
var HeroValuePair = {
template: "#heroValuePair",
functional: true,
props: {
label : String,
c : Number,
t : Number
},
render(createElement, context) {
console.log("HVP: "+context.props.c+" "+context.props.t);
if (typeof context.props.c === undefined) return createElement("td" )
else return [
createElement("td", context.props.label ),
createElement("td", context.props.c ),
createElement("td", context.props.t )
]
}
}
Although the console indicated the render is called correctly, the result is the same: there is neither the rendered nodes, nor the parent Hero component displayed. I tried to move into different file, tried the functional template format, but none worked.

Vue.js - Loop, fetch and display all data from array.push

I have a button that adds new field with the use of array.push. I was able to fetch the right data while using a REST client (Insomnia). I'm aware of how to display the data to a single form using response => this.item = response.data, but how do I loop the response.data to display all the data entered in the array.push fields?
Please see my code below.
Component.vue
<template>
<v-btn #click="addRow()">Add New</v-btn>
</template>
<table class="table">
<tbody>
<tr v-for="(row, index) in books" :key="row.id">
<td>
<base-select
v-model="row.book_id"
:items="books"
item-text="title"
item-value="id"
label="Sample Books"
/>
</td>
<td><v-textarea label="Title" v-model="row.title" /></td>
<td><v-textarea label="Genre" v-model="row.genre" /></td>
<td><v-text-field label="Pages" v-model="row.pages" /></td>
<td><a #click="removeRow(index);">Remove</a></td>
</tr>
</tbody>
</table>
<script>
export default {
data: ()=> ({
books: [],
}),
created () {
this.getBooks ()
},
methods: {
addRow () {
this.books.push({
book_id: '',
title: '',
genre: '',
pages: '',
});
},
getBooks () {
axios.get('/api/books', {
params: { id: this.$route.params.id }
})
.then(response => this.books = // Display all data to each field in row (example there are 3 records existing with the id of params.id) )
.catch(error => console.log(error))
},
removeRow (index) {
this.buttons.splice(index, 1);
},
}
}
</script>
Screnshot
I have 5 data in the database and it reflects the number of rows, but the fields are empty.

How to solve v-switch vuetify only binds one way?

My v-switch from vuetify is only binding one way.
If i load in my data it switches on or off. so its working if i load data in the v-model of the v-switch.
But if i switch the v-switch, it switches off, but does not change anything.
here is the code:
<v-data-table :headers="datatable.headers" :items="datatable.items" class="elevation-1">
<template v-slot:body="{ items }">
<tr v-for="(item, index) in items" :key="index">
<td>{{item.name}}</td>
<td #click="() => { $router.push(`/settings/${item.name.toLowerCase()}`) }"><v-icon small>edit</v-icon></td>
<td><v-switch v-model="inMenu[item.name.toLowerCase()]" :label="`Switch 1: ${inMenu[item.name.toLowerCase()]}`"></v-switch></td>
</tr>
</template>
</v-data-table>
<script>
export default {
data() {
return {
tabs: [
'Content types'
],
tab: null,
datatable: {
items: [],
headers: [{
text: 'Content types', value: "name"
}]
},
settings: null,
inMenu: {},
}
},
mounted() {
this.$axios.get('/settings.json').then(({data}) => {
this.settings = data
});
this.$axios.get('/tables.json').then(({data}) => {
// set all content_types
data.map(item => {
this.datatable.items.push({
name: item
})
})
// check foreach table if it's in the menu
this.datatable.items.forEach(item => {
this.inMenu[item.name.toLowerCase()] = JSON.parse(this.settings.menu.filter(menuitem => menuitem.key == item.name.toLowerCase())[0].value)
})
})
},
updated() {
console.log(this.inMenu)
}
}
</script>
so i clicked on the first switch and it does not change the state
i tried to have a normal prop in the data function.
i made a switch: null prop and it will react fine to that, but not to my code.
Any idea?
My guess is that your data is not reactive when you write:
// check foreach table if it's in the menu
this.datatable.items.forEach(item => {
this.inMenu[item.name.toLowerCase()] = JSON.parse(this.settings.menu.filter(menuitem => menuitem.key == item.name.toLowerCase())[0].value)
})
You should use the $set method instead and write:
// check foreach table if it's in the menu
this.datatable.items.forEach(item => {
this.$set(this.inMenu, item.name.toLowerCase(), JSON.parse(this.settings.menu.filter(menuitem => menuitem.key == item.name.toLowerCase())[0].value)
}))
See https://v2.vuejs.org/v2/guide/reactivity.html for more information on reactivity
Does this solve your problem?

Update data after change without page refreshing using vue.js

I wrote such code:
<template>
<div class="home">
<HelloWorld tableTitle="Goods:">
<table class="table table-striped">
<tbody>
<tr v-for="(i, index) in items.data" :key="index">
<td>{{ i.id }}</td>
<td>{{ i.name }}</td>
<td>{{ i.producer }}</td>
<td><font-awesome-icon v-if="i.received" icon="check" /><font-awesome-icon v-else icon="times" /><td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import HelloWorld from "#/components/HelloWorld.vue";
import axios from "axios";
export default {
name: "home",
components: {
HelloWorld
},
data: () => ({
items: {},
errors: []
}),
beforeMount() {
axios
.get("http://40.115.119.247/AgileSelection/tnt/status")
.then(response => {
this.items = response.data;
console.log(this.items);
})
.catch(e => {
this.error.push(e);
});
}
};
<script>
Now, for refreshing information I just use the refresh button.
What and where should I add some lines of code to update information but without refreshing the page? Because, I am updating data every 5 seconds. So I think that manually updating is not so good idea.
do something like this
// data property
data () {
return {
...
interval: null
}
},
// in your created hook
created () {
this.interval = setInterval(this.refreshData, 5000)
},
beforeDestroy () {
clearInterval(this.interval)
},
// in methods property
methods: {
refreshData () {
// fetch data
axios.get("http://40.115.119.247/AgileSelection/tnt/status")
.then(response => {
this.items = response.data
})
}
}
this will fetch your data from your API and update the list automatically. this will update your UI as well.
you can try using location.reload() after the code where you register the update
for example
handleSubmit() {
this.registerServices(this.organization)
location.reload();
}