I am trying to modify a moment.js instance that resides in vue.js computed property like so.
computed: {
currentDate() {
return moment();
}
}
Whenever I try to call it with a method like this one, nothing happens:
methods: {
prevMonth() {
this.currentDate = moment(this.currentDate).subtract(1, 'months');
}
}
I am guessing this is because computed properties only allow to act as getters(and optionally setters). How can I change this behavior though?
I oversimplified the example since I am using the cmoputed property to fetch data from my vuex store. I am no longer able to manipulate it though.
Is there some way I can populate a local currentDate property with the vuex store's value so I can still manipulate it and add months, etc?
I have though about using the mounted property for this but I only mount my component once. Any help is welcome.
If your currentDate property belongs to your Vuex store, you shouldn't be manipulating it inside your components. You should instead: 1) map the getter locally as a computed property and 2) map the mutation locally as a method.
Here's an example of what your date Vuex module might look like:
export default {
state: {
currentDate: moment()
},
mutations: {
subtractMonth (state, date) {
state.currentDate = moment(state.currentDate).subtract(1, 'months');
}
},
getters: {
getCurrentDate: (state) => {
return state.currentDate
}
}
}
And this is how the component would make use of it, without actually doing anything "locally":
import { mapGetters, mapMutations } from 'vuex'
export default {
computed: {
...mapGetters({
currentDate: 'getCurrentDate'
})
},
methods: {
...mapMutations({
prevMonth: 'subtractMonth'
})
}
}
You'd still be able to bind currentDate inside your component and call prevMonth as before, but now everything is being done via the Vuex single state.
Computed properties are not methods, that you can call. If you want to have such method, move currentDate to methods. The you also can invoke it from mounted.
Related
I use vuex in my Vue 2 project.
I have this HTML element and I try to implement two way binding:
<input v-model="message">
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}
inside get and set I want to use mappers so the code will look cleaner:
computed: {
message: {
get () {
return ...mapState("obj", ["message"])
},
set (value) {
...mapMutations("obj/updateMessage", value)
}
}
}
But I get errors on two rows:
return ...mapState("obj", ["message"]) - Expression expected.
...mapMutations("obj/updateMessage", value) - Declaration or statement expected.
How can I use mappers inside get and set?
UPDATE:
mapMutations and mapState are imported to the component.
You will need to import them first, as you did
import { mapState, mapActions } from 'vuex'
Import actions tho, and not mutations. Indeed, only actions are async and the flow should always be dispatch a action > commit a mutation > state is updated.
Then, plug them where they belong
computed: {
...mapState('obj', ['message']),
// other computed properties ...
}
methods: {
...mapActions('obj', ['updateMessage']),
// other methods ...
}
Then comes the interesting part
computed: {
message: {
get () {
const cloneDeepMessage = cloneDeep(this.message)
// you could make some destructuring here like >> const { id, title, description } = cloneDeepMessage
return cloneDeepMessage // or if destructured some fields >> return { id, title, description }
},
set (value) {
this.updateMessage(value)
}
}
}
As you can see, I would recommend to also import cloneDeep from 'lodash/cloneDeep' to avoid mutating the state directly thanks to cloneDeep when accessing your state.
This is the kind of warning that the Vuex strict mode will give you, that's why I recommend to enable it in development only.
The official docs are not super explicit on this part (you need to read various parts of them and mix them all together) but it's basically a good way of doing things IMHO.
I made a small library to track the loading of all my api calls.
It basically is an array which contains objects describing one loading process. I import this library into a Single File Component to make calls to it. I then pass the array from the library to a child component and want to render all my objects in the array (the loading statuses). I see the data updating in Vue Devtools on the child component, however it is not updated in the page.
What i tried:
passing the vue instance from the parent component and use $set to update the array
using array.splice to update the array
creating a new object with Object.assign, update the new object and then update the array (splice/$set)
passing a key and updating this key when updating thew object
calling methods in the library in the console
sample updating function:
function _finish(name, descFinished) {
const i = _loaders.findIndex(l => l.name == name);
if(i < 0) throw new NameNotFoundException(name);
let loader = Object.assign({}, _loaders[i]);
if(descFinished != undefined) loader.descFinished = descFinished;
loader.loading = false;
loader.error = false;
loader.endTime = new Date();
vm.$set(_loaders, i, loader);
return _loaders[i];
}
I pass to the child component like this
<loading-status v-if="loadstatus" :statuses="loadstatus.getAllLoaders()"></loading-status>
child component looks this this
<template>
<div v-if="statuses">
<div v-for="status in statuses" :key="status.name">
<div>
{{ status.loading == true ? 'true' : 'false' }}
</div>
</div>
</div>
</template>
<script>
export default {
name: 'loading-status',
props: {
statuses: {
type: Array,
required: true
}
}
}
image of the array not being reactive on the page
some more information that may be useful:
the library structure:
function loadmanager(vueinstance) {
//library internals here
// for example my array i want to render:
let _loaders = [];
var publicInterface() {
//public accesable functions here
//i call this to pass the array to my child component
getAllLoaders() {
return _loaders;
}
}
return publicInterface;
}
exports.makeLoadManager = loadmanager;
i import the library in the vue SFC and call it in the mounted hook
import loadhelper from "../../helpers/Loadstatus.js";
...
data() {
return {
loadstatus: null
}
}
...
mounted() {
this.loadstatus = loadhelper.makeLoadManager(this);
}
My question boils down to: How do I reactively render or correctly update an array from a js library in vue.
I would be happy to provide more information if that helps answering my question.
If your library is creating (and managing) an array of "loaders", simplest thing you can do is define empty reactive array in your Vue component's data and in mounted hook assign a reference to your's library array into it. Like this:
import LoadingStatus from "./components/LoadingStatus";
import loadhelper from "./helpers/LoadManager.js";
export default {
name: "App",
components: {
LoadingStatus
},
data() {
return {
statuses: []
};
}
mounted() {
this.statuses = loadhelper.getAllLoaders();
}
};
Vue takes the array provided by your library and makes it reactive. You can use template like this:
<LoadingStatus :statuses="statuses"/>
See the demo
As long as both Vue and your "load manager" are working with the same array (reference), its all good. I'v added setTimeout callback into LoadManager to demonstrate that changes "from the outside of Vue" to the array will make Vue component re-render....
I have a vue store which has the following
store.js
import Vue from 'vue'
import Vuex from 'vuex'
const state = {
supplementStore: {}
}
const actions = {
getDataFromApi ({dispatch, commit}) {
APIrunning.then(response => {
commit('SET_SUPPLEMENT', response)
})
}
}
const mutations = {
SET_SUPPLEMENT (state, data) {
state.supplementStore= data
}
}
const foodstore = {
namespaced: true,
state,
actions,
mutations
}
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
foodstore
}
})
My vue component looks like this
Supp.vue
<template>
<input type="checkbox" v-model="supps.logged">
</template>
<script>
import {mapState, mapActions} from 'vuex'
import store from './store'
export default {
data () {
return {
supps: []
}
},
mounted () {
this.supps = this.supplementStore
},
computed: {
...mapState('foodstore', ['supplementStore'])
}
}
</script>
As you can see I have a component level state called supps which is assigned the value of supplementStore (which is a vuex state) as soon as it is mounted.
mounted () {
this.supps = this.supplementStore
},
supplementStore gets its value from the the API and it is a JSON object which looks like this
supplementStore = {
logged: true
}
Therefore, when my Supp.vue component is mounted my local state supps will become
supps = {
logged: true
}
supps is binded to an input field of type checkbox (Supp.vue) using the v-model directive.
What I want to achieve:
When I toggle the checkbox, supps.logged should toggle between true and false but, supplementStore.logged should remain unchanged (since I have not binded it to my input field).
What I observe in my Vue Devtools:
When I toggle the checkbox, both supps.logged AND supplementStore.logged are toggling in sync i.e both of them are toggling in sync between true and false, whereas I want only supps.logged to get toggled.
Can anyone help me?
In Javascript, object is passed by reference. (This is a reasonably good explanation => https://medium.com/nodesimplified/javascript-pass-by-value-and-pass-by-reference-in-javascript-fcf10305aa9c)
To avoid this problem, you can clone the object when assigning to supps.
mounted () {
this.supps = { ...this.supplementStore } // cloning the object using Javascript Spread syntax
},
Have you tried Object.assign instead? In JS objects are passed by reference. Assigning one to a variable will cause the original one to change if the variable is changed inside.
To clone an object, you may try this:
// this.assignedObj = new object.
// this.obj = original object.
this.assignedObj = Object.assign({}, this.obj);
JSFiddle: https://jsfiddle.net/mr7x4yn0/
Edit: As you can see from the demo, Vue.set or this.$set will not work for you (probably).
The data I was receiving from the API into supplementStore was in the form of an array of objects:
supplementStore = [
{
"logged": true
},
{
"logged": false
}
]
And as Jacob Goh and Yousof K. mentioned in their respective answers that objects and arrays get passed by reference in javascript, I decided to use the following code to assign the value of supplementStore to supps inside my mounted() hook:
mounted () {
let arr = []
for (let i = 0; i < this.supplementStore.length; i++) {
let obj = Object.assign({}, this.supplementStore[i])
arr.push(obj)
}
this.supps = arr
}
Now when I toggle my checkbox, supplementStore.logged remains unchanged while supps.logged toggles between true and false just the way I wanted.
Usually we define in a Nuxt.js component something like this:
<script>
export default {
components: {
// components
}
data() {
return {
// key/value data
}
},
methods: {
// method definitions
}
}
</script>
Is there a way to read the components object as we read data() and methods ?
This is because I have several components and I want to loop on them to refactor parts of my code.
You can get Component data by using $options.
Try this.
created() {
console.log(this.$options.components)
}
it returns an object, keys are the component names, values are the contructors.
codepen - https://codesandbox.io/s/yk9km5m0wv
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