איך חברת Nordhealth משתמשת במאפיינים מותאמים אישית ברכיבי אינטרנט

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

David Darnes
David Darnes

אני דייב, מפתח Front-end בכיר ב-Nordhealth. אני עובד על העיצוב והפיתוח של מערכת העיצוב Nord, שכוללת פיתוח של רכיבי אינטרנט לספריית הרכיבים שלנו. רציתי לשתף איך פתרנו את הבעיות שקשורות לעיצוב רכיבי אינטרנט באמצעות מאפיינים מותאמים אישית של CSS, וכמה מהיתרונות האחרים של שימוש במאפיינים מותאמים אישית במערכות עיצוב ובספריות של רכיבים.

איך אנחנו יוצרים רכיבי אינטרנט

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


import {html, css, LitElement} from 'lit';

export class SimpleGreeting extends LitElement {
  static styles = css`:host { color: blue; font-family: sans-serif; }`;

  static properties = {
    name: {type: String},
  };

  constructor() {
    super();
    this.name = 'there';
  }

  render() {
    return html`

Hey ${this.name}, welcome to Web Components!

`
; } } customElements.define('simple-greeting', SimpleGreeting);
רכיב אינטרנט שנכתב באמצעות Lit.

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

אנקפסולציה של סגנון Shadow DOM

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

ה-Shadow DOM שנבדק בכלי הפיתוח.
דוגמה ל-Shadow DOM ברכיב קלט טקסט רגיל וברכיב האינטרנט של קלט Nord.

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

ההכלה הזו של סגנונות היא יתרון בספריית הרכיבים שלנו. הוא מבטיח שכאשר מישהו משתמש באחד מהרכיבים שלנו, הוא ייראה כמו שהתכוונו, בלי קשר לסגנונות שחלים על דף ההורה. כדי לוודא שהכל תקין, אנחנו מוסיפים את all: unset; לשורש, או ל "מארח", של כל רכיבי האינטרנט שלנו.


:host {
  all: unset;
  display: block;
  box-sizing: border-box;
  text-align: start;
  /* ... */
}
קוד שחוזר על עצמו (boilerplate) של רכיב מסוים שמוחל על שורש הצל (shadow root) או על בורר המארח (host selector).

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

כאן נכנסים לתמונה מאפיינים מותאמים אישית של CSS.

מאפייני CSS בהתאמה אישית

מאפיינים מותאמים אישית הם מאפייני CSS שאתם יכולים לתת להם כל שם שתרצו ולהחיל עליהם כל ערך שצריך. הדרישה היחידה היא להוסיף להם קידומת של שני מקפים. אחרי שמצהירים על המאפיין המותאם אישית, אפשר להשתמש בערך שלו ב-CSS באמצעות הפונקציה var().


:root {
  --n-color-accent: rgb(53, 89, 199);
  /* ... */
}

.n-color-accent-text {
  color: var(--n-color-accent);
}
דוגמה מ-CSS Framework שלנו לאסימון עיצוב כמאפיין מותאם אישית, והשימוש בו במחלקת עזר.

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

היכולת הזו לרשת מאפיינים מותאמים אישית, באמצעות הפונקציה var(), היא הדרך שבה אנחנו חודרים דרך ה-Shadow DOM של רכיבי האינטרנט שלנו ומאפשרים למפתחים שליטה מפורטת יותר בעיצוב הרכיבים שלנו.

מאפיינים מותאמים אישית ברכיב אינטרנט של Nord

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


:root {
  --n-space-m: 16px;
  --n-space-l: 24px;
  /* ... */
  --n-color-background: rgb(255, 255, 255);
  --n-color-border: rgb(216, 222, 228);
  /* ... */
}
מאפייני CSS מותאמים אישית שמוגדרים בסלקטור הבסיסי.

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


:host {
  --n-tab-group-padding: 0;
  --n-tab-list-background: var(--n-color-background);
  --n-tab-list-border: inset 0 -1px 0 0 var(--n-color-border);
  /* ... */
}

.n-tab-group-list {
  box-shadow: var(--n-tab-list-border);
  background-color: var(--n-tab-list-background);
  gap: var(--n-space-s);
  /* ... */
}
מאפיינים מותאמים אישית שמוגדרים ב-shadow root של הרכיב, ואז משמשים בסגנונות של הרכיב. נעשה שימוש גם במאפיינים מותאמים אישית מרשימת אסימוני העיצוב.

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


.n-tab-group-list::before {
  /* ... */
  padding-inline-start: var(--n-tab-group-padding);
}

.n-tab-group-list::after {
  /* ... */
  padding-inline-end: var(--n-tab-group-padding);
}
המאפיין המותאם אישית של המרווח הפנימי של קבוצת הכרטיסיות משמש בכמה מקומות בקוד הרכיב.

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


:host([padding="l"]) {
  --n-tab-group-padding: var(--n-space-l);
}
וריאציה של רכיב הכרטיסייה שבה המרווח הפנימי משתנה באמצעות עדכון אחד של מאפיין מותאם אישית במקום כמה עדכונים.

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


<nord-tab-group label="Title">
  <!-- ... -->
</nord-tab-group>

<style>
  nord-tab-group {
    --n-tab-group-padding: var(--n-space-xl);
  }
</style>
שימוש ברכיב של קבוצת הכרטיסיות בדף ועדכון המאפיין המותאם אישית של המרווח הפנימי לגודל גדול יותר.

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

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

שימושים מתקדמים במאפיינים מותאמים אישית

בזמן כתיבת המאמר הזה, אנחנו לא חושפים את המאפיינים המותאמים אישית האלה בהקשר במסמכי התיעוד שלנו. עם זאת, אנחנו מתכננים לעשות את זה כדי שצוות הפיתוח הרחב יותר שלנו יוכל להבין את המאפיינים האלה ולהשתמש בהם. הרכיבים שלנו ארוזים ב-npm עם קובץ מניפסט, שמכיל את כל מה שצריך לדעת עליהם. לאחר מכן, אנחנו צורכים את קובץ המניפסט כנתונים כשאנחנו פורסים את אתר התיעוד שלנו, באמצעות Eleventy והתכונה Global Data שלו. אנחנו מתכננים לכלול את המאפיינים המותאמים אישית האלה בהקשר בקובץ המניפסט הזה.

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


<nord-divider></nord-divider>

<section>
  <nord-divider></nord-divider>
   <!-- ... -->
</section>

<style>
  nord-divider {
    --n-divider-color: var(--n-color-status-danger);
  }

  section {
    padding: var(--n-space-s);
    background: var(--n-color-surface-raised);
  }
  
  section nord-divider {
    --n-divider-color: var(--n-color-status-success);
  }
</style>
שני מופעים של רכיב המפריד שלנו שצריך להגדיר להם שני צבעים שונים. אחד מהם מקונן בתוך קטע שאנחנו יכולים להשתמש בו כדי ליצור סלקטור ספציפי יותר, אבל אנחנו צריכים לטרגט את המפריד באופן ספציפי.

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

מאפיינים מותאמים אישית פרטיים וציבוריים

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



:host {
  --_n-divider-color: var(--n-divider-color, var(--n-color-border));
  --_n-divider-size: var(--n-divider-size, 1px);
}

.n-divider {
  border-block-start: solid var(--_n-divider-size) var(--_n-divider-color);
  /* ... */
}
ה-CSS של רכיב האינטרנט של המפריד עם המאפיינים המותאמים אישית לפי ההקשר, כך שה-CSS הפנימי מסתמך על מאפיין מותאם אישית פרטי, שהוגדר כמאפיין מותאם אישית ציבורי עם חלופה.

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


<nord-divider></nord-divider>

<section>
  <nord-divider></nord-divider>
   <!-- ... -->
</section>

<style>
  nord-divider {
    --n-divider-color: var(--n-color-status-danger);
  }

  section {
    padding: var(--n-space-s);
    background: var(--n-color-surface-raised);
    --n-divider-color: var(--n-color-status-success);
  }
</style>
שני המפרידים שוב, אבל הפעם אפשר לשנות את הצבע של המפריד על ידי הוספת המאפיין המותאם אישית לפי ההקשר של המפריד לבורר הקטעים. הקו המפריד יירש את המאפיין הזה, וכך ייווצר קטע קוד נקי וגמיש יותר.

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

אני מקווה שהתובנות האלה לגבי האופן שבו אנחנו משתמשים ברכיבי אינטרנט עם מאפיינים מותאמים אישית של CSS היו שימושיות. נשמח לשמוע מה דעתכם, ואם תחליטו להשתמש באחת מהשיטות האלה בעבודה שלכם, תוכלו למצוא אותי ב-X (לשעבר טוויטר) בכתובת ‎@DavidDarnes. אתם יכולים למצוא את Nordhealth ב-Twitter ב-‎@NordhealthHQ, וגם את שאר חברי הצוות שלי, שעבדו קשה כדי ליצור את מערכת העיצוב הזו ולהטמיע את התכונות שמוזכרות במסמך הזה: ‎@Viljamis,‏ ‎@WickyNilliams ו-‎@eric_habich.