lt;pgt;Đây là cách mình thường làm cho dự án Laravel lớn: lt;stronggt;không dùng packagelt;/stronggt;, chỉ tạo các lt;stronggt;Blade Componentslt;/stronggt; để chuẩn hóa giao diện form.lt;/pgt;lt;pgt;Sau này tất cả CRUD chỉ việc gọi component.lt;/pgt;lt;h2gt;Cấu trúclt;/h2gt;lt;pregt;lt;code class=quot;language-textquot;gt;resources/
└── views/
└── components/
└── ui/
├── input.blade.php
├── textarea.blade.php
├── select.blade.php
├── button.blade.php
└── card.blade.php
lt;/codegt;lt;/pregt;lt;hr /gt;lt;h1gt;lt;div class=quot;separatorquot; style=quot;clear: both; text-align: center;quot;gt;lt;a href=quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhwP8JGkKiT_LDoheCRPudgrEYTjNe17i0nG78aJIqM5M1OZ9psbObsDf2Q-Mwsgp4yoljNXMpX820dK5nKOF7FklxWQPVQXzvlBZdiXNA83mbwJ8nNml1x7hWqc3tGMdfkHMLEgdZ1bVQC6I2QFk9fL0Mrv9X1s4Lv_5qmGAjCEW2pZhRlidxiTDK08Ug/s1536/laravel-thu.pngquot; style=quot;margin-left: 1em; margin-right: 1em;quot;gt;lt;img border=quot;0quot; data-original-height=quot;1024quot; data-original-width=quot;1536quot; height=quot;213quot; src=quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhwP8JGkKiT_LDoheCRPudgrEYTjNe17i0nG78aJIqM5M1OZ9psbObsDf2Q-Mwsgp4yoljNXMpX820dK5nKOF7FklxWQPVQXzvlBZdiXNA83mbwJ8nNml1x7hWqc3tGMdfkHMLEgdZ1bVQC6I2QFk9fL0Mrv9X1s4Lv_5qmGAjCEW2pZhRlidxiTDK08Ug/s320/laravel-thu.pngquot; width=quot;320quot; /gt;lt;/agt;lt;spangt;lt;a name=#39;more#39;gt;lt;/agt;lt;/spangt;lt;/divgt;lt;br /gt;1. Cardlt;/h1gt;lt;pgt;lt;code inline=quot;quot;gt;resources/views/components/ui/card.blade.phplt;/codegt;lt;/pgt;lt;pregt;lt;code class=quot;language-bladequot;gt;@props([
#39;title#39; =gt; #39;#39;,
#39;description#39; =gt; #39;#39;
])
lt;div class=quot;bg-white rounded-xl border border-gray-200 shadow-smquot;gt;
@if($title)
lt;div class=quot;px-6 py-4 border-bquot;gt;
lt;h2 class=quot;text-xl font-semibold text-gray-800quot;gt;
{{ $title }}
lt;/h2gt;
@if($description)
lt;p class=quot;text-sm text-gray-500 mt-1quot;gt;
{{ $description }}
lt;/pgt;
@endif
lt;/divgt;
@endif
lt;div class=quot;p-6quot;gt;
{{ $slot }}
lt;/divgt;
lt;/divgt;
lt;/codegt;lt;/pregt;lt;hr /gt;lt;h1gt;2. Inputlt;/h1gt;lt;pgt;lt;code inline=quot;quot;gt;resources/views/components/ui/input.blade.phplt;/codegt;lt;/pgt;lt;pregt;lt;code class=quot;language-bladequot;gt;@props([
#39;label#39;,
#39;name#39;,
#39;type#39; =gt; #39;text#39;,
#39;value#39; =gt; null,
])
lt;div class=quot;mb-5quot;gt;
lt;label
for=quot;{{ $name }}quot;
class=quot;block text-sm font-semibold text-gray-700 mb-2quot;gt;
{{ $label }}
lt;/labelgt;
lt;input
id=quot;{{ $name }}quot;
type=quot;{{ $type }}quot;
name=quot;{{ $name }}quot;
value=quot;{{ old($name, $value) }}quot;
{{ $attributes-gt;merge([
#39;class#39; =gt;
#39;inline-block rounded-lg border border-gray-300 bg-white
px-4 py-2.5
focus:border-blue-500
focus:ring-4
focus:ring-blue-100
outline-none
transition#39;
]) }}gt;
@error($name)
lt;p class=quot;text-red-600 text-sm mt-2quot;gt;
{{ $message }}
lt;/pgt;
@enderror
lt;/divgt;
lt;/codegt;lt;/pregt;lt;hr /gt;lt;h1gt;3. Textarealt;/h1gt;lt;pregt;lt;code class=quot;language-bladequot;gt;@props([
#39;label#39;,
#39;name#39;,
#39;rows#39; =gt; 4,
#39;value#39; =gt; null,
])
lt;div class=quot;mb-5quot;gt;
lt;label
for=quot;{{ $name }}quot;
class=quot;block text-sm font-semibold text-gray-700 mb-2quot;gt;
{{ $label }}
lt;/labelgt;
lt;textarea
id=quot;{{ $name }}quot;
name=quot;{{ $name }}quot;
rows=quot;{{ $rows }}quot;
{{ $attributes-gt;merge([
#39;class#39; =gt;
#39;w-full rounded-lg border border-gray-300
px-4 py-2.5
focus:border-blue-500
focus:ring-4
focus:ring-blue-100#39;
]) }}gt;{{ old($name, $value) }}lt;/textareagt;
@error($name)
lt;p class=quot;text-red-600 text-sm mt-2quot;gt;
{{ $message }}
lt;/pgt;
@enderror
lt;/divgt;
lt;/codegt;lt;/pregt;lt;hr /gt;lt;h1gt;4. Buttonlt;/h1gt;lt;pregt;lt;code class=quot;language-bladequot;gt;@props([
#39;color#39; =gt; #39;blue#39;
])
lt;button
{{ $attributes-gt;merge([
#39;class#39; =gt;
quot;px-6 py-2 rounded-lg
bg-$color-600
hover:bg-$color-700
text-white
transitionquot;
]) }}gt;
{{ $slot }}
lt;/buttongt;
lt;/codegt;lt;/pregt;lt;blockquotegt;lt;pgt;lt;stronggt;Lưu ý:lt;/stronggt; Với Tailwind JIT, class động như lt;code inline=quot;quot;gt;bg-$color-600lt;/codegt; sẽ lt;stronggt;không được buildlt;/stronggt; nếu không safelist. An toàn hơn là dùng lt;code inline=quot;quot;gt;@classlt;/codegt; hoặc lt;code inline=quot;quot;gt;matchlt;/codegt; để ánh xạ màu.lt;/pgt;lt;/blockquotegt;lt;hr /gt;lt;h1gt;5. Selectlt;/h1gt;lt;pregt;lt;code class=quot;language-bladequot;gt;@props([
#39;label#39;,
#39;name#39;,
#39;options#39; =gt; [],
#39;placeholder#39; =gt; null,
#39;multiple#39; =gt; false,
])
lt;label
for=quot;{{ $name }}quot;
class=quot;block text-sm font-semibold text-gray-700 mb-2quot;gt;
{{ $label }}
lt;/labelgt;
lt;select
id=quot;{{ $name }}quot;
name=quot;{{ $multiple ? $name . #39;[]#39; : $name }}quot;
@if($multiple) multiple @endif
{{ $attributes-gt;merge([
#39;class#39; =gt;
#39;inline-block rounded-lg border border-gray-300 bg-white
px-4 py-2.5
focus:border-blue-500
focus:ring-4
focus:ring-blue-100
outline-none
transition#39;
]) }}gt;
@if($placeholder !$multiple)
lt;option value=quot;quot;gt;
{{ $placeholder }}
lt;/optiongt;
@endif
@foreach($options as $key =gt; $option)
@php
/*
* Eloquent model
*/
if (is_object($option)) {
$value = $option-gt;id;
$text = $option-gt;name;
}
/*
* Array:
* [#39;value#39; =gt; #39;draft#39;, #39;label#39; =gt; #39;Nháp#39;]
*/
elseif (is_array($option)) {
$value = $option[#39;value#39;];
$text = $option[#39;label#39;];
}
/*
* Associative array:
* #39;draft#39; =gt; #39;Nháp#39;
*/
else {
$value = $key;
$text = $option;
}
@endphp
lt;option
value=quot;{{ $value }}quot;
@selected(
$multiple
? in_array(
$value,
old($name, [])
)
: old($name) == $value
)gt;
{{ $text }}
lt;/optiongt;
@endforeach
lt;/selectgt;
@error($name)
lt;p class=quot;text-red-600 text-sm mt-2quot;gt;
{{ $message }}
lt;/pgt;
@enderrorlt;/codegt;lt;/pregt;lt;pgt;lt;stronggt;Ghi chú:lt;/stronggt; chọn 01.lt;/pgt;lt;h1gt;6. Select Multiplelt;/h1gt;lt;pregt;lt;code class=quot;language-bladequot;gt;@php
$selectedValues = collect($value)-gt;map(fn ($id) =gt; (string) $id)-gt;toArray();
@endphp
lt;div class=quot;mb-3quot;gt;
lt;label for=quot;{{ $name }}quot; class=quot;form-labelquot;gt;
{{ $label }}
lt;/labelgt;
lt;select
id=quot;{{ $name }}quot;
name=quot;{{ $name }}[]quot;
class=quot;form-select @error($name) is-invalid @enderrorquot;
multiple
gt;
@foreach ($options as $option)
@php
$optionValue = is_array($option)
? $option[#39;value#39;]
: ($option-gt;id ?? $option);
$optionLabel = is_array($option)
? $option[#39;label#39;]
: ($option-gt;name ?? $option-gt;title ?? $option);
@endphp
lt;option
value=quot;{{ $optionValue }}quot;
@selected(in_array((string) $optionValue, $selectedValues, true))
gt;
{{ $optionLabel }}
lt;/optiongt;
@endforeach
lt;/selectgt;
@error($name)
lt;div class=quot;invalid-feedbackquot;gt;
{{ $message }}
lt;/divgt;
@enderror
lt;/divgt;
lt;/codegt;lt;/pregt;lt;blockquotegt;lt;pgt;lt;stronggt;Ghi chú:lt;/stronggt;chọn nhiều mục.lt;/pgt;lt;/blockquotegt;lt;hr /gt;lt;h1gt;7. Sử dụnglt;/h1gt;lt;pregt;lt;code class=quot;language-bladequot;gt;lt;x-app-layoutgt;
lt;div class=quot;max-w-3xl mx-auto py-8quot;gt;
lt;x-ui.card
title=quot;Thêm Categoryquot;
description=quot;Nhập thông tin danh mụcquot;gt;
lt;form
action=quot;{{ route(#39;categories.store#39;) }}quot;
method=quot;POSTquot;gt;
@csrf
lt;x-ui.input
label=quot;Tên Categoryquot;
name=quot;namequot;/gt;
lt;x-ui.input
label=quot;Slugquot;
name=quot;slugquot;/gt;
lt;x-ui.textarea
label=quot;Mô tảquot;
name=quot;descriptionquot;/gt;
lt;div class=quot;flex justify-end gap-3quot;gt;
lt;a
href=quot;{{ route(#39;categories.index#39;) }}quot;
class=quot;px-6 py-2 rounded-lg border hover:bg-gray-100quot;gt;
Hủy
lt;/agt;
lt;x-ui.buttongt;
Lưu
lt;/x-ui.buttongt;
lt;/divgt;
lt;/formgt;
lt;/x-ui.cardgt;
lt;/divgt;
lt;/x-app-layoutgt;
lt;/codegt;lt;/pregt;lt;hr /gt;lt;h2gt;Ưu điểmlt;/h2gt;lt;ulgt;lt;ligt;lt;pgt;✅ Toàn bộ CRUD có giao diện đồng nhất.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;✅ Muốn đổi style chỉ sửa lt;stronggt;1 componentlt;/stronggt;.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;✅ Code lt;code inline=quot;quot;gt;create.blade.phplt;/codegt; và lt;code inline=quot;quot;gt;edit.blade.phplt;/codegt; ngắn, dễ đọc.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;✅ Phù hợp để xây dựng khóa học Laravel dài vì tránh lặp lại hàng trăm dòng HTML.lt;/pgt;lt;/ligt;lt;/ulgt;lt;pgt;Mình còn khuyên thêm một bước nữa: tạo lt;stronggt;bộ UI riêng kiểu AdminLTE nhưng viết bằng Tailwindlt;/stronggt; (card, table, alert, modal, form, pagination, badge...).lt;/pgt;lt;pgt;Khi đó toàn bộ khóa học sẽ có một quot;design systemquot; thống nhất thay vì chỉ có các component rời rạc. Đây là cách nhiều dự án Laravel chuyên nghiệp tổ chức giao diện quản trị.lt;/pgt;lt;pgt;Làm Flash Message:lt;/pgt;lt;pgt;Ý tưởng này rất đáng làm. Chỉ cần include một lần trong lt;code inline=quot;quot;gt;x-app-layoutlt;/codegt; là toàn bộ CRUD sẽ có thông báo.lt;/pgt;lt;pgt;lt;code inline=quot;quot;gt;resources/views/components/alert.blade.phplt;/codegt;lt;/pgt;lt;pregt;lt;code class=quot;language-bladequot;gt;@php
$alerts = [
#39;success#39; =gt; [
#39;title#39; =gt; #39;Thành công#39;,
#39;bg#39; =gt; #39;bg-green-50#39;,
#39;border#39; =gt; #39;border-green-200#39;,
#39;text#39; =gt; #39;text-green-800#39;,
#39;icon#39; =gt; #39;✓#39;,
],
#39;error#39; =gt; [
#39;title#39; =gt; #39;Lỗi#39;,
#39;bg#39; =gt; #39;bg-red-50#39;,
#39;border#39; =gt; #39;border-red-200#39;,
#39;text#39; =gt; #39;text-red-800#39;,
#39;icon#39; =gt; #39;✕#39;,
],
#39;warning#39; =gt; [
#39;title#39; =gt; #39;Cảnh báo#39;,
#39;bg#39; =gt; #39;bg-yellow-50#39;,
#39;border#39; =gt; #39;border-yellow-200#39;,
#39;text#39; =gt; #39;text-yellow-800#39;,
#39;icon#39; =gt; #39;⚠#39;,
],
#39;info#39; =gt; [
#39;title#39; =gt; #39;Thông tin#39;,
#39;bg#39; =gt; #39;bg-blue-50#39;,
#39;border#39; =gt; #39;border-blue-200#39;,
#39;text#39; =gt; #39;text-blue-800#39;,
#39;icon#39; =gt; #39;ⓘ#39;,
],
];
@endphp
@foreach($alerts as $type =gt; $alert)
@if(session($type))
lt;div
x-data=quot;{ show: true }quot;
x-init=quot;setTimeout(() =gt; show = false, 5000)quot;
x-show=quot;showquot;
x-transition
class=quot;mb-6 rounded-lg border {{ $alert[#39;border#39;] }} {{ $alert[#39;bg#39;] }} p-4 shadowquot;gt;
lt;div class=quot;flex justify-betweenquot;gt;
lt;div class=quot;flex gap-3quot;gt;
lt;div class=quot;text-xlquot;gt;
{{ $alert[#39;icon#39;] }}
lt;/divgt;
lt;divgt;
lt;div class=quot;font-semibold {{ $alert[#39;text#39;] }}quot;gt;
{{ $alert[#39;title#39;] }}
lt;/divgt;
lt;div class=quot;{{ $alert[#39;text#39;] }}quot;gt;
{{ session($type) }}
lt;/divgt;
lt;/divgt;
lt;/divgt;
lt;button
@click=quot;show = falsequot;
class=quot;{{ $alert[#39;text#39;] }}quot;gt;
✕
lt;/buttongt;
lt;/divgt;
lt;/divgt;
@endif
@endforeach
lt;/codegt;lt;/pregt;lt;pgt;Sau đó chỉ cần đặt ở đầu nội dung của layout:lt;/pgt;lt;pregt;lt;code class=quot;language-bladequot;gt;lt;x-app-layoutgt;
lt;div class=quot;py-8quot;gt;
lt;div class=quot;max-w-7xl mx-auto sm:px-6 lg:px-8quot;gt;
lt;x-alert /gt;
{{ $slot }}
lt;/divgt;
lt;/divgt;
lt;/x-app-layoutgt;
lt;/codegt;lt;/pregt;lt;pgt;Controller chỉ cần:lt;/pgt;lt;pregt;lt;code class=quot;language-phpquot;gt;return redirect()
-gt;route(#39;categories.index#39;)
-gt;with(#39;success#39;, #39;Thêm Category thành công!#39;);
lt;/codegt;lt;/pregt;lt;pgt;hoặclt;/pgt;lt;pregt;lt;code class=quot;language-phpquot;gt;-gt;with(#39;error#39;, #39;Không thể xóa Category.#39;);
lt;/codegt;lt;/pregt;lt;hr /gt;lt;h3gt;Mình còn đề xuất một bản quot;xịnquot; hơnlt;/h3gt;lt;pgt;Thay vì chỉ có lt;code inline=quot;quot;gt;lt;x-alert /gt;lt;/codegt;, hãy tạo luôn lt;stronggt;Flash Messagelt;/stronggt; tự động biến mất sau 4–5 giây bằng Alpine.js (đã có sẵn trong Breeze).lt;/pgt;lt;pgt;Khi đó thông báo sẽ giống AdminLTE, Filament hoặc Jetstream:lt;/pgt;lt;ulgt;lt;ligt;lt;pgt;Có hiệu ứng trượt xuống.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Tự mờ dần rồi biến mất.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Có nút ✕ để đóng thủ công.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Không cần JavaScript riêng, chỉ vài dòng Alpine.lt;/pgt;lt;/ligt;lt;/ulgt;lt;pgt;Đó là phiên bản mình sẽ chọn cho một khóa Laravel 12 vì nhìn hiện đại hơn hẳn mà gần như không tăng độ khó.lt;/pgt;