Skip to content

Unify/simplify Vue.component and Vue.extend #156

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

Closed
bpierre opened this issue Mar 4, 2014 · 17 comments
Closed

Unify/simplify Vue.component and Vue.extend #156

bpierre opened this issue Mar 4, 2014 · 17 comments

Comments

@bpierre
Copy link

bpierre commented Mar 4, 2014

Let’s say I have a plugin providing a component which is meant to be instantiated with new (not from a template):

// plugin.js
module.exports = function(Vue) {
  Vue.component('plugin-component', {
    create: function() {
      window.alert('I am a very annoying plugin')
    }
  })
}
// app.js
var plugin = require('./plugin')
Vue.use(plugin)

Now, I want to use it for my main VM. Some solutions:

  1. Direct instantiation on the constructor returned by Vue.component(name). Seems verbose and unintuitive:

    // app.js
    // These extra parenthesis are necessary
    // because `new` takes precedence on a function call
    var app = new (Vue.component('plugin-component'))({
      /* … */
    })
  2. Put the constructor in a variable first. Still verbose, but clearer:

    // app.js
    var PluginComponent = Vue.component('plugin-component')
    var app = new PluginComponent({ /* … */ })
  3. Provide a specific solution in my plugin. Clearer app.js, but proprietary API, and adds a lot of cruft in plugin.js.

    // plugin.js
    var component = {
        create: function() {
          window.alert('I am a very annoying plugin')
        }
    }
    module.exports = function(Vue) {
      Vue.component('plugin-component', component)
    }
    module.exports.vmFrom = function(Vue, options) {
      var VM = Vue.component('plugin-component')
      if (!VM) VM = Vue.extend(component)
      return new VM(options)
    }
    module.exports.component = component
    // app.js
    var app = plugin.vmFrom(Vue, { /* … */ })

Proposed solution

Please ignore if I missed some already existing mechanism.

A solution could be to introduce a new global method: Vue.create([String | VM], options). It’s really the same than the #149 proposal, but for instantiation purposes.

The reason to not add this parameter on the Vue constructor itself is because it would feel weird to provide a constructor as a parameter of another constructor.

It would allow to do this:

var app = Vue.create('plugin-component', { /* … */ })

It would also add a new syntax for classic instantiations:

var app = Vue.create({ /* … */ })

Which could have the positive effect of pleasing people who don’t want new in their code no matter what, thanks to the “new considered harmful” articles… I know, it’s a stupid reason :-)

Extend-only plugin API

Somewhat related question: what if a plugin wants to provide an extended VM only, without registering a component? Should this be taken into consideration by the plugin API, or should it be implemented by every plugin the way they want?

Example:

// plugin.js
module.exports = function(Vue) {
  // Components, directives etc. registered by the plugin:
  Vue.component('foo', {})
  Vue.directive('bar', {})
}
module.exports.getA = function(Vue) {
  return Vue.extend({ /* … */ })
}
module.exports.getB = function(Vue) {
  return Vue.extend({ /* … */ })
}
// app.js
var Vue = require('vue')
var plugin = require('./plugin')
var A = plugin.getA(Vue)

What do you think?

@yyx990803
Copy link
Member

In fact I suggest don't even return the extended constructor, just return the options:

In plugin:

exports['component-A'] = { /* ... */ }
exports['component-B'] = { /* ... */ }

In app:

var plugin = require('plugin')
Vue.component('A', plugin['component-A'])
Vue.component('B', plugin['component-B'])

This allows the user code to rename the component. Theoretically, Vue is not required to describe a component.

@bpierre
Copy link
Author

bpierre commented Mar 4, 2014

Yes, but my main concern is: how to instantiate the extended VM from the code?

I could replace this:

// app.js
var PluginComponent = Vue.component('plugin-component')
var app = new PluginComponent({ data: {}, methods: {}, /* … */ })

By this:

// app.js
var PluginComponent = Vue.extend(plugin['plugin-component'])
var app = new PluginComponent({ data: {}, methods: {}, /* … */ })

Which is better because it does not register a component name, but it’s still verbose for the user.

Maybe the plugin API or Vue itself (see the Vue.create() proposal above) should provide something to simplify the instantiation (from code, not templates) of extended VMs?

@yyx990803
Copy link
Member

I get you now. That makes sense. I think in addition to Vue.create, Vue.extend could also take an optional base class:

var ExtendedComponent = Vue.extend('some-registered-component', { /* ... */ })

So Vue.component follows the same convention, accepting the optional base class:

Vue.component('extended-component', 'base-component', { /* ... */ })

Just some rough thoughts, need more polishing. Also @th0r 's previous suggestion of removing chaining and returning the registered component is also worth considering. I think these will have to be part of 0.10 though, since this is going to be pretty big API change.

@yyx990803
Copy link
Member

Or maybe I should try to unify Vue.extend and Vue.component into a single thing.

@bpierre
Copy link
Author

bpierre commented Mar 4, 2014

Yes it could make sense, since a Component is (and will be?) a ViewModel, and both terms appears everywhere. The only difference I see is that a Component is attached to another ViewModel (or a ViewModel instance) with an ID, and can be called from the template.

<element v-component=""> could be replaced by <element v-viewmodel="">, but it does look confusing.

Conversely, the term ViewModel could be replaced with the term Component everywhere (except in the Getting Started section), and .extend() could be replaced by a non-chainable .component():

// Same as the current .extend()
var ExtendedComponent = Vue.component({})

// Get a component, same as the current .component('name')
var ExtendedComponent = Vue.component('extended-component')

// Still the same, except it returns the extended component
var ExtendedComponent = Vue.component('some-registered-component', {})

// Inheritance
var ExtendedComponent = Vue.component('extended-component', 'base-component', {})

// And now that becomes weird…
ExtendedComponent.component('another-extended-component', 'another-base-component', { /* ... */ })

And the .create() equivalents:

// Same as `new Vue({})`
Vue.create({})

// Same as `new (Vue.component('some-registered-component'))({})`
Vue.component('some-registered-component').create({})

// Same as above
Vue.create('some-registered-component', {})

@jcaxmacher
Copy link

Here's another vote for unifying Vue.extend and Vue.component. It seems to me they are doing the same job, with Vue.component also handling registration and retrieval of components.

I like @bpierre's suggestion which favors Vue.component. For terminology, I think it's helpful to say that a Vue component is a reusable, instantiatable ViewModel. Vue components have everything that you get with new Vue({}) or Vue.create({}) except that you cannot define el because you are going to be reusing it.

@ghost
Copy link

ghost commented Mar 5, 2014

I agree with with unifying everything into Vue.component. In most of my projects I am hardly ever using the Vue.extend({}) syntax and just create my components with Vue.component({}). Maybe you can change extends into an inheritance method?

e.g.

Vue.component('foo', {}).extend('bar'); // bar is another instantiated component

@yyx990803
Copy link
Member

Edited issue title to keep it relevant.

@yyx990803
Copy link
Member

Some thoughts after playing with some options:

Vue.component and Vue.extend in fact serve different purposes. This becomes more obvious when we have an extended constructor, say MyComponent. This extended constructor also has its own component and extend methods, but in this case they mean different things:

// sub-component is using Vue as the base class
// and encapsulated inside MyComponent
MyComponent.component('sub-component', { /* ... */ })

// ExtendedComponent is using MyComponent as the base class
// and isn't necessarily part of MyComponent
var ExtendedComponent = MyComponent.extend({ /* ... */ })

In this case unifying the two would actually cause confusion (as noted in @bpierre 's "now that becomes weird..." example).

Back to the original problem, I think a core issue here is Vue.js is trying to support two different paradigms - the first is Backbone-style, class & inheritance based API, and the second is Web Component style, declarative, markup-based API. Think about how you can use a <img> tag and also create an image node with new Image(). I think we do need to support both because this gives greater flexibility, and a little verbosity in this case is outweighed by the benefits.

I think it would still help to provide some sugar for some common tasks, but I'd prefer to do so without affecting the underlying semantics.

@jcaxmacher
Copy link

Let me see if I understand. When a sub-component is created inside an existing component like this:

MyComponent.component('sub-component', { /* ... */ })

It does not inherit the instantiation options of MyComponent, would not be globally registered, and would only usable from within instances of MyComponent, correct?

However, in this example:

var ExtendedComponent = MyComponent.extend({ /* ... */ })

Instances of ExtendedComponent receive all of the properties of MyComponent that are not overriden by the instantiation options, correct?

@yyx990803
Copy link
Member

@Obsoleter yes. .component() is an asset registration method similar to .directive(), .filter() etc. It means "take this constructor function and register it with this id."

Originally it expects a constructor function - directly passing in an option object is just syntax sugar, but now I think that's a source of confusion.

@ghost
Copy link

ghost commented Mar 7, 2014

@yyx990803 I agee on keeping both, there is definitely use cases for either one when you start getting more advanced applications. I think a good explanation of the differences mentioned above in the docs would be enough for people to understand it.

@bpierre
Copy link
Author

bpierre commented Mar 7, 2014

Thanks Evan, the <img> / new Image() example is very clear, it makes sense now. Maybe that could be added somewhere in the documentation (in the Composing ViewModels page, or maybe a new Concepts page).

The plugin API is for adding Components / Directives / etc. to a Vue constructor: an extended Vue can be shared with a simple init function.

What about the .create() method now? :-)

@yyx990803
Copy link
Member

I think an issue with .create() is that it assumes all components are registered globally. It feels a bit weird to do MyComponent.create('sub-component') when sub-component is encapsulated in the MyComponent constructor.

Personally, I recommend the second syntax in your original options:

var PluginComponent = Vue.component('plugin-component')
var app = new PluginComponent({ /* … */ })

Also, to cherry pick a component from a plugin that exports option objects:

var PluginComponent = Vue.extend(plugin.pluginComponent)
var app = new PluginComponent({ /* … */ })

They are a bit verbose, but I think it makes the concepts clearer.

@pesho
Copy link

pesho commented Mar 9, 2014

A small nitpick about Vue.extend (not enough to warrant a separate issue, and somewhat relevant to the current discussion):

In popular JS libraries (jQuery, Underscore, also others) extend methods modify the target object itself. In contrast, Vue.extend() creates a new, sub-classed instance. This may be somewhat confusing to Vue users. Alternative name suggestions: .derive(), .subclass().

@yyx990803
Copy link
Member

@pesho I think extend makes perfect sense because it's more like Backbone.View.extend rather than _.extend. Vue itself is a constructor, it just happens to expose some static methods, it's very different from jQuery or Underscore which mostly is an object holding a bunch of utility methods.

@yyx990803
Copy link
Member

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

4 participants