简体   繁体   中英

VueJS 3 How to trigger methods between two non-parent/child components

I have two components that aren't children of each other and I simply need to trigger a method/function in the 1st component via a function from the 2nd component.

First component:

// Component - Product Select
  app.component('product-select', {
    data() {
      return {
        options: null
      }
    },
    props: {
      modelValue: Array,
    },
    emits: ['update:modelValue'],
    template: `
      <div class="ui fluid labeled multiple search selection large dropdown">
        <input type="hidden"
               name="products"
               :value="modelValue"
               @change="selectProducts">
        <i class="dropdown icon"></i>
        <div class="default text">
          Select Products
        </div>

        <div class="menu">
          <div v-for="(option, index) in options"
               class="item"
               v-bind:data-value="option.name">
            {{ option.name }}
          </div>
        </div>
      </div>
    `,
    methods: {
      // Add the product selections to the modelValue array
      selectProducts(event) {
        let value = event.target.value.split(',');
        this.$emit('update:modelValue', value);

        // Properly empty out the modelValue array if no products are selected
        if (/^(""|''|)$/.test(value) || value == null) {
          this.$emit('update:modelValue', []);
        }
      },
      updateComponent() {
        console.log('Component Updated');
      }
    },
    created: function () {
      // Build the products array via the products JSON file
      fetch(productsJSON)
        .then(response => response.json())
        .then(options => {
          this.options = options;
        });
    }
  });

Second component:

// Component - Product Card
  app.component('product-card', {
    data() {
      return {
        selectedSymptoms: []
      }
    },
    props: {
      modelValue: Array,
      product: String
    },
    template: `
      <div class="ui fluid raised card">
        <symptoms-modal v-bind:name="product + ' - Modal'"
                        v-bind:title="product + ' - Symptoms'"
                        v-on:child-method="updateParent">
        </symptoms-modal>
        <div class="content">
          <div class="header" @click="removeProduct(product)">
            {{ product }}
          </div>
        </div>
        <div v-if="selectedSymptoms.length === 0"
             class="content">
          <div class="description">
            No symptoms selected
          </div>
        </div>
        <div v-for="(symptom, index) in selectedSymptoms"
             class="content symptom">
          <h4 class="ui left floated header">
            {{ symptom }}
            <div class="sub header">
              Rate your {{ symptom }}
            </div>
          </h4>
          <button class="ui right floated icon button"
                  v-bind:name="symptom + ' - Remove'"
                  @click="removeSymptom(symptom)">
            <i class="close icon"></i>
          </button>
          <div class="clear"></div>
          <input type="text"
                 class="js-range-slider"
                 v-bind:name="product + ' - ' + symptom"
                 value=""
          />
        </div>
        <div class="ui bottom attached large button"
             @click="openModal">
          <i class="add icon"></i>
          Add/Remove Symptoms
        </div>
      </div>
    `,
    methods: {
      openModal() {
        // Gets the product name
        // Product Name
        let product = this.product;

        // Builds the modal name
        // Product Name - Modal
        let modal = product + ' - Modal';

        // Gets the modal element
        // name="Product Name - Modal"
        let target = $('[name="' + modal + '"]');

        // Assign the currently selected symptoms to a targettable array
        let array = this.selectedSymptoms;

        // Opens the appropriate modal
        $(target).modal({
          closable: false,
          // Updates all checkboxes when the modal appears if the user
          // removes a symptom from the main screen
          onShow: function () {
            // For each input
            $('input', $(target)).each(function () {
              // If it is checked
              if ($(this).is(':checked')) {
                // If it is a currently selected symptom
                if (jQuery.inArray(this.name, array) != -1) {
                  // Is checked and in array, re-check
                  $(this).prop('checked', true);
                } else {
                  // Is checked and not in array, un-check
                  $(this).prop('checked', false);
                }
              } else {
                if (jQuery.inArray(this.name, array) != -1) {
                  // Is not checked and in array, re-check
                  $(this).prop('checked', true);
                } else {
                  // Is not checked and not in array, do nothing
                }
              }
            });
          },
        }).modal('show');
      },
      updateParent(value_from_child) {
        // Update the symptoms values from the modal
        this.selectedSymptoms = value_from_child;
      },
      removeSymptom(symptom) {
        this.selectedSymptoms.splice($.inArray(symptom, this.selectedSymptoms), 1);
      },
      removeProduct(product) {
        this.$root.selectedProducts.splice($.inArray(product, this.$root.selectedProducts), 1);
      }
    },
    updated() {
      // Add custom range input functionality
      $(".js-range-slider").ionRangeSlider({
        skin: "round",
        grid: false,
        min: 1,
        max: 5,
        from: 2,
        step: 1,
        hide_min_max: true,
        values: [
          "1 - Adverse Reaction", "2 - No Change", "3 - Partial Resolution", "4 - Significant Resolution", "5 - Total Resolution"
        ],
        onChange: function (data) {
          // Name Attribute
          // data.input[0].attributes.name.nodeValue

          // Input Value
          // data.from_value
        }
      });
    }
  });

In the first component, I have the function updateComponent() which is the one I need to trigger. In the second component, I have the function removeProduct() which is what needs to trigger the updateComponent() function.

I've tried using $refs and it didn't work at all, and from my understanding emitting events only works for child > parent components.

There are several ways to do it, and the implementation depends on you constraints.

First of all, vue3 no longer supports an event bus the way Vue2 did. That means that the event listening outside of what the components bubble up is not a library feature any more. Instead they recommend a 3rd party option

Vue docs link

In Vue 3, it is no longer possible to use these APIs to listen to a component's own emitted events from within a component, there is no migration path for that use case.

But the eventHub pattern can be replaced by using an external library implementing the event emitter interface, for example mitt or tiny-emitter .

The bottom line is that it narrows your choice of three "strategies"

  • use 3rd party pub/sub library
  • pass the function itself
  • use a watch (or some part of reactivity) to execute a function.

The other part of the equation is the delivery method.

  • global/singleton
  • prototype/config
  • provide/inject

Global/Singleton

You can setup a file that will hold a singleton reference, which will allow any of the strategies.

create a pubsub instance, then you can listen and emit from anywhere

// pubsub.js
import mitt from 'mitt'
export const emitter = mitt();

Or if you want to just pass the function, you can wrap it, essentially creating a function that holds the instruction to execute another function.

// singleton.js
let updateComponentProxy = () => {};
export const setUpdateComponentProxy = (callback)=>updateComponentProxy=callback;
// component1
created(){
  setUpdateComponentProxy(()=>{this.updateComponent();})
}

// component2
// ..on some trigger
updateComponentProxy()

It's a very ugly implementation, but it works, and maybe in some instances that's appropriate

The 3rd option is using the reactivity. You can do this either by using vuex or a diy super paired-down version of it.

// mystore.js
import {ref} from 'vue';
export const updateComponentCount = ref(0);
export const pushUpdateComponentCount = () => updateComponentCount.value++;
// component1
import {watch} from 'vue';
import {updateComponentCount} from 'mystore.js';
created(){
  watch(updateComponentCount, this.updateComponent}
}

// component2
import {updateComponentCount} from 'mystore.js';
// ..on some trigger
pushUpdateComponentCount();

This will execute the updateComponent function when the value of updateComponentCount changes. You could do a similar thing with vuex , since it wouldnt (usually) be setup to run a function in the component, but provide some variable on the store that would trigger a change that you'd listen to. Also this example uses a counter, but you could even toggle that between true and false because it is not the value that's important it's the mutation.

Provide/Inject

If you are trying to pass information between non-direct child and parent, but part of the same "ancestry", the provide/inject feature. 在此处输入图像描述

This is meant to as a way to pass props without having to hand them along from parent to child, by just having it accessible to any child. You can then use with whichever strategy. There is however a caveat, which is that if you assign it to the root component, it is available to all components, which lends itself to some strategies better than others.

Config Object

If you assign something to a key on the app.config.globalProperties object, (where app is the root component instance) you can have that accessible from any of the child components. For example

import mitt from 'mitt';
const app = createApp(App)
app.config.globalProperties.emitter = mitt();

becomes accessible by

// component 1
created(){
  this.emitter.on('removeProduct', this.updateComponent())
}

// component 2

      removeProduct(product) {
        this.$root.selectedProducts.splice($.inArray(product, this.$root.selectedProducts), 1);
        this.emitter.emit('removeProduct')
      }

If you wanted to use in a vue3 setup() function though, you will need to access it using getCurrentInstance().appContext.config.globalProperties.emitter , since the component instance is not in the context.

This is a common challenge in Vue, since the framework focuses on parent-to-child and child-to-parent data flows. You basically have two choices:

  1. Have ProductSelect $emit events up to a common parent, then pass props down to ProductCard.

  2. Create and import a global event bus.

You might also be interested in Vue's own framework solution:

  1. Use Vuex to implement a central data source.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM