How to reset a component's props in VueJS upon reload? - javascript

I have a component in VueJS that displays a data table based off an array of objects. The component looks like this:
<template>
<div id="table">
<p class="title">Data:</p>
<v-data-table :headers="headers" :items="imported_data">
<template slot="items" slot-scope="props">
<td class="text-xs-left">{{props.item.name}}</td>
<td class="text-xs-right">{{props.item.age}}</td>
</template>
</v-data-table>
</div>
</template>
<script>
export default {
props: {
imported_data: {
type: Array,
required: true
}
},
data: () => ({
headers: [
{ text: "Name", value: "name" },
{ text: "Age", value: "age" }
]
})
};
</script>
I can render this component by doing <DataTableView :imported_data="this.dataset"/>, where this.dataset is an array of objects that populates my data table. My issue is when I go to reinitialize my component with another dataset, the new dataset just appends to the old dataset, it doesn't overwrite the old dataset with the new dataset. How can I make it so that my imported_data prop is reset when I put in new data?
Edit:
The data in imported_data is retrieved from the backend via a simple GET request:
axios
.get("http://localhost:8888/getData")
.then((res) => {
this.dataset = res.data
})
I have a button on the page that gets new data from my backend again, but the 'new' data is appending my 'old' data. The DataTableView component is loaded alongside the button, but isn't shown until this.imported_data is populated via a v-if conditional.

Related

Save the current page in v-data-table

I am using VueJS and Vuetify.
In v-data-table UI Component of vuetify, I want to save the current page that the current user is. For example, if the user is on the page 3 of 10, and he reloads the page, it will automatically go back to page 3 of 10. The current function is when the user reloads, it goes back to page 1 of 10.
footerOptions: {
showFirstLastPage: true,
showCurrentPage: true,
itemsPerPageOptions: [20, 50, 100],
itemsPerPageText: 'Data per Page',
},
getPagination(data) {
sessionStorage.setItem('currentPage', data.page)
},
<v-data-table
:headers="tableHeaders"
:items="users"
item-key="username"
:loading="isLoading"
#item-selected="itemSelect"
#toggle-select-all="selectAll"
#click:row="singleClick"
#dblclick:row="doubleClick"
#pagination="getPagination"
:footer-props="footerOptions"
show-select
multi-sort
dense>
</v-data-table>
Edit: I am already getting the current page and save it to session storage. Now, all I need to make that currentPage binded in the v-data-table. I tried to used the page prop of v-data-table but nothing happens. Still on page 1 even the currentPage is 2/3/4 and so on.
Here is what you should try:
add :page prop on v-data-table to bind the page number to a data property pageNum.
listen for update:page event which fires from v-data-table any time the table's page value changes, then call a method named pageChange
in the pageChange method you should save the page value (passed in for you by the event) to localStorage: localStorage.setItem("page", newPage);
on component creation, set pageNum to the value saved in localStorage: this.pageNum = localStorage.getItem("page");
simplified component code shown below:
<template>
<div>
<v-data-table
:headers="tableHeaders"
:items="users"
:page="pageNum"
#update:page="pageChange"
>
</v-data-table>
</div>
</template>
<script>
export default {
data() {
return {
pageNum: 1,
tableHeaders: [],
users: [],
};
},
methods: {
pageChange(newPage) {
localStorage.setItem("page", newPage);
},
},
created() {
this.pageNum = localStorage.getItem("page");
},
};
</script>
This will persist the page number value that v-data-table holds across page refreshes. You might want to think about clearing localStorage, localStorage.clear();, when the user navigates to a different page so if they come back to the page with v-data-table they are on page 1 again.
I think the issue may be that your users is requested from backend?
You should change the value of pageNum binded to v-data-table after users are got from remote.
<template>
<v-data-table
:items="users"
:page="pageNum"
>
</v-data-table>
</template>
<script>
export default {
data() {
return {
pageNum: 1,
users: [],
};
},
created() {
getUsers().then((promise)=>{
this.users = promise.data
this.pageNum = sessionStorage.getItem('currentPage');
})
},
};
</script>
The key point is that you should set the value of pageNum when v-data-table already has multiple pages.
Here's a super simple solution using the power of setup function and VueUse! 🚀
Check this codesandbox I made: https://codesandbox.io/s/stack-73536361-pagination-currentpage-vueuse-3yycx0?file=/src/components/Example.vue
VueUse is a collection of utility functions based on Composition API. With support for Vue 2 & 3. In this example I'll use the useStorage function that simplifies the process to create a reactive LocalStorage/SessionStorage.
First, install the VueUse npm package: npm i #vueuse/core
Then in your vue component import useStorage from #vueuse/core. And configure your reactive variable in the setup function like this:
<script>
import { useStorage } from '#vueuse/core'
export default {
name: 'Example',
setup() {
const state = useStorage('my-page', {
page: 1
})
return { state }
},
data: () => ({
...
})
}
</script>
Then in your v-data-table get/set the current page with the page prop and make sure to use the .sync modifier and link it to your reactive variable. In this example state.page.
<v-data-table
:headers="headers"
:items="desserts"
:page.sync="state.page"
:footer-props="{
showFirstLastPage: true,
showCurrentPage: true,
:items-per-page="3"
itemsPerPageOptions: [3, 50, 10],
itemsPerPageText: 'Data per Page',
}"
class="elevation-1"
></v-data-table>
That's it! VueUse is awesome, you can add more variables to your reactive state to save the items per page or anything you want.

Hyperlinking table data from API with vue-router

I'm taking in and displaying data from an API (https://restcountries.com/), but I'm having trouble hyperlinking each table entry to its own page with the router-link tag. Nothing happens, no errors, no linked text.
<template>
<div>
<b-table responsive head-variant="dark" striped hover :items="countries" :fields="headings">
<template #cell(name)="data">
<router-link :to="{name: 'country', params: {country: country.name.official}}">
{{ data.value }}
</router-link>
</template>
</b-table>
</div>
</template>
<script>
import axios from '#/config'
export default {
name: 'AllCountries',
components: {},
data() {
return {
headings: [
{
key: 'name.common',
sortable: true
},
{
key: 'capital',
sortable: true
},
],
countries: []
}
},
mounted() {
axios.get('/all')
.then(response => {
console.log(response.data)
this.countries = response.data
})
.catch(error => console.log(error))
}
}
</script>
If I remove the .common from the key 'name', router-link can work, but it will display all variations of country name rather than just the one 'common' name that I want. Also if .common is removed, the router-link does not work as it's shown here I get errors such as:
"TypeError: Cannot read properties of undefined (reading 'name')"
"Property or method "country" is not defined on the instance but referenced during render."
Though I don't get these errors with this exact router-link elsewhere, and 'name' wasn't defined in those files), the only way I've gotten the router-link to link is with these params: { id: data.item._id }(though it links to nothing (it tries to link to '/undefined?fullText=true'))
Params of the router-link should have been params: { country: data.item.name.official }}

b-table reloads nested components when "unshifting" a new row (but not when "pushing")

I have a vue component which contains this table, which also has a component inside its only row:
<template>
<b-table :items="records">
<template slot="row-details">
<child-component/>
</template>
</b-table>
</template>
I'm keeping the data inside the table very simple, for sample purposes:
data() {
return {
records: [{
name: "Parent Row",
_showDetails: true
}]
};
}
The child-component inside the row is quite simple too:
<template>
<div>
<p>{{number}}</p>
</div>
</template>
<script>
export default {
data() {
return {
number: Math.random(),
isUpdating: console.log("Updating Child Component")
};
},
}
</script>
If I add a new row in the parent table using records.push(newRow) everything works fine, I can see the new row and, in the child component, number does not change.
BUT If I add the new row using records.unshift(newRow) the child component is reloaded, the "Updating child component" message shows and number changes every time.
Is this the expected behaviour? How can I keep the number so it does not change when I unshift a new record?
I have created a working sample here.
To minimize re-renders of child components in a b-table, and if your data has a field that is unique for every row (i.e. an ID or primary key), set the primary-key prop to the name of the field that contains the unique row ID. This unique ID will be used as the Vue key for each <tr> element. Vue will then know if it needs to re-render the children or not.
Otherwise, what is happening is that b-table uses the row's index as the key. By pushing new rows on the table, the previous rows' Vue key stays the same (and hence not re-rendered), but if you shift on the top of the rows, those that previously has indexes of 0, 1, 2, etc have been replaced by new rows... and the entire table and it's contents (children) must be re-rendered.
EDIT/UPDATE:
I've discovered the root cause. The row-details slot was always using the row's index as its :key, and not incorporating the primary key value (if available). PR https://github.com/bootstrap-vue/bootstrap-vue/pull/4025 will fix this issue and will be available in the 2.0.0 stable release (being released today hopefully).
UPDATE 2:
BootstrapVue v2.0.0 stable has just been released (2019-09-06)
I think it has something todo with the child component. If you set _showDetails to true to every element you see what is happening. If you press prepend for the first time the number of the prepend will be the current number of parent and parent gets an new number
<template>
<div>
<input value="Append Row" #click="appendRow" type="button">
<input value="Prepend Row" #click="prependRow" type="button">
<b-table :items="records" :fields="fields">
<template v-slot:row-details="scope">
<child-component :number="scope.item.number"/>
</template>
</b-table>
</div>
</template>
<script>
import ChildComponent from "./ChildComponent";
export default {
data() {
return {
fields: ['name'],
records: [
{
name: "Parent Row",
_showDetails: true,
number: Math.random()
}
]
};
},
components: {
"child-component": ChildComponent
},
methods: {
appendRow() {
let newRow = {
name: "New Row (Append)",
_showDetails: true,
number: Math.random()
};
this.records.push(newRow);
},
prependRow() {
let newRow = {
name: "New Row (Prepend)",
_showDetails: true,
number: Math.random()
};
this.records.unshift(newRow);
}
}
};
</script>
Child-component:
<template>
<div>
<p>test {{number}}</p>
</div>
</template>
<script>
export default {
props: ['number'],
data() {
return {
isUpdating: console.log("Updating Child Component")
};
}
};
</script>

VueJS Render VNode

tl;dr:
Given a VueJS VNode object, how do I get the HTML element that would be generated if it were rendered?
e.g.:
> temp1
VNode {tag: "h1", data: undefined, children: Array(1), text: undefined, elm: undefined, …}
> temp1.children[0]
VNode {tag: undefined, data: undefined, children: undefined, text: "Test", elm: undefined, …}
> doSomething(temp1)
<h1>Test</h1>
Goal
I'm attempting to build a small VueJS wrapper around the DataTables.net library.
To mimic the behavior of HTML tables in my markup, I want something like the following:
<datatable>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>Salary</th>
</tr>
</thead>
<tbody>
<datatable-row v-for="person in people">
<td>{{ person.name }}</td>
<td>{{ person.age }}</td>
<td>{{ person.salary }}</td>
</datatable-row>
</tbody>
</datatable>
What I've done so far
I've started to implement this as follows:
DataTable.vue
<template>
<table ref="table" class="display table table-striped" cellspacing="0" width="100%">
<slot></slot>
</table>
</template>
<script>
/* global $ */
export default {
data: () => ({
instance: null
}),
mounted() {
this.instance = $(this.$refs.table).dataTable();
this.$el.addEventListener("dt.row_added", function(e) {
this.addRow(e.detail);
});
},
methods: {
addRow(row) {
// TODO <-----
console.log(row);
}
}
};
</script>
DataTableRow.vue
<script>
/* global CustomEvent */
export default {
mounted() {
this.$nextTick(() => {
this.$el.dispatchEvent(new CustomEvent("dt.row_added", {
bubbles: true,
detail: this.$slots.default.filter(col => col.tag === "td")
}));
});
},
render() { return ""; }
};
What this currently does:
When the page loads, the DataTable is initialized. So the column headers are properly formatted and I see "Showing 0 to 0 of 0 entries" in the bottom left
The CustomEvent is able to bubble up past the <tbody> and be caught by the DataTable element successfully (circumventing the limitation in VueJS that you can't listen to events on slots)
What this does not do:
Actually add the row
My event is giving me an array of VNode objects. There's one VNode per column in my row. The DataTables API has an addRow function which can be called like so:
this.instance.row.add(["col1", "col2", "col3"]);
In my case, I want the resultant element from the rendering of the VNode to be the elements in this array.
var elems = [];
for (var i = 0; i < row.length; i++)
elems[i] = compile(row[i]);
this.instance.row.add(elems);
Unfortunately this compile method eludes me. I tried skimming the VueJS documentation and I tried Googling it, but no dice. I tried manually passing the createElement function (the parameter passed to the render method) but this threw an error. How can I ask VueJS to render a VNode without injecting the result into the DOM?
I ran into the same issue wanting to do basically the same thing with a row details template for DataTables.net.
One solution could be to create a generic component that renders out a VNode and instantiate that programmatically. Here is how my setup for a dynamic detail row that I insert using datatable's row.child() API.
RenderNode.js
export default {
props: ['node'],
render(h, context) {
return this.node ? this.node : ''
}
}
Datatables.vue
Include the renderer component from above
import Vue from 'vue'
import nodeRenderer from './RenderNode'
Instantiate and mount the renderer to get the compiled HTML
// Assume we have `myVNode` and want its compiled HTML
const DetailConstructor = Vue.extend(nodeRenderer)
const detailRenderer = new DetailConstructor({
propsData: {
node: myVNode
}
})
detailRenderer.$mount()
// detailRenderer.$el is now a compiled DOM element
row.child(detailRenderer.$el).show()
You should define your components like with:
import {createApp} from 'vue';
import {defineAsyncComponent} from "vue";
createApp({
components: {
'top-bar': defineAsyncComponent(() => import('#Partials/top-bar'))
}
}).mount("#app")

How to use new Vue in a single file component?

I'm still learning Vue.js and I have a minor issue:
I have a single file component with an array of checkboxes, and I have looked at the documentation for using multiple checkboxes, but the example there requires me to declare:
new Vue({
el: '#example-3',
data: {
checkedNames: []
}
})
Edit However I have set this up in my single file component within the <script> tag within data(), but it just checks/unchecks all boxes at once (but feeds back true/false correctly):
<script>
export default {
name: 'PhrasesDetail',
data () {
return {
game: '',
language: '',
checkedPhrasesArr: {
el: '#selection',
data: {
checkedPhrasesArr: []
}
}
}
},
...
</script>
The Question is where and how do I declare the checkbox array so that it reacts/recognises the individual elements?
These are my checkboxes:
<tr v-for="(keyTransPair, index) in this.language.data">
<td id="selection"><input type="checkbox" :id="index" v-model="checkedPhrasesArr"></td>
<td>{{index}}</td>
...
</tr>
I have assembled a complete example. (I don´t know anything about your language.data object so I´m just using fake data).
<template>
<div>
<!-- Your Checkboxes -->
<table>
<tr v-for="(keyTransPair, index) in language.data">
<td id="selection"><input type="checkbox" :value="keyTransPair" :id="index" v-model="checkedPhrasesArr"></td>
<td>{{index}}</td>
</tr>
</table>
<!-- Show the selected boxes -->
{{ checkedPhrasesArr }}
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
// Your language data
language: {
data: {
keyOne: "one",
keyTwo: "two",
keyThree: "three"
}
},
// The Checkbox data
checkedPhrasesArr: []
}
}
}
</script>
Note that the checkbox values are bound with :value="keyTransPair". You can change the value to anything you want. This value will be added to the array if the checkbox is checked.
By the way: You use <td id="selection"> within the v-for loop. So the id "selection" will not be unique. You should better use a class instead of an id here.

Categories