Formularze

W Reakcie elementy formularza HTML działają trochę inaczej niż pozostałe elementy DOM. Wynika to stąd, że elementy formularza same utrzymują swój wewnętrzny stan. Dla przykładu przyjrzyjmy się zwykłemu formularzowi HTML z jedną wartością - imieniem:

<form>
  <label>
    Imię:
    <input type="text" name="name" />
  </label>
  <input type="submit" value="Wyślij" />
</form>

Powyższy formularz posiada domyślną funkcję automatycznego przekierowania przeglądarki do nowej strony po wysłaniu formularza przez użytkownika. Jeśli zależy ci na tej funkcjonalności, to działa ona również w Reakcie. Jednak w większości przypadków dobrze jest mieć funkcję javascriptową, która obsługuje wysyłanie formularza i ma dostęp do podanych przez użytkownika danych. Standardem stała się obsługa formularzy poprzez tzw. “komponenty kontrolowane”.

Komponenty kontrolowane

W HTML-u, elementy formularza takie jak <input>, <textarea> i <select> najczęściej zachowują swój własny stan, który jest aktualizowany na podstawie danych wejściowych podawanych przez użytkownika. Natomiast w Reakcie zmienny stan komponentu jest zazwyczaj przechowywany we właściwości state (pol. stan) danego komponentu. Jest on aktualizowany jedynie za pomocą funkcji setState().

Możliwe jest łączenie tych dwóch rozwiązań poprzez ustanowienie reactowego stanu jako “wyłącznego źródła prawdy”. Wówczas reactowy komponent renderujący dany formularz kontroluje również to, co zachodzi wewnątrz niego podczas wypełniania pól przez użytkownika. Element input formularza, kontrolowany w ten sposób przez Reacta, nazywamy “komponentem kontrolowanym”

Gdybyśmy chcieli sprawić, aby podany wcześniej przykładowy formularz wyświetlał przy wysłaniu podane przez użytkownika imię, możemy zrobić z niego komponent kontrolowany w następujący sposób:

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('Podano następujące imię: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Imię:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Wyślij" />
      </form>
    );
  }
}

Przetestuj kod na CodePen

Dzięki ustawieniu atrybutu value na elemencie formularza, wyświetlane dane zawsze będą odpowiadały this.state.value. Tym samym reactowy stan jest tutaj źródłem prawdy. Ponieważ zaś handleChange aktualizuje reactowy stan przy każdym wciśnięciu klawisza, wyświetlane dane aktualizują się na bieżąco w miarę wpisywania ich przez użytkownika.

W komponentach kontrolowanych każda zmiana stanu wynika z działania powiązanej z nim funkcji obsługującej (ang. handler function). Ułatwia to modyfikowanie i walidację podawanych przez użytkownika danych wejściowych. Jeśli chcemy na przykład, aby imiona pisane były w całości wielkimi literami, nasza funkcja handleChange mogłaby wyglądać następująco:

handleChange(event) {
  this.setState({value: event.target.value.toUpperCase()});
}

Znacznik textarea

W HTML-u element <textarea> definiuje swój tekst poprzez elementy potomne:

<textarea>
  Cześć, oto przykład tekstu w polu tekstowym.
</textarea>

Natomiast w Reakcie <textarea> wykorzystuje w tym celu atrybut value. Dzięki temu kod formularza zawierającego <textarea> może być podobny do kodu formularza z jednoliniowym elementem input:

class EssayForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: 'Proszę napisać wypracowanie o swoim ulubionym elemencie DOM'
    };

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('Wysłano następujące wypracowanie: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Wypracowanie:
          <textarea value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Wyślij" />
      </form>
    );
  }
}

Zwróć uwagę, że wartość this.state.value jest inicjalizowana w konstruktorze, tak aby pole tekstowe zawierało jakiś domyślny tekst.

Znacznik select

W HTML-u element <select> tworzy rozwijaną listę. Dla przykładu poniższy kod HTML tworzy rozwijaną listę smaków:

<select>
  <option value="grejpfrutowy">Grejpfrutowy</option>
  <option value="limonkowy">Limonkowy</option>
  <option selected value="kokosowy">Kokosowy</option>
  <option value="mango">Mango</option>
</select>

Zwróć uwagę na atrybut selected, który sprawia, że opcją wybraną domyślnie jest opcja “Kokosowy”. W Reakcie zamiast atrybutu selected używamy atrybutu value na głównym znaczniku select. W przypadku komponentów kontrolowanych jest to rozwiązanie bardziej dogodne, ponieważ wartość tego atrybutu aktualizowana jest tylko w jednym miejscu:

class FlavorForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: "kokosowy"};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('Twój ulubiony smak to: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Wybierz swój ulubiony smak:
          <select value={this.state.value} onChange={this.handleChange}>
            <option value="grejpfrutowy">Grejpfrutowy</option>
            <option value="limonkowy">Limonkowy</option>
            <option value="kokosowy">Kokosowy</option>
            <option value="mango">Mango</option>
          </select>
        </label>
        <input type="submit" value="Wyślij" />
      </form>
    );
  }
}

Przetestuj kod na CodePen

Ogólnie elementy <input type="text">, <textarea>, i <select> działają podobnie. Wszystkie przyjmują atrybut value, który można wykorzystać w komponentach kontrolowanych.

Wskazówka

Wartością atrybutu value może być także tablica. Daje to możliwość wyboru spośród wielu opcji w znaczniku select:

<select multiple={true} value={['B', 'C']}>

Znacznik input dla plików

W HTML-u element <input type="file"> pozwala użytkownikom wybrać jeden lub więcej plików z pamięci swojego urządzenia, które następnie mogą być wysłane do serwera lub przetworzone z użyciem kodu JavaScript poprzez interfejs klasy File.

<input type="file" />

Ponieważ wartość tego elementu jest wartością przeznaczoną tylko do odczytu, w Reakcie jest to komponent niekontrolowany. Przedstawimy go wraz z innymi komponentami tego typu w dalszej części dokumentacji.

Obsługa wielu elementów input

Kiedy zachodzi potrzeba obsługi wielu kontrolowanych elementów input, do każdego elementu można dodać atrybut name oraz pozwolić funkcji obsługującej (ang. handler function) zadecydować o dalszych krokach w zależności od wartości atrybutu event.target.name.

Przyjrzyjmy się następującemu przykładowi:

class Reservation extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      wybieraSie: true,
      liczbaGosci: 2
    };

    this.handleInputChange = this.handleInputChange.bind(this);
  }

  handleInputChange(event) {
    const target = event.target;
    const value = target.type === "checkbox" ? target.checked : target.value;
    const name = target.name;

    this.setState({
      [name]: value
    });
  }

  render() {
    return (
      <form>
        <label>
          Wybiera się:
          <input
            name="wybieraSie"
            type="checkbox"
            checked={this.state.wybieraSie}
            onChange={this.handleInputChange} />
        </label>
        <br />
        <label>
          Liczba gości:
          <input
            name="liczbaGosci"
            type="number"
            value={this.state.liczbaGosci}
            onChange={this.handleInputChange} />
        </label>
      </form>
    );
  }
}

Przetestuj kod na CodePen

Zwróć uwagę na wykorzystaną przez nas składnię obliczonych nazw właściwości umożliwioną przez ES6. Pozwala ona na aktualizację klucza stanu odpowiadającego nazwie danego elementu input:

this.setState({
  [name]: value
});

W składni ES5 wyglądałoby to następująco:

var partialState = {};
partialState[name] = value;
this.setState(partialState);

Ponadto ponieważ setState() automatycznie scala podany stan częściowy ze stanem aktualnym, funkcja ta wywoływana jest tylko z nowo dostarczonymi danymi.

Wartość null w kontrolowanym elemencie input

Określenie wartości właściwości (ang. prop) komponentu kontrolowanego zapobiega niepożądanym zmianom danych wejściowych przez użytkownika. Jeśli określisz wartość dla value, a dane wejściowe w dalszym ciągu będzie można edytować, sprawdź, czy przez pomyłkę nie przekazujesz wartości undefined lub null.

Kod poniżej ilustruje ten problem. (Element input jest początkowo zablokowany, ale po krótkiej chwili jego zawartość można edytować.)

ReactDOM.render(<input value="Cześć" />, mountNode);

setTimeout(function() {
  ReactDOM.render(<input value={null} />, mountNode);
}, 1000);

Inne rozwiązania

Stosowanie kontrolowanych komponentów może być niekiedy uciążliwe, ponieważ wymaga nie tylko tworzenia funkcji obsługujących każdą możliwą zmianę twoich danych, lecz także przekazywania stanu elementu input poprzez komponent reactowy. To z kolei może się stać wyjątkowo irytującym doświadczeniem, zwłaszcza gdy konwertujesz istniejący już kod na kod reactowy lub kiedy integrujesz aplikację reactową z biblioteką nie-reactową. W tych sytuacjach warto abyś przyjrzał się komponentom niekontrolowanym, które stanowią alternatywną technikę stosowania formularzy.

Rozwiązania całościowe

Jeśli szukasz rozwiązania kompleksowego umożliwiającego walidację, śledzenie odwiedzonych pól oraz obsługę wysyłania danych, często wybieraną opcją jest biblioteka Formik. Rozwiązanie to bazuje jednak na tych samych zasadach co komponenty kontrolowane i zarządzanie stanem. Dlatego bardzo ważne jest, abyś przyswoił sobie te zasady.