Taking on a different Vue with TSX

The logo for Vue.js

Vue is an awesome framework that has been gaining traction in the Open Source community for quite some time. It borrows (or steals depending on who you ask) some of the best features of today’s most popular frameworks, including Angular, Polymer, and React. One such feature taken from React is the ability to write markup for components using JSX.

TypeScript is an equally awesome superset of JavaScript, meant to add optional static typing to the language. It also adds a lot of value to by the way of TSX, or Typed JSX. This article describes how to get the best of both worlds: writing Vue components using TypeScript and TSX.

If you’ve mastered the art of just reading code, here’s the source.

Installation

Run the following commands in the project root:

# Install through NPM... 
npm i vue vue-class-component vue-property-decorator
npm i babel-core babel-plugin-jsx-v-model babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx babel-preset-latest fuse-box typescript -D
# ... or Yarn
yarn add vue vue-class-component vue-property-decorator
yarn add babel-core babel-plugin-jsx-v-model babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx babel-preset-latest fuse-box typescript -D

I will not go into detail of what all of these dependencies do, but the most important ones that you do not often see:

  • fuse-box — our extremely fast, simple bundler
  • vue-property-decorator — some decorators that simplify how we define components, properties, etc.
  • babel-plugin-transform-vue-jsx — transforms our Vue JSX to use Vue’s createElement function. This function differs slightly from React’s.

Configuration

For the sake of brevity, the only two pieces used for configuration will be the tsconfig.json and the fuse.js files. Create a tsconfig.json file and add the following:

{
"compilerOptions": {
"target": "es2015",
"module": "commonjs",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"lib": [
"es2016",
"dom"
],
"jsx": "preserve",
"noUnusedLocals": true,
"noUnusedParameters": true
}
}

The important piece to note here is the preservation of JSX. This is done to allow Babel to handle JSX transpilation.

Next, create a fuse.js file and add the following:

const { FuseBox, BabelPlugin, WebIndexPlugin } = require('fuse-box')
const fuse = FuseBox.init({
sourceMaps: true,
homeDir: './src',
output: 'dist/$name.js',
plugins: [
BabelPlugin({
config: {
presets: ['latest'],
plugins: ['jsx-v-model', 'transform-vue-jsx']
}
}),
WebIndexPlugin({ template: './src/index.html' })
]
})
fuse.dev()
fuse.bundle('app.js').instructions('>index.ts').watch()
fuse.run()

Notice the use of the transform-vue-jsx plugin that was described earlier. If you’re unfamiliar with FuseBox, read about it here. It should be fairly simple to follow, especially if you are familiar with webpack.

Writing some components

With the configuration complete, create a component in src/components. Call it App.tsx and add the following:

import * as Vue from 'vue'
import { Component, Watch } from 'vue-property-decorator'
@Component
export default class extends Vue {
text = ''
  render() {
return (
<div>
<input type="text" placeholder="Input your name" v-model={this.text} />
</div>
)
}
  @Watch('text')
onInput() {
console.log(this.text)
}
}

If you are unfamiliar with vue-property-decorator, there is a great explanation of its use in this article. The Component decorator is used to define the Vue component and the Watch decorator is used to subscribe to changes to the text property. Also, note the use of v-model. It would not be possible jsx-v-model plugin.

In the src directory, create index.ts and index.html files. Add the following to index.ts:

import App from './components/App'
import * as Vue from 'vue'
new Vue({
el: 'main',
render: h => h(App)
})

And then add some code to the index.html:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Vue TSX Example</title>
</head>
<body>
<main></main>
$bundles
</body>
</html>

We should now be able to run our application using node fuse.js and be met with a quick win.

App.tsx

You should also be able to see text entered into the input field in the browser console.

Now let’s get a little more advanced and add a child component to the example. Create another component called Hello.tsx and add the following:

import * as Vue from 'vue'
import { Component, Prop } from 'vue-property-decorator'
@Component
export default class extends Vue {
@Prop() name
  render() {
if (this.name)
return (
<div>
Hello {this.name}!!!
</div>
)
    return <div>What's your name?</div>
}
}

The syntax should look similar to App.tsx. The minor difference being the use of Prop decorator. This allows the component to receive a name property from its parent.

Finally, change App.tsx to resemble the following:

import * as Vue from 'vue'
import { Component, Watch } from 'vue-property-decorator'
import Hello from './Hello'
@Component
export default class extends Vue {
text = 'World'
  render() {
return (
<div>
<Hello name={this.text} />
<input type="text" placeholder="Input your name" v-model={this.text} />
</div>
)
}
  @Watch('text')
onInput() {
console.log(this.text)
}
}

After adding the Hello component to the render function, refresh your browser and you will be met with the following:

Finished Product

Conclusion

The approach of using Vue with .tsx files is a departure from the norm, but it is also a perfect marriage that keeps Vue’s single file component goal intact while dropping a little baggage along the way (template/script/style tags and the like).

Hopefully you will consider this article useful. You now have a fully functioning example with everything that you should need to be truly dangerous. Happy trails!!!

Sources

Writing Vue.js Render Functions in JSX

Use Vue.js Template Niceties in JSX Components

Writing Class-Based Components with Vue.js and TypeScript