פקדי טפסים משופרים

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

Arthur Evans

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

עם זאת, יכול להיות שיהיה קשה לשחזר את התכונות של פקדי הטפסים המובנים ב-HTML. אלה כמה מהתכונות שמקבל אלמנט <input> באופן אוטומטי כשמוסיפים אותו לטופס:

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

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

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

  • האירוע formdata מאפשר לאובייקט JavaScript שרירותי להשתתף בשליחת הטופס, כך שאפשר להוסיף נתוני טפסים בלי להשתמש ב-<input> מוסתר.
  • ה-API של רכיבים מותאמים אישית המשויכים ל-Forms מאפשר לרכיבים מותאמים אישית לפעול יותר כמו פקדים מובנים בטופס.

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

ממשק API מבוסס-אירועים

האירוע formdata הוא ממשק API ברמה נמוכה שמאפשר לכל קוד JavaScript להשתתף בשליחת טופס. כך פועל המנגנון:

  1. מוסיפים מאזין לאירועים מסוג formdata לטופס שאליו רוצים לבצע אינטראקציה.
  2. כשמשתמש לוחץ על לחצן השליחה, הטופס מפעיל אירוע formdata שכולל אובייקט FormData שמכיל את כל הנתונים שנשלחים.
  3. לכל מאזין של formdata יש הזדמנות להוסיף לנתונים או לשנות אותם לפני שליחת הטופס.

דוגמה לשליחת ערך יחיד בבורר אירועים מסוג formdata:

const form = document.querySelector('form');
// FormData event is sent on <form> submission, before transmission.
// The event has a formData property
form.addEventListener('formdata', ({formData}) => {
  // https://developer.mozilla.org/docs/Web/API/FormData
  formData.append('my-input', myInputValue);
});

אתם יכולים לנסות את זה באמצעות הדוגמה שלנו ב-Glitch. חשוב להריץ אותו ב-Chrome 77 ואילך כדי לראות את ה-API בפעולה.

תאימות דפדפן

תמיכה בדפדפנים

  • Chrome: ‏ 5.
  • Edge:‏ 12.
  • Firefox: 4.
  • Safari: 5.

מקור

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

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

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

  • כשמקפידים להציב רכיב בהתאמה אישית שמשויך לטופס בתוך <form>, הוא משויך באופן אוטומטי לטופס, כמו רכיב בקרה שמסופק על ידי הדפדפן.
  • אפשר לתייג את הרכיב באמצעות רכיב <label>.
  • הרכיב יכול להגדיר ערך שיישלח באופן אוטומטי עם הטופס.
  • הרכיב יכול להגדיר דגל שמציין אם יש לו קלט חוקי או לא. אם אחד מאמצעי הבקרה של הטופס מכיל קלט לא תקין, לא ניתן לשלוח את הטופס.
  • הרכיב יכול לספק קריאות חזרה (callbacks) לחלקים שונים במחזור החיים של הטופס – למשל, כשהטופס מושבת או מתאפס למצב ברירת המחדל שלו.
  • הרכיב תומך בפסאודו-כיתות רגילות של CSS לפקדי טפסים, כמו :disabled ו-:invalid.

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

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

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

  • מוסיפים מאפיין formAssociated סטטי לכיתה של הרכיב המותאם אישית. כך הדפדפן יתייחס לרכיב כאל רכיב בקרה בטופס.
  • מפעילים את ה-method attachInternals() ברכיב כדי לקבל גישה לשיטות ולמאפיינים נוספים של פקדי טופס, כמו setFormValue() ו-setValidity().
  • מוסיפים את המאפיינים והשיטות הנפוצים שנתמכים בפקדי טפסים, כמו name,‏ value ו-validity.

כך הפריטים האלה משתלבים בהגדרה בסיסית של רכיב מותאם אישית:

// Form-associated custom elements must be autonomous custom elements--
// meaning they must extend HTMLElement, not one of its subclasses.
class MyCounter extends HTMLElement {

  // Identify the element as a form-associated custom element
  static formAssociated = true;

  constructor() {
    super();
    // Get access to the internal form control APIs
    this.internals_ = this.attachInternals();
    // internal value for this control
    this.value_ = 0;
  }

  // Form controls usually expose a "value" property
  get value() { return this.value_; }
  set value(v) { this.value_ = v; }

  // The following properties and methods aren't strictly required,
  // but browser-level form controls provide them. Providing them helps
  // ensure consistency with browser-provided controls.
  get form() { return this.internals_.form; }
  get name() { return this.getAttribute('name'); }
  get type() { return this.localName; }
  get validity() {return this.internals_.validity; }
  get validationMessage() {return this.internals_.validationMessage; }
  get willValidate() {return this.internals_.willValidate; }

  checkValidity() { return this.internals_.checkValidity(); }
  reportValidity() {return this.internals_.reportValidity(); }

  
}
customElements.define('my-counter', MyCounter);

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

<form>
  <label>Number of bunnies: <my-counter></my-counter></label>
  <button type="submit">Submit</button>
</form>

הגדרת ערך

ה-method attachInternals() מחזיר אובייקט ElementInternals שמספק גישה לממשקי API של רכיבי בקרה בטופס. השיטה הבסיסית ביותר מביניהן היא setFormValue(), שקובעת את הערך הנוכחי של הבקרה.

השיטה setFormValue() יכולה לקבל אחד משלושת סוגי הערכים:

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

כדי להגדיר ערך פשוט:

this.internals_.setFormValue(this.value_);

כדי להגדיר כמה ערכים, אפשר לבצע את הפעולה הבאה:

// Use the control's name as the base name for submitted data
const n = this.getAttribute('name');
const entries = new FormData();
entries.append(n + '-first-name', this.firstName_);
entries.append(n + '-last-name', this.lastName_);
this.internals_.setFormValue(entries);

אימות קלט

אמצעי הבקרה יכול להשתתף גם באימות טפסים על ידי קריאה ל-method setValidity() באובייקט הפנימי.

// Assume this is called whenever the internal value is updated
onUpdateValue() {
  if (!this.matches(':disabled') && this.hasAttribute('required') &&
      this.value_ < 0) {
    this.internals_.setValidity({customError: true}, 'Value cannot be negative.');
  }
  else {
    this.internals_.setValidity({});
  }
  this.internals.setFormValue(this.value_);
}

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

קריאה חוזרת (callback) במחזור החיים

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

void formAssociatedCallback(form)

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

void formDisabledCallback(disabled)

נקראת אחרי שמצב disabled של הרכיב משתנה, כי המאפיין disabled של הרכיב הזה נוסף או הוסר, או כי המצב disabled השתנה ב-<fieldset> שהוא ישות האב של הרכיב הזה. הפרמטר disabled מייצג את הסטטוס החדש 'מושבת' של הרכיב. לדוגמה, הרכיב עשוי להשבית רכיבים ב-DOM הצל שלו כשהוא מושבת.

void formResetCallback()

הקריאה מתבצעת אחרי איפוס הטופס. הרכיב אמור לאפס את עצמו למצב ברירת מחדל כלשהו. ברכיבי <input>, בדרך כלל צריך להגדיר את המאפיין value כך שיתאים למאפיין value שהוגדר ב-Markup (או במקרה של תיבת סימון, להגדיר את המאפיין checked כך שיתאים למאפיין checked.

void formStateRestoreCallback(state, mode)

הקריאה מתבצעת באחת משתי נסיבות:

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

סוג הארגומנט הראשון תלוי באופן הקריאה ל-method setFormValue(). פרטים נוספים זמינים במאמר שחזור מצב טופס.

מתבצע שחזור של מצב הטופס

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

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

this.internals_.setFormValue(value, state);

השדה value מייצג את הערך שאפשר לשלוח מהפקד. הפרמטר האופציונלי state הוא ייצוג פנימי של מצב אמצעי הבקרה, שיכול לכלול נתונים שלא נשלחים לשרת. הפרמטר state יכול לקבל את אותם סוגים כמו הפרמטר value – הוא יכול להיות מחרוזת, אובייקט File או אובייקט FormData.

הפרמטר state שימושי כאשר אי אפשר לשחזר מצב של אמצעי בקרה על סמך הערך בלבד. לדוגמה, נניח שאתם יוצרים בוחר צבעים עם מספר מצבים: לוח צבעים או גלגל צבעי RGB. הערך שאפשר לשלוח יהיה הצבע שנבחר בפורמט קנוני, כמו "#7fff00". עם זאת, כדי לשחזר את אמצעי הבקרה למצב ספציפי, צריך גם לדעת באיזה מצב הוא היה, כך שהמצב עשוי להיראות כמו "palette/#7fff00".

this.internals_.setFormValue(this.value_,
    this.mode_ + '/' + this.value_);

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

formStateRestoreCallback(state, mode) {
  if (mode == 'restore') {
    // expects a state parameter in the form 'controlMode/value'
    [controlMode, value] = state.split('/');
    this.mode_ = controlMode;
    this.value_ = value;
  }
  // Chrome currently doesn't handle autofill for form-associated
  // custom elements. In the autofill case, you might need to handle
  // a raw value.
}

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

formStateRestoreCallback(state, mode) {
  // Simple case, restore the saved value
  this.value_ = state;
}

דוגמה עובדת

בדוגמה הבאה מוצגות תכונות רבות של אלמנטים מותאמים אישית שמשויכים לטופס. חשוב להריץ אותו ב-Chrome 77 ואילך כדי לראות את ה-API בפעולה.

זיהוי תכונות

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

if ('FormDataEvent' in window) {
  // formdata event is supported
}

if ('ElementInternals' in window &&
    'setFormValue' in window.ElementInternals.prototype) {
  // Form-associated custom elements are supported
}

סיכום

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

האירוע formdata לא מספק יכולות חדשות, אבל הוא מספק ממשק להוספת נתוני הטופס לתהליך השליחה, בלי צורך ליצור רכיב <input> מוסתר.

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

התמונה הראשית (Hero) היא של Oudom Pravat ב-Unsplash.