wonderful-panda / vue-tsx-support

TSX (JSX for TypeScript) support library for Vue
MIT License
578 stars 40 forks source link
jsx tsx typescript vue

npm version build

vue-tsx-support

TSX (JSX for TypeScript) support library for Vue

:warning: BREAKING CHANGES

If your project already uses vue-tsx-support v2, see Migration from V2 section.

TABLE OF CONTENTS

NEW FEATURES

PREREQUISITE

INSTALLATION

  1. Create Vue project with TypeScript and babel support.

    vue-tsx-support is a type checker for TypeScript, not a transpiler.
    You must install babel presets (@vue/babel-preset-app or @vue/babel-preset-jsx) separatedly.

    Vue CLI may help you.

    :bulb: If you want use @vue/composition-api, @vue/babel-preset-jsx >= 1.2.1 or babel-preset-vue-vca is needed.

  2. Install vue-tsx-support from npm

    yarn add vue-tsx-support -D
  3. In tsconfig.json, set "preserve" to jsx and "VueTsxSupport" to jsxFactory

    {
      "compilerOptions": {
        "jsx": "preserve",
        "jsxFactory": "VueTsxSupport",
        "...": "..."
      },
      "include": [
        "..."
      ]
    }
  4. import vue-tsx-support/enable-check.d.ts somewhere,

    import "vue-tsx-support/enable-check"

    or add it to "include" in tsconfig.json

    {
      "compilerOptions": {
        "...": "..."
      },
      "include": [
        "node_modules/vue-tsx-support/enable-check.d.ts",
        "..."
      ]
    }

Migration from V2

  1. In tsconfig.json, set "VueTsxSupport" to jsxFactory

  2. Enable allow-props-object option (Optional)

USAGE

Intrinsic elements

Standard HTML elements are defined as intrinsic elements. So, compiler can check attribute names and attribute types of them:

// OK
<div id="title" />;
// OK
<input type="number" min={ 0 } max={ 100 } />;
// OK
<a href={ SOME_LINK } />;
// NG: because `href` is not a valid attribute of `div`
<div href={ SOME_LINK } />;
// NG: because `id` must be a string
<div id={ 1 } />;

Lower case tags are treated as unknown intrinsic element. TypeScript checks nothing for such tags.

// OK
<foo id="unknown" unknownattr={ 1 } />

Components

Basically, vue-tsx-support checks three types for each component.

Make existing components tsx-ready.

By default, vue-tsx-support does not allow unknown props.
For example, below code causes compilation error.

  import Vue from "vue";
  import AwesomeButton from "third-party-library/awesome-button";

  export default Vue.extend({
    render() {
      // ERROR: because TypeScript does not know that AwesomeButton has 'text' prop.
      return <AwesomeButton text="Click Me!" />;
    }
  });

You can add type information to existing component without modifying component itself, like below:

  import AwesomeButtonOrig from "third-party-library/awesome-button";
  import * as tsx from "vue-tsx-support";

  type AwesomeButtonProps = {
    text: string;
    raised?: boolean;
    rounded?: boolean;
  }

  // Now, AwesomeButton has 1 required prop(text) and 2 optional props(raised, rounded)
  export const AwesomeButton = tsx.ofType<AwesomeButtonProps>().convert(AwesomeButtonOrig);

You also can specify custom event types as second type parameter, and scoped slot types as third type parameter.

For example:

  import AwesomeListOrig from "third-party-library/awesome-list";
  import * as tsx from "vue-tsx-support";

  type Item = { id: string, text: string };

  type AwesomeListProps = {
    items: ReadonlyArray<Item>;
    rowHeight: number;
  }

  type AwesomeListEvents = {
    // member name must be ['on' + event name(with capitalizing first charactor)]
    onRowClicked: { item: Item, index: number };
  }

  type AwesomeListScopedSlots = {
    row: { item: Item }
  }

  export const AwesomeList = tsx.ofType<
    AwesomeListProps,
    AwesomeListEvents,
    AwesomeListScopedSlots
  >().convert(AwesomeListOrig);

Then you can use AwesomeList like below:

  import { VNode } from "vue";
  const App = Vue.extend({
  render(): VNode {
    return (
      <AwesomeList
        items={this.items}
        rowHeight={32}
        onRowClicked={p => console.log(`${p.item.text} clicked!`)}
        scopedSlots={{
          row: item => <div>{item.text}</div>
        }}
      />
    );
  }
  });

Writing components by object-style API (Like Vue.extend)

If you use Vue.extend(), just replace it by componentFactory.create and your component becomes TSX-ready.

Props type is infered from props definition automatically.
For example, props type will be { text: string, important?: boolean } in below code.

:warning: In some environment, as const may be needed to make prop required properly.

  import { VNode } from "vue";
  import * as tsx from "vue-tsx-support";
  const MyComponent = tsx.componentFactory.create({
    props: {
      text: { type: String, required: true },
      important: Boolean,
    } as const, // `as const` is needed in some cases.
    computed: {
      className(): string {
        return this.important ? "label-important" : "label-normal";
      }
    },
    methods: {
      onClick(event: Event) { this.$emit("ok", event); }
    },
    render(): VNode {
      return <span class={this.className} onClick={this.onClick}>{this.text}</span>;
    }
  });

:bulb: You can use component as as shorthand of componentFactory.create.

  import * as tsx from "vue-tsx-support";
  const MyComponent = tsx.component({
    /* snip */
  });

If your component has custom events or scoped slots, use componentFactoryOf instead.

  import { VNode } from "vue";
  import * as tsx from "vue-tsx-support";

  type AwesomeListEvents = {
    onRowClicked: { item: {}, index: number };
  }

  type AwesomeListScopedSlots = {
    row: { item: {} }
  }

  export const AwesomeList = tsx.componentFactoryOf<
    AwesomeListEvents,
    AwesomListScopedSlots
  >().create({
    name: "AwesomeList",
    props: {
      items: { type: Array, required: true },
      rowHeight: { type: Number, required: true }
    },
    computed: { /* ... */},
    method: {
      emitRowClicked(item: {}, index: number): void {
        // Equivalent to `this.$emit("rowClicked", { item, index })`,
        // And event name and payload type are statically checked.
        tsx.emitOn(this, "onRowClicked", { item, index });
      }
    },
    render(): VNode {
      return (
        <div class={style.container}>
          {
            this.visibleItems.map((item, index) => (
              <div style={this.rowStyle} onClick={() => this.$emit("rowClicked", { item, index })}>
                {
                  // slot name ('row') and argument types are statically checked.
                  this.$scopedSlots.row({ item })
                }
              <div>
            )
          }
        </div>
      );
    }
  });

Writing component by class-style API (vue-class-component and/or vue-property-decorator)

If you prefer class-style component by using vue-class-component and/or vue-property-decorator, there are some options to make it tsx-ready.

1. Extends from Component class provided by vue-tsx-support
  import { VNode } from "vue";
  import { Component, Prop } from "vue-property-decorator";
  import * as tsx from "vue-tsx-support";

  type MyComponentProps = {
    text: string;
    important?: boolean;
  }

  @Component
  export class MyComponent extends tsx.Component<MyComponentProps> {
    @Prop({ type: String, required: true })
    text!: string;
    @Prop(Boolean)
    important?: boolean;

    get className() {
      return this.important ? "label-important" : "label-normal";
    }
    onClick(event: MouseEvent) {
      this.$emit("ok", event);
    }
    render(): VNode {
      return <span class={this.className} onClick={this.onClick}>{this.text}</span>;
    }
  }

:warning: Unfortunately, vue-tsx-support can't infer prop types automatically in this case, so you must write type manually.

2. Add _tsx field to tell type information to TypeScript.
  import { VNode } from "vue";
  import { Component, Prop } from "vue-property-decorator";
  import * as tsx from "vue-tsx-support";

  @Component
  export class MyComponent extends Vue {
    _tsx!: {
      // specify props type to `props`.
      props: Pick<MyComponent, "text" | "important">
    };

    @Prop({ type: String, required: true })
    text!: string;
    @Prop(Boolean)
    important?: boolean;

    get className() {
      return this.important ? "label-important" : "label-normal";
    }
    render(): VNode {
      return <span class={this.className}>{this.text}</span>;
    }
  }

You can use DeclareProps<T> instead of { props: T }.

  import { Component, Prop } from "vue-property-decorator";
  import * as tsx from "vue-tsx-support";

  @Component
  export class MyComponent extends Vue {
    _tsx!: tsx.DeclareProps<Pick<MyComponent, "text" | "important">>;

    /* ...snip... */
  }

:bulb: PickProps is more convenient than Pick here, it removes attributes from Vue from completion candidates. (e.g. $data, $props, and so on)

  import { Component, Prop } from "vue-property-decorator";
  import * as tsx from "vue-tsx-support";

  @Component
  export class MyComponent extends Vue {
    _tsx!: tsx.DeclareProps<tsx.PickProps<MyComponent, "text" | "important">>;

    /* ...snip... */
  }

:bulb: When you can make all data, computed and methods private, you can use AutoProps instead.
AutoProps picks all public members other than members from component options(render, created etc).

  import { Component, Prop } from "vue-property-decorator";
  import * as tsx from "vue-tsx-support";

  @Component
  export class MyComponent extends Vue {
    _tsx!: tsx.DeclareProps<tsx.AutoProps<MyComponent>>

    @Prop({ type: String, required: true })
    text!: string;

    @Prop(Boolean)
    important?: boolean;

    // data
    private count = 0;
    // computed
    private get className() {
      return this.important ? "label-important" : "label-normal";
    }
    // methods
    private onClick() {
      this.count += 1;
    }

    render(): VNode {
      return (
        <span class={this.className} onClick={this.onClick}>
          {`${this.text}-${this.count}`}
        </span>
      );
    }
  }

:bulb: If your component has custom events, you can specify events handlers type additionally.

  import { Component, Prop } from "vue-property-decorator";
  import * as tsx from "vue-tsx-support";

  @Component
  export class MyComponent extends Vue {
    _tsx!: tsx.DeclareProps<PickProps<MyComponent, "text" | "important">> &
      tsx.DeclareOnEvents<{ onOk: string }>;

    /* ...snip... */
  }

:bulb: If your component uses scoped slots, you should add type to $scopedSlots by tsx.InnerScopedSlots.

  import { Component, Prop } from "vue-property-decorator";
  import * as tsx from "vue-tsx-support";

  @Component
  export class MyComponent extends Vue {
    _tsx!: tsx.DeclareProps<PickProps<MyComponent, "text" | "important">>;

    $scopedSlots!: tsx.InnerScopedSlots<{ default?: string }>;

    /* ...snip... */
  }

Writing component by composition api (@vue/composition-api)

Vue 3 is not supported. To use composition api with Vue 2, You can use @vue/composition-api.

There are 2 babel presets which support JSX syntax with @vue/composition-api.

To make TSX-ready component by composition api, use component of vue-tsx-support/lib/vca instead of defineComponent of @vue/composition-api.

  import { computed } from "@vue/composition-api";
  import * as vca from "vue-tsx-support/lib/vca";

  const MyComponent = vca.component({
    name: "MyComponent",
    props: {
      text: { type: String, required: true },
      important: Boolean,
    },
    setup(p) {
      const className = computed(() => p.important ? "label-important" : "label-normal");
      return () => (
        <span class={className.value}>{p.text}</span>;
      );
    }
  });

If your component has custom event or scoped slots, specify them types in 2nd argument of setup.

  import { computed, onMounted } from "@vue/composition-api";
  import * as vca from "vue-tsx-support/lib/vca";

  type AwesomeListEvents = {
    onRowClicked: { item: {}, index: number };
  }

  type AwesomeListScopedSlots = {
    row: { item: {} }
  }

  export const AwesomeList = vca.component({
    name: "AwesomeList",
    props: {
      items: { type: Array, required: true },
      rowHeight: { type: Number, required: true }
    },
    setup(p, ctx: vca.SetupContext<AwesomeListEvents, AwesomeListScopedSlots>) {
      const visibleItems = computed(() => ... );
      const emitRowClicked = (item: {}, index: number) => {
        // Equivalent to `ctx.emit("rowClicked", { item, index })`,
        // And event name and payload type are statically checked.
        vca.emitOn(ctx, "onRowClicked", { item, index });
      }

      return () => (
        <div class={style.container}>
          {
            visibleItems.value.map((item, index) => (
              <div onClick={() => emitRowClicked(item, index)}>
                {
                  // slot name ('row') and argument types are statically checked.
                  ctx.slots.row({ item })
                }
              <div>
            )
          }
        </div>
      );
    }
  });

OPTIONS

vue-tsx-support has some options which change behaviour globally. See under the options directory.

To enable each options, import them somewhere

// enable `allow-unknown-props` option
import "vue-tsx-support/options/allow-unknown-props";

:warning: Scope of option is whole project, not a file.

allow-element-unknown-attrs

Make enabled to specify unknown attributes to intrinsic elements

// OK:`foo` is unknown attribute, but can be compiled
<div foo="foo" />;

allow-unknown-props

Make enabled to specify unknown props to Vue component.

const MyComponent = vuetsx.createComponent<{ foo: string }>({ /* ... */ });
// OK: `bar` is unknown prop, but can be compiled
<MyComponent foo="foo" bar="bar" />;

enable-html-attrs

Make enabled to specify HTML attributes to Vue component.

const MyComponent = vuetsx.createComponent<{ foo: string }>({ /* ... */ });
// OK: `min` and `max` are valid HTML attributes
<MyComponent foo="foo" min={ 0 } max={ 100 } />;
// NG: compiler checks type of `min` (`min` must be number)
<MyComponent foo="foo" min="a" />;

enable-nativeon

Make enabled to specify native event listeners to Vue component.

const MyComponent = vuetsx.createComponent<{ foo: string }>({ /* ... */ });
// OK
<MyComponent foo="foo" nativeOnClick={ e => ... } />; // and `e` is infered as MouseEvent

enable-vue-router

Add definitions of router-link and router-view

allow-props-object

Make enabled to pass props as "props".

const MyComponent = vuetsx.createComponent<{ foo: string }>({ /* ... */ });
// OK
<MyComponent props={{ foo: "foo" }} />;

APIS

modifiers

Event handler wrappers which work like some event modifiers available in template

import { modifiers as m } from "vue-tsx-support";

// Basic usage:
//  Equivalent to `<div @keydown.enter="onEnter" />`
<div onKeydown={m.enter(this.onEnter)} />;

// Use multiple modifiers:
//  Equivalent to `<div @keydown.enter.prevent="onEnter" />`
<div onKeydown={m.enter.prevent(this.onEnter)} />;

// Use without event handler:
//  Equivalent to `<div @keydown.esc.prevent />`
<div onKeydown={m.esc.prevent} />;

// Use multiple keys:
//  Equivalent to `<div @keydown.enter.esc="onEnterOrEsc" />`
<div onKeydown={m.keys("enter", "esc")(this.onEnterOrEsc)} />;

// Use exact modkey combination:
//  Equivalent to `<div @keydown.65.ctrl.alt.exact="onCtrlAltA" />`
<div onKeydown={m.keys(65).exact("ctrl", "alt")(this.onCtrlAltA)} />;

Available modifiers

LICENSE

MIT