reactコンポーネント設計-antd 4.0-Button

15662 ワード

前言


フロントエンド開発で最もよく使われるコンポーネントはbuttonではない.

サポートが必要な機能


ツールバーの
説明
を選択します.
デフォルト
バージョン#バージョン#
disabled
ボタン失効状態
boolean false
ghost
ゴースト属性、ボタンの背景を透明にする
boolean
false
href
ジャンプするアドレスをクリックして、この属性buttonの動作とaリンクが一致することを指定します
string
-
htmlType buttonオリジナルのtype値を設定します.オプション値はHTML標準を参照してください.
string button
icon
ボタンのアイコンコンポーネントの設定
ReactNode
-
loading
ボタンの読み込みステータスの設定
boolean\
{ delay: number } false
shape
ボタン形状を設定し、オプション値はcircleroundまたは設定しない
string
-
size
ボタンサイズの設定large \middle \small
なし
target
aリンクのtarget属性に相当しhrefが存在する場合に有効
string
-
type
ボタンの種類を設定します.オプション値はprimary dashed linkまたは設定しません.
string
-
onClick
ボタンクリック時のコールバック
(event) => void
-
block
ボタンの幅を親の幅に変更するオプション
boolean false
danger
危険ボタンの設定
boolean false

こうぞう


antd4.0 buttonは主に3つのtsxファイルから構成されています

button.tsx

/* eslint-disable react/button-has-type */
import * as React from 'react';
import classNames from 'classnames';
import LoadingOutlined from '@ant-design/icons/LoadingOutlined'; // antd 4.0 icon
import omit from 'omit.js';

import Group from './button-group';
// export const configConsumerProps = [
//   'getPopupContainer',
//   'rootPrefixCls',
//   'getPrefixCls',
//   'renderEmpty',
//   'csp',
//   'autoInsertSpaceInButton',
//   'locale',
//   'pageHeader',
// ];
import { ConfigContext, ConfigConsumerProps } from '../config-provider'; //  
import Wave from '../_util/wave';
import { Omit, tuple } from '../_util/type';
import warning from '../_util/warning';
import SizeContext, { SizeType } from '../config-provider/SizeContext';

const rxTwoCNChar = /^[\u4e00-\u9fa5]{2}$/;
const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar);
function isString(str: any) {
  return typeof str === 'string';
}

// 。
function insertSpace(child: React.ReactChild, needInserted: boolean) {
  // null
  if (child == null) {
    return;
  }
  const SPACE = needInserted ? ' ' : '';
  // strictNullChecks oops.
  if (
    typeof child !== 'string' &&
    typeof child !== 'number' &&
    isString(child.type) &&
    isTwoCNChar(child.props.children)
  ) {
    return React.cloneElement(child, {}, child.props.children.split('').join(SPACE));
  }
  if (typeof child === 'string') {
    if (isTwoCNChar(child)) {
      child = child.split('').join(SPACE);
    }
    return {child};
  }
  return child;
}

function spaceChildren(children: React.ReactNode, needInserted: boolean) {
  let isPrevChildPure: boolean = false;
  const childList: React.ReactNode[] = [];
  React.Children.forEach(children, child => {
    const type = typeof child;
    const isCurrentChildPure = type === 'string' || type === 'number';
    if (isPrevChildPure && isCurrentChildPure) {
      const lastIndex = childList.length - 1;
      const lastChild = childList[lastIndex];
      childList[lastIndex] = `${lastChild}${child}`;
    } else {
      childList.push(child);
    }

    isPrevChildPure = isCurrentChildPure;
  });

  // Pass to React.Children.map to auto fill key
  // React.Children.map 
  return React.Children.map(childList, child =>
    insertSpace(child as React.ReactChild, needInserted),
  );
}

const ButtonTypes = tuple('default', 'primary', 'ghost', 'dashed', 'danger', 'link');
export type ButtonType = typeof ButtonTypes[number];
const ButtonShapes = tuple('circle', 'circle-outline', 'round');
export type ButtonShape = typeof ButtonShapes[number];
const ButtonHTMLTypes = tuple('submit', 'button', 'reset');
export type ButtonHTMLType = typeof ButtonHTMLTypes[number];

export interface BaseButtonProps {
  type?: ButtonType;
  icon?: React.ReactNode;
  shape?: ButtonShape;
  size?: SizeType;
  loading?: boolean | { delay?: number };
  prefixCls?: string;
  className?: string;
  ghost?: boolean;
  danger?: boolean;
  block?: boolean;
  children?: React.ReactNode;
}

// Typescript will make optional not optional if use Pick with union.
// Should change to `AnchorButtonProps | NativeButtonProps` and `any` to `HTMLAnchorElement | HTMLButtonElement` if it fixed.
// ref: https://github.com/ant-design/ant-design/issues/15930
export type AnchorButtonProps = {
  href: string;
  target?: string;
  onClick?: React.MouseEventHandler;
} & BaseButtonProps &
  Omit, 'type' | 'onClick'>;

export type NativeButtonProps = {
  htmlType?: ButtonHTMLType;
  onClick?: React.MouseEventHandler;
} & BaseButtonProps &
  Omit, 'type' | 'onClick'>;

export type ButtonProps = Partial;

interface ButtonState {
  loading?: boolean | { delay?: number };
  hasTwoCNChar: boolean;
}

class Button extends React.Component {
  static Group: typeof Group;

  static __ANT_BUTTON = true;

  static contextType = ConfigContext;

  static defaultProps = {
    loading: false,
    ghost: false,
    block: false,
    htmlType: 'button' as ButtonProps['htmlType'],
  };

  private delayTimeout: number;

  private buttonNode: HTMLElement | null;

  constructor(props: ButtonProps) {
    super(props);
    this.state = {
      loading: props.loading,
      hasTwoCNChar: false,
    };
  }

  componentDidMount() {
    this.fixTwoCNChar();
  }

  componentDidUpdate(prevProps: ButtonProps) {
    this.fixTwoCNChar();

    if (prevProps.loading && typeof prevProps.loading !== 'boolean') {
      clearTimeout(this.delayTimeout);
    }

    const { loading } = this.props;
    if (loading && typeof loading !== 'boolean' && loading.delay) {
      this.delayTimeout = window.setTimeout(() => {
        this.setState({ loading });
      }, loading.delay);
    } else if (prevProps.loading !== loading) {
      // eslint-disable-next-line react/no-did-update-set-state
      this.setState({ loading });
    }
  }

  componentWillUnmount() {
    if (this.delayTimeout) {
      clearTimeout(this.delayTimeout);
    }
  }

  saveButtonRef = (node: HTMLElement | null) => {
    this.buttonNode = node;
  };

  handleClick: React.MouseEventHandler = e => {
    const { loading } = this.state;
    const { onClick } = this.props;
    if (loading) {
      return;
    }
    if (onClick) {
      (onClick as React.MouseEventHandler)(e);
    }
  };

  fixTwoCNChar() {
    const { autoInsertSpaceInButton }: ConfigConsumerProps = this.context;

    // Fix for HOC usage like 
    if (!this.buttonNode || autoInsertSpaceInButton === false) {
      return;
    }
    const buttonText = this.buttonNode.textContent;
    if (this.isNeedInserted() && isTwoCNChar(buttonText)) {
      if (!this.state.hasTwoCNChar) {
        this.setState({
          hasTwoCNChar: true,
        });
      }
    } else if (this.state.hasTwoCNChar) {
      this.setState({
        hasTwoCNChar: false,
      });
    }
  }

  isNeedInserted() {
    const { icon, children, type } = this.props;
    return React.Children.count(children) === 1 && !icon && type !== 'link';
  }

  render() {
    const { getPrefixCls, autoInsertSpaceInButton, direction }: ConfigConsumerProps = this.context;

    return (
      
        {size => {
          const {
            prefixCls: customizePrefixCls,
            type,
            danger,
            shape,
            size: customizeSize,
            className,
            children,
            icon,
            ghost,
            block,
            ...rest
          } = this.props;
          const { loading, hasTwoCNChar } = this.state;

          warning(
            !(typeof icon === 'string' && icon.length > 2),
            'Button',
            `\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`,
          );

          const prefixCls = getPrefixCls('btn', customizePrefixCls);
          const autoInsertSpace = autoInsertSpaceInButton !== false;

          // large => lg
          // small => sm
          let sizeCls = '';
          switch (customizeSize || size) {
            case 'large':
              sizeCls = 'lg';
              break;
            case 'small':
              sizeCls = 'sm';
              break;
            default:
              break;
          }

          const iconType = loading ? 'loading' : icon;

          const classes = classNames(prefixCls, className, {
            [`${prefixCls}-${type}`]: type,
            [`${prefixCls}-${shape}`]: shape,
            [`${prefixCls}-${sizeCls}`]: sizeCls,
            [`${prefixCls}-icon-only`]: !children && children !== 0 && iconType,
            [`${prefixCls}-loading`]: !!loading,
            [`${prefixCls}-background-ghost`]: ghost,
            [`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && autoInsertSpace,
            [`${prefixCls}-block`]: block,
            [`${prefixCls}-dangerous`]: !!danger,
            [`${prefixCls}-rtl`]: direction === 'rtl',
          });

          const iconNode = loading ?  : icon || null;
          const kids =
            children || children === 0
              ? spaceChildren(children, this.isNeedInserted() && autoInsertSpace)
              : null;

          const linkButtonRestProps = omit(rest as AnchorButtonProps, ['htmlType', 'loading']);
          if (linkButtonRestProps.href !== undefined) {
            return (
              {iconNode}
{kids}
            );
          }

          // React does not recognize the `htmlType` prop on a DOM element. Here we pick it out of `rest`.
          const { htmlType, ...otherProps } = rest as NativeButtonProps;

          const buttonNode = (
            
          );

          if (type === 'link') {
            return buttonNode;
          }

          return {buttonNode};
        }}
      
    );
  }
}

export default Button;

button-group.tsx

import * as React from 'react';
import classNames from 'classnames';
import { SizeType } from '../config-provider/SizeContext';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';

export interface ButtonGroupProps {
  size?: SizeType;
  style?: React.CSSProperties;
  className?: string;
  prefixCls?: string;
}

const ButtonGroup: React.FC = props => (  // 
  
    {({ getPrefixCls, direction }: ConfigConsumerProps) => {
      const { prefixCls: customizePrefixCls, size, className, ...others } = props;
      const prefixCls = getPrefixCls('btn-group', customizePrefixCls);

      // large => lg
      // small => sm
      let sizeCls = '';
      switch (size) {
        case 'large':
          sizeCls = 'lg';
          break;
        case 'small':
          sizeCls = 'sm';
          break;
        default:
          break;
      }

      const classes = classNames(
        prefixCls,
        {
          [`${prefixCls}-${sizeCls}`]: sizeCls,
          [`${prefixCls}-rtl`]: direction === 'rtl',
        },
        className,
      );

      return 
; }} ); export default ButtonGroup;

index.tsx

import Button from './button';
import ButtonGroup from './button-group';

export { ButtonProps, ButtonShape, ButtonType } from './button';
export { ButtonGroupProps } from './button-group';
export { SizeType as ButtonSize } from '../config-provider/SizeContext';

Button.Group = ButtonGroup;
export default Button;

使用されているライブラリ


classnames

classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'

classnames結合React
 var Button = React.createClass({
  // ...
  render () {
    var btnClass = 'btn';
    // state css
    if (this.state.isPressed) btnClass += ' btn-pressed';
    else if (this.state.isHovered) btnClass += ' btn-over';
    return ;
  }
});

オブジェクトを一括して返すことができます
 var classNames = require('classnames');

var Button = React.createClass({
  // ...
  render () {
    var btnClass = classNames({
      'btn': true,
      'btn-pressed': this.state.isPressed,
      'btn-over': !this.state.isPressed && this.state.isHovered
    });
    return ;
  }
});

nameとclassNameがマッピングされている場合はbindメソッドを使用します.
var classNames = require('classnames/bind');

var styles = {
  foo: 'abc',
  bar: 'def',
  baz: 'xyz'
};

var cx = classNames.bind(styles);

var className = cx('foo', ['bar'], { baz: true }); // => "abc def xyz"
import { Component } from 'react';
import classNames from 'classnames/bind';
import styles from './submit-button.css';

let cx = classNames.bind(styles);

export default class SubmitButton extends Component {
  render () {
    let text = this.props.store.submissionInProgress ? 'Processing...' : 'Submit';//text 
    let className = cx({
      base: true,
      inProgress: this.props.store.submissionInProgress,// 
      error: this.props.store.errorOccurred,
      disabled: this.props.form.valid,
    });
    return ;
  }
};