I'm new to Vue.js but trying to get to grips with it. So far it has gone well and I've got quite far but I am stuck extending a parents template.
I am trying to make dashboard widgets that extend a default widget layout (in Boostrap). Please note that the below code is using Vue, Require, Underscore & Axios.
Parent file - _Global.vue
<template>
<div class="panel panel-default">
<div class="panel-heading">
<b>{{ widgetTitle }}</b>
<div class="pull-right">
<a href="#" v-on:click="toggleMinimized"
v-bind:title="(isMinimized ? 'Show widget' : 'Hide widget')">
<i class="fa fa-fw" v-bind:class="isMinimized ? 'fa-plus' : 'fa-minus'"></i>
</a>
</div>
</div>
<div class="panel-body" v-if="!isMinimized">
<div class="text-center text-muted" v-if="!isLoaded">
<i class="fa fa-spin fa-circle-o-notch"></i><br />
</div>
<parent v-if="isLoaded">
<!-- parent content should appear here when loaded -->
</parent>
</div>
</div>
</template>
<script>
export default {
// setup our widget props
props: {
'minimized': {
'default': false,
'required': false,
'type': Boolean
}
},
// define our data
data: function () {
return {
widgetTitle: 'Set widget title in data',
isLoaded: false,
isMinimized: this.$props.minimized
}
},
// when vue is mounted, open our widget
mounted: function () {
if(!this.isMinimized) {
this.opened();
}
},
// define our methods
methods: {
// store our widget state to database
storeWidgetState: function () {
// set our data to send
let data = {
'action' : 'toggleWidget',
'widget' : this.$options._componentTag,
'state' : !this.isMinimized
};
// post our data to our endpoint
axios.post(axios.endpoint, data);
},
// toggle our minimized data
toggleMinimized: function (e) {
// prevent default
e.preventDefault();
// toggle our minimized state
this.isMinimized = !this.isMinimized;
// trigger opened if we aren't minimized
if(!this.isMinimized) this.opened();
// save our widget state to database
this.storeWidgetState();
},
// triggered when opened from being minimized
opened: function () {
console.log('opened() method is where all widget logic should be placed');
}
}
}
</script>
Child file - Example.vue
Should extend _Global.vue using mixins and then display content within .panel-body
<template>
<div>
I want this content to appear inside the .panel-body div
{{ content }}
<img v-bind:src="image.src" v-bind:alt="image.alt"
v-if="image.src" class="img-responsive" style="margin: 0 auto" />
</div>
</template>
<script>
// import our widgets globals
import Global from './_Global.vue'
export default {
components: {
'parent': {
// what can I possibly put here??
}
},
// use our global mixin for all widgets
mixins: [Global],
// setup our methods for this widget
methods: {
opened: _.debounce(function () {
// make sure this can only be opened once
if(this.hasBeenOpened) return;
this.hasBeenOpened = true;
// temporarily allow axios to make external requests
let axiosHeaders = axios.defaults.headers.common;
let vm = this;
axios.defaults.headers.common = {};
axios.get('https://yesno.wtf/api')
.then(function (res) {
// set our content
vm.content = null;
// set our image content
vm.image.src = res.data.image;
vm.image.alt = res.data.answer;
})
.catch(function (err) {
// set our error text
vm.content = String(err);
})
.then(function () {
// this will always hit..
vm.isLoaded = true;
});
// restore our axios headers for security
axios.defaults.headers.common = axiosHeaders;
}, 300)
},
// additional data
data: function () {
return {
// set our widgets title
widgetTitle: 'Test title',
// logic for the specific widget
hasBeenOpened: false,
content: 'Loaded and ready to go...',
image: {
src: false,
alt: null
}
};
},
}
</script>
Currently my parent template is just completely overwriting my child view. The only way I can get it to work is by explicitly defining the template parameter inside components -> parent: {} but I don't want to have to do that...?
Ok, thanks to Gerardo Rosciano for pointing me the in right direction. I've used to slots to come up with an eventual solution. We then access parent methods and data attributes just to get everything working as it should.
Example.vue - our example widget
<template>
<div>
<widget-wrapper>
<span slot="header">Example widget</span>
<div slot="content">
<img v-bind:src="image.src" v-bind:alt="image.alt"
v-if="image.src" class="img-responsive" style="margin: 0 auto" />
{{ content }}
</div>
</widget-wrapper>
</div>
</template>
<script>
// import our widgets globals
import WidgetWrapper from './_Widget.vue'
export default {
// setup our components
components: {
'widget-wrapper': WidgetWrapper
},
// set our elements props
props: {
'minimized': {
'type': Boolean,
'default': false,
'required': false
}
},
// setup our methods for this widget
methods: {
loadContent: _.debounce(function () {
// make sure this can only be opened once
if(this.hasBeenOpened) return;
this.hasBeenOpened = true;
// temporarily allow axios to make external requests
let axiosHeaders = axios.defaults.headers.common;
let vm = this;
axios.defaults.headers.common = {};
axios.get('https://yesno.wtf/api')
.then(function (res) {
// set our content
vm.content = null;
// set our image content
vm.image.src = res.data.image;
vm.image.alt = res.data.answer;
})
.catch(function (err) {
// set our error text
vm.content = String(err);
})
.then(function () {
// this will always hit..
vm.isLoaded = true;
});
// restore our axios headers for security
axios.defaults.headers.common = axiosHeaders;
}, 300)
},
// additional data
data: function () {
return {
// global param for parent
isLoaded: false,
// logic for the specific widget
hasBeenOpened: false,
content: 'Loaded and ready to go...',
image: {
src: false,
alt: null
}
};
},
}
</script>
_Widget.vue - our base widget that gets extended
<template>
<div class="panel panel-default">
<div class="panel-heading">
<b><slot name="header">Slot header title</slot></b>
<div class="pull-right">
<a href="#" v-on:click="toggleMinimized"
v-bind:title="(minimized ? 'Show widget' : 'Hide widget')">
<i class="fa fa-fw" v-bind:class="minimized ? 'fa-plus' : 'fa-minus'"></i>
</a>
</div>
</div>
<div class="panel-body" v-if="!minimized">
<div class="text-center text-muted" v-if="!isLoaded">
<i class="fa fa-spin fa-circle-o-notch"></i><br />
Loading...
</div>
<div v-else>
<slot name="content"></slot>
</div>
</div>
</div>
</template>
<script>
export default {
// get loaded state from our parent
computed: {
isLoaded: function () {
return this.$parent.isLoaded;
}
},
// set our data element
data: function () {
return {
minimized: false
}
},
// when the widget is mounted, trigger open state
mounted: function () {
this.minimized = this.$parent.minimized;
if(!this.minimized) this.opened();
},
// methods to manipulate our widget
methods: {
// save our widget state to database
storeWidgetState: function () {
// set our data to send
let data = {
'action' : 'toggleWidget',
'widget' : this.$parent.$options._componentTag,
'state' : !this.minimized
};
// post this data to our endpoint
axios.post(axios.endpoint, data);
},
// toggle our minimized state
toggleMinimized: function (e) {
// prevent default
e.preventDefault();
// toggle our minimized state
this.minimized = !this.minimized;
// trigger opened if we aren't minimized
if(!this.minimized) this.opened();
// save our widget state to database
this.storeWidgetState();
},
// when widget is opened, load content
opened: function () {
// make sure we have a valid loadContent method
if(typeof this.$parent.loadContent === "function") {
this.$parent.loadContent();
} else {
console.log('You need to define a loadContent() method on the widget');
}
}
}
}
</script>
Related
layoutchange() {
this.layout = !this.layout;
if (this.layout === true) {
this.perPage = this.layout ? 8 : 12;
this.listProducts();
} else {
this.perPage = !this.layout ? 12 : 8;
this.gridProducts();
}
},
<a class="list-icon" v-bind:class="{ active: layout == true }" v-on:click="layoutchange"></a>
<a class="grid-icon" v-bind:class="{ active: layout == false }" v-on:click="layoutchange"></a>
<ul v-if="layout == true">
//code for product display
<b-pagination v-model="currentPage" :total-rows="rows" :per-page="perPage"></b-pagination>
</ul>
<ul v-if="layout == false">
//code for product display
<b-pagination v-model="currentPage" :total-rows="rows" :per-page="perPage"></b-pagination>
</ul
Basically i am trying to add the api call for the each page,(i have a api which need to call) for suppose if i click on pagination page no 1, i need to fire api, and same page 2 need to call api. Now i have a doubt, Now i am using the b-pagination (bootstrap-vue) are there any event to call for each page? like next previous or any event based. so with same name, i can call api using that.
I am using fr grid and list view, For both i have pagination
Reference document https://bootstrap-vue.org/docs/components/pagination
If there is no event provided by b-pagination that you can use, in that specific usecase, you can just watch the currentPage property.
https://v2.vuejs.org/v2/guide/computed.html#Watchers
export default {
data() {
return {
currentPage: null,
}
},
watch: {
currentPage(newVal) {
if(newVal) {
// Call the api
// Random api endpoint as example
const endpoint = 'https://jsonplaceholder.typicode.com/todos/'
fetch(endpoint + newVal).then((res) => {
console.log(res)
// update corresponding data
})
}
}
},
mounted() {
// Initialise currentPage to your route or 1 by default as example
this.currentPage = 1
}
}
I have a pretty simple view that displays the icons of all characters from a certain game. If I were to visit the URL that displays that view through a router-link, everything works fine and I see the icons, however, if I then refresh the page, the icons disappear.
They also do not render at all if I manually type www.example.com/champions. Why is this happening.
My component:
<template>
<div class='wrapper'>
<div class="champions-container">
<div v-for='champion in champions' class="champion">
<img class='responsive-image' :src="'http://ddragon.leagueoflegends.com/cdn/' + $store.getters.version + '/img/champion/' + champion.image.full" alt="">
</div>
</div>
</div>
</template>
<script>
export default {
data(){
return {
champions: this.$store.state.fullChampions
}
}
}
</script>
And my Vuex store where the champions are stored:
export default new Vuex.Store({
state: {
version: null,
fullChampions: null
},
mutations: {
version(state, data){
state.version = data.version
},
fullChampions(state, data){
state.fullChampions = data.fullChampions
}
},
actions: {
getVersion({commit}){
return axios.get("http://ddragon.leagueoflegends.com/api/versions.json")
.then((response) => {
commit('version', {
version: response.data[0]
})
})
.catch(function (error) {
console.log(error);
})
},
getFullChampions({commit, state}){
return axios.get("https://ddragon.leagueoflegends.com/cdn/" + state.version + "/data/en_US/championFull.json")
.then((response) => {
commit('fullChampions', {
fullChampions: Object.values(response.data.data)
})
})
.catch(function (error) {
console.log(error);
})
},
These might be because of these issues you encountered.
First: that component is not the one that dispatched your getFullChampions function in your vuex, might be in other component.
Second is that, you are already assigning the value of champions wherein the state fullChampions is not updated.
this.champions: this.$store.state.fullChampions // state.fullChampions might not yet updated.
Try this one might help you
watch: {
'$store.state.fullChampions': function() {
this.champions = this.$store.state.fullChampions
},
}
Last is to to do first a condition above your v-for to prevent the element
<div class="champions-container" v-if=""$store.getters.version>
<div v-for='champion in champions' class="champion">
<img class='responsive-image' :src="'http://ddragon.leagueoflegends.com/cdn/' + $store.getters.version + '/img/champion/' + champion.image.full" alt="">
</div>
</div>
Can you try to add this:
watch: {
$route: function(val, OldVal){
this.champions = this.$store.state.fullChampions;
}
},
after yuor data?
Upd.
If you are calling getFullChampions() action, then you can call it within watcher of my example instead of assigning to this.champions.
I have a simple popUp component that I use in my entire app, it get's called by emitting an event on click. There are two types of popUps, success and danger. The success popUp should disappear on it's own after 5 seconds, the danger should get closed when clicked on the x sign. Currently it works the way I have created it, but if the user creates more than one danger popUp, then a success one and again a danger one, the danger one disappears after 5 seconds and not the success one. How can I make so that my success popUp disappears properly after 5 seconds? I am calling it here like this but it seems that it does not delete them correctly:
if(obj.type === 'success') {
setTimeout(this.closePopUp, 5000);
}
Here is my code:
<template>
<div class="popUp-wrapper">
<div
v-for="item in allItems"
:key="item.id"
:class="['popUp', `popUp--type--${item.newPopUpType}`]"
>
<div class="popUp-side">
<p class="exclamation-mark">!</p>
</div>
<h5 class="popUp-message">{{item.message}}</h5>
<div class="popUp-side">
<p class="closing-x" #click="closePopUp(item)" v-if="item.newPopUpType
=== 'danger'">X</p>
</div>
</div>
</div>
</template>
<script>
export default {
data: () => ({
allItems: []
}),
methods: {
closePopUp(item) {
const index = this.allItems.indexOf(item);
this.allItems.splice(index, 1);
},
onPopUpCall(obj) {
var newPopUp = {
newPopUpType: obj.type,
message: obj.message,
id: obj.id
};
if(obj.type === 'success') {
setTimeout(this.closePopUp, 5000);
}
this.allItems.push(newPopUp);
}
},
created() {
this.$root.$on('call-popUp', this.onPopUpCall);
},
destroyed() {
this.$root.$off('call-popUp', this.onPopUpCall);
}
};
</script>
closePopUp requires an item argument based on your code which you are not providing.
Try this:
if(obj.type === 'success') {
setTimeout(() => this.closePopUp(newPopUp), 5000);
}
I have created a settings page where users can update their email addresses. Everything worked fine but suddenly the validation is not updating anymore. Only the first change of the input field triggers validateState().
Any further changes will not trigger this function so the status of that field stays as it is.
I have compared the code with other components that use the same code and they still work fine.
I am using bootstrap-vue components for the form.
<template>
<div class="row">
<div class="col-md-12">
<b-form #submit="onSubmit">
<b-form-group :label="$t('general.email')"
label-for="settingsEmail"
:invalid-feedback="errors.first('email')">
<b-form-input id="settingsEmail"
type="text"
v-model="form.email"
:disabled="saving"
name="email"
:state="validateState('email')"
v-validate="{required: true, email: true}">
</b-form-input>
</b-form-group>
<b-button type="submit" variant="primary" :disabled="saving || !hasChanged() || errors.any()"><i class="fa fa-refresh fa-spin fa-fw" v-if="saving"></i> {{$t('general.save')}}</b-button>
</b-form>
</div>
</div>
</template>
<script>
import {UPDATE_USER} from '../config/actions'
export default {
name: 'settingsAccount',
data() {
return {
form: {},
saving: false
}
},
computed: {
user: function() {
return this.$store.getters.getUser;
}
},
created() {
this.init();
},
methods: {
init() {
this.form.email = this.user.email;
},
hasChanged() {
if(this.form.email !== this.user.email) {
return true;
}
return false;
},
onSubmit(event) {
event.preventDefault();
this.saving = true;
this.$validator.validateAll().then((result) => {
if (result) {
let data = {};
if(this.form.email !== this.user.email) {
data.email = this.form.email;
}
this.$store.dispatch(UPDATE_USER, data).then(() => {
this.saving = false;
this.$validator.reset();
}).catch(() => {
this.saving = false;
});
} else {
this.saving = false;
}
});
},
validateState(ref) {
if (this.veeFields[ref] && (this.veeFields[ref].dirty || this.veeFields[ref].validated)) {
return !this.errors.has(ref)
}
return null
},
}
}
</script>
The problem you're having is that the form data element is an empty object, so it will only trigger reactivity when the whole object changes. Either you need to change your data to be this:
data() {
return {
form: {email:''},
saving: false
}
},
Or in your init function, explicitly add the email property as reactive:
methods: {
init() {
this.$set(form,'email',this.user.email)
},
//...
If you're not clear on why, you can read the details here: https://v2.vuejs.org/v2/guide/reactivity.html
A working example (minus vuex) here: https://codesandbox.io/s/x4kp93w3o
PS, when writing questions about vue, it's very helpful to boil it down to a simpler example. Get rid of vuex, remove your translation stuff. Sometimes the answer will jump out at you once you have it as simple as possible.
I build following component:
var Modal = Vue.component('modal', {
template: `
<div id="modal" class="modal">
<div class="modal-content">
<p>{{ link }}</p>
</div>
</div>
`,
props: [
'link'
],
});
And I would like to change the link data dynamically after I sent successfully an axios post.
My vue instance
new Vue({
el: '#form',
components: {
'modal': Modal
},
data: {
userId: '',
title: '',
body: '',
snippetLink: '',
},
methods: {
publish (e) {
var self = this;
axios.post('/snippets', {
title: this.title,
body: this.content,
})
.then(function (response) {
console.log("success");
self.link = response.data.hash; // Here I tried to add the reponse content to the vue component's p
})
.catch(function (error) {
console.log(error);
})
},
My Html Markup:
<modal link=""></modal>
...
<button type="button"
v-bind:class="{ 'modal-trigger': !isActiveModal }"
#click="publish">Publish
<i class="material-icons right">send</i>
</button>
So I am sending an axios post to my server successfully and get the data, I would like to open a modal window and put the data in a p tag, so far the modal pops up after my post but I am not sure my it does not change the content of the p tag.
As per my understanding , Snippetlink property can be used to hold data from server.
self.Snippetlink = response.data.hash;
and Pass Snippetlink to link attribute of the snippet-model
<snippet-modal :link="Snippetlink"></snippet-modal>
rupesh_padhye's answer is correct. This is just a further explanation.
First of all, to store the response data to a Vue component, you need to define a key in data for that purpose first. So to make this line work: self.link = response.data.hash;, you need to add link as a key for the Vue component's data:
data: {
userId: '',
title: '',
body: '',
snippetLink: '',
link: ''
},
If you mean snippetLink, change the line to self.snippetLink = response.data.hash;
Secondly, to pass the data as a prop to a child component, you have to specify the prop name, as well as the data key being passed. For instance, to pass your component's link as a prop with the same name to the snippet-modal component, you need: <snippet-modal :link="link"></snippet-modal>