Atmos4 / van-element

Simple function to create VanJS custom elements
MIT License
29 stars 1 forks source link

Type-safe usage #1

Open lorefnon opened 8 months ago

lorefnon commented 8 months ago

Hello, thanks for creating & open-sourcing this project. I have been exploring vanjs and this library looks like a very useful companion.

I have a query regarding type-safe usage of elements defined through van-elements. I am trying to use web-component defined through van-elements within a van-js app (for style-isolation, lifecycle hooks etc.).

In this case is it possible to get type-safety for attributes/properties ?

It is relatively easy to create a wrapper component that renders the web component but that is something I'd need to do for each component I define. So was wondering if a better/easier solution was possible.

Atmos4 commented 8 months ago

Hello! Thank you for taking a look at my project.

Unlike React properties, custom elements attributes can be whatever we want and are inherently not type-safe. For this reason, the concept of type safety is quite blur when it comes to Van Elements attributes and I chose to not spend more time digging into how to achieve it.

I am no Typescript wizard, but my guess is that there has to be a way to achieve type-safety with specific HTML attributes. I agree that it that would be a nice bonus to DX, so I will research this a bit further.

In the meantime, if you know more than me or have a clearer idea on how to achieve attribute type-safety, please feel free to contribute to the project! 😊

lorefnon commented 8 months ago

Thanks for getting back. Good to know this is something you are open to considering.

I think this is the simplest solution possible that is also ts friendly:

import { define, component, Attrs } from "van-element";

export interface User {
    name: string
}

export interface HelloWorldAttrs extends Attrs {  // Only primitives allowed
    color: string
    size?: number
}

export interface HelloWorldProps {
    user?: User // Props can be objects
}

const HelloWorldEl = define<HelloWorldAttrs, HelloWorldProps>("hello-world", ({ attrs, props, children }) => {
  // Injected attrs, props functions will only allow compatible object
  const ourAttrs = attrs({
    "color": "red" // attr name -> default value 
    "size": undefined
  });
  const ourProps = props({
    "user": undefined // prop name -> default value 
  });
  // Or alternatively if none of the props have default value: 
  const ourProps = props<HelloWorldProps>(["user"] as const);

  return span(
    { style: () => `color:${ourAttrs.color.val};font-size:${ourAttrs.size.val ?? 'inherit'}` },
    `Hello ${ourProps.user.val?.name ?? 'Stranger'}`
  );
  // Alt. we could use vanx reactive - then .val will not be needed
});

// For users using vanjs to render - we can return a component through a factory
export const HelloWorld = component<HelloWorldAttrs, HelloWorldProps>("hello-world")
// (props: HelloWorldAttrs & HelloWorldProps) => HTMLElement

// For folks consuming element via JSX we can recommend usage of JSX.IntrinsicElements
// Ref: https://www.typescriptlang.org/docs/handbook/jsx.html#attribute-type-checking
declare namespace JSX {
  interface IntrinsicElements {
    "hello-world": HelloWorldAttrs & HelloWorldProps
  }
}

If you are open to API change, I can test it out and send a PR over next few days.

Atmos4 commented 8 months ago

I am open to API changes, however maybe there is a better way to do this than with the very verbose code :D my plan was to keep the main source file under 400B and I don't want to hurt DX either.

This project is meant to be a small wrapper around web components that facilitates hydrating VanJS code inside HTML. It mustn't outgrow this ambition whilst sacrificing ease of use and bundle size.

If what you really want above all is attribute type safety, then I would suggest taking a look at Lit, which takes a much more idiomatic class-based approach to custom elements.

lorefnon commented 8 months ago

Ok, I see where you are coming from. I'll think some more and come back if I have better ideas.

Lit's template string based usage is not particularly type safe either. Solutions like lit-analyzer exist but often fail to report issues. I had assumed that the goal for this project was essentially to be a function oriented alternative to lit-element for vanjs.

Atmos4 commented 8 months ago

Lit's template string based usage is not particularly type safe either

Well, speaking specifically about custom elements attributes, they seem to have good Typescript support from what I can read.

I had assumed that the goal for this project was essentially to be a function oriented alternative to lit-element for vanjs.

You assumed correctly, however the goal of VanJS (and by extension van-element) is also to empower Javascript with reactivity without the need for a giant bundle or compilation process. Which is why Typescript was not my initial focus, and shouldn't be a deciding factor for big API rewrites

However, if there is a solution out there for attribute type-safety that leverages type inference to reduce API rewrites, I am all for it!! 😄 I will also dig a bit to find out if there is a way to achieve this, even if it means rewriting chunks of the core code.