Recentemente, fui apresentado a um design desafiador em funcionamento: um componente com uma fileira de botões na parte superior. O problema era que sempre que o componente não era largo o suficiente para caber em todos os botões, essas ações precisavam ser movidas para um menu suspenso.

Construir uma IU que pode se adaptar a larguras de tela variadas e larguras de contêineres variadas é um desafio que se tornou mais comum com a crescente popularidade de estruturas baseadas em componentes, como React e Vue.js, bem como componentes da web nativos. O mesmo componente pode precisar funcionar em uma ampla área de conteúdo principal e em uma coluna lateral estreita-em todos os dispositivos, nada menos.

O que é ResizeObserver?

O ResizeObserver API é uma ótima ferramenta para criar UIs que podem se adaptar à tela do usuário e largura do contêiner. Usando um ResizeObserver, podemos chamar uma função sempre que um elemento é redimensionado, da mesma forma que ouvir uma janela evento resize .

Os casos de uso para ResizeObserver podem não ser imediatamente óbvios, então vamos dar uma olhada em alguns exemplos práticos.

Preenchimento um contêiner

Para nosso primeiro exemplo, imagine que você deseja mostrar uma fileira de fotos inspiradoras aleatórias abaixo da seção de herói de sua página. Você só deseja carregar quantas fotos forem necessárias para preencher essa linha e deseja adicionar ou remover fotos conforme necessário sempre que a largura do contêiner mudar.

Poderíamos aproveitar os eventos de redimensionamento, mas talvez a largura do nosso componente também muda sempre que um usuário recolhe um painel lateral. É aí que ResizeObserver se torna útil.

Veja a caneta
ResizeObserver-Fill Container
por Kevin Drum ( @kevinleedrum )
em CodePen .

Olhando nosso JavaScript para este exemplo, as primeiras linhas configuram nosso observador:

const resizeObserver=new ResizeObserver (onResize); resizeObserver.observe (document.querySelector (“. container”));

Criamos um novo ResizeObserver, passando uma função de retorno de chamada para o construtor. Em seguida, informamos ao nosso novo observador qual elemento observar.

Lembre-se de que é possível observar vários elementos com um único observador se você encontrar a necessidade.

Depois disso, chegamos à lógica central de nossa IU:

const IMAGE_MAX_WIDTH=200; const IMAGE_MIN_WIDTH=100; função onResize (entradas) {entrada const=entradas [0]; const container=entry.target;/* Calcule quantas imagens cabem no contêiner. */const imagesNeeded=Math.ceil (entry.contentRect.width/IMAGE_MAX_WIDTH); let images=container.children;/* Remova as imagens conforme necessário. */while (images.length> imagesNeeded) {images [images.length-1].remove (); }/* Adicione imagens conforme necessário. */while (images.length

Depois de definir as larguras mínima e máxima para nossas imagens (para que possam preencher toda a largura), declaramos nosso retorno de chamada onResize. O ResizeObserver passa uma matriz de objetos ResizeObserverEntry para nossa função.

Como estamos observando apenas um elemento, nosso array contém apenas uma entrada. Esse objeto de entrada fornece as novas dimensões do elemento redimensionado (por meio da propriedade contentRect), bem como uma referência ao próprio elemento (a propriedade de destino).

Usando a nova largura do nosso elemento atualizado, podemos calcular quantas imagens devem ser mostradas e compare com o número de imagens já mostradas (os filhos do elemento contêiner). Depois disso, é tão simples quanto remover elementos ou adicionar novos elementos.

Para fins de demonstração, estou mostrando imagens aleatórias de Lorem Picsum .

Alterando uma linha flexível para uma coluna

Nosso segundo exemplo aborda um problema que é bastante comum: transformar uma linha flexível de elementos em uma coluna sempre que esses elementos não couberem em uma única linha (sem transbordar ou quebrar).

Com a API ResizeObserver, isso é totalmente possível.

Veja a caneta
ResizeObserver-Flex Direction
de Kevin Drum ( @kevinleedrum )
em CodePen .

Nossa função onResize neste exemplo parece assim:

let rowWidth; função onResize (entradas) {entrada const=entradas [0]; const container=entry.target; if (! rowWidth) rowWidth=Array.from (container.children).reduce ((acc, el)=> getElWidth (el) + acc, 0); const isOverflowing=rowWidth> entry.contentRect.width; if (isOverflowing &&! container.classList.contains (“container-vertical”)) {requestAnimationFrame (()=> {container.classList.add (“container-vertical”);}); } else if (! isOverflowing && container.classList.contains (“container-vertical”)) {requestAnimationFrame (()=> {container.classList.remove (“container-vertical”);}); }}

A função soma as larguras de todos os botões, incluindo a margem, para descobrir a largura do contêiner para mostrar todos os botões em uma linha. Estamos armazenando em cache essa largura calculada em uma variável rowWidth cujo escopo está fora de nossa função, para que não percamos tempo calculando-a toda vez que o elemento for redimensionado.

Assim que soubermos a largura mínima necessária para todos os botões, podemos comparar isso com a nova largura do contêiner e transformar a linha em uma coluna se os botões não couberem. Para conseguir isso, estamos simplesmente alternando uma classe vertical de contêiner no contêiner.

E as consultas de contêiner?

Alguns dos problemas que podem ser resolvidos com ResizeObserver podem ser resolvidos muito mais eficiente com CSS consultas de contêiner , que agora são suportadas em Chrome Canary. No entanto, uma desvantagem das consultas de contêiner é que elas exigem valores conhecidos para largura mínima, proporção de aspecto etc.

ResizeObserver, por outro lado, nos dá poder ilimitado para examinar todo o DOM e escrever a lógica tão complexo quanto quisermos. Além disso, ele já é compatível com todos os principais navegadores.

Componente da barra de ferramentas responsiva

Você se lembra daquele problema de trabalho que mencionei em que precisava mover os botões de maneira responsiva para um menu suspenso? Nosso exemplo final é muito semelhante.

Conceitualmente, este exemplo se baseia no exemplo anterior porque estamos mais uma vez verificando quando estamos estourando um contêiner. Nesse caso, precisamos repetir essa verificação toda vez que removermos um botão para ver se precisamos remover outro botão.

Para reduzir a quantidade de clichê, estou usando Vue.js para este exemplo , embora a ideia deva funcionar para qualquer estrutura. Também estou usando o Popper para posicionar o menu suspenso.

Ver a caneta
ResizeObserver-Barra de ferramentas responsiva
de Kevin Drum ( @kevinleedrum )
em CodePen .

Há bastante um pouco mais de código para este exemplo, mas vamos decompô-lo. Toda a nossa lógica reside dentro de uma instância (ou componente) do Vue:

new Vue ({el:”#app”, data () {return {actions: [“Edit”,”Save”,”Copy”,”Rename”,”Share”,”Delete”], isMenuOpen: false, menuActions: []//Ações que devem ser mostradas no menu};},

Temos três propriedades de dados importantes que compõem o “estado” de nosso componente.

A matriz de ações lista todas as ações que precisamos mostrar em nossa IU O booleano isMenuOpen é um sinalizador que podemos alternar para mostrar ou ocultar o menu de ação. A matriz menuActions conterá uma lista de ações que deveriam ser mostrado no menu (quando não há espaço suficiente para mostrá-los como botões)

Atualizaremos este array conforme necessário em nosso retorno de chamada onResize e nosso HTML será atualizado automaticamente.

calculado: {actionButtons ( ) {//As ações que devem ser mostradas como botões fora do menu retornam this.actions.filter ((action)=>! This.menuActions.includes (action));}},

Somos você cantar uma Vue computed property chamada actionButtons para gerar uma série de ações que devem ser mostradas como botões. É o inverso de menuActions.

Com esses dois arrays, nosso modelo HTML pode simplesmente iterar ambos para criar os botões e itens de menu, respectivamente:

& hellip;

Se você não está familiarizado com Vue sintaxe do modelo , não se preocupe muito. Saiba que estamos criando botões e itens de menu dinamicamente com manipuladores de eventos de clique dessas duas matrizes, e estamos mostrando ou ocultando um menu suspenso baseado no booleano isMenuOpen.

Os atributos ref também nos permitem para acessar esses elementos de nosso script sem ter que usar um querySelector.

O Vue fornece alguns métodos de ciclo de vida que nos permitem configurar nosso observador quando o componente é carregado pela primeira vez e limpá-lo sempre que nosso componente é destruído:

montado () {//Anexar ResizeObserver ao contêiner resizeObserver=new ResizeObserver (this.onResize); resizeObserver.observe (this. $ refs.container);//Fechar o menu em qualquer clique document.addEventListener (“click”, this.closeMenu); }, beforeDestroy () {//Limpe o observador e o ouvinte de evento resizeObserver.disconnect (); document.removeEventListener (“clique”, this.closeMenu); },

Agora vem a parte divertida, que é nosso método onResize:

métodos: {onResize () {requestAnimationFrame (async ()=> {//Coloque todos os botões fora do menu if (this.menuActions. length) {this.menuActions=[]; aguarde isso. $ nextTick ();} const isOverflowing=()=> isso. $ refs.container.scrollWidth> this. $ refs.container.offsetWidth;//Mova os botões para o menu até que o contêiner não esteja mais transbordando enquanto (isOverflowing () && this.actionButtons.length) {const lastActionButton=this.actionButtons [this.actionButtons.length-1]; this.menuActions.unshift (lastActionButton); aguarde isso $. nextTick ();}}); },

A primeira coisa que você pode notar é que envolvemos tudo em uma chamada para requestAnimationFrame . Isso simplesmente limita a frequência com que nosso código pode ser executado (normalmente 60 vezes por segundo). Isso ajuda a evitar que o limite do loop ResizeObserver exceda os avisos do console, o que pode acontecer sempre que o retorno de chamada do observador tenta executar várias vezes durante um único quadro de animação.

Com isso fora do caminho, nossos métodos onResize começam redefinindo para um estado padrão, se necessário. O estado padrão é quando todas as ações são representadas por botões, não itens de menu.

Como parte dessa redefinição, ele aguarda uma chamada para this. $ nextTick , que diz ao Vue para ir em frente e atualizar seu DOM virtual, para que nosso elemento contêiner volte à largura máxima com todos os botões mostrando.

//Coloque todos os botões fora do menu if (this.menuActions.length) {this.menuActions=[]; aguarde isso. $ nextTick (); }

Agora que temos uma linha completa de botões, precisamos verificar se a linha está transbordando para sabermos se precisamos mover algum dos botões para o nosso menu de ação.

Uma maneira simples para identificar se um elemento está estourando, é preciso comparar seu scrollWidth com seu offsetWidth. Se scrollWidth for maior, o elemento está estourando.

const isOverflowing=()=> this. $ Refs.container.scrollWidth> this. $ Refs.container.offsetWidth;

O resto do nosso método onResize é um loop while. Durante cada iteração, verificamos se o contêiner está estourando e, se estiver, movemos mais uma ação para o array menuActions. O loop só é interrompido quando não estamos mais sobrecarregando o contêiner ou quando movemos todas as ações para o menu.

Observe que estamos aguardando isso. $ NextTick () após cada loop, portanto a largura do contêiner pode ser atualizada após a alteração para this.menuActions.

//Mova os botões para o menu até que o contêiner não esteja mais transbordando enquanto (isOverflowing () && this.actionButtons.length) {const lastActionButton=this.actionButtons [this.actionButtons.length-1]; this.menuActions.unshift (lastActionButton); aguarde isso. $ nextTick (); }

Isso engloba toda a magia de que precisávamos para vencer este desafio. Muito do restante do código em nosso componente Vue está relacionado ao comportamento do menu suspenso, que está fora do escopo deste artigo.

Conclusão

Esperançosamente, esses exemplos destacam a utilidade da API ResizeObserver, particularmente em abordagens baseadas em componentes para desenvolvimento de front-end. Junto com consultas de mídia CSS , o up-and-próximas consultas de contêiner e eventos de redimensionamento, ajuda a construir o base de interfaces responsivas na web moderna.