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');
});
};
Related
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();
}
}
I have the following components:
Parent:
<template>
<Child path="instance.json"
v-bind:authenticated="authenticated"
v-bind:authenticator="authenticator"
/>
</template>
<script>
import { getAuthenticator } from '../auth';
export default {
data() {
return {
authenticated: false,
authenticator: null
};
},
beforeMount: async function () {
this.authenticator = getAuthenticator()
this.checkAccess();
},
methods: {
checkAccess() {
this.authenticated = this.authenticator.isAuthenticated();
},
async login() {
this.checkAccess();
await this.authenticator.signIn();
this.checkAccess();
}
}
};
</script>
Child:
<template>
<div id="swagger-ui"></div>
</template>
<script>
import swagger from "swagger-ui-dist";
import "swagger-ui-dist/swagger-ui.css";
export default {
props: ["path", "authenticated", "authenticator"],
mounted: async function() {
if (this.authenticated) {
let token = (await this.authenticator.getToken()).accessToken;
const ui = swagger.SwaggerUIBundle({
url: this.path,
dom_id: "#swagger-ui",
onComplete: function() {
ui.preauthorizeApiKey("token", token);
}
});
} else {
const ui = swagger.SwaggerUIBundle({
url: this.path,
dom_id: "#swagger-ui"
});
}
}
};
</script>
In the parent component, when the login method is called, the authenticated variable changes to true. Since authenticated is passed as a prop to the Child component, I'd expect the Child to be refreshed whenever authenticated is changed. However, the Child does not refresh.
I think that the problem might be caused by the fact that I am not using authenticated in the template of the child at all. Instead, I'm using it only in the mounted hook. In my case, I have no use for authenticated in the template.
I tried two solutions:
calling this.$forceUpdate() in the login method of Parent - that didn't work at all (nothing changed)
Adding :key to the Child, and changing the key each time the login is called - this works, however, it's a bit hacky. I'd like to understand how to do that properly.
what you need is to use a watcher.
Actually, your code is only run once (when de component is mounted), not at each prop change.
<template>
<div id="swagger-ui"></div>
</template>
<script>
import swagger from 'swagger-ui-dist';
import 'swagger-ui-dist/swagger-ui.css';
export default {
props: {
path: {
type: String,
default: '',
},
authenticated: {
type: Boolean,
default: false,
},
authenticator: {
type: Object,
default: () => {},
},
},
watch: {
async authenticated(newValue) {
await this.updateSwagger(newValue);
},
},
async mounted() {
await this.updateSwagger(this.authenticated);
}
methods: {
async updateSwagger(authenticated) {
if (authenticated) {
const token = (await this.authenticator.getToken()).accessToken;
const ui = swagger.SwaggerUIBundle({
url: this.path,
dom_id: '#swagger-ui',
onComplete: function () {
ui.preauthorizeApiKey('token', token);
},
});
} else {
const ui = swagger.SwaggerUIBundle({
url: this.path,
dom_id: '#swagger-ui',
});
}
},
},
};
</script>
It's fine that you're not using it in the template, the issue is that you only check authenticated in the child's mounted hook, which only runs once (and is false at that time).
You should use a watch to track changes to the authenticated prop instead of mounted:
watch: {
authenticated: {
handler(newValue, oldValue) {
this.setUi();
},
immediate: true // Run the watch when `authenticated` is first set, too
}
}
That will call a setUi method every time authenticated changes:
methods: {
async setUi() {
if (this.authenticated) {
let token = (await this.authenticator.getToken()).accessToken;
const ui = swagger.SwaggerUIBundle({
url: this.path,
dom_id: "#swagger-ui",
onComplete: function() {
ui.preauthorizeApiKey("token", token);
}
});
} else {
const ui = swagger.SwaggerUIBundle({
url: this.path,
dom_id: "#swagger-ui"
});
}
}
}
I am building an application with pure javascript and Web Components. I also want to use the MVC Pattern, but now I have a problem with asynchronous calls from the model.
I am developing a meal-list component. The data is coming from an API as JSON in the following format:
[
{
id: 1,
name: "Burger",
},
]
I want the controller to get the data from the model and send it to the view.
meals.js (Model)
export default {
get all() {
const url = 'http://localhost:8080/meals';
let speisekarte = [];
fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}).then(res => {
return res.json()
}).then(data => {
// This prints the result I want to use, but I can't return
console.log(data);
// This does not work
speisekarte = data;
// This also does not work
return data;
});
// is undefined.
return speisekarte;
},
}
This is how I tried to get the data from an API.
meal-list.component.js (Controller)
import Template from './meal-list.template.js'
import Meal from '../../../../data/meal.js'
export default class MealListComponent extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
// Should send the Data from the model to the View
this.shadowRoot.innerHTML = Template.render(Meal.all);
}
}
if (!customElements.get('mp-meal-list')) {
customElements.define('mp-meal-list', MealListComponent);
}
meal-list.template.js (View)
export default {
render(meals) {
return `${this.html(meals)}`;
},
html(meals) {
let content = `<h1>Speisekarte</h1>
<div class="container">`;
content += /* display the data from api with meals.forEach */
return content + '</div>';
},
}
As I mentioned in the comments, I have a problem in returning the async data from the model to the view. Either it is undefined when I try to return data; or if I try to save the data into an array. I could also return the whole fetch() method, but this returns a promise and I dont think the controller should handle the promise.
I already read the long thread in How do I return the response from an asynchronous call? but I could not relate it to my case.
Since you declared speisekarte as an array, I'd expect it to always return as an empty array. When the fetch executes and fulfills the promise, its always too late in the above implementation.
You have to wait for the fetch result and there are multiple options you might consider:
Either providing a callback to the fetch result
Or notifying your application via event dispatch and listeners that your data has been loaded, so it can start rendering
Your link already has a very good answer on the topic callbacks and async/await, I could not put it better than what is explained there.
Thanks to lotype and Danny '365CSI' Engelman I've found the perfect solution for my projct. I solved it with custom events and an EventBus:
meal.js (model)
get meals() {
const url = 'http://localhost:8080/meals';
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}).then(res => {
return res.json()
}).then(data => {
let ce = new CustomEvent(this.ESSEN_CHANGE_EVENT, {
detail: {
action: this.ESSEN_LOAD_ACTION,
meals: data,
}
});
EventBus.dispatchEvent(ce);
});
},
EventBus.js (from book: Web Components in Action)
export default {
/**
* add event listener
* #param type
* #param cb
* #returns {{type: *, callback: *}}
*/
addEventListener(type, cb) {
if (!this._listeners) {
this._listeners = [];
}
let listener = {type: type, callback: cb};
this._listeners.push(listener);
return listener;
},
/**
* trigger event
* #param ce
*/
dispatchEvent(ce) {
this._listeners.forEach(function (l) {
if (ce.type === l.type) {
l.callback.apply(this, [ce]);
}
});
}
}
Now, when the data is ready, a signal to the event bus is sent. The meal-list-component is waiting for the events and then gets the data:
export default class MealListComponent extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = Template.render();
this.dom = Template.mapDOM(this.shadowRoot);
// Load Speisekarte on init
this.dom.meals.innerHTML = Template.renderMeals(MealData.all);
// Custom Eventlistener - always triggers when essen gets added, deleted, updated etc.
EventBus.addEventListener(EssenData.ESSEN_CHANGE_EVENT, e => {
this.onMealChange(e);
});
}
onMealChange(e) {
switch (e.detail.action) {
case EssenData.ESSEN_LOAD_ACTION:
this.dom.meals.innerHTML = Template.renderMEals(e.detail.meals);
break;
}
}
}
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
}
}
}