Banner menú dinámico

Establecer un menú dinámico en nuestro proyecto web, nos permitirá abrir la posibilidad de cambiar la estructura web sin que esto afecte el diseño. En ocasiones podemos encontrarnos en la situación de crear un proyecto cuyas opciones de menú sean dinámicas o dependan directamente del contenido, en estos casos lo ideal es adaptar el proyecto para que el cambio en su estructura sea rápido y ágil con la creación de un menú dinámico.

En el siguiente tutorial exploraremos todos los pasos necesarios para crear dicho menú:

Creación del proyecto, tabla y datos

Se crea un nuevo proyecto en Laravel:

$ laravel new dynamic-menu

Se crea la migración de la tabla menus.

$ php artisan make:migration create_menus_table --create=menus

Nos queda de la siguiente forma:

<?php

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

class CreateMenusTable extends Migration
{
    /**
    * Run the migrations.
    *
    * @return void
    */
    public function up()
    {
        Schema::create('menus', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name', 150);
            $table->string('slug', 150)->unique();
            $table->unsignedInteger('parent')->default(0);
            $table->smallInteger('order')->default(0);
            $table->boolean('enabled')->default(1);
            $table->timestamps();
        }); 
    });
    /**
    * Reverse the migrations.
    *
    * @return void
    */
    public function down()
    {
        Schema::dropIfExists('menus');
    }
}

Se intenta registrar en una única tabla (menus) todo el árbol de opciones que podamos necesitar en nuestro proyecto web, cada opción de menú podrá estar enlazada a una opción «padre» indicando el identificador (id) de la opción «padre» en el campo parent. En otras palabras, si tenemos una opción de segundo nivel que dependa o cuelgue de una opción superior, entonces, el valor del campo parent será el identificador id de la opción superior; y si se trata de una opción de nivel superior, es decir, que no depende de ninguna otra opción, el campo parent contendrá el valor 0. Veamos un ejemplo.

Diagrama menú

Así, la estructura de menú definida en la Figura 1 estará registrada en la tabla de la siguiente forma:

Tabla menu

Creemos una base de datos que albergue la información del proyecto e indicamos sus datos en el archivo .env. Por ejemplo:

CREATE SCHEMA 'dynamic_menu';

Y en el archivo .env

DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=dynamic_menu
DB_USERNAME=homestead
DB_PASSWORD=secret

Vamos a crear el Modelo:

$ php artisan make:model Menu

Y ahora el seeder con la estructura definida en la Figura 1.

$ php artisan make:seeder MenusTableSeeder

Añadimos el código necesario para definir las opciones de nuestro menú, según el ejemplo, en la nueva clase MenusTableSeeder ubicada en database/seeds/MenusTableSeeder.php.

<?php
use App\Menu;
use Illuminate\Database\Seeder;
class MenusTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
	public function run()
    {
        $m1 = factory(Menu::class)->create([
            'name' => 'Opción 1',
            'slug' => 'opcion1',
            'parent' => 0,
            'order' => 0,
        ]);
        factory(Menu::class)->create([
            'name' => 'Opción 2',
            'slug' => 'opcion2',
            'parent' => 0,
            'order' => 1,
        ]);
        $m3 = factory(Menu::class)->create([
            'name' => 'Opción 3',
            'slug' => 'opcion3',
            'parent' => 0,
            'order' => 2,
        ]);
        $m4 = factory(Menu::class)->create([
            'name' => 'Opción 4',
            'slug' => 'opcion4',
            'parent' => 0,
            'order' => 3,
        ]);
        factory(Menu::class)->create([
            'name' => 'Opción 1.1',
            'slug' => 'opcion-1.1',
            'parent' => $m1->id,
            'order' => 0,
        ]);
        factory(Menu::class)->create([
            'name' => 'Opción 1.2',
            'slug' => 'opcion-1.2',
            'parent' => $m1->id,
            'order' => 1,
        ]);
        factory(Menu::class)->create([
            'name' => 'Opción 3.1',
            'slug' => 'opcion-3.1',
            'parent' => $m3->id,
            'order' => 0,
        ]);
        $m32 = factory(Menu::class)->create([
            'name' => 'Opción 3.2',
            'slug' => 'opcion-3.2',
            'parent' => $m3->id,
            'order' => 1,
        ]);
        factory(Menu::class)->create([
            'name' => 'Opción 4.1',
            'slug' => 'opcion-4.1',
            'parent' => $m4->id,
            'order' => 0,
        ]);
        factory(Menu::class)->create([
            'name' => 'Opción 3.2.1',
            'slug' => 'opcion-3.2.1',
            'parent' => $m32->id,
            'order' => 0,
        ]);
        factory(Menu::class)->create([
            'name' => 'Opción 3.2.2',
            'slug' => 'opcion-3.2.2',
            'parent' => $m32->id,
            'order' => 1,
        ]);
        factory(Menu::class)->create([
            'name' => 'Opción 3.2.3',
            'slug' => 'opcion-3.2.3',
            'parent' => $m32->id,
            'order' => 2,
        ]);
    }
}

En el ModelFactory ubicado en database/factories añadimos:

$factory->define(App\Menu::class, function (Faker\Generator $faker) {
    $name = $faker->name;
    $menus = App\Menu::all();
    return [
        'name' => $name,
        'slug' => str_slug($name),
        'parent' => (count($menus) > 0) ? $faker->randomElement($menus->pluck('id')->toArray()) : 0,
        'order' => 0
    ];
});

En el ejemplo que estamos trabajando hemos creado opciones fijas, pero si deseamos crear opciones aleatorias, por ejemplo, 5 opciones, podemos usar la siguiente instrucción en la función run de la clase MenusTableSeeder:

public function run()
{
    factory(Menu::class)->times(5)->create();
}

Volvamos a nuestro ejemplo. Ahora debemos incluir la llamada de nuestro nuevo seeder en la clase DatabaseSeeder que está ubicada en el directorio database/seeds:

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
    * Run the database seeds. 
    *
    * @return void
    */
    public function run()
    {
        $this->call(MenusTableSeeder::class);
    }
}

Y ejecutamos las migraciones y el seeder:

$ php artisan migrate --seed

Bien, ahora que ya tenemos nuestra tabla menus con la información que deseamos, procedemos a trabajar sobre la vista para ver su contenido.

Para dar algo de estilo a nuestro menú dinámico usaremos Bootstrap, aunque es posible usar cualquier framework de CSS o crear los estilos por nosotros mismos.

Tomamos los CDN de Bootstrap desde su página oficial.

Añadimos los CDN relacionado con los CSS dentro de la etiqueta <head> de nuestra vista resources/views/welcome.blade.php. El CDN de tipo JavaScript y la librería de JQuery que podemos encontrar en code.jquery.com, la incluiremos al final de nuestra vista welcome.blade.php, y justo antes del cierre de la etiqueta </body>.

Dentro de la etiqueta <body> añadimos el siguiente código:

<nav class="navbar navbar-default">
    <div class="container-fluid">
        <div class="collapse navbar-collapse">
            <ul class="nav navbar-nav">
                @foreach ($menus as $key => $item)
                    @if ($item['parent'] != 0)
                        @break
                    @endif
                    @include('partials.menu-item', ['item' => $item])
                @endforeach
            </ul>
        </div>
    </div>
</nav>

Por otra parte, añadimos tres nuevas clases de estilos CSS para activar la visualización de las opciones del menú al deslizar el ratón sobre ella. Estos estilos deberán estar definidos dentro de la etiqueta <style>.

.dropdown-menu .sub-menu {
    left: 100%;
    position: absolute;
    top: 0;
    visibility: hidden;
    margin-top: -1px;
}

.dropdown-menu li:hover .sub-menu {
    visibility: visible;
}

.dropdown:hover .dropdown-menu {
    display: block;
}

El resultado final sería el siguiente:

Organización del menú

Observemos con detalle el código anterior:

<nav class="navbar navbar-default">
    <div class="container-fluid">
        <div class="collapse navbar-collapse">
            <ul class="nav navbar-nav">
                @foreach ($menus as $key => $item)
                    @if ($item['parent'] != 0)
                        @break
                    @endif
                    @include('partials.menu-item', ['item' => $item])
                @endforeach
            </ul>
        </div>
    </div>
</nav>

La sentencia @foreach hace referencia a la variable $menus, la cual la definiremos a través de una View Composer, lo incluiremos en la función boot de app/Providers/AppServiceProvider.

public function boot()
{
    view()->composer('welcome', function($view) {
        $view->with('menus', Menu::menus());
    });
}

No olvidemos de añadir la clase Menu en la parte superior de AppServiceProvider.php.

use App\Menu;

Aquí vemos la llamada al método Menu::menus(). Para su funcionalidad necesitaremos otros dos métodos más, optionsMenu y getChildren, que también se encuentran en el modelo Menu.

Veamos el nuevo código del modelo app/Menu.php:

<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Menu extends Model
{
    
    public function getChildren($data, $line)
    {
        $children = [];
        foreach ($data as $line1) {
            if ($line['id'] == $line1['parent']) {
                $children = array_merge($children, [ array_merge($line1, ['submenu' => $this->getChildren($data, $line1) ]) ]);
            }
        }
        return $children;
    }
    public function optionsMenu()
    {
        return $this->where('enabled', 1)
            ->orderby('parent')
            ->orderby('order')
            ->orderby('name')
            ->get()
            ->toArray();
    }
    public static function menus()
    {
        $menus = new Menu();
        $data = $menus->optionsMenu();
        $menuAll = [];
        foreach ($data as $line) {
            $item = [ array_merge($line, ['submenu' => $menus->getChildren($data, $line) ]) ];
            $menuAll = array_merge($menuAll, $item);
        }
        return $menus->menuAll = $menuAll;
    }
}

Observemos en detalle cada uno de ellos. Comencemos por el método menus() el cual es llamado por el View Composer.

Método menus()

El objetivo del método menus() es recorrer todas las opciones del menú y en aquellas opciones “padre” obtener sus “hijos” u opciones que dependerán de la opción principal, y éste grupo de ítems quedarán registrados en un array llamado submenú.

Método optionsMenu()

Retorna un array con las opciones del menú activas (enabled = 1) y ordenadas por parent, order y name.

Método getChildren()

Recorre el array $data para extraer los “hijos” (el valor del campo parent debe coincidir con el id de la opción superior).

Menú ítem

Si observamos nuevamente el código que se encuentra entre las etiquetas <nav> de nuestro welcome.blade.php, podremos observar la inclusión de un nuevo archivo llamado menu-item.blade.php.

@include('partials.menu-item', ['item' => $item])

Vamos a definirlo:

Se crea un nuevo directorio llamado «partials» dentro del menú resources/views, y ahora creamos el nuevo archivo llamado menu-item-blade.php con el siguiente contenido:

@if ($item['submenu'] == [])
    <li>
        <a href="{{ url($item['name']) }}">{{ $item['name'] }} </a>
    </li>
@else
    <li class="dropdown">
        <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{{ $item['name'] }} <span class="caret"></span></a>
        <ul class="dropdown-menu sub-menu">
            @foreach ($item['submenu'] as $submenu)
                @if ($submenu['submenu'] == [])
                    <li><a href="{{ url('menu',['id' => $submenu['id'], 'slug' => $submenu['slug']]) }}">{{ $submenu['name'] }} </a></li>
                @else
                    @include('partials.menu-item', [ 'item' => $submenu ])
                @endif
            @endforeach
        </ul>
    </li>
@endif

El objetivo principal de crear el archivo menu-item-blade.php separándolo del resto, es para utilizarlo en llamadas recursivas, de tal forma que podamos implementar cualquier número de niveles en el menú dinámico.

El código anterior principalmente muestra un condicional (@if) que permite discriminar si se está evaluando una opción con dependencias (submenú) o sin ella. En el caso de que la llave submenu no esté vacía, nuevamente se hará la inclusión del código menu-item-blade.php llamándose recursivamente a sí mismo hasta completar todos los niveles de nuestra estructura de menú.

Finalmente, podemos ver nuestro menú dinámico al acceder a http://localhost:

Resultado final menu

El código de este tutorial está disponible en Github.

Regístrate hoy en Styde y obtén acceso a todo nuestro contenido.