LitElement - базовый JS класс для создания веб-компонентов

Полгода назад я "щупал" Stencil на примере модального окна. Теперь хочется сделать аналогичный компонент, используя LitElement.

В целом, у нативных веб компонентов одна из основных проблем - неудобная работа с DOM. LitElement использует lit-html для рендеринга шаблонов HTML. lit-html перерисовывает только динамические части пользовательского интерфейса, что делает обновления DOM очень быстрыми.

Напишем простое модальное окно на LitElement. Я буду использовать typescript, т.к. хочется использовать декораторы. К моему сожалению, нет офицального starter kit с использованием babel или typescript(.

Начнем

Создаём проект

npm init

Устанавливаем зависимости для разработки

npm install webpack webpack-cli webpack-dev-server typescript ts-loader --save-dev

Создаём webpack конфиг webpack.config.js который будет транспилировать ts код в js.

var path = require('path');

module.exports = {
  entry: './index.ts',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/dist/',
  },
  mode: 'development',
  resolve: {
    extensions: ['.ts', '.js', '.json'],
  },
  module: {
    rules: [{ test: /\.ts/, use: 'ts-loader' }],
  },
  devServer: {
    open: true,
    hot: true
  }
};

Прописываем script'ы для разработки в package.json

  ...
  "scripts": {
    "build": "webpack --config webpack.config.js",
    "start": "webpack-dev-server",
    "clean": "rm -rf package-lock.json node_modules/ dist/",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  ...

Для разработки достаточно запустить команду npm start.

LitElement

Разметка и стили компонента будут позаимствованы у stencil версии компонента.

Сравнительно недавно у LitElement появились декорторы.

  • @customElement() - декоратор класса компонента. Задаёт используемый тег веб-компонента

    /**
     * Use the customElement decorator to define your class as
     * a custom element. Registers <my-element> as an HTML tag.
     */
    @customElement('tyapk-modal')
    export class TyapkModalComponent extends LitElement {}
  • @property() - декоратор "входящего" свойства компонента. Способ получить данные извне. В компоненте модального окна заголовок будет задаваться через @property()

    /**
     * Header
     * Create an observed property. Triggers update on change.
     */
    @property() 
    header = '';

    Использование:

    <tyapk-modal header="О насущном"></tyapk-modal>

    В качестве флага показывать окно или нет используется свойство

    @property()
    show = false;

Шаблоны

Для отображения окна будет использоваться условное выражение:

// Conditional
html`${this.show?html`<p>foo</p>`:html`<p>bar</p>`}`;

В lithtml существуют привязки:

  • Text content: <p>${...}</p> (выводится заголовок <h2>${this.header}</h2>)
  • Attribute: <p id="${...}"></p>
  • Boolean attribute: ?checked="${...}"
  • Property: .value="${...}"
  • Event handler: @event="${...}" (обработчики кликов @click="${() => this.handleClick()}")

Более подбробно о разнице между Attribute и Property

Несколько функций связаных с открытием/закрытием окна:

private _close(detail: string): void {
    // Fire a custom event for others to listen to
    this.dispatchEvent(new CustomEvent('close', { detail }));
}

open(): void {
    this.show = true;
}

handleClick(): void {
    this.show = false;
    this._close('ok');
}

closeModal(result: string): void {
    this.show = false;
    this._close(result);
}

Шаблон компонента модального окна получился следующий

  render(): TemplateResult {
    /**
     * Use JavaScript expressions to include property values in
     * the element template.
     */
    return html`
      ${this.show
        ? html`
            <div class="dialog">
              <div class="dialog__content">
                <header>
                  <h2>${this.header}</h2>
                </header>
                <main>
                  <slot></slot>
                </main>
                <footer>
                  <button
                    class="dialog__ok-btn"
                    @click="${() => this.handleClick()}"
                  >
                    OK
                  </button>
                  <button
                    class="dialog__cancel-btn"
                    @click="${() => this.closeModal('cancel')}"
                  >
                    Отмена
                  </button>
                </footer>
                <div
                  class="dialog__close-btn"
                  @click="${() => this.closeModal('close')}"
                ></div>
              </div>
            </div>
            <div class="overlay"></div>
          `
        : ''}
    `;
  }

Стили рекомендуется писать в статическом свойстве styles

  static get styles(): CSSResult {
    return css`
      .dialog {
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        position: fixed;
        z-index: 1000;
        width: 100%;
        height: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      ...
    `
  }

Исходный код доступен на github

Пример использования модального окна

<tyapk-modal header="Шутка юмора">
    <p>Никого работа программы не удивляет так часто, как ее создателя.</p>
</tyapk-modal>

Работает корректно в браузерах с поддержкой slot&template.

Вместо заключения.

Переписать компонент из StencilJS в LitElement у меня заняло 3 часа, 2 из которых ушло на webpack+typescript и поиск starter проектов для LitElement.


Никого работа программы не удивляет так часто, как ее создателя.

Похожие записи

Svelte for web components

Написание простого модального окна с использованием фреймворка Svelte и компиляцией в веб компонент