Why cover all the components with a shell?

In a real project there was a requirement to set the size of all components to small. At the time, the project was short and you could only use the editor for global replacement. Now you have time, so you can use vue3 syntax to wrap all the components provided by Ant-Design-Vue (2.x) so that you can change the default values of the components at once.

Extract component name

The ant-design-vue UI contains a lot of components, and it takes time to just copy and paste them, so use regular expressions to extract all component names from the node_modules\ant-design-vue\lib\components

Affix,Anchor,AnchorLink,AutoComplete,AutoCompleteOptGroup,AutoCompleteOption,Alert,Avatar,AvatarGroup,BackTop,Badge,Badg eRibbon,Breadcrumb,BreadcrumbItem,BreadcrumbSeparator,Button,ButtonGroup,Calendar,Card,CardGrid,CardMeta,Collapse,Collap sePanel,Carousel,Cascader,Checkbox,CheckboxGroup,Col,Comment,ConfigProvider,DatePicker,RangePicker,MonthPicker,WeekPicke r,Descriptions,DescriptionsItem,Divider,Dropdown,DropdownButton,Drawer,Empty,Form,FormItem,Grid,Input,InputGroup,InputPa ssword,InputSearch,Textarea,Image,ImagePreviewGroup,InputNumber,Layout,LayoutHeader,LayoutSider,LayoutFooter,LayoutConte nt,List,ListItem,ListItemMeta,message,Menu,MenuDivider,MenuItem,MenuItemGroup,SubMenu,Mentions,MentionsOption,Modal,Stat istic,StatisticCountdown,notification,PageHeader,Pagination,Popconfirm,Popover,Progress,Radio,RadioButton,RadioGroup,Rat e,Result,Row,Select,SelectOptGroup,SelectOption,Skeleton,SkeletonButton,SkeletonAvatar,SkeletonInput,SkeletonImage,Slide r,Space,Spin,Steps,Step,Switch,Table,TableColumn,TableColumnGroup,Transfer,Tree,TreeNode,DirectoryTree,TreeSelect,TreeSe lectNode,Tabs,TabPane,TabContent,Tag,CheckableTag,TimePicker,Timeline,TimelineItem,Tooltip,Typography,TypographyLink,Typ ographyParagraph,TypographyText,TypographyTitle,Upload,UploadDragger,LocaleProviderCopy the code

Component packaging

My train of thought

Considering the total number of components is 123, using.vue files to package components is also a lot of work, and there will be a lot of duplication.

Here I chose to define and export all the components one by one in a TS file using defineComponent (one template, batch replacement), build the parameters in the setup function, and render the template in the Render function.

Here you might ask, why not just return a render function in the setup function?

The first version I did at first seemed to have no problem with component encapsulation, but this solution could not call functions inside the component through ref, and in some cases (such as manually calling focus) did not meet requirements.

Parameter passthrough

Let’s start with the code

interface RawProps {
    [key: string] :any
}
​
interface SetupArgs {
    props: RawProps
    slots: any
}
/** * Get the parameters required for component rendering *@param props
 * @param attrs
 * @param slots
 * @param extraArgs* /
function useSetup(props: RawProps, {
    attrs,
    slots,
}: SetupContext<any>, extraArgs: RawProps = {}) :SetupArgs {
    let attrAndProps = computed(() = >({... unref(attrs), ... props, ... extraArgs}));let renderSlots = extendSlots(slots);
    return {
        props: attrAndProps,
        slots: renderSlots,
    };
}
Copy the code
import {Slots} from "vue";
import {isFunction} from "@/utils/is";
​
/** * slot pass *@param slots
 * @param excludeKeys* /
export function extendSlots(slots: Slots, excludeKeys: string[] = []) {
    const _getSlot = (slots: Slots, slot = "default", data? :any) = > {
        if(! slots || !Reflect.has(slots, slot)) {
            return null;
        }
        if(! isFunction(slots[slot])) {console.error(`${slot}is not a function! `);
            return null;
        }
        const slotFn = slots[slot];
        if(! slotFn)return null;
        return slotFn(data);
    };
    const slotKeys = Object.keys(slots);
    const ret: any = {};
    slotKeys.forEach((key) = > {
        if(! excludeKeys.includes(key)) { ret[key] =() = >_getSlot(slots, key); }});return ret;
}
​
Copy the code

Component encapsulation primarily addresses these two points:

  1. Attribute (event) passing

    Vue3 combines $attrs with $Listeners, so only attrs can be processed. (During development, I found that key and ref values on the component are not passed in attrs)

  2. Slot passing (see vue-vben-admin)

    Iterate through the slots of the context provided by vue3 to filter out the slots that need to be excluded.

The component rendering

As mentioned earlier, all component names are extracted, and all components are stored in the Map. The actual code is long, and only the key code is pasted here

import {Button} from "ant-design-vue";
export type ComponentType ="Button"
export const componentMap = new Map<ComponentType, Component>();
componentMap.set("Button", Button);
​
export function get(compName: ComponentType) {
    return componentMap.get(compName) as (typeof defineComponent);
}
Copy the code

With all the components ready, you can start rendering

/** * use render to render, parameters are exposed in setup *@param componentType
 * @param props
 * @param slots* /
function componentRender(componentType: ComponentType, {props, slots}: SetupArgs) {
    let component = get(componentType);
    returnh(component, {... unref(props)}, slots); }Copy the code

Component definition

Here’s an example of wrapping a Button

import VueTypes from "vue-types";
​
export const XButton = defineComponent({
    name: "XButton".props: {
        size: VueTypes.oneOf(["small"."middle"."large"]).def("small"),
        onClick:VueTypes.func // The event decorator returns an array
    },
    setup(props, context) {
        return useSetup(props, context);
    },
    render(setupArgs: SetupArgs) {
        return componentRender("Button", setupArgs); }});Copy the code

The size property of the official Button component defaults to middle, but has been changed to small and is used in the same way as documented.

 <x-button type="primary" @click="handleAdd" class="margin-left-10">
     <template #icon>
        <ImportOutlined/>
     </template>Import < / x - button >Copy the code

For another example, I shared in my previous article that encapsulates an input component that filters whitespace. How do you do that in Vue3?

Come on, code.

type Nullable<T> = T | null;
export const XInput = defineComponent({
    name: "XInput".emits: ["update:value"].props: {
        // By default, the value is trimmed. If not, val=>val
        filterHandler: VueTypes.func.def(val= > val.trim()),
    },
    setup(props, context) {
        let inputRef = ref<Nullable<HTMLElement>>(null);
        let handleFilterValue = (e: any) = > {
            context.emit("update:value", props.filterHandler? .(e.target.value)); };const focus = () = >inputRef? .value? .focus();let setupArgs = useSetup(props, context, {ref: inputRef, onChange: handleFilterValue});
        return{ inputRef, ... setupArgs, focus, }; },render(setupArgs: SetupArgs) {
        return componentRender("Input", setupArgs); }});Copy the code

Special handling

So far, I have found that there is one component that can’t be handled by the above scheme. This component is Table. The slots for the Table component are not fixed. Setting columns’ slots property dynamically generates slots. The current solution is to solve the slot passing problem by defining new slots in the slot

<template>
  <Table
      v-bind="attrAndProps"
  >
    <! Define slot in slot -->
    <template# [name] ="data" v-for="(_,name) in $slots" :key="name">
      <slot :name="name" v-bind="data"></slot>
    </template>
  </Table>
</template>
​
<script lang="ts">
import {computed, defineComponent, unref} from "vue";
import {Table} from "ant-design-vue";
​
export default defineComponent({
  name: "XTable".components: {
    Table,
  },
  setup(props, {attrs}) {
    let attrAndProps = computed(() = >({... unref(attrs), ... props}));return{ attrAndProps, }; }}); </script>Copy the code

Effect of screenshots

The template screenshots

Effect of screenshots

conclusion

Using the above solution, it is easy to build a fully controlled component library with as little code change as possible when changing the default values and changing the UI framework. In the process of exploring this scheme, I also found some existing problems:

  1. The key attribute could not receive the value during the component transfer process. It is assumed that vue has internal processing when rendering the component. Some components that require a key attribute (such as menu-item) can only be replaced by an alias. –

    // Export const XMenuItem = defineComponent({name: "XMenuItem", props: {keyName: VueTypes.string, }, setup(props, context) { return useSetup(props, context, {key: props.keyName}); }, render(setupArgs: SetupArgs) { return componentRender("MenuItem", setupArgs); }});Copy the code
  2. The Table component cannot currently be handled in a uniform manner.

  3. The method provided by the original component needs to refer to the way of Focus and bind REF for external exposure, which is a little troublesome.

The resources

Vue-Vben-Admin

jeecg-boot