Web Components, comunicação entre componentes (parte 5)

gabrieljm

Gabriel José

Posted on June 22, 2021

Web Components, comunicação entre componentes (parte 5)

Essa é a quinta parte da série de tutoriais sobre Web Components, não deixe de ver as outras partes. Neste tutorial vamos ver um pouco mais sobre algumas abordagens de como podemos fazer uma comunicação entre nossos componentes.

Eventos

Sim, já que todo componente que criamos se trata de um elemento HTML customizado, nós podemos ouvir e disparar eventos como qualquer outro elemento faz, além de adicionar eventos customizados também. Eventos serão a forma mais comum que terá para fazer a comunicação entre os elementos.

Disparando eventos

Caso você não sabia nós podemos disparar os eventos do HTML de forma programática, sem a necessidade de interações do usuário.

const clickEvent = new Event('click')

document.querySelector('button').dispatchEvent(clickEvent)
Enter fullscreen mode Exit fullscreen mode

Com esse simples código você verá que o evento atrelado ao botão foi disparado sem que houvesse um real click nele.

A classe Event recebe dois parâmetros, sendo o primeiro o nome do evento e o segundo sendo um objeto de configuração para o evento, em que podemos configurar coisas como bubbles, cancelable, composed. Para saber mais olhe: https://developer.mozilla.org/en-US/docs/Web/API/Event/Event

Criando eventos personalizados

Utilizando de uma API muito parecida com a de eventos que acabamos de ver, podemos usar a classe CustomEvent para criar um evento customizado.

const formErrorEvent = new CustomEvent('form-error', {
  detail: new Error('Form Error')
})
Enter fullscreen mode Exit fullscreen mode

Como pode ver a API é praticamente a mesma, no caso dos custom events nós podemos passar o atributo detail em que podemos passar qualquer valor que queremos propagar a outros elementos.

Essa aliás é uma ótima forma para fazer a comunicação entre os elementos.

Exemplo

Um simples exemplo usando um evento customizado:

<!-- HTML -->
<app-root></app-root>
Enter fullscreen mode Exit fullscreen mode
// Javascript
class AppForm extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: 'open' })
    this.shadowRoot.innerHTML = `
      <form>
        <input placeholder="Name" />
        <button>Submit</button>
      </form>
    `
  }

  connectedCallback() {
    const input = this.shadowRoot.querySelector('input')
    const form = this.shadowRoot.querySelector('form')

    form.addEventListener('submit', ev => {
      ev.preventDefault()

      if(!input.value) {
        const formErrorEvent = new CustomEvent('form-error', {
          detail: new Error('Empty name field')
        })

        this.dispatchEvent(formErrorEvent)
      }
    })
  }
}

customElements.define('app-form', AppForm)

class AppRoot extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: 'open' })
    this.shadowRoot.innerHTML = '<app-form></app-form>'
  }

  connectedCallback() {
    this.shadowRoot
      .querySelector('app-form')
      .addEventListener('form-error', ev => {
        console.log(ev.detail.message)
      })
  }
}

customElements.define('app-root', AppRoot)
Enter fullscreen mode Exit fullscreen mode

API do componente

Eventos são muito úteis quando queremos obter o valor do resultado de uma operação feita por outro elemento ou simplesmente de ser notificador quando algo ocorrer. Porém, existem situações em que queremos simplesmente que o elemento mude seu comportamento ou estado atual, nessas situações construir uma API é a melhor forma de comunicação, pois nós pedimos ao elemento que ele faça algo e ele internamente faz o que for necessário para que aquilo ocorra.

Exemplo

<!-- HTML -->
<app-root></app-root>
Enter fullscreen mode Exit fullscreen mode
// Javascript
class LightSwitch extends HTMLElement {
  // Estado do elemento
  #isOn = false

  constructor() {
    super()
    this.attachShadow({ mode: 'open' })
    this.shadowRoot.innerHTML = `
      <style>
        div {
          width: max-content;
          padding: 14px;
          border-radius: 6px;
        }

        .off {
          background-color: #ddd;
        }

        .on {
          background-color: #08c;
        }
      </style>
      <div class="off">
        <button>Toggle</button>
      </div>
    `
  }

  connectedCallback() {
    this.shadowRoot
      .querySelector('button')
      .addEventListener('click', () => {
        this.toggle()
      })
  }

  /*
    Método público que pode ser usado
    para mudar o estado do elemento
  */
  toggle() {
    this.#isOn = !this.#isOn
    const className = this.#isOn ? 'on' : 'off'
    this.shadowRoot.querySelector('div').className = className
  }
}

customElements.define('light-switch', LightSwitch)

class AppRoot extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: 'open' })
    this.shadowRoot.innerHTML = `
      <light-switch></light-switch>
      <button>
        Toggle from outside
      </button>
    `
  }

  connectedCallback() {
    const lightSwitch = this.shadowRoot
      .querySelector('light-switch')

    this.shadowRoot
      .querySelector('button')
      .addEventListener('click', () => {
        // Chamando o método para alterar o estado do elemento
        lightSwitch.toggle()
      })
  }
}

customElements.define('app-root', AppRoot)
Enter fullscreen mode Exit fullscreen mode

Comunicação por terceiros

Por terceiros, me refiro a outros elementos ou estruturas na qual podemos delegar a parte da comunicação para uma entidade que não é diretamente quem queremos impactar. Esse tipo de abordagem é muito útil quando queremos que algo seja refletido em vários elementos de uma vez e/ou quando não sabemos quais elementos serão afetados. É uma abordagem muito comum para o gerenciamento de estado, seja específico a alguns componentes ou um estado global.

Devo enfatizar que essa é somente uma forma de gerenciar essa parte de estado compartilhado e afins.

Exemplo

O exemplo abaixo é simples, usando um objeto específico para manter o estado de um contador e utilizando de eventos para capturar as mudanças que acontecerem.

<!-- HTML -->
<app-root></app-root>
Enter fullscreen mode Exit fullscreen mode
// Javascript
class CounterStore {
  count = 0
  #events = {
    onCountChange: [] 
  }

  increment() {
    this.count++
    for(const event of this.#events.onCountChange) {
      event()
    }
  }

  onCountChange(listener) {
    this.#events.onCountChange.push(listener)
  }
}

const counterStore = new CounterStore()

class AppRoot extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: 'open' })
    this.shadowRoot.innerHTML = `
      <div>Count: ${counterStore.count}</div>
      <button>Increment</button>
    `
  }

  connectedCallback() {
    this.shadowRoot
      .querySelector('button')
      .addEventListener('click', () => {
        counterStore.increment()
      })

    counterStore.onCountChange(() => {
      this.shadowRoot
        .querySelector('div')
        .innerText = `Count: ${counterStore.count}`
    })
  }
}

customElements.define('app-root', AppRoot)
Enter fullscreen mode Exit fullscreen mode

Conclusão

Agora você viu como podemos mexer com a comunicação entre nossos Web Components, lembrando que isso que mostrei são só abordagens e que é sempre bom lembrar que aqui estamos mexendo com Javascript puro, então há espaço para que você crie sua própria maneira de gerenciar isso. Espero muito que tenha gostado e caso tenha alguma dúvida pode deixar nos comentários e até o próximo!!!

💖 💪 🙅 🚩
gabrieljm
Gabriel José

Posted on June 22, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related