Loading images in vue.js from an API - javascript

Consider this:
An API loads a manifest of image metadata. The images have an ID, and with another API call returns a base64 image from the DB. The model for the manifest is attachmentRecord and the ID is simply a field.
I would rather not preload these large strings into an array (that would work).
so I have this (which lazy loads on any manifest change):
<div v-for="(attachment, index) in attachmentRecord" :key="index">
<img :src="fetchImage(attachment.id)" />
</div>
fetchimage() is a wrapper for an axios function which returns back from a promise. (writing this from memory):
this.axios({
method: "get",
url: url,
}).then(res => res.data)
.catch(() => {
alert("Unable to load raw attachment from this task and ID");
});
}
Now, the network calls go thru fine, the ID passes in correctly, I can see the base 64data, but they don't seem to make it to wrapper function or the src attribute. It always comes up blank. I tried wrapping it in another promise,only to get a promise back to the src attribute. What would be a best practice for this situation in Vue?
Ok, so far I made these changes with Constantin's help:
I tried to strip it down without a helper function:
Vue template Code:
<div v-for="(attachment, index) in attachmentRecord" :key="index">
<img :src="getAttachmentFromTask(attachment.id)" />
base method:
async getAttachmentFromTask(attachmentID) {
if (!attachmentID) alert("Unknown Attachment!");
let sendBack = "";
let url = "/server/..."
await this.axios({
method: "get",
url: url
})
.then(res => {
sendBack = res.data;
})
.catch(() => {
alert("Unable to load raw attachment from this task and ID");
});
// >>>>>>>>>alerts base64 correctly; Vue loads [object Promise] in img
alert(sendBack);
return sendBack;
}

It turns out that Vue doesn't handle async / await as well as I thought. Therefore, you have to save the image data to each attachment in attachmentRecord. This getAttachmentFromTask method now handles this when accessed the first time and populates a data property for the corresponding attachment object. On successive calls, that property is returned if it is already populated. Note the usage of Vue.set() because the property is not available in the initial data, but we want it to be reactive. You can even set up a fallback image like a loader, see the shortly flickering SO logo without text before the larger logo appears:
new Vue({
el: '#app',
data: {
attachmentRecord: [{
id: 1
}]
},
methods: {
getAttachmentFromTask(attachmentIndex, attachmentID) {
let record = this.attachmentRecord[attachmentIndex];
if (!record.data) {
Vue.set(record, 'data', null);
axios.get('https://kunden.48design.de/stackoverflow/image-base64-api-mockup.json').then((result) => {
Vue.set(record, 'data', result.data);
});
}
return this.attachmentRecord[attachmentIndex].data;
}
}
});
img {
max-width: 100vw;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.0/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<div id="app">
<div v-for="(attachment, index) in attachmentRecord" :key="index">
<img :src="getAttachmentFromTask(index, attachment.id) || 'https://cdn.sstatic.net/Sites/stackoverflow/img/apple-touch-icon.png'" />
</div>
</div>
old answer: (Unfortunately doesn't work that way with Vue currently)
Axios requests are asynchronous by default. So the function doesn't wait for then() to return the value. You could add the async keyword before your fetchImage function name and add the await keyword before this.axios. Then make the then callback assign the return value to a variable in the fetchImage function scope and have the function return it.
async fetchImage() {
let returnValue;
await this.axios({
method: "get",
url: url,
}).then(res => { returnValue = res.data; })
.catch(() => {
alert("Unable to load raw attachment from this task and ID");
});
return returnValue;
}

Related

Error `Template parameter names *must* be separated`

I am using mithril.js in frontend application and backend application is running on ipv6 environment.
Calling post ajax request to backend using mithril.js.
async post(url, body = {}) {
return new Promise((resolve, reject) => {
m.request({method: 'POST', url, body}).then((data) => {
resolve(data);
}).catch((err) => {
reject(err.message);
});
});
}
Backed url is like this: http://[340f:c0e0:1d1:5gc0:g4fs:2::]:22923/backend.
But getting this error Template parameter names *must* be separated while calling backend api.
Explanation of the error
Based on the documentation of m.request(), you can specify dynamic URLs:
Request URLs may contain interpolations:
m.request({
method: "GET",
url: "/api/v1/users/:id",
params: {id: 123},
}).then(function(user) {
console.log(user.id) // logs 123
})
In the code above, :id is populated with the data from the params object, and the request becomes GET /api/v1/users/123.
Interpolations are ignored if no matching data exists in the params property.
m.request({
method: "GET",
url: "/api/v1/users/foo:bar",
params: {id: 123},
})
In the code above, the request becomes GET /api/v1/users/foo:bar?id=123
Since your backend URL contains colons, it's interpreted as being a dynamic URL.
According to the documentation of m.buildPathname(), m.request() uses m.buildPathname() internally to process dynamic URLs.
The beginning of m.buildPathname() contains the following check regarding parameters of a path template (dynamic URL = path template populated with path parameters):
if ((/:([^\/\.-]+)(\.{3})?:/).test(template)) {
throw new SyntaxError("Template parameter names *must* be separated")
}
(Source: https://github.com/MithrilJS/mithril.js/blob/v2.0.4/mithril.js#L1288-L1292)
And, again, since your backend URL contains colons, this is where you are getting the error. (You can verify this by trying to run m.buildPathname('http://[340f:c0e0:1d1:5gc0:g4fs:2::]:22923/backend') – you'll get the same error.)
How to fix it
Since you can't get away from that regex check at the beginning of m.buildPathname(), your best bet might be to use a dynamic URL. Like so:
m.buildPathname(':url...', { url: 'http://[340f:c0e0:1d1:5gc0:g4fs:2::]:22923/backend' })
// => http://[340f:c0e0:1d1:5gc0:g4fs:2::]:22923/backend
Or when applied to your code:
async post(url, body = {}) {
return new Promise((resolve, reject) => {
m.request({method: 'POST', url: ':url...', body, params: {url}}).then((data) => {
resolve(data);
}).catch((err) => {
reject(err.message);
});
});
}
Or alternatively you can specify the (dynamic) URL as the first argument of m.request():
async post(url, body = {}) {
return new Promise((resolve, reject) => {
m.request(':url...', {method: 'POST', body, params: {url}}).then((data) => {
resolve(data);
}).catch((err) => {
reject(err.message);
});
});
}
Notice that there are three dots after the path parameter :url. Otherwise its value would be escaped/encoded. This is mentioned in the documentation of path handling. Example:
m.buildPathname(':url', { url: 'http://[340f:c0e0:1d1:5gc0:g4fs:2::]:22923/backend' })
// => http%3A%2F%2F%5B340f%3Ac0e0%3A1d1%3A5gc0%3Ag4fs%3A2%3A%3A%5D%3A22923%2Fbackend
Handling URL parameters
As mentioned in the other answer, if the URL contains parameters, the question mark will be duplicated:
m.buildPathname(':url...', { url: 'https://example.com/foo?bar=baz' })
// => https://example.com/foo??bar=baz
// ^^
One way to solve that would be to include the parameters in the path template:
const url = 'https://example.com/foo?bar=baz'
const [baseUrl, params] = url.split('?')
const template = ':baseUrl...' + (params ? `?${params}` : '')
m.buildPathname(template, { baseUrl })
// => https://example.com/foo?bar=baz
However, if there are colons in the URL parameters, there's a possibility that you'll get the same error as originally ("Template parameter names *must* be separated").
There might be a way to solve this, but the previous code sample is already quite complex for this relatively simple use case. Which leads us to:
Alternative solution: don't use m.request()
m.request() is just "a thin wrapper around XMLHttpRequest." It "returns a promise and triggers a redraw upon completion of its promise chain."
If m.request() is difficult to work with due to using IPv6 URLs (or for other reasons), it can be easier to use something else for doing XHR requests. You could for example use fetch() – just remember to call m.redraw() at the end (m.request() does this automatically).
Sure, m.request() does more than just calls m.redraw() at the end (see the docs), but it's also okay to use something else.
Thanks mts knn for the reply. We have implemented your solution however we faced below issues.
Question mark is passing two times in http url of api. Please find attached below screenshot.
In order to fix this problem, please find the updated code below
async post(url, body = {}) {
var queryIndex = url.indexOf('?');
var httpPart = url.slice(0,queryIndex);
var finalUrl = url.replace(httpPart,":url...");
return new Promise((resolve, reject) => {
m.request({method: 'POST', url: finalUrl, body, params: {url: httpPart}}).then((data) => {
resolve(data);
}).catch((err) => {
reject(err.message);
});
});
}
You can also provide efficient solution If any.

Problem w/state of a vue data object in component

I am updating my original vue project and am getting an error w/data object sports_feeds_boxscores_*. The site has three tabs to pull down scores for the three major leagues. I am adding the player stats for each game now. I first did baseball and all worked fine. Now I am doing football and the problem arises. I have three objects setup for the stats for each league. The nfl also contains an object with the three days of the week they play. What is happening is the stats for Sunday get pulled down ok but then Thursday's stats which should only be one game instead has all sunday's games plus the one thursday game. And then Monday has both Sunday & Thursdays results in it besides Mondays. I have made all the components separate as well as three separate data objects for the component props. And if I first click the nfl tab and then go to the mlb tab all results from nfl data object are in sports_feeds_boxscores_mlb. I setup a site here to better understand whats going on in using Vue.js devtools. Here is the pertinent code:
index.html:
<component
v-if="currentTabComponent === 'tab-mlb'"
v-bind:is="currentTabComponent"
v-bind:props_league_data="sports_feeds_data"
v-bind:props_league_standings="standings"
v-bind:props_baseball_playoffs="baseball_playoffs"
v-bind:props_end_of_season="end_of_season[this.currentTab.toLowerCase()]"
v-bind:props_box_game_scores_mlb="sports_feeds_boxscores_mlb"
class="tab"
>
</component>
<component
v-if="currentTabComponent === 'tab-nfl'"
v-bind:is="currentTabComponent"
v-bind:props_league_data="sports_feeds_data"
v-bind:props_league_data_nfl="nfl_feeds"
v-bind:props_league_standings="standings"
v-bind:props_nfl_playoffs="nfl_playoffs"
v-bind:props_end_of_season="end_of_season[this.currentTab.toLowerCase()]"
v-bind:props_box_game_scores_nfl="sports_feeds_boxscores_nfl"
class="tab"
>
</component>
vue.js:
data() {
return {
sports_feeds_boxscores_mlb: null,
sports_feeds_boxscores_nfl: {
sun: null,
mon: null,
thurs: null
},
sports_feeds_boxscores_nba: null,
etc
/* Component Code */
// First let's get the Game and BoxScores Data
const nflScores = async () => {
this.nfl_feeds.sunday_data = await getScores(
nflDate.sundayDate,
config
);
this.nfl_feeds.thurs_data = await getScores(
nflDate.thursdayDate,
config
);
this.nfl_feeds.mon_data = await getScores(nflDate.mondayDate, config);
// Next we need the gameid's to retrieve the game boxscores for each day
this.nfl_feeds.sunday_data.forEach(function(item, index) {
if (item.isCompleted === "true") {
nflGameIDs.sunday[index] = item.game.ID;
}
});
this.nfl_feeds.thurs_data.forEach(function(item, index) {
if (item.isCompleted === "true") {
nflGameIDs.thursday[index] = item.game.ID;
}
});
this.nfl_feeds.mon_data.forEach(function(item, index) {
if (item.isCompleted === "true") {
nflGameIDs.monday[index] = item.game.ID;
}
});
// Check if boxscores have been retrieved on previous tab click for each day
// if not retrieve the boxscores
this.sports_feeds_boxscores_nfl.sun =
this.sports_feeds_boxscores_nfl.sun ||
(await getBoxScores(nflGameIDs.sunday, url, params));
this.sports_feeds_boxscores_nfl.thurs =
(await getBoxScores(nflGameIDs.thursday, url, params));
this.sports_feeds_boxscores_nfl.mon =
this.sports_feeds_boxscores_nfl.mon ||
(await getBoxScores(nflGameIDs.monday, url, params));
}; /* End nflScores Async function */
getBoxScores.js:
try {
const getBoxScores = async (gameIDs, myUrl, params) => {
gameIDs.forEach(function(item) {
promises.push(
axios({
method: "get",
headers: {
Authorization:
"Basic &&*&&^&&=="
},
url: myUrl + item,
params: params
})
);
});
// axios.all returns a single Promise that resolves when all of the promises passed
// as an iterable have resolved. This single promise, when resolved, is passed to the
// "then" and into the "values" parameter.
await axios.all(promises).then(function(values) {
boxScores = values;
});
console.log(`boxScores is ${boxScores.length}`)
return boxScores;
};
module.exports = getBoxScores;
} catch (err) {
console.log(err);
}
I have split up all the sports_feeds_boxscores objects and at a loss as to why they are sharing state??? Sorry for verbosity of the question but it is somewhat complex. That is why I provided the site where you can see devtools that for instance this.sports_feeds_boxscores_nfl.thurs has 14 elements instead of one after the call to API. And if mlb tab is clicked after nfl tab then mlb results include the nfl results. I would really appreciate help in figuring this out. Thanks in advance...
Update:
I have added getBoxScores.js cause it seems as if I am returning the extra stats from this call.
This was my bad. I didnt realize I had created a closure in getBoxScores.js:
let boxScores = [];
let promises = [];
try {
const getBoxScores = async (gameIDs, myUrl, params) => {
gameIDs.forEach(function(item) {
promises.push(
axios({
method: "get",
headers: {
Authorization:
"Basic &&^^&^&&^FGG="
},
url: myUrl + item,
params: params
})
);
});
Moving declarations inside async function quickly solved trouble. URRRRGGGHHH!!!

Dynamically render XMLHttpRequest data to Vue element

I am trying to lazy load a list of posts from the WordPress REST API, and I want to load more news stories on click of an HTML element.
At the moment, I am having trouble accessing the original Vue instance and updating this.data with response because the async function means the instance is out of scope.
I am also duplicating code at the at the moment in getAggregatorData which is a standard XMLHttpRequest to an endpoint.
The question: How do I access Vue instances from inside an asynchronous call (if at all possible)?
Has anyone had any experience with lazy loading content from a REST API? This is my first refactor with Vue for this sort of technology, so happy to flesh out any questions there are.
<div class="aggregator-load-more d-inline-block ta-centre my-3" v-cloak>
<span class="bg-primary c-white px-1 py-0-5 tf-allcaps" v-on:click="lazyLoad">Load more</span>
</div>
const news = getAggregatorData(
'POST',
WP_VARS.base_url + '/wp-json/wp/v2/' + aggregator.endpoint,
aggregator,
function(error, response){
if (error) {
console.log('Error: could not retrieve data');
} else {
aggregator.offset = response.length;
const vueInstance = new Vue({
el: '.aggregator',
data: {
largeItems: response.slice(0, 1),
smallItems: response.slice(1, response.length),
},
methods: {
lazyLoad() {
const moreStories = getAggregatorData(
'POST',
WP_VARS.base_url + '/wp-json/wp/v2/' + aggregator.endpoint,
aggregator,
function(error, response) {
if(error) {
console.log('Error: failed to retrieve more news stories');
} else {
return response;
}
});
this.smallItems.push(moreStories);
},
},
});
}
}
);
In lazyLoad(), you just need to pass a reference to this (the Vue instance) into the callback of getAggregatorData. This can be simply done with an arrow function, which automatically binds this to the current context in the method, which is the Vue instance:
methods: {
lazyLoad() {
getAggregatorData(
...
👉 (error, response) => {
if(error) {
console.log('Error: failed to retrieve more news stories');
} else {
/* `this` refers to the Vue instance */
this.smallItems.push(response);
}
});
},
},

v-if Not work when the function return true or false [VueJs]

I am working on VueJs, And In my template section .. I defined a condition to check if the image URL exists or not.
template:
<div v-for="(sub, key) in Work.sub" :key="sub.id" >
<span v-if="Image('https://XXXX.blob.core.windows.net/XXXX/XXXXX-' + key +'.png')" >
<b-img :src="'https://XXXX.blob.core.windows.net/XXXX/XXXXX-' + key +'.png'" />
</span>
<span v-else>
<b-img :src="default_avatar"/>
</span>
</div>
In script:
Image: function(img_url)
{
return axios({
method: "GET",
timeout: 3000,
headers: {
.......................
},
url: img_url
})
.then( function(response){
this.ifImageExist = true;
return this.ifImageExist;
})
.catch( function(error){
this.ifImageExist = false;
return this.ifImageExist;
})
},
For default_avatar it is aleady definded in the data section and no problem with it.
My problem is when the Image function checks if the image URL exists or not. If it exists it provides the image in the given URL, but if it does not exist, the image will be blank!
For example:
when I run the code, I result will be like this:
But I want the first image to filled by default image, not show does not exist icon!
How to solve this problem?
First of all, your function Image() doesn't return a Boolean it returns a Promise (from axios),
so v-if evaluates as true;
To get v-if working with a API call (Axios GET), the simplest way is to turn that Image method in a Async Method
Image: async function(img_url)
{
return axios({
method: "GET",
timeout: 3000,
headers: {
.......................
},
url: img_url
})
.then( function(response){
this.ifImageExist = true;
return this.ifImageExist;
})
.catch( function(error){
this.ifImageExist = false;
return this.ifImageExist;
})
},
If you have all build configured right to work with async function, the method will wait for the response and evaluate to the Boolean which v-if is expecting.
Alternative solution: It looks like the only diff between the v-if and v-else is b-img's src, so an alternative would be to move the logic into JavaScript, mapping Work.sub into an array of image URLs that default to default_avatar only if the URL doesn't resolve.
So your template would be:
<template>
<div v-for="(img, index) in images" :key="index">
<b-img :src="img" />
</div>
</template
You'd add a data property to hold the image URLs:
data() {
return {
images: []
}
}
And a watcher on Work.sub, which sets this.images:
watch: {
'Work.sub': {
immediate: true,
async handler(sub) {
// For each image URL, attempt to fetch the image, and if it returns
// data (image exists), collect the URL. Otherwise, default.
this.images = await Promise.all(Object.keys(sub).map(async (key) => {
const img = sub[key];
if (!img) return this.default_avatar;
const url = `//placekitten.com/${img}`;
const { data } = await axios.get(url);
return data ? url : this.default_avatar;
}));
}
}
}
demo

Async Computed in Components - VueJS?

I'm finding a solution to async computed method in Components:
Currently, my component is:
<div class="msg_content">
{{messages}}
</div>
<script>
export default {
computed: {
messages: {
get () {
return api.get(`/users/${this.value.username}/message/`, {'headers': { 'Authorization': 'JWT ...' }})
.then(response => response.data)
}
}
},
}
</script>
Result:
{}
How to rewrite it in Promise mode? Because I think we can async computed by writing into Promise mode.
Computed properties are basically functions that cache their results so that they don't have to be calculated every time they are needed. They updated automatically based on the reactive values they use.
Your computed does not use any reactive items, so there's no point in its being a computed. It returns a Promise now (assuming the usual behavior of then).
It's not entirely clear what you want to achieve, but my best guess is that you should create a data item to hold response.data, and make your api.get call in the created hook. Something like
export default {
data() {
return {
//...
messages: []
};
},
created() {
api.get(`/users/${this.value.username}/message/`, {
'headers': {
'Authorization': 'JWT ...'
}
})
.then(response => this.messages = response.data);
}
}
es7 makes doing this quite trivial by using async and await in conjunction with axios' returned promise. You'll need the vue-async-computed package.
export default {
asyncComputed: {
async myResolvedValue() {
return await api.get(`/users/${this.value.username}/message/`, {'headers': { 'Authorization': 'JWT ...' }})
.then(response => response.data)
}
}
}
I bumped on a similar case where I need to re-run the computed function to fetch from a server every time a data or a props changes.
Without installing any extra package (vue-async-computed or vue3-async-computed as pointed by other answers), you can force a data to reload by creating a "virtual" computed method.
Lets say you want to fetch data from the server every time the user types their username, and depending on what was typed, you want to show a given message from the server.
From the example below, username and messages in this case are both reactive data, but there is no direct connection between them, so lets create a computed that depends on username by returning its value, which will force it to be called every time username is changed. Now you just need to call a function that can be async and will update messages after fetching from the server.
In the example below, I use ":dummy" just to force a call to my computed function.
<template>
<input v-model="username">
<div class="msg_content" :dummy="force_react">
{{messages}}
</div>
</template>
<script>
export default {
data: function () {
return {
messages: "",
username: "",
};
},
computed: {
force_react: function() {
this.get(); // called every time that this.username updates
return this.username; // becase its result depends on username
}
},
methods: {
async get() { // get's called every time that this.username updates
console.log("Got called");
let response = await api.get(`/users/${this.username}/message/`, {'headers': { 'Authorization': 'JWT ...' }});
this.messages = response.data;
}
},
}
</script>
You can see a working example here in Vue SFC playground
Why not using a watcher? Vue Documentation
You can use async function in the callback of the watcher.

Categories