עבודה עם רכיבים מותאמים אישית

מבוא

האינטרנט ממש חסר ביטוי. כדי להבין למה התכוונתי, הציץ באפליקציית אינטרנט "מודרנית" כמו GMail:

Gmail

אין שום דבר מודרני במרק <div>. ועדיין, כך אנחנו בונים אפליקציות אינטרנט. זה עצוב. לא היינו צריכים לדרוש יותר מהפלטפורמה שלנו?

תגי עיצוב סקסיים. רוצה להפוך את זה לסיפור?

HTML הוא כלי מצוין לבניית מסמך, אבל אוצר המילים שלו מוגבל לרכיבים שתקן HTML מגדיר.

מה אם הסימון עבור Gmail לא היה זוועה? מה אם היה יפה:

<hangout-module>
    <hangout-chat from="Paul, Addy">
    <hangout-discussion>
        <hangout-message from="Paul" profile="profile.png"
            profile="118075919496626375791" datetime="2013-07-17T12:02">
        <p>Feelin' this Web Components thing.
        <p>Heard of it?
        </hangout-message>
    </hangout-discussion>
    </hangout-chat>
    <hangout-chat>...</hangout-chat>
</hangout-module>

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

איך מתחילים

Custom Elements (רכיבים מותאמים אישית) מאפשרים למפתחי אתרים להגדיר סוגים חדשים של רכיבי HTML. המפרט הוא אחד מעקרונות ה-API החדשים שנחתמו תחת המטרייה רכיבי אינטרנט, אבל סביר להניח שהוא החשוב ביותר. רכיבי אינטרנט לא קיימים בלי התכונות שאפשר לבטל את הנעילה שלהן על ידי רכיבים מותאמים אישית:

  1. להגדיר רכיבי HTML/DOM חדשים
  2. יצירת רכיבים שמתרחבים מרכיבים אחרים
  3. קיבוץ לוגי של פונקציונליות מותאמת אישית בתג אחד
  4. הרחבת ה-API של רכיבי DOM קיימים

רישום רכיבים חדשים

רכיבים מותאמים אישית נוצרים באמצעות document.registerElement():

var XFoo = document.registerElement('x-foo');
document.body.appendChild(new XFoo());

הארגומנט הראשון של document.registerElement() הוא שם התג של הרכיב. השם חייב להכיל מקף (-). לדוגמה, <x-tags>, <my-element> ו-<my-awesome-app> הם שמות חוקיים, בעוד <tabs> ו-<foo_bar> לא. ההגבלה הזו מאפשרת למנתח להבחין בין רכיבים מותאמים אישית לרכיבים רגילים, אבל גם מבטיחה תאימות להעברה כשמוסיפים תגים חדשים ל-HTML.

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

כברירת מחדל, רכיבים מותאמים אישית יורשים מ-HTMLElement. כלומר, הדוגמה הקודמת מקבילה ל:

var XFoo = document.registerElement('x-foo', {
    prototype: Object.create(HTMLElement.prototype)
});

קריאה ל-document.registerElement('x-foo') מלמדת את הדפדפן על האלמנט החדש, ומחזירה בנאי שבו אפשר להשתמש כדי ליצור מופעים של <x-foo>. לחלופין, אפשר להשתמש בשיטות האחרות של יצירת רכיבים אם אתם לא רוצים להשתמש ב-constructor.

רכיבים מרחיבים

רכיבים מותאמים אישית מאפשרים להרחיב רכיבי HTML (מקוריים) קיימים וגם רכיבים מותאמים אישית אחרים. כדי להרחיב רכיב, צריך להעביר ל-registerElement() את השם ואת prototype של הרכיב שרוצים לרשת ממנו.

הרחבת רכיבים מקוריים

נניח שאינך מרוצה מג'ו <button>. תרצו להעצים את היכולות שלו כדי להפוך ל"לחצן מגה". כדי להרחיב את האלמנט <button>, צריך ליצור רכיב חדש שיורש את prototype של HTMLButtonElement ואת שם האלמנט extends. במקרה הזה, "button":

var MegaButton = document.registerElement('mega-button', {
    prototype: Object.create(HTMLButtonElement.prototype),
    extends: 'button'
});

רכיבים מותאמים אישית שיורשים מרכיבים מותאמים נקראים רכיבים מותאמים אישית של תוסף מסוג תוסף. הם יורשים מגרסה מיוחדת של HTMLElement, למשל, "רכיב X הוא Y".

דוגמה:

<button is="mega-button">

הרחבת רכיב מותאם אישית

כדי ליצור רכיב <x-foo-extended> שמקיף את האלמנט המותאם אישית <x-foo>, פשוט מחליפים את אב הטיפוס שלו ואומרים מאיזה תג אתם יורשים:

var XFooProto = Object.create(HTMLElement.prototype);
...

var XFooExtended = document.registerElement('x-foo-extended', {
    prototype: XFooProto,
    extends: 'x-foo'
});

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

איך משדרגים רכיבים

האם תהיתם אי פעם שמנתח ה-HTML לא מבצע התאמה לתגים לא סטנדרטיים? לדוגמה, נשמח מאוד אם נכריז על <randomtag> בדף. על פי מפרט HTML:

סליחה, <randomtag>! החשבון שלך לא סטנדרטי ועובר בירושה מהHTMLUnknownElement.

הדבר לא נכון לגבי רכיבים מותאמים אישית. רכיבים עם שמות חוקיים של רכיבים מותאמים אישית יורשים מהרכיב HTMLElement. אפשר לאמת את העובדה הזו על ידי הפעלת המסוף: Ctrl + Shift + J (או Cmd + Opt + J ב-Mac), ומדביקים את שורות הקוד הבאות. הן מחזירות true:

// "tabs" is not a valid custom element name
document.createElement('tabs').__proto__ === HTMLUnknownElement.prototype

// "x-tabs" is a valid custom element name
document.createElement('x-tabs').__proto__ == HTMLElement.prototype

רכיבים לא מזוהים

מכיוון שרכיבים מותאמים אישית רשומים באמצעות סקריפט באמצעות document.registerElement(), אפשר להצהיר או ליצור אותם לפני שההגדרה שלהם מתעדת על ידי הדפדפן. לדוגמה, אפשר להצהיר על <x-tabs> בדף, אבל בסוף התהליך document.registerElement('x-tabs') יופעל.

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

הטבלה הבאה יכולה לעזור לכם לשמור על סדר:

שם עובר בירושה מ- דוגמאות
רכיב לא נפתר HTMLElement <x-tabs>, <my-element>
רכיב לא ידוע HTMLUnknownElement <tabs>, <foo_bar>

יצירת רכיבים

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

יצירת תגים מותאמים אישית

הצהירו עליהם:

<x-foo></x-foo>

יוצרים DOM ב-JS:

var xFoo = document.createElement('x-foo');
xFoo.addEventListener('click', function(e) {
    alert('Thanks!');
});

צריך להשתמש באופרטור new:

var xFoo = new XFoo();
document.body.appendChild(xFoo);

יצירת מופע של רכיבי תוסף מסוג

יצירת אלמנטים מותאמים אישית בסגנון תוסף דומה מאוד לתגים מותאמים אישית.

הצהירו עליהם:

<!-- <button> "is a" mega button -->
<button is="mega-button">

יוצרים DOM ב-JS:

var megaButton = document.createElement('button', 'mega-button');
// megaButton instanceof MegaButton === true

כמו שאפשר לראות, יש עכשיו גרסה עמוסה מדי של document.createElement() שמשתמשת במאפיין is="" כפרמטר השני.

צריך להשתמש באופרטור new:

var megaButton = new MegaButton();
document.body.appendChild(megaButton);

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

הוספת מאפיינים ושיטות של JS

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

הנה דוגמה מלאה:

var XFooProto = Object.create(HTMLElement.prototype);

// 1. Give x-foo a foo() method.
XFooProto.foo = function() {
    alert('foo() called');
};

// 2. Define a property read-only "bar".
Object.defineProperty(XFooProto, "bar", {value: 5});

// 3. Register x-foo's definition.
var XFoo = document.registerElement('x-foo', {prototype: XFooProto});

// 4. Instantiate an x-foo.
var xfoo = document.createElement('x-foo');

// 5. Add it to the page.
document.body.appendChild(xfoo);

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

var XFoo = document.registerElement('x-foo', {
  prototype: Object.create(HTMLElement.prototype, {
    bar: {
      get: function () {
        return 5;
      }
    },
    foo: {
      value: function () {
        alert('foo() called');
      }
    }
  })
});

הפורמט הראשון מאפשר להשתמש ב-ES5 Object.defineProperty. השיטה השנייה מאפשרת להשתמש ב-get/set.

שיטות להתקשרות חזרה במחזור החיים

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

שם השיחה החוזרת התקשרה כאשר
createdCallback נוצר מופע של הרכיב
attachedCallback נוספה מופע למסמך
detachedCallback הוסר מופע מהמסמך
attributeChangedCallback(attrName, oldVal, newVal) מאפיין נוסף, הוסר או עודכן

דוגמה: הגדרה של createdCallback() ו-attachedCallback() ב-<x-foo>:

var proto = Object.create(HTMLElement.prototype);

proto.createdCallback = function() {...};
proto.attachedCallback = function() {...};

var XFoo = document.registerElement('x-foo', {prototype: proto});

כל הקריאות החוזרות (callback) במחזור החיים הן אופציונליות, אבל יש להגדיר אותן אם וכאשר הגיוני להשתמש בהן. לדוגמה, נניח שהרכיב מורכב מספיק ופותח חיבור אל IndexedDB ב-createdCallback(). לפני שמסירים אותו מה-DOM, צריך לבצע את פעולות הניקוי הנדרשות ב-detachedCallback(). הערה: לא כדאי להסתמך על כך, לדוגמה, אם המשתמש סוגר את הכרטיסייה, אבל אפשר לחשוב על זה בתור פעולה אפשרית לאופטימיזציה.

תרחישי שימוש נוספים במחזור החיים של מחזור החיים הם להגדרת ברירת מחדל של פונקציות event listener באלמנט:

proto.createdCallback = function() {
  this.addEventListener('click', function(e) {
    alert('Thanks!');
  });
};

הוספת תגי עיצוב

יצרנו את <x-foo>, נתנו לו API ל-JavaScript, אבל הוא ריק! האם ניתן לו קוד HTML לעיבוד?

התקשרות חזרה במחזור החיים שימושית כאן. באופן ספציפי, אנחנו יכולים להשתמש ב-createdCallback() כדי להעניק לרכיב קוד HTML שמוגדר כברירת מחדל:

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
    this.innerHTML = "**I'm an x-foo-with-markup!**";
};

var XFoo = document.registerElement('x-foo-with-markup', {prototype: XFooProto});

יצירת התג הזה ובדיקה בכלי הפיתוח (לחיצה ימנית, יש לבחור באפשרות 'בדיקת רכיב') אמורה להופיע:

▾<x-foo-with-markup>
  **I'm an x-foo-with-markup!**
</x-foo-with-markup>

קידוד הרכיבים הפנימיים ב-DOM DOM

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

DOM של צל מספק רכיבים מותאמים אישית:

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

יצירת רכיב מ-DOM של Shadow דומה ליצירה של רכיב שמעבד תגי עיצוב בסיסיים. ההפרש הוא ב-createdCallback():

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
    // 1. Attach a shadow root on the element.
    var shadow = this.createShadowRoot();

    // 2. Fill it with markup goodness.
    shadow.innerHTML = "**I'm in the element's Shadow DOM!**";
};

var XFoo = document.registerElement('x-foo-shadowdom', {prototype: XFooProto});

במקום להגדיר את ה-.innerHTML של האלמנט, יצרתי בסיס של Shadow Root עבור <x-foo-shadowdom> ולאחר מכן מילאתי אותו בתגי עיצוב. כשההגדרה 'Show Shadow DOM' מופעלת בכלי הפיתוח, מופיע #shadow-root שאפשר להרחיב:

▾<x-foo-shadowdom>
  ▾#shadow-root
    **I'm in the element's Shadow DOM!**
</x-foo-shadowdom>

זה שורש הצל!

יצירת רכיבים מתבנית

תבניות HTML הן עוד ממשק API קלאסי שמתאים במיוחד לעולם של אלמנטים מותאמים אישית.

דוגמה: רישום רכיב שנוצר מ-DOM <template> ו-Shadow:

<template id="sdtemplate">
  <style>
    p { color: orange; }
  </style>
  <p>I'm in Shadow DOM. My markup was stamped from a <template&gt;.
</template>

<script>
  var proto = Object.create(HTMLElement.prototype, {
    createdCallback: {
      value: function() {
        var t = document.querySelector('#sdtemplate');
        var clone = document.importNode(t.content, true);
        this.createShadowRoot().appendChild(clone);
      }
    }
  });
  document.registerElement('x-foo-from-template', {prototype: proto});
</script>

<template id="sdtemplate">
  <style>:host p { color: orange; }</style>
  <p>I'm in Shadow DOM. My markup was stamped from a <template&gt;.
</template>

<div class="demoarea">
  <x-foo-from-template></x-foo-from-template>
</div>

כמה שורות הקוד האלה מכניסות הרבה מאמץ. כדי להבין את כל מה שקורה:

  1. רשמנו רכיב חדש ב-HTML: <x-foo-from-template>
  2. ה-DOM של הרכיב נוצר מ-<template>
  3. הפרטים המפחידים של הרכיב מוסתרים באמצעות DOM של צל
  4. DOM של צל מעניק אנקפסולציה של סגנון הרכיב (למשל, p {color: orange;} לא הופך את הדף כולו לכתום)

טוב מאוד!

עיצוב של רכיבים מותאמים אישית

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

<style>
  app-panel {
    display: flex;
  }
  [is="x-item"] {
    transition: opacity 400ms ease-in-out;
    opacity: 0.3;
    flex: 1;
    text-align: center;
    border-radius: 50%;
  }
  [is="x-item"]:hover {
    opacity: 1.0;
    background: rgb(255, 0, 255);
    color: white;
  }
  app-panel > [is="x-item"] {
    padding: 5px;
    list-style: none;
    margin: 0 7px;
  }
</style>

<app-panel>
    <li is="x-item">Do</li>
    <li is="x-item">Re</li>
    <li is="x-item">Mi</li>
</app-panel>

רכיבי עיצוב עם DOM של צל

חור הארנב נעים הרבה יותר לעומק כשמשלבים את Shadow DOM. רכיבים מותאמים אישית שמשתמשים ב-DOM של צל יורשים את היתרונות הנהדרים שלו.

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

עיצוב DOM מסוג Shadow הוא נושא עצום! למידע נוסף בנושא, מומלץ לקרוא על כמה מהמאמרים האחרים שלי:

מניעת FOUC באמצעות :unresolved

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

דוגמה: מעבר הדרגתי לתגי "x-foo" כשהם רשומים:

<style>
  x-foo {
    opacity: 1;
    transition: opacity 300ms;
  }
  x-foo:unresolved {
    opacity: 0;
  }
</style>

חשוב לזכור ש-:unresolved חל רק על רכיבים לא מזוהים, ולא על רכיבים שיורשים מ-HTMLUnknownElement (למידע נוסף, ראו איך רכיבים משודרגים).

<style>
  /* apply a dashed border to all unresolved elements */
  :unresolved {
    border: 1px dashed red;
    display: inline-block;
  }
  /* x-panel's that are unresolved are red */
  x-panel:unresolved {
    color: red;
  }
  /* once the definition of x-panel is registered, it becomes green */
  x-panel {
    color: green;
    display: block;
    padding: 5px;
    display: block;
  }
</style>

<panel>
    I'm black because :unresolved doesn't apply to "panel".
    It's not a valid custom element name.
</panel>

<x-panel>I'm red because I match x-panel:unresolved.</x-panel>

תמיכה בהיסטוריה ובדפדפן

זיהוי תכונות

זיהוי התכונות הוא בדיקה אם קיים document.registerElement():

function supportsCustomElements() {
    return 'registerElement' in document;
}

if (supportsCustomElements()) {
    // Good to go!
} else {
    // Use other libraries to create components.
}

תמיכת דפדפן

האתר document.registerElement() התחיל לנחות בהתחלה מאחורי דגל ב-Chrome 27 וב-Firefox בסביבות 23. עם זאת, המפרט התפתח לא מעט מאז. Chrome 31 הוא הראשון שיש לו תמיכה אמיתית במפרט המעודכן.

עד שהתמיכה בדפדפן תהיה מעולה, יש polyfill שמשמש את פולימר של Google ואת X-Tag של Mozilla.

מה קרה ל-HTMLElementElement?

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

<element name="my-element">
    ...
</element>

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

חשוב לציין שפולימר מטמיע טופס הצהרתי של רישום אלמנט עם <polymer-element>. איך עושים את זה? היא מבוססת על document.registerElement('polymer-element') ועל השיטות שתיארתי בקטע יצירת רכיבים מתבנית.

סיכום

רכיבים מותאמים אישית נותנים לנו את הכלי להרחיב את אוצר המילים של HTML, ללמד אותו טריקים חדשים ולדלג בין חורי התולעים של פלטפורמת האינטרנט. נשלב אותם עם עקרונות הפלטפורמה החדשים האחרים, כמו Shadow DOM ו-<template>, כדי להבין את התמונה של רכיבי האינטרנט. כיתובים יכולים להיות שוב סקסיים!

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