Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't use @Component on TypeScript abstract component class #91

Closed
Toilal opened this issue Apr 27, 2017 · 22 comments
Closed

Can't use @Component on TypeScript abstract component class #91

Toilal opened this issue Apr 27, 2017 · 22 comments

Comments

@Toilal
Copy link

Toilal commented Apr 27, 2017

I wrote an abstract common class that extends Vue where I want to put some common logic, including some hook methods like created(), but those methods are not invoked by the framework.

Here's the abstract class implementing created().

abstract class DataEditorVue<T extends AbstractData> extends Vue {
  creation: boolean = false;
  data: T = null;

  created() {
    if (!this.data) {

      this.data = this.buildDataInstance();
    }
  }

  abstract buildDataInstance(): T;

  abstract getApiResource(): AbstractDataResource<T>;

  abstract getRouterPath(data: T): string;

  abstract getRouterPathAfterDeletion(data: T): string;

  remove() {
    return this.getApiResource().deleteFromObject(this.data).then(() => {
      return this.$router.push(this.getRouterPathAfterDeletion(this.data));
    });
  }

  create() {
    return this.getApiResource().create(this.data).then((data) => {
      return this.$router.push(this.getRouterPath(data), () => {
        this.creation = false;
        this.data = data;
      });
    });
  }

  update() {
    return this.getApiResource().update(this.data);
  }
}

Here the concrete class, extending abstract one.

@WithRender
@Component({
  components: {EditorControls}
})
class ObservationMilieuVue extends DataEditorVue<ObservationMilieu> {
  @Inject('observationTaxonResource')
  dataResource: ObservationTaxonResource;

  buildDataInstance(): ObservationMilieu {
    let data = new ObservationMilieu();
    data.localisation = {id: this.$route.params.localisationId};
    return data;
  }

  getApiResource(): ObservationTaxonResource {
    return this.dataResource;
  }

  getRouterPath(data: ObservationMilieu): string {
    return `/localisation/${data.localisation.id}/observation-milieu/${data.id}`;
  }

  getRouterPathAfterDeletion(data: ObservationMilieu): string {
    return `/localisation/${data.localisation.id}`;
  }
};

My actual workaround is to add a created() method in concrete class to invoke super.created() manually.

@HerringtonDarkholme
Copy link
Member

HerringtonDarkholme commented Apr 27, 2017

You also need to annotate your parent class with @Component. See https://github.com/vuejs/vue-class-component/blob/master/test/test.ts#L168-L189

vue-class-component will resolve your component inheritance only if all components are annotated.

@Toilal
Copy link
Author

Toilal commented Apr 27, 2017

I tried to do this, but the abstract class has a generic type, and it seems @component doesn't support this (I use TypeScript).

@Toilal
Copy link
Author

Toilal commented Apr 27, 2017

Well in fact, problem is not caused by the generic type, but it's caused by the fact the class is abstract.

@Toilal Toilal changed the title Inherited hook methods are not invoked Can't use @Component on TypeScript abstract component class Apr 27, 2017
@Toilal
Copy link
Author

Toilal commented Apr 27, 2017

It works when removing abstract keyword from class, and implementing previous abstract methods with errors. But it's a lose of some TypeScript compile-time checking here :(

@Component
class DataEditorVue<T extends AbstractData> extends Vue {
  creation: boolean = false;
  data: T = null;

  created() {
    if (!this.data) {
      this.creation = true;
      this.data = this.buildDataInstance();
    }
  }

  buildDataInstance(): T {
    throw new Error('Not implemented !');
  };

  getApiResource(): AbstractDataResource<T> {
    throw new Error('Not implemented !');
  };

  getRouterPath(data: T): string {
    throw new Error('Not implemented !');
  };

  getRouterPathAfterDeletion(data: T): string {
    throw new Error('Not implemented !');
  };

  remove() {
    return this.getApiResource().deleteFromObject(this.data).then(() => {
      return this.$router.push(this.getRouterPathAfterDeletion(this.data));
    });
  }

  create() {
    return this.getApiResource().create(this.data).then((data) => {
      return this.$router.push(this.getRouterPath(data), () => {
        this.creation = false;
        this.data = data;
      });
    });
  }

  update() {
    return this.getApiResource().update(this.data);
  }
}

Do you think it could be possible to make it work with my first proposal for TypeScript users ?

@HerringtonDarkholme
Copy link
Member

HerringtonDarkholme commented Apr 27, 2017

https://github.com/vuejs/vue-class-component/blob/master/src/declarations.ts#L3

{new(): Vue} will cause abstract class to fail compiling.

I would recommend you to use mixin with interface instead of abstract class.

@Toilal
Copy link
Author

Toilal commented Apr 27, 2017

I see ! Thanks for help, i'll have a look to mixins.

@Toilal Toilal closed this as completed Apr 27, 2017
@JsonSong89
Copy link

Hi,@HerringtonDarkholme thanks for your sharing .
bug If I have already extends a class , any way can use mixin with type check?
I define a Component class

@Component
class HelloTrait extends Vue {
    sayHi(name: string): void {
        alert(`Hi ${name}! this from HelloTrait`)
    }
}

and mixin it ,

@Component({
    components: {"test-slot": testSlot},
    mixins: [HelloTrait]
})
class SorterList2VC extends BaseVueList<Sorter> {
    test2() {
        this.sayHi("json")
    }

the function can work fine , but type check show a error
image

Is there any way to resolve the error except this?
image

@HerringtonDarkholme
Copy link
Member

@JsonSong89
Copy link

@HerringtonDarkholme
This is Great and I like it , thank you .
Maybe you can consider extracts it as a single project.
It is so convenience.

@304NotModified
Copy link
Contributor

I'm confused. To use the mixing function, we need to switch to av-ts?

@JsonSong89
Copy link

@304NotModified you also can use mixin in this project .

/**
 * Created by jsons on 2017/5/27.
 */
import {Vue} from 'vue/types/vue'

import {ComponentOptions, FunctionalComponentOptions} from 'vue/types/options'

import {componentFactory} from "vue-class-component/lib/component";


function ComponentForMixin<V, U extends Vue>(options: ComponentOptions<U> | V): any {
    if (typeof options === 'function') {
        return componentFactory(options as any)
    }
    return function (Component: V) {
        return componentFactory(Component as any, options)
    }
}

type VClass<T extends Vue> = {
    new(): T
    extend(option: ComponentOptions<Vue> | FunctionalComponentOptions): typeof Vue
}

function Mixin<T extends Vue>(parent: typeof Vue, ...traits: (typeof Vue)[]): VClass<T> {
    return parent.extend({mixins: traits}) as any
}
export {ComponentForMixin, Mixin}

and use by this

import {Component, ComponentForMixin, Vue, Mixin} from "typings/base"


declare interface ApplePenTrait extends Pen, Apple, TestClass3 {
}
@Component
class Pen extends Vue {
    havePen() {
        alert('I have a pen')
    }
}
@Component
class Apple extends Vue {
    haveApple() {
        alert('I have an apple')
    }
}

@Component
class TestClass3 extends Vue {
    str3 = "TestClass3"
}

// compiles under TS2.2
@ComponentForMixin({
    template: `<span @click="Uh"> click show</span>`
})
export default class ApplePen extends Mixin<ApplePenTrait>(Apple, Pen, TestClass3) {
    havePen() {
        alert('I have a  pen (ApplePen)')
    }

    Uh() {
        this.havePen()
        this.haveApple()
        alert(this.str3)
    }
}

the idea is copy from @HerringtonDarkholme 's av-ts .
thanks him !

@304NotModified
Copy link
Contributor

304NotModified commented Jul 3, 2017

Thanks!

Update got it working,

Unfortunate I can't get it working.

I have this now:

the file ComponentForMixin.ts, which the class Mixin etc

In file myfile.ts:

import { Component, Prop, Watch } from "vue-property-decorator"
import { ComponentForMixin, Mixin } from "./ComponentForMixin"

NB: I can't use

import { ComponentForMixin, Mixin, Vue, Component } from "./ComponentForMixin"

as ComponentForMixin.ts isn't exporting those?

also, I didn't needed declare on the ApplePenTrait

@304NotModified
Copy link
Contributor

To be clear, an abstract Mixin isn't working?

@justrhysism
Copy link

Maybe you can consider extracts it as a single project.

Typed mixins are too useful for me to have to manually set up for each project, so I bit the bullet and extracted the code here into a module.

https://www.npmjs.com/package/vue-mixin-decorator

@lazarljubenovic
Copy link

lazarljubenovic commented May 25, 2018

If I understand this thread correctly, it is not possible to use an abstract class on a mixin.

My use-case is that my mixin expects the component it's used on implements a method. I am not sure how can I make this typesafe. What I'd love to do is this:

@Component
export abstract class DiscardableFormMixin extends Vue {
  public abstract hasUnsavedChanges(): boolean
  public close() {
    if (this.hasUnsavedChanges) { 
      // open discard dialog and await for user's answer
    } else {
      // immediately close
    }
  }
}

So, when I use this mixin on a component, that component needs to tell the mixin what it means that there are some unsaved changes. Currently I'm unable to do this.


What I tried doing was declaring an interface which the same name, which means that TS will merge the class and interface declaration into one:

export interface DiscardableFormMixin {
  hasUnsavedChanges: boolean
}

@Component
export class DiscardableFormMixin extends Vue {
  public close() {
    if (this.hasUnsavedChanges) { 
      // open discard dialog and await for user's answer
    } else {
      // immediately close
    }
  }
}

This allows me to use this.hasUnsavedChanges in the mixin, but when I use the mixin on a class, there is no error if I don't specify the hasUnsavedChanges function.

@Component
export default class SomeForm extends mixins(DiscardableFormMixin) {
}

I need a behavior where this produces an error, telling me that I have to provide an implementation for the hasUnsavedChanges method.


What I realize I can do is declare the interface with a different name.

export interface DiscardableFormMixinInterface {
  hasUnsavedChanges: boolean
}

Then, when using the mixin:

@Component
export default class SomeForm extends mixins(DiscardableFormMixin) implements DiscardableFormMixinInterface {
}

The problem with this approach is (apart from having to re-define the type of this in the mixin, but that's boilerplate I'm fine with) that I'm responsible for remembering to write the implements part. It should be an implicit thing.

@chriszrc
Copy link

The last comment on this vue forum thread seems to have the current best answer; and it uses mixins -

https://forum.vuejs.org/t/how-can-i-use-mixin-with-vue-class-component-and-typescript/21254/8?u=chriszrc

@ReinisV
Copy link

ReinisV commented Jan 4, 2019

@HerringtonDarkholme

{new(): Vue} will cause abstract class to fail compiling.

Outside of the interface, what else would require changing to be able to use abstract classes as mixins?

because even if the interface check is overridden with an any and the code compiles successfully, the methods from the abstract class mixin are not available on the component during runtime.

Just want to know if this is something that would be feasible in the future (or maybe even doable locally).

@ReinisV
Copy link

ReinisV commented Jan 4, 2019

That's interesting. I did actually get abstract class mixins working fine and with type safety. Just had to hack around a bit with TypeScript. Not sure how stable this implementation is, but so far it seems to work fine.

Borrowing @lazarljubenovic example:

// note two things:
// 1) this is not the actually exported class, even though it contains all logic (If we'd export and use this as the actual mixin, nothing would work, since I guess abstract classes are only compile time and not runtime in TS?)
// 2) the @ts-ignore comment, which makes the @Component decorator stop complaining about the fact that it does not like abstract classes

// @ts-ignore
@Component
abstract class DiscardableFormMixinAbstract extends Vue {
  public abstract hasUnsavedChanges(): boolean
  public close() {
    if (this.hasUnsavedChanges) { 
      // open discard dialog and await for user's answer
    } else {
      // immediately close
    }
  }
}

// note two things again:
// 1) this is the actual export, which is an actual class than can be converted to JS
// 2) it again has @ts-ignore to make TypeScript stop complaining about the fact that it does not implement the abstract members of the base class

@Component()
// @ts-ignore
export class MessageModuleAuthorityStoreMixin extends MessageModuleAuthorityStoreMixinAbstract { }

then this will have compile error on the SomeForm class name until you implement the required abstract method:
Non-abstract class 'SomeForm' does not implement inherited abstract member 'hasUnsavedChanges' from class 'DiscardableFormMixin'

@Component
export default class SomeForm extends mixins(DiscardableFormMixin) {
  mounted() {
    // this call is fine and works without problems
    this.close();
  }
}

@clorichel
Copy link

Amazing! Thanks a lot @ReinisV ❤️Not sure how "stable" it is either, but it is indeed working as expected, and sounds like a well contained hack!

Seems like you simply forgot a @ts-ignore before the second @Component. Here is a fully functional example, enhanced for projects using eslint, in an example Login.ts file:

import { Component, Vue, PropSync, Emit } from 'vue-property-decorator';
import { Credentials, SourceGateway } from 'my-package';

// note two things:
// 1) this is not the actually exported class, even though it contains all logic (If we'd export and use this as the actual mixin, nothing would work, since I guess abstract classes are only compile time and not runtime in TS?)
// 2) the @ts-ignore comment, which makes the @Component decorator stop complaining about the fact that it does not like abstract classes

// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
@Component
abstract class Login extends Vue {
  public abstract setSourceGateway(): SourceGateway;

  @PropSync('credentials')
  syncedCredentials!: Credentials;

  @Emit()
  input() {
    return this.syncedCredentials;
  }

  mounted() {
    this.$nextTick(async () => {
      // we can rely on this function, which will have to be implemented
      this.setSourceGateway();
    });
  }
}

// note two things again:
// 1) this is the actual export, which is an actual class than can be converted to JS
// 2) it again has @ts-ignore to make TypeScript stop complaining about the fact that it does not implement the abstract members of the base class

/* eslint-disable @typescript-eslint/ban-ts-ignore */
// @ts-ignore
@Component
// @ts-ignore
export default class LoginMixin extends Login {}
/* eslint-enable @typescript-eslint/ban-ts-ignore */

And the implementation in an actual component, showcasing option merging to override the Prop default value:

import { Component, Prop, Emit, Mixins } from 'vue-property-decorator';
import { Credentials, MyGateway } from 'my-package';
import LoginMixin from '../abstracts/Login'; // or wherever you put it!

@Component({
  components: {
    Icon,
    GenericInput,
  },
})
export default class Login extends Mixins(LoginMixin) {
  // overriding the LoginMixin defaults for credential Prop
  @Prop({ default: 'a default value' }) credentials!: Credentials;

  // implementing abstract setSourceGateway: without it, VSCode
  // and TS will complain, which is exactly what we want here!
  @Emit()
  setSourceGateway() {
    return new MyGateway();
  }

  // from there, this logic is totally specific to your component
}

@rulrok
Copy link

rulrok commented Dec 22, 2020

@clorichel 's approach works but I'm afraid: will it break in the future? Can I use it in long term code? @ktsn

@lazarljubenovic
Copy link

@rulrok Do you have any specific reason to believe it might break?

@rulrok
Copy link

rulrok commented Dec 23, 2020

@lazarljubenovic Just making sure because I'm still a little out of sync with the current transition of framworks to Vue 3.
Also, I remember one recent update of this library had to release a follow up to keep compatibility.

BTW, clorichel's even states it's kinda a hack.
image

So, since it 'breaks' by typescript typing and we just overcome it by a //@ts-ignore, it is a bit worrisome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants