I'm in the process of converting my app from Vue2 to Vue3, but have gotten stalled out on one aspect of my forms.
I'm using SFCs for form element components (FormInput, FormTextArea, FormCheckbox, etc.), and passing them into form container components (FormGroup, FormTab, etc) using slots, like so:
<ssi-form>
<ssi-form-tabs>
<ssi-form-tab title="tab1">
<ssi-form-input title="name" ... />
<ssi-form-checkbox title="active" ... />
</ssi-form-tab>
</ssi-form-tabs>
</ssi-form>
Those parent containers need to view some computed properties of the child form elements to pull error messages to the top.
In Vue2, I used the mounted lifecycle hook (with the options API) to read the slots and access the computed properties, like this:
mounted: function() {
const vm = this;
this.$slots.default.forEach((vNode) => {
const vueComponent = vNode.componentInstance;
vueComponent.$on("invalid", vm.onInvalid);
if (vueComponent.errorCount === undefined) return;
this.childFormElements.push(vueComponent);
});
},
Using this setup, I could grab the errorCount
computed property from each child in the slot, so I could aggregate errors going up to the top level of the form.
Now that I'm switching to Vue3, it seems like componentInstance
doesn't exist. I tried setting up similar logic using the onMounted
directive, but when I access the slot elements, I can't find any way to see their errorCount
computed property:
onMounted(() => {
slots.default().forEach((vNode) => {
console.log(vNode);
});
});
The logged object does not contain the computed property. I thought I found something useful when I read about defineExpose
, but even after exposing the errorCount
property, nothing comes up.
Here is the <script setup>
from the SFC for the text input that I'm trying to work with:
<script setup lang="ts">
import { ref, defineProps, defineEmits, computed } from "vue";
let props = defineProps<{
label: string,
id: string,
modelValue: string|number,
type?: string,
description?: string,
min?: string|number,
max?: string|number,
pattern?: string,
message?: string
}>();
let emit = defineEmits(["input", "update:modelValue", "invalid"]);
let state = ref(null);
let error = ref("");
const input = ref(null);
function onInput(event: Event) {
validate();
emit('update:modelValue', event.target.value)
}
// methods
function validate() {
let inputText = input.value;
if (inputText === null) return;
inputText.checkValidity();
state.value = inputText.validity.valid;
error.value = inputText.validationMessage;
}
const errorCount = computed(() => {
return state.value === false ? 1 : 0;
});
defineExpose({errorCount})
</script>
So the question is - how can a parent component read the errorCount
property from a component placed into a slot?
CodePudding user response:
I believe this is not doable, since it has already been considered a bad practice since Vue 2.
defineExpose
does help in such a situation, but since you said
I could grab the
errorCount
computed property from each child in the slot
maybe Provide / Inject could be a better approach?
CodePudding user response:
Internal properties shouldn't be used without a good reason, especially because they don't belong to public API and can change their behaviour or be removed without notice. A preferable solution is the one that can achieve the goal by means of public API.
Here the solution is to process slots in container component's render function. Vnode objects are the templates for rendered elements and they can be transformed at this point, e.g. a ref from the scope of container component can be added.
If the instances of child components need to be accessed to add event listeners, they can be added to a vnode at this point:
() => {
const tabChildren = slots.default?.() || [];
for (childVnode of tabChildren) {
// Specifically check against a list of components that need special treatment
if (childVnode.type === SsiFormInputComponent) {
childVnode.props = {
...childVnode.props,
ref: setTabChildRef,
onInvalid: invalidHandler,
};
}
}
return tabChildren;
}
Where setTabChildRef
is ref function that maintains a collection of children refs, although it's not needed for the sole purpose of adding event listeners.