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

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

David Darnes
David Darnes

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


// TODO: DevSite - Code sample removed as it used inline event handlers
באמצעות רכיב האינטרנט שנוצר למעלה בדף.

מכסה בסגנון DOM של צל

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

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

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

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


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

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

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

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

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


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

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

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

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

מאפיינים מותאמים אישית ברכיב אינטרנט של 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 מוגדרים בבורר הרמה הבסיסית (root).

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

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


<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 שלו. אנחנו מתכננים לכלול את המאפיינים המותאמים אישית לפי הקשר בקובץ המניפסט של הנתונים.

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


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

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