How to prevent parent component from reloading when changing a parameterised child component in Vue js - vue.js

I have a page where a ClientPortfolio (parent component) containing a list of Securities (child component) are loaded in a v-data-table list.
The issue I have is that ClientPortfolio is fully reloaded every time I click on a security in the list causing the entire list to be refreshed causing scroll and selected class to reset, as well as unncessary performance overhead.
I have looked at the documentation of Vue and nothing seems to point out how to only refresh a child component when it has parameters, it looks like the parent component is being refreshed as the route is changing every time a security is selected, despite expecting that Vue would know that only sub (nested route) is changing hence need to only reload the child component
The closest answer I got was explained on https://github.com/vuejs/vue-router/issues/230 which does not explain in the code how to achieve this.
routes.js:
routes: [
{
path: '/client/:clientno/portfolios/:portfolioNo',
component: ClientPortfolios,
children: [
{ path: 'security/:securityNo', component: Security }
]
},
]
Router link in ClientPortfolios.vue:
<router-link tag="tr" style="cursor:pointer"
:to="`/client/${$route.params.clientno}/portfolios/${selectedPortfolioSequenceNo}/security/${props.item.SecurityNo}-${props.item.SequenceNo}`"
:key="props.item.SecurityNo+props.item.SequenceNo">
</router-link>
Router view (for Security component) in ClientPortfolios.vue:
<v-flex xs10 ml-2>
<v-layout>
<router-view :key="$route.fullPath"></router-view>
</v-layout>
</v-flex>
Any hint on how to prevent parent from getting reloaded is appreciated.
EDIT: Trying to get closer to the issue, I notice that the "Key" attr in ClientPortfolios changes (as shown in the Vue debug window above) whenever I change the Security, could that be the reason? Is there a way to assign a key to ClientPortfolios component although its not a child one? Or a way to not update its key when navigating to different securities?
UPDATE: Full code
ClientPortfolios.vue
<template>
<v-layout row fill-height>
<v-flex xs2>
<v-layout column class="ma-0 pa-0 elevation-1">
<v-flex>
<v-select v-model="selectedPortfolioSequenceNo" :items="clientPortfolios" box label="Portfolio"
item-text="SequenceNo" item-value="SequenceNo" v-on:change="changePortfolio">
</v-select>
</v-flex>
<v-data-table disable-initial-sort :items="securities" item-key="Id" hide-headers hide-actions
style="overflow-y: auto;display:block;height: calc(100vh - 135px);">
<template slot="items" slot-scope="props">
<router-link tag="tr" style="cursor:pointer"
:to="{ name: 'Security', params: { securityNo: props.item.SecurityNo+'-'+props.item.SequenceNo } }"
>
</router-link>
</template>
<template v-slot:no-data>
<v-flex class="text-xs-center">
No securities found
</v-flex>
</template>
</v-data-table>
</v-layout>
</v-flex>
<v-flex xs10 ml-2>
<v-layout>
<keep-alive>
<router-view></router-view>
</keep-alive>
</v-layout>
</v-flex>
</v-layout>
</template>
<script>
import Security from '#/components/Security'
export default {
components: {
security: Security
},
data () {
return {
portfoliosLoading: false,
selectedPortfolioSequenceNo: this.$route.params.portfolioNo,
selectedPortfolio: null,
securityNo: this.$route.params.securityNo
}
},
computed: {
clientPortfolios () {
return this.$store.state.ClientPortfolios
},
securities () {
if (this.clientPortfolios == null || this.clientPortfolios.length < 1) {
return []
}
let self = this
this.selectedPortfolio = global.jQuery.grep(this.clientPortfolios, function (portfolio, i) {
return portfolio.SequenceNo === self.selectedPortfolioSequenceNo
})[0]
return this.selectedPortfolio.Securities
}
},
mounted () {
this.getClientPortfolios()
},
activated () {
},
methods: {
changePortfolio () {
this.$router.push({
path: '/client/' + this.$route.params.clientno + '/portfolios/' + this.selectedPortfolioSequenceNo
})
},
getClientPortfolios: function () {
this.portfoliosLoading = true
let self = this
this.$store.dispatch('getClientPortfolios', {
clientNo: this.$route.params.clientno
}).then(function (serverResponse) {
self.portfoliosLoading = false
})
}
}
}
</script>
Security.vue
<template>
<v-flex>
<v-layout class="screen-header">
<v-flex class="screen-title">Security Details </v-flex>
</v-layout>
<v-divider></v-divider>
<v-layout align-center justify-space-between row class="contents-placeholder" mb-3 pa-2>
<v-layout column>
<v-flex class="form-group" id="security-portfolio-selector">
<label class="screen-label">Sequence</label>
<span class="screen-value">{{security.SequenceNo}}</span>
</v-flex>
<v-flex class="form-group">
<label class="screen-label">Security</label>
<span class="screen-value">{{security.SecurityNo}}-{{security.SequenceNo}}</span>
</v-flex>
<v-flex class="form-group">
<label class="screen-label">Status</label>
<span class="screen-value-code" v-if="security.Status !== ''">{{security.Status}}</span>
</v-flex>
</v-layout>
</v-layout>
</v-flex>
</template>
<script>
export default {
props: ['securityNo'],
data () {
return {
clientNo: this.$route.params.clientno,
securityDetailsLoading: false
}
},
computed: {
security () {
return this.$store.state.SecurityDetails
}
},
created () {
if (this.securityNo.length > 1) {
this.getSecurityDetails()
}
},
methods: {
getSecurityDetails: function () {
let self = this
this.securityDetailsLoading = true
this.$store.dispatch('getSecurityDetails', {
securityNo: this.securityNo,
clientNo: this.clientNo
}).then(function (serverResponse) {
self.securityDetailsLoading = false
})
}
}
}
</script>
router.js
const router = new Router({
mode: 'history',
routes: [
{
path: '/',
component: Dashboard
},
{
path: '/client/:clientno/details',
component: Client,
props: true
},
{
path: '/client/:clientno/portfolios/:portfolioNo',
component: ClientPortfolios,
name: 'ClientPortfolios',
children: [
{ path: 'security/:securityNo',
component: Security,
name: 'Security'
}
]
}
]
})
UPDATE:
Just to update this as it’s been a while, I finally got to find out what the problem is, which is what #matpie indicated elsewhere, I have found out that my App.vue is the culprit where there is a :key add to the very root of the application: <router-view :key="$route.fullPath" /> this was a template I used from somewhere but never had to look at as it was "working", after removing the key, all is working as it should, marking matpie answer accepted.

Preventing component reload is the default behavior in Vue.js. Vue's reactivity system automatically maps property dependencies and only performs the minimal amount of work to ensure the DOM is current.
By using a :key attribute anywhere, you are telling Vue.js that this element or component should only match when the keys match. If the keys don't match, the old one will be destroyed and a new one created.
It looks like you're also pulling in route parameters on the data object (Security.vue). Those will not update when the route parameters change, you should pull them in to a computed property so that they will always stay up-to-date.
export default {
computed: {
clientNo: (vm) => vm.$route.params.clientno,
}
}
That will ensure that clientNo always matches what is found in the router, regardless of whether Vue decides to re-use this component instance. If you need to perform other side-effects when clientNo changes, you can add a watcher:
vm.$watch("clientNo", (clientNo) => { /* ... */ })

Could you please check again after removing the local registration of the security component? As it's not needed because this is being handled by the vue router itself.
components: { // delete this code
security: Security
},

Instead of using router here. Declare two variable at root level for selected security and portfolio,
list the securities based on the selected portfolio.
on selecting a security from displayed securities, update the root variable using,
this.$root.selectedSecurityId = id;
you can have watch at security component level.
In root,
<security selectedid="selectedSecurityId" />
In component security,
....
watch:{
selectedid:function(){
//fetch info and show
}
}
...
the components will be look like following,
<portfolio>
//active. list goes here
</portfolio>
........
<security selectedid="selectedSecurityId">
//info goes here
</security>
Above approach will help to avoid routers. hope this will help.

I had a similar issue once. IMO it was caused by path string parsing.
Try to set a name for your route. And replace your router-link to param with an object.
And remove router-view :key prop. It doesn't need to be there. It is used to force component update when a route changes. It is usually a sign of bad code. Your component (Security) should react to route params update. Not the parent component force it to.
So, try to change your code to:
routes: [
{
path: '/client/:clientno/portfolios/:portfolioNo',
component: ClientPortfolios,
name: "ClientPortfoliosName", // it can be anything you want. It`s just an alias for internal use.
children: [
{
path: 'security/:securityNo',
name: "PortfolioSecurities", // anyway, consider setting route names as good practice
component: Security
}
]
},
]
<router-link tag="tr" style="cursor:pointer"
:to="{ name: 'PortfolioSecurities', params: { clientno: $route.params.clientno, portfolioNo: selectedPortfolioSequenceNo, securityNo: props.item.SecurityNo+'-'+props.item.SequenceNo } }"
:key="props.item.SecurityNo+props.item.SequenceNo">
</router-link>
And it should work.
P.S. In your router-link you shall point to the route you want to navigate to. In this case PortfolioSecurities

Related

Make Vuetify v-text-field component required by default

Currently, you can set rules that will work once the user changes the value of the input. For example:
Template part
<v-text-field
v-model="title"
label="Title"
></v-text-field>
Logic
export default {
data () {
return {
title: '',
email: '',
rules: {
required: value => !!value || 'Required.'
},
}
}
}
When the user focuses and removes focus from that element, or when the user deletes all its content, the required rule is triggered.
But what happens if we want to start with the rule enabled as soon as the component is mounted or created? Is there a way to achieve this?
I searched around vuetify but I could not find info about this nor examples in my humble google searches. I will appreciate help. I'm new in vue world. Thanks.
You could do the following:
Create a v-form and place your textfields inside the form. Don't forget to give your v-form a v-model and a ref too.
On mounted you can access the v-form via this.$refs and call .validate() just as Jesper described in his answer. In the codesandbox below you can see that the textfields immediately go red and display the "Required." text.
<v-form v-model="formValid" ref="myForm">
<v-text-field label="Field 1" :rules="rules.required"></v-text-field>
<v-text-field label="Field 2" :rules="rules.required"></v-text-field>
</v-form>
<script>
export default {
data() {
return {
formValid: false,
rules: {
required: [value => !!value || "Required."]
}
};
},
mounted() {
this.$refs.myForm.validate();
}
};
</script>
Example:
You should change your validation a little bit to achieve this.
<ValidationProvider rules="required" v-slot="{ errors }" ref="title">
<v-text-field
v-model="title"
label="Title"
></v-text-field>
</ValidationProvider>
And then you should call this.$refs.title.validate()
If you trigger this when mounted() is called, it should validate all the fields right away, as you're requesting.

How to use another method's variable in a vue component?

I have two methods in a vue component.
First makes the user choose from a v-select, either itemone or itemtwo. Then, to retreive the value for later i call #change to assign the variable to a method declared later - getItemValue.
Second is a submit button, when clicked, we go to handleSubmit.
After handleSubmit is called, I want to use the value I got from getItemValue (in variable theItem), but how can I call another method if it's out of my scope?
Mycomponent.vue
<template>
<v-form
ref="form"
v-model="valid"
lazy-validation
>
<v-select
v-model="select"
:items="items"
#change="getItemValue"
></v-select>
<v-btn
#click="handleSubmit"
>
Submit
</v-btn>
</v-form>
</template>
<script>
export default {
data: () => ({
items: [
'itemone',
'itemtwo'
],
}),
methods: {
getItemValue(theItem) {
},
handleSubmit(e) {
e.preventDefault()
// i need "theItem" here!
}
},
}
</script>
v-model already writes to your local variable, so there is absolutely no need to setup a get method to write the select value to a variable.
Actually, v-model is a bit more complicated than just 'write' to a variable, but the important bit is that in your template you are setting up v-model="select", which basically means that whenever the user uses the select to pick a value, your local select variable will be updated with the selected value.
Now, there is no select in your example component data, I don't know why. But if you had it, you could just sent that variable in your handleSubmit:
<template>
<v-form
ref="form"
v-model="valid"
lazy-validation
>
<v-select
v-model="select"
:items="items"
></v-select>
<v-btn
#click="handleSubmit"
>
Submit
</v-btn>
</v-form>
</template>
<script>
export default {
data: () => ({
select: '',
items: [
'itemone',
'itemtwo'
],
}),
methods: {
handleSubmit(e) {
e.preventDefault()
doSomethingWith(this.select); // this will be updated at this point
// with the option the user selected
}
},
}
</script>
Now, however, be aware that if the select variable is a component prop, then you should not do this right away, since props are not intended to be modified directly by child components. If that would be the case, please update your question with more info.
You would simple set the variable (theItem) value to the data
getItemValue(theItem) {
this.theItem;
},
and then retrieve it later
handleSubmit(e) {
e.preventDefault()
// i need "theItem" here!
// simple access theItem
console.log('theItem', this.theItem);
}

How to store state of tabs based on route?

Is it possible to store different state of a <v-tab> component based on the route i'm currently on? What i'm basically trying to achieve is, have a single component like the code below that i use in several different routes, but have it remember the tab i'm currently at depending on the route.
<v-layout justify-space-around row>
<v-tab v-for="tab in navbarTabs" :key="tab.id">{{ tab.name }}</v-tab>
</v-layout>
data() {
return {
navbarTabs: [
{
id: 0,
name: "Home"
},
{
id: 1,
name: "Schedule"
},
{
id: 2,
name: "Tech card"
},
{
id: 3,
name: "Specifications"
},
{
id: 4,
name: "Control card"
},
{
id: 5,
name: "Packing"
}
]
}
}
I previously managed to achieve the desired result by having a few identical <v-tab> components that i visualised conditionally based on current route with v-if, but that solutions seems a bit crude and not scalable at all.
Can someone give me some pointers on how to achieve that? Thanks in advance!
The v-tabs component has a value prop which you can use to open a tab with a given key. And the v-tab component has a to prop which you can use to set the route of a tab. So essentially, you can use Vue Router params to select a tab and you can set the route using the v-tab's to prop.
You can define your route like this to pass the selectedTab param to the component:
routes: [
{ path: '/tabs/:selectedTab', component: TabsComponent, props: true },
]
And your TabsComponent can look something like this:
<template>
<v-layout justify-space-around row>
<v-tabs v-model="selectedTab">
<v-tab
v-for="tab in navbarTabs"
:key="tab.id"
:to=`/tabs/${tab.id}`
>{{ tab.name }}</v-tab>
</v-layout>
</template>
<script>
export default {
props: ['selectedTab'],
}
</script>

Vuetify v-text-field Default Slot Not Working

I am trying to use a custom filter with the Vuetify v-text-field control. I am having trouble getting a value to show using the default slot of the v-text-field control. It is apparently derived from v-input, which seems to work fine.
This does not work:
<v-text-field>
{{ purchasePrice | currency }}
</v-text-field>
This works:
<v-input>
{{ purchasePrice | currency }}
</v-input>
Am I missing a template slot or something? I've been able to successfully use the "append" and "prepend" slots on this control, but not the "default" slot. Any suggestions?
Thanks.
I just ran into this as well, and did some source diving. Documenting my findings below:
As of Vuetify 2.5.8 (most recent version), and any other 2+ version, default slot is ignored on v-text-element.
The relevant part in the source code of VTextField.ts:
genDefaultSlot () {
return [
this.genFieldset(),
this.genTextFieldSlot(),
this.genClearIcon(),
this.genIconSlot(),
this.genProgress(),
]
},
it overrides genDefaultSlot method of VInput.ts, which is included as a mixin in VTextField.ts:
genDefaultSlot () {
return [
this.genLabel(),
this.$slots.default,
]
},
I could only make it work with named slots: (Also, I'm reusing this component, so it accepts another slot on the inside)
<template>
<v-layout>
<v-text-field
:type="type"
v-bind="
// https://vuejs.org/v2/guide/components-props.html#Disabling-Attribute-Inheritance
$attrs
"
#input="$emit('update', $event)"
v-on="
// https://vuejs.org/v2/guide/components-custom-events.html#Binding-Native-Events-to-Components
$listeners
"
>
<!-- ⬇️ HERE ⬇️ -->
<template v-slot:label>
<slot></slot>
</template>
</v-text-field>
</v-layout>
</template>
<script>
import { defaultMaterialTextFiledsProps } from '~/config/inputsStyle'
// See https://github.com/chrisvfritz/vue-enterprise-boilerplate/blob/master/src/components/_base-input-text.vue
export default {
// Disable automatic attribute inheritance, so that $attrs are
// passed to the <input>, even if it's not the root element.
// https://vuejs.org/v2/guide/components-props.html#Disabling-Attribute-Inheritance
inheritAttrs: false,
props: {
type: {
type: String,
default: 'text',
// Only allow types that essentially just render text boxes.
validator(value) {
return [
'email',
'number',
'password',
'search',
'tel',
'text',
'url'
].includes(value)
}
}
}
}
</script>

Why is the this.$on() callback triggered upon mount of a component?

I have a component which $emit to its parent upon some activity.
The parent listens to this event and triggers a function upon reception. This is assembled in mount():
<template>
<v-app>
<v-toolbar fixed app>
<v-toolbar-title v-text="title"></v-toolbar-title>
<v-spacer></v-spacer>
<v-btn color="error" dark large>Large Button</v-btn>
</v-toolbar>
<v-content>
<new-case v-on:dirty="updateDirty"></new-case>
</v-content>
<v-footer app>
<span>© 2017</span> dirty: {{dirty}}
</v-footer>
</v-app>
</template>
<script>
export default {
data() {
return {
case: {
id: null,
title: null,
},
cases: [],
title: 'Vuetify.js',
dirty: false,
}
},
watch: {
dirty: () => {
console.log('requesting relaod of data')
}
},
methods: {
updateDirty(what) {
console.log('requesting update of dirty state '+what)
this.dirty = what
}
},
mounted() {
this.$on('dirty', this.updateDirty())
}
}
</script>
The whole mechanism works fine (the emmited even is correctly handled by the parent), except that when the component is mounted, I see in the console
17:36:01.380 App.vue?ea99:36 requesting update of dirty state undefined
17:36:01.449 App.vue?ea99:31 requesting relaod of data
17:36:01.561 backend.js:1 vue-devtools Detected Vue v2.5.13
Why is this.updateDirty() triggered upon the mount of the component? (even though nothing was emitted yet - not only the component which would emit something is not used yet, but the DevTools Vue panel does not show any events yet)
The issue is with you $on call itself. The parenthesis after updateDirty, this.$on('dirty', this.updateDirty()), is the culprit, it is telling JS to run the function and store the result as the event handler. Try this.$on('dirty', this.updateDirty) instead so you're passing the reference to the function not the result.