vuejs: v-if directive for event? - javascript

Can I react to an event in a vue template? Say a child component dispatches an event $dispatch('userAdded'), could I do something like this in the parent component:
<div class="alert alert-info" v-if="userAdded">
User was created!
</div>
or, if not, can I access variables of the child component?
<div class="alert alert-info" v-if="$refs.addView.form.successful">
User was created!
</div>
I tried both without success.
Also, while I'm here, is there an expressive way to hide elements after a certain amount of time? Something like (to hide after 2s):
<div class="alert alert-info" v-if="$refs.addView.form.successful" hide-after="2000">
User was created!
</div>
Thanks!
edit: wrote my own hide-after directive:
Vue.directive('hide-after', {
update: function(value) {
setTimeout(() => this.el.remove(), value);
}
});
<div class="alert alert-info" v-hide-after="2000">
This will be shown for 2 seconds
</div>

Yes you can but you need to take this approach.
Create a child that dispatches an event
In the parent component create an event listener for the event and also a data property that the event listener will set locally on the component instance
In the parent bind your v-if to the local data component
The code would look something like
parent
HTML
<div v-if="showAlert"></div>
Js
events: {
'alert.show': function () {
this.showAlert = true
},
'alert.hide': function () {
this.showAlert = false
}
},
data () {
return {
showAlert: false
}
}
Child
Js
methods: {
showAlert (show) {
show ? this.$dispatch('alert.show') : this.$dispatch('alert.hide')
}
}
The reason you should avoid using the $child and $parent is that it makes that component always depend on the fact that the parent will have the alert property and makes the child component lest modular
Since dispatch goes up until it hits a listener you can have several nested components in between the parent and child dispatching the alert control
UPDATE
Alternately, since you do not like the LOE of using events you can create a 2-way property on the child that either the parent or child can update
Example
Parent
HTML
<div v-if="showAlert"></div>
<child-component :show-alert.sync="showAlert"></child-component>
JS
data () {
return {
showAlert: false
}
}
Child
js
props: {
showAlert: {
type: Boolean,
twoWay: true
}
},
methods: {
showAlertInParent (show) {
this.$set('showAlert', show)
}
}

The whole idea of events is that you can react to them. But you want the reaction to pass by the model. You really don't want unrelated bits of markup listening and reacting 'independently'. $dispatch is deprecated. To do this now, do the following...
In the child component, emit an event as follows
this.$emit('didIt' {wasItAwful:'yep',wereYouScared:'absolutely'});
In the parent, you register the event listener with v-on, as an attribute of the child's tag...
<adventure-seeking-child v-on:did-it='myChildDidIt' />
Then, in the parent's methods, define your handler.
methods : { myChildDidIt : function(payload){ ... } }
Docs are here.

Related

VueJS Delay on Update Props

So I have a simple component, let's say Transaction.vue, and inside it there is a child component named TransactionEditModal.vue.
In the Transaction.vue component, I call the method "openTransactionEditModal" by clicking a button.
The flow of the method is that I am editing my child property "this.chosenTransactionId" first before opening the modal using "showModal()".
// Transaction.vue
<button ref="editTransactionButton" v-on:click="openTransactionEditModal($event)" class="btn btn-primary">
<i style="pointer-events: none" class="far fa-edit"></i>
</button>
<TransactionEditModal ref="transactionEditModal" v-bind:transactionId="chosenTransactionId" />
<script>
data: function () {
return {
chosenTransactionId: "",
}
}
methods: {
openTransactionEditModal(event: MouseEvent) {
if (event.currentTarget instanceof HTMLButtonElement) {
this.chosenTransactionId = event.currentTarget.id;
console.log("Chosen Transaction Id is Updated", event.currentTarget.id);
}
var transactionEditModal: any = this.$refs.transactionEditModal; transactionEditModal.initializeExistingValues(this.transactions[6]);
transactionEditModal.showModal();
}
}
</script>
// TransactionEditModal.vue
<script>
props: {
transactionId: String,
},
methods: {
showModal() {
console.log("Child component props should have been updated, is it? ", this.transactionId);
var reportApproveModal: JQuery<HTMLDivElement> = $('#transactionEditModal');
reportApproveModal.modal('show');
},
}
</script>
But why is it that the props is only updated after second click?
Result:
// First Click
Chosen Transaction Id is Updated 5aa1dfc7-4b2f-4dbe-911f-98d70a2624f2 Transaction.vue:365
Child component props should have been updated, is it? TransactionEditModal.vue:36
// Second Click
Chosen Transaction Id is Updated 5aa1dfc7-4b2f-4dbe-911f-98d70a2624f2 Transaction.vue:365
Child component props should have been updated, is it? 5aa1dfc7-4b2f-4dbe-911f-98d70a2624f2 TransactionEditModal.vue:36
UPDATE 1
After using watch functionality in the child component I get this result:
Chosen Transaction Id is Updated 5aa1dfc7-4b2f-4dbe-911f-98d70a2624f2 Transaction.vue:365
Child component props should have been updated, is it? TransactionEditModal.vue:36
Means updated after watch 5aa1dfc7-4b2f-4dbe-911f-98d70a2624f2 TransactionEditModal.vue:42
And I am inferring that it is updated after showModal() is called, which I actually found it to be weird, maybe updating props are asynchronous?
// TransactionEdit.vue
<script>
watch: {
transactionId(newVal, oldVal) {
console.log('Means updated after watch', newVal, oldVal);
},
},
</script>
The reason why the props is only updated after second click is because Vue uses an asynchronous update queue to update the component, which means that when you update the props in the openTransactionEditModal method, it doesn't take effect immediately. Instead, it is added to the update queue and processed after the current event loop. When you click the button the first time, the props are updated and added to the update queue, but the showModal method is called before the update is processed, so the child component still has the old props. When you click the button the second time, the update is already processed and the child component now has the updated props.
Using the watch functionality in the child component you can check the updated value of the props and check the exact time it was updated.
You can refactor the code to make sure the child component receives the updated props before the showModal method is called:
// Transaction.vue
<button v-on:click="openTransactionEditModal($event)" class="btn btn-primary">
<i class="far fa-edit"></i>
</button>
<TransactionEditModal v-bind:transactionId="chosenTransactionId" #modal-open="showModal" ref="transactionEditModal" />
<script>
data: function () {
return {
chosenTransactionId: "",
}
},
methods: {
openTransactionEditModal(event: MouseEvent) {
if (event.currentTarget instanceof HTMLButtonElement) {
this.chosenTransactionId = event.currentTarget.id;
}
this.$nextTick(() => {
this.$refs.transactionEditModal.initializeExistingValues(this.transactions[6]);
this.$refs.transactionEditModal.$emit('modal-open');
});
},
showModal() {
var reportApproveModal: JQuery<HTMLDivElement> = $('#transactionEditModal');
reportApproveModal.modal('show');
},
}
</script>
// TransactionEditModal.vue
<script>
props: {
transactionId: String,
},
</script>
I have added a custom event 'modal-open' which is emitted from the parent component after updating the chosenTransactionId and it triggers the showModal() method. This way, we ensure that the child component has already received the updated props before the modal is opened.
In addition, I have also wrapped the initializeExistingValues() and $emit('modal-open') function inside this.$nextTick(() =>{}) to ensure that the value has been updated before calling the function.
Also, I have removed the unnecessary ref attributes and the type casting of HTMLButtonElement.
Are you sure you have made the correct transactionId reactive?
In Transaction.vue you have defined a reactive variable called transactionId.
But then in the code of that component you refer to a variable called chosenTransactionId.
I suggest you rationalise it to just transactionId for both.
Or, if you really need two separate variables, then add chosenTransationId to your data function in Transaction.vue.
How about only allowing the modal to exist, once the transactionId is set?
You could change
<TransactionEditModal ref="transactionEditModal" v-bind:transactionId="chosenTransactionId" />
to
<TransactionEditModal
v-if="chosenTransactionId"
ref="transactionEditModal"
v-bind:transactionId="chosenTransactionId"
/>
This is the more conventional approach. You could even send the initialization information in a v-bind parameter to the child. That would avoid the parent component having to directly call a function within the Modal.
In general it is better to avoid the pattern of calling functions in a child. Instead, make the child exist (with v-if) only when all the necessary information is available and ready to be passed to it.

Vue Watcher not working on component created with Vue.extend

I have a Parent component with a select input which is bound through v-model to a variable in data.
Besides, I create child components dynamically using Vue.extend, which i pass the propsData which also includes the value of the select.
This components have a watcher for the prop that is related to the select input.
When i create the component it receives the props succesfully, The problem comes when I update the value of the select input that doesn't trigger the watcher on the child component.
I've been looking for similar situations but have not found something that helps me solve this problem, i don't know why it doesn't trigger the watcher on the child component when the select input changes.
Any help would be very preciated.
Here i create the component dynamically:
let PresupuestoFormularioVue = Vue.extend(PresupuestoFormulario)
let instance = new PresupuestoFormularioVue({
propsData: {
//The prop related to select input
seguro: this.seguro,
}
})
instance.$mount()
this.$refs.formularioContenedor.appendChild(instance.$el)
And this is the watcher in the component which isn't working:
watch:{
seguro:{
handler: function( newVal ){
console.log(newVal)
},
},
},
It's not the watch that doesn't work. It's the bindings. You're assigning the current value of this.seguro, not the reactive object itself. However, a new Vue() can add this binding for you.
As a sidenote, whether PresupuestoFormulario is a Vue.extend() doesn't matter. It can be any valid VueConstructor: a Vue.extend(), Vue.component() or a valid SFC (with name and template): export default {...}.
Here's how to do it:
methods: {
addPresupuestoFormulario() {
const div = document.createElement('div');
this.$el.appendChild(div);
new Vue({
components: { PresupuestoFormulario },
render: h => h("presupuesto-formulario", {
props: {
seguro: this.seguro
}
})
}).$mount(div)
}
}
The <div> initially appended to the parent will get replaced upon mounting with the actual template of PresupuestoFormulario and the bindings will be set, exactly as if you had <presupuesto-formulario :seguro="seguro" /> in the parent template from the start.
The really cool part about it is that the parent component doesn't need to have PresupuestoFormulario declared in its components.
Here's a working example:
const Test = Vue.component('test', {
template: `<div>message: {{message}}</div>`,
props: ['message'],
watch: {
message: console.log
}
})
new Vue({
el: '#app',
data: () => ({
msg: "¯\\_(ツ)_/¯"
}),
methods: {
addComponent() {
const div = document.createElement("div");
this.$el.appendChild(div);
new Vue({
components: {
Test
},
render: h => h("test", {
props: {
message: this.msg
}
})
}).$mount(div);
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2"></script>
<div id="app">
<input v-model="msg">
<button #click="addComponent">Add dynamic child</button>
</div>
A separate note, about using this.$el.appendChild(). While this works when you're using a root Vue instance (a so-called Vue app), it will likely fail when using a normal Vue component, as Vue2 components are limited to having only 1 root element.
It's probably a good idea to have an empty container (e.g: <div ref="container" />) in the parent, and use this.$refs.container.appendChild() instead.
All of props that you want check in watcher, should be a function. If you want read more about this go to vue document codegrepper.
watch: {
// whenever seguro changes, this function will run
seguro: function (newValue, oldValue) {
console.log(newValue,oldValue)
}
}

PointerEvent object being returned instead of child data on emit

I am working on creating a vue component library. I have build a button component that has data of it's width and left position. I'm trying to emit that data to the parent (a tabs component) when it's clicked. I have troubleshooted quite a bit, and have narrowed down most of the problem. My child component (button) is emitting the correct thing, but it looks like the parent component (tabs) is receiving the value of the click/pointerevent object instead of the data passed on the emit. I'm certain this is some issue in my parent click handle method, but can't pinpoint what exactly. I've included code snippets for the components and their click handler methods.
This is pared down, but essentially, I want to emit the width (and eventually left position) of the child button to the parent tab upon clicking the child/button. I want to assign that emitted width/left position to the slider to move some reactive underlining whenever a button is clicked in the tabs. I built in a console log statement on the click event that returns the emitted value from the child, and then returns the received value from the parent. Right now, the child is emitting the correct value when button is clicked, but parent is receiving and trying to assign a PointerEvent object. Thanks for any feedback!
Child (button) template and relevant script:
<template>
<div class="button #click="click" ref="button">
<slot />
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'Button',
emits: [
'click'
],
data () {
return {
width: '',
left: ''
}
},
setup() {
const button = ref(null)
return {
button
}
},
mounted () {
this.$nextTick(() => {
this.left = Math.ceil(this.button.getBoundingClientRect().left)
this.width = Math.ceil(this.button.getBoundingClientRect().width)
})
},
methods: {
click () {
this.$emit('click', this.width)
console.log(`${this.width} has been emitted to the tabs component`)
}
}
}
</script>
Parent (tab) template and relevant script:
<template>
<div class="tabs" #click="updateSliderWidth">
slot
</div>
<div class="slider" :style="sliderWidth">
</template>
<script>
import Button from './Button.vue'
export default {
name: 'Tabs',
components: {
Button
},
methods: {
updateSliderWidth (value) {
this.sliderWidth = value
console.log(`${value} has been received and assigned by parent`)
}
},
data () {
return {
sliderWidth: ''
}
}
}
</script>
I can't see any problems with your code, except that you don't use the Button component in the parent component. Instead you are using a div. This would explain, why you're getting a PointerEvent. This Event is passed as first parameter to the event, if you don't pass anything explicitly.
Here a demo: https://stackblitz.com/edit/vue-opruyd?file=src%2FApp.vue

Wrapping a JS library in a Vue Component

I need to know which is best practice for wrapping a JS DOM library in Vue. As an example I will use EditorJS.
<template>
<div :id="holder"></div>
</template>
import Editor from '#editorjs/editorjs'
export default {
name: "editorjs-wrapper",
props: {
holder: {
type: String,
default: () => {return "vue-editor-js"},
required: true
}
},
data(){
return {
editor: null
};
},
methods: {
init_editor(){
this.editor = new Editor({
holder: this.holder,
tools: {
}
});
}
},
mounted(){
this.init_editor();
},
//... Destroy on destroy
}
First:
Supose I have multiple instances of <editorjs-wrapper> in the same View without a :hook then all intances would have the same id.
<div id="app">
<editorjs-wrapper></editorjs-wrapper>
<editorjs-wrapper></editorjs-wrapper>
</div>
Things get little weird since they both try to process the DOM #vue-editor-js. Would it be better if the component generated a random id as default?
Second:
EditorJS provides a save() method for retrieving its content. Which is better for the parent to be able to call save() method from the EditorJS inside the child?
I have though of two ways:
$emit and watch (Events)
// Parent
<div id="#app">
<editorjs-wrapper #save="save_method" :save="save">
</div>
// Child
...
watch: {
save: {
immediate: true,
handler(new_val, old_val){
if(new_val) this.editor.save().then(save=>this.$emit('save', save)) // By the way I have not tested it it might be that `this` scope is incorrect...
}
}
}
...
That is, the parent triggers save in the child an thus the child emits the save event after calling the method from the EditorJS.
this.$refs.childREF
This way would introduce tight coupling between parent and child components.
Third:
If I want to update the content of the child as parent I don't know how to do it, in other projects I have tried without success with v-modal for two way binding:
export default{
name: example,
props:{
content: String
},
watch:{
content: {
handler(new_val, old_val){
this.update_content(new_val);
}
}
},
data(){
return {
some_js_framework: // Initialized at mount
}
},
methods: {
update_content: function(new_val){
this.some_js_framework.update(new_val)
},
update_parent: function(new_val){
this.$emit('input', this.some_js_framework.get_content());
}
},
mounted(){
this.some_js_framework = new Some_js_framework();
this.onchange(this.update_parent);
}
}
The problem is:
Child content updated
Child emit input event
Parent updates the two-way bidding of v-model
Since the parent updated the value the child watch updates and thus onchange handler is triggered an thus 1. again.

How to emit an event from Vue.js Functional component?

As the title of the question, this context is not available in the functional component. So if I have to emit an event, how can I do that?
For example in below code snippet:
<template functional>
<div>
<some-child #change="$emit('change')"></some-child>
</div>
</template>
My functional component doesn't have this context and hence $emit is not available. How can I bubble-up this event?
Child Component
<template functional>
<button #click="listeners['custom-event']('message from child')">
Button from child
</button>
</template>
Parent Component
<template>
<div>
<child-component #custom-event="call_a_method" />
</div>
</template>
See it in action on codesandbox
Do you want to emit the event from the vue instance?
export default {
functional: true,
render(createElement, { listeners }) {
return createElement(
"button",
{
on: {
click: event => {
const emit_event = listeners.event_from_child;
emit_event("Hello World!Is this the message we excpected? :/");
}
}
},
"Pass event to parent"
);
}
};
See it also a sandbox example here
This is explained in the docs Passing Attributes and Events to Child Elements/Components:
If you are using template-based functional components, you will also have to manually add attributes and listeners. Since we have access to the individual context contents, we can use data.attrs to pass along any HTML attributes and listeners (the alias for data.on) to pass along any event listeners.
At the most basic level, you can delegate all listeners like this:
<some-child v-on="listeners"></some-child>
If you only want to bind the change listener, you can do:
<some-child #change="listeners.change"></some-child>
but this will fail if listeners.change is undefined/null (not provided to the functional component).
If you need to handle the situation where there is no change listener, then you can do this:
<some-child #change="listeners.change && listeners.change($event)"></some-child>
otherwise you would have to settle by writing the render function by hand, since I don't think it is possible to conditionally assign the change listener to <some-child> in the template of a functional component. (Or maybe you can? I'm not sure.)
If you want to pass event listener conditionally you can do it inside functional component template like this:
v-on="listeners.change ? { change: listeners.change } : null"
The issue of conditionally attaching listeners is discussed here
a component with jsx:
export default {
name: "MyText",
functional: true,// functional component
props: {
value: {
type: [String, Number],
default: ""
}
},
render(h, context) {
const { props } = context;
// with jsx
// return (
// <button
// onClick={() => {
// console.log(context.listeners);
// context.listeners.input(Math.random().toString(36));
// context.listeners["my-change"](Math.random().toString(36));
// context.data.on.change(Math.random().toString(36));
// }}
// >
// {props.value}
// </button>
// );
// or use h function
return h(
"h1",
{
on: {
// emit some event when click h1
click: () => {
// has value prop has has input event auto
// event name come what event u listen in parent component
console.log(context.listeners);
context.listeners.input(Math.random().toString(36));
context.listeners["my-change"](Math.random().toString(36));
context.data.on.change(Math.random().toString(36));
}
}
},
props.value
);
}
};
conext.listeners is just an alias for context.data.on.
in parent componet, you should listen my-change and change, or has error.
event name inside component comes what event u listen in parent component
<MyText
v-model="value"
#change="change"
#my-change="myChange"
#u-change="uChange"
/>
vue 2.6.11 works well.
see the codesandbox online
Parent:
<Child #onFunction="handleFunction">
and this is the child component:
Child
<template functional>
<div>
<some-child #change="execute"></some-child>
</div>
</template>
methods:
execute(){
#emit("onFunction")
}

Categories

Resources