TS+react自建组件库 04

这篇博客介绍了如何构建一个React组件库,包括Input组件,具备禁用、图标、大小等特性;AutoComplete组件,支持数据数组、筛选和回调;以及Select组件,提供多选和下拉选择功能。这些组件丰富了UI交互,并且易于整合到项目中。
摘要由CSDN通过智能技术生成

TS+react自建组件库 04

KGD的第七个组件-Input

需求分析

属性分析:能否禁用、图标、规格大小、输入框前缀、输入框后缀

设计实现

import {FC, ReactElement, ChangeEvent, InputHTMLAttributes} from 'react'
import { IconProp } from '@fortawesome/fontawesome-svg-core'
import classNames from 'classnames'

import Icon from '../Icon' 

type InputSize = 'lg' | 'sm'

interface BaseInput {
  /**输入框是否禁用,默认为false */
  disabled ?: boolean,
  /**输入框的图标 */
  icon ?: IconProp,
  /**输入框规格类型 */
  size ?: InputSize,
  /**输入框前缀 */
  prepend ?: string | ReactElement,
  /**输入框后缀 */
  append ?: string | ReactElement,
  /**输入框输入内容后调用函数 */
  onChange ?: (e: ChangeEvent<HTMLInputElement>) => void
}

export type InputProps = BaseInput & Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>

/**
 * 支持 HTMLInput 的所有基本属性
 * ### 引用方法
 * 
 * ~~~js
 * import { Input } from 'kgd'
 * ~~~
 */

const Input : FC<InputProps> = (props) => {
  const {
    disabled,
    icon,
    size,
    prepend,
    append,
    style,
    ...restProps
  } = props;

  const classes = classNames('kgd-input-wrapper', {
    'is-disabled': disabled,
    [`input-size-${size}`] : size,
    'input-group': prepend || append,
    'input-group-append': !!append,
    'input-group-prepend': !!prepend
  })

  const fixControlledValue = (value: any) => typeof value === 'undefined' || value === null ? '' : value
  
  if('value' in props) {
    delete restProps.defaultValue
    restProps.value = fixControlledValue(props.value)
  }

  return(
    <div
      className = {classes}
      style = {style}
    >
      {prepend && <div className="kgd-input-group-prepend">{prepend}</div>}
      {icon && <div className="icon-wrapper"><Icon icon={icon} title={`title-${icon}`}/></div>}
      <input 
        className="kgd-input-inner"
        disabled={disabled}
        {...restProps}
      />
      {append && <div className="kgd-input-group-append">{append}</div>}
    </div>
  )
}

export default Input;

KGD的第八个组件-AutoComplete

需求分析

属性分析:数据数组、筛选函数、回调函数、自定义样式渲染函数

设计实现

import classNames from 'classnames'
import {
  FC,
  useState,
  ChangeEvent,
  ReactElement,
  useEffect,
  KeyboardEvent,
  useRef
} from 'react'

import Input, { InputProps } from '../Input'
import Icon from '../Icon'
import Transition from '../Transition'

import useDebounce from '../../hooks/useDebounce'
import useClickOutside from '../../hooks/useClickOutside'


interface DataSourceObject {
  value: string,
}

export type DataSourceType<T = {}> = T & DataSourceObject

export interface AutoCompleteProps extends Omit<InputProps, 'onSelect'> {
  /**	返回输入建议的方法,可以拿到当前的输入,然后返回同步的数组或者是异步的 Promise
    * type DataSourceType<T = {}> = T & DataSourceObject 
    */
  fetchSuggestions: (str: string) => DataSourceType[] | Promise<DataSourceType[]>,
  /**点击选中建议项时触发的回调 */
  onSelect?: (item: DataSourceType) => void,
  /**支持自定义渲染下拉项,返回 ReactElement */
  renderOption?: (item: DataSourceType) => ReactElement
}

/**
 * 输入框自动完成功能。当输入值需要自动完成时使用,支持同步和异步两种方式 支持 Input 组件的所有属性 支持键盘事件选择
 * ### 引用方法
 * 
 * ~~~js
 * import { AutoComplete } from 'kgd'
 * ~~~
 */

const AutoComplete: FC<AutoCompleteProps> = (props) => {
  const {
    fetchSuggestions,
    onSelect,
    value,
    renderOption,
    ...restProps
  } = props

  const [inputValue, setInputValue] = useState(value as string)
  const [suggestions, setSuggestions] = useState<DataSourceType[]>([])
  const [loading, setLoading] = useState(false)
  const [highLightIndex, setHighLightIndex] = useState(-1)
  const [showDropDown, setShowDropDown] = useState(false)

  const triggerSearch = useRef(false)
  const componentRef = useRef<HTMLDivElement>(null)
  const debouncedValue = useDebounce(inputValue)

  useClickOutside(componentRef, () => {
    setSuggestions([])
    setLoading(false)
  })

  useEffect(() => {
    if (debouncedValue && triggerSearch.current) {
      const results = fetchSuggestions(debouncedValue)
      setLoading(true)
      if(results instanceof Promise)  
        results.then(data => {
          setSuggestions(data)
          setLoading(false)
          if (data.length > 0) setShowDropDown(true)
        }) 
      else{
        setSuggestions(results)
        setShowDropDown(true)
        if (results.length > 0) setShowDropDown(true)
      } 
    } else {
      setShowDropDown(false)
      setLoading(false)
    }
    setHighLightIndex(-1)
  }, [debouncedValue, fetchSuggestions])

  const highLight = (index: number) => {
    if (index < 0) index = suggestions.length - 1
    if (index >= suggestions.length) index = 0
    setHighLightIndex(index)
  }

  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
    switch (e.key) {
      case 'Enter':
        suggestions[highLightIndex] && handleSelecet(suggestions[highLightIndex])()
        break
      case 'ArrowUp':
        highLight(highLightIndex - 1)
        break
      case 'ArrowDown':
        highLight(highLightIndex + 1)
        break
      case 'Escape':
        setShowDropDown(false)
        setLoading(false)
        break
      default: break
    }
  }

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value.trim()
    setInputValue(value)
    triggerSearch.current = true
  }

  const handleSelecet = (item: DataSourceType) => () => {
    setInputValue(item.value)
    setShowDropDown(false)
    if (onSelect) onSelect(item)
    triggerSearch.current = false
  }

  const renderTemplate = (item: DataSourceType) => renderOption ? renderOption(item) : item.value

  const generateDropDown = () => {
    return (
      <Transition
        in={showDropDown || loading}
        animation="zoom-in-top"
        timeout={300}
        onExited={() => {setSuggestions([])}}
      >
        <ul className="kgd-suggestion-list">
          {suggestions.map((item, index) => {
            const classes = classNames('suggestion-item', {
              'is-active': index === highLightIndex
            })

            return (
              <li
                key={index}
                onClick={handleSelecet(item)}
                className={classes}
              >
                {renderTemplate(item)}
              </li>
            )
          })
          }
        </ul>
      </Transition>
    )
  }

  return (
    <div
      className='kgd-auto-complete'
      ref={componentRef}
    >
      <Input
        value={inputValue}
        onChange={handleChange}
        onKeyDown={handleKeyDown}
        {...restProps}
      />
      {loading &&
        <ul className="kgd-suggestion-list">
          <div
            className="suggstions-loading-icon"
          >
            <Icon icon='spinner' spin />
          </div>
        </ul>}
      {(suggestions.length > 0) && generateDropDown()}
    </div>
  )

}

export default AutoComplete;

KGD的第九个组件-Select

需求分析

属性分析:是否禁用、是否多选、默认文字、默认选中条目

设计实现

Select
import { 
  FC, 
  useState, 
  createContext, 
  useEffect,
  Children,
  cloneElement,
  FunctionComponentElement,
  useRef,
  useCallback
 } from 'react'
import classNames from 'classnames'

import Input from '../Input'
import Icon from '../Icon'
import Transition from '../Transition'
import { OptionProps} from "./Option"

import useClickOutside from '../../hooks/useClickOutside'

export interface SelectProps {
  /**指定默认选中的条目 可以是字符串或者字符串数组 */
  defaultValue?: string | string[];
  /**选择框默认文字 */
  placeholder?: string;
  /**是否禁用 */
  disabled?: boolean;
  /**是否可以多选 */
  multiple?: boolean;
  /**select input的name属性 */
  name?: string;
  /**选中值发生变化时触发 */
  onChange?: (selectedValue: string, selectedValues: string[]) => void
  /**下拉框隐藏/出现时触发 */
  onVisibleChange?: (visible: boolean) => void
}

interface IselectContext {
  setInputValue: any
  setTagsArray: any
  tagsArray: string[]
  multiple: boolean
  onChange?: (selectedValue: string, selectedValues: string[]) => void
}

export const SelectContext = createContext<IselectContext>({
  setInputValue: () => {},
  setTagsArray: () => {},
  tagsArray: [],
  multiple: false,
  onChange: () => {}
})

/**
 * 下拉选择器。 弹出一个下拉菜单给用户选择操作,用于代替原生的选择器,或者需要一个更优雅的多选器时。
 * ### 引用方法
 * 
 * ~~~js
 * import { Select } from 'kgd'
 * ~~~
 */

const Select: FC<SelectProps> = (props) => {

  const {
    defaultValue,
    placeholder,
    disabled,
    multiple,
    name,
    children,
    onChange,
    onVisibleChange
  } = props

  const [clickControl, setClickControl] = useState(false)
  const [inputValue, setInputValue] = useState(defaultValue)
  const [tagsArray, setTagsArray] = useState([])

  const divRef = useRef<HTMLDivElement>(null)

  useClickOutside(divRef, () => {
    setClickControl(false)
    onVisibleChange && onVisibleChange(clickControl)
  })

  const removeTag = useCallback((index: number) => {
    return () => {        
      const newArray = tagsArray.slice()
      newArray.splice(index,1)
      setTagsArray(newArray)
    }
  },[tagsArray])

  const renderTags = useCallback((tagsArray: string[]) => {

    return tagsArray.map((tag, index) => (
      <span
        key={index}
        className='kgd-tag'
      >
        {tag}
        <Icon
          icon='times'
          onClick = {removeTag(index)}
        />
      </span>
    )
    )
  },[removeTag])

  useEffect(() => {
    renderTags(tagsArray)
  }, [tagsArray,renderTags])

  const classes = classNames('kgd-select', {
    'menu-is-open': clickControl,
    'is-multiple': multiple,
  })

  const openMenu = () => {
    setClickControl(!clickControl)
    onVisibleChange && onVisibleChange(clickControl)
  }

  const getLiContext: IselectContext = {
    setInputValue,
    setTagsArray,
    tagsArray,
    multiple: multiple ? multiple : false,
    onChange
  }

  

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

  return (
    <div 
    className={classes} 
    ref = {divRef}
    >
      <div className='kgd-select-input'>
        <Input
          readOnly={true}
          placeholder={tagsArray.length > 0 ? '' : placeholder}
          icon='angle-down'
          value={inputValue}
          onClick={openMenu}
          disabled = {disabled}
          name = {name}
        />
      </div>
      <Transition
        animation='zoom-in-top'
        in={clickControl}
        timeout={200}
      >
        <SelectContext.Provider value={getLiContext}>
          <ul 
          className='kgd-select-dropdown'
          onClick={!multiple ? openMenu : () => {}}
          >
            {renderChildren()}
          </ul>
        </SelectContext.Provider>
      </Transition>
      <div className='kgd-selected-tags'>
        {multiple && renderTags(tagsArray)}
      </div>
    </div>
  )
}

Select.defaultProps = {
  name: 'kgd-select',
  placeholder: '请选择'
}

export default Select;

Option
import {
  FC, 
  LiHTMLAttributes, 
  useContext,
} from 'react'
import classNames from 'classnames'

import {SelectContext} from '../'
import Icon from '../../Icon'

interface BaseOption {
  /**选项下标 */
  index ?: string;
  /**	默认根据此属性值进行筛选,该值不能相同 */
  value ?: string;
  /**选项的标签,若不设置则默认与 value 相同 */
  label ?: string;
  /**是否禁用该选项 */
  disabled ?: boolean;
}

export type OptionProps = BaseOption & LiHTMLAttributes<HTMLLIElement>

const Option : FC<OptionProps> = (props) => {

  const {
    value,
    index,
    label,
    disabled
  } = props

  const context = useContext(SelectContext)

  const handleClick = () => {
    let tagIndex = context.tagsArray.indexOf(value as string)

    !context.multiple && 
    context.setInputValue(value) && 
    context.onChange &&
    context.onChange(value as string,context.tagsArray)

    if(context.tagsArray.length > 0 && context.multiple) {
      tagIndex > -1 ? removeTag(tagIndex) :
      context.setTagsArray([...context.tagsArray,value])
    }
    else{
      context.setTagsArray([value])
    }
  }

  const removeTag = (index : number) => {
      context.tagsArray.splice(index,1)
      context.setTagsArray(context.tagsArray)
  }

  const classes = classNames('kgd-select-item',{
    'is-disabled':disabled,
    'is-selected': context.tagsArray.includes(value as string)
  })

  return(
    <li 
      key = {index}
      className = {classes}
      onClick = {handleClick}
    >
      {label ? label : value}
      {
      context.tagsArray.includes(value as string) && 
        <Icon 
          className = 'primary'
          icon = 'check'
        />
      }
    </li>
  )
}

Option.displayName = 'Option'

export default Option;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菜鸟小胖砸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值