01.旧版组件

  1. import * as React from 'react';
  2. import classNames from 'classnames';
  3. import RcInputNumber from 'rc-input-number';
  4. export interface InputNumberProps {
  5. prefixCls?: string; // 预留预设class,这里在defaultProps中默认为'ant-input-number'
  6. min?: number;
  7. max?: number;
  8. value?: number;
  9. step?: number | string;
  10. defaultValue?: number;
  11. tabIndex?: number; //tab 键控制次序,就是快捷切换
  12. onKeyDown?: React.FormEventHandler<any>; // 用户按下键盘按键时的回调函数
  13. onChange?: (value: number | string | undefined) => void;
  14. disabled?: boolean;
  15. size?: 'large' | 'small' | 'default';
  16. formatter?: (value: number | string | undefined) => string;
  17. parser?: (displayValue: string | undefined) => number;
  18. placeholder?: string; // placeholder提示
  19. style?: React.CSSProperties; // 用户自定义style
  20. className?: string; // 用户自定义class
  21. name?: string; // 用户自定义name属性,毕竟底层是input标签
  22. id?: string; // 用户自定义id
  23. precision?: number;
  24. }
  25. export default class InputNumber extends React.Component<InputNumberProps, any> {
  26. static defaultProps = {
  27. prefixCls: 'ant-input-number',
  28. step: 1,
  29. };
  30. private inputNumberRef: any;
  31. render() {
  32. const { className, size, ...others } = this.props;
  33. const inputNumberClass = classNames({
  34. [`${this.props.prefixCls}-lg`]: size === 'large',
  35. [`${this.props.prefixCls}-sm`]: size === 'small',
  36. }, className);
  37. return <RcInputNumber ref={(c: any) => this.inputNumberRef = c} className={inputNumberClass} {...others} />;
  38. }
  39. focus() {
  40. this.inputNumberRef.focus();
  41. }
  42. blur() {
  43. this.inputNumberRef.blur();
  44. }
  45. }

02.2021版react hook 组件

  1. import * as React from 'react';
  2. import classNames from 'classnames';
  3. import KeyCode from 'rc-util/lib/KeyCode';
  4. import { composeRef } from 'rc-util/lib/ref';
  5. import getMiniDecimal, { DecimalClass, toFixed, ValueType } from './utils/MiniDecimal';
  6. import StepHandler from './StepHandler';
  7. import { getNumberPrecision, num2str, validateNumber } from './utils/numberUtil';
  8. import useCursor from './hooks/useCursor';
  9. import useUpdateEffect from './hooks/useUpdateEffect';
  10. /**
  11. * We support `stringMode` which need handle correct type when user call in onChange
  12. */
  13. const getDecimalValue = (stringMode: boolean, decimalValue: DecimalClass) => {
  14. if (stringMode || decimalValue.isEmpty()) {
  15. return decimalValue.toString();
  16. }
  17. return decimalValue.toNumber();
  18. };
  19. const getDecimalIfValidate = (value: ValueType) => {
  20. const decimal = getMiniDecimal(value);
  21. return decimal.isInvalidate() ? null : decimal;
  22. };
  23. export interface InputNumberProps<T extends ValueType = ValueType>
  24. extends Omit<
  25. React.InputHTMLAttributes<HTMLInputElement>,
  26. 'value' | 'defaultValue' | 'onInput' | 'onChange'
  27. > {
  28. /** value will show as string */
  29. stringMode?: boolean;
  30. defaultValue?: T;
  31. value?: T;
  32. prefixCls?: string;
  33. className?: string;
  34. style?: React.CSSProperties;
  35. min?: T;
  36. max?: T;
  37. step?: ValueType;
  38. tabIndex?: number;
  39. controls?: boolean;
  40. // Customize handler node
  41. upHandler?: React.ReactNode;
  42. downHandler?: React.ReactNode;
  43. keyboard?: boolean;
  44. /** Parse display value to validate number */
  45. parser?: (displayValue: string | undefined) => T;
  46. /** Transform `value` to display value show in input */
  47. formatter?: (value: T | undefined, info: { userTyping: boolean; input: string }) => string;
  48. /** Syntactic sugar of `formatter`. Config precision of display. */
  49. precision?: number;
  50. /** Syntactic sugar of `formatter`. Config decimal separator of display. */
  51. decimalSeparator?: string;
  52. onInput?: (text: string) => void;
  53. onChange?: (value: T) => void;
  54. onPressEnter?: React.KeyboardEventHandler<HTMLInputElement>;
  55. onStep?: (value: T, info: { offset: ValueType; type: 'up' | 'down' }) => void;
  56. // focusOnUpDown: boolean;
  57. // useTouch: boolean;
  58. // size?: ISize;
  59. }
  60. const InputNumber = React.forwardRef(
  61. (props: InputNumberProps, ref: React.Ref<HTMLInputElement>) => {
  62. const {
  63. prefixCls = 'rc-input-number',
  64. className,
  65. style,
  66. min,
  67. max,
  68. step = 1,
  69. defaultValue,
  70. value,
  71. disabled,
  72. readOnly,
  73. upHandler,
  74. downHandler,
  75. keyboard,
  76. controls = true,
  77. stringMode,
  78. parser,
  79. formatter,
  80. precision,
  81. decimalSeparator,
  82. onChange,
  83. onInput,
  84. onPressEnter,
  85. onStep,
  86. ...inputProps
  87. } = props;
  88. const inputClassName = `${prefixCls}-input`;
  89. const inputRef = React.useRef<HTMLInputElement>(null);
  90. const [focus, setFocus] = React.useState(false);
  91. const userTypingRef = React.useRef(false);
  92. const compositionRef = React.useRef(false);
  93. // ============================ Value =============================
  94. // Real value control
  95. const [decimalValue, setDecimalValue] = React.useState<DecimalClass>(() =>
  96. getMiniDecimal(value ?? defaultValue),
  97. );
  98. function setUncontrolledDecimalValue(newDecimal: DecimalClass) {
  99. if (value === undefined) {
  100. setDecimalValue(newDecimal);
  101. }
  102. }
  103. // ====================== Parser & Formatter ======================
  104. /**
  105. * `precision` is used for formatter & onChange.
  106. * It will auto generate by `value` & `step`.
  107. * But it will not block user typing.
  108. *
  109. * Note: Auto generate `precision` is used for legacy logic.
  110. * We should remove this since we already support high precision with BigInt.
  111. *
  112. * @param number Provide which number should calculate precision
  113. * @param userTyping Change by user typing
  114. */
  115. const getPrecision = React.useCallback(
  116. (numStr: string, userTyping: boolean) => {
  117. if (userTyping) {
  118. return undefined;
  119. }
  120. if (precision >= 0) {
  121. return precision;
  122. }
  123. return Math.max(getNumberPrecision(numStr), getNumberPrecision(step));
  124. },
  125. [precision, step],
  126. );
  127. // >>> Parser
  128. const mergedParser = React.useCallback(
  129. (num: string | number) => {
  130. const numStr = String(num);
  131. if (parser) {
  132. return parser(numStr);
  133. }
  134. let parsedStr = numStr;
  135. if (decimalSeparator) {
  136. parsedStr = parsedStr.replace(decimalSeparator, '.');
  137. }
  138. // [Legacy] We still support auto convert `$ 123,456` to `123456`
  139. return parsedStr.replace(/[^\w.-]+/g, '');
  140. },
  141. [parser, decimalSeparator],
  142. );
  143. // >>> Formatter
  144. const inputValueRef = React.useRef<string | number>('');
  145. const mergedFormatter = React.useCallback(
  146. (number: string, userTyping: boolean) => {
  147. if (formatter) {
  148. return formatter(number, { userTyping, input: String(inputValueRef.current) });
  149. }
  150. let str = typeof number === 'number' ? num2str(number) : number;
  151. // User typing will not auto format with precision directly
  152. if (!userTyping) {
  153. const mergedPrecision = getPrecision(str, userTyping);
  154. if (validateNumber(str) && (decimalSeparator || mergedPrecision >= 0)) {
  155. // Separator
  156. const separatorStr = decimalSeparator || '.';
  157. str = toFixed(str, separatorStr, mergedPrecision);
  158. }
  159. }
  160. return str;
  161. },
  162. [formatter, getPrecision, decimalSeparator],
  163. );
  164. // ========================== InputValue ==========================
  165. /**
  166. * Input text value control
  167. *
  168. * User can not update input content directly. It update with follow rules by priority:
  169. * 1. controlled `value` changed
  170. * * [SPECIAL] Typing like `1.` should not immediately convert to `1`
  171. * 2. User typing with format (not precision)
  172. * 3. Blur or Enter trigger revalidate
  173. */
  174. const [inputValue, setInternalInputValue] = React.useState<string | number>(() => {
  175. const initValue = defaultValue ?? value;
  176. if (decimalValue.isInvalidate() && ['string', 'number'].includes(typeof initValue)) {
  177. return Number.isNaN(initValue) ? '' : initValue;
  178. }
  179. return mergedFormatter(decimalValue.toString(), false);
  180. });
  181. inputValueRef.current = inputValue;
  182. // Should always be string
  183. function setInputValue(newValue: DecimalClass, userTyping: boolean) {
  184. setInternalInputValue(
  185. mergedFormatter(
  186. // Invalidate number is sometime passed by external control, we should let it go
  187. // Otherwise is controlled by internal interactive logic which check by userTyping
  188. // You can ref 'show limited value when input is not focused' test for more info.
  189. newValue.isInvalidate() ? newValue.toString(false) : newValue.toString(!userTyping),
  190. userTyping,
  191. ),
  192. );
  193. }
  194. // >>> Max & Min limit
  195. const maxDecimal = React.useMemo(() => getDecimalIfValidate(max), [max]);
  196. const minDecimal = React.useMemo(() => getDecimalIfValidate(min), [min]);
  197. const upDisabled = React.useMemo(() => {
  198. if (!maxDecimal || !decimalValue || decimalValue.isInvalidate()) {
  199. return false;
  200. }
  201. return maxDecimal.lessEquals(decimalValue);
  202. }, [maxDecimal, decimalValue]);
  203. const downDisabled = React.useMemo(() => {
  204. if (!minDecimal || !decimalValue || decimalValue.isInvalidate()) {
  205. return false;
  206. }
  207. return decimalValue.lessEquals(minDecimal);
  208. }, [minDecimal, decimalValue]);
  209. // Cursor controller
  210. const [recordCursor, restoreCursor] = useCursor(inputRef.current, focus);
  211. // ============================= Data =============================
  212. /**
  213. * Find target value closet within range.
  214. * e.g. [11, 28]:
  215. * 3 => 11
  216. * 23 => 23
  217. * 99 => 28
  218. */
  219. const getRangeValue = (target: DecimalClass) => {
  220. // target > max
  221. if (maxDecimal && !target.lessEquals(maxDecimal)) {
  222. return maxDecimal;
  223. }
  224. // target < min
  225. if (minDecimal && !minDecimal.lessEquals(target)) {
  226. return minDecimal;
  227. }
  228. return null;
  229. };
  230. /**
  231. * Check value is in [min, max] range
  232. */
  233. const isInRange = (target: DecimalClass) => !getRangeValue(target);
  234. /**
  235. * Trigger `onChange` if value validated and not equals of origin.
  236. * Return the value that re-align in range.
  237. */
  238. const triggerValueUpdate = (newValue: DecimalClass, userTyping: boolean): DecimalClass => {
  239. let updateValue = newValue;
  240. let isRangeValidate = isInRange(updateValue) || updateValue.isEmpty();
  241. // Skip align value when trigger value is empty.
  242. // We just trigger onChange(null)
  243. // This should not block user typing
  244. if (!updateValue.isEmpty() && !userTyping) {
  245. // Revert value in range if needed
  246. updateValue = getRangeValue(updateValue) || updateValue;
  247. isRangeValidate = true;
  248. }
  249. if (!readOnly && !disabled && isRangeValidate) {
  250. const numStr = updateValue.toString();
  251. const mergedPrecision = getPrecision(numStr, userTyping);
  252. if (mergedPrecision >= 0) {
  253. updateValue = getMiniDecimal(toFixed(numStr, '.', mergedPrecision));
  254. }
  255. // Trigger event
  256. if (!updateValue.equals(decimalValue)) {
  257. setUncontrolledDecimalValue(updateValue);
  258. onChange?.(updateValue.isEmpty() ? null : getDecimalValue(stringMode, updateValue));
  259. // Reformat input if value is not controlled
  260. if (value === undefined) {
  261. setInputValue(updateValue, userTyping);
  262. }
  263. }
  264. return updateValue;
  265. }
  266. return decimalValue;
  267. };
  268. // ========================== User Input ==========================
  269. // >>> Collect input value
  270. const collectInputValue = (inputStr: string) => {
  271. recordCursor();
  272. // Update inputValue incase input can not parse as number
  273. setInternalInputValue(inputStr);
  274. // Parse number
  275. if (!compositionRef.current) {
  276. const finalValue = mergedParser(inputStr);
  277. const finalDecimal = getMiniDecimal(finalValue);
  278. if (!finalDecimal.isNaN()) {
  279. triggerValueUpdate(finalDecimal, true);
  280. }
  281. }
  282. };
  283. // >>> Composition
  284. const onCompositionStart = () => {
  285. compositionRef.current = true;
  286. };
  287. const onCompositionEnd = () => {
  288. compositionRef.current = false;
  289. collectInputValue(inputRef.current.value);
  290. };
  291. // >>> Input
  292. const onInternalInput: React.ChangeEventHandler<HTMLInputElement> = (e) => {
  293. let inputStr = e.target.value;
  294. // optimize for chinese input experience
  295. // https://github.com/ant-design/ant-design/issues/8196
  296. if (!parser) {
  297. inputStr = inputStr.replace(/。/g, '.');
  298. }
  299. collectInputValue(inputStr);
  300. // Trigger onInput later to let user customize value if they want do handle something after onChange
  301. onInput?.(inputStr);
  302. };
  303. // ============================= Step =============================
  304. const onInternalStep = (up: boolean) => {
  305. // Ignore step since out of range
  306. if ((up && upDisabled) || (!up && downDisabled)) {
  307. return;
  308. }
  309. // Clear typing status since it may caused by up & down key.
  310. // We should sync with input value.
  311. userTypingRef.current = false;
  312. let stepDecimal = getMiniDecimal(step);
  313. if (!up) {
  314. stepDecimal = stepDecimal.negate();
  315. }
  316. const target = (decimalValue || getMiniDecimal(0)).add(stepDecimal.toString());
  317. const updatedValue = triggerValueUpdate(target, false);
  318. onStep?.(getDecimalValue(stringMode, updatedValue), {
  319. offset: step,
  320. type: up ? 'up' : 'down',
  321. });
  322. inputRef.current?.focus();
  323. };
  324. // ============================ Flush =============================
  325. /**
  326. * Flush current input content to trigger value change & re-formatter input if needed
  327. */
  328. const flushInputValue = (userTyping: boolean) => {
  329. const parsedValue = getMiniDecimal(mergedParser(inputValue));
  330. let formatValue: DecimalClass = parsedValue;
  331. if (!parsedValue.isNaN()) {
  332. // Only validate value or empty value can be re-fill to inputValue
  333. // Reassign the formatValue within ranged of trigger control
  334. formatValue = triggerValueUpdate(parsedValue, userTyping);
  335. } else {
  336. formatValue = decimalValue;
  337. }
  338. if (value !== undefined) {
  339. // Reset back with controlled value first
  340. setInputValue(decimalValue, false);
  341. } else if (!formatValue.isNaN()) {
  342. // Reset input back since no validate value
  343. setInputValue(formatValue, false);
  344. }
  345. };
  346. const onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
  347. const { which } = event;
  348. userTypingRef.current = true;
  349. if (which === KeyCode.ENTER) {
  350. if (!compositionRef.current) {
  351. userTypingRef.current = false;
  352. }
  353. flushInputValue(true);
  354. onPressEnter?.(event);
  355. }
  356. if (keyboard === false) {
  357. return;
  358. }
  359. // Do step
  360. if (!compositionRef.current && [KeyCode.UP, KeyCode.DOWN].includes(which)) {
  361. onInternalStep(KeyCode.UP === which);
  362. event.preventDefault();
  363. }
  364. };
  365. const onKeyUp = () => {
  366. userTypingRef.current = false;
  367. };
  368. // >>> Focus & Blur
  369. const onBlur = () => {
  370. flushInputValue(false);
  371. setFocus(false);
  372. userTypingRef.current = false;
  373. };
  374. // ========================== Controlled ==========================
  375. // Input by precision
  376. useUpdateEffect(() => {
  377. if (!decimalValue.isInvalidate()) {
  378. setInputValue(decimalValue, false);
  379. }
  380. }, [precision]);
  381. // Input by value
  382. useUpdateEffect(() => {
  383. const newValue = getMiniDecimal(value);
  384. setDecimalValue(newValue);
  385. // When user typing from `1.2` to `1.`, we should not convert to `1` immediately.
  386. // But let it go if user set `formatter`
  387. if (newValue.isNaN() || !userTypingRef.current || formatter) {
  388. // Update value as effect
  389. setInputValue(newValue, userTypingRef.current);
  390. }
  391. }, [value]);
  392. // ============================ Cursor ============================
  393. useUpdateEffect(() => {
  394. if (formatter) {
  395. restoreCursor();
  396. }
  397. }, [inputValue]);
  398. // ============================ Render ============================
  399. return (
  400. <div
  401. className={classNames(prefixCls, className, {
  402. [`${prefixCls}-focused`]: focus,
  403. [`${prefixCls}-disabled`]: disabled,
  404. [`${prefixCls}-readonly`]: readOnly,
  405. [`${prefixCls}-not-a-number`]: decimalValue.isNaN(),
  406. [`${prefixCls}-out-of-range`]: !decimalValue.isInvalidate() && !isInRange(decimalValue),
  407. })}
  408. style={style}
  409. onFocus={() => {
  410. setFocus(true);
  411. }}
  412. onBlur={onBlur}
  413. onKeyDown={onKeyDown}
  414. onKeyUp={onKeyUp}
  415. onCompositionStart={onCompositionStart}
  416. onCompositionEnd={onCompositionEnd}
  417. >
  418. {controls && (
  419. <StepHandler
  420. prefixCls={prefixCls}
  421. upNode={upHandler}
  422. downNode={downHandler}
  423. upDisabled={upDisabled}
  424. downDisabled={downDisabled}
  425. onStep={onInternalStep}
  426. />
  427. )}
  428. <div className={`${inputClassName}-wrap`}>
  429. <input
  430. autoComplete="off"
  431. role="spinbutton"
  432. aria-valuemin={min as any}
  433. aria-valuemax={max as any}
  434. aria-valuenow={decimalValue.isInvalidate() ? null : (decimalValue.toString() as any)}
  435. step={step}
  436. {...inputProps}
  437. ref={composeRef(inputRef, ref)}
  438. className={inputClassName}
  439. value={inputValue}
  440. onChange={onInternalInput}
  441. disabled={disabled}
  442. readOnly={readOnly}
  443. />
  444. </div>
  445. </div>
  446. );
  447. },
  448. ) as (<T extends ValueType = ValueType>(
  449. props: React.PropsWithChildren<InputNumberProps<T>> & {
  450. ref?: React.Ref<HTMLInputElement>;
  451. },
  452. ) => React.ReactElement) & { displayName?: string };
  453. InputNumber.displayName = 'InputNumber';
  454. export default InputNumber;

03.React-classnames库

classnames库让我们的在react中使用classname的时候可以一次多个添加类名,并且免去了复杂的三元运算的判断或者if else的判断,第一在代码的可读性方面就会很差

  1. classNames('foo', 'bar'); // => 'foo bar'
  2. classNames('foo', { bar: true }); // => 'foo bar'
  3. classNames({ 'foo-bar': true }); // => 'foo-bar'
  4. classNames({ 'foo-bar': false }); // => ''
  5. classNames({ foo: true }, { bar: true }); // => 'foo bar'
  6. classNames({ foo: true, bar: true }); // => 'foo bar'
  7. // lots of arguments of various types
  8. classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'
  9. // other falsy values are just ignored
  10. classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'
  11. var arr = ['b', { c: true, d: false }];
  12. classNames('a', arr); // => 'a b c'
  13. // 类似模版字符串
  14. let buttonType = 'primary';
  15. classNames({ [`btn-${buttonType}`]: true });
  16. var classNames = require('classnames');
  17. var Button = React.createClass({
  18. // ...
  19. render () {
  20. var btnClass = classNames({
  21. btn‘: true,
  22. 'btn-pressed': this.state.isPressed,
  23. 'btn-over': !this.state.isPressed && this.state.isHovered
  24. });
  25. return <button className={btnClass}>{this.props.label}</button>;
  26. }
  27. });

这样也会更加直观;

其他的部分需要给予对应的数据的支持和结构上的数据的更新;