Vue 3 access child component from slots

Viewed 7047

I am currently working on a custom validation and would like to, if possible, access a child components and call a method in there.

Form wrapper

<template>
    <form @submit.prevent="handleSubmit">
        <slot></slot>
    </form>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
    setup(props, { slots }) {
        const validate = (): boolean => {
            if (slots.default) {
                slots.default().forEach((vNode) => {
                    if (vNode.props && vNode.props.rules) {
                        if (vNode.component) {
                            vNode.component.emit('validate');
                        }
                    }
                });
            }

            return false;
        };

        const handleSubmit = (ev: any): void => {
            validate();
        };

        return {
            handleSubmit,
        };
    },
});
</script>

When I call slot.default() I get proper list of child components and can see their props. However, vNode.component is always null

My code is based from this example but it is for vue 2.

If someone can help me that would be great, or is this even possible to do.

2 Answers

In the link you provided, Linus mentions using $on and $off to do this. These have been removed in Vue 3, but you could use the recommended mitt library.

One way would be to dispatch a submit event to the child components and have them emit a validate event when they receive a submit. But maybe you don't have access to add this to the child components?

JSFiddle Example

<div id="app">
    <form-component>
        <one></one>
        <two></two>
        <three></three>
    </form-component>
</div>
const emitter = mitt();

const ChildComponent = {
    setup(props, { emit }) {
        emitter.on('submit', () => {  
            console.log('Child submit event handler!');

            if (props && props.rules) {
                emit('validate');
            }
        });
    },
};

function makeChild(name) {
    return {
        ...ChildComponent,
        template: `<input value="${name}" />`,
    };
}

const formComponent = {
    template: `
        <form @submit.prevent="handleSubmit">
            <slot></slot>
            <button type="submit">Submit</button>
        </form>
    `,

    setup() {
        const handleSubmit = () => emitter.emit('submit');
        return { handleSubmit };
    },
};

const app = Vue.createApp({
    components: {
        formComponent,
        one: makeChild('one'),
        two: makeChild('two'),
        three: makeChild('three'),
    }
});

app.mount('#app');

I found another solution, inspired by quasar framework.

  1. Form component provide() bind and unbind function.
    bind() push validate function to an array and store in Form component.
  2. Input component inject the bind and unbind function from parent Form component.
    run bind() with self validate() function and uid
  3. Form listen submit event from submit button.
    run through all those validate() array, if no problem then emit('submit')

Form Component

import {
  defineComponent,
  onBeforeUnmount,
  onMounted,
  reactive,
  toRefs,
  provide
} from "vue";
export default defineComponent({
  name: "Form",
  emits: ["submit"],
  setup(props, { emit }) {
    const state = reactive({
      validateComponents: []
    });
    provide("form", {
      bind,
      unbind
    });
    onMounted(() => {
      state.form.addEventListener("submit", onSubmit);
    });
    onBeforeUnmount(() => {
      state.form.removeEventListener("submit", onSubmit);
    });
    function bind(component) {
      state.validateComponents.push(component);
    }
    function unbind(uid) {
      const index = state.validateComponents.findIndex(c => c.uid === uid);
      if (index > -1) {
        state.validateComponents.splice(index, 1);
      }
    }
    function validate() {
      let valid = true;
      for (const component of state.validateComponents) {
        const result = component.validate();
        if (!result) {
          valid = false;
        }
      }
      return valid;
    }
    function onSubmit() {
      const valid = validate();
      if (valid) {
        emit("submit");
      }
    }
  }
});

Input Component

import { defineComponent } from "vue";
export default defineComponent({
  name: "Input",
  props: {
    rules: {
      default: () => [],
      type: Array
    },
    modelValue: {
      default: null,
      type: String
    }
  }
  setup(props) {
    const form = inject("form");
    const uid = getCurrentInstance().uid;
    onMounted(() => {
      form.bind({ validate, uid });
    });
    onBeforeUnmount(() => {
      form.unbind(uid);
    });
    function validate() {
      // validate logic here
      let result = true;
      props.rules.forEach(rule => {
        const value = rule(props.modelValue);
        if(!value) result = value;
      })
      return result;
    }
  }
});

Usage

<template>
  <form @submit="onSubmit">
    <!-- rules function -->
    <input :rules="[(v) => true]">
    <button label="submit form" type="submit">
  </form>
</template>
Related