Web Components, comunicação entre componentes (parte 5)
Tags: javascript, events, webcomponents, webdev
Published: 2021-06-22 21:13:24

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.

```js
const clickEvent = new Event('click')

document.querySelector('button').dispatchEvent(clickEvent)
```

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](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.

```js
const formErrorEvent = new CustomEvent('form-error', {
  detail: new Error('Form Error')
})
```

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
<!-- HTML -->
<app-root></app-root>
```

```js
// 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)
```

## 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
<!-- HTML -->
<app-root></app-root>
```

```js
// 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)
```

## 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
<!-- HTML -->
<app-root></app-root>
```

```jsx
// 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)
```

---

## 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!!!