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

[Feature] Component composition with higher-order components in .vue files #723

Open
linde12 opened this issue Mar 16, 2017 · 3 comments
Open

Comments

@linde12
Copy link

linde12 commented Mar 16, 2017

From the looks of it, it's not possible to use HOC's in .vue files because of the way vue-loader loader works. Instead you have to apply your HOC in a .js file.

In, for instance, React+Redux containers you would usually define your component and and "connect" it to the store using a HOC from the react-redux package. It could look something like this:

class MyComponent extends React.Component {
  render () {
    return <div>Hello {this.props.name}</div>
  }
}

export default connect(({name}) => ({name}), null)(MyComponent);

When trying to do something similar with .vue files the render function of the HOC gets replaced instead of the render function of the component you're applying your HOC to.

The following code will cause vue-loader to replace the render function of the component returned by connect instead of replacing the render function of MyComponent:

<template><div>Hello {{name}}</div></template>
<script>
  const MyComponent = {
    props: ['name']
  }
  export connect(({name}) => ({name}), null)(MyComponent)
</script>

To work around this you can, in your .vue, export MyComponent and have a .js which exports the connected MyComponent, but i find it unnecessarily complicated.

tl;dr

I want to be able to better reuse code with higher-order functions in Vue, the way you do in e.g. React.

@linde12
Copy link
Author

linde12 commented Mar 30, 2017

If a practical use case for this is needed i'll try to provide one:

As a user i want to connect my form-fields to Vuex via a HOC to avoid having local state. I make a custom component called TextInput and like so:

<template>
<div class="error" v-if="input.error" v-text="error"></div>
<some-input-component type="text" :value="input.value" :name="input.name" v-else>
</template>
<script>
  export default connectField({
    props: ['input'],
    name: 'my-textinput'
  })
</script>

The connectField function returns a component which renders the passed component as a child. To the child, connectField passes input which contains relevant information for each field.

This won't work as it is today. The user would have to create a separate file where he/she uses connectField which isn't parsed by vue-loader.

Imagine if the user had several of these components:

  • RadioButton
  • Checkbox
  • Switch
  • Textarea
  • Select
  • ...

For each of these components the user would have to create two files, even if it adds no additional value.

I personally feel that this adds unwanted complexity to do something so trivial as to create a HOC. I'm not sure how we would solve this problem and would be happy to hear some ideas!

@olsonpm
Copy link

olsonpm commented May 13, 2018

I'm also interested in HoC's via single file components and just finished digging through vue-loader and some related code - I don't see how this could be accomplished with a sensible API.

Currently vue-loader assumes the whole component is the vue file. My understanding of what you're asking is for some way to declare in your <script> what represents your current component and what's wrapping it, and I can't think of a sensible way to accomplish that. e.g. in your example what's to say connectField isn't just a helper function to normalize your component options? How is vue-loader supposed to figure out it's an HoC?

@JohannesLamberts
Copy link

JohannesLamberts commented Jun 23, 2018

IMHO this would be a great feature.
Most explicit but also most complicated could be to use a custom block with a custom loader, but I don't see any way so far.

From a short look into the vue-loader I would say the following would be at least a simple solution.

  • Expect a hoc property on the component exported from the script section.
  • Extend node_modules/vue-loader/lib/component-normalizer.js:
// check, if hocs should be loaded
if (options.hoc) {

  // allow to be used like { hoc: withA } as well as { hoc: [ withA, withB ] }
  if (!Array.isArray(options.hoc)) {
    options.hoc = [options.hoc];
  }

  if (typeof scriptExports === "function") {
    throw new Error(`Loading vue HOCs not yet supported for Vue.extend`);
  }

  // just inserted compose here for readability, doesn't have to be in scope of normalizeComponent
  const compose = stages => Component => stages
    .reverse()
    .reduce((result, fn, i) => {
        if (typeof fn !== "function") {
          throw new Error(`Expected HOC to be function, got ${typeof fn} for element #${i}`);
        }
        return fn(result);
      },
      Component
    );

  const enhance = compose(options.hoc);

  scriptExports = enhance(scriptExports);
  options = scriptExports;

}

➕easy to develop
➕easy to use
➖validated at runtime
➖introduces a new property on vue-components that can't be used on Vue.component(...)

Another great feature would be to pass a post-processing function with the webpack options. That function would be called with the parsed component and could implement logic like above.

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

No branches or pull requests

4 participants