-
Notifications
You must be signed in to change notification settings - Fork 141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Question] Support for typed custom Directives #554
Comments
After posting I realize that this is probably less of a question, and more of a feature request, let me know what you think guys, thank you! |
@Maxim-Mazurok, this thing works but looks a bit messy and I'm still looking for a cleaner solution. type VClickOutsideDirective = {
'v-click-outside'?: Function;
};
declare module 'vue' {
export interface HTMLAttributes extends VClickOutsideDirective {}
}
import { defineCustomElement } from "vue";
import ClickOutside from "click-outside-vue3";
export default defineCustomElement({
name: "Hello",
directives: {
clickOutside: ClickOutside.directive,
},
render() {
return <div v-click-outside={() => console.log("clicked outside")}></div>;
},
}); |
Yeah, that's not ideal because TS will think that this directive can be used in all other components as well, even though they don't import this directive. But thanks for sharing, this is definitely an option! |
@Maxim-Mazurok since the option above was not as good, I found a new type-safe solution. (ignore const focus = {
mounted(el: any) {
el.focus();
},
};
...
import { withDirectives, openBlock } from 'vue';
...
<>
{withDirectives(
// `openBlock` function must be called within `withDirectives` before the actual node to which the directives will be apply
(openBlock(),
(
<input
class="input"
type="text"
/>
)),
[[focus]]
)}
</> Also do not forget to register directive in component. SomeComponent.directives = {
focus
}; |
Oh wow, kudos for finding a type-safe solution, but I'm afraid it's not human-safe :D It took me a while to understand what's going on there. By the way, I think that it's still not completely type-safe. Now we know that directive exists, but we still don't know what kind of arguments it expects, I think that if I tried to use mine, like so: |
It's type-safe, depends on what angle you look at. 🙃 From from detective perspective we can make it strong typed, but from component view, yes... we have some problems with |
@Maxim-Mazurok, it took me a long time, and I tried my best. Util types: /**
* @see https://docs.microsoft.com/en-us/javascript/api/@azure/keyvault-certificates/requireatleastone?view=azure-node-latest
*/
export type RequireAtLeastOne<T> = {
[K in keyof T]-?: Required<Pick<T, K>> &
Partial<Pick<T, Exclude<keyof T, K>>>;
}[keyof T]; Util types related to export type DirectiveModifier<M extends string> = Partial<
Record<M, true | undefined>
>;
type DirectiveRegisterFunction = () => Record<string, Directive>;
type DirectiveUseFunction<V = any, A = string, M extends any = string> = (
directiveArgument?: Partial<{
value: V;
arg: A;
// @ts-expect-error <-- I gave up here ;(
modifiers: M extends void ? void : DirectiveModifier<M>;
}>
) => DirectiveArguments;
export interface _DirectiveBinding<
V = any,
A = string,
M extends string = string,
D = any
> {
instance: ComponentPublicInstance | null;
value: V;
oldValue: V | null;
arg?: A;
modifiers: DirectiveModifier<M>;
dir: ObjectDirective<D, V>;
}
export type DirectiveOption<
DirectiveElement extends HTMLElement,
DirectiveValue = any,
DirectiveArg = string,
DirectiveModifier extends string = string
> = {
created?: (
el: DirectiveElement,
binding: _DirectiveBinding<DirectiveValue, DirectiveArg, DirectiveModifier>,
vnode: VNode<any, DirectiveElement>,
prevVNode: null
) => void;
beforeMount?: (
el: DirectiveElement,
binding: _DirectiveBinding<DirectiveValue, DirectiveArg, DirectiveModifier>,
vnode: VNode<any, DirectiveElement>,
prevVNode: null
) => void;
mounted?: (
el: DirectiveElement,
binding: _DirectiveBinding<DirectiveValue, DirectiveArg, DirectiveModifier>,
vnode: VNode<any, DirectiveElement>,
prevVNode: null
) => void;
beforeUpdate?: (
el: DirectiveElement,
binding: _DirectiveBinding<DirectiveValue, DirectiveArg, DirectiveModifier>,
vnode: VNode<any, DirectiveElement>,
prevVNode: VNode<any, DirectiveElement>
) => void;
updated?: (
el: DirectiveElement,
binding: _DirectiveBinding<DirectiveValue, DirectiveArg, DirectiveModifier>,
vnode: VNode<any, DirectiveElement>,
prevVNode: VNode<any, DirectiveElement>
) => void;
beforeUnmount?: (
el: DirectiveElement,
binding: _DirectiveBinding<DirectiveValue, DirectiveArg, DirectiveModifier>,
vnode: VNode<any, DirectiveElement>,
prevVNode: null
) => void;
unmounted?: (
el: DirectiveElement,
binding: _DirectiveBinding<DirectiveValue, DirectiveArg, DirectiveModifier>,
vnode: VNode<any, DirectiveElement>,
prevVNode: null
) => void;
getSSRProps?: (
binding: _DirectiveBinding<DirectiveValue, DirectiveArg, DirectiveModifier>,
vnode: VNode
) => Data | undefined;
deep?: boolean;
}; Custom function that allows to register and use directive: /**
* @see https://vuejs.org/guide/reusability/custom-directives.html#introduction
*
* @example
* type VFontDirectiveModifier = 'normal' | 'italic' | 'oblique';
*
* export const vFontDirective = createDirective<
* HTMLElement,
* number,
* void,
* VFontDirectiveModifier
* >('font', {
* beforeMount(el, binding) {
* if (binding.modifiers.normal === true) {
* el.style.fontStyle = 'normal';
* } else if (binding.modifiers.italic === true) {
* el.style.fontStyle = 'italic';
* } else if (binding.modifiers.oblique === true) {
* el.style.fontStyle = 'oblique';
* }
*
* el.style.fontSize = `${binding.value}px`;
* },
*
* updated(el, binding) {
* el.style.fontSize = `${binding.value}px`;
* },
* });
*
* ...
*
* SomeComponent.directives = {
* ...vFontDirective.register()
* };
*
* ...
* {withDirectives(<h1>Heading</h1>, [
* vFontDirective.use({
* value: 40,
* modifiers: {
* italic: true
* }
* })
* ])}
* ...
*/
export const createDirective = <
DirectiveElement extends HTMLElement = HTMLElement,
DirectiveValue = any,
DirectiveArg = string,
DirectiveModifier extends any = string
>(
name: Readonly<string>,
directiveObject: RequireAtLeastOne<
DirectiveOption<
DirectiveElement,
DirectiveValue,
DirectiveArg,
// @ts-expect-error <-- And here too ;(
DirectiveModifier
>
>
) => {
const register: DirectiveRegisterFunction = () =>
({
[name]: directiveObject
} as any);
const use: DirectiveUseFunction<
DirectiveValue,
DirectiveArg,
DirectiveModifier
> = (directiveArgument) =>
[
directiveObject,
directiveArgument?.value,
directiveArgument?.arg,
directiveArgument?.modifiers
] as any[];
return {
register,
use
};
}; |
That looks pretty promising, good job 👍 |
Here is an update without |
🧐 Problem Description
As mentioned here, I'd like to have type safety for custom directives when using Vue 3 and JSX + Typescript (TSX).
💻 Sample code
🚑 Other information
It works, but gives an annoying type error, saying:
The current workaround that I applied is to use
@ts-ignore
(not thatvite
won't build it otherwise, but just to make the red line go away in VS Code), I didn't have time to figure out which interface I have to augment, potentially even patching node_modules using patch-packageThe text was updated successfully, but these errors were encountered: