שלוש דוגמאות lit כדי לראות איך זה עובד

12/06/2024

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

1. דוגמה 1 - קומפוננטת Counter

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

https://lit.dev/playground/#gist=aae47ec2f637d596504c4b1debe447e8

זה הקוד:

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

@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
  static styles = css`p { color: blue }`;

  @property()
  counter = 0;

  render() {
    return html`
        <p>Hello, ${this.counter}</p>
        <button @click=${() => this.counter++}>+1</button>
        <button @click=${() => this.counter--}>-1</button>
        `;
  }
}

כן זה מזכיר את ריאקט ו vue וכל פריימוורק אחר שאפשר לדמיין. מתחת לפני השטח הפריימוורק הוא ריאקטיבי (ללא Virtual DOM), כלומר כש counter משתנה באופן אוטומטי ליט הולך ל DOM ומעדכן רק את שני השדות שמשתמשים בערך שלו.

הטיפול ב styles גם מעניין ונותן פיתרון למשהו שמסבך את כולם, למרות שאישית אני אשאר עם tailwind שפותר את כל הבעיות בעיניי טוב יותר.

2. דוגמה 2 - רשימה עם הוספת פריטים

דוגמה שניה היא רשימה שמטפלת באירועי הוספת פריטים. הקוד בקישור: https://lit.dev/playground/#gist=5462bb49778066b70d31c4d69a85711d.

וזה הקוד:

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

@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
  static styles = css`p { color: blue }`;

  @property()
  messages = ['Hello World!'];

  addMessage(e) {
    e.preventDefault();
    const input = e.target.querySelector('input[name="newitem"]')
    const text = input.value;
    if (text != '') {
      this.messages = [...this.messages, text];
      input.value = '';
    }
  }

  clear() {
    this.messages = [];
  }

  render() {
    return html`
        <div>
        <form @submit=${this.addMessage}>
          <label>New Message:
          <input type="text" name="newitem" />
          <button>Add</button>
          <input type="reset" value="Clear" @click=${this.clear} />
          </label>
        </form>
        <ul>
          ${this.messages.map(m => html`<li>${m}</li>`)}
        </ul>
        </div>
        `;
  }
}

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

3. דוגמה 3 - חיבור שתי קומפוננטות בעמוד

בדוגמה האחרונה רציתי לבנות שתי קומפוננטות ולהעביר מידע ביניהן. הדוגמה כאן: https://lit.dev/playground/#gist=e2a3073eda6074adf160685aa62ffccc וזה הקוד שלה:

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

@customElement('my-listitem')
export class MyListitem extends LitElement {  
  @property({type: Boolean})
  header = false;

  handleDelete() {
    const event = new Event('delete', {bubbles: true, composed: true});
    this.dispatchEvent(event);
  }

  render() {
    if (this.header) {
      return html`<li>---<slot></slot>---</li>`
    } else {
      return html`<li><slot></slot><button @click=${this.handleDelete}>X</button></li>`
    }    
  }
}


@customElement('my-list')
export class MyList extends LitElement {
  static styles = css`p { color: blue }`;

  @property()
  messages = ['Hello World!', 'a', 'b', 'c'];

  addMessage(e) {
    e.preventDefault();
    const input = e.target.querySelector('input[name="newitem"]')
    const text = input.value;
    if (text != '') {
      this.messages = [...this.messages, text];
      input.value = '';
    }
  }

  clear() {
    this.messages = [];
  }

  deleteItem(toDelete, e) {
    const target = e.composedPath()[0];
    console.log(target);
    this.messages = this.messages.filter((msg, index) => index !== toDelete)
  }

  render() {
    return html`
        <div>
        <form @submit=${this.addMessage}>
          <label>New Message:
          <input type="text" name="newitem" />
          <button>Add</button>
          <input type="reset" value="Clear" @click=${this.clear} />
          </label>
        </form>
        <ul>
          ${this.messages.map((m, i) => html`<my-listitem
            @delete=${this.deleteItem.bind(this, i)}
            ?header=${i === 0}>${m}</my-listitem>`)}
        </ul>
        </div>
        `;
  }
}

המנגנונים החדשים בקצרה:

  1. אפשר להעביר ערכים ל property גם מבחוץ. לבאים מריאקט הערבוב הזה בין State ל Property נראה מבלבל. ראיתי בתיעוד שאפשר להגדיר משתנים ריאקטיביים פרטיים וציבוריים בשביל לעשות סדר אבל לא ניסיתי.

  2. האלמנט slot מאפשר לשים את "הילדים" בתוך אלמנט. יש גם אפשרות להגיע ממש לאלמנטים של הילדים ואפשר לכתוב כמה slot-ים ואז לקבוע לאיזה slot כל ילד ילך, הכל לפי הממשק של Shadow DOM.

  3. כשקומפוננטה יוצרת קומפוננטה אחרת היא יכולה להעביר אליה מידע דרך properties (למשל כמו שהעברתי את header), דרך הילדים או דרך האזנה לאירועים. בדוגמה שלי הקומפוננטה הפנימית מייצרת אירוע delete שאותו הקומפוננטה החיצונית מזהה כדי למחוק פריט מהרשימה.

  4. קומפוננטה פנימית מדווחת "למעלה" על שינויים דרך שליחת אירועים עם הפונקציה dispatchEvent. זה המנגנון הרגיל של DOM לשיגור מידע, ונשים לב שאני לא צריך להגדיר מראש בקומפוננטה איזה אירועים היא צפויה לשגר (בניגוד לריאקט שהכריח אותי להגדיר Property לכל קוד טיפול באירוע).

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