What steps are required to transform a TypeScript class with decorators into a proper Vue component?

When I inquire about the inner workings of vue-class-component, it seems that my question is often deemed too broad. Despite examining the source code, I still struggle to grasp its functionality and feel the need to simplify my understanding.

Consider this straightforward example from the Vue documentation:

export default {
  props: ['foo'],
  created() {
    console.log(this.foo)
  }
}

In terms of ECMAScript (and OOP), it's evident that the following class does not align with the aforementioned object.

export default class Component {

  private foo!: string;

  protected created(): void {
    console.log(this.foo)
  }
}

This leads me to believe that utilizing decorators could address the inherent discrepancies:

@MagicDecorator
class Component {

  @VueProperty({ type: String })
  protected foo!: string;

  @VueLifecycleHook
  protected created(): void {
   console.log(this.foo)
  }
}

Is it plausible to convert this approach back to the initial listing? Does this accurately depict the problem at hand?

Please note that while my goal isn't to replicate the exact functionality of vue-class-component, I am open to enhancements. For instance, I intend to incorporate decorators into lifecycle hooks, data, and computed properties unlike what vue-class-component offers.

Answer №1

Indeed, you are correct. The decorator performed all the necessary magic here. This functionality is not exclusive to TypeScript, as it can also be achieved using JavaScript in conjunction with the babel decorator plugin. While the source code of vue-class-components provides a comprehensive explanation, let's attempt to create a basic version ourselves using only JavaScript.

Our objective is to design a decorator that can transform a class into a Vue component object, resembling this structure:

class MyComponent {
  data() {
    return {
      count: 0,
    };
  }
  plus() {
    this.count++;
  }
}
// will be converted to something like
const MyComponent = {
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    plus() {
      this.count++;
    }
  }
}

The process is relatively straightforward. We create a new object and transfer all the methods from the class to this object. Let's begin by defining our decorator function:

function MagicDecorator(ComponentClass) {
  const options = {};
  return options;
}

The options object will serve as our transformed output. Next, we need to iterate through the class to identify its properties and methods.

function MagicDecorator(ComponentClass) {
  const options = {};
  Object.getOwnPropertyNames(ComponentClass.prototype).forEach((key) => {
    console.log(key); // data, plus
  });
  return options;
}

It's important to note that

Object.keys(ComponentClass.prototype)
will not suffice since these are non-enumerable properties established using Object.defineProperty().

For intrinsic hook methods such as mounted, created, or data, we simply copy them directly. You can refer to Vue's source code for a complete list of hook methods.

const hooks = [
  'data',
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeDestroy',
  'destroyed',
  'beforeUpdate',
  'updated',
  'activated',
  'deactivated',
  'render'
];

function MagicDecorator(ComponentClass) {
  const options = {};
  Object.getOwnPropertyNames(ComponentClass.prototype).forEach((key) => {
    if (hooks.includes(key)) {
      options[key] = ComponentClass.prototype[key];
    }
  });
  return options;
}

Simply copying these methods into our options suffices.

Subsequently, for custom methods, we need to place them within a methods object.

function MagicDecorator(ComponentClass) {
  const options = {
    methods: {},
  };
  Object.getOwnPropertyNames(ComponentClass.prototype).forEach((key) => {
    if (hooks.includes(key)) {
      options[key] = ComponentClass.prototype[key];
      return
    }
    if (typeof ComponentClass.prototype[key] === 'function') {
      options.methods[key] = ComponentClass.prototype[key];
    }
  });
  return options;
}

At this stage, our implementation already functions efficiently and manages numerous simple components effectively. The previously mentioned counter component is now fully supported by our decorator.

However, considering that Vue incorporates computed properties, let's extend our support to accommodate this feature as well.

Computed properties are facilitated through getters and setters. It becomes slightly intricate because accessing them directly triggers the getter:

ComponentClass.prototype[key]; // This invokes the getter

Thankfully, by utilizing

Object.getOwnPropertyDescriptor()
, we can retrieve the actual getter and setter functions. Afterwards, we just need to incorporate them into the computed field.

const options = {
  methods: {},
  computed: {},
};

// remaining...

const descriptor = Object.getOwnPropertyDescriptor(
  ComponentClass.prototype,
  key
);
if (descriptor.get || descriptor.set) {
  options.computed[key] = {
    get: descriptor.get,
    set: descriptor.set
  };
}

In the vue-class-components source code, they manage methods through descriptors as well:

if (typeof descriptor.value === 'function') {
  options.methods[key] = descriptor.value;
  return;
}

Lastly, we opt not to handle the constructor, adding a conditional check at the outset of the loop to ignore it:

if (key === 'constructor') {
  return;
}

As a result, a functional example has been achieved. Witness it in action here: https://codesandbox.io/s/stackoverflow-vue-class-component-uhh2jg?file=/src/MagicDecorator.js

Note 1: our basic example does not currently support data initialization via a simple class property:

class MyComponent {
  count = 0 // This type is unsupported

  // Only this format is accommodated
  data() {
    return { count: 0 }
  }
}

To enable support for class properties, they must be converted into reactive properties manually.

Note 2: Babel backs two versions of decorators. In alignment with vue-class-component's source code, I've opted for the legacy variant. Therefore, remember to specify {legacy: true} options within the

@babel/plugin-proposal-decorators
plugin.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Selecting an option with a specific index in Angular 2 RC2

I have encountered a situation where the select options are non-unique, with the same value representing different things. This is how our data is structured and I need to work within those constraints. <select id="mySelect"> <option value = "1 ...

Resolving TypeScript error when importing images statically in Next.js

In order to enhance the performance of images in my nextjs app, I am working on image optimization. However, I encountered an issue stating: Cannot find module '/images/homeBg.jpg' or its corresponding type declarations. The image is actually st ...

Error Message: "Displaying unexpected output in Vuetify Autocomplete"

I recently started using Vue 3 and I'm working on implementing the vuetify autocomplete feature in my project. After referring to the official documentation of Vuetify, I encountered an issue where the Autocomplete is displaying [object Object]. Any h ...

Style Vue slots one by one

I am a beginner delving into the world of vue (specifically, vue 2) and aiming to grasp the concept of utilizing slots in the most effective manner. Below is the code I'm working with: <template> <div> <button cla ...

One creative method for iterating through an array of objects and making modifications

Is there a more efficient way to achieve the same outcome? Brief Description: routes = [ { name: 'vehicle', activated: true}, { name: 'userassignment', activated: true}, { name: 'relations', activated: true}, { name: &apos ...

The supabase signup function keeps showing me the message "Anonymous sign-ins are disabled." Can anyone help me understand why this is happening?

I'm currently in the process of setting up authentication in Next.js with supabase, but encountering an issue when attempting to execute the signUp function. The error message I'm seeing is: Anonymous sign-ins are disabled Below is the snippet o ...

Using the as operator in TypeScript for type casting a string

function doSomething(a : any) { let b = (a as Array<any>) alert(typeof b) // displays "string" } doSomething("Hello") The alert is showing "string" instead of what I anticipated, which was something along the lines of a null value. The docu ...

Encountering a type error when attempting to filter TypeORM 0.3.5 by an enum column that is

I have the following configuration: export enum TestEnum { A = 'A', B = 'B' C = 'C' } @Entity() export class User { @PrimaryGeneratedColumn() id: number @Column({enum: TestEnum}) test: TestEnum } ...

Encountering a 404 error when attempting to make an Axios post request

Utilizing Axios for fetching data from my backend endpoint has been resulting in a 404 error. Oddly enough, when I manually enter the URI provided in the error message into the browser, it connects successfully and returns an empty object as expected. Her ...

Beginner: Add "shared" module elements to app.module and include them in app.component.html as part of the app's layout

I am trying to incorporate three components from a "shared" module into app.component.html. Here is my current setup: <header></header> <div class="main-wrapper"> <div class="bg-trick"></div> &l ...

"Error encountered: Route class unable to reach local function in TypeScript Express application" #codingissues

Experiencing an 'undefined' error for the 'loglogMePleasePlease' function in the code snippet below. Requesting assistance to resolve this issue. TypeError: Cannot read property 'logMePleasePlease' of undefined This error ...

encountering issues with configuring TypeScript LSP in NeoVim with the use of the lazy package manager

Encountered an error in nvim when opening a .ts file. Using mason, mason-lspconfig, and nvim-lspconfig for lsp setup. Lua language lsp is functioning properly, but facing errors with ts files as shown in the screenshot below: https://i.stack.imgur.com/gYM ...

Unable to find solutions for all parameters in AnalysisComponent: ([object Object], ?, ?, [

As a newcomer to the Angular framework, I encountered an issue when adding project services. Error: Can't resolve all parameters for AnalysisComponent: ([object Object], ?, ?, [object Object], [object Object], [object Object], [object Object], [obj ...

The FaceBook SDK in React Native is providing an incorrect signature when trying to retrieve a token for iOS

After successfully implementing the latest Facebook SDK react-native-fbsdk-next for Android, I am facing issues with its functionality on IOS. I have managed to obtain a token, but when attempting to use it to fetch pages, I keep getting a "wrong signature ...

Issue with retrieving JSON objects in Next.js

I've been developing a straightforward crypto price tracker using the coingecko API. At the moment, my code is unable to retrieve any of the JSON objects from the API link, and curiously, no errors or warnings are being generated to indicate what the ...

Encountered a problem when implementing flowbite in a project using NextJS and TypeScript

I recently added tailwind and flowbite to my NextJS project. After importing "flowbite" in the _app.tsx file, I encountered the following error message: ReferenceError: document is not defined at Object.366 (D:\shopflo\next-tailwin ...

Using Javascript to map an array with objects from a different array and then returning the computed array

I'm struggling to solve a seemingly simple issue. I have an array that looks like this: Array 1: [ { "id": 1, "date": "2019-03-27", "time": 1, "max_tasks": 3, "reservations": [ 5, 2 ...

Error notifications continue to appear despite the presence of data in the input field

I am utilizing a component to exhibit various information (such as first name, last name, phone number, etc.) fetched from the API. The main focus is on executing CRUD operations, particularly the update operation. Referencing the image below: An is ...

In the production build, the RegEx validation is lacking and fails to accept certain characters like 0, 2, 7, a, c, u, x, and occasionally z

Incorporating Angular 15.2.10 and Typescript 4.9.5, the RegEx utilized in one of my libraries and exposed via a service is outlined as follows: private readonly _DISALLOWED_CHARS_REGEX_GENERAL = new RegExp(/^[^\\/\?\!\&\: ...

Exploring the new features of FabricJS version 3.4.0: Enhancing performance with filters and understanding the limitations of maxTexture

Introduction: I have been experimenting with fabricJS image filtering features to incorporate them into my web application, but I have encountered the following issue. It appears that fabricJS default sets the image size cap (textureSize) on filters at 2 ...