Розробка бібліотеки Web Components (Stencil)

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка бібліотеки Web Components (Stencil)
Складна
~1-2 тижні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Розробка бібліотеки Web Components (Stencil)

Stencil — компілятор компонентів від Ionic. Написані компоненти компілюються в нативні Web Components з опціональною підтримкою Angular, React, Vue через автогенеровані обгортки. Stencil — не runtime-бібліотека, а інструмент збірки: найбільший бандл містить тільки нативні API плюс мінімальний polyfill-шар.

Ключова різниця від Lit: Stencil генерує framework-специфічні пакети. Якщо бібліотека повинна працювати як native в Angular (з двостороннім біндингом, формами), як React-компоненти (з типізованими props) — Stencil робить це з коробки.

Встановлення та ініціалізація

npm init stencil@latest
# Вибрати: component library
cd my-library
npm install

Структура проекту:

src/
├── components/
│   ├── ui-button/
│   │   ├── ui-button.tsx        ← компонент
│   │   ├── ui-button.css        ← стилі
│   │   ├── ui-button.e2e.ts     ← E2E тести
│   │   └── ui-button.spec.ts    ← unit тести
│   └── ui-input/
├── utils/
├── index.ts
└── index.html

stencil.config.ts

Компонент на Stencil

Stencil використовує TSX (як React) та декоратори для розмітки компонента:

import {
  Component,
  Host,
  h,
  Prop,
  State,
  Event,
  EventEmitter,
  Method,
  Watch,
  Element,
  Listen,
} from '@stencil/core'

@Component({
  tag: 'ui-button',
  styleUrl: 'ui-button.css',
  shadow: true,  // включити Shadow DOM
  // scoped: true,  // замість Shadow DOM — scoped CSS (немає slots, але працює з формами)
})
export class UiButton {
  // Посилання на host елемент
  @Element() el!: HTMLElement

  // Props — публічні властивості/атрибути
  @Prop() variant: 'primary' | 'secondary' | 'ghost' = 'primary'
  @Prop() size: 'sm' | 'md' | 'lg' = 'md'
  @Prop({ reflect: true }) disabled = false
  @Prop({ mutable: true }) loading = false  // mutable — компонент може змінювати

  // Внутрішній стан
  @State() private focused = false

  // События
  @Event({ eventName: 'uiClick', bubbles: true, composed: true })
  uiClick!: EventEmitter<{ nativeEvent: MouseEvent }>

  // Watch — реакція на зміну prop/state
  @Watch('disabled')
  onDisabledChange(newVal: boolean) {
    this.el.setAttribute('aria-disabled', String(newVal))
  }

  // Listen — слухаємо події (на host або document)
  @Listen('focus', { target: 'window' })
  onWindowFocus(e: FocusEvent) {
    // ...
  }

  // Публічний метод — викликається з JS
  @Method()
  async focusButton() {
    this.el.shadowRoot?.querySelector('button')?.focus()
  }

  private handleClick = (e: MouseEvent) => {
    if (this.disabled || this.loading) return
    this.uiClick.emit({ nativeEvent: e })
  }

  render() {
    return (
      <Host
        class={{
          'is-disabled': this.disabled,
          'is-loading': this.loading,
        }}
        aria-disabled={this.disabled ? 'true' : null}
      >
        <button
          type="button"
          disabled={this.disabled || this.loading}
          class={`btn btn--${this.variant} btn--${this.size}`}
          onClick={this.handleClick}
          onFocus={() => (this.focused = true)}
          onBlur={() => (this.focused = false)}
        >
          {this.loading && <span class="spinner" aria-hidden="true"></span>}
          <slot name="icon-start"></slot>
          <slot></slot>
          <slot name="icon-end"></slot>
        </button>
      </Host>
    )
  }
}
/* ui-button.css */
:host {
  display: inline-flex;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border: none;
  cursor: pointer;
  font-family: inherit;
  font-weight: 600;
  border-radius: var(--ui-radius, 8px);
  transition: background 0.15s, transform 0.1s;
}

.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn:active:not(:disabled) { transform: scale(0.97); }

.btn--primary  { background: var(--ui-primary, #7000ff); color: #fff; }
.btn--secondary { background: transparent; border: 1.5px solid var(--ui-primary, #7000ff); color: var(--ui-primary, #7000ff); }
.btn--ghost    { background: transparent; color: var(--ui-primary, #7000ff); }

.btn--sm { padding: 6px 14px;  font-size: 13px; }
.btn--md { padding: 10px 22px; font-size: 15px; }
.btn--lg { padding: 14px 30px; font-size: 17px; }

Stencil Config: кілька output targets

Головна можливість Stencil — скомпілювати один компонент в кілька форматів:

// stencil.config.ts
import { Config } from '@stencil/core'
import { angularOutputTarget } from '@stencil/angular-output-target'
import { reactOutputTarget } from '@stencil/react-output-target'
import { vueOutputTarget } from '@stencil/vue-output-target'

export const config: Config = {
  namespace: 'my-ui',

  outputTargets: [
    // 1. Нативні Web Components
    {
      type: 'dist',
      esmLoaderPath: '../loader',
    },

    // 2. dist-custom-elements — tree-shakeable
    {
      type: 'dist-custom-elements',
      customElementsExportBehavior: 'auto-define-custom-elements',
      externalRuntime: false,
    },

    // 3. React обгортки (автогенерація)
    reactOutputTarget({
      componentCorePackage: 'my-ui-core',
      proxiesFile: '../my-ui-react/src/components.ts',
      includeDefineCustomElements: true,
    }),

    // 4. Vue обгортки
    vueOutputTarget({
      componentCorePackage: 'my-ui-core',
      proxiesFile: '../my-ui-vue/src/components.ts',
    }),

    // 5. Angular обгортки з NgModule
    angularOutputTarget({
      componentCorePackage: 'my-ui-core',
      outputType: 'standalone',  // або 'component'
      directivesProxyFile: '../my-ui-angular/src/directives/proxies.ts',
    }),

    // 6. Документація
    { type: 'docs-readme' },
    { type: 'docs-json', file: './dist/docs.json' },

    // 7. Custom Elements Manifest для IDE-підсказок
    { type: 'docs-vscode', file: './dist/vscode.html-data.json' },
  ],

  testing: {
    browserHeadless: 'new',
  },
}

Автогенеровані React-обгортки

Після npm run build в my-ui-react/src/components.ts:

// Автогенеровано Stencil — не редагувати вручну
import { createReactComponent } from './react-component-lib'
import { defineCustomElements } from 'my-ui-core/loader'
defineCustomElements()

export const UiButton = /*@__PURE__*/ createReactComponent<
  JSX.UiButton,
  HTMLUiButtonElement
>('ui-button')

export const UiInput = /*@__PURE__*/ createReactComponent<
  JSX.UiInput,
  HTMLUiInputElement
>('ui-input')

Використання в React:

import { UiButton, UiInput } from 'my-ui-react'

function Form() {
  const handleClick = (e: CustomEvent<{ nativeEvent: MouseEvent }>) => {
    console.log('clicked', e.detail)
  }

  return (
    <form>
      <UiInput label="Email" type="email" required />
      <UiButton
        variant="primary"
        onUiClick={handleClick}  // типізований event handler
        loading={false}
      >
        Відправити
      </UiButton>
    </form>
  )
}

Тестування

Stencil включає Stencil Testing Utils поверх Jest + Puppeteer:

// ui-button.spec.ts — unit тести
import { newSpecPage } from '@stencil/core/testing'
import { UiButton } from './ui-button'

describe('ui-button', () => {
  it('renders with default props', async () => {
    const page = await newSpecPage({
      components: [UiButton],
      html: '<ui-button>Click me</ui-button>',
    })

    expect(page.root).toEqualHtml(`
      <ui-button>
        <mock:shadow-root>
          <button class="btn btn--primary btn--md" type="button">
            <slot name="icon-start"></slot>
            <slot></slot>
            <slot name="icon-end"></slot>
          </button>
        </mock:shadow-root>
        Click me
      </ui-button>
    `)
  })

  it('disables button when disabled prop is set', async () => {
    const page = await newSpecPage({
      components: [UiButton],
      html: '<ui-button disabled></ui-button>',
    })

    const button = page.root?.shadowRoot?.querySelector('button')
    expect(button?.disabled).toBe(true)
  })

  it('emits uiClick event', async () => {
    const page = await newSpecPage({
      components: [UiButton],
      html: '<ui-button></ui-button>',
    })

    const clickSpy = jest.fn()
    page.root?.addEventListener('uiClick', clickSpy)

    page.root?.shadowRoot?.querySelector('button')?.click()

    expect(clickSpy).toHaveBeenCalled()
  })
})

Монорепозиторій: структура мульти-пакетного проекту

packages/
├── core/           ← Stencil компоненти (my-ui-core)
├── react/          ← React обгортки (my-ui-react)
├── vue/            ← Vue обгортки (my-ui-vue)
├── angular/        ← Angular обгортки (my-ui-angular)
└── docs/           ← Storybook

Терміни

5–8 компонентів з dist та React output — 2–3 тижні. Повна бібліотека з Angular/Vue обгортками, E2E-тестами, Storybook, CD-пайплайном та npm-публікацією — 6–10 тижнів.