I'm searching to trigger event from a vue instance which is encapsulated inside a shadowDOM.
For the moment, my code create a new Vue instance inside a shadowDOM and print the template without the style (because nuxt load the style inside the base vue instance).
I can call methods of each instance
But, when i'm trying to add an event / listener on my component, it doesn't work.
DOM.vue
<template>
<section ref='shadow'></section>
</template>
<script>
import bloc from '#/components/bloc.vue'
import Vue from 'vue'
export default {
data() {
return {
vm: {},
shadowRoot: {},
isShadowReady: false
}
},
watch: {
isShadowReady: function() {
let self = this
this.vm = new Vue({
el: self.shadowRoot,
components: { bloc },
template: "<bloc #click='listenClick'/>",
methods: {
listenClick() { console.log("I clicked !") },
callChild() { console.log("I'm the child !") }
},
created() {
this.callChild()
self.callParent()
}
})
}
},
mounted() {
const shadowDOM = this.$refs.shadow.attachShadow({ mode: 'open' })
this.shadowRoot = document.createElement('main')
shadowDOM.appendChild(this.shadowRoot)
this.isShadowReady = true
},
methods: {
callParent() {
console.log("I'am the parent")
},
}
}
</script>
bloc.vue
<template>
<div>
<p v-for='word in words' style='text-align: center; background: green;'>
{{ word }}
</p>
</div>
</template>
<script>
export default {
data() {
return {
words: [1,2,3,4,5,6]
}
}
}
</script>
Any idea ?
Thank you
Hi so after i watched at your code i think it's better to use the $emit to pass event from the child to the parent and i think it's more optimized then a #click.native
template: "<bloc #someevent='listenClick'/>",
<template>
<div #click="listenClickChild">
<p v-for='(word, idx) in words' :key="idx" style='text-align: center; background: green;'>
{{ word }}
</p>
</div>
</template>
<script>
export default {
data() {
return {
words: [1,2,3,4,5,6]
}
},
methods: {
listenClickChild() {
this.$emit('someevent', 'someValue')
}
}
}
</script>
bloc #click.native='listenClick'/>
Related
I have two components - parent and child. The child emits an event, which should trigger a console output in a parent component. However nothing is printed in console.
TestPage.vue:
<template>
<div v-on:hidden="hiddenEvent">
<TestComponent></TestComponent>
</div>
</template>
<script>
import TestComponent from "#/app/test/TestComponent";
export default {
name: "TestPage",
components: {TestComponent},
methods: {
hiddenEvent() {
console.log("hiddenEvent");
}
}
}
</script>
<style scoped>
</style>
TestComponent.vue:
<template>
<div class="test-container" v-if="!isHidden"
v-on:click="this.flip">
{{this.text}}
</div>
</template>
<script>
export default {
name: "TestComponent",
data() {
return {
text: "abc",
isHidden: false
}
},
methods: {
flip() {
this.isHidden = !this.isHidden;
if (this.isHidden) {
this.$emit("hidden");
} else {
this.$emit("revealed");
}
}
}
}
</script>
<style scoped>
.test-container {
padding: 2em;
background-color: #2EAC68;
}
</style>
When I click on the abc:
it disappears:
So I know the flip method is executed. However the parent's hiddenEvent is not executed, because hiddenEvent is never printed to console.
What am I doing wrong? I need the parent TestPage to be able to react to the event emitted by the child TestComponent
You should use v-on:hidden="hiddenEvent" on TestComponent tag not on the wrapping div.
The
<TestComponent></TestComponent>
in TestPage.vue should be updated to:
<TestComponent v-on:hidden="hiddenEvent"></TestComponent>
See in sandbox
You are listening to event from the child so put it on the child:
const app = Vue.createApp({
methods: {
hiddenEvent() {
console.log("hiddenEvent");
}
}
})
app.component('testComponent', {
template: `
<div class="test-container" v-if="!isHidden"
v-on:click="this.flip">
{{this.text}}
</div>
`,
data() {
return {
text: "abc",
isHidden: false
}
},
methods: {
flip() {
this.isHidden = !this.isHidden;
if (this.isHidden) {
this.$emit("hidden");
} else {
this.$emit("revealed");
}
}
}
})
app.mount('#demo')
.test-container {
padding: 2em;
background-color: #2EAC68;
}
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<div >
<test-component #hidden="hiddenEvent"></test-component>
</div>
</div>
I've a problem to load the css of my bloc component.
The webpage component allow to create an iframe and set some content inside easily.
It load correctly the template and script tag but not the css (it doesn't load).
Sometime it works, most of the time, it didn't.
I was thinking that it was a problem with the loading of the component but no.
If I load the component before or after the render of my "webpage" component : it don't load.
I've try with the auto import to true and after to false, but it solve nothing.
I have 2 components : webpage and bloc.
bloc.vue
<template>
<div class="bloc">
<p>Le texte</p>
</div>
</template>
<style>
.bloc {
background-color: blue;
padding: 20px;
}
</style>
webpage.vue
<script>
import Vue from "vue";
export default {
props: {
css: {
type: String,
required: false,
},
},
data() {
return {
load: false,
};
},
render(h) {
return h("iframe", {
on: { load: this.renderChildren },
});
},
beforeUpdate() {
//freezing to prevent unnessessary Reactifiation of vNodes
this.iApp.children = Object.freeze(this.$slots.default);
},
mounted() {
if (!this.load) this.renderChildren();
},
methods: {
// https://forum.vuejs.org/t/render-inside-iframe/6419/12
renderChildren() {
this.load = true;
const children = this.$slots.default;
const head = this.$el.contentDocument.head;
const body = this.$el.contentDocument.body;
let style = this.$el.contentDocument.createElement("style");
style.textContent += this.$props.css;
head.appendChild(style);
const iApp = new Vue({
name: "iApp",
// freezing to prevent unnessessary Reactifiation of vNodes
data: { children: Object.freeze(children) },
render(h) {
return h("body", this.children);
},
});
this.iApp = iApp; // cache instance for later updates
this.iApp.$mount(body); // mount into iframe
},
},
};
</script>
app.vue
<template>
<Webpage>
<component :is="name"></component>
</Webpage>
</template>
<script>
import bloc from "#/components/Bloc";
import Webpage from "#/components/Webpage";
export default {
components: {
bloc,
Webpage,
},
computed: {
name() {
return "bloc";
},
},
};
</script>
Do you have an idea where this might come from ?
The codesanbox : https://codesandbox.io/s/error-style-component-import-1t1hs?file=/pages/index.vue
Thank you.
Probably Webpage component overrides it.
Try moving your style to index and <Webpage class="bloc">
I am trying to make a VueJS plugin that exports a global method, which when called, will popup a message with an input text field. Ideally, I want to be able to make the following call from any Vue component:
this.$disaplayMessageWithInput("Title","Body","Value");
And a popup should come on the screen.
I've tried building it but when the install() calls this.$ref., it isn't recognized:
DeleteConfirmation.vue
<template>
<b-modal size="lg" ref="deleteConfirmationModal" :title="this.title" header-bg-variant="danger" #ok="confirmDelete" #cancel="confirmCancel">
<p>
{{this.body}}
</p>
</b-modal>
</template>
<script>
export default {
data()
{
return {
title: null,
body: null,
valueCheck: null,
value: null
};
},
install(vue, options)
{
Vue.prototype.$deleteConfirmation = function(title, body, expectedValue)
{
this.title = title;
this.body = body;
this.valueCheck = expectedValue;
this.$refs.$deleteConfirmation.show()
}
},
}
</script>
app.js
import DeleteConfirmation from './components/global/DeleteConfirmation/DeleteConfirmation';
Vue.use(DeleteConfirmation);
The call I am trying to make is:
$vm0.$deleteConfirmation("title","body","val");
I get the below error at the run time:
app.js?id=c27b2799e01554aae7e1:33 Uncaught TypeError: Cannot read property 'show' of undefined
at Vue.$deleteConfirmation (app.js?id=c27b2799e01554aae7e1:33)
at <anonymous>:1:6
Vue.$deleteConfirmation # app.js?id=c27b2799e01554aae7e1:33
(anonymous) # VM1481:1
It looks like, this.$refs in DeleteConfirmation.vue is undefined.
Try to avoiding $ref with vue ( $ref is here for third party and some very special case )
$ref isn't reactive and is populate after the render ...
the best solution for me is using a event bus like this :
const EventBus = new Vue({
name: 'EventBus',
});
Vue.set(Vue.prototype, '$bus', EventBus);
And then use the event bus for calling function of your modal ...
(
this.$bus.on('event-name', callback) / this.$bus.off('event-name');
this.$bus.$emit('event-name', payload);
)
You can create a little wrapper around the bootstrap modal like mine
( exept a use the sweet-modal)
<template>
<div>
<sweet-modal
:ref="modalUid"
:title="title"
:width="width"
:class="klass"
class="modal-form"
#open="onModalOpen"
#close="onModalClose"
>
<slot />
</sweet-modal>
</div>
</template>
<script>
export default {
name: 'TModal',
props: {
eventId: {
type: String,
default: null,
},
title: {
type: String,
default: null,
},
width: {
type: String,
default: null,
},
klass: {
type: String,
default: '',
},
},
computed: {
modalUid() {
return `${this._uid}_modal`; // eslint-disable-line no-underscore-dangle
},
modalRef() {
return this.$refs[this.modalUid];
},
},
mounted() {
if (this.eventId !== null) {
this.$bus.$on([this.eventName('open'), this.eventName('close')], this.catchModalArguments);
this.$bus.$on(this.eventName('open'), this.modalRef ? this.modalRef.open : this._.noop);
this.$bus.$on(this.eventName('close'), this.modalRef ? this.modalRef.close : this._.noop);
}
},
beforeDestroy() {
if (this.eventId !== null) {
this.$off([this.eventName('open'), this.eventName('close')]);
}
},
methods: {
onModalOpen() {
this.$bus.$emit(this.eventName('opened'), ...this.modalRef.args);
},
onModalClose() {
if (this.modalRef.is_open) {
this.$bus.$emit(this.eventName('closed'), ...this.modalRef.args);
}
},
eventName(action) {
return `t-event.t-modal.${this.eventId}.${action}`;
},
catchModalArguments(...args) {
if (this.modalRef) {
this.modalRef.args = args || [];
}
},
},
};
</script>
<style lang="scss" scoped>
/deep/ .sweet-modal {
.sweet-title > h2 {
line-height: 64px !important;
margin: 0 !important;
}
}
</style>
AppModal.vue
<template>
<div class="modal-wrapper" v-if="visible">
<h2>{{ title }}</h2>
<p>{{ text }}</p>
<div class="modal-buttons">
<button class="modal-button" #click="hide">Close</button>
<button class="modal-button" #click="confirm">Confirm</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
visible: false,
title: '',
text: ''
}
},
methods: {
hide() {
this.visible = false;
},
}
}
</script>
Modal.js (plugin)
import AppModal from 'AppModal.vue'
const Modal = {
install(Vue, options) {
this.EventBus = new Vue()
Vue.component('app-modal', AppModal)
Vue.prototype.$modal = {
show(params) {
Modal.EventBus.$emit('show', params)
}
}
}
}
export default Modal
main.js
import Modal from 'plugin.js'
// ...
Vue.use(Modal)
App.vue
<template>
<div id="app">
// ...
<app-modal/>
</div>
</template>
This looks pretty complicated. Why don't you use a ready-to-use popup component like this one? https://www.npmjs.com/package/#soldeplata/popper-vue
How can I let 2 single file components communicate with each other.
For example: I have 2 file components. Content.vue and a Aside.vue
How can i create something like, when I click on a button inside Aside.vue that something will update inside Content.vue
this is how the 2 single file compontents look inside the index.html:
<div class="container articleContainer">
<article-content></article-content>
<article-aside></article-aside>
</div>
Aside.vue:
<template>
<aside>
<span #click="updateCounter">Dit is een aside.</span>
</aside>
</template>
<script>
export default {
data() {
return {
aside: "aside message"
}
}
}
</script>
Content.vue
<template>
<article>
<p>{{ counter }}</p>
<button #click="updateCounter">Update Counter</button>
</article>
</template>
<script>
export default {
data() {
return {
counter: 0
}
}
methods: {
updateCounter: function() {
this.counter = this.counter + 2;
},
}
}
</script>
When I click on the span inside the Aside template how can I use updateCounter to update the counter inside Content.vue.
if your app is not aas big or complex to use vuex , you can set up an EventBus like this:
export const EventBus = new Vue();// in your main.js file
in Aside.vue:
<template>
<aside>
<span #click="updateCounter">Dit is een aside.</span>
</aside>
</template>
<script>
import {EventBus} from './path/to/main.js'
export default {
data() {
return {
aside: "aside message"
}
},
methods:{
updateCounter(){
EventBus.emit('updateCounter');
}
}
}
</script>
in Content.vue
<template>
<article>
<p>{{ counter }}</p>
<button #click="updateCounter">Update Counter</button>
</article>
</template>
<script>
import {EventBus} from './path/to/main.js'
export default {
data() {
return {
counter: 0
}
}
created() {
EventBus.on('updateCounter', () => {
this.counter = this.counter + 2;
});
},
methods: {
updateCounter: function() {
this.counter = this.counter + 2;
},
}
}
</script>
Option 1: Have a value in the App.vue that gets reflected by both the components. (That's the this.$parent.someParentMethod(someValue);-way, which would be mixed with props).
Option 2 (way easier, cleaner and best-practice): vuex
Communication between any components using Event Bus
Event Bus is not limited to a parent-child relation. You can share information between any components.
<script>
export default
name: 'ComponentA',
methods: {
sendGlobalMessage() {
this.$root.$emit('message_from_a', arg1, arg2);
}
}
}
</script>
In the above ComponentA, we are firing an event “message_from_a” and passing arguments. Arguments are optional here. Any other component can listen to this event.
<script>
export default
name: 'ComponentB',
mounted() {
this.$root.$on('message_from_a', (arg1, arg2) => {
console.log('Message received');
});
}
}
</script>
In ComponentB to listen an event, we have to register it first. We can do so by putting an event listener inside mounted() callback. This callback will be triggered when an event is fired from any component.
source
I'm trying to get a method to execute on a parent component when a button in one of its child components is clicked. I am using single file components with Webpack. Here's the code for the child component:
<template>
<button v-on:click="add">Click</button>
</template>
<script>
export default {
methods: {
add: () => {
console.log('foo')
this.$dispatch('addClick')
}
}
}
</script>
And the code for the parent:
<template>
<div id="app">
<count :total="total"></count>
<click></click>
</div>
</template>
<script>
import count from './components/count.vue'
import click from './components/click.vue'
export default {
components: {
count,
click
},
data: () => {
return {
total: 0
}
},
methods: {
addToTotal: () => {
console.log('bar')
this.total += 1
}
},
events: {
addClick: 'addToTotal'
}
}
</script>
The count component is just an h1 element that displays the total variable. When I click the button, "foo" logs to the console, but "bar" does not and the total doesn't change. Any ideas on what I'm doing wrong? Thanks in advance!
You are using lambda notation for your methods, which is giving them the wrong this. If you use function instead, it will work.
Vue.component('click', {
template: '#clicker',
methods: {
add: function() {
console.log('foo')
this.$dispatch('addClick')
}
}
})
new Vue({
el: '#app',
data: () => {
return {
total: 0
}
},
methods: {
addToTotal: function () {
console.log('bar')
this.total += 1
}
},
events: {
addClick: 'addToTotal'
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
<template id="clicker">
<button v-on:click="add">Click</button>
</template>
<div id="app">
<count :total="total"></count>
<click></click>
{{total}}
</div>
Use two-way .sync binding type modifier on total property of count component so that the value will be updated when parent total value is changed. Here is an example:
Vue.component('click', {
template: '<button v-on:click="add">Click</button>',
methods: {
add: function () {
this.$dispatch('addClick');
}
}
});
Vue.component('count', {
template: '<h1 v-text="total"></h1>',
props: {
total: {
type: Number,
twoWay: true
}
}
});
new Vue({
el: '#app',
data: {
total: 1
},
methods: {
addTotal: function () {
this.total++;
}
},
events: {
addClick: 'addTotal'
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.25/vue.min.js"></script>
<div id="app">
<count :total.sync="total"></count>
<click></click>
</div>