Añadir una galería de imágenes a un editor de texto enriquecido en Blazor

El cuadro de diálogo Insert Gallery del ejemplo de Blazor con las carpetas blog, products y team y cuatro fotos subidas, una de ellas seleccionada

RichTextEditor es un editor de JavaScript, así que en una Blazor Web App se crea y se elimina mediante JS interop, y la galería se comunica con endpoints HTTP normales junto a tus componentes. Esta página muestra las partes de Blazor: dónde van los scripts y las opciones del editor, cuándo puede crear el editor el componente, cómo volver a eliminarlo y cómo encajan las rutas de la galería en Program.cs. Usa el renderizado Interactive Server de .NET 8.

Scripts y opciones en App.razor

Los archivos del editor, el rte-files.js compartido y tres pequeñas funciones de interop van en el <head> de App.razor, que se renderiza una sola vez y no lo renderiza un componente interactivo:

Razor · Components/App.razor
<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>
<script>
    // Called from Home.razor. The editor options stay in JavaScript, where they can also hold
    // functions (custom buttons, event handlers) that cannot be sent from .NET as JSON.
    window.rteBlazor = {
        create: function (host) {
            host.rte = new RichTextEditor(host, {
                toolbar: "full",
                galleryEndpoint: "/rte/gallery",
                maxUploadFileSize: 4000000 // same limit as the server, checked before the upload starts
            });
        },
        // Returned as bytes: .NET reads them as a stream, so documents over Blazor's 32 KB
        // SignalR message limit still arrive.
        getHtml: function (host) {
            return new TextEncoder().encode(host.rte.getHTMLCode());
        },
        destroy: function (host) {
            if (!host || !host.rte) return;
            // editor.destroy() exists only in newer editor builds
            if (typeof host.rte.destroy === "function") host.rte.destroy();
            host.rte = null;
            host.innerHTML = "";
        }
    };
</script>

Las opciones del editor, con galleryEndpoint, se escriben en JavaScript en lugar de enviarse desde C#. Las opciones pueden contener funciones, como botones personalizados y controladores de eventos, y esas no pueden viajar desde .NET como JSON. getHtml se explica en Leer el HTML del editor en Blazor Server.

Crear el editor

Razor · Components/Pages/Home.razor
protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (!firstRender) return;
    await JS.InvokeVoidAsync("rteBlazor.create", host);
    created = true;
}

La página tiene @rendermode InteractiveServer, y Blazor la prerrenderiza: el primer renderizado ocurre en el servidor, sin ningún navegador conectado. En ese momento no es posible usar JS interop. OnAfterRenderAsync solo se ejecuta cuando el circuito está conectado, y firstRender garantiza que el editor se cree una vez y no en cada nuevo renderizado. El elemento @ref="host" del marcado es un <div> vacío; Blazor nunca escribe dentro de él, así que no compite con el editor por el DOM.

Eliminarlo

Razor · Components/Pages/Home.razor
public async ValueTask DisposeAsync()
{
    if (!created) return;
    try { await JS.InvokeVoidAsync("rteBlazor.destroy", host); }
    catch (JSDisconnectedException) { } // the browser tab is already gone
}
  • El indicador created. Sin él, al desechar la copia prerrenderizada del componente se lanzaba InvalidOperationException: JavaScript interop calls cannot be issued at this time.
  • JSDisconnectedException ocurre cuando el usuario cierra la pestaña: el componente se desecha cuando el navegador ya no está.
  • Puede que destroy() no exista. La versión publicada del editor que probamos no tiene editor.destroy(), así que rteBlazor.destroy solo lo llama si existe y después vacía el elemento host.

Las rutas de la galería

C# · Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
var app = builder.Build();

app.UseStaticFiles();
app.UseRteStaticFiles();   // /uploads, /richtexteditor, /rte-files.js
app.UseAntiforgery();      // Blazor's own forms; the /rte endpoints opt out (RteEndpoints.cs)

app.MapRteFiles();         // GET+POST /rte/gallery, POST /rte/upload
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();

app.Run();

MapRteFiles() añade GET y POST /rte/gallery y POST /rte/upload como endpoints de Minimal API. El listado de la galería es el mismo JSON que en la guía de la galería para ASP.NET Core. Las rutas se asignan antes de MapRazorComponents, y las rutas POST llaman a .DisableAntiforgery(), porque de lo contrario el app.UseAntiforgery() de la plantilla rechazaría las solicitudes del editor. Subir imágenes desde un editor de texto enriquecido en Blazor explica por qué los archivos no pasan por el circuito.

Qué hace hoy el cuadro de diálogo

El cuadro de diálogo publicado indica "Accepted formats: JPG, JPEG, JFIF, PNG, GIF, WEBP, BMP, AVIF". El servidor acepta cinco de ellos, así que un .bmp se envía, se rechaza con un 400 y el cuadro de diálogo muestra su propio "Upload failed for pic.bmp." en lugar del mensaje del servidor.

Pruébalo

Ejecuta el proyecto de ejemplo de esta guía con dotnet run (SDK de .NET 8), abre http://localhost:5304, espera a que aparezca la barra de herramientas y haz clic en Insert Gallery. Verás las carpetas y las imágenes de la carpeta uploads/ de la aplicación, como en la imagen del principio. El listado también responde desde un shell:

Shell
curl "http://localhost:5304/rte/gallery?folder=team"
{"currentFolder":"team","currentFolderDisplay":"/team","parentFolder":"","folders":[],"images":[]}

La misma guía para otras plataformas

JavaScript · Next.js · PHP · ASP.NET Core · Razor Pages · MVC 5 · Web Forms

Para seguir leyendo