3-CRUD de Tareas en Laravel con Explicación de Componentes Blade
Componentes de Blade en Laravel, que se crean con el comando php artisan make:component. Estos componentes sirven para crear elementos reutilizables en tus vistas (como botones, cards, formularios, etc.). Vamos a explicarlos de forma clara:
🔹 Tipos de Componentes en Laravel
Hay dos tipos principales de componentes Blade:
Componentes Anónimos (simples, sin clase asociada).
Componentes de Clase (tienen una clase PHP para lógica adicional).
1. Componentes Anónimos (Archivos simples)
Son ideales para componentes sencillos (ej: alertas, badges).
Se crean manualmente en resources/views/components.
Son ideales para componentes sencillos (ej: alertas, badges).
Se crean manualmente en resources/views/components.
Ejemplo:
Paso 1: Crea un archivo Blade (ej: alert.blade.php):
<!-- resources/views/components/alert.blade.php -->
<div class="alert alert-{{ $type }}">
{{ $slot }} <!-- Contenido que se pasa al componente -->
</div>Paso 2: Usalo en cualquier vista:
<x-alert type="success">
¡Éxito! Tu acción se completó.
</x-alert>2. Componentes de Clase (Con lógica PHP)
Tienen una clase asociada para manejar datos o lógica compleja.
Se crean con Artisan:
Tienen una clase asociada para manejar datos o lógica compleja.
Se crean con Artisan:
Ejemplo: Crear un componente "Card"
Paso 1: Genera el componente:
php artisan make:component CardEsto crea dos archivos:
app/View/Components/Card.php(clase del componente).resources/views/components/card.blade.php(vista).
Paso 2: Define propiedades en la clase:
// app/View/Components/Card.php
class Card extends Component {
public $title;
public $description;
public function __construct($title, $description) {
$this->title = $title;
$this->description = $description;
}
public function render() {
return view('components.card');
}
}Paso 3: Diseña la vista (card.blade.php):
<div class="card">
<h2>{{ $title }}</h2>
<p>{{ $description }}</p>
{{ $slot }} <!-- Contenido dinámico -->
</div>Paso 4: Úsalo en una vista:
<x-card title="Laravel" description="Framework PHP">
<button>Ver más</button>
</x-card>🔥 Ventajas de Usar Componentes
Reutilización: Evitas repetir código HTML/PHP.
Organización: Separas la lógica (clase) de la presentación (vista).
Props: Puedes pasar datos dinámicos (title, description, etc.).
Slots: Permites contenido personalizado dentro del componente.
Reutilización: Evitas repetir código HTML/PHP.
Organización: Separas la lógica (clase) de la presentación (vista).
Props: Puedes pasar datos dinámicos (title, description, etc.).
Slots: Permites contenido personalizado dentro del componente.
📌 Ejemplo Práctico: Botón Reutilizable
php artisan make:component Button
php artisan make:component ButtonClase (Button.php):
public $color;
public function __construct($color = 'blue') {
$this->color = $color;
}Vista (button.blade.php):
<button class="bg-{{ $color }}-500 text-white p-2 rounded">
{{ $slot }}
</button>Uso:
<x-button color="red">Eliminar</x-button>
<x-button>Guardar</x-button> <!-- Color por defecto: blue -->🎯 Conclusión
php artisan make:component crea componentes con clase + vista.
Los anónimos son para piezas simples (solo vista).
Usa props para datos y slots para contenido flexible.
php artisan make:component crea componentes con clase + vista.
Los anónimos son para piezas simples (solo vista).
Usa props para datos y slots para contenido flexible.
CRUD de Tareas en Laravel con Explicación de Componentes Blade
Voy a recrear el CRUD explicando detalladamente cada componente Blade y su función. Este enfoque te ayudará a entender mejor cómo estructurar tus vistas en Laravel.
Estructura Base Blade
1. Layout Principal (resources/views/layouts/app.blade.php)
<!DOCTYPE html>
<html lang="es">
<head>
<!-- @yield define un área que puede ser rellenada por las vistas hijas -->
<title>@yield('title', 'Gestor de Tareas')</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- @stack crea una pila para acumular contenido (usado para CSS/JS específicos) -->
@stack('styles')
</head>
<body>
<!-- Cabecera común a todas las páginas -->
<header class="bg-primary text-white py-3">
<div class="container">
<div class="d-flex justify-content-between align-items-center">
<!-- Las vistas pueden sobrescribir el header -->
<h1>@yield('header')</h1>
<!-- Área para acciones específicas del header -->
@yield('header-actions')
</div>
</div>
</header>
<!-- Contenido principal (obligatorio en todas las vistas) -->
<main class="container py-4">
@yield('content')
</main>
<!-- Footer común -->
<footer class="bg-light py-3 mt-4">
<div class="container text-center">
<p class="mb-0">Sistema de Tareas © {{ date('Y') }}</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Pila para scripts específicos de vistas -->
@stack('scripts')
</body>
</html>Componentes Blade Explicados
2. Vista Index (resources/views/tasks/index.blade.php)
<!-- @extends indica qué layout usar como base -->
@extends('layouts.app')
<!-- @section rellena el yield 'title' en el layout -->
@section('title', 'Listado de Tareas')
<!-- @section rellena el yield 'header' -->
@section('header', 'Todas las Tareas')
<!-- @section rellena el yield 'header-actions' -->
@section('header-actions')
<!-- Botón para crear nueva tarea -->
<a href="{{ route('tasks.create') }}" class="btn btn-light">
<i class="bi bi-plus-circle"></i> Nueva Tarea
</a>
@endsection
<!-- @section rellena el yield 'content' (parte principal) -->
@section('content')
<!-- @include incluye una sub-vista (partial) -->
@include('partials.alerts')
<!-- Tarjeta contenedora -->
<div class="card shadow-sm">
<div class="card-body">
<!-- @forelse es un loop con caso vacío incorporado -->
@forelse($tasks as $task)
<!-- Componente de tarea individual -->
<div class="task-item py-2 border-bottom">
<!-- @each podría usarse aquí para iterar un componente -->
<div class="d-flex justify-content-between align-items-center">
<span>{{ $task->name }}</span>
<div class="actions">
<!-- Formulario de edición (usando @method('PUT')) -->
<a href="{{ route('tasks.edit', $task->id) }}" class="btn btn-sm btn-outline-primary">
Editar
</a>
<!-- Formulario de eliminación -->
<form action="{{ route('tasks.destroy', $task->id) }}" method="POST" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger"
onclick="return confirm('¿Eliminar esta tarea?')">
Eliminar
</button>
</form>
</div>
</div>
</div>
@empty
<!-- Caso cuando no hay tareas -->
<div class="text-center py-5">
<h4 class="text-muted">No hay tareas registradas</h4>
<a href="{{ route('tasks.create') }}" class="btn btn-primary mt-3">
Crear Primera Tarea
</a>
</div>
@endforelse
<!-- Paginación (si está habilitada) -->
@if($tasks->hasPages())
<div class="mt-4">
{{ $tasks->links() }}
</div>
@endif
</div>
</div>
@endsection
<!-- @push añade contenido a la pila 'scripts' -->
@push('scripts')
<script>
// Scripts específicos para esta vista
console.log('Página de listado cargada');
</script>
@endpush3. Partial de Alertas (resources/views/partials/alerts.blade.php)
<!-- Componente reutilizable para mostrar mensajes flash -->
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show mb-4">
{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
@if($errors->any())
<div class="alert alert-danger alert-dismissible fade show mb-4">
<ul class="mb-0">
<!-- @foreach para iterar errores -->
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif4. Vista de Formulario (resources/views/tasks/form.blade.php)
<!-- Componente de formulario reutilizable -->
<div class="card shadow-sm">
<div class="card-header">
<h5 class="card-title mb-0">
<!-- $slot permite contenido dinámico -->
{{ $slot }}
</h5>
</div>
<div class="card-body">
<form action="{{ $action }}" method="POST">
@csrf
<!-- @isset comprueba si la variable está definida -->
@isset($method)
@method($method)
@endisset
<div class="mb-3">
<label for="name" class="form-label">Nombre de la Tarea</label>
<input type="text" class="form-control @error('name') is-invalid @enderror"
id="name" name="name" value="{{ old('name', $task->name ?? '') }}" required>
<!-- @error muestra errores de validación -->
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="d-flex justify-content-end gap-2">
<a href="{{ route('tasks.index') }}" class="btn btn-secondary">
Cancelar
</a>
<button type="submit" class="btn btn-primary">
{{ $submitText ?? 'Guardar' }}
</button>
</div>
</form>
</div>
</div>5. Vista Create (resources/views/tasks/create.blade.php)
@extends('layouts.app')
@section('title', 'Crear Nueva Tarea')
@section('header', 'Nueva Tarea')
@section('content')
<!-- @component es una alternativa para incluir componentes -->
@component('tasks.form', [
'action' => route('tasks.store'),
'task' => new App\Models\Task(), // Objeto vacío para evitar errores
'submitText' => 'Crear Tarea'
])
Crear Nueva Tarea
@endcomponent
@endsection6. Vista Edit (resources/views/tasks/edit.blade.php)
@extends('layouts.app')
@section('title', 'Editar Tarea')
@section('header', 'Editar Tarea')
@section('content')
<!-- Uso del componente form con parámetros -->
@include('tasks.form', [
'action' => route('tasks.update', $task->id),
'method' => 'PUT',
'task' => $task,
'submitText' => 'Actualizar'
])
@endsectionExplicación de Componentes Clave
Herencia (@extends):
Establece la plantilla base que se utilizará
Todas las vistas "extienden" del layout principal
Secciones (@section/@yield):
@sectiondefine contenido para áreas específicas@yieldmuestra el contenido de esas secciones en el layout
Pilas (@stack/@push):
Permiten agregar CSS/JS específicos para cada vista
El contenido se acumula y se renderiza donde está
@stack
Condicionales (@if, @unless, @isset):
Control de flujo para mostrar/ocultar elementos
Ejemplo: mostrar errores solo cuando existen
Bucles (@foreach, @for, @while):
Para iterar sobre colecciones de datos
@forelseincluye caso vacío incorporado
Componentes (@component/@slot):
Crea bloques reutilizables con parámetros
Alternativa a
@includecon más flexibilidad
Includes (@include):
Para dividir vistas en partes reutilizables
Ejemplo: formulario compartido entre create/edit
Directivas de Formulario:
@csrf: Agrega token de seguridad@method: Para métodos HTTP como PUT/DELETE@error: Manejo de errores de validación
Ventajas de esta Estructura
DRY (Don't Repeat Yourself):
El formulario se escribe una vez y se reutiliza
El layout base evita repetir código HTML
Organización Clara:
Cada componente tiene una responsabilidad única
Fácil de mantener y escalar
Flexibilidad:
Las vistas pueden sobrescribir solo lo necesario
Componentes aceptan parámetros dinámicos
Legibilidad:
El código es más fácil de entender
Separación clara entre estructura y contenido
Esta estructura es escalable y puedes agregar fácilmente:
Más componentes específicos
Secciones adicionales en el layout
Estilos/scripts específicos por vista
Más partials para elementos reutilizables
RUD de Tareas con Componentes de Clase en Laravel
Ahora voy a recrear el CRUD utilizando componentes de clase como php artisan make:component NoteCard. Este enfoque es más moderno y organizado, permitiendo una mejor separación de responsabilidades.
1. Crear los Componentes
Primero, generamos los componentes necesarios:
php artisan make:component TaskCard
php artisan make:component TaskForm
php artisan make:component Alert2. Componente Alert (app/View/Components/Alert.php)
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class Alert extends Component
{
public $type;
public $message;
public $dismissible;
public function __construct($type = 'success', $message = null, $dismissible = true)
{
$this->type = $type;
$this->message = $message ?? session('success');
$this->dismissible = $dismissible;
}
public function render()
{
return view('components.alert');
}
}resources/views/components/alert.blade.php:
@if($message)
<div class="alert alert-{{ $type }} @if($dismissible) alert-dismissible fade show @endif mb-4">
{{ $message }}
@if($dismissible)
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
@endif
</div>
@endif3. Componente TaskCard (app/View/Components/TaskCard.php)
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class TaskCard extends Component
{
public $task;
public $showActions;
public function __construct($task, $showActions = true)
{
$this->task = $task;
$this->showActions = $showActions;
}
public function render()
{
return view('components.task-card');
}
}resources/views/components/task-card.blade.php:
<div class="task-card card shadow-sm mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<span class="task-name">{{ $task->name }}</span>
@if($showActions)
<div class="task-actions btn-group">
<a href="{{ route('tasks.edit', $task->id) }}"
class="btn btn-sm btn-outline-primary">
Editar
</a>
<form action="{{ route('tasks.destroy', $task->id) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger"
onclick="return confirm('¿Eliminar esta tarea?')">
Eliminar
</button>
</form>
</div>
@endif
</div>
</div>
</div>4. Componente TaskForm (app/View/Components/TaskForm.php)
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use App\Models\Task;
class TaskForm extends Component
{
public $task;
public $action;
public $method;
public $submitText;
public function __construct($task = null, $action = null, $method = 'POST', $submitText = 'Guardar')
{
$this->task = $task ?? new Task();
$this->action = $action ?? ($task ? route('tasks.update', $task->id) : route('tasks.store'));
$this->method = $method;
$this->submitText = $submitText;
}
public function render()
{
return view('components.task-form');
}
}resources/views/components/task-form.blade.php:
<div class="task-form card shadow-sm">
<div class="card-body">
<form action="{{ $action }}" method="POST">
@csrf
@method($method)
<div class="mb-3">
<label for="name" class="form-label">Nombre de la Tarea</label>
<input type="text" class="form-control @error('name') is-invalid @enderror"
id="name" name="name" value="{{ old('name', $task->name) }}" required>
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="d-flex justify-content-end gap-2">
<a href="{{ route('tasks.index') }}" class="btn btn-secondary">
Cancelar
</a>
<button type="submit" class="btn btn-primary">
{{ $submitText }}
</button>
</div>
</form>
</div>
</div>5. Actualización de las Vistas
Layout Principal (resources/views/layouts/app.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', 'Gestor de Tareas')</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
@stack('styles')
</head>
<body>
<header class="bg-primary text-white py-3">
<div class="container">
<div class="d-flex justify-content-between align-items-center">
<h1>@yield('header')</h1>
@yield('header-actions')
</div>
</div>
</header>
<main class="container py-4">
<x-alert />
@yield('content')
</main>
<footer class="bg-light py-3 mt-4">
<div class="container text-center">
<p class="mb-0">Sistema de Tareas © {{ date('Y') }}</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
@stack('scripts')
</body>
</html>Vista Index (resources/views/tasks/index.blade.php)
@extends('layouts.app')
@section('title', 'Listado de Tareas')
@section('header', 'Todas las Tareas')
@section('header-actions')
<a href="{{ route('tasks.create') }}" class="btn btn-light">
<i class="bi bi-plus-circle"></i> Nueva Tarea
</a>
@endsection
@section('content')
@forelse($tasks as $task)
<x-task-card :task="$task" />
@empty
<div class="text-center py-5">
<h4 class="text-muted">No hay tareas registradas</h4>
<a href="{{ route('tasks.create') }}" class="btn btn-primary mt-3">
Crear Primera Tarea
</a>
</div>
@endforelse
@if($tasks->hasPages())
<div class="mt-4">
{{ $tasks->links() }}
</div>
@endif
@endsectionVista Create (resources/views/tasks/create.blade.php)
@extends('layouts.app')
@section('title', 'Crear Nueva Tarea')
@section('header', 'Nueva Tarea')
@section('content')
<x-task-form submit-text="Crear Tarea" />
@endsectionVista Edit (resources/views/tasks/edit.blade.php)
@extends('layouts.app')
@section('title', 'Editar Tarea')
@section('header', 'Editar Tarea')
@section('content')
<x-task-form :task="$task" method="PUT" submit-text="Actualizar" />
@endsection6. Ventajas de este Enfoque
Mejor Organización:
Cada componente tiene su propia clase y vista
Lógica bien encapsulada
Reutilización:
Los componentes pueden usarse en múltiples vistas
Fácil de mantener y actualizar
Claridad:
Las vistas principales son más limpias y legibles
La lógica compleja está en las clases de componentes
Flexibilidad:
Componentes aceptan parámetros configurables
Fácil de extender y personalizar
Coherencia:
Estilos y comportamientos consistentes
Menos duplicación de código
Uso de los Componentes
Componente Alert:
<x-alert type="success" :message="session('success')" />Componente TaskCard:
<x-task-card :task="$task" :show-actions="false" />Componente TaskForm:
<x-task-form :task="$task" method="PUT" submit-text="Actualizar" />
Este enfoque con componentes de clase es ideal para aplicaciones medianas a grandes, donde la reutilización y mantenibilidad del código son prioritarias. Cada componente se convierte en un bloque de construcción independiente que puede ser probado y desarrollado por separado
mplementación de Componentes en el CRUD de Tareas (Explicación Paso a Paso)
Basándome en el ejemplo anterior del CRUD simple de tareas, voy a explicar cómo implementar componentes de clase (php artisan make:component) para mejorar la estructura.
Explicación del Ejemplo Original (sin componentes)
El CRUD original tenía:
Estructura básica:
Layout principal (
app.blade.php)Vistas para listar, crear y editar tareas
Un único campo "nombre" para las tareas
Flujo de trabajo:
Problemas del enfoque original:
Código duplicado en formularios
Poca reutilización de elementos UI
Mezcla de lógica y presentación
Implementación con Componentes
Paso 1: Crear el Componente TaskForm
Generamos el componente:
php artisan make:component TaskFormEsto crea dos archivos:
app/View/Components/TaskForm.php(clase del componente)resources/views/components/task-form.blade.php(vista del componente)
Paso 2: Configurar el Componente TaskForm
app/View/Components/TaskForm.php:
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use App\Models\Task;
class TaskForm extends Component
{
public $task;
public $action;
public $method;
public $submitText;
public function __construct($task = null)
{
// Si no se pasa una tarea, creamos una nueva instancia
$this->task = $task ?? new Task();
// Determinar acción y método HTTP
$this->action = $task ? route('tasks.update', $task->id) : route('tasks.store');
$this->method = $task ? 'PUT' : 'POST';
$this->submitText = $task ? 'Actualizar' : 'Guardar';
}
public function render()
{
return view('components.task-form');
}
}resources/views/components/task-form.blade.php:
<div class="card mt-4">
<div class="card-body">
<form action="{{ $action }}" method="POST">
@csrf
@method($method)
<div class="mb-3">
<label for="name" class="form-label">Nombre de la Tarea</label>
<input type="text" class="form-control @error('name') is-invalid @enderror"
id="name" name="name" value="{{ old('name', $task->name) }}" required>
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="d-flex justify-content-end">
<a href="{{ route('tasks.index') }}" class="btn btn-secondary me-2">Cancelar</a>
<button type="submit" class="btn btn-primary">{{ $submitText }}</button>
</div>
</form>
</div>
</div>Paso 3: Modificar las Vistas para Usar el Componente
Vista Create (resources/views/tasks/create.blade.php):
@extends('layouts.app')
@section('title', 'Crear Nueva Tarea')
@section('content')
<div class="container">
<h1 class="my-4">Crear Nueva Tarea</h1>
<!-- Usamos el componente -->
<x-task-form />
</div>
@endsectionVista Edit (resources/views/tasks/edit.blade.php):
@extends('layouts.app')
@section('title', 'Editar Tarea')
@section('content')
<div class="container">
<h1 class="my-4">Editar Tarea</h1>
<!-- Pasamos la tarea existente al componente -->
<x-task-form :task="$task" />
</div>
@endsectionPaso 4: Crear un Componente para las Alertas
php artisan make:component Alertapp/View/Components/Alert.php:
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class Alert extends Component
{
public $type;
public $message;
public function __construct($type = 'success', $message = null)
{
$this->type = $type;
$this->message = $message ?? session('success');
}
public function render()
{
return view('components.alert');
}
}resources/views/components/alert.blade.php:
@if($message)
<div class="alert alert-{{ $type }} alert-dismissible fade show mt-4">
{{ $message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endifPaso 5: Actualizar el Layout para Usar Alertas
resources/views/layouts/app.blade.php (agregar esto en el main):
<main class="container py-4">
<!-- Componente de alerta -->
<x-alert />
@yield('content')
</main>Beneficios de esta Implementación
Reducción de duplicación:
El formulario se escribe una sola vez
Lógica compartida entre create/edit
Mejor organización:
Mantenimiento más fácil:
Cambios en formularios solo en un lugar
Comportamiento consistente
Reutilización:
Los componentes pueden usarse en otras partes
Fácil de extender
Ejemplo Completo: Vista Index con Componentes
Podemos llevar esto más lejos creando un componente para las tarjetas de tareas:
php artisan make:component TaskCardapp/View/Components/TaskCard.php:
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class TaskCard extends Component
{
public $task;
public function __construct($task)
{
$this->task = $task;
}
public function render()
{
return view('components.task-card');
}
}resources/views/components/task-card.blade.php:
<div class="card mb-3">
<div class="card-body d-flex justify-content-between align-items-center">
<span>{{ $task->name }}</span>
<div>
<a href="{{ route('tasks.edit', $task->id) }}" class="btn btn-sm btn-warning">Editar</a>
<form action="{{ route('tasks.destroy', $task->id) }}" method="POST" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('¿Eliminar?')">Eliminar</button>
</form>
</div>
</div>
</div>Ahora la vista index quedaría:
resources/views/tasks/index.blade.php:
@extends('layouts.app')
@section('title', 'Lista de Tareas')
@section('content')
<div class="container">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Lista de Tareas</h1>
<a href="{{ route('tasks.create') }}" class="btn btn-primary">Nueva Tarea</a>
</div>
@forelse($tasks as $task)
<x-task-card :task="$task" />
@empty
<div class="alert alert-info">No hay tareas registradas</div>
@endforelse
</div>
@endsectionConclusión
La implementación con componentes:
Organiza mejor el código
Facilita el mantenimiento
Promueve la reutilización
Separa claramente la lógica de la presentación
Los componentes son especialmente útiles cuando:
Tienes elementos que se repiten
Necesitas mantener consistencia en la UI
Quieres encapsular lógica compleja
Comentarios
Publicar un comentario