如何在React项目中封装定制的筛选组件?

如何在React项目中封装定制的筛选组件?今天是个好日子,一个充满爱的节日。抓住五月最后的时间,决定来更一篇文章,让那些过节的人感到卷狗的可怕。520,给我冲!

前言

今天是个好日子,一个充满爱的节日。抓住五月最后的时间,决定来更一篇文章,让那些过节的人感到卷狗的可怕。前段时间还是一直在边工作边看机会,但是没有看到合适的机会,所以还是决定慢慢寻觅,毕竟第一志愿还是想去大厂工作,所以也就没有关注一些小厂的机会。总的来说,就是革命尚未成功,同志还需努力啊。

背景

老规矩,聊主题之前还是先说背景。在公司上班还是在不断的接需求进行开发,期间有一个需求就是实现表格的表头筛选功能,其实也不是需求,就是在做表格渲染数据时,数据量很大,使用antdTable组件时会使页面渲染时长变长,影响体验。故选择自行用div盒子渲染成table表格,自己来写表格样式,那就没有现成的表头筛选组件了,所以就需要自己来封装一个表头筛选组件,方便后续其他页面的使用和其他同事的调用了。

image.png

image.png

就是上面这么一个效果:有一个输入框对选项列表进行筛选,命中的会勾选中,然后点击重置按钮可以清空搜索框值并关闭筛选组件,点击确定则将选中的值进行数据查询,这应该算是定制的筛选组件了吧。

其实写这种封装组件的文章我还真有点迷惘,不知道怎么表达才能让你知道我所想表达的内容,我在慢慢探索如何写此类文章的方式,希望后续的表达能帮助到你,让你能懂得此文的组件封装过程。话不多说,下面就来动手搞一搞这样一个组件,看看我的做法是否和你的思路相符合,Let’s go!

开发

首先封装一个组件的根本就是方便后续复用它,其次就是别人的不适用于自己的需求场景,所以这时我们就需要去封装自己所需的组件,如果自己的组件能帮助到同事及其他人那会是一个值得开心的事情(我就很享受这种感觉)。

此文的技术栈是使用的ReactUI框架还是与之匹配的Antd库,期间还会有一些typescript的语法和一些hooks的使用,所以技术栈不是React,或者不熟悉相关知识的伙伴试着理解思路,后续自行使用其他技术栈去封装类似的组件。

接下来就会根据背景提到的效果,将其拆分为细小块去聊→

组件布局

引用组件:

import React, {useEffect, useState, useRef, forwardRef, useImperativeHandle} from 'react';
import {Button, Checkbox, Dropdown, Form, Input, List} from "antd";
import {FilterFilled} from "@ant-design/icons";
import {ObjTest} from "../../../util/common";
import { v4 as uuidv4 } from 'uuid';
import '../style/dropdown.less';
<Form 
    className={['custom-table-filter-dropdown-wrap', className].join(' ')} 
    name={formName} 
    form={form}
>
    <Dropdown
        overlayClassName={['custom-table-filter-dropdown', overlayClassName].join(' ')}
        visible={visible}
        placement={placement}
        overlay={(
            <div className="dropdown-body" ref={containerRef}>
                <div className="pb10">
                    <Form.Item name="keywords" noStyle>
                        <Input
                            placeholder="请输入搜索关键字筛选"
                            onChange={changeHandle}
                            allowClear
                        />
                    </Form.Item>
                </div>
                <Form.Item name="pkids" noStyle initialValue={[]}>
                    <Checkbox.Group>
                        <List
                            split={false}
                            dataSource={data}
                            renderItem={rowRenderer}
                        />
                    </Checkbox.Group>
                </Form.Item>
                <div className="mt10 flexic">
                    <Button
                        type="link"
                        size="small"
                        onClick={resetHandle}
                    >重置
                    </Button>
                    <Button
                        className="mla button-red"
                        type="primary"
                        size="small"
                        onClick={confirmHandle}
                    >确定
                    </Button>
                </div>
            </div>
        )}
    >
        <Form.Item noStyle shouldUpdate>
            {() => (
                <FilterFilled
                    className={[ObjTest.isNotEmptyArray(getFieldValue('pkids')) ? 'active' : ''].join(' ')}
                    onClick={showHandle}
                />
            )}
        </Form.Item>
    </Dropdown>
</Form>

从上面代码可以看到,在外层是用Form组件包裹住了内部的组件代码,这样做的一个目的主要就是为了让其受控,便于我们取值和避免异步赋值的问题。然后组件的显示隐藏组件使用了Dropdown组件,这里也可以选用Popover组件,就看平时喜欢使用哪个组件了,而它的显示或隐藏则会根据点击FilterFilled图标来控制是否让其显示。组件内部的的核心就包含:输入框,复选框和操作按钮(重置、确定),复选框则是使用List组件对其进行包裹,这是为了以后复选框的值如果很多这就可以使用列表的虚拟滚动效果了。

逻辑代码

上面组件的主体代码就是如此,下面就会将上面布局中涉及的方法逐一进行解释。

首先是定义外部可能传入变量,即props值:

interface FilterDropdownProps {
    overlayClassName?: string; // 下拉样式类名
    className?: string; // 筛选类名
    placement?: Placement; // 显示位置
    filters: any[]; // 数据源
    onConfirm?: (values: string[]) => void; // 确定
    onReset?: () => void; // 重置
    onTrigger?: () => void; // 点击按钮显示下拉时触发
    isObject?: boolean; // 数据是否是数组对象
}

接着定义Dropdown组件显示位置的变量值集合:

declare const Placements: ["topLeft", "topCenter", "topRight", "bottomLeft", "bottomCenter", "bottomRight"];
declare type Placement = typeof Placements[number];

然后是组件内部自身所需要的变量定义:

const {
    overlayClassName = '',
    className = '',
    placement,
    filters,
    onConfirm = (values: any[]) => {},
    onReset = () => {},
    onTrigger = () => {},
    isObject = false,
} = props;
const [data, setData] = useState<any[]>([]);
const [visible, setVisible] = useState<boolean>(false);
const [formName, setFormName] = useState(null);

const [form] = Form.useForm();
const { resetFields, getFieldsValue, getFieldValue, setFieldsValue } = form;

const containerRef = useRef<any>(null);

在组件布局中,可以看到Form表单上的form控制实例是由setFormName方法设置,这里使用了uuid插件进行使用,目的就是为了防止多个筛选组件表单实例重复。设置form实例代码如下:

useEffect(() => {
    setFormName(uuidv4());
}, [])

接着将外部传入的筛选项数据数组使用内部data变量来接收:

useEffect(() => {
    setData(filters || []);
}, [filters])

现在,数据有了,那就要将其渲染显示出来,组件布局中的使用List组件渲染筛选项,但是没有展示具体代码, rowRenderer方法的具体代码如下:

const rowRenderer = (item: any) => {
    return (
        <List.Item> <Checkbox value={ObjTest.isObj(item) ? item.value : item} > {ObjTest.isObj(item) ? item.label : item} </Checkbox> </List.Item>
    )
}

可以看到在内部对筛选项进行判断,这是由于传入的数据数组可能是对象数组,也可能是字符串数组。如果是对象则显示的label值,选中时则是传出value值,而对于字符串数组则显示传出都是它本身而已。这里的判断就是为了让该筛选组件适用更多的数据场景,从而使其更加灵活。

渲染完毕后,要想让Dropdown出现并进行操作,就要去点击FilterFilled图标让其显示出来,showHandle方法代码如下:

/** * 显示下拉 */
const showHandle = () => {
    setVisible(true);
    onTrigger();
}

接着,那就是要操作筛选项了,首先输入框要对其筛选并自动将匹配到的选项自动勾选,所以输入框的onChange事件对应的changeHandle方法的具体代码如下:

/** * 搜索关键字 * @param e */
const changeHandle = (e: any) => {
    const { value } = e.target;
    let target = [...filters];
    if (value) {
        if (isObject) {
            target = (filters || []).filter((v: any) => v.label.includes(value));
            let pkids = (target || []).map(v => v.value);
            setFieldsValue({pkids});
        } else {
            target = (filters || []).filter((v: any) =>  v.includes(value));
            setFieldsValue({pkids: target});
        }
    } else {
        resetFields();
    }
    setData(target);
}

可以看到,根据外部传入的isObject字段用于判断筛选项数组是字符串数组还是对象数组,从而获取查询需要的筛选值集合。

筛选项操作完成后,可能选择的项数有点多,那不想一个个去取消选中,则直接点击重置按钮就可以清空选项,隐藏下拉并告诉外部组件查询数据,resetHandle方法如下:

/** * 重置 */
const resetHandle = () => {
    setData(filters);
    resetFields();
    setVisible(false);
    onReset();
}

确定按钮则是会将选中的值传出,隐藏下拉并告诉外部组件查询数据,confirmHandle方法如下:

/** * 确定 */
const confirmHandle = () => {
    const { pkids } = getFieldsValue();
    setVisible(false);
    onConfirm(pkids);
}

但是有一种情况就是选择值不想查询,这时点击非下拉区域需要将组件隐藏,这时就需要去监听用户点击的位置并处理相应逻辑,代码如下:

useEffect(() => {
    // 点击非下拉位置关闭下拉
    const clickEvent = (e: any) => {
        if (containerRef.current && !containerRef.current.contains(e.target) && visible) {
            setVisible(false);
        }
    }
    document.addEventListener('click', clickEvent, false);
    return () => {
        document.removeEventListener('click', clickEvent, false);
    }
}, [visible])

当这一切的完成时,还有一个问题需要注意的那就是外部组件同时使用了多个该筛选组件时,需要清空对应自己的选中项时,就需要将重置方法暴露给外部组件进行调用,如下:

useImperativeHandle(ref, () => ({
    // 暴露方法给父组件
    resetFields,
}))

好了,组件布局中所涉及的方法就介绍完了,顺序就是根据组件的渲染,操作这种顺序进行介绍的,下面就会将这个组件的完整代码贴出→

完整代码

import React, {useEffect, useState, useRef, forwardRef, useImperativeHandle} from 'react';
import {Button, Checkbox, Dropdown, Form, Input, List} from "antd";
import {FilterFilled} from "@ant-design/icons";
import {ObjTest} from "../../../util/common";
import { v4 as uuidv4 } from 'uuid';
import '../style/dropdown.less';


declare const Placements: ["topLeft", "topCenter", "topRight", "bottomLeft", "bottomCenter", "bottomRight"];
declare type Placement = typeof Placements[number];


interface FilterDropdownProps {
    overlayClassName?: string; // 下拉样式类名
    className?: string; // 筛选类名
    placement?: Placement; // 显示位置
    filters: any[]; // 数据源
    onConfirm?: (values: string[]) => void; // 确定
    onReset?: () => void; // 重置
    onTrigger?: () => void; // 点击按钮显示下拉时触发
    isObject?: boolean; // 数据是否是数组对象
}


/** * 自定义筛选下拉 * @param props * @constructor */
const FilterDropdown = (props: FilterDropdownProps, ref: any) => {
    const {
        overlayClassName = '',
        className = '',
        placement,
        filters,
        onConfirm = (values: any[]) => {},
        onReset = () => {},
        onTrigger = () => {},
        isObject = false,
    } = props;
    const [data, setData] = useState<any[]>([]);
    const [visible, setVisible] = useState<boolean>(false);
    const [formName, setFormName] = useState(null);

    const [form] = Form.useForm();
    const { resetFields, getFieldsValue, getFieldValue, setFieldsValue } = form;

    const containerRef = useRef<any>(null);


    useEffect(() => {
        setFormName(uuidv4());
    }, [])


    useEffect(() => {
        // 点击非弹窗位置关闭弹窗
        const clickEvent = (e: any) => {
            if (containerRef.current && !containerRef.current.contains(e.target) && visible) {
                setVisible(false);
            }
        }
        document.addEventListener('click', clickEvent, false);
        return () => {
            document.removeEventListener('click', clickEvent, false);
        }
    }, [visible])


    useEffect(() => {
        setData(filters || []);
    }, [filters])


    useImperativeHandle(ref, () => ({
        // 暴露方法给父组件
        resetFields,
    }))


    /** * 搜索关键字 * @param e */
    const changeHandle = (e: any) => {
        const { value } = e.target;
        let target = [...filters];
        if (value) {
            if (isObject) {
                target = (filters || []).filter((v: any) => v.label.includes(value));
                let pkids = (target || []).map(v => v.value);
                setFieldsValue({pkids});
            } else {
                target = (filters || []).filter((v: any) =>  v.includes(value));
                setFieldsValue({pkids: target});
            }
        } else {
            resetFields();
        }
        setData(target);
    }


    /** * 重置 */
    const resetHandle = () => {
        setData(filters);
        resetFields();
        setVisible(false);
        onReset();
    }


    /** * 确定 */
    const confirmHandle = () => {
        const { pkids } = getFieldsValue();
        setVisible(false);
        onConfirm(pkids);
    }


    /** * 显示下拉 */
    const showHandle = () => {
        setVisible(true);
        onTrigger();
    }


    const rowRenderer = (item: any) => {
        return (
            <List.Item> <Checkbox value={ObjTest.isObj(item) ? item.value : item} > {ObjTest.isObj(item) ? item.label : item} </Checkbox> </List.Item>
        )
    }


    return (
        <Form className={['custom-table-filter-dropdown-wrap', className].join(' ')} name={formName} form={form}> <Dropdown overlayClassName={['custom-table-filter-dropdown', overlayClassName].join(' ')} visible={visible} placement={placement} overlay={( <div className="dropdown-body" ref={containerRef}> <div className="pb10"> <Form.Item name="keywords" noStyle> <Input placeholder="请输入搜索关键字筛选" onChange={changeHandle} allowClear /> </Form.Item> </div> <Form.Item name="pkids" noStyle initialValue={[]}> <Checkbox.Group> <List split={false} dataSource={data} renderItem={rowRenderer} /> </Checkbox.Group> </Form.Item> <div className="mt10 flexic"> <Button type="link" size="small" onClick={resetHandle} >重置 </Button> <Button className="mla button-red" type="primary" size="small" onClick={confirmHandle} >确定 </Button> </div> </div> )} > <Form.Item noStyle shouldUpdate> {() => ( <FilterFilled className={[ObjTest.isNotEmptyArray(getFieldValue('pkids')) ? 'active' : ''].join(' ')} onClick={showHandle} /> )} </Form.Item> </Dropdown> </Form>
    )
}


export default forwardRef(FilterDropdown);

好了,上面就是该筛选组件的完整代码,其实还可以对其进行拓展,那就是有时候我们的项目中不需要输入框进行搜索并自动勾选,所以我们可以再props中定义一个字段isShowSearch用于是否需要显示搜索框,而搜索框的原有逻辑也不动,显示时就走自动勾选逻辑,不显示就手动选中即可。

使用组件

组件封装完毕,那就是要检验成果了,行与不行就在这一刻了(必须行!),go→

引入组件

import FilterDropdownNormal from './component/FilterDropdownNormal';

表头布局

<div className="cell flex">
    <p>设计号/项目名称</p>
    <FilterDropdownNormal
        className="mla"
        overlayClassName="project-practice-dropdown"
        isObject={true}
        filters={projectList}
        onTrigger={() => {
            getNameList(); // 查询筛选项
        }}
        onReset={() => {
            resetSearchForm(); // 清空全部筛选组件值
            query(); // 查询表格数据
        }}
        onConfirm={(values: string[]) => {
            form.setFieldsValue({projectCodeList: values}); // 外部接收选中集合
            query(); // 查询表格数据
        }}
    />
</div>
<div className="cell flexic">
    <p>业态</p>
    <FilterDropdownNormal
        className="mla"
        overlayClassName="project-practice-dropdown"
        ref={formatFilter}
        filters={formatList}
        onTrigger={() => {
            getNameList(); // 查询筛选项
        }}
        onReset={() => {
            form.setFieldsValue({formatNameList: []}); // 清空
            query(); // 查询表格数据
        }}
        onConfirm={(values: string[]) => {
            form.setFieldsValue({formatNameList: values}); // 外部接收选中集合
            query(); // 查询表格数据
        }}
    />
</div>

外部组件定义一个Form表单来接收选中的值:

<Form form={form} name="projectTableForm">
    <Form.Item name="projectCodeList" initialValue={[]} hidden>
        <Input type="hidden" />
    </Form.Item>
    <Form.Item name="formatNameList" initialValue={[]} hidden>
        <Input type="hidden" />
    </Form.Item>
</Form>

逻辑代码

/** * 重置所有搜索表单字段 */
const resetSearchForm = () => {
    resetRest();
    form.resetFields();
}
/** * 重置除项目名称以外的字段 */
const resetRest = () => {
    if (formatFilter.current) {
        formatFilter.current.resetFields();
    }
    
    // ...多个
    
    setPartList([]);
    
    // ...多个
}

样式文件

.custom-table-filter-dropdown-wrap {
    .anticon {
        font-size: 12px;
        color: #bfbfbf;
        &.active {
            color: #1890FF;
        }
        &:hover {
            color: #fff;
        }
    }
}
.custom-table-search-dropdown-wrap {
    .anticon {
        font-size: 16px;
        color: #bfbfbf;
        &.active {
            color: #1890FF;
        }
        &:hover {
            color: #fff;
        }
    }
}
.custom-table-filter-dropdown {
    background: #fff;
    box-shadow: 0px 9px 28px 8px rgba(0, 0, 0, 0.05), 0px 6px 16px 0px rgba(0, 0, 0, 0.08), 0px 3px 6px -4px rgba(0, 0, 0, 0.12);
    border-radius: 4px;
    .dropdown-body {
        padding: 10px;
    }
}
.custom-table-search-dropdown {
    background: #fff;
    box-shadow: 0px 9px 28px 8px rgba(0, 0, 0, 0.05), 0px 6px 16px 0px rgba(0, 0, 0, 0.08), 0px 3px 6px -4px rgba(0, 0, 0, 0.12);
    border-radius: 4px;
    .dropdown-body {
        padding: 10px;
    }
}

好了,如何封装一个筛选组件的方法和过程就聊完了,这个组件可以用于任何可以使用到的场景,并不限制于表格表头上,只是我们需求和表格打交道很多,所以用在表格上就很平常。该组件还可以对其进行拓展和优化,这就需要针对真实需求来对其做相应更改,这里只是介绍了一下思路,如果你有更好的方式可以在评论区进行交流沟通。

最后,借今天这个甜蜜的日子,祝大家520快乐!我也要去过节去了,nice!

xdm看文至此,点个赞👍再走哦,3Q^_^

往期精彩文章

后语

伙伴们,如果觉得本文对你有些许帮助,点个👍或者➕个关注再走呗^_^ 。另外如果本文章有问题或有不理解的部分,欢迎大家在评论区评论指出,我们一起讨论共勉。

今天的文章如何在React项目中封装定制的筛选组件?分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/15935.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注