How to pass data between vue components - javascript

This is regarding Vuex and passing of data from one component to another. I want the Module variable within CoreMods.vue to be passed to the ExternalWebpage.vue. Or rather, be watched in the external webpage for changes.
My store.js looks like this:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.store({
state:{
CoreModule: ""
},
mutations:{
update: (state, n) => {
state.CoreModule = n;
}
},
getters:{
updated: state =>{
return state.CoreModule;
}
},
actions:{
async createChange({ commit }, n) {
commit("update", n);
}
}
});
I have a CoreMods.vue
methods:{
checkModule() {
if(!this.completed_cm.includes(this.Module)) {
if (this.core.includes(this.Module)) {
this.completed_cm.push(this.Module);
this.$store.dispatch('createChange',this.Module);
}
},
An ExternalWebpage.vue
watch:{
'$store.state.CoreModule': function(){
var cm = this.$store.getters.updated;
if(this.CompletedCore.indexOf(cm) == -1){
this.CompletedCore.push(cm);
}
}
}
I can't make use of props by importing one component in another. This is because:
1) I do not want the entire component placed within the parent component.
2) CoreMod is a component on the home page. Upon clicking a link on the home page, it leads to ExternalWebpage. (This has already been implemented using router)
Currently this code isn't working. Can someone help find a solution/alternative. Additionally, how should I add this part to the main.js?
Thanks!!!

possible solution is to return vuex store value from computed and watch the computed value.
computed: {
coreModule () {
return this.$store.state.CoreModule;
}
},
watch:{
'coreModule': function(){
var cm = this.$store.getters.updated;
if(this.CompletedCore.indexOf(cm) == -1){
this.CompletedCore.push(cm);
}
}
}

Related

Wait for VueX value to load, before loading component

When a user tries to directly navigate load a component url, an http call is made in my vuex actions, which will define a value in my state once it resolves.
I don't want to load my component until the http call is resolved, and the state value is defined.
For Example, in my component
export default {
computed: {
...mapState({
// ** this value needs to load before component mounted() runs **
asyncListValues: state => state.asyncListValues
})
},
mounted () {
// ** I need asyncListValues to be defined before this runs **
this.asyncListValues.forEach((val) => {
// do stuff
});
}
}
How can I make my component wait for asyncListValues to load, before loading my component?
One way to do it is to store state values.
For example, if your store relies on single API, you would do something like this. However, for multiple APIs, it's a good idea to store each api load state individually, or using a dedicated object for each API.
There are usualy 4 states that you can have, which I prefer to have in a globally accessible module:
// enums.js
export default {
INIT: 0,
LOADING: 1,
ERROR: 2,
LOADED: 3
};
Then, you can have the variable stored in the vuex state, where the apiState is initialized with INIT. you can also initialize the array with [], but that shouldn't be necessary.
import ENUM from "#/enums";
// store.js
export default new Vuex.Store({
state: {
apiState: ENUM.INIT,
accounts: [],
// ...other state
},
mutations: {
updateAccounts (state, accounts) {
state.accounts = accounts;
state.apiState = ENUM.LOADED;
},
setApiState (state, apiState) {
state.apiState = apiState;
},
},
actions: {
loadAccounts ({commit) {
commit('setApiState', ENUM.LOADING);
someFetchInterface()
.then(data=>commit('updateAccounts', data))
.catch(err=>commit('setApiState', ENUM.ERROR))
}
}
});
Then, by adding some computed variables, you can toggle which component is shown. The benefit of using state is that you can easily identify the Error state, and show a loading animation when state is not ready.
<template>
<ChildComponent v-if="apiStateLoaded"/>
<Loader v-if="apiStateLoading"/>
<Error v-if="apiStateError"/>
</template>
<script>
import ENUM from "#/enums";
export default {
computed: {
...mapState({
apiState: state=> state.apiState
}),
apiStateLoaded() {
return this.apiState === ENUM.LOADED;
},
apiStateLoading() {
return this.apiState === ENUM.LOADING || this.apiState === ENUM.INIT;
},
apiStateError() {
return this.apiState === ENUM.ERROR;
},
})
}
</script>
aside... I use this pattern to manage my applications as a state machine. While this example utilizes vuex, it can be adapted to use in a component, using Vue.observable (vue2.6+) or ref (vue3).
Alternatively, if you just initialize your asyncListValues in the store with an empty array [], you can avoid errors that expect an array.
Since you mentioned vue-router in your question, you can use beforeRouteEnter which is made to defer the rendering of a component.
For example, if you have a route called "photo":
import Photo from "../page/Photo.vue";
new VueRouter({
mode: "history",
routes: [
{ name: "home", path: "/", component: Home },
{ name: "photo", path: "/photo", component: Photo }
]
});
You can use the beforeRouteEnter like this:
<template>
<div>
Photo rendered here
</div>
</template>
<script>
export default {
beforeRouteEnter: async function(to, from, next) {
try {
await this.$store.dispatch("longRuningHttpCall");
next();
} catch(exception) {
next(exception);
}
}
}
</script>
What it does is, waiting for the action to finish, updating your state like you want, and then the call to next() will tell the router to continue the process (rendering the component inside the <router-view></router-view>).
Tell me if you need an ES6-less example (if you do not use this syntax for example).
You can check the official documentation of beforeRouteEnter on this page, you will also discover you can also put it at the route level using beforeEnter.
One approach would be to split your component into two different components. Your new parent component could handle fetching the data and rendering the child component once the data is ready.
ParentComponent.vue
<template>
<child-component v-if="asyncListValues && asyncListValues.length" :asyncListValues="asyncListValues"/>
<div v-else>Placeholder</div>
</template>
<script>
export default {
computed: {
...mapState({
asyncListValues: state => state.asyncListValues
})
}
}
</script>
ChildComponent.vue
export default {
props: ["asyncListValues"],
mounted () {
this.asyncListValues.forEach((val) => {
// do stuff
});
}
}
Simple way for me:
...
watch: {
vuexvalue(newVal) {
if (newVal == 'XXXX')
this.loadData()
}
}
},
computed: {
...mapGetters(['vuexvalue'])
}
Building on some of the other answers, if you're using Router, you can solve the problem by only calling RouterView when the state has been loaded.
Start with #daniel's approach of setting a stateLoaded flag when the state has been loaded. I'll just keep it simple here with a two-state flag, but you can elaborate as you like:
const store = createStore({
state () {
return {
mysettings: {}, // whatever state you need
stateLoaded: false,
}
},
mutations: {
set_state (state, new_settings) {
state.settings = new_settings;
state.stateLoaded = true;
},
}
}
Then, in app.vue you'll have something like this:
<div class="content">
<RouterView/>
</div>
Change this to:
<div class="content">
<RouterView v-if="this.$store.state.stateLoaded"/>
</div>
The v-if won't even attempt to do anything with RouterView until the (reactive) stateLoaded flag goes true. Therefore, anything you're rendering with the Router won't get called, and so there won't be any undefined state variables in it when it does get loaded.
You can of course build on this with a v-else to perhaps show a "Loading..." screen or something, just in case the state loading takes longer than expected. Using #daniel's multi-state flag, you could even report if there was a problem loading the state, and offer a Retry button or something.

VueJS: Best practice for working with global object between components?

there is User.js class and user object(user = new User();).
The user object is being used in all nested components. in User class there are so many important methods.
How can I simply use/access this.user or this.$user and its methods in any component?
1-solution (temporary working solution): Setting user in vuex's store and define in all components' data:
data(){
return {
user:this.$store.state.user
}
}
Cons: in every component, this should be added. Note: there are so many components.
2-solution: adding user to Vue's prototype like plugin:
Vue.prototype.$user = user
Cons: when user's data changes, it doesn't effect in DOM element (UI).
3-solution: putting to components's props.
Cons: in every component, this should be added. Note: Again there are so many components.
All of the solutions I found have issues, especially as the project gets larger and larger.
Any suggestion and solution will be appreciated!
Note: Applies for Vue 2x
Proposal 1: Using getters from vuex
You could use getters along with mapGetters from Vuex to include users within computed properties for each component.
Vuex
getters: {
// ...
getUser: (state, getters) => {
return getters.user
}
}
component
import { mapGetters } from 'vuex'
computed: {
...mapGetters([getUser])
}
Proposal 2: add a watcher via plugin
Vue
// When using CommonJS via Browserify or Webpack
const Vue = require('vue')
const UserPlug = require('./user-watcher-plugin')
// Don't forget to call this
Vue.use(UserPlug)
user-watcher-plugin.js
const UserPlug = {
install(Vue, options) {
// We call Vue.mixin() here to inject functionality into all components.
Vue.watch: 'user'
}
};
export default UserPlug;
Proposal 3: add a computed property user as plugin via mixin
Vue
// When using CommonJS via Browserify or Webpack
const Vue = require('vue')
const UserPlug = require('./user-watcher-plugin')
// Don't forget to call this
Vue.use(UserPlug)
user-watcher-plugin.js
const UserPlug = {
install(Vue, options) {
// We call Vue.mixin() here to inject functionality into all components.
Vue.mixin({
computed: {
user: function() {
return this.$store.state.user
}
}
})
}
};
export default UserPlug;
Based on #Denis answer, specifically Proposal 3, Here is the UserPlugin.js:
import store from '#/store/store';
import User from './User';
const UserPlugin = {
install(Vue) {
const $user = new User();
window.$user = $user;
store.commit('setUser', $user);
Vue.mixin({
computed: {
$user() {
return store.state.user;
}
}
});
}
};
export default UserPlugin;
and main.js:
import UserPlugin from './common/UserPlugin';
Vue.use(UserPlugin);
new Vue({
render: h => h(App)
}).$mount('#app');
For further usage, I published small library for solving these kinda issues:
https://www.npmjs.com/package/vue-global-var
Assuming you don't actually use all methods/attributes of user in every component, but a subset of them everytime, I don't see any reason why solution 1 & 2 do not work for you, since passing the whole user object to every component is not necessary.
Let's say your object User have some attributes (a1, a2, a3, etc.) and methods (m1, m2, m3...). If a component only needs some of them (e.g. a1, a2, m1, m2, m3) then with Vuex, you can use mapping functions (mapState, mapGetters, mapMutations and mapActions) to get the exact info from user
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
computed: {
...mapState('user', [ 'a1' ]),
...mapGetters('user', [ 'a2' ])
},
methods: {
...mapMutations('user', [ 'm1' ]),
...mapActions('user', [ 'm2', 'm3' ])
}
}
For solution 2 (using prototype), to make component update when user data changes, you can map the necessary data to component via methods.
export default {
methods: {
userA1() {
return this.$user.attributes.a1;
},
userM1() {
this.$user.methods.m1();
}
// and so on
}
}
Even better, you can create mixins to explicitly map data from user, and reuse your mixins to avoid duplicated code in components. It can be applied for both Vuex solution and prototype solution.
// mixin1:
const mixin1 = {
computed: {
...mapState('user', [ 'a1' ]),
},
methods: {
...mapMutations('user', [ 'm1' ])
}
}
// mixin2:
const mixin2 = {
computed: {
...mapGetters('user', [ 'a2' ]),
},
methods: {
...mapActions('user', [ 'm2', 'm3' ])
}
}
// component1
export default {
mixins: [ mixin1 ]
}
// component 2
export default {
mixins: [ mixin1, mixin2 ]
}
But if you really need to pass the whole object user to every component, then nothing could do. Rather, you should review your implementation and see if there is any better way to break the object into smaller meaningful ones.
You can use mixins to add User.js to your root component like
import userLib from './User';
//User.js path should correct
Then
var app = new Vue({
router,
mixins: [
userLib
],
//.....
});
After that you can use any of these User method in your any component like
this.$parent.userClassMehtod();
or if any data access
this.$parent.userClassData;
Finally dont forget to add export default{//..} in User.js
Note: This is only work if you export all method of User.js into export default
I just created the minimal codesandbox to clear the idea of how dependency Injection works in vue.
You can have a second Vue instance and declare a reactive property.
See: Reactivity in depth

Vuex getter to populate a component using v-for

I'm building a vue2 component, with a vuex store object. The component looks like this:
<template>
<ul id="display">
<li v-for="item in sourceData()">
{{item.id}}
</li>
</ul>
</template>
<script>
export default {
mounted: function () {
console.log('mounted')
},
computed: {
sourceData: function() {
return this.$store.getters.visibleSource
}
}
}
</script>
The store is populated via an ajax call at the beginning of the process, in the main javascript entry:
new Vue({
store,
el: '#app',
mounted: function() {
this.$http.get('/map/' + this.source_key + '/' + this.destination_key)
.then(function (response) {
store.commit('populate', response.data)
})
.catch(function (error) {
console.dir(error);
});
}
});
I'm not seeing any errors, and when I use the Vue devtools explorer I can see that my component's sourceData attribute is populated with hundreds of items. I'd expect that once this data is populated, I'd see a bunch of li rows with item.id in them appear on the page.
But despite no errors and apparently good data in the component, I am not seeing the template render anything.
Do I need to use some sort of callback to fire the component after the vuex store is populated?
EDIT: adding store code:
import Vue from 'vue';
import Vuex from 'vuex';
import { getSource, getDestination } from './getters'
Vue.use(Vuex)
export const store = new Vuex.Store({
state: {
field_source: [],
field_destination: []
},
getters: {
visibleSource: state => {
// this just does some formatting
return getSource(state.field_source)
},
visibleDestination: state => {
return getDestination(state.field_destination)
}
},
mutations: {
populate(state, data) {
state.field_source = data.source
state.field_destination = data.destination
}
}
})
EDIT2: Maybe it's not a problem with the v-for-- I don't see anything from the template being rendered, not even the main ul tag, which I'd expect to see (empty) even if there was a problem further in the script.
sourceData is a computed property, not a method. You don't need to invoke it. Don't use it like v-for="item in sourceData()", use it like v-for="item in sourceData".
Other than that, on your 'populate' mutation you are overwritting the observed/reactive objects.
Either use Vue.set():
mutations: {
populate(state, data) {
// was state.field_source = data.source
Vue.set(state, 'field_source', data.source);
// was state.field_destination = data.destination
Vue.set(state, 'field_destination', data.destination);
}
}
Or push all elements to the existing, observed/reactive, arrays:
mutations: {
populate(state, data) {
// was state.field_source = data.source
state.field_source.push(...data.source);
// was state.field_destination = data.destination
state.field_destination.push(...data.destination);
}
}

vuex store doesn't update component

I'm new to vue, so I'm probably making a rookie error.
I have a root vue element - raptor.js:
const Component = {
el: '#app',
store,
data: {
productList: store.state.productlist
},
beforeCreate: function () {
return store.dispatch('getProductList', 'getTrendingBrands');
},
updated: function (){
console.log(111);
startSlider();
}
};
const vm = new Vue(Component);
Using this template
<div class="grid-module-single popular-products" id="app">
<div class="row">
<div class="popular-items-slick col-xs-12">
<div v-for="product in productList">
...
</div>
</div>
</div>
My store is very simple store/index.js:
import Vue from 'vue';
import Vuex from 'vuex';
import model from '../../utilities/model';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
productlist: []
},
mutations: {
setProductList(state, data) {
state.productlist = data;
}
},
actions: {
getProductList({ commit }, action) {
return model.products().then(data => commit('setProductList', data));
}
}
});
In my vuex devtool, I can see, that the store is being updated
https://www.screencast.com/t/UGbw7JyHS3
but my component is not being updated:
https://www.screencast.com/t/KhXQrePEd
Question:
I can see from the devtools, that my code is working. The store is being updated with data. My component is not being updated,however. I thought it was enough just to add this in the data property on the component:
data: {
productList: store.state.productlist
}
but apparently the data object doesn't seem to be automatically synced with the store. So either I'm doing a complete vue no-no somewhere, or I need to tweak the code a bit. Anyway can anyone help me in the right direction.
Thanks a lot.
UPDATE
Figured it out myself. Just had to replace the components data part with a computed method:
data:
data: {
productList: store.state.productlist
}
and replace it with.
computed: {
productList () {
return store.state.productlist;
}
},
data only work once on component before render, so you can use computed instead.
like above answer, or you can use mapstate
import {mapState} from 'vuex'
...
computed: mapState({
productList: state => state.productList
})
First - use getter to do this mapGetters, also you need to watch this property somehow, you can set store subscription or just with watch method trough component.
this.$store.subscribe((mutation, state) => {
if (mutation.type === 'UPDATE_DATA') {
...
}
}
You are calling the store into the productList data property in the wrong way.
You can try it:
data: {
productList: $store.state.productlist
}
Otherwise you have to import store in each component that are using the store.

How to design a store in Vuex to handle clicks in nested, custom components?

I'm trying to design a store to manage the events of my Vuex application. This far, I have the following.
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const state = { dataRows: [], activeDataRow: {} };
const mutations = {
UPDATE_DATA(state, data) { state.dataRows = data; state.activeDataRow = {}; },
};
export default new Vuex.Store({ state, mutations });
I'm going to have a number of list items that are supposed to change the value of the data in the store when clicked. The design of the root component App and the menu bar Navigation is as follows (there will be a bunch of actions in the end so I've collected them in the file actions.js).
<template>
<div id="app">
<navigation></navigation>
</div>
</template>
<script>
import navigation from "./navigation.vue"
export default { components: { navigation } }
</script>
<template>
<div id="nav-bar">
<ul>
<li onclick="console.log('Clickaroo... ');">Plain JS</li>
<li #click="updateData">Action Vuex</li>
</ul>
</div>
</template>
<script>
import { updateData } from "../vuex_app/actions";
export default {
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
}
}
</script>
Clicking on the first list item shows the output in the console. However, when clicking on the second one, there's nothing happening, so I'm pretty sure that the event isn't dispatched at all. I also see following error when the page's being rendered:
Property or method "updateData" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
I'm very new to Vuex so I'm only speculating. Do I need to put in reference to the updateData action in the store, alongside with state and mutations? How do I do that? What/where's the "data option" that the error message talks about? Isn't it my components state and it's properties?
Why the error
You are getting the error, because when you have <li #click="updateData"> in the template, it looks for a method updateData in the vue component which it does not find, so it throws the error. To resolve this, you need to add corresponding methods in the vue component like following:
<script>
import { updateData } from "../vuex_app/actions";
export default {
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
},
methods:{
updateData: () => this.$store.dispatch("updateData")
}
}
</script>
What this.$store.dispatch("updateData") is doing is calling your vuex actions as documented here.
What/where's the "data option"
You don't have any data properties defined, data properties for a vue component can be used, if you want to use that only in that component. If you have data which needs to be accessed across multiple components, you can use vuex state as I believe you are doing.
Following is the way to have data properties for a vue component:
<script>
import { updateData } from "../vuex_app/actions";
export default {
date: {
return {
data1 : 'data 1',
data2 : {
nesteddata: 'data 2'
}
}
}
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
},
methods:{
updateData: () => this.$store.dispatch("updateData")
}
}
</script>
You can use these data properties in the views, have computed properies based on it, or create watchers on it and many more.

Categories

Resources