Adicionar uma galeria de imagens ao RichTextEditor no Angular

A caixa de diálogo Insert Gallery no exemplo em Angular, listando as pastas blog, products e team e quatro imagens do servidor Express

A caixa de diálogo Insert Gallery do RichTextEditor precisa das mesmas três coisas no Angular que em qualquer aplicação de página única: os scripts do editor carregados antes do app, um componente que cria e destrói o editor nos momentos certos, e um servidor de desenvolvimento que encaminha as requisições da caixa de diálogo para a sua API. Esta página mostra como isso fica em um componente standalone do Angular 22, testado com o exemplo em Express usando ng serve.

Carregar os scripts do editor no index.html

O editor é um conjunto de scripts de navegador clássicos que definem uma variável global RichTextEditor. Coloque-os em src/index.html, antes do bundle do Angular:

HTML · src/index.html
<!-- Plain browser scripts from public/, loaded before the Angular bundle. -->
<link rel="stylesheet" href="/richtexteditor/rte_theme_default.css">
<script src="/richtexteditor/rte.js"></script>
<script src="/richtexteditor/plugins/all_plugins.js"></script>
<script src="/rte-files.js"></script>

Os arquivos ficam em public/richtexteditor/. Em um projeto Angular novo, o angular.json já copia tudo o que está em public/ para o build como assets estáticos, então não é preciso mudar nada ali. O exemplo mantém o editor como arquivos separados em /richtexteditor/, do jeito que o download os distribui. Ele não usa o array "scripts" do angular.json, que junta os scripts em um único arquivo. Não testamos o editor dessa forma.

O componente

TypeScript · src/app/rich-text-editor.ts
// RichTextEditor is the global from the scripts in index.html.
declare const RichTextEditor: new (element: HTMLElement, options: object) => EditorInstance;
type EditorInstance = { setHTMLCode(html: string): void; getHTMLCode(): string; destroy(): void };

@Component({
  selector: 'app-rich-text-editor',
  template: `<div #container></div>`,
})
export class RichTextEditorComponent implements AfterViewInit, OnDestroy {
  readonly options = input.required<object>();
  readonly initialHtml = input('');
  private readonly container = viewChild.required<ElementRef<HTMLDivElement>>('container');
  private editor?: EditorInstance;

  ngAfterViewInit(): void {
    this.editor = new RichTextEditor(this.container().nativeElement, this.options());
    this.editor.setHTMLCode(this.initialHtml());
  }

  ngOnDestroy(): void {
    this.editor?.destroy();
    this.editor = undefined;
  }

  getHTML(): string {
    return this.editor?.getHTMLCode() ?? '';
  }
}
  • declare const RichTextEditor informa o TypeScript sobre a variável global, apenas com os três métodos que o componente usa.
  • viewChild.required fornece a div do template. ngAfterViewInit é o primeiro hook em que ela existe, então o editor é criado ali.
  • ngOnDestroy chama destroy(), então o editor some junto com o componente, por exemplo em uma mudança de rota ou quando um @if fica falso. O exemplo tem uma única página, então a nossa verificação no navegador não exercitou esse hook.
  • Signal inputs (input.required, input) carregam as opções e o HTML inicial. Eles são lidos uma única vez, na criação.

O projeto foi criado com ng new --minimal --zoneless. O editor funcionou sem zone.js: ele trata os próprios eventos de DOM, e nada no componente depende da detecção de mudanças. O app passa as opções e lê o HTML por meio de uma referência de template:

TypeScript · src/app/app.ts
const options = {
  toolbar: 'full',
  galleryEndpoint: '/rte/gallery', // forwarded to Express by src/proxy.conf.json
  maxUploadFileSize: 4000000,
};

O botão Show HTML do exemplo chama editor.getHTML() e imprimiu <p>Hello from Angular.</p>.

Encaminhar /rte/ e /uploads/ durante o desenvolvimento

O ng serve serve o app na porta 5113. O arquivo de proxy envia as rotas de galeria e upload e as imagens para o exemplo em Express na porta 5101:

JSON · src/proxy.conf.json
{
  "/rte/": { "target": "http://localhost:5101", "secure": false },
  "/uploads/": { "target": "http://localhost:5101", "secure": false }
}

O exemplo o configura uma única vez no angular.json, em projects.frontend-angular.architect.serve.options, como "proxyConfig": "src/proxy.conf.json". Também executamos com ng serve --proxy-config src/proxy.conf.json, com o mesmo resultado. O servidor de desenvolvimento do Angular é baseado no Vite. O Angular converte em expressões regulares as chaves que contêm caracteres de glob, como /api/**, e repassa as chaves simples ao Vite sem alterações. O Vite as compara por prefixo, então mantenha a barra final. Com "/rte/", /rte-files.js não foi encaminhado: o ng serve serviu a cópia do próprio app a partir de public/, sem o cabeçalho X-Powered-By: Express.

O proxy serve apenas para o ng serve. Um build de produção não tem proxy: sirva-o junto com a API a partir de uma única origem.

O que a caixa de diálogo mostra

A imagem no topo é a caixa de diálogo neste app Angular, listando as pastas e imagens do servidor Express descrito no guia da galeria em Node.js. Como em qualquer framework, a dica da galeria na versão lançada lista JFIF, BMP e AVIF, que o servidor recusa. Um upload recusado mostra a mensagem da própria caixa de diálogo, "Upload failed for <name>."

Teste

Baixe o projeto de exemplo deste guia e o exemplo em Express, e inicie primeiro o Express. Depois, no projeto Angular:

Shell
npm install
npm start
  ➜  Local:   http://localhost:5113/

curl "http://localhost:5113/rte/gallery?folder="
{"currentFolder":"","currentFolderDisplay":"/","parentFolder":null,"folders":[...],"images":[...]}

O npm start primeiro copia os arquivos do editor para public/. Defina RTE_DIR com a pasta do seu editor ou coloque a pasta em public/richtexteditor você mesmo. Abra http://localhost:5113 e clique em Insert Gallery. Executamos a sequência inteira no Chromium com Angular 22.2: a lista correspondeu ao servidor, New folder e Upload funcionaram pelo proxy, e Insert colocou a imagem no texto.

O mesmo guia para outras plataformas

React · Vue

Leia a seguir