Properly using click:outside with vuetify dialog - javascript

I have a v-dialog that I use to pop up a date picker when needed. To show it, I bind a value with v-modle. I use the click:outside events to trigger to function that is supposed to close it once it's clicked outside so I can trigger it again (if I click outside, the dialog is dismissed, but the value stays 'true', so I can't show it more than once). There must be something I'm doing wrong.
Here's my dialog :
<template>
<v-dialog
v-model="value"
#click:outside="closeDialog"
>
<v-date-picker
v-model="date"
/>
<v-divider/>
<div>fermer</div>
</v-dialog>
</template>
<script>
export default {
name: 'DatePickerDialog',
props: ['value'],
data() {
return {
colors,
date: null,
}
},
methods: {
closeDialog() {
this.value = false;
}
}
}
</script>
And what calls it is as simple as this :
<template>
<div>
<v-btn #click="inflateDatePicker">inflate date picker</v-btn>
<date-picker-dialog v-model="showDatePicker"/>
</div>
</template>
<script>
import DatePickerDialog from '../../../../views/components/DatePickerDialog';
export default{
name: "SimpleTest",
components: {
DatePickerDialog
},
data() {
return {
showDatePicker: false,
};
},
methods:{
inflateDatePicker() {
this.showDatePicker = true;
},
},
}
</script>
So I can inflate it with no problem. Then though I'm not able to go inside closeDialog() (verified by debugging and trying to log stuff). So what is happening that makes it so I'm not able to enter the function? Documentation doesn't specify if you need to use it differently from regular #click events

The problem was in my closeDialog function. this.value = false did not notify the parent component of the value change. So I had to change it to this in order for it to work properly :
closeDialog() {
this.$emit('input', false);
},
With this is works perfectly fine.

Related

How does MaterialUI do this?

If you look at their autocomplete component: https://mui.com/material-ui/react-autocomplete/
After you click a suggestion in the dropdown, the input box keeps focus... How do they do that? In every variation of this in my own vue app (not using material UI) I can't get the click event to stop an input from losing focus.
I have tried googling this for quite some time and there is no clear solution that I see. For example, people suggest mousedown/touchstart but that would break scrolling (via dragging the dropdown). MaterialUI obviously doesn't have this problem, and doesn't seem to be using mousedown.
I've tried analyzing the events using Chrome dev tools and I can only see a single click event, but with minified code it's difficult to tell what's going on.
Vuetify also does this: https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/components/VAutocomplete/VAutocomplete.ts
I did find this too which is helpful, if anyone comes across this issue https://codepen.io/Pineapple/pen/MWBVqGW
Edit Here's what Im doing:
<app-input-autocomplete
#autocomplete-select="onSelect"
#autocomplete-close="onClose"
:open="open">
<template #default="{ result }">
<div class="input-autocomplete-address">
{{ result.address }}
</div>
</template>
</app-input-autocomplete>
and then in app-input-autocomplete:
<template>
<app-input
#focus="onFocus"
#blur="onBlur"
v-bind="$attrs">
<template #underInput>
<div ref="dropdown" v-show="open" class="input-autocomplete-dropdown">
<div class="input-autocomplete-results">
<div v-for="result in results" :key="result.id" #click="onClick(result)" class="input-autocomplete-result">
<slot :result="result" />
</div>
</div>
</div>
</template>
</app-input>
</template>
<script>
import { ref, toRef } from 'vue';
import AppInput from '#/components/AppInput.vue';
import { onClickOutside } from '#vueuse/core';
export default {
components: {
AppInput,
},
inheritAttrs: false,
props: {
open: {
type: Boolean,
default: false,
},
results: {
type: Array,
default: () => ([]),
},
},
emits: ['autocomplete-close', 'autocomplete-select'],
setup(props, { emit }) {
const dropdown = ref(null);
const open = toRef(props, 'open');
const focused = ref(false);
onClickOutside(dropdown, () => {
if (!focused.value && open.value) {
emit('autocomplete-close');
}
});
return {
dropdown,
focused,
};
},
methods: {
onFocus() {
this.focused = true;
},
onBlur() {
this.focused = false;
},
onClick(result) {
this.$emit('autocomplete-select', result);
},
},
};
</script>
I solved this by doing the following, thanks to #Claies for the idea to look, and also this link:
https://codepen.io/Pineapple/pen/MWBVqGW
event.preventDefault on mousedown
#click on result behaves like normal (close input)
#click/#focus on input set open = true
#blur sets open = false

Trying to avoid mutating a prop that goes 3 levels deep by using $emit

EDIT: Here's a repo I made for easier parsing.
I have a Component that lists products in a datatable. The first column of the table is a link that shows a modal with a form of the product that was clicked (using its ID). I'm using the PrimeVue library for styling and components.
<template>
<Column field="id" headerStyle="width: 5%">
<template #body="slotProps">
<ProductForm :product="slotProps.data" :show="showModal(slotProps.data.id)" />
<a href="#" #click.stop="toggleModal(slotProps.data.id)">
<span class="pi pi-external-link"> </span>
</a>
</template>
</Column>
</template>
<script>
import ProductForm from "./forms/ProductForm";
export default {
data() {
return {
activeModal: 0,
}
},
components: { ProductForm },
methods: {
toggleModal: function (id) {
if (this.activeModal !== 0) {
this.activeModal = 0;
return false;
}
this.activeModal = id;
},
showModal: function (id) {
return this.activeModal === id;
},
},
</script>
The modal is actually a sub component of the ProductForm component (I made a template of the Modal so I could reuse it). So it's 3 components all together (ProductList -> ProductForm -> BaseModal). Here's the product form:
<template>
<div>
<BaseModal :show="show" :header="product.name">
<span class="p-float-label">
<InputText id="name" type="text" :value="product.name" />
<label for="name">Product</label>
</span>
</BaseModal>
</div>
</template>
<script>
import BaseModal from "../_modals/BaseModal";
export default {
props: ["product", "show"],
components: { BaseModal },
data() {
return {};
},
};
</script>
When the modal pops up it uses the ProductForm subcomponent. Here is the BaseModal component:
<template>
<div>
<Dialog :header="header" :visible.sync="show" :modal="true" :closable="true" #hide="doit">
<slot />
</Dialog>
</div>
</template>
<script>
export default {
props: {
show: Boolean,
header: String,
},
methods: {
doit: function () {
let currentShow = this.show;
this.$emit("showModel", currentShow)
},
},
data() {
return {
};
},
};
</script>
I'm passing the product object, and a show boolean that designates if the modal is visible or not from the first component (ProductList) all the way down through the ProductForm component and finally to the BaseModal component. The modal is a PrimeVue component called Dialog. The component actually has it's own property called "closable" which closes the modal with an X button when clicked, that is tied to an event called hide. Everything actually works. I can open the modal and close it. For some reason I have to click the another modal link twice before it opens after the initial.
The issue is when I close a modal, I get the Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "show" error. I've tried everything to emit to the event and change the original props value there, but the error persists (even from the code above) but I'm not sure if because I'm 3 components deep it won't work. I'm pretty new to using props and slots and $emit so I know I'm doing something wrong. I'm also new to laying out components this deep so I might not even be doing the entire layout correctly. What am I missing?
Well you are emitting the showModel event from BaseModal but you are not listening for it on the parent and forwarding it+listening on grandparent (ProductForm)
But the main problem is :visible.sync="show" in BaseModal. It is same as if you do :visible="show" #update:visible="show = $event" (docs). So when the Dialog is closed, PrimeVue emits update:visible event which is picked by BaseModal component (thanks to the .sync modifier) and causes the mutation of the show prop inside BaseModal and the error message...
Remember to never use prop value directly with v-model or .sync
To fix it, use the prop indirectly via a computed with the setter:
BaseModal
<template>
<div>
<Dialog :header="header" :visible.sync="computedVisible" :modal="true" :closable="true">
<slot />
</Dialog>
</div>
</template>
<script>
export default {
props: {
show: Boolean,
header: String,
},
computed: {
computedVisible: {
get() { return this.show },
set(value) { this.$emit('update:show', value) }
}
},
};
</script>
Now you can add same computed into your ProductForm component and change the template to <BaseModal :show.sync="computedVisible" :header="product.name"> (so when the ProductForm receives the update:show event, it will emit same event to it's parent - this is required as Vue event do not "bubble up" as for example DOM events, only immediate parent component receives the event)
Final step is to handle update:show in the ProductList:
<ProductForm :product="slotProps.data" :show="showModal(slotProps.data.id)" #update:show="toggleModal(slotProps.data.id)"/>

Vuetify trigger a tooltip

I'm using vuetify, and I have a tooltip over a button.
I doesn't want to show the tooltip on hover nor on click, I want to show the tooltip if some event is triggered.
translate.vue
<v-tooltip v-model="item.showTooltip" top>
<template v-slot:activator="{}">
<v-btn #click="translateItem(item)"> Call API to translate</v-btn>
</template>
<span>API quota limit has been reached</span>
</v-tooltip>
<script>
export default(){
props: {
item: { default: Objet}
}
methods: {
translateItem: function (item) {
axios
.post(baseURL + "/translateAPI", {
text: item.originTrad;
})
.then((res) => {
if (apiQuotaLimitReached(res) {
// If limit is reached I want to show the tooltip for some time
item.showTooltip = true;
setTimeout(() => {item.showTooltip = false;}, 3000);
} else { ..... }}}
</script>
itemSelect.vue (where I create object item and then use router push to transmit it to the translation page)
<script>
export default(){
methods: {
createItem: function () {
item.originTrad = "the text to translate"
....
item.showTooltip = false;
this.$router.push({
name: "translate",
params: {
"item": item,
},
}); }}
</script>
As you can see I removed the v-slot:activator="{ on }" and v-on="on" that I found on the exemple:https://vuetifyjs.com/fr-FR/components/tooltips/ , because I don't want to show the tooltip on hover. But It doesn't work as expected, the tooltip is not showing properly.
Some help would be great :)
For starters, you are trying to change a prop in the child component, something that you should not do:
[Vue warn]: Avoid mutating a prop directly since the value will be
overwritten whenever the parent component re-renders. Instead, use a
data or computed property based on the prop's value. Prop being
mutated: "item.showTooltip"
So start by making a separate data variable for showTooltip (doesn't need to be a property of item perse) and try setting it to true to see what happens (and of course change v-model="item.showTooltip" to v-model="showTooltip" on v-tooltip)

Can't open Vuetify dialog programmatically in setTimeout callback

By default, the displaying of Vuetify dialog is controlled by a button toggling the value of dialog Boolean variable.
I was assuming that programmatically changing the value of this variable would allow to show or hide the dialog, but it doesn't. Why not?
Here's my code:
<template>
<div>
<v-dialog v-model="dialog">
<v-card>
Dialog content
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
data() {
return {
dialog: false
}
},
mounted() {
console.log(this.dialog);
setTimeout(function() {
this.dialog = true;
console.log(this.dialog);
}, 2000);
}
}
</script>
Console shows false on page load, then true after 2 seconds. But dialog still doesn't show up...
You should use arrow function ()=> as setTimeout callback :
mounted() {
console.log(this.dialog);
setTimeout(()=> {
this.dialog = true;
console.log(this.dialog);
}, 2000);
}
See the Pen
Vuetify Dialog example by boussadjra (#boussadjra)
on CodePen.
you're having some trouble calling the variable inside the function of the setTimeout.
try this:
<template>
<div>
<v-dialog v-model="dialog">
<v-card>
Dialog content
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
data() {
return {
dialog: false
}
},
mounted() {
that = this
console.log(this.dialog);
setTimeout(function() {
that.dialog = true;
console.log(that.dialog);
}, 2000);
}
}
</script>
try to read this answer from a related issue with calling this inside anonymous functions

How can i access the errors computed property on custom input component validated using vee-validate?

I am trying to use VeeValidate in a custom input component.
I tried using $emit on #input and #blur but the validation occurs on next tick and i end up failing to catch the validation on event.
onEvent (event) {
console.log('error length', this.errors.items.length)
if (this.errors.items.length) {
this.hasError = true
this.$emit('has-error',this.errors.items)
} else {
this.hasError = false
this.$emit('on-input', event)
}
}
I also tried injecting the validator from the parent so as to be able to access the errors computed property directly but there might be 1-2-3 levels of nesting between the parent page and the custom input component itself. I would have to inject the validator through all of them and these component are meant to be reusable.
export default {
//in custom input component
inject: ['$validator'],
}
The best idea i got is to watch the errors computed property and emit an event when a change occurs whit the errors in that instance of the component.
watch: {
errors: function (errorsNew) {
console.log('errors watch',errorsNew)
}
},
The problem is that i can't seem to watch the errors computed property introduced by vee-validate.
Some simplified version of code:
parent
<template>
<div id="app">
<CustomInput
:label="'Lable1'"
:value="'value from store'"
:validations="'required|max:10|min:5'"
:name="'lable1'"
/>
<button>Save</button>
</div>
</template>
<script>
import CustomInput from "./components/CustomInput";
export default {
name: "App",
components: {
CustomInput
}
};
</script>
CustomInput
<template>
<div>
<label >{{ label }}</label>
<input :value="value" :name="name" v-validate="validations">
<span v-if="this.errors.items.length">{{this.errors.items[0].msg}}</span>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
label: {
type: String,
required: true
},
value: {
type: String,
default: ""
},
validations: {
type: String,
default: ""
},
name: {
type: String,
required: true
}
},
watch: {
errors: function(errorsNew) {
console.log("errors watch", errorsNew);
this.$emit('has-error')
}
}
};
</script>
Can someone please help me access the validation errors from the custom input?
Update
I created a simple fiddle if anyone finds it easier to test it https://codesandbox.io/s/mqj9y72xx
I think the best way to handle this is to inject $validator into the child component. That seems to be how the plugin recommends it be done: https://baianat.github.io/vee-validate/advanced/#injecting-parent-validator.
So, in your CustomInput, you'd add inject: ['$validator'],.
Then, in the App.vue, you can have this in the template:
<div>
These are the errors for "lable1" in App.vue:
<span v-if="errors.has('lable1')">{{errors.first('lable1')}}</span>
</div>
I think that's really it.
Working example based off your example: https://codesandbox.io/s/pw2334xl17
I realize that you've already considered this, but the inject method searches up the component tree until it finds a $validator instance, so perhaps you should disable automatic injection in your app at the root level, thus every component searching for a validator to inject will all get that one. You could do that using:
Vue.use(VeeValidate, { inject: false });

Categories