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 API proposal #17

Open
wants to merge 10 commits into
base: master
from

Conversation

Projects
None yet
@yyx990803
Copy link
Member

yyx990803 commented Feb 27, 2019

Rendered

@ashokgelal

This comment has been minimized.

Copy link

ashokgelal commented Feb 27, 2019

What about local component registration?

Also, I might start using TypeScript after this :)

@yyx990803

This comment has been minimized.

Copy link
Member Author

yyx990803 commented Feb 27, 2019

@ashokgelal any existing option can be mapped to a static property:

class Foo extends Vue {
  static components = {
    Bar,
    Baz
  }
}
@ashokgelal

This comment has been minimized.

Copy link

ashokgelal commented Feb 27, 2019

@yyx990803 That's what I thought. And does that mean with Typescript it would be something like @components({ ... })

It'd be nice to mention local component registration in the RFC as well.

@crutchcorn

This comment has been minimized.

Copy link

crutchcorn commented Feb 27, 2019

In regards to using [[Set]], I could easily see this being a large point of confusion, especially for those coming from other front-end component libraries where this behavior does not cause problems. Might it be a good idea to mention a warning of some kind in the parent Vue class if this is done? I'm not even sure that's the right way to go but I'm moreover noting that I personally would have had a fair amount of confusion had not read this RFC previously

@yyx990803

This comment has been minimized.

Copy link
Member Author

yyx990803 commented Feb 27, 2019

@ashokgelal no, you do that the same way in TypeScript. @prop is the only decorator that we currently plan to include (for the reason discussed in the decorators section)

@crutchcorn the note on [[Set]] vs [[Define]] is just there for those who are interested in the details (mostly library authors). In practice it doesn't really "cause problems" of any kind - users don't need to be aware of this.

@CyberAP

This comment has been minimized.

Copy link

CyberAP commented Feb 27, 2019

How would inject/provide look like?

@jaesung2061

This comment has been minimized.

Copy link

jaesung2061 commented Feb 27, 2019

Would we need to us TS if we wanted prop validation?

Edit: Nevermind, dumb question.

@aparajita

This comment has been minimized.

Copy link

aparajita commented Feb 27, 2019

Would this work in the script section of an SFC? Don't especially want to give up all of the advantages of having a separate <template> section vs. a string.

@yyx990803

This comment has been minimized.

Copy link
Member Author

yyx990803 commented Feb 28, 2019

@aparajita of course!

@aparajita

This comment has been minimized.

Copy link

aparajita commented Feb 28, 2019

@yyx990803 Well, your first "basic usage" example uses a template string, and none of the examples show usage within an SFC, so I hope you can see why I was wondering. It's obvious to you, but not necessarily to everyone else. The more explicit you are in docs, the better.

In any case, I would suggest that the overwhelming "basic usage" will be in an SFC, not using a template string, and perhaps the first example should be changed accordingly.

@twaite

This comment has been minimized.

Copy link

twaite commented Feb 28, 2019

So it sounds like based on the Alternatives section that you're going to be using decorators, is there any way we could something like this to make it simpler to keep our interfaces in one spot without generics?

interface propType {
  numeric: number,
  text: string,
}

class MyComponent extends Vue {
  @props()
  props: propType = {
    numeric: 0,
    text: 'string',
  }

  created() {
    this.msg
  }
}

I guess this doesn't solve the issue of validators. But I wish that I could create one interface for props and one for my data. that keeps the different concerns isolated.

@sombriks

This comment has been minimized.

Copy link

sombriks commented Feb 28, 2019

as long as the old object notation remains valid, this is perfect.

@brianjohnsonsr

This comment has been minimized.

Copy link

brianjohnsonsr commented Feb 28, 2019

I'm sold on it, given the last paragraph in the adoption strategy. As long as the vue-class-component API can remain the same while the implementation is improved, I see no issues.

@Justineo

This comment has been minimized.

Copy link
Member

Justineo commented Feb 28, 2019

Should component inheritance be covered in the RFC? Like how one component extends another one, possibly with mixins included at the same time.

@634750802

This comment has been minimized.

Copy link

634750802 commented Feb 28, 2019

Hi, I have some questions:

  1. How could I declare a non-observable field?
  2. How to create a method that has the same name with reserved name?

For question 1, maybe vue can provide a decorator @Observable() or @NonObservable().

For question 2, maybe vue can provide some special constant key to declare a hook. For example:

import Vue, { Options, Lifecycles } from 'vue'

class Component extends Vue {

    [Options.provide] () {
        return { bar: 'foo' }
    }

    [Options.data] () {
        return { foo: 'bar' }
    }

    [Lifecycles.created] () { 
        // ... 
    }

}
@caridy

This comment has been minimized.

Copy link

caridy commented Feb 28, 2019

Note about mixins, we are trying to align multiple frameworks to provide sugar for the current stage 1 proposal for mixins, and so far, people seems to like it. Have you consider using:

class Foo extends mix(Bar).with(X, Y) { 
    ...
}

The implementation of that utility is quite simple, and in maps exactly to the syntax that we are proposal for the language:

class MixinBuilder {
    constructor(superclass) {
        this.superclass = superclass;
    }

    // This is a method of the class, not a "with" statement
    with(...mixins) {
        return mixins.reduce((c, mixin) => mixin(c), this.superclass);
    }
}

export default superclass => new MixinBuilder(superclass);
@alexsasharegan

This comment has been minimized.

Copy link

alexsasharegan commented Feb 28, 2019

What distinguishes a method from a filter?

@GlebkaF

This comment has been minimized.

Copy link

GlebkaF commented Feb 28, 2019

What about checking prop type in template?
It would be great if Vue could leverage typescript to check prop types :) Any plans?

@michaelolof

This comment has been minimized.

Copy link

michaelolof commented Feb 28, 2019

Guessing watchers will also be defined via decorators.

@zuoez02

This comment has been minimized.

Copy link

zuoez02 commented Feb 28, 2019

Which will be called first? constructor() or beforeCreate()? Currently beforeCreate will be called first.

import Vue from 'vue';
import Component from 'vue-class-component';

@Component
class App extends Vue {
  constructor() {
    super();
    console.log('constructor');
  }

  beforeCreate() {
    console.log('beforeCreate');
  }

  created() {
    console.log('created');
  }
}

new App();

// beforeCreate
// constructor
// created

image

@zaun

This comment has been minimized.

Copy link

zaun commented Feb 28, 2019

Could you remove the need the prop decorator if props were only available via this.$props? I don’t see a need for them to be directly on this like a component’s data is. In fact it makes it clear your accessing a prop rather than data.

Also, how do you go about defining what the types are for this.$refs? Im having to cast them currently and would like to avoid that in the future if at all possible. Ideally if you try to access a $refs property that’s not setup with a ref attribute in the template it should error.

@donnysim

This comment has been minimized.

Copy link

donnysim commented Feb 28, 2019

I also like the idea of only having this.$props and this.$data, no direct access on this. This also makes a clear distinction that this.isLoggedIn is a computed property, and this.success() is a class methods instead of:

data() {
  return {
    success: () => { /* ... */},
  };
},

or

props: {
  success: {
    type: Function,
  },
},

being accessed through this.success() where prop, data and class method can exist with the same name.

There also is a need for non-reactive properties, like storing Popper instance, without having it trigger useless updates.

@rickyruiz

This comment has been minimized.

Copy link

rickyruiz commented Feb 28, 2019

The RFC does not mention anything about Vue.extend explicitly. As a TypeScript user, I've never needed vue-class-component.

  • Using classes will be the recommended option for TypeScript users?
  • Are there going to be any disadvantages if I keep using Vue.extend instead of classes?
@zuoez02

This comment has been minimized.

Copy link

zuoez02 commented Feb 28, 2019

@rickyruiz This is Class API proposal

@octref

This comment has been minimized.

Copy link
Member

octref commented Mar 2, 2019

might be hard to put Vetur into CI pipeline

I'll provide a CLL version. Other than collecting type errors some people also asked for exposing Vetur formatter on CLI.

@ktsn

This comment has been minimized.

Copy link
Member

ktsn commented Mar 4, 2019

Is there a way to add extra lifecycle hook as methods? I think there is a common need for that and vue-class-component provides such functionality with registerHooks. https://github.com/vuejs/vue-class-component#adding-custom-hooks

About defining options on static properties, maybe we can provide dedicated static method or encourage to use extend method to do that because it has an advantage of options type inference, auto completion and ThisType inference for other functions.

class MyComponent extends Vue {}

MyComponent.addOptions({
  template: `
    <div>hello</div>
  `
})

// or

const MyComponentWithOptions = MyComponent.extend({
  template: `
    <div>hello</div>
  `
})
@yyx990803

This comment has been minimized.

Copy link
Member Author

yyx990803 commented Mar 4, 2019

@ktsn I think libraries like vue-router can be adjusted so that they also check for component prototype methods to look for hooks - so that hooks like beforeRouteEnter don't need to be explicitly extracted into component options. I have have ideas to re-design how these hooks should work since the library cannot provide good type support for arguments passed to class methods.

@JosephSilber

This comment has been minimized.

Copy link

JosephSilber commented Mar 4, 2019

For future-proofing (and to avoid clashes), how about if we have a single method that returns an object with lifecycle hooks?

lifecycleHooks() {
    return {
        mounted: () => doSomething
    };
}
@Jinjiang

This comment has been minimized.

Copy link
Member

Jinjiang commented Mar 4, 2019

Is there still a way to define methods with existing option names by class API?

For example how to transform the code below which is actually worked in v2:

new Vue({
  el: '#app',
  template: `
    <h1
      @click="created"
      @mouseleave="methods"
    >
      Hello World {{ data() }}
    </h1>
  `,
  methods: {
    created() {
      alert()
    },
    methods() {
      alert()
    },
    data() {
      return 1
    }
  }
})

Thanks.

@yyx990803

This comment has been minimized.

Copy link
Member Author

yyx990803 commented Mar 4, 2019

@Jinjiang with the current RFC, you can't. All built-in hooks become "reserved" and will throw a warning if you try to use them in templates. This is why I'm considering prefixing them with $.

@dsebastien

This comment has been minimized.

Copy link

dsebastien commented Mar 5, 2019

@yyx990803 I concur with @HamedFathi, constructor injection would be great to have with TypeScript.

With it, we can have stricter code where the properties don't have to accept null or undefined; instead they can be strictly checked in the constructor, giving more peace of mind for the rest of the lifetime of the object.

Without it, TypeScript will annoy us with errors like these:
Property '...' has no initializer and is not definitely assigned in the constructor

And the solution to that is either to define an initializer directly (defeats the purpose of DI?) or to accept that the value may be null or undefined (which is kind of sad in TS).

What do you think?

@joeherold

This comment has been minimized.

Copy link

joeherold commented Mar 6, 2019

@yyx990803 thats so funny. react tries to get away from classes with the hooks attempt and functional componetns, you tend to get there? hmmmm....

Introducing Hooks: They let you use state and other React features without writing a class.

@jamelt

This comment has been minimized.

Copy link

jamelt commented Mar 6, 2019

As far as the lifecycle methods and naming collisions, I've run into similar issues using vue-class-component.

For me, it simply wasn't a big deal. Instead of using a data member named updated, I just used isUpdated — sort of an internal prefix convention.

export class Component extends Vue {
  // updated: boolean 
  isUpdated: boolean

  updated() {
    // component updated
  }
}

That being said, establishing a convention of preceding all lifecycle hooks with a $ might be better and bring some uniformity to the code.

@jamelt

This comment has been minimized.

Copy link

jamelt commented Mar 6, 2019

The computed property lifecycle hooks that were proposed provide some uniformity, but I think makes the code considerably less readable. I don't know if its benefits outweigh its costs.

@adamreisnz

This comment has been minimized.

Copy link

adamreisnz commented Mar 6, 2019

@Jinjiang with the current RFC, you can't. All built-in hooks become "reserved" and will throw a warning if you try to use them in templates. This is why I'm considering prefixing them with $.

Coming from an AngularJS background, I would definitely be in favour of prefixing the built in hook methods with $. AngularJS had their $onInit, $onChanges etc, and this works extremely well to differentiate and prevent clashes between a users' own methods. 👍

@uoc1691

This comment has been minimized.

Copy link

uoc1691 commented Mar 6, 2019

This is great!

One question. How will Vuex store actions, getters, and states fit into this?

@beeplin

This comment has been minimized.

Copy link

beeplin commented Mar 7, 2019

Some thoughts coming from the discussion about @prop vs this.$props and created() vs $created():

The old object-based syntax does lots of internal magics, such as

  1. lifting properties in data and computed and props and methods to this scope,
  2. implicitly inserting pre-defined properties like $parent, $children', $slotstothis`, etc.

That is why it is hard to do type inference and intellisense in editors.

In this new proposal, problems from 1) above (implicit property lifting) are already well handled, except for how to deal with props. The lack of a stable decorator syntax in JS forces us to use a new scope this.$props, which is actually a better design compared to the old auto-lifting mechanism IMHO.

However, the problems from 2) above (implicitly $- property insertion) are not touched yet. When writing components, editors do not know what this.$props/$parent/$slots is. In TS we can use interface to handle some of the cases, but JS users need a better solution.

This is actually not only about editor intellisense, but more importantly for better component interface declaration for large-scale projects. As we all know, a vue component has three sets of interface: props, slots, emits. In the old object-base syntax, components have only props explicitly declared. Does a component need a slot? What events to emit to its parent? No easy way to know before you go through all the codes.

In this new proposal things may be even worse: now you can even go with implicit this.$props without explicit prop declaration.

So I am thinking if it's possible to require all the interally-defined $-properties to be explicitly defined before you use it. Something like:

export default App extends Vue {
  $props: {
    a: Number,
    b: Striing,
  },
  $slots: {
    c: VNodes // the proper type for a slot?
  },
  $events: {
    eventName: Number // type for the event payload. People have to declare an event in $events before emitting it in component methods.
  },
  $parent: VueComponent // declare that this component relies on its parent, and make people be cautious when refactoring
  $children: [VueComponent] // declare that this components relies on its children
  }
....
}
@cesalberca

This comment was marked as off-topic.

Copy link

cesalberca commented Mar 7, 2019

Could we have the ability to have named exports of components inside .vue files? Right now every .vue can only be imported with a default import, making consistency in naming difficult and autoimports not always working. Here is an article regarding named imports vs default imports: “Why we have banned default exports in Javascript and you should do the same” by Chris Kaczor https://link.medium.com/debnD7E8RU

@LinusBorg

This comment was marked as off-topic.

Copy link
Member

LinusBorg commented Mar 7, 2019

@cesalberca your question is unrelated to this RFC.

@cesalberca

This comment was marked as off-topic.

Copy link

cesalberca commented Mar 7, 2019

Now that I think about it you are right @LinusBorg, sorry for the noise

@132yse

This comment was marked as off-topic.

Copy link

132yse commented Mar 8, 2019

If we can use HOC instead of mixin, vuex can become the following API intead of mapXXX

@map({
  state: ['count'],
  actions: ['up', 'down'],
  effects: ['upAsync']
})

But the type is still bad.

@HerringtonDarkholme

This comment was marked as off-topic.

Copy link
Member

HerringtonDarkholme commented Mar 8, 2019

@132yse We already have vuex proposal in #14.

@LinusBorg

This comment has been minimized.

Copy link
Member

LinusBorg commented Mar 8, 2019

@beeplin

However, the problems from 2) above (implicitly $- property insertion) are not touched yet. However, the problems from 2) above (implicitly $- property insertion) are not touched yet.

All of those properties are present on the type of the Vue constrcutor that components extend from, Typescript and Intellisense can recognize this, so they are. And I think it's a pretty common thing to rely on properties and methods from a base class when extending from it, no?

So I don't see why we should force people to go through the hassle of manually annotating all of these properties like $parent?

@VitaliyLavrenko

This comment has been minimized.

Copy link

VitaliyLavrenko commented Mar 8, 2019

Classes is good, but I like old syntax

export default {
  data: () => ({
    message: ''
  })
}

And if I want use class I would like to receive something like:

export default class Message extends Vue {
  data = {
    message: ''
  }

  props = {
    isShow: {
      type: Boolean,
      default: false
     }
  }

  created() {
    console.log(`I'm created!`)
  }

  get computedVar(){
    return Math.random()* 100
  }
}

And get data from this.$data, props - this.$props. If not possible in current version, get it in this like in 2.x.

It be hell if data and props fields can be defined in any place of class.

Good If user define data fields and props in top of class, but if in middle or add some defile before methods?

@LinusBorg

This comment has been minimized.

Copy link
Member

LinusBorg commented Mar 8, 2019

I like old syntax

Then continue using it. It will be fully supported in v3.

And if I want use class I would like to receive something like:
[example code omitted]

If you take a look at the proposal, that's pretty much supported by the suggested API. What specifically do you have trouble with?

@beeplin

This comment has been minimized.

Copy link

beeplin commented Mar 8, 2019

@LinusBorg

So I don't see why we should force people to go through the hassle of manually annotating all of these properties like $parent?

$slots, $scopedSlots (and not-existing $events) are just like $props: they are component's interface exposed to its parent component. In practice we always feel it would be wonderful to be able to declare explicitly slots and events just like props. It would greatly facilitate multiple-developer cooperation in big projects.

$parent and $children are a little bit different. I admit they can hardly be taken as "interface", but they are something external, out of the component author's direct control. Most well-designed components should not have $parent/$children, IMO. And when refactoring components structure in large project, it is a common mistake to move a component to another context so it loses it's parent/children and stop functioning.

In one word, it's not about intellisense for editors/IDEs, but for humen readers to be able to understand the component's interface and context dependencies at one quick glance (like: 'Aha, this component accepts two named slots, let's see what they are for!' or 'Oops, this component declares $parent at the beginning, be careful not to breake it when refactoring!').

Maybe it's possible to have a global configuration option to enable this mandatory interface/context declaration (and maybe built-time/run-time checking in dev mode)?

@mika76

This comment has been minimized.

Copy link

mika76 commented Mar 14, 2019

Any comment on how filters would work?

@pikax

This comment has been minimized.

Copy link

pikax commented Mar 14, 2019

About the props, would be nice to have a similar syntax to react:

interface MyProps{ 
  msg: string; //required string
  extra?: string; // not required
}

class MyComponent extends Vue<MyProps, {}> {
  // defaulting props
  static  defaultProps = {
   extra: "test"
  }
  count: number = 1
  created() {
    this.$props.msg
    this.$props.extra //defaults to 'test'
  }
}

Most of the type errors should be caught by the typescript building.

Still missing runtime validation, we could have similar to

class Vue<TProps, TData>{
    $props: TProps;
}

interface MyProps {
    msg: string; //required string
    extra?: string; // not required
}

interface PropValidator<T = any> {
    default?: T,
    validator?: (value: T) => boolean;
}

type PropsValidation<T> = {
    [P in keyof T]: PropValidator<T[P]>;
};

class MyComponent extends Vue<MyProps, {}> {
    // runtime validation
    static propsValidation: PropsValidation<MyProps> = {
        msg: {
            validator: (value) => {
                if (typeof value !== 'string') {
                    //throw or return?
                    // throw new Error('invalid prop type');
                    return false;
                }
            }
        },
        extra: {
            default: 'test'
        }
    }

    created() {
        this.$props.msg
        this.$props.extra //defaults to 'test'
    }
}

or we could simplify by having validation per instance:

class Vue<TProps, TData>{
    propsValidation?: PropsValidation<TProps>;
    $props: TProps;
}

class MyComponent extends Vue<MyProps, {}> {
    // runtime validation
    propsValidation = {
        msg: {
            validator: (value) => {
                if (typeof value !== 'string') {
                    //throw or return?
                    // throw new Error('invalid prop type');
                    return false;
                }
            }
        },
        extra: {
            default: 'test'
        }
    }

    created() {
        this.$props.msg
        this.$props.extra //defaults to 'test'
    }
}
@andredewaard

This comment has been minimized.

Copy link

andredewaard commented Mar 18, 2019

Looks great! Couldn't wait working with it.
Is there something similar coming to Vuex?
I'm now working with Vuex-module-decorators but considering switching to Vuex-class-component.
Any recommendation from the community which to use?

@LinusBorg

This comment has been minimized.

Copy link
Member

LinusBorg commented Mar 18, 2019

Is there something similar coming to Vuex?

@sqal

This comment has been minimized.

Copy link

sqal commented Mar 18, 2019

i am curious about one thing. With TypeScript we will be able to define state/props types as shown in the example but what about slots?

class MyComponent extends Vue<MyProps, MyData> {
  render() {
    return this.$slots.default({ foo: 'foo' })
  }
}


class SomeComponent extends Vue {
  render() {
   // will i get props intellisense?
    return h(MyComponent, ({ bar }) => {

    })
  }
}
@pikax

This comment has been minimized.

Copy link

pikax commented Mar 18, 2019

@sqal should be fairly simple to define it.

// NOTE sample declaration
declare function h<T, TData>(component: Vue<T, TData>, func: (c: T) => any);

class Vue<TProps = any, TData = any> {
  $props: TProps;
  $slots: any;
}

interface MyProps {
  foo: string;
  bar: string;
}
const MyComponent = new  class cMyComponent extends Vue<MyProps, {}> {}

class SomeComponent extends Vue {
  render() {
    // will i get props intellisense?
    return h(MyComponent, ({ bar }) => {});
  }
}

the Vue type is much more complex, but we should be able to get the props and data from the Vue<Props, Data>

@HerringtonDarkholme

This comment has been minimized.

Copy link
Member

HerringtonDarkholme commented Mar 19, 2019

@pikax I think @sqal 's example is more complicated. It requires users declare what slots will receive.

However, I still think TypeScript can capture this pattern, at the cost of explicit declaration.

interface SlotProp {
  foo: string
}
class MyComponent extends Vue {
  $slots: {
    default(slotProp: SlotProp): VNode[]
  }
  render() {
        return this.$slots.default({ foo: 'foo' })
  }
}

class Usage extends Vue {
  render() {
    return h(MyComponent, ({foo}) => { h('span', foo)})
  }
}

declare function h<T extends Vue>(t: typeof T, slot: T['$slots']['default']): VNode[]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.