Skip to content

Raiondesu/vue-functional-props

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vue-functional-props

Handle functional component props properly

travis npm size code quality

It's a small side-effect-free library with a single purpose - provide Vue 3 devs a simple way of type-safe handling properties for functional components.

Installation

npm i -S vue-functional-props
or
yarn add vue-functional-props

Usage

npm:

npm i -S vue-functional-props

browser:

<!-- ES2015 -->
<script type="module">
  import { component } from 'https://unpkg.com/vue-functional-props';

  // use it here
</script>

<!-- ES5 with IE11+ general syntax polyfills, global object - `vue-functional-props` -->
<!-- Polyfill `window.Promise` and `Object.assign` yourself! -->
<script src="https://unpkg.com/vue-functional-props/dist/umd.js"></script>

Importing:

// TS-module (pure typescript),
// allows compilation settings to be set from the project config
import { component } from 'vue-functional-props/src';

// ES-module (npm/node, typescript)
import { component } from 'vue-functional-props';

// ESNext (no polyfills for esnext)
import { component } from 'vue-functional-props/dist/esnext';

// ES-module (browser, node)
import { component } from 'https://unpkg.com/vue-functional-props';

// Classic node commonjs
const { component } = require('vue-functional-props/dist/js');

Documenation example

Official Vue 3 docs state that functional component props can only be added in one way:

import { h } from 'vue';

// here, in TS, a user has to define props separately in two places, which produces code duplication
const DynamicHeading = (props: { level: number }, context) => {
  return h(`h${props.level}`, context.attrs, context.slots);
};

DynamicHeading.props = {
  level: Number;
};

export default DynamicHeading;

This is far from perfect for user experience, and is definitely in need of some sort of a wrapper (with type inference, preferably).

This tiny (~300B gzipped) library allows to achieve just that!

import { h } from 'vue';
import { withProps } from 'vue-functional-props'

// No code duplication whatsoever!
export default withProps({
  level: Number,
}, (props, context) => {
  // here props.level is already defined
  return h(`h${props.level}`, context.attrs, context.slots);
});

API

import { component, withProps, prop } from 'vue-functional-props';

withProps

function withProps<P, S>(props: P, setup: S): S

A simple function wrapper that accepts a standard vue props object definition and a setup function and adds props to the setup function definition so they can be recognized by vue.

Usage with object prop notation:

withProps({
  level: Number,
  someProp: {
    type: String,
    required: true
  },
  otherProp: {
    type: String,
    default: ''
  }
}, (props, context) => {
  props.level // number | undefined
  props.someProp // string
  props.otherProp // string | undefined

  return h(`h${props.level}`, context.attrs, context.slots);
});

Usage with an array notation:

withProps(
  // `as const` cast is needed for array notation in order for TS to infer the type
  ['level', 'someProp', 'otherProp'] as const,
  (props, context) => {
    // No way around `any` if using array notation
    props.level // any
    props.someProp // any
    props.otherProp // any

    return h(`h${props.level}`, context.attrs, context.slots);
  }
);

component

function component(name: string, inheritAttrs: boolean = true)

Useful for when you need to define name, events and other properties,
but still need a functional component.

Note: it doesn't call defineComponent!

Returns a type-safe functional component builder with the following methods:

  • withProps - identical to the exported withProps, but accepts only one argument - props definition. Returns the same object as the component function.
  • emits - accepts a map of event declaration like this. Unfortunately, only object event delcaration is supported at the moment. Returns the same object as the component function.
  • setup - accepts the functional component itself, providing type-safety. Returns the component itself. Must be called last.
import { h } from 'vue';
import { component } from 'vue-functional-props';

export default component(
  /* name:         */ 'DynamicHeading',
  /* inheritAttrs: */ false
)
  .emits({
    click(e: MouseEvents) {}
  })
  .withProps({
    level: {
      type: [Number, String],
      required: true,
      validator: level => Number(level) > 0 && Number(level) <= 6
    }
  })
  .setup((props, context) => {
    // here props.level is defined and typed as `number | string`
    return h(`h${props.level}`, {
      ...context.attrs,
      on: {
        // Here, `emit` is typed, and event name is autocompleted
        click: e => context.emit('click', e)
      }
    }, context.slots);
  });

/* Equivalent to:
const DynamicHeading = (props, context) => {
  // Absolutely no typesafety here, everything is `any`
  return h(`h${props.level}`, {
    ...context.attrs,
    on: {
      click: e => context.emit('click', e)
    }
  }, context.slots);
};

DynamicHeading.displayName = 'DynamicHeading';
DynamicHeading.inheritAttrs = false;
DynamicHeading.props = {
  level: {
    type: [Number, String],
    required: true,
    validator: level => Number(level) > 0 && Number(level) <= 6
  }
};
DynamicHeading.emits = {
  click(e: MouseEvents) {}
};

export default DynamicHeading;
*/

withProps and emits can be called in any order or not called at all:

// Valid
component('DynamicHeading', false)
  .withProps(['level'])
  .setup((props, context) => {
    return h(`h${props.level}`, {
      ...context.attrs,
      on: { click: e => context.emit('click', e) }
    }, context.slots);
  });


// Also Valid
component('DynamicHeading', false)
  .emits({ click(e: MouseEvent) {} })
  .setup((props, context) => {
    return h(`h${context.attrs.level}`, {
      ...context.attrs,
      on: { click: e => context.emit('click', e) }
    }, context.slots);
  });

// Also Valid
component('DynamicHeading', false)
  .setup((props, context) => {
    return h(`h${context.attrs.level}`, {
      ...context.attrs,
      on: { click: e => context.emit('click', e) }
    }, context.slots);
  });

// INVALID!
component('DynamicHeading', false)
  .setup((props, context) => {
    return h(`h${context.attrs.level}`, {
      ...context.attrs,
      on: { click: e => context.emit('click', e) }
    }, context.slots);
  })
  .emits({ click(e: MouseEvent) {} });
// .setup must be called last!

prop

function prop<T, D = T>(options: Prop<T, D>): () => Prop<T, D>

T - a complex type for the prop.
D - provide if default differs from T.

Enables type validaton for complex types in props without the need to pass constructors or runtime validators.
Basically, a NOOP without TypeScript.

Currently needs to be an empty function, that retutns a function which accepts the prop options, like this:

prop<SomeType>(
  // Empty function call
)({
  // Real function call with prop options
  type: Object
})

This is needed to infer the option type correctly due to a design flaw in TypeScript.

import { prop, withProps } from 'vue-functional-props';

// Some complex objects, for example
export declare interface TTableRow {}
export declare interface ITableColumn {}

export default withProps({
  /**
   * The collection of rows for the table
   */
  rows: prop<TTableRow[]>()({
    type: Array,
    default: () => [],
  }),

  /**
   * Collection of columns to be displayed
   */
  columns: prop<ITableColumn[]>()({
    type: Array,
    default: () => [],
  }),
}, (props) => {
  props.rows // TTableRow[]
  props.columns // ITableColumn[]

  return () => /* some render function */;
});

Contribute

First, fork the repo and clone it:

git clone https://github.com/%your-github-username%/vue-functional-props.git

Then:

npm install

Then:

npm run dev

Then introduce changes and propose a PR!
I'll be happy to review it!


Something's missing or found a bug?
Feel free to create an issue!