Részletesebb ismerkedés a React-tel: Form-ok, input-ok - 4.rész

Gömböcz Zsolt | 2022. 10. 03. 10:36 | Olvasási idő: 4 perc

Címkék: #CSS #Érvényesítés (Validation) #git #hooks #HTML #React #React JS #SPA #Űrlap (Form) #Vendégblogger

Kicsivel nagyobb lélegzetvételű bejegyzés lesz ez, mivel meg fogjuk nézni miként tudunk React-ban beviteli mezőket és űrlapokat kezelni, validálni. A projekthez egy kis részét fogjuk felhasználni, viszont szeretném részletesen megmutatni mennyire egyszerű külső könyvtárak nélkül, valós időben kezelni a HTML5 által biztosított beviteli mezőket.
react-input

Két részre bontom a bejegyzést, először megnézzük az legtöbbet használt input mezők kezelését és emellett validálni is fogunk. A bejegyzés második felében pedig implementálunk egy kereső mezőt (field), mivel ez fog lehetőséget biztosítani arra, hogy városokra keressünk rá, és kapjunk meg róluk adatokat.

Ezen felül nem fogok most foglalkozni külső library-kkel, persze ezek nagyban meg tudják könnyíteni a munkát, azonban a biztos alapok után már bárki könnyedén el tud sajátítani egy-két hasznos könyvtárat.


HTML5 mezők

A legtöbbet használt beviteli mezők és azok típusai:

  • <input type="text" /> : Szöveg mező, nem ugyanaz, mint a szövegdoboz
  • <input type="email" /> : E-mail címekhez használt beviteli mező, validálja a bevitt szöveget automatikusan (ellenőrzi, hogy tartalmaz-e kukac és pont karaktereket, de azt nem ellenőrzi, hogy valós, létező e-mail cím-e).
  • <input type="password" /> : Tartalma rejtett, jelszó bevitelére alkalmas.
  • <input type="date | time | datetime-local" /> : Dátum és/vagy időpont kiválasztására alkalmas. (A type-nál felsorolás miatt van 3 érték, természetesen egyet kell használni.)
  • <input type="radio" /> : Megfelelő HTML struktúrával egyet lehet kiválasztani közülük.
  • <input type="checkbox" /> : Megfelelő HTML struktúrával egy vagy többet lehet kiválasztani.
  • <select><option></option></select> : Legördülő menü egy vagy több választási lehetőséggel.
  • <input type="submit" /> : Gomb amivel az űrlapot tudjuk elküldeni.

Természetesen, ezen kívül még van néhány input mező, de ezek a legtöbbet használtak. Ezeket fogjuk megnézni miként is tudjuk React-ben használni, és egy CSS mentes űrlapot is elkészítünk velük, majd megnézzük a teljes űrlap elküldését, kezelését, validálását.


Űrlap komponens létrehozása

Nem fogunk minden input mezőnek külön saját komponenst készíteni, elég, ha a form-unkat tudjuk egyetlen egy komponensként használni. Kezdjük azzal, hogy új ágat hozunk létre ennek a bejegyzésnek (git branch blog-3) majd lépjünk is át (git checkout blog-3), majd git merge blog-2 -vel adjuk hozzá a blog-2 ( vagy amiben dolgoztunk ág ) tartalmát. Kezdjük azzal, hogy az input-ok teszteléséhez hozzunk létre egy új útvonalat az App.js fájlban (kiemelem az érintett sorokat):

import Navbar from "./components/navbar/Navbar";
import { Routes, Route } from "react-router-dom";
import { useState } from "react";

import Home from "./components/home/Home";
import Weather from "./components/weather/Weather";
import NotFound from "./components/notfound/NotFound";
import Protected from "./components/protected/Protected";
import Form from "./components/form/Form";

const App = () => {
	const [logged] = useState(false); // Change to test protected route

	return (
		<div className={"flex-col flex h-screen"}>
			<Navbar />

			<Routes>
				<Route path="/" element={<Home />} />
				<Route path="/weather" element={<Weather />} />
				<Route
					path="/protected"
					element={
						<Protected isLogged={logged}>
							<div>Sensitive data</div>
						</Protected>
					}
				/>
				<Route path="/form" element={<Form />} />
				<Route path="*" element={<NotFound />} />
			</Routes>
		</div>
	);
};

export default App;

A Form komponensünket amit előbb importáltunk és kirendelerülnk a /form útvonalon, hozzuk létre az src/components/form/Form.js elérési útvonalon és a tartalma nagyon egyszerűen fog kinézni:

import React from 'react'
import FormGroup from '../globals/FormGroup'

const Form = () => {
  return (
    <div>
        <FormGroup />
    </div>
  )
}

export default Form

Mivel én a magát a nézet fájlt Form-nak neveztem el, így FormGroup néven fogok létrehozni egy komponenst, ebben fogjuk használni az input mezőket. A components-en belül létrehoztam egy globals mappát, ebben fogjuk tárolni a nézet-független komponens fájlokat. Ide hozzuk is létre a FormGroup.js fájlt (src/components/globals/FormGroup.js):


Kezdhetjük is létrehozni a beviteli mezőket, és struktúrát adni a komponensnek. A HTML által biztosított <form> tag-et is fogjuk használni és a submit típusú <button> tag-et is, hogy teljesértékű űrlapot kapjunk, azonban később látni fogjuk, hogy ezek nélkül is lehet használni beviteli mezőket. Így fog kinézni a FormGroup komponensünk:

import React from 'react'

const FormGroup = () => {
    const handleFormSubmit = (e) => {
        e.preventDefault();
    }
    return (
        <form onSubmit={handleFormSubmit} className={'flex flex-col justify-center items-center gap-4'}>
            <div className={'flex flex-col'}>
                <label>Szöveg:</label>
                <input type="text" name={'text_field'} className={'text-black'} />
            </div>
            <div className={'flex flex-col'}>
                <label>E-mail:</label>
                <input type="email" name={'email_field'} className={'text-black'} />
            </div>
            <div className={'flex flex-col'}>
                <label>Jelszó:</label>
                <input type="password" name={'password_field'} className={'text-black'} />
            </div>
            <div className={'flex flex-col'}>
                <label>Dátum:</label>
                <input type="date" name={'date_field'} className={'text-black'} />
            </div>
            <div className={'flex flex-col'}>
                <div className={'grid grid-cols-2 gap-2'}>
                    <label>Rádió 1</label>
                    <input type="radio" name={'radio'} value={'radio_1'} />
                    <label>Rádió 2</label>
                    <input type="radio" name={'radio'} value={'radio_2'} />
                </div>
            </div>
            <div className={'flex flex-col'}>
                <div className={'grid grid-cols-2 gap-2'}>
                    <label>Check 1</label>
                    <input type="checkbox" name={'checkbox_1'} />
                    <label>Check 2</label>
                    <input type="checkbox" name={'checkbox_2'} />
                </div>
            </div>
            <div className={'flex flex-col'}>
                <select name={'select_field'}>
                    <option value={'select_1'}>select 1</option> 
                    <option value={'select_2'}>select 2</option> 
                </select>
            </div>
            
            <button type="submit" className={'border border-white p-2'}>Form küldése</button>
        </form>
    )
}

export default FormGroup

Eddig használtunk saját CSS szabályokat azonban a Tailwind nagyon meg tudja könnyíteni az életünket, az űrlapot és a beviteli mezők elhelyezkedését már ezzel oldottam meg.

Észrevehetjük, hogy nem feltétlenül tűnik még egésznek a történet, de vessük is bele magunkat. Tudjuk, hogy egy action és method nélkül nem működik megfelelően az űrlap, amit a <form> tag-en belül kellene alkalmazni attribútumként. Azonban nekünk nem érdekünk, hogy egy adott útvonalra POST-ként küldjük el az adatokat. Legtöbb esetben elég is lenne, de mi szeretnénk mi magunk validálni / megtekinteni értékeinket, és ezek után megalkotni a saját struktúránkat, amit majd elküldhetünk egy szerver felé. (Ezzel még nem fogunk foglalkozni most.) 


Validálás és küldés kezelése

Kezdjük azzal, hogy hozzárendelünk egy függvényt az űrlapunkhoz az onSubmit attribútumban, és megakadályozzuk az alapértelmezett működését, vagyis az adatok elküldését.

import React from 'react'

const FormGroup = () => {
    const handleFormSubmit = (e) => {
        e.preventDefault();
    }
    return (
        <form onSubmit={handleFormSubmit} className={'flex flex-col justify-center items-center gap-4'}>
            ...
        </form>
    )
}

export default FormGroup

Hozzunk létre egy változót a useState hook segítségével (ezt importáljuk is a fájl tetején), és minden beviteli mező változásakor frissítsük a tartalmát (esetleg hasznos lehet kipróbálni, hogy a form lekezelő függvényben ki is írjuk - console.log() - a Chrome-ban a Components-re a formData értékeit, de ezt utána törölhetjük is).

import React, { useState } from 'react'

const FormGroup = () => {
    const [formData, setFormData] = useState({});

    const handleFormSubmit = (e) => {
        e.preventDefault();
        console.log(formData);
    }
    return (
        <form onSubmit={handleFormSubmit} className={'flex flex-col justify-center items-center gap-4'}>
            <div className={'flex flex-col'}>
                <label>Szöveg:</label>
                <input type="text" name={'text_field'} className={'text-black'} 
                    onChange={(e) => setFormData({...formData, text_field: e.target.value})} 
                />
            </div>
            <div className={'flex flex-col'}>
                <label>E-mail:</label>
                <input type="email" name={'email_field'} className={'text-black'} 
                    onChange={(e) => setFormData({...formData, email_field: e.target.value})}
                />
            </div>
            <div className={'flex flex-col'}>
                <label>Jelszó:</label>
                <input type="password" name={'password_field'} className={'text-black'} 
                    onChange={(e) => setFormData({...formData, password_field: e.target.value})}
                />
            </div>
            <div className={'flex flex-col'}>
                <label>Dátum:</label>
                <input type="date" name={'date_field'} className={'text-black'} 
                    onChange={(e) => setFormData({...formData, date_field: e.target.value})}
                />
            </div>
            <div className={'flex flex-col'}>
                <div className={'grid grid-cols-2 gap-2'} 
                    onChange={(e) => setFormData({...formData, radio: e.target.value})}
                >
                    <label>Rádió 1</label>
                    <input type="radio" name={'radio'} value={'radio_1'} />
                    <label>Rádió 2</label>
                    <input type="radio" name={'radio'} value={'radio_2'} />
                </div>
            </div>
            <div className={'flex flex-col'}>
                <div className={'grid grid-cols-2 gap-2'}>
                    <label>Check 1</label>
                    <input type="checkbox" name={'checkbox_1'} 
                        onChange={(e) => setFormData({...formData, checkbox_1: e.target.checked})}
                    />
                    <label>Check 2</label>
                    <input type="checkbox" name={'checkbox_2'} 
                        onChange={(e) => setFormData({...formData, checkbox_2: e.target.checked})}
                    />
                </div>
            </div>
            <div className={'flex flex-col'}>
                <select name={'select_field'} onChange={(e) => setFormData({...formData, select_field: e.target.selectedIndex})}>
                    <option value={'select_1'}>select 1</option> 
                    <option value={'select_2'}>select 2</option> 
                </select>
            </div>
            
            <button type="submit" className={'border border-white p-2'}>Form küldése</button>
        </form>
    )
}

export default FormGroup

Az adatok kiírásának eredménye egy tesztfuttatás során a Components lapfülön látható (előbb bal oldalt ki kell jelölni a FormGroup elemet, utána pedig jobb oldalon a hooks alatti State objektumot nyissuk le az előtte lévő kis háromszög segítségével):

Ha a küldés gombra kattintunk, akkor a változónk értéke megfelel a beviteli mezőkbe beírtak együttesének. Ezekután szabadon elvégezhetünk pár kliens oldali validálást, HTML5 is segít ebben, hisz az input tag-ekre rakhatunk különböző szabályokat (built-in validations), de most próbáljuk ki mi magunk pár mezőt szabályok közé szorítani:

import React, { useState } from 'react'

const FormGroup = () => {
    const [formData, setFormData] = useState({});

    const handleFormSubmit = (e) => {
        e.preventDefault();
        let isValid = true;
        if(formData?.text_field === undefined || formData?.text_field.length < 3 || formData?.text_field.length > 5) {
            isValid = false;
        }   

        const selectedDate = formData?.date_field ? new Date(formData?.date_field) : undefined;
        if(selectedDate !== undefined && selectedDate > Date.now()) {
            isValid = false;
        }
        
        console.log(isValid);
    }
    return (...)
}

export default FormGroup

Láthatjuk, hogy a szöveges mezőnk "üressége" (nincs kitöltve), vagy a nem megfelelő hossz átállítja a változónk értéket, ami a form megfelelőségét jelzi. A dátumnál, ha nincs kiválasztva az nem okoz problémát, azonban, ha van kiválasztva dátum, és a mai naphoz képest jövőbe mutat, akkor is invalid (validáció szempontjából false) eredményt mond az űrlapra. Nagyon szélsőséges példák, azonban jó példa ez arra, hogy nem érdemes teljes mértékben elzárkózni külső könyvtáraktól, ha alkalmazásunk növekedni kezd.

Itt látható a Console-ban, amikor üresen akarom elküldeni az űrlapot:


Kereső beviteli mező létrehozása

Az imént láttuk milyen az, ha egy egész űrlapot szeretnénk egészként kezelni, azonban nekünk az alkalmazáshoz csak egyetlen egy szöveges mező fog kelleni. Ez a leggyakoribb forma, ahol még nem feltétlenül szükséges külső könyvtár bevonása. Kezdjük is azzal, hogy létrehozunk egy komponenst, ami a kereső mezőnket fogja tartalmazni, én ezt a globals mappába fogom megtenni (src/components/globals/SearchBar.js):


Kezdjük is az input field-del, és a hozzá tartozó állapot változó létrehozásával, továbbá pár CSS class-t "ráadtam", hogy a mérete megfelelő legyen:

import React, { useEffect, useState } from 'react'

const SearchBar = () => {
    const [query, setQuery] = useState("");
    useEffect(() => {
      console.log(query);
    }, [query])
    
    return (
        <input type="text" onChange={(e) => setQuery(e.target.value)} className={'w-80 mx-auto h-10 text-xl text-black rounded-xl searchbar'} />
    )
}

export default SearchBar

Menjünk át a Weather.js fájlunkba, és importáljuk ezt a keresőt, hogy le tudjuk tesztelni, hozzáadtam pár utility class-t is:

import React from "react";
import SearchBar from "../globals/SearchBar";

const Weather = () => {
	return (
	<div className={'flex flex-col container mx-auto'}>
		<SearchBar />
	</div>);
};

export default Weather;


Most, ha gépelünk a mezőbe, akkor a konzolba láthatjuk, hogy minden egyes leütés után megkapjuk az input mező értékét. Pontosan ez kell nekünk hisz ahogy gépelünk úgy fogunk keresést indítani város, országok után. De ez nem feltétlen praktikus hisz felesleges és nagyon megterhelő a szerver szempontjából kérést indítani minden leütés után. Alkalmazzunk egy késleltetést gépelés közben, szakszó rá a debouncer (itt olvashattok róla többet: What is debouncing in JavaScript?), ez segít abban, hogy ne egyből küldjünk kérést a szerver felé, hanem egy bizonyos idő eltelte után, és ez az idő minden változás után visszaáll, így elkerüljük a gyors gépelés okozta folyamatos lekérdezést a későbbiekben. Módosítsuk eszerint a SearchBar.js fájlt:

import React, { useEffect, useState } from 'react'

const SearchBar = () => {
    const [query, setQuery] = useState("");

    useEffect(() => {
        const debounce = setTimeout(() => {
            console.log(query);
          }, 450)
      
          return () => clearTimeout(debounce)
    }, [query])
    
    return (
        <input type="text" onChange={(e) => setQuery(e.target.value)} className={'w-80 mx-auto h-10 text-xl text-black rounded-xl searchbar'} />
    )
}

export default SearchBar


Láthatjuk, hogy most márcsak a végeredményt kapjuk meg, miután végeztünk a gépeléssel. Szóval összegezzük hogyan is működik ez a kereső:

  • Van egy state-ünk, ami arra szolgál, hogy gépelés során tároljuk az input mező értékét. 
  • A useEffect hook fogja "figyelni" a state változását, mivel a dependency array-be beadtuk ezt a változót (ha nem adunk meg semmit, akkor csak mount-kor fog lefutni).
  • A useEffect-ben definiált debounce változó tárolja a setTimeout függvény ID-jét, a useEffect visszatérési függvénye (cleanup function) törli ezt a timeout-ot ha elnavigálunk (un-mount), hogy ne fusson kettő párhuzamosan, ha újra ide navigálunk.
  • A setTimeout függvény pedig vár 0.45 másodpercet mielőtt kiírná a változónk tartalmát, ez felülíródik és újra kezdődik ha változik az input mező értéke (tart a gépelés).


Összegezzünk

A beviteli mezők, űrlapok kezelése nagyon komoly és összetett folyamat, amit nem szabad félvállról venni, ezért se említettem meg, hogy ez a legjobb megoldás. Azonban mielőtt nekiállnánk külső könyvtárakat használni, nagyvonalakban tudnunk kell, hogy működnek. Nekünk ehhez a projekthez nem fog kelleni külső segédeszköz bevonása.

A következő részben kicsit szépíteném az oldalt, "refaktorálok", megnéznénk a Tailwind előnyeit. Ha esetleg valakinek kérdése, meglátása, ötlete van a bejegyzéssel kapcsolatban, itt tud kapcsolatba lépni velünk: Kapcsolat

A bejegyzést lektorálta, a programkódokat kipróbálta és ellenőrizte: Gludovátz Attila (az ő Github projektje itt érhető el, ezen belül is blog-3 branch-et szerkesztettük ebben a bejegyzésben).

Előző rész: Deployment - 3. rész

Következő rész: Hamarosan