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
¿Qué son las Vistas en Laravel?
Configuración Inicial y Estructura
Sintaxis Básica de Blade
Templates (Plantillas) con Blade
Partials (Fragmentos de Vistas)
Directivas de Control en Blade
Componentes de Laravel
Trabajando con Formularios en Blade
Stacks y Assets
Blade y JavaScript
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
# Crear una vista simple touch resources/views/saludo.blade.php
<!-- 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
// 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
php artisan make:controller SaludoController
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class SaludoController extends Controller { public function index() { return view('saludo'); } }
// routes/web.php Route::get('/saludo', [SaludoController::class, 'index']);
2. Configuración Inicial y Estructura
Estructura de la carpeta views
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)
<!-- 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
<!-- 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>
@endpush3. Sintaxis Básica de Blade
Mostrar datos con {{ }}
<!-- 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
<!-- 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
{{-- 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 */
@endphp4. Templates (Plantillas) con Blade
Directivas principales para layouts
| Directiva | Uso | Descripción |
|---|---|---|
@extends('layout') | Vista hija | Indica qué layout usar |
@yield('section') | Layout | Define una sección que puede ser sobrescrita |
@section('section') | Vista hija | Define el contenido de una sección |
@stop o @endsection | Vista hija | Finaliza una sección |
@show | Layout | Finaliza una sección y la muestra |
@parent | Vista hija | Incluye el contenido del layout en la sección |
Ejemplo completo de layout
<!-- 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>© {{ 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
<!-- 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>
@endsection5. Partials (Fragmentos de Vistas)
Los partials son pequeños fragmentos de vistas reutilizables.
Crear un partial de barra de navegación
<!-- 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
<!-- 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>
@endifPartial de campos de formulario
<!-- 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
<!-- 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
<!-- 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
<!-- @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>
@endswitchBucles
<!-- @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
@endwhileVariables de bucle $loop
@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>
@endforeachAutenticación y Autorización
<!-- 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>
@endcanany7. Componentes de Laravel
Los componentes son una alternativa más poderosa a los partials.
Crear un componente
# 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 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' }; } }
<!-- 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
<!-- 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
<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
composer require laravelcollective/html{!! 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
<!-- 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>
@endprependAssets con Vite (Laravel 12)
<!-- 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'])
@endpushAsignar variables a JavaScript
<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
<!-- 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
<!-- 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 -->
@livewireScripts11. Buenas Prácticas
1. Organización de Vistas
// 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
✅ 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
✅ @section('content')
✅ @yield('title')
✅ @stack('scripts')
❌ @section('contenido')
❌ @yield('titulo')4. Mantener lógica fuera de las vistas
<!-- ❌ 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
<!-- ✅ 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>
@endforeach6. Optimizar rendimiento
<!-- 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
<!-- ✅ 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
# 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
| Concepto | Descripción | Directiva/Comando |
|---|---|---|
| Vistas | Archivos HTML con Blade | .blade.php en resources/views/ |
| Layouts | Plantillas base | @extends(), @yield(), @section() |
| Partials | Fragmentos reutilizables | @include() |
| Componentes | Partials avanzados | php artisan make:component |
| Stacks | Assets por sección | @push(), @prepend(), @stack() |
| Variables | Mostrar datos | {{ $variable }} |
| Estructuras | Lógica de control | @if, @foreach, @forelse |
| Seguridad | Protección XSS | {{ }} escapa, {!! !!} no escapa |
¡Con estas herramientas, puedes crear vistas profesionales, organizadas y mantenibles en Laravel 12
Comentarios
Publicar un comentario