Handle functional component props properly
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.
npm i -S vue-functional-props
or
yarn add vue-functional-props
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');
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);
});
import { component, withProps, prop } from 'vue-functional-props';
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);
}
);
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 exportedwithProps
, but accepts only one argument - props definition. Returns the same object as thecomponent
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 thecomponent
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!
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 */;
});
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!