Adicionar uma galeria de imagens ao RichTextEditor no Angular

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:
<!-- 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
// 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 RichTextEditorinforma o TypeScript sobre a variável global, apenas com os três métodos que o componente usa.viewChild.requiredfornece adivdo template.ngAfterViewInité o primeiro hook em que ela existe, então o editor é criado ali.ngOnDestroychamadestroy(), então o editor some junto com o componente, por exemplo em uma mudança de rota ou quando um@iffica 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:
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:
{
"/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:
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.