@haixing_hu/vue3-class-component

1.8.2 • Public • Published

vue3-class-component

npm package License 中文文档 CircleCI Coverage Status

This library allows you to create your Vue components using the class-style syntax. It draws heavy inspiration from vue-class-component, with a few notable differences:

Table of Contents

Installation

yarn add @haixing_hu/vue3-class-component @haixing_hu/typeinfo @haixing_hu/clone

or

npm install @haixing_hu/vue3-class-component @haixing_hu/typeinfo @haixing_hu/clone

Note that @haixing_hu/typeinfo and @haixing_hu/clone are peer dependencies of this library, so you need to install them separately.

Configuration

This library uses the most recent (currently May 2023) stage 3 proposal of JavaScript decorators. Therefore, you must configure Babel with @babel/plugin-transform-class-properties and the @babel/plugin-proposal-decorators plugins.

NOTE: To support the stage 3 proposal of JavaScript decorator metadata, the version of the Babel plugin @babel/plugin-proposal-decorators must be at least 7.23.0.

Note: There is a critical bug in versions of @babel/helpers greater than 7.23.0 but less than 8.0.0 (not yet released). It incorrectly sets the kind property in the context of decorators on classes to 'field' when it should be 'class'. For more details, refer to Babel's issue #16179 and issue #16180. Therefore, we need to enforce the use of version 7.23.0 of @babel/helpers in package.json. Specifically, add the following code to package.json:

{
  "resolutions": {
    "@babel/helpers": "7.23.0"
  }
}

Bundling with webpack

  1. Install the required dependencies:
    yarn add @haixing_hu/vue3-class-component @haixing_hu/typeinfo @haixing_hu/clone
    yarn add --dev @babel/core @babel/runtime @babel/preset-env
    yarn add --dev @babel/plugin-proposal-decorators @babel/plugin-transform-class-properties @babel/plugin-transform-runtime
  2. Configure Babel by using the @babel/plugin-transform-class-properties and @babel/plugin-proposal-decorators plugins. A possible Babel configuration file babelrc.json is as follows:
    {
      "presets": [
        "@babel/preset-env"
      ],
      "plugins": [
        "@babel/plugin-transform-runtime",
        ["@babel/plugin-proposal-decorators", { "version": "2023-05" }],
        "@babel/plugin-transform-class-properties"
      ]
    }

For detailed configuration instructions, you can refer to:

Bundling with vite

  1. Install the required dependencies:
    yarn add @haixing_hu/vue3-class-component @haixing_hu/typeinfo @haixing_hu/clone
    yarn add --dev @babel/core @babel/runtime @babel/preset-env
    yarn add --dev @babel/plugin-proposal-decorators @babel/plugin-transform-class-properties @babel/plugin-transform-runtime
  2. Configure Babel by using @babel/plugin-transform-class-properties and @babel/plugin-proposal-decorators plugins. A possible Babel configuration file babelrc.json is as follows:
    {
      "presets": [
        ["@babel/preset-env", { "modules": false }]
      ],
      "plugins": [
        "@babel/plugin-transform-runtime",
        ["@babel/plugin-proposal-decorators", { "version": "2023-05" }],
        "@babel/plugin-transform-class-properties"
      ]
    }
    Note: When bundling with vite, make sure to set the modules parameter of @babel/preset-env to false.
  3. Configure vite by modifying the vite.config.js file to add support for Babel. A possible vite.config.js file is as follows:
    import { fileURLToPath, URL } from 'node:url';
    import { defineConfig } from 'vite';
    import vue from '@vitejs/plugin-vue';
    import * as babel from '@babel/core';
    
    // A very simple Vite plugin support babel transpilation
    const babelPlugin = {
      name: 'plugin-babel',
      transform: (src, id) => {
        if (/\.(jsx?|vue)$/.test(id)) {              // the pattern of the file to handle
          return babel.transform(src, {
            filename: id,
            babelrc: true,
          });
        }
      },
    };
    // https://vitejs.dev/config/
    export default defineConfig({
      plugins: [
        vue({
          script: {
            babelParserPlugins: ['decorators'],     // must enable decorators support
          },
        }),
        babelPlugin,                                // must be after the vue plugin
      ],
      resolve: {
        alias: {
          '@': fileURLToPath(new URL('./src', import.meta.url)),
        },
      },
    });
    Note: In the above configuration file, we've implemented a simple vite plugin to transpile the code processed by the vite-plugin-vue plugin using Babel. Although there's a vite-plugin-babel plugin that claims to add Babel support to vite, we found it doesn't correctly handle vue Single File Components (SFCs). After closely examining its source code, we determined that to achieve correct transpilation, we need to apply Babel after vite-plugin-vue processes the source code. Therefore, the very simple plugin function above suffices for our needs. As an alternative, you can use our version of vite-plugin-babel, and the following is an example configuration:
    import { fileURLToPath, URL } from 'node:url';
    import { defineConfig } from 'vite';
    import vue from '@vitejs/plugin-vue';
    import babel from '@haixing_hu/vite-plugin-babel';
    
    export default defineConfig({
      plugins: [
        vue({
          script: {
            babelParserPlugins: ['decorators'],     // must enable decorators support
          },
        }),
        babel(),
      ],
      resolve: {
        alias: {
          '@': fileURLToPath(new URL('./src', import.meta.url)),
        },
      },
    });

For detailed configuration instructions, you can refer to:

Usage Example

<template>
  <div class="hello-page">
    <div class="message">{{ message }}</div>
    <div class="computed-message">{{ computedMessage }}</div>
    <div class="value">{{ value }}</div>
    <input v-model="newMessage">
    <button @click="setMessage(newMessage)">Set Message</button>
  </div>
</template>
<script>
import { Component, toVue } from 'vue3-class-component';

@Component
class HelloPage {
  message = 'hello';

  value = 0;

  newMessage = '';

  mounted() {
    this.value = this.$route.params.value;
  }

  get computedMessage() {
    return this.message + '!';
  }

  setMessage(s) {
    this.message = s;
  }
}

export default toVue(MyComponent); // don't forget calling `toVue`
</script>

The above code is equivalent to the following code:

<template>
  <div class="hello-page">
    <div class="message">{{ message }}</div>
    <div class="computed-message">{{ computedMessage }}</div>
    <div class="value">{{ value }}</div>
    <input v-model="newMessage">
    <button @click="setMessage(newMessage)">Set Message</button>
  </div>
</template>
<script>
export default {
  name: 'HelloPage',
  data() {
    return {
      message: 'hello',
      value: 0,
      newMessage: '',
    };
  },
  mounted() {
    this.value = this.$route.params.value;
  },
  computed: {
    computedMessage() {
      return this.message + '!';
    },
  },
  methods: {
    setMessage(s) {
      this.message = s;
    },
  },
};
</script>

Supported Options

The @Component decorator can be used with an options argument, which will be passed to the generated options of the Vue component. For example:

@Component({
  name: 'Hello',  // override the name of the class
  components: {
    PhoneLink,
  },
  filters: {
    capitalize: (s) => s.toUpperCase(),
  },
})
class HelloPage {
  message = 'hello';

  value = 0;

  newMessage = '';

  mounted() {
    this.value = this.$route.params.value;
  }

  get computedMessage() {
    return this.message + '!';
  }

  setMessage(s) {
    this.message = s;
  }
}

export default toVue(MyComponent); // don't forget calling `toVue`

is equivalent to:

export default {
  name: 'Hello',
  components: {
    PhoneLink,
  },
  filters: {
    capitalize: (s) => s.toUpperCase(),
  },
  data() {
    return {
      message: 'hello',
      value: 0,
      newMessage: '',
    };
  },
  mounted() {
    this.value = this.$route.params.value;
  },
  computed: {
    computedMessage() {
      return this.message + '!';
    },
  },
  methods: {
    setMessage(s) {
      this.message = s;
    },
  },
};

The following table lists all the keywords in the Vue options API and whether it is supported in the argument of the @Component decorator:

Category Option Supported Description
State data NO Reactive states of the component should be defined as class fields.
State props NO Component properties should be defined as class fields and marked with @Prop decorators.
State computed NO Computed properties should be defined as class getters.
State methods NO Component methods should be defined as class methods.
State watch NO Watchers should be defined as class methods marked with @Watch decorators.
State emits YES Custom events emitted by the Vue component can be declared in the @Component options.
State expose YES Exposed public properties can be declared in the @Component options.
Rendering template YES The string template can be declared in the @Component options.
Rendering render NO The render function should be defined as a class method.
Rendering compilerOptions YES Compiler options for string templates can be declared in the @Component options.
Rendering slot YES Vue component slots can be declared in the @Component options.
Lifecycle beforeCreate NO The beforeCreate hook should be defined as a class method.
Lifecycle created NO The created hook should be defined as a class method.
Lifecycle beforeMount NO The beforeMount hook should be defined as a class method.
Lifecycle mounted NO The mounted hook should be defined as a class method.
Lifecycle beforeUpdate NO The beforeUpdate hook should be defined as a class method.
Lifecycle updated NO The updated hook should be defined as a class method.
Lifecycle beforeUnmount NO The beforeUnmount hook should be defined as a class method.
Lifecycle unmounted NO The unmounted hook should be defined as a class method.
Lifecycle errorCaptured NO The errorCaptured hook should be defined as a class method.
Lifecycle renderTracked NO The renderTracked hook should be defined as a class method.
Lifecycle renderTriggered NO The renderTriggered hook should be defined as a class method.
Lifecycle activated NO The activated hook should be defined as a class method.
Lifecycle deactivated NO The deactivated hook should be defined as a class method.
Lifecycle serverPrefetch NO The serverPrefetch hook should be defined as a class method.
Composition provide NO provide properties should be defined as class fields and marked with @Provide decorators.
Composition inject NO inject properties should be defined as class fields and marked with @Inject decorators.
Composition mixins YES Mixed-in objects array can be declared in the @Component options.
Composition extends YES Base Vue component to extend from can be declared in the @Component options.
Misc name YES Vue component name can be declared in the @Component options; otherwise, the class name of the decorated class will be used.
Misc inheritAttrs YES inheritAttrs can be declared in the @Component options.
Misc components YES Registered components of the Vue component can be declared in the @Component options.
Misc directives YES Registered directives of the Vue component can be declared in the @Component options.

Predefined Decorators

This library provides the following commonly used decorators for class-style Vue components:

@Prop decorator

The @Prop decorator is applied to class fields to declare the props of the Vue component.

For example:

@Component
class MyComponent {
  // if the prop has a default value, its type and default value will be infered
  // automatically
  @Prop
  message = 'hello';

  @Prop({ type: Number, validator: (v) => (v >= 0) })
  value;

  // non-primitive default value DO NOT need to be wrapped by a factory function
  @Prop
  person = {
    id: 1,
    name: 'John',
    age: 32,
    gender: 'MALE',
  };

  // multiple possible types can be represented as an array of constructors.
  @Prop({ type: [Boolean, String] })
  lazy;

  // if the argument of the decorator is a function, it will be treated as the
  // type of the prop.
  @Prop(Number)
  value2;
  
  // if the argument of the decorator is an array of constructors, it will be
  // treated as the possible types of the prop.
  @Prop([Boolean, String, Number])
  value3;
}

export default toVue(MyComponent);

is equivalent to:

export default {
  name: 'MyComponent',
  props: {
    message: {
      type: String,
      default: 'hello',
    },
    value: {
      type: Number,
      required: true,
      validator: (v) => {
        return v >= 0
      },
    },
    person: {
      type: Object,
      required: false,
      default: () => ({
        id: 1,
        name: 'John',
        age: 32,
        gender: 'MALE',
      }),
    },
    lazy: {
      type: [Boolean, String],
      required: true,
    },
    value2: {
      type: Number,
      required: true,
    },
    value3: {
      type: [Boolean, String, Number],
      required: true,
    },
  },
};

The @Prop decorator may have an optional argument. The argument of the @Prop decorator is an object with the following options:

Option Type Default Description
type Function undefined The data type of the prop, which should be a constructor function.
required Boolean false Indicates whether the prop is required or not.
default any undefined Specifies the default value of the prop.
validator Function undefined A custom validation function for the prop.
  • type: This option defines the expected data type of the prop, and it can be one of the following: String, Number, Boolean, Array, Object, Date, Function, Symbol, a custom class, a custom constructor function, or an array of these types. In development mode, Vue will validate if the prop's value matches the declared type and will issue a warning if it doesn't. For more details, see Prop Validation.

    If multiple possible types are allowed for the prop, an array of constructors can be specified in this option. For example: { type: [Boolean, String] }.

    Note that a prop with a Boolean type affects its value casting behavior both in development and production modes. See Boolean Casting for more details.

    If this option is not specified, the library will infer the type from the initial value of the decorated class field.

  • default: Use this option to provide a default value for the prop when it is not passed by the parent component or has an undefined value.

    If this option is not specified, the library will automatically infer the default value from the initial value of the decorated class field.

    It's worth noting that the Vue library requires non-primitive default values of props to be wrapped with factory functions, but our library handles this automatically. Therefore, you don't need to wrap non-primitive default values with factory functions when declaring props.

  • required: Use this option to specify whether the prop is required or not. In a non-production environment, a console warning will be generated if this value is truthy and the prop is not provided.

    If this option is not specified, the library will automatically infer whether the initial value of the decorated class field is provided to determine if the prop is required.

  • validator: This option allows you to define a custom validation function that takes the prop value as its sole argument. In development mode, a console warning will be generated if this function returns a falsy value, indicating that the validation has failed.

If the argument of the @Prop decorator is a function, or an array of functions, it will be treated as the specified type of the new prop. For example,

@Component
class MyComponent {
  @Prop(Number)
  value1;

  @Prop([Boolean, String, Number])
  value2;
}

If a default value is provided when defining a property, there is no need to specify its type and default value again, as the system will automatically infer them. For example:

@Component
class MyComponent {
  @Prop
  message = '';

  @Prop
  value = 0;
}

@VModel decorator

The @VModel decorator is similar to the @Prop decorator, except that it supports the v-model binding. See Component v-model for more details.

For example:

<template>
  <div class="my-component">
    <input v-model="message" />
  </div>
</template>
<script>
import { Component, VModel, toVue } from '@haixing_hu/vue3-class-component';  
  
@Component
class MyComponent {
  @VModel({ type: String, validator: (v) => (v.length >= 0) })
  message;
}

export default toVue(MyComponent);
</script>

is equivalent to:

<template>
  <div class="my-component">
    <input v-model="message" />
  </div>
</template>
<script>
export default {
  name: 'MyComponent',
  props: {
    modelValue: {
      type: String,
      required: true,
      validator: (v) => {
        return v.length >= 0;
      },
    },
  },
  emits: ['update:modelValue'],
  computed: {
    message: {
      get() {
        return this.modelValue;
      },
      set(value) {
        this.$emit('update:modelValue', value);
      },
    },
  },
};
</script>

The @VModel prop in the component defined above can be used as follows:

<template>
  <div class="use-my-component">
    <my-component v-model="msg" />
  </div>
</template>

NOTE:

  • The default v-model binding property name 'modelValue' should not be used as a class field name or a class method name.
  • For the sake of simplifying implementation, this library does not support multiple v-model bindings. Additionally, it does not support v-model modifiers, nor does it allow for changing the default v-model binding property name.

Similar to the @Prop decorator, the @VModel decorator can also accept an optional argument. This argument for the @VModel decorator is an object containing additional options. The options available for @VModel are identical to those supported by @Prop. See @Prop decorator for more details.

@Watch decorator

The @Watch decorator is marked on class methods to declare watchers of the Vue component.

For example:

@Component
class MyComponent {
  value = 123;

  person = {
    id: 1,
    name: 'John',
    age: 32,
    gender: 'MALE',
  };

  @Watch('value')
  onValueChanged(val, oldVal) {
    console.log(`The value is changed from ${oldVal} to ${val}.`);
  }

  @Watch('person', { deep: true })
  onPersonChanged(val, oldVal) {
    console.log(`The person is changed from ${oldVal} to ${val}.`);
  }
}

export default toVue(MyComponent);

is equivalent to:

export default {
  name: 'MyComponent',
  data() {
    return {
      value: 123,
    };
  },
  watch: {
    value(val, oldVal) {
      console.log(`The value is changed from ${oldVal} to ${val}.`);
    },
    person: {
      deep: true,
      handler(val, oldVal) {
        console.log(`The person is changed from ${oldVal} to ${val}.`);
      },
    },
  },
};

The @Watch decorator can take one or two arguments. The first argument of the @Watch decorator specifies the path of the watched states or watched properties. The second optional argument of the @Watch decorator is an object with the following options:

Option Type Default Description
deep Boolean false Indicates whether the watcher should perform a deep traversal of the source, especially if it is an object or an array.
immediate Boolean false Specifies whether the watcher should be triggered immediately after its creation.
flush String 'pre' Defines the flushing timing of the watcher. It can be one of 'pre', 'post', or 'sync'.
  • deep: Forces a deep traversal of the source, particularly if it is an object or an array, allowing the callback to be triggered on deep mutations. Refer to Deep Watchers for more information.
  • immediate: Triggers the callback immediately upon the creation of the watcher. The old value will be undefined on the first call. Refer to Eager Watchers for more details.
  • flush: Adjusts the timing at which the callback is executed. It can be one of 'pre', 'post', or 'sync'. See Callback Flush Timing and watchEffect() for further details.

NOTE: Unlike the @Watch decorator in vue-property-decorator, the @Watch decorator in this library does not support watching the same state or property with more than one watching handler. As this is not a common use case, we have chosen to simplify the implementation of the @Watch decorator.

@Provide decorator

The @Provide decorator is marked on class fields to declare provided values that can be injected by descendant components.

For example:

const myInjectedKey = Symbol('myInjectedKey');

@Component
class AncestorComponent {
  @Provide
  message = 'hello';

  @Provide({key: myInjectedKey, reactive: true})
  @Prop
  value = 123;

  @Provide({ reactive: true })
  person = {
    id: 1,
    name: 'John',
    age: 32,
    gender: 'MALE',
  };
}

export default toVue(AncestorComponent);

is equivalent to:

import { computed } from 'vue'

export default {
  name: 'AncestorComponent',
  props: {
    value: {
      type: Number,
      default: 123,
      required: false,
    },
  },
  data() {
    return {
      message: 'hello',
    };
  },
  provide() {
    return {
      message: this.message,                        // non-reactive
      [myInjectedKey]: computed(() => this.value),  // reactive
      person: computed(() => this.person),          // reactive
    };
  },
};

The @Provide and @Inject decorators are used together to enable an ancestor component to serve as a dependency injector for all of its descendants, regardless of how deep the component hierarchy goes, as long as they are in the same parent chain. For more details, please refer to Provide / Inject.

The @Provide decorators may be used with an optional argument. This optional argument is an object with the following options:

Option Type Default Description
key String | Symbol undefined The key of the provided value.
reactive Boolean false Indicates whether the provided value is reactive.
  • key: The key is used by child components to locate the correct value to inject. The key could be either a string or a symbol. Refer to working with symbol keys for more details. If this option is not specified, the name of the field decorated by the @Provide decorator will be used as the key.
  • reactive: Specifies whether the provided value is reactive. By default, the provided values are not reactive, meaning that changing the provided value in the ancestor component will not affect the injected value in the descendant components. If this option is set to true, the provided value will be made reactive. Refer to working with reactivity for more details.

NOTE: vue-property-decorator provides @Provide and @ProvideReactive decorators to declare non-reactive and reactive provided values, respectively. However, this library simplifies the implementation by offering only one @Provide decorator with an optional reactive option. Since provided values are typically non-reactive, we have decided to set the default value of the reactive option to false.

@Inject decorator

The @Inject decorator is marked on class fields to declare injected values.

For example:

@Component
class DescendantComponent {
  @Inject
  message;

  @Inject({from: myInjectedKey, default: 0})
  injectedValue;

  // non-primitive default value DO NOT need to be wrapped by a factory function
  @Inject({ default: {id: 0, name: 'unknown'} })
  person;
}

export default toVue(DescendantComponent);

is equivalent to:

export default {
  name: 'DescendantComponent',
  inject: {
    message: {              // non-reactive
      from: 'message',
      default: undefined,
    },
    injectedValue: {        // reactive, since the provided `myInjectedKey` is reactive
      from: myInjectedKey,
      default: 0,
    },
    person: {               // reactive, since the provided `person` is reactive
      from: 'person',
      default: () => ({id: 0, name: 'unknown'}),
    },
  },
};

The @Provide and @Inject decorators are used together to enable an ancestor component to act as a dependency injector for all its descendants, regardless of how deep the component hierarchy goes, as long as they are in the same parent chain. For more details, please refer to Provide / Inject.

The @Inject decorators can have an optional argument, which is an object with the following options:

Option Type Default Description
from String | Symbol undefined The key of the source provided value to be injected.
default any undefined The default value of the injected value.
  • from: The value of this option specifies the key of the provided value to be injected. The key could be either a string or a symbol. Refer to working with symbol keys for more details. If this option is not specified, the name of the field decorated by the @Injected decorator will be used as the key.
  • default: The default value of the injected property. Note that similar to the default option of the @Prop decorator, this library will automatically transform the non-primitive default values into factory functions.

NOTE: If the provided value is non-reactive, the corresponding injected value is also non-reactive. If the provided value is reactive, the corresponding injected is also reactive. Refer to working with reactivity for more details.

NOTE: vue-property-decorator provides @Inject and @InjectReactive decorators to declare non-reactive and reactive injected values, respectively. However, this library simplifies the implementation by providing only one @Inject decorator, and the reactivity of the injected value is determined by the reactivity of the provided value.

Customize Decorators

This library provides a createDecorator() function for creating custom decorators. The function takes a callback function as an argument and returns a decorator function. The callback function will be invoked with the following parameters:

  • Class: The constructor of the decorated class.
  • instance: The default constructed instance of the decorated class. This default instance can be used to access all the class instance fields of the decorated class.
  • target: The target value being decorated, which could be a class method, a getter, or a setter. Note that if the decorated target is a class field, this argument will always be undefined.
  • context: The context object containing information about the target being decorated, as described in stage 3 proposal of JavaScript decorators and stage 3 proposal of JavaScript decorator metadata.
  • options: The Vue component options object. Changes to this object will impact the provided component. This object encompasses all the properties that a Vue component options object should possess, and it includes an additional property, fields, which is an object containing all the reactive states of the Vue component. In other words, it's the object returned by the data() function of the Vue component. Modifying the fields property of options allows you to alter the reactive states returned by the Vue component's data() function.

The callback function is called by the library to allow it to modify the Vue component options. The return value of the callback function will be ignored.

The createDecorator() function returns a decorator function that takes the following two arguments:

Here is an example of how to use it:

const Log = createDecorator((Class, instance, target, context, options) => {
  if (context?.kind !== 'method') {
    throw new Error('The @Log decorator can only be used to decorate a class method.');
  }
  const methodName = context.name;
  const originalMethod = options.methods[methodName];
  options.methods[methodName] = function (...args) {
    console.log(`${Class.name}.${methodName}: ${args.join(', ')}`);
    return originalMethod.apply(this, args);
  };
});

The above example demonstrates how to create a @Log decorator, which can be employed to log the arguments of a class method. For instance:

@Component
class HelloPage {
  message = 'hello';

  value = 0;

  newMessage = '';

  @Log
  mounted() {
    this.value = this.$route.params.value;
  }

  get computedMessage() {
    return this.message + '!';
  }

  @Log
  setMessage(s) {
    this.message = s;
  }
}

export default toVue(MyComponent);

NOTE: The @Log decorator mentioned above cannot be applied to the getter or setter of the component class.

Contributing

If you find any issues or have suggestions for improvements, please feel free to open an issue or submit a pull request to the GitHub repository.

License

vue3-class-component is distributed under the Apache 2.0 license. See the LICENSE file for more details.

Package Sidebar

Install

npm i @haixing_hu/vue3-class-component

Weekly Downloads

111

Version

1.8.2

License

Apache License 2.0

Unpacked Size

2.85 MB

Total Files

69

Last publish

Collaborators

  • haixing-hu