continuously propagate event up chain - javascript

If I have nested children that I want to propagate an event all the way up to the root component, is there any simple way to pass on the event?
i.e.
<root #custom_event="doSomething" #another_event="doSomethingElse">
<child1 #custom_event="passItAlong" #another_event="passItAlong">
<child2 #custom_event="passItAlong" #another_event="passItAlong">
<child3 #click="$emit('custom_event', 'data')">
</child3>
</child2>
</child1>
</root>

You have multiple options here:
You can use this.$root.$emit and then event will be sent to all components at once, you can listen to it as this.$root.$on
You can create eventBus as explained here and then use it wherever you need to:
// The most basic event bus
// Imprt vue.js
import Vue from 'vue';
// Create empty vue.js instance to use as event bus
const Bus = new Vue({});
// Export event bus instance
export default Bus;
// Using the most basic event bus
import Vue from 'vue';
import Bus from './basic';
Vue.component('my-first-component', {
methods: {
sampleClickAction() {
Bus.$emit('my-sample-event');
}
}
});
Vue.component('my-second-component', {
created() {
Bus.$on('my-sample-event', () => {
// Do something…
});
}
});

Related

Vue 3 Event Bus with Composition API

I have setup mitt and trying to dispatch event to another component but I am having hard time because in the setup() method it doesn't have this for accessing app instance.
Here is what I tried:
import App from './App.vue'
const el = document.getElementById('app')
import mitt from 'mitt';
const emitter = mitt();
const app = createApp(App)
app.config.globalProperties.emitter = emitter;
app.mount(el);
And in the component, I want to dispatch an event
export default {
setup() {
function toggleSidebar() {
this.emitter.emit('toggle-sidebar');
console.log(this); // binds to setup(), not the vue instance.
}
}
}
As this doesn't exist, I can't access the .emitter. What am I missing? How to use officially suggested mitt in Vue 3 composition api?
By the way if I use the v2 syntax, I can access this.emitter. But I am curious about Composition API way
export default {
mounted() {
console.log(this.emitter); // works
}
}
To use an event bus in Vue 3 Composition API, use Vue 3's new provide api in main.js, and then inject in any component:
1. Install mitt:
npm install mitt
2. Provide:
main.js
import { createApp } from 'vue';
import App from './App.vue';
import mitt from 'mitt'; // Import mitt
const emitter = mitt(); // Initialize mitt
const app = createApp(App);
app.provide('emitter', emitter); // ✅ Provide as `emitter`
app.mount('#app');
3. Inject
3a. Any Component - Emit an event
import { inject } from 'vue'
export default {
setup() {
const emitter = inject('emitter'); // Inject `emitter`
const mymethod = () => {
emitter.emit('myevent', 100);
};
return {
mymethod
}
}
}
Call mymethod from a button click or something.
3b. Any Component - Listen for the event
import { inject } from 'vue'
export default {
setup() {
const emitter = inject('emitter'); // Inject `emitter`
emitter.on('myevent', (value) => { // *Listen* for event
console.log('myevent received!', `value: ${value}`);
});
},
}
Console
myevent received! value: 100
You may be able to use getCurrentInstance to get the global property
component:
import { getCurrentInstance } from 'vue';
export default {
setup() {
// get current instance
const internalInstance = getCurrentInstance();
// get the emitter from the instance
const emitter = internalInstance.appContext.config.globalProperties.emitter;
}
}
So far I have used this code to make the "emitter" available.
//main.ts
import mitt from 'mitt'
const emitter = mitt()
export default emitter
And then inside the components I use
import emitter from '#/main';
This worked so far in Vue2 and Vue3 - at least with the options API.
I have to admit though that I currently run into some trouble with the new vite server and the hot module reload (hmr).
Is this style suboptimal in any way?

VueJS 2 can't emit event in mounted(), created() hooks

Out of the blue, emitter stopped working:
event-bus.js
import Vue from 'vue';
export const EventBus = new Vue();
import { EventBus } from '../event-bus';
...
mounted() {
this.getCart();
}
...
methods: {
getCart() {
axios.get(`${app.api_erp_url}/cart/${this.cartId}`).then((response) => {
this.cart = response.data;
EventBus.$emit('cartLoaded', this.cart); // this not working
});
}
},
another-component.vue
mounted() {
// MiniCart.vue
EventBus.$on('cartLoaded', (payload) => {
...
});
},
No matter how I try to emit the event inside mounted/created, it will not work. No problems when firing events on click or something.
Created sandbox: https://codesandbox.io/s/gracious-kilby-m43ih?fontsize=14&hidenavigation=1&theme=dark
Child components mount before their parent component.
This is the sequence occurring in your example:
HelloWorld (parent) is created
Test (child) is created
Test (child) is mounted, which emits an event
HelloWorld (parent) is mounted, which subscribes to the event that was already emitted
If you want HelloWorld to catch the event from its children, subscribe to the event in the created hook.
demo
According to this You should use kebab-case format to name your custom events :
EventBus.$emit('cartLoaded', this.cart);//not correct
EventBus.$emit('cart-loaded', this.cart); //correct
May be the event emitted before MiniCart.vue component registered the event.
Meaning in code.
EventBus.$emit('cartLoaded', this.cart);
this run first time before the event has been registered
EventBus.$on('cartLoaded', (payload) => {
...
});

How to replace this.$parent.$emit in Vue 3?

I have migrated my application to Vue 3.
Now my linter shows a deprecation error, documented here: https://eslint.vuejs.org/rules/no-deprecated-events-api.html.
The documentation shows how to replace this.$emit with the mitt library, but it doesn't show how to replace this.$parent.$emit.
In your child component:
setup(props, { emit }) {
...
emit('yourEvent', yourDataIfYouHaveAny);
}
Your parent component:
<your-child #yourEvent="onYourEvent" />
...
onYourEvent(yourDataIfYouHaveAny) {
...
}
With script setup syntax, you can do:
<script setup>
const emit = defineEmits(['close', 'test'])
const handleClose = () => {
emit('close')
emit('test', { anything: 'yes' })
}
</script>
No need to import anything from 'vue'. defineEmits is included.
Read more here: https://learnvue.co/2020/01/4-vue3-composition-api-tips-you-should-know/
Due to the composition api, it allows you to use the $attrs inherited in each component to now fulfill this need.
I assume that you are using this.$parent.emit because you know the the child will always be part of the same parent. How do I simulate the above behavior with $attrs?
Lets say I have a table containing row components. However I wish to respond to row clicks in table's parent.
Table Definition
<template>
<row v-bind="$attrs" ></row>
</template>
Row Definition
<template name="row" :item="row" #click=onClick(row)>
Your Row
</template>
export default {
emits: {
row_clicked: () =>{
return true
}
},
onClick(rowData){
this.$emit('row_clicked',rowData)
}
}
Finally, a component containing your table definition, where you have a method to handle the click.
<table
#row_clicked=clicked()
>
</table
Your table component should effectively apply #row_clicked to the row component thus triggering when row emits the event.
There is similar way of doing it by using the context argument that is passed in second argument inside the child component (the one that will emit the event)
setup(props, context){
context.emit('myEventName')
}
...then emit it by calling the context.emit method within the setup method.
In your parent component you can listen to it using the handler like so:
<MyParentComponent #myEventName="handleMyEventName" />
Of course, in the setup method of the MyParentComponent component you can declare the handler like this
//within <script> tag of MyParentComponent
setup(props){
const handleMyEventName() => {
...
}
return { handleMyEventName }
}

How to add a watcher for context change?

I am working on adding an analytics tracker to my react app. I want to primarily capture 2 things:
1) All click events.
2) All page change events.
I was trying to figure out how to approach this problem and found some help on SO with this:
How can I create a wrapper component for entire app?
The above post basically had me creating a parent wrapper and using the React Context API to pass data to the nested elements. The idea is great, but I'm still missing a few pieces here after reading the context API.
Heres what I have following that pattern.
Tracker.js
import PropTypes from "prop-types"
import * as React from "react"
import { connect } from "react-redux"
import TrackingManager from './TrackingManager'
import ScriptManager from "./ScriptManager"
import { isLeftClickEvent } from "../utils/Utils"
const trackingManager = new TrackingManager()
export const TrackerProvider = React.createContext()
/**
* Tracking container which wraps the supplied Application component.
* #param Application
* #param beforeAction
* #param overrides
* #returns {object}
*/
class Tracker extends React.Component {
constructor(props) {
super(props)
this.state = {
pageName: ''
}
}
componentDidMount() {
this._addClickListener()
this._addSubmitListener()
}
componentWillUnmount() {
// prevent side effects by removing listeners upon unmount
this._removeClickListener()
this._removeSubmitListener()
}
componentDidUpdate() {
console.log('TRACKER UPDATE')
}
pageLoad = pageName => {
console.log('LOADING PAGE')
this.setState({ pagename }, trackingManager.page(this.state))
}
/**
* Add global event listener for click events.
*/
_addClickListener = () => document.body.addEventListener("click", this._handleClick)
/**
* Remove global event listern for click events.
*/
_removeClickListener = () => document.body.removeEventListener("click", this._handleClick)
/**
* Add global event listener for submit events.
*/
_addSubmitListener = () => document.body.addEventListener("submit", this._handleSubmit)
/**
* Remove global event listern for click events.
*/
_removeSubmitListener = () => document.body.removeEventListener("submit", this._handleSubmit)
_handleSubmit = event => {
console.log(event.target.name)
}
_handleClick = event => {
// ensure the mouse click is an event we're interested in processing,
// we have discussed limiting to external links which go outside the
// react application and forcing implementers to use redux actions for
// interal links, however the app is not implemented like that in
// places, eg: Used Search List. so we're not enforcing that restriction
if (!isLeftClickEvent(event)) {
return
}
// Track only events when triggered from a element that has
// the `analytics` data attribute.
if (event.target.dataset.analytics !== undefined) {
let analyticsTag = event.target.dataset.analytics
console.log("Analytics:", analyticsTag)
trackingManager.event("eventAction", {"eventName": analyticsTag, "pageName": "Something"})
}
}
/**
* Return tracking script.
*/
_renderTrackingScript() {
/**
* If utag is already loaded on the page we don't want to load it again
*/
if (window.utag !== undefined) return
/**
* Load utag script.
*/
return (
<ScriptManager
account={process.env.ANALYTICS_TAG_ACCOUNT}
profile={process.env.ANALYTICS_TAG_PROFILE}
environment={process.env.ANALYTICS_TAG_ENV}
/>
)
}
render() {
return (
<TrackerProvider.Provider value={
{
state: this.state,
loadPage: this.pageLoad
}
}>
{this.props.children}
{this._renderTrackingScript()}
</TrackerProvider.Provider>
)
}
}
export default Tracker
index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Router, Switch, Route } from 'react-router-dom'
import { Provider } from 'react-redux'
import store from './lib/store'
import history from './lib/history'
import MyComp from './containers/components/MyComp'
import Tracker from './lib/tracking/Tracker'
import './assets/stylesheets/bootstrap.scss'
import './bootstrap-ds.css'
import './index.css'
import './assets/stylesheets/scenes.scss'
ReactDOM.render((
<Tracker>
<Provider store={store}>
<Router history={history}>
<Switch>
<Route path={'/analytics'} component={MyComp} />
</Switch>
</Router>
</Provider>
</Tracker>
), document.getElementById('root'))
MyComp.js
import React from 'react
import { TrackerProvider } from '../../lib/tracking/Tracker
const MyComp = () => {
return (
<TrackerProvider.Consumer>
{context =>
<>
<div>This is my test page for track events for analytics</div>
<button data-analytics="TEST_BUTTON">Test Analytics</button>
</>
}
</TrackerProvider.Consumer>
)
}
export default MyComp
Here's what I'm struggling with a little bit:
1. When I load a nested child component that consumes the context, how do I notify the Parent (<Tracker />) to trigger some function? Similar to componentDidUpdate.
In essence a user navigates to the MyComp page and the pageLoad function is fired in the Tracker.2. How do I update the Context from MyComp without depending on some click event in the render method to run a funciton. So maybe in componentDidUpdate I can update the context.
I noticed you had connect from react-redux. Redux already provides its state to all the components in your app, so if you're already using Redux, you don't need to mess with the context API directly.
It's possible to create a higher-order component (a component that takes a component and returns a component) and attach event listeners to that capable of catching all the click events in your app.
A click disptaching HOC might look something like this:
import React from 'react';
import { useDispatch } from 'react-redux';
import logClick from '../path/to/log/clicks.js';
const ClickLogger = Component => (...props) => {
const dispatch = useDispatch();
return <div onClick={e => dispatch(logClick(e))}>
<Component {...props } />
</div>;
};
logClick will be a Redux action creator. Once you've got your log actions dispatching to Redux, you can use redux middleware to handle your log actions. If you want to hit a tracking pixel on a server or something, you could use redux-saga to trigger the logging effects.
If you want to track every page load, you can create a higher-order component which uses the useEffect hook with an empty array ([]) as the second argument. This will fire an effect on the first render, but no subsequent renders.

Vue event bus await mount

I have a Map component which initializes leaflet on the DOM like so:
Map.vue
<template>
<div ref="map"/>
<template>
<script>
import * as L from 'leaflet';
import mapEventBus from '../event-buses/map.vue';
export default {
mounted(){
const map = L.map(this.$refs.map);
mapEventBus.$on('add-marker',(newMarker) => {
newMarker.addTo(map);
});
}
}
</script>
And then I have another component which needs to add a marker that is built on the components creation.
OtherComponent.vue
<template>
<div/>
</template>
<script>
import mapEventBus from '../event-buses/map.vue';
export default {
created(){
mapEventBus.$emit('add-marker',L.marker([51.5, -0.09]));
}
}
</script>
Because the map is initialized after the OtherComponent has already tried emitting to the event bus, the event is never fired. What would be the best way to "await" for the map to be initialized and then add the marker to the map. I though about having a "cache" of pending markers that is added on the map creation but that seems clunky.
Example:
https://codesandbox.io/s/2ov71xnz3r
OK, so you've got a little chicken and egg problem there. You have an element you need to update via refs (some way to hack data into a 3rd party plugin), but you get the data BEFORE you mount the HTML.
What you need to do is separate out the immediate catch into a data variable, then on mount, check to see if it exists and if so update the HTML element.
I'm not answering your question above, because the problem is simplified in the codesandbox example you provided.
Here is the solution based on that:
https://codesandbox.io/s/3rnyp31n4p
<script>
import { EventBus } from '../eventBus.js'
export default {
data: () => ({
immediateMessage: null
}),
beforeCreate() {
EventBus.$on("immediate-message", message => {
this.immediateMessage = message;
});
},
mounted() {
if (this.immediateMessage) {
this.$refs.immediateMessageEl.innerHTML += this.immediateMessage;
}
EventBus.$on("delayed-message", message => {
this.$refs.delayedMessageEl.innerHTML += message;
});
}
};
</script>
Note, the beforeCreate() binds to the event and sets a variable, then we use that variable once the DOM is mounted.
Check out lifecycle hooks page for more info https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram
This is definitely not the most elegant solution, but will definitely get you going.

Categories

Resources