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;