# decorateCookedElement para cada mudança de página?

**URL:** <https://meta.discourse.org/t/decoratecookedelement-for-each-page-change/214218>\
**Category:** Development\
**Created:** [Janeiro 8, 2022, 4:54am UTC](https://meta.discourse.org/t/decoratecookedelement-for-each-page-change/214218 "2022-01-08T04:54:17Z")\
**Posts on this page:** 1\
**Showing post:** 2

<div class="post-metadata">

**Author:** ![Johani](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/johani/32/176920_2.png) [@Johani](https://meta.discourse.org/u/Johani)\
**Post date:** [Janeiro 11, 2022, 6:58pm UTC](https://meta.discourse.org/t/decoratecookedelement-for-each-page-change/214218/2 "2022-01-11T18:58:07Z")

</div>

Este será um post um pouco longo, já que você postou em #Development. No entanto, é muito geral e trata mais de explicar os padrões a serem usados no Discourse, independentemente da biblioteca que deseja integrar ou dos elementos de post que deseja segmentar. Acontece apenas que o script que você escolheu é usado como exemplo principal.

Então, o MediaElement oferece algumas maneiras de inicializar um novo player de áudio.

1. Você pode adicionar uma classe CSS e alguns atributos ao elemento, e ele cuidará disso automaticamente.

2. Você o inicializa manualmente.

Você está usando atualmente a opção #1, então vamos analisar isso por um momento. Quando um script oferece “inicialização automática em um elemento”, geralmente é uma melhoria de qualidade de vida adicionada pelo autor do script. No script, eles geralmente ouvem o evento de carregamento do documento e realizam algum trabalho nos elementos do DOM que possuem a classe/os atributos que instruem você a adicionar.

Ok, então por que não está funcionando? Como você viu, funciona bem no carregamento inicial da página, mas não na navegação subsequente dentro do aplicativo. O que houve?

A resposta curta é que o Discourse é um aplicativo de página única. Elementos como as tags `<HTML>` e `<body>` são enviados apenas uma vez. Então, em certo sentido, o documento só é carregado uma vez. Assim, quando você navega após o carregamento inicial da página, não há mais eventos nativos de “carregamento” enviados. Lembre-se, o documento já foi carregado na visualização inicial da página. Tudo o que acontece depois disso é tratado pelo Discourse.

Claro, isso não significa que não haja eventos disparados na navegação subsequente. No entanto, esses são eventos específicos do Discourse. Portanto, os autores de scripts de terceiros não teriam como saber disso com antecedência. Imagine ser um autor de script e ter que atender a centenas de plataformas diferentes? Não é bom, né?

Então, não podemos usar a maneira de qualidade de vida que o autor do script adicionou tão gentilmente. E agora? Bem, lembre-se de que ainda podemos inicializar o script manualmente nos elementos de destino. Então, vamos tentar fazer isso.

Anteriormente, mencionei que há apenas um evento de carregamento nativo (no nível do navegador), mas uma plataforma como o Discourse não funcionaria bem se não tivesse seu próprio sistema de eventos. Por exemplo, a API de plugins tem um método que [permite disparar scripts na navegação virtual de páginas](https://github.com/discourse/discourse/blob/main/app/assets/javascripts/discourse/app/lib/plugin-api.js#L614-L622).

Você deve usar esse método? Não. Esse método é muito útil para coisas como análise de dados e afins. Não há sentido em disparar um script que lida apenas com tags `<audio>` em todas as páginas — especialmente se a página não tiver nenhuma delas.

Então, qual é o próximo passo? Bem, a boa notícia é que você já descobriu. [decorateCookedElement](https://github.com/discourse/discourse/blob/main/app/assets/javascripts/discourse/app/lib/plugin-api.js#L301-L323) é o método correto a ser usado aqui.

Ele oferece uma maneira de… espere por isso… decorar posts 🎉

O Discourse **garante** que qualquer decorador que você adicionar será aplicado a todos os posts.

Bem, você está carregando o script em um decorador de post, então ele deve ser adicionado e deve funcionar. Por que não funciona na navegação subsequente?

Para isso, você precisa entender como `loadScript()` funciona. Seu código já verifica se há elementos de destino válidos antes de carregar o script, então 👍

No entanto, imagine uma situação em que você tenha 20 a 30 posts seguidos onde todos possuem elementos válidos. Faria sentido carregar o script 20 a 30 vezes? Obviamente não.

`loadScript()` é inteligente o suficiente para detectar se o script já foi carregado. Ele não carregará duplicatas e também não recarregará um script se já tiver terminado o download. Você pode ver isso aqui.

> <https://github.com/discourse/discourse/blob/1472e47aae5bfdfb6fd9abfe89beb186c751f514/app/assets/javascripts/discourse/app/lib/load-script.js#L60-L74>

`fullUrl` acima é a URL que você passa para `loadScript()` ao chamá-lo, assim como no seu exemplo.

Então, agora que sabemos disso, podemos entender por que não funciona na navegação subsequente.

```plaintext
Você visita o tópico-a > ele tem um elemento de áudio > loadScript() carrega o script > o script faz a coisa sofisticada de "inicialização automática" > o script é inicializado nos seus elementos > você obtém elementos de áudio personalizados

então...

você visita o tópico-b > ele tem elementos de áudio > loadScript() vê que o script já foi carregado > nenhuma "inicialização automática" sofisticada > você obtém os elementos de áudio padrão > tristeza se segue

```

Então, como corrigir isso? Bem, é para isso que serve a opção #2 mencionada anteriormente.

> 1. Você pode adicionar uma classe CSS e alguns atributos ao elemento, e ele cuidará disso automaticamente.
> 
> 2. Você o inicializa manualmente.

Então, vamos fazer isso. Já está documentado na [página que você compartilhou](https://github.com/mediaelement/mediaelement/blob/d8a667fd65560093fb227700a82805600ec9e8e3/docs/usage.md). Precisamos chamar isso no nosso elemento de destino assim:

```javascript
// Você pode usar uma string para o ID do player (ou seja, `player`),
// ou `document.querySelector()` para qualquer seletor
var player = new MediaElementPlayer("player", {
  // ... opções
});

```

Seu código já trata cada elemento de áudio individualmente 👍, então só precisamos modificar isso:

```javascript
audioplayers.forEach(function (el) {
  el.classList.add("mejs__player");
  const controls = settings.theme_uploads.mejs - controls;
  // console.log("controls: " + controls);
  el.setAttribute(
    "data-mejsoptions",
    `{
                    "pluginPath": "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
                    "iconSprite": "https://friends.jimkleiber.com/uploads/default/original/1X/a17d9708a19654d9155dd9b79a79a05dea580067.svg",
                    "alwaysShowControls": "true",
                    "features": ["playpause", "current", "progress", "duration", "volume"]
                }`
  );
  el.setAttribute("preload", "auto");
});

```

para isso:

```diff
audioplayers.forEach(function (el) {
-- el.classList.add("mejs__player");
-- const controls = settings.theme_uploads.mejs - controls;
-- // console.log("controls: " + controls);
-- el.setAttribute(
-- "data-mejsoptions",
-- `{
-- "pluginPath": "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
-- "iconSprite": "https://friends.jimkleiber.com/uploads/default/original/1X/a17d9708a19654d9155dd9b79a79a05dea580067.svg",
-- "alwaysShowControls": "true",
-- "features": ["playpause", "current", "progress", "duration", "volume"]
-- }`
-- );
-- el.setAttribute("preload", "auto");
++ 
++ new MediaElementPlayer(el, {
++ // ... opções
++ });
});

```

e, em seguida, pegamos as opções que usávamos para adicionar como um atributo e as passamos como um objeto assim:

```diff
audioplayers.forEach(function (el) {
 new MediaElement(el, {
++ pluginPath: "//cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
++ iconSprite: settings.theme_uploads["mejs-controls"],
++ alwaysShowControls: "true",
++ features: ["playpause", "current", "progress", "duration", "volume"]
 });
});

```

Vamos deixar isso de lado por enquanto e olhar o restante do decorador. Aqui está o que temos até agora:

```javascript
let loadScript = require("discourse/lib/load-script").default;

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");
    // console.log("player: " + audioplayers[0]);
    if (Object.entries(audioplayers).length > 0) {
      // console.log("audioplayers has length");
      loadScript(
        `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement.min.js`
      );
      loadScript(
        `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
      );
    }

    // forEach usado anteriormente estava aqui
  },
  { id: "mediaelement-js", onlyStream: true }
);

```

Se você notar, está chamando `loadScript()` em dois scripts diferentes. Não tenho certeza se isso é intencional, mas você só precisa de um deles. Pense nisso como um pacote completo e um leve. Você quer o player de áudio personalizado. Então, você precisa do pacote completo. Vamos remover o outro.

```diff
let loadScript = require("discourse/lib/load-script").default;

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");
    // console.log("player: " + audioplayers[0]);
    if (Object.entries(audioplayers).length > 0) {
      // console.log("audioplayers has length");
-- loadScript(
-- `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement.min.js`
-- );
      loadScript(
        `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
      );
    }

    // forEach usado anteriormente estava aqui
  },
  { id: "mediaelement-js", onlyStream: true }
);

```

Você está verificando se há players de áudio no post e carregando o script condicionalmente com base nisso. Isso pode ser simplificado assim. Primeiro, verifique o comprimento diretamente.

```diff
let loadScript = require("discourse/lib/load-script").default;

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");
    // console.log("player: " + audioplayers[0]);
-- if (Object.entries(audioplayers).length > 0) {
++ if (audioplayers.length) {
      // console.log("audioplayers has length");

      loadScript(
        `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
      );
    }

    // forEach usado anteriormente estava aqui
  },
  { id: "mediaelement-js", onlyStream: true }
);

```

em seguida, mova isso para o topo e apenas `retorne` se o comprimento for falso (comprimento \< 0). Também removi os comentários do código.

```diff
let loadScript = require("discourse/lib/load-script").default;

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");

++ if (!audioplayers.length) {
++ return;
++ }

    loadScript(
      `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
    );

    // forEach usado anteriormente estava aqui
  },
  { id: "mediaelement-js", onlyStream: true }
);

```

Como o `src` do script nunca mudará, vamos movê-lo para um `const`. `loadScript()` também é sempre o mesmo. Vamos torná-lo um `const` também.

```diff
++ const loadScript = require("discourse/lib/load-script").default;

++ const MEDIA_ELEMENT_SRC =
++ "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");

    if (!audioplayers.length) {
      return;
    }

++ loadScript(MEDIA_ELEMENT_SRC);

    // forEach usado anteriormente estava aqui
  },
  { id: "mediaelement-js", onlyStream: true }
);

```

e vamos deixar este também de lado por enquanto. Antes de continuarmos, precisamos falar um pouco mais sobre como `loadScript()` funciona.

Se você quiser usar alguma parte de um script, deseja garantir que ele seja carregado antes de fazer qualquer trabalho, não é? Bem, `loadScript()` cuida disso para você. Ele retorna uma [Promessa](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). Promessas parecem assustadoras à primeira vista, mas são realmente simples. Uma Promessa é literalmente isso… _uma promessa_.

Você quer fazer algum trabalho… você promete ao navegador que avisará quando o trabalho estiver concluído… o navegador espera por você. É realmente tão simples quanto isso. O resto é apenas entender a sintaxe.

Não vou gastar muito tempo com isso, pois está um pouco fora do escopo deste tópico.

Vamos continuar. `loadScript()` é baseado em promessa. O Discourse promete ao navegador avisá-lo quando o script tiver sido totalmente carregado — seja porque o script não existe e precisa ser carregado ou simplesmente para verificar se já foi carregado.

Então, se fizermos algo assim:

```diff
const loadScript = require("discourse/lib/load-script").default;

const MEDIA_ELEMENT_SRC =
  "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");

    if (!audioplayers.length) {
      return;
    }

++ loadScript(MEDIA_ELEMENT_SRC).then(() => {
++ // isso só será disparado se o script tiver sido/carregado
++ console.log("meu script foi carregado");
++ });

    // forEach usado anteriormente estava aqui
  },
  { id: "mediaelement-js", onlyStream: true }
);

```

Agora, podemos voltar ao nosso loop `forEach` anterior e adicioná-lo exatamente ali, e saberemos com certeza que o script estará disponível.

```diff
const loadScript = require("discourse/lib/load-script").default;

const MEDIA_ELEMENT_SRC =
  "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");

    if (!audioplayers.length) {
      return;
    }

    loadScript(MEDIA_ELEMENT_SRC).then(() => {
++ audioplayers.forEach(function (el) {
++ new MediaElementPlayer(el, {
++ pluginPath: "//cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
++ iconSprite: settings.theme_uploads["mejs-controls"],
++ alwaysShowControls: "true",
++ features: ["playpause", "current", "progress", "duration", "volume"]
++ });
++ });
    });
  },
  { id: "mediaelement-js", onlyStream: true }
);

```

Agora, olhe para o trecho acima e veja se algo o incomoda…

As opções que passamos para a instância do script são sempre as mesmas, mas não as temos em um `const`. Vamos corrigir isso.

```diff
    const loadScript = require("discourse/lib/load-script").default;
    
    const MEDIA_ELEMENT_SRC =
      "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";
    
++ const MEDIA_ELEMENT_CONFIG = {
++ pluginPath: "//cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
++ iconSprite: settings.theme_uploads["mejs-controls"],
++ alwaysShowControls: "true",
++ features: ["playpause", "current", "progress", "duration", "volume"]
++ };
    
    api.decorateCookedElement(
      element => {
        const audioplayers = element.querySelectorAll("audio");
    
        if (!audioplayers.length) {
          return;
        }
    
        loadScript(MEDIA_ELEMENT_SRC).then(() => {
          audioplayers.forEach(function (el) {
++ new MediaElementPlayer(el, MEDIA_ELEMENT_CONFIG);
          });
        });
      },
      { id: "mediaelement-js", onlyStream: true }
    );

```

e é isso. Você estava muito perto no seu código; só precisava entender um pouco mais o `loadScript()`.

Agora, juntamos tudo isso.

Aba `common > header`

```xml
<script type="text/discourse-plugin" version="0.8.42">
  const loadScript = require("discourse/lib/load-script").default;

  const MEDIA_ELEMENT_SRC =
    "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";

  const MEDIA_ELEMENT_CONFIG = {
    pluginPath: "//cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
    iconSprite: settings.theme_uploads["mejs-controls"],
    alwaysShowControls: "true",
    features: ["playpause", "current", "progress", "duration", "volume"]
  };

  api.decorateCookedElement(
    element => {
      const audioplayers = element.querySelectorAll("audio");

      if (!audioplayers.length) {
        return;
      }

      loadScript(MEDIA_ELEMENT_SRC).then(() => {
        audioplayers.forEach(function (el) {
          new MediaElementPlayer(el, MEDIA_ELEMENT_CONFIG);
        });
      });
    },
    { id: "mediaelement-js", onlyStream: true }
  );
</script>

```

e então um pouco de CSS para carregar o CSS do script e evitar tremores enquanto o script troca os elementos.

`common > css`

```scss
@import "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelementplayer.min.css";

.cooked {
  --audio-player-height: 40px;

  .mejs__container,
  audio {
    // igualar a altura do player do Media-Element.js para evitar tremores
    height: var(--audio-player-height);
    display: block;
  }
}

```

Agora você deve ver o player personalizado em todos os posts que possuem elementos válidos.

Com isso resolvido, você deve notar que a biblioteca que você escolheu é bastante antiga. Ela é transpilada para navegadores antigos e tenta fazer polyfill de muitas funcionalidades que desde então se tornaram padrão.

Se você sabe por que quer usá-la, tudo bem. No entanto, se você está apenas usando-a para personalizar a aparência do player, recomendo evitá-la. Não verifiquei, mas provavelmente há alternativas modernas muito mais leves.

O melhor de tudo isso é que a implementação não muda do que foi mostrado acima. Não importa quais elementos você deseja segmentar ou quais scripts deseja usar. O mesmo padrão se aplica. A única coisa que muda é a inicialização do script personalizado. Toda biblioteca decente tem uma documentação muito boa que o guiará por isso. Então, você apenas coloca isso no padrão acima.

---

_[View the full topic](https://meta.discourse.org/t/decoratecookedelement-for-each-page-change/214218)._
