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

Class style props definition #465

Open
ktsn opened this issue Oct 4, 2020 · 31 comments
Open

Class style props definition #465

ktsn opened this issue Oct 4, 2020 · 31 comments
Labels

Comments

@ktsn
Copy link
Member

ktsn commented Oct 4, 2020

Summary

To be able to define component props with class properties. You can use prop helper to specify detailed prop options:

import { Vue, prop } from 'vue-class-component'

// Define props in a class
class Props {
  count = prop({
    // Same as Vue core's prop option
    type: Number,
    required: true,
    validator: (value) => value >= 0
  })
}

// Pass the Props class to `Vue.with` so that the props are defined in the component
export default class MyComp extends Vue.with(Props) {}

In TypeScript, you can omit prop helper when you only need to define its type (runtime validation does not happen in that case):

import { Vue, prop } from 'vue-class-component'

class Props {
  // optional prop
  foo?: string

  // required prop
  bar!: string

  // optional prop with default
  baz = prop<string>({ default: 'default value' })
}

export default class MyComp extends Vue.with(Props) {}

You need to specify "useDefineForClassFields": true for TypeScript compiler option to let Vue Class Component aware of the properties without initializer (in the above example foo and bar):

{
  "compilerOptions": {
    "useDefineForClassFields": true
  }
}

Motivation

One motivation is to properly type Props type parameter of a component for props type checking in TSX and Vetur. TSX can validate props type on compile type thanks to TypeScript:

import { defineComponent } from 'vue'

// The type Props = { count: number } in component type
const Counter = defineComponent({
  props: {
    count: {
      type: Number,
      required: true
    }
  }
})

<Counter count={'Hello'} /> // Error because `count` is of type `number`

Vetur also offers similar prop type validation on <template> block. To utilize these features, we need to properly type Props type parameter of a component.

The other motivation is less verbosity. Vue's basic props option requires us to define props with values then infers the prop type from the value. For example, we have to annotate complex type with PropType utility:

interface Person {
  firstName: string
  lastName: string
}

const App = defineComponent({
  props: {
    // Specify value `Object` then annotate it with `PropType<Person>`
    person: Object as PropType<Person>
  }
})

This is relatively verbose compared to the existing @Prop decorator approach from vue-property-decorator.

interface Person {
  firstName: string
  lastName: string
}

@Component
class App extends Vue {
  // Just specify `Person` type (and `@Prop` decorator)
  @Prop person: Person
}

Ideally, the new approach should as short as the decorator approach.

Details

We will introduce two API: Vue.with static method and prop helper function.

Vue.with(...) method receives a class constructor that describes the component props. It collects all class properties and generates props option for the component under the hood. It also respects the property types for the props types:

import { Vue } from 'vue-class-component'

class Props {
  optional?: string
  required!: number
}

class App extends Vue.with(Props) {
  // Vue.with generates the following props option under the hood
  // props: { optional: null, required: null }

  mounted() {
    // It retains the property types for props
    this.optional // string | undefined
    this.required // number
  }
}

Note that we have to specify useDefineForClassFields: true component option in TypeScript to make the above code works.

We can also specify detailed prop options by using prop helper (e.g. default, validator). The prop helper receives exact same as Vue core's props option object:

class Props {
  // with validator
  count: number = prop({
    validator: (count: number) => count >= 0
  })
  
  // with default
  // You can specify the type via `prop` type parameter
  amount = prop<number>({ default: 1 })
}

Note that we have to specify the type of prop via prop helper type parameter when we use default value. This is to differentiate required prop and with-default prop on the type level. That is, required should be always of type string but withDefault should be of type string in the component while being of type string | undefined when it is used on a parent component since it does not have to receive a value. If the type is able to be inferred from the default value, you don't have to specify it.

class Props {
  // type is `string`
  required!: string

  // type is `WithDefault<string>`
  withDefault = prop({ default: 'default' })
}

class App extends Vue.with(Props) {
  mounted() {
    this.required // string
    this.withDefault // string
  }
}

// In the usage of TSX/template
// required: string
// withDefault: string | undefined
<App required="Hello" />

Alternative approaches

Decorator approach

There has been an approach with @Prop decorator but there are issues which already described in #447

TL;DR

  • Cannot type the Props type parameter, then there is no way to check props type.
  • There are concerns regarding uncertainties of the spec.

Mixin approach

This is an approach proposed in #447. But it turned out too verbose compared to the decorator approach. There is also a feedback that defining it as a mixin is not intuitive.

@Mikilll94
Copy link

Mikilll94 commented Oct 4, 2020

@ktsn
I have few questions:

  1. Is this line not going to conflict with Vue global object.
import { Vue, prop } from 'vue-class-component'

What if someone has in the same file this line?

import Vue from 'vue'
  1. These three lines are equivalent, right?
baz = prop<string>({ default: 'default value' })
baz: string = prop({ default: 'default value' })
baz = prop({ default: 'default value' })
  1. What if someone wants to extends also from another base class or from mixin? How the syntax will look like?

@TiBianMod
Copy link

TiBianMod commented Oct 5, 2020

@Mikilll94

I don't think you can use type annotations when you using the prop function (at least not the normal way)...
I think this is invalid (at least for now)...

baz: string = prop({ default: 'default value' })

The signature from the prop function is..

declare function prop<T>(options: PropOptionsWithDefault<T>): WithDefault<T>;

so you need to write the above example like this to be valid..

import { WithDefault } from 'vue-class-component';

baz: WithDefault<string> = prop({ default: 'default value' })
// you need to use the WithDefault interface :(
// or shorter like this..
baz = prop<string>({ default: 'default value' })

my thoughts on this two examples...They are both the same, but...

baz = prop<string>({ default: 'default value' }) // valid only on typescript of course
baz = prop({ default: 'default value' }) // valid on typescript and javascript

@ktsn

Can you consider renaming the Vue.props to something more generic.
Because doesn't make sense for a component to extends props, sorry :(

Something like this for example...

import { Vue } from 'vue-class-component';

class MyComponent extends Vue.component {

}

or

import { Component } from 'vue-class-component';

class MyComponent extends Component {

}

or both :)

and you can pass the props or whatever like an argument on the component..

import { Component } from 'vue-class-component';

class MyComponent extends Component(Props, [Mixins], Whatever) {

}

@ktsn
Copy link
Member Author

ktsn commented Oct 6, 2020

@Mikilll94

  1. Is this line not going to conflict with Vue global object.

There is no Vue constructor Vue 3. So the conflict never happens.

  1. What if someone wants to extends also from another base class or from mixin? How the syntax will look like?

Mixin is also a Vue constructor, hence it will looks like:

class App extends mixins(Foo, Bar).props(Props) {}

@TiBianMod

Let's discuss Vue and Vue.props individually. As for Vue, I don't think there is much benefit to rename it with Component or something because Vue is already common in Vue v2 as a base class of a component, and most of the users should be familiar with it. I guess renaming it would just increase the migration cost from v7 and involve confusion as there was @Component in v7. If you want to discuss it further, please post to #406

As for Vue.props, I agree with reconsidering the naming as I thought that there is too much prop appeared in the code and there should be a better API interface. However, I'm not sure if we can do Vue(Props) because Vue is a class constructor which is prohibited to be called as a function by ECMAScript spec.

I'm not good at wording but maybe we should use abstract words for that like Vue.add(Props), Vue.use(Props) etc.

@TiBianMod
Copy link

I guess renaming it would just increase the migration cost from v7 and involve confusion as there was @Component

I understand, you are right, the cost and the confusion will be big

I'm not good at wording but maybe we should use abstract words for that like Vue.add(Props), Vue.use(Props) etc.

I like very much the idea of abstract words.

how about something like this?

// Component allways extends Vue
class App extends Vue {

}

// using Props
class App extends Vue.useProps(Props) {

}

// using Mixins
class App extends Vue.useMixins(Mix1, Mix2) {

}

// Chaining 
class App extends Vue.useProps(Props).useMixins(Mix1, Mix2) {

}

what you think?

@coyotte508
Copy link

coyotte508 commented Oct 8, 2020

I, for one, am a very happy user of vue-property-decorator, and it works well:

class MyComponent extends Vue {
  @Prop()
  simpleOptionalProp?: string;

  @Prop({type: String})
  propWithTypeValidation!: string

  @Prop({default: 'test'})
  propWithDefaultValue!: string
}

It makes code really readable / simple, with everything in the class definitions and not imported types. I understand the concerns with the uncertainty of the spec but we'd just have to move to the next major version of vue-class-component if the times comes. Wouldn't already need to do so because Options is a decorator anyway?

I would further add that:

  • The title of this repo is 'ECMAScript / TypeScript decorator for class-style Vue components.'
  • vue-class-component has 390k weekly downloads on NPM, vue-property-decorator has 358k weekly downloads. Dirty math makes it that 92% of vue-class-component's users also download vue-property-decorator and most likely use @Prop.

@ktsn
Copy link
Member Author

ktsn commented Oct 8, 2020

Well, the uncertainty is not the only reason to propose this approach as stated in the original post. And decorator cannot properly type the Props type.

@coyotte508
Copy link

coyotte508 commented Oct 8, 2020

I'm assuming you mean the type of this.$props, apologies if I'm wrong.

A large library like Vuetify only uses $props 4 times in its whole codebase, to forward props to rendered components.

I understand the argument but I think that the convenience of the decorator needs to be weighed against the few times $props is used and would not be type-checked.

And I understand if the role of vue-class-component is to be perfect / and libraries like vue-property-decorator are there to offer more convenience at the cost of a little of that perfection, it's just that at the moment its maintainer is staying silent about Vue 3.

@ktsn
Copy link
Member Author

ktsn commented Oct 8, 2020

It's not just perfection but we can utilize props type checking feature in TSX and Vetur. Please carefully read the proposal.

@ktsn
Copy link
Member Author

ktsn commented Oct 8, 2020

Regarding the naming, I came up with Vue.with(Props). It may describe itself clearer as we can read it "Vue (Component) with Props". Besides, we can extend it when we introduce more functionality with the static method like Vue.with({ Props, otherOptions }).

@TiBianMod
Copy link

  • Related to useDefineForClassFields property.
    Be aware of behavior changes after enabling the useDefineForClassFields.

I'm very sad that I didn't know this property before :(

The quote is from #447 (comment)

Personally I don't like the idea of props to be proxied on this, I really hope we change this behavior and all props to be accessible only through this.$props

const Props = props({
    something: String,
});

export class SomeComponent extends Props {

    something = 'CLASS PROPERTY aka Data';

    public render() {
        return (
            <div>
                {this.$props.something} - {this.something}
            </div>
        );
    }

}
<SomeComponent something="PROP VALUE" />

The result of this component is:

<div>
    PROP VALUE - PROP VALUE
</div>

but where is the value of the something property?

I really hope we change this behavior @ktsn !!!

Like you can see in the above example, if you have same property name on class and props, the props will take always present and you don't have access of the class property

The results of this component will be..

<div>
     PROP VALUE - PROP VALUE
</div>

After specifying "useDefineForClassFields": true on your tsconfig this behavior will change.
The result of this component without any other change will be..

<div>
     PROP VALUE - CLASS PROPERTY aka Data
</div>

Now like you can see the class property is respected and present, and thanks to vue-class-component the class property is fully reactive.

So the class property will be always respected first and if the class property does not exist then will check for component props

REALLY REALLY NICE :)

For me this was the behavior I was looking for and I hope no one changes this behavior in the future.

@ktsn

I hope in the next version we can access props only through this.$props :)

@azampagl
Copy link

Just going to add 2 cents for consideration in regards to decorators. I’ve tried to follow all of the threads regarding the subject and completey understand the direction.

My team and I have a major application (100k’s lines of code) and many of the components utilize this library - the decorator approach was very elegant.

Even if decorators are not the suggested / ideal, it would be nice to have for backwards compatibility even if it means losing the ability to properly type the Props type for the time being (and other benefits). It at least provides us the option of upgrading our code base to Vue 3, and slowly implementing your preferred prop helper approach over time.

Love the library, thanks for all the hard work!

@noor-tg
Copy link

noor-tg commented Oct 12, 2020

Well, the uncertainty is not the only reason to propose this approach as stated in the original post. And decorator cannot properly type the Props type.

@ktsn
what about

import 'reflect-metadata'
import { Vue, Component, Prop } from 'vue-property-decorator'

@Component
export default class MyComponent extends Vue {
  @Prop() age!: number
}

If you'd like to set type property of each prop value from its type definition, you can use reflect-metadata.

isn't this easier to to use with ts ?

@rdhainaut
Copy link

rdhainaut commented Oct 13, 2020

I read the thread, the answers, the motivations of the proposal and I allow myself to react.

First of all, I am grateful for the work and efforts of this library. A big thank-you.

I recognize the potential benefit of this proposal but find that there are a few points which have been overlooked (or have been left out).

  1. First of all from an object-oriented point of view using inheritance is just the wrong way. This is one of the SOLID principles (prefer composition to inheritance). The interface does not meet expectations but is more correct from an object-oriented point of view. This is also a breaking change impacting all the components.

  2. I remind you that multiple inheritance is not possible in JS / TS. I have already developed components that have been able to take advantage of the inheritance. Example: StepOne, StepTwo, StepThree which inherits from StepAbstract (thus sharing properties and methods). It works perfectly in Vue. With your solution how to share methods through these components?

  3. I would also like to know how the $ refs will be managed? For me the problem seems the same but the double inheritance is not possible.

  4. In the arguments, there was a comparison with ReactJS (which uses interfaces to set properties). Another comparison, often forgotten, is Angular which allows to define @input (equivalent to Prop) and which are type checked in the template by the angular service. So that seems technically possible, doesn't it?

  5. Defining props inside the class is still more visible and readable. Why ? Because we have an "entity", an object that defines the behavior of the component in one place. If we do inheritance, it surprises and it makes the reading less "linear" because we have to think of an object tree. It's just how the cognition charge works.

  6. In the other thread, the decorators were questioned because they are in an uncertain state. This problem has been known from the start and represents a risk. This point should be handled by typescript / Babel and not by libraries. Also, if this is really a problem then you should no longer use a decorator. For me to stop using just one doesn't really make sense.

For me, there is a huge implication in the new proposal. The pure and simple stop of the decorators.
Personnaly, i am a huge fan of decorator because i found that solution elegant and works better with oriented object.

@ktsn
Copy link
Member Author

ktsn commented Oct 18, 2020

I'll clarify the following points:

  1. vue-property-decorator will be updated for Vue v3 + Vue Class Component v8
  2. This proposal doesn't mean deprecating @Prop in vue-property-decorator
  3. If you have reasons to use @Prop in your existing code base (e.g. migration cost), you can continue to use it.

@Nour-DEV

Reflect API (and emitDecoratorMetadata) is runtime API so it doesn't affect types.

@rdhainaut

1

Can you show a concrete example of the case that the proposed API becomes an issue by using inheritance? You can just say "inheritance is wrong" but it doesn't push the discussion forward.

I know the principle to prefer composition to inheritance and also the case of harmful inheritance when abused but I don't think all inheritance is bad. There are cases that inheritance is properly used - For example, both Android and iOS SDK let developers inherit built in UI components to customize them. There is a similar case of it on the web with custom elements and you need to inherit a native element (e.g. HTMLElement) to define it.

Besides, if you say inheritance is bad, why are you ok with the existing Vue Class Component API because you have to inhertit Vue constructor always?

@Component
export default MyComp extends Vue { // inheriting Vue
}

What is the difference from the proposed approach?

2

If you mean mixins by "multiple inheritances", you can already do that by mixins helper.

If you just mean you want to annotate StepAbstract with props:

export default StepOne extends StepAbstract.props(Props) {
}

export default StepTwo extends StepAbstract.props(Props) {
}

export default StepThree extends StepAbstract.props(Props) {
}

3

How does this relate to the proposed API?

4

I know Angular's approach since I read its code when I implement template type checking in Vetur. Yes, it is "technically" possible because Angular develop everything by themselves - compiler, type checker, language server, etc. In addition, in Vue, it would need more tooling to develop because it also supports render function and TSX. You may have to implement typescript plugin and custom webpack loader to intercept TS type checking mechanism.

Why don't we just follow the standard TypeScript rule, then properly type the props so that we don't need to develop and maintain the bunch of toolings?

5

I believe the discussion of defining props inside vs. outside class turned out trivial and just personal preferences because there are both types of API in the existing UI frameworks which are React and Angular and are well accepted.

6

I don't understand what you mean by "be handled by typescript / Babel". TS and Babel will just implement the spec, if the spec changes it, of course, affects libraries and even the end users' code.

Let me tell you an example. The current @Prop decorator heavily relies on the behavior of TypeScript experimental decorator which is even different from stage 1 decorator technically. One difference is that the behavior when there is no initializer for a class property:

class Test extends Vue {
  // No initializer class property
  @Prop foo
}

In TypeScript (without useDefineForClassFields), class property with no initializer will not be initialized while Babel (and likely the final decorator spec) will initialize it with undefined which is consistent with the class property spec. (You can see the difference in TypeScript docs)

This subtle difference affects the following case:

class Test extends Vue {
  foo = 'foo + ' + this.bar
  @Prop bar
  baz = 'baz + ' + this.bar 
}

In TypeScript, this works without any issues. But in Babel, this.bar in baz's initializer will be undefined since bar will be initialized even when we inject the prop value in it.

There are also other APIs that the latest decorator spec does not support while the previous one provides. e.g. accessing property descriptors.

These kinds of changes may or may not be able to handle in library code, no one knows about it since the spec is not fixed yet. If we can't handle it when the final decorator spec is implemented, the end users have to rewrite their code to get rid of this issue or Vue Class Component may have to stop providing the decorator.

As for the existing decorator that Vue Class Component provides, I concluded to provide it continuously because class decorator is relatively stable - the spec has not been changed so much from stage 1 which the decorator just receives class constructor via the first argument. Also, I don't mean we should stop using every decorator. If the usage is well-matched with decorators and the risk is relatively low, I think it's ok to include.

@rdhainaut
Copy link

@ktsn Thanks for taking the time to respond.

The 3 clarified points are important, thank you for clarifying.
But there is one point that I did not quite understand. vue-property-decorator is based on vue-class-component. So if you change, vue-property-decorator should change (in the future) too, right?
For me vue-property-decorator and vue-class-components are "bundled" together because you are the core of class base component.

I will try to be clear in my answer

1. Inheritance is not bad but is not flexible (which is why I used the words: "a wrong way" for your solution).
Your solution is not based on an object design but on a technical limitation and requires an inheritance.

1bis. Why accept the Vue inheritance as a component?
Every component in my app is a Vue component. So it is semantically and object oriented correct.
In addition, this heritage is "top level".
The properties (or Prop class) are not at the same level.

1 and 1bis This limits how I can code my application.

Example:

// example working actually in Vue 2 + vue-property-decorator
@Component
export default class StepAbstract extends Vue {
  @Prop()
  nextStepPath: string;

  public GoToNext(): void {
    this.$router.push(this.nextStepPath);
  }
}

@Component
export default class StepOne extends StepAbstract {
  @Prop()
  customer: ICustomer;

  get fullname(): string {
    return customer.firstname + ' ' + customer.lastname;
  }

@Component
export default class StepTwo extends StepAbstract {
 @Prop()
 payment: IPayment;

 get preferedPaymentMethod(): string {
  // code removed for brievety
 }
}

// How to do with your solution for this example?

2. Vue offers different $variables ($el, $parent, ...) in a view instance. What if tomorrow we want to type this. $var in the template? Say I want to type this.$refs (@Ref decorator from vue-property-decorator). We cannot reuse the same technique because the inheritance cannot be mutliple.

// example working actually in Vue 2 + vue-property-decorator
IValidable interface {
  IsValid (): boolean;
}

@Component
export default class PaymentForm {
  @Ref ()
  refIbanElement: IValidable;
}

<template>
<form>
  <iban-element-from-third-party-component ref = "refIbanElement" />
  <input type = "submit" disabled = "!refIbanElement.IsValid ()" />
</form>
</template>

// How to do with your logic without decorator for this example? (see point below before to answer)

3. Even if vue property decorator @Ref will continue to exist, I want to illustrate here that if we no longer want to use decorators we would be stuck with inheritance

4. I can't imagine the time and effort it took for the Angular team or the Vue team (+ yours) for the library.

But I wouldn't want to use a solution that distorts object-oriented and its principles, otherwise I might as well stop using classes and switch to function composition.

5. It s not just personnal preference. This is literally the development experience (UX for code) and how cognition works. The discussions are the same in design.

You can use a glass ketchup bottle and struggle to squeeze the ketchup out or you rethink the bottle, reverse the direction and do it in a soft material that allows you to squeeze the bottle to squeeze the ketchup out. At the end you have your ketchup but the effort is not the same.

More seriously, one of the main principles of Vue is the SFC (the single file component). It was successful because everything is in the same file. Everything is visible in one place and linear. My class is already a container. Breaking it into fragments (except for reuse as the benefit gets greater) weighs down the mental design of the code.

6. Thank you for the additional information. I just wanted to say that if we choose to use the decorators we have to live with the disadvantages because it is not standard. What I don't understand is why stop the @Prop decorator but use the @Options decorator (formerly @Component)?

@Mikilll94
Copy link

@ktsn

Let me tell you an example. The current @Prop decorator heavily relies on the behavior of TypeScript experimental decorator which is even different from stage 1 decorator technically. One difference is that the behavior when there is no initializer for a class property:

class Test extends Vue {
  // No initializer class property
  @Prop foo
}

In TypeScript (without useDefineForClassFields), class property with no initializer will not be initialized while Babel (and likely the final decorator spec) will initialize it with undefined which is consistent with the class property spec. (You can see the difference in TypeScript docs)

This subtle difference affects the following case:

class Test extends Vue {
  foo = 'foo + ' + this.bar
  @Prop bar
  baz = 'baz + ' + this.bar 
}

In TypeScript, this works without any issues. But in Babel, this.bar in baz's initializer will be undefined since bar will be initialized even when we inject the prop value in it.

There are also other APIs that the latest decorator spec does not support while the previous one provides. e.g. accessing property descriptors.

I remember that this issue with @Prop decorator was a reason why vue-class-component could not add decorators from vue-property-decorator to its source code (see this #96). What is the current status of this problem? I noticed in the first post that you want to enforce setting the useDefineForClassFields flag in tsconfig.json. Will this not break vue-property-decorator in Vue 3? Decorators were working fine with Typescript experimental decorators but this flag will enable the Babel implementation which was problematic, right?

@ktsn
Copy link
Member Author

ktsn commented Oct 20, 2020

@rdhainaut

// example working actually in Vue 2 + vue-property-decorator
@Component
export default class StepAbstract extends Vue {
  @Prop()
  nextStepPath: string;

  public GoToNext(): void {
    this.$router.push(this.nextStepPath);
  }
}

@Component
export default class StepOne extends StepAbstract {
  @Prop()
  customer: ICustomer;

  get fullname(): string {
    return customer.firstname + ' ' + customer.lastname;
  }

@Component
export default class StepTwo extends StepAbstract {
 @Prop()
 payment: IPayment;

 get preferedPaymentMethod(): string {
  // code removed for brievety
 }
}

// How to do with your solution for this example?

->

class Props {
  nextStepPath: string
}

export default class StepAbstract extends Vue.props(Props) {
  public GoToNext(): void {
    this.$router.push(this.nextStepPath);
  }
}

// ---

class Props {
  customer: ICustomer
}

export default class StepOne extends StepAbstract.props(Props) {
  get fullname(): string {
    return customer.firstname + ' ' + customer.lastname;
  }

// ---

class Props {
  payment: IPayment
}

export default class StepTwo extends StepAbstract.props(Props) {
 get preferedPaymentMethod(): string {
  // code removed for brievety
 }
}

// example working actually in Vue 2 + vue-property-decorator
IValidable interface {
  IsValid (): boolean;
}

@Component
export default class PaymentForm {
  @Ref ()
  refIbanElement: IValidable;
}

<template>
<form>
  <iban-element-from-third-party-component ref = "refIbanElement" />
  <input type = "submit" disabled = "!refIbanElement.IsValid ()" />
</form>
</template>

// How to do with your logic without decorator for this example? (see point below before to answer)

->

You can just use data as it's ref under the hood.

export default class PaymentForm {
  refIbanElement: IValidable;
}

<template>
<form>
  <iban-element-from-third-party-component ref = "refIbanElement" />
  <input type = "submit" disabled = "!refIbanElement.IsValid ()" />
</form>
</template>

More seriously, one of the main principles of Vue is the SFC (the single file component). It was successful because everything is in the same file. Everything is visible in one place and linear. My class is already a container. Breaking it into fragments (except for reuse as the benefit gets greater) weighs down the mental design of the code.

It's still everything is in one place as they are all in <script> block.

why stop the @prop decorator but use the @options decorator (formerly @component)?

I already answered in the previous post.

As for the existing decorator that Vue Class Component provides, I concluded to provide it continuously because class decorator is relatively stable - the spec has not been changed so much from stage 1 which the decorator just receives class constructor via the first argument. Also, I don't mean we should stop using every decorator. If the usage is well-matched with decorators and the risk is relatively low, I think it's ok to include.

@Mikilll94

If you want to use decorator, you can just keep useDefineForClassFields off. If you want to use both, you may want to keep useDefineForClassFields off and always define Props initializer with prop e.g.:

class Props {
  foo = prop<string>()
}

@rdhainaut
Copy link

rdhainaut commented Oct 23, 2020

Thank for your answer @ktsn
The code has help me to see that there is no limitation. I am not used to see a function at class definition level but it looks like to be something that exists in js world. I personally prefer the decorator syntax or interface declaration (i find that more clear / elegant) but I understand why you change this.

@Mikilll94
Copy link

@ktsn
What about the emits helper? Has it been removed from vue-class-component?

@ktsn
Copy link
Member Author

ktsn commented Nov 21, 2020

It'll come in minor update on v8.

@MergeCommits
Copy link

MergeCommits commented Dec 22, 2020

I was testing out the new props definition in rc.1 and I noticed that the intellisense in my IDE (Webstorm) seems to break when props are injected through a subclass of Vue.

So for example:

import { Options, prop, Vue } from "vue-class-component";

class Props {
    public defaultText = prop<string>({ default: "" });
}

export default class SomeComponent extends Vue.with(Props) {
    private inputText = "";

    public created(): void {
        this.inputText = this.defaultText; // Can hover to see "public Props.defaultText: string"
    }
}

The above works fine, right down to the defaultText field in Props no longer being marked as unused.

--

However when I introduce a new class I made:

// RVue.ts
import { Vue } from "vue-class-component";

export default class RVue extends Vue {
    // Some misc. stuff here.
}



// SomeComponent.vue
import { prop } from "vue-class-component";
import RVue from "@/RVue";

class Props {
    public defaultText = prop<string>({ default: "" });
}

export default class SomeComponent extends RVue.with(Props) {
    private inputText = "";

    public created(): void {
        this.inputText = this.defaultText; // Nothing on hover.
    }
}

I no longer get intellisense on props, and it shows fields in the Props class as being unused.

Is this an issue with this library or potentially an issue on Webstorm's side? VSCode works fine with the second code snippet.

Or maybe I'm missing something with how I'm setting up my inheritance?


It seems as though the .with(Props) syntax breaks inheritance in rc.1.

// RVue.ts
import { Vue } from "vue-class-component";

export default class RVue extends Vue {
    // Some misc. stuff here.
}

// SomeComponent.vue 
export default class SomeComponent extends RVue {
     private upcastTest: RVue = this; // Compiles fine.
}

Without introducing the new props style the above works fine, but when props are introduced it throws an error.

class Props {
    public something = prop<string>({});
}

export default class SomeComponent extends RVue.with(Props) {
     private upcastTest: RVue = this; // Error: Type 'this' is not assignable to type 'RVue'.
}

In addition, any protected or public properties on RVue are no longer accessible through child classes. This effectively makes any polymorphism you'd want to do on the classes impossible.

@brandonson
Copy link

Possibly related to the inheritance issue. The new .with style also seems to cause issues for vue-test-utils, though it does work fine in the browser. Not entirely clear whether I'm seeing a vue-test-utils issue, a vue-class-component issue, or something in between.

That said, during my test runs it seems like it might even prevent field resolution for fields declared within the component class itself when using the component with vue-test-utils. I get Property "foo" was accessed during render but is not defined on instance. for all accesses, whether it's for something from the prop class or something declared in the component class. (That said, it's an ungodly hour of the morning at this point, so maybe I'm missing something in one or more cases.)

Providing prop names in an @Options decorator and defining fields works fine. Overall this syntax seems like it would be much nicer, though, so if there's a way around this testing issue that'd be great!

@ukaaa
Copy link

ukaaa commented Jan 20, 2021

I have the same issue as @brandonson while using vue-test-utils. When running a test, it sounds like it can't find the fields and methods inside my component class.

Providing prop names in an @Options decorator and defining fields works fine. Overall this syntax seems like it would be much nicer, though, so if there's a way around this testing issue that'd be great!

Does that work for you? When I add data to @Options my class body is happy but my IDE doesn't know any of the data attributes anymore from within the template. And the test still tells me it can't access the property. What seems to help is providing the same data field within the options property of mount from vue-test-utils. But I just want the test to know and use the fields and methods as described in my component class.

@tsofist
Copy link

tsofist commented Feb 12, 2021

Please note that if Vue.with can work with several classes (via arguments, like a mixins), it will greatly facilitate writing component libraries where many of the same properties are repeated from component to component.

@charles-allen
Copy link

charles-allen commented Feb 24, 2021

Thanks to @ktsn for your thorough explanations & for continuously improving this. vue-class-component is what makes Vue great. It's maddening reading Vue docs that are written in some custom meta-language when we all already know existing equivalent TS syntax for fields, methods, getters, setters, etc. IMO classes should be the primary syntax for Vue & docs!

I get the feeling many other commenters feel the same, and that explains why many people are resistant to losing the light, class-friendly @Prop annotation. These are the key factors that won me over:

  • @Prop annotations are already a compromise; it is not defined by TS. I'm happy to accept a different compromise (provided it's lightweight & we keep true equivalents such as fields, methods, etc)
  • Props represents the API of your component; there's an argument that it should be explicit & upfront
  • The nearest TS equivalent to Props is a constructor; the .with(...) syntax is visually quite similar (more so than @Prop)
  • I came here ready to argue for familiar syntax, but when pushed, I will favor consistent type-safety (especially adding it to templates, which I believe is absent in Vue 2?)

Just wanted to add that I think your example looks better with an inline class expression (untested, but I think it works edit: I'm using it). Looks a bit like a default constructor. I think withProps would make it more fluent:

import { Vue, prop } from 'vue-class-component'

export default class MyComp 
  extends Vue.with(class {
    foo?: string
    bar!: string
    baz = prop<string>({ default: 'default value' })
  }) {
  // Rest of component
}

Edit: For reference, at some point this stopped working, and I moved to @Prop. It might be working again now, but I can no-longer vouch for it.

@tscpp
Copy link

tscpp commented Mar 3, 2021

What if the name of the property is e.g. 'package'? It is a reserved keyword and can not be used in template, only as a property inside the class. It would be nice to have the option 'name', to specify the prop name.

@nicolidin
Copy link

It'll come in minor update on v8.

Hi @ktsn there is still not the Vue.event(), is that normal? I think there is no way with the version 8.0.0-beta.4 to declare events!
Hope you can do something, thanks:)

@r70kg
Copy link

r70kg commented Jul 13, 2021

[ v8 ] how to use ref ? thanks @ktsn

haoqunjiang added a commit to haoqunjiang/vite that referenced this issue Jul 14, 2021
Since TypeScript 4.3, `target: "esnext"` indicates that
`useDefineForClassFields: true` as the new default.
See <microsoft/TypeScript#42663>

So I'm explicitly adding this field to the tsconfigs to avoid any
confusions.

Note that `lit-element` projects must use
`useDefineForClassFields: false` because of <https://github.com/lit/lit-element/issues/1030>

Vue projects must use `useDefineForClassFields: true` so as to support
class style `prop` definition in `vue-class-component`:
<vuejs/vue-class-component#465>

Popular React state management library MobX requires it to be `true`:
<https://mobx.js.org/installation.html#use-spec-compliant-transpilation-for-class-properties>

Other frameworks seem to have no particular opinion on this.

So I turned it on in all templates except for the `lit-element` one.
@andyfu6
Copy link

andyfu6 commented Feb 22, 2022

When the next version will be released?

@Makkalay
Copy link

Makkalay commented Nov 22, 2024

All is nice but $props.test

Property 'test' does not exist on type 'Partial<{}> & Omit<{} & VNodeProps & AllowedComponentProps & ComponentCustomProps, never>'

and what I see here is very ugly with Vue.extend.

@ktsn please return old method to declare props , it some how worked.

@Makkalay
Copy link

Makkalay commented Nov 22, 2024

@ktsn sorry, no need I think I do it myself


vue + classes are nice

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

No branches or pull requests