I have a code block like this.
<template slot="name" slot-scope="row">{{row.value.first}} {{row.value.last}}</template>
Also I have a header.
{ isActive: true, age: 38, name: { first: 'Jami', last: 'Carney' } },
{ isActive: false, age: 27, name: { first: 'Essie', last: 'Dunlap' } },
{ isActive: true, age: 40, name: { first: 'Thor', last: 'Macdonald' } },
This code is running clearly but I want to show data from my API. Which terms do I need to know? I used Axios before in React. Where can I define Axios method? Do I need to change the template slot instead of :v-slot ?
Although you can make API calls directly from inside the component code, it does not mean that you should. It's better to decouple API calls into a separate module.
Here's a good way to do it which properly follows Separation of Concern (SoC) principle:
Create a directory services under src if it's not already there.
Under services, create new file named api.service.js.
api.service.js
import axios from 'axios';
const baseURL = 'http://localhost:8080/api'; // Change this according to your setup
export default axios.create({
baseURL,
});
Create another file peopleData.service.js
import api from './api.service';
import handleError from './errorHandler.service'; // all the error handling code will be in this file, you can replace it with console.log statement for now.
export default {
fetchPeopleData() {
return api.get('/people')
.catch((err) => handleError(err));
},
// All other API calls related to your people's (users'/customers'/whatever is appropriate in your case) data should be added here.
addPerson(data) {
return api.post('/people', data)
.catch((err) => handleError(err));
},
}
Now you can import this service into your component and call the function.
<template>
... Template code
</template>
<script>
import peopleDataService from '#/services/peopleData.service';
export default {
data() {
return {
rows: [],
};
},
mounted() {
peopleDataService.fetchPeopleData().then((res) => {
if (res && res.status == 200) {
this.rows = res.data;
}
});
},
}
</script>
You haven't given us any idea about your current setup. If you're using Vue-Router, it's better to fetch data in navigation guards, especially if your component is relying on the data: Data Fetching
Simply shift the code from mounted() into a navigation guard. this may not be available, so you will have to use next callback to set rows array, it's explained in the link above.
You can use Axios in methods or mounted.
mounted(){
this.loading = true;
axios
.get(`${this.backendURL}/api/v1/pages/layouts` , authHeader())
.then(response => (this.layouts = response.data.data))
.catch(handleAxiosError);
}
methods: {
/**
* Search the table data with search input
*/
uncheckSelectAll(){
this.selectedAll = false
},
onFiltered(filteredItems) {
// Trigger pagination to update the number of buttons/pages due to filtering
this.totalRows = filteredItems.length;
this.currentPage = 1;
},
handlePageChange(value) {
this.currentPage = value;
axios
.get(`${this.backendURL}/api/v1/pages?per_page=${this.perPage}&page=${this.currentPage}` , authHeader())
.then(response => (this.pagesData = convert(response.data.data),
this.pagesDataLength = response.data.pagination.total));
},
handlePerPageChange(value) {
this.perPage = value;
this.currentPage = 1;
axios
.get(`${this.backendURL}/api/v1/pages?per_page=${this.perPage}&page=${this.currentPage}` , authHeader())
.then(response => (this.pagesData = convert(response.data.data),
this.pagesDataLength = response.data.pagination.total));
},
deletePage(){
this.loading = true
this.$bvModal.hide("modal-delete-page");
window.console.log(this.pageIdentity);
if (!roleService.hasDeletePermission(this.pageIdentity)){
return;
}
axios
.delete(`${this.backendURL}/api/v1/pages/${this.page.id}` , authHeader())
.then(response => (
this.data = response.data.data.id,
axios
.get(`${this.backendURL}/api/v1/pages?per_page=${this.perPage}&page=${this.currentPage}` , authHeader())
.then(response => (this.pagesData = convert(response.data.data),
this.pagesDataLength =
response.data.pagination.total)),
alertBox(`Page deleted succesfully!`, true)
))
.catch(handleAxiosError)
.finally(() => {
this.loading = false
});
}
Related
I'm trying to populate my app with data coming from a websocket in the most modular way possible trying to use best practices etc. Which is hard because even when I have dig very deep for advice on the use of websockets / Vuex and Vue I still can't find a pattern to get this done. After going back and forth I have decided to use a store to manage the state of the websocket and then use that vuex module to populate the state of other components, basically a chat queue and a chat widget hence the need to use websockets for real time communication.
This is the websocket store. As you can see I'm transforming the processWebsocket function into a promise in order to use async/await in other module store actions. The way I see this working (and I'm prob wrong, so PLEASE feel free to correct me) is that all the components that will make use of the websocket module state will wait until the state is ready and then use it (this is not working at the moment):
export const namespaced = true
export const state = {
connected: false,
error: null,
connectionId: '',
statusCode: '',
incomingChatInfo: [],
remoteMessage: [],
messageType: '',
ws: null,
}
export const actions = {
processWebsocket({ commit }) {
return new Promise((resolve) => {
const v = this
this.ws = new WebSocket('xyz')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
resolve(event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
})
},
}
export const mutations = {
SET_REMOTE_DATA(state, remoteData) {
const wsData = JSON.parse(remoteData.data)
if (wsData.connectionId && wsData.connectionId !== state.connectionId) {
state.connectionId = wsData.connectionId
console.log(`Retrieving Connection ID ${state.connectionId}`)
} else {
state.messageType = wsData.type
state.incomingChatInfo = wsData.documents
}
},
SET_CONNECTION(state, message) {
if (message == 'open') {
state.connected = true
} else state.connected = false
},
SET_ERROR(state, error) {
state.error = error
},
}
When I debug the app everything is working fine with the websocket store, I can see its state, the data from the server is there etc. The problem comes when I try to populate other components properties using the websocket. By the time other components need the websocket state this is not ready yet so I'm getting errors. Here's an example of one of my components trying to use the websocket state, I basically call an action from the created cycle method:
<template>
<ul class="overflow-y-auto overflow-hidden pr-2">
<BaseChat
v-for="(chat, index) in sortingIncomingChats"
:key="index"
:chat="chat"
:class="{ 'mt-0': index === 0, 'mt-4': index > 0 }"
/>
</ul>
</template>
<script>
import { mapState } from 'vuex'
import BaseChat from '#/components/BaseChat.vue'
export default {
components: {
BaseChat,
},
created() {
this.$store.dispatch('chatQueue/fetchChats')
},
data() {
return {
currentSort: 'timeInQueue',
currentSortDir: 'desc',
chats: [],
}
},
computed: {
sortingIncomingChats() {
return this.incomingChats.slice().sort((a, b) => {
let modifier = 1
if (this.currentSortDir === 'desc') modifier = -1
if (a[this.currentSort] < b[this.currentSort])
return -1 * modifier
if (a[this.currentSort] > b[this.currentSort])
return 1 * modifier
return 0
})
},
},
}
</script>
This is the chatQueue Vuex module that have the fetchChats action to populate data from the websocket to the APP:
export const namespaced = true
export const state = () => ({
incomingChats: [],
error: '',
})
export const actions = {
fetchChats({ commit, rootState }) {
const data = rootState.websocket.incomingChats
commit('SET_CHATS', data)
},
}
export const mutations = {
SET_CHATS(state, data) {
state.incomingChats = data
},
SET_ERROR(state, error) {
state.incomingChats = error
console.log(error)
},
}
This is where I get errors because "rootState.websocket.incomingChats" is not there yet when its called by the fetchChats module action, so I get:
TypeError: Cannot read properties of undefined (reading 'slice')
I have tried to transform that action into an async / await one but it's not working either, but as I mentioned I'm really new to async/await so maybe I'm doing something wrong here:
async fetchChats({ commit, rootState }) {
const data = await rootState.websocket.incomingChats
commit('SET_CHATS', data)
},
Any help will be really appreciated.
In case somebody have the same problem what I ended up doing is adding a getter to my websocket module:
export const getters = {
incomingChats: (state) => {
return state.incomingChatInfo
},
}
And then using that getter within a computed value in the component I need to populate with the websocket component.
computed: {
...mapGetters('websocket', ['incomingChats']),
},
And I use the getter on a regular v-for loop within the component:
<BaseChat
v-for="(chat, index) in incomingChats"
:key="index"
:chat="chat"
:class="{ 'mt-0': index === 0, 'mt-4': index > 0 }"
/>
That way I don't have any kind of sync problem with the websocket since I'm sure the getter will bring data to the component before it tries to use it.
I am using Vue, Electron and Sequelize, when I npm run electron:serve, sequelize promise doesn't return anything, data property won't set or show unless I save the code again to refresh the existing window:
<script>
export default {
data () {
users : [],
}
},
created () {
console.log('created')
this.fetchData();
},
methods: {
fetchData () {
let self = this
let allUsers = db.User.findAll({
attributes: ['id', 'first_name', 'last_name', 'email', 'password'],
raw : true
}).then(res => {
console.log('then working');
self.users = res;
}).catch(err => {
console.log('there was an error' + err)
})
console.log('fetchData method called' + allUsers)
},
}
</script>
I can see the fetchData() is being called but nothing show after .then( ... unless page is saved and refreshed.
Please take a look at the component data section. It doesn't have users:
data () {
dialog: false,
}
All properties that should be reactive are declared in the data section of a component.
I have a simple VueJS SPA served by Express. Express also handles API endpoints called by Vue front-end.
Express is connected to Postgres, and API endpoints interact with the database (perform basic CRUD operations).
In my database, I have a single "patient" table, with columns "first_name", "last_name", "date_of_birth", and "id".
In the created() hook of PatientList.vue component, database is queried for all patients, and this information is saved to component data, displayed using v-for loop.
My PatientList.vue code is:
<script>
import auth from '#/auth/authenticator';
import { mapMutations } from 'vuex';
export default {
components: {
name: 'PatientsList',
},
data() {
return {
patients: [],
}
},
computed: {
accessTokenGetter: {
get: function () {
return this.$store.getters.accessToken;
},
},
patientEditStatusGetter: {
get: function () {
return this.$store.getters.g_patientEditStatusCheck;
},
},
},
methods: {
...mapMutations([
'm_startPatientEditProcess',
'm_endPatientEditProcess',
'm_clearPatientEditState',
'm_cachePatient'
]),
cachePatientHandler(ptnt) {
console.log('PatientList.vue method cachePatientHandler', ptnt);
var patientObject = {
'date_of_birth': ptnt.date_of_birth.split('T')[0],
'first_name': ptnt.first_name,
'last_name': ptnt.last_name,
'patient': ptnt.patient,
'uid': ptnt.uid
}
this.m_endPatientEditProcess(false);
this.m_clearPatientEditState('');
this.m_startPatientEditProcess(true);
this.m_cachePatient(patientObject);
},
getPatients() {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://voyager.wrk.health/patients/index');
xhr.setRequestHeader('Authorization', `Bearer ${this.accessTokenGetter}`);
xhr.setRequestHeader('Cache-control', 'no-cache');
xhr.onload = () => {
var data = JSON.parse(xhr.response);
for( var i=0, r = data.results; i<r.length; i++ ){
this.patients.push(r[i]);
}
};
xhr.onerror = () => {
console.log(xhr.statusText);
};
xhr.send();
},
},
beforeCreate() {
},
created() {
console.log('PatientList.vue created()');
if(auth.isUserLogged()){
this.getPatients();
} else {
router.go('/');
}
},
};
</script>
In order to edit a patient, I have router-link to edit page. Router-link has click-handler, argument passed in is iterable from v-for loop (i.e. single patient object). I have 4 mutations related to this
const mutations = {
m_startPatientEditProcess(state, trueStatus) {
console.log('Vuex patient m_startPatientEditProcess');
state.patientEditStatus = trueStatus;
},
m_endPatientEditProcess(state, falseStatus) {
console.log('Vuex patient m_endPatientEditProcess');
state.patientEditStatus = falseStatus;
},
m_clearPatientEditState(state, emptyString) {
console.log('Vuex patient m_clearPatientEditState');
state.patientDetails.date_of_birth = emptyString;
state.patientDetails.first_name = emptyString;
state.patientDetails.last_name = emptyString;
state.patientDetails.patient = emptyString;
state.patientDetails.uid = emptyString;
},
m_cachePatient(state, patientObj) {
console.log('Vuex patient m_cachePatient, received: ', patientObj);
state.patientDetails.date_of_birth = patientObj.date_of_birth;
state.patientDetails.first_name = patientObj.first_name;
state.patientDetails.last_name = patientObj.last_name;
state.patientDetails.patient = patientObj.patient;
state.patientDetails.uid = patientObj.uid;
},
Also, my PatientEdit.vue code is:
<script>
import { mapMutations } from 'vuex';
export default {
components: {
name: 'PatientEdit',
},
data() {
return {
patientToEdit: {
first_name: '',
last_name: '',
date_of_birth: '',
patient: '',
uid: '',
},
patientDetailsLoaded: false,
}
},
computed: {
patientToEditDetailsGetter: {
get: function() {
return this.$store.getters.g_patientToEditDetails;
}
},
accessTokenGetter: {
get: function() {
return this.$store.getters.accessToken;
}
}
},
methods: {
...mapMutations([
'm_endPatientEditProcess',
'm_clearPatientEditState',
]),
populatePatientEditState() {
const pDeets = this.patientToEditDetailsGetter;
this.patientToEdit.first_name = pDeets.first_name;
this.patientToEdit.last_name = pDeets.last_name;
this.patientToEdit.date_of_birth = pDeets.date_of_birth;
this.patientToEdit.patient = pDeets.patient;
this.patientToEdit.uid = pDeets.uid;
this.patientDetailsLoaded = true;
},
submitUpdatedPatientDetails() {
const payload = Object.assign({}, this.patientToEdit);
const xhr = new XMLHttpRequest();
xhr.open('PUT', `https://voyager.wrk.health/patients/update/${payload.uid}`)
xhr.setRequestHeader('Content-type', 'application/json');
xhr.setRequestHeader('Authorization', `Bearer ${this.accessTokenGetter}`);
xhr.onload = async () => {
try {
await console.log(xhr.response);
await console.log('Sent patient data to update endpoint \n Ready to be redirected.');
await Promise.all([this.m_endPatientEditProcess(false), this.m_clearPatientEditState('')]);
await this.$router.push('/patients/index');
} catch (e) {
throw new Error(e);
}
}
xhr.send(JSON.stringify(payload));
}
},
created() {
this.populatePatientEditState();
},
};
</script>
My reasoning was to avoid unnecessary request to database.
Everything works as intended. I have a store.subscription set up to save Vuex state to localStorage (for session persistence when this application is refreshed).
Store subscription logs state and mutation, everything is normal like so:
First store output
If I open a new tab or window (cookies left untouched), and try to perform the same update operations, my store subscription freaks out, and I cannot auto-populate my PatientEdit page with patient information from Vuex.
According to the output, suddenly mutation is committing things that I never specified like so:
Store output 2
Why does this happen?
Thanks for reading.
NB: If I have missed information necessary to figure this behaviour out, please let me know.
Edit 1:
Vuex store:
import Vue from 'vue';
import Vuex from 'vuex';
import session from './modules/session';
import patient from './modules/patient';
Vue.use(Vuex);
const store = new Vuex.Store({
modules: {
session,
patient,
},
mutations: {
initStore(state) {
console.log('Vuex root state checking for local snapshot');
if (localStorage.getItem('store')) {
console.log('Snapshot found, hydrating...');
this.replaceState(Object.assign(store, JSON.parse(localStorage.getItem('store'))));
}
},
},
});
store.commit('initStore');
store.subscribe((mutation, state) => {
console.warn('Subscription detected');
console.log('mutation: ', mutation);
console.log('state: ', state);
localStorage.setItem('store', JSON.stringify(state));
});
export default store;
You end up with a "cannot stringify circular JSON" error, because you are turning the state, but also the getters, mutations and actions into a string. These contain references to the object you are trying to stringify, which results in an infinite loop.
This is not a problem in your first run, because your localStorage is still empty then. You correctly stringify your state, but when you reload the following line runs:
this.replaceState(Object.assign(store, JSON.parse(localStorage.getItem('store'))));
This line replaces your state with your store, extended with what you have in localStorage. If you replace store with state things should work much better.
I'm developing a simple vuejs app where I have a few identical APIs serving content that is parsed in a similar way. I would like to make the code to fetch the content common across the various API calls, and only have a need to pass the API endpoint to what fetches the content.
Here's my code
var content = new Vue({
el: '#story',
data: {
loaded: [],
current: 0,
hasMore:"",
nextItems:"",
errors: []
},
mounted() {
axios.get("/storyjs")
.then(response => {
this.loaded = this.loaded.concat(response.data.content)
this.hasMore = response.data.hasMore
this.nextItems = response.data.nextItem
}).catch(e => {
this.errors.push(e)
})
},
methods: {
fetchNext: function() {
axios.get(this.nextItems)
.then(response => {
this.loaded = this.loaded.concat(response.data.content)
this.hasMore = response.data.hasMore
this.nextItems = response.data.nextItem
this.current+=1
}).catch(e => {
//TODO CLEAR errors before pushing
this.errors.push(e)
})
},
next: function() {
if (this.current+1 < this.loaded.length) {
this.current+=1
} else {
this.fetchNext()
}
},
prev: function() {
this.current = (this.current-1 >= 0) ? this.current-1 : 0
}
},
delimiters: ['[{', '}]']
})
Right now, I've replicated the above object for stories, poems, and many other things. But I would ideally like to combine them into one. Strategies I tried to search for included having a parent component as this object, but I think I'm probably thinking wrong about some of this.
Really appreciate the help!
I went with mixins. This is the solution I implemented.
apiObject.js (Reusable object)
var apiObject = {
data: function() {
return {
loaded: [],
current: 0,
hasMore: "",
nextItems: "",
errors: []
};
},
methods: {
fetchContent: function(apiEndpoint) {
axios
.get(apiEndpoint)
.then(response => {
this.loaded = this.loaded.concat(response.data.content);
this.hasMore = response.data.hasMore;
this.nextItems = response.data.nextItem;
})
.catch(e => {
this.errors.push(e);
});
},
fetchNext: function() {
axios
.get(this.nextItems)
.then(response => {
this.loaded = this.loaded.concat(response.data.content);
this.hasMore = response.data.hasMore;
this.nextItems = response.data.nextItem;
this.current += 1;
})
.catch(e => {
//TODO CLEAR errors before pushing
this.errors.push(e);
});
},
next: function() {
if (this.current + 1 < this.loaded.length) {
this.current += 1;
} else if (this.hasMore == true) {
this.fetchNext();
}
},
prev: function() {
this.current = this.current - 1 >= 0 ? this.current - 1 : 0;
}
}
};
story.js (Specific usage)
var storyComponent = Vue.extend({
mixins: [apiObject],
created() {
this.fetchContent("/story");
}
});
new Vue({
el: "#story",
components: {
"story-component": storyComponent
},
delimiters: ["[{", "}]"]
});
and then, you could either define the template in the component itself, or use the inline-template way of creating the template in the html file, which is what I did
output.html with all js files included
<div id="story">
<story-component inline-template>
[{loaded[current].title}]
</story-component>
</div>
There are many ways to tackle this, but perhaps once you reach this level of complexity in the model of the components/application state, the most sensible strategy would be to use a central state store.
See the State Management chapter of the vue guide and possibly the excellent vuex.
There you could factor the common logic in suitable local classes/functions and call them from store actions (for async operations you have to use actions, which will commit mutations with respective state changes at completion of the asynchronous operations.
I'm really struggling trying to test a request in VueJS using Mocha/Chai-Sinon, with Axios as the request library and having tried a mixture of Moxios and axios-mock-adaptor. The below examples are with the latter.
What I'm trying to do is make a request when the component is created, which is simple enough.
But the tests either complain about the results variable being undefined or an async timout.
Am I doing it right by assigning the variable of the getData() function? Or should Ireturn` the values? Any help would be appreciated.
Component
// Third-party imports
import axios from 'axios'
// Component imports
import VideoCard from './components/VideoCard'
export default {
name: 'app',
components: {
VideoCard
},
data () {
return {
API: '/static/data.json',
results: null
}
},
created () {
this.getData()
},
methods: {
getData: function () {
// I've even tried return instead of assigning to a variable
this.results = axios.get(this.API)
.then(function (response) {
console.log('then()')
return response.data.data
})
.catch(function (error) {
console.log(error)
return error
})
}
}
}
Test
import Vue from 'vue'
import App from 'src/App'
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'
let mock = new MockAdapter(axios)
describe('try and load some data from somewhere', () => {
it('should update the results variable with results', (done) => {
console.log('test top')
mock.onGet('/static/data.json').reply(200, {
data: {
data: [
{ id: 1, name: 'Mexican keyboard cat' },
{ id: 2, name: 'Will it blend?' }
]
}
})
const VM = new Vue(App).$mount
setTimeout(() => {
expect(VM.results).to.be.null
done()
}, 1000)
})
})
I am not sure about moxios mock adaptor, but I had a similar struggle. I ended up using axios, and moxios, with the vue-webpack template. My goal was to fake retreiving some blog posts, and assert they were assigned to a this.posts variable.
Your getData() method should return the axios promise like you said you tried - that way, we have some way to tell the test method the promise finished. Otherwise it will just keep going.
Then inside the success callback of getData(), you can assign your data. So it will look like
return axios.get('url').then((response) {
this.results = response
})
Now in your test something like
it('returns the api call', (done) => {
const vm = Vue.extend(VideoCard)
const videoCard = new vm()
videoCard.getData().then(() => {
// expect, assert, whatever
}).then(done, done)
)}
note the use of done(). That is just a guide, you will have to modify it depending on what you are doing exactly. Let me know if you need some more details. I recommend using moxios to mock axios calls.
Here is a good article about testing api calls that helped me.
https://wietse.loves.engineering/testing-promises-with-mocha-90df8b7d2e35#.yzcfju3qv
So massive kudos to xenetics post above, who helped in pointing me in the right direction.
In short, I was trying to access the data incorrectly, when I should have been using the $data property
I also dropped axios-mock-adaptor and went back to using moxios.
I did indeed have to return the promise in my component, like so;
getData: function () {
let self = this
return axios.get(this.API)
.then(function (response) {
self.results = response.data.data
})
.catch(function (error) {
self.results = error
})
}
(Using let self = this got around the axios scope "problem")
Then to test this, all I had to do was stub the request (after doing the moxios.install() and moxios.uninstall for the beforeEach() and afterEach() respectively.
it('should make the request and update the results variable', (done) => {
moxios.stubRequest('./static/data.json', {
status: 200,
responseText: {
data: [
{ id: 1, name: 'Mexican keyboard cat' },
{ id: 2, name: 'Will it blend?' }
]
}
})
const VM = new Vue(App)
expect(VM.$data.results).to.be.null
VM.getData().then(() => {
expect(VM.$data.results).to.be.an('array')
expect(VM.$data.results).to.have.length(2)
}).then(done, done)
})