To enchant defineComponent

preface

  • Vue3 has been released for a long time. I believe you have read a lot of articles about Vue3 + typescript, especially the script setup + typescript officially released not long ago. This is really fragrant. All the used friends say it is good!
  • But the focus of this article is not this, but jsx + typescript; I don't know how many small partners, like Xiaobian, use both template and jsx for development; Cross jump repeatedly and switch back and forth; Of course, this is not optional. Generally, jsx + typescript is used when writing small components, and template + typescript is usually used when writing pages;
  • As a front-end developer with more than four years of experience in developing hundreds of components, today I'd like to share with you some thoughts and experiences of developing components using jsx + typescript in Vue3;

Relevant data:

background

For example, now we need to develop a stepper component to bind a digital variable in both directions. When clicking the plus sign, the binding value is increased by one, and when clicking the minus sign, the binding value is decreased by one; It looks like this:

First, the source code of the simple implementation of this component using defineComponent in the previous paragraph:

const DefineNumber = defineComponent({
    props: {
        modelValue: {type: Number}
    },
    emits: {
        'update:modelValue': (val?: number) => true,
        'add-num': (val: number) => true,
        'sub-num': (val: number) => true,
    },
    setup(props, ctx) {
        const handler = {
            onClickAdd: () => {
                const val = props.modelValue == null ? 1 : props.modelValue + 1
                ctx.emit('add-num', val)
                ctx.emit('update:modelValue', val)
            },
            onClickSub: () => {
                const val = props.modelValue == null ? 1 : props.modelValue - 1
                ctx.emit('sub-num', val)
                ctx.emit('update:modelValue', val)
            },
        }
        return () => (
            <div>
                <button onClick={handler.onClickSub}>-</button>
                <button>{props.modelValue == null ? 'N' : props.modelValue}</button>
                <button onClick={handler.onClickAdd}>+</button>
            </div>
        )
    },
})
Copy code

Use this stepper component in the parent component:

export const DemoPage = defineComponent(() => {

    const state = reactive({
        count: 123
    })

    return () => <>
        <h1>Hello world:{state.count}</h1>
        <DefineNumber
            v-model={state.count}
            onAdd-num={val => console.log('add', val)}
        />
    </>
})
Copy code

This is a controlled component. If there is no v-model binding value or state.count is not a responsive variable, this component will not be used;

event

You can see that the event type is defined as follows:

emits: {
    'update:modelValue': (val?: number) => true,
    'add-num': (val: number) => true,
    'sub-num': (val: number) => true,
},
Copy code

This is how you listen to events:

onAdd-num={val => console.log('add', val)}
Copy code

Common events such as add num are currently correct, and you can get the correct type prompt. But two-way binding events are not good;

  • For example, if you change the value of state.count to an object {}, you will find that the v-model does not prompt an error. In fact, the current version of definecomponent( Vue@3.2.21 )It is not deduced that the type of v-model should be modelValue;
  • If you want to listen for the update:modelValue event, you can listen in the template as follows: @ update:modelValue; However, in TSX, the method of onadd num cannot be used to monitor events. onUpdate:modelValue will report an error in TSX because this colon is not a special symbol that can be compiled. To listen to this event in TSX, you can only write as follows:
<DefineNumber
    v-model={state.count}
    onAdd-num={val => console.log('add', val)}
    {...{
        'onUpdate:modelValue': (val) => console.log('get change', val)
    }}
/>
Copy code

Next, let's take a look at the writing method improved by Xiaobian;

import {designComponent} from 'plain-ui-composition'

const DesignNumber = designComponent({
    props: {
        modelValue: {type: Number}
    },
    emits: {
        onUpdateModelValue: (val?: number) => true,
        onAddNum: (val: number) => true,
        onSubNum: (val: number) => true,
    },
    setup({props, event}) {

        const {emit} = event

        const handler = {
            onClickAdd: () => {
                const val = props.modelValue == null ? 1 : props.modelValue + 1
                emit.onAddNum(val)
                emit.onUpdateModelValue(val)
            },
            onClickSub: () => {
                const val = props.modelValue == null ? 1 : props.modelValue - 1
                emit.onSubNum(val)
                emit.onUpdateModelValue(val)
            },
        }

        return () => (
            <div>
                <button onClick={handler.onClickSub}>-</button>
                <button>{props.modelValue == null ? 'N' : props.modelValue}</button>
                <button onClick={handler.onClickAdd}>+</button>
            </div>
        )
    },
})

// Use in parent component
export const DemoPage = defineComponent(() => {
    const state = reactive({
        count: 123
    })
    return () => <>
        <h1>Hello world:{state.count}</h1>
        <DefineNumber
            v-model={state.count}
            onAdd-num={val => console.log('add', val)}
            {...{
                'update:modelValue': (val) => console.log('get change', val)
            }}
        />
        <DesignNumber
            v-model={state.count}
            onAddNum={val => console.log('add', val)}
            onUpdateModelValue={val => console.log('get update value', val, val?.toFixed(0))}
            onChange={val => console.log('get change value', val, val?.toFixed(0))}
        />
    </>
})
Copy code
  • First, defineComponent becomes designComponent;
  • Then there are many changes in the writing of event definition. When the emits option defines the event type, the name of the event is the name of the listening event in TSX, but it will be automatically converted to Henggang name when dispatching the event at runtime. For example, the onAddNum event will automatically dispatch an event named add num when it is dispatched at runtime (event.emit.onAddNum(val)), so that you can correctly listen to the event and get the correct type prompt whether in @ add num in template or onAddNum in TSX;
  • Meanwhile, designComponent will also deduce the type of v-model into the type of modelValue. Therefore, if the type of state.count is not number|undefined, there will be ts type detection error in the v-model attribute of DesignNumber;
  • There is also an implicit rule in designComponent, that is, when distributing the event onUpdateModelValue, three events will be distributed at one time, in the following order:
    • update-model-value
    • update:modelValue
    • change
  • The reason for dispatching the first event is to adapt to listening to the onUpdateModelValue event in tsx;
  • The reason for dispatching the second event is to adapt the v-model syntax sugar two-way binding value;
  • Sending the third event is to facilitate developers to easily monitor the value change of components when binding events; For example, if the developer wants to get the new value and old value of the binding value in a change, it can be written as follows:
<DesignNumber
    v-model={state.count}
    onUpdateModelValue={val => console.log([
        ['Parameter is a new value', val],
        ['here state.count Is the old value', state.count]
    ])}
    onChange={val => console.log([
        ['Parameter is a new value', val],
        ['here state.count It's also a new value', state.count]
    ])}
/>
Copy code
  • Because the dispatch event update:modelValue is a synchronous process, the binding value state.count obtained by onUpdateModelValue is the old value before the dispatch and execution of this event. After the execution of this event, the binding value state.count obtained by onChange is the new value;
  • In addition, the event monitoring mechanism inside the component is removed in Vue3, and designComponent is added here. Students with a lot of component development experience should know how important the event monitoring mechanism is for component development. In combination with the event definition of emits option, a set of component internal event API with type prompt as priority is designed in deisgnComponent. The use example is as follows:
const DesignNumber = designComponent({
    props: {
        modelValue: {type: Number}
    },
    emits: {
        onUpdateModelValue: (val?: number) => true,
        onAddNum: (val: number) => true,
        onSubNum: (val: number) => true,
    },
    setup({props, event}) {

        /*Dispatch event*/
        event.emit.onAddNum(100)

        /*Listening events*/
        const eject = event.on.onAddNum(val => {
            // Here val will be automatically derived as number type
            console.log('on_1', val)
        })

        /*Unregister Event */
        eject()

        /*Listen for an event*/
        event.once.onAddNum(val => {
            // Here val will be automatically derived as number type
            console.log('once', val)
        })

        /*Listen for events and remove them when the component is destroyed*/
        /*Generally speaking, when a component is destroyed, its own events will be automatically logged off. However, if the current component listens to events of other components that have not been destroyed, you need to log off this event listening when destroying*/
        onBeforeUnmount(event.on.onAddNum(val => {
            console.log('on_2', val)
        }))

        /*Manual logoff event*/
        const handler: Parameters<typeof event.on.onAddNum>[0] = val => {
            console.log('on_2', val)
        }
        event.on.onAddNum(handler)
        setTimeout(() => {
            event.off.onAddNum(handler)
        })

        return () => null
    },
})
Copy code

slot

  • In Vue3, there is no special description on the definition of slots. When Xiaobian saw the release of the official version of Vue3, he was a little disappointed with this piece of content. Because of the maintenance of the slot, it once caused great trouble to Xiaobian in the previous Vue2 version;
  • In the previous Vue2, a component does not need to declare events or slots when it is defined. The events distributed by the component and the slots used are all around the file. Sometimes it is not sure whether the distributed events are called in other components. When you need to adjust a component written by others, you often need to search $emits, slot and other keywords inside the component to judge which events and slots the component will send. Which slots are normal slots, which slots are scope slots, and what are the parameter types of scope slots. Basically, all of this depends on the developer's self-consciousness, and the information is supplemented in the component as comments. Sometimes there is no way to enforce this annotation specification, because there is no way to adjust the specification of old components, and there is no way to completely control the code quality of component developers;
  • As early as Vue2's @ Vue / composition API, designComponent had a set of options to define event types, as well as slots and scope slots, as shown below;

For example, now the DesignNumber component needs to be able to customize the content of the addition and subtraction button (slot) and the content of the display value (scope slot, parameter is the current value); The example code is as follows:

const DesignNumber = designComponent({
    props: {
        modelValue: {type: Number}
    },
    emits: {
        onUpdateModelValue: (val?: number) => true,
        onAddNum: (val: number) => true,
        onSubNum: (val: number) => true,
    },
    slots: [
        'add',
        'sub',
    ],
    scopeSlots: {
        default: (scope?: number) => {},
    },
    setup({props, event: {emit}, slots, scopeSlots}) {

        const handler = {
            onClickAdd: () => {
                const val = props.modelValue == null ? 1 : props.modelValue + 1
                emit.onAddNum(val)
                emit.onUpdateModelValue(val)
            },
            onClickSub: () => {
                const val = props.modelValue == null ? 1 : props.modelValue - 1
                emit.onSubNum(val)
                emit.onUpdateModelValue(val)
            },
        }

        return () => (
            <div>
                <button onClick={handler.onClickSub}>{slots.sub(<span>-</span>)}</button>
                {
                    scopeSlots.default(
                        props.modelValue,
                        <button>{props.modelValue == null ? 'N' : props.modelValue}</button>,
                    )
                }
                <button onClick={handler.onClickAdd}>{slots.add(<span>+</span>)}</button>
            </div>
        )
    },
})

// Use components
export const DemoPage = defineComponent(() => {
    const state = reactive({
        count: 123
    })
    return () => <>
        <h1>Hello world:{state.count}</h1>
        {/*<DefineNumber
            v-model={state.count}
            onAdd-num={val => console.log('add', val)}
            {...{
                'update:modelValue': (val) => console.log('get change', val)
            }}
        />*/}

        <DesignNumber v-model={state.count}/>

        <DesignNumber v-model={state.count} v-slots={{
            add: () => <span>add</span>,
            sub: () => <span>sub</span>,
            default: val => <input type="text" value={val}/>
        }}/>
    </>
})
Copy code
  • slot
    • The slots option is an array of strings;
    • The setup function will get a slots object. Each value of the slots object is a function, and the function parameter is the default slot content; When the component receives the custom slot content, it uses the custom content; otherwise, it uses the default slot content;
  • Scope slot
    • The scopeSlots option is an object. The key of the object is the slot name, and the value is the function type of the scope slot. This function has only one parameter. The type defined by this parameter is the scope object type obtained when using this component;
    • The setup function will get a scopeSlots object, and each value is a function that renders the contents of the scope slot. The function has two parameters. The first parameter is the scope object and the second parameter is the default content. When the parent component does not customize the scope slot, the default content is rendered;
  • v-slots
    • There are two ways to pass slots to components in jsx. This is the official one. One is to pass an object through v-slots. The key of the object is the name of the slot, and value must be a function. The other is through the location of the children of the component; For example, the wording in the above example can be changed to:
<DesignNumber v-model={state.count}>
    {{ 
        add: () => <span>add</span>,
        sub: () => <span>sub</span>,
        default: val => <input type="text" value={val}/>
    }}
</DesignNumber>
Copy code

Note that at present, plain UI composition only supports v-slots with type derivation. The above method of passing through children still does not support type derivation (that is, in the above code, the val parameter in the default slot function will be deduced as an implicit any type). However, plain design composition supports children's passing and type derivation; It can only be said that Xiaobian is not good at learning at present, and it is temporarily unable to define the type of component children.

quote

  • The most common way of communication between parent and child components should be that the parent component passes attributes to the child component, and the parent component listens to the events sent by the child component; However, this method has great limitations and low flexibility. At this time, in order to give full play to its ability, the child component can expose some methods and state variables. After the parent component obtains the reference of the child component, it can use these exposed methods and variables to realize more complex functions; A common scenario is that when writing a form, the verification function of the form component must be called before submitting the form. After the verification is passed, the form data can be submitted to the background;
  • There are two general ways to get a reference:
    • Get the reference of dom node;
    • Get the reference of the custom component;
  • In the designComponent, a function called useRefs is provided to manage the reference to child nodes in order to get sufficient type prompt when obtaining references. The example is as follows:;
const DesignNumber = designComponent({
    props: {
        modelValue: {type: Number}
    },
    emits: {
        onUpdateModelValue: (val?: number) => true,
        onAddNum: (val: number) => true,
        onSubNum: (val: number) => true,
    },
    setup({props, event: {emit}}) {
        const handler = {
            onClickAdd: () => {
                const val = props.modelValue == null ? 1 : props.modelValue + 1
                emit.onAddNum(val)
                emit.onUpdateModelValue(val)
            },
            onClickSub: () => {
                const val = props.modelValue == null ? 1 : props.modelValue - 1
                emit.onSubNum(val)
                emit.onUpdateModelValue(val)
            },
        }
        const methods = {
            reset: (val?: number) => {
                emit.onUpdateModelValue(val == null ? 0 : val)
            },
        }
        return {
            refer: {
                methods,
            },
            render: () => (
                <div>
                    <button onClick={handler.onClickSub}>-</button>
                    <button>{props.modelValue == null ? 'N' : props.modelValue}</button>
                    <button onClick={handler.onClickAdd}>+</button>
                </div>
            ),
        }
    },
})

// Code using components
export const DemoPage = defineComponent(() => {

    const {refs, onRef} = useRefs({
        number: DesignNumber,                   // Gets a reference to the DesignNumber component
        btn: iHTMLButtonElement,                // Gets the reference of the button node
    })

    const state = reactive({
        count: 123
    })

    const handler = {
        onReset: () => {
            console.log(refs)
            refs.number?.methods.reset()
        },
    }

    return () => <>
        <h1>Hello world:{state.count}</h1>
        <DesignNumber v-model={state.count} ref={onRef.number}/>
        <button ref={onRef.btn} onClick={handler.onReset}>reset value</button>
    </>
})
Copy code
  • First, use useRefs to declare the sub components to be referenced, and you will get refs and onRef objects;
  • You need to assign the value in onRef to the ref attribute of the corresponding sub component, and then you can use refs as a total component reference object.
  • In addition, it also has the function of type prompt; For example, the type of refs.number is the last refer ence object returned by the DesignNumber component. If the question mark is removed from the val parameter type of the reset function in the example code, it becomes a necessary parameter. At this time, refs. Number?. in DemoPage Methods. Reset() will have a type prompt error, and the required parameter val is missing; Similarly, at this time, refs.btn is an HTMLButtonElementdom object, and you can get the corresponding type prompt;
  • iHTMLButtonElement is an anonymous object {}, but the type is HTMLButtonElement
    • Source code: export const iHTMLButtonElement = {} as typeof HTMLButtonElement
    • The reason is that there is no HTMLButtonElement object in some non browser environments, such as applets, such as SSR. Here, plain UI position exports a simple object used to assist type prompt;
    • If you can ensure that the code runs in the browser environment, it is also possible to replace iHTMLButtonElement with HTMLButtonElement;

injection

  • The above mentioned how the parent component gets the type prompt when referring to the child component, which is only applicable to the parent and child components. This method is not applicable when using provide/inject to communicate with descendant components.
  • The next example shows how to get the type of injected object during injection;
// Parent component that provides status to descendant components
const DesignNumber = designComponent({
    provideRefer: true,
    name: 'design-number',
    setup() {
        const methods = {
            reset: (val?: number) => {
                console.log('reset', val)
            },
        }
        return {
            refer: {
                methods,
            },
            render: () => null,
        }
    },
})

// Descendant component injection object
const DesignNumberChild = designComponent({
    setup() {
        // inject does not give a default value, which means that the DesignNumber of the parent component must be injected, otherwise there will be a runtime error
        const certainParent = DesignNumber.use.inject()
        console.log(certainParent.methods.reset())

        // inject has a default value of null. When the parent component DesignNumber is not injected, the default value is null
        const uncertainParent = DesignNumber.use.inject(null)
        console.log(uncertainParent?.methods.reset())   // An optional operator must be added after uncertain part?, Otherwise, there will be a ts error of Object is possibly null
        
        return () => null
    },
})
Copy code
  • First, DesignNumber, the parent component that provides data to the descendant component, needs to provide two options:
    • name:'design-number'
    • provideRefer:true
  • In this way, provide ('@ @ design number', refer) will be automatically executed when the component is running
  • Then, the child component only needs to call DesignNumber.use.inject() to inject the state variables provided by the parent component. The inject function is the same as the Vue3 standard inject function, except that the inject function provides the function of type prompt;

inherit

  • In Vue3, attributes are passed to a sub component. If some attributes are not declared in props and emits, this attribute will be stored in attrs and passed to the root node of the sub component by default. If the sub component is multiple root nodes, a runtime warning will be triggered;
  • In tsx, passing attributes not defined in props or emits to a component will lead to ts compilation errors;
  • Next, the example shows how to declare inherited attribute types in designComponent;
const DesignNumber = designComponent({
    props: {
        modelValue: {type: Number},
        max: {type: Number},
        min: {type: Number,},
    },
    emits: {
        onUpdateModelValue: (val?: number) => true,
        onAddNum: (val: number) => true,
        onSubNum: (val: number) => true,
    },
    setup() {
        return () => null
    },
})
const InheritHTMLButton = designComponent({
    inheritPropsType: iHTMLButtonElement,
    props: {
        // Customize the type attribute and override the type attribute of the button
        // Because it is defined in props, type is not automatically passed to the root node button
        type: {type: Number}
    },
    setup() {
        return () => <button/>
    },
})
const InheritDesignNumber = designComponent({
    inheritPropsType: DesignNumber,
    props: {
        // The max attribute type of overriding DesignNumber is string
        max: {type: String},
        // Custom mandatory attributes
        precision: {type: Number, required: true}
    },
    emits: {
        // The parameter type of onAddNum event that overrides DesignNumber is string
        onAddNum: (val: string) => true,
        onAddNum2: (val: number) => true,
    },
    setup() {
        return () => <DesignNumber/>
    },
})

export const DemoPage = defineComponent(() => {
    const state = reactive({
        count: 123
    })
    return () => <>
        {/*tabindex,Inherit the properties of the button*/}
        {/*type,Overridden properties*/}
        <InheritHTMLButton tabindex={1} type={100}/>

        {/*precision,Required attributes for customization*/}
        {/*max,The override type is string*/}
        {/*min,Inheritance properties*/}
        {/*onAddNum,The override type is function, and the function parameter is string*/}
        {/*onAddNum2,Custom events*/}
        {/*onSubNum,Inherited event type*/}
        <InheritDesignNumber
            precision={100}
            max={"100"}
            min={100}
            onAddNum={val => console.log(val.charAt(0))}
            onAddNum2={val => console.log(val.toPrecision(0))}
            onSubNum={val => console.log(val.toPrecision(0))}
        />
    </>
})
Copy code
  • In the example, there are two components that inherit properties:
    • InheritHTMLButton inherits the properties of the native button component;
    • InheritDesignNumber inherits the properties of the custom component DesignNumber;
  • When defining a component, you can specify the inherited attribute type through the inheritPropsType option; The only function of this option is to provide inherited attribute type prompt, which has no effect at runtime;
  • If the attribute and event defined by the component itself conflict with the inherited attribute event name, the last attribute event with the same name is mainly defined by the component itself, because this attribute will not be automatically passed to the root node at this time;
  • inheritPropsType combined with inheritAttrs option can also be used in another way;
    • For example, it is now necessary to package a PlInput component based on the input native component, but the root node of the PlInput component is not input, but a div, because this div can enrich the functions of the PlInput component, such as displaying suffix icons, front content slots, rear content slots, etc;
    • In this case, it seems reasonable to set the attribute type of PlInput as HTMLDivElement when defining the inheritance attribute type. However, in real development scenarios, there are often many scenarios in which the attribute is set for the input node, but there are few scenarios in which the attribute is set for the root node div.
    • Based on this scenario, you can do the following: 1. Set the inheritPropsType inheritance property type to HTMLInputElement; 2. Setting inheritAttrs:false does not automatically pass additional attributes to the root node, but manually passes attrs to the input node in the setup function. The example code is as follows:
const PlInput = designComponent({
    inheritPropsType: HTMLInputElement,
    inheritAttrs: false,
    props: {
        modelValue: {type: String},
        wrapperAttrs: {type: Object}
    },
    setup({props, attrs}) {
        return () => (
            /*If the developer needs to set the root node attribute, it can be set through the wrapperAttrs attribute object*/
            <div {...props.wrapperAttrs}>
                {/*Manually pass attrs to the input node*/}
                <input type="text" {...attrs}/>
            </div>
        )
    },
})

export const App = () => <>
    {/*div If there is no type attribute, there will be an error message for ts compilation*/}
    {/*<div type="submit"/>*/}

    {/*PlInput It inherits the HTMLInputElement attribute type, so it supports receiving the type attribute; Because inheritAttrs:false is set, although the type is not defined in props, it will not be passed to the root node div, but manually passed to the input node through attrs*/}
    <PlInput wrapperAttrs={{class: 'class-on-div'}} class="class-on-input" type="submit"/>
</>
Copy code

binding

  • The emergence of plain UI composition was explored step by step by the Xiaobian when developing the component library plain UI;
  • At present, all plain UI components that support binding are uncontrolled components; There are many articles on the Internet detailing the differences, advantages and disadvantages between controlled components and uncontrolled components, which will not be repeated here. Here is a brief introduction to useModel, a combination function used to quickly realize the binding value of uncontrolled components in plain UI composition;

Single value binding: implement a counter component. Click the plus (minus) button to add one (minus one) to the bound value count

const PlNumber = designComponent({
    props: {
        modelValue: {type: Number}
    },
    emits: {
        onUpdateModelValue: (val?: number) => true,
    },
    setup({props, event: {emit}}) {

        const model = useModel(() => props.modelValue, emit.onUpdateModelValue)

        return () => (
            <div>
                <button onClick={() => model.value = (model.value == null ? 0 : model.value - 1)}>-</button>
                <button>{model.value == null ? 'N' : model.value}</button>
                <button onClick={() => model.value = (model.value == null ? 0 : model.value + 1)}>+</button>
            </div>
        )
    },
})

export const App = designComponent({
    setup() {
        const state = reactive({
            count: undefined
        })
        return () => <>
            <PlNumber v-model={state.count}/>
            <PlNumber v-model={state.count}/>
            {state.count}
        </>
    },
})
Copy code

Multi value binding:

  • Implement a component for editing numbers: PlNumber;
  • Define a range attribute. If range is not set to true, edit the single value, and the binding is also a single value;
  • When range is true, edit multiple values, and the binding is also multiple values;
const PlNumber = designComponent({
    props: {
        modelValue: {type: [Number, String]},
        range: {type: Boolean},
        start: {type: [Number, String]},
        end: {type: [Number, String]},
    },
    emits: {
        onUpdateModelValue: (val?: number | string) => true,
        onUpdateStart: (val?: number | string) => true,
        onUpdateEnd: (val?: number | string) => true,
    },
    setup({props, event: {emit}}) {

        const model = useModel(() => props.modelValue, emit.onUpdateModelValue)
        const startModel = useModel(() => props.start, emit.onUpdateStart)
        const endModel = useModel(() => props.end, emit.onUpdateEnd)

        return () => (
            <div>
                {
                    !props.range ? <>
                        <button onClick={() => model.value = (model.value == null ? 0 : Number(model.value) - 1)}>-</button>
                        <button>{model.value == null ? 'N' : model.value}</button>
                        <button onClick={() => model.value = (model.value == null ? 0 : Number(model.value) + 1)}>+</button>
                    </> : (<>
                        <input type="text" v-model={startModel.value}/>
                        to
                        <input type="text" v-model={endModel.value}/>
                    </>)
                }
            </div>
        )
    },
})

export const App = designComponent({
    setup() {
        const state = reactive({
            formData: {
                value: undefined,
                startValue: undefined,
                endValue: undefined,
            },
        })
        return () => <>
            {/*Use alone*/}
            <PlNumber v-model={state.formData.value}/>
            {/*Enter numeric range*/}
            <PlNumber range v-models={[
                [state.formData.startValue, 'start'],
                [state.formData.endValue, 'end'],
            ]}/>
            {JSON.stringify(state.formData)}
        </>
    },
})
Copy code

When binding multiple values in template, it is written as follows:

<template>
    <pl-number v-model:start="state.formData.startValue" v-model:end="state.formData.endValue"/>
</template>
Copy code

epilogue

  • A video recorded by Xiaobian and some descriptions of some examples in this article are attached. The video address is: www.bilibili.com/video/BV1Q4...
  • Plain UI is a Vue3.0 component library, which is developed on the basis of plain UI composition. At present, all components are developed using jsx + typescript + composition api. Students in need can refer to the source code of some components; At present, the default theme color of the component library is green, and the online document address is: plain-pot.gitee.io/plain-ui-do... ; When I have time, I will write an article to introduce the interesting contents of personal R & D in this component library;
  • Plain UI composition has a twin brother, plain design composition. This library is a tool library that helps developers use VueCompositionApi and two-way binding functions in existing React applications through a few lines of code configuration. Its type prompt is more powerful and accurate than plain UI composition; And API is as like as two peas. Online document address: plain-pot.gitee.io/plain-desig... ; If you have time, Xiaobian will also write a document to introduce the library;
  • Plain design is a set of React component library developed based on plain design composition. At present, the default theme color is dark blue. The online document address is: plain-pot.gitee.io/plain-desig... ; This component library can be directly used in existing React applications and coexist with other existing React component libraries; Interestingly, the source code of the components inside is very similar to that of plain UI, which coincides with the foundation / adapter architecture proposed by Semi Design a few days ago; The difference is that at present, plain only implements Vue3.0 and React; The foundation part is implemented by plain UI composition and plain design composition, and the adapter is implemented by the developer. The code reuse rate in the adapter is as high as 99%. In most cases, after removing the types in the components and turning them into es6 source code, it is difficult to distinguish which is Vue component and which is React component;
  • Xiaobian spent more than two years exploring Vue + jsx + typescript. The above is a summary of some exploration contents. Of course, different people will have different views. Some people may think that encapsulating a designComponent based on defineComponent is "you tamper with my lyrics at will is painting a snake and adding feet". Some people may also get inspiration from some examples in this article. Anyway, first of all, Xiaobian is not malicious and does not intend to change anyone's programming habits. If you get some inspiration from this article with jsx + typescript + composition api, congratulations and learn another trick. If you prefer to use template + typescript instead of jsx, just laugh it off. Finally, all of the above open source libraries are not KPI driven (at least they don't get any return or promotion at work) and are not implemented for the purpose of implementation, but only when they have this idea in daily life.

Posted on Sun, 07 Nov 2021 22:02:33 -0500 by Gladiator2043