How to test Vuetify VHover? - javascript

I have created the following component that wraps Vuetify VHover, VTooltip and VBtn to simplify my app.
<template>
<div>
<v-hover v-if="tooltip">
<v-tooltip
slot-scope="{ hover }"
bottom
>
<v-btn
slot="activator"
:theme="theme"
:align="align"
:justify="justify"
:disabled="disabled"
:depressed="type === 'depressed'"
:block="type === 'block'"
:flat="type === 'flat'"
:fab="type === 'fab'"
:icon="type === 'icon'"
:outline="type === 'outline'"
:raised="type === 'raised'"
:round="type === 'round'"
:color="hover ? colorHover : color"
:class="{ 'text-capitalize': label, 'text-lowercase': icon }"
:size="size"
#click="onClick()"
>
<span v-if="label">{{ label }}</span>
<v-icon v-else>{{ icon }}</v-icon>
</v-btn>
<span>{{ tooltip }}</span>
</v-tooltip>
</v-hover>
<v-hover v-else>
<v-btn
slot-scope="{ hover }"
:theme="theme"
:align="align"
:justify="justify"
:disabled="disabled"
:depressed="type === 'depressed'"
:block="type === 'block'"
:flat="type === 'flat'"
:fab="type === 'fab'"
:icon="type === 'icon'"
:outline="type === 'outline'"
:raised="type === 'raised'"
:round="type === 'round'"
:color="hover ? colorHover : color"
:class="{ 'text-capitalize': label, 'text-lowercase': icon }"
:size="size"
#click="onClick()"
>
<span v-if="label">{{ label }}</span>
<v-icon v-else>{{ icon }}</v-icon>
</v-btn>
</v-hover>
</div>
</template>
<script>
import VueTypes from 'vue-types'
export default {
name: 'v-btn-plus',
props: {
align: VueTypes.oneOf(['bottom', 'top']),
justify: VueTypes.oneOf(['left', 'right']),
color: VueTypes.string.def('primary'),
colorHover: VueTypes.string.def('secondary'),
disabled: VueTypes.bool.def(false),
icon: VueTypes.string,
label: VueTypes.string,
position: VueTypes.oneOf(['left', 'right']),
tooltip: VueTypes.string,
size: VueTypes.oneOf(['small', 'medium', 'large']).def('small'),
theme: VueTypes.oneOf(['light', 'dark']),
type: VueTypes.oneOf(['block', 'depressed', 'fab', 'flat', 'icon', 'outline', 'raised', 'round']).def('raised')
},
methods: {
onClick() {
this.$emit('click')
}
},
created: function() {
// Workaround as prop validation on multiple props is not possible
if (!this.icon && !this.label) {
console.error('[Vue warn]: Missing required prop, specify at least one of the following: "label" or "icon"')
}
}
}
</script>
<style scoped>
</style>
I want to test VHover and VTooltip and have defined the following spec file.
import { createLocalVue, mount } from '#vue/test-utils'
import Vuetify from 'vuetify'
import VBtnPlus from '#/components/common/VBtnPlus.vue'
describe('VStatsCard.vue', () => {
let localVue = null
beforeEach(() => {
localVue = createLocalVue()
localVue.use(Vuetify)
})
it('renders with default settings when only label is specified', async () => {
const label = 'Very cool'
const defaultColor = 'primary'
const defaultType = 'raised'
const defaultSize = 'small'
const wrapper = mount(VBtnPlus, {
localVue: localVue,
propsData: { label }
})
expect(wrapper.text()).toMatch(label)
expect(wrapper.html()).toContain(`${defaultType}="true"`)
expect(wrapper.html()).toContain(`size="${defaultSize}"`)
expect(wrapper.html()).toContain(`class="v-btn theme--light ${defaultColor} text-capitalize"`)
expect(wrapper.html()).not.toContain(`v-icon"`)
wrapper.find('button').trigger('mouseenter')
await wrapper.vm.$nextTick()
const btnHtml = wrapper.find('.v-btn').html()
expect(btnHtml).toContain('secondary')
expect(btnHtml).not.toContain('primary')
const tooltipId = btnHtml.match(/(data-v-.+?)(?:=)/)[1]
const tooltips = wrapper.findAll('.v-tooltip_content')
let tooltipHtml = null
for (let tooltip of tooltips) {
const html = tooltip.html()
console.log(html)
if (html.indexOf(tooltipId) > -1) {
tooltipHtml = html
break
}
}
expect(tooltipHtml).toContain('menuable_content_active')
})
})
The wrapper.find('button').trigger('mouseenter') does not work as expected. When I look at the html code after the nextTick it is the same as before trigger was called. It looks like I'm missing a part of the html. I was excpecting to see the following html.
<div data-v-d3e326b8="">
<span data-v-d3e326b8="" class="v-tooltip v-tooltip--bottom">
<span>
<button data-v-d3e326b8="" type="button" class="v-btn v-btn--depressed theme--light orange text-lowercase" size="small">
<div class="v-btn__content"><i data-v-d3e326b8="" aria-hidden="true" class="v-icon mdi mdi-account theme--light"></i></div>
</button>
</span>
</span>
</div>
All I'm getting is the <button> part.
Any suggestions how to get this to work?

Explicitly triggering mouseenter events doesn't normally have an effect, likely because browsers ignore simulated/non-trusted events. Instead, you could set v-hover's value to true to force the hover state:
VBtnPlus.vue
<template>
<div>
<v-hover :value="hovering">...</v-hover>
</div>
</template>
<script>
export default {
data() {
return {
hovering: false,
}
},
//...
}
</script>
VBtnPlus.spec.js
it('...', async () => {
wrapper.vm.hovering = true;
// test for hover state here
})

I just ran into this myself using Nuxt, Vuetify, and Jest. I followed
this example for Vuetify 2.x.
Below is a very simple example of what I did with my code.
As a side note, when I tried to set the wrapper.vm.[dataField] = [value] directly, Jest threw an error not allowing direct set access on the object. In the example.spec.js below, calling wrapper.setData({...}) will allow you to set the data value without any issue.
example.vue:
<template lang="pug">
v-hover(
v-slot:default="{ hover }"
:value="hoverActive"
)
v-card#hoverable-card(
:elevation="hover ? 20 : 10"
)
</template>
<script>
export default {
data() {
return {
hoverActive: false
}
}
</script>
example.spec.js
import { mount, createLocalVue } from '#vue/test-utils'
import Vuetify from 'vuetify'
import example from '#/components/example.vue'
const localVue = createLocalVue()
localVue.use(Vuetify)
describe('example', () => {
const wrapper = mount(example, {
localVue
})
it('should have the correct elevation class on hover', () => {
let classes = wrapper.classes()
expect(classes).toContain('elevation-10')
expect(classes).not.toContain('elevation-20')
wrapper.setData({ hoverActive: true })
classes = wrapper.classes()
expect(classes).not.toContain('elevation-10')
expect(classes).toContain('elevation-20')
})
})

https://github.com/vuejs/vue-test-utils/issues/1421
it('2. User interface provides one help icon with tooltip text', async (done) => {
// stuff
helpIcon.trigger('mouseenter')
await wrapper.vm.$nextTick()
requestAnimationFrame(() => {
// assert
done()
})
})

Related

why is this vanilla Js HTMLEl.style = " " not working?

I work in a big Vue project, I was trying to switch classes => couldn't so tried something easier ( testing purposes only) => change El style with.style.
So, the idea here is to switch the form backgroundColor when focus state is active on certain input... I was able to switch that same Input bgColor with this eventListener but never the form. Here's the code:
<template>
<form #submit.prevent ref="formElement" class="w-full form">
<h1
class="mt-5 mb-3 text-2xl text-primary font-bold capitalize md:mt-0 md:mb-5 md:text-3xl"
>
{{ t('fullNameForm.title') }}
</h1>
<p class="mb-5 text-sm text-secondary md:mb-10">
{{ t('fullNameForm.description') }}
</p>
<div class="flex flex-col gap-y-4">
<Input
id="firstName"
autocomplete="given-name"
:errors="v$.firstName.$errors"
v-model.trim="v$.firstName.$model"
>
{{ t('firstName') }}
</Input>
<Input
id="lastName"
autocomplete="family-name"
:errors="v$.lastName.$errors"
v-model.trim="v$.lastName.$model"
>
{{ t('lastName') }}
</Input>
</div>
<div
class="w-full h-24 px-4 py-6 absolute left-0 bottom-0 sm:static sm:px-0"
>
<Button
type="submit"
:loading="loading"
:disabled="v$.$invalid"
class="w-full h-12 capitalize"
#click="submit()"
>
{{ t('next') }}
</Button>
</div>
</form>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, onMounted } from 'vue'
import { useStore } from 'vuex'
import { useI18n } from 'vue-i18n'
import useVuelidate from '#vuelidate/core'
import { required, minLength, maxLength } from '#vuelidate/validators'
import Input from '#/components/Input.vue'
import Button from '#/components/ButtonV2.vue'
export default defineComponent({
name: 'FullNameForm',
components: {
Input,
Button
},
emits: ['submit'],
setup(props, { emit }) {
const store = useStore()
const { t } = useI18n()
// focus events don't bubble, must use capture phase
// document.body.addEventListener(
// 'focus',
// e => {
// const target = e.target
// {
// document.body.classList.add('keyboard')
// }
// },
// true
// )
// document.body.addEventListener(
// 'blur',
// () => {
// document.body.classList.remove('keyboard')
// },
// true
// )
const formElement = ref<HTMLElement | null>(null)
const formEl = document.querySelector('.form') as HTMLElement | null
onMounted(() => {
window.addEventListener('keydown', event => {
if (event.key === 'Enter') submit()
})
const lastnameInput = document.querySelector(
'#lastName'
) as HTMLElement | null
if (lastnameInput || formEl) {
lastnameInput?.addEventListener('focus', () => {
lastnameInput.style.backgroundColor = 'red'
if (formElement.value && formEl && lastnameInput) {
formElement.value.style.backgroundColor = 'orange'
formElement.value.classList.add('keyboard')
formEl.style.backgroundColor = 'gray'
}
})
}
})
return {
t,
form,
v$,
loading,
submit,
formElement
}
}
})
</script>
<style scoped>
.form.keyboard {
background: rgb(81, 179, 146);
}
</style>
I discovered 2 things:
the second if never works
Any formEl or formElement in the first if doesn't work because it's a possible null Element, I already tried to switch operators => || to && => Nothing works, even the input stop reacting.`

vue3 composition api adds or removes classes for custom input boxes

<script lang="ts" setup>
import { ref, computed } from 'vue'
const { modelValue = '' } = defineProps<{
modelValue: string
}>()
const emit = defineEmits(['update:modelValue'])
const isFilled = ref(false)
const value = computed({
get() {
return modelValue
},
set(value: string) {
isFilled.value = value.length > 0
emit('update:modelValue', value)
}
})
</script>
<template>
<input v-model="value" :class="{ 'filled': isFilled }" v-bind="$attrs" />
</template>
sfc playground
This will not take effect when the key is pressed for the first time.
For example: press abcdefg, the last input box displays bcdefg, delete it with the backspace key and try again, it is still the same.
I commented out isFilled.value = value.length > 0 and it works fine, but otherwise, how can I add a class to the element?
The effect I need is to add a class named filled to it when the value of the input box is not empty.
I would have used a watcher for this task, example code:
<script lang="ts" setup>
import { ref, watch } from 'vue'
const { modelValue = '' } = defineProps<{
modelValue: string
}>()
const emit = defineEmits(['update:modelValue'])
const isFilled = ref(false)
const inputText = ref("")
watch(inputText,(newValue, oldValue)=>{
isFilled.value = inputText.value.length > 0
emit('update:modelValue', inputText.value)
})
</script>
<template>
<input v-model="inputText" :class="{ 'filled': isFilled }" v-bind="$attrs" />
</template>
If you just want to add the class you can do that inline aswell.
example:
<script lang="ts" setup>
import { ref, watch } from 'vue'
const inputText = ref("")
</script>
<template>
<input v-model="inputText" :class="{ 'filled': (inputText.length>0) }" v-bind="$attrs" />
</template>

How to create infinite scroll in Vuetify Autocomplete component?

I have a page with Vuetify Autocomplete component, and REST API backend with '/vendors' method. This method takes limit, page and name parameters and returns JSON with id and name fields.
I made some code with lazy list load on user input event. But now I want to add the ability to load this list on user scroll event.
For example, by default there is a list of 100 vendors. User scrolled this list until the end, then "some event" is called and loads next 100 of vendors. Then user keeps scrolling and the action is repeated.
Is it possible to made this with Vuetify Autocomplete component, or should i use another library?
Example code of current component is shown below:
<template>
<v-autocomplete
:items="vendors"
v-model="selectedVendorId"
item-text="name"
item-value="id"
label="Select a vendor"
#input.native="getVendorsFromApi"
></v-autocomplete>
</template>
<script>
export default {
data () {
return {
page: 0,
limit: 100,
selectedVendorId: null,
vendors: [],
loading: true
}
},
created: function (){
this.getVendorsFromApi();
},
methods: {
getVendorsFromApi (event) {
return new Promise(() => {
this.$axios.get(this.$backendLink
+ '/vendors?limit=' + this.limit
+ '&page=' + this.page
+ '&name=' + (event ? event.target.value : ''))
.then(response => {
this.vendors = response.data;
})
})
}
}
}
</script>
I was able to get auto-loading going with the Vuetify AutoComplete component. It's a bit of a hack, but basically the solution is to use the v-slot append item, the v-intersect directive to detect if that appended item is visible, and if it is, call your API to fetch more items and append it to your current list.
<v-autocomplete
:items="vendors"
v-model="selectedVendorId"
item-text="name"
item-value="id"
label="Select a vendor"
#input.native="getVendorsFromApi"
>
<template v-slot:append-item>
<div v-intersect="endIntersect" />
</template>
</v-autocomplete>
...
export default {
methods: {
endIntersect(entries, observer, isIntersecting) {
if (isIntersecting) {
let moreVendors = loadMoreFromApi()
this.vendors = [ ...this.vendors, ...moreVendors]
}
}
}
}
In my use case, I was using API Platform as a backend, using GraphQL pagination using a cursor.
<component
v-bind:is="canAdd ? 'v-combobox' : 'v-autocomplete'"
v-model="user"
:items="computedUsers"
:search-input.sync="search"
item-text="item.node.userProfile.username"
hide-details
rounded
solo
:filter="
(item, queryText, itemText) => {
return item.node.userProfile.username.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1
} "
:loading="loading"
item-value="username"
class="text-left pl-1"
color="blue-grey lighten-2"
:label="label"
>
<template v-slot:selection="{ item }">
<v-chip v-if="typeof item == 'object'">
<v-avatar left>
<v-img v-if="item.node.userProfile.image" :src="item.node.userProfile.image" />
<v-icon v-else>mdi-account-circle</v-icon>
</v-avatar>
{{ item.node.userProfile.firstName }} {{ item.node.userProfile.lastName }}
</v-chip>
<v-chip v-else-if="typeof item == 'string'">
{{ item }}
</v-chip>
</template>
<template v-slot:item="{ item: { node } }">
<v-list-item-avatar >
<img v-if="node.userProfile.avatar" :src="node.userProfile.avatar" />
<v-icon v-else>mdi-account-circle</v-icon>
</v-list-item-avatar>
<v-list-item-content class="text-left">
<v-list-item-title>
{{ $t('fullName', { firstName: node.userProfile.firstName, lastName: node.userProfile.lastName } )}}
</v-list-item-title>
<v-list-item-subtitle v-html="node.userProfile.username"></v-list-item-subtitle>
</v-list-item-content>
</template>
<template v-slot:append-item="">
<div v-intersect="endIntersect" >
</div>
</template>
</component>
import { VCombobox, VAutocomplete } from "vuetify/lib";
import debounce from "#/helpers/debounce"
import { SEARCH_USER_BY_USERNAME } from "#/graphql/UserQueries";
const RESULTS_TO_SHOW = 5
export default {
props: {
canAdd: {
type: Boolean,
default: false,
},
value: [Object, String],
label: String,
},
components: { VCombobox, VAutocomplete },
apollo: {
users: {
query: SEARCH_USER_BY_USERNAME,
variables() {
return {
username: this.search,
numberToShow: RESULTS_TO_SHOW,
cursor: null,
}
},
watchLoading(isLoading) {
this.loading = isLoading
},
skip() {
if (this.search) {
return !(this.search.length > 1)
}
return true
},
},
},
data() {
return {
user: this.value,
search: "",
cursor: null,
loading: false,
};
},
watch: {
user(newValue) {
let emit = newValue
if (newValue) {
emit = newValue.node
}
this.$emit("input", emit);
},
value(newValue) {
if (this.user && this.user.node != newValue) {
if (newValue == null) {
this.user = null
}
else {
this.user = { node: newValue };
}
}
},
search(newValue) {
this.debouncedSearch(newValue)
},
},
methods: {
endIntersect(entries, observer, isIntersecting) {
if (isIntersecting && this.users && this.users.pageInfo.hasNextPage) {
let cursor = this.users.pageInfo.endCursor
this.$apollo.queries.users.fetchMore({
variables: { cursor: cursor},
updateQuery: (previousResult, { fetchMoreResult }) => {
let edges = [
...previousResult.users.edges,
...fetchMoreResult.users.edges,
]
let pageInfo = fetchMoreResult.users.pageInfo;
return {
users: {
edges: edges,
pageInfo: pageInfo,
__typename: previousResult.users.__typename,
}
}
}
})
}
},
debouncedSearch: debounce(function (search) {
if (this.users) {
this.$apollo.queries.users.refetch({
username: search,
numberToShow: RESULTS_TO_SHOW,
cursor: null,
});
}
}, 500),
filter(item, queryText) {
return item.node.userProfile.username.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1
}
},
computed: {
computedUsers() {
if (this.users){
return this.users.edges
}
return []
},
skip() {
if (this.search) {
return this.search.length > 1
}
return false
}
}
};
</script>
Update June 12, 2021:
If you are using Vuetify 2.X, use Brettins' solution based on append-item slot and v-intersect directive.
Old answer:
Looks like it's not possible with default v-autocomplete component (at least in vuetify 1.5.16 or lower).
The component that provides the most similar functionality is VueInfiniteAutocomplete.
But keep in mind that in this case there may be problems with styles, validation, etc.
There is an example with this library.
<template>
<div>
<vue-infinite-autocomplete
:data-source="getAsyncOptions"
:fetch-size="limit"
v-on:select="handleOnSelect"
:value="autocompleteViewValue"
>
</vue-infinite-autocomplete>
</div>
</template>
<script>
export default {
data () {
return {
selectedVendorId : null,
limit: 100,
autocompleteViewValue: null
}
},
methods: {
getAsyncOptions(text, page, fetchSize) {
return new Promise((resolve, reject) => {
resolve(
this.$axios.get(this.$backendLink
+ '/vendors?limit=' + fetchSize
+ '&page=' + page
+ '&name=' + text)
.then(response => {
//Response MUST contain 'id' and 'text' fields, and nothing else.
//If there are other fields, you should remove it here
//and create 'id' and 'text' fields in response JSON by yourself
return response.data;
})
)
});
},
handleOnSelect(selectedItem) {
this.autocompleteViewValue = selectedItem.text;
this.selectedVendorId = selectedItem.id;
}
}
}
</script>
P.S.: If you just want to use v-autocomplete component with server-side pagination, you could create a "Load more..." button using append-item slot, as suggested in this issue.

How can I reuse Vue and Vuetify code?

<script>
export default {
data () {
return {
my_alert: false,
text: '',
color: 'success'
}
},
methods: {
openAlt (text, color) {
this.text = text
this.color = color
this.my_alert = true
}
}
}
</script>
<template>
<div>
<v-btn #click="openAlt('This is alert', 'error')">Click me!</v-btn>
<v-snackbar v-model="my_alert"
:timeout="2000"
:color="color"
top right>
{{ text }}
<v-btn icon ripple #click="my_alert = false">
<v-icon>close</v-icon>
</v-btn>
</v-snackbar>
</div>
</template>
I'm studying Vue.js and Vuetify. I wrote a code that shows alert after click on v-btn.
How can I reuse this alert outside of this page?
I want to modularise this code and use it for alerts for all of my pages.
Thanks for your answer.
I think creating a mixin works as well:
Say that you create alertMixin.js as below:
const alertMixin = {
data () {
return {
myAlert: false,
text: '',
color: 'success',
}
},
methods: {
openAlt (text, color) {
this.text = text;
this.color = color;
this.myAlert = true;
}
}
};
export default alertMixin;
And use it where ever you want like this:
<script>
import alertMixin from '#/path/to/mixin/alertMixin';
export default {
name: 'where-ever-you-want',
mixins: [alertMixin],
};
</script>
Hi welcome to vuetiful world of vue.
You can easily do that making the alert as an component and importing it where ever you want.
For any file where you want to use your alert code, you could just import your alert component and use it just like any other HTML component.
import alert from './path/to/component
<template>
<appAlert></appAlert>
</template>
<script>
components:{
appAlert: alert
}
</script>
There more you can do with components, Read about Vue components
I hope it helps.
Here's my new code.
App.vue
<template>
<v-app>
<v-content>
<router-view/>
</v-content>
<alt></alt>
</v-app>
</template>
<script>
export default {
name: 'App'
}
</script>
main.js
// ...
import alt from './components/alt'
Vue.prototype.$EventBus = new Vue()
Vue.config.productionTip = false
Vue.component('alt', alt)
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
alt.vue
<script>
export default {
data () {
return {
my_alert: false,
text: '',
color: 'success'
}
},
created () {
this.$EventBus.$on('show_alt', (str, color) => {
var text = str
var clr = color
if (!text) text = '설정되지 않은 문구'
if (!clr) clr = 'error'
this.text = text
this.color = clr
this.my_alert = true
})
}
}
</script>
<template>
<div>
<v-snackbar v-model="my_alert"
:timeout="2000"
:color="color"
top right>
{{ text }}
<v-btn icon ripple #click="my_alert = false">
<v-icon>close</v-icon>
</v-btn>
</v-snackbar>
</div>
</template>
At last, altTest.vue
<template>
<v-btn #click="openAlt('This is alert', 'error')">Click Me!</v-btn>
</template>
<script>
export default {
data () {
return {
}
},
methods: {
openAlt (str, color) {
return this.$EventBus.$emit('show_alt', str, color)
}
}
}
</script>
I imported alt.vue to main.js as global, and it's added App.vue.
So, I can open alert in altTest.vue without import(but, it need a method openAlt()).
But, I think this is not simple..

Mouseover or hover vue.js

I would like to show a div when hovering over an element in vue.js. But I can't seem to get it working.
It looks like there is no event for hover or mouseover in vue.js. Is this really true?
Would it be possible to combine jquery hover and vue methods?
i feel above logics for hover is incorrect. it just inverse when mouse hovers. i have used below code. it seems to work perfectly alright.
<div #mouseover="upHere = true" #mouseleave="upHere = false" >
<h2> Something Something </h2>
<some-component v-show="upHere"></some-component>
</div>
on vue instance
data: {
upHere: false
}
Here is a working example of what I think you are asking for.
http://jsfiddle.net/1cekfnqw/3017/
<div id="demo">
<div v-show="active">Show</div>
<div #mouseover="mouseOver">Hover over me!</div>
</div>
var demo = new Vue({
el: '#demo',
data: {
active: false
},
methods: {
mouseOver: function(){
this.active = !this.active;
}
}
});
There's no need for a method here.
HTML
<div v-if="active">
<h2>Hello World!</h2>
</div>
<div v-on:mouseover="active = !active">
<h1>Hover me!</h1>
</div>
JS
new Vue({
el: 'body',
data: {
active: false
}
})
To show child or sibling elements it's possible with CSS only. If you use :hover before combinators (+, ~, >, space). Then the style applies not to hovered element.
HTML
<body>
<div class="trigger">
Hover here.
</div>
<div class="hidden">
This message shows up.
</div>
</body>
CSS
.hidden { display: none; }
.trigger:hover + .hidden { display: inline; }
With mouseover and mouseleave events you can define a toggle function that implements this logic and react on the value in the rendering.
Check this example:
var vm = new Vue({
el: '#app',
data: {btn: 'primary'}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<div id='app'>
<button
#mouseover="btn='warning'"
#mouseleave="btn='primary'"
:class='"btn btn-block btn-"+btn'>
{{ btn }}
</button>
</div>
I think what you want to achieve is with the combination of
#mouseover, #mouseout, #mouseenter and #mouseleave
So the two best combinations are
"#mouseover and #mouseout"
or
"#mouseenter and #mouseleave"
And I think, It's better to use the 2nd pair so that you can achieve the hover effect and call functionalities on that.
<div #mouseenter="activeHover = true" #mouseleave="activeHover = false" >
<p v-if="activeHover"> This will be showed on hover </p>
<p v-if ="!activeHover"> This will be showed in simple cases </p>
</div>
on vue instance
data : {
activeHover : false
}
Note: 1st pair will effect/travel on the child elements as well but 2nd pair will only effect where you want to use it not the child elements. Else you will experience some glitch/fluctuation by using 1st pair. So, better to use 2nd pair to avoid any fluctuations.
I hope, it will help others as well :)
Though I would give an update using the new composition api.
Component
<template>
<div #mouseenter="hovering = true" #mouseleave="hovering = false">
{{ hovering }}
</div>
</template>
<script>
import { ref } from '#vue/composition-api'
export default {
setup() {
const hovering = ref(false)
return { hovering }
}
})
</script>
Reusable Composition Function
Creating a useHover function will allow you to reuse in any components.
export function useHover(target: Ref<HTMLElement | null>) {
const hovering = ref(false)
const enterHandler = () => (hovering.value = true)
const leaveHandler = () => (hovering.value = false)
onMounted(() => {
if (!target.value) return
target.value.addEventListener('mouseenter', enterHandler)
target.value.addEventListener('mouseleave', leaveHandler)
})
onUnmounted(() => {
if (!target.value) return
target.value.removeEventListener('mouseenter', enterHandler)
target.value.removeEventListener('mouseleave', leaveHandler)
})
return hovering
}
Here's a quick example calling the function inside a Vue component.
<template>
<div ref="hoverRef">
{{ hovering }}
</div>
</template>
<script lang="ts">
import { ref } from '#vue/composition-api'
import { useHover } from './useHover'
export default {
setup() {
const hoverRef = ref(null)
const hovering = useHover(hoverRef)
return { hovering, hoverRef }
}
})
</script>
You can also use a library such as #vuehooks/core which comes with many useful functions including useHover.
Reference: Vuejs composition API
It's possible to toggle a class on hover strictly within a component's template, however, it's not a practical solution for obvious reasons. For prototyping on the other hand, I find it useful to not have to define data properties or event handlers within the script.
Here's an example of how you can experiment with icon colors using Vuetify.
new Vue({
el: '#app'
})
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/vuetify/dist/vuetify.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify/dist/vuetify.js"></script>
<div id="app">
<v-app>
<v-toolbar color="black" dark>
<v-toolbar-items>
<v-btn icon>
<v-icon #mouseenter="e => e.target.classList.toggle('pink--text')" #mouseleave="e => e.target.classList.toggle('pink--text')">delete</v-icon>
</v-btn>
<v-btn icon>
<v-icon #mouseenter="e => e.target.classList.toggle('blue--text')" #mouseleave="e => e.target.classList.toggle('blue--text')">launch</v-icon>
</v-btn>
<v-btn icon>
<v-icon #mouseenter="e => e.target.classList.toggle('green--text')" #mouseleave="e => e.target.classList.toggle('green--text')">check</v-icon>
</v-btn>
</v-toolbar-items>
</v-toolbar>
</v-app>
</div>
With mouseover only the element stays visible when mouse leaves the hovered element, so I added this:
#mouseover="active = !active" #mouseout="active = !active"
<script>
export default {
data(){
return {
active: false
}
}
</script>
I came up with the same problem, and I work it out !
<img :src='book.images.small' v-on:mouseenter="hoverImg">
There is a correct working JSFiddle: http://jsfiddle.net/1cekfnqw/176/
<p v-on:mouseover="mouseOver" v-bind:class="{on: active, 'off': !active}">Hover over me!</p>
Please take a look at the vue-mouseover package if you are not satisfied with how does this code look:
<div
#mouseover="isMouseover = true"
#mouseleave="isMouseover = false"
/>
vue-mouseover provides a v-mouseover directive that automaticaly updates the specified data context property when the cursor enters or leaves an HTML element the directive is attached to.
By default in the next example isMouseover property will be true when the cursor is over an HTML element and false otherwise:
<div v-mouseover="isMouseover" />
Also by default isMouseover will be initially assigned when v-mouseover is attached to the div element, so it will not remain unassigned before the first mouseenter/mouseleave event.
You can specify custom values via v-mouseover-value directive:
<div
v-mouseover="isMouseover"
v-mouseover-value="customMouseenterValue"/>
or
<div
v-mouseover="isMouseover"
v-mouseover-value="{
mouseenter: customMouseenterValue,
mouseleave: customMouseleaveValue
}"
/>
Custom default values can be passed to the package via options object during setup.
Here is a very simple example for MouseOver and MouseOut:
<div id="app">
<div :style = "styleobj" #mouseover = "changebgcolor" #mouseout = "originalcolor">
</div>
</div>
new Vue({
el:"#app",
data:{
styleobj : {
width:"100px",
height:"100px",
backgroundColor:"red"
}
},
methods:{
changebgcolor : function() {
this.styleobj.backgroundColor = "green";
},
originalcolor : function() {
this.styleobj.backgroundColor = "red";
}
}
});
This worked for me for nuxt
<template>
<span
v-if="item"
class="primary-navigation-list-dropdown"
#mouseover="isTouchscreenDevice ? null : openDropdownMenu()"
#mouseleave="isTouchscreenDevice ? null : closeDropdownMenu()"
>
<nuxt-link
to="#"
#click.prevent.native="openDropdownMenu"
v-click-outside="closeDropdownMenu"
:title="item.title"
:class="[
item.cssClasses,
{ show: isDropdownMenuVisible }
]"
:id="`navbarDropdownMenuLink-${item.id}`"
:aria-expanded="[isDropdownMenuVisible ? true : false]"
class="
primary-navigation-list-dropdown__toggle
nav-link
dropdown-toggle"
aria-current="page"
role="button"
data-toggle="dropdown"
>
{{ item.label }}
</nuxt-link>
<ul
:class="{ show: isDropdownMenuVisible }"
:aria-labelledby="`navbarDropdownMenuLink-${item.id}`"
class="
primary-navigation-list-dropdown__menu
dropdown-menu-list
dropdown-menu"
>
<li
v-for="item in item.children" :key="item.id"
class="dropdown-menu-list__item"
>
<NavLink
:attributes="item"
class="dropdown-menu-list__link dropdown-item"
/>
</li>
</ul>
</span>
</template>
<script>
import NavLink from '#/components/Navigation/NavLink';
export default {
name: "DropdownMenu",
props: {
item: {
type: Object,
required: true,
},
},
data() {
return {
isDropdownMenuVisible: false,
isTouchscreenDevice: false
};
},
mounted() {
this.detectTouchscreenDevice();
},
methods: {
openDropdownMenu() {
if (this.isTouchscreenDevice) {
this.isDropdownMenuVisible = !this.isDropdownMenuVisible;
} else {
this.isDropdownMenuVisible = true;
}
},
closeDropdownMenu() {
if (this.isTouchscreenDevice) {
this.isDropdownMenuVisible = false;
} else {
this.isDropdownMenuVisible = false;
}
},
detectTouchscreenDevice() {
if (window.PointerEvent && ('maxTouchPoints' in navigator)) {
if (navigator.maxTouchPoints > 0) {
this.isTouchscreenDevice = true;
}
} else {
if (window.matchMedia && window.matchMedia("(any-pointer:coarse)").matches) {
this.isTouchscreenDevice = true;
} else if (window.TouchEvent || ('ontouchstart' in window)) {
this.isTouchscreenDevice = true;
}
}
return this.isTouchscreenDevice;
}
},
components: {
NavLink
}
};
</script>
<style scoped lang="scss">
.primary-navigation-list-dropdown {
&__toggle {
color: $white;
&:hover {
color: $blue;
}
}
&__menu {
margin-top: 0;
}
&__dropdown {
}
}
.dropdown-menu-list {
&__item {
}
&__link {
&.active,
&.nuxt-link-exact-active {
border-bottom: 1px solid $blue;
}
}
}
</style>
You can also use VueUse composables.
This is for mouse hover
<script setup>
import { useElementHover } from '#vueuse/core'
const myHoverableElement = ref()
const isHovered = useElementHover(myHoverableElement)
</script>
<template>
<button ref="myHoverableElement">
{{ isHovered }}
</button>
</template>
This one for mouse hover
import { useMouse } from '#vueuse/core'
const { x, y, sourceType } = useMouse()
or even in a specific element.
TLDR: quite a few handy composables for your Vue2/Vue3 apps!

Categories

Resources