לדלג לתוכן

6.7 טפסים בריאקט פתרון

פתרון - טפסים בריאקט

פתרון תרגיל 1

import { useState } from "react";

interface LoginData {
    email: string;
    password: string;
    rememberMe: boolean;
}

interface LoginErrors {
    email?: string;
    password?: string;
}

function LoginForm() {
    const [form, setForm] = useState<LoginData>({
        email: "",
        password: "",
        rememberMe: false,
    });
    const [errors, setErrors] = useState<LoginErrors>({});

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const { name, value, type, checked } = e.target;
        setForm({
            ...form,
            [name]: type === "checkbox" ? checked : value,
        });
    };

    const validate = (): boolean => {
        const newErrors: LoginErrors = {};

        if (!form.email.includes("@")) {
            newErrors.email = "Email must contain @";
        }
        if (form.password.length < 6) {
            newErrors.password = "Password must be at least 6 characters";
        }

        setErrors(newErrors);
        return Object.keys(newErrors).length === 0;
    };

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        if (validate()) {
            console.log("Login:", form);
        }
    };

    const isDisabled = !form.email || !form.password;

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label>Email:</label>
                <input
                    name="email"
                    type="email"
                    value={form.email}
                    onChange={handleChange}
                />
                {errors.email && <p style={{ color: "red" }}>{errors.email}</p>}
            </div>
            <div>
                <label>Password:</label>
                <input
                    name="password"
                    type="password"
                    value={form.password}
                    onChange={handleChange}
                />
                {errors.password && <p style={{ color: "red" }}>{errors.password}</p>}
            </div>
            <label>
                <input
                    name="rememberMe"
                    type="checkbox"
                    checked={form.rememberMe}
                    onChange={handleChange}
                />
                Remember me
            </label>
            <button type="submit" disabled={isDisabled}>
                Login
            </button>
        </form>
    );
}

פתרון תרגיל 2

import { useState } from "react";

interface RegistrationData {
    fullName: string;
    email: string;
    password: string;
    confirmPassword: string;
    age: string;
    gender: string;
    interest: string;
    agreeToTerms: boolean;
}

interface RegistrationErrors {
    fullName?: string;
    email?: string;
    password?: string;
    confirmPassword?: string;
    age?: string;
    agreeToTerms?: string;
}

function RegistrationForm() {
    const [form, setForm] = useState<RegistrationData>({
        fullName: "",
        email: "",
        password: "",
        confirmPassword: "",
        age: "",
        gender: "male",
        interest: "technology",
        agreeToTerms: false,
    });
    const [errors, setErrors] = useState<RegistrationErrors>({});

    const handleChange = (
        e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
    ) => {
        const target = e.target;
        let value: string | boolean;

        if (target instanceof HTMLInputElement && target.type === "checkbox") {
            value = target.checked;
        } else {
            value = target.value;
        }

        setForm({ ...form, [target.name]: value });
    };

    const validate = (): boolean => {
        const newErrors: RegistrationErrors = {};

        if (form.fullName.trim().length < 2) {
            newErrors.fullName = "Name must be at least 2 characters";
        }
        if (!form.email.includes("@")) {
            newErrors.email = "Invalid email";
        }
        if (form.password.length < 8) {
            newErrors.password = "Password must be at least 8 characters";
        }
        if (form.confirmPassword !== form.password) {
            newErrors.confirmPassword = "Passwords do not match";
        }
        if (!form.age || Number(form.age) < 1 || Number(form.age) > 120) {
            newErrors.age = "Enter a valid age";
        }
        if (!form.agreeToTerms) {
            newErrors.agreeToTerms = "You must agree to the terms";
        }

        setErrors(newErrors);
        return Object.keys(newErrors).length === 0;
    };

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        if (validate()) {
            console.log("Registration:", form);
        }
    };

    return (
        <div>
            <form onSubmit={handleSubmit}>
                <div>
                    <input name="fullName" value={form.fullName} onChange={handleChange} placeholder="Full Name" />
                    {errors.fullName && <p style={{ color: "red" }}>{errors.fullName}</p>}
                </div>
                <div>
                    <input name="email" type="email" value={form.email} onChange={handleChange} placeholder="Email" />
                    {errors.email && <p style={{ color: "red" }}>{errors.email}</p>}
                </div>
                <div>
                    <input name="password" type="password" value={form.password} onChange={handleChange} placeholder="Password" />
                    {errors.password && <p style={{ color: "red" }}>{errors.password}</p>}
                </div>
                <div>
                    <input name="confirmPassword" type="password" value={form.confirmPassword} onChange={handleChange} placeholder="Confirm Password" />
                    {errors.confirmPassword && <p style={{ color: "red" }}>{errors.confirmPassword}</p>}
                </div>
                <div>
                    <input name="age" type="number" value={form.age} onChange={handleChange} placeholder="Age" />
                    {errors.age && <p style={{ color: "red" }}>{errors.age}</p>}
                </div>
                <div>
                    <label><input type="radio" name="gender" value="male" checked={form.gender === "male"} onChange={handleChange} /> Male</label>
                    <label><input type="radio" name="gender" value="female" checked={form.gender === "female"} onChange={handleChange} /> Female</label>
                    <label><input type="radio" name="gender" value="other" checked={form.gender === "other"} onChange={handleChange} /> Other</label>
                </div>
                <div>
                    <select name="interest" value={form.interest} onChange={handleChange}>
                        <option value="technology">Technology</option>
                        <option value="sports">Sports</option>
                        <option value="music">Music</option>
                        <option value="art">Art</option>
                    </select>
                </div>
                <div>
                    <label>
                        <input name="agreeToTerms" type="checkbox" checked={form.agreeToTerms} onChange={handleChange} />
                        I agree to the terms
                    </label>
                    {errors.agreeToTerms && <p style={{ color: "red" }}>{errors.agreeToTerms}</p>}
                </div>
                <button type="submit">Register</button>
            </form>

            <div style={{ marginTop: "20px", padding: "16px", border: "1px solid #ddd" }}>
                <h3>Preview:</h3>
                <p>Name: {form.fullName}</p>
                <p>Email: {form.email}</p>
                <p>Age: {form.age}</p>
                <p>Gender: {form.gender}</p>
                <p>Interest: {form.interest}</p>
                <p>Agreed to terms: {form.agreeToTerms ? "Yes" : "No"}</p>
            </div>
        </div>
    );
}

כל השדות מנוהלים עם handler אחד. ההבחנה בין checkbox לשאר השדות נעשית דרך target.type === "checkbox".

פתרון תרגיל 3

import { useState } from "react";

interface Skill {
    id: number;
    name: string;
    level: "beginner" | "intermediate" | "advanced";
}

function SkillsForm() {
    const [skills, setSkills] = useState<Skill[]>([]);
    const [nextId, setNextId] = useState(1);
    const [skillName, setSkillName] = useState("");
    const [skillLevel, setSkillLevel] = useState<Skill["level"]>("beginner");
    const [error, setError] = useState("");

    const handleAdd = () => {
        if (!skillName.trim()) {
            setError("Skill name cannot be empty");
            return;
        }
        if (skills.some((s) => s.name.toLowerCase() === skillName.trim().toLowerCase())) {
            setError("Skill already exists");
            return;
        }

        setSkills([...skills, { id: nextId, name: skillName.trim(), level: skillLevel }]);
        setNextId(nextId + 1);
        setSkillName("");
        setSkillLevel("beginner");
        setError("");
    };

    const handleRemove = (id: number) => {
        setSkills(skills.filter((s) => s.id !== id));
    };

    return (
        <div>
            <h2>Skills ({skills.length})</h2>
            <div style={{ marginBottom: "16px" }}>
                <input
                    type="text"
                    value={skillName}
                    onChange={(e) => setSkillName(e.target.value)}
                    placeholder="Skill name"
                />
                <select
                    value={skillLevel}
                    onChange={(e) => setSkillLevel(e.target.value as Skill["level"])}
                >
                    <option value="beginner">Beginner</option>
                    <option value="intermediate">Intermediate</option>
                    <option value="advanced">Advanced</option>
                </select>
                <button onClick={handleAdd}>Add</button>
                {error && <p style={{ color: "red" }}>{error}</p>}
            </div>
            <ul style={{ listStyle: "none", padding: 0 }}>
                {skills.map((skill) => (
                    <li
                        key={skill.id}
                        style={{
                            display: "flex",
                            justifyContent: "space-between",
                            padding: "8px",
                            border: "1px solid #ddd",
                            marginBottom: "4px",
                        }}
                    >
                        <span>
                            {skill.name} - {skill.level}
                        </span>
                        <button onClick={() => handleRemove(skill.id)}>Delete</button>
                    </li>
                ))}
            </ul>
        </div>
    );
}

פתרון תרגיל 4

import { useState } from "react";

interface ProfileData {
    name: string;
    email: string;
    bio: string;
    city: string;
}

const initialProfile: ProfileData = {
    name: "Alice Johnson",
    email: "alice@example.com",
    bio: "Full-stack developer with 5 years of experience.",
    city: "Tel Aviv",
};

const cities = ["Tel Aviv", "Jerusalem", "Haifa", "Beer Sheva", "Eilat"];

function ProfileEditor() {
    const [form, setForm] = useState<ProfileData>(initialProfile);
    const [savedProfile, setSavedProfile] = useState<ProfileData>(initialProfile);
    const [showSuccess, setShowSuccess] = useState(false);

    const handleChange = (
        e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
    ) => {
        setForm({ ...form, [e.target.name]: e.target.value });
        setShowSuccess(false);
    };

    const hasChanges =
        form.name !== savedProfile.name ||
        form.email !== savedProfile.email ||
        form.bio !== savedProfile.bio ||
        form.city !== savedProfile.city;

    const handleSave = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        setSavedProfile(form);
        setShowSuccess(true);
    };

    const handleCancel = () => {
        setForm(savedProfile);
        setShowSuccess(false);
    };

    return (
        <form onSubmit={handleSave}>
            <div>
                <label>Name:</label>
                <input name="name" value={form.name} onChange={handleChange} />
            </div>
            <div>
                <label>Email:</label>
                <input name="email" type="email" value={form.email} onChange={handleChange} />
            </div>
            <div>
                <label>Bio:</label>
                <textarea name="bio" value={form.bio} onChange={handleChange} rows={4} />
            </div>
            <div>
                <label>City:</label>
                <select name="city" value={form.city} onChange={handleChange}>
                    {cities.map((city) => (
                        <option key={city} value={city}>{city}</option>
                    ))}
                </select>
            </div>
            <button type="submit" disabled={!hasChanges}>Save</button>
            <button type="button" onClick={handleCancel} disabled={!hasChanges}>Cancel</button>
            {showSuccess && <p style={{ color: "green" }}>Changes saved!</p>}
        </form>
    );
}

כפתור "בטל" הוא type="button" כדי שלא יגרום לשליחת הטופס. ה-hasChanges מחושב על ידי השוואת כל שדה בטופס לערך השמור.

פתרון תרגיל 5

import { useState } from "react";

type QuestionType = "text" | "multiple-choice" | "yes-no";

interface Question {
    id: number;
    text: string;
    type: QuestionType;
    options: string[];
}

function SurveyBuilder() {
    const [title, setTitle] = useState("");
    const [questions, setQuestions] = useState<Question[]>([]);
    const [nextId, setNextId] = useState(1);
    const [showPreview, setShowPreview] = useState(false);

    const addQuestion = () => {
        setQuestions([
            ...questions,
            { id: nextId, text: "", type: "text", options: [] },
        ]);
        setNextId(nextId + 1);
    };

    const removeQuestion = (id: number) => {
        setQuestions(questions.filter((q) => q.id !== id));
    };

    const updateQuestion = (id: number, field: string, value: string) => {
        setQuestions(
            questions.map((q) => {
                if (q.id !== id) return q;
                if (field === "type") {
                    const newType = value as QuestionType;
                    return {
                        ...q,
                        type: newType,
                        options: newType === "multiple-choice" ? ["", ""] : [],
                    };
                }
                return { ...q, [field]: value };
            })
        );
    };

    const addOption = (questionId: number) => {
        setQuestions(
            questions.map((q) =>
                q.id === questionId ? { ...q, options: [...q.options, ""] } : q
            )
        );
    };

    const removeOption = (questionId: number, optionIndex: number) => {
        setQuestions(
            questions.map((q) =>
                q.id === questionId
                    ? { ...q, options: q.options.filter((_, i) => i !== optionIndex) }
                    : q
            )
        );
    };

    const updateOption = (questionId: number, optionIndex: number, value: string) => {
        setQuestions(
            questions.map((q) =>
                q.id === questionId
                    ? { ...q, options: q.options.map((opt, i) => (i === optionIndex ? value : opt)) }
                    : q
            )
        );
    };

    if (showPreview) {
        return (
            <div>
                <h2>{title || "Untitled Survey"}</h2>
                {questions.map((q, i) => (
                    <div key={q.id} style={{ marginBottom: "16px" }}>
                        <p><strong>{i + 1}. {q.text}</strong></p>
                        {q.type === "text" && <input type="text" placeholder="Your answer" disabled />}
                        {q.type === "yes-no" && (
                            <div>
                                <label><input type="radio" name={`q-${q.id}`} disabled /> Yes</label>
                                <label><input type="radio" name={`q-${q.id}`} disabled /> No</label>
                            </div>
                        )}
                        {q.type === "multiple-choice" && (
                            <div>
                                {q.options.map((opt, j) => (
                                    <div key={j}>
                                        <label><input type="radio" name={`q-${q.id}`} disabled /> {opt || "(empty)"}</label>
                                    </div>
                                ))}
                            </div>
                        )}
                    </div>
                ))}
                <button onClick={() => setShowPreview(false)}>Back to Editor</button>
            </div>
        );
    }

    return (
        <div>
            <h2>Survey Builder</h2>
            <input
                type="text"
                value={title}
                onChange={(e) => setTitle(e.target.value)}
                placeholder="Survey Title"
                style={{ fontSize: "18px", marginBottom: "16px", width: "100%" }}
            />
            {questions.map((q, i) => (
                <div key={q.id} style={{ border: "1px solid #ddd", padding: "16px", marginBottom: "12px", borderRadius: "4px" }}>
                    <div style={{ display: "flex", justifyContent: "space-between" }}>
                        <strong>Question {i + 1}</strong>
                        <button onClick={() => removeQuestion(q.id)}>Delete</button>
                    </div>
                    <input
                        type="text"
                        value={q.text}
                        onChange={(e) => updateQuestion(q.id, "text", e.target.value)}
                        placeholder="Question text"
                        style={{ width: "100%", marginBottom: "8px" }}
                    />
                    <select
                        value={q.type}
                        onChange={(e) => updateQuestion(q.id, "type", e.target.value)}
                    >
                        <option value="text">Text</option>
                        <option value="multiple-choice">Multiple Choice</option>
                        <option value="yes-no">Yes / No</option>
                    </select>
                    {q.type === "multiple-choice" && (
                        <div style={{ marginTop: "8px" }}>
                            {q.options.map((opt, j) => (
                                <div key={j} style={{ display: "flex", gap: "8px", marginBottom: "4px" }}>
                                    <input
                                        type="text"
                                        value={opt}
                                        onChange={(e) => updateOption(q.id, j, e.target.value)}
                                        placeholder={`Option ${j + 1}`}
                                    />
                                    <button onClick={() => removeOption(q.id, j)}>X</button>
                                </div>
                            ))}
                            <button onClick={() => addOption(q.id)}>Add Option</button>
                        </div>
                    )}
                </div>
            ))}
            <button onClick={addQuestion}>Add Question</button>
            <button onClick={() => setShowPreview(true)} style={{ marginLeft: "8px" }}>
                Preview
            </button>
        </div>
    );
}

זה תרגיל מורכב שמשלב ניהול סטייט מקונן (מערך של שאלות, כל שאלה עם מערך אופציות), טפסים דינמיים (הוספה ומחיקה של שדות), ורינדור מותנה (switch בין עורך ותצוגה מקדימה).

תשובות לשאלות

  1. ב-controlled components, ריאקט שולטת בערך השדה דרך סטייט (value + onChange). ב-uncontrolled, ה-DOM שולט בערך ומשתמשים ב-ref לגישה. controlled מומלץ ברוב המקרים כי מאפשר ולידציה בזמן אמת ושליטה מלאה. uncontrolled מתאים לשדות פשוטים כמו file input, או כשרוצים ביצועים מקסימליים עם טפסים ענקיים.

  2. בלי preventDefault, הדפדפן שולח את הטופס לשרת ומרענן את הדף. בריאקט אנחנו רוצים לטפל בשליחה ב-JavaScript בלי רענון, לכן חייבים למנוע את ההתנהגות ברירת המחדל.

  3. כל input מקבל אטריביוט name שתואם לשדה בסטייט. ב-handler, קוראים e.target.name ו-e.target.value, ומעדכנים עם computed property: setForm({ ...form, [name]: value }). זה שקול לכתיבת handler נפרד לכל שדה, אבל בקוד אחד.

  4. checkbox לא משתמש ב-value לציון המצב שלו - ה-value הוא הערך שנשלח בטופס (מחרוזת). המצב האמיתי (מסומן או לא) נמצא ב-checked, שהוא בוליאני.