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

How can we transfer or exclude all boolean properties from one class to another or a "type"?

Within my Nestjs application, there is an entity class structured like this: @ObjectType() export class Companies { @Field(() => Int) @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) public id: number; @Field() @Column ...

There is a chance that the object could be 'undefined' when attempting to add data to it

I created an object and a property called formTemplateValues. I am certain that this property exists, but I am getting an error message saying: "Object is possibly 'undefined'". It should not be undefined because I specifically created it. Why am ...

Adding markers to a Leaflet map using coordinates retrieved from a Supabase database - a step-by-step guide

I am looking to incorporate markers on a map using coordinates stored in a Supabase database column. Below is my Vue code: <l-marker v-for="(marker, index) in markers" :key="index" ref="markersRef" :lat-lng="marker.po ...

What are some ways to control providers in targeted tests using ng-mocks?

I recently started utilizing ng-mocks to streamline my testing process. However, I am struggling to figure out how to modify the value of mock providers in nested describes/tests after MockBuilder/MockRender have already been defined. Specifically, my que ...

Error: Oops! The super expression can't be anything other than null or a function in JavaScript/TypeScript

I am facing an issue with class inheritance in my code. I have a class A that extends class B, which in turn extends class C. Whenever I try to create a new instance of class A within a function, I encounter the following error message: Uncaught TypeError: ...

The filename is distinct from the file already included solely by the difference in capitalization. Material UI

I have recently set up a Typescript React project and incorporated Material UI packages. However, I encountered an error in VS Code when trying to import these packages - although the application still functions properly. The error message reads: File na ...

Refresh Material-Ui's Selection Options

Is there a way to properly re-render the <option> </option> inside a Material UI select component? My goal is to transfer data from one object array to another using the Material UI select feature. {transferData.map(data => ( <option ...

Having trouble displaying API values in b-form-select component in Vue.js?

I am using an API to fetch users data and I want to bind these users to a b-form-select component in Bootstrap Vue. However, after making the request, I only see "null" in the b-form-select. Here is my request: getAllUsers() { axios.get(&a ...

When incorporating leaflet-routing-machine with Angular 7, Nominatim seems to be inaccessible

Greetings! As a first-time user of the Leafletjs library with Angular 7 (TypeScript), I encountered an error while using Leaflet routing machine. Here is the code snippet that caused the issue. Any ideas on how to resolve this problem? component.ts : L. ...

Transferring information between screens in Ionic Framework 2

I'm a beginner in the world of Ionic and I've encountered an issue with my code. In my restaurant.html page, I have a list of restaurants that, when clicked, should display the full details on another page. However, it seems that the details for ...

Utilizing dynamic class and color binding features in VueJs?

I need help with implementing a Custom Sort method on my divs to arrange them in ascending or descending order. My query is how can I pre-set the icon color to grey by default, and only change it to black when clicked, while keeping the others greyed out a ...

Angular page not reflecting show/hide changes from checkbox

When the user clicks on the checkbox, I need to hide certain contents. Below is the code snippet: <input id="IsBlock" class="e-field e-input" type="checkbox" name="IsBlock" style="width: 100%" #check> To hide content based on the checkbo ...

The Angular AJAX call was unsuccessful due to the Content-Type request header field being forbidden by the Access-Control-Allow-Headers in the preflight response

Here is the code I am using to send a post request from Angular 6 to my web service. const headers = new HttpHeaders({ 'Content-Type': 'application/json' }); const headeroptions = { headers: headers }; return this.http.post(this. ...

Determine the return type of a function based on a key parameter in an interface

Here is an example of a specific interface: interface Elements { divContainer: HTMLDivElement; inputUpload: HTMLInputElement; } My goal is to create a function that can retrieve elements based on their names: getElement(name: keyof Elements): Elemen ...

Having issues with triggering click events using querySelector in Angular 2

Having trouble using querySelector with click() method in Angular it is causing a compilation error: 'click' is not recognized as a method on type 'Element' document.body.querySelector(".class").click(); ...

Something went wrong: Unable to access the properties of an undefined variable named 'gametitle'

I am able to see the variables on the html-page, but I encountered an error specifically with the value of the gametitle ERROR TypeError: Cannot read properties of undefined (reading 'gametitle') Below is the content of the ts-file: import { ...

Utilizing RavenDB with NodeJS to fetch associated documents and generate a nested outcome within a query

My index is returning data in the following format: Company_All { name : string; id : string; agentDocumentId : string } I am wondering if it's possible to load the related agent document and then construct a nested result using selectFie ...

Troubleshooting Bootstrap Select and dynamic rows problem in a Vue.js project

I am facing an issue with my dynamic table/rows in vue.js where I am using bootstrap-select to enhance the dropdown select. My form includes an add/remove line feature for dynamic functionality. However, I am unable to load the options of a select using bo ...

Leveraging ternary operators within HTML elements

Currently, I am utilizing the Vue.js framework to create a dynamic list that updates based on two different list objects. My main objective is to adjust the border of the list cards depending on a specific condition. Below are the defined cards: <li ...

The ideal method to transmit reactive data effectively is through Vue.js

Implementing vue.js. I've created an authentication file (auth.js) that stores the user information upon detecting a change in authentication state. There are other sections of the website that need to update when the user information changes. What is ...