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

Arthur Evans

פורסם: 8 באוגוסט 2019

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

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

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

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

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

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

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

Event-based API

Browser Support

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

Source

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

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

דוגמה לשליחת ערך יחיד ב-formdata event listener:

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);
});

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

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

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

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

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

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

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

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

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

// Form-associated custom elements must be autonomous custom elements.
// 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);

אימות קלט

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

קריאות חוזרות במחזור חיים

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

void formAssociatedCallback(form)

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

void formDisabledCallback(disabled)

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

לדוגמה, יכול להיות שהרכיב ישבית רכיבים ב-DOM של הצל שלו כשהוא מושבת.

void formResetCallback()

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

void formStateRestoreCallback(state, mode)

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

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

הסוג של הארגומנט הראשון תלוי באופן הקריאה של השיטה 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 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;
}

זיהוי תכונות

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

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