Search code examples
typescriptvuejs2vue-class-components

How to correctly inherit from a parent component in Typescript?


I'm using Vue v2 with Typescript and I'm trying to extend a parent class:

My parent class is called BaseSelect and looks like this:

<template>
  <select :value="value" @change="$emit('change', $event.target.value)">
    <option value="">default option</option>
    <slot />
  </select>
</template>

<script lang="ts">
import { Component, Model, Vue } from 'vue-property-decorator';

@Component({})
export default class BaseSelect extends Vue {
  @Model('change', { type: String, required: false, default: '' })
  private readonly value!: string

  private valid = true;

  validate(): boolean {
    this.valid = !!this.value;
    return this.valid;
  }
}
</script>

My child class BaseSelectGender looks like this:

<template>
  <base-select :value="value" @change="$emit('change', $event)">
    <option value="male">I'm male</option>
    <option value="female">I'm female</option>
  </base-select>
</template>

<script lang="ts">
import { Component } from 'vue-property-decorator';
import { BaseSelect } from '@/components/base';

@Component({
  components: { BaseSelect }
})
export default class BaseSelectGender extends BaseSelect {}
</script>

When I use <base-select-gender> in my code there are two instances of the BaseSelect component (and therefore two different instances of the valid variable):

  1. The first instance which is created because of the inheritance
  2. The second instance which is created because of the usage of <base-select> in the child

This leads to some problems when the valid variable changes because the wrong instance of the variable is reflected in the DOM.

So my question is now: How can I extend a base class and also use it or at least extend the html code in the template part of my child component?


Solution

  • Workaround

    So I found a workaround where I'm using the ref attribute to have access to the attributes and methods of the "inner" component (the one in the template section).

    This allows me to manually synchronize all the attributes that I need (make sure that the required attributes do not have a private modifier).

    My BaseSelectGender component now looks like this:

    <template>
      <base-select 
        :value="value" 
        @change="$emit('change', $event)"
        v-bind="{ ...$attrs, ...$props }"
        ref="inner"
      >
        <option value="male">I'm male</option>
        <option value="female">I'm female</option>
      </base-select>
    </template>
    
    <script lang="ts">
    import { Component } from 'vue-property-decorator';
    import { BaseSelect } from '@/components/base';
    
    @Component({
      components: { BaseSelect }
    })
    export default class BaseSelectGender extends BaseSelect {
      get inner() {
        return this.$refs.inner as BaseSelect;
      }
    
      setValid(valid: boolean) {
        this.valid = valid;
        this.inner.setValid(valid);
      }
    
      validate(): boolean {
        this.valid = this.inner.validate();
        return this.valid;
      }
    }
    </script>
    

    Pro-tip: Use v-bind="{ ...$attrs, ...$props }" to pass through all the attributes and props from the outer component to the inner component.