sábado, 24 de enero de 2026

Shadcn/ui popup dialog

 ¿Tiene shadcn ui algún popup dialog que adentro pueda tener cualquier HTML y JSX?

Sí, shadcn/ui incluye un componente de Dialog que funciona como un popup modal y dentro de su contenido (DialogContent) podés renderizar cualquier HTML o JSX sin restricciones. Está construido sobre los primitives de Radix UI, lo que garantiza accesibilidad, manejo de foco y soporte para teclado.

🔹 Cómo funciona el Dialog en shadcn/ui

  • Componentes principales:

    • Dialog: contenedor raíz.

    • DialogTrigger: define el elemento que abre el popup.

    • DialogContent: el área del popup donde podés poner cualquier HTML/JSX.

    • DialogHeader, DialogTitle, DialogDescription: opcionales para estructurar el contenido.

    • DialogFooter: opcional para botones de acción.

    • DialogClose: para cerrar el popup.

  • Flexibilidad: Dentro de DialogContent podés incluir formularios, inputs, listas, imágenes, componentes personalizados, incluso otros hooks de React. No hay limitación: es un contenedor React normal.

🔹 Ejemplo práctico

tsx
import {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogClose,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";

export function SoapDialog() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="outline">Abrir popup</Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-[425px]">
        <DialogHeader>
          <DialogTitle>Editar jabón</DialogTitle>
          <DialogDescription>
            Podés modificar cualquier dato del jabón aquí.
          </DialogDescription>
        </DialogHeader>
        {/* Aquí podés poner cualquier HTML/JSX */}
        <form className="space-y-4">
          <input type="text" placeholder="Nombre" className="border p-2 w-full" />
          <textarea placeholder="Descripción" className="border p-2 w-full" />
        </form>
        <DialogFooter>
          <Button type="submit">Guardar</Button>
          <DialogClose asChild>
            <Button variant="secondary">Cancelar</Button>
          </DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

👉 Dentro de DialogContent podés poner cualquier cosa: inputs, tablas, componentes React, incluso otro layout complejo.

🔹 Variantes disponibles

  • Alert Dialog (confirmaciones destructivas).

  • Scrollable Dialog (contenido largo con scroll).

  • Fullscreen Dialog (ocupa toda la pantalla).

  • Animaciones con Motion para transiciones suaves.

✨ Conclusión

  • Sí, shadcn/ui tiene un Dialog que actúa como popup modal.

  • Dentro de DialogContent podés renderizar cualquier HTML o JSX.

  • Es accesible, flexible y extensible, ideal para formularios, confirmaciones o contenido complejo.

¿Querés que te arme un ejemplo donde el Dialog contenga un componente React completo (por ejemplo, un formulario con validación y estado) para ver cómo se integra dentro del popup?

Sí.

Genial, Gabriel 🙌. Te armo un ejemplo completo donde el Dialog de shadcn/ui contiene un formulario React con validación y estado, para que veas cómo se integra dentro del popup:

🔹 Ejemplo: Formulario dentro de un Dialog

tsx
import * as React from "react"
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"

export function SoapDialogForm() {
  const [name, setName] = React.useState("")
  const [description, setDescription] = React.useState("")
  const [error, setError] = React.useState("")

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    if (!name.trim()) {
      setError("El nombre es obligatorio")
      return
    }
    // Aquí podrías enviar al back end
    console.log("Datos enviados:", { name, description })
    setError("")
  }

  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="outline">Editar jabón</Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-[425px]">
        <DialogHeader>
          <DialogTitle>Editar jabón</DialogTitle>
          <DialogDescription>
            Modificá los datos y guardalos en el back end.
          </DialogDescription>
        </DialogHeader>

        <form onSubmit={handleSubmit} className="space-y-4">
          <input
            type="text"
            placeholder="Nombre"
            value={name}
            onChange={(e) => setName(e.target.value)}
            className="border p-2 w-full"
          />
          <textarea
            placeholder="Descripción"
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            className="border p-2 w-full"
          />
          {error && <p className="text-red-500 text-sm">{error}</p>}
          <DialogFooter>
            <Button type="submit">Guardar</Button>
            <DialogClose asChild>
              <Button variant="secondary">Cancelar</Button>
            </DialogClose>
          </DialogFooter>
        </form>
      </DialogContent>
    </Dialog>
  )
}

🔹 Qué pasa aquí

  • El Dialog abre un popup modal.

  • Dentro de DialogContent hay un formulario React con estado (useState).

  • Se valida que el nombre no esté vacío antes de enviar.

  • Podés reemplazar el console.log por una llamada al back end (fetch, axios, o React Query).

  • El DialogClose permite cerrar el popup desde un botón.

✨ Con esto ves que el Dialog de shadcn/ui es totalmente flexible: podés meter cualquier HTML/JSX, manejar estado local, validaciones y lógica de negocio sin limitaciones.

¿Querés que te muestre cómo integrar este formulario con React Query para que al guardar se actualice automáticamente la lista de jabones en tu UI?

Sí.

Perfecto, Gabriel 🙌. Vamos a integrar el formulario dentro del Dialog de shadcn/ui con React Query, de modo que al guardar se actualice automáticamente la lista de jabones en tu UI.

🔹 Repositorio de datos

ts
// repositories/soapRepository.ts
export async function getSoaps() {
  const res = await fetch("/api/soaps");
  if (!res.ok) throw new Error("Error al obtener jabones");
  return res.json();
}

export async function updateSoap(id: string, data: any) {
  const res = await fetch(`/api/soaps/${id}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  });
  if (!res.ok) throw new Error("Error al actualizar jabón");
  return res.json();
}

🔹 Hook con React Query

ts
// hooks/useSoaps.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { getSoaps, updateSoap } from "@repositories/soapRepository";

export function useSoaps() {
  return useQuery({
    queryKey: ["soaps"],
    queryFn: getSoaps,
  });
}

export function useUpdateSoap() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ id, data }: { id: string; data: any }) =>
      updateSoap(id, data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["soaps"] }); // ← refresca la lista
    },
  });
}

🔹 Formulario dentro del Dialog

tsx
import * as React from "react";
import {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogClose,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { useUpdateSoap } from "@hooks/useSoaps";

export function SoapDialogForm({ soap }: { soap: any }) {
  const [name, setName] = React.useState(soap.nombre);
  const [description, setDescription] = React.useState(soap.descripcion);
  const mutation = useUpdateSoap();

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    mutation.mutate({ id: soap.id, data: { nombre: name, descripcion: description } });
  }

  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="outline">Editar {soap.nombre}</Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-[425px]">
        <DialogHeader>
          <DialogTitle>Editar jabón</DialogTitle>
          <DialogDescription>
            Modificá los datos y guardalos en el back end.
          </DialogDescription>
        </DialogHeader>

        <form onSubmit={handleSubmit} className="space-y-4">
          <input
            type="text"
            placeholder="Nombre"
            value={name}
            onChange={(e) => setName(e.target.value)}
            className="border p-2 w-full"
          />
          <textarea
            placeholder="Descripción"
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            className="border p-2 w-full"
          />
          <DialogFooter>
            <Button type="submit">Guardar</Button>
            <DialogClose asChild>
              <Button variant="secondary">Cancelar</Button>
            </DialogClose>
          </DialogFooter>
        </form>
      </DialogContent>
    </Dialog>
  );
}

🔹 Lista de jabones que se refresca automáticamente

tsx
import { useSoaps } from "@hooks/useSoaps";
import { SoapDialogForm } from "@components/SoapDialogForm";

export default function SoapList() {
  const { data: soaps, isLoading } = useSoaps();

  if (isLoading) return <p>Cargando jabones...</p>;

  return (
    <div className="space-y-4">
      {soaps.map((soap: any) => (
        <div key={soap.id} className="border p-4">
          <h2>{soap.nombre}</h2>
          <p>{soap.descripcion}</p>
          <SoapDialogForm soap={soap} />
        </div>
      ))}
    </div>
  );
}

✨ Flujo completo

  1. SoapList carga la lista de jabones desde el back end.

  2. Cada jabón tiene un botón que abre un Dialog con el formulario.

  3. Al guardar, se dispara la mutación con React Query.

  4. onSuccess invalida la query → se refresca la lista automáticamente.

  5. La UI siempre queda sincronizada con el back end.

¿Querés que te prepare también un diagrama visual del flujo mostrando cómo el Dialog dispara la mutación y cómo React Query refresca la lista de jabones?

Ayer
¿Se pueden combinar las variantes scrollable dialog y Fullscreen dialog?

Muy buena pregunta, Gabriel 👌. En shadcn/ui (que se apoya en los Dialog primitives de Radix UI), las variantes como Scrollable y Fullscreen no son “tipos distintos de componente”, sino combinaciones de estilos y props aplicadas sobre el mismo DialogContent.

🔹 Cómo funcionan las variantes

  • Scrollable Dialog: se logra aplicando clases utilitarias (ej. max-h-[80vh] overflow-y-auto) al DialogContent para que el contenido tenga scroll interno.

  • Fullscreen Dialog: se logra aplicando clases que expanden el DialogContent a ocupar toda la pantalla (ej. w-screen h-screen max-w-none).

🔹 Combinación posible

Sí, podés combinarlas. Por ejemplo, un DialogContent que ocupe toda la pantalla y además tenga scroll interno:

tsx
<DialogContent className="w-screen h-screen max-w-none overflow-y-auto">
  <DialogHeader>
    <DialogTitle>Jabón en pantalla completa</DialogTitle>
    <DialogDescription>
      Este contenido ocupa toda la pantalla y se puede scrollear.
    </DialogDescription>
  </DialogHeader>
  <div className="space-y-4 p-6">
    {/* Aquí podés poner cualquier JSX largo */}
    <p>Sección 1...</p>
    <p>Sección 2...</p>
    <p>Sección 3...</p>
    {/* etc */}
  </div>
</DialogContent>

👉 Esto combina ambas variantes:

  • Fullscreen: ocupa toda la pantalla.

  • Scrollable: si el contenido es más largo que la altura, aparece scroll interno.

✨ Conclusión

  • Las variantes en shadcn/ui son patrones de estilo, no componentes separados.

  • Podés mezclarlas libremente aplicando las clases adecuadas.

  • Un DialogContent fullscreen puede ser scrollable sin problemas.

¿Querés que te arme un ejemplo extendido con contenido muy largo (ej. lista de productos) para que veas cómo se comporta el scroll dentro de un fullscreen dialog?

No hay comentarios: