8.5 מנטין הוקים הרצאה
מנטין הוקים - Mantine Hooks¶
בשיעור זה נלמד את ההוקים השימושיים שמנטין מציעה. הוקים אלו חוסכים כתיבת קוד חוזר ומספקים פונקציונליות נפוצה באופן אלגנטי.
useDisclosure - ניהול פתוח/סגור¶
ההוק הנפוץ ביותר - מנהל מצב בוליאני (פתוח/סגור) עם פעולות open, close, toggle.
import { useDisclosure } from "@mantine/hooks";
import { Modal, Drawer, Button, Group, Stack, Text } from "@mantine/core";
function DisclosureExample() {
const [modalOpened, modalHandlers] = useDisclosure(false);
const [drawerOpened, drawerHandlers] = useDisclosure(false);
const [expanded, { toggle }] = useDisclosure(false);
return (
<>
<Group>
<Button onClick={modalHandlers.open}>פתח מודאל</Button>
<Button onClick={drawerHandlers.open}>פתח מגירה</Button>
<Button onClick={toggle}>
{expanded ? "הסתר" : "הצג"} פרטים
</Button>
</Group>
{expanded && (
<Text mt="md">פרטים נוספים שמוצגים כשלוחצים על הכפתור</Text>
)}
<Modal opened={modalOpened} onClose={modalHandlers.close} title="מודאל">
<Text>תוכן המודאל</Text>
</Modal>
<Drawer opened={drawerOpened} onClose={drawerHandlers.close} title="מגירה">
<Text>תוכן המגירה</Text>
</Drawer>
</>
);
}
- מחזיר מערך:
[value, { open, close, toggle }] - הפרמטר הראשון הוא הערך ההתחלתי (false כברירת מחדל)
- ניתן להעביר callbacks שיופעלו ב-open ו-close
const [opened, { open, close }] = useDisclosure(false, {
onOpen: () => console.log("נפתח"),
onClose: () => console.log("נסגר"),
});
useForm - ניהול טפסים¶
הוק חזק לניהול טפסים עם ולידציה:
import { useForm } from "@mantine/form";
import { TextInput, NumberInput, Select, Button, Stack, Paper, Title } from "@mantine/core";
interface FormValues {
name: string;
email: string;
age: number | "";
role: string;
}
function RegistrationForm() {
const form = useForm<FormValues>({
mode: "uncontrolled",
initialValues: {
name: "",
email: "",
age: "",
role: "",
},
validate: {
name: (value) =>
value.trim().length < 2 ? "השם חייב להכיל לפחות 2 תווים" : null,
email: (value) =>
/^\S+@\S+$/.test(value) ? null : "כתובת אימייל לא תקינה",
age: (value) =>
value === "" ? "גיל הוא שדה חובה" :
value < 13 ? "הגיל המינימלי הוא 13" : null,
role: (value) =>
value ? null : "יש לבחור תפקיד",
},
});
function handleSubmit(values: FormValues) {
console.log("נשלח:", values);
}
return (
<Paper shadow="sm" p="xl" withBorder maw={450} mx="auto">
<Title order={3} mb="lg">הרשמה</Title>
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<TextInput
label="שם מלא"
placeholder="הכנס שם"
required
key={form.key("name")}
{...form.getInputProps("name")}
/>
<TextInput
label="אימייל"
placeholder="your@email.com"
required
key={form.key("email")}
{...form.getInputProps("email")}
/>
<NumberInput
label="גיל"
placeholder="הכנס גיל"
min={0}
max={120}
required
key={form.key("age")}
{...form.getInputProps("age")}
/>
<Select
label="תפקיד"
placeholder="בחר תפקיד"
data={["מפתח", "מעצב", "מנהל פרויקט", "QA"]}
required
key={form.key("role")}
{...form.getInputProps("role")}
/>
<Button type="submit" fullWidth mt="md">
שלח
</Button>
</Stack>
</form>
</Paper>
);
}
פיצ'רים מתקדמים של useForm¶
const form = useForm({
mode: "uncontrolled",
initialValues: {
name: "",
email: "",
terms: false,
},
validate: {
name: (value) => (value.length < 2 ? "קצר מדי" : null),
email: (value) => (/^\S+@\S+$/.test(value) ? null : "אימייל לא תקין"),
terms: (value) => (value ? null : "חובה לאשר תנאים"),
},
validateInputOnBlur: true,
validateInputOnChange: true,
});
// פעולות שימושיות
form.setFieldValue("name", "יוסי");
form.reset(); // איפוס לערכים ההתחלתיים
form.validate(); // הפעלת ולידציה ידנית
form.isValid(); // בדיקה אם הטופס תקין
form.isDirty(); // בדיקה אם הטופס השתנה
form.setFieldError("email", "כתובת תפוסה");
// ולידציה מבוססת ערכים אחרים
const formWithCross = useForm({
mode: "uncontrolled",
initialValues: {
password: "",
confirmPassword: "",
},
validate: {
confirmPassword: (value, values) =>
value !== values.password ? "הסיסמאות לא תואמות" : null,
},
});
ניהול רשימות בטופס¶
import { useForm } from "@mantine/form";
import { TextInput, Button, Group, Stack, ActionIcon } from "@mantine/core";
function DynamicForm() {
const form = useForm({
mode: "uncontrolled",
initialValues: {
employees: [{ name: "", email: "" }],
},
});
return (
<form>
<Stack>
{form.getValues().employees.map((_, index) => (
<Group key={form.key(`employees.${index}`)}>
<TextInput
placeholder="שם"
{...form.getInputProps(`employees.${index}.name`)}
style={{ flex: 1 }}
/>
<TextInput
placeholder="אימייל"
{...form.getInputProps(`employees.${index}.email`)}
style={{ flex: 1 }}
/>
<ActionIcon
color="red"
onClick={() => form.removeListItem("employees", index)}
>
X
</ActionIcon>
</Group>
))}
<Button
variant="light"
onClick={() =>
form.insertListItem("employees", { name: "", email: "" })
}
>
הוסף עובד
</Button>
</Stack>
</form>
);
}
useMediaQuery - זיהוי גודל מסך¶
import { useMediaQuery } from "@mantine/hooks";
import { Text, Stack } from "@mantine/core";
function MediaQueryExample() {
const isMobile = useMediaQuery("(max-width: 768px)");
const isTablet = useMediaQuery("(min-width: 769px) and (max-width: 1024px)");
const isDesktop = useMediaQuery("(min-width: 1025px)");
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
return (
<Stack>
<Text>
סוג מכשיר: {isMobile ? "טלפון" : isTablet ? "טאבלט" : "דסקטופ"}
</Text>
{isMobile ? (
<Text>תצוגת טלפון - תפריט המבורגר</Text>
) : (
<Text>תצוגת דסקטופ - תפריט מלא</Text>
)}
{prefersReducedMotion && (
<Text>המשתמש מעדיף תנועה מופחתת - מבטלים אנימציות</Text>
)}
</Stack>
);
}
useViewportSize - גודל חלון¶
import { useViewportSize } from "@mantine/hooks";
import { Text } from "@mantine/core";
function ViewportExample() {
const { height, width } = useViewportSize();
return (
<Text>
גודל חלון: {width}x{height} פיקסלים
</Text>
);
}
useClipboard - העתקה ללוח¶
import { useClipboard } from "@mantine/hooks";
import { Button, TextInput, Group, Tooltip } from "@mantine/core";
function ClipboardExample() {
const clipboard = useClipboard({ timeout: 2000 });
return (
<Group>
<TextInput
value="https://example.com/share/abc123"
readOnly
style={{ flex: 1 }}
/>
<Tooltip label={clipboard.copied ? "הועתק!" : "העתק"}>
<Button
color={clipboard.copied ? "green" : "blue"}
onClick={() => clipboard.copy("https://example.com/share/abc123")}
>
{clipboard.copied ? "הועתק" : "העתק"}
</Button>
</Tooltip>
</Group>
);
}
- clipboard.copy(text) - מעתיק טקסט ללוח
- clipboard.copied - האם הועתק לאחרונה (מתאפס אחרי timeout)
- clipboard.error - האם היתה שגיאה
useLocalStorage - אחסון מקומי¶
import { useLocalStorage } from "@mantine/hooks";
import { TextInput, Switch, Select, Stack, Text, Paper, Button } from "@mantine/core";
function LocalStorageExample() {
const [name, setName] = useLocalStorage({
key: "user-name",
defaultValue: "",
});
const [darkMode, setDarkMode] = useLocalStorage({
key: "dark-mode",
defaultValue: false,
});
const [language, setLanguage] = useLocalStorage({
key: "language",
defaultValue: "he",
});
return (
<Paper p="lg" shadow="sm" withBorder maw={400}>
<Stack>
<Text fw={500}>הגדרות (נשמרות ב-localStorage)</Text>
<TextInput
label="שם"
value={name}
onChange={(e) => setName(e.currentTarget.value)}
/>
<Switch
label="מצב כהה"
checked={darkMode}
onChange={(e) => setDarkMode(e.currentTarget.checked)}
/>
<Select
label="שפה"
value={language}
onChange={(val) => setLanguage(val || "he")}
data={[
{ value: "he", label: "עברית" },
{ value: "en", label: "English" },
]}
/>
<Text size="sm" c="dimmed">
הערכים נשמרים אוטומטית ויישארו גם אחרי רענון הדף
</Text>
</Stack>
</Paper>
);
}
useHotkeys - קיצורי מקלדת¶
import { useHotkeys } from "@mantine/hooks";
import { notifications } from "@mantine/notifications";
import { Text, Paper, Stack, Kbd, Group } from "@mantine/core";
function HotkeysExample() {
useHotkeys([
["mod+s", () => {
console.log("שמירה");
// notifications.show({ message: "נשמר בהצלחה" });
}],
["mod+k", () => {
console.log("פתיחת חיפוש");
}],
["mod+shift+d", () => {
console.log("מצב כהה");
}],
["escape", () => {
console.log("ביטול");
}],
]);
return (
<Paper p="lg" shadow="sm" withBorder maw={400}>
<Stack>
<Text fw={500}>קיצורי מקלדת זמינים:</Text>
<Group>
<Kbd>Ctrl</Kbd> + <Kbd>S</Kbd>
<Text size="sm">שמירה</Text>
</Group>
<Group>
<Kbd>Ctrl</Kbd> + <Kbd>K</Kbd>
<Text size="sm">חיפוש</Text>
</Group>
<Group>
<Kbd>Ctrl</Kbd> + <Kbd>Shift</Kbd> + <Kbd>D</Kbd>
<Text size="sm">מצב כהה</Text>
</Group>
<Group>
<Kbd>Esc</Kbd>
<Text size="sm">ביטול</Text>
</Group>
</Stack>
</Paper>
);
}
- mod מתורגם ל-Ctrl בווינדוס ו-Cmd במק
- ניתן להשתמש בכל שילוב מקשים
useDebouncedValue - ערך מושהה¶
import { useDebouncedValue } from "@mantine/hooks";
import { TextInput, Text, Stack, Paper } from "@mantine/core";
import { useState } from "react";
function SearchWithDebounce() {
const [value, setValue] = useState("");
const [debounced] = useDebouncedValue(value, 300);
return (
<Paper p="lg" shadow="sm" withBorder maw={400}>
<Stack>
<TextInput
label="חיפוש"
placeholder="הקלד לחיפוש..."
value={value}
onChange={(e) => setValue(e.currentTarget.value)}
/>
<Text size="sm">
ערך מיידי: <b>{value}</b>
</Text>
<Text size="sm">
ערך מושהה (300ms): <b>{debounced}</b>
</Text>
<Text size="xs" c="dimmed">
ה-API ייקרא רק כשהערך המושהה משתנה, ולא בכל הקלדה
</Text>
</Stack>
</Paper>
);
}
- useDebouncedValue מחזיר את הערך רק אחרי שהמשתמש הפסיק להקליד
- שימושי לחיפוש - מונע קריאות API מיותרות
useIntersection - זיהוי רכיב בתצוגה¶
import { useIntersection } from "@mantine/hooks";
import { Paper, Text, Stack } from "@mantine/core";
function IntersectionExample() {
const { ref, entry } = useIntersection({
root: null,
threshold: 0.5,
});
const isVisible = entry?.isIntersecting || false;
return (
<Stack>
<Text>גלול למטה כדי לראות את האלמנט</Text>
<div style={{ height: "100vh" }} />
<Paper
ref={ref}
p="xl"
shadow="sm"
withBorder
bg={isVisible ? "green.0" : "gray.0"}
style={{ transition: "background-color 0.3s" }}
>
<Text fw={500}>
{isVisible ? "האלמנט נראה!" : "האלמנט מוסתר"}
</Text>
</Paper>
<div style={{ height: "100vh" }} />
</Stack>
);
}
- שימושי ל-lazy loading, אנימציות בגלילה, ואינסוף גלילה (infinite scroll)
- threshold: 0.5 אומר שהאלמנט נחשב "נראה" כש-50% ממנו בתצוגה
useScrollIntoView - גלילה לרכיב¶
import { useScrollIntoView } from "@mantine/hooks";
import { Button, Paper, Text, Stack, Group } from "@mantine/core";
function ScrollToExample() {
const { scrollIntoView, targetRef } = useScrollIntoView<HTMLDivElement>({
offset: 60, // offset עבור header קבוע
duration: 500,
});
const { scrollIntoView: scrollToTop, targetRef: topRef } =
useScrollIntoView<HTMLDivElement>();
return (
<Stack>
<div ref={topRef} />
<Group>
<Button onClick={() => scrollIntoView({ alignment: "center" })}>
גלול למטה
</Button>
</Group>
<div style={{ height: "100vh" }}>
<Text c="dimmed" ta="center" mt="xl">
גלול או לחץ על הכפתור
</Text>
</div>
<Paper ref={targetRef} p="xl" shadow="md" withBorder bg="blue.0">
<Text fw={500}>הגעתם לכאן!</Text>
<Button
mt="md"
variant="light"
onClick={() => scrollToTop({ alignment: "start" })}
>
חזרה למעלה
</Button>
</Paper>
</Stack>
);
}
הוקים נוספים שימושיים¶
useClickOutside - לחיצה מחוץ לרכיב¶
import { useClickOutside } from "@mantine/hooks";
import { Paper, Text } from "@mantine/core";
import { useState } from "react";
function ClickOutsideExample() {
const [opened, setOpened] = useState(false);
const ref = useClickOutside(() => setOpened(false));
return (
<>
<button onClick={() => setOpened(true)}>פתח</button>
{opened && (
<Paper ref={ref} shadow="md" p="md" withBorder>
<Text>לחץ מחוץ לאלמנט הזה לסגירה</Text>
</Paper>
)}
</>
);
}
useToggle - מעבר בין ערכים¶
import { useToggle } from "@mantine/hooks";
import { Button, Text } from "@mantine/core";
function ToggleExample() {
const [value, toggle] = useToggle(["light", "dark"] as const);
return (
<>
<Text>מצב נוכחי: {value}</Text>
<Button onClick={() => toggle()}>החלף מצב</Button>
</>
);
}
useWindowScroll - מיקום גלילה¶
import { useWindowScroll } from "@mantine/hooks";
import { Button, Text, Group } from "@mantine/core";
function ScrollExample() {
const [scroll, scrollTo] = useWindowScroll();
return (
<Group>
<Text>
גלילה: X={scroll.x}, Y={scroll.y}
</Text>
<Button onClick={() => scrollTo({ y: 0 })}>
חזרה למעלה
</Button>
</Group>
);
}
דוגמה מלאה - טופס יצירת קשר עם הוקים¶
import { useForm } from "@mantine/form";
import { useMediaQuery, useClipboard, useHotkeys, useDisclosure } from "@mantine/hooks";
import {
Paper, Title, TextInput, Select, Textarea, Button, Stack, Group,
Modal, Text, Tooltip,
} from "@mantine/core";
function ContactFormWithHooks() {
const isMobile = useMediaQuery("(max-width: 768px)");
const clipboard = useClipboard({ timeout: 2000 });
const [submitted, submitHandlers] = useDisclosure(false);
const form = useForm({
mode: "uncontrolled",
initialValues: {
name: "",
email: "",
subject: "",
message: "",
},
validate: {
name: (v) => (v.trim().length < 2 ? "שם קצר מדי" : null),
email: (v) => (/^\S+@\S+$/.test(v) ? null : "אימייל לא תקין"),
subject: (v) => (v ? null : "בחר נושא"),
message: (v) => (v.trim().length < 10 ? "ההודעה קצרה מדי" : null),
},
validateInputOnBlur: true,
});
// קיצור מקלדת לשליחה
useHotkeys([
["mod+enter", () => {
if (form.isValid()) {
submitHandlers.open();
}
}],
]);
function handleSubmit() {
submitHandlers.open();
form.reset();
}
const referenceId = "REF-" + Math.random().toString(36).substring(2, 8).toUpperCase();
return (
<>
<Paper
shadow="sm"
p={isMobile ? "md" : "xl"}
withBorder
maw={isMobile ? "100%" : 500}
mx="auto"
>
<Title order={isMobile ? 3 : 2} mb="lg">
צור קשר
</Title>
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<TextInput
label="שם מלא"
placeholder="הכנס שם"
required
key={form.key("name")}
{...form.getInputProps("name")}
/>
<TextInput
label="אימייל"
placeholder="your@email.com"
required
key={form.key("email")}
{...form.getInputProps("email")}
/>
<Select
label="נושא"
placeholder="בחר נושא"
data={["שאלה כללית", "תמיכה טכנית", "משוב", "אחר"]}
required
key={form.key("subject")}
{...form.getInputProps("subject")}
/>
<Textarea
label="הודעה"
placeholder="כתוב את ההודעה..."
minRows={4}
required
key={form.key("message")}
{...form.getInputProps("message")}
/>
<Button type="submit" fullWidth>
שלח
</Button>
</Stack>
</form>
</Paper>
<Modal opened={submitted} onClose={submitHandlers.close} title="נשלח בהצלחה" centered>
<Stack>
<Text>ההודעה נשלחה בהצלחה. מספר הפניה שלך:</Text>
<Group>
<Text fw={700} size="lg">{referenceId}</Text>
<Tooltip label={clipboard.copied ? "הועתק!" : "העתק"}>
<Button
size="xs"
variant="light"
color={clipboard.copied ? "green" : "blue"}
onClick={() => clipboard.copy(referenceId)}
>
{clipboard.copied ? "הועתק" : "העתק"}
</Button>
</Tooltip>
</Group>
<Button onClick={submitHandlers.close} fullWidth mt="md">
סגור
</Button>
</Stack>
</Modal>
</>
);
}
סיכום¶
- useDisclosure מנהל מצב פתוח/סגור בצורה נקייה, שימושי ל-Modal, Drawer וכל toggle
- useForm מספק ניהול טפסים מלא עם ולידציה, getInputProps, ותמיכה ברשימות דינמיות
- useMediaQuery מאפשר התאמה למסכים שונים בלוגיקת JavaScript
- useClipboard מספק העתקה ללוח עם משוב למשתמש
- useLocalStorage שומר ערכים ב-localStorage עם API פשוט של useState
- useHotkeys מגדיר קיצורי מקלדת גלובליים
- useDebouncedValue מונע קריאות מיותרות בחיפוש ושדות טקסט
- useIntersection מזהה מתי רכיב נכנס לתצוגה
- useScrollIntoView מאפשר גלילה חלקה לרכיב ספציפי
- שילוב מספר הוקים יחד יוצר חוויית משתמש עשירה ומקצועית