0
votes

In our project we have a list components, which renders a list of a group of items, one of which can be edited at a time. It uses slots to get what to render for each item. Simplified, it looks a little like this:

<template>
  <div>
    <div v-for="item in items">
      <slot name="display" :item="item" v-if="!editing(item)"></slot>
      <slot name="edit" :item="item" v-else></slot>
    </div>
    <button @click="clickDone">Done</button>
  </div>
<template>
<script>
export default {
  props: ["items"],
  methods: {
    clickDone() {
      ...
    }
  }
}

This is used in another component that needs to display an editable list of complex of objects. It looks a little like this:

<EditableList :items="listOfComplexObjects">
  <template #edit="item">
    <component :is="item.componentType" v-model="item" />
  </template>
</EditableList>

There is then a number of different components that could be rendered for each item in the list, depending on what type it is.

In the first code snippet you can see the 'Done' button. This button is what the list uses to turn an 'editable' item into a 'display' item. The problem we've run into is we'd like to invoke validation on the click of this button. However the validation is going to be different for each item type component. Also the validation needs to mark certain fields in the item type component as invalid and show error messages on them. So this validation makes most sense to live within the item type components. So we are trying to consider how to the 'Done' button to call a validation method on a component within the slot.

What we've considered:

  1. by using a ref on the 'component' we can use something like this.$parent.refs.refName.validate to invoke the validator from within the editable list. The problem with this is it breaks encapsulation between the editable list, the parent component and the list item components.

  2. moving the 'done' button into the list item components. Clicking the done button will invoke local validation and then emit an event which the parent component will pass on to the editable list to manipulate it's internal state. This solution is our current best but it feels like it adds unnecessary boiler plate code to the non-editable list components.

What we wish we could do:

  • have each list item component implement a validate method that the parent component passes into the editable list as a kind of 'slot' or prop specific to each item. Then the editable list can invoke that validation method on the click of done and the list item component can update its state with error messages.

How can the above most easily and correctly be achieved in Vue.js?

2
Do you want to be able to cancel the transition form editing to display if the validation fails? - János Veres
@Janos Yes, I want a condition like 'if (validation) { ... }" where ... is the code to change from 'edit' to 'display' mode - Caleb

2 Answers

0
votes

I hope you use are using something like VeeValidate, because that could potentially solve this relatively easy. With that in place, you can inject the validator in the dynamic component and validation on the whole form. What was not yet clear to me if you want to validate Form data which is next to your , but that is what your this.$parent.refs... idea suggests.

So look into VeeValidate injections and then you can handle it like this:

<script>
  export default {
    inject: [„$validator“],
    props: ["items"],
    methods: {
      clickDone() {
        this.$validator.validateAll().then((result) => {
          if (!result) {
            // todo do optional ui feedback — field error classes will be set automatically
            return false
          }
           ...do editing transition...
        })
      }
    }
}

It essentially merges the two validation scopes (parent and child), however introduces a tight coupling.

0
votes

We've finally found a nice way to solve this problem. In case it's helpful to anyone else, this is the way to do it.

We maintains a map of component references, and each time a new component is added, this map is updated:

<component :is="item.componentType" v-model="item" :ref="item.componentType" />
data: function() {
  return {
    componentMap: {}
  };
},

methods: {
  onNewComponent(componentType) {
    this.$nextTick(function() {
      const componentRefs = this.$refs[componentType];

      if (Array.isArray(componentRefs)) {
        this.$set(this.componentMap, componentType componentRefs);
      } else {
        this.$set(this.componentMap, componentType, [componentRefs]);
      }
    });
  }
}

Then we can make calls on those component references. For example:

for (let componentReference of this.componentMap[componentType ]) {
  valid = componentReference.validate() && valid;
}

The key to making this work, which we didn't realise at the beginning, is the use of this.$nextTick. It appears without this, when we try to get the reference it is undefined, but by using this we are able to get access to the refs and save them for use later on.