UI 구성

player를 추가하는 폼을 만들자 AddPlayerForm 컴포넌트를 추가한다. stateful한 컴포넌트가 되어야 하므로 class 컴포넌트로 작성한다. form 내부에 input 박스와 submit 버튼을 추가한다.

html 의 input은 닫는 태그가 없지만 JSX는 반드시 닫는 태그가 존재해야 한다는것에 주의하자.

components/AddPlayerForm.jsx

import React, {Component} from 'react';

export class AddPlayerForm extends Component {
  render() {
    return (
      <div className="container">
        <form className="row player align-items-center">
          <div className="col-9">
            {/*<label htmlFor="playerName" className="form-label">Player Name</label>*/}
            <input type="text" className="form-control" id="playerName" placeholder="input player name"></input>
          </div>
          <div className="col-3 d-grid">
            <button type="submit" className="btn btn-primary">Add Player</button>
          </div>
        </form>
      </div>
    )
  }
}

App 컴포넌트에 AddPlayerForm 컴포넌트를 맨 하단에 추가한다(L14)

App.jsx

...
  render() {
    return (
      <div className="container p-3">
        <Header title="My scoreboard" players={this.state.players} />

        {/*Players List*/}
        { this.state.players.map(item =>
          <Player key={item.id}
                  name={item.name} score={item.score} id={item.id}
                  removePlayer={this.handleRemovePlayer}
                  changeScore={this.handleChangeScore} />) }

        <AddPlayerForm></AddPlayerForm>
      </div>
    );
  }
}

controlled component 만들기

HTML element는 자기 자신의 state를 가지고 있고, 사용자의 입력에 따라 그 state를 업데이트한다. 리액트에서는 이와 같이 mutable state는 컴포넌트의 state 속성에 정의하고 setState로 업데이트해야 한다. 그러므로 input form은 리액트에서 컨트롤되어야 하는데 이와 같은 컴포는트를 controlled component라고 한다. 사용자가 input을 입력하면 바로 상태가 바뀌고 바뀐 상태가 바로 리렌더링되서 화면에 보여주게 된다. input 의 value를 controlled component로 만들어보자.

1) 먼저 시간에 따라 변하는 입력값을 state에 value로 정의하고(L2 ~ L4) input에 value와 연결한다(L17). 그리고 테스트해보자. input 에 값을 넣으면 값이 입력이 되는가? 입력이 되지 않고 하단 warning을 확인해보자. onChange 이벤트를 구현하라고 나올것이다.

2) onChange 이벤트 구현 (L17)

components/AddPlayerForm.jsx

export class AddPlayerForm extends Component {
  state = {
    value: ''
  };

  handleValueChange = (e) => {
    this.setState({value: e.target.value});
  };

  render() {
    return (
      <div className="container">
        <form className="row player align-items-center">
          <div className="col-9">
            {/*<label htmlFor="playerName" className="form-label">Player Name</label>*/}
            <input type="text" className="form-control" id="playerName" placeholder="input player name"
                   value={this.state.value} onChange={this.handleValueChange}></input>
          </div>
          <div className="col-3 d-grid">
            <button type="submit" className="btn btn-primary">Add Player</button>
          </div>
        </form>
      </div>
    )
  }
}

player 추가 로직 구현

첫번째로 부모 컴포넌트에 player를 추가하는 handleAddPlayer 함수를 작성하고(L2 ~ L4) 자식에게 이 함수를 속성으로 넘긴다. (L18)

App.jsx

...
  handleAddPlayer = (name) => {
    console.log(name);
  };

  render() {
    return (
      <div className="container p-3">
        <Header title="My scoreboard" players={this.state.players} />

        {/*Players List*/}
        { this.state.players.map(item =>
          <Player key={item.id}
                  name={item.name} score={item.score} id={item.id}
                  removePlayer={this.handleRemovePlayer}
                  changeScore={this.handleChangeScore} />) }

        <AddPlayerForm addPlayer={this.handleAddPlayer}></AddPlayerForm>
      </div>
    );
  }
}

두번째로 AddPlayerForm에서 이 props의 함수를 호출한다.

버튼을 클릭시 onSubmit 이벤트가 발생하고 handleSubmit을 호출한다(L21). handleSubmit에서는 기본 이벤트를 막아야 화면이 재로딩되는것을 막는다(L11). 그리고, 부모로부터 받은 addPlayer 함수를 호출시 입력된 name을 입력 파라메터로 넘겨준다(L12). 입력필드는 다시 초기화시킨다(L13).

export class AddPlayerForm extends Component {
  state = {
    value: ''
  };

  handleValueChange = (e) => {
    this.setState({value: e.target.value});
  };

  handleSubmit = (e) => {
    e.preventDefault();
    this.props.addPlayer(this.state.value);
    this.setState({
      value: ''
    });
  }

  render() {
    return (
      <div className="container">
        <form className="row player align-items-center" onSubmit={this.handleSubmit}>
          <div className="col-9">
            {/*<label htmlFor="playerName" className="form-label">Player Name</label>*/}
            <input type="text" className="form-control" id="playerName" placeholder="input player name"
                   value={this.state.value} onChange={this.handleValueChange}></input>
          </div>
          <div className="col-3 d-grid">
            <button type="submit" className="btn btn-primary">Add Player</button>
          </div>
        </form>
      </div>
    )
  }
}

버튼을 클릭시 console에 이름이 제대로 찍히는지 확인한다. 만일 콘솔에 로그가 제대로 찍히면 자식과 부모의 통신이 제대로 구현이 된것이다.

이제 세번째로 부모 함수에서 로직을 구현한다.

먼저 가장 큰 id 값을 구한다음 +1을 더한다. 기존배열의 맨앞에 객체를 추가하는 unshift 함수는 immutable 이 아니므로 먼저 deep copy를 수행해야 한다.

App.jsx

  handleAddPlayer = (name) => {
    console.log(name);
    this.setState(prevState => {
      const players = [ ... prevState.players ];

      const maxObject = _.maxBy(players, 'id');
      const maxId = maxObject.id + 1;
      console.log(maxId);
      players.unshift({id: maxId, name, score: 0});

      return { players };
    });
  };

html5 validation

input에 아무것도 입력되지 않으면 submit이 되지 않도록 하되 html5 validation을 사용하자.

input에 required 속성만 추가하면 된다.

+ Q: required 속성을 추가하고 입력값이 없이 클릭시 submit 이벤트가 일어나는가?

일어나지 않는다. html5 validation을 수행하기 때문이다.

html5 validation을 하지 않기 위해서는 form에 noValidate 를 추가한다. 이것을 추가하고 입력필드를 비우고 버튼을 클릭하면 submit이 일어날 것이다. 이제 submit이 일어나면 handleSubmit에서 입력필드들이 유효한지 직접 체크하자.

먼저 form에 접근할 Ref와 input 필드에 접근할 input Ref를 만든다(L8 ~ L9)

이제 DOM에 연결한다(L38, L42)

submit 이벤트가 일어나면 current속성으로 DOM 노드에 접근한다. (L19, L20)

input 필드가 유효한지 validity.valid로 체크한다(L22)

form이 유효한지 checkValidity() 함수로 체크한다(L23)

export class AddPlayerForm extends Component {
  state = {
    value: ''
  };

  constructor(props) {
    super(props);
    this.formRef = React.createRef();
    this.textRef = React.createRef();
  }

  handleValueChange = (e) => {
    this.setState({value: e.target.value});
  };

  handleSubmit = (e) => {
    e.preventDefault();

    const form = this.formRef.current; // form node
    const player = this.textRef.current; // input node

    console.log(player.validity.valid);
    console.log(form.checkValidity());

    if (!form.checkValidity()) {
      return;
    }

    this.props.addPlayer(this.state.value);
    this.setState({
      value: ''
    });
  }

  render() {
    return (
      <div className="container">
        <form ref={this.formRef} noValidate
              className="row player align-items-center" onSubmit={this.handleSubmit}>
          <div className="col-9">
            {/*<label htmlFor="playerName" className="form-label">Player Name</label>*/}
            <input ref={this.textRef}
                   type="text" className="form-control" id="playerName" placeholder="input player name"
                   required
                   value={this.state.value} onChange={this.handleValueChange}></input>
          </div>
          <div className="col-3 d-grid">
            <button type="submit" className="btn btn-primary">Add Player</button>
          </div>
        </form>
      </div>
    )
  }
}

bootstrap validation

html5 validation 에서 보여주는 UI는 브라우저마다 다 각각 다르다.  bootstrap validation에서 지원해주는 UI를 사용해보자.

form에 needs-validation 클래스를 추가한다(L27)

input아래에 invalid-feedback을 추가한다(L34 ~ L36)

submit 버튼을 클릭시 만일 form이 invalid라면 form에 was-validated 클래스를 추가한다(L12).  만일 valida하다면 was-validated 클래스를 삭제한다(L20)

...
handleSubmit = (e) => {
    e.preventDefault();

    const form = this.formRef.current; // form node
    const player = this.textRef.current; // input node

    console.log(player.validity.valid);
    console.log(form.checkValidity());

    if (!form.checkValidity()) {
      form.classList.add('was-validated');
      return;
    }

    this.props.addPlayer(this.state.value);
    this.setState({
      value: ''
    });
    form.classList.remove('was-validated');
  }

  render() {
    return (
      <div className="container">
        <form ref={this.formRef} noValidate
              className="row player align-items-center needs-validation" onSubmit={this.handleSubmit}>
          <div className="col-9">
            {/*<label htmlFor="playerName" className="form-label">Player Name</label>*/}
            <input ref={this.textRef}
                   type="text" className="form-control" id="playerName" placeholder="input player name"
                   required
                   value={this.state.value} onChange={this.handleValueChange}></input>
            <div className="invalid-feedback">
              Please input name.
            </div>
          </div>
          <div className="col-3 d-grid">
            <button type="submit" className="btn btn-primary">Add Player</button>
          </div>
        </form>
      </div>
    )
  }
}

+ quiz