Vue JS emit event to child component from parent component not triggering - javascript

I'm working in a Nuxt JS project. I have several components, and one of my components called ComplianceOptoutRawText includes a child component called ComplianceOptoutViaExternalService, and it's this child component where I'm using a v-on to listen for an $emit event from the ComplianceOptoutRawText so I can toggle a variable, but I'm not seeing it at all in my console logs and need to know what I'm doing wrong and need to change.
Here's my markup with everything that's appropriate:
ComplianceOptoutRawText
<template>
<div>
<div>
<div class="tw-hidden md:tw-block tw-font-bold tw-mb-4">
<ComplianceOptoutViaExternalService>
<template #trigger>
<button #click="$emit('shown-optout-intent', true)" type="button">here</button>
</template>
</ComplianceOptoutViaExternalService>
</div>
</div>
</div>
</template>
And then the child component itself:
ComplianceOptoutViaExternalService
<template>
<div>
<slot name="trigger"></slot>
<article v-show="wantsToOptOut" v-on:shown-optout-intent="hasShownOptoutIntent" class="tw-bg-white tw-p-4 md:tw-p-6 tw-rounded-xl tw-text-gray-800 tw-border tw-border-gray-300 tw-text-left tw-mt-4">
<validation-observer v-if="!didOptOut" ref="optoutServiceForm" key="optoutServiceForm" v-slot="{ handleSubmit }" tag="section">
<form>
...
</form>
</validation-observer>
</article>
</div>
</template>
<script>
export default {
data () {
return {
wantsToOptOut: false,
isOptingOut: false,
didOptOut: false
}
},
methods: {
/*
** User has shown opt out intent
*/
hasShownOptoutIntent (value) {
this.wantsToOptOut = !this.wantsToOptOut
}
}
}
</script>
Note that my child component uses a slot, this is so I can position everything as needed in the parent component, but at it's core, I have a parent component emitting a value and then listening for it via v-on:shown-optout-intent="hasShownOptoutIntent" which runs the hasShownOptoutIntent method.
But I never see anything from the button, even if I console.log this. What am I missing?

I'm doing something similar with an embedded component, so it's perhaps worth trying:
<button #click="$emit('update:shown-optout-intent', true)" type="button">here</button>
... and then:
v-on:shown-optout-intent.sync="hasShownOptoutIntent"
As an aside, it's safe to remove v-on and use: :shown-optout-intent.

Related

What's the best way to trigger a child emit from the parent component?

Right now I'm passing a trigger prop from the parent to child component, which triggers the emit from the child to the parent.
parent component:
<form #submit.prevent="state.store=true" method="post" enctype="multipart/form-data">
<child-component :triggerEmit=state.store #emitSomething="getSomething()"/>
child component:
const emit = defineEmits([
'emitBody'
])
watchEffect(async () => {
if (props.triggerEmit) {
emit('emitSomething', value)
}
})
This gets confusing quickly, if the components grow in size and I was wondering if there is a simpler way to trigger child emits from the parent, since this seems to be a common use case.
Edit:
Trying to trigger the child method directly from the parent (not working).
child:
const childMethod = () => {
console.log('check')
}
parent:
html:
<child ref="childRef"/>
script setup:
const childRef = ref()
childRef.value.childMethod()
Page throws error:
Cannot read properties of undefined (reading 'childMethod')
As per my understanding you want to access multiple child component methods/properties from the parent component. If Yes, you can achieve that by create a ref and access the methods.
In template :
<!-- parent.vue -->
<template>
<button #click="$refs.childComponentRef.childComponentMethod()">Click me</button>
<child-component ref="childComponentRef" />
</template>
In script :
With Vue 2 :
this.$refs.childComponentRef.childComponentMethod( );
With Vue 3 Composition Api :
setup( )
{
const childComponentRef = ref( );
childComponentRef.value.childComponentMethod( )
return {
childComponentRef
}
}
In this case, the parent's trigger is effectively querying the child for its event data so that it could call getSomething() on it. The parent already owns getSomething(), so it really only needs the child data.
Another way to get that data is to use v-model to track the child data:
In the child component, implement v-model for a prop (a string for example) by declaring a modelValue prop and emitting an 'update:modelValue' event with the new value as the event data:
<!-- ChildName.vue -->
<script setup>
defineProps({ modelValue: String })
defineEmits(['update:modelValue'])
</script>
<template>
<label>Name
<input type="text" :value="modelValue" #input="$emit('update:modelValue', $event.target.value)">
</label>
</template>
In the parent, add a reactive object, containing a field for each child's v-model:
<!-- ParentForm.vue -->
<script setup>
import { reactive } from 'vue'
const formData = reactive({
name: '',
age: 0,
address: {
city: '',
state: '',
},
})
</script>
<template>
<form>
<child-name v-model="formData.name" />
<child-age v-model="formData.age" />
<child-address v-model:city="formData.address.city" v-model:state="formData.address.state" />
<button>Submit</button>
</form>
</template>
Now, the parent can call getSomething() on each field upon submitting the form:
<!-- ParentForm.vue -->
<script setup>
import { toRaw } from 'vue'
⋮
const getSomething = field => {
console.log('getting', field)
}
const submit = () => {
Object.entries(toRaw(formData)).forEach(getSomething)
}
</script>
<template>
<form #submit.prevent="submit">
⋮
</form>
</template>
demo

vue.js and slot in attribute

I'm trying to build a vue.js template that implements following:
<MyComponent></MyComponent> generates <div class="a"></div>
<MyComponent>b</MyComponent> generates <div class="a" data-text="b"></div>.
Is such a thing possible?
EDIT
Here is the best I can reach:
props: {
text: {
type: [Boolean, String],
default: false
}
},
and template
<template>
<div :class="classes()" :data-text="text">
<slot v-bind:text="text"></slot>
</div>
</template>
but the binding does not work, text always contains false.
You can use the mounted() method to get text using $slot.default property of the component to get the enclosing text. Create a text field in data and update inside mounted() method like this :
Vue.component('mycomponent', {
data: () => ({
text: ""
}),
template: '<div class="a" :data-text=text></div>',
mounted(){
let slot = this.$slots.default[0];
this.text=slot.text;
}
});
Note: It will only work for text, not for Html tags or components.
You're mixing slots and properties here. You'll have to pass whatever you want to end up as your data-text attribute as a prop to your component.
<MyComponent text="'b'"></MyComponent>
And in your template you can remove the slot
<template>
<div :class="classes()" :data-text="text"></div>
</template>
Another thing: it looks like your binding your classes via a method. This could be done via computed properties, take a look if you're not familiar.
You can try this.
<template>
<div :class="classes()">
<slot name="body" v-bind:text="text" v-if="hasDefaultSlot">
</slot>
</div>
</template>
computed: {
hasDefaultSlot() {
console.log(this)
return this.$scopedSlots.hasOwnProperty("body");
},
}
Calling
<MyComponent>
<template v-slot:body="props">
b
</template>
</MyComponent>

Jump to position on click (access class from other Vue component)

Explanation of problem
If a user clicks on the login link the view shall jump down to the login window where a user can type in userdata.
I am aware how to do this within a single file using document.getElementById('login-window').scrollIntoView()
However, I have a project with various single Vue.js component files. The login-link is within one "label" component. But the actual login-window is located in another component called "loginWindow", thus also the id / class "login-window" is stored in "loginWindow".
I tried to grab the "login-window" element with getElementById within my "label" component, but I believe it cannot access it since it is in another component.
This is the template code from "loginWindow"
<template>
<LoginGrid class="login-window" :as-grid="true" :class="classes" autocomplete="off">
<OCard class="login-card" :border="false">
<div class="login-headline f-copy f-bold l-color-primary">{{ t('headline') }}</div>
<!-- online state -->
<template v-if="isLogged">
<OButton color="secondary" #click="onClickLogout">{{ t('logout-label') }}</OButton>
</template>
<!-- offline state -->
<template v-else>
<div class="login-inputs">
<LoginInput
class="login-input-card-number" />
...
</div>
...
<OLink
type="secondary"
class="login-mode-link f-regular f-copy-small"
:no-router="true"
#click="onSwitchMode"
>
{{ modeLabel }}
</OLink>
...
</template>
</OCard>
</LoginGrid>
</template>
Here is what I've tried exactly
Within my "label" component I have implemented this method with an else-statement:
export default {
name: 'loginWindow',
...
methods: {
onClick() {
if (this.isLogged) {
...
} else {
if (isBrowser) {
document.getElementById("login-window").scrollIntoView();
}
}
},
},
}
So if the user is not logged-in, then onClick() it should scroll to the id of "login-window".
However, this solution does not work. I get an error saying "Cannot read property 'scrollIntoView' of null".
Any ideas how to do this with JavaScript within a Vue.js component?
login-window is Class not ID in your HTML. Try this:
document.querySelector(".login-window").scrollIntoView();

v-on:click not working bootstrap

Initially I thought this was an issue with how I was using the #click directive according to this question. I added the .native to the directive and my method is still not getting invoked.
I know this is bootstrap as if I use a normal <button> then the method is invoked as expected.
There are no errors in the logs so it is just as if the element is not registering the directive?
UpcomingBirthdays.vue
<template>
<div>
<h1>{{ section_title }}</h1>
<b-card style="max-width: 20rem;"
v-for="birthday in birthdays.birthdays"
:key="birthday.name"
:title="birthday.name"
:sub-title="birthday.birthday">
<b-button href="#"
#click.native="toWatch(birthday, $event)"
variant="primary">Watch
</b-button>
</b-card>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
name: "UpcomingBirthdays",
data: () => {
return {
section_title: "Upcoming Birthdays",
};
},
methods: {
toWatch: (birthday, event) => {
event.preventDefault();
console.log("watched called");
console.log(birthday.name);
console.log(`BEFORE: ${birthday.watch}`);
birthday.watch = !birthday.watch;
console.log(`AFTER: ${birthday.watch}`);
}
},
computed: mapState([
"birthdays",
]),
};
</script>
<style>
</style>
EDIT
Worth mentioning that when using HTML5 <button>, I do not have to append the .native property to the #click directive.
EDIT 2
Here is my codesandbox I created to replicate this error. I would expect an error here to say BirthdaysApi is not defined but I am not getting anything when the button is clicked.
Just remove the href="#" from your buttons (this makes the Bootstrap b-button component render your buttons as anchors) and it's working as expected:
https://codesandbox.io/s/w0yj3vwll7
Edit:
Apparently this is intentional behaviour from the authors, a decision I disagree upon. What they are doing is apparently executing event.stopImmediatePropagation() so any additional listener isn't triggered.
https://github.com/bootstrap-vue/bootstrap-vue/issues/1146

Accessing and filtering child components defined in a Vue.js component's <slot>

I'm in the process of implementing a combined select dropdown and searchbox component for a UI library I'm writing, using Vue.js to drive interactions, but I'm running into a problem with how Vue.js seems to handle communication between parent and child components.
Basically, what I'm aiming to achieve is to allow users of the library to define search-select elements in HTML using something like this:
<search-select>
<search-option value="foo">Foo</search-option>
<search-option value="bar">Bar</search-option>
<search-option value="baz">Baz</search-option>
<search-option value="foobar">Foobar</search-option>
</search-select>
My template for the search-select component looks like this:
<template>
<div class="search-select">
<div class="placeholder" ref="placeholder"></div>
<ul class="dropdown">
<input
type="text"
placeholder="Type to filter..."
v-on:input="updateFilter"
>
<slot></slot>
</ul>
</div>
</template>
The idea is that the search-select component will add a text input box above the specified inputs, which the user can type in to filter the options that can be selected. The finished component would look something like this.
However, from what I can tell in the documentation, Vue.js doesn't provide any direct way for me to access the properties of the child components from within the parent, nor any way to listen for click events or similar from children within a parent's <slot>.
This means that I don't have a way to filter visibility of the children based on the user's input, or to update the value of the search-select component when a user clicks on one of the children.
The Vue.js documentation mentions ways to pass events from child components to parents, but none seem applicable to elements defined within slots - it appears that parents can only listen for events from components explicitly defined within them.
How would one implement the type of two-way communication between parent and child components required for this use case, without violating any Vue.js best practices related to sharing information between components?
You can use event bus to solve this problem. Just keep every option listening to input event, then letting it decide whether hide itself or not depending on the argument passed.
See fiddle or demo below.
const bus = new Vue();
Vue.component('search-option', {
template: `
<option v-if="show" :value="this.$attrs.value"><slot></slot></option>
`,
created() {
bus.$on('filter', (input) => {
this.show = this.$attrs.value.includes(input);
});
},
beforeDestory() {
bus.$off('filter');
},
data() {
return {
show: true,
};
},
});
Vue.component('search-select', {
template: `
<div class="search-select">
<div class="placeholder" ref="placeholder"></div>
<ul class="dropdown">
<input
type="text"
placeholder="Type to filter..."
v-on:input="updateFilter"
v-model="myinput"
>
<slot></slot>
</ul>
</div>
`,
methods: {
updateFilter() {
console.log('update filter');
bus.$emit('filter', this.myinput);
},
},
data() {
return {
myinput: undefined,
};
},
});
Vue.component('parent', {
template: `
<div class="parent">
<search-select>
<search-option value="foo">Foo</search-option>
<search-option value="bar">Bar</search-option>
<search-option value="baz">Baz</search-option>
<search-option value="foobar">Foobar</search-option>
</search-select>
</div>
`,
});
new Vue({
el: '#app',
})
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<div id="app">
<parent></parent>
</div>

Categories

Resources