TL;DR: I'm using TypeSript. I want to call methods on a sub-component via it's ref from inside a hook (like onMounted, onBeforeUnmount, etc.). In order to do that I need to know which type to assign to the template ref. By default it's a VueProxy<>, wich doesn't help me.
Long version:
When I include sub-components in my component template I want to access them in a typesafe manner, specifically in order to call a method on the sub-component.
I know that the "official" way is to only communicate through props and events and that works fine. However, there are some use cases where the best way is to simply call a method on a sub component.
In my case, I have an editor wrapped in a sub-component. This editor is pretty complex and has it's own internal document model. The parent component is only interested in the serialized JSON string of the document in order to save it together with some metadata that's coming from other components. It should not have to care about the internal model of the editor. Since serializing the document is potentially expensive, I have debounced the input event on the editor so that the document is serialized and sent to the parent via a custom event once the user stops typing for a bit. However, that raises the potential for data loss should the user accidentally navigate away from the page before the debounced event is sent. So I want to create a hook that flushes the debounced events in the child components, like this:
<template>
<div>
<my-editor ref="myEditor" @input="save"/>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue-composition-api";
import MyEditor from "@/components/myeditor.vue";
export default defineComponent({
setup() {
const myEditor = ref<typeof MyEditor>(); // this is assigned correctly on mounting
onBeforeUnmount(() => {
if (myEditor.value) {
myEditor.value.flushEvents(); // this gives an error "Property 'flushEvents' does not exist on Type VueProxy<{...}>
}
});
return { myEditor };
}
});
</script>
So the main issue is the typing here. This all works great when the template ref is a normal HTMLElement. I can call all methods and access all properties in a typesafe way. Unfortunately I don't know what type to assign to the ref for a component. VueProxy doesn't seem to help much.
This works great for props. They are automatically typed correctly. It would be awesome to have this for refs as well.
I have this same problem in my navigation guards by the way, but that is likely a different issue.
Also: I know there are ways around this. If I just don't assign a type to the ref everything will work fine, but TypeScript will complain all the time, which breaks some build processes and dilutes the actually usefull errors.
This issue is not the related with the composition-api plugin.
This issue is related with the shims.d.ts, when you import a vue component, import MyEditor from "@/components/myeditor.vue"; Typescript doesn't know what actual type of it, it will import the type described in the shims.
usually you have something like:
//https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-plugin-typescript/generator/template/src/shims-vue.d.ts
declare module "*.vue" {
import Vue from "vue";
export default Vue;
}
If this shim is used, typeof MyEditor will be the same as typeof Vue, and therefor not having the flushEvents.
Unfortunately this is a limitation of the current tooling, the Vue team is working hard to improve this, and hopefully in the future this will be fixed.
Ok, that makes sense for the import. However, I could type cast it manually, right? I just need to know the correct type for my vue component. I also can manually type the ref, if I know the right type (as in const input = ref<HTMLInputElement>();)
Of course I can manhandle the compiler by writing my own interface like this:
interface flushable {
flushEvents(): void;
}
export default defineComponent({
setup() {
const myEditor = ref<flushable>();
onBeforeUnmount(() => {
myEditor.value!.flushEvents();
});
}
}
That feels really dirty though. For one, I have to manually maintain the interface signature, which leads to code duplication and potential errors should the actual component and the interface ever drift apart - which is bound to happen at some point. Basically I'm completely bypassing the type system here, leading to code that is less maintainable and harder to understand. It would be easier if I could somehow make my Component implement that interface and have the compiler check it, but my component definition is a function and not a class in the composition api so that doesn't work.
I guess what I'm saying is: I want a type that I can pass to ref<>() so that I can access my components directly. This would likely entail an extension / modification or something of VueProxy. Something that allows me to get at the thing inside of VueProxy. I can see it's in there, because the actual type reported by TypeScript (typeof MyComponent) is VueProxy<{flushEvents: () => void;}>. (of course there's more in the curly braces, I shortened it for this post).
@sseeland, I think the correct usage is
const myEditor = ref<InstanceType<typeof MyEditor>>()
myEditor.value?.flushEvents()
With the latest version v0.6.7, the code should work for you :)
Works like a charm!
The usage with the question mark operator does not work unfortunately but that might be a problem with my TypeScript setup. If I wrap the method call in an explicit if clause it works.
I really like how the composition API is taking shape, you're doing great work! Thank you very much for helping out!
Most helpful comment
@sseeland, I think the correct usage is
With the latest version v0.6.7, the code should work for you :)