state of currentPage is changing after coming back to page in vue - vue.js

I am struggling to maintain the state of current page in vuex. I am tring to store the state of pagination when I click on any record and the on click of back button I should get the same page.
Below is the code snippest :
table.html
<b-table show-empty
outlined
hover
stacked="md"
:sort-by.sync="sortBy"
:items="companies"
:fields="[
{key: 'name', label: this.$t('shared.name'), sortable: true},
{key: 'companyId', label: this.$t('shared.orgNumber'), sortable: true},
{key: 'properties.state', label: this.$t('shared.status'), sortable: true},
{key: 'serviceProviderName', label: this.$t('shared.serProvider'), sortable: true}
]"
:current-page="currentPage"
:per-page="perPage"
:filter="filter"
v-bind:empty-text='$t("shared.emptyText")'
v-bind:empty-filtered-text='$t("shared.emptyFilteredText")'
#filtered="onFiltered"
#row-clicked="showCompany"
tbody-tr-class="row-cursor">
</b-table>
<b-pagination :total-rows="totalRows" :per-page="perPage" v-model="currentPage"
class="float-right"/>
in js file I have added computed like this :
computed: {
currentPage:{
get () {
return this.$store.getters.currentPage;
},
set (value) {
this.$store.commit('SET_CURRENT_PAGE', value);
}
},
},
Now the problem is when I am click on page number 2 and then click on any record and on click of record, new page is opening and then when I come back from that page I am not getting page number as 2. It is changing again to 1. In developer tool I can see it is storing the state but it is chaning when I come back to the page
I dont know why the state of currentPage is changing. Please help!

currentPage will reset after you leave the page.
To prevent such a behavior you should open record page in new window/tab or save the currentPage value in cookies or localStorage.
If you prefer the latter I would advice you to use vuex-persistedstate

You should use #change attribute on your b-pagination component. If you bind it to a function returning items, it can use currentPage inside it to fetch the data for current page.
<b-pagination #change="fetchItems" ....>
Inside your component you could have a method
methods: {
fetchItems () {
// Use this.$store.getters.currentPage inside this function
// to fetch paginated data
return items;
}
}
You could also have a mount method to fetch paginated data using currentPage, this way you could fetch the data when you come back on page.
mounted () {
fetchItems()
}

The issue is when you leave the component it goes back to the ether. When you come back to that component, the page is built again. You should use the lifecycle hook created() to retrieve the saveed page from the store. That way the currentPage variable will have the page set prior to rendering all of the elements. It's probably better to use regular variable currentPage since there isn't a need for it to be a computed function.

You need to maintain a flag to achieve this using store/vuex.
Take a look at the code below. I hope this is something you were looking for
<b-table show-empty
outlined
hover
stacked="md"
:sort-by.sync="sortBy"
:items="companies"
:fields="[
{key: 'name', label: this.$t('shared.name'), sortable: true},
{key: 'companyId', label: this.$t('shared.orgNumber'), sortable: true},
{key: 'properties.state', label: this.$t('shared.status'), sortable: true}
]"
:current-page="currentPage"
:per-page="perPage"
:filter="filter"
v-bind:empty-text='$t("shared.emptyText")'
v-bind:empty-filtered-text='$t("shared.emptyFilteredText")'
#filtered="onFiltered"
#row-clicked="showCompany"
tbody-tr-class="row-cursor">
</b-table>
<b-pagination :total-rows="totalRows" :per-page="perPage" v-model="currentPage"
class="float-right"/>
data() {
return {
vuexStateApplied: false
}
}
methods: {
onFiltered(filteredItems) {
if (!this.vuexStateApplied && !this._.isEmpty(this.$store.getters.currentPage)) {
this.currentPage = this.$store.getters.currentPage
// By setting this flag we are ensuring not to
// apply same currentPage value again on
// filtering the table data
this.vuexStateApplied = true
}
}
}
beforeRouteLeave() {
// update with new value
this.$store.commit('SET_CURRENT_PAGE', value);
}
OR
destoryed() {
// update with new value
this.$store.commit('SET_CURRENT_PAGE', value);
}

Related

How do you remove a subsring when data is loaded into a prop?

I am using bootstrap table to display some error messages that are stored in a database. I fetch them with Axios.
When showing the error messages in the table row, I use a substring to minimize the output to 30 characters, as they can often be over 1000 characters long.
Then I have a modal component that takes in the array as a prop and output the same error message when you click on a specific message in the table.
The problem is that I do not want the modal to show the substring when I click on one of the messages in the table. I would like the message to pop up in the modal WITHOUT the substring while still keeping it in the table, so that the user is able to see the full message when click on the substringed message.
How can I accomplish this?
Parent:
<template>
<b-container>
<b-card class="mt-4">
<h5>{{ $t('events') }}</h5>
<b-table
:items="errors"
:fields="fields"
:per-page="[5, 10]"
selectable
:select-mode="'single'"
#row-selected="onRowSelected"
#row-clicked="showModal"
sort-desc
/>
</b-card>
<error-log-entry-modal ref="errorLogEntryModal" :selected-error-log="selectedRows"/>
</b-container>
</template>
<script>
import {axiosService} from '#/services/error';
import ErrorLogEntryModal from '#/components/error-log/ErrorLogEntryModal';
export default {
components: {
ErrorLogEntryModal,
},
data() {
return {
errors: null,
selectedRows: []
};
},
computed: {
fields() {
return [
{
key: 'errorMessage',
label: this.$t('message'),
sortable: true
},
]
},
},
methods: {
load(){
if
errorService.getErrorLogs().then(result => {
result.data.forEach(log => log.errorMessage = log.errorMessage.substring(0,30));
this.errors = result.data
})
},
onRowSelected(fields){
this.selectedRows = fields
},
showModal(){
if (this.selectedRows) {
this.$refs.errorLogEntryModal.show()
}
},
},
created() {
this.load()
}
};
</script>
child:
<template>
<b-modal
modal-class="error-log-modal"
v-model="showModal"
size="xl"
title="Error Log">
<b-col class="lg-12" v-for="log in selectedErrorLog">
{{ log.errorMessage }}
</b-col>
</b-modal>
</template>
<script>
export default {
props: {
selectedErrorLog: Array
},
data() {
return {
showModal: false,
};
},
methods: {
show(){
this.showModal = true
}
}
};
</script>
Sounds like you want to undo making errorMessage a substring of its initial value. But that is not possible, once data is gone, it is lost.
If you want to show a shortened message in the table, but the full message in the modal, you can just store the shortened message in another property instead of overriding the errorMessage property:
errorService.getErrorLogs().then(result => {
result.data.forEach(log => log.errorMessageShort = log.errorMessage.substring(0,30));
this.errors = result.data
})
Then you can use errorMessageShort in the table and errorMessage in the modal.
You don't need to re-assign the error message after trimming that. You can keep the error message as it is, and at the time of rendering, display only 30 characters but when you will pass this string as a prop, it will be passed as a whole value.
My meaning is-
Remove this code-
log.errorMessage = log.errorMessage.substring(0,30));
and simply keep-
this.errors = result.data
At the time of rendering, show the error message like this-
<div v-for="error in errors">{{ error.substring(0, 30) }}</div>
And when you need to pass the error to the modal component, you can pass it as it is because it didn't trim actually.
<Modal :error="error" />
This approach is moreover like toggling between trimmed and full text. By doing this, you don't need to use multiple variables to store trimmed errors and full errors individually.

Dynamic item template slots within v-data-table with custom components & helpers

Say I have a custom component that uses Vuetify's v-data-table within.
Within this component, there's multiple other custom components such as loaders and specific column-based components for displaying data in a certain way.
I found myself using the same code for filtering, retrieving data, loaders etc. across the project - so not very DRY.
The things that vary are:
API request url to retrieve data from (which I can pass to this generic component)
headers for v-data-table (which I pass to this generic component)
specific item slot templates!
(One file using this same code would need a column modification like the below, requiring different components sometimes too):
<template v-slot:[`item.FullName`]="{ item }">
<router-link class="black--text text-decoration-none" :to="'/users/' + item.Id">
<Avatar :string="item.FullName" />
</router-link>
</template>
Where another would have for example:
<template v-slot:[`item.serial`]="{ item }">
<copy-label :text="item.serial" />
</template>
There are many more unique "column templates" that I use obviously, this is just an example.
modifying items passed to v-data-table in a computed property (to add "actions" or run cleanups and/or modify content before displaying it - not related to actual HTML output, but value itself)
computed: {
items () {
if (!this.data || !this.data.Values) {
return []
}
return this.data.Values.map((item) => {
return {
device: this.$getItemName(item),
serial: item.SerialNumber,
hwVersion: this.$getItemHwVersion(item),
swVersion: this.$getItemSwVersion(item),
actions: [
{ to: '/devices/' + item.Id, text: this.$t('common.open') },
{ to: '/devices/' + item.Id + '/replace', text: this.$t('common.replace') }
],
...item
}
})
}
there are some unique methods that I can use on certain template slot item modifications, such as dateMoreThan24HoursAgo() below:
<template v-slot:[`item.LastLogin`]="{ item }">
<span v-if="dateMoreThan24HoursAgo(item.LastLogin)">{{ item.LastLogin | formatDate }}</span>
<span v-else>
{{ item.LastLogin | formatDateAgo }}
</span>
</template>
I can always make this global or provide them as a prop so this point should not be a big issue.
So my questions are:
What is the best way to use one component with v-data-table within but dynamically pass template slots and also allow item modification prior to passing the array to the v-data-table (as per point 3 and 4 above)
is there a better way to approach this since this seems too complex (should I just keep separate specific files)? It does not feel very DRY, that's why I'm not very fond of the current solution.
Basically I would be happy to have something like:
data: () => {
return {
apiPath: 'devices',
headers: [
{ text: 'Device', align: 'start', value: 'device', sortable: false, class: 'text-none' },
{ text: 'Serial Number', sortable: false, value: 'serial', class: 'text-none' },
{ text: 'Status', value: 'Status', class: 'text-none' },
{ text: 'Calibration', value: 'NextCalibrationDate', class: 'text-none' },
{ text: '', sortable: false, align: 'right', value: 'actions' }
],
itemsModify: (items) => {
return items.map((item) => {
return {
device: this.$getItemName(item),
serial: item.SerialNumber,
actions: [
{ to: '/devices/' + item.Id, text: this.$t('common.open') },
{ to: '/devices/' + item.Id + '/replace', text: this.$t('common.replace') }
],
...item
}
})
},
columnTemplatesPath: '/path/to/vue/file/with/templates'
}
}
And then I'd just call my dynamic component like so:
<GenericTable
:api-path="apiPath"
:headers="headers"
:items-modify="itemsModify"
:column-templates-path="columnTemplatesPath"
/>
Relevant but not exactly a solution to my question:
Is it possible to use dynamic scoped slots to override column values inside <v-data-table>?
Dynamically building a table using vuetifyJS data table

Dynamic form problem in vue2: [TypeError: Cannot read property '_withTask' of undefined]

I have to create a dynamic form in vue2. I want to save the values of the dynamic fields in an named object so that I can pass them along on submit.
The following code is working fine except I get an error in the console when I change the input value the first time (value will be propagated correctly though):
[TypeError: Cannot read property '_withTask' of undefined]
Here is how I define the props:
props: {
fields: {
type: Object,
default: {startWord: 'abc'}
},
},
And this is how I populate the model from the input field:
v-model="fields[field.id]"
Here is the entire code:
<template>
<div>
<!-- Render dynamic form -->
<div v-for="(field, key) in actionStore.currentAction.manifest.input.fields">
<!-- Text -->
<template v-if="field.type == 'string'">
<label>
<span>{{key}} {{field.label}}</span>
<input type="text" v-bind:placeholder="field.placeholder"
v-model="fields[field.id]"/>
</label>
</template>
<!-- Footer -->
<footer class="buttons">
<button uxp-variant="cta" v-on:click="done">Done</button>
</footer>
</div>
</template>
<script>
const Vue = require("vue").default;
const {Bus, Notifications} = require('../../Bus.js');
module.exports = {
props: {
fields: {
type: Object,
default: {startWord: 'abc'}
},
},
computed: {
actionStore() {
return this.$store.state.action;
},
},
methods: {
done() {
console.log('fields', this.fields);
Bus.$emit(Notifications.ACTION_INPUT_DONE, {input: this.fields});
}
},
}
</script>
So again, everything is working just fine (showing initial value in input, propagating the new values to the model etc.). But I get this '_withTask' error when I first enter a new character (literally only on the first keystroke). After that initial error it doesn't pop up again.
-- Appendix --
This is what the manifest/fields look like:
manifest.input = {
fields: [
{ id: 'startWord', type: 'string', label: 'Start word', placeholder: 'Enter start word here...' },
{ id: 'startWordDummy', type: 'string', label: 'Start word dummy', placeholder: 'Enter start word here...' },
{ id: 'wordCount', type: 'integer', label: 'Word count' },
{ id: 'clean', type: 'checkbox', label: 'Clean up before' },
]
}
-- Update --
I just discovered that if I set the dynamic field values initially with static values I don't get the error for those fields set this way:
created() {
this.fields.startWord = 'abc1';
},
But this is not an option since it will be a dynamic list of fields. So what is the best way to handle scenarios like this?
From documentation: Due to the limitations of modern JavaScript (and the abandonment of Object.observe), Vue cannot detect property addition or deletion. Since Vue performs the getter/setter conversion process during instance initialization, a property must be present in the data object in order for Vue to convert it and make it reactive.
As I understand it's bad idea create keys of your object by v-model.
What would I do in HTML:
<input type="text"
:placeholder="field.placeholder"
#input="inputHandler(event, field.id)" />
Then in JS:
methods: {
// ...
inputHandler({ target }, field) {
this.fields[field] = target.value;
}
},

BootstrapVue Table not displaying data

I am trying to use the bootstrapVue table found here
If i use their example exactly the table works great.
<b-table striped hover :items="items"></b-table>
However once i use
<b-table striped hover :fundingInstructions="fundingInstructions"></b-table>
My table does not display and when i look in the dev tools i see that my table has [object,Object] for each object i return from my api.
If i console log my data i see an array [] with multiple objects. How do i get the table to display my data?
const items = [
{ isActive: true, date:'10/20/2018', amount:'$4568.00', city:'FL Palm Beach' },
{ isActive: false, date:'10/21/2018', amount:'$789.23', city:'FL Daytona Beach' },
{ isActive: false, date:'10/21/2018', amount:'$999.99', city:'FL Key West' },
{ isActive: true, date:'10/22/2018', amount:'$589.00', city:'FL Deltona' }
]
export default {
data() {
return {
fundingInstructions : [],
fields: [ 'subMerchantName', 'fundsTransferId', 'amount' ,'accType', 'submitDate' ],
items: items
}
},
methods:{
async GetFundingInstructionInfo(){
this.fundingInstructions = await api.getAllFundingInstructions()
}
Ok, so i figured out what the problem was and hence the solution.
In the table html it should look like this
<b-table striped hover :items="fundingInstructions"></b-table>
It seems that items is a key term and your collection goes in the quotation. That's it!

Initializing checkboxes in vue-bootstrap b-table rows

I am working on a page using vue-bootstrap that has a b-form-checkbox-group embedded in each row. Those are appearing fine. The problem comes when I try to assign values to the checked prop. When i assign a static number the boxes will be checked like they should be when the page loads but when I try to set it up to receive responses from jquery get calls the boxes are only sometimes filled with no pattern as far as I can see.
<nit-card :title="host">
<b-table
:items="interfaceNames"
:fields="switch_interface_config_col_names">
<template slot="tagged" slot-scope="row">
<b-form-checkbox-group :options="vlans"
:checked="interfaceNames[row.index].taggedVlans"
stacked>
</b-form-checkbox-group>
</template>
<template slot="untagged" slot-scope="row">
<b-form-radio-group :options="vlans"
:checked="interfaceNames[row.index].untaggedVlans"
stacked>
</b-form-radio-group>
</template>
</b-table>
</nit-card>
Above is the html for the table. nit-card is a vue.component that looks like this.
Vue.component('nit-card', {
props: ['title'],
template: `
<div class="card">
<div class="card-header">
<h2 class="card-title">{{ title }}</h2>
</div>
<div class="card-body">
<slot></slot>
</div>
</div>
`
})
And some of the js code with an exmaple of ine of the methods that are called to get the data for the checked prop.
let generic_name = new Vue({
el: '#generic_name',
data: function(){
return{
interfaceNames: [],
vlans: [],
switch_interface_config_col_names: [
{key: "interfaceName", sortable: true},
{key: "description", sortable: false},
{key: "tagged", sortable: false},
{key: "untagged", sortable: false},
],
submit_params: [
["Switch", "text", "Hostname or IP", true],
["Interface", "text", "Interface Name", true],
],
host: "",
}
},
methods: {
getTagged: function(){
let that = this
for(let i = 0; i < that.interfaceNames.length; i++){
let url_encoding_re = /\//g;
let interfaceName = that.interfaceNames[i].interfaceName.replace(url_encoding_re, '%5c')
$.get(api_url + "/switches/" + that.host + "/interfaces/" + interfaceName + "/vlans/tagged", function(response) {
console.log(response)
that.interfaceNames[i].taggedVlans = response.vlans
})
}
},
}
The way that I imagine it flowing is that the interfaceNames array stores the data and then it is accessed by the html using the row.index value. What is actually happening is that maybe half of the checkbox forms have the checked values in their checked prop even though when I look at the root vue the values are present in interfaceNames[x].taggedVlans.
What do i need to do to have the checked prop filled consistently for these check-box-groups?
EDIT: By toggling the table with a v-if manually (Ex a button after the table renders) all of the checkboxs will appear correctly. Once I have a reliable way to toggle automatically the table I will post it here.
The issue came from how i was adding the objects to the interfaceNames array. Vue.js cant detect changes made to arrays when you directly set the value through the index. To make the array reactive use Vue.set(this.arrayName, indexValue, new value) (as seen here https://v2.vuejs.org/v2/guide/list.html#Caveats)
For this specific example above Vue.set(that.interfaceNames, i, {key: value, key: value, ...}) worked. Since it was done in a loop for all the values of the array the entire array was made reactive.