Vue3

Vue3 is the latest iteration of the Vue framework.

The attraction for me is:

  • Component based development.
  • Each component can be encapsulated in a single (.vue) file that encapsulates:
    • the presentation (Templated HTML),
    • view logic script (JavaScript or Typescript),
    • style (SCSS).
  • A model can be built outside of the Vue component.
  • Single code base for front end client.
  • Typescript support.
  • An unobtrusive reactivity system.
  • Node based development environment using VSCode.

The downside is that the documentation and examples are oriented towards JavaScript and not using the Vue components.

This post shows and example of getting started with typescript. In particular:

  • Extra steps to add to the template project.
  • Instantiating a hierarchy of .vue components.
  • Passing data and events between components
  • Adding a model that is isolated from the framework.
  • Importing 3rd party JavaScript plugins.

This post is not definitive, it is a scratchpad for experimenting.

The associated source for this post is vue3-typescript-hello-world-plus-alpha.

Getting started.

Install the vue cli. I’m using npm.

I used GitHub to create my repo so I want to merge with that. Also using my local ‘foo’ presets for typescript support.

$ vue create --merge vue3-typescript-hello-world-plus-alpha
> foo ([Vue 3] babel, typescript, eslint)

Updating the Template

The template has been changed for this example:

  • src/components/HelloWorld.vue removed.
  • src/components/InputParam.vue added - this will be the data input form.
  • src/components/OutputTable.vue added - this will be the output view.
  • src/model/LoanCalc.ts added - this is a simple data model and algorithm.

Adding Components to main.ts

The template code imports the component into App.vue. It seems easier to import components into main.ts. The components imported in main.ts will be in scope for all other components.

e.g. Import in App.vue:

import InputParam from './components/InputParam.vue';

@Options({
  components: {
    InputParam,
  },
})

Import in main.ts:

import InputParam from './components/InputParam.vue';

const app = createApp(App);
app.component('InputParam',InputParam);
app.mount('#app')

I’m not sure which method is best.

Adding Code Outside of the Vue3 Tree

I’d like to make sure the core logic of the application is not mixed in with the Vue presentation.

Update App.vue to instantiate the model.

e.g. Create a model/LoanCalc.ts to compute loan interest.

Import.

import {LoanCalc} from "./model/LoanCalc"

Instantiate the loan_calc object in App.

  • Declare the loan_calc as a member of App. This is declared with a definite assignment assertion via !. This is as it is not initialized until vue calls data().
  • Use data() method to instantiate the class and return it via a Record<string, unknown>. Doing it here ensures reactivity.
export default class App extends Vue {
     loan_calc!: LoanCalc;
     data(): Record<string, unknown> {
         var loan_calc = new LoanCalc(...);
         return {
             'loan_calc' : loan_calc,
         }
     }
}

Parameters for Components

In the component

The components/InputsParam.vue takes a string to use a heading and a custom object (LoanParam) with a set of parameters.

The @Options decorator defines the component options.

  • This is optional
  • The props define attributes that can accept data from the parent component.
import { Options, Vue } from 'vue-class-component';
import {LoanParams} from "../model/LoanCalc"

@Options({
  props: {
    msg: String,
    params: LoanParams
  }
})

The parameters are bound to the component class members:

export default class InputParam extends Vue {
  msg!: string;
  params!: LoanParams;
}

Then the template in the component can in turn use and change the parameters. As they are reactive the changes are propagated up and down the hierarchy.

<template>
<h1></h1>
  Interest Rate: <input v-model.number="params.rate_percent" type="number" /> 
</template>

In the parent

The parameters are passed in App.vue from the component instantiation.

  • The colon (:) is short is a shorthand for v-bind and binds the object (pass by reference? value semantics??).
  <InputParam :params=loan_calc.params msg="Welcome to Your Vue.js + TypeScript App" />

Computed Results

This is where Vue does it’s magic. Provided the data and results are exposed to Vue the (reactivity system)[https://v3.vuejs.org/guide/reactivity.html#what-is-reactivity] takes care of the update.

In this example:

  • The InputParam.vue component does data entry.
  • The OutputTable.vue displays the results.
  • The App.vue connects these.
  • The src/model/LoanCalc.ts has a simple model with classes for each action above.

The starting point in the input parameters LoanParams. These are instantiated in App.vue’s data() method. The use of data() will allow Vue to wrap the class with a reactive proxy.

The reactivity proxy will :

  • Ensure anything derived from LoanParams (e.g. LoanResults) is also wrapped in a reactive proxy.
  • Build up a graph of dependencies and distribute events.
  • When data() is called the reactivity is linked to the InputParams.vue.
  • When results() is called a the reactivity is linked to the OutputTable.vue.
<script lang="ts">
import { Vue } from 'vue-class-component';
import {LoanCalc, LoanParams, LoanResults} from "./model/LoanCalc"

export default class App extends Vue {
     loan_calc!: LoanCalc;
     loan_params!: LoanParams;
     data(): Record<string, unknown>  {
         var params = new LoanParams(500000, 25, 3.0);
         var loan_calc = new LoanCalc(params);
         return {
             'loan_calc' : loan_calc,
             'loan_params' : params,
         }
     }
    results(): LoanResults {
        return this.loan_calc.calc();
    }
}
</script>

The template can simply access the members of the component class and pass the reactive versions to the component.

<template>
  <InputParam :params=loan_params msg="Input Parameters" />
  <OutputTable :results=results() msg="Calculated Results" />
</template>

Importing JavaScript plugins

Add a 3rd party package.

e.g Add vue3-money.

Update package.json:

  "dependencies": {
    "v-money3": "^3.19.1",
    ...
   }

Need to add a typings directory for local type definitions. See adding-custom-type-definitions-to-a-third-party-library.

Update to tsconfig.json

    "typeRoots": 
       [ "./typings", "./node_modules/@types"],
    "include": [           
        "typings/**/index.d.ts",
         ...
    ]

The index.d.ts file in it’s simplest form imports everything.

Add typings/v-money3/index.d.ts.

declare module 'v-money3' 

Or it might import/export explicit symbols.

{
    import money, { Money3Component, Money3Directive } from 'v-money3';
    export default money;
    export { Money3Component, Money3Directive };
  }

Then update main.ts to enable globally.

import money from 'v-money3'

...

app.use(money)

Formatting Text

Filters are depreciated in vue3. Instead normal functions can be exported.

Methods of the component class can be called from the template.

<template>
...
      <tr>
      <th>Total Payments</th>
      <td>
      </td></tr>
      <tr><th>Monthly Payment</th>
      <td></td>
      </tr>
...
</template>

The methods are defined in the default exported class:

import {format as money_format} from 'v-money3';

export default class OutputTable extends Vue {
    ...
    alias_money_format = money_format;
    format(v: number) : string {
      return "$" + money_format(v);
   }
}

Verbose logging

By default console.log() does not go to the console.

export ELECTRON_ENABLE_LOGGING=1
npm run serve

Not Sure if These Are Needed?

Adding the typescript-eslint plugin.

This is not used in the example for this article, but has helped when using vscode.

  plugins: ['@typescript-eslint'],
  // Prerequisite `eslint-plugin-vue`, being extended, sets
  // root property `parser` to `'vue-eslint-parser'`, which, for code parsing,
  // in turn delegates to the parser, specified in `parserOptions.parser`:
  / /https://github.com/vuejs/eslint-plugin-vue#what-is-the-use-the-latest-vue-eslint-parser-error
  parserOptions: {
    ecmaVersion: 2020,
     parser: require.resolve('@typescript-eslint/parser'),
     extraFileExtensions: ['.vue'],
         ecmaFeatures: {
          jsx: true
    }
  }
  extends: [
    'plugin:@typescript-eslint/eslint-recommended',
  } 

Update Rules

Add some exceptions to the linter. Better to fix the errors, but here for later reference.

overrides: [{
    files: ['*.ts', '*.tsx'],
    rules: {
      // The core 'no-unused-vars' rules (in the eslint:recommeded ruleset)
      // does not work with type definitions
      'no-unused-vars': 'off',
      'no-empty-function': 'off',
        'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
        'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
    }
  }]

References

Vue3

  • https://v3.vuejs.org/guide/typescript-support.html

Vue3 + Typescript:

  • https://davidjamesherzog.github.io/2020/12/30/vue-typescript-decorators/
  • https://github.com/Yama-Tomo/vue-vuex-typescript-sample
  • https://github.com/vuejs/vue-test-utils-typescript-example
  • https://www.detroitlabs.com/blog/2018/02/28/adding-custom-type-definitions-to-a-third-party-library/
  • https://github.com/vuejs/eslint-plugin-vue#what-is-the-use-the-latest-vue-eslint-parser-error
  • https://www.thisdot.co/blog/your-first-vue-3-app-using-typescript