Vuetify loader which depends on API response - vue.js

I made a login form and button with preloader:
<v-btn #click="login" :loading="loading4" :disabled="loading4"
#click.native="loader = 'loading4'">
{{ $t('forms.labels.loginBtn') }}
<span slot="loader" class="btn-loader">
<v-icon light>cached</v-icon>
</span>
</v-btn>
I want to show btn preloader when api response is 'pending'.
I fetch api status from computed:
...mapGetters({
loginStatus: 'auth/authStatus'
}),
In Vuetify docs I found only solution with setTimeout and I don't know how to customize it to my api response:
watch: {
loader () {
const l = this.loader
this[l] = !this[l]
setTimeout(() => (this[l] = false), 3000)
this.loader = null
}
}
My store:
const state = {
token: localStorage.getItem('user-token'),
status: null
}
I want to show preloader only when state status is 'loading'. I'm changing state using mutation.
How to do this and what this[l] means?
Thanks.

Using brackets or [ and ] is just an another way for accessing properties in your Javascript object aside from the dot or . operator.
The brackets is usually used for accessing properties dynamically.
For example, the most common way to access an object property is like this:
this.loading4 = true;
But, you can also do it like this if you want to:
this['loading4'] = true;
and you can also supply a variable instead of a string literal:
const l = 'loading4';
this[l] = true;
It's basically like you are treating your object like a multi-dimensional array in PHP.

You could also try using loading as a state in your Vuex module without using watch hook just like #Jeremy Walters suggested.
Vuex
state: {
loading: false
},
getters: {
isLoading(state) {
return state.loading
},
mutations: {
loginSuccess(state, payload) {
state.loading = false //ends the loader
...
},
loginFailed(state, payload) {
state.loading = false //ends the loader
...
},
},
actions: {
login({state,commit},credentials) {
state.loading = true //starts the loader
axios.post('/api/auth/login', credentials)
.then((response) => {
commit("loginSuccess", response.data)
})
.catch((error) => {
commit("loginFailed", error.response.data)
})
}
Then in your component
HTML
<v-btn #click="login" :loading="isLoading" :disabled="isLoading">
{{ $t('forms.labels.loginBtn') }}
<span slot="loader" class="btn-loader">
<v-icon light>cached</v-icon>
</span>
</v-btn>
JS
...mapGetters({
'isLoading'
})
Regarding the explanation for the this[l], it was already explained nicely by the answer below.

Related

Vuetify autocomplete not showing suggestions with more than one search term

When I make a search with Vuetify <v-autocomplete> and my API, mytext and myvalue are correctly updated and displayed in the suggestions only if write a word like FOO, if I write a string like FOO BAR, then I get the correct result with console.log(response.data) in the API call method, but I get nothing in the suggestions of <v-autocomplete>.
<template>:
<v-autocomplete
v-model="select"
:loading="loading"
:items="items"
item-text="mytext"
item-value="myvalue"
:search-input.sync="search"
hide-no-data
hide-details
label="My Autocomplete"
>
<template v-slot:item="data">
<v-list-item-content>
<v-list-item-title v-html="data.item.mytext"></v-list-item-title>
<v-list-item-subtitle
v-html="data.item.myvalue"
></v-list-item-subtitle
></v-list-item-content>
</template>
</v-autocomplete>
<script>:
<script>
export default {
data() {
return {
select: null,
loading: false,
items: [],
search: null
}
},
watch: {
search(val) {
console.log('search: ' + val)
val && val !== this.select && this.query(val)
}
},
methods: {
async query(v) {
this.loading = true
await this.$axios
.get('/api/foo', {
params: {
search: this.search
}
})
.then((response) => {
console.log(response.data)
this.items = response.data
})
.catch((error) => {
console.log(error)
})
.finally(() => {
this.loading = false
})
}
}
}
</script>
The search variable seems to be linked to the items variable.
You can apply no-filter prop to your v-autocomplete component.
<v-autocomplete
...
no-filter
...
>
</v-autocomplete>
As written in documentation for this prop:
Do not apply filtering when searching. Useful when data is being
filtered server side
https://vuetifyjs.com/en/api/v-autocomplete/#props
I finally fixed it by adding this prop to <v-autocomplete>:
:filter="() => true"

Reusable Vue Components - How to use props to define v-for array and path for unique Axios responses

I'm using Vue components for multiple inputs with different axios url's and responses. Then using a v-for loop for the response to be displayed which can be selected.
idea:
Input 1 > Axios GET user data
Input 2 > Axios GET colour data
Input 3 > Axios GET model data
etc, each Axios response can have a different response array and objects.
I can set the different Axios GET url's by using props, but how can I use props to define the v-for array path and object path?
example image sample showing needed link between prop and v-for:
Can I use props to define the array path and object in the f-vor loop? In the example code below I need to use the prop from the component to define the array and object paths. note I'm using a axios sample response for this demo.
Vue.component("my-component", {
template: `
<div style="position:absolute"><input :placeholder="this.input_placeholder" #keyup="if(input_value.length > 2 ){ search() }" v-on:blur="input_clear()" v-model="input_value" /><i v-if="loading_spinner" class="fas fa-spinner fa-pulse"></i><div class="supplier_select_popup" v-if="response_popup_show"><div v-for="data,i in response_array.bpi" v-on:click="response_select(i)">{{ data.code }}</div></div></div>`,
props: {
api_url: "",
api_response_path: "",
data_path: "",
},
data: function() {
return {
input_placeholder: "Search",
input_value: "",
selected_value: "",
loading_spinner: false,
response_popup_show: false,
response_array: [],
};
},
methods: {
// Fetch Data
search: function() {
this.response_popup_show = false
this.loading_spinner = true
clearTimeout(this.myVar)
this.myVar = setTimeout(
function() {
axios
.get(
this.api_url
)
.then((response) => {
this.response_array = response.data
console.log(this.response_array)
this.response_popup_show = true
this.loading_spinner = false
})
.catch((error) => {
console.log(error)
this.errored = true;
this.response_popup_show = false
})
.finally(() => (this.loading = false))
}.bind(this),
1000
);
},
// Response Select
response_select: function(i) {
this.input_value = [i]
this.selected_value = [i]
this.response_popup_show = false
},
// Response Clear
input_clear: function() {
var self = this;
setTimeout(function() {
self.response_popup_show = false
self.loading_spinner = false
if (self.selected_value.length != 0) {
self.input_value = self.selected_value
} else {
self.input_value = ""
}
}, 100);
}
}
});
const App = new Vue({
el: "#app",
methods: {}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>
<script src="https://kit.fontawesome.com/17cdac82ba.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<my-component class="clickable" api_url="https://api.coindesk.com/v1/bpi/currentprice.json" api_response_path="response_array.bpi" data_path="date.code">
</my-component>
</div>
Yes, you can do this by passing the property you want to use for both the API response and each data item, but I prefer to generalize it by passing "map" functions, that way you aren't limited in any way by how you want to transform the data:
props: [
'map_response',
'map_data',
]
<div v-for="data, i in map_response(responseArray)">
{{ map_data(data) }}
</div>
You pass the props like this:
<my-component
class="clickable"
api_url="http://api.example.com/stuff"
:map_response="response => response.bpi"
:map_data="data => data.code"
>

Duplicate keys Vuex Getters every time route changes

I am getting an array of objects from firebase and showing them characters component in list format using V-for. Everytime I go to homepage and returning to characters page the list are getting multiplied and showing me duplicate keys.
characters.vue:
<template>
<ul class="characters-list">
<li v-for="allHero in getAllHeros" v-bind:key="allHero.id">
<router-link :to="{ name: 'characterDetail', params: { id: allHero.id } }">
<div class="hero-thumbnail">
<img :src="allHero.imageUrl" :alt="allHero.title" />
</div>
<div class="hero-detail">
<h3>{{ allHero.name }}</h3>
</div>
</router-link>
</li>
</ul>
</template>
import database from "#/firebase/init";
computed: {
...mapGetters({ getAllHeros: "getAllHeros" }),
},
created() {
database
.collection("heros")
.get()
.then(snapshot => {
snapshot.forEach(doc => {
let heros = doc.data();
heros.id = doc.id;
this.$store.dispatch('fetchAllHeros', heros)
});
});
}
VUEX Module -
const state = {
allHeros: []
};
const getters = {
getAllHeros: state => {
return state.allHeros;
}
};
const actions = {
async fetchAllHeros({ commit }, heros) {
commit("setAllHeros", heros);
}
};
const mutations = {
setAllHeros: (state, payload) => {
state.allHeros.push(payload);
}
};
When you route to a new page your Vuex store does not necessarily get reset to its initial state. Therefore every time that component is created you are adding more heros to the vuex store which is resulting in duplicate heros being added.
To prevent this, you can just use some simple logic to check if any heroes have been loaded:
created() {
if(this.getAllHeros.length == 0){
//Get heros from database..
};
}

Vue.js Send an index with #input event

Vue version : 3.1.1
Hey guys,
I'm working with dynamic Creation Component, which means a user can add whatever of component he wants.I create it base on this documentation dynamic component creation.
And I use this component vue image uploader.
I need to send an index when the user wants to upload the image, like this :
<div v-for="(line, index) in lines" v-bind:key="index">
{{index}}//if i log the index its 0,1,2,3 and its ok
...
<image-uploader
:preview="true"
:class-name="['fileinput', { 'fileinput--loaded': line.hasImage }]"
:capture="false"
:debug="0"
:auto-rotate="true"
output-format="blob"
accept="image/*"
#input="setImage(output , index)"
:ref="'fileUpload'+index"
>
...
And the setImage funciton :
setImage: function(output,index) {
console.log(index);
console.log(output);
return ;
this.lines[index].hasImage = true;
this.lines[index].image = output;
let formData = new FormData();
formData.append("file", output);
Ax.post(upload_route, formData, {
headers: { "Content-Type": "multipart/form-data" }
})
.then(response => {
// upload successful
})
.catch(error => console.log(error));
}
And the log result is:
The index always is 0 :(
How can i send an index when i want to upload it?
I read this passing event and index and test it but it's not working on component.
Because This is a custom event not a DOM event.
what should I do?
thanks.
Because you're actually passing the return value of setImage to the #input, not the method.
You can't just add extra parameters to setImage, as ImageUploader component just emit an image to the setImage. If you need to add extra parameters to that method, you need to create custom element that wrap ImageUploader.
It's something like this:
ImageUpload.vue
<template>
<image-uploader
:debug="0"
:autoRotate="true"
outputFormat="blob"
:preview="true"
:className="['fileinput', { 'fileinput--loaded' : hasImage }]"
:capture="false"
accept="image/*"
doNotResize="['gif', 'svg']"
#input="setImage"
v-on="listeners" />
</template>
<script>
export default {
props: {
index: {
required: true,
type: Number
}
},
data() {
return {
hasImage: false,
image: null
};
},
computed: {
listeners() {
const listeners = { ...this.$listeners };
const customs = ["input"];
customs.forEach(name => {
if (listeners.hasOwnProperty(name)) {
delete listeners[name];
}
});
return listeners;
}
},
methods: {
setImage(image) {
this.hasImage = true;
this.image = image;
this.$emit("input", this.index, image); // here, we emit two params, as index for the first argument, and the image at the second argument
}
}
};
</script>
Then, you can use that component something like this:
<template>
<div class="container">
<div v-for="(line, index) in lines" :key="index">
<image-upload :index="index" #input="setImage"/>
</div>
</div>
</template>
<script>
import ImageUpload from "./ImageUpload";
export default {
components: {
ImageUpload
},
data() {
return {
lines: ["1", "2", "3", "4"]
};
},
methods: {
setImage(index, image) {
console.log("Result", index, image);
}
}
};
</script>
See the working example: https://codesandbox.io/s/vue-template-ccn0e
Just use $event like this...
#input="setImage($event, index)"
...and you're done!

How to update state in a component when the value changed at vuex store?

In vuex store I have this mutations which receives msg from one component and is to show/hide prompt message at another component (Like You are logged in propmpt after successful login) :
setPromptMsg: function (state, msg) {
state.showPromptMsg = true;
state.promptMsg = msg;
function sleep (time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
sleep(3000).then(() => {
store.showPromptMsg = false;
state.promptMsg = '';
console.log('show message set to false');
});
},
In the compoenet, I receive showPromptMsg from the store as a computed property:
computed: {
showPromptMsg () {
return this.$store.state.showPromptMsg;
},
promptMsg () {
return this.$store.state.promptMsg;
}
}
The show/hide prompt message in the template:
<div v-show="showPromptMsg">
<div class="text-center" >
<strong> {{promptMsg}} </strong>
</div>
</div>
The problem is that when the prompt is timedout, i.e. showPromptMsg is set to false at the store, the change is not reflected into the component, so the notification box does not disappear.
I'm wondering what is the idiomatic way to resolve this problem?
The code is setting
store.showPromptMsg = false;
I expect you want
state.showPromptMsg = false;
In your NotificationBarComponent.vue template:
<div>
<div class="text-center" >
<strong> {{ message }} </strong>
</div>
</div>
In your NotificationBarComponent.vue component definition add a prop to pass custom message to display and on mounted start the timeout to hide the notification:
export.default {
props: ['message'],
mounted() {
window.setTimeout(() => {
this.$store.commit('handleMessageState', false)
}, 3000);
}
};
in your store create a property to manage the notification display isNotificationBarDisplayed: false
handleMessageState(state, payload) {
state.isNotificationBarDisplayed = payload;
}
anywhere you want to use it:
<notification-bar-component v-show="isNotificationBarDisplayed" message="Some message here"></notification-bar-component>
computed: {
isNotificationBarDisplayed () {
return this.$store.state.isNotificationBarDisplayed;
},
}