Lit למפתחי React

1. מבוא

מה זה Lit

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

מה תלמדו

איך לתרגם כמה מושגים של React ל-Lit, כמו:

  • ‫JSX ותבניות
  • רכיבים ואביזרים
  • State & Lifecycle
  • תוכן מושך
  • ילדים
  • Refs
  • מצב גישור

מה תפַתחו

בסוף שיעור ה-Codelab הזה תוכלו להמיר מושגים של רכיבי React לאנלוגים שלהם ב-Lit.

הדרישות

  • הגרסה העדכנית של Chrome,‏ Safari,‏ Firefox או Edge.
  • ידע ב-HTML, ב-CSS, ב-JavaScript ובכלי הפיתוח ל-Chrome.
  • ידע ב-React
  • (מתקדם) אם רוצים ליהנות מחוויית הפיתוח הטובה ביותר, מומלץ להוריד את VS Code. תצטרכו גם את lit-plugin ל-VS Code ואת NPM.

2. Lit לעומת React

הרבה מהמושגים והיכולות הבסיסיים של Lit דומים לאלה של React, אבל יש גם כמה הבדלים חשובים:

הוא קטן

‫Lit הוא קטן מאוד: הוא מגיע ל-5kb בערך אחרי מיניפיקציה ודחיסה ב-gzip, לעומת 40kb ומעלה של React + ReactDOM.

תרשים עמודות של גודל החבילה אחרי מיניפיקציה ודחיסה, ב-KB. ספריית Lit היא 5kb וספריית React + React DOM היא 42.2kb

הוא מהיר

במדדים ציבוריים שמשווים בין מערכת התבניות של Lit,‏ lit-html, לבין VDOM של React,‏ lit-html מהירה יותר מ-React ב-8-10% במקרה הגרוע, וב-50%ומעלה בתרחישי השימוש הנפוצים יותר.

‫LitElement (מחלקת הבסיס של הרכיבים ב-Lit) מוסיף תקורה מינימלית ל-lit-html, אבל הביצועים שלו טובים ב-16-30%מאלה של React כשמשווים בין תכונות של רכיבים כמו שימוש בזיכרון, אינטראקציה וזמני הפעלה.

תרשים עמודות מקובץ של ביצועים שמשווה בין lit ל-React באלפיות השנייה (ערך נמוך יותר מצביע על ביצועים טובים יותר)

לא צריך לבנות

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

יכול להיות שאפילו לא תצטרכו NPM כדי להתחיל, אם אתם משתמשים במודולים של ES וב-CDN מודרניים כמו Skypack או UNPKG.

אבל אם רוצים, עדיין אפשר ליצור קוד Lit ולבצע בו אופטימיזציה. האיחוד האחרון של מפתחים סביב מודולי ES מקוריים היה טוב ל-Lit – Lit הוא פשוט JavaScript רגיל ואין צורך בממשקי CLI ספציפיים למסגרות או בטיפול בבנייה.

לא תלוי ב-framework

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

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

צוות Lit עבד על פרויקט ניסיוני בשם @lit-labs/react שינתח באופן אוטומטי את רכיבי Lit וייצור עטיפת React, כך שלא תצטרכו להשתמש בהפניות.

בנוסף, התוסף Custom Elements Everywhere יראה לכם אילו מסגרות וספריות פועלות בצורה טובה עם רכיבים בהתאמה אישית.

תמיכה מלאה ב-TypeScript

אפשר לכתוב את כל קוד Lit ב-JavaScript, אבל קוד Lit נכתב ב-TypeScript וצוות Lit ממליץ למפתחים להשתמש גם ב-TypeScript.

צוות Lit עבד עם קהילת Lit כדי לעזור לתחזק פרויקטים שמביאים בדיקת סוגים של TypeScript ו-IntelliSense לתבניות Lit בזמן הפיתוח ובמשך זמן של תהליך build באמצעות lit-analyzer ו-lit-plugin.

צילום מסך של סביבת פיתוח משולבת (IDE) שבו מוצגת בדיקת סוג לא תקינה להגדרת הערך הבוליאני שמסומן במסגרת למספר

צילום מסך של סביבת פיתוח משולבת (IDE) שמוצגות בה הצעות של IntelliSense

הכלים למפתחים מובנים בדפדפן

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

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

תמונה של כלי הפיתוח של Chrome שבה מוצגות התוצאות הבאות: ‎ $0 returns <mwc-textfield>, ‏ ‎$0.value returns hello world, ‏ ‎$0.outlined returns true, ‏ ו-‎{$0} shows property expansion

הוא מבוסס על עיבוד בצד השרת (SSR)

הספרייה Lit 2 נבנתה עם תמיכה ב-SSR. בזמן כתיבת ה-Codelab הזה, צוות Lit עדיין לא פרסם את כלי ה-SSR בצורה יציבה, אבל הוא כבר פרסם רכיבים שעברו רינדור בצד השרת במוצרי Google, ובדק את ה-SSR באפליקציות React. צוות Lit צפוי לפרסם את הכלים האלה ב-GitHub בקרוב.

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

ההסכמה להשתתפות נמוכה

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

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

בנוסף, הרבה מסגרות מודרניות תומכות בפלט ברכיבי אינטרנט, כך שבדרך כלל הן יכולות להתאים בתוך רכיב Lit.

3. הגדרה ושימוש ב-Playground

יש שתי דרכים לעבוד עם ה-codelab הזה:

  • אפשר לעשות את זה באינטרנט, בדפדפן
  • (מתקדם) אפשר לעשות את זה במחשב המקומי באמצעות VS Code

גישה לקוד

במהלך ה-codelab יוצגו קישורים ל-Lit playground, כמו זה:

הסביבה היא ארגז חול לקוד שפועל באופן מלא בדפדפן. הוא יכול לקמפל ולהריץ קובצי TypeScript ו-JavaScript, והוא יכול גם לפתור באופן אוטומטי ייבוא של מודולים של node. למשל:

// before
import './my-file.js';
import 'lit';

// after
import './my-file.js';
import 'https://cdn.skypack.dev/lit';

אתם יכולים לבצע את כל המדריך ב-Lit playground, ולהשתמש בנקודות הביקורת האלה כנקודות התחלה. אם אתם משתמשים ב-VS Code, אתם יכולים להשתמש בנקודות הבדיקה האלה כדי להוריד את קוד ההתחלה של כל שלב, וגם כדי לבדוק את העבודה שלכם.

הסבר על ממשק המשתמש של Playground

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

צילום מסך של ממשק המשתמש של Lit playground שבו מודגשים הקטעים שבהם תשתמשו ב-codelab הזה.

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

הגדרה של VS Code (מתקדם)

אלה היתרונות של השימוש בהגדרה הזו של VS Code:

  • בדיקת סוג התבנית
  • השלמה אוטומטית ו-IntelliSense של תבניות

אם כבר התקנתם את NPM ואת VS Code (עם הפלאגין lit-plugin) ואתם יודעים איך להשתמש בסביבה הזו, אתם יכולים פשוט להוריד את הפרויקטים האלה ולהתחיל לעבוד איתם. כדי לעשות את זה:

  • לוחצים על לחצן ההורדה
  • מחלצים את התוכן של קובץ ה-tar לספרייה
  • (אם משתמשים ב-TS) מגדירים quick tsconfig שמפיק מודולים של es ו-es2015+‎
  • מתקינים שרת פיתוח שיכול לפתור מפרטי מודולים חשופים (צוות Lit ממליץ על ‎@web/dev-server)
  • מריצים את שרת הפיתוח ופותחים את הדפדפן (אם משתמשים ב-‎ @web/dev-server אפשר להשתמש ב-npx web-dev-server --node-resolve --watch --open)
    • אם משתמשים בדוגמה package.json משתמשים ב-npm run dev

4. ‫JSX ותבניות

בקטע הזה נסביר את היסודות של יצירת תבניות ב-Lit.

תבניות JSX ו-Lit

‫JSX הוא תוסף לתחביר של JavaScript שמאפשר למשתמשי React לכתוב בקלות תבניות בקוד JavaScript שלהם. תבניות Lit משמשות למטרה דומה: להציג את ממשק המשתמש של רכיב כפונקציה של המצב שלו.

תחביר בסיסי

ב-React, אפשר להציג את המחרוזת 'hello world' ב-JSX כך:

import 'react';
import ReactDOM from 'react-dom';

const name = 'Josh Perez';
const element = (
  <>
    <h1>Hello, {name}</h1>
    <div>How are you?</div>
  </>
);

ReactDOM.render(
  element,
  mountNode
);

בדוגמה שלמעלה יש שני רכיבים ומשתנה name כלול. ב-Lit, מבצעים את הפעולות הבאות:

import {html, render} from 'lit';

const name = 'Josh Perez';
const element = html`
  <h1>Hello, ${name}</h1>
  <div>How are you?</div>`;

render(
  element,
  mountNode
);

שימו לב: בתבניות של Lit לא צריך React Fragment כדי לקבץ כמה רכיבים בתבניות.

ב-Lit, התבניות עטופות ב-html tagged template LITeral, וזה המקום שבו Lit מקבל את השם שלו.

ערכי תבנית

תבניות Lit יכולות לקבל תבניות Lit אחרות, שנקראות TemplateResult. לדוגמה, מקיפים את name בתגי הטיה (<i>) ומקיפים אותו במחרוזת תבנית מתויגת הערה: חשוב להשתמש בתו גרש הפוך (`) ולא בתו גרש יחיד (').

import {html, render} from 'lit';

const name = html`<i>Josh Perez</i>`;
const element = html`
  <h1>Hello, ${name}</h1>
  <div>How are you?</div>`;

render(
  element,
  mountNode
);

רכיבי TemplateResultLit יכולים לקבל מערכים, מחרוזות, רכיבי TemplateResultLit אחרים וגם הנחיות.

לדוגמה, נסו להמיר את קוד React הבא ל-Lit:

const itemsToBuy = [
  <li>Bananas</li>,
  <li>oranges</li>,
  <li>apples</li>,
  <li>grapes</li>
];
const element = (
  <>
    <h1>Things to buy:</h1>
    <ol>
      {itemsToBuy}
    </ol>
  </>);

ReactDOM.render(
  element,
  mountNode
);

התשובה:

import {html, render} from 'lit';

const itemsToBuy = [
  html`<li>Bananas</li>`,
  html`<li>oranges</li>`,
  html`<li>apples</li>`,
  html`<li>grapes</li>`
];
const element = html`
  <h1>Things to buy:</h1>
  <ol>
    ${itemsToBuy}
  </ol>`;

render(
  element,
  mountNode
);

העברה והגדרה של מאפיינים

אחד ההבדלים הגדולים בין התחביר של JSX לבין התחביר של Lit הוא תחביר קשירת הנתונים. לדוגמה, נניח שיש קלט React עם קשירות:

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
  <input
      disabled={disabled}
      className={`static-class ${myClass}`}
      defaultValue={value}/>;

ReactDOM.render(
  element,
  mountNode
);

בדוגמה שלמעלה, מוגדר קלט שמבצע את הפעולות הבאות:

  • הגדרת המשתנה disabled (מושבת) למשתנה מוגדר (false במקרה הזה)
  • הגדרת הכיתה ל-static-class בתוספת משתנה (במקרה הזה "static-class my-class")
  • הגדרת ערך ברירת מחדל

ב-Lit, מבצעים את הפעולות הבאות:

import {html, render} from 'lit';

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
  <input
      ?disabled=${disabled}
      class="static-class ${myClass}"
      .value=${value}>`;

render(
  element,
  mountNode
);

בדוגמה של Lit, נוסף קישור בוליאני כדי להפעיל או להשבית את המאפיין disabled.

לאחר מכן, יש קשירה ישירות למאפיין class ולא למאפיין className. אפשר להוסיף כמה קשרים למאפיין class, אלא אם משתמשים בהנחיה classMap, שהיא כלי עזר הצהרתי להחלפת מחלקות.

לבסוף, המאפיין value מוגדר בקלט. בניגוד ל-React, הפעולה הזו לא תגדיר את רכיב הקלט כקריאה בלבד, כי היא פועלת לפי ההטמעה וההתנהגות המקוריות של הקלט.

תחביר של קישור מאפייני Lit

html`<my-element ?attribute-name=${booleanVar}>`;
  • הקידומת ? היא תחביר הקישור להחלפת מאפיין ברכיב
  • שווה ערך ל-inputRef.toggleAttribute('attribute-name', booleanVar)
  • שימושי לרכיבים שמשתמשים ב-disabled כי disabled="false" עדיין נקרא כ-true על ידי ה-DOM כי inputElement.hasAttribute('disabled') === true
html`<my-element .property-name=${anyVar}>`;
  • הקידומת . היא תחביר הקישור להגדרת מאפיין של רכיב
  • שווה ערך ל-inputRef.propertyName = anyVar
  • מתאים להעברת נתונים מורכבים כמו אובייקטים, מערכים או מחלקות
html`<my-element attribute-name=${stringVar}>`;
  • קישור למאפיין של רכיב
  • שווה ערך ל-inputRef.setAttribute('attribute-name', stringVar)
  • מתאים לערכים בסיסיים, לסלקטורים של כללי סגנון ול-querySelectorAll

העברת רכיבי handler

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
  <input
      onClick={() => console.log('click')}
      onChange={e => console.log(e.target.value)} />;

ReactDOM.render(
  element,
  mountNode
);

בדוגמה שלמעלה, מוגדר קלט שמבצע את הפעולות הבאות:

  • רישום המילה click (לחיצה) ביומן כשהמשתמש לוחץ על הקלט
  • רישום ביומן של ערך הקלט כשמשתמש מקליד תו

ב-Lit, מבצעים את הפעולות הבאות:

import {html, render} from 'lit';

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
  <input
      @click=${() => console.log('click')}
      @input=${e => console.log(e.target.value)}>`;

render(
  element,
  mountNode
);

בדוגמה של Lit, יש listener שנוסף לאירוע click עם @click.

בשלב הבא, במקום להשתמש ב-onChange, יש קשירה לinputאירוע המקורי של <input>, כי changeאירוע המקורי מופעל רק ב-blur (React מבצעת הפשטה של האירועים האלה).

תחביר של גורם מטפל באירועים ב-Lit

html`<my-element @event-name=${() => {...}}></my-element>`;
  • הקידומת @ היא תחביר הקישור של רכיב event listener
  • שווה ערך ל-inputRef.addEventListener('event-name', ...)
  • משתמש בשמות אירועים מקוריים של DOM

5. רכיבים ואביזרים

בקטע הזה נסביר על פונקציות ורכיבים של כיתות ב-Lit. בקטעים הבאים נסביר על מצב ועל Hooks.

רכיבים של כיתה ו-LitElement

האלמנט LitElement הוא המקבילה של Lit לרכיב מחלקה ב-React, והמושג 'מאפיינים תגובתיים' ב-Lit הוא שילוב של props ו-state ב-React. לדוגמה:

import React from 'react';
import ReactDOM from 'react-dom';

class Welcome extends React.Component {
  constructor(props) {
    super(props);
    this.state = {name: ''};
  }

  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

const element = <Welcome name="Elliott"/>
ReactDOM.render(
  element,
  mountNode
);

בדוגמה שלמעלה יש רכיב React ש:

  • הצגת name
  • הגדרת ערך ברירת המחדל של name למחרוזת ריקה ("")
  • הקצאה מחדש של name אל "Elliott"

כך עושים את זה ב-LitElement

ב-TypeScript:

import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  @property({type: String})
  name = '';

  render() {
    return html`<h1>Hello, ${this.name}</h1>`
  }
}

ב-JavaScript:

import {LitElement, html} from 'lit';

class WelcomeBanner extends LitElement {
  static get properties() {
    return {
      name: {type: String}
    }
  }

  constructor() {
    super();
    this.name = '';
  }

  render() {
    return html`<h1>Hello, ${this.name}</h1>`
  }
}

customElements.define('welcome-banner', WelcomeBanner);

ובקובץ ה-HTML:

<!-- index.html -->
<head>
  <script type="module" src="./index.js"></script>
</head>
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>

סקירה של מה שקורה בדוגמה שלמעלה:

@property({type: String})
name = '';
  • המאפיין מגדיר מאפיין ציבורי ריאקטיבי – חלק מה-API הציבורי של הרכיב
  • חושף מאפיין (כברירת מחדל) וגם מאפיין ברכיב
  • המאפיין מגדיר איך לתרגם את המאפיין של הרכיב (שהוא מחרוזת) לערך
static get properties() {
  return {
    name: {type: String}
  }
}
  • הפונקציה הזו זהה ל-@property TS decorator, אבל היא פועלת באופן מקורי ב-JavaScript
render() {
  return html`<h1>Hello, ${this.name}</h1>`
}
  • הפונקציה הזו מופעלת בכל פעם שמשנים מאפיין ריאקטיבי כלשהו
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  ...
}
  • הפעולה הזו משייכת שם של תג HTML Element להגדרת מחלקה
  • בגלל התקן Custom Elements, שם התג חייב לכלול מקף (-).
  • ‫this ב-LitElement מתייחס למופע של הרכיב המותאם אישית (<welcome-banner> במקרה הזה)
customElements.define('welcome-banner', WelcomeBanner);
  • זוהי הגרסה המקבילה ב-JavaScript של ה-decorator‏ @customElement ב-TS
<head>
  <script type="module" src="./index.js"></script>
</head>
  • ייבוא ההגדרה של הרכיב המותאם אישית
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>
  • הוספת האלמנט המותאם אישית לדף
  • המאפיין name מוגדר לערך 'Elliott'

רכיבי פונקציה

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

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Elliott"/>
ReactDOM.render(
  element,
  mountNode
);

ב-Lit זה ייראה כך:

import {html, render} from 'lit';

function Welcome(props) {
  return html`<h1>Hello, ${props.name}</h1>`;
}

render(
  Welcome({name: 'Elliott'}),
  document.body.querySelector('#root')
);

6. State & Lifecycle

בקטע הזה נסביר על הסטטוס ומחזור החיים של Lit.

מדינה (State)

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

מאפיינים ריאקטיביים ציבוריים

// React
import React from 'react';

class MyEl extends React.Component {
  constructor(props) {
    super(props)
    this.state = {name: 'there'}
  }

  componentWillReceiveProps(nextProps) {
    if (this.props.name !== nextProps.name) {
      this.setState({name: nextProps.name})
    }
  }
}

// Lit (TS)
import {LitElement} from 'lit';
import {property} from 'lit/decorators.js';

class MyEl extends LitElement {
  @property() name = 'there';
}
  • מוגדר על ידי @property
  • בדומה למאפיינים ולמצב של React, אבל ניתנים לשינוי
  • ‫API ציבורי שהגישה אליו מוגדרת על ידי צרכני הרכיב

מצב ריאקטיבי פנימי

// React
import React from 'react';

class MyEl extends React.Component {
  constructor(props) {
    super(props)
    this.state = {name: 'there'}
  }
}

// Lit (TS)
import {LitElement} from 'lit';
import {state} from 'lit/decorators.js';

class MyEl extends LitElement {
  @state() name = 'there';
}
  • מוגדר על ידי @state
  • דומה למצב ב-React אבל ניתן לשינוי
  • מצב פנימי פרטי שבדרך כלל יש אליו גישה מתוך הרכיב או מחלקות המשנה

מחזור חיים

מחזור החיים של Lit דומה מאוד לזה של React, אבל יש כמה הבדלים חשובים.

constructor

// React (js)
import React from 'react';

class MyEl extends React.Component {
  constructor(props) {
    super(props);
    this.state = { counter: 0 };
    this._privateProp = 'private';
  }
}

// Lit (ts)
class MyEl extends LitElement {
  @property({type: Number}) counter = 0;
  private _privateProp = 'private';
}

// Lit (js)
class MyEl extends LitElement {
  static get properties() {
    return { counter: {type: Number} }
  }
  constructor() {
    this.counter = 0;
    this._privateProp = 'private';
  }
}
  • הערך המקביל ב-Lit הוא גם constructor
  • אין צורך להעביר שום דבר לקריאת הסופר
  • הפעולה בוצעה על ידי (לא כולל את כל האפשרויות):
    • document.createElement
    • document.innerHTML
    • new ComponentClass()
    • אם שם תג שלא שודרג מופיע בדף וההגדרה נטענת ונרשמת ב-@customElement או ב-customElements.define
  • פונקציה דומה ל-constructor ב-React

render

// React
render() {
  return <div>Hello World</div>
}

// Lit
render() {
  return html`<div>Hello World</div>`;
}
  • הערך המקביל ב-Lit הוא גם render
  • יכולה להחזיר כל תוצאה שניתן לעבד, למשל TemplateResult או string וכו'.
  • בדומה ל-React, ‏ render() צריכה להיות פונקציה טהורה
  • הרכיב יוצג בכל צומת שמוחזר על ידי createRenderRoot() (ShadowRoot כברירת מחדל)

componentDidMount

‫componentDidMount דומה לשילוב של שני רכיבי ה-callback של מחזור החיים של Lit:‏ firstUpdated ו-connectedCallback.

firstUpdated

import Chart from 'chart.js';

// React
componentDidMount() {
  this._chart = new Chart(this.chartElRef.current, {...});
}

// Lit
firstUpdated() {
  this._chart = new Chart(this.chartEl, {...});
}
  • מופעל בפעם הראשונה שהתבנית של הרכיב מעובדת ברכיב הבסיס של הרכיב
  • הפונקציה תופעל רק אם הרכיב מחובר, למשל, היא לא תופעל דרך document.createElement('my-component') עד שהצומת יצורף לעץ ה-DOM
  • זהו מקום טוב לבצע הגדרת רכיבים שדורשת את ה-DOM שמעובד על ידי הרכיב
  • בניגוד ל-React, שינויים בנכסים ריאקטיביים ב-firstUpdated יגרמו לרינדור מחדש של componentDidMount, אבל בדרך כלל הדפדפן יקבץ את השינויים לאותו פריים. אם השינויים האלה לא דורשים גישה ל-DOM של שורש, בדרך כלל הם צריכים להיכנס ל-willUpdate

connectedCallback

// React
componentDidMount() {
  this.window.addEventListener('resize', this.boundOnResize);
}

// Lit
connectedCallback() {
  super.connectedCallback();
  this.window.addEventListener('resize', this.boundOnResize);
}
  • הפונקציה מופעלת בכל פעם שהרכיב המותאם אישית מוכנס לעץ ה-DOM
  • בניגוד לרכיבי React, כשמנתקים רכיבים מותאמים אישית מה-DOM, הם לא נהרסים ולכן אפשר 'לחבר' אותם כמה פעמים
    • לא תתבצע יותר שיחה עם firstUpdated
  • שימושי לאתחול מחדש של ה-DOM או לחיבור מחדש של פונקציות event listener שנוקו אחרי ניתוק
  • הערה: יכול להיות שהפונקציה connectedCallback תיקרא לפני firstUpdated, ולכן בפעם הראשונה שהיא תיקרא, יכול להיות שה-DOM לא יהיה זמין.

componentDidUpdate

// React
componentDidUpdate(prevProps) {
  if (this.props.title !== prevProps.title) {
    this._chart.setTitle(this.props.title);
  }
}

// Lit (ts)
updated(prevProps: PropertyValues<this>) {
  if (prevProps.has('title')) {
    this._chart.setTitle(this.title);
  }
}
  • התשובה המקבילה היא updated (באמצעות זמן העבר באנגלית של המילה update)
  • בניגוד ל-React, ‏ updated נקרא גם במהלך הרינדור הראשוני
  • פונקציה דומה ל-componentDidUpdate ב-React

componentWillUnmount

// React
componentWillUnmount() {
  this.window.removeEventListener('resize', this.boundOnResize);
}

// Lit
disconnectedCallback() {
  super.disconnectedCallback();
  this.window.removeEventListener('resize', this.boundOnResize);
}
  • הפונקציה שוות ערך ל-disconnectedCallback
  • בניגוד לרכיבי React, כשמנתקים רכיבים מותאמים אישית מה-DOM, הרכיב לא נהרס
  • בשונה מ-componentWillUnmount, הפונקציה disconnectedCallback נקראת אחרי שהרכיב מוסר מהעץ
  • ה-DOM בתוך הרכיב הבסיסי עדיין מצורף לתת-העץ של הרכיב הבסיסי
  • שימושי לניקוי של פונקציות event listener והפניות שגויות, כדי שהדפדפן יוכל לבצע איסוף אשפה של הרכיב

פעילות גופנית

import React from 'react';
import ReactDOM from 'react-dom';

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

בדוגמה שלמעלה, יש שעון פשוט שמבצע את הפעולות הבאות:

  • הפונקציה מעבדת את המחרוזת 'Hello World!‎' השעה היא" ואז מציג את השעה
  • השעון יתעדכן כל שנייה
  • כשמבטלים את ההרכבה, המרווח מתנקה על ידי קריאה ל-tick

קודם מתחילים עם הצהרת המחלקה של הרכיב:

// Lit (TS)
// some imports here are imported in advance
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';

@customElement('lit-clock')
class LitClock extends LitElement {
}

// Lit (JS)
// `html` is imported in advance
import {LitElement, html} from 'lit';

class LitClock extends LitElement {
}

customElements.define('lit-clock', LitClock);

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

// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';

@customElement('lit-clock')
class LitClock extends LitElement {
  @state() // declares internal reactive prop
  private date = new Date(); // initialization
}

// Lit (JS)
import {LitElement, html} from 'lit';

class LitClock extends LitElement {
  static get properties() {
    return {
      // declares internal reactive prop
      date: {state: true}
    }
  }

  constructor() {
    super();
    // initialization
    this.date = new Date();
  }
}

customElements.define('lit-clock', LitClock);

בשלב הבא, מעבדים את התבנית.

// Lit (JS & TS)
render() {
  return html`
    <div>
      <h1>Hello, World!</h1>
      <h2>It is ${this.date.toLocaleTimeString()}.</h2>
    </div>
  `;
}

עכשיו מטמיעים את ה-method‏ tick.

tick() {
  this.date = new Date();
}

השלב הבא הוא הטמעה של componentDidMount. שוב, המקבילה של Lit היא תערובת של firstUpdated ו-connectedCallback. במקרה של הרכיב הזה, קריאה ל-tick עם setInterval לא דורשת גישה ל-DOM בתוך רכיב הבסיס. בנוסף, פרק הזמן יתבטל כשהרכיב יוסר מעץ המסמך, כך שאם הוא יצורף מחדש, פרק הזמן יצטרך להתחיל מחדש. לכן, connectedCallback היא בחירה טובה יותר במקרה הזה.

// Lit (TS)
@customElement('lit-clock')
class LitClock extends LitElement {
  @state()
  private date = new Date();
  // initialize timerId for TS
  private timerId = -1 as unknown as ReturnType<typeof setTimeout>;

  connectedCallback() {
    super.connectedCallback();
    this.timerId = setInterval(
      () => this.tick(),
      1000
    );
  }

  ...
}

// Lit (JS)
constructor() {
  super();
  // initialization
  this.date = new Date();
  this.timerId = -1; // initialize timerId for JS
}

connectedCallback() {
  super.connectedCallback();
  this.timerId = setInterval(
    () => this.tick(),
    1000
  );
}

לבסוף, מנקים את המרווח כך שהוא לא יבצע את הטיק אחרי שהרכיב מנותק מעץ המסמך.

// Lit (TS & JS)
disconnectedCallback() {
  super.disconnectedCallback();
  clearInterval(this.timerId);
}

הקוד המלא אמור להיראות כך:

// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';

@customElement('lit-clock')
class LitClock extends LitElement {
  @state()
  private date = new Date();
  private timerId = -1 as unknown as ReturnType<typeof setTimeout>;

  connectedCallback() {
    super.connectedCallback();
    this.timerId = setInterval(
      () => this.tick(),
      1000
    );
  }

  tick() {
    this.date = new Date();
  }

  render() {
    return html`
      <div>
        <h1>Hello, World!</h1>
        <h2>It is ${this.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    clearInterval(this.timerId);
  }
}

// Lit (JS)
import {LitElement, html} from 'lit';

class LitClock extends LitElement {
  static get properties() {
    return {
      date: {state: true}
    }
  }

  constructor() {
    super();
    this.date = new Date();
  }

  connectedCallback() {
    super.connectedCallback();
    this.timerId = setInterval(
      () => this.tick(),
      1000
    );
  }

  tick() {
    this.date = new Date();
  }

  render() {
    return html`
      <div>
        <h1>Hello, World!</h1>
        <h2>It is ${this.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    clearInterval(this.timerId);
  }
}

customElements.define('lit-clock', LitClock);

7. תוכן מושך

בקטע הזה תלמדו איך לתרגם מושגי React Hook ל-Lit.

המושגים של React Hooks

ה-hooks של React מספקים דרך לרכיבי פונקציה להתחבר למצב. יש לכך כמה יתרונות.

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

בנוסף, ההתמקדות ברכיבים מבוססי-פונקציות פתרה בעיות מסוימות בתחביר מבוסס-המחלקות של React, כמו:

  • צריך להעביר את props מconstructor אל super
  • האתחול הלא מסודר של מאפיינים ב-constructor
    • זו הייתה סיבה שצוינה על ידי צוות React בזמנו, אבל היא נפתרה על ידי ES2019
  • בעיות שנגרמו בגלל this כבר לא מתייחסות לרכיב

מושגים של React Hooks ב-Lit

כמו שצוין בקטע Components & Props, ‏ Lit לא מציעה דרך ליצור רכיבים בהתאמה אישית מפונקציה, אבל LitElement כן נותנת מענה לרוב הבעיות העיקריות ברכיבי מחלקה של React. לדוגמה:

// React (at the time of making hooks)
import React from 'react';
import ReactDOM from 'react-dom';

class MyEl extends React.Component {
  constructor(props) {
    super(props); // Leaky implementation
    this.state = {count: 0};
    this._chart = null; // Deemed messy
  }

  render() {
    return (
      <>
        <div>Num times clicked {count}</div>
        <button onClick={this.clickCallback}>click me</button>
      </>
    );
  }

  clickCallback() {
    // Errors because `this` no longer refers to the component
    this.setState({count: this.count + 1});
  }
}

// Lit (ts)
class MyEl extends LitElement {
  @property({type: Number}) count = 0; // No need for constructor to set state
  private _chart = null; // Public class fields introduced to JS in 2019

  render() {
    return html`
        <div>Num times clicked ${count}</div>
        <button @click=${this.clickCallback}>click me</button>`;
  }

  private clickCallback() {
    // No error because `this` refers to component
    this.count++;
  }
}

איך Lit פותר את הבעיות האלה?

  • ‫constructor לא מקבלת ארגומנטים
  • כל הקישורים של @event מקושרים אוטומטית אל this
  • ‫this ברוב המקרים מתייחס להפניה של הרכיב המותאם אישית
  • אפשר עכשיו ליצור מופעים של מאפייני כיתה כחברים בכיתה. הפעולה הזו מנקה יישומים שמבוססים על בנאי

Reactive Controllers

המושגים העיקריים שמאחורי Hooks קיימים ב-Lit בתור reactive controllers. דפוסי בקרה תגובתיים מאפשרים שיתוף של לוגיקה עם מצב, פיצול רכיבים לחלקים קטנים יותר ומודולריים יותר, וכן התחברות למחזור החיים של עדכון רכיב.

בקר תגובתי הוא ממשק אובייקט שיכול להתחבר למחזור החיים של עדכון מארח בקר כמו LitElement.

מחזור החיים של ReactiveController ושל reactiveControllerHost הוא:

interface ReactiveController {
  hostConnected(): void;
  hostUpdate(): void;
  hostUpdated(): void;
  hostDisconnected(): void;
}
interface ReactiveControllerHost {
  addController(controller: ReactiveController): void;
  removeController(controller: ReactiveController): void;
  requestUpdate(): void;
  readonly updateComplete: Promise<boolean>;
}

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

import React from 'react';
import ReactDOM from 'react-dom';

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

בדוגמה שלמעלה, יש שעון פשוט שמבצע את הפעולות הבאות:

  • הפונקציה מעבדת את המחרוזת 'Hello World!‎' השעה היא" ואז מציג את השעה
  • השעון יתעדכן כל שנייה
  • כשמבטלים את ההרכבה, המרווח מתנקה על ידי קריאה ל-tick

יצירת ה-scaffolding של הרכיב

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

// Lit (TS) - index.ts
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators.js';

@customElement('my-element')
class MyElement extends LitElement {
  render() {
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${'time to get Lit'}.</h2>
      </div>
    `;
  }
}

// Lit (JS) - index.js
import {LitElement, html} from 'lit';

class MyElement extends LitElement {
  render() {
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${'time to get Lit'}.</h2>
      </div>
    `;
  }
}

customElements.define('my-element', MyElement);

יוצר את אמצעי הבקרה

עכשיו עוברים אל clock.ts ויוצרים כיתה לClockController ומגדירים את constructor:

// Lit (TS) - clock.ts
import {ReactiveController, ReactiveControllerHost} from 'lit';

export class ClockController implements ReactiveController {
  private readonly host: ReactiveControllerHost;

  constructor(host: ReactiveControllerHost) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
  }

  private tick() {
  }

  hostDisconnected() {
  }
}

// Lit (JS) - clock.js
export class ClockController {
  constructor(host) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
  }

  tick() {
  }

  hostDisconnected() {
  }
}

אפשר ליצור בקר ריאקטיבי בכל דרך, כל עוד הוא משתף את הממשק ReactiveController, אבל הצוות של Lit מעדיף להשתמש בתבנית של מחלקה עם constructor שיכולה לקבל ממשק ReactiveControllerHost וגם מאפיינים אחרים שנדרשים לאתחול הבקר, ברוב המקרים הבסיסיים.

עכשיו צריך לתרגם את הקריאות החוזרות (callbacks) של מחזור החיים של React לקריאות חוזרות של בקר. בקצרה:

  • componentDidMount
    • ל-connectedCallback של LitElement
    • לשלט hostConnected
  • ComponentWillUnmount
    • ל-disconnectedCallback של LitElement
    • לשלט hostDisconnected

מידע נוסף על תרגום מחזור החיים של React למחזור החיים של Lit זמין בקטע State & Lifecycle.

לאחר מכן מטמיעים את הקריאה החוזרת hostConnected ואת השיטות tick, ומנקים את המרווח ב-hostDisconnected כמו בדוגמה שבקטע State & Lifecycle (מצב ומחזור חיים).

// Lit (TS) - clock.ts
export class ClockController implements ReactiveController {
  private readonly host: ReactiveControllerHost;
  private interval = 0 as unknown as ReturnType<typeof setTimeout>;
  date = new Date();

  constructor(host: ReactiveControllerHost) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
    this.interval = setInterval(() => this.tick(), 1000);
  }

  private tick() {
    this.date = new Date();
  }

  hostDisconnected() {
    clearInterval(this.interval);
  }
}

// Lit (JS) - clock.js
export class ClockController {
  interval = 0;
  host;
  date = new Date();

  constructor(host) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
    this.interval = setInterval(() => this.tick(), 1000);
  }

  tick() {
    this.date = new Date();
  }

  hostDisconnected() {
    clearInterval(this.interval);
  }
}

שימוש בבקר

כדי להשתמש בבקר השעון, צריך לייבא את הבקר ולעדכן את הרכיב ב-index.ts או ב-index.js.

// Lit (TS) - index.ts
import {LitElement, html, ReactiveController, ReactiveControllerHost} from 'lit';
import {customElement} from 'lit/decorators.js';
import {ClockController} from './clock.js';

@customElement('my-element')
class MyElement extends LitElement {
  private readonly clock = new ClockController(this); // Instantiate

  render() {
    // Use controller
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }
}

// Lit (JS) - index.js
import {LitElement, html} from 'lit';
import {ClockController} from './clock.js';

class MyElement extends LitElement {
  clock = new ClockController(this); // Instantiate

  render() {
    // Use controller
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }
}

customElements.define('my-element', MyElement);

כדי להשתמש בבקר, צריך ליצור מופע של הבקר על ידי העברת הפניה למארח הבקר (שהוא רכיב <my-element>), ואז להשתמש בבקר בשיטת render.

הפעלת עיבוד מחדש בבקר

שימו לב שהשעה מוצגת, אבל היא לא מתעדכנת. הסיבה לכך היא שהבקר מגדיר את התאריך כל שנייה, אבל המארח לא מעדכן אותו. הסיבה לכך היא שהשינוי ב-date מתבצע במחלקה ClockController ולא ברכיב. כלומר, אחרי שמגדירים את date בבקר, צריך להגיד למארח להריץ את מחזור העדכון שלו באמצעות host.requestUpdate().

// Lit (TS & JS) - clock.ts / clock.js
private tick() {
  this.date = new Date();
  this.host.requestUpdate();
}

עכשיו השעון מתקתק!

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

8. ילדים

בקטע הזה נסביר איך משתמשים ב-slots כדי לנהל ילדים ב-Lit.

משבצות זמן וילדים

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

ב-React, רכיבי צאצא עוברים בירושה דרך מאפייני props. משבצת ברירת המחדל היא props.children והפונקציה render מגדירה את המיקום של משבצת ברירת המחדל. לדוגמה:

const MyArticle = (props) => {
 return <article>{props.children}</article>;
};

חשוב לזכור ש-props.children הם רכיבי React ולא רכיבי HTML.

ב-Lit, הילדים מורכבים בפונקציית הרינדור עם רכיבי slot. שימו לב: רכיבי צאצא לא עוברים בירושה באותו אופן כמו ב-React. ב-Lit, רכיבי צאצא הם רכיבי HTML שמוצמדים לרכיבי placeholder. הקובץ המצורף הזה נקרא חיזוי.

@customElement("my-article")
export class MyArticle extends LitElement {
  render() {
    return html`
      <article>
        <slot></slot>
      </article>
   `;
  }
}

כמה חריצים

ב-React, הוספה של כמה רכיבי Slot דומה להעברה של כמה מאפיינים.

const MyArticle = (props) => {
  return (
    <article>
      <header>
        {props.headerChildren}
      </header>
      <section>
        {props.sectionChildren}
      </section>
    </article>
  );
};

באופן דומה, הוספה של עוד רכיבי <slot> יוצרת עוד משבצות ב-Lit. הוגדרו כמה משבצות באמצעות המאפיין name: <slot name="slot-name">. כך הילדים יכולים להצהיר לאיזה משבצת הם ישויכו.

@customElement("my-article")
export class MyArticle extends LitElement {
  render() {
    return html`
      <article>
        <header>
          <slot name="headerChildren"></slot>
        </header>
        <section>
          <slot name="sectionChildren"></slot>
        </section>
      </article>
   `;
  }
}

תוכן ברירת המחדל של יחידת המודעות

משבצות יציגו את עץ המשנה שלהן אם לא מוקצים צמתים למשבצת הזו. כשצמתים מוקרנים למקום מסוים, לא מוצג עץ המשנה של המקום הזה, אלא הצמתים המוקרנים.

@customElement("my-element")
export class MyElement extends LitElement {
  render() {
    return html`
      <section>
        <div>
          <slot name="slotWithDefault">
            <p>
             This message will not be rendered when children are attached to this slot!
            <p>
          </slot>
        </div>
      </section>
   `;
  }
}

הקצאת ילדים למשבצות

ב-React, רכיבי צאצא מוקצים למקומות שמורים באמצעות המאפיינים של רכיב. בדוגמה שלמטה, רכיבי React מועברים למאפיינים headerChildren ו-sectionChildren.

const MyNewsArticle = () => {
 return (
   <MyArticle
     headerChildren={<h3>Extry, Extry! Read all about it!</h3>}
     sectionChildren={<p>Children are props in React!</p>}
   />
 );
};

ב-Lit, הילדים מוקצים למקומות באמצעות המאפיין slot.

@customElement("my-news-article")
export class MyNewsArticle extends LitElement {
  render() {
    return html`
      <my-article>
        <h3 slot="headerChildren">
          Extry, Extry! Read all about it!
        </h3>
        <p slot="sectionChildren">
          Children are composed with slots in Lit!
        </p>
      </my-article>
   `;
  }
}

אם אין משבצת ברירת מחדל (לדוגמה, <slot>) ואין משבצת עם מאפיין name (לדוגמה, <slot name="foo">) שתואם למאפיין slot של צאצאי הרכיב המותאם אישית (לדוגמה, <div slot="foo">), הצומת הזה לא יוקרן ולא יוצג.

9. Refs

לפעמים מפתח צריך לגשת ל-API של HTMLElement.

בקטע הזה מוסבר איך מקבלים הפניות לרכיבים ב-Lit.

React References

רכיב React עובר המרה (transpilation) לסדרה של קריאות לפונקציות שיוצרות DOM וירטואלי כשהן מופעלות. ה-DOM הווירטואלי הזה מפוענח על ידי ReactDOM ומעבד HTMLElements.

ב-React, ‏ Refs הם מקום בזיכרון שמכיל HTMLElement שנוצר.

const RefsExample = (props) => {
 const inputRef = React.useRef(null);
 const onButtonClick = React.useCallback(() => {
   inputRef.current?.focus();
 }, [inputRef]);

 return (
   <div>
     <input type={"text"} ref={inputRef} />
     <br />
     <button onClick={onButtonClick}>
       Click to focus on the input above!
     </button>
   </div>
 );
};

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

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

אחרי הרינדור הראשוני, React תגדיר את inputRef.current ל-HTMLInputElement שנוצר באמצעות המאפיין ref.

הדלקת התכונה 'הפניות' באמצעות @query

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

האלמנט המקביל ל-refs ב-React ב-Lit הוא HTMLElement שמוחזר על ידי העיטורים @query ו-@queryAll.

@customElement("my-element")
export class MyElement extends LitElement {
  @query('input') // Define the query
  inputEl!: HTMLInputElement; // Declare the prop

  // Declare the click event listener
  onButtonClick() {
    // Use the query to focus
    this.inputEl.focus();
  }

  render() {
    return html`
      <input type="text">
      <br />
      <!-- Bind the click listener -->
      <button @click=${this.onButtonClick}>
        Click to focus on the input above!
      </button>
   `;
  }
}

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

  • הגדרת מאפיין ב-MyElement באמצעות ה-decorator‏ @query (יצירת getter עבור HTMLInputElement).
  • מגדיר ומצרף קריאה חוזרת (callback) לאירוע מסוג קליק בשם onButtonClick.
  • הקלט מתמקד בקליק על הלחצן

ב-JavaScript, העיטורים @query ו-@queryAll מבצעים את הפעולות querySelector ו-querySelectorAll בהתאמה. זוהי הגרסה המקבילה ב-JavaScript של @query('input') inputEl!: HTMLInputElement;

get inputEl() {
  return this.renderRoot.querySelector('input');
}

אחרי שרכיב Lit מעביר את התבנית של שיטת render אל הבסיס של my-element, הדקורטור @query מאפשר ל-inputEl להחזיר את רכיב input הראשון שנמצא בבסיס העיבוד. הפונקציה תחזיר null אם @query לא תוכל למצוא את הרכיב שצוין.

אם יש כמה רכיבי input בשורש העיבוד, הפונקציה @queryAll תחזיר רשימה של צמתים.

10. מצב גישור

בקטע הזה תלמדו איך לנהל את המצב בין רכיבים ב-Lit.

רכיבים לשימוש חוזר

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

const CounterButton = (props) => {
  const label = props.step < 0
    ? `- ${-1 * props.step}`
    : `+ ${props.step}`;


  return (
    <button
      onClick={() =>
        props.addToCounter(props.step)}>{label}</button>
  );
};

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

  • יוצר תווית על סמך הערך props.step.
  • הפונקציה מעבדת לחצן עם התווית ‎+step או ‎-step
  • מעדכן את רכיב האב על ידי קריאה ל-props.addToCounter עם props.step כארגומנט בלחיצה

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

@customElement('counter-button')
export class CounterButton extends LitElement {
  @property({type: Number}) step: number = 0;

  onClick() {
    const event = new CustomEvent('update-counter', {
      bubbles: true,
      detail: {
        step: this.step,
      }
    });

    this.dispatchEvent(event);
  }

  render() {
    const label = this.step < 0
      ? `- ${-1 * this.step}`  // "- 1"
      : `+ ${this.step}`;      // "+ 1"

    return html`
      <button @click=${this.onClick}>${label}</button>
    `;
  }
}

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

  • יצירת מאפיין ריאקטיבי step
  • שליחת אירוע מותאם אישית בשם update-counter עם הערך של רכיב step בלחיצה

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

רכיבים ששומרים מצב

ב-React, נהוג להשתמש ב-Hooks כדי לנהל את המצב. אפשר ליצור רכיב MyCounter על ידי שימוש חוזר ברכיב CounterButton. שימו לב איך הערך addToCounter מועבר לשני המקרים של CounterButton.

const MyCounter = (props) => {
 const [counterSum, setCounterSum] = React.useState(0);
 const addToCounter = useCallback(
   (step) => {
     setCounterSum(counterSum + step);
   },
   [counterSum, setCounterSum]
 );

 return (
   <div>
     <h3>&Sigma;: {counterSum}</h3>
     <CounterButton
       step={-1}
       addToCounter={addToCounter} />
     <CounterButton
       step={1}
       addToCounter={addToCounter} />
   </div>
 );
};

בדוגמה שלמעלה:

  • יוצרת מצב count.
  • יוצרת קריאה חוזרת שמוסיפה מספר לcountמצב.
  • ‫CounterButton משתמש ב-addToCounter כדי לעדכן את count ב-step בכל קליק.

אפשר להשיג הטמעה דומה של MyCounter ב-Lit. שימו לב שהערך addToCounter לא מועבר אל counter-button. במקום זאת, פונקציית הקריאה החוזרת משויכת כפונקציית event listener לאירוע @update-counter באלמנט אב.

@customElement("my-counter")
export class MyCounter extends LitElement {
  @property({type: Number}) count = 0;

  addToCounter(e: CustomEvent<{step: number}>) {
    // Get step from detail of event or via @query
    this.count += e.detail.step;
  }

  render() {
    return html`
      <div @update-counter="${this.addToCounter}">
        <h3>&Sigma; ${this.count}</h3>
        <counter-button step="-1"></counter-button>
        <counter-button step="1"></counter-button>
      </div>
    `;
  }
}

בדוגמה שלמעלה:

  • יוצרת מאפיין ריאקטיבי בשם count שיעדכן את הרכיב כשהערך ישתנה
  • קושר את הקריאה החוזרת (callback) של addToCounter אל רכיב ה-event listener של @update-counter
  • מתעדכן count על ידי הוספת הערך שנמצא ב-detail.step של האירוע update-counter
  • הגדרת הערך step של counter-button באמצעות המאפיין step

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

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

11. עיצוב

בקטע הזה מוסבר על סגנונות ב-Lit.

עיצוב

‫Lit מציע כמה דרכים לעיצוב רכיבים, וגם פתרון מובנה.

סגנונות בתוך השורה

‫Lit תומך בסגנונות מוטבעים וגם בקישור אליהם.

import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';

@customElement('my-element')
class MyElement extends LitElement {
  render() {
    return html`
      <div>
        <h1 style="color:orange;">This text is orange</h1>
        <h1 style="color:rebeccapurple;">This text is rebeccapurple</h1>
      </div>
    `;
  }
}

בדוגמה שלמעלה יש 2 כותרות, שלכל אחת מהן יש סגנון מוטבע.

עכשיו מייבאים ומשייכים גבול מ-border-color.js לטקסט הכתום:

...
import borderColor from './border-color.js';

...

html`
  ...
  <h1 style="color:orange;${borderColor}">This text is orange</h1>
  ...`

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

styleMap

styleMap ההוראה מאפשרת להשתמש ב-JavaScript כדי להגדיר סגנונות מוטבעים. לדוגמה:

import {LitElement, html, css} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {styleMap} from 'lit/directives/style-map.js';

@customElement('my-element')
class MyElement extends LitElement {
  @property({type: String})
  color = '#000'

  render() {
    // Define the styleMap
    const headerStyle = styleMap({
      'border-color': this.color,
    });

    return html`
      <div>
        <h1
          style="border-style:solid;
          <!-- Use the styleMap -->
          border-width:2px;${headerStyle}">
          This div has a border color of ${this.color}
        </h1>
        <input
          type="color"
          @input=${e => (this.color = e.target.value)}
          value="#000">
      </div>
    `;
  }
}

בדוגמה שלמעלה:

  • מוצג h1 עם גבול וכלי לבחירת צבעים
  • שינוי הערך של border-color לערך מבוחר הצבעים

בנוסף, יש את styleMap שמשמש להגדרת הסגנונות של h1. התחביר של styleMap דומה לתחביר של קישור מאפיינים ב-React‏ style.

CSSResult

הדרך המומלצת להגדיר סגנון לרכיבים היא באמצעות css tagged template literal.

import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';

const ORANGE = css`orange`;

@customElement('my-element')
class MyElement extends LitElement {
  static styles = [
    css`
      #orange {
        color: ${ORANGE};
      }

      #purple {
        color: rebeccapurple;
      }
    `
  ];

  render() {
    return html`
      <div>
        <h1 id="orange">This text is orange</h1>
        <h1 id="purple">This text is rebeccapurple</h1>
      </div>
    `;
  }
}

בדוגמה שלמעלה:

  • הצהרה על תבנית מילולית מתויגת של CSS עם קשירה
  • הגדרת הצבעים של שני רכיבי h1 עם מזהים

היתרונות של שימוש בתג התבנית css:

  • ניתוח פעם אחת לכל כיתה לעומת פעם אחת לכל מופע
  • הטמעה עם שימוש חוזר במודולים
  • יכולים בקלות להפריד סגנונות לקבצים נפרדים
  • תאימות ל-polyfill של מאפייני CSS בהתאמה אישית

בנוסף, שימו לב לתג <style> ב-index.html:

<!-- index.html -->
<style>
  h1 {
    color: red !important;
  }
</style>

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

תגי סגנון

אפשר גם להוסיף תגי <style> ישירות לתבניות. הדפדפן יבטל את הכפילויות של תגי הסגנון האלה, אבל אם תמקמו אותם בתבניות, הם ינותחו לכל מופע של רכיב, ולא לכל מחלקה כמו במקרה של תבנית עם התג css. בנוסף, ביטול הכפילויות של CSSResult בדפדפן מהיר הרבה יותר.

אפשר גם להשתמש ב-<link rel="stylesheet"> בתבנית כדי להגדיר סגנונות, אבל לא מומלץ לעשות את זה כי זה עלול לגרום להבהוב ראשוני של תוכן לא מעוצב (FOUC).

12. נושאים מתקדמים (אופציונלי)

‫JSX ותבניות

‫Lit ו-Virtual DOM

‫Lit-html לא כולל DOM וירטואלי רגיל שמבצע השוואה בין כל צומת בנפרד. במקום זאת, הוא משתמש בתכונות ביצועים שמוטמעות במפרט של תבנית ליטרלית עם תג ב-ES2015. תבניות ליטרליות עם תג הן מחרוזות של תבניות ליטרליות עם פונקציות תג שמצורפות אליהן.

דוגמה לטמפלט ליטרל:

const str = 'string';
console.log(`This is a template literal ${str}`);

דוגמה לטמפלט ליטרלי עם תג:

const tag = (strings, ...values) => ({strings, values});
const f = (x) => tag`hello ${x} how are you`;
console.log(f('world')); // {strings: ["hello ", " how are you"], values: ["world"]}
console.log(f('world').strings === f(1 + 2).strings); // true

בדוגמה שלמעלה, התג הוא הפונקציה tag והפונקציה f מחזירה הפעלה של תבנית מילולית עם תג.

חלק גדול מהקסם של הביצועים ב-Lit נובע מהעובדה שלמערכי המחרוזות שמועברים לפונקציית התג יש את אותו מצביע (כפי שמוצג ב-console.log השני). הדפדפן לא יוצר מחדש מערך strings חדש בכל הפעלה של פונקציית התג, כי הוא משתמש באותו תבנית מילולית (כלומר באותו מיקום ב-AST). כך, הקישור, הניתוח והטמנת התבניות של Lit יכולים לנצל את התכונות האלה בלי להוסיף הרבה עומס על זמן הריצה.

התנהגות מובנית של הדפדפן לגבי תבניות מילוליות עם תגים מעניקה ל-Lit יתרון משמעותי בביצועים. רוב ה-DOMs הווירטואליים הרגילים מבצעים את רוב העבודה שלהם ב-JavaScript. עם זאת, רוב ההשוואה של תבניות מילוליות עם תגים מתבצעת ב-C++ של הדפדפן.

אם רוצים להתחיל להשתמש ב-HTML tagged template literals עם React או Preact, צוות Lit ממליץ על ספריית htm.

עם זאת, כמו במקרה של אתר Google Codelabs וכמה עורכי קוד באינטרנט, תראו שהדגשת תחביר של תבנית מילולית עם תגים היא לא נפוצה במיוחד. חלק מסביבות הפיתוח המשולבות ועורכי הטקסט תומכים בהם כברירת מחדל, כמו Atom והכלי להדגשת בלוקים של קוד ב-GitHub. צוות Lit עובד בשיתוף פעולה הדוק עם הקהילה כדי לתחזק פרויקטים כמו lit-plugin, שהוא תוסף ל-VS Code שמוסיף הדגשת תחביר, בדיקת סוגים ו-IntelliSense לפרויקטים של Lit.

‫Lit & JSX + React DOM

קוד JSX לא פועל בדפדפן, ובמקום זאת נעשה שימוש במעבד מקדים כדי להמיר את קוד ה-JSX לקריאות לפונקציות JavaScript (בדרך כלל באמצעות Babel).

לדוגמה, Babel יבצע את השינוי הבא:

const element = <div className="title">Hello World!</div>;
ReactDOM.render(element, mountNode);

לזה:

const element = React.createElement('div', {className: 'title'}, 'Hello World!');
ReactDOM.render(element, mountNode);

לאחר מכן, React DOM לוקח את הפלט של React ומתרגם אותו ל-DOM בפועל – מאפיינים, תכונות, מאזיני אירועים וכל השאר.

‫Lit-html משתמשת במחרוזות תבנית מתויגות שיכולות לפעול בדפדפן בלי טרנספילציה או מעבד מקדים. כלומר, כדי להתחיל להשתמש ב-Lit, צריך רק קובץ HTML, סקריפט של מודול ES ושרת. הנה סקריפט שאפשר להריץ בדפדפן:

<!DOCTYPE html>
<html>
  <head>
    <script type="module">
      import {html, render} from 'https://cdn.skypack.dev/lit';

      render(
        html`<div>Hello World!</div>`,
        document.querySelector('.root')
      )
    </script>
  </head>
  <body>
    <div class="root"></div>
  </body>
</html>

בנוסף, מערכת התבניות של Lit,‏ lit-html, לא משתמשת ב-Virtual DOM רגיל, אלא ב-DOM API ישירות. לכן, הגודל של Lit 2 הוא פחות מ-5kb אחרי מיניפיקציה ודחיסה ב-gzip, לעומת React (2.8kb) + react-dom (39.4kb)‎‏ 40kb אחרי מיניפיקציה ודחיסה ב-gzip.

אירועים

‫React משתמשת במערכת אירועים סינתטית. כלומר, בספרייה react-dom צריך להגדיר כל אירוע שישמש כל רכיב, ולספק רכיב event listener שווה ערך בפורמט camelCase לכל סוג של צומת. כתוצאה מכך, ל-JSX אין שיטה להגדרת רכיב event listener לאירוע מותאם אישית, ומפתחים צריכים להשתמש ב-ref ואז להחיל listener באופן אימפרטיבי. כך נוצרת חוויית פיתוח לא אופטימלית כשמשלבים ספריות שלא מיועדות ל-React, ולכן צריך לכתוב רכיב wrapper ספציפי ל-React.

‫Lit-html ניגש ישירות ל-DOM ומשתמש באירועים מקוריים, כך שהוספת מאזינים לאירועים היא פשוטה כמו @event-name=${eventNameListener}. המשמעות היא שפחות ניתוח בזמן ריצה מתבצע כדי להוסיף פונקציות event listener וגם כדי להפעיל אירועים.

רכיבים ואביזרים

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

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

אלה כמה יתרונות של רכיבים מותאמים אישית כמערכת רכיבים:

  • הם מובנים בדפדפן ולא דורשים שימוש בכלים
  • התאמה לכל API של דפדפן מ-innerHTML ומ-document.createElement עד querySelector
  • בדרך כלל אפשר להשתמש בהם בכל המסגרות
  • אפשר לרשום אותו באופן עצלני באמצעות customElements.define ולבצע 'הידרציה' של ה-DOM

אלה כמה חסרונות של רכיבים בהתאמה אישית בהשוואה לרכיבי React:

  • אי אפשר ליצור רכיב בהתאמה אישית בלי להגדיר מחלקה (ולכן אין רכיבים פונקציונליים דמויי JSX)
  • חייב להכיל תג סוגר
    • הערה: למרות הנוחות למפתחים, ספקי דפדפנים בדרך כלל מצטערים על מפרט התגים עם סגירה עצמית, ולכן מפרטים חדשים יותר לא כוללים תגים עם סגירה עצמית
  • הוספת צומת נוסף לעץ ה-DOM, שעלול לגרום לבעיות בפריסה
  • חובה להירשם באמצעות JavaScript

צוות Lit בחר להשתמש ברכיבים בהתאמה אישית במקום במערכת רכיבים בהתאמה אישית, כי הרכיבים בהתאמה אישית מוטמעים בדפדפן. הצוות מאמין שהיתרונות של שימוש ברכיבים בהתאמה אישית בכל המסגרות עולים על היתרונות של שכבת הפשטה של רכיבים. למעשה, המאמצים של צוות Lit בתחום lit-ssr התגברו על הבעיות העיקריות ברישום JavaScript. בנוסף, חברות מסוימות כמו GitHub משתמשות ברישום עצל של רכיבים מותאמים אישית כדי לשפר בהדרגה את הדפים עם אלמנטים אופציונליים.

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

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

לדוגמה – בהינתן ההגדרה הבאה של LitElement:

// data-test.ts
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('data-test')
class DataTest extends LitElement {
  @property({type: Number})
  num = 0;

  @property({attribute: false})
  data = {a: 0, b: null, c: [html`<div>hello</div>`, html`<div>world</div>`]}

  render() {
    return html`
      <div>num + 1 = ${this.num + 1}</div>
      <div>data.a = ${this.data.a}</div>
      <div>data.b = ${this.data.b}</div>
      <div>data.c = ${this.data.c}</div>`;
  }
}

מוגדר מאפיין פרימיטיבי ריאקטיבי num שימיר את ערך המחרוזת של מאפיין ל-number, ואז מוצג מבנה נתונים מורכב עם attribute:false שמשבית את הטיפול במאפיינים של Lit.

כך מעבירים נתונים לרכיב המותאם אישית הזה:

<head>
  <script type="module">
    import './data-test.js'; // loads element definition
    import {html} from './data-test.js';

    const el = document.querySelector('data-test');
    el.data = {
      a: 5,
      b: null,
      c: [html`<div>foo</div>`,html`<div>bar</div>`]
    };
  </script>
</head>
<body>
  <data-test num="5"></data-test>
</body>

State & Lifecycle

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

static getDerivedStateFromProps

אין מקבילה ב-Lit כי גם props וגם state הם מאפייני מחלקה זהים

shouldComponentUpdate

  • הערך המקביל ב-lit הוא shouldUpdate
  • מופעל בעיבוד הראשון, בניגוד ל-React
  • פונקציה דומה ל-shouldComponentUpdate ב-React

getSnapshotBeforeUpdate

ב-Lit, ‏ getSnapshotBeforeUpdate דומה ל-update ול-willUpdate

willUpdate

  • התקשרת לפני update
  • בניגוד ל-getSnapshotBeforeUpdate, הפונקציה willUpdate נקראת לפני render
  • שינויים במאפיינים ריאקטיביים ב-willUpdate לא מפעילים מחדש את מחזור העדכון
  • מקום טוב לחישוב ערכי נכסים שתלויים בנכסים אחרים ומשמשים בשאר תהליך העדכון
  • השיטה הזו נקראת בשרת ב-SSR, ולכן לא מומלץ לגשת ל-DOM כאן

update

  • התקשרת אחרי willUpdate
  • בניגוד ל-getSnapshotBeforeUpdate, הפונקציה update נקראת לפני render
  • שינויים במאפיינים ריאקטיביים ב-update לא מפעילים מחדש את מחזור העדכון אם הם משתנים לפני הקריאה ל-super.update
  • מקום טוב לאיסוף מידע מ-DOM שמקיף את הרכיב לפני שהפלט המעובד מועבר ל-DOM
  • השיטה הזו לא מופעלת בשרת ב-SSR

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

יש כמה קריאות חוזרות (callback) של מחזור החיים שלא הוזכרו בקטע הקודם כי אין להן אנלוגיה ב-React. סוגי המשנה הם:

attributeChangedCallback

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

adoptedCallback

מופעל כשהרכיב מועבר למסמך חדש, למשל מ-HTMLTemplateElement's documentFragment אל document הראשי. הקריאה החוזרת הזו היא גם חלק ממפרט הרכיבים המותאמים אישית, וצריך להשתמש בה רק בתרחישי שימוש מתקדמים שבהם הרכיב משנה מסמכים.

מאפיינים ושיטות אחרים של מחזור החיים

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

updateComplete

זהו Promise שמושלם כשהעדכון של הרכיב מסתיים, כי מחזורי החיים של העדכון והעיבוד הם אסינכרוניים. דוגמה:

async nextButtonClicked() {
  this.step++;
  // Wait for the next "step" state to render
  await this.updateComplete;
  this.dispatchEvent(new Event('step-rendered'));
}

getUpdateComplete

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

class MyElement extends LitElement {
  ...
  async getUpdateComplete() {
    await super.getUpdateComplete();
    await this.myChild.updateComplete;
  }
}

performUpdate

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

hasUpdated

הערך של המאפיין הזה הוא true אם הרכיב עודכן לפחות פעם אחת.

isConnected

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

תרשים של מחזור החיים של עדכון Lit

מחזור החיים של העדכון כולל 3 חלקים:

  • לפני העדכון
  • עדכון
  • אחרי העדכון

לפני העדכון

גרף אציקלי מכוון של צמתים עם שמות של קריאות חוזרות (callback). constructor to requestUpdate. ‫@property ל-Property Setter. ‏attributeChangedCallback ל-Property Setter. הפונקציה Property Setter מפנה אל hasChanged, הפונקציה hasChanged מפנה אל requestUpdate, והפונקציה requestUpdate מפנה אל הגרף הבא של מחזור החיים של העדכון.

אחרי requestUpdate, צפוי עדכון מתוזמן.

עדכון

גרף אציקלי מכוון של צמתים עם שמות של קריאות חוזרות (callback). חץ מהתמונה הקודמת של נקודות במחזור החיים לפני העדכון מצביע על performUpdate. ‏performUpdate מצביע על shouldUpdate. ‏shouldUpdate מצביע על complete update if false (השלמת העדכון אם הערך הוא false) וגם על willUpdate. ‏willUpdate מצביע על update. ‏update מצביע על render וגם על הגרף הבא של מחזור החיים אחרי העדכון. ‏render מצביע גם על הגרף הבא של מחזור החיים אחרי העדכון.

אחרי העדכון

גרף אציקלי מכוון של צמתים עם שמות של קריאות חוזרות (callback). חץ מהתמונה הקודמת של נקודות במחזור החיים של העדכון אל firstUpdated. חץ מ-firstUpdated אל updated. חץ מ-updated אל updateComplete.

תוכן מושך

למה כדאי להשתמש בטיזרים

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

בקשות API, ווים ובקרים

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

  • index.tsx
    • הצגת טקסט
    • הצגת התשובה של useAPI
      • מזהה משתמש + שם משתמש
      • הודעת השגיאה
        • שגיאה 404 כשהמשתמש מגיע למשתמש 11 (כך זה אמור להיות)
        • שגיאת ביטול אם אחזור הנתונים מה-API בוטל
      • טוען הודעה
    • התוסף הזה מעבד כפתור פעולה
      • המשתמש הבא: מאחזר את ה-API עבור המשתמש הבא
      • ביטול: מבטל את האחזור של ה-API ומציג שגיאה
  • useApi.tsx
    • הגדרת וו מותאם אישית useApi
    • תהיה אפשרות לאחזר אובייקט משתמש באופן אסינכרוני מ-API
    • פלט:
      • שם משתמש
      • אם הטעינה של האחזור מתבצעת
      • הודעות שגיאה
      • קריאה חוזרת לביטול האחזור
    • מבטל את האחזורים שמתבצעים אם הרכיב מוסר

כאן אפשר לראות את ההטמעה של Lit + Reactive Controller.

מסקנות:

  • רכיבי בקרה תגובתיים דומים יותר ל-custom hooks
  • העברת נתונים שלא ניתן לעבד בין קריאות חוזרות (callback) ואפקטים
    • ‫React משתמש ב-useRef כדי להעביר נתונים בין useEffect לבין useCallback
    • ‫Lit משתמש במאפיין פרטי של מחלקה
    • התגובה בעצם מחקה את ההתנהגות של מאפיין מחלקה פרטית

בנוסף, אם אתם מאוד אוהבים את התחביר של רכיבי הפונקציה של React עם Hooks, אבל אתם רוצים את אותו סביבה ללא בנייה של Lit, צוות Lit ממליץ מאוד על ספריית Haunted.

ילדים

מיקום ברירת מחדל

אם לא מציינים תג slot לרכיבי HTML, הם מוקצים למקום ברירת המחדל ללא שם. בדוגמה שלמטה, הפונקציה MyApp תציב פסקה אחת במשבצת שנקראת. הפסקה השנייה תוגדר כברירת מחדל למקום ללא שם".

‫

@customElement("my-element")
export class MyElement extends LitElement {
  render() {
    return html`
      <section>
        <div>
          <slot></slot>
        </div>
        <div>
          <slot name="custom-slot"></slot>
        </div>
      </section>
   `;
  }
}

@customElement("my-app")
export class MyApp extends LitElement {
  render() {
    return html`
      <my-element>
        <p slot="custom-slot">
          This paragraph will be placed in the custom-slot!
        </p>
        <p>
          This paragraph will be placed in the unnamed default slot!
        </p>
      </my-element>
   `;
  }
}

עדכונים לגבי משבצות

כשמשתנה המבנה של צאצאים של משבצת, מופעל אירוע slotchange. רכיב Lit יכול לקשור event listener לאירוע slotchange. בדוגמה שלמטה, המשבצת הראשונה שנמצאה ב-shadowRoot תתועד ביומן של assignedNodes במסוף ב-slotchange.

@customElement("my-element")
export class MyElement extends LitElement {
  onSlotChange(e: Event) {
    const slot = this.shadowRoot.querySelector('slot');
    console.log(slot.assignedNodes({flatten: true}));
  }

  render() {
    return html`
      <section>
        <div>
          <slot @slotchange="{this.onSlotChange}"></slot>
        </div>
      </section>
   `;
  }
}

Refs

יצירת קובץ עזר

גם Lit וגם React חושפות הפניה ל-HTMLElement אחרי שהפונקציות render שלהן נקראות. אבל כדאי לבדוק איך React ו-Lit מרכיבים את ה-DOM שמוחזר מאוחר יותר באמצעות Lit @query decorator או React Reference.

‫React הוא צינור פונקציונלי שיוצר רכיבי React ולא רכיבי HTMLElements. מכיוון ש-Ref מוצהר לפני ש-HTMLElement מעובד, מוקצה מקום בזיכרון. זו הסיבה לכך ש-null הוא הערך הראשוני של Ref, כי רכיב ה-DOM בפועל עדיין לא נוצר (או עבר רינדור), כלומר useRef(null).

אחרי ש-ReactDOM ממיר רכיב React ל-HTMLElement, הוא מחפש מאפיין בשם ref ב-ReactComponent. אם יש אפשרות, ReactDOM ממקם את ההפניה של HTMLElement אל ref.current.

‫LitElement משתמש בפונקציית תג התבנית html מ-lit-html כדי ליצור רכיב תבנית מתחת לפני השטח. ‫LitElement מטביע את התוכן של התבנית בshadow DOM של רכיב מותאם אישית אחרי העיבוד. ‫Shadow DOM הוא עץ DOM בהיקף מוגדר שמכוסה על ידי shadow root. הדקורטור @query יוצר getter למאפיין, שבעצם מבצע this.shadowRoot.querySelector על שורש ההיקף.

שאילתה של כמה רכיבים

בדוגמה שלמטה, העיטור @queryAll יחזיר את שתי הפסקאות ב-shadow root בתור NodeList.

@customElement("my-element")
export class MyElement extends LitElement {
  @queryAll('p')
  paragraphs!: NodeList;

  render() {
    return html`
      <p>Hello, world!</p>
      <p>How are you?</p>
   `;
  }
}

בעצם, @queryAll יוצרת getter ל-paragraphs שמחזיר את התוצאות של this.shadowRoot.querySelectorAll(). ב-JavaScript, אפשר להצהיר על getter כדי לבצע את אותה מטרה:

get paragraphs() {
  return this.renderRoot.querySelectorAll('p');
}

אלמנטים לשינוי שאילתה

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

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

@customElement("my-dissappearing-paragraph")
export class MyDisapppearingParagraph extends LitElement {
  @queryAsync('p')
  paragraph!: Promise<HTMLElement>;

  renderParagraph() {
    const randomNumber = Math.floor(Math.random() * 10)
    if (randomNumber % 2 === 0) {
      return "";
    }

    return html`<p>This checkbox is checked!`
  }

  render() {
    return html`
      ${this.renderParagraph()}
   `;
  }
}

מצב גישור

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

מצב חיצוני

אפשר להשתמש ב-Redux, ב-MobX או בכל ספרייה אחרת לניהול מצב לצד Lit.

רכיבי Lit נוצרים בהיקף הדפדפן. לכן, כל ספרייה שקיימת גם בהיקף הדפדפן זמינה ל-Lit. נוצרו הרבה ספריות מדהימות כדי להשתמש במערכות קיימות לניהול מצב ב-Lit.

בסדרת הסרטונים הזו של Vaadin מוסבר איך להשתמש ב-Redux ברכיב Lit.

כדאי לעיין ב-lit-mobx של Adobe כדי לראות איך אתר בקנה מידה גדול יכול להפיק תועלת מ-MobX ב-Lit.

כדאי גם לעיין ב-Apollo Elements כדי לראות איך מפתחים משלבים GraphQL ברכיבי האינטרנט שלהם.

‫Lit פועל עם תכונות מובנות של הדפדפן, ואפשר להשתמש ברכיב Lit ברוב הפתרונות לניהול מצב בהיקף הדפדפן.

עיצוב

Shadow DOM

כדי להצפין סגנונות ו-DOM באופן מקורי בתוך רכיב מותאם אישית, Lit משתמש ב-Shadow DOM. התוסף Shadow Roots יוצר עץ צללים נפרד מעץ המסמך הראשי. המשמעות היא שרוב הסגנונות מוגבלים למסמך הזה. חלק מהסגנונות חודרים, כמו צבע וסגנונות אחרים שקשורים לגופן.

בנוסף, Shadow DOM מציג מושגים וסלקטורים חדשים במפרט CSS:

:host, :host(:hover), :host([hover]) {
  /* Styles the element in which the shadow root is attached to */
}

slot[name="title"]::slotted(*), slot::slotted(:hover), slot::slotted([hover]) {
  /*
   * Styles the elements projected into a slot element. NOTE: the spec only allows
   * styling the direcly slotted elements. Children of those elements are not stylable.
   */
}

שיתוף סגנונות

‫Lit מאפשר לשתף בקלות סגנונות בין רכיבים בצורה של CSSTemplateResults באמצעות תגי תבנית של css. לדוגמה:

// typography.ts
export const body1 = css`
  .body1 {
    ...
  }
`;

// my-el.ts
import {body1} from './typography.ts';

@customElement('my-el')
class MyEl Extends {
  static get styles = [
    body1,
    css`/* local styles come after so they will override bod1 */`
  ]

  render() {
    return html`<div class="body1">...</div>`
  }
}

בחירת עיצוב

שורשי צל יוצרים קצת בעיות בשימוש בערכות נושא רגילות, שמתבססות בדרך כלל על גישות של תגי סגנון מלמעלה למטה. הדרך המקובלת לטפל בערכות נושא באמצעות רכיבי אינטרנט שמשתמשים ב-Shadow DOM היא לחשוף API של סגנון באמצעות מאפייני CSS מותאמים אישית. לדוגמה, זו תבנית שמשמשת ב-Material Design:

.mdc-textfield-outline {
  border-color: var(--mdc-theme-primary, /* default value */ #...);
}
.mdc-textfield--input {
  caret-color: var(--mdc-theme-primary, #...);
}

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

html {
  --mdc-theme-primary: #F00;
}
html[dark] {
  --mdc-theme-primary: #F88;
}

אם אתם חייבים להשתמש בשיטת עיצוב מלמעלה למטה ולא יכולים לחשוף סגנונות, תמיד אפשר להשבית את Shadow DOM על ידי החלפת createRenderRoot בהחזרת this. כך התבנית של הרכיבים תעבור רינדור לרכיב המותאם אישית עצמו ולא ל-Shadow Root שמצורף לרכיב המותאם אישית. אם תעשו את זה, לא תוכלו יותר להשתמש ב: style encapsulation, ‏ DOM encapsulation ו-slots.

ייצור

IE 11

אם אתם צריכים לתמוך בדפדפנים ישנים יותר כמו IE 11, תצטרכו לטעון כמה polyfills, שגודלם הוא כ-33kb נוספים. מידע נוסף מפורט כאן.

חבילות מותנות

צוות Lit ממליץ להציג שני חבילות שונות, אחת לדפדפן IE 11 ואחת לדפדפנים מודרניים. יש לכך כמה יתרונות:

  • הצגת ES 6 מהירה יותר ותציג את רוב הלקוחות שלכם
  • הידור של ES 5 מגדיל משמעותית את גודל החבילה
  • חבילות מותנות מאפשרות לכם ליהנות מהיתרונות של שני העולמות
    • תמיכה ב-IE 11
    • אין האטה בדפדפנים מודרניים

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