Vuejs: Callback after render - vuejs2

I have a Bootstrap popover that I want to attach to an element that has a conditional render; therefore, I must trigger $().popover() after the element has been attached to the DOM.
Is there a way to trigger a callback after a v-if statement inserts the elements into the DOM?

Use this in vuejs 2:
updated: function() {
$('[data-toggle="tooltip"]').tooltip();
},
take a look here

The right way to do this, is making it a directive, so you can hook into the life cycle of a DOM element.
Using nextTick is not the right way to do it for a few reasons, it can break if the DOM reacts and re render a part of your view. You are not destroying tooltips after initialisation. This can break because nextTick is async, and something in between render and nextTick can change your DOM state.
https://v2.vuejs.org/v2/guide/custom-directive.html
/* Enable Bootstrap popover using Vue directive */
Vue.directive('popover', {
bind: bsPopover,
update: bsPopover,
unbind (el, binding) {
$(el).popover('destroy');
}
});
function bsPopover(el, binding) {
let trigger;
if (binding.modifiers.focus || binding.modifiers.hover || binding.modifiers.click) {
const t = [];
if (binding.modifiers.focus) t.push('focus');
if (binding.modifiers.hover) t.push('hover');
if (binding.modifiers.click) t.push('click');
trigger = t.join(' ');
}
$(el).popover('destroy'); //update
$(el).popover({
title: typeof binding.value==='object'? binding.value.title : undefined,
content: typeof binding.value==='object'? binding.value.content : binding.value,
placement: binding.arg,
trigger: trigger,
html: binding.modifiers.html
});
}
//DEMO
new Vue({
el: '#app',
data: {
foo: "Hover me",
bar: "There",
baz: {content: "<b>Hi</b><br><i>There</i>", title: "Test"},
}
});
<link href="https://unpkg.com/bootstrap#3.3.7/dist/css/bootstrap.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://unpkg.com/bootstrap#3.3.7/dist/js/bootstrap.js"></script>
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<h4>Bootstrap popover with Vue.js Directive</h4>
<br>
<input v-model="foo" v-popover.hover="foo"/>
<button v-popover.click="bar">Click me</button>
<button v-popover.html="baz">Html</button>
<br>
<button v-popover:top="foo">Top</button>
<button v-popover:left="foo">Left</button>
<button v-popover:right="foo">Right</button>
<button v-popover:bottom="foo">Bottom</button>
<button v-popover:auto="foo">Auto</button>
</div>

Vue.nextTick() defers the execution of the callback to be executed after the next update of the DOM, see: VueJS API reference

Related

Vuejs conditional rendering v-if

I have this part of code wherein in my view theres a condition if I want to show it or not
<div v-if="toShow" ref="target"></div>
and in my javascript code I trigger toShow to true
this.toShow = true
this.$refs.target // always null
but when I use setTimeout() the value is not null
I need a solution wherein I dont want to use setTimeout() because I'm toggling toShow everytime for my transition so what happens is a have a lot of nested setTimeout() in my code.
You can use $nextTick which waits until the next DOM update cycle. It should be much better than setTimeout because it gets called quickly after the DOM updates rather than a specific time later.
I've created a fiddle below showing it working.
new Vue({
el: "#app",
data: () => {
return {
show: false
};
},
methods: {
toggleShow() {
this.show = !this.show;
console.log(this.$refs.target);
this.$nextTick(() => {
console.log(this.$refs.target);
});
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button #click="toggleShow">Toggle Show</button>
<div v-if="show">
<h5>Showing</h5>
<div ref="target"></div>
</div>
</div>

Why is Vue changing the parent value from a child without emitting an event

I am fairly new to Vue but doesn't this behavior completely contradict the design of props down, events up?
I have managed to stop it by using Object.assign({}, this.test_object ); when initializing the value in child-component but shouldn't that be the default behaviour?
Here is some background.
I am trying to have a dirty state in a much larger application (Eg a value has changed so a user must save the data back to the database before continuing on their way)
I had an event being emitted, and caught by the parent but the code I had to test the value and init the dirty state was not running as the value had already been changed in the parent component.
Vue.component( 'parent-component', {
template: '#parent-component',
data: function() {
return {
testObject: {
val: 'Test Value'
}
}
}
});
Vue.component( 'child-component', {
template: '#child-component',
props: {
test_object: Object
},
data: function() {
return {
child_object: this.test_object
}
}
});
Vue.config.productionTip = false;
new Vue({
el: '#app',
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script type="text/x-template" id="parent-component">
<div>
<child-component :test_object="testObject"></child-component>
<p>This is in the parent component</p>
<p><code>testObject.val = {{testObject.val}}</code></p>
</div>
</script>
<script type="text/x-template" id="child-component">
<div>
<label for="html_input">HTML Input</label>
<input style="border:1px solid #CCC; display:block;" type="text" name="html_input" v-model="child_object.val" />
</div>
</script>
<div id="app">
<parent-component></parent-component>
</div>
Use of v-model is a very deceptive thing. If you are not careful, you might end-up mutating data that doesn't belong to your component. In your case, you are accidentally passing read-only props directly to the v-model. It doesn't know if it is a prop or a local component state.
What you are doing is the right solution but considering one-way/unidirectional data flow in mind, we can rewrite this example in more explicit and elegant fashion:
Your component definition would be:
Vue.component( 'parent-component', {
template: '#parent-component',
data: function() {
return {
testObject: {
val: 'Test Value'
}
}
},
methods: {
// Added this method to listen for input event changes
onChange(newValue) {
this.testObject.val = newValue;
// Or if you favor immutability
// this.testObject = {
// ...this.testObject,
// val: newValue
// };
}
}
});
Your templates should be:
<script type="text/x-template" id="parent-component">
<div>
<child-component :test_object="testObject"
#inputChange="onChange"></child-component>
<p>This is in the parent component</p>
<p><code>testObject.val = {{testObject.val}}</code></p>
</div>
</script>
<!-- Instead of v-model, you can use :value and #input binding. -->
<script type="text/x-template" id="child-component">
<div>
<label for="html_input">HTML Input</label>
<input type="text" name="html_input"
:value="test_object.val"
#input="$emit('inputChange', $event.target.value)" />
</div>
</script>
Key things to note:
When using v-model, ensure that you are strictly working on a local value/data of the component. By no means, it should be referenced copy of external prop.
A custom form-like component can be readily converted into the one that can work with v-model provided you accept current value as :value prop and event as #input. v-model will just work out of the box.
Any modification to the value should happen in the same component.

How to re-mount a component in VueJS?

I have a component which is mounted as part of the DOM rendering. The skeleton of the application is
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>title</title>
</head>
<body>
<div id="app">
<my-component></my-component>
<button>press this button to reload the component</button>
</div>
</body>
</html>
<my-component> is functional (it displays a few form inputs) and $emit data to the parent.
Is there a way to re-mount it? The goal is to have a component content and setup as if it was just rendered for the first time (including a reset of the data() elements which hold its state).
There are some solutions to that but they all assume a rewrite of data(), which I would like to avoid.
My understanding is that a component is actuall HTML/CSS/JS code injected in the dom in the right place during the rendering so I fear that the concept of "re-mounting" it does not exist - I just wanted to make sure before going the data()-rewrite way.
The trick is to alter the key
When the key changes, vue regards it as a new component, so it will unmount the "old" component, and mount a "new" component.
See example, the created() hook will only run once, so if you see the value change, you're seeing a brand new object.
example:
Vue.component('my-component', {
template: `<div>{{ rand }}</div>`,
data() {
return {
rand: ''
}
},
created() {
this.rand = Math.round(Math.random() * 1000)
}
});
new Vue({
el: '#app',
data: {
componentKey:0
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.8/vue.min.js"></script>
<div id="app">
<my-component :key="componentKey"></my-component>
<button #click="componentKey=!componentKey">press this button to reload the component</button>
</div>
In your template you'll add the v-if directive:
<template>
<my-component v-if="renderComponent" />
</template>
In your script you'll add in this method that uses nextTick:
<script>
export default {
data() {
return {
renderComponent: true,
};
},
methods: {
forceRerender() {
// Remove my-component from the DOM
this.renderComponent = false;
this.$nextTick(() => {
// Add the component back in
this.renderComponent = true;
});
}
}
};
</script>
This is what's going on here:
Initially renderComponent is set to true, so my-component is rendered
When we call forceRerender we immediately set renderComponent to false
We stop rendering my-component because the v-if directive now evaluates to false
On the next tick renderComponent is set back to true
Now the v-if directive evaluates to true, so we start rendering my-component again

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>

Vue.js bind to DOM custom event with dots in name (like bootstrap events)

Using Vue 2.1.10
I can bind to DOM events with v-on directive. For example:
v-on:click
To bind to DOM click.
But I can't figure how to bind to an event that has dots in the name. such as "show.bs.modal" from bootstrap.
Currently, I use a workaround binding in the created hook with Regular DOM Methods, but I really would like to use the declarative syntax for that. How can this be achieved? thanks
EDIT:
The question is about allowed syntax: how can I do something like:
Vue.component('comp',{
template:'<div v-on:show.bs.modal="sunrise"></div',
methods:{
sunrise:function(e){
}
}
})
I was facing the very same problem when working on old projects.
Luckily I found the answer here: vue2 doc
<!-- object syntax (2.4.0+) -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>
This works on Bootstrap 5.1.1 with Vue 2.16.14:
<div class="modal" v-on="{ 'hide.bs.modal': handleModalClose }">
...
</div>
I think dots are not supported in v-on but you could create a custom directive to create an event listener for that event.
Not sure if there is something easier but something like in the demo below or this fiddle should work.
The demo creates a new event with dots in name but that should also work with bootstrap events (not tested). Please let me know if it's not working with bootstrap and I'll have a look.
Unbinding only works if you're using v-if. If you're removing that element with Javascript directly. The event will still be there.
var helloEvent = new Event('demo.event.hello');
document.addEventListener('demo.event.hello', function(e) {
// this is just for testing event dispatching!
console.log('main event listener');
}, false);
const bindCustomEvent = {
getName: function(binding) {
return binding.arg + '.' +
Object.keys(binding.modifiers).map(key => key).join('.');
},
bind: function(el, binding, vnode) {
const eventName = bindCustomEvent.getName(binding);
console.log(el, eventName);
document.addEventListener(eventName, binding.value);
},
unbind: function(el, binding) {
const eventName = bindCustomEvent.getName(binding);
console.log('unbinding', eventName);
document.removeEventListener(eventName, binding.value);
}
};
Vue.directive('bindCustomEvent', bindCustomEvent);
new Vue({
el: '#app',
data() {
return {
enabled: true,
eventMsg: ''
};
},
methods: {
sunrise: function(e) {
console.log('received event');
this.eventMsg = 'received event';
},
testEvent: function() {
document.dispatchEvent(helloEvent);
},
toggle: function() {
console.log('toggle', this.enabled);
this.enabled = !this.enabled;
if (!this.enabled) {
this.eventMsg = '';
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.js"></script>
<div id="app">
<div v-bind-custom-event:demo.event.hello="sunrise" v-if="enabled">
Hello, {{eventMsg}}
</div>
<!--
The following markup is not working
<div v-on="demo.event.hello:sunrise" v-if="enabled">
Hello, {{eventMsg}}
</div>-->
<button #click="testEvent()">
Change
</button>
<button #click="toggle">
<span v-if="enabled">disable custom event</span>
<span v-else>enable custom event</span>
</button>
</div>