Search code examples
typescriptvue.jsvue-class-componentsvue-property-decorator

Into what we need to convert the TypeScript class using decorators to get the valid Vue component?


If I asked "How vue-class-component works", most likely this question has been marked as too wide. By the way, I checked it's source code, but did not understood how it works, so I need to start from something simple.

Here is the simple example from the Vue documentation:

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

From the viewpoint of ECMAScript (and even OOP), below class is NOT equivalent to above object.

export default class Component {

  private foo!: string;

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

So, I suppose the problem statement is using decorators as below

@MagicDecorator
class Component {

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

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

convert it to first listing. Is this problem statement right?

Please not that I don't have target to do exactly as it vue-class-component - impovements are welcome. For example, I'm going to add decorators to lifecycle hooks, data and computed properties unlike vue-class-component.


Solution

  • Yes you are correct. The decorator did all the magics. It's not directly related to TypeScript. It can be done by using only JavaScript and the decorator plugin of babel. I think the source code of vue-class-components has explained everything, but let's build a minimum version by ourself. To keep things simple, I'm using JavaScript only.

    Our goal is to create a decorator that can convert a class to a vue component object, like:

    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++;
        }
      }
    }
    

    It's actually quite straightforward. We create a new object, and copy all the methods of the class to the object. Let's create our decorator function first:

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

    The options will be our converted result. Now, we want to loop through the class, to find out what properties and what methods it has.

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

    Please note that Object.keys(ComponentClass.prototype) won't work. Because those are non-enumerable properties (defined by Object.defineProperty())

    Now, for built-in hook methods like mounted, created, or data, we just copy it directly. You can find the list of hook methods in the Vue source code.

    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;
    }
    

    Yes it's that simple, just copy it to our options.

    Now, for custom methods, we have to put it in 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;
    }
    

    Actually now it's already working and can handle many simple components! Like the above counter component, it's fully supported by our decorator now.

    But we know that Vue has computed properties. Let's support this feature as well.

    Computed properties are support via getters and setters. Now it's a little bit tricky, because you will notice that we can access them directly by

    ComponentClass.prototype[key]; // This will trigger the getter
    

    Because when you access it this way, your are actually calling the getter. Luckily, we can use Object.getOwnPropertyDescriptor() to get the actual getter and setter functions. And then all we need to do is put it into the computed field.

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

    In the source code of vue-class-components, they handle methods by using the descriptor as well:

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

    Finally, we are not going to support the constructor. We just add this to the very first of the loop and ignore it:

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

    Now, we have a full working example. See it in action here: https://codesandbox.io/s/stackoverflow-vue-class-component-uhh2jg?file=/src/MagicDecorator.js

    Note 1: our minimum example doesn't support data by using a simple class property:

    class MyComponent {
      count = 0 // We don't support this
    
      // Only this is supported
      data() {
        return { count: 0 }
      }
    }
    

    If we want to support the class property, we have to convert it to a reactive property by ourself.

    Note 2: Babel supports two versions of decorators. To align with the source code of vue-class-component, I'm using the legacy one. So you have to specify {legacy: true} options in the @babel/plugin-proposal-decorators plugin.