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

אל תשתמשו ב-<input> מוסתר כדי להציג ערך של פקד מותאם אישית בטופס. השתמשו ברכיבים מותאמים אישית שמשויכים לטופס (FACE): באמצעות formAssociated ו-ElementInternals, הרכיב שולח את הערך שלו, משתתף באימות ומשקף את מצב הטופס באופן מקורי.

class RatingInput extends HTMLElement {
  // MANDATORY: opt in. Without this, attachInternals() form APIs are unavailable.
  static formAssociated = true;

  #internals;
  #value = '';

  constructor() {
    super();
    this.#internals = this.attachInternals();
    this.attachShadow({ mode: 'open', delegatesFocus: true });
  }

  connectedCallback() {
    // Validate initial state once attributes (such as `required`) are present.
    this.#validate();
  }

  get value() { return this.#value; }
  set value(v) {
    this.#value = v;
    // Push the value into the owning <form> for submission.
    this.#internals.setFormValue(v);
    this.#validate();
  }

  #validate() {
    if (this.hasAttribute('required') && !this.#value) {
      // Mark invalid and supply the native validation message + anchor.
      this.#internals.setValidity(
        { valueMissing: true },
        'Please choose a rating.',
        this.shadowRoot.querySelector('[tabindex]'),
      );
    } else {
      this.#internals.setValidity({}); // clear
    }
  }

  // Optional FACE lifecycle hooks:
  formResetCallback() { this.value = ''; }
  formDisabledCallback(disabled) {
    // Disable internal controls so they leave the tab order and ignore clicks.
    // Do NOT toggle `disabled` on `this`, or a `<fieldset disabled>` toggle will
    // leave the host permanently stuck with its own `disabled` attribute.
    for (const el of this.shadowRoot.querySelectorAll('button')) {
      el.disabled = disabled;
    }
  }
  formStateRestoreCallback(state) { this.value = state; }
}
customElements.define('rating-input', RatingInput);

נקודות עיקריות:

  • השדה static formAssociated = true הוא חובה. הוא פותח את השיטות של ElementInternals שקשורות לטופס ואת מחזור החיים של formXxxCallback.
  • השדה setFormValue(value) הוא השדה שנשלח, ואין צורך בקלט מוסתר. מעבירים FormData לפקדים עם כמה ערכים.
  • ‫setValidity(flags, message, anchor) משתלב עם אימות אילוצים מקורי: הטופס לא יישלח אם הוא לא תקין, חלים :invalid/:valid, ורכיב העוגן מקבל מיקוד בדוח. כדי למחוק, צריך להתקשר אל setValidity({}). המחלקות המדומות :user-valid/:user-invalid שמודעות לאינטראקציה לא מקושרות באופן עקבי לרכיבים שמשויכים לטפסים בדפדפנים שונים. חשוב לוודא שיש תמיכה לפני שמסתמכים עליהן, ולא להניח שהן עוקבות אחרי האינטראקציה של המשתמשים כמו שהן עושות בפקדים מקוריים.
  • צריך להטמיע את formResetCallback, formDisabledCallback ו-formStateRestoreCallback כדי שהרכיב יתנהג כמו פקד מקורי באיפוס, ב-fieldset[disabled] ובשחזור של מילוי אוטומטי קדימה/אחורה. ב-formDisabledCallback(disabled), צריך להשבית את אמצעי הבקרה הפנימיים (או להסיר אותם מסדר הכרטיסיות ולהתעלם מהקלט) במקום להגדיר את disabled במארח – הגדרת מאפיין המארח בתוך הקריאה החוזרת (callback) מונעת הפעלה מחדש כשמפעילים מחדש את <fieldset> הקודם, והמארח כבר תואם ל-:disabled באופן אוטומטי.
  • צריך לפעול לפי המוסכמות של הפלטפורמה לגבי אירועים שנשלחים, ולהעדיף את האירועים המובנים input ו-change על פני אירועים בהתאמה אישית. האירוע input מופעל כשהערך משתנה כתוצאה ישירה מפעולת המשתמש (הקלדה, גרירת פס הזזה). האירוע change מופעל פעם אחת בכל רצף של שינויים, כשהמשתמש מבצע את הפעולה (שחרור פס ההזזה, טשטוש השדה). אפשר ליצור אותם באמצעות InputEvent ו-Event בהתאמה.

אסטרטגיות חלופיות

לפני שמסתמכים על התכונה, צריך לזהות אותה באמצעות תמיכה ב-static formAssociated: 'attachInternals' in HTMLElement.prototype && 'setFormValue' in ElementInternals.prototype. אם הוא לא זמין, צריך לחזור ל-<input> מקורי שמוסתר באופן חזותי ומסונכרן עם הערך של הפקד, כדי שהטופס עדיין יישלח – המקרה היחיד שבו התבנית של הקלט המוסתר מוצדקת.