IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> TS+react自建组件库 02 -> 正文阅读

[JavaScript知识库]TS+react自建组件库 02

作者:recommend-item-box type_download clearfix

TS+react自建组件库 02

KGD的第二个组件-Alert

需求分析

类型分析-type:成功、默认、危险、警告

功能分析-:默认弹出框、可添加描述弹出框

设计实现

import React, {FC, useState} from 'react'

import classNames from 'classnames'

export enum AlertType {
  Success = 'success',
  Default = 'default',
  Danger = 'danger',
  Warning = 'warning',
}

interface BaseAlert { 
  title ?: string,
  description ?: string,
  type ?: AlertType,
  onClose ?: () => void,
  closable ?: boolean
}


type AlertProps = BaseAlert & React.HTMLAttributes<HTMLDivElement>

const Alert : FC<AlertProps> = (props) => {
  const {
    className,
    title, 
    description, 
    type, 
    onClose, 
    closable
  } = props

  const classes = classNames('kgd-alert',className, {
    [`kgd-alert-${type}`] : type,
    'closable' : type === AlertType.Warning ? false : closable,
    'zoom-in-top-appear-done' : 'zoom-in-top-appear-done',
    'zoom-in-top-enter-done' : 'zoom-in-top-enter-done',
  })
     

  const [visible, setVisible] = useState(true)

  const closeAlert  = (onClose : Function) => {
    return () => {
      setVisible(false)
      onClose()
    }
  }

  return visible ?
  ( 
    <div
      className={classes}
    >
      <span>{title}</span>
      <span 
      className = 'kgd-alert-close'  
      onClick = {closeAlert(onClose as () => void)}
      >
        ×
      </span>
      <p>{description ? description : null}</p>
    </div>
  ): null
}

Alert.defaultProps = {
  type : AlertType.Default,
  closable : true,
  onClose : () => {}
}

export default Alert;

KGD的组件测试

测试框架选择:JEST

安装指令

yarn add --dev jest

文档

调用指令

npx jest 测试文件名	--watch

React测试工具:React Testing Library

辅助小工具:jest-dom

(tips:脚手架创建项目时,已内置安装)

测试代码

Button

import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Button, { ButtonProps, ButtonType, ButtonSize } from './'
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}>Nice</Button>)
    const element = wrapper.getByText('Nice') 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}>Nice</Button>)
    const element = wrapper.getByText('Nice')
    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={ButtonType.Link} href="http://dummyurl">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}>Nice</Button>)
    const element = wrapper.getByText('Nice') as HTMLButtonElement
    expect(element).toBeInTheDocument()
    expect(element.disabled).toBeTruthy()
    fireEvent.click(element)
    expect(disabledProps.onClick).not.toHaveBeenCalled()
  })
})

Alert

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

describe('test Alert component', () => {
  it('should render the correct default Alert', () => {
    const wrapper = render(<Alert title = 'Alert-test'/>)
    const element = wrapper.getByText('Alert-test').parentNode as HTMLElement
    expect(element).toBeInTheDocument()
    expect(element.tagName).toEqual('DIV')
    expect(element).toHaveClass('kgd-alert kgd-alert-default')
  })
  it('should render the correct component based on different closeFunction', () => {
    const wrapper = render(<Alert title = 'Alert-onClose-test' onClose = {() => {console.log('aaa')}}/>)
    const element = wrapper.getByText('Alert-onClose-test').nextElementSibling as HTMLElement
    expect(element).toBeInTheDocument()
    expect(element).toHaveClass('kgd-alert-close')
    fireEvent.click(element)
  })
})

KGD的第三个组件-Menu

需求分析

基本样式分析:横向、纵向

功能分析:默认、不可选、下拉菜单

属性分析:选项下标、是否选中(高亮)、用户自定义类名

设计实现

Menu

import React, {FC, createContext, useState} from 'react'
import classNames from 'classnames'
import { MenuItemProps } from './MenuItem'

type MenuMode = 'horizontal' | 'vertical'
type selectCallback = (selectedIndex: string) => void

interface BaseMenu {
  mode ?: MenuMode;
  defaultIndex ?: string;
  onSelect ?: selectCallback;
  classNames ?: string;
  style ?: React.CSSProperties;
  defaultOpenSubMenus ?: string[];
}

interface IMenuContext {
  index : string;
  onSelect ?: selectCallback;
  mode ?: MenuMode;
  defaultOpenSubMenus ?: string[];
}

export const MenuContext = createContext<IMenuContext>({index:'0'})

export type MenuProps = BaseMenu & React.HTMLAttributes<HTMLUListElement>

const Menu:FC<MenuProps> = (props) => {
  const {
    className, 
    style, 
    defaultIndex, 
    mode,
    children,
    onSelect,
    defaultOpenSubMenus,
  } = props

  const classes = classNames('kgd-menu', className, {
    'menu-vertical' : mode === 'vertical',
    'menu-horizontal' : mode !== 'vertical'
  })

  const [currentActive, setActive] = useState(defaultIndex)

  const handleClick = (index:string) => {
    setActive(index);
    if(onSelect) onSelect(index);
  }

  const passedContext : IMenuContext = {
    index : currentActive ? currentActive : '0',
    onSelect: handleClick,
    mode,
    defaultOpenSubMenus,
  }

  const renderChildren = () => {
    return React.Children.map(children,(child, index) => {
      const childElement = child as React.FunctionComponentElement<MenuItemProps>
      const { displayName } = childElement.type
      if(displayName === 'MenuItem' || displayName === 'SubMenu') 
      return React.cloneElement(childElement, { 
        index:index.toString()
      })
      else console.error('Warning: Menu has a child which is not a MenuItem component')
    })
  }
  

  return(
    <ul 
    className={classes}
    style={style}
    >
      <MenuContext.Provider value = {passedContext}>
        {renderChildren()}
      </MenuContext.Provider>
    </ul>
  )
}

Menu.defaultProps = {
  defaultIndex : '0',
  mode : 'horizontal',
  defaultOpenSubMenus : [],
}

export default Menu;

MenuItem

import {FC, useContext} from 'react'
import classNames from 'classnames'
import {MenuContext} from '../'

interface BaseMenuItem {
  index ?: string;
  disabled ?: boolean;
  className?: string;
  style ?: React.CSSProperties;
}

export type MenuItemProps = BaseMenuItem & React.LiHTMLAttributes<HTMLLIElement>

const MenuItem:FC<MenuItemProps> = (props) => {
  const {
    className,
    style,
    children,
    index,
    disabled,
  } = props

  const context = useContext(MenuContext)

  const classes = classNames('menu-item', className, {
    'is-disabled': disabled,
    'is-active' : context.index === index
  })

  const handleClick = () => {
    if(context.onSelect && !disabled && (typeof index === 'string')) {
      context.onSelect(index)
    }
  }

  return(
    <li
    style={style}
    className = {classes}
    onClick = {handleClick}
    >
      {children}
    </li>
  )
}

MenuItem.displayName = 'MenuItem'

MenuItem.defaultProps = {
  index : '0',
}

export default MenuItem;

SubMenu

import React,{FC, useContext, useState} from 'react'
import classNames from 'classnames'
import {MenuContext} from '../'
import {MenuItemProps} from '../MenuItem'

interface BaseSubMenu {
  index ?: string;
  title : string;
  className ?: string
}

export type SubMenuProps = BaseSubMenu & React.LiHTMLAttributes<HTMLLIElement>

const SubMenu : FC<SubMenuProps> = (props) => {

  const {
    title, 
    index, 
    className, 
    children
  } = props

  const context = useContext(MenuContext)

  const classes = classNames('submenu-item menu-item', className, {
    'is-active' : context.index === index,
  })

  const openedSubMenus = context.defaultOpenSubMenus as Array<string>
  const isopened = (index && context.mode === 'vertical') ? openedSubMenus.includes(index) :false

  const [menuOpen,setOpen] = useState(isopened)

  const handleClick = (e:React.MouseEvent) => {
    e.preventDefault()
    setOpen(!menuOpen)
  }

  let timer:any
  const handleMouse = (e:React.MouseEvent,toggle:boolean) => {
    clearTimeout(timer)
    e.preventDefault()
    timer = setTimeout(() => {
      setOpen(toggle)
    },200)
  }

  const clickEvents = context.mode === 'vertical' ? {
    onClick : handleClick
  } : {}

  const hoverEvents = context.mode === 'vertical' ? 
  {} : {
    onMouseEnter:(e:React.MouseEvent) => {handleMouse(e,true)},
    onMouseLeave:(e:React.MouseEvent) => {handleMouse(e,false)}
  }

  const renderChildren = () => {
    const classes = classNames('kgd-submenu', {
      'menu-opened' : menuOpen
    })
    const ChildComponet = React.Children.map(children,(child, i) => {
      const childElement = child as React.FunctionComponentElement<MenuItemProps>
      if(childElement.type.displayName === 'MenuItem') return React.cloneElement(childElement, {
        index:`${index}-${i}`
      })
      else console.error('Warning: Menu has a child which is not a MenuItem component')
    })
    return(
      <ul
        className = {classes}
      >
        {ChildComponet}
      </ul>
    )
  }

  return(
    <li
      className = {classes}
      key = {index}
      {...hoverEvents}
    >
      <div 
      className = 'submenu-title'
      {...clickEvents}
      >
        {title}
      </div>
      {renderChildren()}
    </li>
  )
}

SubMenu.displayName = 'SubMenu'

export default SubMenu;

KGD的第四个组件-Tabs

需求分析

基本样式分析:线条形式、卡片形式

功能分析:默认、不可选

属性分析:选项下标、是否选中(高亮)、用户自定义类名、用户自定义选项卡样式

设计实现

Tabs

import React, {FC, useState, createContext} from 'react'
import classNames from 'classnames'
import {TabItemProps} from './TabItem'

type tabsType = 'line' | 'card'
type SelectCallback = (SelectIndex:number) => void

interface BaseTabs {
  defaultIndex ?: number;
  className ?: string;
  onSelect ?: SelectCallback
  type ?: tabsType
  defaultOpenTabs ?: number[];
}

export type TabsProps = BaseTabs & React.HTMLAttributes<HTMLUListElement>

interface ITabsContext {
  type ?: tabsType;
  index : number;
  onSelect ?: SelectCallback;
  defaultOpenTabs ?: number[];
}

export const TabsContext = createContext<ITabsContext>({
  index:0,
})

const Tabs : FC<TabsProps> = (props) => {

  const {
    className,
    type,
    onSelect,
    defaultIndex,
    children,
    defaultOpenTabs
   } = props;

   const classes = classNames('kgd-tabs-nav', className, {
     'nav-card' : type === 'card',
     'nav-line' : type === 'line'
   })

   const [currentActive, setActive] = useState(defaultIndex)
   const [content, setContent] = useState()
   const [tabsOpen,setTabsopen] = useState(false)

   const handleClick = (index:number) => {
    setActive(index)
    onSelect && onSelect(index)
   }

  const passedContext : ITabsContext = {
    index : currentActive ? currentActive : 0,
    onSelect: handleClick,
    type,
    defaultOpenTabs,
  }  

  const getContent = (content : any, tabsOpen : boolean) => {
    return tabsOpen ? (
        <div className = 'kgd-tabs-content'>
          <div className = 'kgd-tab-panel'>
            {content}
          </div>
        </div> 
    ): null
  }

  const ChildrenContent = (content : any, tabsOpen : boolean) => {
    setContent(content)
    setTabsopen(tabsOpen)
  }

  const renderChildren = () => {
    return React.Children.map(children,(child, index) => {
      const childElement = child as React.FunctionComponentElement<TabItemProps>
      const { displayName } = childElement.type
      if(displayName === 'TabsItem') 
      return React.cloneElement(childElement, { 
        index,
        ChildrenContent
      })
      else console.error('Warning: Tabs has a child which is not a TabsItem component')
    })
  }

  return (
    <>
      <ul
        className={classes}
      >
        <TabsContext.Provider value = {passedContext}>
          {renderChildren()}
        </TabsContext.Provider>
      </ul>
      {getContent(content,tabsOpen)}
    </>
  )
}

Tabs.defaultProps = {
  defaultIndex : 0,
  defaultOpenTabs : []
}

export default Tabs;

TabItem

import React, {FC, useContext, useState, useEffect} from 'react'
import classNames from 'classnames'
import {TabsContext} from '../'

interface BaseTabsItem {
  index ?: number,
  label : any,
  disabled ?: boolean,
  ChildrenContent ?: Function,
}

export type TabItemProps = BaseTabsItem & React.LiHTMLAttributes<HTMLLIElement>

const TabItem : FC<TabItemProps> = (props) => {

  const {index, label, disabled, className, children, ChildrenContent} = props

  const context = useContext(TabsContext)

  
  const openedTabs = context.defaultOpenTabs as Array<number>
  const isopened = openedTabs.includes(index as number)

  const [tabsOpen,setOpen] = useState(isopened)

  useEffect(() =>{
    index === context.index && setOpen(true)
    index === context.index && ChildrenContent && ChildrenContent(children, tabsOpen)
  },[index,context.index,ChildrenContent,children,tabsOpen])

  const classes = classNames('kgd-tabs-nav-item', className, {
    'disabled': disabled,
    'is-active' : context.index === index
  })

  const handleClick = () => {
    if(context.onSelect && !disabled && (typeof index === 'number')) {
      context.onSelect(index)
      setOpen(!tabsOpen)
      ChildrenContent && ChildrenContent(children, tabsOpen)
    }
  }

  return (
      <li
        className = {classes}
        onClick = {handleClick}
        key = {index}
      >
        {label}
      </li>
  )
}

TabItem.displayName = 'TabsItem'

TabItem.defaultProps = {
  index : 0,
}

export default TabItem;
  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-10-15 11:41:41  更:2021-10-15 11:42:36 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/20 11:16:57-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码