前端反卷计划-组件库-04-Button组件开发

简介: 前端反卷计划-组件库-04-Button组件开发

Hi, 大家好!我是程序员库里。

今天开始分享如何从0搭建UI组件库。这也是前端反卷计划中的一项。

在接下来的日子,我会持续分享前端反卷计划中的每个知识点。

以下是前端反卷计划的内容:

image.png

image.png

目前这些内容持续更新到了我的 学习文档 中。感兴趣的欢迎一起学习!

Button

4.1 需求分析

以antd design的Button组件为例

按钮类型

image.png

按钮尺寸

image.png

不可用状态

image.png

4.2 Demo

<Button size='large' type='primary' disabled>
按钮
</Button>

4.3 API

属性 说明 类型 默认值
type 按钮类型 primary default danger link default
size 按钮尺寸 lg sm -
disabled 设置按钮失效状态 boolean FALSE
href 点击跳转的地址,指定此属性 button 的行为和 a 链接一致 string -

4.4 开发

  1. 创建Button组件目录如下:

--components
    --Button
        --_style.scss            // 样式文件
        --button.stories.tsx     // demo
        --button.test.tsx        // 单元测试
        --button.tsx            // 核心代码逻辑
        --index.tsx            //  导出组件
  1. 定义按钮尺寸大小枚举值

export type ButtonSize = 'lg' | 'sm'
  1. 定义按钮类型枚举值

export type ButtonType = 'primary' | 'default' | 'danger' | 'link'
  1. 定义按钮的props

import React, { FC, ButtonHTMLAttributes, AnchorHTMLAttributes } from 'react'

interface BaseButtonProps {
  className?: string;
  /**设置 Button 的禁用 */
  disabled?: boolean;
  /**设置 Button 的尺寸 */
  size?: ButtonSize;
  /**设置 Button 的类型 */
  btnType?: ButtonType;
  children: React.ReactNode;
  href?: string;
}
// ButtonHTMLAttributes 是 React 中的一个内置泛型类型,它用于表示 HTML 按钮元素 (<button>) 上可以接受的属性。这些属性包括按钮的标准 HTML 属性,如 onClick、disabled、type 等
type NativeButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLElement>
// AnchorHTMLAttributes 是 React 中的一个内置泛型类型,它用于表示 HTML 锚点元素 (<a>) 上可以接受的属性。这些属性包括锚点元素的标准 HTML 属性,例如 href、target、onClick 等
type AnchorButtonProps = BaseButtonProps & AnchorHTMLAttributes<HTMLElement>
// Partial可选
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>
  1. 根据传入的props决定按钮尺寸、按钮类型等逻辑

const {
    btnType,
    className,
    disabled,
    size,
    children,
    href,
    ...restProps
  } = props
  // btn, btn-lg, btn-primary
  const classes = classNames('btn', className, {
    [`btn-${btnType}`]: btnType,
    [`btn-${size}`]: size,
    'disabled': (btnType === 'link') && disabled
  })
  1. 判断是显示a标签还是button按钮

if (btnType === 'link' && href) {
    return (
      <a
        className={classes}
        href={href}
        {...restProps}
      >
        {children}
      </a>
    )
  } else {
    return (
      <button
        className={classes}
        disabled={disabled}
        {...restProps}
      >
        {children}
      </button>
    )
  }
  1. 定义按钮样式变量

src/styles/_variable.scss

// 按钮
// 按钮基本属性
$btn-font-weight:             400;
$btn-padding-y:               .375rem !default;
$btn-padding-x:               .75rem !default;
$btn-font-family:             $font-family-base !default;
$btn-font-size:               $font-size-base !default;
$btn-line-height:             $line-height-base !default;

//不同大小按钮的 padding 和 font size
$btn-padding-y-sm:            .25rem !default;
$btn-padding-x-sm:            .5rem !default;
$btn-font-size-sm:            $font-size-sm !default;

$btn-padding-y-lg:            .5rem !default;
$btn-padding-x-lg:            1rem !default;
$btn-font-size-lg:            $font-size-lg !default;

// 按钮边框
$btn-border-width:            $border-width !default;

// 按钮其他
$btn-box-shadow:              inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default;
$btn-disabled-opacity:        .65 !default;

// 链接按钮
$btn-link-color:              $link-color !default;
$btn-link-hover-color:        $link-hover-color !default;
$btn-link-disabled-color:     $gray-600 !default;


// 按钮 radius
$btn-border-radius:           $border-radius !default;
$btn-border-radius-lg:        $border-radius-lg !default;
$btn-border-radius-sm:        $border-radius-sm !default;

$btn-transition:              color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;
  1. 编写按钮基本样式

.btn {
  position: relative;
  display: inline-block;
  font-weight: $btn-font-weight;
  line-height: $btn-line-height;
  color: $body-color;
  white-space: nowrap;
  text-align: center;
  vertical-align: middle;
  background-image: none;
  border: $btn-border-width solid transparent;
  @include button-size( $btn-padding-y,  $btn-padding-x,  $btn-font-size,  $border-radius);
  box-shadow: $btn-box-shadow;
  cursor: pointer;
  transition: $btn-transition;
  &.disabled,
  &[disabled] {
    cursor: not-allowed;
    opacity: $btn-disabled-opacity;
    box-shadow: none;
    > * {
      pointer-events: none;
    }
  }
}

在这里为了不重复复制样式代码,我们采用mixin来处理。

比如上面代码中的@include button-size 函数,这个是scss的一个特性,可以从官网上看下介绍。

@include button-size( $btn-padding-y,  $btn-padding-x,  $btn-font-size,  $border-radius);

要使用上面的方法,需要在mixin编写上面的函数

新建 src/styles/_mixin.scss,编写如下代码:

这里解释一下:相当于在button-size中传了4个参数,使用这4个参数来定义样式属性,使用的时候即可传入对应的样式变量即可。

@mixin button-size($padding-y, $padding-x, $font-size, $border-raduis) {
  padding: $padding-y $padding-x;
  font-size: $font-size;
  border-radius: $border-raduis;
}
  1. 编写按钮尺寸大小的代码,这里同样适用mixin,使用button-size函数进行复用。

.btn-lg {
  @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg);
}
.btn-sm {
  @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm);
}
  1. 编写按钮类型的样式代码,这里同样适用mixin,使用了button-style,这就需要在_mixin.scss中进行定义

.btn-primary {
  @include button-style($primary, $primary, $white)
}
.btn-danger {
  @include button-style($danger, $danger, $white)
}

.btn-default {
  @include button-style($white, $gray-400, $body-color, $white, $primary, $primary)
}
  1. 在_mixin.scss中定义 button-style函数

@mixin button-style(
  $background,
  $border,
  $color,
  $hover-background: lighten($background, 7.5%),
  $hover-border: lighten($border, 10%),
  $hover-color: $color,
) {
  color: $color;
  background: $background;
  border-color: $border;
  &:hover {
    color: $hover-color;
    background: $hover-background;
    border-color: $hover-border;    
  }
  &:focus,
  &.focus {
    color: $hover-color;
    background: $hover-background;
    border-color: $hover-border;    
  }
  &:disabled,
  &.disabled {
    color: $color;
    background: $background;
    border-color: $border;    
  }
}
  1. 编写 按钮是 link 标签时候的样式代码

.btn-link {
  font-weight: $font-weight-normal;
  color: $btn-link-color;
  text-decoration: $link-decoration;
  box-shadow: none;
  &:hover {
    color: $btn-link-hover-color;
    text-decoration: $link-hover-decoration; 
  }
  &:focus,
  &.focus {
    text-decoration: $link-hover-decoration;
    box-shadow: none;
  }
  &:disabled,
  &.disabled {
    color: $btn-link-disabled-color;
    pointer-events: none;
  }
}
  1. 完整代码

import React, { FC, ButtonHTMLAttributes, AnchorHTMLAttributes } from 'react'
import classNames from 'classnames'

export type ButtonSize = 'lg' | 'sm'
export type ButtonType = 'primary' | 'default' | 'danger' | 'link'

interface BaseButtonProps {
  className?: string;
  /**设置 Button 的禁用 */
  disabled?: boolean;
  /**设置 Button 的尺寸 */
  size?: ButtonSize;
  /**设置 Button 的类型 */
  btnType?: ButtonType;
  children: React.ReactNode;
  href?: string;
}
type NativeButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLElement>
type AnchorButtonProps = BaseButtonProps & AnchorHTMLAttributes<HTMLElement>
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>
/**
 * 页面中最常用的的按钮元素,适合于完成特定的交互
 * ### 引用方法
 * 
 * ~~~js
 * import { Button } from 'curry-design'
 * ~~~
 */
export const Button: FC<ButtonProps> = (props) => {
  const {
    btnType,
    className,
    disabled,
    size,
    children,
    href,
    ...restProps
  } = props
  // btn, btn-lg, btn-primary
  const classes = classNames('btn', className, {
    [`btn-${btnType}`]: btnType,
    [`btn-${size}`]: size,
    'disabled': (btnType === 'link') && disabled
  })
  if (btnType === 'link' && href) {
    return (
      <a
        className={classes}
        href={href}
        {...restProps}
      >
        {children}
      </a>
    )
  } else {
    return (
      <button
        className={classes}
        disabled={disabled}
        {...restProps}
      >
        {children}
      </button>
    )
  }
}

Button.defaultProps = {
  disabled: false,
  btnType: 'default'
}

export default Button;

4.1.14 效果展示

http://localhost:6006/?path=/docs/example-button--docs

4.5 单元测试

  1. 测试工具

https://testing-library.com/docs/react-testing-library/intro/

pnpm install --save-dev @testing-library/react
  1. jest-dom

https://testing-library.com/docs/ecosystem-jest-dom/

增加dom操作的类型断言

npm install @testing-library/jest-dom --save-dev

create-react-app已经帮我们导入了src/setupTests.ts

// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

安装jest的ts类型,create-react-app默认自带了,就不用安装了。之前版本没有,就需要安装。

npm install --save-dev @types/jest

4.5.1 测试1:展示正确的默认按钮

import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Button from './button'

const defaultProps = {
    onClick: jest.fn()
}

describe('test Button component', () => {
    // 渲染正确的默认的按钮
    it('should render the correct default button', () => {
      // 通过 render 来渲染一个按钮,赋值给wrapper
      const wrapper = render(<Button {...defaultProps}>按钮</Button>)
      // 使用wrapper的getByText获取指定文字的按钮,赋值给element
      const element = wrapper.getByText('按钮') as HTMLButtonElement
      // 使用expect函数,表示断言。将element传入expect,调用toBeInTheDocument表示按钮插入到了页面中
      expect(element).toBeInTheDocument()
      // 获取按钮的tagName,使用toEqual函数看按钮的tagName是否叫BUTTON
      expect(element.tagName).toEqual('BUTTON')
      // 使用toHaveClass函数来判断按钮是否有btn btn-default这两个class
      expect(element).toHaveClass('btn btn-default')
      // 传入按钮的disabled,使用toBeFalsy来判断按钮是否带有disabled属性,toBeFalsy表示false
      expect(element.disabled).toBeFalsy()
      // 使用fireEvent执行点击事件
      fireEvent.click(element)
      // 执行上述点击事件后,使用toHaveBeenCalled来判断按钮是否被点击了,toHaveBeenCalled表示按钮被点击了。
      expect(defaultProps.onClick).toHaveBeenCalled()
    })
})

在终端输入:npm run test 执行下测试用例,看是否通过。可以看到测试用例通过了。
image.png

4.5.2 测试2:根据传入的props渲染对应的按钮

const testProps: ButtonProps = {
    btnType: ButtonType.Primary,
    size: ButtonSize.Large,
    className: 'klass'
}

it('should render the correct component based on different props', () => {
        const wrapper = render(<Button {...testProps}>按钮</Button>)
        const element = wrapper.getByText('按钮')
        expect(element).toBeInTheDocument()
        expect(element).toHaveClass('btn-primary btn-lg klass')
    })

测试结果:

image.png

4.5.3 测试3:测试按钮类型是a标签

 it('should render a link when btnType equals link and href is provided', () => {
        const wrapper = render(<Button btnType='link' href="http://www.baidu.com">Link</Button>)
        const element = wrapper.getByText('Link')
        expect(element).toBeInTheDocument()
        expect(element.tagName).toEqual('A')
        expect(element).toHaveClass('btn btn-link')
    })

测试结果:
image.png

4.5.4 测试4:测试按钮的disabled属性

const disabledProps: ButtonProps = {
    disabled: true,
    onClick: jest.fn(),
}
it('should render disabled button when disabled set to true', () => {
        const wrapper = render(<Button {...disabledProps}>按钮</Button>)
        const element = wrapper.getByText('按钮') as HTMLButtonElement
        expect(element).toBeInTheDocument()
        expect(element.disabled).toBeTruthy()
        fireEvent.click(element)
        expect(disabledProps.onClick).not.toHaveBeenCalled()
    })

测试结果:

image.png

以上4个测试用例全部通过。

4.5.6 完整测试用例代码

import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Button, { ButtonProps, ButtonSize, ButtonType } from './button'

const defaultProps = {
    onClick: jest.fn()
}

const testProps: ButtonProps = {
    btnType: ButtonType.Primary,
    size: ButtonSize.Large,
    className: 'klass'
}
const disabledProps: ButtonProps = {
    disabled: true,
    onClick: jest.fn(),
}

describe('test Button component', () => {
    it('should render the correct default button', () => {
        const wrapper = render(<Button {...defaultProps}>按钮</Button>)
        const element = wrapper.getByText('按钮') as HTMLButtonElement
        expect(element).toBeInTheDocument()
        expect(element.tagName).toEqual('BUTTON')
        expect(element).toHaveClass('btn btn-default')
        expect(element.disabled).toBeFalsy()
        fireEvent.click(element)
        expect(defaultProps.onClick).toHaveBeenCalled()
    })

    it('should render the correct component based on different props', () => {
        const wrapper = render(<Button {...testProps}>按钮</Button>)
        const element = wrapper.getByText('按钮')
        expect(element).toBeInTheDocument()
        expect(element).toHaveClass('btn-primary btn-lg klass')
    })
    it('should render a link when btnType equals link and href is provided', () => {
        const wrapper = render(<Button btnType='link' href="http://www.baidu.com">Link</Button>)
        const element = wrapper.getByText('Link')
        expect(element).toBeInTheDocument()
        expect(element.tagName).toEqual('A')
        expect(element).toHaveClass('btn btn-link')
    })
    it('should render disabled button when disabled set to true', () => {
        const wrapper = render(<Button {...disabledProps}>按钮</Button>)
        const element = wrapper.getByText('按钮') as HTMLButtonElement
        expect(element).toBeInTheDocument()
        expect(element.disabled).toBeTruthy()
        fireEvent.click(element)
        expect(disabledProps.onClick).not.toHaveBeenCalled()
    })
})

系列篇

前端反卷计划-组件库-01-环境搭建

前端反卷计划-组件库-02-storybook

前端反卷计划-组件库-03-组件样式

持续更新

目前这些内容持续更新到了我的 学习文档 中。感兴趣的欢迎一起学习!

相关文章
|
1月前
|
前端开发 JavaScript
除了 jsPDF,还有哪些前端库可以用于生成 PDF?
【10月更文挑战第21天】这些前端库都有各自的特点和优势,你可以根据具体的项目需求、技术栈以及对功能的要求来选择合适的库。不同的库在使用方法、性能表现以及功能支持上可能会有所差异,需要根据实际情况进行评估和选择。
|
29天前
|
前端开发 JavaScript 开发者
揭秘前端高手的秘密武器:深度解析递归组件与动态组件的奥妙,让你代码效率翻倍!
【10月更文挑战第23天】在Web开发中,组件化已成为主流。本文深入探讨了递归组件与动态组件的概念、应用及实现方式。递归组件通过在组件内部调用自身,适用于处理层级结构数据,如菜单和树形控件。动态组件则根据数据变化动态切换组件显示,适用于不同业务逻辑下的组件展示。通过示例,展示了这两种组件的实现方法及其在实际开发中的应用价值。
34 1
|
2月前
|
缓存 前端开发 JavaScript
前端serverless探索之组件单独部署时,利用rxjs实现业务状态与vue-react-angular等框架的响应式状态映射
本文深入探讨了如何将RxJS与Vue、React、Angular三大前端框架进行集成,通过抽象出辅助方法`useRx`和`pushPipe`,实现跨框架的状态管理。具体介绍了各框架的响应式机制,展示了如何将RxJS的Observable对象转化为框架的响应式数据,并通过示例代码演示了使用方法。此外,还讨论了全局状态源与WebComponent的部署优化,以及一些实践中的改进点。这些方法不仅简化了异步编程,还提升了代码的可读性和可维护性。
|
2月前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
2月前
|
前端开发 JavaScript
CSS样式穿透技巧:利用scoped与deep实现前端组件样式隔离与穿透
CSS样式穿透技巧:利用scoped与deep实现前端组件样式隔离与穿透
187 1
|
2月前
|
存储 前端开发 JavaScript
🚀 10 个 GitHub 存储库,助你成为前端巨匠✨
本文介绍了10个极具价值的GitHub存储库,旨在帮助各级JavaScript开发人员提升技能。这些资源涵盖了从基本概念到高级算法、编码风格指南、面试准备等各个方面,包括经典书籍、实用工具和面试手册。无论您是刚入门的新手还是有经验的开发者,这些存储库都能为您提供丰富的学习资源,助您在JavaScript领域更进一步。探索这些资源,开启您的学习之旅吧!
59 0
🚀 10 个 GitHub 存储库,助你成为前端巨匠✨
|
2月前
|
前端开发 JavaScript 开发者
Web组件:一种新的前端开发范式
【10月更文挑战第9天】Web组件:一种新的前端开发范式
44 2
|
2月前
|
前端开发 JavaScript Go
前端开发趋势:从响应式设计到Web组件的探索
【10月更文挑战第1天】前端开发趋势:从响应式设计到Web组件的探索
38 3
|
2月前
|
存储 人工智能 前端开发
前端大模型应用笔记(三):Vue3+Antdv+transformers+本地模型实现浏览器端侧增强搜索
本文介绍了一个纯前端实现的增强列表搜索应用,通过使用Transformer模型,实现了更智能的搜索功能,如使用“番茄”可以搜索到“西红柿”。项目基于Vue3和Ant Design Vue,使用了Xenova的bge-base-zh-v1.5模型。文章详细介绍了从环境搭建、数据准备到具体实现的全过程,并展示了实际效果和待改进点。
141 2
|
2月前
|
JavaScript 前端开发 程序员
前端学习笔记——node.js
前端学习笔记——node.js
44 0