איך Nordhealth משתמשת בנכסים מותאמים אישית ברכיבי האינטרנט

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

David Darnes
David Darnes

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

כדי ליצור את רכיבי ה-Web שלנו, אנחנו משתמשים ב-Lit, ספרייה שמספקת הרבה קוד סטנדרטי כמו מצב, סגנונות ברמת ההיקף, תבניות ועוד. 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.

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

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

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

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

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

האנקפסולציה של הסגנונות היא יתרון בספריית הרכיבים שלנו. כך אנחנו יכולים להבטיח יותר שכשמישהו ישתמש באחד מהרכיבים שלנו, הוא ייראה כמו שרצינו, ללא קשר לסגנונות שחלים על דף ההורה. כדי לוודא זאת, אנחנו מוסיפים את all: unset; לשורש (root) או ל'מארח' של כל רכיבי ה-Web שלנו.


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

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

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

מאפיינים מותאמים אישית של CSS

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


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

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

לגבי ירושה, כל המאפיינים המותאמים אישית עוברים בירושה, בהתאם להתנהגות הרגילה של ערכים ומאפיינים רגילים של 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);
  /* ... */
}
מאפיינים מותאמים אישית שמוגדרים ברמה של שורש הצל של הרכיב, ולאחר מכן נעשה בהם שימוש בסגנונות של הרכיב. נעשה גם שימוש במאפיינים מותאמים אישית מרשימת האסימונים של העיצוב.

בנוסף, נבצע הפשטה של ערכים מסוימים שספציפיים לרכיב אבל לא נמצאים באסימונים שלנו, ונהפוך אותם לנכס מותאם אישית לפי הקשר. מאפיינים מותאמים אישית שמותאמים להקשר של הרכיב מספקים לנו שני יתרונות עיקריים. ראשית, המשמעות היא שאנחנו יכולים להשתמש ב-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);
}
וריאציה של רכיב הכרטיסייה שבה הארגזים הלבנים משתנים באמצעות עדכון יחיד של נכס מותאם אישית, במקום מספר עדכונים.

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


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

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

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

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

שימוש נוסף במאפיינים מותאמים אישית

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

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


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

תמונה ראשית (Hero) של Dan Cristian Pădureț