How to re-mount a component in VueJS? - vue.js

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

Related

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.

autohide an element in VueJS after 1 second

OK, so I'm new to Vue ( basically, new to JS in general, but I'm playing with Vue right now ) and what I want to do, is to auto hide an element ( not on click ) inside the template tag of a component. In jQuery this would look like:
$(function() {
setTimeout(function() {
$(".hideElement").hide()
}, 1000);
});
but how this works in Vue? my component looks like this:
<template>
<div>
<h1 class="hideElement"> HELLO </h1>
</div>
</template>
<script> // nothing here
</script>
<style> // nothing here
</style>
I know how to toggle the element on click of a button, but I just want to auto hide it after 1 second without any click events everytime the users enter this component ( which is a new "page" )
You could just add a property in the data object and use v-show directive to determine whether the element should be visible or not. If the boolean is false the element is hidden, if true the element is visible.
Method Created called synchronously after the instance is created.
<template>
<div>
<h1 v-show="elementVisible" class="hideElement"> HELLO </h1>
</div>
</template>
<script>
export default {
data() {
return {
elementVisible: true
}
},
created() {
setTimeout(() => this.elementVisible = false, 1000)
}
}
</script>

Why v-if is not showing the heading when boolean value changes?

I am very new to vue js. I am just learning to use it from laracasts. What I want to do is communicate between root class and subclass. Here, user will put a coupon code and when he changes focus it will show a text.
My html code is like this
<body>
<div id="root">
<coupon #applied="couponApplied">
<h1 v-if="isCouponApplied">You have applied the coupon.</h1>
</div>
<script src="https://unpkg.com/vue#2.5.21/dist/vue.js"></script>
<script src="main.js"></script>
</body>
My main.js is like this,
Vue.component('coupon', {
template: '<input #blur="applied">',
methods: {
applied()
{
this.$emit('applied');
}
}
});
new Vue({
el: '#root',
data: {
isCouponApplied:false,
},
methods:{
couponApplied()
{
this.isCouponApplied = true;
}
}
});
I am checking using vue devtools extension in chrome. There is no error. The blur event is triggered. isCouponApplied also changes to true. But the h1 is not showing. Can anyone show me where I made the mistake?
The problem is that you are not closing your <coupon> tag
<div id="root">
<coupon #applied="couponApplied"></coupon>
<h1 v-if="isCouponApplied">You have applied the coupon.</h1>
</div>
Should fix your issue. If you don't close your tag, the parser will auto-close it, but it will do so at the close of its wrapping container (the root div), so the h1 content will be seen as inside the <coupon> element, and will be replaced by your component's template.

Dynamic component event bus not work in vue?

I just used event bus in vuejs for the dynamic component, but when I emit an event from one component, I cannot listen to that event in another component. Below is the demo I created by codesandbox, what I want to do is output 'test in tes2' from test2component, but it outputs nothing. But when I uncomment the $bus listener in HomeCompnent, the two console.log are all executed.
main.js
Vue.propotype.$bus = new Vue();
HomeComponent
<template>
<div>
<component :is="currentComponent" #changeComponent="changeComponent"></component>
</div>
</template>
<script>
import Test1Component from "./Test1Component";
import Test2Component from "./Test2Component";
export default {
created() {
// this.$bus.$on("test", () => console.log("event bus test"));
},
components: {
Test1Component,
Test2Component
},
data() {
return {
currentComponent: "Test1Component"
};
},
methods: {
changeComponent() {
this.currentComponent = "Test2Component";
}
}
};
</script>
<style>
</style>
Test1Component
<template>
<div>
<h1>test1</h1>
<button #click="changeComponent">click me</button>
</div>
</template>
<script>
export default {
methods: {
changeComponent() {
this.$emit("changeComponent");
this.$bus.$emit("test");
}
}
};
</script>
<style>
</style>
Test2Component
<template>
<h1>test2</h1>
</template>
<script>
export default {
created() {
this.$bus.$on("test", () => console.log("test in test2"));
}
};
</script>
<style>
</style>
This problem is a good demonstration of the downsides of the event bus pattern:
You, the developer, have to carefully ensure that sender and receiver of all events actually exist at the same moment in time.
In your scenario, that's not the case:
When Test1Component emits the event, Test2Component doesn't exist yet.
After Test2Component has been created by HomeComponent just a moment later, the event is already "gone".
My usual disclaimer as a Vue core team member: Don't use the Event bus pattern.
Event Bus is just fine but problem is in implementation.
You are very correct in case of you un-comment code In home component, I tested on your snippet it emit only once event bus test [console.log]
With events you need to take care this
You need to define listener. [ make sure you define them before emitting event ]
Now you just emit event. [ to work we need listener to listen first]
In your case you wrote listener function $on in created event of theTest2Component
Now just think for now, You don't have any listeners at beginning as in home compo you just commented that listener code .[ its initial step ]
Now when you click on click me button you are changing component to new component[Test2Component], It is mounted and its created event will fire then this listener will start listening to event
but you missed this
this.$emit("changeComponent"); // this is first [ fires/emit ]
this.$bus.$emit("test"); // THEN THIS EMIT
So, When it just start changing component from compo1 tocompo2testfired/emitted directly without wait, its notsynchronousit isAsynchronousIt will not wait to finish all stuff ofchangeComponent. It will be emitted immediately [test`]
Guess what Now when test emits, at that time Dom operation to add component, ITS NOT DONE YET, and test is emitted
So, No listeners are there, so no console.log
But if you see if, you UNCOMMENT listener in home function listener is well defined before emit of test event so it output in console.
I hope you understand it, if not let me know point I will explain it in details
Another thins is that you added $bus in prototype so all components has its own bus. instead you can use GLOBAL event Bus
as in example you can see.
in es6 you can do
//bus.js
const bus = new Vue({});
export default bus;
to import it in other components
import bus from './bus.js';
// ... do bus.$on ..
// ... do bus.$emit ..
var $bus = new Vue({});
Vue.component('Test1', {
template: `
<div class="blog-post">
<h3>test1</h3>
<button #click="changeComponent">click me</button>
</div>
`,
methods: {
changeComponent() {
$bus.$emit("chng");
$bus.$emit("test");
}
}
});
Vue.component('Test2', {
template: `
<div class="blog-post">
<h3>test2</h3>
</div>
`,
created() {
$bus.$on("test", () => console.log("test in test2 will not fire as we are little late to listen it"));
}
});
new Vue({
el: '#app',
created: function(){
console.log('created');
$bus.$on("chng", () => this.changeComponent());
$bus.$on("test", () => console.log("test in test2 main/HOME"));
},
data() {
return {
currentComponent: "Test1"
};
},
methods: {
changeComponent() {
this.currentComponent = "Test2";
}
}
});
<!DOCTYPE html>
<html>
<head>
<script> console.info = function(){} </script>
<script src="https://code.jquery.com/jquery.min.js"></script>
<script src="https://vuejs.org/js/vue.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" type="text/css" />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Stack Overflow - Hardik Satasiya</title>
<style>.half{width:50%;float:left;}</style>
</head>
<body>
<div id="app">
<component :is="currentComponent" #changeComponent="changeComponent"></component>
</div>
</body>
</html>
Your test is not correct.
Basically, Test2Component is not created in the time Test1Component emit this.$bus.$emit("test");
You saw 2 console.log because codesandbox use hot reload modules, so this.$bus.$on("test", () => console.log("test in test2")); is still registered if when you change your code.
If you unregister when component destroyed in Test2Component, test in test2 will never be logged
Test2Component.vue
<template>
<div>
<h1>test2</h1>
<button #click="changeComponent">click me</button>
</div>
</template>
<script>
export default {
created() {
console.log("Component 2 is created");
this.$bus.$on("test", this.testLog);
},
beforeDestroy() {
this.$bus.$off("test", this.testLog);
},
methods: {
testLog() {
console.log("test in test2");
},
changeComponent() {
this.$emit("changeComponent");
}
}
};
</script>
<style>
</style>
Demo: https://codesandbox.io/s/2485jw460y
If you comment
beforeDestroy() {
this.$bus.$off("test", this.testLog);
},
in component 2, you will see test in test2 is printed many times after few clicks
(if you change the code, please refresh to run from beginning)

Vuejs: Callback after render

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