I have following computed property in my Vue component:
computed: {
contragent_id: {
get() {
return appData.getters["basket/contragent_id"];
},
set(value) {
return value;
},
},
settings: {
get() {
return appData.getters["basket/delivery_settings"];
},
set(value) {
return value;
},
},
presets: {
get() {
return appData.getters["basket/delivery_presets"];
},
set(value) {
return value;
},
},
},
And i have the Vuex store module :
const moduleBasket = {
namespaced: true,
state: () => ({
presets: {
contragents: [],
partners: {},
delivery: {
types: {},
methods: {},
tc_methods: {},
tc_list: {},
addresses: [],
},
},
summary: {
delivery: {
delivery_type: "",
delivery_method: "",
delivery_tc: "",
delivery_tc_method: "",
delivery_tc_city: "",
delivery_address_id: "",
},
payment: {
method: "",
methodText: "",
},
address: {},
comment: "",
partner_id: "",
contragent_id: "",
price: {
total: "",
nds: "",
delivery: "",
discount: "",
},
fillial: {
address: "",
},
},
}),
mutations: {
changeFillial(state, fillial) {
Vue.set(state.summary, "fillial", fillial);
},
changePartnerId(state, partner_id) {
Vue.set(state.summary, "partner_id", partner_id);
},
changeContragentId(state, contragent_id) {
Vue.set(state.summary, "contragent_id", contragent_id);
// state.summary.contragent_id = contragent_id;
},
changeContragents(state, contragnets) {
Vue.set(state.presets, "contragents", contragnets);
// state.presets.contragents = contragnets;
},
changePartners(state, partners) {
Vue.set(state.presets, "partners", partners);
// state.presets.partners = partners;
},
changeDeliveryPresets(state, presets) {
// console.log('changeDeliveryPresets');
Vue.set(state.presets, "delivery", presets);
// state.presets.delivery = presets;
},
changeDeliveryPresets__types(state, types) {
// console.log('changeDeliveryPresets__types');
Vue.set(state.presets.delivery, "types", types);
// state.presets.delivery.types = types;
},
changeDeliveryPresets__methods(state, methods) {
Vue.set(state.presets.delivery, "methods", methods);
// state.presets.delivery.methods = methods;
},
changeDeliveryPresets__tc_methods(state, tc_methods) {
Vue.set(state.presets.delivery, "tc_methods", tc_methods);
// state.presets.delivery.tc_methods = tc_methods;
},
changeDeliveryPresets__tc_list(state, tc_list) {
Vue.set(state.presets.delivery, "tc_list", tc_list);
// state.presets.delivery.tc_list = tc_list;
},
changeDeliveryPresets__addresses(state, addresses) {
Vue.set(state.presets.delivery, "addresses", addresses);
// state.presets.delivery.addresses = addresses;
},
changeDelivery(state, delivery) {
for (let key in delivery) {
Vue.set(state.summary.delivery, key, delivery[key]);
// state.summary.delivery[key]=delivery[key];
}
},
changeDelivery__tc_city(state, tc_city) {
Vue.set(state.summary.delivery, "delivery_tc_city", tc_city);
// state.summary.delivery.delivery_tc_city = tc_city;
},
changeDelivery__tc(state, tc) {
Vue.set(state.summary.delivery, "delivery_tc", tc);
// state.summary.delivery.delivery_tc = tc;
},
changeDelivery__method(state, method) {
Vue.set(state.summary.delivery, "delivery_method", method);
// state.summary.delivery.delivery_method = method;
},
changeDelivery__delivery_type(state, delivery_type) {
console.log("setting delivery");
Vue.set(state.summary.delivery, "delivery_type", delivery_type);
// state.summary.delivery.delivery_type = delivery_type;
},
changeDelivery__address_id(state, address_id) {
Vue.set(state.summary.delivery, "delivery_address_id", address_id);
// state.summary.delivery.delivery_address_id = address_id;
},
changeDelivery__address(state) {
state.summary.address = {};
if (state && state.summary.delivery) {
if (state.presets && state.presets.addresses && state.summary.delivery && ((state.summary.delivery.delivery_method == "tc" && state.summary.delivery.delivery_tc_method == "door") || state.summary.delivery.delivery_method == "courier")) {
if (state.presets && state.presets.addresses && state.summary.delivery && state.summary.delivery && state.summary.delivery.delivery_address_id) {
Vue.set(
state.summary,
"address",
state.presets.addresses.find((address) => address.id == state.summary.delivery.delivery_address_id)
);
}
} else {
if (state.summary.delivery && state.summary.delivery.delivery_tc_city) {
Vue.set(state.summary, "address", {
address: state.summary.delivery.delivery_tc_city,
});
}
}
}
},
},
actions: {
//COMMON DATA
changeFillial: ({ commit }, { fillial }) => commit("changeFillial", fillial),
changePartnerId: ({ commit }, { partner_id }) => {
console.log("partner id commit");
commit("changePartnerId", partner_id);
},
changeContragentId: ({ commit }, { contragent_id }) => {
commit("changeContragentId", contragent_id);
},
changeContragents: ({ commit }, { contragents }) => {
commit("changeContragents", contragents);
},
changePartners: ({ commit }, { partners }) => {
commit("changePartners", partners);
},
//PRESETS
changeDeliveryPresets: ({ commit }, { presets }) => {
commit("changeDeliveryPresets", presets);
commit("changeDelivery__address");
},
changeDeliveryPresets__types: ({ commit }, { types }) => {
commit("changeDeliveryPresets__types", types);
},
changeDeliveryPresets__methods: ({ commit }, { methods }) => {
commit("changeDeliveryPresets__methods", methods);
},
changeDeliveryPresets__tc_methods: ({ commit }, { tc_methods }) => {
commit("changeDeliveryPresets__tc_methods", tc_methods);
},
changeDeliveryPresets__tc_list: ({ commit }, { tc_list }) => {
commit("changeDeliveryPresets__tc_list", tc_list);
},
changeDeliveryPresets__addresses: ({ commit }, { addresses }) => {
commit("changeDeliveryPresets__addresses", addresses);
},
//SUMMARY
changeDelivery: ({ commit }, { delivery }) => {
commit("changeDelivery", delivery);
commit("changeDelivery__address");
},
changeDelivery__tc_city: ({ commit }, { tc_city }) => {
commit("changeDelivery__tc_city", tc_city);
},
changeDelivery__tc: ({ commit }, { tc }) => {
commit("changeDelivery__tc", tc);
},
changeDelivery__tc_method: ({ commit }, { tc_method }) => {
commit("changeDelivery__tc_method", tc_method);
},
changeDelivery__delivery_type: ({ commit }, { delivery_type }) => {
commit("changeDelivery__delivery_type", delivery_type);
},
changeDelivery__delivery_method: ({ commit }, { delivery_method }) => {
commit("changeDelivery__delivery_method", delivery_method);
},
changeDelivery__address_id: ({ commit }, { address_id }) => {
commit("changeDelivery__address_id", address_id);
},
},
getters: {
summary: (state) => {
return state.summary;
},
presets: (state) => {
return state.presets;
},
delivery_tc_title: (state) => {
return state.presets && state.presets.delivery && state.presets.delivery.tc_list && state.summary && state.summary.delivery && state.summary.delivery.delivery_tc ? state.presets.delivery.tc_list[state.summary.delivery.delivery_tc] : "";
},
delivery_tc_method_title: (state) => {
return state.presets && state.presets.delivery && state.presets.delivery.tc_methods && state.summary && state.summary.delivery && state.summary.delivery.delivery_tc_method ? state.presets.delivery.tc_methods[state.summary.delivery.delivery_tc_method] : "";
},
contragent_id: (state) => {
return state.summary && state.summary.contragent_id ? state.summary.contragent_id : "";
},
contragent: (state) => {
return state.presets && state.presets.contragents && state.summary && state.summary.contragent_id ? state.presets.contragents[state.summary.contragent_id] : "";
},
partner_id: (state) => {
return state.summary && state.summary.partner_id ? state.summary.partner_id : "";
},
partner: (state) => {
return state.presets && state.presets.partners && state.presets.partners.length && state.summary && state.summary.partner_id ? state.presets.partners.find((partner) => partner.id == state.summary.partner_id) : "";
},
partners: (state) => {
return state.presets && state.presets.partners ? state.presets.partners : "";
},
contragents: (state) => {
return state.presets && state.presets.contragents ? state.presets.contragents : "";
},
delivery: (state) => {
return state.summary && state.summary.delivery ? state.summary.delivery : "";
},
delivery_presets: (state) => {
return state.presets && state.presets.delivery ? state.presets.delivery : "";
},
delivery_settings: (state, getters) => {
return {
delivery: getters["delivery"],
address: getters["address"],
fillial: getters["fillial"],
};
},
fillial: (state) => {
return state.summary && state.summary.fillial ? state.summary.fillial : "";
},
},
};
In the template the
{{ presets }}
{{ settings}}
returns { "types": {}, "methods": {}, "tc_methods": {}, "tc_list": {}, "addresses": [] } { "delivery": { "delivery_type": "", "delivery_method": "", "delivery_tc": "", "delivery_tc_method": "", "delivery_tc_city": "", "delivery_address_id": "" }, "fillial": { "address": "" } }
like initial in store
When i use getter in console appData.getters["basket/delivery_settings"]
it return the valid real time object
Also settimeouts like that
mounted() {
setTimeout(() => {
console.log(this.presets);
console.log(this.settings);
}, 10000);
},
returns the valid object too.
Whats is my mistake?Can someone help me?
The setters of computed are returning. Instead, they should be committing.
Think of getter + setter computed as a collection of read (getter) and write (setter) functions.
Let's take one of the computed apart:
contragent_id: {
get() {
return appData.getters["basket/contragent_id"];
},
set(value) {
return value;
},
}
Let's consider a read operation:
const myValue = this.contragent_id
if the code would be able to describe what it does, it would be saying something like:
I read the value from appData.getters["basket/contragent_id"] (which returns the value of appData.state.summary.contragent_id) and store it in a constant named myValue.
Great! That's exactly what it should be doing. Now let's consider a write operation (a setter):
console.log(this.contragent_id) // Expect: ''; Receive: ''
this.contragent_id = '42'
console.log(this.contragent_id) // Expect: '42'. Receive: ''
the current code (return value) would be described by:
Here's "42"! I'm giving it back to you, I'm not doing anything with it.
It should be described by:
I write '42' into state.summary.contragent_id, by committing this value to the 'basket/changeContragentId' mutation.
, which would look like:
set(value) {
appData.commit('basket/changeContragentId', value)
}
Once the mutation is performed, because of it, the getter immediately updates and starts returning the current value of state.summary.contragent_id, via getters['basket/contragent_id'], which is now 42 (or whatever we assigned).
In other words, with the committing setter, the above code will produce:
console.log(this.contragent_id) // Expect: ''; Receive: ''
this.contragent_id = '42'
console.log(this.contragent_id) // Expect: '42'; Receive: '42'
So the local computed contragent_id can now be read from and written to. In fact, we'd be interacting with state.summary.contragent_id, via getters and mutations.
On a separate note, we shouldn't be using Vue.set() everywhere in our mutations.
We probably want to write the above mutation as:
changeContragentId(state, contragent_id) {
state.summary = { ...state.summary, contragent_id };
}
Why?
{ ...state.summary, contragent_id }
is shorthand for:
{ ...state.summary, contragent_id: contragent_id }
, which creates a fresh object from state.summary containing everything that state.summary contained, plus the passed argument contragent_id's value, assigned to the contragent_id property of this new object, regardless of whether or not the old state.summary had such a property or any value in it. By assignment, state.summary's value is replaced with this new object. This replacement is what notifies the getters which, in turn, update all components using them (or reading directly from state).
This is the key: replacing the root prop of the state.
If we would have used
state.summary.contragent_id = value
state.summary object wouldn't have been replaced, and the change would not be visible in <template>.
In some cases (when something else is changed at the same time), we might see changes made to deep properties, giving a wrong impression about how Vue works.
I see a lot of repetition in both mutations and getters. It's unnecessary.
For example, we could replace all state.summary related mutations with a single one:
mutations: {
updateSummary(state, update) {
state.summary = { ...state.summary, ...update }
}
}
and now we could replace all state.summary related setters in components computed with calls to this mutation. Example:
computed: {
contragent_id: {
get() { return appData.getters['basket/contragent_id'] },
set(contragent_id) {
appData.commit('basket/updateSummary', { contragent_id })
}
},
partner_id: {
get() { return appData.getters['basket/partner_id'] },
set(partner_id) {
appData.commit('basket/updateSummary', { partner_id })
}
},
delivery: {
get() { return appData.getters['basket/delivery'] },
set(delivery) {
appData.commit('basket/updateSummary', { delivery })
}
},
fillial: (state) => {
get() { return appData.getters['basket/fillial'] },
set(fillial) {
appData.commit('basket/updateSummary', { fillial })
}
}
}
, which could be written as:
computed: {
...Object.assign(
{},
...["contragent_id", "partner_id", "delivery", "fillial"].map((key) => ({
[key]: {
get() {
return appData.getters[`basket/${key}`];
},
set(value) {
appData.commit("basket/updateSummary", { [key]: value });
},
},
}))
),
};
This looks complicated, doesn't it?
It's useful, we could re-use it in any component where we want to get or set state.summary props and all we'd have to do is specify a different array of keys, depending on what we want to expose.
So let's write a function, taking in the array of keys and place it in some storeHelpers.js file:
import { appData } from './path/to/appData'
export const mapSummary = (keys) =>
Object.assign(
{},
...keys.map((key) => ({
[key]: {
get() {
return appData.getters[`basket/${key}`];
},
set(value) {
appData.commit("basket/updateSummary", { [key]: value });
},
},
}))
);
Now we can
import { mapSummary } from './path/to/storeHelpers'
export default {
computed: {
...mapSummary(["contragent_id", "partner_id", "delivery", "fillial"])
}
}
Similarly, you could do this for any object you want to update inside the state, so you wouldn't have to write a mutation for each of its properties. In your case, you probably want to have updatePresets and updatePresetsDelivery mutation functions and mapPresets & mapPresetsDelivery helpers, because both state.presets and state.presets.delivery are objects.
If your state has a lot of objects inside other objects, rather than writing one mutation and one mapper function for each individual object (which is still an improvement over writing a mutation for each individual object property), you might consider a single updateBasket mutation and one mapBasket helper which would also take the object's path as a parameter. Now, that mutation, having a dynamic path, would be a good use case for Vue.set().
Keep in mind nesting objects inside other objects inside a state is generally a sign of poor architecture. In your specific case, I'd probably go for three separate stores: one called basketSummary, one called basketPresets and one called basketPresetsDelivery (or basketDelivery).
Alternatively, you could go for nested stores, which would be be basket/summary, basket/presets and basket/presets/delivery, since you're using namespacing. Namespacing was actually developed to address cases like yours.
This subject is debatable (a lot of developers have strong opinions about store architecture) but, from my experience, having many smaller stores, each controlling a flat structure (a level: the properties of only one object) is generally preferable to having one big store, with lots of getters, actions and mutations, each having multiple levels of object spreading (or having to use Vue.set()).
It simplifies testing and debugging.
Ultimately, it allows more granular/precise control over complex data structures, considerably reducing the time spent developing (and debugging).
Readability in getters could also be improved:
getters: {
contragent_id: (state) => {
return state.summary && state.summary.contragent_id
? state.summary.contragent_id
: "";
}
}
could be written as:
getters: {
contragent_id: (state) => state.summary?.contragent_id || ""
}
I'm using the Composition API together with Vue 2 (by using #vue/composition-api) combined with the following two libraries (#vuelidate/core": "^2.0.0-alpha.18,
#vuelidate/validators": "^2.0.0-alpha.15).
I am trying to use sameAs to check whether the entered email and repeated email are a match and return an error if not. Although this is not working as smooth as expected.
This is my validation.js file
import { required, email, maxLength, sameAs } from "#vuelidate/validators";
export default {
email: {
required,
email,
maxLength: maxLength(100),
},
repeatEmail: {
required,
email,
maxLength: maxLength(100),
sameAsEmail: sameAs('email')
},
}
and this is my validation-errors.js file. (probably irrelevant for this question though)
export default {
email: [
{
key: "required",
message: "Email is required.",
id: "emailRequired",
},
{
key: "email",
message: "Wrong format on your email.",
id: "emailFormat",
},
{
key: "maxLength",
message: "Email can't be longer than 100 characters.",
id: "emailLength",
},
],
repeatEmail: [
{
key: "required",
message: "Email is required.",
id: "emailRequired",
},
{
key: "email",
message: "Wrong format on your email.",
id: "emailFormat",
},
{
key: "maxLength",
message: "Email can't be longer than 100 characters.",
id: "emailLength",
},
{
key: "sameAsEmail",
message: "Email isn't matching.",
id: "sameAsEmailFormat",
},
],
}
And this is how I try to use it in my component.
import validations from "#/validation";
import validationErrors from "#/validation-errors";
import { useVuelidate } from "#vuelidate/core";
import { reactive, toRefs } from '#vue/composition-api';
export default {
setup() {
const state = reactive({
email: "",
repeatEmail: "",
});
const $v = useVuelidate(validations, state);
return {
...toRefs(state)
}
}
}
So when I enter the same input in both the inputfield for email and repeatEmail correctly it gives me true as an invalid value.
Built-in validators such as sameAs don't have access to the state, so they aren't supposed to be workable when used like sameAs('email').
This way validators are supposed to be defined in setup function in order to access the state:
const emailRef = computed(() => state.email);
const validations = {
...
repeatEmail: {
...
sameAsEmail: sameAs(emailRef)
},
Otherwise this needs to be done with custom validators instead of built-ins that will access the state on component instance with getCurrentInstance.
if you can use the same validation in vuejs 3 with composition syntax because using compute function get update state
code password = computed(() => state.password);
const validations = {
...
confirm-password: {
...
sameAs: sameAs(emailRef)
},
How can I test the method createUser() of my Vue component? I want to test if the createUser() method throws an Error if the firstname < 2 for example. How is this possible?
I'm not really familiar with testing VUE components. It's my first time, so I have no idea how to get access the VUE component and how to submit for a example a username to the component
<script>
import {ApiService} from '../ApiService.js';
import {User} from '../User.js';
//const API_URL = 'http://localhost:8080';
const apiService = new ApiService();
export default {
name: "CreateUser",
data() {
return {
input: {
username: "",
firstname: "",
lastname: "",
title: "",
password: "",
groupId: "",
groups: [],
},
}
},
/.../
methods: {
getAllGroups() {
apiService.getAllGroups().then((data) => {
this.input.groups = data;
});
},
createUser() {
if (this.input.firstname == null || this.input.firstname.length < 2 || this.input.firstname > 50) {
throw ("Firstname to short/long/empty");
} else {
let user = new User(this.input.username, this.input.lastname, this.input.title, this.input.firstname, this.input.password, this.input.groupId)
apiService.createUser(user).then(() => {
location.reload()
});
}
},
I tried the following, but something doesn't not work
import { shallowMount } from '#vue/test-utils';
import UserModal from "../src/views/UserModal";
describe('UsarModal', () => {
it('should throw error when first name is too short', () => {
const myItems = [
{
username: "Heinz",
firstname: "H",
lasname: "Müller"}
]
const wrapper = shallowMount(UserModal, {
input: {
myItems
}
})
expect(wrapper.vm.createUser()).toThrow("Firstname to short/long/empty")
})
})
since in the code, it is throwing an error, so we will need to add a catch block in our test case to test this scenario. PFB example for your case:
try {
wrapper.vm.createUser();
} catch (error) {
expect(error).toBe('Firstname to short/long/empty');
}
let me know if you face any issue.
What you did in your example is very close. You just need to wrap the function call that is going to throw the exception in a lambda, e.g.
expect(() => wrapper.vm.createUser()).toThrow("Firstname to short/long/empty")
As described in the docs: https://jestjs.io/docs/expect#tothrowerror
It's probably doing something like internally wrapping the lambda in a try/catch, but I think using this method is a bit nicer than wrapping in a try/catch in your own test.
Thanks for the tip! But still something is not working right. It seems for me, that it does not accept my mock-data for input. I want to test if a got an error when I use a username which is too short. But even when I put a username that's long enough, the try catch block catches my error as you can see in the attached picture.
import {shallowMount} from '#vue/test-utils'
import UserModal from "./UserModal";
describe('ParentComponent', () => {
test("displays 'Emitted!' when custom event is emitted", () => {
const wrapper = shallowMount(UserModal, {
input: {
username: "asfsf",
firstname: "thomas",
lastname: "bird",
title: "Dr.",
password: "asdasda",
groupId: "2",
groups: [],}
});
try {
wrapper.vm.createUser();
} catch (error) {
expect(error).toBe("username missing");
}
})
});
Error Log