Moduł 2 - Zajęcia 4 - Formularze

Formularze

Article 1: Niekontrolowane elementy

Podstawowym celem każdego formularza jest zebranie danych od użytkownika. Do tego celu możemy wykorzystać właściwość elements elementu formualrza (form) dostępną podczas obsługi jego wysyłania. Alternatywnie możemy wykorzystać interfejs FormData.

            class LoginForm extends Component {
                handleSubmit = evt => {  
                    evt.preventDefault();

                    const form = evt.currentTarget;
                    const login = form.elements.login.value;  
                    const password = form.elements.password.value;
  
                    console.log(login, password);
    
                    this.props.onSubmit({ login, password });

                    form.reset();  
                };

                render() {
                    return (
                    <form onSubmit={this.handleSubmit}>
                    <input type="text" name="login" />
                    <input type="password" name="password" />
                    <button type="submit">Login<p/button>
                    </form>
                    );
                }
            }

            ReactDOM.render(
            <LoginForm onSubmit={values => console.log(values)} />,
            document.getElementById("root")
            );
          

Dostęp do danych w takiej formie jest właściwy, gdy dane pól formularza potrzebne są nam tylko w czasie jego wysyłania ('submit').

Article 2: Kontrolowane elementy

Dane pól formularza mogą nam być również potrzebne w innych komponentach albo w momencie zmiany pola. W takiej sytuacji elementy formularza powinny być kontrolowane. Najprościej mówiąć, oznacza to, że wartości wszystkich pól powinny znajdować się w stanie.

  • Pole w state określa wartość atrybutu value danego elementu formularza.
  • Zdarzenie onChange przekazuje się metodę zmieniającą wartość pola w stanie.

Otrzymujemy obwód zamknięty:

  • Po zdarzeniu onChange, metoda klasy aktualizuje pole w stanie.
  • Po zmianie stanu zachodzi re-render.
  • Input otrzymuje zaktualizowaną wartość.

Wadą takiego rozwiązania jest to, że cały formularz zostanie przerenderowany po każdej zmianie któregokolwiek z pól. Dla małych formularzy nie stanowi to jednak problemu.

            class App extends Component {
                state = {
                    inputValue: "",
                };

                handleChange = evt => {
                    this.setState({ inputValue: evt.target.value });
                };

                render() {
                    const { inputValue } = this.state;
                    return (
                    <input type="text" value={inputValue} onChange={this.handleChange} />
                    );
                }
            }
          

Zachodzi tutaj taka prawidłowość, że to nie interfejs określa, jakie mamy dane. Przeciwnie, to dane określają to, co widzi użytkownik, aktualizując DOM po zmianie stanu komponentu.

Article 3: Skomplikowane formularze

Utworzymy teraz formularz rejestracji.

            class SignUpForm extends Component {
                state = {
                    login: "",
                };

                // Odpowiada za aktualizację stanu
                handleChange = e => {
                    this.setState({ login: e.target.value });
                };

                // Wywoływany jest podczas wysyłania formularza
                handleSubmit = evt => {
                    evt.preventDefault();
                    console.log(`Signed up as: ${this.state.login}`);

                    // Props, który przekazywany jest do formularza do wywołania podczas jego wysyłania
                    this.props.onSubmit({ ...this.state });
                };

                render() {
                    const { login } = this.state;

                    return (
                    <form onSubmit={this.handleSubmit}>
                    <label>
                    Name
                    <input
                    type="text"
                    placeholder="Enter login"
                    value={login}
                    onChange={this.handleChange}
                    />
                    </label>
                    <button type="submit">Sign up as {login}</button>
                    </form>
                    );
                }
            }

            ReactDOM.render(
            <SignUpForm onSubmit={values => console.log(values)} />,
            document.getElementById("root")
            );
          

Dodajemy jeszcze pola dla email oraz password. W tym celu wykorzystamy bardzo przydatny wzorzec aktualizacji stanu wielu pól z użyciem jednej metody.

            // Dla poprawy czytelności kodu można przenieść stan początkowy formularza poza ciało klasy.
            // Możemy tak zrobić, jeżeli wartości nie są obliczane dynamicznie.
            const INITIAL_STATE = {
              login: "",
              email: "",
              password: "",
            };

            class SignUpForm extends React.Component {
              state = { ...INITIAL_STATE };

            // Dla wszystkich elementów wykorzystamy jedną funkcję obsługującą zmianę stanu ('handler').
            // Inputy będziemy rozróżniać za pomocą atrybutu `name`
              handleChange = evt => {
                const { name, value } = evt.target;
                this.setState({ [name]: value });
              };

              handleSubmit = evt => {
                evt.preventDefault();
                const { login, email, password } = this.state;

                console.log(`Login: ${login}, Email: ${email}, Password: ${password}`);

                this.props.onSubmit({ ...this.state });
                this.reset();
              };

              reset = () => {
                this.setState({ ...INITIAL_STATE });
              };

              render() {
                const { login, email, password } = this.state;

                return (
                  <form onSubmit={this.handleSubmit}>
                    <label>
                      Name
                      <input
                        type="text"
                        placeholder="Enter login"
                        name="login"
                        value={login}
                        onChange={this.handleChange}
                      />
                    </label>
                    <label>
                      Email
                      <input
                        type="email"
                        placeholder="Enter email"
                        name="email"
                        value={email}
                        onChange={this.handleChange}
                      />
                    </label>
                    <label>
                      Password
                      <input
                        type="password"
                        placeholder="Enter password"
                        name="password"
                        value={password}
                        onChange={this.handleChange}
                      />
                    </label>

                    <button type="submit">Sign up as {login}</button>
                  </form>
                );
              }
            }
          

Article 4: Generowanie identyfikatorów dla elementów formularza

Dostępność (accessibility, a11y) to obecnie bardzo ważny temat w sieci. Atrybut HTML for dla tagu label pomaga technologiom wspomagającym użytkowników lepiej zrozumieć i odczytać interfejs formularza. Jako, że for to słowo kluczowe w JavaScript, w React JSX atrybut nosi nazwę htmlFor.

Do generowania interaktywnych elementów formularzy wykorzystywane jest następujące podejście: dla każdego egzemplarza komponentu, przy jego inicjalizacji, tworzony jest zestaw unikalnych identyfikatorów przechowywanych na egzemplarzu. W ten sposób między różnymi formularzami uzyskamy unikalne id.

            // Możemy wykorzystać dowolną bibliotekę do generowania unikalnych łańcuchów
            import { nanoid } from "nanoid";

            class Form extends React.Component {
              loginInputId = nanoid();

              render() {
                return (
                  <form>
                    <label htmlFor={this.loginInputId}>Login</label>
                    <input type="text" name="login" id={this.loginInputId} />
                  </form>
                );
              }
            }
          

Wykorzystując bibliotekę nanoid tworzymy unikalny identyfikator pola i zapisujemy go w polu klasy. Wartość zostanie wygenerowana podczas inicjalizacji klasy, i dzięki wykorzystaniu pola klasy, nie ulegnie zmianie podczas re-renderów komponentu. Biblioteka zagwarantuje nam unikalność identyfikatorów dla wszystkich elementów, również pomiędzy wieloma formularzami.

Article 5: Pola wyboru typu tak/nie (checkbox)

Cechy charakterystyczne pól wyboru:

  • Może mieć tylko dwa stany: true lub false
  • Aktualną wartość ze stanu przekazujemy do pola checked.
  • Podczas obsługi zdarzenia onChange dostęp do wartości pola udostępniony jest poprzez event.target.checked.
  • Jeżeli chcemy, aby przycisk wyboru przechowywał inną wartość niż logiczną, możemy również wykorzystać atrybut value.

Dodajmy do naszego formularza rejestracji pole wyboru do potwierdzenia zgody użytkownika. Przycisk wysyłania furmularza ('submit') dezaktywujemy, dopóki pole wyboru nie zostanie kliknięte.

            const INITIAL_STATE = {
              login: "",
              email: "",
              password: "",
              agreed: false,
            };

            class SignUpForm extends React.Component {
              state = {
                ...INITIAL_STATE,
              };

              handleChange = evt => {
                const { name, value, type, checked } = evt.target;
            // Jeżeli typ elementu to checkbox, bierzemy wartość checked,
            // w przeciwnym razie value
                this.setState({ [name]: type === "checkbox" ? checked : value });
              };

              handleSubmit = e => {
                e.preventDefault();
                const { login, email, password, agreed } = this.state;
                console.log(
                  `Login: ${login}, Email: ${email}, Password: ${password}, Agreed: ${agreed}`
                );

            /* ... */
              };

              render() {
                const { login, email, password, agreed } = this.state;

                return (
                  <form onSubmit={this.handleSubmit}>
                    {/* ... */}
                    <label>
                      Agree to terms
                      <input
                        type="checkbox"
                        name="agreed"
                        checked={agreed}
                        onChange={this.handleChange}
                      />
                    </label>

                    <button type="submit" disabled={!agreed}>
                      Sign up as {login}
                    </button>
                  </form>
                );
              }
            }
          

Article 6: Pole wielu opcji (radio)

Pole typu radio posiada zarówno atrybut name (określający przypisaną wartość pola), jak również checked (określający, czy dana wartość została wybrana).

            <input
              type="radio"
              checked={this.state.gender === "male"}
              value="male"
              onChage={this.handleGenderChange}
            />
          

Dodajmy grupę przycisków opcji do naszego formularza.

            // Wykorzystujemy obiekt konfiguracyjny (enum) do zdefiniowania dostępnych wartości pola wyboru.
            // Jest to postrzegane jako dobry wzorzec, w porównaniu z tzw. "magicznymi łańcuchami" wartości, które biorą się znikąd.
            const Gender = {
              MALE: "male",
              FEMALE: "female",
            };

            const INITIAL_STATE = {
              login: "",
              email: "",
              password: "",
              agreed: false,
              gender: null,
            };

            class SignUpForm extends React.Component {
              state = {
                ...INITIAL_STATE,
              };

            /*... */

              render() {
                const { login, email, password, agreed, gender } = this.state;

                return (
                  <form onSubmit={this.handleSubmit}>
                    {/* ... */}

                    <section>
                      <h2>Choose your gender</h2>
                      <label>
                        Male
                        <input
                          type="radio"
                          checked={gender === Gender.MALE}
                          name="gender"
                          value={Gender.MALE}
                          onChange={this.handleChange}
                        />
                      </label>
                      <label>
                        Female
                        <input
                          type="radio"
                          checked={gender === Gender.FEMALE}
                          name="gender"
                          value={Gender.FEMALE}
                          onChange={this.handleChange}
                        />
                      </label>
                    </section>

                    <button type="submit" disabled={!agreed}>
                      Sign up as {login}
                    </button>
                  </form>
                );
              }
            }
          

Article 6: Pola wyboru (Select)

Obsługa pola wyboru typu select jest analogiczna. Wykorzystujemy atrybut value oraz zdarzenie onChange.

            const INITIAL_STATE = {
              login: "",
              email: "",
              password: "",
              agreed: false,
              gender: null,
              age: "",
            };

            class SignUpForm extends React.Component {
              state = {
                ...INITIAL_STATE,
              };

            /* ... */

              render() {
                const { login, email, password, agreed, gender, age } = this.state;

                return (
                  <form onSubmit={this.handleSubmit}>
                    {/* ... */}

                    <label>
                      Choose your age
                      <select name="age" value={age} onChange={this.handleChange}>
                        <option value="" disabled>
                          ...
                        </option>
                        <option value="18-25">18-25</option>
                        <option value="26-35">26-35</option>
                        <option value="36+">36+</option>
                      </select>
                    </label>

                    <button type="submit" disabled={!agreed}>
                      Sign up as {login}
                    </button>
                  </form>
                );
              }
            }