I'm getting started with Vue, I need to create a form of tiered select fields. That is the selected option for A, uses that to call the API to get the options for B, which determines options for C.
I'm still pretty new to frontend frameworks so this might be a terrible design. However not every inclusion of A (SelectState.vue) in a view requires all the children so making them modular was my first thought.
Currently I have a top level component that displays the select options:
SelectState.vue
<template>
<div id="select-state">
<span>{{ label }}</span>
<select v-model="selectedState">
<option v-for="state in states" :key="state">
{{ state }}
</option>
</select>
</div>
</template>
<script>
export default {
name: 'select-state',
data: function () {
return {
selectedState: '',
states: ['TX']
}
},
props: ['label']
// this.states = axios.get('xxx')
}
</script>
Index.vue
<template>
<div id="form">
<v-select-state label="State"></v-select-state>
<v-select-zip label="Zip"></v-select-zip>
</div>
</template>
<script>
import SelectState from './SelectState.vue'
import SelectZip from './SelectZip.vue'
export default {
name: 'Index',
components: {
'v-select-state': SelectState,
'v-select-Zip': SelectZip
}
}
</script>
Then I have a SelectZip.vue that is identical to SelectState.vue except that it has a parameter for its axios.get('XXX', params = {'state': ???}). But I'm stuck on how to "pass" that necessary parameter.
Thanks in advance!
edit: In conjunction with #dziraf's answer my working although verbose SelectedZip.vue is as follows:
<template>
<div id="select_zip">
<span>{{ label }}</span>
<select v-model="selected_zip">
<option v-for="zip in zips" :key="zip">
{{ zip }}
</option>
</select>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'select_zip',
data: function () {
return {
zips: []
}
},
props: ['label'],
computed: {
selected_zip: {
get () { return this.$store.state.formModule.zip },
set (value) { this.$store.commit('formModule/setZips', value) }
},
selected_state: {
get () { return this.$store.state.formModule.state }
}
},
methods: {
getValidZips (state) {
axios.post('/api/v1/get_valid_zips', {
params:{'state': state }})
.then(response => {
this.zips = response.data
})
.catch(error => {
console.log(error)
})
}
},
watch: {
selected_state (value) {
this.getValidZips(value)
}
}
}
</script>
You can pass it by adding 'state' props to your select components from your main form component, but I think it isn't a good long-term solution.
Instead, consider using Vuex. An example configuration would look like this:
#/store/modules/form.js
const Form = {
namespaced: true,
state: {
state: '',
zip: ''
},
getters: {},
mutations: {
setState (state, payload) {
state.state = payload
},
setZip (state, payload) {
state.zip = payload
}
},
actions: {}
}
export default Form
#/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import Form from './modules/form'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
formModule: Form,
}
})
export default store
#/main.js
// your impots
import store from './store/index'
// your configs
new Vue({
el: '#app',
router,
store, // add store to your main Vue instance so it's accessible with this.$store
axios,
components: { App },
template: '<App/>'
});
This would be your SelectState.vue:
<template>
<div id="select-state">
<span>{{ label }}</span>
<select v-model="selectedState">
<option v-for="state in states" :key="state">
{{ state }}
</option>
</select>
</div>
</template>
<script>
export default {
name: 'select-state',
data: function () {
return {
states: ['TX']
}
},
computed: {
selectedState: {
get() { return this.$store.state.formModule.state },
set(value) { this.$store.commit('formModule/setState', value) }
}
},
props: ['label']
}
</script>
Your SelectZip.vue would be the same, except you would instead use your store's zip as your v-model.
Your store variables are accessible across your app and you can access them with computed properties in your components.
Related
I'm having trouble getting a route param to pass directly into a component. I followed multiple sets of directions in the docs (including using the Composition API as in the following code), but I'm still getting undefined when the CourseModule.vue first renders.
Route Definition
{
path: '/module/:id',
name: 'Course Module',
props: true,
component: () => import('../views/CourseModule.vue'),
},
CourseModule.vue:
<template>
<div class="AppHome">
<CustomerItem />
<CourseModuleItem :coursemodule-id="this.CoursemoduleId"/>
</div>
</template>
<script>
import { useRoute } from 'vue-router';
import CustomerItem from '../components/customers/customer-item.vue';
import CourseModuleItem from '../components/coursemodules/coursemodule-item.vue';
export default {
setup() {
const route = useRoute();
alert(`CourseModule.vue setup: ${route.params.id}`);
return {
CoursemoduleId: route.params.id,
};
},
components: {
CustomerItem,
CourseModuleItem,
},
mounted() {
alert(`CourseModule.vue mounted: ${this.CoursemoduleId}`);
},
};
</script>
coursemodule-item.vue:
<template>
<div id="module">
<div v-if="module.data">
<h2>Course: {{module.data.ModuleName}}</h2>
</div>
<div v-else-if="module.error" class="alert alert-danger">
{{module.error}}
</div>
<Loader v-else-if="module.loading" />
</div>
</template>
<script>
import Loader from '../APILoader.vue';
export default {
props: {
CoursemoduleId: String,
},
components: {
Loader,
},
computed: {
module() {
return this.$store.getters.getModuleById(this.CoursemoduleId);
},
},
mounted() {
alert(`coursemodule-item.vue: ${this.CoursemoduleId}`);
this.$store.dispatch('setModule', this.CoursemoduleId);
},
};
</script>
The output from my alerts are as follows:
CourseModule.vue setup: zzyClJDQ3QAKuQ2R52AC35k3Hc0yIgft
coursemodule-item.vue: undefined
CourseModule.vue mounted: zzyClJDQ3QAKuQ2R52AC35k3Hc0yIgft
As you can see, the path parameter works fine in the top level Vue, but not it's still not getting passed into the component.
your kebab-cased :coursemodule-id props that you're passing to the CourseModuleItem component becomes a camelCased coursemoduleId props
Prop Casing (camelCase vs kebab-case)
try this
// coursemodule-item.vue
...
props: {
coursemoduleId: String,
},
...
mounted() {
alert(`coursemodule-item.vue: ${this.coursemoduleId}`);
this.$store.dispatch('setModule', this.coursemoduleId);
},
I'm building a web app using Nuxt v2.15 and #nuxtjs/i18n v7.2. I'm using Vuex for state management. In my global state, I want to create a getter that returns a value based on this.$i18n.locale.
What is the best way to access the current app context, which has $i18n attached to it, in my Vuex getter method?
nuxt.config.js
export default {
modules: [
"#nuxtjs/i18n", // https://i18n.nuxtjs.org/
],
i18n: {
locales: { /* configuration */ },
defaultLocale: "en",
detectBrowserLanguage: {
fallbackLocale: "en",
redirectOn: "root",
},
langDir: "~/locales/",
vueI18n: {
fallbackLocale: "en",
messages: { /* translation data */ },
},
},
};
store/index.js
export const state = () => ({
// Root module for Vuex store: https://nuxtjs.org/docs/directory-structure/store/
});
export const getters = {
loginOpts() {
const locale = this.$i18n.locale;
const uiLocales = [locale];
if (locale === "zh") {
uiLocales.push("zh-CN");
}
return { params: { ui_locales: uiLocales } };
},
};
components/login.vue
<template>
<div>
<v-btn v-if="$auth.loggedIn" #click="$auth.logout()">Sign Out</v-btn>
<v-btn v-else #click="$auth.loginWith('auth0', $store.getters.loginOpts)">Sign In</v-btn>
</div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "Login",
data() {
return {};
},
computed: {
...mapGetters(["loginOpts"]),
},
};
</script>
What I expect
I expect to be able to access this.$i18n from a Vuex getter, or to have some means of doing so.
What actually happens
In the getter method, this is undefined.
TypeError: Cannot read properties of undefined (reading '$i18n')
What I've tried
I see here that a getter is passed four arguments: state, getters, rootState, and rootGetters.
I've tried:
RTFM for Vuex State, Getters, and Nuxt i18n
accessing state.$i18n
adding $i18n: this.$i18n to the root state
I had a similar problem last week where I absolutely needed to use i18n inside a vuex getter.
I wouldn't recommend this as its probably not the most performant thing in the world but it worked for me and solved a huge problem while I was working on a government application.
components/login.vue
<template>
<div>
<v-btn v-if="$auth.loggedIn" #click="$auth.logout()">Sign Out</v-btn>
<v-btn v-else #click="$auth.loginWith('auth0', theLoginOpts)">Sign In</v-btn>
</div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "Login",
data() {
return {
theLoginOpts: $store.getters.loginOpts(this) // The secret sauce
};
},
computed: {
...mapGetters(["loginOpts"]),
},
};
</script>
store/index.js
export const state = () => ({
// Root module for Vuex store: https://nuxtjs.org/docs/directory-structure/store/
});
export const getters = {
loginOpts: (state) => (app) => { // App is the secret sauce from earlier
const locale = app.$i18n.locale;
const uiLocales = [locale];
if (locale === "zh") {
uiLocales.push("zh-CN");
}
return { params: { ui_locales: uiLocales } };
},
};
After digging through docs, experimenting, and discussing with the folks who were kind enough to offer answers here, it is clear that i18n is not directly accessible in a Vuex getter method, despite the fact that #nuxt/i18n registers a Vuex module called i18n and everything I've read about Vuex modules suggests this should be possible.
I did however come across the docs for #nuxt/i18n callbacks, which led me to create a small plugin that sets the value of locale and uiLocales in the global state using a mutation.
The end result looks like this:
nuxt.config.js
export default {
modules: [
"#nuxtjs/i18n", // https://i18n.nuxtjs.org/
],
plugins: [
"~/plugins/i18n.js",
],
i18n: {
locales: { /* configuration */ },
defaultLocale: "en",
detectBrowserLanguage: {
fallbackLocale: "en",
redirectOn: "root",
},
langDir: "~/locales/",
vueI18n: {
fallbackLocale: "en",
messages: { /* translation data */ },
},
},
};
plugins/i18n.js
export function findLocaleConfig (i18n, locale) {
return (
i18n.locales.find(({ iso, code }) => [iso, code].includes(locale)) || {}
);
}
export default function ({ app }) {
app.store.commit("localeChange", findLocaleConfig(app.i18n, app.i18n.locale));
app.i18n.onLanguageSwitched = (oldLocale, newLocale) => {
app.store.commit("localeChange", findLocaleConfig(app.i18n, newLocale));
};
}
store/index.js
export const state = () => ({
locale: null,
uiLocales: [],
});
export const mutations = {
localeChange(state, locale) {
state.locale = locale.code;
state.uiLocales = [locale.code, locale.iso];
},
};
components/login.vue
<template>
<div>
<v-btn v-if="$auth.loggedIn" #click="$auth.logout()">Sign Out</v-btn>
<v-btn v-else #click="$auth.loginWith('auth0', loginOpts)">Sign In</v-btn>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {};
},
computed: {
loginOpts() {
const uiLocales = this.$store.state.uiLocales;
return { params: { ui_locales: uiLocales } };
},
},
};
</script>
You can access a the locale in an Vuex action with: this.app.i18n.locale.
This cannot be accessed in a state or getter (a getter is a not a place for it anyway IMO).
PS: The above means that means that you can access this value anywhere you have access to the Nuxt context.
The simple way we did in our project is like below:
In component computed prop
teamsForSelector() {
return this.$store.getters['company/teamsForSelector'](this.$i18n);
}
and in state getters
teamsForSelector: (state) => ($i18n) => {
const teamsForSelector = state.teams.map((team) => {
return {
id: team.teamUid,
label: team.teamName,
};
});
teamsForSelector.unshift({
id: 'all-teams',
label: $i18n.t('key'),
});
return teamsForSelector;
}
I have a Vue 3 app. I am trying to setup a store for state management. In this app, I have the following files:
app.vue
component.vue
main.js
store.js
These files include the following:
store.js
import { reactive } from 'vue';
const myStore = reactive({
selectedItem: null
});
export default myStore;
main.js
import { createApp } from 'vue';
import App from './app.vue';
import myStore from './store';
const myApp = createApp(App);
myApp.config.globalProperties.$store = myStore;
myApp.mount('#app');
component.vue
<template>
<div>
<div v-if="item">You have selected an item</div>
<div v-else>Please select an item</div>
<button class="btn btn-primary" #click="generateItem">Generate Item</button>
</div>
</template>
<script>
export default {
props: {
item: Object
},
watch: {
item: function(newValue, oldValue) {
alert('The item was updated.');
}
},
methods: {
generateItem() {
const item = {
id:0,
name: 'Some random name'
};
this.$emit('itemSelected', item);
}
}
}
</script>
app.vue
<template>
<component :item="selectedItem" #item-selected="onItemSelected" />
</template>
<script>
import Component form './component.vue';
export default {
components: {
'component': Component
},
data() {
return {
...this.$store
}
},
methods: {
onItemSelected(item) {
console.log('onItemSelected: ');
console.log(item);
this.$store.selectedItem = item;
}
}
}
</script>
The idea is that the app manages state via a reactive object. The object is passed into the component via a property. The component can then update the value of the object when a user clicks the "Generate Item" button.
I can see that the selectedValue is successfully passed down as a property. I have confirmed this by manually setting selectedValue to a dummy value to test. I can also see that the onItemSelected event handler works as expected. This means that events are successfully flowing up. However, when the selectedItem is updated in the event handler, the updated value is not getting passed back down to the component.
What am I doing wrong?
$store.selectedItem stops being reactive here, because it's read once in data:
data() {
return {
...this.$store
}
}
In order for it to stay reactive, it should be either converted to a ref:
data() {
return {
selectedItem: toRef(this.$store, 'selectedItem')
}
}
Or be a computed:
computed: {
selectedItem() {
return this.$store.selectedItem
}
}
I'm new with VueJS, and I'm creating a VueJS app where you can get some informations about a Github User,
(example: https://api.github.com/users/versifiction)
I created a store with VueX, but I need to update the value written by the user in the input,
My "inputValue" is always at "" (its default value) and when I type inside the input, the store value still at ""
I tried this :
Input.vue
<template>
<div class="input">
<input
type="text"
:placeholder="placeholder"
v-model="inputValue"
#change="setInputValue(inputValue)"
#keyup.enter="getResult(inputValue)"
/>
<input type="submit" #click="getResult(inputValue)" />
</div>
</template>
<script>
import store from "../store";
export default {
name: "Input",
props: {
placeholder: String,
},
computed: {
inputValue: () => store.state.inputValue,
},
methods: {
setInputValue: (payload) => {
store.commit("setInputValue", payload);
}
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>
and this :
store/index.js
import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
inputValue: "",
},
getters: {
getInputValue(state) {
return state.inputValue;
}
},
mutations: {
setInputValue(state, payload) {
console.log("setInputValue");
console.log("payload ", payload);
state.inputValue = payload;
},
},
});
According to the vuex docs in the form handling section you should do :
:value="inputValue"
#change="setInputValue"
and
methods: {
setInputValue: (event) => {
store.commit("setInputValue", event.target.value);
}
}
The simplest and elegant way to bind vuex and a component would be to use computed properties.
The above code would become,
<input
type="text"
:placeholder="placeholder"
v-model="inputValue"
#keyup.enter="getResult(inputValue)"
/>
and inside your computed properties, you'll need to replace inputValue with following code.
computed: {
inputValue: {
set(val){
this.$store.commit(‘mutationName’, val)
},
get() {
return this.$store.stateName
}
}
}
My goal is to create an 'edit account' form such that a user can modify their account data. I want to present the account data in a form that is already filled with the users data i.e username, email, address ...
The user can then modify the data in the form and submit this form that will update their user information.
I am using v-model to bind the form input to an object called accountInfo in my data, that looks like this:
data() {
return {
accountInfo: {
firstName: ''
}
}
}
And here is an example of a form input in my template:
<input v-model.trim="accountInfo.firstName" type="text" class="form-control" id="first-name" />
The values for the key's in the object are currently empty strings but I would like the values to come from an object called userProfile that is a state property in vuex.
In my 'edit account' component I am mapping the vuex state by importing:
import { mapState } from "vuex";
then using the following in a computed property
computed: {
...mapState(["userProfile"])
}
What I would like to do is instead of having empty strings as the values of accountInfo, assign them values from the userProfile computed property mapped from vuex, like so:
data() {
return {
accountInfo: {
firstName: this.userProfile.fristName,
}
}
}
This will provide the desired initial data for my form but unfortunately this doesn't work, presumably because data is rendered earlier on in the life cycle than computed properties.
Full code:
EditAccount.vue
<template>
<div class="container-fluid">
<form id="sign_up_form" #submit.prevent>
<div class="form-row">
<div class="form-group col-md-6">
<input v-model.trim="signupForm.firstName" type="text" class="form-control" id="first_name" />
</div>
</div>
</form>
</div>
</template>
<script>
import { mapState } from "vuex";
import SideBar from "../common/SideBar.vue";
export default {
name: "EditAccount",
computed: {
...mapState(["userProfile"])
},
data() {
return {
accountInfo: {
firstName: this.userProfile.firstName
}
};
}
};
</script>
store.js:
export const store = new Vuex.Store({
state: {
userProfile: {firstName: "Oamar", lastName: "Kanji"}
}
});
You were right, computeds are evaluated after the initial data function is called.
Quick fix
In the comments, #Jacob Goh mentioned the following:
$store should be ready before data function is called. Therefore, firstName: this.$store.state.userProfile.firstName should just work.
export default {
name: 'EditAccount',
data() {
return {
accountInfo: {
firstName: this.$store.state.userProfile.firstName
}
}
}
};
Really need computeds?
See #bottomsnap's answer, where setting the initial value can be done in the mounted lifecycle hook.
With your code, it would look like this:
import { mapState } from 'vuex';
export default {
name: 'EditAccount',
computed: {
...mapState(['userProfile'])
},
data() {
return {
accountInfo: {
firstName: ''
}
}
}
mounted() {
this.accountInfo.firstName = this.userProfile.firstName;
}
};
Though it may render once without the value, and re-render after being mounted.
Container versus presentation
I explain Vue's communication channels in another answer, but here's a simple example of what you could do.
Treat the Form component as presentation logic, so it doesn't need to know about the store, instead receiving the profile data as a prop.
export default {
props: {
profile: {
type: Object,
},
},
data() {
return {
accountInfo: {
firstName: this.profile.firstName
}
};
}
}
Then, let the parent handle the business logic, so fetching the information from the store, triggering the actions, etc.
<template>
<EditAccount :profile="userProfile" :submit="saveUserProfile"/>
</template>
<script>
import { mapState, mapActions } from "vuex";
export default {
components: { EditAccount },
computed: mapState(['userProfile']),
methods: mapActions(['saveUserProfile'])
}
</script>
While Jacob is not wrong saying that the store is ready, and that this.$store.state.userProfile.firstName will work, I feel this is more a patch around a design problem that can easily be solved with the solution above.
Bind your input with v-model as you were:
<div id="app">
<input type="text" v-model="firstName">
</div>
Use the mounted lifecycle hook to set the initial value:
import Vue from 'vue';
import { mapGetters } from 'vuex';
new Vue({
el: "#app",
data: {
firstName: null
},
computed: {
...mapGetters(["getFirstName"])
},
mounted() {
this.firstName = this.getFirstName
}
})