I need to activate a modal component from vuex store. I was using 'this.$refs['modalSuccess'].show()' to show the modal inside the component when the result API was successed!
But I needed to change the function 'sendLeadResponse' from methods (component) to action (store). After that, I cannot activate the modal anymore with this 'this.$refs['modalSuccess'].show()'.
Is there any way to call it from a store?
This is the following flow:
Button: activate a method inside the component;
Method: activate an action from store;
Action: it uses a external API;
Modal: If the result is ok it activates a modal which it is inside the component;
COMPONENT WITH BUTTON AND THE MODAL
<template>
<section>
<div class="w-100 d-md-flex justify-content-md-end">
<SmallButton
smallButtonText="Quero ser cliente →"
#event="createLeadObject()"
id="show-btn"
/>
</div>
<b-modal
ref="modalSuccess"
ok-only
> Obrigado pelo interesse! Em breve entraremos em contato.
</b-modal>
</div>
</section>
</template>
<script>
import SmallButton from '../SmallButton.vue'
export default {
name: 'BeClientForm',
components: {
SmallButton
},
methods: {
createLeadObject(){
const dataLeadObject = {
date: new Date(),
fullName: this.lead.name,
email: this.lead.email,
phone: this.lead.phone,
comment: this.lead.comment
}
this.$store.dispatch('sendLeadResponse', dataLeadObject)
},
}
}
</script>
ACTION FROM STORE
actions: {
async sendLeadResponse({commit}, dataLeadObject){
const jsonDataObject = JSON.stringify(dataLeadObject)
await fetch("http://localhost:5000/api/lead/leadResponse", {
method: "POST",
headers: {"Content-type": "application/json"},
body: jsonDataObject
})
.then((resp) => resp.json())
.then((data) => {
if (data.error) {
commit('MESSAGE_RESPONSE', data.error)
}
else {
commit('RESET_LEAD_RESPONSE')
!!!!!!!!!!!!! this.$refs['modalSuccess'].show() !!!!!!!!!!!!!! [it is not working)
}
})
},
}
The Vuex store is designed to only care about the state. It does not have direct access to your components or this.$refs. What you can do is set a piece of state in your store based on the result of your fetch and have your component access that state, and/or return a promise from your action so the result is handed directly back to your component
async sendLeadResponse({ commit }, dataLeadObject) {
const jsonDataObject = JSON.stringify(dataLeadObject);
// assign promise from fetch
const response = await fetch('http://localhost:5000/api/lead/leadResponse', {
method: 'POST',
headers: { 'Content-type': 'application/json' },
body: jsonDataObject
})
.then(resp => resp.json())
.then(data => {
if (data.error) {
commit('MESSAGE_RESPONSE', data.error);
// promise to resolve to false
return false;
} else {
commit('RESET_LEAD_RESPONSE');
// promise to resolve to true
return true;
}
});
// return promise
return response
},
// change to async
async createLeadObject() {
const dataLeadObject = {
date: new Date(),
fullName: this.lead.name,
email: this.lead.email,
phone: this.lead.phone,
comment: this.lead.comment
};
const response = await this.$store.dispatch('sendLeadResponse', dataLeadObject);
// if response is 'true', show modal
if (response) {
this.$refs['modalSuccess'].show();
}
}
Related
I'm trying to call an API and once I have the response I want to target the image_url property but I'm getting this error Error in render: "TypeError: Cannot read property 'image_url' of undefined" and Cannot read property 'image_url' of undefined at VueComponent.getImages1 (Info.vue?42ed:39)
I've tested my code in another Javascript file and over there it works but for some reason not here. I've also checked for if the parent state is sending data correctly to child state through Vue console and it has worked.
Info.vue
<template>
<section>
<img :src="getImages1()" alt="./assets/notFound.png" />
<img :src="getImages2()" alt="./assets/notFound.png" />
</section>
</template>
<script>
import axios from "axios";
export default {
props: {
anime1: String,
anime2: String,
},
methods: {
animeFind(anime) {
axios
.get(`https://api.jikan.moe/v3/search/anime?q=${anime}`)
.then(async function(response) {
const id = await response.data["results"][0]["mal_id"];
await axios
.get(`https://api.jikan.moe/v3/anime/${id}`)
.then(function(response) {
return response.data;
})
.catch(function(error) {
return error; // take care of this later
});
})
.catch(function(error) {
return error; // take care of this later
});
},
// eslint-disable-next-line no-unused-vars
getImages1() {
let response = this.animeFind(this.anime1);
return response["image_url"];
},
getImages2() {
let response = this.animeFind(this.anime2);
return response["image_url"];
},
},
};
</script>
<style></style>
I tried doing this and it worked
main.js
const axios = require("axios");
const animeFind = (anime) =>
axios
.get(`https://api.jikan.moe/v3/search/anime?q=${anime}`)
.then(async function (response) {
const id = await response.data["results"][0]["mal_id"];
await axios
.get(`https://api.jikan.moe/v3/anime/${id}`)
.then(function (response) {
console.log(response.data["image_url"]);
})
.catch(function (error) {
console.log(error);
});
})
.catch(function (error) {
console.log(error);
});
animeFind("Naruto");
animeFind("Cowboy Bebop");
This is the parent component, when the button is clicked only then should the image change
<template>
<section class="hero">
<div class="parent-1">
<h1 class="title is-1">Compare two animes! :)</h1>
</div>
<div class="columns">
<div class="column">
<b-field class="label" label="Anime 1">
<b-input value="Enter the first anime!" v-model="anime1"></b-input>
</b-field>
</div>
<div class="column">
<b-field class="label" label="Anime 2">
<b-input value="Enter the second anime!" v-model="anime2"></b-input>
</b-field>
</div>
</div>
<div class="button-spacing">
<b-button class="button" type="is-primary" #click="checkComplete"
>Compare!</b-button
>
</div>
<Info :anime1="anime1" :anime2="anime2" v-if="success">Wow</Info>
</section>
</template>
<script>
import Vue from "vue";
import Buefy from "buefy";
import "buefy/dist/buefy.css";
import Info from "./Info.vue";
Vue.use(Buefy);
export default {
components: {
Info,
},
data() {
return {
anime1: "",
anime2: "",
success: false,
};
},
methods: {
// log() {
// console.log(this.anime1);
// console.log(this.anime2);
// },
checkComplete() {
if (this.anime1.length > 0 && this.anime2.length > 0) {
// let animeData1 = this.animeFind(this.anime1);
// let animeData2 = this.animeFind(this.anime2);
this.success = true;
return this.$buefy.toast.open({
message: "Yay, just a moment now!",
type: "is-success",
position: "is-bottom",
duration: 3000,
});
}
this.success = false;
return this.$buefy.toast.open({
duration: 3000,
message: `Please fill out both fields`,
position: "is-bottom",
type: "is-danger",
});
},
},
};
</script>
I think you're still a little confused with promises. your animFind function is not returning anything.
Instead try
<template>
<section>
<img :src="url1" alt="./assets/notFound.png" />
<img :src="url2" alt="./assets/notFound.png" />
</section>
</template>
<script>
import axios from "axios";
export default {
props: {
anime1: String,
anime2: String,
},
data() {
return {
url1: '',
url2: '',
error: ''
}
},
methods: {
animeFind(anime, data) {
axios
.get(`https://api.jikan.moe/v3/search/anime?q=${anime}`)
.then(response => {
const id = response.data["results"][0]["mal_id"];
axios
.get(`https://api.jikan.moe/v3/anime/${id}`)
.then(response => this[data] = response.data["image_url"]);
})
.catch(error => {
this.error = error; // take care of this later
});
}
},
watch: {
anime1: {
immediate: true,
handler(newVal, oldVal) {
this.animeFind(newVal, 'url1');
},
},
anime2: {
immediate: true,
handler(newVal, oldVal) {
this.animeFind(newVal, 'url2');
},
},
},
};
</script>
Notice the use if arrow functions to stay in the vue scope
The getImages() function return before the animeFind() would return. So the getImages() will return undefined.
You can put the axios call into hooks and when you return the response.data object, you can assign it to a property in the data object. You use this property instead the function in the template, so the component will be reactive.
Notice that you should use regular function on the outer function in the axios call and arrow functions on the then() responses for getting a proper this.
I am taking care of only one image example for simplicity, but editing this is not so complicated.
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template>
<section>
<p>Show image here</p>
<img :src="urlResponse['image_url']" alt="./assets/notFound.png">
</section>
</template>
<script>
import axios from "axios";
export default {
data() {
return {
urlResponse: {}
};
},
props: {
anime1: String,
anime2: String
},
created() {
this.animeFind(this.anime1);
},
updated() {
this.animeFind(this.anime1);
},
methods: {
animeFind: function(anime) {
axios
.get(`https://api.jikan.moe/v3/search/anime?q=${anime}`)
.then(async response => {
const id = await response.data["results"][0]["mal_id"];
await axios
.get(`https://api.jikan.moe/v3/anime/${id}`)
.then(response => {
this.urlResponse = Object.assign(
{},
this.urlResponse,
response.data
);
return response.data;
})
.catch(function(error) {
return error; // take care of this later
});
})
.catch(function(error) {
return error; // take care of this later
});
}
}
};
</script>
<style></style>
Im using vuex and I have an action
storeExpense(context, params){
axios.post('api/expenses', params)
.then( response => {
console.log("Expense Created");
})
.catch( error => {
console.log(error);
});
}
and on my Expense.vue im using the action via
this.$store.dispatch('storeExpense',this.expense)
.then( response => {
this.modalShow = false
this.$swal(
'Success',
'Expense has been created!',
'success'
)
})
I dont have an error but after the expense was created the state is not updating therefore I need to refresh the page in order for my table to get the latest data.
I have a mutation called
mutateExpenses(state, payload){
state.expenses = payload
}
however when i use this after the response it overrides the whole state.expenses object to a single object because this.expense is a single object
Im new to vuex.
You must update your store using mutations that are called inside your actions.
I suggest you to dive a bit into the Vuex documentation, especially the mutations and actions :)
Here is an example of how to use the store :
It goes dispatch --> action --> mutation
// Your store
const store = new Vuex.Store({
state: {
posts: [],
isLoading: false
},
mutations: {
// Must be called by actions AND ONLY by actions
add(state, post) {
// Add the given post to the 'posts' array in our state
Vue.set(state.posts, state.posts.length, post)
},
busy(state) {
Vue.set(state, 'isLoading', true)
},
free(state) {
Vue.set(state, 'isLoading', false)
}
},
actions: {
create({
commit
}, post) {
commit('busy')
axios.post('https://jsonplaceholder.typicode.com/posts', post)
.then(response => {
// Call the mutation method 'add' to add the newly created post
commit('add', response.data)
})
.catch((reason) => {
// Handle errors
})
.finally(() => {
commit('free')
});
},
}
})
// Your Vue app
new Vue({
el: "#app",
store,
data: {
post: {
title: 'foo',
body: 'bar',
userId: 1
}
},
methods: {
onButtonClicked() {
this.$store.dispatch('create', this.post)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.1.0/vuex.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<div id="app">
<button #click="onButtonClicked">Create Post</button>
<div>Posts : <span v-if="$store.state.isLoading">Requesting</span></div>
<div v-for="post in $store.state.posts">
{{post}}
</div>
</div>
I'm using vuex to manage the state in my application and doing one way binding with my form.
<script>
import { mapGetters } from 'vuex'
import store from 'vuex-store'
import DataWidget from '../../../../uiComponents/widget'
export default {
data () {
return {
isEdit: false,
msg: {
id: 0,
content: '',
isEnabled: false
}
}
},
components: {
DataWidget
},
computed: mapGetters({
messageId: 'messageId',
messageContent: 'messageContent',
isMessageEnabled: 'isMessageEnabled',
isMessageValid: 'isMessageValid'
}),
methods: {
onSave () {
store.dispatch('saveMessage', this.msg, { root: true })
if (this.isMessageValid) {
this.isEdit = !this.isEdit
}
}
},
created () {
this.msg.id = this.messageId
this.msg.content = this.messageContent
this.msg.isEnabled = this.isMessageEnabled
}
}
</script>
<b-form-textarea id="content" v-model="msg.content" :rows="3" required aria-required="true" maxlength="250"></b-form-textarea>
On load, the values on created() are not binded until I perform an action on the page or refresh the page.
I have tried mounted () hooked same thing.
My Vuex store (Message Module) looks like this:
const state = {
messageId: 0,
messageContent: '',
isMessageEnabled: false,
isMessageValid: true
}
const getters = {
messageId: state => state.messageId,
messageContent: state => state.messageContent,
isMessageEnabled: state => state.isMessageEnabled,
isMessageValid: state => state.isMessageValid
}
const actions = {
getMessage ({commit, rootGetters}) {
api.fetch('api/Preference/Message', rootGetters.token)
.then((data) => {
commit(types.MESSAGE_LOAD, data)
})
}
}
const mutations = {
[types.MESSAGE_LOAD] (state, payload) {
state.messageId = payload ? payload.id : 0
state.messageContent = payload ? payload.content : ''
state.isMessageEnabled = payload ? payload.enabled : false
}
}
export default {
state,
getters,
actions,
mutations
}
and I have a global action (action.js) the gets multiple data:
export const loadSetting = ({ commit, rootGetters }) => {
api.fetchAsync('api/Preference/all', rootGetters.token)
.then((data) => {
commit(types.MESSAGE_LOAD, data.message)
commit(types.HELPDESK_LOAD, data.helpDesk)
commit(types.VOLUME_LOAD, data.volumes)
commit(types.DOWNLOAD_LOAD, data.downloadService)
})
}
My api call:
async fetchAsync (url, token = '') {
let data = await axios.get(HOST + url, {
headers: {
'Authorization': 'bearer ' + token
}
})
return data
}
The problem is your'e calling an async method in Vuex but in the created method, you're treating it like a sync operation and expect to get a value.
You need to use the computed properties you created since they are reactive and will update on every change. In order to make the computed writeable change it to be like this:
computed: {
...mapGetters({
messageId: 'messageId',
isMessageEnabled: 'isMessageEnabled',
isMessageValid: 'isMessageValid'
}),
messageContent(){
get () {
return this.$store.getters.messageContent
},
set (value) {
//this is just an example, you can do other things here
this.$store.commit('updateMessage', value)
}
}
}
And change the html to use messageContent:
<b-form-textarea id="content" v-model="messageContent" :rows="3" required aria-required="true" maxlength="250"></b-form-textarea>
For more info refer to this: https://vuex.vuejs.org/en/forms.html
I'm stuck at a crossroads with a component I am working on.
I have the following component "RecentUpdates"
Within it I am passing props down to a few other components, as you can see from the top of the file.
My problem is when adding a new post, I can not figure out how to get the correct update object array back and i also can not figure out the correct 'Vue way' to update the data prop that is being passed down to the "PostList" component.
<template>
<div>
<PostFilter v-on:selectedCategory="getSelectedPosts" v-on:showAllPosts="showAllPosts" :user="user" :categories="categories"/>
<PostList v-if="recent_posts[0]" :categories="categories" :posts="recent_posts[0]" :user="user"/>
<Pagination v-on:getPreviousPage="getPreviousPage" v-on:getNextPage="getNextPage"/>
</div>
</template>
<script>
import PostList from './PostList';
import PostFilter from './PostFilter';
import Pagination from './Pagination';
import EventBus from '../event-bus';
export default {
name: 'RecentUpdates',
data: () => ({
errors: [],
recent_posts: [],
}),
props: ['categories', 'user'],
components: {
PostList,
PostFilter,
Pagination
},
created() {
if (this.user.meta.selected_categories[0] == 0) {
this.showAllPosts();
}
// do not call here, not working as expected
// is switching selected category to an incorrect one
// this.updateList();
this.getSelectedCategory();
},
watch: {
recent_posts: function(newValue) {
EventBus.$on('addPost', function(newPost) {
console.log(newPost);
this.$forceUpdate();
//this.recent_posts.push(newPost);
//this.$set(this.recent_posts, newPost, newPost);
// this.$nextTick(function () {
// this.recent_posts.push(newPost);
// });
});
console.log(this.recent_posts[0]);
// this.$nextTick(function () {
// console.log(this.recent_posts[0]) // => 'updated'
// });
// if (this.user.meta.selected_categories[0] == 0) {
// EventBus.$on('addPost', this.showAllPosts);
// } else {
// EventBus.$on('addPost', this.getSelectedCategory);
// }
//this.updateList();
}
},
methods: {
// updateList() {
// if (this.user.meta.selected_categories[0] == 0) {
// EventBus.$on('addPost', this.showAllPosts);
// //EventBus.$emit('newPost');
// } else {
// EventBus.$on('addPost', this.getSelectedCategory);
// //EventBus.$emit('newPost');
// }
// },
getSelectedCategory() {
let categoryId = this.user.meta.selected_categories[0];
this.getSelectedPosts(categoryId);
},
showAllPosts() {
axios.get('/wp-json/wp/v2/posts?_embed=true&status=[publish,resolved,unresolved]',
{headers: {'X-WP-Nonce': portal.nonce}})
.then(response => {
this.recent_posts = [];
//this.recent_posts = response.data;
//console.log(response.data);
this.recent_posts.push(response.data);
console.log(this.recent_posts[0]);
})
.catch(e => {
this.errors.push(e);
});
},
getSelectedPosts(categoryId) {
axios.get('/wp-json/wp/v2/posts?_embed=true&status=[publish,resolved,unresolved]&categories=' + categoryId,
{headers: {'X-WP-Nonce': portal.nonce}})
.then(response => {
this.recent_posts = [];
//console.log(response.data);
this.recent_posts.push(response.data);
console.log(this.recent_posts[0]);
})
.catch(e => {
this.errors.push(e);
});
},
/**
* Pagination methods
*
*/
getPreviousPage(page) {
axios.get('/wp-json/wp/v2/posts?_embed=true&status=[publish,resolved,unresolved]&page=' + page,
{headers: {'X-WP-Nonce': portal.nonce}})
.then(response => {
this.recent_posts = response.data;
})
.catch(e => {
this.errors.push(e);
});
},
getNextPage(page) {
axios.get('/wp-json/wp/v2/posts?_embed=true&status=[publish,resolved,unresolved]&page=' + page,
{headers: {'X-WP-Nonce': portal.nonce}})
.then(response => {
this.recent_posts = response.data;
})
.catch(e => {
this.errors.push(e);
});
}
},
}
</script>
<style>
</style>
So there are a number of issues I see reading through your code.
You have a recent_posts data property, which is an array. When you make your ajax call to get the posts you push the response which is also an array into the recent_posts array. Why? Why not just set recent_posts = response.data? Then you won't have to be passing recent_posts[0] around.
You're setting up your EventBus handler inside a watcher. This is really unusual. Typically you would set up a handler inside created or mounted.
this inside the EventBus handler likely refers to the EventBus and not your Vue. Ideally, you would set the handler to be a method on the component, which is already bound to the Vue. Something like EventBus.$on("addPost", this.addPost).
Once you've done all that, adding a new post should be as simple as this.recent_posts.push(newPost).
Here is what I might recommend.
export default {
name: 'RecentUpdates',
data(){
return {
errors: [],
recent_posts: []
}
},
props: ['categories', 'user'],
components: {
PostList,
PostFilter,
Pagination
},
created() {
if (this.user.meta.selected_categories[0] == 0) {
this.showAllPosts();
}
this.getSelectedCategory();
EventBus.$on("addPost", this.addPost)
},
beforeDestroy(){
EventBus.$off("addPost", this.addPost)
},
methods: {
getPosts(url){
axios.get(url, {headers: {'X-WP-Nonce': portal.nonce}})
.then(response => this.recent_posts = response.data)
.catch(e => this.errors.push(e))
},
showAllPosts() {
const url = '/wp-json/wp/v2/posts?_embed=true&status=[publish,resolved,unresolved]';
this.getPosts(url);
},
getSelectedPosts(categoryId) {
const url = '/wp-json/wp/v2/posts?_embed=true&status=[publish,resolved,unresolved]&categories=' + categoryId;
this.getPosts(url);
},
addPost(newPost){
this.recent_posts.push(newPost)
},
... //other methods
},
}
Try using kebab-case in your event listeners instead of camelCase:
Example: v-on:selectedCategory="getSelectedPosts" should be v-on:selected-category="getSelectedPosts".
Example: v-on:showAllPosts="showAllPosts" should be v-on:show-all-posts="showAllPosts" or even using the shortcut #show-all-posts="showAllPosts".
UPDATE: If you can provide the code of the other components so we can have a clearer vision of your problem, But you only want to track changes that happens on an object or an array in vue.js you need to deep watch them.
your watcher should be :
watch: {
recent_posts: {
deep: true,
handler: function( oldValue, newValue) {
console.log( "recent_posts has changed" );
// A post has been added, updated or even deleted
}
}
}
I got a issue here with methods firing not in the correct order.
I can't figure out how to make the this.props.history.pushState(null, '/authors'); wait in the saveAuthor() method.
Help will be greatly appreciated.
import React, { Component } from 'react';
import AuthorForm from './authorForm';
import { History } from 'react-router';
const source = 'http://localhost:3000/authors';
// History Mixin Component Hack
function connectHistory (Component) {
return React.createClass({
mixins: [ History ],
render () {
return <Component {...this.props} history={this.history}/>
}
})
}
// Main Component
class ManageAuthorPage extends Component {
state = {
author: { id: '', firstName: '', lastName: '' }
};
setAuthorState(event) {
let field = event.target.name;
let value = event.target.value;
this.state.author[field] = value;
return this.setState({author: this.state.author});
};
generateId(author) {
return `${author.firstName.toLowerCase()}-${author.lastName.toLowerCase()}`
};
// Main call to the API
postAuthor() {
fetch(source, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: this.generateId(this.state.author),
firstName: this.state.author.firstName,
lastName: this.state.author.lastName
})
});
};
// Calling Save author method but the this.props.history goes first rather than this.postAuthor();
saveAuthor(event) {
event.preventDefault();
this.postAuthor();
this.props.history.pushState(null, '/authors');
};
render() {
return (
<AuthorForm
author={this.state.author}
onChange={this.setAuthorState.bind(this)}
onSave={this.saveAuthor.bind(this)}
/>
);
}
}
export default connectHistory(ManageAuthorPage)
Fetch is an asynchronous function. Execution continues to the next line before the request is finished. You need to queue code to run after the request finishes. The best way to do that would be to make your postAuthor method return the promise, and then use the promise's .then method in the caller.
class ManageAuthorPage extends Component {
// ...
postAuthor() {
return fetch(source, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: this.generateId(this.state.author),
firstName: this.state.author.firstName,
lastName: this.state.author.lastName
})
});
};
saveAuthor(event) {
event.preventDefault();
this.postAuthor().then(() => {
this.props.history.pushState(null, '/authors');
});
};
// ...
}
If you're using a transpiler that supports ES7 async functions, then you could even do this in your saveAuthor method, which is equivalent and easier to read:
async saveAuthor(event) {
event.preventDefault();
await this.postAuthor();
this.props.history.pushState(null, '/authors');
};
So this is because your postAuthor method has an asynchronous call to fetch() inside of it. This is a time where you would want to pass in a function as a callback to the function, and then invoke that function inside the "completion" callback of the fetch call. The code would look something like this:
postAuthor(callback) {
fetch(source, {
/* Methods, headers, etc. */
}, () => {
/* Invoking the callback function that you passed */
callback();
});
);
saveAuthor(event) {
event.preventDefault();
/* Pass in a function to be invoked from within postAuthor when it is complete */
this.postAuthor(() => {
this.props.history.pushState(null, '/authors');
});
};