Integración Laravel, TinyMCE e ImgBB

Integración Laravel, TinyMCE e ImgBB

Introducción

TinyMCE es uno de los editores de texto enriquecido más utilizados, cuenta con un amplio rango de herramientas y extensiones para cubrir todo lo que necesitas a la hora de producir contenido.

Acompañar tu contenido con imágenes suele ser una tarea necesaria para que el resultado final de tu publicación sea más entendible y entretenido. Sin embargo debes considerar donde se guardarán estas imágenes. Para esta tarea TinyMCE cuenta con una función images_upload_handler para gestionar donde subirlas.

ImgBB es un servicio de hosting de imágenes, y en este proyecto vamos a integrarlo con TinyMCE.

Tanto TinyMCE como ImgBB cuentan con packages que se incorporan muy bien con Laravel.

En esta publicación vamos a ver paso a paso como realizar toda esta configuración.

Proyecto

Este proyecto contará con un formulario con un <textarea>, el cual contendrá nuestro editor enriquecido. Cuando se inserte una imagen, la función images_upload_handler se ejecutará. Esta función realizará una petición asíncrona, el cual la aplicación capturará y recibirá la imagen.

El package de ImgBB subirá la imagen a nuestra cuenta y retornará un enlace. Cuando el formulario se envíe, se guardará en una tabla de nuestra base de datos. Finalmente recuperaremos todos los registros de la tabla, y los mostraremos en una grilla.

Instalación y configuración del proyecto

Laravel

Comencemos creando un proyecto Laravel. Yo lo llamé tutorial-laravel-tinymce-imgbb.

composer create-project laravel/laravel tutorial-laravel-tinymce-imgbb

Una vez instalado, entra a la carpeta tutorial-laravel-tinymce-imgbb que se ha generado y corre el comando para instalar las dependencias NPM.

npm install

Tu proyecto Laravel ya está listo, ahora continuamos con la configuración de ImgBB.

ImgBB

Primero ve a imgbb.com y créate una cuenta gratuita. Una vez estés en tu panel ve al menú Acerca que está en la parte superior izquierda y elige la opción API. Podrás ver un campo con el API KEY, cópialo y vuelve a la app Laravel.

pantalla_api_key_edited.png

Ahora selecciona el fichero de variables de entorno .env y crea una nueva variable IMGBB_API_KEY y pega el API KEY.

IMGBB_API_KEY="Tu API KEY aquí"

Ahora instalaremos el package que utilizará el API. Ejecuta el siguiente comando.

composer require 101infotech/imgbb

Una vez instalado la dependencia hacemos un publish de los ficheros de configuración.

php artisan vendor:publish  --tag="ImgBB"

La configuración para usar API ImgBB está completo, ahora seguimos con TinyMCE.

TinyMCE

Lo primero es instalar la dependencia de TinyMCE.

composer require tinymce/tinymce

La documentación de TinyMCE explica como realizar la configuración haciendo uso de Laravel Mix. La última versión de Laravel utiliza Vite. Podemos modificar el archivo vite.config.js para copiar el directorio de TinyMCE dentro del directorio public. Para ello necesitaremos un plugin para Vite. Lo instalamos como sigue.

npm i vite-plugin-static-copy -D

Ahora abrimos el fichero vite.config.js y añadimos el siguiente código.

import { viteStaticCopy } from 'vite-plugin-static-copy';

Una vez añadido el plugin lo usamos como sigue.

viteStaticCopy({
            targets: [
              {
                src: normalizePath(path.resolve(__dirname, 'vendor', 'tinymce', 'tinymce')),
                dest: normalizePath(path.resolve(__dirname, 'public'))
              },
            ]
          }),

En la opción src colocamos la carpeta que queremos copiar, la opción dest colocamos la carpeta destino, en este caso es la carpeta public. El fichero completo queda como sigue.

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import { viteStaticCopy } from 'vite-plugin-static-copy';

import { normalizePath } from 'vite';
import path from 'node:path';

export default defineConfig({
    plugins: [
        laravel({
            input: [
                'resources/css/app.css',
                'resources/js/app.js',
            ],
            refresh: true,
        }),
        viteStaticCopy({
            targets: [
              {
                src: normalizePath(path.resolve(__dirname, 'vendor', 'tinymce', 'tinymce')),
                dest: normalizePath(path.resolve(__dirname, 'public'))
              },
            ]
          }),
    ],
});

Para realizar está tarea corremos el comando npm run build. Luego de correr el comando podremos ver el directorio tinymce dentro de la carpeta public.

Ahora podemos añadirlo a nuestra vista insertando el siguiente script.

<script src="./tinymce/tinymce.min.js"></script>

Base de datos

Accedemos a nuestro gestor de base de datos y creamos una para nuestra app. Yo lo llamé laravel_tinymce_imgbb. Abre el fichero .env y pega el nombre de la base de datos en la variable de entorno DB_DATABASE.

DB_DATABASE=laravel_tinymce_imgbb

Modelo y migración

Para crear el modelo y la migración de nuestra tabla corremos el siguiente comando.

php artisan make:model Post -m

Esto creará dos ficheros, uno para el modelo y otro para la migración. El archivo del modelo está en app/Models/Post.php. Aquí añadimos los campos 'mass assignment'.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'body'
    ];
}

Abrimos el fichero para la migración, está en database/migrations/create_posts_table.php.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->longText('body');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
};

Para terminar corremos el comando para la migración.

php artisan migrate

Cuando finalice tendremos una tabla posts con la estructura que definimos en el fichero de migración.

Finalizamos la etapa de configuración. Seguimos con las vista y los componentes.

Vistas y componentes

Nuestra app tendrá una sola vista home.blade.php. El mismo tendrá el formulario con el <textarea> y el editor enriquecido.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script src="./tinymce/tinymce.min.js"></script>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-slate-900 min-h-screen">
    <main class="w-[960px] m-auto">
        <h1 class="text-3xl text-center text-stone-300 py-4">Integración Laravel, TinyMCE e ImgBB</h1>
        <form action="{{ route('save-post') }}" method="post">
            @csrf
            <textarea name="body" id="textarea" cols="30" rows="10"></textarea>
            <input type="submit" value="Enviar" class="bg-sky-900 hover:bg-sky-500 hover:cursor-pointer p-2 mt-2 text-stone-300 rounded block ml-auto">
        </form>
        <section class="bg-slate-800 m-2 p-2 rounded">
            @foreach ($posts as $post)
                <x-post :post="$post"/>
            @endforeach
        </section>
    </main>
</body>
</html>

Como se observa en el código anterior tenemos un componente llamado post. Para crearlo, dentro del diretorio views añadimos una carpeta components y dentro creamos un fichero post.blade.php.

Dentro del fichero post insertamos el siguiente código.

@props(['post'])
<section class="p-4 mb-2 bg-slate-700 text-stone-300">{!! $post->body !!}</section>

Finalizamos con las vista, a continuación añadiremos el script de TinyMCE que realizará la petición asíncrona.

Script TinyMCE

Justo después de la etiqueta de cierre de </main> agregamos el siguiente script.

<script>
        document.addEventListener('DOMContentLoaded', function() {
            tinymce.init({
                selector: 'textarea#textarea',
                plugins: 'code table lists image',
                toolbar: 'undo redo | blocks | bold italic | alignleft aligncenter alignright | indent outdent | bullist numlist | code | table | image',
                images_upload_handler: (blobInfo) => new Promise((resolve, reject) => {
                    const formData = new FormData();
                    formData.append('file', blobInfo.blob(), blobInfo.filename());
                    axios({
                        method: 'POST',
                        url: '/image-upload',
                        data: formData,
                    })
                    .then((r) => {
                        resolve(r.data.location);
                    })
                    .catch((e) => {
                        reject(`Error: ${e.message}`);
                    });
                })
            });
        }, false);
    </script>

Analicemos un poco los sucede aquí. Lo primero, una vez que el DOM es completamente cargado se inicializa el editor enriquecido con tinymce.init. Esta función recibe un objeto con un conjunto de opciones. La primer opción selector recibe el id del <textarea> en donde se cargará el editor enriquecido. La otras dos opciones son plugins que se deben cargar, y el menú de opciones.

La tercer opción corresponde a la función images_upload_handler que es la opción que gestiona donde subir las imágenes. Dentro de la función declaramos un objeto FormData. Mediante formData.append le pasamos el objeto blobInfo que contiene los datos de la imagen.

En la siguiente línea utilizamos axios para realizar una petición POST. Le pasamos un objeto en donde le decimos el método, definimos una url al cual será enviada la petición y la opción data al cual le pasamos el objeto formData.

La respuesta de la petición se guarda en el parámetro r. Luego resolvemos la promesa con resolve(r.data.location). Dentro del objeto r.data.location está guardado el enlace a nuestra imagen. TinyMCE se encarga de cargar este enlace en el atributo src de la etiqueta <img>.

Hemos terminado con la parte front-end de nuestra app. Ahora seguimos con el controlador.

Controlador

Esta app tendrá un solo controlador, lo creamos con el siguiente comando.

php artisan make:controller PostController

Nuestro controlador contiene tres funciones show, uploadImage y save.

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;
use Infotech\ImgBB\ImgBB;

class PostController extends Controller
{
    public function show()
    {
        $posts = Post::all();
        return view('home', ['posts' => $posts]);
    }

    public function uploadImage(Request $request)
    {
        $data = ImgBB::image($request->file('file'));
        return response()->json([
            'location' => $data['data']['url'],
        ]);
    }

    public function save(Request $request)
    {
        $post = new Post();
        $post->body = $request->body;
        $post->save();

        return redirect()->route('home');
    }
}

La primer función show recupera todos los registros de la tabla posts lo guarda en la variable $posts y luego lo pasa a la vista home.

La función uploadImage recibe la imagen a través de $request->file('file') y lo pasa a ImgBB::image que se encargará de subir la imagen a nuestra cuenta ImgBB y guarda la respuesta en la variable $data. Finalmente retornamos una respuesta json en donde definimos una clave location con el valor $data['data']['url'].

La función save define una instancia del modelo Post, luego recibe el contenido del formulario a través de $post->body=$request->body y guardamos los datos. Por último realizamos un redireccionamiento hacia la misma vista home.

Hemos terminado con el controlador, continuamos definiendo las ruta.

Rutas

La aplicación tendrá solo tres rutas.

<?php

use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

Route::get('/', [PostController::class, 'show'])->name('home');

Route::post('/image-upload', [PostController::class, 'uploadImage'])->name('image-upload');

Route::post('/save', [PostController::class, 'save'])->name('save-post');

La ruta GET es la entrada a nuestra app, y se enlaza al médoto show del controlador PostController. Es la encargada de mostrar la vista home.

La primer ruta POST es la que captura la petición asíncrona y se enlaza al método uploadImage. Es la encargada de subir la imagen a nuestra cuenta ImgBB.

La última ruta POST se enlaza al método save. Se encarga de guardar los datos en la tabla posts.

Conclusión

Hemos finalizado con el proyecto. Hemos visto como integrar de forma sencilla Laravel, TinyMCE e ImgBB. Podemos insertar una imagen en nuestro contenido, subirlo de forma automática a nuestra cuenta ImgBB, guardar el contenido en una base de datos y recuperar los datos para finalmente mostrarlo en una grilla.

Puedes encontrar proyecto completo en mi repositorio de GitHub haciendo clic AQUÍ.

Espero que lo hayas encontrado entretenido, instructivo y claro. Si tienes alguna duda, puedes hacérmelo saber en los comentarios. Pronto estaré subiendo más tutoriales.

Nos vemos en la próxima. Saludos!👋😊