Access to template refs with composition API
Hi,
I was playing around with Composition API and IMHO access to template refs is confusing, slightly complicated and might cause unexpected errors in the code.
1. Problem
Currently to use template refs I have to:
<!-- 1. Add ref="" attribute to the element -->
<input
type="text"
ref="input">
setup() {
const input = ref<HTMLElement>(null); // 2. Create empty ref with name matching the name of the ref in the template
function onValidationError() {
// Do some magic
input.value?.focus();
}
return {
input, // 3. Return ref from setup method
onValidationError,
};
}
Problems with this approach:
1.) Without type annotation this line isn't in any way different from regular refs used to store reactive data: const input = ref(null);.
2.) Name of the variable must match ref="" in the template. I personally like to add Element (eg. inputElement) to variable names to indicate that they are DOM elements.
3.) I have to look into both setup method and template to know that this is a reference to DOM element.
4.) When ref="" in the template is renamed, the name of the variable must also be updated.
5.) There might a case when someone tries to use ref with the same name as ref="" in the template and assign some value to it - Vue will keep overriding it to the DOM element.
2. Possible solution
In Vue 2 with @vue/composition-api plugin it's possible to access template refs from context like so (TypeScript will throw an error, because refs doesn't exists on SetupContext interface, however it works fine):
setup(props, { refs }) {
console.log(refs.input);
}
However the same property doesn't exist in Vue 3 (SetupContext interface).
I think that this approach is much better, but I'd like to know your opinion.
Your proposition makes sense to me.
Vue 3 still exposes $refs in views, so why not expose it inside SetupContext?
It would be more consistent.
I, too, think that the current design for using DOM refs with composition apis is not intuitive. It caused me a fair bit of frustration on my first try.
Related: vuejs/vue-next#660, I suggested that when using composition api template
refsshould be written to any property, even if it's not backed by aref(could be a reactive object, or even just a plain object if you don't care about reactivity). Didn't get much support.
I don't disagree on some points, but I am also unsure if one or the other is better.
I personally like the ref API as it is, but I'll admit it has felt a little awkward when explaining to others. The link between ref() and ref= feels more like a coincidence than a logical relationship.
That said, being able to populate reactive refs with DOM refs allows for more easily decoupling the logic around those refs. With refs on the setup context interface, you would have to pass that to composition functions that deal with refs, and I don't see any good way you could rename them. For example:
setup(_, { refs }) {
const { focus /* method */ } = useFocus(refs)
return { focus }
}
The problem I see with this example is that we have to look into the useFocus function to know what our ref must be named. And the only way I see to provide the ability to rename the ref is an extra parameter to that function to use as a dynamic key. When using ref as the API has it now, it's as simple as renaming the variable returned from useFocus.
Also, on a couple of the points you bring up, I'm not sure the alternative works any better:
- For "(2) Name of the variable must match ref="" in the template." The name of the key you are accessing off of the
refsobject in the alternative API also has to match the ref in the template. You don't get autocomplete, so you need to spell the key correctly, same as spelling the variable correctly. - "(4) When ref="" in the template is renamed, the name of the variable must also be updated." When you rename the ref in the template you also need to rename the key you are accessing off of the
refsobject.
Other thoughts:
- "(3) I have to look into both setup method and template to know that this is a reference to DOM element." Fair enough, but you could easily create a one line helper function in your code base that just aliases
refastemplateRef. That would make it clear. - "(5) There might be a case when someone tries to use ref with the same name as ref="" in the template and assign some value to it - Vue will keep overriding it to the DOM element." Also a fair point, but along the same line as (3), you could have a
templateRefhelper with a return type of readonly (it just wouldn't actually be readonly) so that folks will know not to touch it. - (1) is basically the point as (5), it seems. And if you have a
templateRefhelper, you could do other useful things like adding dev mode validation to ensure it actually gets populated, and with the correct kind of thing.
That said, being able to populate reactive refs with DOM refs allows for more easily decoupling the logic around those refs. With refs on the setup context interface, you would have to pass that to composition functions that deal with refs, and I don't see any good way you could rename them
Fair point. The same could be said about passing a prop to a function, which is probably a lot more common than passing a ref. I think the only way to do the currently is:
setup(props, { refs }) {
// Example with refs, props works in exactly the same way
const { el1, el2: renamed } = toRefs(refs);
useFocus(renamed);
}
I think passing DOM elements to mixins should be an unusual case. The "better" way to compose stuff that manipulates DOM is to encapsulate the behavior in either a directive or a component.
You example useFocus screams v-focus to me (I know, it's just an example. Not saying there are no good use cases, but saying they shouldn't be common).
It seems to me that using the ref inside the component itself is the main use case and I wouldn't mind doing refs.element.
One thing that annoys me a lot is that there would be no strong typing, unlike props. Unless you declare refs in some way, the best you could do is refs: Record<string, Element>. Which falls short of validating el2 is the right name and fails to provide a stronger type for it (e.g. HTMLInputElement).
And that's not even correct since refs can also be arrays (when used in v-for).
(Can Vetur help?)
Typing is a big drawback :(
The newly introduced useTemplateRef() solves this issue.