vuejs / jsx-vue2

monorepo for Babel / Vue JSX related packages

Home Page:https://jsx-vue2-playground.netlify.app/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Vue 3 JSX Design

Amour1688 opened this issue · comments

Edit by @yyx990803 : there are currently two JSX transform implementations for Vue 3 with slightly differing syntax (for Vue specific features). We are using this thread to unify the design and land on an official specification of how Vue features should be handled in JSX.

Babel JSX Transform [Github]
alpha

  • patchFlags
  • same as Vue 3 Compiler
  • compatible with Vue 2.x

Syntax

Content

functional component

const App = () => <div></div>

with render

const App = {
  render() {
    return <div>Vue 3.0</div>
  }
}
const App = defineComponent(() => {
  const count = ref(0);

  const inc = () => {
    count.value++;
  };

  return () => (
    <div onClick={inc}>
      {count.value}
    </div>
  )
})

Fragment

const App = () => (
  <>
    <span>I'm</span>
    <span>Fragment</span>
  </>
)

Attributes/Props

const App = () => <input type="email" />

with a dynamic binding:

const placeholderText = 'email'
const App = () => (
  <input
    type="email"
    placeholder={placeholderText}
  />
)

Directives

It is recommended to use camelCase version of it (vModel) in JSX, but you can use kebab-case too (v-model).

v-show

const App = {
  data() {
    return { visible: true };
  },
  render() {
    return <input vShow={this.visible} />;
  },
};

v-model

  • You should use underscore (_) instead of dot (.) for modifiers (vModel_trim={this.test})
export default {
  data: () => ({
    test: 'Hello World',
  }),
  render() {
    return (
      <>
        <input type="text" vModel_trim={this.test} />
        {this.test}
      </>
    )
  },
}

custom directive

const App = {
  directives: { custom: customDirective },
  setup() {
    return () => (
      <a
        vCustom={{
          value: 123,
          arg: 'arg',
        }}
      />
    );
  },
}

Compatible with Vue2 should give a deprecated warning.
I prefer not to support in future, if compatible, we need to add runtime, performance will be lost.

About slots,proposed API look like:

<com vSlots={{xxx: ({val})=>[<div>{val}</div>]}}>
    <span></span>
</com>

children has higher priority than vSlots.default or does not support vSlots.default.

@Amour1688 Great job, in addition to the above, I have some same or different proposals:

Experimental project: vue-next-jsx

Unified syntax

Since tsx does not support JSXNamespacedName and the . cannot appear in the attribute name, it is necessary to unify how to write directives in jsx.

Proposed syntax:

  • Don't support directives shorthand in jsx
  • Use - instead of :
  • and use _ instead of .

Examples

  • v-on
<p v-on-click_stop={ handler }></p>
<Comp v-on-myevent_a_b={ handler } />
  • v-model
    Note that: in Vue3, we use v-model:foo instead of :foo.sync modifier
<input v-model={ refVal.value }></p>
<Comp v-model-foo_a_b={ refVal.value } />

pros

  • No mixing/confusion using camel case and hyphen
  • Can be used for both jsx and tsx

cons

  • Not compatible with vue2's jsx syntax

Restricted slot

Since the scoped slot is still manually built in the Vue2' jsx, so I propose the only way to provide slots for components is:

<Comp>{ mySlots }</Comp>
const mySlots = {
    default: () => [ <p>default</p> ],
    foo: (obj) => [ <p>{ obj.somePorp }</p> ]
}

pros

  • Unified use of named slots and scoped slots
  • Avoid v-slot="props" resulting in type loss in ts

cons

  • Not compatible with vue2's jsx syntax

KeepAlive And Teleport

In Vue3, the children of KeepAlive and Teleport components will not be built as slots, so we need to handle it.

Fragment

Although we don't must to support fragment in jsx plugin, because Vue3 will handle it automatically, but it does bring development convenience:

render() {
    return (
        <>
            <p>Foo</p>
            <div>Bar</div>
        </>
    )
}

Optimization mode

Vue3 makes full use of compile-time information to generate PatchFlags for runtime update performance improvement, Maybe the jsx plugin can also do some similar work

Specify source

Some people install vue, but some install @vue/runtime-dom, so this should be configurable:

{
  "presets": [
    "@babel/env"
  ],
  "plugins": [
    ["@hcysunyang/vue-next-jsx", {
      // Specify source
      "source": "@vue/runtime-dom"
    }]
  ]
}

It affects the import statement:

import { .. } from 'vue'
import { .. } from '@vue/runtime-dom'

v-html / v-text

In Typescript, we must use domPropsInnerHTML, if we support v-html it will be more friendly.

@HcySunYang

Don't support directives shorthand in jsx
Use - instead of :
and use _ instead of .

We should declare events instead of skipping the check. In fact,when the event is declared in the props, we can still trigger by emit.
I prefer the camel case.

@HcySunYang

Don't support directives shorthand in jsx
Use - instead of :
and use _ instead of .

We should declare events instead of skipping the check. In fact,when the event is declared in the props, we can still trigger by emit.
I prefer the camel case.

Directives

<input vModel={this.newTodoText} />

with a modifier:

<input vModel_trim={this.newTodoText} />

with an argument:

<input onClick={this.newTodoText} />

with an argument and modifiers:

<input onClick_stop_prevent={this.newTodoText} />

v-html:

<p domPropsInnerHTML={html} />

If support modifiers, we can also declare it.
I am not familiar with ts, is there any other better way.

@tangjinzhou

In Typescript, if you do this:

<p onClick_stop={ handler }>text</p>

will get an error:

image

This is because onClick is treated as a standard html attribute.

In fact, users can still use onClick, but it is not allowed to add modifiers on it, if you want to add modifiers, please use:

<p v-on-click_stop={ handler }></p>

In Typescript, if you do this:

<p onClick_stop={ handler }>text</p>

will get an error:

image

This is because onClick is treated as a standard html attribute.

In fact, users can still use onClick, but it is not allowed to add modifiers on it, if you want to add modifiers, please use:

<p v-on-click_stop={ handler }></p>

If it's possible to extend types.

v-on-click It doesn't look good.

I think use props.onXX may be a better method

@tangjinzhou

In Typescript, if you do this:

<p onClick_stop={ handler }>text</p>

will get an error:

image

This is because onClick is treated as a standard html attribute.

In fact, users can still use onClick, but it is not allowed to add modifiers on it, if you want to add modifiers, please use:

<p v-on-click_stop={ handler }></p>

@HcySunYang

maybe we should try to resolve it. If skip the check, the effect of using ts will no longer exist.

We can declare it like react :

declare module 'react' {
  interface Attributes {
    vModel?: any;
    // onClick
    onClick_stop?: any;
  }
}

Dots to separate is more readable. We know that dot is a layer inside something
Unserscore means compound names.

I love to use dash in HTML but in JSX can be confused with a minus sign.

We are now using the vueComponent/jsx to refactor ant-design-vue. I expect a smaller change cost, so I hope to be compatible with the syntax in vue2. Even if it may cause a performance loss, we should give the old project enough time to migrate.

Why not use JSX as is and use event handlers like this?

<button onClick={activateLasers}>
  Activate Lasers
</button>

I feel it more intuitive for devs coming with React background.

@BlackSonic, Of course, you can.

One of the purposes of the jsx plugin is to bring the convenience of the template, such as event modifiers, but this does not prevent you from directly using onClick.

    return (
      <>
        <input type="text" vModel_trim={this.test} />
        {this.test}
      </>
    )

I think v-model-trim or vModelTrim would be infinitely better than vModel_trim which is both camel case and snake case at the same time. 😬

    return (
      <>
        <input type="text" vModel_trim={this.test} />
        {this.test}
      </>
    )

I think v-model-trim or vModelTrim would be infinitely better than vModel_trim which is both camel case and snake case at the same time.

vModelTrim is better than vModel_trim. It's an excellent idea. We need consistent design to reduce the cognitive cost of users.

Imo there is a mismatch with JSX props syntax and JS. JS object syntax would be much easier to use:

const placeholderText = 'email'
const name = 'emailInput'
const App = () => (
  <input {
    type: "email",
    placeholder: placeholderText,
    name
  } />
)

@Amour1688 The vModelTrim is confusing, what is the dir name(Model or ModelTrim?) and what is the modifier? 🤔

@Amour1688 The vModelTrim is confusing, what is the dir name(Model or ModelTrim?) and what is the modifier? 🤔

Em... So vModelTrim maybe not a good choice. 🤔

What about <input type="text" vModel={ trim(this.test) } /> where trim is to be imported from 'vue'.
The trim function wouldn't transform the content of this.test, but doing something with the get/set for getting a model trimmed.
It's more closer to the JSX philosophy but looks like a filter (feature removed in Vue 3).

Another examples :

<button onClick={middle(clickHandler)} />
<input onChange={ctrl(e(textHandler))} />

Or better one using pipes :

<button onClick={clickHandler |> middle} />
<input onKeyUp={textChanged |> ctrl |> e} />
<textarea vModel={clickHandler |> trim}></textarea>

Or better one using pipes

Unfortunately there is no way Vue can rely on Stage 1 features.

But I love the proposal of using just function composition because I think the main goal of using jsx is for using "just javascript" (despite jsx being not js).

What about <input type="text" vModel={ trim(this.test) } /> where trim is to be imported from 'vue'.
The trim function wouldn't transform the content of this.test, but doing something with the get/set for getting a model trimmed.

Maybe easier solution would be to not provide modifiers in JSX as it's not a standard JS feature. In the codebases I worked on there weren't many usages of modifiers, but even if there are many, how difficult it is to use object spread? This can even be transpiled:

export default {
  data: () => ({
    test: 'Hello World',
  }),
  methods: {
    onSubmit: () => {},
  },
  render() {
    return (
      <form ...({ 'onSubmit.prevent': onSubmit }) > ... 
        <input type="text" ...({ 'v-model.trim': this.test }) />
        {this.test}
      </form>
    )
  },
}

But in the end I don't even care whether it was transpiled. Yes we create new objects on every render, but that's basically it.

Recognizing that many people have some misunderstandings, in fact, in Vue3 you can write jsx just like in React. We are discussing how to support directives args and modifiers, which are optional additional capabilities.

how about this :

base

<button onClick={clickHandler} />

factory function

import {factoryClick, factoryDirective } from 'vue'
<button onClick={factoryClick(clickHandler,['middle'])} />
<input vModel={factoryDirective(a, ['trim'])} />

factoryfunction(value, modifiers: string[]) just return value
JSX parse modifiers , when factoryXXX from vue

Directives and Modifiers are optional features.
Maybe @JasKang plan is a good choice.

I like vModel-foo_trim for v-model:foo.trim="bar"

Adding my cent here, thought the mix of dashes and underscores is not visually good to me. Overall, the 1:1 mapping of @HcySunYang 's syntax I think is more straightforward for people to read without reading extract docs.

Vote for v-on:myevent.a.b -> v-on-myevent_a_b

Overall, the 1:1 mapping of @HcySunYang 's syntax I think is more straightforward for people to read without reading extract docs.
@antfu

Yes, but with some function utilities could be more explicit and less wierd to use. Not to mention wierd props we could have..
These functions could be added outside of the Vue core and directly added to the JSX project with a tree shaking behaviour.

import { trim } from 'vue/jsx'; // or 'vue-jsx'
<input vModel={trim(localModel)} /> {/* default model */}

import Comp, { foo } from './comp'
<Comp vModel={trim(foo(localModel))} /> {/* foo model of Comp */}

foo could be defined in the component like this :

import { toModel } from vue-jsx'';
const localRef = ref('value');
export const foo = toModel(localRef);

This improves the link between components and the typing too !

I like @edimitchel 's plan(function style) or @JasKang 's plan, because other plans breaks type-safety.

commented

explicit is better than implicit, I think just use plain Objects

<Comp v-model:foo.trim="bar" />

// to

<Comp 
  vModel={[
    { prop: 'foo', value: 'foo1', options: { trim: true } },
    { prop: 'bar', value: 'bar1', options: { trim: false, ...extraOptions } }
  ]} 
/>

Just a little personal opinion about function style:

  • As a React user, I prefer to use functions, because such JSX will be simple and pure.

  • As a Vue user, I prefer to use syntactic sugar because vue's syntactic sugar is already familiar to me, and it does help a lot.

As a Vue user, I prefer to use syntactic sugar because vue's syntactic sugar is already familiar to me, and it does help a lot.

But that syntax is from the template syntax, they are two totally different use cases.

Just make typings/inference easy in .tsx files (it also gives IDE's like vscode info for .jsx files) and reduce the need for maintenance.

Whether you do v-on-click_stop_prevent or onClick_stop_prevent - doesn't every combination of event handlers + modifier have to be typed in this plugin via a declare module shim (even the order of modifiers like onClick_prevent_stop etc.)? If they're not provided I'd guess we won't get any auto-completion or even warnings for spelling mistakes or wrong types when passing down props.

I'd think it's best to keep standard TSX functionality like in React (since it has a big ecosystem and IDE support) like has been discussed. Here's React's definitions but I've seen meta-frameworks like next-js augment these types and add jsx elements like <style jsx></style>, which is typed with a shim and then handled when bundling.

React's typings are unnecessarily long since their codebase is written in flow (instead of typescript) but this is where Vue 3 could have an advantage, but according to Typescript's documentation on JSX - for better integration with a library one should use that library's typings. I've only seen wonderful-panda's vue-tsx-support and while I like the effort, I think a lot of the syntax can be provided from vue itself now or its default jsx library - at least we should be able to type our jsx elements and components properly in Vue 3 without any massive boilerplate (we already have defineComponent).

Regarding the "syntactic sugar" we can limit these to simple props like vShow/v-show is easy to support, but directives/models/events with modifiers etc. could also be simple tags, like vOn and vModel, which accept something like an object or generate via factories, like mentioned already in this thread.

Evan You describes how a simpler signature for custom render functions might be made available via wrapping it:

withModel(h(SomeComp), {
  prop: 'foo', // optional
  value: foo,
  update: value => (foo = value),
  modifiers: { trim: true }
})

But I think JSX can make the step from template syntax to render function smoother...
The template syntax:

<Comp
  v-model:foo.trim="text"
  v-model:bar.number="num"
/>

becomes something like @magicdawn mentioned:

<Comp vModel={[
  { prop: 'foo', value: text, modifiers: { trim: true } },
  { prop: 'bar', value: num, modifiers: { number: true } }
]}/>

which can be simplified with utilities (vModel prop accepts an array of VModels or a single VModel object):

import {createVModel as m} from "vue/jsx";
<Comp vModel={[
  m('foo', text, {trim: true}),
  m('bar', num, {number: true})
]}/>

which is finally compiled to h (or even straight to createVNode depending on how the plugin is written):

h(Comp, {
  foo: text,
  'onUpdate:foo': value => (text = value),
  fooModifiers: { trim: true },
  bar: num,
  'onUpdate:bar': value => (bar = value),
  barModifiers: { number: true }
})

I'm not sure what the JSX plugin is capable of - would the helper function create runtime overhead or can the expression be transpiled into an optimized render function straight away - or is that the reason you're striving for long string-based props?
If that's the case I guess the plugin could "generate" all possible combinations of typescript JSX properties shim when it's packaged.

One final thing I'd like to mention is that there's discussions going on in Typescript regarding React's new jsx factory rfc for React 17 and autoimports, which gained experimental support in Babel 7.9.0. It would be great if Vue team had something to say in these matters since Vue components could be simplified to a functional component in a .tsx file without the import of the h function.

Vue3 is usually advertised with "better typescript support" since the team spent a lot of effort rewriting the whole library in it - so I'd think developing in .tsx files could be less awkward if the new jsx implementation would keep typescript support in mind.
Providing a playground (like codesandbox) where it's possible to try out the tsx support would be great, both for experimentation and giving feedback - I had trouble with both vue-cli and vite when it came to TSX since the support isn't ready in these.

commented

@bjarkihall I like the simplified syntax very much. I wonder if createVModel can be skipped (not sure what it does ;p). Also, what you think about changing the order of the arguments so that they would be aligned with arguments order of the withDirectives helper

<Comp vModel={[text, 'foo', { trim: true }]} /> // v-model:foo.trim="text"

<Comp 
  vModel={[
    [text,, 'foo', { trim: true }], // v-model:foo.trim="text"
    [num, dynamicProp] // v-model:[dynamicProp]="num"
  ]} 
/>

<Comp 
  vPin={[200, undefined, { top: true }]} // v-pin.top="200"
/>

@sqal good catch with the withDirectives syntax, the order was just based on the comments above but should ofc align with other structures in vue, like directives.

// Directive, value, argument, modifiers
so vModel is:
// ("model"), value, argument, modifiers

This example is from vue docs:

<div v-demo:foo.a.b="message" />
// name: "demo"
// value: "hello!"
// expression: "message"
// argument : "foo"
// modifiers: {"a": true, "b": true}

So that would be:

withDirectives(
  h('div'), [
    [ resolveDirective("demo"), message, "foo", {"a": true, "b": true} ]
  ]
);

But JSX could be:

<div vDirectives={[
  [ resolveDirective("demo"), message, "foo", {"a": true, "b": true} ]
]} />

Where vDirectives/vDirs prop accepts the type DirectiveArguments (2-dimensional array) but vDirective/vDir a single array.
If the first element is a string, it would be automatically resolved, so "demo" -> resolveDirective("demo").

So this is the end result:

<div vDir={[ "demo", message, "foo", {"a": true, "b": true} ]} />

or if the modifiers are always :true, it could be even more compact:

<div vDir={[ "demo", message, "foo", ["a", "b"] ]} />

The goal here is to start with a generic enough prop which the others can derive from and be typed without having to do anything too manual. I've not seen JSX tags being added by most users, so your example of v-pin:

<Comp 
  vPin={[200, undefined, { top: true }]} // v-pin.top="200"
/>

would preferably be converted to:

<Comp 
  vDir={["pin", 200, undefined, { top: true }]} // v-pin.top="200"
/>

except if vue or a 3rd party library would like to include a special tag (the obvious case is vShow but could also be vPin), they could just do that and add the types.

Regarding utility/factory function vs. array/tuples - the core could be array based but functions just optionally offer better aid when typing these out, since functions have named arguments and logic while tuples just depend on their indices while being faster:

// vue/jsx:
const createDirectiveArg(directive: Directive, value?: any, argument?: string, modifiers?: DirectiveModifiers) => ([directive, value, argument, modifiers]);
// and this would even be possible:
const createDirectiveArg(directive: string, value?: any, argumentOrModifiers?: string | string[]) => (typeof argumentOrModifiers === "string" ? [directive, value, argumentOrModifiers] : [directive, value, undefined, argumentOrModifiers]);

// Comp.tsx:
import {createDirectiveArg as d} from "vue/jsx";
<Comp vDir={d("pin", 200, ["top"])} /> // v-pin.top="200"

It's just a matter of how much info/flexibility you want to get from the typing system.
There's nothing stopping you from writing a resolver for the string in general:

<Comp vDir={'v-pin.top="200"'} />

where the transformation step just magically finds out what the string means like in templates, but you lose the type safety.

I did some research and I started to like @bjarkihall 's proposal, vModel can be used on intrinsic elements and value-based elements. For intrinsic elements, vModel can not take args, so we can simplify it(only value and modifiers):

In the jsx library, we can extend the IntrinsicElements interface:

// type of modifiers
type IntrinaicVModelModifiers = {
  trim?: boolean
  lazy?: boolean
  number?: boolean
}
// type of value
type ModelValue = string | number | boolean

type IntrinsicVModelArg =
  | ModelValue
  | [ModelValue]
  | [ModelValue, IntrinaicVModelModifiers]

declare module 'vue' {
  interface InputHTMLAttributes {
    vModel?: IntrinsicVModelArg
  }
  interface SelectHTMLAttributes {
    vModel?: IntrinsicVModelArg
  }
  interface TextareaHTMLAttributes {
    vModel?: IntrinsicVModelArg
  }
}

With these, we can get good type support:

const App = {
  setup() {
    const refText = ref('')

    return () => (
      <>
        {/* Only value */}
        <input vModel={refText.value} />
        {/* with modifier */}
        <input vModel={[refText.value, { trim: true }]} />
      </>
    )
  }
}

For further, we can encapsulate the function, which is optional for users:

function m(v: ModelValue, m?: IntrinaicVModelModifiers):  IntrinsicVModelArg{
  return !m ? v : [v, m]
}

Type hint just like this:
image

For value-based elements(Components):

// value, arg, modifiers
type ComponentModelArg = 
  | [any]
  | [any, string]
  | [any, string, Record<string, boolean>]

// Just for demonstration, maybe we can do better type inference(the `defineComponent` in vue core).
const Child = ({
  setup() {
    return <p>child</p>
  }
} as any) as { new (): { $props: { vModel?: ComponentModelArg[] } } }

With these, we can get some basic types of support:

const App = {
  setup() {
    const refText = ref('')

    return () => (
      <>
        <Child vModel={[
          [refText.value],
          [refText.value, 'foo', { a: true }]
        ]} />
      </>
    )
  }
}

In addition, users can use utility functions for type hinting (optionally):

function cm(value: any, props: string, modifiers?: Record<string, boolean>): ComponentModelArg {
  return modifiers ? [value, props, modifiers] : [value, props]
}

Type hint:
image

For vOn, we can do something similar.

@HcySunYang that's exactly what I was trying to hint at, the suggestions we'd get from the IDE etc - it's nice to see the visuals from your experimentation.

vShow/vHtml/vSlots, vOn/vOns/vEvent/vEvents, vModel/vModels, vDir/vDirs/vDirective/vDirectives is a pretty compact list and easy to type and they kind of rely on the same principles, order of arguments and logic - they are also opt-in for vue usage so onClick would work normally and even the utility functions are just optionally there to guide users and make the code more readable, while giving type safety. If users or library authors would like more built-in props, they can be optionally added too, as long as the jsx transformation can detect it, but I'd call that a more advanced use case.

It's the flexibility vs simplicity tradeoff:
e.g. v-pin would not be part of the official directives but could be declared in many ways:

<Comp v-pin.top={200} /> // ❌ jsx could support this - but tsx would break
<Comp vPin_top={200} /> // ❌ would need to be added specially with each modifier
<Comp vPin={[200, undefined, { top: true }]} /> // ⚠ custom tag which would either need to provide a function too or even be handled by the library providing it
<Comp vDir={'v-pin.top="200"'} /> // ⚠ this could be officially supported - not type checked though
<Comp vDir={["pin", 200, undefined, { top: true }]} /> // ✔ no dependencies needed, fully type safe
<Comp vDir={d("pin", 200, ["top"])} /> // ✔ vue/jsx could provide some default utility functions, still type safe
<Comp vDir={vPin(200, "top")} /> // ✔ a mix - library author/user only needs to supply the vPin function, still type safe
<Comp vDirs={[...]}/> // etc.

There are many ways to do things because people tend to disagree on syntax but I'd think having the options limited, offering the low-level array syntax but recommending utility functions first in the docs would be solid, both for usage and maintenance.

So here's some kind of a summary, if I understand the TSX usage correctly:

const el = <div/>; // el would become VueElement/VNode?
let fc: FunctionalComponent = (p, ctx) => <div/>; // I'd love it if we could just import FC, but {FunctionalComponent as FC} should work...
let fc2: FunctionalComponent<{foo: string}> = (p, ctx) => <div data-foo={p.foo}>{ctx.slots}</div>; // these are mostly typed!
  • Props/Attr/DefaultValue:
interface MyProps {
  dataHello: string
}
fc: FunctionalComponent<MyProps> = ({dataHello = 'world!'}) => <div dataHello={dataHello} />; // yields <div data-hello="world!"></div>
fc = (p: MyProps) => <div {...p} />; // does it too, given p = { dataHello: 'world!' }
  • NativeEvents:
<button onClick={doThat} />
<button vOn={[doThat, 'click']} /> // the same
  • EventModifiers (<a v-on:click.stop.prevent="doThat"></a>):
<a vOn={[doThat, 'click', {stop: true, prevent: true}]} />

or shorter (might need an optional helper function):

<a vOn={[doThat, 'click', ['stop', 'prevent']]} />
  • Multiple Events (<a v-on:mousemove.stop.prevent="doThis" v-on:click.stop.prevent="doThat"></a>)
const ev: (handler: VueEventHandler, eventType: VueEventTypes, modifiers: EventModifiers[]) => VEventArg;
<a vOns={[
  ev(doThis, 'mousemove', ['stop', 'prevent'])
  ev(doThat, 'click', ['stop', 'prevent']),
]}/>

I'm not sure I like vOn/vOns - maybe it could be offered as short names for the defaults vEvent/vEvents? This is a rare usage since most users would probably just use onClick prop etc.
The utility functions would also become unified, though I think they'd have 2 versions: a long descriptive name and a 1-2 letter one. Offering a utility function per type would also allow us to give clear types for standard directives/events/models available and their modifiers.

  • The events and models are just types of directives, like discussed above, so we'd have a unified model to represent these.
    I think it becomes much more clear once it starts to be written properly w. types/functions in TS while looking at what types Vue 3 already has to offer and what h and createVNode already expect.
  • Teleport/Keepalive/Transition/Suspense will need to be special JSX.Elements defined by the library, Fragments can be resolved with <></>. It looks like we could just refer to some of these components straight away, but I don't know how well that plays with TSX - can a VNode type be a JSX element?
  • Slots: I'd also think <Comp>{slotsObject}</Comp> is better than <Comp vSlots={slotsObject} />, but maybe they should just both work and children would have higher priority than slotsObject.default, where:
<Comp>{{
  foo: () => <div>{this.foo}</div>,
  bar: () => <div>{this.bar}</div>
}}</Comp>

would be equal to:

<Comp vSlots={{
  foo: ()=> <div>{this.foo}</div>,
  bar: ()=> <div>{this.bar}</div>
}} />

and becomes something like:

h(Comp, {
  slots: {
    $stable: true, // this might need attention?
    foo: () => h('div', this.foo),
    bar: () => h('div', this.bar)
  }
})
  • Lastly some built in tags need to be available, like key/ref and vnode lifecycle hooks (onVnodeXXX, e.g. onVnodeMounted), except you just want to delegate these to vOn, but I think these are limited enough number of built-ins. I'm not sure if style/class need to be typed specially? Typeless functionality might be ok (typescript might not like vPin but it will still be transformed) and if one doesn't need types (and are just moving something from templates to TSX) we could offer transformation of strings like v="pin:foo.bar" and v={[...] as string[]}.
  • If most of the patchflags can be thrown in that would be great for v1 but I think I've listed everything that we'd need now from a TSX perspective, I think most of these are already available but might need some polish/testing.

@bjarkihall Excellent Job.

For events, I prefer @JasKang or @edimitchel 's design.

  • base
<button onClick={clickHandler} />
  • factory function
import { factoryClick } from 'vue'
<button onClick={factoryClick(clickHandler,['middle'])} />

In template, Vue provides a method named withModifiers, so we got this.

// Before

<div @click.self="xx">click me</div>

// after

{
  onClick: withModifiers(xx, ["self"])
}

Then in JSX, I think when you want to use modifiers with your event, withModifiers will be a better choice which can keep type-safety well, without any syntactic sugar.

import { withModifiers } from 'vue'

<div onClick={withModifiers(xx, ['self'])}>click me</div>

As for vOn

<a vOn={[doThat, 'click', {stop: true, prevent: true}]} />

This proposal may lose type safe. Since emit is not type safe, we recommend using events declared in props by props.onEvent.

ant-design-vue and vant are now using vueComponent/jsx to refactor.

@Amour1688 ah, I was confused with the events in Vue 2 but can see the change now in rfc/0008, so events are syntactically like directives in templates but are really just props that start with onXXX in render functions.

I was just describing how any v- tag could be written and parsed with the same low-level array syntax - that wouldn't need any imported functions and were using the same type structure as Vue itself underneath for directives/models/events/etc.
The singular/plural naming was to tell the single/many tags of the same directive-type apart (if the array is 1 vs 2 dimensional), and the imported helpers were just to show how to make them more descriptive.

Both methods (tuple or helperFunction->tuple) can be typed properly and are therefor type safe and I think the only syntactic sugar would be the vOn tag itself - which I think would be good to reserve and handle either way, just for completion.

A short typing example:
image
These kind of types can be created/imported straight from the core vue library and be hidden away, like @HcySunYang demonstrated, the only way we'd not be able to type things is if the jsx syntax was based on parsing a string pattern like "v-pin:foo.bar" - but keys (string unions), tuples, objects and functions can all provide inference/typing, it just depends on how standalone the plugin is supposed to be, not having to import any functions but still have a feature-complete and type-checked vue-jsx is a nice possibility while others prefer helper functions - I personally really like the withModifiers method - it closely resembles what I generally had in mind, thanks for pointing that out. :)

@Amour1688
agree with you , I think JSX should remain unified and avoid more mental burdens

Hopefully this explains the hierarchy better:

4 parameters (directive, value, argument, modifiers)

<div v-demo:foo.a.b="message" />
<div vDir={...} /> // the only syntactic sugar (you have to have something) is the vDir tag, which can be typed

use either:

  1. ['demo', message, 'foo', {a: true, b: true}]
  2. ['demo', message, 'foo', ['a', 'b']]
    or import a utility function that returns this array:
  3. d('demo', message, 'foo', ['a', 'b'])

3 parameters (value, argument, modifiers)

<div v-model:foo.a.b="message" />
<div vModel={...} />

use either:

  1. [message, 'foo', {a: true, b: true}]
  2. [message, 'foo', ['a', 'b']]
    or import a utility function that returns this array:
  3. m(message, 'foo', ['a', 'b'])

What I described above resembled this but maybe it doesn't make sense after all:

<div v-on:click.a.b="message" />
<div vOn={...} />

use either:

  1. [message, 'click', {a: true, b: true}]
  2. [message, 'click', ['a', 'b']]
    or import a utility function that returns this array:
  3. ev(message, 'click', ['a', 'b'])

2 parameters (value, modifiers)

<div v-on-click.a.b="message" />
<div onClick={...} />

use either:

  1. [message, {a: true, b: true}]
  2. [message, ['a', 'b']]
    or import a utility function that returns this array:
  3. ev(message, ['a', 'b']) // ev could be called "withModifiers", like you were using above
  4. ev2(message, 'a', 'b') // would also be possible - ev2: (handler: EventHandler, ...(modifiers: EventModifiers[])) => EventArg

And for (value, argument):

<div v-model:foo="message" />
<div vModel={...} />

use either:

  1. [message, 'foo']
    or import a utility function that returns this array:
  2. m(message, 'foo') // m could be called "withParameter" like withModifiers, or it can just be the same function with different signatures

1 parameter (value)

<div v-on-click="message" />
<div onClick={message} /> // just use the value, this also applies to simple vModel, vShow, etc.

Note that the functions d, m, ev, ev2 are just placeholders for demonstration and by no means the finalized API - these could all be the same function, like h is for render functions, but instead of returning a VNode it's returning a DirectiveArgs tuple. They could be "withDirective", "withModel", "withModifiers" functions already found in vue, as long as they help people with the types and return the correct data structure for the transformation step.

The Vue JSX tags (vDir, vModel, vOn, etc) would have the same base type - a tuple of (directive, value, argument, modifiers), which has subsets depending on how much of the tuple you're going to use - the imported utility function can help you type and construct the tuple (depending on if it's a directive, event or a model for example).
If the tag has a plural name (vDirs), it's just an array of these tuples.

I do like this short style, very clean and can be well self explained with hint. <a vOn={[doThat,'click', ['stop','prevent'icsoftfont>} />
It's really unnecessary to introduce extra import just for this syntax hint.

For the purpose of experimenting and collecting feedback, I implemented the syntax discussed above:

<>
    <h1>The 1:1 mapping syntax, it reduces the mental burden, recommended for users who use js:</h1>
    
    <input v-model_number={ refVal.value } />
    <input v-on-click_stop={ handler } />

    <p>You can still use the original jsx syntax:</p>
    <div onClick={ hander }></div>


    <h1>Better type hinting and type safety, recommended to typescript users</h1>

    <input vModel={ [refVal.value, { number: true }] } />
    <input vModel={ [refVal.value, ['number']] } />
    <Comp vModel={ [refVal.value, 'foo', { a: true }] } />
    <Comp vModel={ [refVal.value, 'foo', ['a']] } />
    <Comp vModel={ [refVal.value, dynamic, ['a']] } />

    <p>withModifiers:</p>
    <div onClick={ withModifiers(handler, ['self']) }></div>
</>

See this Readme for details: vue-next-jsx#for-typescript-users.

For type hints, you can check our dts test: intrinaicElements.test-d.tsx.

Note: Type support for components needs to wait until this issue to be fixed

And the Playground: https://vue-next-jsx.netlify.app/.

Personly, I think the main principle of vue3 in JSX should be :

typescript first

more syntactic, less function

It's confused for newer about using vue's internal function like withModifiers or other utility functions provided by this repo in JSX.

I like
<input vModel={ [refVal.value, { number: true }] } />

provided by @HcySunYang above

About vOns.

I think we can make event handlers type-safe by this feature.

And in terms of typing, I think A is more preferable than B.

// A
<a vOns={{
  mousemove: ev(doThis, ...),
  click: ev(doThat, ...)
}} />

// B
<a vOns={[
  ev(dThis, 'mousemove', ...),
  ev(doThat, 'click', ...)
]} />

I've seen some great discussions here, while I don't have definitive answers to all the questions yet, I'll just write down some of my thoughts here:

  • I don't think we should add too much syntax sugar into JSX. If we do, we should do it in a type-safe and JSX-idiomatic fashion (i.e camelCase). For example I like the idea of vModel={[val, [modifiers]]}.

  • If we can agree on a type-safe usage for directives and event handlers, then I think we should later remove template-like syntax like v-on-click_stop, v-model_number or vModel_trim - they create extra complexities for typing, another way of doing things, plus JSX is for users who don't want to use templates anyway, so what's the point in making it look like templates?

For Directive, My implementation in v1.0.0-beta.3.

Users who don't want to use templates also need an easier way to write their code. It's unnecessary to write vModel={[val]}
with single param. So I also support it in this way.

<input vModel={val} />
<A vModel={[val, 'arg']} />
<A vModel={[val, ['a', 'b']]}  />
<A vModel={[val, 'arg', ['a', 'b']]}  />

As for arg and modifiers:

  • ['a', 'b'] is more convenient than { a: true, b: true } to write. Why not ['a', 'b'] ?
  • Syntax like vModel_trim will give a deprecated warning.
  • Do we really need dynamic arg like this ?
const x = 'xx';
<A vModel={[val, x]} />

Users who don't want to use templates also need an easier way to write their code. It's unnecessary to write vModel={[val]}
with single param

This is for consideration of type hints, since users can bind any value, in other words, the type of the value is any, we can define the type like this:

export type IntrinsicVModelArg = [any] | [any, IntrinaicVModelModifiers]

But we can't do this(can we?):

export type IntrinsicVModelArg = any | [any] | [any, IntrinaicVModelModifiers]
commented
export type IntrinsicVModelArg = any | [any] | [any, IntrinaicVModelModifiers]

Is it possible that we can use generic types like following

export type IntrinsicVModelArg<ModelType> = ModelType | [ModelType] | [ModelType, IntrinaicVModelModifiers]

And user can nominate the type while using the directives.

@Amour1688 @HcySunYang I see the JSX explorer, thanks you ! That is very great !
https://vue-next-jsx.netlify.app/

@edimitchel Maybe you posted the wrong link, that is mine. 🤔

@edimitchel Maybe you posted the wrong link, that is mine. 🤔

Yes haha ! Sorry, I first posted his link then I found yours ! I change it !

any | [any] leads to the type any, so allowing it would break the typings. That's why you'd have to separate the definitions:

const value: any;
<Comp vModel={value} /> // This is ambiguous, how would you type it? Should a model value have a narrower type?
<Comp vModelValue={value} /> // You could add yet another attribute type just to skip the array
<Comp vModel={[value]} /> // I think most users would prefer this instead since it's a single syntax

// For multiple models:
const value2: any;
const modifiers: string[] | Record<string, boolean>;
const modifiers2: string[] | Record<string, boolean>;
// this solves the ambiguity for multiple models:
<Comp vModels={[[value, 'first', modifiers], [value2, 'second', modifiers2]]} />
// this is like the event syntax described by @wonderful-panda:
<Comp vModels={{
  first: [value, modifiers],
  second: [value2, modifiers2]
}} />
// I guess this would be possible too, but can't work on vModels straight for the same reason as described above:
<Comp vModelValues={{
  first: value,
  second: value2
}} />

// if value and value2 would be named the same as the args this would become:
const first: any;
const second: any;
<Comp vModelValues={{ first, second }} />;

The any type is really bad for type inference so since the value can be of type any, we have to be able to clearly define if you're passing a value (any), array of values ([any, ...]) or an array of arrays of values ([[any, ...], [any, ...], ...]) - these are vModelValue, vModel and vModels. If the value has a clearer type, I guess we could make all of these work in a single attribute but it's not clear which types are valid and which are not - it would potentially force users to cast to unknown and then to the valid type, which is much more confusing for users and maintainers than just having separate attributes.

The vCustom example also breaks typings - you'd have to provide a "string | Directive" type to the tuple and have a single attribute like vDir/vDirective to resolve/use the directive.

The object syntax for modifiers is probably just linked to destructuring, judging by how they are used in the vModel definitions, but I guess this also opens up for passing more types of payload then booleans to the directive handlers - although that wouldn't be supported by template syntax since modifiers are either present there or not.

I've been experimenting with writing down all of the types we've discussed so far, while looking through vue-next repo, especially the current state of the jsx definitions and the types and tests available, while reading through all of the rfcs.
I have some experimental types but would like to work on them a bit further.

If people would like to experiment themselves, they can define vue-shim.d.ts in the root of their project, the directives/models/events attributes could be fewer, I just wanted to include the ideas for completion:

import Vue, {Slot, Slots} from 'vue';
declare module "vue" {
  export interface VueJSX {
		
    // Basic props/attributes:
    v?: string | string[] | {[v: string]: any}; // escape hatch, can use the template compiler to resolve v- strings and their values
    vShow?: boolean;
    vHtml?: string;
    vSlot?: vSlot;
    vSlots?: vSlots;
    className?: string; // would be nice to make transition from react easier, allow both class and classname

    // Directives:
    vD?: vD;
    vDir?: vD;
    vDirective?: vD;
    vDs?: vDs;
    vDirs?: vDs;
    vDirectives?: vDs;
    vDVs?: vDVs;
    vDirectiveValues?: vDVs;

    // Models:
    vM?: vM;
    vModel?: vM;
    vMs?: vMs;
    vModels?: vMs;
    vMV?: vMV;
    vModelValue?: vMV; // values have any as type, which would break other typings
    vMVs?: vMVs;
    vModelValues?: vMV;

    // Events:
    vE?: vE;
    vOn?: vE;
    vEvent?: vE;
    vEs?: vEs;
    vOns?: vEs;
    vEvents?: vEs;
    vEV?: vEV;
    vEventValue?: vEV;
    vEVs?: vEVs;
    vEventValues?: vEVs;
  }

  // This seemed to be the only thing silencing errors for native jsx elements, like <a />
  export interface HTMLAttributes extends VueJSX {}
  // Is there a way to make e.g. this possible: withDirectives(<div />, [[resolveDirective("demo"), message, "foo", { a: true, b: true }]])
  export interface VNode extends JSX.Element {}
}
declare global {
  namespace JSX {
    interface Element {}
    // I didn't find out how to make the slots syntax become a valid JSX child:
    // interface ElementChildrenAttribute {
    //   vSlots: vSlots;
    // }
    interface IntrinsicAttributes extends VueJSX {}
  }
}

Here are some experimental types:

// How should we go about any vs handpicked values vs boolean (| undefined | null)?
type AnyType = null | undefined | boolean | number | string | symbol | bigint | Function | any[] | object;
type CleanValueType = boolean; // val === any would break type safety

// Default values:
type vdir = string | Directive;
type vval = any; // this can cause trouble!
type evval = Function;
type varg = string;
type vmods = string[] | Record<string, boolean>;

// Directive Tuples:
type vDirMain<
  dir = vdir,
  val = vval,
  arg = varg,
  mods = vmods
> =
  | [dir, val, arg, mods]  // v-demo:foo.a.b="message"
  | [dir, val, arg | mods] // v-demo:foo="message" | // v-demo.a.b="message"
  | [dir, val]             // v-demo="message"
  | [dir] | dir            // v-demo, allow plain dir string | Object

type vDirNameless<
  val = vval,
  arg = varg,
  mods = vmods
> =
  | [val, arg, mods]       // v-demo:foo.a.b="message"
  | [val, arg | mods]      // v-demo:foo="message" | v-demo.a.b="message"
  | [val]                  // v-demo="message"
  | CleanValueType         // v-demo

// Directive Object:
type vDirsObjType = {[key: string]: vDirNameless};

type vDirsObj<
  dirs = vDirsObjType,
  val = vval,
  arg = varg,
  mods = vmods
> = {
  [K in keyof dirs]: (vDirNameless<val, arg, mods> | vArgObj<val, vArgObjType, mods>)
}

// Arg Object:
type vArgObjType = {[key: string]: vArgless};
type vArgless<
  val = vval,
  mods = vmods
> = 
  | [val, mods]
  | [val]
  | CleanValueType

type vArgObj<
  val = vval,
  arg = vArgObjType,
  mods = vmods
> = {
  [K in keyof arg]: vArgless<val, mods>
}

// Final construction:
type vD<d=vdir,v=vval,a=varg,m=vmods> = vDirMain<d,v,a,m>; // [dir, val?, arg?, mods?]
type vDs<d=vdir,v=vval,a=varg,m=vmods> = vDirsObj<vDirsObjType,v,a,m> | vD<d,v,a,m>[]; // [[dir, val?, arg?, mods?], ...] | {dir: [val, arg?, mods?] | boolean, ...} | { dir: { arg: [val, mods?] | boolean } }
type vM<v=vval,a=varg,m=vmods> = vDirNameless<v,a,m>; // [val, arg?, mods?] | boolean
type vMs<v=vval,a=varg,m=vmods> = vArgObj<v,vArgObjType,m> | vM<v,a,m>[]; // [[val, arg?, mods?] | boolean, ...] | { arg: [val, mods?] | boolean }
type vE<v=evval,a=varg,m=vmods> = vDirNameless<v,a,m>; // same as in model
type vEs<v=evval,a=varg,m=vmods> = vArgObj<v,vArgObjType,m> | vE<v,a,m>[];

// Value types:
type vMV<v=vval> = v; // value typings are loose
type vMVs<v=vval,arg=varg> = v[] | {[K in keyof arg]: v};
type vEV<v=evval> = v;
type vEVs<v=evval,arg=varg> = v[] | {[K in keyof arg]: v};
type vDVs<v=AnyType> = {
  [d: string]: {
    [a: string]: v // v === any lets way too many things slip through...
  }
}

// Slots: (I'm still not sure how to go about mixing VNodes and JSX.Element types and how to specify ElementChildrenAttribute)
type vSlot = Slot | VNode | JSX.Element;
type vSlots = Slots | vSlot[];

Which I made some runtime TS tests for (just to see if something was breaking when editing the types):

// SINGLE DIR TESTS:
const dirStr = 'demo';
const dirObj = resolveDirective('demo');
const val = 0 as AnyType;
const arg = 'foo';
const modsArr = ['a', 'b'];
const modsObj = {a: true, b: true};

// @ts-expect-error
const a0Err: vD = val;
const a0: vD = dirStr;
const a0D: vD = dirObj;
// @ts-expect-error
const a1Empty: vD = [];
// @ts-expect-error
const a1Err: vD = [val];
const a1: vD = [dirStr];
const a1Obj: vD = [dirObj];
const a2: vD = [dirStr, val];
const a3: vD = [dirStr, val, arg];
const a3Arr: vD = [dirStr, val, modsArr];
const a3Obj: vD = [dirStr, val, modsObj];
const a4Arr: vD = [dirStr, val, arg, modsArr];
const a4Obj: vD = [dirStr, val, arg, modsObj];
// @ts-expect-error
const a4Err: vD = [dirStr, val, modsArr, arg];

// MULTIPLE DIRS TESTS:
const dirStr2 = 'demo2';
const dirObj2 = resolveDirective('demo2');
const val2 = 1 as AnyType;
const arg2 = 'bar';
const modsArr2 = ['c', 'd'];
const modsObj2 = {c: true, d: true};
// @ts-expect-error
const b0Err: vDs = val2;
// @ts-expect-error
const b0: vDs = dirStr2;
// @ts-expect-error
const b0D: vDs = dirObj2;

const ba0: vDs = []; // can eliminate with spread operator, but why not...
// @ts-expect-error
const ba1Err: vDs = [val2];
const ba1: vDs = [dirStr2];
const ba1Obj: vDs = [dirObj2];
// @ts-expect-error
const ba1TooMany: vDs = [dirStr2, val2];
// @ts-expect-error
const ba2_0: vDs = [[]];
const ba2_1: vDs = [[dirStr]];
const ba2_2: vDs = [[dirStr2, val2]];
// @ts-expect-error
const ba2_2_reverse: vDs = [[val2, dirStr2]];
// @ts-expect-error
const ba2_2_blended: vDs = [val2, [val2, dirStr2]];

const ba3: vDs = [[dirStr2, val2, arg2]];
const b3Arr: vDs = [[dirStr2, val2, modsArr2]];
const b3Obj: vDs = [[dirStr2, val2, modsObj2]];
const b4Arr: vDs = [[dirStr2, val2, arg2, modsArr2]];
const b4Obj: vDs = [[dirStr2, val2, arg2, modsObj2]];
const b4Obj_blended: vDs = [[dirStr2, val2, arg2, modsObj2], [dirStr2], [dirObj2]];
// @ts-expect-error
const b4Err: vDs = [[dirStr, val, modsArr, arg]];

// MULTIPLE DIRS OBJECT TEST:
const c1Empty: vDs = {}; // couldn't find a way to eliminate empty object types, but why not...
const c1Number: vDs = {
  0: [val2]
}
const c1: vDs = {
  [dirStr2]: [val2]
};
const c2: vDs = {
  [dirStr]: [val],
  [dirStr2]: [val2]
};
const c3: vDs = {
  [dirStr]: [val, arg, modsArr],
  // @ts-expect-error
  [dirStr2]: [val2, modsArr2, arg2],
  on: {
    click: [e=>console.log(e.target), ['prevent', 'stop']]
  }
};
const c4: vDs = {
  // @ts-expect-error
  0: [val, arg, modsArr, modsArr],
  1: undefined,
  2: null,
  3: true,
  4: false,
  // @ts-expect-error
  5: val,
  6: {}, // this is hard to block...
  // @ts-expect-error
  7: {foo:'bar'},
  // @ts-expect-error
  [dirStr]: [val, arg, arg, modsArr],
  // @ts-expect-error
  [dirStr2]: [dirStr2, val2, arg2, modsArr2],
};
const cNested: vDVs = {
  on: {
    click: e=>{},
    mousemove: e=>{}
  },
  model: {
    val,
    val2
  },
  // @ts-expect-error
  foo: ['aa', 'a', 'a', 'a', 'a']
}

The fun starts when types like directive names, modifiers are extended with string literals, event names/handlers are mapped with e.g. WindowEventMap and we can write utility functions to generate the structures. These are just proof of concept, since I wanted to see how the typings would hold together in regards to complexity vs. flexibility. Hopefully it can help someone else too and maybe someone with better TS/TSX skills can guide this to a better/cleaner path, these are just the collections of most of the ideas we've had so far. :)

@bjarkihall Great and comprehensive explanation, thanks, did you take a look at https://github.com/HcySunYang/vue-next-jsx? Actually that is our goal, it has dts testing: test-dts and runtime support: runtime, maybe you can contribute something if you are interested.

@HcySunYang yes I took a look at your repo and @Amour1688 's too but I'm a bit confused since there are two repos and I rather want the design to be fully agreed upon - but since a great idea of a syntax can also be impractical to implement/type, we definitely need to do some experimentation, I've seen some neat approaches and I really like the playground you provided.

It's also a bit complicated to contribute in general since the vue jsx typings reside in the vue-next repo, and it also has some tests for jsx while @wonderful-panda 's "vue-tsx-support" repo has some great typings too, which could likely benefit the ones in the official vue-next repo - can't there just be a single source where we can branch from and experiment and make PRs? :)

Where would the official typings, tests and plugin end up, once most of the features have been decided upon and before Vue 3 is released?

@bjarkihall We can extend the type without intruding into vue-next, I have made this PR, the type of jsx can be completed on this basis

In this thread, I learned a lot of knowledge that I didn't realize before, especially the points of @bjarkihall and @wonderful-panda and their past practices. Now I think we have reached a preliminary consensus and we need to work together. @yyx990803 Maybe we need to be in the same repo and contribute together instead of implementing these functionalities separately., this will only bring more confusion to those who want to contribute and users, if that is possible, then, I am willing to archive the vue-next-jsx project and issue a statement to indicate the correct way to the user.

It would be nice to compile all of the above into a single spec and then we can get to work if we have a proper spec done.

P.S. Looks like we can only achieve consensus if we make a spec & same RFC process as Vue 3 went through.
P.P.S. I intentionally decided not to participate in the opinion battle over JSX design since I barely used it the last 8 months.

commented

Sorry to barge in I literally just discovered yesterday I could use jsx and even the jsx on vue2 is a bit confusingspecially the slots part.

I didn't see any mentions of slots here were they deprecated in vue3?

Additionally are there any plans to just have a single jx source of truth?

I have seen at least some 3 jsx repos and then you have the jsx doc page on Vue which doesn't have the same examples as this page.

I tried yesterday understanding how to use slots and I saw so many different syntaxes and the whole scoped slots thing which was also deprecated.

This issue seems to be very hot right now so I just wanted to leave my feedback/questions for all you guys.

Also regarding the syntax for this issue the best one so far is the latest one which has vModel , vDirective and then it receives the same thing (what is hard is understanding what each argument is and it ends up being a bit weird to have an array of strings at the end why not flat that out as the last event and just gather it as an array in the inside. )

@dagadbm if you're talking about spreading (not flattening) the mods array I suggested it in "ev2" in a comment above - you'd always have to know if you're either giving arg + mods (string, string[]) og just mods (string[]) - so if you want to spread the mods array, you'd need to know there's no arg first by either always expect it:

<Comp vDir={['demo', value, undefined, 'a', 'b']} /> // v-demo.a.b="value" - you can also write ['demo', value, , 'a', 'b'] or ['demo', value,  '', 'a', 'b'] which is harder to spot

or defining either new attribute (I leaning towards limiting the number of possible attributes since they just confuse things) or a helper function (ev2) which doesn't expect the arg:

<Comp vDir={dirWithMods('demo', value, 'a', 'b')} /> // v-demo.a.b - dirWithMods(directive: string | Directive, value: any, ...mods as string[])

I guess the previous one can be typed (and inferred) properly and it's the least of things to learn and doesn't need any array, so maybe it's clearest: [dir?, val?, arg?, ...mods?][] | { [dir: string]: { [arg: string]: [val, ...mods] } }, although hard to spot the difference between args & mods in the spread types - you'd have to know what's in 3rd vs 4th index of the tuple.

Regarding slots, they've been discussed here - pass the object syntax as children, it's even possible in react and is explained here, it's a similar pattern to render props (slots is just an object of named render functions).

JSX should work straight away as an h/createVNode-wrapper/factory and TSX could even work without babel.
What this thread is mostly discussing is support for better typing and additional syntactic sugar via v- attributes (abstractions for directives, models, events, arguments, modifiers, etc).

The typings need to be worked on since right now JSX.Element expects any to be a valid element, slots are not inferred like emits, FunctionalComponents return any, declareComponent does not validate the return value either, etc.

I think we should start by defining VueElement interface which JSX.Element can extend (no special v- attributes to start with) and clean up the types a bit. If we have a solid JSX core with typescript support we can carefully add attributes like vDirective, vOn, vModel, etc.
This would break some tests like this one but this would also fail in React, since number is not a valid ReactElement:

const el: VueElement = 0 // TS-error
const el2: VueElement = <>{0}</> // valid

so:

import {FunctionalComponent, VueElement} from "vue-next/jsx" // FunctionalComponent: (props?, ctx?) => VueElement
const FC: FunctionalComponent = () => 0 // should be invalid since JSX.Element can only extend an interface
const FC: FunctionalComponent = () => (0 as unknown as VueElement) // should work
const FC2: FunctionalComponent = () => <>{0}</> // valid

I'm still just reading through the v3 docs and just experimenting with vue-next repo locally, but it's promising.

Guys, as an active user of jsx in Vue, I would like to tell my opinion:

  1. It should be just like plain jsx (like react, yes, don't reinvent the wheel)
  2. Stop design jsx api keeping in mind templates

As @yyx990803 mentioned early – people who decided to use jsx just hate templates.

Thanks.

@isnifer plain JSX should already work in Vue, just like React:

const el = <div onClick={e=>console.log(e)}></div> // const el = h('div', {onClick: e=>console.log(e)})
const StatelessComp = (props, {attrs, slots}) => <div {...props} {...attrs}>{slots}</div> // const StatelessComp = (props, {attrs, slots}) => h('div', {...props, ...attrs}, slots)

The problem is TSX and the amount of any types used for JSX.Element (like FunctionalComponent, defineComponent(fn), RenderFunction, etc) and Vue types/functions that expect a vNode, like withDirectives - if you pass a JSX.Element into these you get:

Argument of type 'Element' is not assignable to parameter of type 'VNode<RendererNode, RendererElement, { [key: string]: any; }>'. Type 'Element' is missing the following properties from type 'VNode<RendererNode, RendererElement, { [key: string]: any; }>': __v_isVNode, __v_skip, type, props, and 18 more.ts(2345)

The discussion has mostly been about being able to use TSX with Vue as a replacement for createVNode/h, not template syntax itself - just like it abstracts createElement in React, and I was suggesting we skipped all "magic" as a starter and actually made things just work like they do in React TSX.

The additional attributes (vModel, vOn, vShow, etc) are reserved/custom directives, which are a Vue render function feature (not just in templates).
You can wrap vNodes in functions to give them directives like described above, but alternatively we've been discussing props-syntax (vDir) which would not require any import, would be type-safe, unified and easy for the runtime to apply - the transformation step should be minimal, if any. But this shouldn't really be rushed, I think.

Also, "hating templates" isn't the only factor of using JSX in Vue. It creates a nice transition to the library for React users, allows you to get a lower-level access to component creation, allows you to gain greater DX since it has support from TS itself and a huge ecosystem with solutions like CSS-in-JS, which some prefer, it also allows you to create multiple small components per file, etc.
SFCs could get better with some additional DX. It's just they need a .vue file support through their own extension like Vetur, which struggles to provide TS features like renaming symbols and each feature (like intellisense, type-checking, color highlighting, etc) has to play catch-up with every .js/.ts feature - the competition hasn't really been fair, but it's great to have the option of using TSX instead of SFC/templatestrings/html, for those who prefer it.

I'd like to not support directives to push the community going in the right way(ok ok, directive is not the wrong way) and to simplify the vnode object structure.

As to better DX, we can just:

const count = ref(0);
const {value, onChange} = v_model(count);

// v-model: <input v-model="count" /> to
<input {...v_model(count)} />
// but it seems things like <input {...v_model(reactive_obj.count)}/> is impossible

// v-custom: <div v-loading="is_loading"></div>
<Loading is_loading={is_loading}><div></div></Loading>

Greetings, I've done some testing with Vue 3 and JSX today and found a difference between the behavior of Vue.h and h functions from other popular Virtual Dom Libraries (React, Preact, etc).

Since the Vue 3 JSX Discussion is going on here I thought I would add a comment here rather then open a new formal issue on the main https://github.com/vuejs/vue-next repository.

Below are example code and demo links.

Online Babel Repl
https://babeljs.io/repl

This valid JSX:

// @jsx Vue.h
function App() {
    return (
        <Vue.Fragment>
            <h1>Hello World</h1>
            <div>Test with Vue 3</div>
        </Vue.Fragment>
    )
}

Vue.render(
    <App />,
    document.getElementById('root')
);

Is compiled to this by Babel

// @jsx Vue.h
function App() {
  return Vue.h(Vue.Fragment, null, Vue.h("h1", null, "Hello World"), Vue.h("div", null, "Test with Vue 3"));
}

Vue.render(Vue.h(App, null), document.getElementById('root'));

The issue is that an array is currently required for the 3rd parameter in Vue while Babel generates each additional child element as an additional function argument.

// Will not work with Vue 3 (RC 5)
// Vue.h(type, props, ...children)
Vue.h(Vue.Fragment, null, Vue.h("h1", null, "Hello World"), Vue.h("div", null, "Test with Vue 3"));

// Will work with Vue 3
// Vue.h(type, props, [children])
Vue.h(Vue.Fragment, null, [Vue.h("h1", null, "Hello World"), Vue.h("div", null, "Test with Vue 3")]);

Working Demo - Vue 3 online with JSX
I found this because I developed a online small JSX Compiler and was testing Vue 3 with it. For background the JSX Compiler jsxLoader can be used by modern browsers instead of Babel (for IE it downloads Babel Standalone). In my demo I modify the Vue.h function for compatibility with Babel Standalone and the jsxLoader that I wrote.

Original React and Preact Versions of the above page

Changes needed for Vue 3
I tested vue-next (RC 5) and adding the extra 2 lines of code does not break any existing unit tests, however to properly add it additional unit test and TypeScript definitions have to be added. (I've tested that locally as well and can make it work).

export function h(type: any, propsOrChildren?: any, children?: any): VNode {
  if (arguments.length > 3) {
    return createVNode(type, propsOrChildren, Array.from(arguments).slice(2));
  } else ...

@yyx990803 Can this be a feature Vue.h(type, props, ...children) be supported in Vue 3? Personally I think it should as it allows Vue 3 to behave similar to other JSX Libraries out of the box. If it can be supported I would be willing to submit the needed changes and unit tests.

For example

function s(el: RendererElement) {
  if (el.props && Reflect.has(el.props,'v-show')) {
    let val = el.props['v-show']
    Reflect.deleteProperty(el.props, 'v-show')
    return withDirectives(h(el), [[vShow, val]])
  } 
  return el
}
 {s(<span v-show={false} >test</span>)}

Syntax sugar is not necessary

@cereschen, the syntax sugar is being discussed so you don't need to wrap your components with a function or even implement it yourself - of course it's not "necessary", it's just to remove the need of the extra syntax for the user.

In your case you're telling vue that s is supposed to modify the behavior of the component and vShow is an undefined behavior which s handles and extracts. Why would you even want to declare this simple behavior in 2 places: in the component, where it's removed either way and in the implementation of the wrapper?

// vShow.ts:
import {withDirective, vShow} from 'vue'
export function vShow(el: RendererElement) {
  if (el.props && Reflect.has(el.props,'vShow')) {
    const val = el.props['vShow']
    Reflect.deleteProperty(el.props, 'vShow')
    return withDirectives(h(el), [[vShow, val]])
  } 
  return el
}

// Comp.tsx:
import {vShow as s} from 'vShow'
const Comp = () => s(<span vShow={false}>test</span>)

But you could just implement s like this:

import {vShowSimple as s} from 'vShow'
const Comp = () => s(<span>test</span>, false)

Or have the syntax sugar just take care of vShow without any import:

const Comp = () => <span vShow={false}>test</span>

vShow is just being used as the simplest directive to argue about, since it doesn't accept any argument or modifiers and has a simple boolean value.
As soon as you start adding more complexity/flexibility, you quickly start implementing more cases and wonder why this isn't just handled by vue instead.

I don't think JSX transformation should do too much magic, it's just a higher level syntax for h/createVNode - a Babel plugin could just add performance optimizations but nothing more, starting using vue with JSX/TSX shouldn't require too much setup or tools to get it working.
The main lack of JSX support is simply in the typings (you can get away with way too much without the IDE/TSC ever warning you of any syntactic errors) and how directives (including models etc) are applied to vNodes - I'm okay with it being wrapper-function based and later finding a way to have them abstracted with reserved props, if it doesn't affect performance too much, but that would not be a part of the JSX plugin but the vNode interpretation itself.

@bjarkihall
Indeed, I found this approach problematic when dealing with v-models, but the reason I did it was vite doesn't work with babel loader
In fact, my way is just to wrap a function around the root, which can be done recursively, and not just v-show
But as I said variable passing is always a problem

@isnifer But JSX reinvented the wheel. Template brings the original wheel back.

Hi @ConradSollitt, your problem has been solved, will see it in the next release rc8.

I understand perfectly that it is not pleasant to use vue 3 like react using jsx / tsx, I love using templates but I consider that we have to give all possible options to the user. But if we don't have real alternatives ... I prefer to use 'h' like react /preact until we have our own 'h' (vh, vueh, createElement...).

Here I put some examples where the vue 3 syntax can be applied or not (v-if, v-for, v-show...):

vue3 ( webpack + babel): it's possible. Using vue-next-jsx.

vue3 (webpack + babel): it's not possible. Without vue-next-jsx.

vue3 (without build / transform): it's not possible (Maybe in the future 2020). Example:
vue3.

vite (esbuild / rollup): it's not possible now (Maybe in the future 2020). Actually, the file vuejsxcompat (the fix of @ConradSollitt) is used to work with h like react / preact. Now with the fix in vue 3, this file could be deleted.

Possible changes in esbuildService

/// transform esbuild
vue: { jsxFactory: 'h', jsxFragment: 'Fragment' }
// change concatenated
code += "\nimport { h, Fragment } from 'vue'"
// In the near future (own design)
vue: { jsxFactory: 'vh', jsxFragment: 'Fragment' }

In summary, people will do what they want.

Have a nice day 😊

commented
<Button onClick={handleClick} modifier={['passive', 'capture', 'once']}>btn</Button>

Add an new attribute to use with directives.
new attribute is Optional!

Just wanted to point out, TS 4 is released so we can have named tuples now.
Here's an idea of steps to take to make the DX a bit nicer:

  1. Fix the TSX support (typings). It's a bit awkward to work with slots, emits and just components outside of .vue files in general.
  2. Add opt-in Directives support (built-in and custom directives, event modifiers and models) through custom attributes/props (vDirective, vModel, vOn, vShow, etc.) with named tuples which the runtime can apply to the vnode.
  3. HMR support - is it implemented in vue (in the h function), the build tool (e.g. vitejs, webpack), or the babel JSX plugin?
  4. Babel plugin can add performance flags and optionally more features, but I'd really like to be able to just use tsc to compile and run a vue project without any extra compilers, just .js/.jsx/.ts/.tsx files.

Thanks @ivanheral and @yyx990803 for making this update!

I’ve reviewed the code change and verified that the demo I created works in several browsers directly from a CDN using Babel Standalone or the JSX Complier I wrote. I don’t expect to develop large Vue 3 apps this way myself (or many people to do so) but it’s nice to know the option exists. Personally Vue Templates are my favorite and I found at work that non- developers (for example Data Scientist with some JS knowledge) can develop dynamic apps with Vue as they could understand the templates much quicker than they wanted to spend learning detailed DOM API’s or JSX.

In addition to the recent JSX testing I keep verifying new updates of Vue 3 with a variety of Vue 2 code I wrote and so far everything is working nicely!

wrapper it with 'h', could be a good option.

Example updated with v-show: vue3+v-show

import { createApp, ref, h } from "https://unpkg.com/vue@next/dist/vue.runtime.esm-browser.prod.js";
import htm from "https://unpkg.com/htm?module";
const html = htm.bind(h);

const App = {
  setup() {
    const show = ref(true);
    const show_hide = () => {
      show.value = !show.value;
    }
    return () => html `
        ${ show.value ? html`<span style="display: block;">Hello React</span>` : null }
        <span style="display: block;" v-show="${show}">Hello Vue 3</span>
        <button style="display: block;" onClick=${show_hide}>show/hide</button>
      `
  }
}
createApp(App).mount("#app")

With the upcoming template string type, we can have fully type checked JSX experience in Vue.

I hope we can bring issue "HMR" in top priority.
I really love use TSX in project replace Vue template because for now Vetur in VScode make code hint really slow, specially when working in large Vue app. For my current project making admin, i need wait avg 3 ~ 4 seconds after stop typing to see vetur validate template/ts code, this issue not happend if writing in .tsx file because vscode make it really fast as hell. Only biggest problem when writing TSX for now is Vue v3 not support hmr, reload full page everytime make a chagne only ok when project small but fast run in issue when bigger.

If Vue team can find quick solution (or plugin webpack, maybe) support HMR feature it will be best! Other issue like directive - slots etc we had workaround solution really, so we can wait a little longer.

any news about HMR support for jsx ?

There is some discussion around slots here, but it's a bit hard to follow.

If you're using JSX, I would argue there is no reason to have slots vs props / renderProps (fn=scoped slot).

However, I am mostly curious if we will gain the ability to pass the default slot as a prop rather than as the child of an element.

This becomes very useful in React, the ability to use the children prop.

Thanks

Hi, I'm new to Vue and trying to figure out best practices.
Ideally, I'd like to have type checking at compile-time.
But that seems tricky. The template code in my components only checks its proptypes at runtime.

Coming from React, I'm familiar with JSX and kind of liked it.
So I thought I'd try using Vue with it.
But the way it's supported is confusing to me.
The render functions section in the official docs seems rather verbose.
And the Babel JSX plugin for Vue doesn't seem like the official way to go.

I've tried reading this github issue but am a bit lost.
It would be great if someone could provide a bit more context.
What's the story on TSX in Vue?
What is missing from the current tooling and why do we need those Typescript 4.1 template strings?

Hi, I'm new to Vue and trying to figure out best practices.
Ideally, I'd like to have type checking at compile-time.
But that seems tricky. The template code in my components only checks its proptypes at runtime.

Coming from React, I'm familiar with JSX and kind of liked it.
So I thought I'd try using Vue with it.
But the way it's supported is confusing to me.
The render functions section in the official docs seems rather verbose.
And the Babel JSX plugin for Vue doesn't seem like the official way to go.

I've tried reading this github issue but am a bit lost.
It would be great if someone could provide a bit more context.
What's the story on TSX in Vue?
What is missing from the current tooling and why do we need those Typescript 4.1 template strings?

hey @lhk . what do you expect from using tsx with vue ?
right now I am using tsx with vue 2 (not vue 3). and with the vue class component plugin. so it is less verbose than the docs (at least for vue 2).

As for vue 3 simply setup a new project with vue cli . select vue 3 and typescript when generating the application. and then install this plugin. then setup the plugin in babel config.

after the setup . you just make new tsx file and add some thing like

import { defineComponent } from 'vue'

const Component = defineComponent({
  setup() {
     return () => (<div>hi</div>)
  }
})

or

import { defineComponent } from 'vue'

const Component = defineComponent({
  render() {
     return (<div>hi</div>)
  }
})

jsx for vue 3
https://github.com/vuejs/jsx-next#installation

Hi everybody, what is the difference in vue 3 to use or not use the defineComponent method?

export const ComponentOne  =defineComponent({
setup(){
   return () => <div>Hi</div>
   }
});

//and 

export const ComponentTwo  ={
 setup(){
     return () => <div>Hi</div>
    }
}

Both cases work just fine