Añadir una galería de imágenes a RichTextEditor en Angular

El cuadro de diálogo Insert Gallery en el ejemplo de Angular, con las carpetas blog, products y team y cuatro imágenes del servidor Express

El cuadro de diálogo Insert Gallery de RichTextEditor necesita en Angular las mismas tres cosas que en cualquier aplicación de una sola página: los scripts del editor cargados antes que la aplicación, un componente que cree y destruya el editor en el momento adecuado, y un servidor de desarrollo que reenvíe las solicitudes del cuadro de diálogo a tu API. Esta página muestra cómo queda en un componente standalone de Angular 22, probado con el ejemplo de Express y ng serve.

Carga los scripts del editor en index.html

El editor es un conjunto de scripts clásicos para el navegador que definen una variable global RichTextEditor. Ponlos en src/index.html, antes del bundle de 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>

Los archivos están en public/richtexteditor/. En un proyecto nuevo de Angular, angular.json ya copia todo lo que hay en public/ a la compilación como recursos estáticos, así que no hace falta cambiar nada ahí. El ejemplo mantiene el editor como archivos separados bajo /richtexteditor/, tal como vienen en la descarga. No usa el array "scripts" de angular.json, que agrupa los scripts en un solo archivo. No probamos el editor de esa forma.

El 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 le informa a TypeScript de la variable global, con solo los tres métodos que usa el componente.
  • viewChild.required da el div de la plantilla. ngAfterViewInit es el primer hook en el que existe, así que el editor se crea ahí.
  • ngOnDestroy llama a destroy(), para que el editor desaparezca con el componente, por ejemplo al cambiar de ruta o cuando un @if pasa a false. El ejemplo tiene una sola página, así que nuestra comprobación en el navegador no ejercitó este hook.
  • Las entradas signal (input.required, input) llevan las opciones y el HTML inicial. Se leen una sola vez, al crear el editor.

El proyecto se creó con ng new --minimal --zoneless. El editor funcionó sin zone.js: gestiona sus propios eventos del DOM, y nada en el componente depende de la detección de cambios. La aplicación pasa las opciones y lee el HTML a través de una referencia de plantilla:

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

El botón Show HTML del ejemplo llama a editor.getHTML() y mostró <p>Hello from Angular.</p>.

Reenvía /rte/ y /uploads/ durante el desarrollo

ng serve sirve la aplicación en el puerto 5113. El archivo de proxy envía las rutas de la galería y de carga, y las imágenes, al ejemplo de Express en el puerto 5101:

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

El ejemplo lo define una vez en angular.json, dentro de projects.frontend-angular.architect.serve.options, como "proxyConfig": "src/proxy.conf.json". También lo ejecutamos como ng serve --proxy-config src/proxy.conf.json, con el mismo resultado. El servidor de desarrollo de Angular está construido sobre Vite. Angular convierte en expresiones regulares las claves que contienen caracteres glob, como /api/**, y pasa las claves simples a Vite sin cambios. Vite las compara por prefijo, así que mantén la barra final. Con "/rte/", /rte-files.js no se reenvió: ng serve sirvió la copia propia de la aplicación desde public/, sin encabezado X-Powered-By: Express.

El proxy es solo para ng serve. Una compilación de producción no tiene proxy: sírvela junto con la API desde un mismo origen.

Qué muestra el cuadro de diálogo

La imagen del principio es el cuadro de diálogo en esta aplicación Angular, con las carpetas y las imágenes del servidor Express descrito en la guía de la galería para Node.js. Como en cualquier framework, la indicación de la galería publicada enumera JFIF, BMP y AVIF, que el servidor rechaza. Una carga rechazada muestra el mensaje propio del cuadro de diálogo, "Upload failed for <name>.".

Pruébalo

Descarga el proyecto de ejemplo de esta guía y el ejemplo de Express, e inicia primero Express. Después, en el proyecto de Angular:

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

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

npm start copia primero los archivos del editor en public/. Define RTE_DIR con la carpeta de tu editor, o coloca tú mismo la carpeta en public/richtexteditor. Abre http://localhost:5113 y haz clic en Insert Gallery. Ejecutamos toda la secuencia en Chromium con Angular 22.2: la lista coincidía con el servidor, New folder y Upload funcionaron a través del proxy, e Insert puso la imagen en el texto.

La misma guía para otras plataformas

React · Vue

Para seguir leyendo