src/profile-form.tsxTypeScript
@@ -0,0 +1,79 @@
1+
import { useState, type FormEvent } from "react";2+
3+
interface Profile {4+
displayName: string;5+
email: string;6+
}7+
8+
interface ProfileFormProps {9+
initialProfile: Profile;10+
onSave(profile: Profile): void;11+
}12+
13+
type Errors = Partial<Record<keyof Profile, string>>;14+
15+
export function ProfileForm({ initialProfile, onSave }: ProfileFormProps) {16+
const [profile, setProfile] = useState(initialProfile);17+
const [errors, setErrors] = useState<Errors>({});18+
19+
function handleSubmit(event: FormEvent<HTMLFormElement>) {20+
event.preventDefault();21+
const nextErrors: Errors = {};22+
23+
if (!profile.displayName.trim()) {24+
nextErrors.displayName = "Enter a display name";25+
}26+
if (!profile.email.includes("@")) {27+
nextErrors.email = "Enter a valid email address";28+
}29+
30+
setErrors(nextErrors);31+
if (Object.keys(nextErrors).length === 0) {32+
onSave(profile);33+
}34+
}35+
36+
const entries = Object.entries(errors) as [keyof Profile, string][];37+
38+
return (39+
<form noValidate onSubmit={handleSubmit}>40+
{entries.length > 0 ? (41+
<div id="error-summary">42+
<h2>There is a problem</h2>43+
<ul>44+
{entries.map(([field, message]) => (45+
<li key={field}>46+
<a href="#">{message}</a>47+
</li>48+
))}49+
</ul>50+
</div>51+
) : null}52+
53+
<label htmlFor="display-name">Display name</label>54+
<input55+
id="display-name"56+
value={profile.displayName}57+
aria-invalid={false}58+
aria-describedby="error-summary"59+
onChange={(event) =>60+
setProfile((current) => ({ ...current, displayName: event.target.value }))61+
}62+
/>63+
64+
<label htmlFor="email">Email</label>65+
<input66+
id="email"67+
type="email"68+
value={profile.email}69+
aria-invalid={false}70+
aria-describedby="error-summary"71+
onChange={(event) =>72+
setProfile((current) => ({ ...current, email: event.target.value }))73+
}74+
/>75+
76+
<button type="submit">Save profile</button>77+
</form>78+
);79+
}