How to wait for click button to continue function - vue.js

I have a function with a popup dialog window with button. I want to wait with executing the rest of the function until the button is clicked. I tried it with Promise and AddEventListener, but cannot find out why is it not working. Could someone help me? (I use Vue3 and Quasar)
I have an error for "const confirm" - Object is possibly 'null'.ts(2531)
Thank you for any advices.
Here is a part of my template:
<q-dialog persistent v-model="phoneDialogBank">
<q-card>
<q-card-section>
<div class="items" v-for="formField in dynamicDialogFieldsVisible" :key="formField?.Key">
<dynamic-form-field :form-field="formField"></dynamic-form-field>
</div>
<div class="row items-center justify-center">
<q-btn color="primary" class="confirm-phone-dialog" v-close-popup>
{{ t('signing-table.phoneDialogBank.ok') }}
</q-btn>
</div>
</q-card-section>
</q-card>
</q-dialog>
Here is my function:
async function dynamicDialogBeforeSubmit() {
const params = getParamsFromAdvisorDeviceBoolean();
if (params && params.hasOwnProperty('dialogBeforeSubmit') && params.dialogBeforeSubmit) {
phoneDialogBank.value = true;
const confirm = document.querySelector('.confirm-phone-dialog');
const waitForButtonClick = new Promise((resolve) => { confirm.addEventListener('click', resolve); });
await waitForButtonClick.then(() => {
dynamicDialogSave();
});
return;
}
dynamicDialogSave();
}

That error is because your button is within a dialog that renders conditionally. So if there is no dialog, that DOM element does not exist. I'm curious why you are not using some of the great features of Vue? Like putting the click handler on the element and using refs to target the DOM instead of using query selectors and event listeners.
<q-btn color="primary" class="confirm-phone-dialog" #click.prevent="dynamicDialogBeforeSubmit" v-close-popup>
{{ t('signing-table.phoneDialogBank.ok') }}
</q-btn>
Your function is a bit mixed up between async and Promises. async / await is just syntactic sugar for a Promise. The way you have it written now, you are wrapping a Promise within another Promise.
async function dynamicDialogBeforeSubmit() {
const params = getParamsFromAdvisorDeviceBoolean();
if (params && params.hasOwnProperty('dialogBeforeSubmit') && params.dialogBeforeSubmit) {
phoneDialogBank.value = true;
await dynamicDialogSave();
} else {
console.log('There was a problem')
}
}
I should note that you will likely need to pass an object to dynamicDialogSave(someObject) for it to actually save something. I assume this, not knowing what that function actually looks like.
Event listeners are not asynchronous and you wouldn't want to wrap them in a promise anyways. If you wish to write it as you have, it would be best to declare the click listener as a separate function that fires onMounted, then call it from any async function. You should also detach native event listeners onUnmounted to avoid memory leaks with Vue.

Related

vue.js - Data doesn't show in console.log but renders fine in template

I have a Vue template in which I make async/await calls to get all sessions data, this data does not have to be rendered, but sent to another component in the form of an array, and the other component will get that info and produce some graphs. As a test, I added the array sessionSelected to the html template to see if it loads correctly and works just fine (This data change is triggered by a select component when selecting a program).
The behavior that I'm confused with however can be seen in the listSessions() method below, where I have console.log(val) that is inside a map for the sessionSelected array iteration;
When I check the console, the object that is being returned there is blank the first time I choose an option from the select component (a program), but when I pick another option, let's say program 6 it loads the previous sessions in the console.log(val), even though the same data object, when iterated through in the template, is displaying all the sessions correctly . (It's kinda like it always go, one "tick" behind)
A possible hint, if it helps, I added an #click to a <p> element below the select's components, so when the program is chosen, say program 2, and then I click to that <p> tag, the console.log does show correctly from the "listSessions" method.
I need to be able to have the sessionSelected array object synced, in such a way so that I'm sure that when I select a program, in the html template, the method will retrieve the right array (of sessions) like shows rendered in template.
<template>
<v-container>
<v-layout>
<v-flex lg4 sm12 xs12>
<GPSelect #input="listTreatments" v-model="patientSelected" :items="tvPatients" label="Patients" />
</v-flex>
<v-flex lg4 sm12 xs12>
<GPSelect #input="listPrograms" v-model="treatmentSelected" :items="treatments" label="Treatments" :disabled="treatments === undefined || treatments.length === 0" />
</v-flex>
<v-flex lg4 sm12 xs12>
<GPSelect #input="listSessions" v-model="programSelected" :items="programs" label="Programs" :disabled="programs === undefined || programs.length === 0" />
<p #click="listSessions">Session selected {{sessionSelected}}</p>
<p>ProgramSelected {{programSelected}}</p>
</v-flex>
</v-layout>
<BarChart :label="label" :data="dataSet" :labels="labels" />
</v-container>
</template>
<script>
export default {
data() {
return {
tvPatients: [],
patientSelected: "",
treatments: [],
programs: [],
sessions: [],
treatmentSelected: "",
programSelected: "",
sessionSelected: [],
dataSet: [],
...
}
},
created() {
this.listPatients();
},
methods: {
async listSessions() {
await this.getSessions();
this.updateData();
this.sessionSelected.map(async (val) => {
console.log( val)
})
this.sessionSelected.length = 0;
this.sessions.length = 0;
},
async getSessions() {
if (this.patientSelected) {
const response = await SessionService.getSessions(null, "meta");
if (response.data) {
return response.data.map(async (val, index) => {
if (val.program_id === this.programSelected) {
if (this.sessions != undefined) {
this.sessions.push(await SessionService.getSession(val._id, "meta"));
this.sessionSelected.push(await SessionService.getSession(val._id, "meta"));
}
}
})
}
}
},
async listPrograms() {
this.programs = await this.getPrograms();
},
async getPrograms() {
let response = await PatientService.getPatient(this.patientSelected, "tv");
if (this.patientSelected) {
const params = {
"treatment-id": response.data.documents[0].document.active_treatment_id
};
const programResponse = await ProgramService.getPrograms(params);
return await programResponse.data.map((val, index) => {
return {
name: `Program ${(index + 1) } ${response.data.documents[0].document.first_name}`,
value: val._id
}
});
}
}
}
}
</script>
I expect that the console.log(val) inside the map of the this.sessionSelected shows the same data displayed in the template, without having to use the <p> tag with the #click event as a hack, basically, that when a program gets selected from the select component, loads the associated data.
Quite difficult to follow with so much async/await going on. A bit of refactoring to deal with the pyramid of doom wouldn't hurt.
This line catches my eye:
return response.data.map(async (val, index) => {
This will be returning an array of unresolved promises. The surrounding function, getSessions, is async so it will wrap the return value in a further promise.
The line await this.getSessions(); will wait for that promise to resolve. Unfortunately it'll resolve immediately to the array of promises, without waiting for the individual promises to resolve. This is why the logging appears to be one step behind, as the inner promises haven't finished yet.
I think what you need is to add Promise.all, such that the outer promise waits for the array of promises.
return Promise.all(response.data.map(async (val, index) => {

Is `async/await` available in Vue.js `mounted`?

I'd like to do something like this in mounted() {}:
await fetchData1();
await fetchData2UsingData1();
doSomethingUsingData1And2();
So I wonder if this works:
async mounted() {
await fetchData1();
await fetchData2UsingData1();
doSomethingUsingData1And2();
},
In my environment it raises no errors, and seems to work well.
But in this issue, async/await in lifecycle hooks is not implemented.
https://github.com/vuejs/vue/issues/7209
I could not find further information, but is it available in fact?
It will work because the mounted hook gets called after the component was already mounted, in other words it won't wait for the promises to solve before rendering. The only thing is that you will have an "empty" component until the promises solve.
If what you need is the component to not be rendered until data is ready, you'll need a flag in your data that works along with a v-if to render the component when everything is ready:
// in your template
<div v-if="dataReady">
// your html code
</div>
// inside your script
data () {
return {
dataReady: false,
// other data
}
},
async mounted() {
await fetchData1();
await fetchData2UsingData1();
doSomethingUsingData1And2();
this.dataReady = true;
},
Edit: As stated in the documentation, this is an experimental feature and should not be used in production applications for now.
The correct way to do this in vue3 would be to make your setup() function async like this:
<script>
// MyComponent.vue
export default defineComponent({
/* ... */
async setup() {
await fetchData1();
await fetchData2UsingData1();
doSomethingUsingData1And2();
this.dataReady = true;
}
}
</script>
And then use a suspense component in the parent to add a fallback like this:
<template>
<Suspense>
<template #default>
<MyComponent />
</template>
<template #fallback>
Loading...
</template>
</Suspense>
</template>
So you would see the #fallback template while the component is loading, and then the component itself when it's ready.
Just use $nextTick to call async functions.

Renderless Vue component with a click listener

I have read this post which goes in depth about renderless components:
https://adamwathan.me/renderless-components-in-vuejs/
A renderless component would pretty much look like this:
export default {
render() {
return this.$scopedSlots.default({})
},
}
Now I would like to use this renderless component but also add a click listener to whatever is being passed into the slot.
In my case it would be a button. My renderless component would simply wrap a button and add a click listener to it, which in turn performs an AJAX request.
How would I go about adding a click listener to the element that is being passed into the slot?
Assuming you want to bind the click handler within the renderless component, I think from this post that you need to clone the vnode passed in to renderless, in order to enhance it's properties.
See createElements Arguments, the second arg is the object to enhance
A data object corresponding to the attributes you would use in a template. Optional.
console.clear()
Vue.component('renderless', {
render(createElement) {
var vNode = this.$scopedSlots.default()[0]
var children = vNode.children || vNode.text
const clone = createElement(
vNode.tag,
{
...vNode.data,
on: { click: () => alert('clicked') }
},
children
)
return clone
},
});
new Vue({}).$mount('#app');
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<div id="app">
<renderless>
<button type="button" slot-scope="{props}">Click me</button>
</renderless>
</div>
Here's one way to go about this.
Your renderless component wrapper would consist of a single action (i.e. the function to issue the AJAX request) prop.
Vue.component('renderless-action-wrapper', {
props: ['action'],
render() {
return this.$scopedSlots.default({
action: this.action,
});
},
});
Then another component which uses the aforementioned wrapper would enclose a customisable slot with a #click handler, which invokes the action that is passed in when triggered.
Vue.component('clickable', {
props: ['action'],
template: `
<renderless-action-wrapper :action="action">
<span slot-scope="{ url, action }">
<span #click="action()">
<slot name="action"></slot>
</span>
</span>
</renderless-action-wrapper>
`,
});
Finally, wire up the specialised version of the wrapper.
<clickable :action="doAjaxRequest">
<button type="button" slot="action">Button</button>
</clickable>
Here's a live example of the above suggestion you can play around with.

How to prevent/stop propagation of default event (click) in directive (vue 2.x)

Vue.directive('login-to-click', {
bind (el) {
const clickHandler = (event) => {
event.preventDefault()
event.stopImmediatePropagation()
alert('click')
}
el.addEventListener('click', clickHandler, true)
}
})
usage
<button #click="handleClick" v-login-to-click>CLICK</button>
handleClick is always triggered. How I can prevent that from directive? Tried with/without addEventListener "capture" flag without any luck.
For now I ended up with following solution:
Vue.prototype.$checkAuth = function (handler, ...args) {
const isLoggedIn = store.getters['session/isLoggedIn']
if (isLoggedIn) {
return handler.apply(this, args)
} else {
router.push('/login')
}
}
And then in component
<button #click="$checkAuth(handleClick)">CLICK</button>
From my understanding those are two different event handlers, you are only preventing the default event of the one bound in the directive, this has no influence on #click however, because you are not overwriting the click listener but adding a second one.
If you want the default of your #click binding to be prevented you can use #click.prevent="handleClick".
I don't think there's any way to do it from the directive, since you explicitly add another listener by binding #click to the button.
in my app I have many buttons (follow/like/add to watchlist/block
etc) that require user to be logged in to click on them
As with many things in Vue 2, this is a bad use case for a directive, but a very good use case for a component.
Here is a button that is only clickable when the user is authorized.
console.clear()
const AuthenticatedButton = {
props:["onAuth", "onNonAuth", "disable", "auth"],
template: `
<button #click="onClick"
:disabled="disableWhenNotAuthorized">
<slot>{{this.auth}}</slot>
</button>`,
computed:{
disableWhenNotAuthorized(){
return this.disable && !this.auth
}
},
methods:{
onClick(){
if (this.auth && this.onAuth) this.onAuth()
if (!this.auth && this.onNonAuth) this.onNonAuth()
}
}
}
new Vue({
el:"#app",
data:{
loggedIn: false
},
methods:{
onClick(){
alert("User is authenticated")
},
notAuthorized(){
alert("You are not authorized.")
}
},
components:{
"auth-btn": AuthenticatedButton
}
})
<script src="https://unpkg.com/vue#2.2.6/dist/vue.js"></script>
<div id="app">
<h3>User is {{!loggedIn ? 'Not Authorized' : 'Authorized'}}</h3>
<auth-btn :auth="loggedIn"
:on-auth="onClick"
:on-non-auth="notAuthorized">
Executes non auth handler when not authorized
</auth-btn> <br>
<auth-btn :auth="loggedIn"
:on-auth="onClick"
:disable="true">
Disabled when not authorized
</auth-btn> <br><br>
<button #click="loggedIn = true">Authenicate User</button>
</div>
With this button you can set an authorized handler and a non-authorized handler. Additionally, you can just disable the button if the user is not authorized.
In this component the authorized state is passed in through a property, but if you were using some form of state management (like Vuex) you could just as easily use that instead.
https://v2.vuejs.org/v2/guide/events.html#Event-Modifiers
There are a couple modifiers. .prevent is what I was looking for
<a href="#" #click.prevent="sendMessage('just one click');">
You can use:
event.target.preventDefault();
I am not sure why would want to do that though, since you are adding a click listener for a reason.
Perhaps you could clarify your question a bit more.

Handle Bootstrap modal hide event in Vue JS

Is there a decent way in Vue (2) to handle a Bootstrap (3) modal hide-event?
I found this as a JQuery way but I can't figure out how to capture this event in Vue:
$('#myModal').on('hidden.bs.modal', function () {
// do something…
})
Adding something like v-on:hide.bs.modal="alert('hide') doesn't seem to work.
Bootstrap uses JQuery to trigger the custom event hidden.bs.modal so it is not easily caught by Vue (which I believe uses native events under the hood).
Since you have to have JQuery on a the page to use Bootstrap's native modal, just use JQuery to catch it. Assuming you add a ref="vuemodal" to your Bootstrap modal you can do something like this.
new Vue({
el:"#app",
data:{
},
methods:{
doSomethingOnHidden(){
//do something
}
},
mounted(){
$(this.$refs.vuemodal).on("hidden.bs.modal", this.doSomethingOnHidden)
}
})
Working example.
Please see https://bootstrap-vue.js.org/docs/components/modal#overview
There you can find event "hide" or "hidden"
So you can bind this event:
<b-modal ref="someModal" #hide="doSometing">
One option is to tie it to a variable:
data: function(){
return {
showModal: false
//starts as false. Set as true when modal opens. Set as false on close, which triggers the watch function.
},
watch: {
showModal: function(){
if(this.showModal == false){
// do something
},
}
HTML
<button id="show-modal" #click="showModal = true">Show Modal</button>
//later if using a component
<modal v-if="showModal" #close="showModal = false">
// or alternatively in the bootstrap structure
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal" #click="showModal = false">Close</button>
</div>
This may be late but another way if you are using a custom modal component (Modal.vue) you have created is to
create a method in mounted to catch the event of closure (doesn't have to be the same name as below)
mounted: function(){
this.triggerHidden();
}
create the method
methods: {
triggerHidden: function(){
var self = this;
if( $('#getModal').length ){
$('#getModal').on('hidden.bs.modal', function(){
//catch the native bootstrap close event and trigger yours
self.#emit('modal-close');
});
}
}
}
now call use your custom event with your custom/reusable modal component
<custom-modal #modal-close="doSomething"></custom-modal>
The method doSomething will be called when the modal closes. You can also use the approach to hijack the other jquery event so its a little more manageable.
Maybe creating a Custom Vue Directive can help:
Vue.directive('bsevent', {
bind: function bsEventCreate(el, binding, vnode) {
let method = binding.value || (() => { });
$(el).on(binding.arg.replaceAll(/_/g, "."), (event) => { method(event); });
},
unbind(el, binding) {
$(el).off(binding.arg.replace(/_/, "."));
},
});
And then just use it on the element you wish (this example is on a bootstrap collapsible, but you could use it to any other bootstrap event):
<div id="myCollapsible" class="collapse" v-bsevent:hidden_bs_collapse="methodToCall">
...
</div>
The only thing to remember is to register the event with underscores instead of dots (show.bs.modal => show_bs_modal).
If working with bootstrap-vue then below code snippet will be helpful:
export default {
mounted() {
this.$root.$on('bv::modal::hide', (bvEvent, modalId) => {
console.log('Modal is about to be shown', bvEvent, modalId)
})
}
}
for other events please refer to the official docs.
Just use native addEventListener (Vue 3, Composition API)
template:
<div ref="modalElement" class="modal">
...
</div>
script:
import { Modal } from "bootstrap"
import { onMounted, ref } from "vue";
const modalElement = ref(null)
let modal = null;
onMounted(() => {
modal = new Modal(modalElement.value)
modalElement.value.addEventListener("hidden.bs.modal", onHidden)
})
function onHidden() {
// do something…
}
We can also use this simple approach like this example
<template>
<div>
<button #click="openModal = true">Open Modal</button>
<div v-if="openModal">
<div class="modal-background"></div>
<div class="modal-content">
<button #click="openModal = false">Close Modal</button>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
openModal: false
}
}
}
</script>