برای توسعه دهندگان React روشن شد

۱. مقدمه

لیت چیست؟

Lit یک کتابخانه ساده برای ساخت کامپوننت‌های وب سریع و سبک است که در هر فریم‌ورکی یا بدون هیچ فریم‌ورکی کار می‌کنند. با Lit می‌توانید کامپوننت‌ها، برنامه‌ها، سیستم‌های طراحی و موارد دیگر را با قابلیت اشتراک‌گذاری بسازید.

آنچه یاد خواهید گرفت

نحوه ترجمه چندین مفهوم React به Lit مانند:

  • JSX و قالب‌بندی
  • اجزا و قطعات
  • وضعیت و چرخه حیات
  • قلاب‌ها
  • کودکان
  • مراجع
  • دولت میانجی

آنچه خواهید ساخت

در پایان این آزمایشگاه کد، شما قادر خواهید بود مفاهیم کامپوننت‌های React را به معادل‌های Lit آنها تبدیل کنید.

آنچه نیاز دارید

  • آخرین نسخه کروم، سافاری، فایرفاکس یا اج.
  • آشنایی با HTML، CSS، جاوا اسکریپت و ابزارهای توسعه کروم
  • آشنایی با ری‌اکت
  • (پیشرفته) اگر می‌خواهید بهترین تجربه توسعه را داشته باشید، VS Code را دانلود کنید. همچنین برای VS Code و NPM به lit-plugin نیاز خواهید داشت.

۲. لیت در مقابل ری‌اکت

مفاهیم و قابلیت‌های اصلی Lit از بسیاری جهات شبیه به React است، اما Lit تفاوت‌ها و تمایزات کلیدی نیز دارد:

کوچک است

Lit حجم کمی دارد: در مقایسه با React + ReactDOM که بیش از ۴۰ کیلوبایت حجم دارد، حجم آن به صورت فشرده و فشرده شده حدود ۵ کیلوبایت است.

نمودار میله‌ای از حجم بسته نرم‌افزاری کوچک‌سازی و فشرده‌شده بر حسب کیلوبایت. حجم نوار Lit برابر با ۵ کیلوبایت و حجم React + React DOM برابر با ۴۲.۲ کیلوبایت است.

سریع است

در بنچمارک‌های عمومی که سیستم قالب‌بندی Lit، یعنی lit-html، را با VDOM React مقایسه می‌کنند، lit-html در بدترین حالت ۸ تا ۱۰ درصد و در موارد استفاده رایج‌تر، بیش از ۵۰ درصد سریع‌تر از React عمل می‌کند.

LitElement (کلاس پایه کامپوننت Lit) سربار کمی به lit-html اضافه می‌کند، اما در مقایسه با ویژگی‌های کامپوننت مانند استفاده از حافظه و زمان تعامل و راه‌اندازی، عملکرد React را ۱۶ تا ۳۰ درصد بهبود می‌بخشد .

نمودار میله‌ای گروه‌بندی‌شده از عملکرد مقایسه‌ای lit با React بر حسب میلی‌ثانیه (هرچه کمتر باشد بهتر است)

نیازی به ساخت ندارد

با ویژگی‌های جدید مرورگر مانند ماژول‌های ES و قالب‌های برچسب‌گذاری‌شده، Lit برای اجرا نیازی به کامپایل ندارد . این بدان معناست که محیط‌های توسعه را می‌توان با یک برچسب اسکریپت + یک مرورگر + یک سرور راه‌اندازی کرد و شما می‌توانید آن را اجرا کنید.

با ماژول‌های ES و CDNهای مدرن امروزی مانند Skypack یا UNPKG ، ممکن است حتی برای شروع به NPM هم نیازی نداشته باشید!

اگرچه، اگر بخواهید، هنوز می‌توانید کد Lit را بسازید و بهینه کنید . ادغام اخیر توسعه‌دهندگان پیرامون ماژول‌های ES بومی برای Lit خوب بوده است - Lit فقط جاوا اسکریپت معمولی است و نیازی به CLI های مخصوص فریم‌ورک یا مدیریت ساخت ندارد .

چارچوب آگنوستیک

اجزای Lit بر اساس مجموعه‌ای از استانداردهای وب به نام اجزای وب ساخته می‌شوند. این بدان معناست که ساخت یک جزء در Lit در چارچوب‌های فعلی و آینده کار خواهد کرد . اگر از عناصر HTML پشتیبانی کند، از اجزای وب نیز پشتیبانی می‌کند.

تنها مشکل تعامل فریم‌ورک‌ها زمانی است که فریم‌ورک‌ها پشتیبانی محدودی از DOM دارند. React یکی از این فریم‌ورک‌ها است، اما از طریق Refها امکان فرار از مشکلات را فراهم می‌کند و Refها در React تجربه خوبی برای توسعه‌دهندگان نیستند.

تیم Lit روی یک پروژه آزمایشی به نام @lit-labs/react کار کرده است که به طور خودکار کامپوننت‌های Lit شما را تجزیه کرده و یک پوشش React ایجاد می‌کند تا نیازی به استفاده از refها نباشد.

علاوه بر این، Custom Elements Everywhere به شما نشان می‌دهد که کدام فریم‌ورک‌ها و کتابخانه‌ها با عناصر سفارشی به خوبی کار می‌کنند!

پشتیبانی درجه یک از TypeScript

اگرچه می‌توان تمام کدهای Lit را با جاوااسکریپت نوشت، اما Lit با تایپ‌اسکریپت نوشته شده است و تیم Lit به توسعه‌دهندگان توصیه می‌کند که از تایپ‌اسکریپت نیز استفاده کنند!

تیم Lit با جامعه Lit همکاری کرده است تا به حفظ پروژه‌هایی که بررسی نوع TypeScript و intellisense را به قالب‌های Lit چه در زمان توسعه و چه در زمان ساخت با lit-analyzer و lit-plugin می‌آورند، کمک کند.

تصویر صفحه یک IDE که بررسی نوع نادرست برای تنظیم مقدار بولی مشخص شده روی یک عدد را نشان می‌دهد

تصویری از یک IDE که پیشنهادات Intellisense را نشان می‌دهد

ابزارهای توسعه (Dev Tools) در مرورگر تعبیه شده‌اند

اجزای Lit فقط عناصر HTML در DOM هستند . این بدان معناست که برای بررسی اجزای خود، نیازی به نصب هیچ ابزار یا افزونه‌ای برای مرورگر خود ندارید .

شما می‌توانید به سادگی ابزارهای توسعه را باز کنید، یک عنصر را انتخاب کنید و ویژگی‌ها یا وضعیت آن را بررسی کنید.

تصویر ابزار توسعه کروم که نشان می‌دهد $0 مقدار <mwc-textfield> را برمی‌گرداند، $0.value مقدار hello world را برمی‌گرداند، $0.outlined مقدار true را برمی‌گرداند و {$0} بسط ویژگی را نشان می‌دهد.

با در نظر گرفتن رندر سمت سرور (SSR) ساخته شده است

Lit 2 با در نظر گرفتن پشتیبانی از SSR ساخته شده است. در زمان نوشتن این آزمایشگاه کد، تیم Lit هنوز ابزارهای SSR را به صورت پایدار منتشر نکرده است، اما تیم Lit در حال حاضر اجزای رندر شده سمت سرور را در محصولات گوگل مستقر کرده و SSR را در برنامه‌های React آزمایش کرده است. تیم Lit انتظار دارد که به زودی این ابزارها را به صورت خارجی در GitHub منتشر کند.

در عین حال، می‌توانید پیشرفت تیم Lit را اینجا دنبال کنید.

خرید کم است

استفاده از Lit نیاز به تعهد خاصی ندارد! می‌توانید کامپوننت‌ها را در Lit بسازید و به پروژه فعلی خود اضافه کنید. اگر آنها را دوست ندارید، لازم نیست کل برنامه را به طور همزمان تبدیل کنید زیرا کامپوننت‌های وب در فریم‌ورک‌های دیگر نیز کار می‌کنند!

آیا کل یک برنامه را در Lit ساخته‌اید و می‌خواهید آن را به چیز دیگری تغییر دهید؟ خب، می‌توانید برنامه Lit فعلی خود را درون چارچوب جدید خود قرار دهید و هر آنچه را که می‌خواهید به اجزای چارچوب جدید منتقل کنید.

علاوه بر این، بسیاری از چارچوب‌های مدرن از خروجی در اجزای وب پشتیبانی می‌کنند ، به این معنی که آنها معمولاً می‌توانند خودشان درون یک عنصر Lit قرار بگیرند.

۳. آماده شدن و گشت و گذار در زمین بازی

دو روش برای انجام این codelab وجود دارد:

  • شما می‌توانید این کار را کاملاً آنلاین، در مرورگر انجام دهید
  • (پیشرفته) می‌توانید این کار را روی دستگاه محلی خود با استفاده از VS Code انجام دهید

دسترسی به کد

در سراسر آزمایشگاه کد، لینک‌هایی به زمین بازی Lit مانند این وجود خواهد داشت:

این محیط یک محیط کدنویسی است که به طور کامل در مرورگر شما اجرا می‌شود. این محیط می‌تواند فایل‌های TypeScript و JavaScript را کامپایل و اجرا کند و همچنین می‌تواند به طور خودکار importها را به ماژول‌های node حل کند.

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

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

شما می‌توانید کل آموزش را در زمین بازی Lit انجام دهید و از این نقاط کنترل به عنوان نقاط شروع استفاده کنید. اگر از VS Code استفاده می‌کنید، می‌توانید از این نقاط کنترل برای دانلود کد شروع هر مرحله و همچنین برای بررسی کار خود استفاده کنید.

بررسی رابط کاربری زمین بازی روشن

نوار تب انتخاب فایل با عنوان بخش ۱، بخش ویرایش کد با عنوان بخش ۲، پیش‌نمایش خروجی با عنوان بخش ۳ و دکمه‌ی پیش‌نمایش بارگذاری مجدد با عنوان بخش ۴ نامگذاری شده است.

تصویر رابط کاربری زمین بازی Lit بخش‌هایی را که در این آزمایشگاه کد استفاده خواهید کرد، برجسته می‌کند.

  1. انتخابگر فایل. به دکمه بعلاوه توجه کنید...
  2. ویرایشگر فایل.
  3. پیش‌نمایش کد.
  4. دکمه بارگذاری مجدد.
  5. دکمه دانلود.

تنظیمات VS Code (پیشرفته)

در اینجا مزایای استفاده از این تنظیمات VS Code آورده شده است:

  • بررسی نوع الگو
  • هوش مصنوعی قالب و تکمیل خودکار

اگر NPM و VS Code (به همراه افزونه lit-plugin ) را از قبل نصب کرده‌اید و می‌دانید که چگونه از آن محیط استفاده کنید، می‌توانید به سادگی این پروژه‌ها را با انجام موارد زیر دانلود و شروع کنید:

  • دکمه دانلود را فشار دهید
  • محتویات فایل tar را در یک دایرکتوری استخراج کنید
  • (اگر TS دارید) یک tsconfig سریع تنظیم کنید که ماژول‌های es و es2015+ را خروجی دهد.
  • یک سرور توسعه‌دهنده نصب کنید که بتواند مشخصات ماژول‌های خالی را حل کند (تیم Lit سرور @web/dev-server را توصیه می‌کند).
    • در اینجا یک مثال package.json آورده شده است.
  • سرور توسعه را اجرا کنید و مرورگر خود را باز کنید (اگر از @web/dev-server استفاده می‌کنید، می‌توانید npx web-dev-server --node-resolve --watch --open استفاده کنید).
    • اگر از فایل مثال package.json استفاده می‌کنید، از npm run dev استفاده کنید.

۴. JSX و قالب‌بندی

در این بخش، اصول اولیه قالب‌بندی در Lit را خواهید آموخت.

قالب‌های JSX و Lit

JSX یک افزونه سینتکس برای جاوا اسکریپت است که به کاربران React اجازه می‌دهد تا به راحتی قالب‌ها را در کد جاوا اسکریپت خود بنویسند. قالب‌های Lit نیز هدف مشابهی دارند: بیان رابط کاربری یک کامپوننت به عنوان تابعی از وضعیت آن.

سینتکس پایه

در React شما یک JSX hello world را به این شکل رندر می‌کنید:

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، قالب‌ها با یک template با برچسب html به نام LIT eral پوشانده می‌شوند، که اتفاقاً نام Lit از همین جا گرفته شده است!

مقادیر الگو

قالب‌های Lit می‌توانند قالب‌های Lit دیگری را که به عنوان TemplateResult شناخته می‌شوند، بپذیرند. برای مثال، name در تگ‌های ایتالیک ( <i> ) قرار دهید و آن را با یک قالب با برچسب literal قرار دهید. توجه: حتماً از کاراکتر backtick ( ` ) استفاده کنید، نه از کاراکتر نقل قول تکی ( ' ).

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

TemplateResult های Lit می‌توانند آرایه‌ها، رشته‌ها، TemplateResult های دیگر و همچنین دستورالعمل‌ها را بپذیرند.

برای تمرین، کد 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
);

در مثال بالا، یک ورودی تعریف شده است که موارد زیر را انجام می‌دهد:

  • غیرفعال را به یک متغیر تعریف شده تنظیم می‌کند (در این مورد 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 اضافه شده است.

در مرحله بعد، به جای className یک اتصال (binding) مستقیماً به ویژگی class وجود دارد. می‌توان چندین اتصال (binding) به ویژگی class اضافه کرد، مگر اینکه از دستورالعمل classMap استفاده کنید که یک کمک‌کننده اعلانی برای تغییر وضعیت کلاس‌ها است.

در نهایت، ویژگی value روی input تنظیم می‌شود. برخلاف React، این کار باعث نمی‌شود عنصر input فقط خواندنی باشد زیرا از پیاده‌سازی و رفتار native input پیروی می‌کند.

نحو اتصال prop با Lit

html`<my-element ?attribute-name=${booleanVar}>`;
  • پیشوند ? سینتکس اتصال برای تغییر وضعیت یک ویژگی روی یک عنصر است.
  • معادل inputRef.toggleAttribute('attribute-name', booleanVar)
  • برای عناصری که از disabled استفاده می‌کنند مفید است، زیرا disabled="false" همچنان توسط DOM به عنوان true خوانده می‌شود زیرا inputElement.hasAttribute('disabled') === true
html`<my-element .property-name=${anyVar}>`;
  • پیشوند . ‎ سینتکس اتصال برای تنظیم یک ویژگی از یک عنصر است.
  • معادل inputRef.propertyName = anyVar
  • برای ارسال داده‌های پیچیده مانند اشیاء، آرایه‌ها یا کلاس‌ها مناسب است.
html`<my-element attribute-name=${stringVar}>`;
  • به ویژگی یک عنصر متصل می‌شود
  • معادل inputRef.setAttribute('attribute-name', stringVar)
  • مناسب برای مقادیر پایه، انتخابگرهای قوانین استایل و انتخابگرهای پرس‌وجو

گردانندگان عبوری

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

در مثال بالا، یک ورودی تعریف شده است که موارد زیر را انجام می‌دهد:

  • کلمه "کلیک" را هنگام کلیک روی ورودی ثبت کن
  • ثبت مقدار ورودی هنگام تایپ یک کاراکتر توسط کاربر

در زبان لیت (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>`;
  • پیشوند @ سینتکس اتصال برای یک شنونده رویداد است.
  • معادل inputRef.addEventListener('event-name', ...)
  • از نام‌های رویداد بومی DOM استفاده می‌کند

۵. اجزا و لوازم جانبی

در این بخش با کامپوننت‌ها و توابع کلاس Lit آشنا خواهید شد. State و Hookها در بخش‌های بعدی با جزئیات بیشتری پوشش داده می‌شوند.

کامپوننت‌های کلاس و LiteElement

معادل Lit برای یک کامپوننت کلاس React، LitElement است و مفهوم "ویژگی‌های واکنشی" در 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 انجام می‌دهید

در تایپ‌اسکریپت:

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>`
  }
}

در جاوا اسکریپت:

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}
  }
}
  • این تابع همان عملکرد دکوراتور TS با @property را دارد، اما به صورت بومی در جاوا اسکریپت اجرا می‌شود.
render() {
  return html`<h1>Hello, ${this.name}</h1>`
}
  • این تابع هر زمان که هر ویژگی واکنشی تغییر کند، فراخوانی می‌شود.
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  ...
}
  • این یک نام تگ عنصر HTML را با تعریف کلاس مرتبط می‌کند.
  • با توجه به استاندارد عناصر سفارشی، نام تگ باید شامل یک خط تیره (-) باشد.
  • this در یک LitElement به نمونه عنصر سفارشی اشاره دارد (در این مورد <welcome-banner> )
customElements.define('welcome-banner', WelcomeBanner);
  • این معادل جاوا اسکریپت دکوراتور @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
);

در زبان لیت این به صورت زیر خواهد بود:

import {html, render} from 'lit';

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

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

۶. وضعیت و چرخه حیات

در این بخش با وضعیت و چرخه حیات Lit آشنا خواهید شد.

ایالت

مفهوم «ویژگی‌های واکنشی» در Lit ترکیبی از state و props در 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
  • مشابه props و state در 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
  • مشابه 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>`;
}
  • معادل روشن آن نیز render است
  • می‌تواند هر نتیجه‌ی قابل رندری را برگرداند، مثلاً TemplateResult یا string و غیره.
  • مشابه React، render() باید یک تابع خالص باشد
  • در هر گره‌ای که تابع createRenderRoot() برگرداند، رندر خواهد شد (به طور پیش‌فرض ShadowRoot ).

componentDidMount

componentDidMount شبیه ترکیبی از هر دو فراخوانی چرخه عمر firstUpdated و connectedCallback در Lit است.

firstUpdated

import Chart from 'chart.js';

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

// Lit
firstUpdated() {
  this._chart = new Chart(this.chartEl, {...});
}
  • اولین باری که قالب کامپوننت در ریشه کامپوننت رندر می‌شود، فراخوانی می‌شود
  • فقط در صورتی فراخوانی می‌شود که عنصر متصل باشد، مثلاً تا زمانی که آن گره به درخت DOM اضافه نشود، از طریق document.createElement('my-component') فراخوانی نمی‌شود.
  • این مکان خوبی برای انجام تنظیمات کامپوننتی است که نیاز به DOM رندر شده توسط کامپوننت دارد.
  • برخلاف componentDidMount در React، تغییرات در ویژگی‌های reactive در firstUpdated باعث رندر مجدد می‌شود، اگرچه مرورگر معمولاً تغییرات را در همان فریم دسته‌بندی می‌کند. اگر این تغییرات نیازی به دسترسی به 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 یا اتصال مجدد شنونده‌های رویداد که هنگام قطع اتصال پاک‌سازی شده بودند، مفید است.
  • نکته: 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);
}
  • معادل Lit مشابه disconnectedCallback است.
  • برخلاف کامپوننت‌های React، وقتی عناصر سفارشی از DOM جدا می‌شوند، کامپوننت از بین نمی‌رود.
  • برخلاف componentWillUnmount ، disconnectedCallback پس از حذف عنصر از درخت فراخوانی می‌شود.
  • DOM درون ریشه هنوز به زیردرخت ریشه متصل است
  • برای پاک‌سازی شنونده‌های رویداد و ارجاعات نشتی‌دار مفید است تا مرورگر بتواند کامپوننت را جمع‌آوری زباله کند.

ورزش

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! It is" را رندر می‌کند و سپس زمان را نمایش می‌دهد.
  • هر ثانیه ساعت را به‌روزرسانی می‌کند
  • هنگام dismount شدن، بازه زمانی که با دستور 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>
  `;
}

حالا، متد 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);

۷. قلاب‌ها

در این بخش، نحوه ترجمه مفاهیم React Hook به Lit را خواهید آموخت.

مفاهیم قلاب‌های React

هوک‌های ری‌اکت راهی برای کامپوننت‌های تابع فراهم می‌کنند تا به حالت «هوک» کنند. این کار چندین مزیت دارد.

  • آنها استفاده مجدد از منطق stateful را ساده می‌کنند.
  • کمک به تقسیم یک کامپوننت به توابع کوچکتر

علاوه بر این، تمرکز روی کامپوننت‌های مبتنی بر تابع، برخی از مشکلات مربوط به سینتکس مبتنی بر کلاس React مانند موارد زیر را برطرف کرد:

  • نیاز به ارسال props از constructor به super
  • مقداردهی اولیه نامرتب ویژگی‌ها در constructor
    • این دلیلی بود که در آن زمان توسط تیم React بیان شد اما توسط ES2019 حل شد.
  • مشکلات ناشی از this دیگر به کامپوننت مربوط نمی‌شوند

مفاهیم قلاب‌های React در Lit

همانطور که در بخش کامپوننت‌ها و پروپرتی‌ها ذکر شد، 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++;
  }
}

لیت چگونه به این مسائل رسیدگی می‌کند؟

  • constructor هیچ آرگومانی نمی‌گیرد
  • تمام مقیدسازی‌های @event به صورت خودکار به this متصل می‌شوند
  • this در اکثریت قریب به اتفاق موارد به مرجع عنصر سفارشی اشاره دارد
  • اکنون می‌توان از ویژگی‌های کلاس به عنوان اعضای کلاس نمونه‌سازی کرد. این کار پیاده‌سازی‌های مبتنی بر سازنده را ساده‌تر می‌کند.

کنترل‌کننده‌های واکنشی

مفاهیم اصلی پشت Hookها در Lit به عنوان کنترلرهای واکنشی وجود دارند. الگوهای کنترلر واکنشی امکان اشتراک‌گذاری منطق stateful، تقسیم کامپوننت‌ها به بیت‌های کوچک‌تر و ماژولارتر و همچنین اتصال به چرخه حیات به‌روزرسانی یک عنصر را فراهم می‌کنند.

یک کنترلر واکنشی ، یک رابط شیء است که می‌تواند به چرخه حیات به‌روزرسانی یک میزبان کنترلر مانند 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 ، چرخه حیات کنترلر در کنار چرخه حیات میزبان فراخوانی می‌شود. برای مثال، مثال ساعت را از بخش وضعیت و چرخه حیات به خاطر بیاورید:

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! It is" را رندر می‌کند و سپس زمان را نمایش می‌دهد.
  • هر ثانیه ساعت را به‌روزرسانی می‌کند
  • هنگام dismount شدن، بازه زمانی که با دستور tick فراخوانی می‌شود را پاک می‌کند.

ساخت داربست اجزا

ابتدا با تعریف کلاس کامپوننت شروع کنید و تابع 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 استفاده کند، اما استفاده از یک کلاس با constructor که می‌تواند یک رابط ReactiveControllerHost و همچنین هر ویژگی دیگری که برای مقداردهی اولیه کنترلر لازم است را بپذیرد، الگویی است که تیم Lit ترجیح می‌دهد برای اکثر موارد اساسی از آن استفاده کند.

حالا باید کالبک‌های چرخه حیات 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();
}

الان دیگه باید ساعت تیک تاک کنه!

برای مقایسه عمیق‌تر موارد استفاده رایج از قلاب‌ها، لطفاً به بخش مباحث پیشرفته - قلاب‌ها مراجعه کنید.

۸. کودکان

در این بخش، نحوه استفاده از اسلات‌ها برای مدیریت کودکان در Lit را خواهید آموخت.

اسلات‌ها و کودکان

اسلات‌ها با امکان تودرتو کردن کامپوننت‌ها، ترکیب‌بندی را ممکن می‌سازند.

در React، فرزندان از طریق props به ارث می‌رسند. اسلات پیش‌فرض props.children است و تابع render مشخص می‌کند که اسلات پیش‌فرض کجا قرار می‌گیرد. برای مثال:

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

به خاطر داشته باشید که props.children کامپوننت‌های React هستند و نه عناصر HTML.

در Lit، فرزندان در تابع رندر با عناصر اسلات ترکیب می‌شوند. توجه داشته باشید که فرزندان به همان شیوه React به ارث نمی‌رسند. در Lit، فرزندان عناصر HTML هستند که به اسلات‌ها متصل شده‌اند. این پیوست Projection نامیده می‌شود.

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

اسلات‌های چندگانه

در React، اضافه کردن چندین اسلات اساساً مشابه ارث‌بری props بیشتر است.

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 به propهای 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"> ) مطابقت داشته باشد، وجود نداشته باشد، آن گره نمایش داده نمی‌شود و نمایش داده نمی‌شود.

۹. مراجع

گاهی اوقات، یک توسعه‌دهنده ممکن است نیاز به دسترسی به API یک HTMLElement داشته باشد.

در این بخش، نحوه‌ی به دست آوردن ارجاعات عناصر در Lit را خواهید آموخت.

مراجع واکنشی

یک کامپوننت React به مجموعه‌ای از فراخوانی‌های تابع تبدیل می‌شود که هنگام فراخوانی، یک DOM مجازی ایجاد می‌کنند. این DOM مجازی توسط ReactDOM تفسیر شده و HTMLElements را رندر می‌کند.

در React، Refها فضایی در حافظه هستند که شامل یک 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 از طریق ویژگی ref ، inputRef.current را روی HTMLInputElement تولید شده تنظیم می‌کند.

روشن کردن "References" با @query

Lit نزدیک به مرورگر عمل می‌کند و یک انتزاع بسیار نازک نسبت به ویژگی‌های بومی مرورگر ایجاد می‌کند.

معادل React برای refs در 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 کارهای زیر را انجام می‌دهد:

  • با استفاده از دکوراتور @query (ایجاد یک getter برای یک HTMLInputElement )، یک ویژگی (property) در MyElement تعریف می‌کند.
  • یک رویداد کلیک با نام onButtonClick تعریف و به آن متصل می‌کند.
  • ورودی را روی کلیک دکمه متمرکز می‌کند

در جاوا اسکریپت، دکوراتورهای @query و @queryAll به ترتیب querySelector و querySelectorAll را انجام می‌دهند. این معادل جاوا اسکریپت @query('input') inputEl!: HTMLInputElement;

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

بعد از اینکه کامپوننت Lit قالب متد render را به ریشه my-element ارسال کرد، دکوراتور @query اکنون به inputEl اجازه می‌دهد تا اولین عنصر input یافت شده در ریشه رندر را برگرداند. اگر @query نتواند عنصر مشخص شده را پیدا کند، null برمی‌گرداند.

اگر چندین عنصر input در ریشه رندر وجود داشته باشد، @queryAll لیستی از گره‌ها را برمی‌گرداند.

۱۰. دولت میانجی

در این بخش، یاد خواهید گرفت که چگونه در Lit، حالت (state) را بین کامپوننت‌ها میانجیگری کنید.

اجزای قابل استفاده مجدد

React از خطوط لوله رندر تابعی با جریان داده از بالا به پایین تقلید می‌کند. والدین از طریق props وضعیت را به فرزندان ارائه می‌دهند. فرزندان از طریق callbackهای موجود در props با والدین ارتباط برقرار می‌کنند.

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 Component کارهای زیر را انجام می‌دهد:

  • step ایجاد ویژگی واکنشی
  • یک رویداد سفارشی به نام update-counter ارسال می‌کند که مقدار step عنصر را هنگام کلیک حمل می‌کند.

رویدادهای مرورگر از عناصر فرزند به عناصر والد منتقل می‌شوند. رویدادها به فرزندان اجازه می‌دهند رویدادهای تعاملی و تغییرات وضعیت را پخش کنند. React اساساً وضعیت را در جهت مخالف ارسال می‌کند، بنابراین مشاهده اینکه کامپوننت‌های React رویدادها را به همان روشی که Lit Components ارسال و دریافت می‌کنند، ارسال می‌کنند و به آنها گوش می‌دهند، غیرمعمول است.

کامپوننت‌های حالت‌مند

در React، استفاده از قلاب‌ها برای مدیریت حالت (state) رایج است. یک کامپوننت 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 برای به‌روزرسانی step count کلیک‌ها استفاده می‌کند.

پیاده‌سازی مشابهی از MyCounter را می‌توان در Lit انجام داد. توجه کنید که addToCounter به counter-button ارسال نمی‌شود. در عوض، تابع فراخوانی به عنوان یک شنونده رویداد به رویداد @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 ایجاد می‌کند که با تغییر مقدار، کامپوننت را به‌روزرسانی می‌کند.
  • تابع فراخوانی addToCounter را به شنونده رویداد @update-counter متصل می‌کند.
  • به‌روزرسانی‌ها با اضافه کردن مقدار موجود در detail.step از رویداد update-counter count .
  • مقدار step counter-button را از طریق ویژگی step تنظیم می‌کند.

استفاده از ویژگی‌های واکنشی در Lit برای پخش تغییرات از والدین به فرزندان مرسوم‌تر است. به طور مشابه، استفاده از سیستم رویداد مرورگر برای نمایش حبابی جزئیات از پایین به بالا، روش خوبی است.

این رویکرد از بهترین شیوه‌ها پیروی می‌کند و به هدف Lit مبنی بر ارائه پشتیبانی چند پلتفرمی برای اجزای وب پایبند است.

۱۱. استایل دادن

در این بخش با استایل‌بندی در 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>
    `;
  }
}

در مثال بالا دو عنوان وجود دارد که هر کدام دارای سبک درون‌خطی هستند.

حالا یک حاشیه از border-color.js وارد کرده و به متن نارنجی متصل کنید:

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

...

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

محاسبه‌ی رشته‌ی استایل هر بار ممکن است کمی آزاردهنده باشد، بنابراین Lit دستورالعملی برای کمک به این کار ارائه می‌دهد.

سبک‌نقشه

دستورالعمل styleMap استفاده از جاوا اسکریپت را برای تنظیم استایل‌های درون‌خطی آسان‌تر می‌کند. برای مثال:

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 از نحوی مشابه با سینتکس اتصال ویژگی style در React پیروی می‌کند.

CSSResult

روش پیشنهادی برای استایل‌دهی به کامپوننت‌ها، استفاده از template literal با برچسب css است.

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 :

  • یک بار به ازای هر کلاس در مقابل هر نمونه تجزیه می‌شود
  • با در نظر گرفتن قابلیت استفاده مجدد ماژول، پیاده‌سازی شده است
  • به راحتی می‌توان استایل‌ها را در فایل‌های خودشان جدا کرد
  • سازگار با ویژگی‌های سفارشی CSS، چندمنظوره

علاوه بر این، به تگ <style> در index.html توجه کنید:

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

Lit استایل‌های کامپوننت‌های شما را تا ریشه‌هایشان پوشش می‌دهد. این یعنی استایل‌ها به داخل و خارج نشت نمی‌کنند. برای انتقال استایل‌ها به کامپوننت‌ها، تیم Lit استفاده از CSS Custom Properties را توصیه می‌کند زیرا می‌توانند به پوشش استایل‌های Lit نفوذ کنند.

برچسب‌های سبک

همچنین می‌توان تگ‌های <style> را به سادگی در قالب‌های خود درون‌خطی کرد. مرورگر این تگ‌های استایل را حذف می‌کند، اما با قرار دادن آنها در قالب‌های شما، آنها به ازای هر نمونه کامپوننت تجزیه می‌شوند، برخلاف قالب‌های دارای برچسب css که به ازای هر کلاس تجزیه می‌شوند. علاوه بر این، حذف تکرار CSSResult توسط مرورگر بسیار سریع‌تر است.

استفاده از <link rel="stylesheet"> در قالب شما نیز برای استایل‌ها امکان‌پذیر است، اما این نیز توصیه نمی‌شود زیرا ممکن است باعث ایجاد یک نمایش اولیه از محتوای بدون استایل (FOUC) شود.

۱۲. مباحث پیشرفته (اختیاری)

JSX & Templating

Lit & Virtual DOM

Lit-html does not include a conventional Virtual DOM that diffs each individual node. Instead it utilizes performance features intrinsic to ES2015's tagged template literal spec. Tagged template literals are template literal strings with tag functions attached to them.

Here is an example of a template literal:

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

Here is an example of a tagged template literal:

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

In the above example, the tag is the tag function and the f function returns an invocation of a tagged template literal.

A lot of the performance magic in Lit comes from the fact that the string arrays passed into the tag function have the same pointer (as shown in the second console.log ). The browser does not recreate a new strings array on each tag function invocation, because it is using the same template literal (ie in the same location in the AST). So Lit's binding, parsing, and template caching can take advantage of these features without much runtime diffing overhead.

This built-in browser behavior of tagged template literals gives Lit quite a performance advantage. Most conventional Virtual DOMs do the majority of their work in JavaScript. However, tagged template literals do most of their diffing in the browser's C++.

If you'd like to get started using HTML tagged template literals with React or Preact, the Lit team recommends the htm library .

Though, as is the case with the Google Codelabs site and several online code editors, you will notice that tagged template literal syntax highlighting is not very common. Some IDEs and text editors support them by default such as Atom and GitHub's codeblock highlighter. The Lit team also works very closely with the community to maintain projects such as the lit-plugin which is a VS Code plugin that will add syntax highlighting, type checking, and intellisense to your Lit projects.

Lit & JSX + React DOM

JSX does not run in the browser and instead uses a preprocessor to convert JSX to JavaScript function calls (typically via Babel).

For example, Babel will transform this:

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

into this:

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

React DOM then takes the React output and translates it to actual DOM – properties, attributes, event listeners, and all.

Lit-html uses tagged template literals which can run in the browser without transpilation or a preprocessor. This means that in order to get started with Lit, all you need is an HTML file, an ES module script, and a server. Here's a completely browser-runnable script:

<!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>

Additionally, since Lit's templating system, lit-html, does not use a conventional Virtual DOM but rather uses the DOM API directly, Lit 2's size is under 5kb minified and gzipped compared to React (2.8kb) + react-dom's (39.4kb) 40kb minified and gizipped.

رویدادها

React uses a synthetic event system. This means that react-dom must define every event that will be used on every component and provide a camelCase event listener equivalent for each type of node. As a result, JSX does not have a method to define an event listener for a custom event and developers must use a ref and then imperatively apply a listener. This creates a sub-par developer experience when integrating libraries that don't have React in mind thus resulting in having to write a React-specific wrapper.

Lit-html directly accesses the DOM and uses native events, so adding event listeners is as easy as @event-name=${eventNameListener} . This means that less runtime parsing is done for adding event listeners as well as firing events.

Components & Props

React components & custom elements

Under the hood, LitElement uses custom elements to package its components. Custom elements introduce some tradeoffs between React components when it comes to componentization (state and lifecycle is discussed further in the State & Lifecycle section).

Some advantages Custom Elements have as a component system:

  • Native to the browser and do not require any tooling
  • Fit into every browser API from innerHTML and document.createElement to querySelector
  • Can typically be used across frameworks
  • Can be lazily registered with customElements.define and "hydrate" DOM

Some disadvantages Custom Elements have compared to React components:

  • Cannot create a custom element without defining a class (thus no JSX-like functional components)
  • Must contain a closing tag
    • Note: despite the developer convenience browser vendors tend to regret the self-closing tag spec which is why newer specs tend to not include self-closing tags
  • Introduces an extra node to the DOM tree which may cause layout issues
  • Must be registered via JavaScript

Lit has gone with custom elements over a bespoke element system because the custom elements are built into the browser, and the Lit team believes that the cross-framework benefits outweigh the benefits provided by a component abstraction layer. In fact, the Lit team's efforts in the lit-ssr space have overcome the main issues with JavaScript registration. Additionally, some companies such as GitHub take advantage of custom element lazy registration to progressively enhance pages with optional flair.

Passing data to custom elements

A common misconception with custom elements is that data can only be passed in as strings. This misconception likely comes from the fact that element attributes can only be written as strings. Though it is true that Lit will cast string attributes to their defined types, custom elements can also accept complex data as properties.

For example – given the following LitElement definition:

// 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>`;
  }
}

A primitive reactive property num is defined which will convert an attribute's string value into a number , and then complex data structure is introduced with attribute:false which deactivates Lit's attribute handling.

This is how to pass data to this custom element:

<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

Other React Lifecycle Callbacks

static getDerivedStateFromProps

There is no equivalent in Lit as props and state are both the same class properties

shouldComponentUpdate

  • Lit equivalent is shouldUpdate
  • Called on first render unlike React
  • Similar in function to React's shouldComponentUpdate

getSnapshotBeforeUpdate

In Lit, getSnapshotBeforeUpdate is similar to both update and willUpdate

willUpdate

  • Called before update
  • Unlike getSnapshotBeforeUpdate , willUpdate is called before render
  • Changes to reactive properties in willUpdate do not re-trigger the update cycle
  • Good place to compute property values that depend on other properties and are used in the rest of the update process
  • This method is called on the server in SSR, so accessing the DOM is not advised here

update

  • Called after willUpdate
  • Unlike getSnapshotBeforeUpdate , update is called before render
  • Changes to reactive properties in update do not re-trigger the update cycle if changed before calling super.update
  • Good place to capture information from the DOM surrounding the component before the rendered output is committed to the DOM
  • This method is not called on the server in SSR

Other Lit Lifecycle Callbacks

There are several lifecycle callbacks that were not mentioned in the previous section because there is no analog to them in React. They are:

attributeChangedCallback

It is invoked when one of the element's observedAttributes changes. Both observedAttributes and attributeChangedCallback are part of the custom elements spec and implemented by Lit under the hood to provide an attribute API for Lit elements.

adoptedCallback

Invoked when the component is moved to a new document eg from an HTMLTemplateElement 's documentFragment to the main document . This callback is also a part of the custom elements spec and should only be used for advanced use cases when the component changes documents.

Other lifecycle methods and properties

These methods and properties are class members you can call, override, or await to help manipulate the lifecycle process.

updateComplete

This is a Promise that resolves when the element has finished updating as the update and render lifecycles are asynchronous. An example:

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

getUpdateComplete

This is a method that should be overridden to customize when updateComplete resolves. This is common when a component is rendering a child component and their render cycles must be in sync. eg,

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

performUpdate

This method is what calls the update lifecycle callbacks. This should generally not be needed except for rare cases where updating must be done synchronously or for custom scheduling.

hasUpdated

This property is true if the component has updated at least once.

isConnected

A part of the custom elements spec, this property will be true if the element is currently attached to the main document tree.

Lit Update Lifecycle Visualization

There are 3 parts to the update lifecycle:

  • Pre-update
  • به‌روزرسانی
  • Post-update

Pre-Update

A directed acyclic graph of nodes with callback names. constructor to requestUpdate. @property to Property Setter. attributeChangedCallback to Property Setter. Property Setter to hasChanged. hasChanged to requestUpdate. requestUpdate points out to the next, update lifecycle graph.

After requestUpdate , a scheduled update is awaited.

به‌روزرسانی

A directed acyclic graph of nodes with callback names. Arrow from previous image of pre-update lifecycle points to performUpdate. performUpdate to shouldUpdate. shouldUpdate points to both ‘complete update if false’ as well as willUpdate. willUpdate to update. update to both render as well as to the next, post-update lifecycle graph. render also points to the next, post-update lifecycle graph.

Post-Update

A directed acyclic graph of nodes with callback names. Arrow from previous image of update lifecycle points to firstUpdated. firstUpdated to updated. updated to updateComplete.

Hooks

Why hooks

Hooks were introduced into React for simple function component use cases that required state. In many simple cases function components with hooks tend to be much simpler and more readable than their class component counterparts. Though, when introducing asynchonous state updates as well as passing data between hooks or effects, the hooks pattern tends to not suffice, and a class-based solution like reactive controllers tend to shine.

API request hooks & controllers

It is common to write a hook that requests data from an API. For example, take this React function component that does the following:

  • index.tsx
    • Renders text
    • Renders useAPI 's response
      • User ID + User name
      • Error Message
        • 404 when reaches user 11 (by design)
        • Abort error if API fetch is aborted
      • Loading Message
    • Renders an action button
      • Next user: which fetches the API for the next user
      • Cancel: which aborts the API fetch and displays an error
  • useApi.tsx
    • Defines a useApi custom hook
    • Will async fetch a user object from an api
    • Emits:
      • نام کاربری
      • Whether the fetch is loading
      • Any error messages
      • A callback to abort the fetch
    • Aborts fetches in progress if dismounted

Here is the Lit + Reactive Controller implementation .

Takeaways:

  • Reactive Controllers are most like custom hooks
  • Passing non-renderable data between callbacks and effects
    • React uses useRef to pass data between useEffect and useCallback
    • Lit uses a private class property
    • React is essentially mimicking the behavior of a private class property

Additionally, if you really like the React function component syntax with hooks but the same buildless environment of Lit, the Lit team highly recommends the Haunted library.

کودکان

Default Slot

When HTML elements are not given a slot attribute, they are assigned to the default unnamed slot. In the example below, MyApp will slot one paragraph into a named slot. The other paragraph will default to the unnamed slot".

@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>
   `;
  }
}

Slot Updates

When the structure of slot descendants change, a slotchange event is fired. A Lit component can bind an event-listener to a slotchange event. In the example below, the first slot found in the shadowRoot will have their assignedNodes logged to the console on 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>
   `;
  }
}

مراجع

Reference generation

Lit and React both expose a reference to an HTMLElement after their render functions have been called. But it's worth reviewing how React and Lit compose the DOM that is later returned through a Lit @query decorator or a React Reference.

React is a functional pipeline that creates React Components not HTMLElements. Because a Ref is declared before an HTMLElement is rendered, a space in memory is allocated. This is why you see null as the initial value of a Ref, because the actual DOM element hasn't yet been created (or rendered) ie useRef(null) .

After ReactDOM converts a React Component into an HTMLElement, it looks for an attribute called ref in the ReactComponent. If available, ReactDOM places the HTMLElement's reference to ref.current .

LitElement uses the html template tag function from lit-html to compose a Template Element under the hood. LitElement stamps the template's contents to a custom element's shadow DOM after render. The shadow DOM is a scoped DOM tree encapsulated by a shadow root. The @query decorator then creates a getter for the property which essentially performs a this.shadowRoot.querySelector on the scoped root.

Query Multiple Elements

In the example below, the @queryAll decorator will return the two paragraphs in the shadow root as a 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>
   `;
  }
}

Essentially, @queryAll creates a getter for paragraphs that returns the results of this.shadowRoot.querySelectorAll() . In JavaScript, a getter can be declared to perform the same purpose:

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

Query Changing Elements

The @queryAsync decorator is better suited to handle a node that can change based on the state of another element property.

In the example below, @queryAsync will find the first paragraph element. However, a paragraph element will only be rendered when renderParagraph randomly generates an odd number. The @queryAsync directive will return a promise that will resolve when the first paragraph is available.

@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()}
   `;
  }
}

Mediating State

In React, convention is to use callbacks because state is mediated by React itself. React does it's best to not rely on state provided by elements. The DOM is simply an effect of the rendering process.

External State

It's possible to use Redux, MobX, or any other state management library alongside Lit.

Lit components are created in browser scope. So any library that also exists in browser scope is available to Lit. Many amazing libraries have been built to utilize existing state management systems in Lit.

Here is a series by Vaadin explaining how to leverage Redux in a Lit component.

Take a look at lit-mobx from Adobe to see how a large scale site can leverage MobX in Lit.

Also, check out Apollo Elements to see how developers are including GraphQL in their web components.

Lit works with native browser features and most state management solutions in browser scope can be used in a Lit component.

استایل

Shadow DOM

To natively encapsulate styles and DOM within a Custom Element, Lit uses Shadow DOM . Shadow Roots generate a shadow tree separate from the main document tree. This means that most styles are scoped to this document. Certain styles do leak through such as color, and other font-related styles.

Shadow DOM also introduces new concepts and selectors to the CSS spec:

: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.
   */
}

Sharing Styles

Lit makes it easy to share styles between components in the form of CSSTemplateResults via css template tags. For example:

// 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>`
  }
}

Theming

Shadow roots present a bit of a challenge to conventional theming which typically are top-down style tag approaches. The conventional way to tackle theming with Web Components that use Shadow DOM is to expose a style API via CSS Custom Properties . For example, this is a pattern that Material Design uses:

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

The user would then change the theme of the site by applying custom property values:

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

If top-down theming is a must and you are unable to expose styles, it is always possible to disable Shadow DOM by overriding createRenderRoot to return this which will then render your components' template to the custom element itself rather than to a shadow root attached to the custom element. With this you will lose: style encapsulation, DOM encapsulation, and slots.

تولید

IE 11

If you need to support older browsers like IE 11, you will have to load some polyfills which come out to about another 33kb. More information can be found here .

Conditional Bundles

The Lit team recommends serving two different bundles, one for IE 11 and one for modern browsers. There are several benefits to this:

  • Serving ES 6 is faster and will serve most of your clients
  • Transpiled ES 5 significantly increases bundle size
  • Conditional bundles give you the best of both worlds
    • IE 11 support
    • No slowdown on modern browsers

More info on how to build a conditionally served bundle can be found on our documentation site here .