In my application I want to show a modal to introduce the user in my application, so it will appear only in the first time he logs in. What I am doing is storing isNewUser in the global state and using it to know if it should render the modal or not using the same process described in this answer. (I'm not using event bus)
Here is my parent component:
<template>
<Intro :value="isNewUser" #input="finishTutorial" />
</template>
mounted() {
const store = this.$store;
this.isNewUser = store.state.auth.user.isNewUser;
},
When the user logs in and this component is rendered I saw the dialog being rendered and closing. If I hit f5 it reloads the page and dialog is showed correctly.
If I do the bellow modification it works, but I don't want to solve the problem this way since it won't work for all cases, it will depend on the speed of the user computer/internet.
mounted() {
setTimeout(() => {
const store = this.$store;
this.isNewUser = store.state.auth.user.isNewUser;
}, 2000);
},
I've tried using v-if as well
<template>
<Intro v-if="isNewUser" :value="true" #input="finishTutorial" />
</template>
<script>
export default {
components: {
Intro,
},
data() {
return {
isNewUser: false,
};
},
mounted() {
const store = this.$store;
this.isNewUser = store.state.auth.user.isNewUser;
},
methods: {
async finishTutorial() {
this.$store.dispatch('auth/finishTutorial');
this.isNewUser = false;
},
},
};
</script>
You can use a computed property to do so:
computed: {
isNewUser() {
return this.$store.state.auth.user.isNewUser;
}
}
and in the template you would do like so:
<template>
<Intro :value="isNewUser" #input="finishTutorial" />
</template>
Related
I'm looking for a reusable way to display a full page loader (Sidebar always visible but the loader should cover the content part of the page) till all necessary api fetches has been done.
I've got a parent component LaunchDetails wrapped in a PageLoader component
LaunchDetails.vue
<template>
<PageLoader>
<router-link :to="{ name: 'launches' }"> Back to launches </router-link>
<h1>{{ name }}</h1>
<section>
<TabMenu :links="menuLinks" />
</section>
<section>
<router-view />
</section>
</PageLoader>
</template>
<script>
import TabMenu from "#/components/general/TabMenu";
export default {
data() {
return {
menuLinks: [
{ to: { name: "launchOverview" }, display_name: "Overview" },
{ to: { name: "launchRocket" }, display_name: "Rocket" },
],
};
},
components: {
TabMenu,
},
created() {
this.$store.dispatch("launches/fetchLaunch", this.$route.params.launch_id);
},
computed: {
name() {
return this.$store.getters["launches/name"];
},
},
};
</script>
PageLoader.vue
<template>
<Spinner v-if="isLoading" full size="medium" />
<slot v-else></slot>
</template>
<script>
import Spinner from "#/components/general/Spinner.vue";
export default {
components: {
Spinner,
},
computed: {
isLoading() {
return this.$store.getters["loader/isLoading"];
},
},
};
</script>
The LaunchDetails template has another router-view. In these child pages new fetch requests are made based on data from the LaunchDetails requests.
RocketDetails.vue
<template>
<PageLoader>
<h2>Launch rocket details</h2>
<RocketCard v-if="rocket" :rocket="rocket" />
</PageLoader>
</template>
<script>
import LaunchService from "#/services/LaunchService";
import RocketCard from "#/components/rocket/RocketCard.vue";
export default {
components: {
RocketCard,
},
mounted() {
this.loadRocket();
},
data() {
return {
rocket: null,
};
},
methods: {
async loadRocket() {
const rocket_id = this.$store.getters["launches/getRocketId"];
if (rocket_id) {
const response = await LaunchService.getRocket(rocket_id);
this.rocket = response.data;
}
},
},
};
</script>
What I need is a way to fetch data in the parent component (LaunchDetails). If this data is stored in the vuex store, the child component (LaunchRocket) is getting the necessary store data and executes the fetch requests. While this is done I would like to have a full page loader or a full page loader while the parent component is loading and a loader containing the nested canvas.
At this point the vuex store is keeping track of an isLoading property, handled with axios interceptors.
All code is visible in this sandbox
(Note: In this example I could get the rocket_id from the url but this will not be the case in my project so I'm really looking for a way to get this data from the vuex store)
Im introduce your savior Suspense, this feature has been added in vue v3 but still is an experimental feature. Basically how its work you create one suspense in parent component and you can show a loading when all component in any depth of your application is resolved. Note that your components should be an async component means that it should either lazily loaded or made your setup function (composition api) an async function so it will return an async component, with this way you can fetch you data in child component and in parent show a fallback if necessary.
More info: https://vuejs.org/guide/built-ins/suspense.html#suspense
You could use Events:
var Child = Vue.component('child', {
data() {
return {
isLoading: true
}
},
template: `<div>
<span v-if="isLoading">Loading …</span>
<span v-else>Child</span>
</div>`,
created() {
this.$parent.$on('loaded', this.setLoaded);
},
methods: {
setLoaded() {
this.isLoading = false
}
}
});
var Parent = Vue.component('parent', {
components: { Child },
data() {
return {
isLoading: true
}
},
template: `<div>
Parent
<Child />
</div>`,
mounted() {
let request1 = new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
let request2 = new Promise((resolve, reject) => {
setTimeout(resolve, 2000);
});
Promise.all([ request1, request2 ]).then(() => this.$emit('loaded'))
}
});
new Vue({
components: { Parent },
el: '#app',
template: `<Parent />`
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
This may be considered an anti-pattern since it couples the parent with the child and events are considered to be sent the other way round. If you don't want to use events for that, a watched property works just fine, too. The non-parent-child event emitting was removed in Vue 3 but can be implemented using external libraries.
I have a 'client-display' component containing a list of clients that I get in my store via mapGetter. I use 'v-for' over the list to display all of them in vuetify 'v-expansion-panels', thus one client = one panel. In the header of those panels, I have a 'edit-delete' component with the client passed to as a prop. This 'edit-delete' basically just emits 'edit' or 'delete' events when clicked on the corresponding icon with the client for payload. When I click on the edit icon, the edit event is then catched in my 'client-display' so I can assign the client to a variable called 'client' (sorry I know it's confusing a bit). I pass this variable to my dialog as a prop and I use this dialog to edit the client.
So the probleme is : When I edit a client, it does edit properly, but if I click on 'cancel', I find no way to revert what happened in the UI. I tried keeping an object with the old values and reset it on a cancel event, but no matter what happens, even the reference values that I try to keep in the object change, and this is what is the most surprising to me. I tried many things for this, such as initiating a new object and assigning the values manually or using Object.assign(). I tried a lot of different ways to 'unbind' all of this, nothing worked out. I'd like to be able to wait for the changes to be commited in the store before it's visible in the UI, or to be able to have a reference object to reset the values on a 'cancel' event.
Here are the relevant parts of the code (I stripped a lot of stuff to try and make it easier to read, but I think everything needed is there):
Client module for my store
I think this part works fine because I get the clients properly, though maybe something is binded and it should not
const state = {
clients: null,
};
const getters = {
[types.CLIENTS] : state => {
return state.clients;
},
};
const mutations = {
[types.MUTATE_LOAD]: (state, clients) => {
state.clients = clients;
},
};
const actions = {
[types.FETCH]: ({commit}) => {
clientsCollection.get()
.then((querySnapshot) => {
let clients = querySnapshot.docs.map(doc => doc.data());
commit(types.MUTATE_LOAD, clients)
}).catch((e) => {
//...
});
},
}
export default {
state,
getters,
mutations,
...
}
ClientsDisplay component
<template>
<div>
<div>
<v-expansion-panels>
<v-expansion-panel
v-for="c in clientsDisplayed"
:key="c.name"
>
<v-expansion-panel-header>
<div>
<h2>{{ c.name }}</h2>
<edit-delete
:element="c"
#edit="handleEdit"
#delete="handleDelete"
/>
</div>
</v-expansion-panel-header>
<v-expansion-panel-content>
//the client holder displays the client's info
<client-holder
:client="c"
/>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</div>
<client-add-dialog
v-model="clientPopup"
:client="client"
#cancelEdit="handleCancel"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import * as clientsTypes from '../../../../store/modules/clients/types';
import ClientDialog from './ClientDialog';
import EditDelete from '../../EditDelete';
import ClientHolder from './ClientHolder';
import icons from '../../../../constants/icons';
export default {
name: 'ClientsDisplay',
components: {
ClientHolder,
ClientAddDialog,
EditDelete,
},
data() {
return {
icons,
clientPopup: false,
selectedClient: null,
client: null,
vueInstance: this,
}
},
created() {
this.fetchClients();
},
methods: {
...mapGetters({
'stateClients': clientsTypes.CLIENTS,
}),
...mapActions({
//this loads my clients in my state for the first time if needed
'fetchClients': clientsTypes.FETCH,
}),
handleEdit(client) {
this.client = client;
this.clientPopup = true;
},
handleCancel(payload) {
//payload.uneditedClient, as defined in the dialog, has been applied the changes
},
},
computed: {
isMobile,
clientsDisplayed() {
return this.stateClients();
},
}
}
</script>
EditDelete component
<template>
<div>
<v-icon
#click.stop="$emit('edit', element)"
>edit</v-icon>
<v-icon
#click.stop="$emit('delete', element)"
>delete</v-icon>
</div>
</template>
<script>
export default {
name: 'EditDelete',
props: ['element']
}
</script>
ClientDialog component
Something to note here : the headerTitle stays the same, even though the client name changes.
<template>
<v-dialog
v-model="value"
>
<v-card>
<v-card-title
primary-title
>
{{ headerTitle }}
</v-card-title>
<v-form
ref="form"
>
<v-text-field
label="Client name"
v-model="clientName"
/>
<address-fields
v-model="clientAddress"
/>
</v-form>
<v-card-actions>
<v-btn
#click="handleCancel"
text
>Annuler</v-btn>
<v-btn
text
#click="submit"
>Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import AddressFields from '../../AddressFields';
export default {
name: 'ClientDialog',
props: ['value', 'client'],
components: {
AddressFields,
},
data() {
return {
colors,
clientName: '',
clientAddress: { province: 'QC', country: 'Canada' },
clientNote: '',
uneditedClient: {},
}
},
methods: {
closeDialog() {
this.$emit('input', false);
},
handleCancel() {
this.$emit('cancelEdit', { uneditedClient: this.uneditedClient, editedClient: this.client})
this.closeDialog();
},
},
computed: {
headerTitle() {
return this.client.name
}
},
watch: {
value: function(val) {
// I watch there so I can reset the client whenever I open de dialog
if(val) {
// Here I try to keep an object with the value of this.client before I edit it
// but it doesn't seem to work as I intend
Object.assign(this.uneditedClient, this.client);
this.clientName = this.client.name;
this.clientContacts = this.client.contacts;
this.clientAddress = this.client.address;
this.clientNote = '';
}
}
}
}
</script>
To keep an independent copy of the data, you'll want to perform a deep copy of the object using something like klona. Using Object.assign is a shallow copy and doesn't protect against reference value changes.
Scenario / context
I have an overview component which contains a table and an add button. The add button opens a modal component. When i fill in some text fields in the modal and click the save button, a callback (given as prop) is called so the parent component (the overview) is updated. The save button also triggers the model toggle function so the model closes.
So far works everything like expected but when i want to add a second entry, the modal is "pre-filled" with the data of the recently added item.
Its clear to me that this happens because the model component keeps mounted in the background (so its just hidden). I could solve this by "reset" the modals data when the toggle function is triggered but i think there should be a better way.
I have a similar issue when i want to fetch data in a modal. Currently i call the fetch function in the mounted hook of the modal. So in this case the fetch happens when the parent component mounts the modal. This does not make sense as it should only (and each time) fetch when the modal is opened.
I think the nicest way to solve this is to mount the modal component dynamically when i click the "add" (open modal) button but i can't find how i can achieve this. This also avoids that a lot of components are mounted in the background which are possibly not used.
Screenshot
Example code
Overview:
<template>
<div>
// mount of my modal component
<example-modal
:toggleConstant = modalToggleUuid
:submitHandler = submitHandler />
// The overview component HTML is here
</div>
</template>
<script>
export default {
data() {
return {
modalToggleUuid: someUuid,
someList: [],
}
},
mounted() {
},
methods: {
showModal: function() {
EventBus.$emit(this.modalToggleUuid);
},
submitHandler: function(item) {
this.someList.push(item);
}
}
}
</script>
Modal:
<template>
<div>
<input v-model="item.type">
<input v-model="item.name">
<input v-model="item.location">
</div>
</template>
<script>
export default {
data() {
return {
modalToggleUuid: someUuid,
item: {},
}
},
mounted() {
// in some cases i fetch something here. The data should be fetched each time the modal is opened
},
methods: {
showModal: function() {
EventBus.$emit(this.modalToggleUuid);
},
submitHandler: function(item) {
this.someList.push(item);
}
}
}
</script>
Question
What is the best practive to deal with the above described scenario?
Should i mount the modal component dynamically?
Do i mount the component correctly and should i reset the content all the time?
You are on the right way and in order to achieve what you want, you can approach this issue with v-if solution like this - then mounted() hook will run every time when you toggle modal and it also will not be present in DOM when you are not using it.
<template>
<div>
// mount of my modal component
<example-modal
v-if="isShowModal"
:toggleConstant="modalToggleUuid"
:submitHandler="submitHandler"
/>
// The overview component HTML is here
</div>
</template>
<script>
export default {
data() {
return {
isShowModal: false,
modalToggleUuid: someUuid,
someList: []
};
},
mounted() {},
methods: {
showModal: function() {
this.isShowModal = true;
},
submitHandler: function(item) {
this.someList.push(item);
this.isShowModal = false;
}
}
};
</script>
Destroyed hook is called later than i need.
I tried to use beforeDestroy instead of destroy, mounted hook instead of created. The destroy hook of previous components is always called after the created hook of the components that replaces it.
App.vue
<div id="app">
<component :is="currentComponent"></component>
<button #click="toggleComponent">Toggle component</button>
</div>
</template>
<script>
import A from './components/A.vue';
import B from './components/B.vue';
export default {
components: {
A,
B
},
data: function(){
return {
currentComponent: 'A'
}
},
methods: {
toggleComponent() {
this.currentComponent = this.currentComponent === 'A' ? 'B' : 'A';
}
}
}
</script>
A.vue
<script>
export default {
created: function() {
shortcut.add('Enter', () => {
console.log('Enter pressed from A');
})
},
destroyed: function() {
shortcut.remove('Enter');
}
}
</script>
B.vue
<script>
export default {
created: function() {
shortcut.add('Enter', () => {
console.log('Enter pressed from B');
})
},
destroyed: function() {
shortcut.remove('Enter');
}
}
</script>
Result:
// Click Enter
Enter pressed from A
// now click on toggle component button
// Click Enter again
Enter pressed from A
Expected after the second enter to show me Enter pressed from B.
Please don't show me diagrams with vue's lifecycle, i'm already aware of that, I just need the workaround for this specific case.
Dumb answers like use setTimeout are not accepted.
EDIT: Made some changes to code and description
If you are using vue-router you can use router guards in the component (as well as in the router file) where you have beforeRouteLeave obviously only works where there is a change in route, see here:
https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards
Description
I'm trying to take advantage of the keep-alive functionality of vue-js 2.3 so my AJAX call is made only once.
Problem
The second time I try to open the popup component I get this error :
Error in nextTick: "TypeError: Cannot read property 'insert' of undefined"
TypeError: Cannot read property 'insert' of undefined
Steps
Click on the button to display the popup
Wait for one second
Close the popup
Click again on the button
https://jsfiddle.net/4fwphqhv/
Minimal reproduction example
<div id="app">
<button #click="showDialog = true">Show Component PopUp</button>
<keep-alive>
<popup v-if="showDialog" :show-dialog.sync="showDialog"></popup>
</keep-alive>
</div>
<template id="popup">
<el-dialog :visible.sync="show" #visible-change="updateShowDialog">{{asyncData}}</el-dialog>
</template>
Vue.component('popup', {
template: '#popup',
props : ['showDialog'],
data(){
return {
show: this.showDialog,
asyncData: "Loading please wait"
}
},
methods: {
updateShowDialog(isVisible) {
if (isVisible) return false;
this.$emit('update:showDialog', false )
}
},
created:function (){
const _this = this
setTimeout(() => _this.asyncData = 'Async Data was loaded' , 1000)
},
});
var vm = new Vue({
el: '#app',
data: {
showDialog: false,
},
});
Real code of the popup component
<template>
<el-dialog title="Order in progress" size="large" :visible.sync="show" #visible-change="updateShowLoadOrder"></el-dialog>
</template>
<script>
let popUpData;
export default {
name: '',
data () {
return {
ordersInProgress: [],
show: this.showLoadOrder
}
},
props: ['showLoadOrder'],
methods: {
updateShowLoadOrder (isVisible) {
if (isVisible) return false;
this.$emit('update:showLoadOrder', false)
}
},
created () {
const _this = this;
if (!popUpData) {
axios.get('api/mtm/apiGetOrdersInProgress').then((response) => {
_this.ordersInProgress = popUpData = response.data;
});
} else {
this.ordersInProgress = popUpData;
}
}
}
</script>
Ok. So your problem here is the wrong life-cycle hook.
If you change created to activated... it should work. It did for me in your JS fiddle.
activated:function (){
setTimeout(() => this.asyncData = 'Async Data was loaded' , 1000)
}
There are two other hooks, activated and deactivated. These are for keep-alive components, a topic that is outside the scope of this article. Suffice it to say that they allow you to detect when a component that is wrapped in a tag is toggled on or off. You might use them to fetch data for your component or handle state changes, effectively behaving as created and beforeDestroy without the need to do a full component rebuild.
SOURCE: here