Vue: How to use component prop inside mapFields - javascript

I have general component and vuex store. For easy two-way binding I use vuex-map-fields. On component side it has mapFields method which creates get&set with mutations.
I want to pass namespace from vuex module with props but it seems to be impossible.
<my-component namespace="ns1" />
// my-component code
export default {
props: ["namespace"],
computed: {
...mapFields(??this.namespace??, ["attr1", "attr2"])
}
}
Of course, there is no way to use this in such way so we don't have access to props. How can I specify namespace as prop in such case?

The problem (as you probably gathered) is that computed properties are constructed before this is available, but you can get around it by deferring resolution of the this.namespace property until the computed property is called (which won't happen until component construction is finished).
The concept is based on this post Generating computed properties on the fly.
The basic pattern is to use a computed with get() and set()
computed: {
foo: {
get() { this.namespace...},
set() { this.namespace...},
}
}
but rather than type it all out in the component we can create a helper function based on the vuex-map-fields mapFields() function (see here for the original).
The normalizeNamespace() function that comes with vuex-map-fields does not support what we want to do, so we drop it and assume the namespace is always passed in (and that the store module uses the standard getField and updateField functions).
I have adapted one of the vuex-map-fields Codesandbox examples here.
Note the namespace is in data rather than props for conveniance, but props should work also.
Template
<template>
<div id="app">
<div>
<label>foo </label> <input v-model="foo" /> <span> {{ foo }}</span>
</div>
<br />
<div>
<label>bar </label> <input v-model="bar" /> <span> {{ bar }}</span>
</div>
</div>
</template>
Helper
<script>
const mapFields2 = (namespaceProp, fields) => {
return Object.keys(fields).reduce((prev, key) => {
const path = fields[key];
const field = {
get() {
const namespace = this[namespaceProp];
const getterPath = `${namespace}/getField`;
return this.$store.getters[getterPath](path);
},
set(value) {
const namespace = this[namespaceProp];
const mutationPath = `${namespace}/updateField`;
this.$store.commit(mutationPath, { path, value });
}
};
prev[key] = field;
return prev;
}, {});
};
export default {
name: "App",
data() {
return {
nsProp: "fooModule"
};
},
computed: {
...mapFields2("nsProp", { foo: "foo", bar: "bar" })
}
};
</script>
Store
import Vue from "vue";
import Vuex from "vuex";
import { getField, updateField } from "vuex-map-fields";
import App from "./App";
Vue.use(Vuex);
Vue.config.productionTip = false;
const store = new Vuex.Store({
modules: {
fooModule: {
namespaced: true,
state: {
foo: "initial foo value",
bar: "initail bar value"
},
getters: {
getField
},
mutations: {
updateField
}
}
}
});
new Vue({
el: "#app",
components: { App },
store,
template: "<App/>"
});

Related

Vue 3.0 How to assign a prop to a ref without changing the prop

I'm sending from the parent component a prop: user. Now in the child component I want to make a copy of it without it changing the prop's value.
I tried doing it like this:
export default defineComponent({
props: {
apiUser: {
required: true,
type: Object
}
},
setup(props) {
const user = ref(props.apiUser);
return { user };
}
});
But then if I change a value of the user object it also changes the apiUser prop. I thought maybe using Object.assign would work but then the ref isn't reactive anymore.
In Vue 2.0 I would do it like this:
export default {
props: {
apiUser: {
required: true,
type: Object
}
},
data() {
return {
user: {}
}
},
mounted() {
this.user = this.apiUser;
// Now I can use this.user without changing this.apiUser's value.
}
};
Credits to #butttons for the comment that lead to the answer.
const user = reactive({ ...props.apiUser });
props: {
apiUser: {
required: true,
type: Object
}
},
setup(props) {
const userCopy = toRef(props, 'apiUser')
}
With the composition API we have the toRef API that allows you to create a copy from any source reactive object. Since the props object is a reactive, you use toRef() and it won't mutate your prop.
This is what you looking for: https://vuejs.org/guide/components/props.html#one-way-data-flow
Create data where you add the prop to
export default {
props: ['apiUser'],
data() {
return {
// user only uses this.apiUser as the initial value;
// it is disconnected from future prop updates.
user: this.apiUser
}
}
}
Or if you use api composition:
import {ref} from "vue";
const props = defineProps(['apiUser']);
const user = ref(props.apiUser);
You also may want to consider using computed methods (see also linked doc section from above) or v-model.
Please note that the marked solution https://stackoverflow.com/a/67820271/2311074 is not working. If you try to update user you will see a readonly error on the console. If you don't need to modify user, you may just use the prop in the first place.
As discussed in comment section, a Vue 2 method that I'm personally fond of in these cases is the following, it will basically make a roundtrip when updating a model.
Parent (apiUser) ->
Child (clone apiUser to user, make changes, emit) ->
Parent (Set changes reactively) ->
Child (Automatically receives changes, and creates new clone)
Parent
<template>
<div class="parent-root"
<child :apiUser="apiUser" #setUserData="setUserData" />
</div>
</template>
// ----------------------------------------------------
// (Obviously imports of child component etc.)
export default {
data() {
apiUser: {
id: 'e134',
age: 27
}
},
methods: {
setUserData(payload) {
this.$set(this.apiUser, 'age', payload);
}
}
}
Child
<template>
<div class="child-root"
{{ apiUser }}
</div>
</template>
// ----------------------------------------------------
// (Obviously imports of components etc.)
export default {
props: {
apiUser: {
required: true,
type: Object
}
},
data() {
user: null
},
watch: {
apiUser: {
deep: true,
handler() {
// Whatever clone method you want to use
this.user = cloneDeep(this.apiUser);
}
}
},
mounted() {
// Whatever clone method you want to use
this.user = cloneDeep(this.apiUser);
},
methods: {
// Whatever function catching the changes you want to do
setUserData(payload) {
this.$emit('setUserData', this.user);
}
}
}
Apologies for any miss types

How can I reactively update a value in a component from a store value?

I have two components and a basic store as per the docs here: https://v2.vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch.
I want to make it so that when I type into an input the value in a different component is updated by using the store.
Basic example here.
App.vue
<template>
<div id="app">
<h1>Store Demo</h1>
<BaseInputText /> Value From Store: {{ test }}
</div>
</template>
<script>
import BaseInputText from "./components/BaseInputText.vue";
import { store } from "../store.js";
export default {
// This should reactively changed as per the input
computed: {
test: function() {
return store.state.test;
}
},
components: {
BaseInputText
}
};
</script>
BaseInput.vue
<template>
<input type="text" class="input" v-model="test" />
</template>
<script>
import { store } from "../store.js";
export default {
data() {
return {
test: store.state.test
};
},
// When the value changes update the store
watch: {
test: function(newValue) {
store.setTest(newValue);
}
}
};
</script>
store.js
export const store = {
debug: true,
state: {
test: "hi"
},
setTest(newValue) {
if (this.debug) console.log("Set the test field with:", newValue);
this.state.test = newValue;
}
};
I want to make it so that when I type a string into the input the test variable in App.vue is updated. I'm trying to understand how the store pattern works. I'm aware of how to use props.
I also have a working copy here: https://codesandbox.io/s/loz79jnoq?fontsize=14
Updated
2.6.0+
To make store reactive use Vue.observable (added in in 2.6.0+)
store.js
import Vue from 'vue'
export const store = Vue.observable({
debug: true,
state: {
test: 'hi'
}
})
BaseInputText.vue
<input type="text" class="input" v-model="state.test">
...
data() {
return {
state: store.state
};
},
before 2.6.0
store.js
import Vue from 'vue'
export const store = new Vue({
data: {
debug: true,
state: {
test: 'hi'
}
}
})
BaseInputText.vue
<input type="text" class="input" v-model="state.test">
...
data() {
return {
state: store.state
};
}
Old answer
From documentation However, the difference is that computed properties are cached based on their reactive dependencies.
The store is not reactive
Change to
App.vue
data() {
return {
state: store.state
};
},
computed: {
test: function() {
return this.state.test;
}
},
It looks bad but I don't see another way to make it work

Separating vuex stores for dynamically created components

This was the question got me stuck for a little bit. Unfortunately, I coudn't find answer here (asking also didn't help). So after doing some research and asking here and there, it seems that I got the solution to this issue.
If you have a question that you already know the answer to, and you
would like to document that knowledge in public so that others
(including yourself) can find it later.
Of course, my answer may not be the ideal one, moreover I know it is not, that's the key point why I'm posting - to improve it.
Note, I'm not using actions in example. The idea is the same.
Let's begin with stating the problem:
Imagine we have App.vue which dynamically generates its local component named Hello.
<template>
<div id="app">
<div>
<hello v-for="i in jobs" :key="i" :id="i"></hello>
<button #click="addJob">New</button>
</div>
</div>
</template>
<script>
import Hello from './components/Hello'
export default {
components: {
Hello
}...
store.js
export const store = new Vuex.Store({
state: {
jobs: []
}
})
We are using v-for directive to generate components by iterating through an array jobs. Our store as of now consists of only state with an empty array.
Button New should do 2 things:
1) create new component Hello, in other words add element to jobs (let it be numbers), which are going to be assigned as key and id of <hello>, and passed to local component as props.
2) generate local stores - modules - to keep any data scoped to newly created components.
Hello.vue
<template>
<div>
<input type="number" :value="count">
<button #click="updateCountPlus">+1</button>
</div>
</template>
export default {
props: ['id']
}
Simple component - input with a button adding 1.
Our goal is to design something like this:
For the first operation of NEW button - generating components - we add mutation to our store.js
mutations: {
addJob (state) {
state.jobs.push(state.jobs.length + 1)
...
}
Second, creating local modules. Here we're going to use reusableModule to generated multiple instances of a module. That module we keep in separate file for convinience. Also, note use of function for declaring module state.
const state = () => {
return {
count: 0
}
}
const getters = {
count: (state) => state.count
}
const mutations = {
updateCountPlus (state) {
state.count++
}
}
export default {
state,
getters,
mutations
}
To use reusableModule we import it and apply dynamic module registration.
store.js
import module from './reusableModule'
const {state: stateModule, getters, mutations} = module
export const store = new Vuex.Store({
state: {
jobs: []
},
mutations: {
addJob (state) {
state.jobs.push(state.jobs.length + 1)
store.registerModule(`module${state.jobs.length}`, {
state: stateModule,
getters,
mutations,
namespaced: true // making our module reusable
})
}
}
})
After, we're going to link Hello.vue with its storage. We may need state, getters, mutations, actions from vuex. To access storage we need to create our getters. Same with mutations.
Home.vue
<script>
export default {
props: ['id'],
computed: {
count () {
return this.$store.getters[`module${this.id}/count`]
}
},
methods: {
updateCountPlus () {
this.$store.commit(`module${this.id}/updateCountPlus`)
}
}
}
</script>
Imagine we have lots of getters, mutations and actions. Why not use {mapGetters} or {mapMutations}? When we have several modules and we know the path to module needed, we can do it. Unfortunately, we do not have access to module name.
The code is run when the component's module is executed (when your app
is booting), not when the component is created. So these helpers can
only be used if you know the module name ahead of time.
There is little help here. We can separate our getters and mutations and then import them as an object and keep it clean.
<script>
import computed from '../store/moduleGetters'
import methods from '../store/moduleMutations'
export default {
props: ['id'],
computed,
methods
}
</script>
Returning to App component. We have to commit our mutation and also let's create some getter for App. To show how can we access data located into modules.
store.js
export const store = new Vuex.Store({
state: {
jobs: []
},
getters: {
jobs: state => state.jobs,
sumAll (state, getters) {
let s = 0
for (let i = 1; i <= state.jobs.length; i++) {
s += getters[`module${i}/count`]
}
return s
}
}
...
Finishing code in App component
<script>
import Hello from './components/Hello'
import {mapMutations, mapGetters} from 'vuex'
export default {
components: {
Hello
},
computed: {
...mapGetters([
'jobs',
'sumAll'
])
},
methods: {
...mapMutations([
'addJob'
])
}
}
</script>
Hi and thank you for posting your question and your solution.
I started learning Vuex couple days ago and came across a similar problem. I've checked your solution and came up with mine which doesn't require registering new modules. I find it to be quite an overkill and to be honest I don't understand why you do it. There is always a possibility I've misunderstood the problem.
I've created a copy of your markup with a few differences for clarity and demonstration purposes.
I've got:
JobList.vue - main custom component
Job.vue - job-list child custom component
jobs.js - vuex store module file
JobList.vue (which is responsible for wrapping the job(s) list items)
<template>
<div>
<job v-for="(job, index) in jobs" :data="job" :key="job.id"></job>
<h3>Create New Job</h3>
<form #submit.prevent="addJob">
<input type="text" v-model="newJobName" required>
<button type="submit">Add Job</button>
</form>
</div>
</template>
<script>
import store from '../store/index'
import job from './job';
export default {
components: { job },
data() {
return {
newJobName: ''
};
},
computed: {
jobs() {
return store.state.jobs.jobs;
}
},
methods: {
addJob() {
store.dispatch('newJob', this.newJobName);
}
}
}
</script>
The Job
<template>
<div>
<h5>Id: {{ data.id }}</h5>
<h4>{{ data.name }}</h4>
<p>{{ data.active}}</p>
<button type="button" #click="toggleJobState">Toggle</button>
<hr>
</div>
</template>
<script>
import store from '../store/index'
export default {
props: ['data'],
methods: {
toggleJobState() {
store.dispatch('toggleJobState', this.data.id);
}
}
}
</script>
And finally the jobs.js Vuex module file:
export default {
state: {
jobs: [
{
id: 1,
name: 'light',
active: false
},
{
id: 2,
name: 'medium',
active: false
},
{
id: 3,
name: 'heavy',
active: false
}
]
},
actions: { //methods
newJob(context, jobName) {
context.state.jobs.push({
id: context.getters.newJobId,
name: jobName,
active: false
});
},
toggleJobState(context, id) {
context.state.jobs.forEach((job) => {
if(job.id === id) { job.active = !job.active; }
})
}
},
getters: { //computed properties
newJobId(state) { return state.jobs.length + 1; }
}
}
It's possible to add new jobs to the store and as the "active" property suggest, you can control every single individual job without the need for a new custom vuex module.

Vuex input with v-model not reactive

I try to explain it as simple as possible. I have something like this. Simple Vue root, Vuex store and input with v-model inside navbar id.
That input is not reactive... Why?!
HTML
<div id="navbar">
<h2>#{{ test }}</h2>
<input v-model="test" />
</div>
store.js
import Vuex from 'vuex'
export const store = new Vuex.Store({
state: {
test: 'test'
},
getters: {
test (state) {
return state.test
}
}
})
Vue Root
import { store } from './app-store.js'
new Vue({
el: '#navbar',
store,
computed: {
test () {
return this.$store.getters.test
}
}
})
You're binding to a computed property. In order to set a value on a computed property you need to write get and set methods.
computed:{
test:{
get(){ return this.$store.getters.test; },
set( value ){ this.$store.commit("TEST_COMMIT", value );}
}
}
And in your store
mutations:{
TEST_COMMIT( state, payload ){
state.test=payload;
}
}
Now when you change the value of the input bound to test, it will trigger a commit to the store, which updates its state.
You don't want to use v-model for that. Instead, use #input="test" in your input field and in the your methods hook:
test(e){
this.$store.dispatch('setTest', e.target.value)
}
Inside your Vuex store:
In mutations:
setTest(state, payload){
state.test = payload
},
In actions:
setTest: (context,val) => {context.commit('setTest', val)},
The input should now be reactive and you should see the result in #{{test}}
Here is an example of how I handle user input with Vuex: http://codepen.io/anon/pen/gmROQq
You can easily use v-model with Vuex (with actions/mutations firing on each change) by using my library:
https://github.com/yarsky-tgz/vuex-dot
<template>
<form>
<input v-model="name"/>
<input v-model="email"/>
</form>
</template>
<script>
import { takeState } from 'vuex-dot';
export default {
computed: {
...takeState('user')
.expose(['name', 'email'])
.commit('editUser') // payload example: { name: 'Peter'}
.map()
}
}
</script>
The computed property is one-way. If you want two-way binding, make a setter as the other answers suggest or use the data property instead.
import { store } from './app-store.js'
new Vue({
el: '#navbar',
store,
// computed: {
// test() {
// return this.$store.getters.test;
// }
// }
data: function() {
return { test: this.$store.getters.test };
}
});
But a setter is better to validate input value.

How to avoid the need of writing this.$store.state.donkey all the time in Vue?

I'm learning Vue and I noticed that I have the following syntax more or less everywhere.
export default {
components: { Navigation, View1 },
computed: {
classObject: function() {
return {
alert: this.$store.state.environment !== "dev",
info: this.$store.state.environment === "dev"
};
}
}
}
It's a pain to write out this.$store.state.donkey all the time and it lowers the readability too. I'm sensing that I'm doing it in a less than optimal way. How should I refer to the state of the store?
you can set computed properties for both states & getters i.e.
computed: {
donkey () {
this.$store.state.donkey
},
ass () {
this.$store.getters.ass
},
...
Whilst you still need to call the $state.store once you can then reference a donkey or an ass on your vm...
To make things even easier you can pull in the vuex map helpers and use them to find your ass ... or donkey:
import { mapState, mapGetters } from 'vuex'
default export {
computed: {
...mapState([
'donkey',
]),
...mapGetters([
'ass',
]),
...mapGetters({
isMyAss: 'ass', // you can also rename your states / getters for this component
}),
now if you look at this.isMyAss you'll find it ... your ass
worth noting here that getters, mutations & actions are global - therefore they are referenced directly on your store, i.e. store.getters, store.commit & store.dispatch respectively. This applies whether they are in a module or in the root of your store. If they are in a module check out namespacing to prevent overwriting previously used names: vuex docs namespacing. However if you are referencing a modules state, you must prepend the name of the module, i.e. store.state.user.firstName in this example user is a module.
Edit 23/05/17
Since the time of writing Vuex has been updated and its namespacing feature is now a go to when you work with modules. Simply add namespace: true to your modules export, i.e.
# vuex/modules/foo.js
export default {
namespace: true,
state: {
some: 'thing',
...
add the foo module to your vuex store:
# vuex/store.js
import foo from './modules/foo'
export default new Vuex.Store({
modules: {
foo,
...
then when you are pulling this module into your components you can:
export default {
computed: {
...mapState('foo', [
'some',
]),
...mapState('foo', {
another: 'some',
}),
...
this makes modules very simple and clean to use, and is a real saviour if you are nesting them multiple levels deep: namespacing vuex docs
I have put together an example fiddle to showcase the various ways you can reference and work with your vuex store:
JSFiddle Vuex Example
Or check out the below:
const userModule = {
state: {
firstName: '',
surname: '',
loggedIn: false,
},
// #params state, getters, rootstate
getters: {
fullName: (state, getters, rootState) => {
return `${state.firstName} ${state.surname}`
},
userGreeting: (state, getters, rootState) => {
return state.loggedIn ? `${rootState.greeting} ${getters.fullName}` : 'Anonymous'
},
},
// #params state
mutations: {
logIn: state => {
state.loggedIn = true
},
setName: (state, payload) => {
state.firstName = payload.firstName
state.surname = payload.surname
},
},
// #params context
// context.state, context.getters, context.commit (mutations), context.dispatch (actions)
actions: {
authenticateUser: (context, payload) => {
if (!context.state.loggedIn) {
window.setTimeout(() => {
context.commit('logIn')
context.commit('setName', payload)
}, 500)
}
},
},
}
const store = new Vuex.Store({
state: {
greeting: 'Welcome ...',
},
mutations: {
updateGreeting: (state, payload) => {
state.greeting = payload.message
},
},
modules: {
user: userModule,
},
})
Vue.component('vuex-demo', {
data () {
return {
userFirstName: '',
userSurname: '',
}
},
computed: {
loggedInState () {
// access a modules state
return this.$store.state.user.loggedIn
},
...Vuex.mapState([
'greeting',
]),
// access modules state (not global so prepend the module name)
...Vuex.mapState({
firstName: state => state.user.firstName,
surname: state => state.user.surname,
}),
...Vuex.mapGetters([
'fullName',
]),
...Vuex.mapGetters({
welcomeMessage: 'userGreeting',
}),
},
methods: {
logInUser () {
this.authenticateUser({
firstName: this.userFirstName,
surname: this.userSurname,
})
},
// pass an array to reference the vuex store methods
...Vuex.mapMutations([
'updateGreeting',
]),
// pass an object to rename
...Vuex.mapActions([
'authenticateUser',
]),
}
})
const app = new Vue({
el: '#app',
store,
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuex"></script>
<div id="app">
<!-- inlining the template to make things easier to read - all of below is still held on the component not the root -->
<vuex-demo inline-template>
<div>
<div v-if="loggedInState === false">
<h1>{{ greeting }}</h1>
<div>
<p><label>first name: </label><input type="text" v-model="userFirstName"></p>
<p><label>surname: </label><input type="text" v-model="userSurname"></p>
<button :disabled="!userFirstName || !userSurname" #click="logInUser">sign in</button>
</div>
</div>
<div v-else>
<h1>{{ welcomeMessage }}</h1>
<p>your name is: {{ fullName }}</p>
<p>your firstName is: {{ firstName }}</p>
<p>your surname is: {{ surname }}</p>
<div>
<label>Update your greeting:</label>
<input type="text" #input="updateGreeting({ message: $event.target.value })">
</div>
</div>
</div>
</vuex-demo>
</div>
As you can see if you wanted to pull in mutations or actions this would be done in a similar way but in your methods using mapMutations or mapActions
Adding Mixins
To extend the above behaviour you could couple this with mixins and then you'd only have to set up the above computed properties once and pull in the mixin on the component that needs them:
animals.js (mixin file)
import { mapState, mapGetters } from 'vuex'
export default {
computed: {
...mapState([
'donkey',
...
your component
import animalsMixin from './mixins/animals.js'
export default {
mixins: [
animalsMixin,
],
created () {
this.isDonkeyAnAss = this.donkey === this.ass
...

Categories