kb.erickguedes.com
HTML5: Estrutura e Semântica Web

Web Components

Aula 4 de 6

Web Components

Web Components são um conjunto de APIs nativas do navegador para criar componentes reutilizáveis e encapsulados.

Custom Elements

class MeuBotao extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  static get observedAttributes() {
    return ['tipo', 'label'];
  }

  connectedCallback() {
    this.render();
    this.addEventListener('click', this._handleClick);
  }

  disconnectedCallback() {
    this.removeEventListener('click', this._handleClick);
  }

  attributeChangedCallback(nome, oldVal, newVal) {
    if (oldVal !== newVal) this.render();
  }

  _handleClick() {
    this.dispatchEvent(new CustomEvent('meu-click', {
      detail: { tipo: this.getAttribute('tipo') },
      bubbles: true
    }));
  }

  render() {
    const label = this.getAttribute('label') || 'Clique';
    const tipo = this.getAttribute('tipo') || 'primario';

    this.shadowRoot.innerHTML = `
      <style>
        button {
          padding: 0.75rem 1.5rem;
          border: none;
          border-radius: 6px;
          cursor: pointer;
          font-size: 1rem;
          transition: transform 0.2s;
        }
        button:hover { transform: scale(1.05); }
        .primario { background: #3498db; color: white; }
        .perigo { background: #e74c3c; color: white; }
      </style>
      <button class="${tipo}">${label}</button>
    `;
  }
}

customElements.define('meu-botao', MeuBotao);
<meu-botao tipo="primario" label="Salvar"></meu-botao>
<meu-botao tipo="perigo" label="Excluir"></meu-botao>

<script>
  document.querySelector('meu-botao').addEventListener('meu-click', (e) => {
    console.log('Botão clicado:', e.detail.tipo);
  });
</script>

Shadow DOM e Slots

class CardComponent extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          border: 1px solid #ddd;
          border-radius: 12px;
          padding: 1rem;
          max-width: 300px;
          font-family: system-ui, sans-serif;
        }
        ::slotted(img) {
          width: 100%;
          border-radius: 8px;
        }
        .titulo {
          font-size: 1.25rem;
          font-weight: bold;
          margin: 0.5rem 0;
        }
      </style>
      <slot name="imagem"></slot>
      <div class="titulo"><slot name="titulo">Título Padrão</slot></div>
      <slot></slot>
    `;
  }
}

customElements.define('meu-card', CardComponent);
<meu-card>
  <img slot="imagem" src="foto.jpg" alt="Descrição">
  <span slot="titulo">Card Personalizado</span>
  <p>Conteúdo do card via slot padrão.</p>
</meu-card>

Templates HTML

<template id="template-lista">
  <style>
    ul { list-style: none; padding: 0; }
    li { padding: 0.5rem; border-bottom: 1px solid #eee; }
    li:hover { background: #f5f5f5; }
  </style>
  <ul id="lista"></ul>
</template>

<div id="container-lista"></div>
class ListaDinamica extends HTMLElement {
  constructor() {
    super();
    const template = document.getElementById('template-lista');
    const clone = template.content.cloneNode(true);
    this.attachShadow({ mode: 'open' }).appendChild(clone);
  }

  set items(valores) {
    const ul = this.shadowRoot.getElementById('lista');
    ul.innerHTML = valores.map(item => `<li>${item}</li>`).join('');
  }
}

customElements.define('lista-dinamica', ListaDinamica);

// Uso
const lista = document.querySelector('lista-dinamica');
lista.items = ['HTML', 'CSS', 'JavaScript'];

lit-html Básico

<script type="module">
  import { html, render } from 'https://cdn.jsdelivr.net/npm/lit-html@3/+esm';

  const counter = (count) => html`
    <style>
      .counter { display: flex; align-items: center; gap: 1rem; }
    </style>
    <div class="counter">
      <button @click=${() => update(count - 1)}>-</button>
      <span>${count}</span>
      <button @click=${() => update(count + 1)}>+</button>
    </div>
  `;

  function update(count) {
    render(counter(count), document.getElementById('app'));
  }

  update(0);
</script>

Lab: Componente de Accordion

Crie um Web Component <meu-accordion> com Shadow DOM, slots e eventos customizados.

# Teste em servidor local
npx live-server --port=5500

Web Components são o padrão nativo da plataforma — independentes de framework, funcionam para sempre.