הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

היום למדתי: CSS Custom Highlight

22/06/2025

הדבר היחיד שכיף יותר מלמחוק קוד הוא לשכוח שיטת עבודה מסורבלת כי כבר לא צריך אותה. מנגנון CSS Custom Highlight מאפשר לנו "לתפוס" חלק מהטקסט של אלמנט ב DOM ולעצב אותו בצורה שונה, בלי שנצטרך לעטוף את החלק הזה ב span. שימו לב לדוגמה הבאה-

<p id="foo">CSS is designed to enable the separation of content and presentation, including layout, colors, and fonts.</p>
::highlight(user-1-highlight) {
  background-color: yellow;
  color: black;
}
const p = document.getElementById("foo");
const text = p.firstChild;
const range = new Range();
range.setStart(text, 10);
range.setEnd(text, 20);

const highlight = new Highlight(range);

CSS.highlights.set("user-1-highlight", highlight);

הקוד צובע את תווים 10 עד 20 מהטקסט שבפיסקה, בלי לשנות את מבנה ה DOM ולהוסיף span-ים מיותרים. המנגנון הזה מושלם להדגשת תוצאות חיפוש, צביעת קוד או עריכה משותפת של מסמך כשכל משתמש כותב בצבע אחר.

ומה שבאמת מופלא ב API הזה הוא שאין בעיה לסמן בין אלמנטים, כלומר setStart ו setEnd של range לא חייבים לקבל את אותו אלמנט. הנה עדכון לדוגמה עם סימון חוצה פיסקאות:

<p id="foo">CSS is designed to enable the separation of content and presentation, including layout, colors, and fonts.</p>
<p id="bar">and it can span multiple paragraphs too</p>
const foo = document.getElementById("foo");
const start = foo.firstChild;

const bar = document.getElementById("bar");
const end = bar.firstChild;
const range = new Range();
range.setStart(start, 20);
range.setEnd(end, 5);

const highlight = new Highlight(range);

CSS.highlights.set("user-1-highlight", highlight);

בהרבה מקרים תרצו לשלב את זה עם TreeWalker כדי לחשב את האינדקסים, אבל עדיין המנגנון משמעותית יותר יעיל ופשוט מהוספה והזזת span-ים.

תיעוד מלא שתרצו לקרוא אפשר למצוא ב MDN בקישור: https://developer.mozilla.org/en-US/docs/Web/API/CSSCustomHighlight_API

ועל Tree Walker אפשר לקרוא כאן: https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker

הרהורים על טיילווינד ועיצוב בתוך קומפוננטות

21/06/2025

אחד הרעיונות שהיו פופולריים כשהתחלתי ללמוד HTML היה האפשרות להחליף עיצוב לחלוטין באמצעות שינוי CSS. זה המשיך ללוות אותנו לתוך האיפיון של CSS3 וגם למודולים חדשים יותר של CSS, כולל ובמיוחד במודולים של פלקסבוקס וגריד.

הגישה אז היתה שאפשר יהיה לכתוב קבצי CSS שונים כדי לייצר עיצובים מאוד שונים לאותה מערכת. השתמשנו ב JavaScript כדי לשנות קלאסים, אבל הגדרת העיצוב עצמה שהתאימה לקלאסים אלה הגיעה מקובץ CSS שאמור להיות קל לתחזוקה והחלפה.

אני מודה שזה לא ממש עבד. בהרבה אתרים שינוי עיצוב דרש גם שינוי במבנה ה HTML וראינו הרבה מעקפים כמו לכתוב את אותו קטע בעמוד פעמיים ב HTML ולהחליט מתוך CSS איזה מהם להציג. ובכל זאת היה משהו קסום בחלום הזה שאפשר יהיה לבנות Theme שונה למערכת רק באמצעות קובץ CSS.

היום עם ריאקט וטיילווינד נראה שזרקנו את החלום הזה לחלוטין. הנה קומפוננטת מונה לחיצות האהובה עליי:

import React, { useState } from "react";

export default function ShinyCounter() {
  const [count, setCount] = useState(0);

  return (
    <div className="min-h-screen flex items-center justify-center bg-gradient-to-r from-pink-400 via-yellow-300 to-purple-500">
      <div className="bg-white bg-opacity-20 backdrop-blur-lg border border-white border-opacity-30 rounded-2xl p-10 shadow-2xl text-center">
        <h1 className="text-5xl font-extrabold text-white drop-shadow-md mb-6">
          Shiny Counter
        </h1>
        <div className="text-7xl font-bold text-white drop-shadow-lg mb-6">
          {count}
        </div>
        <div className="flex gap-4 justify-center">
          <button
            onClick={() => setCount(count - 1)}
            className="px-6 py-3 rounded-xl text-white font-bold bg-gradient-to-br from-red-400 to-pink-500 hover:scale-105 active:scale-95 transition transform duration-200 shadow-lg"
          >
            - Decrease
          </button>
          <button
            onClick={() => setCount(0)}
            className="px-6 py-3 rounded-xl text-white font-bold bg-gradient-to-br from-yellow-300 to-orange-400 hover:scale-105 active:scale-95 transition transform duration-200 shadow-lg"
          >
            Reset
          </button>
          <button
            onClick={() => setCount(count + 1)}
            className="px-6 py-3 rounded-xl text-white font-bold bg-gradient-to-br from-green-400 to-blue-500 hover:scale-105 active:scale-95 transition transform duration-200 shadow-lg"
          >
            + Increase
          </button>
        </div>
      </div>
    </div>
  );
}

אין איך לשנות לה את ה CSS כדי לקבל עיצוב אחר או להשתמש בה בעמוד אחר ושהיא תקבל את העיצוב מהעמוד. זאת הקומפוננטה. זה העיצוב שלה. Take it or leave it.

הגישה היום רואה את ה HTML, ה CSS וה JavaScript בתור יחידה אחת. בשביל לשנות את העיצוב עלינו לכתוב קומפוננטה אחרת. זה חלק מהחשיבות של הוצאת הלוגיקה ל Hook, כלומר בסיפור של ה Counter נוכל לכתוב:

import React from "react";

function useCounter(initialValue = 0) {
  const [count, setCount] = React.useState(initialValue);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const reset = () => setCount(initialValue);

  return { count, increment, decrement, reset };
}

export default function ShinyCounter() {
  const { count, increment, decrement, reset } = useCounter();

  return (
    <div className="min-h-screen flex items-center justify-center bg-gradient-to-r from-pink-400 via-yellow-300 to-purple-500">
      <div className="bg-white bg-opacity-20 backdrop-blur-lg border border-white border-opacity-30 rounded-2xl p-10 shadow-2xl text-center">
        <h1 className="text-5xl font-extrabold text-white drop-shadow-md mb-6">
          Shiny Counter
        </h1>
        <div className="text-7xl font-bold text-white drop-shadow-lg mb-6">
          {count}
        </div>
        <div className="flex gap-4 justify-center">
          <button
            onClick={decrement}
            className="px-6 py-3 rounded-xl text-white font-bold bg-gradient-to-br from-red-400 to-pink-500 hover:scale-105 active:scale-95 transition transform duration-200 shadow-lg"
          >
            - Decrease
          </button>
          <button
            onClick={reset}
            className="px-6 py-3 rounded-xl text-white font-bold bg-gradient-to-br from-yellow-300 to-orange-400 hover:scale-105 active:scale-95 transition transform duration-200 shadow-lg"
          >
            Reset
          </button>
          <button
            onClick={increment}
            className="px-6 py-3 rounded-xl text-white font-bold bg-gradient-to-br from-green-400 to-blue-500 hover:scale-105 active:scale-95 transition transform duration-200 shadow-lg"
          >
            + Increase
          </button>
        </div>
      </div>
    </div>
  );
}

export { useCounter };

ועכשיו ה Counter בעמוד השני יוכל להיות הרבה יותר מינימליסטי או עתידני או מה שנבחר ועדיין הלוגיקה לא תשתנה.

מבנה זה מתחבר לכח העל של ה AI לזהות תבניות ולשכפל אותן. במקום שיהיה לי קובץ CSS לאתר וכל HTML חדש אוטומטית יקבל את העיצוב מאותו קובץ CSS, עכשיו יש לי קומפוננטה ובשביל להשתמש בה בדף אחר באתר או באתר אחר אני עובר דרך ה AI

Create a `HomepageCounter` component based on the existing `AboutpageCounter` but matching the design to the home page.

דברים ש AI עשה ממש טוב

20/06/2025

בוובינר אתמול דיברנו על איך לגשת לשינויי ארכיטקטורה גדולים עם AI. ראינו כמה חשוב להשקיע זמן בלבנות תוכנית מראש בעזרת מצב Ask של ה AI, איך משפרים את התוכנית בעצמנו או בעזרת רעיונות של אותו AI ואיך בסוף הופכים אותה לתוכנית עבודה מסודרת, קובץ ארכיטקטורה ומימוש.

לקראת סוף הוובינר עלתה שאלה חשובה - אם אנחנו "מבזבזים" כל כך הרבה זמן באיטרציה על הפרומפטים ובמחשבה על כל פרט קטן, אולי היה עדיף לכתוב את השינוי הזה כבר ידנית?

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

רוב השינויים במערכת הם לא כאלה.

רוב השינויים במערכת הם שינויים המשכיים, שינויים שמתבססים על המצב הקיים ומרחיבים אותו: פיתוח פיצ'ר שלא משנה את איך שהמידע שמור בבסיס הנתונים, תיקון בעיית ביצועים שמתפרסת על עשרות או מאות קבצים, התקנת ספריית JavaScript חדשה ועדכון הקוד בכל האפליקציה להשתמש בה.

זה לא היה במסגרת הנושאים של הוובינר (אולי יתאים למפגש אחר), אבל הנה כמה פיצ'רים שבניתי ב langlets שבזכות ה AI לקחו הרבה הרבה פחות זמן ממה שהיו צריכים לקחת.

  1. מסך בית שמציג גריד של קורסים - דף הבית של לאנגלטס מורכב מגריד של כל הקורסים במערכת. זה היה הפרומפט בשבילו:
Implement the courses#index page based on the attached HTML file, matching the design to our system #file:show.html.erb

List of courses in top row is taken from current_user.recommended_for_me. If the list is empty or user is not signed in do not show the first row

List of learning path in the second row is LearningPath.all

Use swiper js library to handle swiping (it's already installed in the project). Add a stimulus controller to handle the JS

When writing JavaScript use stimulus best practice that is data-target for element querying and data-action for events

Provide only filter by language (use course's language) for this one

Use our existing site's top bar for user authentication. Ignore the top bar in the provided HTML

Use only tailwind native classes

Design to integrate:
...

זה פרומפט ארוך ולא הדבקתי כאן את כולו כי סיימתי אותו ממש בדוגמת HTML/CSS שעיצבתי עם Gemini למסך בית מקביל, אבל שימו לב לעיקרי הדברים - הפרומפט לוקח דף קונספט בעיצוב שונה לגמרי מהמערכת שלי ואוטומטית מתאים את העיצוב לשאר הדפים במערכת, הוא משלב ספריית צד-לקוח בשם swiper וחוסך לי לחפש את ה API שלה. הפרומפט יצר קובץ HTML, קובץ JavaScript ושינה כמה קבצי רובי, שינוי של כמה מאות שורות סך הכל. אחרי ההרצה עדיין היו תקלות והייתי צריך להמשיך עוד כמה איטרציות של תיקונים ושיפורים אבל הארכיטקטורה של הפיתרון היתה נכונה מההפעלה הראשונה בגלל שדברים השתלבו במבנה הפרויקט הקיים.

  1. התאמת עיצוב של כל דפי המשתמשים במכה אחת - התחברות, רישום ושכחתי סיסמה מציגים טופס שנראה מאוד דומה. כשמשנים משהו באחד מהם צריך לשנות אותו גם בדפים האחרים. בפרויקט שלי לא רציתי לכתוב אבסטרקציה לחלק המשותף כי יש סיכוי טוב שארצה לשנות חלק מהמסכים בלי להשפיע על האחרים. ה AI מתמודד עם השכפול בצורה מדהימה. ברגע שאני מבקש שינוי בטופס ומדביק ב Context של שלושת הדפים הוא משנה במכה אחת את שלושתם.

  2. יצירת דפי פעילות חדשים - אם תכנסו לאחד הקורסים בלאנגלטס תראו שהם מורכבים מדפי פעילויות מסוגים שונים כמו התאמת משפט לתרגום, הרכבת משפט מבנק מילים, התאמת מילים לתרגומים ועוד. בעבר השקעתי הרבה מחשבה בבניית אבסטרקציות כדי שאפשר יהיה לבנות "דפי פעילות" חדשים בקלות. זה היה מאתגר כי מצד אחד צריך קומפוננטות משותפות שנראות דומה (Design System אחיד) אבל מצד שני ההתנהגות של כל מסך יכולה להיות מאוד שונה. בעזרת AI אני יכול היום ליצור דפי פעילויות חדשים בפרומפט אחד וכמעט תמיד מקבל תוצאה טובה מהפרומפט הראשון. כל מה שצריך זה להדביק את הקוד של 3-4 דפי פעילויות ולבד ה AI מבין את התבנית, מה משותף, לפי איזה סטנדרטים כותבים את הקוד ומאיפה מגיע המידע. כן לכל דף יש את האתגרים שלו אבל היתרון שאני מצליח בכתיבת דף פעילות חדש לקבל בסיס ראשוני שנראה מעוצב לפי המבנה של המערכת ואז אני יכול להתמקד ב JavaScript וב CSS של אותו דף פעילות כדי לסדר אותו.

שורה תחתונה העבודה עם AI שונה בין הדברים הקשים לדברים הקלים:

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

  2. בדברים הקלים, ובעיקר פיתוח פיצ'רים בתוך עיצוב וארכיטקטורה קיימת, ה AI יכול לרוץ מאוד מהר ולתת מאות ואלפי שורות קוד שלא שוברים שום דבר. גם כשיש בהם באגים אותם באגים תחומים בתוך פונקציה או קלאס בודד ולא משפיעים על המבנה של כל המערכת.

הבנת ההבדל בין פיצ'רים קשים לקלים היא אחד המפתחות לעבודה יעילה עם AI ומימוש פיצ'רים מהר בלי לפגוע באיכות.

חופש הפעולה של הטייס האוטומטי

19/06/2025

הייתם נותנים לטייס אוטומטי לקבוע לאן צריך להגיע?

מה לגבי שינויים במסלול? הייתם נותנים לטייס אוטומטי להטיס את המטוס מדרך אחרת כי הוא חושב ששם יותר בטוח או שיש נוף יותר יפה?

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

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

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

בוובינר היום אראה מספר פרומפטים שעוברים מרחק גדול. בכל פרומפט נבין את הארכיטקטורה של המערכת, את המשימה לביצוע ואיך לכתוב את הפרומפט כדי שגם ה AI יבין טוב את המשימה. בצורה איטרטיבית נלמד את הבעיות ואת אי ההבנות שיש בינינו לבין הטייס האוטומטי ולאט לאט גם איך לתקן אותן. נתלבט מתי כדאי להמשיך לכתוב פרומפט נוסף ומתי עדיף לקחת צעד אחורה ולשפר את הפרומפט הראשון ואיך לצמצם סיכונים להזיות ול Loop-ים. תחשבו על זה כמו Prompt Engineering מתקדם למתכנתים.

רוצים להצטרף? לא מסובך מלאו את המייל בתיבה בעמוד הבא ואשלח לכם את הקישור לזום במייל חוזר:

https://tocode.ravpage.co.il/tocodeai

נתראה בעשר.

היום למדתי: הפקודה sendBeacon ב JavaScript

18/06/2025

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

דפדפנים נוטים לקחת את הצד של המשתמש בסיפור הזה וכשמנסים לשלוח fetch או XMLHttpRequest ביציאה מהעמוד הדפדפן פשוט לא ישלח את ההודעה. לפי התיעוד ב MDN אנשים השתמשו בכל מיני טריקים כדי לגרום לדפדפן להישאר עוד קצת בעמוד האהוב עליי היה ליצור אלמנט img שה src שלו הוא כתובת השרת לדיווח על יציאה.

פקודת navigator.sendBeacon היא הדרך של יצרני הדפדפנים לעזור לשני הצדדים: גם לאפשר למפתחים לקבל אירועים כשמשתמשים עוזבים את העמוד וגם לאפשר למשתמשים לעזוב עמודים מהר בלי לחכות לדיווחי הסטטיסטיקות. כך זה עובד:

  1. אנחנו מפעילים פקודת navigator.sendBeacon שחוזרת מיד בלי לחכות לבקשת התקשורת.

  2. משתמש יכול להמשיך לעזוב את העמוד.

  3. כשלדפדפן יהיה זמן הוא ישלח את הדיווח לאתר שלכם.

אם השרת שלכם מצפה לקבל מידע בפורמט JSON תוכלו להשתמש בקוד הבא כדי לשלוח את המידע מהדפדפן לשרת בלי לחכות לתשובה:

const url = 'https://example.com/collect';
const data = {
  event: 'pageUnload',
  timestamp: Date.now(),
  userId: 'abc123'
};

// Convert the JSON object to a string
const jsonString = JSON.stringify(data);

// Convert the string to a Blob with the appropriate MIME type
const blob = new Blob([jsonString], { type: 'application/json' });

// Send the beacon
navigator.sendBeacon(url, blob);

הקוד שולח את המידע בתור blob בשביל לקבוע את ה content-type של הבקשה (מה שגורם לפריימוורק צד שרת בדרך כלל לפענח את ה JSON בצורה אוטומטית). אם זה לא חשוב לכם ואתם מוכנים לשלוח טקסט ולפענח את ה JSON בשרת בעצמכם תוכלו לכתוב גם:

navigator.sendBeacon(url, JSON.stringify(data));

והרבה פעמים תרצו להפעיל את זה כשמשתמש עוזב את העמוד ובשביל זה כדאי להתחבר לאירוע visibilitychange באופן הבא:

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    navigator.sendBeacon("/log", analyticsData);
  }
});

חדש ב node: בדיקה אם זה הסקריפט הראשי

17/06/2025

הנה פיצ'ר מפייתון שלא היה חסר לי במקומות אחרים ובכל זאת נכנס לאחרונה ל node בגירסה 24.2 - האפשרות לבדוק אם המודול רץ מתוך התוכנית הראשית או בתור מודול שמיובא לתוכנית אחרת. ניקח לדוגמה את הקוד הבא:

export function foo() {
  return 'Hello, world';
}

function main() {
  const message = foo();
  console.log(message);
}

if (import.meta.main) {
  main();
}

אפשר להגיע לקוד הזה בשתי דרכים. דרך אחת היא להפעיל משורת הפקודה את התוכנית כלומר:

node main.mjs

דרך שניה היא מתוך קובץ אחר לייבא פונקציה לדוגמה מתוך קובץ JavaScript אחר כותבים:

import {foo} from './main.mjs';

console.log(foo());

עד גירסה 24.2 של node לא היתה לנו דרך להבדיל בין שני המקרים. נכון תגידו, ממילא זה לא היה רעיון טוב לערבב פונקציות ליבוא וקוד main באותו קובץ אבל יש use case מעניין אחר לסיפור הזה. שימו לב למודול הבא:

import assert from 'node:assert/strict';
import { test } from 'node:test';

export function twice(x) {
  return x * 2;
}

if (import.meta.main) {
  test('twice(3) == 6', () => {
    assert.equal(twice(3), 6);
  });
}

עד היום היה לנו לכל מודול קובץ אחד של קוד וקובץ שני של בדיקות. עכשיו עם הכתיב החדש אפשר לכתוב את שני הדברים באותו קובץ. מי שרוצה להשתמש בפונקציה יטען אותו עם import ובשביל להריץ את הבדיקות פשוט מריצים את הקובץ בתור תוכנית ראשית.

חישוב סטטיסטיקות מונים עם DOM

16/06/2025

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

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

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

האילוץ הזה לא קיים בעבודה רגילה עם DOM, שם אני יכול לטייל בעץ לכל כיוון שאני רוצה ולהתחבר למידע ואירועים של קומפוננטות שנמצאות לידי או מעליי. קובץ ה HTML של מונה הלחיצות עם הסטטיסטיקות לכן יכול להיראות פשוט כך:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app">
      <div class="counter-group">
        <counter-stats></counter-stats>
        <counter-component></counter-component>
        <counter-component></counter-component>
        <counter-component></counter-component>
      </div>
    </div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

בשביל לחשב את הסטטיסטיקות קומפוננטת counter-stats צריכה בסך הכל לטייל צעד אחד למעלה בעץ ל counter-group ומשם לחפש את כל הילדים שהם counter-component ולקרוא את מספר הלחיצות שלהם. זאת הפונקציה:

  updateStats() {
    if (!this.counterGroup) return;

    // Get all counter components from the parent group
    const counters = this.counterGroup.querySelectorAll('counter-component');
    const counts = Array.from(counters).map(counter => {
      return parseInt(counter.dataset.count) || 0;
    });

    if (counts.length === 0) {
      this.updateStatValues(0, 0);
      return;
    }

    const minValue = Math.min(...counts);
    const maxValue = Math.max(...counts);

    this.updateStatValues(minValue, maxValue);
  }

אנחנו רוצים להפעיל את הפונקציה כל פעם שמישהו לחץ על כפתור באחד המונים, וכאן נכנס עוד כח חשוב של DOM והוא אירועים בהתאמה אישית. זה הקוד מתוך counter שמדווח על שינוי:

// Emit custom event for stats tracking
this.dispatchEvent(new CustomEvent('counter:update-count', {
  bubbles: true,
  detail: { count: this.count }
}));

בעצם כל פעם שיש שינוי אני מדווח על אירוע. מבנה האירועים של ה DOM אומר שאני לא צריך לדעת מי מקשיב לאירוע ולכל אירוע יכולים להיות מספר מאזינים. המאזין, במקרה שלנו ה counter-stats אחראי על הקוד שלו שירוץ אחרי שהאירוע התרחש. זה מאוד שונה ממנגנון הסטייט של ריאקט, שם קוד הטיפול באירוע יכול לשנות סטייט אבל ה DOM הולך להתעדכן רק אחרי שקוד הטיפול יסיים ויחזיר את השליטה לריאקט. אם תנסו לבנות מננגון דומה בריאקט ובעקבות לחיצה תלכו לקרוא את ה DOM כדי להבין מה הערך של מונה מסוים אתם תקבלו את הערך לפני השינוי (כי אחרי setState צריך לחכות שריאקט יבצע render כדי שהעדכון יגיע ל DOM).

הקוד המלא לשתי הקומפוננטות הוא:

class CounterComponent extends HTMLElement {
  constructor() {
    super();

    // Create shadow DOM with open mode for styling
    this.attachShadow({ mode: 'open' });

    // Initialize count from data attribute or default to 0
    this.count = parseInt(this.dataset.count) || 0;

    this.render();
    this.setupEventListeners();
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: inline-block;
          margin: 10px;
        }
      </style>
      <div class="counter-container">
        <button class="counter-button">Click me!</button>
        <p class="counter-text">
          Count: <span class="counter-value">${this.count}</span>
        </p>
      </div>
    `;
  }

  setupEventListeners() {
    const button = this.shadowRoot.querySelector('.counter-button');
    button.addEventListener('click', () => this.incrementCounter());
  }

  incrementCounter() {
    this.count++;

    // Update the data attribute to persist state in DOM
    this.dataset.count = this.count;

    // Update only the counter value span, not the entire component
    const valueSpan = this.shadowRoot.querySelector('.counter-value');
    valueSpan.textContent = this.count;

    // Emit custom event for stats tracking
    this.dispatchEvent(new CustomEvent('counter:update-count', {
      bubbles: true,
      detail: { count: this.count }
    }));
  }

  // Called when component is connected to DOM
  connectedCallback() {
    // Update count from data attribute if it changed
    const dataCount = parseInt(this.dataset.count);
    if (!isNaN(dataCount) && dataCount !== this.count) {
      this.count = dataCount;
      const valueSpan = this.shadowRoot.querySelector('.counter-value');
      if (valueSpan) {
        valueSpan.textContent = this.count;
      }
    }

    // Emit initial event for stats tracking
    this.dispatchEvent(new CustomEvent('counter:update-count', {
      bubbles: true,
      detail: { count: this.count }
    }));
  }

  // Observe data-count attribute changes
  static get observedAttributes() {
    return ['data-count'];
  }

  // Handle attribute changes
  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'data-count' && this.shadowRoot) {
      const newCount = parseInt(newValue) || 0;
      if (newCount !== this.count) {
        this.count = newCount;
        const valueSpan = this.shadowRoot.querySelector('.counter-value');
        if (valueSpan) {
          valueSpan.textContent = this.count;
        }

        // Emit custom event for stats tracking
        this.dispatchEvent(new CustomEvent('counter:update-count', {
          bubbles: true,
          detail: { count: this.count }
        }));
      }
    }
  }
}

// Define the custom element
customElements.define('counter-component', CounterComponent);

// Counter Stats Component
class CounterStatsComponent extends HTMLElement {
  constructor() {
    super();

    // Create shadow DOM with open mode for styling
    this.attachShadow({ mode: 'open' });

    this.render();
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          margin: 10px;
          padding: 15px;
          border: 2px solid #ccc;
          border-radius: 8px;
          background-color: #f9f9f9;
          font-family: Arial, sans-serif;
        }
        .stats-container {
          display: flex;
          gap: 20px;
          justify-content: center;
        }
        .stat-item {
          text-align: center;
        }
        .stat-label {
          font-weight: bold;
          color: #555;
          margin-bottom: 5px;
        }
        .stat-value {
          font-size: 1.2em;
          color: #333;
          font-weight: bold;
        }
      </style>
      <div class="stats-container">
        <div class="stat-item">
          <div class="stat-label">Min Value:</div>
          <div class="stat-value min-value">0</div>
        </div>
        <div class="stat-item">
          <div class="stat-label">Max Value:</div>
          <div class="stat-value max-value">0</div>
        </div>
      </div>
    `;
  }

  connectedCallback() {
    // Find the parent .counter-group element
    this.counterGroup = this.closest('.counter-group');

    if (this.counterGroup) {
      // Listen for counter update events
      this.counterGroup.addEventListener('counter:update-count', this.handleCounterUpdate.bind(this));

      // Calculate initial stats
      this.updateStats();
    }
  }

  disconnectedCallback() {
    if (this.counterGroup) {
      this.counterGroup.removeEventListener('counter:update-count', this.handleCounterUpdate.bind(this));
    }
  }

  handleCounterUpdate(event) {
    // Update stats whenever any counter changes
    this.updateStats();
  }

  updateStats() {
    if (!this.counterGroup) return;

    // Get all counter components from the parent group
    const counters = this.counterGroup.querySelectorAll('counter-component');
    const counts = Array.from(counters).map(counter => {
      return parseInt(counter.dataset.count) || 0;
    });

    if (counts.length === 0) {
      this.updateStatValues(0, 0);
      return;
    }

    const minValue = Math.min(...counts);
    const maxValue = Math.max(...counts);

    this.updateStatValues(minValue, maxValue);
  }

  updateStatValues(min, max) {
    // Update only the specific value spans, not the entire element
    const minSpan = this.shadowRoot.querySelector('.min-value');
    const maxSpan = this.shadowRoot.querySelector('.max-value');

    if (minSpan) minSpan.textContent = min;
    if (maxSpan) maxSpan.textContent = max;
  }
}

// Define the custom element
customElements.define('counter-stats', CounterStatsComponent);

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

https://tocode.ravpage.co.il/tocodeai

הזמנה לוובינר: פיתוח פיצ'רים מלאים עם AI

15/06/2025

השבוע בוובינר "מדברים AI" אני הולך לנתח שלושה פרומפטים אמיתיים שבנו עבורי פיצ'רים אמיתיים במוצר - פיצ'רים שמורכבים ממספר קבצים ומאות שורות קוד בתוך מוצר קיים.

אנחנו נראה איך ליישם את העקרונות של Prompt Engineering בחיים שלנו כמפתחים עם קופיילוט:

  1. איך לארגן מראש את הפרויקט כדי שאפשר יהיה להוסיף אליו פיצ'רים מהר עם AI.

  2. איך להשתמש בקונטקסט המתאים בפרומפט כדי לכוון את ה AI למימוש שאני רוצה שהוא יכתוב.

  3. איך לבנות את הפרומפט בצורה איטרטיבית, לזהות כל פעם איפה הניסוח לא היה מספיק טוב ולשפר כדי לקבל בסוף מימוש עובד מקצה לקצה.

הוובינר מתקיים כבכל שבוע בחמישי בשעה עשר בבוקר בזום. לקבלת הקישור לזום יש להירשם בעמוד הזה:

https://tocode.ravpage.co.il/tocodeai

והקישור יישלח אליכם למייל וגם תקבלו מייל תזכורת בחמישי בבוקר.

נ.ב. קבוצת מדברים AI היא קבוצת לימוד פתוחה למפתחים שרוצים להשתפר ב AI. אם יש לכם שאלות, התייעצויות או הצלחות ודברים שעבדו לכם עם AI ואתם רוצים לשתף עם הקבוצה אני מאוד אשמח. תכתבו לי ונתאם מסודר.

כמה זמן לוקח להתקין ספריה מ npm?

14/06/2025

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

ואת אותו משחק אנחנו משחקים עם ספריות npm. צריכים לבדוק אם מספר זוגי? להוסיף אנימציה? להתחבר ל API? לתכנן זמנים? יש ספריה לזה. רק תפעילו

npm add moment

ולא תצטרכו לכתוב את הלוגיקה המסובכת לבד.

אבל בעולם האמיתי נרצה להשקיע קצת יותר זמן ותשומת לב בהוספה של ספריה חדשה לפרויקט, ונרצה להבין:

  1. "כמה הספריה עולה" כלומר מה הגודל שהיא מוסיפה לגודל הבאנדל או כמה זמן היא מוסיפה לזמן טעינת העמוד?

  2. מה האלטרנטיבות? איזה ספריות אחרות עושות עבודה דומה? למה החלטתי דווקא על הספריה הזו? לדוגמה ספריית day.js מציעה אלטרנטיבה קטנה בהרבה מ moment עם אותו API.

  3. האם יש דרכים שונות להשתמש בספריה והאם אני בוחר את הדרך האופטימלית עבורי? לדוגמה הספריה motion מגיעה בגירסה מלאה וגירסת מיני, ואם אין לכם צורך בכל האנימציות תוכלו לקבל ביצועים הרבה יותר טובים בעבודה עם גירסת המיני.

  4. האם הספריה מתוחזקת? מה הסיכוי שמישהו יכניס קוד זדוני לשרשרת האספקה שלה או לספריה עצמה, ודרך הקוד הזה יגיע אליי למערכת?

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

  6. מה מדיניות התמיכה לאחור של מפתחי הספריה? האם בעוד שנה עדיין אוכל להשתמש בה או שכל שלושה חודשים אצטרך לעשות Refactor בשביל לשדרג גירסאות? זכורה לרעה כאן ריאקט ראוטר שכל חצי שנה יצאה בגירסה חדשה עם API שונה.

היום עם ה AI קל מאי פעם להגיע למידע. אתם רחוקים מרחק פרומפט אחד מקבלת תשובה מפורטת לכל השאלות ואפילו מיצירת מימוש קטן של אותו מנגנון מחוץ לספריה. בואו נהנה מזה ונשתמש במידע כדי לקבל החלטות טובות יותר.

הבעיה של ריאקט ו State

13/06/2025

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

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

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

כל זה עובד טוב בספר הלימוד אבל בחיים האמיתיים יש בעיה שחוזרת כמעט בכל יישום ריאקט שנכתוב: סטייט נוטה לעלות למעלה לאזורים המשותפים יותר מהר ממה שהיינו רוצים.

ניקח דוגמה פשוטה של מונה לחיצות:

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div className="counter" >
      <p>Count = {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  )
}

ועכשיו נרצה לשים מספר מונים כאלה על המסך ומעליהם לכתוב מה הערך המקסימלי של מונה כלשהו מקבוצת המונים. נו, זה לא נשמע מסובך, אני מתחיל עם App:

import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import Counter from './Counter'
import CounterGroup from './CounterGroup'
function App() {

  return (
    <CounterStatistics />
    <Counter />
    <Counter />
    <Counter />
  )
}

export default App

ואז מנסה לכתוב את CounterStatistics כדי להציג את המקסימום והמינימום - ופה פוגש את ריאקט ותיאוריית הסטייט שלו. כי בשביל ש CounterStatistics יוכל להשתמש במידע, כלומר בערכים שמופיעים במוני הלחיצות, אני צריך שהמידע הזה יעלה למעלה ויישמר בקומפוננטה משותפת, לא בתור משתנה בודד בתוך Counter אלא בתור מערך של מספרים. זאת הדרך הריאקטית קדימה:

function App() {
  const [counterValues, inc] = useCounters(3);
  return (
    <>
      <CountersStats values={counterValues} />      
      <Counter value={counterValues[0]} inc={inc(0)} />
      <Counter value={counterValues[1]} inc={inc(1)} />
      <Counter value={counterValues[2]} inc={inc(2)} />
    </>
  )
}

ועכשיו הייתי צריך לשנות את הקומפוננטה של Counter שלא תכיל סטייט פנימי רק בשביל לשים אותה בתוך קומפוננטה שתציג סטטיסטיקות.

וכן אני מבין מה קורה פה ולמה היינו צריכים ללכת את הדרך הזאת, אבל באמת יש פה שאלה אם זה שווה את המאמץ, ואולי דווקא במקרים כאלה כן שווה לשכפל את הסטייט ולשמור סטייט נפרד נוסף לסטטיסטיקות, כלומר:

function App() {
  const [min, setMin] = useState(0);
  const [max, setMax] = useState(0);

  function update(value) {
    setMin(Math.min(min, value));
    setMax(Math.max(max, value));
  }

  return (
    <>
      <CountersStats />
      <Counter onChange={update} />
      <Counter onChange={update} />
      <Counter onChange={update} />
    </>
  )
}

וכן גם פה צריך לשנות קצת את Counter ולהוסיף את onChange בתור מאפיין אופציונאלי אבל זה שינוי פחות משמעותי מלהעביר את הסטייט החוצה.

מה דעתכם?