Vuejs, components and cache - vue.js

i was looking for a way to 'cache' some API fetched data in my application, and find out it can simply be done with localStorage (or even sessionsStorage)
While it works fine, i'm looking for a way to have all the cached data in one localStorage entry (in JSON)
The problem is i can't figure out how to build the JSON tree without overriding previous item. Let me explain :
On my dashboard, I have two components, one that load categories, the other loading last articles. Each component uses a different endpoint.
this is how i set cache (for categories) :
methods: {
async getCategories() {
let response = await fetch('https://carreblanc.zendesk.com/api/v2/help_center/fr/categories.json', {
method: 'GET',
headers: zendeskHeaders,
})
if (response.status === 200) {
let data = await response.json();
this.categories = data.categories
// Build cache
let cacheData = []
cacheData.push(data)
sessionStorage.setItem('zhc_db_cat_cache', JSON.stringify(cacheData))
}
else {
console.log("Something went wrong (" + response.status + ")");
return Promise.reject(response);
}
},
setUrl(url) {
return decodeURI(url.substring(url.indexOf('-') + 1))
}
},
for articles :
methods: {
async getArticles() {
let response = await fetch('https://carreblanc.zendesk.com/api/v2/help_center/fr/articles.json', {
method: 'GET',
headers: zendeskHeaders,
})
if (response.status === 200) {
let data = await response.json();
this.articles = data.articles
// Build cache
let cacheData = []
cacheData.push(data)
sessionStorage.setItem('zhc_db_art_cache', JSON.stringify(cacheData));
}
else {
console.log("Something went wrong (" + response.status + ")");
return Promise.reject(response);
}
},
setUrl(url) {
return decodeURI(url.substring(url.indexOf('-') + 1))
}
},
this results in two entries in the sessionStorage :
I want only one item, let's say 'zhc_cache', in which i can update data while components load..
for example : dashboard: {
categories: {
......
},
articles: {
......
}
}
I think there's something going on with the async function, but i can't figure it out,
any help will be appreciated :)
Thanks

If i understand it correctly, you just want one JSON object with categories AND data?
In that case, i think you should create an object ("zn_cache"), in which you add categories and articles
//If you want to keep both functions, just set vuejs data variables (categories and articles) and affect them the data you fetched from the api
this.categories = data.categories
this.articles = data.articles
Then you can create a global object with both (check the syntax I'm not 100% sure)
let dashboard = { 'categories' : this.categories, 'articles' : this.articles}
Then you can store this bad boy

#antoinehenrio
works like a charm ;)
here is the dashboard now
const zendeskConfig = {
categoriesEndpoint: 'https://carreblanc.zendesk.com/api/v2/help_center/fr/categories.json',
articlesEndpoint: 'https://carreblanc.zendesk.com/api/v2/help_center/fr/articles.json'
};
import Categories from '/skin/frontend/carreblanc/revamp/js/zendesk/hc/components/dashboard/categories.js'
import Recents from '/skin/frontend/carreblanc/revamp/js/zendesk/hc/components/dashboard/recent-activity.js'
export default {
components: {
Categories,
Recents
},
data() {
return {
categories: [],
articles: [],
}
},
methods: {
getData() {
Promise.all([
fetch(zendeskConfig.categoriesEndpoint).then(res => res.ok && res.json() || Promise.reject(res)),
fetch(zendeskConfig.articlesEndpoint).then(res => res.ok && res.json() || Promise.reject(res))
]).then(([resCategories, resArticles]) => {
this.categories = resCategories.categories
this.articles = resArticles.articles
let cacheData = { dashboard: { 'categories' : this.categories, 'articles' : this.articles} }
sessionStorage.setItem('zhc_db_cache', JSON.stringify(cacheData))
})
}
},
created() {
this.getData()
},
template: `
<div class="row">
<div class="col col-12">
<Categories :categories=this.categories />
</div>
<div class="col col-12">
<Recents :articles=this.articles />
</div>
</div>
`
}
and the result :
I have to reconfigure the other views, but it should be easy now
Thanks again !

Related

How to fetch data with slot in Vue?

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
});
}

Method Not Allowed (PUT) while using Django Rest paired with Vue.js frontend

I'm trying to make a PUT request to my RestAPI using Vue.js. It totally works in my ModelViewSet view, but i encounter Method Not Allowed (PUT) when i try it through Vue.js. I'm also able to use "POST" in the same onSubmit function.
Here is the code for onSubmit function in my QuestionEditor.vue:
<template>
<div class="container mt-2">
<h1 class="mb-3">Ask a Question</h1>
<form #submit.prevent="onSubmit">
<textarea v-model="question_body" class="form-control" placeholder="What do you want to ask?" row="3">
</textarea>
<br>
<button type="submit" class="btn btn-success">
Publish
</button>
</form>
<p v-if="error" class="muted mt-2">{{ error }}</p>
</div>
</template>
<script>
import { apiService } from "../common/api.service.js"
export default {
name: "QuestionEditor",
props: {
slug: {
type: String,
required: null
}
},
data() {
return{
question_body: null,
error: null
}
},
methods: {
onSubmit() {
if (!this.question_body){
this.error = "You can't send an empty question!";
} else if (this.question_body.length > 240) {
this.error = "Exceeding 240-character limit!";
} else {
let endpoint = "/api/questions/";
let method = "POST";
if (this.slug !== undefined) {
endpoint += `${ this.slug }/`;
method = "PUT";
}
apiService(endpoint, method, { content: this.question_body })
.then(question_data => {
this.$router.push({
name: 'Question',
params: {slug: question_data.slug}
})
})
}
}
},
async beforeRouteEnter(to, from, next){
if (to.params.slug !== undefined) {
let endpoint = `/api/questions/${to.params.slug}/`;
let data = await apiService(endpoint);
return next(vm => (vm.question_body = data.content));
} else{
return next();
}
},
created() {
document.title = "Editor - QuestionTime";
}
}
</script>
<style scoped>
</style>
and here is apiService function used to retrieve data from API:
import { CSRF_TOKEN } from "./csrf_token.js"
async function getJson(response) {
if (response.status === 204) return '';
return response.json()
}
function apiService(endpoint, method, data) {
const config = {
method: method || "GET",
body: data !== undefined ? JSON.stringify(data) : null,
headers: {
'content-type': 'application/json',
'X-CSRFTOKEN': CSRF_TOKEN
}
};
return fetch(endpoint, config)
.then(getJson)
.catch(error => console.log(error))
}
export { apiService };
and finally the api view:
class QuestionViewSet(viewsets.ModelViewSet):
queryset = Question.objects.all().order_by("-created_at")
lookup_field = "slug"
serializer_class = QuestionSerializer
permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]
def perform_create(self, serializer):
serializer.save(author=self.request.user)
update: question serializer:
class QuestionSerializer(serializers.ModelSerializer):
author = serializers.StringRelatedField(read_only=True)
created_at = serializers.SerializerMethodField(read_only=True)
slug = serializers.SlugField(read_only=True)
answers_count = serializers.SerializerMethodField()
user_has_answered = serializers.SerializerMethodField()
class Meta:
model = Question
exclude = ["updated_at"]
def get_created_at(self, instance):
return instance.created_at.strftime("%B %d %Y")
def get_answers_count(self, instance):
return instance.answers.count()
def get_user_has_answered(self, instance):
request = self.context.get("request")
return instance.answers.filter(author=request.user).exists()
and url.py for api:
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import QuestionViewSet, AnswerCreateAPIView, AnswerListAPIView, AnswerRUDAPIView, AnswerLikeAPIView
router = DefaultRouter()
router.register(r"questions", QuestionViewSet)
urlpatterns = [
path("", include(router.urls)),
path("questions/<slug:slug>/answer/", AnswerCreateAPIView.as_view(), name="answer-create"),
path("questions/<slug:slug>/answers/", AnswerListAPIView.as_view(), name="answer-list"),
path("answers/<int:pk>/", AnswerRUDAPIView.as_view(), name="answer-detail"),
path("answers/<int:pk>/like/", AnswerLikeAPIView.as_view(), name="answer-like"),
]
Update2: permissions file:
from rest_framework import permissions
class IsAuthorOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.author == request.
Solved!!! I downgraded from web-pack version 1 alpha to 0.4.3 and now everything seems to work fine. It was my mistake to go with a semi-developed version.
I think I might now what happens.
You either need to add pk field in your serializer (fields in Meta has to have id field)
or your endpoint is wrong.
} else {
let endpoint = "api/questions/";
let method = "POST";
if (this.slug !== undefined) {
endpoint += `${ this.slug }/`;
method = "PUT";
}
apiService(endpoint, method, { content: this.question_body })
.then(question_data => {
this.$router.push({
name: 'Question',
params: {slug: question_data.slug}
})
})
If all you do in here is changing the method, then it's not enough, as you POST to /resource but PUT to /resource/<id>.
Therefore, PUT to /resource is not allowed.
You can override this logic in ModelViewSet to take the id from body, but I wouldn't recommend doing that.
EDIT: I just noticed you add endpoint += ${ this.slug }/;. Could you please show how your endpoint for PUT looks like over here? With this slug. Is that a proper id in your serializer?
i think you have to allow cors headers on your server side
pip install django-cors-headers
in django app settings.py:
INSTALLED_APPS = [
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
]
CORS_ORIGIN_ALLOW_ALL = True
Looks like your issue in permissions
permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]
My guess is that your IsAuthorOrReadOnly is causing this. Could you please provide source code for it?

Axios GET not including params in Nuxt template

I want to pass an id to axios so that I can switch url dynamically.
My axios request in my template is as follows:
async asyncData({ params }) {
const { data } = await axios.get('http://localhost:8000/api/', {
params: {
id: 1
}
})
return { data }
}
The request being passed to my api is:
GET /api/?id=1
but I need
GET /api/1
What is happening here?
It looks like the asyncData function is called once when the page is loaded. I am still no wiser as to why it does not accept params in the way outlined in the docs and numerous tutorials, but it would not refresh the page because it is never called again.
To refresh the page data with a new api call, you need to return the axios promise from within the methods part of the export. The code below does the axios get request first, then adds or subtracts 1 from the id with plus and minus functions.
<script>
import axios from 'axios'
export default {
head() {
return {
title: 'Weather'
}
},
data: function() {
return { counter: 1 }
},
methods: {
plus: function(counter, data, datalength) {
this.counter += 1
axios.get('http://localhost:8000/api/' + this.counter).then(res => {
console.log(this.counter)
console.log(res.data)
return (this.data = res.data)
})
},
minus: function(counter, data) {
if (this.counter >= 2) {
this.counter -= 1
axios.get('http://localhost:8000/api/' + this.counter).then(res => {
console.log(this.counter)
console.log(res.data)
return (this.data = res.data)
})
} else {
this.counter = 1
}
}
},
async asyncData({ params, counter }) {
let { data } = await axios.get('http://localhost:8000/api/1')
return { data }
}
}
</script>
If anybody wants to elaborate or post a better solution, please go ahead - but I'm posting this because I searched so many tutorials and nothing worked until I found a way to interpret the documentation, which is certainly not beginner-friendly.

Computed Getter causes maximum stack size error

I'm trying to implement the following logic in Nuxt:
Ask user for an ID.
Retrieve a URL that is associated with that ID from an external API
Store the ID/URL (an appointment) in Vuex
Display to the user the rendered URL for their entered ID in an iFrame (retrieved from the Vuex store)
The issue I'm currently stuck with is that the getUrl getter method in the store is called repeatedly until the maximum call stack is exceeded and I can't work out why. It's only called from the computed function in the page, so this implies that the computed function is also being called repeatedly but, again, I can't figure out why.
In my Vuex store index.js I have:
export const state = () => ({
appointments: {}
})
export const mutations = {
SET_APPT: (state, appointment) => {
state.appointments[appointment.id] = appointment.url
}
}
export const actions = {
async setAppointment ({ commit, state }, id) {
try {
let result = await axios.get('https://externalAPI/' + id, {
method: 'GET',
protocol: 'http'
})
return commit('SET_APPT', result.data)
} catch (err) {
console.error(err)
}
}
}
export const getters = {
getUrl: (state, param) => {
return state.appointments[param]
}
}
In my page component I have:
<template>
<div>
<section class="container">
<iframe :src="url"></iframe>
</section>
</div>
</template>
<script>
export default {
computed: {
url: function (){
let url = this.$store.getters['getUrl'](this.$route.params.id)
return url;
}
}
</script>
The setAppointments action is called from a separate component in the page that asks the user for the ID via an onSubmit method:
data() {
return {
appointment: this.appointment ? { ...this.appointment } : {
id: '',
url: '',
},
error: false
}
},
methods: {
onSubmit() {
if(!this.appointment.id){
this.error = true;
}
else{
this.error = false;
this.$store.dispatch("setAppointment", this.appointment.id);
this.$router.push("/search/"+this.appointment.id);
}
}
I'm not 100% sure what was causing the multiple calls. However, as advised in the comments, I've now implemented a selectedAppointment object that I keep up-to-date
I've also created a separate mutation for updating the selectedAppointment object as the user requests different URLs so, if a URL has already been retrieved, I can use this mutation to just switch the selected one.
SET_APPT: (state, appointment) => {
state.appointments = state.appointments ? state.appointments : {}
state.selectedAppointment = appointment.url
state.appointments = { ...state.appointments, [appointment.appointmentNumber]: appointment.url }
},
SET_SELECTED_APPT: (state, appointment) => {
state.selectedAppointment = appointment.url
}
Then the getUrl getter (changed its name to just url) simply looks like:
export const getters = {
url: (state) => {
return state.selectedAppointment
}
}
Thanks for your help guys.

how to implement reusable api-calling component in vuejs?

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.