vue-compose

0.7.3 • Public • Published

vue-compose

Create awesome Vue HOCs

Installation

npm install --save vue-compose

Usage

import {
  compose,
  withProps,
  defaultProps,
  withHandlers,
  withData,
  setName,
} from 'vue-compose';
import C from './component.vue';
 
const enhance = compose(
  withProps((props) => ({
    someProp: 'foo',
  })),
  defaultProps({
    notPassedIn: true
  }),
  withHandlers({
    someEvent(e){
      // do some stuff
    }
  }),
  withData({
    someDataProp: {
      initialValue: 'bah',
      listener: 'updateBah',
    }
  }),
  setName('My Component'),
);
 
const enhanced = enhance(C);

API

HOC Creators

The following methods will create a new HOC that wraps the provided component.

mapProps

(
  mapper: (props : Object) => Object,
) => (Component) => Component;

Maps the props into a new object. Any props that are not returned will not be passed into the base component.

mapProps((props) => ({
  propA: props.propA,
  propB: props.propB + 1,
}));

withProps

(
  mapper: {
    [propName: string]: Object | (props: Object) => any
  } | (props : Object) => Object,
) => (Component) => Component;

Maps props into a new object. The returned object will be merged with the existing props.

withProps({
  someStaticProp: 'foo'
});
 
withProps({
  someDynamicProp: (props) => props.propA + 1
});
 
withProps((props) => ({
  someDynamicProp: props.propA + 1
}))

defaultProps

(
  defaults: Object,
) => (Component) => Component;

Sets default values for any props that are currently undefined.

defaultProps({
  someProp: 'bah'
});

acceptProps

(
 props: Object | Array<string>,
) => (Component) => Component;

Adds additional props to the component's props option. This allows you to accept props that aren't expected by the base component.

compose(
  mapProps((props) => ({
    someOldProp: props.someNewProp
  })),
  acceptProps([ 'someNewProp' ]),
);

withHandlers

(
  listeners: {
    [handlerName: string]: (...args: Array<any>) => any
  },
) => (Component) => Component;

Adds event listeners to the base component's v-on attribute.

withHandlers({
  someEvent(arg){
    // do some event stuff
  }
});

If you add a listener, that event won't propagate any further. If you want to intercept an event but still propagate it, you will need to re-emit the event:

withHandlers({
  someEvent(arg){
    // do some event stuff
    this.$emit('someEvent', arg);
  }
});

Handlers can access eachother with the handle prefix, meaning you can trigger one handler from another:

withHandlers({
  click(e){
    /* ... */
  },
  dblClick(e){
    // do something
    this.handleClick(e);
  }
})

withPassive

(
  listeners: {
    [handlerName: string]: (...args: Array<any>) => any
  },
) => (Component) => Component;

Just like withHandlers except the event is automatically propagated up.

withNativeHandlers

(
  listeners: {
    [handlerName: string]: (...args: Array<any>) => any
  },
) => (Component) => Component;

Adds native DOM even listeners to the component.

withData

(
  data: {
    [name: string]: {
      prop?: string,
      listener?: string,
      handler?: (...args: any) => any,
      initialValue?: any | (props: Object) => any,
    }
  },
) => (Component) => Component;

Creates a stateful HOC with the specified data properties.

name

The name of the data property. Note that if this conflicts with an incoming prop, it will kill the prop.

prop

The name of the prop that the data will be passed into. By default it is the same as name.

listener

The name of the event that will trigger the update handler. By default it is the same as name.

handler

A function that handles updating the data value when the listener event is emitted.

initialValue

The initial value for the data property. If it is a function, it will be evaluated when the HOC is instantiated.

Example with the default options:

withData({
  something: {
    prop: 'something',
    listener: 'something',
    handler(v){
      this.something = v;
    },
    initialValue: undefined
  }
});

withHooks

(
  hooks: {
    [hookName: string]: Function,
  },
) => (Component) => Component;

Adds hooks to the component, i.e. beforeCreate, created, mounted, etc.

withHooks({
  mounted(){
    // some stuff here
  }
});

withClass

(
  classes: any | (props: Object) => any,
) => (Component) => Component;

Adds classes to the base component.

withStyle

(
  style: any | (props: Object) => any,
) => (Component) => Component;

Adds styles to the base component.

provide

(
  provide: Object | () => Object
) => (Component) => Component;

Leverages Vue's provide/inject functionality. Use in conjunction with the inject method.

branch

(
  testFn: (props: Object) => boolean,
  trueFn: (h: Function) => vNode,
  falseFn?: (h: Function) => vNode,
) => (Component) => Component

Use an alternate render function based on a predecate function. For example, if you want to render a loading spinner based on a loading prop. If you don't provide a falseFn, it will fall back to using the component's original render function; this is the most common use case.

Note that you must use render functions, it is not possible to insert template syntax here. This does mean, however, that you can still use jsx syntax.

Mutators

Mutators don't create a new HOC, but actually mutate the provided component with new attributes.

withComputed

(
  computed: {
    [name: string]: Function
  },
) => (Component) => Component;

Adds computed properties to the component.

compose(
  withComputed({
    compA(){
      return 'foo';
    }
  }),
  withProps({
    propA(){
      return this.compA;
    }
  }),
)

withMethods

(
  methods: {
    [name: string]: Function
  },
) => (Component) => Component;

Adds methods to the component.

compose(
  withMethods({
    methodA(){
      return 'foo';
    }
  }),
  withProps({
    propA(){
      return this.methodA();
    }
  }),
)

setName

(
  name: string,
) => (Component) => Component;

Sets the name of the component.

inject

(
  inject: Array<string> | Object
) => (Component) => Component;

Injects data into the component provided by provide.

Utilities

compose

(
  ...hocCreators: Array<Function>
) => (ctor: Component) => Component;

Chain multiple HOCs together. Compose will combine the HOCs from right-to-left (or bottom-to-top). The result is a function that accepts a Component that will then be applied to all of the HOCs.

componentFromProp

(
  propName: string | Component,
) => Component;

Creates a component using the provided prop value. The prop can either be a string - such as 'input' or a component.

const C = componentFromProp('component');

You can then use it like:

<div>
  <C component="button"/>
</div>

or:

<div>
  <C :component="MyComponent"/>
</div>

componentFromSlot

(
  componentOptions?: Object
) => Component;

Creates a component that just outputs its slot content. This is useful if you want to apply styles etc. through a component, but don't want to render additional html elements.

Any props passed to this component will be passed through to the slot component.

NOTE: This is still an experimental feature and should be used with caution.

A couple of important notes:

  • There must only be one root element inside the compponent
  • For multiple root elements, use a <template>
const enhance = compose(
  withClass('someExtraClass'),
  withStyle({ width: '400px' }),
);
const C = ehance(componentFromSlot());
<C>
  <div>I will now have someExtraClass and a width of 400px</div>
</C>

createSink

(
  fn: (props: Object) => void,
) => Component;

Creates a sink component. This component does not render anything, or take any configuration options, but calls fn at render time. The props parameter contains all props and attrs passed into the component.

The createSink method is useful when unit testing your compose functions.

import { enhance } from '../my-component';
 
it('passes a foo prop', (done) => {
  const Sink = createSink((props) => {
    expect(props.foo).to.equal('bah');
    done();
  });
 
  const Enhanced = enhance(Sink);
 
  mount(Enhanced);
});

renderNothing

Component

A component that never renders.

import { renderNothing } from 'vue-compose';
 
export default {
  components: { NotFinishedYet: renderNothing },
  template: '<div><not-finished-yet/></div>',
}

You can also use this in a branch to only render under certain conditions:

branch(
  ({ loading }) => loading,
  renderNothing,
)(MyComponent)

FAQ

Why is recompose.X missing?

React and Vue look very similar on the surface, but they are actually entirely different beasts. Vue handles a lot more stuff behind the scenes. It's quite easy for React developers to want to shoehorn React techniques into a Vue application, but often there is no need because Vue handles things differently.

A couple of examples:

  • withPropsOnChange, pure, onlyUpdateForKeys - because of it's functional nature, React needs a bit of help deciding whether or not re-render components. This isn't something that Vue suffers with because of its reactive nature, it's already able to work out whether a component is dirty.
  • renameProp, flattenProp - React's props are super flexible, meaning you can accept any random assortment of props and then reorganise them before passing them into the next component. Vue requires all props to be defined upfront (so it can watch them), so chances are the props will already be named correctly, and if you want to accept props under different names etc. you have to explicitly add them to the component with acceptProps.

Why isn't withHandlers wrapped in a closure?

Recompose's withHandlers accepts a function that returns the handler function. This means you have access to the component's props via a closure. Vue is predominantly OOP orientated (see below) so there is no need to wrap props in a closure as you can access them with this.myProp or this.$props.myProp.

Why are there not more functional HOCs?

Context! Vue is not really written for functional components, and although you can set the functional flag on a component, you still don't get a truly functional component in the React sense. Your render function still receives a context object with properties of the current state of the component. On top of that you lose a lot of Vue's awesome features like computed properties that make memoization totally unecessary. And finally, it is a lot harder to pass non-prop options through multiple functional hocs, because the entire context is not retained.

How can I use this with Vuex?

Easily. You can easily combine vue-compose with vuex's helper functions:

const enhance = compose(
  withProps({
    ...mapState(['loading']),
    ...mapGetters('users', {
      name: 'userName'
    })
  }),
  withHandlers(
    mapActions('users', [
      'changeName'
    ])
  ),
);

Dependencies (1)

Dev Dependencies (0)

    Package Sidebar

    Install

    npm i vue-compose

    Weekly Downloads

    374

    Version

    0.7.3

    License

    Apache-2.0

    Unpacked Size

    55.4 kB

    Total Files

    12

    Last publish

    Collaborators

    • jackmellis