# Integración Laravel, TinyMCE e ImgBB

## Introducción

[TinyMCE](https://www.tiny.cloud/) 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](https://imgbb.com/) 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](https://laravel.com/).

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](https://laravel.com)

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

```javascript
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.

```javascript
npm install
```

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

### [ImgBB](https://imgbb.com)

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](https://cdn.hashnode.com/res/hashnode/image/upload/v1664225595265/8sK-7wA16.png align="left")

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

```apache
IMGBB_API_KEY="Tu API KEY aquí"
```

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

```javascript
composer require 101infotech/imgbb
```

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

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

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

### [TinyMCE](https://tiny.cloud)

Lo primero es instalar la dependencia de TinyMCE.

```javascript
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](https://vitejs.dev/). 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.

```javascript
npm i vite-plugin-static-copy -D
```

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

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

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

```javascript
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.

```javascript
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`.

```html
<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`.

```apache
DB_DATABASE=laravel_tinymce_imgbb
```

### Modelo y migración

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

```javascript
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
<?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
<?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.

```javascript
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.

```html
<!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.

```html
@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.

```html
<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.

```javascript
php artisan make:controller PostController
```

Nuestro controlador contiene tres funciones `show`, `uploadImage` y `save`.

```php
<?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
<?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Í](https://github.com/AlegreCode/tutorial-laravel-tinymce-imgbb).

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!👋😊
