I have just made a flash message component, which receives flash messages from the eventBus and then displays the flash message for 3 seconds before disappearing. The component is as follows:
<template>
<transition name="fade">
<div v-if="visible" v-bind:class="type" role="alert">{{ message }}</div>
</transition>
</template>
<style scoped>
.fade-enter-active, .fade-leave-active {
transition: opacity .5s
}
.fade-enter, .fade-leave-to {
opacity: 0
}
</style>
<script>
import EventBus from '../../config/EventBus';
export default {
name: 'flash-view',
data() {
return {
type: '',
message: '',
visible: false,
};
},
mounted() {
EventBus.subscribeFlashMessage(data => this.setData(data));
setTimeout(() => (
this.visible = false
), 3000);
},
methods: {
setData(data) {
this.setType(data.type);
this.message = data.message;
this.visible = true;
},
setType(type) {
this.type = `alert alert-${type}`;
},
},
};
</script>
The component works perfectly for the first flash message, however if flash messages are triggered subsequently or if I change routes (VueRouter) then the flash message does not disappear. I presume this is because javascript is retriggered, which nulls the effect of setTimeout, however I have no idea how to fix this in Vue.
I have managed to fix the bug, thanks to Belmin Bedak's help. The following is now implemented with created() instead of mounted(), as mounted gets retriggered for every update cycle. Created() sets the listener once, and watch is used to check if the visible has been changed (this happens when a new event is pushed to the listener). If the visible is changed, the setTimeout gets triggered properly.
<template>
<transition name="fade">
<div
v-if="visible"
v-bind:class="type"
role="alert"
v-text="message"
>
</div>
</transition>
</template>
<style scoped>
.fade-enter-active, .fade-leave-active {
transition: opacity .5s
}
.fade-enter, .fade-leave-to {
opacity: 0
}
</style>
<script>
export default {
name: 'flash-view',
data() {
return {
type: '',
message: '',
visible: false,
};
},
created() {
this.$on('flashMessage', data => this.setData(data));
},
methods: {
setData(data) {
this.type = `alert alert-${data.type}`;
this.message = data.message;
this.visible = true;
},
setFadeOut() {
setTimeout(() => (
this.visible = false
), 2500);
},
},
watch: {
visible: 'setFadeOut',
},
};
</script>
Related
From my parent component I'm calling my custom input child component this way:
<custom-input
v-model="$v.form.userName.$model"
:v="$v.form.userName"
type="text"
/>
And here's my custom input component:
<template>
<input
v-bind="$attrs"
:value="value"
v-on="inputListeners"
:class="{ error: v && v.$error }"
>
</template>
<script>
export default {
inheritAttrs: false,
props: {
value: {
type: String,
default: ''
},
v: {
type: Object,
default: null
}
},
computed: {
inputListeners () {
const vm = this
return Object.assign({},
this.$listeners,
{
input (event) {
vm.$emit('blur', event.target.value)
}
}
)
}
}
}
</script>
This triggers validation errors from the very first character entered in the input field (which is arguably poor UX, so I really don't understand why this is default behavior).
Anyway, how to trigger such errors only on blur event?
This is not default behavior - it's your code!
Vuelidate validates (and raise errors) only after field is marked as dirty by calling $touch method. But when you are using $model property ($v.form.userName.$model) for v-model, it calls $touch automatically - docs
So either do not use $model for binding and call $touch by yourself on blur event (or whenever you want)
Alternatively you can try to use .lazy modifier on v-model but that is supported only on native input elements (support for custom components is long time request)
Example below shows how to implement it yourself....
Vue.use(window.vuelidate.default)
Vue.component('custom-input', {
template: `
<input
v-bind="$attrs"
:value="value"
v-on="inputListeners"
:class="status(v)"
></input>
`,
inheritAttrs: false,
props: {
value: {
type: String,
default: ''
},
v: {
type: Object,
default: null
},
lazy: {
type: Boolean,
default: false
}
},
computed: {
inputListeners() {
const listeners = { ...this.$listeners }
const vm = this
const eventName = this.lazy ? 'change' : 'input'
delete listeners.input
listeners[eventName] = function(event) {
vm.$emit('input', event.target.value)
}
return listeners
}
},
methods: {
status(validation) {
return {
error: validation.$error,
dirty: validation.$dirty
}
}
}
})
const { required, minLength } = window.validators
new Vue({
el: "#app",
data: {
userName: ''
},
validations: {
userName: {
required,
minLength: minLength(5)
}
}
})
input {
border: 1px solid silver;
border-radius: 4px;
background: white;
padding: 5px 10px;
}
.dirty {
border-color: #5A5;
background: #EFE;
}
.dirty:focus {
outline-color: #8E8;
}
.error {
border-color: red;
background: #FDD;
}
.error:focus {
outline-color: #F99;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://unpkg.com/vuelidate/dist/vuelidate.min.js"></script>
<script src="https://unpkg.com/vuelidate/dist/validators.min.js"></script>
<div id="app">
<custom-input v-model="$v.userName.$model" :v="$v.userName" type="text" lazy></custom-input>
<pre>Model: {{ userName }}</pre>
<pre>{{ $v }}</pre>
</div>
Try to emit the input event from the handler of blur event so :
instead of :
v-on="inputListeners"
set
#blur="$emit('input', $event.target.value)"
I'm using Nuxt.js with Infinite loading in order to serve more list articles as the users scrolls down the page. I've placed the infinite-loading plugin at the bottom of my list of articles (which lists, from the very beginning, at least 10 articles, so we have to scroll down a lot before reaching the end of the initial list).
The problem is that as soon as I open the page (without scrolling the page), the infiniteScroll method is triggered immediately and more articles are loaded in the list (I'm debugging printing in the console "I've been called").
I don't understand why this happens.
<template>
<main class="mdl-layout__content mdl-color--grey-50">
<subheader :feedwebsites="feedwebsites" />
<div class="mdl-grid demo-content">
<transition-group name="fade" tag="div" appear>
<feedarticle
v-for="feedArticle in feedArticles"
:key="feedArticle.id"
:feedarticle="feedArticle"
#delete-article="updateArticle"
#read-article="updateArticle"
#write-article="updateArticle"
#read-later-article="updateArticle"
></feedarticle>
</transition-group>
</div>
<infinite-loading spinner="circles" #infinite="infiniteScroll">
<div slot="no-more"></div>
<div slot="no-results"></div
></infinite-loading>
</main>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import subheader from '~/components/subheader.vue'
import feedarticle from '~/components/feedarticle.vue'
export default {
components: {
subheader,
feedarticle,
},
props: {
feedwebsites: {
type: Array,
default() {
return []
},
},
},
computed: {
...mapState({
feedArticles: (state) => state.feedreader.feedarticles,
}),
...mapGetters({
getInfiniteEnd: 'feedreader/getInfiniteEnd',
}),
},
methods: {
async updateArticle(id, status) {
try {
const payload = { id, status }
await this.$store.dispatch('feedreader/updateFeedArticle', payload)
} catch (e) {
window.console.log('Problem with uploading post')
}
},
infiniteScroll($state) {
window.console.log('I've been called')
setTimeout(() => {
this.$store.dispatch('feedreader/increasePagination')
try {
this.$store.dispatch('feedreader/fetchFeedArticles')
if (this.getInfiniteEnd === false) $state.loaded()
else $state.complete()
} catch (e) {
window.console.log('Error ' + e)
}
}, 500)
},
},
}
</script>
<style scoped>
.fade-leave-to {
opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease-out;
}
</style>
Put <infinite-loading> in <client-only> tag :
<client-only>
<infinite-loading></infinite-loading>
</client-only>
I am trying to bind a class from a parent component to a child component via a computed switch case to an slot.
Parent:
<template>
<mcTooltip :elementType="'text'"><p>Test</p></mcTooltip>
</template>
<script>
import mcTooltip from '#/components/mcTooltip/index.vue';
export default {
components: {
mcTooltip
}
};
</script>
Child:
<template>
<div>
<slot :class="[elementClass]" />
</div>
</template>
<script>
export default {
props: {
elementType: {
type: String,
required: true,
// must have one of these elements
validator: (value) => {
return ['text', 'icon', 'button'].includes(value);
}
}
},
data() {
return {};
},
computed: {
elementClass: () => {
// return this.elementType ? 'tooltip--text' : 'tooltip--text';
// calls prop value for verification
switch (this.elementType) {
case 'text':
return 'tooltip--text';
case 'icon':
return 'tooltip--icon';
case 'button':
return 'tooltip--button';
default:
return 'tooltip--text';
}
}
},
};
</script>
<style lang="scss" scoped>
.tooltip--text {
text-decoration: underline dotted;
cursor: pointer;
&:hover {
background: $gray_220;
}
}
</style>
Whatever I try I dont seem to make it work in any way. Thats my latest attempt. The vue devtools say to my computed prop "(error during evaluation)".
I found a solution, the way I did it is as following:
<div
v-show="showTooltip"
ref="mcTooltipChild"
:class="['tooltip__' + elementType]"
></div>
elementType: {
type: String,
default: 'small',
},
I want to make a Vue Mixin that applies similar hover events and classes.
Right now I add this to each component, but would prefer to make this into a mixin.
Is this possible, or is there an easier way to accomplish this without having to include #mouseenter and #mouseleave?
<div
#mousenter="hovering=true"
#mouseleave="hovering=false"
:class="[hovering ? 'elevation-4' : 'elevation-2']">`
I'd prefer to import something like:
export default {
data: () => ({ hovering: false }),
mounted(){
// something here to use mouseenter/mouseleave
}
}
You could do it like this:
Vue.config.devtools = false
Vue.config.productionTip = false
const myMixin = {
data: () => ({ hovering: false }),
}
new Vue({
el: "#app",
mixins: [myMixin]
})
section {
height: 200px;
width: 200px;
}
.elevation-4 {
background-color: red
}
.elevation-2 {
background-color: green
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<section
#mouseenter="hovering=true"
#mouseleave="hovering=false"
:class="[hovering ? 'elevation-4' : 'elevation-2']">
</section>
</div>
You can define a mixin like so:
lib/mixins/hover.js
export default {
data() {
return { isHovering: false };
},
computed: {
klass() {
return this.isHovering ? 'o-hoverable--on' : 'o-hoverable--off';
},
},
methods: {
onEnter() {
this.isHovering = true;
},
onLeave() {
this.isHovering = false;
},
},
};
And then use it like this:
index.vue
<template>
<div :class="klass" #mouseenter="onEnter" #mouseleave="onLeave">hover me</div>
</template>
<script>
import hover from '~/lib/mixins/hover';
export default {
mixins: [hover],
};
</script>
Note that you'll still need to manually bind the events and the class, but you'll get to reuse the definitions of both.
trying to make a component (tag: simple-div-container) that on a button press will create 2 (tag: simple-div) components, inside each new simple-div there will be a simple-div-container.
so I can create "endless" components inside each other.
I have simple-div-container and when I press the button I get 2 simple-div
but I don't get inside them the NEW simple-div-container.
I get an error:
Failed to mount component: template or render function not defined.
code for tag: simple-div-container
<template>
<div>
<button #click="insert2Div" class="div-controler">insert 2 div</button>
<div v-if="divs" class="horizontal-align">
<simplediv v-if="divs" :style="{height: simpleDivHeight + 'px',width: simpleDivWidthPrecent/2 + '%', border: 1 + 'px solid' }"
:height="simpleDivHeight" :widthPrecent="simpleDivWidthPrecent" :isRender="true"></simplediv>
<simplediv v-if="divs" :style="{height: simpleDivHeight + 'px',width: simpleDivWidthPrecent/2 + '%', border: 1 + 'px solid' }"
:height="simpleDivHeight" :widthPrecent="simpleDivWidthPrecent" :isRender="true"></simplediv>
</div>
</div>
</template>
<script>
import SimpleDiv from '../simple-div/simpleDiv.vue';
export default {
props: {
simpleDivHeight: {
require: true,
type: Number
},
simpleDivWidthPrecent: {
require: true,
type: Number
}
},
data() {
return {
divs: false,
}
},
methods: {
insert2Div() {
console.log('insert 2 div')
this.divs = true;
},
},
components: {
simplediv: SimpleDiv
},
}
</script>
<style scoped>
.horizontal-align {
display: flex;
flex-direction: row;
}
</style>
code for tag: simple-div
<template>
<div>
<simple-div-container v-if="isRender" :simpleDivHeight="height" :simpleDivWidthPrecent="widthPrecent/2"></simple-div-container>
</div>
</template>
<script>
import simpleDivContainer from '../simple-div-container/simpleDivContainer.vue';
export default {
props: {
height: {
require: true,
type: Number
},
widthPrecent: {
require: true,
type: Number
},
isRender:{
require: true,
type: Boolean
}
},
data() {
return {
isDivContainer: false
}
},
components: {
'simple-div-container': simpleDivContainer
}
}
</script>
<style scoped>
.div-controler {
position: fixed;
transform: translate(-10%, -320%);
}
</style>
an interesting point: if i add to simple-div data some property(while webpack listens to changes) than it will automatically rerender and the new simpe-div-container will show
You have a circular reference problem. You should check if registering the simple-div component in the beforeCreate lifecycle hook helps. In the simple-div-container:
In the simple-div-container:
beforeCreate () {
this.$options.components.simple-div = require('../simple-div/simpleDiv.vue')
}