10: Vistas y Blade en Laravel 12 - Guía Práctica

 

10: Vistas y Blade en Laravel 12 - Guía Práctica

Introducción

Las vistas en Laravel son la capa de presentación de tu aplicación. Utilizan el potente motor de plantillas Blade, que te permite escribir HTML limpio y organizado, combinado con lógica de presentación de manera elegante. En este tutorial, aprenderás a crear vistas, usar templates, partials y aprovechar al máximo las capacidades de Blade.

Índice

  1. ¿Qué son las Vistas en Laravel?

  2. Configuración Inicial y Estructura

  3. Sintaxis Básica de Blade

  4. Templates (Plantillas) con Blade

  5. Partials (Fragmentos de Vistas)

  6. Directivas de Control en Blade

  7. Componentes de Laravel

  8. Trabajando con Formularios en Blade

  9. Stacks y Assets

  10. Blade y JavaScript

  11. Buenas Prácticas


1. ¿Qué son las Vistas en Laravel?

Las vistas contienen el HTML que será mostrado al usuario. Están ubicadas en resources/views/ y utilizan la extensión .blade.php para aprovechar el motor de plantillas Blade.

Crear tu primera vista

bash
# Crear una vista simple
touch resources/views/saludo.blade.php
blade
<!-- resources/views/saludo.blade.php -->
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mi Primera Vista</title>
</head>
<body>
    <h1>¡Hola Mundo desde Laravel 12!</h1>
</body>
</html>

Renderizar una vista desde una ruta

php
// routes/web.php

use Illuminate\Support\Facades\Route;

Route::get('/saludo', function () {
    return view('saludo'); // No necesita la extensión .blade.php
});

Renderizar una vista desde un controlador

bash
php artisan make:controller SaludoController
php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class SaludoController extends Controller
{
    public function index()
    {
        return view('saludo');
    }
}
php
// routes/web.php
Route::get('/saludo', [SaludoController::class, 'index']);

2. Configuración Inicial y Estructura

Estructura de la carpeta views

text
resources/views/
├── layouts/
│   └── app.blade.php          # Template principal
├── pasteles/
│   ├── index.blade.php        # Lista de pasteles
│   ├── create.blade.php       # Formulario de creación
│   ├── edit.blade.php         # Formulario de edición
│   ├── show.blade.php         # Detalle de pastel
│   └── partials/
│       ├── table.blade.php    # Tabla de pasteles
│       └── fields.blade.php   # Campos de formulario
├── partials/
│   ├── layout/
│   │   ├── navbar.blade.php   # Barra de navegación
│   │   └── footer.blade.php   # Pie de página
│   └── alerts.blade.php        # Mensajes de alerta
└── home.blade.php              # Página de inicio

Template principal (Layout)

blade
<!-- resources/views/layouts/app.blade.php -->
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    
    <title>@yield('title', 'Mi Aplicación')</title>
    
    <!-- Bootstrap 5 CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    
    <!-- Font Awesome -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    
    <!-- Estilos personalizados -->
    @stack('styles')
</head>
<body>
    <!-- Barra de navegación -->
    @include('partials.layout.navbar')
    
    <!-- Mensajes de alerta -->
    @include('partials.alerts')
    
    <!-- Contenido principal -->
    <main class="py-4">
        <div class="container">
            @yield('content')
        </div>
    </main>
    
    <!-- Pie de página -->
    @include('partials.layout.footer')
    
    <!-- JavaScript -->
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
    
    @stack('scripts')
</body>
</html>

Vista que extiende el layout

blade
<!-- resources/views/pasteles/index.blade.php -->
@extends('layouts.app')

@section('title', 'Lista de Pasteles')

@section('content')
    <div class="row">
        <div class="col-12">
            <div class="d-flex justify-content-between align-items-center mb-3">
                <h1><i class="fas fa-cake"></i> Lista de Pasteles</h1>
                <a href="{{ route('pasteles.create') }}" class="btn btn-primary">
                    <i class="fas fa-plus"></i> Nuevo Pastel
                </a>
            </div>
            
            @include('pasteles.partials.table')
        </div>
    </div>
@endsection

@push('scripts')
<script>
    $(document).ready(function() {
        console.log('Página de pasteles cargada');
    });
</script>
@endpush

3. Sintaxis Básica de Blade

Mostrar datos con {{ }}

blade
<!-- Escapa automáticamente el contenido (seguro) -->
<h1>Hola, {{ $nombre }}</h1>
<p>Email: {{ $email }}</p>

<!-- Mostrar datos de un objeto -->
<h2>{{ $pastel->nombre }}</h2>
<p>Precio: ${{ number_format($pastel->precio, 2) }}</p>
<p>Descripción: {{ $pastel->descripcion ?? 'Sin descripción' }}</p>

<!-- HTML sin escapar (¡cuidado con XSS!) -->
{!! $contenido_html !!}

Variables y asignaciones

blade
<!-- Definir variables en Blade -->
@php
    $precioConIVA = $pastel->precio * 1.16;
    $estaDisponible = $pastel->disponible ? 'Sí' : 'No';
    $estilos = ['bg-primary', 'text-white', 'p-3'];
@endphp

<p>Precio con IVA: ${{ number_format($precioConIVA, 2) }}</p>
<p>Disponible: {{ $estaDisponible }}</p>

<div class="{{ implode(' ', $estilos) }}">
    Contenido con clases dinámicas
</div>

Comentarios en Blade

blade
{{-- Este es un comentario en Blade (no aparece en el HTML) --}}

<!-- Esto es un comentario HTML (aparece en el código fuente) -->

@php
    // Comentario PHP dentro de Blade
    /* Comentario multi-línea PHP */
@endphp

4. Templates (Plantillas) con Blade

Directivas principales para layouts

DirectivaUsoDescripción
@extends('layout')Vista hijaIndica qué layout usar
@yield('section')LayoutDefine una sección que puede ser sobrescrita
@section('section')Vista hijaDefine el contenido de una sección
@stop o @endsectionVista hijaFinaliza una sección
@showLayoutFinaliza una sección y la muestra
@parentVista hijaIncluye el contenido del layout en la sección

Ejemplo completo de layout

blade
<!-- resources/views/layouts/admin.blade.php -->
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@yield('title', 'Panel de Administración')</title>
    
    <!-- CSS base del layout -->
    <link rel="stylesheet" href="{{ asset('css/admin.css') }}">
    
    <!-- CSS específico de la vista -->
    @yield('styles')
    
    <!-- CSS adicional con stack -->
    @stack('extra-styles')
</head>
<body>
    <div class="admin-wrapper">
        <!-- Sidebar -->
        <aside class="admin-sidebar">
            @include('admin.partials.sidebar')
        </aside>
        
        <!-- Contenido principal -->
        <main class="admin-main">
            <!-- Header -->
            <header class="admin-header">
                @section('header')
                    <h1>Panel de Administración</h1>
                @show
            </header>
            
            <!-- Contenido -->
            <div class="admin-content">
                @yield('content')
            </div>
            
            <!-- Footer -->
            <footer class="admin-footer">
                @section('footer')
                    <p>&copy; {{ date('Y') }} Mi Aplicación. Todos los derechos reservados.</p>
                @show
            </footer>
        </main>
    </div>
    
    <!-- JavaScript base -->
    <script src="{{ asset('js/admin.js') }}"></script>
    
    <!-- JavaScript específico -->
    @yield('scripts')
    
    <!-- JavaScript adicional con stack -->
    @stack('extra-scripts')
</body>
</html>

Vista hija que usa el layout

blade
<!-- resources/views/admin/dashboard.blade.php -->
@extends('layouts.admin')

{{-- Sobrescribir el título --}}
@section('title', 'Dashboard - Panel de Administración')

{{-- Agregar CSS específico --}}
@section('styles')
    <link rel="stylesheet" href="{{ asset('css/dashboard.css') }}">
@endsection

{{-- Sobrescribir el header completamente --}}
@section('header')
    <div class="d-flex justify-content-between align-items-center">
        <h1><i class="fas fa-chart-line"></i> Dashboard</h1>
        <div class="admin-user-info">
            <span>{{ auth()->user()->name }}</span>
            <img src="{{ auth()->user()->avatar }}" alt="Avatar" class="rounded-circle">
        </div>
    </div>
@endsection

{{-- Sobrescribir el contenido principal --}}
@section('content')
    <div class="row">
        <div class="col-md-3">
            <div class="card text-white bg-primary">
                <div class="card-body">
                    <h5 class="card-title">Total Pasteles</h5>
                    <p class="card-text display-4">{{ $totalPasteles }}</p>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="card text-white bg-success">
                <div class="card-body">
                    <h5 class="card-title">Ingresos</h5>
                    <p class="card-text display-4">${{ number_format($ingresos, 2) }}</p>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="card text-white bg-warning">
                <div class="card-body">
                    <h5 class="card-title">Pedidos</h5>
                    <p class="card-text display-4">{{ $pedidos }}</p>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="card text-white bg-danger">
                <div class="card-body">
                    <h5 class="card-title">Usuarios</h5>
                    <p class="card-text display-4">{{ $usuarios }}</p>
                </div>
            </div>
        </div>
    </div>
    
    <div class="row mt-4">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">
                    <h5>Últimos Pasteles</h5>
                </div>
                <div class="card-body">
                    @include('admin.partials.ultimos-pasteles')
                </div>
            </div>
        </div>
        <div class="col-md-4">
            <div class="card">
                <div class="card-header">
                    <h5>Acciones Rápidas</h5>
                </div>
                <div class="card-body">
                    <div class="d-grid gap-2">
                        <a href="{{ route('pasteles.create') }}" class="btn btn-primary">
                            <i class="fas fa-plus"></i> Nuevo Pastel
                        </a>
                        <a href="{{ route('usuarios.index') }}" class="btn btn-info">
                            <i class="fas fa-users"></i> Gestionar Usuarios
                        </a>
                        <a href="{{ route('reportes.index') }}" class="btn btn-success">
                            <i class="fas fa-file-alt"></i> Generar Reporte
                        </a>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

{{-- Scripts específicos --}}
@section('scripts')
    <script src="{{ asset('js/charts.js') }}"></script>
    <script>
        $(document).ready(function() {
            // Inicializar gráficos
            initDashboardCharts();
        });
    </script>
@endsection

{{-- Usar @parent para mantener el footer del layout --}}
@section('footer')
    @parent
    <div class="mt-2 text-muted small">
        Última actualización: {{ now()->format('d/m/Y H:i') }}
    </div>
@endsection

5. Partials (Fragmentos de Vistas)

Los partials son pequeños fragmentos de vistas reutilizables.

Crear un partial de barra de navegación

blade
<!-- resources/views/partials/layout/navbar.blade.php -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
    <div class="container">
        <a class="navbar-brand" href="{{ route('home') }}">
            <i class="fas fa-cake"></i> Pastelería
        </a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav me-auto">
                <li class="nav-item">
                    <a class="nav-link {{ request()->routeIs('pasteles.index') ? 'active' : '' }}" 
                       href="{{ route('pasteles.index') }}">
                        <i class="fas fa-list"></i> Pasteles
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link {{ request()->routeIs('pedidos.index') ? 'active' : '' }}" 
                       href="{{ route('pedidos.index') }}">
                        <i class="fas fa-shopping-cart"></i> Pedidos
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link {{ request()->routeIs('clientes.index') ? 'active' : '' }}" 
                       href="{{ route('clientes.index') }}">
                        <i class="fas fa-users"></i> Clientes
                    </a>
                </li>
            </ul>
            <ul class="navbar-nav">
                @auth
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" 
                           data-bs-toggle="dropdown">
                            <i class="fas fa-user"></i> {{ auth()->user()->name }}
                        </a>
                        <ul class="dropdown-menu dropdown-menu-end">
                            <li>
                                <a class="dropdown-item" href="{{ route('profile.edit') }}">
                                    <i class="fas fa-user-cog"></i> Mi Perfil
                                </a>
                            </li>
                            <li><hr class="dropdown-divider"></li>
                            <li>
                                <form action="{{ route('logout') }}" method="POST">
                                    @csrf
                                    <button type="submit" class="dropdown-item">
                                        <i class="fas fa-sign-out-alt"></i> Cerrar Sesión
                                    </button>
                                </form>
                            </li>
                        </ul>
                    </li>
                @else
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('login') }}">
                            <i class="fas fa-sign-in-alt"></i> Iniciar Sesión
                        </a>
                    </li>
                @endauth
            </ul>
        </div>
    </div>
</nav>

Partial de alertas

blade
<!-- resources/views/partials/alerts.blade.php -->
@if(session('success'))
    <div class="container mt-3">
        <div class="alert alert-success alert-dismissible fade show" role="alert">
            <i class="fas fa-check-circle"></i> {{ session('success') }}
            <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
        </div>
    </div>
@endif

@if(session('error'))
    <div class="container mt-3">
        <div class="alert alert-danger alert-dismissible fade show" role="alert">
            <i class="fas fa-exclamation-circle"></i> {{ session('error') }}
            <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
        </div>
    </div>
@endif

@if(session('warning'))
    <div class="container mt-3">
        <div class="alert alert-warning alert-dismissible fade show" role="alert">
            <i class="fas fa-exclamation-triangle"></i> {{ session('warning') }}
            <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
        </div>
    </div>
@endif

@if($errors->any())
    <div class="container mt-3">
        <div class="alert alert-danger alert-dismissible fade show" role="alert">
            <h5><i class="fas fa-times-circle"></i> Errores de Validación</h5>
            <ul class="mb-0">
                @foreach($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
            <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
        </div>
    </div>
@endif

Partial de campos de formulario

blade
<!-- resources/views/pasteles/partials/fields.blade.php -->
<div class="mb-3">
    {!! Form::label('nombre', 'Nombre del Pastel', ['class' => 'form-label']) !!}
    {!! Form::text('nombre', null, [
        'class' => 'form-control' . ($errors->has('nombre') ? ' is-invalid' : ''),
        'placeholder' => 'Ej: Pastel de Chocolate',
        'required'
    ]) !!}
    @error('nombre')
        <div class="invalid-feedback">{{ $message }}</div>
    @enderror
</div>

<div class="mb-3">
    {!! Form::label('sabor', 'Sabor', ['class' => 'form-label']) !!}
    {!! Form::select('sabor', [
        '' => 'Selecciona un sabor...',
        'chocolate' => 'Chocolate',
        'vainilla' => 'Vainilla',
        'cheesecake' => 'Cheesecake',
        'fresa' => 'Fresa',
        'coco' => 'Coco'
    ], null, [
        'class' => 'form-select' . ($errors->has('sabor') ? ' is-invalid' : ''),
        'required'
    ]) !!}
    @error('sabor')
        <div class="invalid-feedback">{{ $message }}</div>
    @enderror
</div>

<div class="mb-3">
    {!! Form::label('precio', 'Precio', ['class' => 'form-label']) !!}
    <div class="input-group">
        <span class="input-group-text">$</span>
        {!! Form::number('precio', null, [
            'class' => 'form-control' . ($errors->has('precio') ? ' is-invalid' : ''),
            'step' => '0.01',
            'min' => '0',
            'placeholder' => '0.00'
        ]) !!}
    </div>
    @error('precio')
        <div class="invalid-feedback">{{ $message }}</div>
    @enderror
</div>

<div class="mb-3">
    {!! Form::label('descripcion', 'Descripción', ['class' => 'form-label']) !!}
    {!! Form::textarea('descripcion', null, [
        'class' => 'form-control' . ($errors->has('descripcion') ? ' is-invalid' : ''),
        'rows' => 3,
        'placeholder' => 'Describe tu pastel...'
    ]) !!}
    @error('descripcion')
        <div class="invalid-feedback">{{ $message }}</div>
    @enderror
</div>

<div class="mb-3 form-check">
    {!! Form::checkbox('disponible', 1, null, [
        'class' => 'form-check-input' . ($errors->has('disponible') ? ' is-invalid' : ''),
        'id' => 'disponible'
    ]) !!}
    {!! Form::label('disponible', 'Disponible para venta', ['class' => 'form-check-label']) !!}
    @error('disponible')
        <div class="invalid-feedback">{{ $message }}</div>
    @enderror
</div>

Partial de tabla de pasteles

blade
<!-- resources/views/pasteles/partials/table.blade.php -->
<div class="table-responsive">
    <table class="table table-striped table-hover" id="pasteles-table">
        <thead>
            <tr>
                <th>ID</th>
                <th>Nombre</th>
                <th>Sabor</th>
                <th>Precio</th>
                <th>Disponible</th>
                <th>Estado</th>
                <th>Acciones</th>
            </tr>
        </thead>
        <tbody>
            @forelse($pasteles as $pastel)
                <tr>
                    <td>{{ $pastel->id }}</td>
                    <td>
                        <a href="{{ route('pasteles.show', $pastel) }}" class="text-decoration-none">
                            <i class="fas fa-info-circle text-info"></i>
                            {{ $pastel->nombre }}
                        </a>
                    </td>
                    <td>
                        <span class="badge bg-{{ $pastel->sabor === 'cheesecake' ? 'warning' : 'info' }}">
                            {{ ucfirst($pastel->sabor) }}
                        </span>
                    </td>
                    <td>${{ number_format($pastel->precio ?? 0, 2) }}</td>
                    <td>
                        @if($pastel->disponible)
                            <span class="badge bg-success">
                                <i class="fas fa-check"></i> Sí
                            </span>
                        @else
                            <span class="badge bg-danger">
                                <i class="fas fa-times"></i> No
                            </span>
                        @endif
                    </td>
                    <td>
                        @if($pastel->trashed())
                            <span class="badge bg-secondary">Eliminado</span>
                        @else
                            <span class="badge bg-primary">Activo</span>
                        @endif
                    </td>
                    <td>
                        <div class="btn-group" role="group">
                            <a href="{{ route('pasteles.show', $pastel) }}" 
                               class="btn btn-sm btn-info" title="Ver">
                                <i class="fas fa-eye"></i>
                            </a>
                            <a href="{{ route('pasteles.edit', $pastel) }}" 
                               class="btn btn-sm btn-warning" title="Editar">
                                <i class="fas fa-edit"></i>
                            </a>
                            @if($pastel->trashed())
                                <form action="{{ route('pasteles.restore', $pastel) }}" 
                                      method="POST" class="d-inline">
                                    @csrf
                                    <button type="submit" class="btn btn-sm btn-success" title="Restaurar">
                                        <i class="fas fa-undo"></i>
                                    </button>
                                </form>
                            @else
                                <form action="{{ route('pasteles.destroy', $pastel) }}" 
                                      method="POST" class="d-inline">
                                    @csrf
                                    @method('DELETE')
                                    <button type="submit" class="btn btn-sm btn-danger" 
                                            onclick="return confirm('¿Eliminar este pastel?')" 
                                            title="Eliminar">
                                        <i class="fas fa-trash"></i>
                                    </button>
                                </form>
                            @endif
                        </div>
                    </td>
                </tr>
            @empty
                <tr>
                    <td colspan="7" class="text-center text-muted py-3">
                        <i class="fas fa-info-circle fa-2x d-block mb-2"></i>
                        No hay pasteles registrados
                    </td>
                </tr>
            @endforelse
        </tbody>
    </table>
</div>

<!-- Paginación -->
<div class="d-flex justify-content-center">
    {{ $pasteles->links() }}
</div>

Incluir partials con datos

blade
<!-- Incluir parcial con datos específicos -->
@include('pasteles.partials.table', ['pasteles' => $pastelesDisponibles])

<!-- Incluir parcial con condicional -->
@includeWhen($mostrarTabla, 'pasteles.partials.table')

<!-- Incluir parcial si existe -->
@includeIf('pasteles.partials.table')

<!-- Incluir desde una colección (cada item) -->
@each('pasteles.partials.item', $pasteles, 'pastel', 'pasteles.partials.empty')

6. Directivas de Control en Blade

Condicionales

blade
<!-- @if, @elseif, @else, @endif -->
@if($pasteles->count() > 10)
    <div class="alert alert-info">
        Hay más de 10 pasteles disponibles
    </div>
@elseif($pasteles->count() > 5)
    <div class="alert alert-warning">
        Hay entre 6 y 10 pasteles
    </div>
@else
    <div class="alert alert-danger">
        Hay 5 o menos pasteles
    </div>
@endif

<!-- @unless (lo opuesto a @if) -->
@unless($pastel->disponible)
    <div class="alert alert-warning">
        Este pastel no está disponible
    </div>
@endunless

<!-- @isset y @empty -->
@isset($pastel->precio)
    <p>Precio: ${{ $pastel->precio }}</p>
@endisset

@empty($pasteles)
    <p>No hay pasteles disponibles</p>
@endempty

<!-- @switch -->
@switch($pastel->sabor)
    @case('chocolate')
        <span class="badge bg-brown">Chocolate</span>
        @break
    @case('vainilla')
        <span class="badge bg-cream">Vainilla</span>
        @break
    @case('cheesecake')
        <span class="badge bg-yellow">Cheesecake</span>
        @break
    @default
        <span class="badge bg-secondary">{{ $pastel->sabor }}</span>
@endswitch

Bucles

blade
<!-- @for -->
@for($i = 0; $i < 10; $i++)
    <p>Iteración número {{ $i + 1 }}</p>
@endfor

<!-- @foreach -->
@foreach($pasteles as $pastel)
    <div class="card mb-2">
        <div class="card-body">
            <h5>{{ $pastel->nombre }}</h5>
            <p>Sabor: {{ $pastel->sabor }}</p>
            
            @if($loop->first)
                <span class="badge bg-primary">Primero</span>
            @endif
            
            @if($loop->last)
                <span class="badge bg-success">Último</span>
            @endif
        </div>
    </div>
@endforeach

<!-- @forelse (foreach con empty) -->
@forelse($pasteles as $pastel)
    <div class="card mb-2">
        <div class="card-body">
            <h5>{{ $pastel->nombre }}</h5>
            <p>Sabor: {{ $pastel->sabor }}</p>
        </div>
    </div>
@empty
    <div class="alert alert-info">
        No se encontraron pasteles
    </div>
@endforelse

<!-- @while -->
@while($contador < 5)
    <p>Contador: {{ $contador }}</p>
    @php $contador++ @endphp
@endwhile

Variables de bucle $loop

blade
@foreach($pasteles as $pastel)
    <tr>
        <td>{{ $loop->index }}</td>          <!-- Índice actual (0-based) -->
        <td>{{ $loop->iteration }}</td>      <!-- Iteración actual (1-based) -->
        <td>{{ $loop->remaining }}</td>      <!-- Iteraciones restantes -->
        <td>{{ $loop->count }}</td>          <!-- Total de elementos -->
        <td>
            @if($loop->first) <span class="badge bg-primary">Primero</span> @endif
            @if($loop->last) <span class="badge bg-success">Último</span> @endif
        </td>
        <td>{{ $loop->even ? 'Par' : 'Impar' }}</td>
        <td>{{ $loop->odd ? 'Impar' : 'Par' }}</td>
        <td>{{ $loop->depth }}</td>          <!-- Nivel de anidamiento -->
        <td>{{ $loop->parent->iteration ?? 'N/A' }}</td> <!-- Iteración del padre -->
    </tr>
@endforeach

Autenticación y Autorización

blade
<!-- Autenticación -->
@auth
    <p>Bienvenido, {{ auth()->user()->name }}</p>
@endauth

@guest
    <p>Por favor, inicia sesión</p>
@endguest

<!-- Autorización con Gates/Policies -->
@can('crear-pasteles')
    <a href="{{ route('pasteles.create') }}" class="btn btn-primary">Crear Pastel</a>
@endcan

@cannot('crear-pasteles')
    <p>No tienes permisos para crear pasteles</p>
@endcannot

@canany(['crear-pasteles', 'editar-pasteles'])
    <p>Tienes permisos para crear o editar pasteles</p>
@endcanany

7. Componentes de Laravel

Los componentes son una alternativa más poderosa a los partials.

Crear un componente

bash
# Crear un componente de clase
php artisan make:component PastelCard

# Crear un componente anónimo
php artisan make:component pasteles/card --view

Componente de clase

php
<?php

namespace App\View\Components;

use App\Models\Pastel;
use Illuminate\View\Component;

class PastelCard extends Component
{
    public Pastel $pastel;
    public bool $showActions;

    public function __construct(Pastel $pastel, bool $showActions = true)
    {
        $this->pastel = $pastel;
        $this->showActions = $showActions;
    }

    public function render()
    {
        return view('components.pastel-card');
    }

    // Métodos disponibles en la vista
    public function getPrecioFormateado()
    {
        return '$' . number_format($this->pastel->precio, 2);
    }

    public function getClaseSabor()
    {
        return match($this->pastel->sabor) {
            'chocolate' => 'bg-brown',
            'vainilla' => 'bg-cream',
            'cheesecake' => 'bg-warning',
            default => 'bg-secondary'
        };
    }
}
blade
<!-- resources/views/components/pastel-card.blade.php -->
<div class="card h-100 shadow-sm">
    <div class="card-body">
        <div class="d-flex justify-content-between align-items-start">
            <h5 class="card-title">{{ $pastel->nombre }}</h5>
            <span class="badge {{ $getClaseSabor() }}">
                {{ ucfirst($pastel->sabor) }}
            </span>
        </div>
        
        <p class="card-text">{{ $pastel->descripcion ?? 'Sin descripción' }}</p>
        
        <div class="d-flex justify-content-between align-items-center mt-3">
            <span class="h4 text-primary">{{ $getPrecioFormateado() }}</span>
            @if($pastel->disponible)
                <span class="badge bg-success">Disponible</span>
            @else
                <span class="badge bg-danger">No disponible</span>
            @endif
        </div>
        
        @if($showActions)
            <div class="d-grid gap-2 mt-3">
                <a href="{{ route('pasteles.show', $pastel) }}" class="btn btn-info btn-sm">
                    <i class="fas fa-eye"></i> Ver detalles
                </a>
            </div>
        @endif
    </div>
    
    @if($pastel->created_at)
        <div class="card-footer text-muted small">
            Creado: {{ $pastel->created_at->diffForHumans() }}
        </div>
    @endif
</div>

Usar componentes

blade
<!-- Usar componente de clase -->
<x-pastel-card :pastel="$pastel" :show-actions="true" />

<!-- Usar componente anónimo -->
<x-pasteles.card :pastel="$pastel" />

<!-- Con atributos -->
<x-pastel-card 
    :pastel="$pastel" 
    show-actions="{{ auth()->user()->can('ver-pasteles') }}"
    class="mb-3"
/>

<!-- Con slot -->
<x-pastel-card :pastel="$pastel">
    <x-slot:footer>
        <div class="text-muted small">
            Última actualización: {{ $pastel->updated_at->diffForHumans() }}
        </div>
    </x-slot:footer>
</x-pastel-card>

8. Trabajando con Formularios en Blade

Formulario básico

blade
<form action="{{ route('pasteles.store') }}" method="POST" enctype="multipart/form-data">
    @csrf
    @method('POST') <!-- O @method('PUT') para actualizar -->
    
    <!-- Campos del formulario -->
    <div class="mb-3">
        <label for="nombre" class="form-label">Nombre</label>
        <input type="text" 
               name="nombre" 
               id="nombre" 
               class="form-control @error('nombre') is-invalid @enderror"
               value="{{ old('nombre', $pastel->nombre ?? '') }}"
               required>
        @error('nombre')
            <div class="invalid-feedback">{{ $message }}</div>
        @enderror
    </div>
    
    <!-- Select -->
    <div class="mb-3">
        <label for="sabor" class="form-label">Sabor</label>
        <select name="sabor" id="sabor" class="form-select @error('sabor') is-invalid @enderror">
            <option value="">Selecciona un sabor...</option>
            <option value="chocolate" {{ old('sabor', $pastel->sabor ?? '') == 'chocolate' ? 'selected' : '' }}>
                Chocolate
            </option>
            <option value="vainilla" {{ old('sabor', $pastel->sabor ?? '') == 'vainilla' ? 'selected' : '' }}>
                Vainilla
            </option>
        </select>
        @error('sabor')
            <div class="invalid-feedback">{{ $message }}</div>
        @enderror
    </div>
    
    <!-- Checkbox -->
    <div class="mb-3 form-check">
        <input type="checkbox" 
               name="disponible" 
               id="disponible" 
               class="form-check-input"
               value="1"
               {{ old('disponible', $pastel->disponible ?? false) ? 'checked' : '' }}>
        <label for="disponible" class="form-check-label">Disponible</label>
    </div>
    
    <!-- File input -->
    <div class="mb-3">
        <label for="imagen" class="form-label">Imagen</label>
        <input type="file" name="imagen" id="imagen" class="form-control">
    </div>
    
    <button type="submit" class="btn btn-primary">
        <i class="fas fa-save"></i> Guardar
    </button>
</form>

Formularios con Laravel Collective

bash
composer require laravelcollective/html
blade
{!! Form::open(['route' => 'pasteles.store', 'method' => 'POST', 'files' => true]) !!}
    {!! Form::label('nombre', 'Nombre', ['class' => 'form-label']) !!}
    {!! Form::text('nombre', null, [
        'class' => 'form-control' . ($errors->has('nombre') ? ' is-invalid' : ''),
        'placeholder' => 'Ej: Pastel de Chocolate'
    ]) !!}
    @error('nombre')
        <div class="invalid-feedback">{{ $message }}</div>
    @enderror
    
    {!! Form::label('sabor', 'Sabor', ['class' => 'form-label']) !!}
    {!! Form::select('sabor', [
        '' => 'Selecciona un sabor...',
        'chocolate' => 'Chocolate',
        'vainilla' => 'Vainilla',
        'cheesecake' => 'Cheesecake'
    ], null, ['class' => 'form-select']) !!}
    
    {!! Form::file('imagen', ['class' => 'form-control']) !!}
    
    {!! Form::submit('Guardar', ['class' => 'btn btn-primary']) !!}
{!! Form::close() !!}

9. Stacks y Assets

Definir y usar stacks

blade
<!-- En el layout principal -->
<head>
    @stack('styles')
    @stack('scripts')
</head>

<!-- En la vista hija -->
@push('styles')
    <link rel="stylesheet" href="{{ asset('css/pasteles.css') }}">
@endpush

@prepend('scripts')
    <script src="{{ asset('js/validacion.js') }}"></script>
@endprepend

Assets con Vite (Laravel 12)

blade
<!-- En el layout principal -->
@vite(['resources/css/app.css', 'resources/js/app.js'])

<!-- Para módulos específicos -->
@vite(['resources/css/pasteles.css', 'resources/js/pasteles.js'])

<!-- Con stacks -->
@push('scripts')
    @vite(['resources/js/admin.js'])
@endpush

Asignar variables a JavaScript

blade
<script>
    // Variables globales
    window.Laravel = {
        csrfToken: '{{ csrf_token() }}',
        userId: '{{ auth()->id() }}',
        user: @json(auth()->user()),
        pasteles: @json($pasteles)
    };
    
    // Usar en scripts
    console.log(window.Laravel.user);
</script>

<!-- Usando la directiva @json (más segura) -->
<script>
    const usuario = @json(auth()->user());
    const pasteles = @json($pasteles->pluck('nombre'));
</script>

10. Blade y JavaScript

Integración con Alpine.js

blade
<!-- Instalar Alpine.js -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

<!-- Usar Alpine en Blade -->
<div x-data="{ 
    search: '', 
    pasteles: @json($pasteles),
    filteredPasteles() {
        if (this.search === '') return this.pasteles;
        return this.pasteles.filter(p => 
            p.nombre.toLowerCase().includes(this.search.toLowerCase())
        );
    }
}">
    <input type="text" x-model="search" placeholder="Buscar pasteles..." class="form-control">
    
    <div x-for="pastel in filteredPasteles()" :key="pastel.id" class="card mb-2">
        <div class="card-body">
            <h5 x-text="pastel.nombre"></h5>
            <p x-text="'Sabor: ' + pastel.sabor"></p>
            <span x-text="'$' + Number(pastel.precio).toFixed(2)"></span>
        </div>
    </div>
</div>

Livewire en Blade

blade
<!-- Instalar Livewire -->
composer require livewire/livewire

<!-- En el layout -->
@livewireStyles

<!-- En la vista -->
@livewire('pasteles-list')

<!-- O con props -->
@livewire('pasteles-list', ['sabor' => 'chocolate'])

<!-- Scripts -->
@livewireScripts

11. Buenas Prácticas

1. Organización de Vistas

php
// Buena práctica: Estructura clara
resources/views/
├── layouts/
├── auth/
├── admin/
│   ├── dashboard.blade.php
│   ├── usuarios/
│   │   ├── index.blade.php
│   │   └── edit.blade.php
│   └── partials/
│       └── sidebar.blade.php
├── pasteles/
│   ├── index.blade.php
│   ├── create.blade.php
│   └── partials/
│       └── table.blade.php
└── partials/
    ├── alerts.blade.php
    └── layout/
        ├── navbar.blade.php
        └── footer.blade.php

2. Nombres de Vistas Descriptivos

blade
✅ pasteles/index.blade.php
✅ pasteles/create.blade.php
✅ pasteles/edit.blade.php

❌ pasteles/lista.blade.php
❌ pasteles/formulario.blade.php

3. Usar @section y @yield adecuadamente

blade
✅ @section('content')
✅ @yield('title')
✅ @stack('scripts')

❌ @section('contenido')
❌ @yield('titulo')

4. Mantener lógica fuera de las vistas

blade
<!-- ❌ Mal: Lógica en la vista -->
@php
    $precioConIVA = $pastel->precio * 1.16;
    $descuento = $pastel->precio > 100 ? 0.1 : 0;
    $precioFinal = $pastel->precio * (1 - $descuento);
@endphp

<!-- ✅ Bien: Lógica en el controlador o servicio -->
<p>Precio con IVA: ${{ number_format($pastel->precio_iva, 2) }}</p>
<p>Precio final: ${{ number_format($pastel->precio_final, 2) }}</p>

5. Usar componentes para elementos reutilizables

blade
<!-- ✅ Bien: Componente -->
<x-pastel-card :pastel="$pastel" />

<!-- ❌ Mal: Repetir código -->
@foreach($pasteles as $pastel)
    <div class="card">
        <h5>{{ $pastel->nombre }}</h5>
        <p>{{ $pastel->descripcion }}</p>
        <span>${{ $pastel->precio }}</span>
    </div>
@endforeach

6. Optimizar rendimiento

blade
<!-- Usar caché para consultas -->
@php
    $pasteles = Cache::remember('pasteles', 3600, function() {
        return Pastel::all();
    });
@endphp

<!-- Paginación en lugar de cargar todo -->
{{ $pasteles->links() }}

7. Accesibilidad

blade
<!-- ✅ Buenas prácticas de accesibilidad -->
<label for="nombre" class="form-label">Nombre</label>
<input id="nombre" name="nombre" type="text" aria-required="true">

<!-- Incluir texto alternativo en imágenes -->
<img src="{{ $pastel->imagen }}" alt="Pastel de {{ $pastel->nombre }}">

<!-- Usar roles ARIA -->
<nav role="navigation" aria-label="Navegación principal">

Comandos y Recursos Útiles

bash
# Crear una nueva vista
touch resources/views/nueva-vista.blade.php

# Crear un componente
php artisan make:component NombreComponente

# Crear un componente anónimo
php artisan make:component carpeta/nombre --view

# Ver todas las rutas
php artisan route:list

# Limpiar caché de vistas
php artisan view:clear

# Cachear vistas para producción
php artisan view:cache

Recursos adicionales


Resumen

ConceptoDescripciónDirectiva/Comando
VistasArchivos HTML con Blade.blade.php en resources/views/
LayoutsPlantillas base@extends(), @yield(), @section()
PartialsFragmentos reutilizables@include()
ComponentesPartials avanzadosphp artisan make:component
StacksAssets por sección@push(), @prepend(), @stack()
VariablesMostrar datos{{ $variable }}
EstructurasLógica de control@if, @foreach, @forelse
SeguridadProtección XSS{{ }} escapa, {!! !!} no escapa

¡Con estas herramientas, puedes crear vistas profesionales, organizadas y mantenibles en Laravel 12

Comentarios

Entradas más populares de este blog

Tutorial CRUD con Laravel 12-Pastel::factory(50)->create();

CRUD Completo con Laravel 12 - Guía Práctica Paso a Paso-productos

CRUD en Laravel 12 con Git - Guía para Principiantes