Кроссфреймовая библиотека веб-компонентов 📚 с использованием Lit (часть I)

Веб-компоненты — это новый набор технологий, которые позволяют нам создавать пользовательские элементы и инкапсулировать их функциональность отдельно от остального кода, что делает их многократно используемыми и совместно используемыми в ваших веб-приложениях, идеально подходящими для разработки систем индивидуального дизайна.

В этом примере мы создадим два компонента для карточек, взяв их из одной из моих любимых библиотек паттернов Inclusive Components. Выражаю благодарность Хейдону Пикерингу за отличную книгу.

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

Создание, масштабирование и поддержка веб-компонентов с помощью ванильного Javascript может быстро стать беспорядочным, поэтому я решил повысить выразительность с помощью lit, который использует Typescript.
В любом случае, будет лучше, если вы не забудете взглянуть на реализацию ванильного JS, чтобы понять, как работает реализация веб-компонентов под капотом.

Вы можете найти документацию по lit по этой ссылке, а оригинальный Html и Js код элемента card здесь.

Итак, давайте испачкаем руки

Я начал создание пустого git-репозитория по ссылке.
Не стесняйтесь добавлять свой вклад, а улучшения всегда приветствуются.

Я добавил библиотеки Typescript, Eslint, Jasmine и Open Web Components, чтобы поддерживать лучшие практики работы с кодом и тестировать компоненты после:

Моя структура папок будет представлять собой папку компонентов, в которой могут быть общие вещи.
В данном случае мы будем разделять некоторые стили между двумя компонентами.

Далее мы начнем создавать новый декоратор класса @customElement ('card-image') в card.ts, который расширяется от LitElement, наследует и инкапсулирует некоторые методы для доступа к API веб-компонентов:

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

@customElement('card-image')
export class Card extends LitElement {}

Вход в полноэкранный режим Выход из полноэкранного режима

После этого мы можем начать определять интерфейс для передачи свойств компонента, я объявил его в том же файле, но не стесняйтесь создать отдельный, если вы думаете, что он будет использоваться повторно:

...

export interface CardConfig {
  altText: string;
  ctaText: string;
  image: string;
  link: string;
  text: string;
  textDesc: string;
  textDescLink: string;
  title: string;
}

@customElement('card-image')
export class Card extends LitElement {}

Вход в полноэкранный режим Выход из полноэкранного режима

Пришло время определить объект конфигурации и Html, который будет возвращаться в методе рендеринга:

...

@customElement('card-image')
export class Card extends LitElement {

    @property({ type: Object }) card!: CardConfig;

    render() {
        return html`
          <li class="card">
            <div class="img">
                <img src="${this.card.image}" alt="${this.card.altText}" />
            </div>
            <div class="text">
              <h2>
                <a id="card-link" href="${this.card.link}" aria-describedby="desc-a-card"
                  >${this.card.title}</a
                >
              </h2>
              <p>${this.card.text}</p>
              <span class="cta" aria-hidden="true" id="desc-a-card">${this.card.ctaText}</span>
              <small><a href="${this.card.textDescLink}">${this.card.textDesc}</a></small>
            </div>
          </li>
        `;
  }


}

Войти в полноэкранный режим Выход из полноэкранного режима

Теперь мы можем определить стили; мы также импортируем общий стиль, который является определением шрифта:

...

import { sharedStyles } from './shared/style';

const componentStyle = css`
  h2 {
    margin-bottom: 1rem;
  }

  .card + .card {
    margin-top: 1.5rem;
  }

  @supports (display: grid) {
    .card + .card {
      margin-top: 0;
    }
  }

  .card {
    cursor: pointer;
    border: 1px solid;
    border-radius: 0.25rem;
    display: flex;
    flex-direction: column;
    position: relative;
  }

  .card .text {
    padding: 1rem;
    flex: 1 0 auto;
    display: flex;
    flex-direction: column;
    cursor: pointer;
  }

  .card p {
    max-width: 60ch;
  }

  .card .img {
    height: 6.5rem;
    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 calc(100% - 1rem));
  }

  .card .img img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    filter: grayscale(100%);
  }

  .card a {
    outline: none;
  }

  .card h2 a {
    text-decoration: none;
  }

  .card h2 a:focus {
    text-decoration: underline;
  }

  .card h2:focus-within ~ .cta {
    box-shadow: 0 0 0 0.125rem;
  }

  .card:focus-within h2 a:focus {
    text-decoration: none;
  }

  .card small {
    display: block;
    text-align: right;
  }

  .card small a {
    position: relative;
    text-decoration: none;
    padding: 0.5rem 0;
  }

  .card small a:hover,
  .card small a:focus {
    text-decoration: underline;
  }

  .card .text > * + * {
    margin-top: 0.75rem;
  }

  .card .text > :nth-last-child(3) {
    margin-bottom: 0.75rem;
  }

  .card .text > :nth-last-child(2) {
    margin-top: auto;
    padding-top: 0.75rem;
  }

  .cta {
    padding: 0.75rem;
    border: 1px solid;
    border-radius: 0.25rem;
    text-align: center;
  }

  .cta > a {
    text-decoration: none;
  }
`;

@customElement('card-image')
export class Card extends LitElement {

    static styles = [sharedStyles, componentStyle];

    ...

}

Вход в полноэкранный режим Выход из полноэкранного режима

Наконец, мы настроим два события, чтобы решить проблему избыточного события щелчка мыши, перечисленную здесь

...

@customElement('card-image')
export class Card extends LitElement {

    ...

    @query('#card-link') cardLinkEl!: HTMLAnchorElement;

    render() {
    return html`
      <li class="card">
        <div class="img">
          <img
            src="${this.card.image}"
            alt="${this.card.altText}"
            @mousedown="${this.mouseDown}"
            @mouseup="${this.handleClick}"
          />
        </div>
        <div class="text">
          <h2>
            <a
              id="card-link"
              href="${this.card.link}"
              @mousedown="${this.mouseDown}"
              @mouseup="${this.handleClick}"
              aria-describedby="desc-a-card"
              >${this.card.title}</a
            >
          </h2>
          <p>${this.card.text}</p>
          <span class="cta" aria-hidden="true" id="desc-a-card"
            ><a href="${this.card.link}">${this.card.ctaText}</a>
          </span>
          <small><a href="${this.card.textDescLink}">${this.card.textDesc}</a></small>
        </div>
      </li>
    `;
  }

  mouseDown() {
    this.down = Number(new Date());
  }

  handleClick() {
    this.up = Number(new Date());
    const total = this.up - this.down;

    if (total < 200) {
      this.cardLinkEl.click();
    }
  }

}

Вход в полноэкранный режим Выход из полноэкранного режима

И теперь у нас должен получиться компонент с одной карточкой, который выглядит следующим образом:

В следующей части мы протестируем этот компонент и сделаем другой, который повторно использует оригинальную карточку.

Оцените статью
devanswers.ru
Добавить комментарий