How to send and receive data between two Vue roots? - vue.js

app.html
<script defer src='app.js'></script>
<script defer src='plugin.js'></script>
<div id='app'></div>
<div id='plugin'></div>
app.js
const store = new Vuex.Store({
state:{
hi:'hi'
}
})
const app = new.Vue({
el:'#app',
store
})
plugin.js
const plugin = new.Vue({
mounted(){
console.log('How can I get hi in store?')
}
})
I want to use Vuex to retrieve data from Vue instances loaded from different files. I have to use this method because the plugins are different for each page and they load dynamically.
But rootState doesn't seem to be able to get app's state, because the root is different. Is there a way to access the data using windows, mixins or some other global object or method?

If you want 'shared state' you can use localStorage, sure, but your question asks more about 'passing' data. In this case you can use an event emitter and listener. Since window has an Event Api you can do it like this:
// rootA - emitter
{
methods: {
emitDataToComponentB (a, b, c) {
const event = new CustomEvent('someEvent', { ...arguments })
window.dispatchEvent(event)
}
}
}
// rootB - listener
{
created () {
this.listen()
},
methods: {
listen () {
window.addEventListener('someEvent', (args) => {
console.log('#someEvent', args)
}
}
}
note: Only code required to meet the need shown. You'd want to make sure you detach any listeners at an appropriate time (eg rootB.beforeDestroy())

Related

How to `emit` event out of `setup` method in vue3?

I know I can call the emit method from the setup method, but is there any way to emit event from any other functions without passing the emit method from setup method(not the the functions in the methods option, but a useXXX function) ?
setup function takes two arguments, First one is props.
And the second one is context which exposes three component properties, attrs, slots and emit.
You can access emit from context like:
export default {
setup(props, context) {
context.emit('event');
},
};
or
export default {
setup(props, { emit }) {
emit('event');
},
};
Source
in vue3 typescript setup
<script setup lang="ts">
const emit = defineEmits()
emit('type', 'data')
<script>
20220626
<script setup lang="ts">
const emit = defineEmits(['emit_a', 'emit_b'])
emit('emit_a')
emit('emit_b', 'emit_b_data')
<script>
With Vue 3 setup syntax sugar
<script setup lang="ts">
import { defineEmits } from 'vue'
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
function yourFunction (id: number) {
emit('change', id)
}
<script>
See docs: https://v3.vuejs.org/api/sfc-script-setup.html#typescript-only-features
Here's the proper way to emit events programmatically (using javascript) in vue3:
export default defineComponent({
// See: https://vuejs.org/guide/components/events.html#declaring-emitted-events=
emits: 'myEventName', // <--- don't forget to declare custom events emitted
setup(_, { emit }) {
emit('myEventName') // <--- emit custom event programmatically whenever we want
},
})
The emits function can just as easily be passed as a param to any function not declared inside setup.
Side-note regarding other answers: we should avoid using getCurrentInstance(), which was intended for library authors needing access to internals of vue components (a.k.a. this of vue v2), when there are better alternatives. Especially when those alternatives were designed explicitly for our use case.
methods: {
minhaFuncao(){
let data = "conteudo";
this.$emit("nomeDoMEuEvento", data);
}
}
SEE MORE AT :https://github.com/Carlos-Alexandre-Leutz/emitir-eventos-filho-pra-pai-com-dados-no-vue3
export const useEmit = () => {
const vm = getCurrentInstance()
const emitFactory = (event: string) => (...args: any[]) => vm.emit(event, ...args)
return {
emit: vm.emit,
emitModel: emitFactory('update:modelValue')
}
}
const useButtonHandlers = () => {
const { emit } = useEmit()
const onClick = () => emit('click')
return {
onClick
}
}
You can use getCurrentInstance from Vue. You can check it out in the docs.
Usage is like
function useFunctionThatEmitsSomething(){
const instance = getCurrentInstance();
// do something
instance.emit('event');
}
Edit: Even though this answer solves the author's problem, as per the linked docs, this method is intended only for ADVANCED use cases, e.g authoring a plugin or library. For common use cases, like building a simple SPA, using this is TOTALLY DISCOURAGED and should be avoided at all costs, since it can lead to unreadable and unmaintenable code. If you feel the need to use this in a case like that, you're probably doing something wrong.

Is there a way to know when the replaceState function of vuex store is called?

I'm using vuex-presistedstate in my project. In the source code on github the plugin calls store.replaceState to hydrate store from storage. Is there a way to know when the store hydrates?
The vuex-presistedstate plugin has a configuration option called rehydrated that allows you to pass a function that will be called immediately after replaceState. If you only care about calls to replaceState from that plugin then that should fit your needs nicely.
I don't believe the store itself provides a 'hook' for when replaceState is called. The method replaceState is implemented here:
https://github.com/vuejs/vuex/blob/e0126533301febf66072f1865cf9a77778cf2176/src/store.js#L183
As you can see from the code it doesn't do much. Even subscribers registered using subscribe aren't called. However, you could potentially use watch to register a watcher on a specific property within the state and use that to detect when the state is replaced:
https://vuex.vuejs.org/api/#watch
Of course you'd need to be careful to structure things so that only the calls to replaceState trigger the watcher, which may get fiddly.
A further alternative would be to patch/override the replaceState method. Replace it with your own method that calls out to the original, giving you a hook point for any extra functionality you might need.
I've attempted to demonstrate all of the above in the example below:
// Can't use localStorage in an SO snippet...
const fakeStorage = {
vuex: `{"flag": {}, "number": ${Math.random()}}`
}
const storage = {
getItem (key) {
return fakeStorage[key]
},
setItem (key, value) {
fakeStorage[key] = value
},
removeItem (key) {
delete fakeStorage[key]
}
}
// Override replaceState with our own version
class StoreOverride extends Vuex.Store {
replaceState (...args) {
super.replaceState(...args)
console.log('replaceState called')
}
}
const store = new StoreOverride({
state: {
flag: {},
number: 0
},
plugins: [
createPersistedState({
rehydrated () {
console.log('rehydrated called')
},
storage
})
]
})
store.subscribe(() => {
console.log('store.subscribe called (will not happen for replaceState)')
})
store.watch(state => state.flag, () => {
console.log('store.watch called')
})
console.log('creating Vue instance')
new Vue({
el: '#app',
store,
methods: {
onReplace () {
// The property 'number' is changed so we can see something happen
this.$store.replaceState({
flag: {},
number: this.$store.state.number + 1
})
}
}
})
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<script>
Vue.config.devtools = false
Vue.config.productionTip = false
</script>
<script src="https://unpkg.com/vuex#3.1.1/dist/vuex.js"></script>
<script src="https://unpkg.com/vuex-persistedstate#2.7.0/dist/vuex-persistedstate.umd.js"></script>
<div id="app">
<button #click="onReplace">Replace</button>
<p>{{ $store.state.number }}</p>
</div>
You can try rewrite store.replaceState:
import store from './your/path/to/store'
// ...
store.replaceState = (fn => (...params) => {
console.log('do something before replaceState called')
fn.apply(store, params)
console.log('do something after replaceState called')
})(store.replaceState)
In this way, you can watch not only replaceState but any methods on store instance if you want.

How to add emit information to a component dynamically generated using Vue.extent()?

I'm dynamically generating instances of my child component "Action.vue" using Vue.extent() this way:
let ActionClass = Vue.extend(Action)
let ActionInstance = new ActionClass({
store
})
ActionInstance.$mount()
this.$refs.actions.appendChild(ActionInstance.$el)
This works fine. However, besides access to the store, child component also needs to emit an event (in response to user interaction with one of its elements) for the parent component to execute a method.
How to achieve this?
You can use instance.$on method to add eventListenersdynamically :
Consumer
import Dummy from "./Dummy.vue";
import Vue from "vue";
export default {
name: "HelloWorld",
props: {
msg: String
},
methods: {
mount: function() {
const DummyClass = Vue.extend(Dummy);
const store = { data: "some data" };
const instance = new DummyClass({ store });
instance.$mount();
instance.$on("dummyEvent", e => console.log("dummy get fired", e));
this.$refs.actions.appendChild(instance.$el);
}
}
};
Child component
export default {
methods: {
fire: function() {
console.log("fired");
this.$emit("dummyEvent", { data: "dummyData" });
}
}
};
Here is the Sandbox
https://v2.vuejs.org/v2/guide/components-custom-events.html
https://v2.vuejs.org/v2/api/#Options-Lifecycle-Hooks
You can use a lifecylce hook (for example: mounted) to emit the event when the child has been created.
you can listen to the events as documented in the documentation.
the store can be reached through this.$store.

Transfer Data From One Component to Another

I have a component which makes a call to my backend API. This then provides me with data that I use for the component. I now want to create another component which also uses that data. While I could just do another api call that seems wasteful.
So, in Profile.vue i have this in the created() function.
<script>
import axios from 'axios';
import { bus } from '../main';
export default {
name: 'Profile',
data() {
return {
loading: false,
error: null,
profileData: null,
getImageUrl: function(id) {
return `http://ddragon.leagueoflegends.com/cdn/9.16.1/img/profileicon/` + id + `.png`;
}
}
},
beforeCreate() {
//Add OR Remove classes and images etc..
},
async created() {
//Once page is loaded do this
this.loading = true;
try {
const response = await axios.get(`/api/profile/${this.$route.params.platform}/${this.$route.params.name}`);
this.profileData = response.data;
this.loading = false;
bus.$emit('profileData', this.profileData)
} catch (error) {
this.loading = false;
this.error = error.response.data.message;
}
}
};
</script>
I then have another child component that I've hooked up using the Vue router, this is to display further information.
MatchHistory compontent
<template>
<section>
<h1>{{profileDatas.profileDatas}}</h1>
</section>
</template>
<script>
import { bus } from '../main';
export default {
name: 'MatchHistory',
data() {
return {
profileDatas: null
}
},
beforeCreate() {
//Add OR Remove classes and images etc..
},
async created() {
bus.$on('profileData', obj => {
this.profileDatas = obj;
});
}
};
</script>
So, I want to take the info and display the data that I have transferred across.
My assumption is based on the fact that these components are defined for two separate routes and an event bus may not work for your situation based on the design of your application. There are several ways to solve this. Two of them listed below.
Vuex (for Vue state management)
Any local storage option - LocalStorage/SessionStorage/IndexDB e.t.c
for more information on VueX, visit https://vuex.vuejs.org/.
for more information on Localstorage, visit https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage.
for more information on session storage, visit https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage
The flow is pretty much the same for any of the options.
Get your data from an API using axios as you did above in Profile.vue
Store the retrieved data with VueX or Local/Session storage
Retrieve the data from Vuex or local/session storage in the created method of MatchHistory.vue component
For the local / session storage options, you will have to convert your object to a json string as only strings can be stored in storage. see below.
in Profile.vue (created)
const response = await axios.get(........)
if(response){
localStorage.setItem('yourstoragekey', JSON.stringify(response));
}
In MatchHistory.Vue (created)
async created() {
var profileData = localStorage.getItem('yourstoragekey')
if(profileData){
profileData = JSON.parse(profileData );
this.profileData = profileData
}
}
You can use vm.$emit to create an Eventbus
// split instance
const EventBus = new Vue({})
class IApp extends Vue {}
IApp.mixin({
beforeCreate: function(){
this.EventBus = EventBus
}
})
const App = new IApp({
created(){
this.EventBus.$on('from-mounted', console.log)
},
mounted(){
this.EventBus.$emit('from-mounted', 'Its a me! Mounted')
}
}).$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
further readings
You can make use of the VUEX which is a state management system for Vue.
When you make api call and get the data you need, you can COMMIT a MUTATION and pass your data to it. What it will do, it will update your STATE and all of your components will have access to its state (data)
In your async created(), when you get response, just commit mutation to your store in order to update the state. (omitted example here as the vuex store will need configuration before it can perform mutations)
Then in your child component,
data(){
return {
profileDatas: null
}
},
async created() {
this.profileDatas = $store.state.myData;
}
It might seem like an overkill in your case, but this approach is highly beneficial when working with external data that needs to be shared across multiple components

Vue sharing state between sibling components

I probably do not want to use vuex for state management yet as it is probably overkill for now.
I took a look at https://v2.vuejs.org/v2/guide/components.html#Non-Parent-Child-Communication. I am using single file component so I am not sure where do I define the shared bus such that both components will have reference to it.
var bus = new Vue()
ChildA.Vue
watch: {
sideNav: (newValue) => {
bus.$emit('sideNav-changed', newValue)
}
}
ChildB.Vue
created () {
bus.$on('sideNav-changed', data => {
console.log(`received new value: ${data}`)
// update own copy here
})
}
Parent.Vue
<template>
<childA>
</childA>
<childB>
</childB>
</template>
I want to watch any changes of sideNav on ChildA and emit it to ChildB to update its own value.
Found the answer to it...
I declare it on the main.js
const bus = new Vue() // Single event hub
// Distribute to components using global mixin
Vue.mixin({
data: function () {
return {
bus : bus
}
}
})
And also change
watch: {
sideNav: (newValue) => {
bus.$emit('sideNav-changed', newValue)
}
}
to
watch: {
sideNav: function (newValue) {
bus.$emit('sideNav-changed', newValue)
}
}
Is this answer any good to you? You can do everything with events, but if you can avoid them, you should. You might not want vuex for now. That's where I am. But you want, right from the start, a store in global scope and reactive pipes. You "declare" the relationship between an element on the page and an item in the store, then basta. Vue takes care of the rest. You don't care about events.
The simplest way to do this would be to just attach it to the window i.e.
window.bus = new Vue()
Then it will be available in all of your components without the need to define a global mixin e.g. this will still work:
watch: {
sideNav(newValue) {
bus.$emit('sideNav-changed', newValue)
}
}