Vue.js & vuex - Reset a form to current state - vuejs2

I have a form in one of my components that I'm initially populating by cloning a piece of state using:
created () {
this.currentUser = Object.assign({}, this.$store.getters.user)
}
I want to add a button to the form that enables the user to reset it back to it's initial value after they have made changes.
I've tried to do this by adding a function to a button like so:
methods: {
cancelChanges () {
this.currentUser = Object.assign({}, this.$store.getters.user)
}
}
However, this isn't working. What's the correct way to do this?
Codepen here - https://codepen.io/stockzy/pen/xPGYXN?editors=1010#0

Okay, I figured it out (with a little help).
I was binding my data to the store, instead of copying a new instance to the component. This fixed it:
created () {
this.currentUser = Object.assign({}, JSON.parse(JSON.stringify(this.$store.getters.user)))
}
and the same for the button:
cancelChanges () {
this.currentUser = Object.assign({}, JSON.parse(JSON.stringify(this.$store.getters.user)))
}

Related

Computed property react to localstorage change

I'm saving an array into local storage
and adding/removing from the array like.
I want the count of the array to update in the component as and when new items get added to the array in localstorage
I am using a computed property:
numOfCodes: {
// getter
get: function() {
let storageItems = localStorage.getItem("items");
if (storageItems) {
var items = JSON.parse(storageItems);
return items.length;
}
return 0;
}
}
The count is not changing as expected. it remains the same.
I have tried using vuex, but still have the issue. the goal is having the value react to the localstorage change
I think a solution to this would be to use vuex, I've mocked up an example below:
On your component:
computed: {
...mapGetters({
itemsCount: 'mockLocalStorage/itemsCount'
})
},
created() {
this.setItems(...);
},
methods: {
...mapActions({
setItems: 'mockLocalStorage/setItems'
})
}
In vuex:
state = {
items: []
};
getters = {
itemsCount: state => state.items.length
};
actions: {
setItems({ commit }, items) {
localStorage.setItem('items', items);
commit('setItems', items);
}
};
this.itemsCount would then be reactive in your component, and you could create a few more actions to add and remove individual items.
The localStorage does not share the reactivity system of Vue. This whole process is handled by Vue itself. See also here. I think you should be able to manually trigger a re-render by forcing Vue to update all of its components using forceUpdate. However, keep in mind that you would have to trigger the re-render whenever you update the localStorage or whenever you expect it to be updated.
Use a watcher.
props: ['storageItems', 'itemsLength'],
watch: {
storageItems: function(newVal, oldVal) {
this.storageItems = newVal
this.itemsLength = newVal.length
}
}

Validate form input fields in child component from a parent component with Vuelidate

I am new to Vue Js and Vuelidate. Just tried to validate form input fields from a parent component like here: https://github.com/monterail/vuelidate/issues/333
Child component in the parent:
<contact-list ref="contactList" :contacts="contacts" #primaryChanged="setPrimary" #remove="removeContact" #ready="isReady => readyToSubmit = isReady"/>
The method in the child:
computed: {
ready() {
return !this.$v.email.$invalid;
}
},
watch: {
ready(val) {
this.$emit('ready', val);
}
},
methods: {
touch() {
this.$v.email.$touch();
}
}
I'm calling the touch() method from the parent like so:
submit() {
this.$refs.contactList.touch();
},
But I get this error:
Error in event handler for "click": "TypeError: this.$refs.contactList.touch is not a function".
Any ideas? Thanks.
I was facing the same problem. Here is what I have done to solve it.
Created a global event pool. Where I can emit events using $emit and my child can subscribe using $on or $once and unsubscribe using $off. Inside your app.js paste the below code. Below is the list of event pool actions.
Emit: this.$eventPool.$emit()
On: this.$eventPool.$on()
Off: this.$eventPool.$off()
once: this.$eventPool.$once()
Vue.prototype.$eventPool = new Vue();
Inside my child components, I have created a watch on $v as below. Which emits the status of the form to the parent component.
watch: {
"$v.$invalid": function() {
this.$emit("returnStatusToParent", this.$v.$invalid);
}
}
Now inside you parent component handle the status as below.
<ChildComponent #returnStatusToParent="formStatus =>isChildReady=formStatus"></ChildComponent>
Now to display the proper errors to the users we will $touch the child form. For that, we need to emit an event in the above-created event pool and our child will subscribe to that.
parent:
this.$eventPool.$emit("touchChildForm");
child:
mounted() {
this.$eventPool.$on("touchChildForm", () => {
this.$v.$touch();
this.$emit("returnStatusToParent", this.$v.$invalid);
});
},
destroyed() {
this.$eventPool.$off("touchChildForm", () => `{});`
}
Hope it helps :)
I'm adding my answer after this question already has an accepted solution, but still hope it might help others. I have been at this for the entire week. None of the above solutions work for my scenario because there are children components nested 2 levels deep so the "ref" approach won't work when I need the utmost parent component to trigger all validations and be able to know if the form is valid.
In the end, I used vuex with a fairly straightforward messages module. Here is that module:
const state = {
displayMessages: [],
validators: []
};
const getters = {
getDisplayMessages: state => state.displayMessages,
getValidators: state => state.validators
};
const actions = {};
const mutations = {
addDisplayMessage: (state, message) => state.displayMessages.push(message),
addValidator: (state, validator) => {
var index = 0;
while (index !== -1) {
index = _.findIndex(state.validators, searchValidator => {
return (
searchValidator.name == validator.name &&
searchValidator.id == validator.id
);
});
if (index !== -1) {
console.log(state.validators[index]);
state.validators.splice(index, 1);
}
}
state.validators.push(validator);
}
};
export default {
state,
getters,
actions,
mutations
};
Then each component has this in its mounted event:
mounted() {
this.addValidator( {name: "<name>", id: 'Home', validator: this.$v}) ;
}
Now when a user clicks the "Submit" button on the home page, I can trigger all validations like so:
this.getValidators.forEach( (v) => {
console.log(v);
v.validator.$touch();
});
I can just as easily check the $error, $invalid properties of the vuelidate objects. Based on my testing, the vuelidate reactivity remains intact so even though the objects are saved to vuex, any changes on the component fields are reflected as expected.
I plan to leave the messages and styling to convey the errors in the gui to the components themselves, but this approach lets me pause the form submission when an error occurs.
Is this a good thing to do? I honestly have no idea. The only hokey bit if having to remove validators before adding them. I think that's more an issue with my component logic, than an issue with this as a validation solution.
Given that this has taken me a whole week to arrive at, I'm more than happy with the solution, but would welcome any feedback.
Had a similar issue trying to validate child components during a form submission on the parent component. My child components are only one level deep so if you have deeper nesting this way may not work or you have to check recursively or something. There are probably better ways to check but this worked for me. Good luck.
// parent component
methods: {
onSave() {
let formIsInvalid = this.$children.some(comp => {
if (comp.$v) { // make sure the child has a validations prop
return comp.$v.$invalid
}
})
if (!formIsInvalid) {
// submit data
}
else {
// handle invalid form
}
}
I have found another solution for this validation, it's very simple. Child component in the parent:
<contact-list ref="customerContacts" :contacts="customer.contacts" />
Validations in child component:
:validator="$v.model.$each[index].name
...
validations: {
model: {
required,
$each: {
name: {
required
},
email: {
required,
email
},
phone: {
required
}
}
}
}
And on submit in the parent:
async onSubmit() {
if(this.$refs.customerContacts.valid())
...

Make Vue template wait for global object returned by AJAX call

I'm trying to wait for certain strings in a sort of dictionary containing all the text for buttons, sections, labels etc.
I start out by sending a list of default strings to a controller that registers all the strings with my CMS in case those specific values do not already exist. After that I return a new object containing my "dictionaries", but with the correct values for the current language.
I run the call with an event listener that triggers a dispatch() on window.onload, and then add the data to a Vuex module state. I then add it to a computed prop.
computed: {
cartDictionary() {
return this.$store.state.dictionaries.myDictionaries['cart']
}
}
So now here's the problem: In my template i try to get the values from the cartDictionaryprop, which is an array.
<h2 class="checkout-section__header" v-html="cartDictionary['Cart.Heading']"></h2>
But when the component renders, the prop doesn't yet have a value since it's waiting for the AJAX call to finish. And so of course I get a cannot read property of undefined error.
Any ideas on how to work around this? I would like to have the dictionaries accessible through a global object instead of passing everything down through props since it's built using atomic design and it would be insanely tedious.
EDIT:
Adding more code for clarification.
My module:
const dictionaryModule = {
namespaced: true,
state: {
dictionaries: []
},
mutations: {
setDictionaries (state, payload) {
state.dictionaries = payload
}
},
actions: {
getDictionaries ({commit}) {
return new Promise((resolve, reject) => {
Dictionaries.init().then(response => {
commit('setDictionaries', response)
resolve(response)
})
})
}
}
}
My Store:
const store = new Vuex.Store({
modules: {
cart: cartModule,
search: searchModule,
checkout: checkoutModule,
filter: filterModule,
product: productModule,
dictionaries: dictionaryModule
}
})
window.addEventListener('load', function () {
store.dispatch('dictionaries/getDictionaries')
})
I think you can watch cartDictionary and set another data variable.
like this
<h2 class="checkout-section__header" v-html="cartHeading"></h2>
data () {
return {
cartHeading: ''
}
},
watch: {
'cartDictionary': function (after, before) {
if (after) {
this.cartHeading = after
}
}
}
Because this.$store.state.dictionaries.myDictionarie is undefined at the the begining, vuejs can't map myDictionarie['core']. That's why your code is not working.
You can do this also
state: {
dictionaries: {
myDictionaries: {}
}
}
and set the dictionaries key values during resolve.
I also would have liked to see some more of your code, but as i can't comment your questions (you need rep > 50), here it goes...
I have two general suggestions:
Did you setup your action correctly? Mutations are always synchronous while actions allow for asynchronous operations. So, if you http client returns a promise (axios does, for example), you should await the result in your action before calling the respective mutation. See this chapter in the official vuex-docs: https://vuex.vuejs.org/guide/actions.html
You shouldn't be using something like window.onload but use the hooks provided by Vue.js instead. Check this: https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram
EDIT: As a third suggestion: Check, whether action and mutation are called properly. If they are handled in their own module, you have to register the module to the state.

Which Lifecycle hook after axios get but before DOM render

I'm trying to render my DOM, dependent on some data I'm returning from an axios get. I can't seem to get the timing right. The get is in the created hook, but there is a delay between the get and actually receiving the data. Basically if there is info in seller_id then I need to show the cancel button, otherwise don't. Here is my code:
this is in my created hook
axios.get('https://bc-ship.c9users.io/return_credentials').then(response => {
this.seller_id = response.data.seller_id;
this.selected_marketplace = response.data.marketplace;
this.token = response.data.auth_token;
});
and then this is the logic to show or hide the button. I've tried created, mounted, beforeUpdate, and updated all with no luck. I've also tried $nextTick but I can't get the timing correct. This is what I have currently:
beforeUpdate: function () {
// this.$nextTick(function () {
function sellerIdNotBlank() {
var valid = this.seller_id == '';
return !valid;
}
if(sellerIdNotBlank()){
this.show_cancel_button = true;
}
// })
},
First, it is pointless to get your data from backend and try to sync with Vue.js lifecycle methods. It never works.
Also, you should avoid beforeUpdate lifecycle event. It is often a code smell. beforeUpdate is to be used only when you have some DOM manipulations done manually and you need to adjust them again before Vue.js attempt to re-render.
Further, show_cancel_button is a very good candidate for a computed property. Here is how component will look:
const componentOpts = {
data() {
return {
seller_id: '',
// ... some more fields
};
},
created() {
axios.get('https://bc-ship.c9users.io/return_credentials').then(response => {
this.seller_id = response.data.seller_id;
this.selected_marketplace = response.data.marketplace;
this.token = response.data.auth_token;
});
},
computed: {
show_cancel_button() {
return this.seller_id !== '';
}
}
}

Run method before route

I have a login modal that I activate by setting .is-active to it. For this, I have a method like this:
methods: {
toggleModal: function (event) {
this.isActive = !this.isActive
}
}
that I run onclick. Depending on the boolean value of isActive, my modal gets the class .is-active.
Thing is, in my modal I have a button that takes the user to a new view which means it's rendering a new component, with this code:
<router-link class="control" #click="toggleModal()" to="/register">
As you can see, it's routing to /register. Before doing this, I need to run toggleModal() so that the modal gets closed. Right now it's routing without running the method which means that the new view has my modal overlay which is... not optimal.
Is there any good way to do this in Vue? Could I maybe create a method, that first calls toggleModal(), and then routes from the method?
Thanks.
I would define a method that calls toggleModal first, then navigates. Like so:
methods: {
navigateAway () {
this.isActive = !this.isActive
this.$router.push('/register')
}
}
You don't need the event argument unless you intend on capturing more data from the event or event target. You could also wrap the router push in a setTimeout if you so desire, for perhaps cleaner looking view changes.
methods: {
navigateAway () {
let vm = this
vm.isActive = !vm.isActive
setTimeout(function () {
vm.$router.push('/register')
}, 50)
}
}
Of course, there are hooks that you can use from vue-router that make this easy. Example (assuming you're using single file components and Vue.js 2.x):
export default {
data () {
return {
isActive: false
}
},
beforeRouteLeave (to, from, next) {
this.isActive = false // I assume that you would not want to leave it open upon navigating away
next()
}
}
Link to vue router hooks: https://router.vuejs.org/en/advanced/navigation-guards.html