Wracamy do formy!
W programowaniu zawsze warto się nie poddawać. Po serii porażek i stania w miejscu przychodzi moment kiedy znajdujesz rozwiązanie i wszystko wraca na właściwy tor.
Dokładnie tak miałem po kilku dniach walczenia z formularzami, o czym możecie przeczytać w poprzednich wpisach. Teraz mogę powiedzieć tylko, że bitwę chwilowo wygrywam ja – co miało działać, działa.
Poprzednie wpisy z serii:
- Wstęp
- Kilka słów o projekcie
- Pierwsza konfiguracja środowiska
- Krótko o początkach z Reactem
- Bardzo brzydkie style
- Uruchomienie Reduxa i Routera
- Jednak Bootstrap
- Poprawki i nowości
- Początek koszmaru – formularze
- Gorzkie żale związane z walidacją
- Pierwsze, nienajlepsze spotkanie z biblioteką redux-form
Tytuł tego posta jest dwuznaczny. Z jednej strony powrót do formy odnosi się do ponownego wejścia w rytm kodowania, którego nie miałem w ostatnim czasie. Z drugiej jednak strony formularze to była ostatnio rzecz, przy której miałem ochotę się poddać i to one są dzisiaj tematem wpisu.
Mamy to!
Po męczarniach z próbami dodania walidacji do formularza logowania w końcu to mamy! Nie jest idealnie i na razie są to raczej podwaliny pod porządną walidację, ale działa. W przyszłości dojdzie jeszcze walidacja po stronie serwera więc formularz będzie np. jeszcze sprawdzał czy użytkownik w ogóle istnieje.
Chyba nigdy wcześniej pojawienie się błędu mówiącego o pustym polu nie było tak satysfakcjonujące :D
Co się zmieniło po stronie kodu?
Login.js
W pliku Login.js zacząłem korzystać z komponentu biblioteki redux-form.
const Login = React.createClass({ render: function() { return ( <Row> <Col smOffset={2} mdOffset={3} lgOffset={4} md={6} sm={8} lg={4}> <Form horizontal onSubmit={this.props.handleSubmit(this.props.loginUser)} method="post"> <Field name="email" type="text" component={FormInput} label="Email" /> <Field name="password" type="password" component={FormInput} label="Password" /> <Col smOffset={2} sm={10}> <Field name="rememberMe" label="Remember me" component={FormCheckbox} /> </Col> <Col smOffset={2} sm={10}> <Button type='submit'> Sign in </Button> </Col> </Form> </Col> </Row> ) } }) export default reduxForm({ form: 'login-form' })(Login)
Ostatnie 3 linijki są potrzebne żeby biblioteka mogła zarządzać wartościami z formularza. Wartość form zawiera unikalną nazwę tego formularza. Jest to potrzebne ponieważ biblioteka redux-form korzysta z reduxa, a jak wiadomo reduxowy store jest globalny w aplikacji.
Komponent Field pochodzi z biblioteki redux-form i obudowuje inputy. Wyświetlanie samych inputów przeniosłem do osobnego pliku. Tutaj korzystam już z gotowych komponentów.
render-fields.js
W tym pliku mam teraz komponenty wyświetlające bootstrapowe inputy. Z tymi komponentami był właśnie największy problem kiedy pierwszy raz próbowałem korzystać z biblioteki redux-form.
import React from 'react' import {FormGroup, ControlLabel, FormControl, Checkbox, HelpBlock} from 'react-bootstrap' const FormInput = ({input, label, type, meta: {touched, error}}) => ( <FormGroup validationState={error ? "error" : null}> <ControlLabel>{label}</ControlLabel> <FormControl type={type} {...input} placeholder={label} /> {touched && error && <HelpBlock>{error}</HelpBlock>} </FormGroup> ) const FormTextarea = ({input, label, meta: {touched, error}}) => ( <FormGroup validationState={error ? "error" : null}> <ControlLabel>{label}</ControlLabel> <FormControl {...input} componentClass="textarea" placeholder={label} /> {touched && error && <HelpBlock>{error}</HelpBlock>} </FormGroup> ) const FormCheckbox = ({input, label, type, meta: {touched, error}}) => ( <div> <ControlLabel>{label}</ControlLabel> <Checkbox {...input} placeholder={label} /> {touched && error && <HelpBlock>{error}</HelpBlock>} </div> ) export {FormInput, FormTextarea, FormCheckbox}
Parametr touched informuje o tym czy pole było „dotknięte” czyli czy użytkownik miał na nim focus. Pole error przechowuje treść błędu dla danego pola. Sprawdzanie tych dwóch wartości pozwala określić czy powinniśmy wyświetlić błąd.
W tym momencie mam komponenty dla 3 różnych inputów, ale w przyszłości pewnie pojawią się kolejne.
login-container.js
Kontener dla naszego widoku. Perełka ostatnich dni :D Tutaj odbywa się walidacja formularza (druga część pliku). Regex do sprawdzania poprawności adresu email wziąłem ze StackOverflow i wygląda strasznie. Ale działa.
Promise w metodzie loginUser pochodzi z przykładu do biblioteki redux-form i po podłączeniu serwera będzie usunięty. Metoda then() raczej jest oczywista – wykonuje się kiedy Promise zadziała. Metoda catch() wywoływana jest kiedy wystąpi wyjątek, który rzucam w metodzie walidującej formularz. Normalnie biblioteka sama to ogarnia, ale ja dodatkowo potrzebowałem odblokować w tym momencie widok.
import React from 'react' import Login from './login' import store from '../../../store' import { SubmissionError} from 'redux-form' import { browserHistory } from 'react-router' import * as userActions from '../../../actions/user/user-actions' import * as commonActions from '../../../actions/common/common-actions' const LoginContainer = React.createClass({ render: function() { return ( <Login loginUser={this.loginUser} /> ) }, loginUser: function(values) { const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) store.dispatch(commonActions.blockUi()) return sleep(1000) .then(() => { this.validate(values) store.dispatch(userActions.loginUserSuccess({ firstName: 'Marek', lastName: 'Zając', rememberMe: values.rememberMe })) store.dispatch(commonActions.unblockUi()) browserHistory.push('/') }) .catch(error => { this.setState({ error: error }) store.dispatch(commonActions.unblockUi()) return Promise.reject(error) }) }, validate: function(values) { return this.validateEmail(values) && this.validatePassword(values) }, validateEmail: function(values) { var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ if(values.email == '' || values.email == null){ throw new SubmissionError({email: 'Email can\'t be empty'}) } else if(!re.test(values.email)){ throw new SubmissionError({email: 'Email is not valid'}) } }, validatePassword: function(values) { if(values.password == '' || values.password == null){ throw new SubmissionError({password: 'Password can\'t be empty'}) } } }) export default LoginContainer
Co było nie tak z biblioteką redux-form?
W poprzednim wpisie mówiłem m.in. o tym, że nie działają przykłady podane na stronie tej biblioteki. W tym momencie powiem, że warto przeglądać w takich momentach GitHub danej biblioteki. Wiecie co się okazało? Że na stronie w przykładzie kod formularza wygląda tak:
import React from 'react' import { Field, reduxForm } from 'redux-form' const { DOM: { input } } = React const SubmitValidationForm = (props) => { const { error, handleSubmit, pristine, reset, submitting } = props return ( <form onSubmit={handleSubmit}> <div> <label>Username</label> <Field name="username" component={username => <div> <input type="text" {...username} placeholder="Username"/> {username.touched && username.error && <span>{username.error}</span>} </div> }/> </div> <div> <label>Password</label> <Field name="password" component={password => <div> <input type="password" {...password} placeholder="Password"/> {password.touched && password.error && <span>{password.error}</span>} </div> }/> </div> {error && <strong>{error}</strong>} <div> <button type="submit" disabled={submitting}>Log In</button> <button type="button" disabled={pristine || submitting} onClick={reset}>Clear Values</button> </div> </form> ) } export default reduxForm({ form: 'submitValidation' // a unique identifier for this form })(SubmitValidationForm)
A na GitHubie już ten sam formularz jest generowany w ten sposób:
import React from 'react' import { Field, reduxForm } from 'redux-form' import submit from './submit' const renderField = ({ input, label, type, meta: { touched, error } }) => ( <div> <label>{label}</label> <div> <input {...input} placeholder={label} type={type}/> {touched && error && <span>{error}</span>} </div> </div> ) const SubmitValidationForm = (props) => { const { error, handleSubmit, pristine, reset, submitting } = props return ( <form onSubmit={handleSubmit(submit)}> <Field name="username" type="text" component={renderField} label="Username"/> <Field name="password" type="password" component={renderField} label="Password"/> {error && <strong>{error}</strong>} <div> <button type="submit" disabled={submitting}>Log In</button> <button type="button" disabled={pristine || submitting} onClick={reset}>Clear Values</button> </div> </form> ) } export default reduxForm({ form: 'submitValidation' // a unique identifier for this form })(SubmitValidationForm)
Widzicie różnicę? W pierwszym input jest wstawiony bezpośrednio w komponencie. W drugim wykorzystana jest osobna metoda, którą widzieliście też u mnie.
Pierwszy za cholerę nie działał. Drugi działa. Nie wiem z czego to wynika, ale spodziewałbym się, że jeśli ktoś pokazuje mi jak mam użyć jego kodu to robi to tak, że nie trzeba się przekopywać przez repozytorium.
Dziękuję dobranoc.
Dlatego nie lubię tak dynamicznych dziedzin jak aplikacje w JS bo twórcy bibliotek nie starają się zachować jakości materiałów, których z powodu dynamiki zmian i tak nie ma za wiele.
Biegniemy dalej
Tak jak pani na obrazku rozpoczynającym wpis tak i my biegniemy dalej po programistycznych schodach.
Po tym jak zadziałała mi pierwsza walidacja postanowiłem zabrać się za najważniejsze funkcjonalności aplikacji czyli tworzenie opowiadań. Mam nadzieję, że po świętach będzie więcej mięska do pokazania ;)
Jeśli chcesz śledzić mój brzydki kod to odwiedź GitHuba.
Żeby nie przegapić żadnych newsów i śledzić na bieżąco postępy polub też mój fanpage na Facebooku.
Leave a Comment