react Table准备Spin Empty ConfigProvider组件实现

目录
  • 前言
  • 目录结构
  • 开搞ConfigProvider
  • Empty组件实现
  • Spin组件

前言

继续搞react组件库,该写table了,学习了arco design的table的运行流程,发现准备工作还是挺多的,我们就先解决以下问题吧!

比如你要配置国际化,组件库的所有组件都要共享当前语言的变量,比如是中文,还是英文,这样组件才能渲染对应国家的字符串。

也就是说,你自己的组件库有什么想全局共享的变量,就写在这个组件里。

table使用的地方

  const {
    getPrefixCls, // 获取css前缀
    loadingElement, // loading显示的组件
    size: ctxSize, // size默认值
    renderEmpty, // 空数据时Empty组件显示的内容
    componentConfig, // 全局component的config
  } = useContext(ConfigContext);

我简单解释一下,getPrefixCls获取了组件的css前缀,比如arco deisgn 的前缀自然是arco了,他们的组件的所有css都会加上这个前缀,现在组件库都这么玩。

其他的就不详细描述了,比如table请求数据有loading,你想自定义loading样式可以在loadingElement属性上配置等等,也就是说全局你自定义的loading组件,所有组件都会共享,不用你一个一个去配置了。

而这里的 useContext(ConfigContext) ConfigContext就是ConfigProvider组件创建的context,类似这样(细节不用纠结,后面我们会实现这个组件):

export const ConfigContext = createContext<ConfigProviderProps>({
  getPrefixCls: (componentName: string, customPrefix?: string) => `${customPrefix || defaultProps.prefixCls}-${componentName}`,
  ...defaultProps,
});
 <ConfigContext.Provider value={config}>{children}</ConfigContext.Provider>;

Spin组件就是显示loading态的组件,这里改造了arco的Spin组件,主要添加了样式层,我认可将样式层和js控制的html,也就是jsx分层

主要体现在,组件里新增getClassnames和getStyles两个函数,配合css,收敛所有组件的样式。

在复杂组件里,我还会尝试收敛数据层和渲染层,但是spin组件和后面的empty组件太简单了,就没有做这步

在table中这样使用

<Spin element={loadingElement} {...loading}>
        {renderTable()}
</Spin>

Empty组件

table组件没有数据的时候就会显示它

这篇基本全是代码,大家简单看看就好,重点是下一篇将table组件,这里主要是做个记录

目录结构

├── ConfigProvider
│   ├── config // 配置文件
│   │   ├── constants.tsx // 常量
│   │   └── utils_fns // 工具函数文件夹
│   ├── index.tsx
│   └── interface.ts // ts定义文件
├── Empty
│   ├── config  // 配置文件
│   │   ├── constants.ts
│   │   └── utils_fns // 工具函数文件夹
│   │       ├── getDesDefault.ts
│   │       ├── xxx
│   │       └── index.ts
│   ├── index.tsx
│   ├── interface.ts  // ts定义文件
│   └── style // 样式文件
│       ├── index.less
│       └── index.ts
├── Icon // Icon是单独一个项目,自动化生成Icon,还有点复杂度的,这个后面组件库详细讲吧
│   ├── index.tsx
│   └── style
│       └── index.less
├── Spin
│   ├── config
│   │   ├── hooks // 自定义hook
│   │   └── utils_fns
│   ├── index.tsx
│   ├── interface.ts
│   └── style
│       ├── index.less
│       └── index.ts
├── Table
│   ├── config
│   │   └── util_fns
│   └── table.tsx
├── config // 公共配置文件
│   ├── index.ts
│   └── util_fns
│       ├── index.ts
│       └── pickDataAttributes.ts
├── index.ts
├── locale // 国际化文件夹
│   ├── default.tsx
│   ├── en-US.tsx
│   ├── interface.tsx
│   └── zh-CN.tsx
└── style // 样式文件夹
    ├── base.less
    ├── common.less
    ├── index.less
    ├── normalize.less
    └── theme

开搞ConfigProvider

index.tsx,详情见注释

import React, { createContext, useCallback, useMemo } from 'react';
// omit相当于lodash里的omit,不过自己写的性能更好,因为没有那么多兼容性,很简单
// useMergeProps是合并外界传入的props,和默认props还有组件全局props的hook
import { omit, useMergeProps } from '@mx-design/utils';
// 国际化文件,默认是中文
import defaultLocale from '../locale/default';
// 接口
import type { ConfigProviderProps } from './interface';
// componentConfig是空对象
// PREFIX_CLS是你想自定义的css样式前缀
import { componentConfig, PREFIX_CLS } from './config/constants';
// 渲染空数据的组件
import { renderEmpty } from './config/utils_fns';
// 默认参数
const defaultProps: ConfigProviderProps = {
  locale: defaultLocale,
  prefixCls: PREFIX_CLS,
  getPopupContainer: () => document.body,
  size: 'default',
  renderEmpty,
};
// 默认参数
export const ConfigContext = createContext<ConfigProviderProps>({
  ...defaultProps,
});
function ConfigProvider(baseProps: ConfigProviderProps) {
  // 合并props,baseProps也就是用户传入的props优先级最高
  const props = useMergeProps<ConfigProviderProps>(baseProps, defaultProps, componentConfig);
  const { prefixCls, children } = props;
// 获取css前缀名的函数
  const getPrefixCls = useCallback(
    (componentName: string, customPrefix?: string) => {
      return `${customPrefix || prefixCls || defaultProps.prefixCls}-${componentName}`;
    },
    [prefixCls]
  );
 // 传递给所有子组件的数据
  const config: ConfigProviderProps = useMemo(
    () => ({
      ...omit(props, ['children']),
      getPrefixCls,
    }),
    [getPrefixCls, props]
  );
// 使用context实现全局变量传递给子组件的目的
  return <ConfigContext.Provider value={config}>{children}</ConfigContext.Provider>;
}
ConfigProvider.displayName = 'ConfigProvider';
export default ConfigProvider;
export type { ConfigProviderProps };

注意在default中,有个renderEmpty函数,实现如下:

export function renderEmpty() {
  return <Empty />;
}

所以,我们接着看Empty组件如何实现

这里顺便贴一下ConfigProvider中的类型定义,因为初期组件比较少,参数不多,大多数从arco deisgn源码copy的

import { ReactNode } from 'react';
import { Locale } from '../locale/interface';
import type { EmptyProps } from '../Empty/interface';
import type { SpinProps } from '../Spin/interface';
export type ComponentConfig = {
  Empty: EmptyProps;
  Spin: SpinProps;
};
/**
 * @title ConfigProvider
 */
export interface ConfigProviderProps {
  /**
   * @zh 用于全局配置所有组件的默认参数
   * @en Default parameters for global configuration of all components
   * @version 2.23.0
   */
  componentConfig?: ComponentConfig;
  /**
   * @zh 设置语言包
   * @en Language package setting
   */
  locale?: Locale;
  /**
   * @zh 配置组件的默认尺寸,只会对支持`size`属性的组件生效。
   * @en Configure the default size of the component, which will only take effect for components that support the `size` property.
   * @defaultValue default
   */
  size?: 'mini' | 'small' | 'default' | 'large';
  /**
   * @zh 全局组件类名前缀
   * @en Global ClassName prefix
   * @defaultValue arco
   */
  prefixCls?: string;
  getPrefixCls?: (componentName: string, customPrefix?: string) => string;
  /**
   * @zh 全局弹出框挂载的父级节点。
   * @en The parent node of the global popup.
   * @defaultValue () => document.body
   */
  getPopupContainer?: (node: HTMLElement) => Element;
  /**
   * @zh 全局的加载中图标,作用于所有组件。
   * @en Global loading icon.
   */
  loadingElement?: ReactNode;
  /**
   * @zh 全局配置组件内的空组件。
   * @en Empty component in component.
   * @version 2.10.0
   */
  renderEmpty?: (componentName?: string) => ReactNode;
  zIndex?: number;
  children?: ReactNode;
}

Empty组件实现

index.tsx

import React, { memo, useContext, forwardRef } from 'react';
import { useMergeProps } from '@mx-design/utils';
import { ConfigContext } from '../ConfigProvider';
import type { EmptyProps } from './interface';
import { emptyImage, getDesDefault } from './config/utils_fns';
import { useClassNames } from './config/hooks';
function Empty(baseProps: EmptyProps, ref) {
  // 获取全局参数
  const { getPrefixCls, locale: globalLocale, componentConfig } = useContext(ConfigContext);
  // 合并props
  const props = useMergeProps<EmptyProps>({}, componentConfig?.Empty, baseProps);
  const { style, className, description, icon, imgSrc } = props;
  // 获取国际化的 noData字符串
  const { noData } = globalLocale.Empty;
  // class样式层
  const { containerCls, wrapperCls, imageCls, descriptionCls } = useClassNames({ getPrefixCls, className });
  // 获取描述信息
  const alt = getDesDefault(description);
  return (
    <div ref={ref} className={containerCls} style={style}>
      <div className={wrapperCls}>
        <div className={imageCls}>{emptyImage({ imgSrc, alt, icon })}</div>
        <div className={descriptionCls}>{description || noData}</div>
      </div>
    </div>
  );
}
const EmptyComponent = forwardRef(Empty);
EmptyComponent.displayName = 'Empty';
export default memo(EmptyComponent);
export type { EmptyProps };

useClassNames,主要是通过useMemo缓存所有的className,一般情况下,这些className都不会变

import { cs } from '@mx-design/utils';
import { useMemo } from 'react';
import { ConfigProviderProps } from '../../../ConfigProvider';
import { EmptyProps } from '../..';
interface getClassNamesProps {
  getPrefixCls: ConfigProviderProps['getPrefixCls'];
  className: EmptyProps['className'];
}
export function useClassNames(props: getClassNamesProps) {
  const { getPrefixCls, className } = props;
  const prefixCls = getPrefixCls('empty');
  const classNames = cs(prefixCls, className);
  return useMemo(
    () => ({
      containerCls: classNames,
      wrapperCls: `${prefixCls}-wrapper`,
      imageCls: `${prefixCls}-image`,
      descriptionCls: `${prefixCls}-description`,
    }),
    [classNames, prefixCls]
  );
}

getDesDefault,

import { DEFAULT_DES } from '../constants';
export function getDesDefault(description) {
  return typeof description === 'string' ? description : DEFAULT_DES;
}

getEmptyImage

import { IconEmpty } from '@mx-design/icon';
import React from 'react';
import { IEmptyImage } from '../../interface';
export const emptyImage: IEmptyImage = ({ imgSrc, alt, icon }) => {
  return imgSrc ? <img alt={alt} src={imgSrc} /> : icon || <IconEmpty />;
};

Spin组件

也很简单,值得一提的是,你知道写一个debounce函数怎么写吗,很多网上的人写的简陋不堪,起码还是有个cancel方法,好吧,要不你useEffect想在组件卸载的时候,清理debounce的定时器都没办法。

debounce实现

interface IDebounced<T extends (...args: any) => any> {
  cancel: () => void;
  (...args: any[]): ReturnType<T>;
}
export function debounce<T extends (...args: any) => any>(func: T, wait: number, immediate?: boolean): IDebounced<T> {
  let timeout: number | null;
  let result: any;
  const debounced: IDebounced<T> = function (...args) {
    const context = this;
    if (timeout) clearTimeout(timeout);
    if (immediate) {
      let callNow = !timeout;
      timeout = window.setTimeout(function () {
        timeout = null;
      }, wait);
      if (callNow) result = func.apply(context, args);
    } else {
      timeout = window.setTimeout(function () {
        result = func.apply(context, args);
      }, wait);
    }
    // Only the first time you can get the result, that is, immediate is true
    // if not,result has little meaning
    return result;
  };
  debounced.cancel = function () {
    clearTimeout(timeout!);
    timeout = null;
  };
  return debounced;
}

顺便我们在写一个useDebounce的hook吧,项目中也要用

import { debounce } from '@mx-design/utils';
import { useCallback, useEffect, useState } from 'react';
import type { SpinProps } from '../../interface';
interface debounceLoadingProps {
  delay: SpinProps['delay'];
  propLoading: SpinProps['loading'];
}
export const useDebounceLoading = function (props: debounceLoadingProps): [boolean] {
  const { delay, propLoading } = props;
  const [loading, setLoading] = useState<boolean>(delay ? false : propLoading);
  const debouncedSetLoading = useCallback(debounce(setLoading, delay), [delay]);
  const getLoading = delay ? loading : propLoading;
  useEffect(() => {
    delay && debouncedSetLoading(propLoading);
    return () => {
      debouncedSetLoading?.cancel();
    };
  }, [debouncedSetLoading, delay, propLoading]);
  return [getLoading];
};

index.tsx

import React, { useContext } from 'react';
import { useMergeProps } from '@mx-design/utils';
import { ConfigContext } from '../ConfigProvider';
import type { SpinProps } from './interface';
import InnerLoading from './InnerLoading';
import { useClassNames, useDebounceLoading } from './config/hooks';
function Spin(baseProps: SpinProps, ref) {
  const { getPrefixCls, componentConfig } = useContext(ConfigContext);
  const props = useMergeProps<SpinProps>(baseProps, {}, componentConfig?.Spin);
  const { style, className, children, loading: propLoading, size, icon, element, tip, delay, block = true } = props;
  const [loading] = useDebounceLoading({ delay, propLoading });
  const { prefixCls, wrapperCls, childrenWrapperCls, loadingLayerCls, loadingLayerInnerCls, tipCls } = useClassNames({
    getPrefixCls,
    block,
    loading,
    tip,
    children,
    className,
  });
  return (
    <div ref={ref} className={wrapperCls} style={style}>
      {children ? (
        <>
          <div className={childrenWrapperCls}>{children}</div>
          {loading && (
            <div className={loadingLayerCls} style={{ fontSize: size }}>
              <span className={loadingLayerInnerCls}>
                <InnerLoading prefixCls={prefixCls} icon={icon} size={size} element={element} tipCls={tipCls} tip={tip} />
              </span>
            </div>
          )}
        </>
      ) : (
        <InnerLoading prefixCls={prefixCls} icon={icon} size={size} element={element} tipCls={tipCls} tip={tip} />
      )}
    </div>
  );
}
const SpinComponent = React.forwardRef<unknown, SpinProps>(Spin);
SpinComponent.displayName = 'Spin';
export default SpinComponent;
export { SpinProps };

LoadingIcon.tsx

import { IconLoading } from '@mx-design/icon';
import { cs } from '@mx-design/utils';
import React, { FC, ReactElement } from 'react';
import { ConfigProviderProps } from '../../../ConfigProvider';
import type { SpinProps } from '../../interface';
interface loadingIconProps {
  prefixCls: ConfigProviderProps['prefixCls'];
  icon: SpinProps['icon'];
  size: SpinProps['size'];
  element: SpinProps['element'];
}
export const LoadingIcon: FC<loadingIconProps> = function (props) {
  const { prefixCls, icon, size, element } = props;
  return (
    <span className={`${prefixCls}-icon`}>
      {icon
        ? // 这里可以让传入的icon自动旋转
          React.cloneElement(icon as ReactElement, {
            className: `${prefixCls}-icon-loading`,
            style: {
              fontSize: size,
            },
          })
        : element || <IconLoading className={`${prefixCls}-icon-loading`} style={{ fontSize: size }} />}
    </span>
  );
};

以上就是react Table准备Spin Empty ConfigProvider组件实现的详细内容,更多关于react Table组件的资料请关注我们其它相关文章!

(0)

相关推荐

  • React优雅的封装SvgIcon组件示例

    目录 React如何优雅的封装SvgIcon组件 第一步:安装svg-sprite-loader 第二步:配置webpack 第三步:创建icons/svg文件夹,并且加载所有svg文件 第四步:创建 SvgIcon 组件 第五步:在组件中使用 SvgIcon 注意可能会遇到的bug 总结 React如何优雅的封装SvgIcon组件 相信使用过vue的伙伴们,或多或少都接触或使用过vue-element-admin,其中许多封装都不禁让人拍案叫绝,因为本人之前是vue入门前端的,所以对vue-e

  • React 实现具备吸顶和吸底功能组件实例

    目录 背景 实现 结语 背景 现在手机应用经常有这样一个场景: 页面上有一个导航,导航位置在页面中间位置,当页面顶部滚动到导航位置时,导航自动吸顶,页面继续往下滚动时,它就一直在页面视窗顶部显示,当往上滚动时,经过最初位置时,导航自动复原,不再吸顶. 效果就如京东超市首页的导航栏一样: 下面我们就来具体实现这样一个 React 组件,实现后还会再扩展延伸一下 吸底 功能,因为 吸底 场景也不少. 具体要求: 需要可以设置是 吸顶 还是 吸底. 吸顶 可以设置距离视窗顶部的位置,吸顶 可以设置距离

  • React+高德地图实时获取经纬度,定位地址

    目录 1. 初始化地图 2. 地图扎点 3. 开启定位 4. 监听地图变化 5. 获取详细地址 6. 扎点动画

  • React实现一个倒计时hook组件实战示例

    目录 前言 思路 实现 总结 前言 本篇文章主要实现一个无样式的倒计时 hook 组件,通常不同地方的倒计时样式都不同,但倒计时的逻辑基本是都是一样的,因此可以抽离成一个工具方法或者一个 hook 组件,这样让倒计时逻辑可以进行通用,样式让业务方具体去实现. 思路 倒计时可能需要显示剩余时间的单位有:天.时.分.秒.毫秒,可能只需显示一个,也可能都需要显示. 注意细节: 只显示某一单位的时间或者需要显示的最大单元时间,需要可以大于正常时间最大限制,比如要剩余 100 小时 58 分时,小时需要可

  • React如何利用Antd的Form组件实现表单功能详解

    一.构造组件 1.表单一定会包含表单域,表单域可以是输入控件,标准表单域,标签,下拉菜单,文本域等. 这里先引用了封装的表单域 <Form.Item /> 2.使用Form.create处理后的表单具有自动收集数据并校验的功能,但如果不需要这个功能,或者默认的行为无法满足业务需求,可以选择不使用Form.create并自行处理数据 经过Form.create()包装过的组件会自带this.props.form属性,this.props.form提供了很多API来处理数据,如getFieldDe

  • 手把手教您实现react异步加载高阶组件

    本篇文章通过分析react-loadable包的源码,手把手教你实现一个react的异步加载高阶组件 1. 首先我们想象中的react异步加载组件应该如何入参以及暴露哪些API? // 组件应用 import * as React from 'react'; import ReactDOM from 'react-dom'; import Loadable from '@component/test/Loadable'; import Loading from '@component/test/

  • React antd tabs切换造成子组件重复刷新

    描述: Tabs组件在来回切换的过程中,造成TabPane中包含的相同子组件重复渲染,例如: <Tabs activeKey={tabActiveKey} onChange={(key: string) => this.changeTab(key)} type="card" > <TabPane tab={"对外授权"} key="/authed-by-me"> <AuthedCollections colle

  • react使用antd的上传组件实现文件表单一起提交功能(完整代码)

    最近在刚刚开始使用react做项目,非常不熟练,非常小白.小白同学可以阅读了,因为我会写的非常简单,直白. 项目中需要实现表单中带附件提交,上传文件不单独保存调接口. import { Form, Button, Upload } from 'antd'; import { UploadOutlined } from '@ant-design/icons'; const normFile = (e) => { console.log('Upload event:', e); if (Array.

  • React实现一个通用骨架屏组件示例

    目录 骨架屏是什么? Demo 设计思路 具体实现 骨架屏是什么? 找到这里的同志,或多或少都对骨架屏有所了解,请容许我先啰嗦一句.骨架屏(Skeleton Screen)是一种优化用户弱网体验的方案,可以有效缓解用户等待的焦躁情绪. Demo 先看个demo 大概了解下最终的产物及其使用方式 npm install obiusm-react-components import { Skeleton } from 'obiusm-react-components'; <Skeleton isVi

  • React经典面试题之倒计时组件详解

    目录 闲聊 倒计时组件——需求描述: 问题 最后 闲聊 关于面试大家常常吐槽:“面试造火箭,工作拧螺丝.”,从而表达了对工作内容和能力要求匹配不一的现状. 不排除有些公司想要探查候选人的技术上限或者说综合技术能力,希望得到一个可拓展性更高的人才.也有些公司是不知道如何筛选候选人,所以随便找了一些网上的面试题,各种原理,细枝末节的东西.不是说这些东西不好,但我觉得首要考察候选人是否能够胜任该岗位,同时他如果能懂原理,还有细节,那自然是锦上添花. 闲话聊完了,关于React我觉得能考察实际能力一道题

  • React Native 中限制导入某些组件和模块的方法

    目录 限制使用 Touchable 限制使用 Image 同时限制两者 示例 有些时候,我们不希望使用某些组件,而是想使用其他组件.这时候,我们可以使用一个名为 no-restricted-imports 的eslint 规则,该规则允许你指定一个或多个组件的名称,以防止使用这些组件. no-restricted-imports 是 eslint 自带的一个规则,我们不需要额外引入一个插件. 限制使用 Touchable 假如我们希望小伙伴们不要使用 Touchable 系列组件,而是使用 Pr

  • react装饰器与高阶组件及简单样式修改的操作详解

    使用装饰器调用 装饰器 用来装饰类的,可以增强类,在不修改类的内部的源码的同时,增强它的能力(属性或方法) 装饰器使用@函数名写法,对类进行装饰,目前在js中还是提案,使用需要配置相关兼容代码库. react脚手架创建的项目默认是不支持装饰器,需要手动安装相关模块和添加配置文件 安装相关模块 yarn add -D customize-cra react-app-rewired  @babel/plugin-proposal-decorators 修改package.json文件中scripts

  • React可定制黑暗模式切换开关组件

    目录 正文 如何使用它. 1.安装和下载 2.导入DarkModeToggle组件 3.将黑暗模式切换添加到应用程序中 4.默认的组件道具 预览 正文 一个用于React的可定制的黑暗模式切换开关组件. 如何使用它. 1.安装和下载 npm install @anatoliygatt/dark-mode-toggle @emotion/react @emotion/styled 2.导入DarkModeToggle组件 import { useState } from 'react'; impo

  • React实现锚点跳转组件附带吸顶效果的示例代码

    React实现锚点跳转组件附带吸顶效果 import React, { useRef, useState, useEffect } from 'react'; import styles from './index.less'; import classnames from 'classnames'; function AnchorTabber(props) { const root = useRef(null); const header = useRef(null); const { att

随机推荐