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 בין עורך ותצוגה מקדימה).
תשובות לשאלות¶
-
ב-controlled components, ריאקט שולטת בערך השדה דרך סטייט (
value+onChange). ב-uncontrolled, ה-DOM שולט בערך ומשתמשים ב-refלגישה. controlled מומלץ ברוב המקרים כי מאפשר ולידציה בזמן אמת ושליטה מלאה. uncontrolled מתאים לשדות פשוטים כמו file input, או כשרוצים ביצועים מקסימליים עם טפסים ענקיים. -
בלי
preventDefault, הדפדפן שולח את הטופס לשרת ומרענן את הדף. בריאקט אנחנו רוצים לטפל בשליחה ב-JavaScript בלי רענון, לכן חייבים למנוע את ההתנהגות ברירת המחדל. -
כל input מקבל אטריביוט
nameשתואם לשדה בסטייט. ב-handler, קוראיםe.target.nameו-e.target.value, ומעדכנים עם computed property:setForm({ ...form, [name]: value }). זה שקול לכתיבת handler נפרד לכל שדה, אבל בקוד אחד. -
checkbox לא משתמש ב-
valueלציון המצב שלו - ה-value הוא הערך שנשלח בטופס (מחרוזת). המצב האמיתי (מסומן או לא) נמצא ב-checked, שהוא בוליאני.