Kurs React Native · Moduł 5: Formularze

Projekt: Formularz rejestracji Atlantydy

3 min czytania ·

Połączmy wszystko w kompletny formularz rejestracji!

Kompletny formularz

import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import {
  View,
  Text,
  TextInput,
  Switch,
  Pressable,
  ScrollView,
  KeyboardAvoidingView,
  Platform,
  ActivityIndicator,
  Alert,
} from 'react-native';

// Schema
const registrationSchema = z.object({
  name: z.string().min(2, 'Min. 2 znaki').max(50),
  email: z.string().email('Nieprawidłowy email'),
  password: z
    .string()
    .min(8, 'Min. 8 znaków')
    .regex(/[A-Z]/, 'Wymagana wielka litera')
    .regex(/[0-9]/, 'Wymagana cyfra'),
  confirmPassword: z.string(),
  birthDate: z.date().refine(
    (date) => {
      const age = (Date.now() - date.getTime()) / (365.25 * 24 * 60 * 60 * 1000);
      return age >= 18;
    },
    { message: 'Musisz mieć min. 18 lat' }
  ),
  newsletter: z.boolean(),
  terms: z.literal(true, {
    errorMap: () => ({ message: 'Musisz zaakceptować regulamin' }),
  }),
}).refine((data) => data.password === data.confirmPassword, {
  message: 'Hasła muszą być identyczne',
  path: ['confirmPassword'],
});

type FormData = z.infer<typeof registrationSchema>;

export function RegistrationForm() {
  const {
    control,
    handleSubmit,
    formState: { errors, isSubmitting },
    reset,
  } = useForm<FormData>({
    resolver: zodResolver(registrationSchema),
    defaultValues: {
      name: '',
      email: '',
      password: '',
      confirmPassword: '',
      birthDate: new Date(),
      newsletter: true,
      terms: false,
    },
  });

  const onSubmit = async (data: FormData) => {
    try {
      // Symulacja API call
      await new Promise((resolve) => setTimeout(resolve, 2000));

      Alert.alert(
        'Sukces!',
        `Witaj w Atlantydzie, ${data.name}!`,
        [{ text: 'OK', onPress: () => reset() }]
      );
    } catch (error) {
      Alert.alert('Błąd', 'Nie udało się zarejestrować');
    }
  };

  return (
    <KeyboardAvoidingView
      style={styles.container}
      behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
    >
      <ScrollView contentContainerStyle={styles.scroll}>
        <Text style={styles.title}>Rejestracja w Atlantydzie</Text>

        {/* Imię */}
        <View style={styles.field}>
          <Text style={styles.label}>Imię</Text>
          <Controller
            control={control}
            name="name"
            render={({ field: { onChange, onBlur, value } }) => (
              <TextInput
                style={[styles.input, errors.name && styles.inputError]}
                placeholder="Twoje imię"
                onBlur={onBlur}
                onChangeText={onChange}
                value={value}
              />
            )}
          />
          {errors.name && (
            <Text style={styles.error}>{errors.name.message}</Text>
          )}
        </View>

        {/* Email */}
        <View style={styles.field}>
          <Text style={styles.label}>Email</Text>
          <Controller
            control={control}
            name="email"
            render={({ field: { onChange, onBlur, value } }) => (
              <TextInput
                style={[styles.input, errors.email && styles.inputError]}
                placeholder="email@atlantyda.sea"
                keyboardType="email-address"
                autoCapitalize="none"
                onBlur={onBlur}
                onChangeText={onChange}
                value={value}
              />
            )}
          />
          {errors.email && (
            <Text style={styles.error}>{errors.email.message}</Text>
          )}
        </View>

        {/* Hasło */}
        <View style={styles.field}>
          <Text style={styles.label}>Hasło</Text>
          <Controller
            control={control}
            name="password"
            render={({ field: { onChange, onBlur, value } }) => (
              <TextInput
                style={[styles.input, errors.password && styles.inputError]}
                placeholder="Min. 8 znaków"
                secureTextEntry
                onBlur={onBlur}
                onChangeText={onChange}
                value={value}
              />
            )}
          />
          {errors.password && (
            <Text style={styles.error}>{errors.password.message}</Text>
          )}
        </View>

        {/* Potwierdź hasło */}
        <View style={styles.field}>
          <Text style={styles.label}>Potwierdź hasło</Text>
          <Controller
            control={control}
            name="confirmPassword"
            render={({ field: { onChange, onBlur, value } }) => (
              <TextInput
                style={[styles.input, errors.confirmPassword && styles.inputError]}
                placeholder="Powtórz hasło"
                secureTextEntry
                onBlur={onBlur}
                onChangeText={onChange}
                value={value}
              />
            )}
          />
          {errors.confirmPassword && (
            <Text style={styles.error}>{errors.confirmPassword.message}</Text>
          )}
        </View>

        {/* Newsletter */}
        <Controller
          control={control}
          name="newsletter"
          render={({ field: { onChange, value } }) => (
            <View style={styles.switchRow}>
              <Text>Newsletter Atlantydy</Text>
              <Switch value={value} onValueChange={onChange} />
            </View>
          )}
        />

        {/* Regulamin */}
        <Controller
          control={control}
          name="terms"
          render={({ field: { onChange, value } }) => (
            <View>
              <View style={styles.switchRow}>
                <Text>Akceptuję regulamin</Text>
                <Switch value={value} onValueChange={onChange} />
              </View>
              {errors.terms && (
                <Text style={styles.error}>{errors.terms.message}</Text>
              )}
            </View>
          )}
        />

        {/* Submit */}
        <Pressable
          style={[styles.button, isSubmitting && styles.buttonDisabled]}
          onPress={handleSubmit(onSubmit)}
          disabled={isSubmitting}
        >
          {isSubmitting ? (
            <ActivityIndicator color="#fff" />
          ) : (
            <Text style={styles.buttonText}>Zarejestruj się</Text>
          )}
        </Pressable>
      </ScrollView>
    </KeyboardAvoidingView>
  );
}

Ten projekt łączy wszystkie poznane techniki: TextInput, KeyboardAvoidingView, react-hook-form, walidację z Zod, Switch i stylowanie!

Kod do tej lekcji: App.tsx
import React from 'react';
import { View, Text, TextInput, Switch, Pressable, StyleSheet } from 'react-native';
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  name: z.string().min(2, 'Min. 2 znaki'),
  email: z.string().email('Nieprawidłowy email'),
  newsletter: z.boolean(),
});

type FormData = z.infer<typeof schema>;

export default function App() {
  const {
    control,
    handleSubmit,
    formState: { errors },
  } = useForm<FormData>({
    // TODO: podłącz walidację schematu przez resolver: zodResolver(schema)
    defaultValues: { name: '', email: '', newsletter: true },
  });

  const onSubmit = (data: FormData) => console.log(data);

  return (
    <View style={styles.container}>
      {/* TODO: dodaj <Controller name="name"> z <TextInput> (onChange, onBlur, value) i placeholderem "Imię" */}

      {/* TODO: dodaj <Controller name="email"> z <TextInput> (keyboardType="email-address") i placeholderem "Email" */}

      <Controller
        control={control}
        name="newsletter"
        render={({ field: { onChange, value } }) => (
          <View style={styles.row}>
            <Text style={styles.label}>Newsletter Atlantydy</Text>
            {/* TODO: dodaj <Switch value={value} onValueChange={onChange} /> */}
          </View>
        )}
      />

      <Pressable style={styles.button} onPress={handleSubmit(onSubmit)}>
        <Text style={styles.buttonText}>Zarejestruj się</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    justifyContent: 'center',
    backgroundColor: '#1a5276',
  },
  input: {
    backgroundColor: '#fff',
    borderRadius: 8,
    padding: 14,
    fontSize: 16,
    color: '#1a5276',
    marginBottom: 8,
  },
  row: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginVertical: 12,
  },
  label: {
    color: '#fff',
    fontSize: 16,
  },
  error: {
    color: '#e74c3c',
    marginBottom: 12,
  },
  button: {
    backgroundColor: '#2980b9',
    borderRadius: 8,
    padding: 14,
    alignItems: 'center',
    marginTop: 12,
  },
  buttonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
});

Hasła z dokumentacji w tej lekcji

Widzisz błąd w tej lekcji?

Zadania praktyczne w grze

  • Edytor kodu

    Uzupełnij kod łączący wszystkie poznane techniki.

Przydatne artykuły