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.
Así, la estructura de menú definida en la Figura 1 estará registrada en la tabla de la siguiente forma:
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:
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:
El código de este tutorial está disponible en Github.
Regístrate hoy en Styde y obtén acceso a todo nuestro contenido.