Transferring data between routed components


#1

Hi,
this is mock of my problem, which I’m not able to solve:

  • General parent component
<template>
 <input-component
   :updateValue.sync="updateValue"
>
 </input-component>
</template>
<script>
export default {
  data() {
     return {
       updateValue: 0,
     };
  }
}
</script>
  • Definition of InputComponent
<template>
  <button @click="openGrid"></button>
</template>

<script>
export default {
 props: {
   updateValue: {},
 },
 methods: {
   openGrid() {
	this.$router.push({ 
           name: 'GridComponent', 
           params: { dataMapper: this.dataMapper }
    },

   dataMapper(data) {
      this.$emit('update:updateValue', data.updateValue);
   }
 }
}
</script>
  • Definition of GridComponent
<template>
 <button @click="afterUserClick"></button>
</template>

<script>
export default {
 props: {
  dataMapper: Function,
 },
 methods: {
  afterUserClick() {
    let data = { updateValue: 10};
    dataMapper(data);
  }
 } 
}
</script>

The problem is, that I am not able transfer selected data from GridComponent back to General parent component through InputComponent.

The result is, that event update:updateValue is not catched and updateValue remains 0.

My assumption is, that General parent component is in destroyed state. Could you please suggest correct solution, how this example should be done.

Thanks

PS. This example is not copy-pasted from my solution, however it is simplified version, but the problem of sharing data between components remains.


#2

I found your code quite tricky to reason about. Passing functions as route parameters which are intended to $emit from another component is confusing. You should really look to state management to pass data between components.

I actually tried a few times to spot the source of your issue, and while I see some issues, it’s not clear to me the hierarchy of your components. If you are loading GridComponent into the route, how can you $emit to its parent?? I don’t see any router-view in the parent component.


#3

The reason, why I have to pass arguments to GridComponent as callback, is because our application InputComponent takes props with callback function, dataMapper function is let’s call it default behaviour.

Probably next example should make use case of application clearer.

More precisly InputComponent looks like this:

<template>
   <input
      :value.sync="id"
      class="input-with-select"
      :disabled="true"
    ></input>
    <input
      :value.sync="text"
      class="input-with-select"
      :disabled="true"
    ></input>
    <button @click="openGrid"></button>
    <button @click="clear"></button> 
</template>

<script>
export default {
	name: 'InputComponent',

	props: {
		dataMapper: Function,

		id: {},
		text: {},
	},

	computed: {
		dataMapperHandler() {
			return this.dataMapper || this.defaultDataMapper;
		},
	},

	methods: {
		openGrid() {
			this.$router.push({
				name: 'GridComponent',
				params: {
					afterRowSelection: this.dataMapperHandler,
				},
			});
		},

		defaultDataMapper(data) {
			this.$emit('update:id', data.id);
			this.$emit('update:text', data.text);
		},

		clear() {
			const data = { id: null, text: null };
			this.dataMapperHandler(data);
		},
	},
};
</script>

More precisly General parent component

<template>
 <input-component
   :id.sync="FirstId"
   :text.sync="FirstText"
>
 </input-component>
 <input-component
   :dataMapper="dataMapper"
   :id.sync="SecondId"
   :text.sync="SecondText"
>
 </input-component>
</template>
<script>
export default {
  name: 'ParentComponent',
  computed: {
   ...mapFields(["FirstId", "FirstText", "SecondId", "SecondText" ])
  },
 methods: {
   dataMapper(data) {
       SecondId = data.id;
       SecondText = data.text || "DefaultText";
   }
 }
}
</script>

More precisly GridComponent

<template>
 <button @click="afterUserClick"></button>
</template>

<script>
export default {
 props: {
  dataMapper: Function,
 },
 methods: {
  afterUserClick() {
    let data = { id: 1, text: "" };
    dataMapper(data);
    this.$router.back();
  }
 } 
}
</script>

The component are register similar like this:

const router = new Router({
	routes: [
		{
			path: '/',
			name: 'home',
			component: Home,
		},
		{
			path: '/Parent',
			name: 'parent',
			component: ParentComponent,
         },
         {
			path: '/Grid',
			name: 'grid',
			component: GridComponent,
         }]

Basically, what I want to achieve is to enable user to select data from another grid table component and those data then are loaded back to component:

  1. User clicks on button
  2. New GridTable shows up
  3. User clicks the row’s button within there are data he wants to import
  4. Data are transfered back to starting component

I hope I made it a little more understandable.


#4

I still find your code difficult to reason about. And I still would recommend using Vuex. Something like this is very simple and the logic becomes clear when using state management.

That said, here are a few things you need to address:

  • If you intend to use route params as props, you need to set a props: true flag on your route. https://router.vuejs.org/guide/essentials/passing-props.html#boolean-mode

  • When referencing anything within the components context you need to use this. dataMapper(data) should be this.dataMapper(data). The same goes with this.SecondId and this.SecondText

  • I believe the context is passed with your defaultDataMapper method so that when it’s passed as a param rather than emitting from GridComponent it will still emit from InputComponent. Not sure if this is intended.


#5

If you intend to use route params as props, you need to set a props: true flag on your route. https://router.vuejs.org/guide/essentials/passing-props.html#boolean-mode

In production code this is set to true

When referencing anything within the components context you need to use this . dataMapper(data) should be this.dataMapper(data) . The same goes with this.SecondId and this.SecondText

In production code it’s with this.

I believe the context is passed with your defaultDataMapper method so that when it’s passed as a param rather than emitting from GridComponent it will still emit from InputComponent. Not sure if this is intended.

Yeah, that’s true. I’ve always passed defaultDataMapper as param and it is true that event is emmited from InputComponent. However, there is no one to catch the event, because the GeneralParentComponent is in destroyed state, therefor I assumed, that all event handlers are unregistered.
Btw when I breakpoint the defaultDataMapper function I can see in this instance whole Vue component instance even with proper $parent element setup, just event handling does not work.

And I still would recommend using Vuex

Yes, tried that at first as well, but I didn’t find any feasible option how to design store. Therefor I hoped for some miracle event communication would work.

Thanks for getting this deep in problem, we appreciate it, our team’ve been stucked on this for a while.


#6

Why only in Production mode? This is a standard. These should be used no matter what the environment.

Hard to advise without knowing the whole application, but it should just be a matter of using a module or two. It could be a simple as just managing the users state. e.g. when user selects a row, add it and the data he imports to the users Vuex state. When user returns to the grid get the selected row and data from the store and apply it as needed. Probably over simplified, but the tools are definitely there to share state/data between components using Vuex.