Yii2 Widgets, Asset Bundles & Frontend Integration

Các bài trước tập trung vào backend — Active Record, authentication, forms, REST APIs. Nhưng Yii2 còn có frontend layer cực mạnh: widgets generate HTML phức tạp từ PHP, asset bundle system quản lý CSS/JS dependencies, và Pjax cho AJAX-powered page updates mà không cần viết JavaScript.
Widgets như GridView có thể render data table hoàn chỉnh với sorting, pagination, filtering, và action buttons — chỉ từ một lời gọi PHP duy nhất. ListView làm tương tự cho card-based layouts. DetailView hiển thị single record dưới dạng labeled table. Và nếu built-in widgets không đủ, bạn có thể build cái riêng.
Kết thúc bài này, task manager sẽ có polished frontend với sortable data tables, card views, AJAX-powered interactions, managed assets, và con đường rõ ràng để tích hợp modern JavaScript frameworks.
Bạn Sẽ Build Gì
✅ GridView cho sortable, paginated data tables với action columns
✅ ListView cho card-based và custom item layouts
✅ DetailView cho hiển thị single-record
✅ Pjax cho AJAX-powered page updates không cần full reload
✅ Build custom widgets tái sử dụng
✅ Asset bundles quản lý CSS/JS dependencies
✅ Tích hợp Bootstrap 5, Vue.js, và React với Yii2
1. GridView — Data Tables Dễ Như Ăn Bánh
GridView là con ngựa chiến của frontend Yii2. Đưa cho nó data provider và nó render complete HTML table với sorting, pagination, filtering, và action buttons.
GridView Cơ Bản
// views/task/index.php
use yii\grid\GridView;
use yii\grid\ActionColumn;
use yii\grid\SerialColumn;
<?= GridView::widget([
'dataProvider' => $dataProvider,
'columns' => [
['class' => SerialColumn::class], // Số thứ tự: 1, 2, 3...
'title',
'description:ntext', // format dạng plain text (HTML escaped)
'status',
'priority',
'due_date:date', // format dạng date
['class' => ActionColumn::class], // Nút View, Update, Delete
],
]) ?>Controller cung cấp data:
// controllers/TaskController.php
public function actionIndex()
{
$dataProvider = new \yii\data\ActiveDataProvider([
'query' => Task::find()->where(['user_id' => \Yii::$app->user->id]),
'sort' => [
'defaultOrder' => ['created_at' => SORT_DESC],
],
'pagination' => [
'pageSize' => 20,
],
]);
return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}Nó render data table đầy đủ tính năng — column headers click được để sort, pagination xuất hiện ở dưới, và mỗi row có links view/update/delete.
Custom Columns
Override column rendering cho hiển thị phong phú hơn:
<?= GridView::widget([
'dataProvider' => $dataProvider,
'tableOptions' => ['class' => 'table table-striped table-bordered'],
'columns' => [
['class' => SerialColumn::class],
[
'attribute' => 'title',
'format' => 'raw', // cho phép HTML
'value' => function ($model) {
return Html::a(
Html::encode($model->title),
['view', 'id' => $model->id],
['class' => 'fw-bold']
);
},
],
// Status dạng colored badges
[
'attribute' => 'status',
'format' => 'raw',
'value' => function ($model) {
$colors = [
Task::STATUS_TODO => 'secondary',
Task::STATUS_IN_PROGRESS => 'primary',
Task::STATUS_DONE => 'success',
];
$color = $colors[$model->status] ?? 'secondary';
return Html::tag('span', $model->getStatusLabel(), [
'class' => "badge bg-$color",
]);
},
'filter' => [
Task::STATUS_TODO => 'To Do',
Task::STATUS_IN_PROGRESS => 'In Progress',
Task::STATUS_DONE => 'Done',
],
],
// Priority với sao
[
'attribute' => 'priority',
'format' => 'raw',
'value' => function ($model) {
return str_repeat('★', $model->priority)
. str_repeat('☆', 3 - $model->priority);
},
],
// Due date highlight khi quá hạn
[
'attribute' => 'due_date',
'format' => 'date',
'contentOptions' => function ($model) {
if ($model->due_date && strtotime($model->due_date) < time()) {
return ['class' => 'text-danger fw-bold'];
}
return [];
},
],
// Custom action column
[
'class' => ActionColumn::class,
'template' => '{view} {update} {complete} {delete}',
'buttons' => [
'complete' => function ($url, $model) {
if ($model->status === Task::STATUS_DONE) {
return '';
}
return Html::a('✓', ['complete', 'id' => $model->id], [
'class' => 'btn btn-sm btn-success',
'title' => 'Đánh dấu hoàn thành',
'data-method' => 'post',
]);
},
],
],
],
]) ?>GridView Với Filtering (SearchModel)
Cho column-level filters, tạo search model:
// models/TaskSearch.php
namespace app\models;
use yii\base\Model;
use yii\data\ActiveDataProvider;
class TaskSearch extends Task
{
public function rules()
{
return [
[['id', 'status', 'priority'], 'integer'],
[['title', 'description'], 'safe'],
[['due_date'], 'date', 'format' => 'php:Y-m-d'],
];
}
public function search($params)
{
$query = Task::find()
->where(['user_id' => \Yii::$app->user->id]);
$dataProvider = new ActiveDataProvider([
'query' => $query,
'sort' => [
'defaultOrder' => ['created_at' => SORT_DESC],
],
]);
$this->load($params);
if (!$this->validate()) {
return $dataProvider;
}
// Áp dụng filters
$query->andFilterWhere(['status' => $this->status]);
$query->andFilterWhere(['priority' => $this->priority]);
$query->andFilterWhere(['like', 'title', $this->title]);
$query->andFilterWhere(['like', 'description', $this->description]);
$query->andFilterWhere(['due_date' => $this->due_date]);
return $dataProvider;
}
}Controller:
public function actionIndex()
{
$searchModel = new TaskSearch();
$dataProvider = $searchModel->search(\Yii::$app->request->queryParams);
return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}View với filter row:
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel, // Bật filter row
'columns' => [
['class' => SerialColumn::class],
'title',
[
'attribute' => 'status',
'filter' => Task::getStatusOptions(),
'value' => function ($model) {
return $model->getStatusLabel();
},
],
'priority',
'due_date:date',
['class' => ActionColumn::class],
],
]) ?>Giờ mỗi column có text input hoặc dropdown để filter. Gõ vào column header và table filter tức thì.
2. ListView — Card-Based Layouts
ListView render mỗi item dùng custom view partial — hoàn hảo cho card layouts, blog post lists, hoặc bất kỳ hiển thị non-tabular nào.
ListView Cơ Bản
// views/task/cards.php
use yii\widgets\ListView;
<?= ListView::widget([
'dataProvider' => $dataProvider,
'itemView' => '_task_card', // render views/task/_task_card.php cho mỗi item
'layout' => '{summary}{items}{pager}',
'itemOptions' => ['class' => 'col-md-4 mb-3'],
'options' => ['class' => 'row'],
'emptyText' => 'Không tìm thấy tasks nào.',
]) ?>Item partial:
// views/task/_task_card.php
use yii\helpers\Html;
<div class="card h-100 <?= $model->isOverdue() ? 'border-danger' : '' ?>">
<div class="card-body">
<h5 class="card-title">
<?= Html::a(Html::encode($model->title), ['view', 'id' => $model->id]) ?>
</h5>
<p class="card-text text-muted">
<?= Html::encode($model->description) ?>
</p>
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-<?= $model->getStatusColor() ?>">
<?= $model->getStatusLabel() ?>
</span>
<small class="text-muted">
Hạn: <?= Yii::$app->formatter->asDate($model->due_date) ?>
</small>
</div>
</div>
<div class="card-footer">
<?= Html::a('Sửa', ['update', 'id' => $model->id],
['class' => 'btn btn-sm btn-outline-primary']) ?>
<?= Html::a('Xóa', ['delete', 'id' => $model->id], [
'class' => 'btn btn-sm btn-outline-danger',
'data-method' => 'post',
'data-confirm' => 'Bạn có chắc chắn?',
]) ?>
</div>
</div>Biến Có Sẵn Trong Item View
Item partial nhận các biến này tự động:
| Biến | Kiểu | Mô tả |
|---|---|---|
$model | ActiveRecord | Data model hiện tại |
$key | mixed | Giá trị primary key |
$index | int | Index zero-based của item |
$widget | ListView | ListView widget instance |
ListView Với Custom Summary
<?= ListView::widget([
'dataProvider' => $dataProvider,
'itemView' => '_task_card',
'summary' => 'Hiển thị {begin}-{end} trong tổng {totalCount} tasks',
'pager' => [
'class' => \yii\widgets\LinkPager::class,
'options' => ['class' => 'pagination justify-content-center'],
'linkOptions' => ['class' => 'page-link'],
],
]) ?>Chuyển Đổi Giữa Grid và List View
Pattern phổ biến — cho users toggle giữa table và card views:
// views/task/index.php
<?php
$viewMode = Yii::$app->request->get('view', 'grid');
?>
<div class="btn-group mb-3">
<?= Html::a('Bảng', ['index', 'view' => 'grid'],
['class' => 'btn btn-sm btn-' . ($viewMode === 'grid' ? 'primary' : 'outline-primary')]) ?>
<?= Html::a('Thẻ', ['index', 'view' => 'list'],
['class' => 'btn btn-sm btn-' . ($viewMode === 'list' ? 'primary' : 'outline-primary')]) ?>
</div>
<?php if ($viewMode === 'list'): ?>
<?= ListView::widget([
'dataProvider' => $dataProvider,
'itemView' => '_task_card',
'options' => ['class' => 'row'],
'itemOptions' => ['class' => 'col-md-4 mb-3'],
]) ?>
<?php else: ?>
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [/* ... */],
]) ?>
<?php endif; ?>3. DetailView — Hiển Thị Single Record
DetailView hiển thị single model dưới dạng bảng label-value pairs.
// views/task/view.php
use yii\widgets\DetailView;
<?= DetailView::widget([
'model' => $model,
'attributes' => [
'id',
'title',
'description:ntext', // format dạng multi-line text
[
'attribute' => 'status',
'format' => 'raw',
'value' => Html::tag('span', $model->getStatusLabel(), [
'class' => 'badge bg-' . $model->getStatusColor(),
]),
],
[
'attribute' => 'priority',
'value' => str_repeat('★', $model->priority)
. str_repeat('☆', 3 - $model->priority),
],
'due_date:date',
[
'attribute' => 'user_id',
'label' => 'Người thực hiện',
'value' => $model->user->username ?? 'Chưa giao',
],
'created_at:datetime',
'updated_at:datetime',
],
]) ?>Output là bảng hai cột sạch sẽ:
| Label | Value |
|---|---|
| ID | 42 |
| Title | Deploy REST API |
| Status | In Progress (badge) |
| Priority | ★★★ |
| Due Date | 25/03/2026 |
| Người thực hiện | chanh |
Format Shortcuts
Yii2 hỗ trợ format shorthand trong attribute:format:
| Format | Ví dụ | Output |
|---|---|---|
text | 'title:text' | Text HTML-escaped |
ntext | 'description:ntext' | Multi-line text (nl2br) |
html | 'content:html' | Raw HTML (dùng cẩn thận) |
date | 'due_date:date' | Date đã format |
datetime | 'created_at:datetime' | Date + time đã format |
email | 'email:email' | Link mailto click được |
url | 'website:url' | Link click được |
boolean | 'is_active:boolean' | Yes/No |
integer | 'count:integer' | Số có grouping |
4. Pjax — AJAX Mà Không Cần Viết JavaScript
Pjax (pushState + AJAX) cập nhật một phần trang mà không cần full reload. Bọc bất kỳ widget nào trong Pjax và sorting, pagination, filtering tự động qua AJAX.
Bọc GridView Với Pjax
use yii\widgets\Pjax;
<?php Pjax::begin(['id' => 'task-grid-pjax']) ?>
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
'title',
'status',
'priority',
'due_date:date',
['class' => ActionColumn::class],
],
]) ?>
<?php Pjax::end() ?>Giờ click sort headers, pagination links, hoặc gõ trong filter fields chỉ cập nhật grid — không full page reload. URL trên address bar cập nhật qua pushState, nên bookmarks và nút back vẫn hoạt động.
Pjax Với Forms
Submit forms qua AJAX và cập nhật chỉ Pjax container:
<?php Pjax::begin(['id' => 'task-create-pjax']) ?>
<?php $form = ActiveForm::begin([
'options' => ['data-pjax' => true], // Submit qua Pjax
]) ?>
<?= $form->field($model, 'title') ?>
<?= $form->field($model, 'status')->dropDownList(Task::getStatusOptions()) ?>
<?= Html::submitButton('Tạo', ['class' => 'btn btn-primary']) ?>
<?php ActiveForm::end() ?>
<?php Pjax::end() ?>Trigger Pjax Reload Từ JavaScript
// Reload specific Pjax container
$.pjax.reload({container: '#task-grid-pjax'});
// Reload sau custom AJAX call
$('#complete-btn').click(function() {
$.post('/task/complete', {id: 42}, function() {
$.pjax.reload({container: '#task-grid-pjax'});
});
});Pjax Options
<?php Pjax::begin([
'id' => 'task-pjax',
'timeout' => 5000, // AJAX timeout tính bằng ms
'enablePushState' => true, // Cập nhật URL bar
'enableReplaceState' => false, // Replace vs push history entry
'clientOptions' => [
'method' => 'GET',
],
]) ?>Pjax Events
// Hiện spinner khi Pjax loading
$('#task-grid-pjax').on('pjax:send', function() {
$(this).addClass('loading');
});
$('#task-grid-pjax').on('pjax:complete', function() {
$(this).removeClass('loading');
});
// Xử lý Pjax errors
$('#task-grid-pjax').on('pjax:error', function(event) {
console.error('Pjax thất bại, fallback về full reload');
// Mặc định: full page reload khi lỗi
});5. Build Custom Widgets
Khi built-in widgets không phù hợp, tạo cái riêng. Widget là reusable component đóng gói rendering logic.
Widget Đơn Giản
// widgets/TaskStats.php
namespace app\widgets;
use yii\base\Widget;
use app\models\Task;
class TaskStats extends Widget
{
/** @var int User ID để hiện stats */
public $userId;
public function run()
{
$base = Task::find()->where(['user_id' => $this->userId]);
$stats = [
'total' => (clone $base)->count(),
'todo' => (clone $base)->andWhere(['status' => Task::STATUS_TODO])->count(),
'in_progress' => (clone $base)->andWhere([
'status' => Task::STATUS_IN_PROGRESS,
])->count(),
'done' => (clone $base)->andWhere(['status' => Task::STATUS_DONE])->count(),
];
return $this->render('task-stats', ['stats' => $stats]);
}
}Widget view:
// widgets/views/task-stats.php
<div class="row text-center">
<div class="col-3">
<div class="card bg-light">
<div class="card-body">
<h3><?= $stats['total'] ?></h3>
<small class="text-muted">Tổng</small>
</div>
</div>
</div>
<div class="col-3">
<div class="card bg-warning bg-opacity-10">
<div class="card-body">
<h3><?= $stats['todo'] ?></h3>
<small class="text-muted">Cần Làm</small>
</div>
</div>
</div>
<div class="col-3">
<div class="card bg-primary bg-opacity-10">
<div class="card-body">
<h3><?= $stats['in_progress'] ?></h3>
<small class="text-muted">Đang Làm</small>
</div>
</div>
</div>
<div class="col-3">
<div class="card bg-success bg-opacity-10">
<div class="card-body">
<h3><?= $stats['done'] ?></h3>
<small class="text-muted">Hoàn Thành</small>
</div>
</div>
</div>
</div>Sử dụng trong bất kỳ view nào:
use app\widgets\TaskStats;
<?= TaskStats::widget(['userId' => Yii::$app->user->id]) ?>Widget Với Begin/End (Bọc Content)
Cho widgets bọc content (như panels hoặc modals):
// widgets/Card.php
namespace app\widgets;
use yii\base\Widget;
use yii\helpers\Html;
class Card extends Widget
{
public $title;
public $footer;
public $options = [];
public function init()
{
parent::init();
$this->options['class'] = 'card ' . ($this->options['class'] ?? '');
echo Html::beginTag('div', $this->options);
echo Html::tag('div',
Html::tag('h5', Html::encode($this->title), ['class' => 'card-title']),
['class' => 'card-header']
);
echo Html::beginTag('div', ['class' => 'card-body']);
ob_start(); // Buffer content giữa begin() và end()
}
public function run()
{
$content = ob_get_clean();
echo $content;
echo Html::endTag('div'); // card-body
if ($this->footer) {
echo Html::tag('div', $this->footer, ['class' => 'card-footer']);
}
echo Html::endTag('div'); // card
}
}Sử dụng:
use app\widgets\Card;
<?php Card::begin(['title' => 'Tổng Quan Tasks', 'options' => ['class' => 'mb-3']]) ?>
<p>Nội dung dashboard tasks ở đây.</p>
<?= TaskStats::widget(['userId' => Yii::$app->user->id]) ?>
<?php Card::end() ?>Widget Với JavaScript (Client-Side Behavior)
// widgets/CountdownTimer.php
namespace app\widgets;
use yii\base\Widget;
use yii\helpers\Html;
use yii\helpers\Json;
class CountdownTimer extends Widget
{
public $targetDate;
public $label = 'Thời gian còn lại';
public function run()
{
$id = $this->getId();
$options = Json::encode([
'targetDate' => $this->targetDate,
]);
// Register JavaScript
$js = <<<JS
(function() {
var options = $options;
var el = document.getElementById('$id');
var target = new Date(options.targetDate).getTime();
function update() {
var now = new Date().getTime();
var diff = target - now;
if (diff <= 0) {
el.querySelector('.countdown-value').textContent = 'Quá hạn!';
el.querySelector('.countdown-value').classList.add('text-danger');
return;
}
var days = Math.floor(diff / 86400000);
var hours = Math.floor((diff % 86400000) / 3600000);
el.querySelector('.countdown-value').textContent = days + ' ngày ' + hours + ' giờ';
}
update();
setInterval(update, 60000);
})();
JS;
$this->view->registerJs($js);
return Html::tag('div',
Html::tag('small', Html::encode($this->label), ['class' => 'text-muted']) .
Html::tag('span', '', ['class' => 'countdown-value fw-bold ms-2']),
['id' => $id, 'class' => 'countdown-timer']
);
}
}<?= CountdownTimer::widget([
'targetDate' => $task->due_date,
'label' => 'Còn:',
]) ?>6. Asset Bundles — Quản Lý CSS & JavaScript
Asset bundles là cách Yii2 quản lý CSS và JavaScript files. Thay vì thủ công thêm <link> và <script> tags, bạn khai báo asset bundles xử lý dependencies, ordering, và publication.
Asset Bundles Hoạt Động Thế Nào
Yii2 resolve dependencies và output <link> với <script> tags theo đúng thứ tự: jQuery trước, rồi Yii core, rồi Bootstrap, rồi app code.
AppAsset Mặc Định
// assets/AppAsset.php
namespace app\assets;
use yii\web\AssetBundle;
class AppAsset extends AssetBundle
{
public $basePath = '@webroot';
public $baseUrl = '@web';
public $css = [
'css/site.css',
];
public $js = [
'js/app.js',
];
public $depends = [
'yii\web\YiiAsset',
'yii\bootstrap5\BootstrapAsset',
];
}Register trong layout:
// views/layouts/main.php
use app\assets\AppAsset;
AppAsset::register($this);Tạo Custom Asset Bundles
Cho task-specific JavaScript module:
// assets/TaskAsset.php
namespace app\assets;
use yii\web\AssetBundle;
class TaskAsset extends AssetBundle
{
public $basePath = '@webroot';
public $baseUrl = '@web';
public $css = [
'css/task.css',
];
public $js = [
'js/task-manager.js',
];
public $depends = [
'app\assets\AppAsset', // load sau AppAsset
];
}Register chỉ trên pages cần:
// views/task/index.php
use app\assets\TaskAsset;
TaskAsset::register($this);Thuộc Tính Asset Bundle
| Thuộc tính | Mô tả | Ví dụ |
|---|---|---|
$basePath | Thư mục local cho published assets | '@webroot' |
$baseUrl | URL prefix cho assets | '@web' |
$sourcePath | Thư mục nguồn (cho assets cần publishing) | '@vendor/bower/lib' |
$css | CSS files cần register | ['css/style.css'] |
$js | JS files cần register | ['js/app.js'] |
$depends | Asset bundles mà bundle này phụ thuộc | ['yii\web\YiiAsset'] |
$jsOptions | Options cho JS tags | ['position' => View::POS_HEAD] |
$cssOptions | Options cho CSS tags | ['media' => 'print'] |
$publishOptions | Options cho asset publishing | ['forceCopy' => YII_DEBUG] |
Register Inline CSS/JS
Cho scripts dùng một lần, register trực tiếp từ views:
// Register inline CSS
$this->registerCss('
.task-overdue { background-color: #fff3f3; }
.task-done { opacity: 0.6; }
');
// Register inline JS ở cuối document (mặc định)
$this->registerJs('
$(".task-row").on("click", function() {
window.location = $(this).data("url");
});
');
// Register JS tại vị trí cụ thể
$this->registerJs('var TASK_API = "/api/v1/tasks";', \yii\web\View::POS_HEAD);Vị Trí JS
| Hằng số | Mô tả |
|---|---|
View::POS_HEAD | Trong <head> |
View::POS_BEGIN | Ngay sau <body> mở |
View::POS_END | Ngay trước </body> (mặc định) |
View::POS_READY | Trong jQuery(document).ready() |
View::POS_LOAD | Trong jQuery(window).on('load') |
CDN Assets
Cho assets từ CDN:
// assets/FontAwesomeAsset.php
namespace app\assets;
use yii\web\AssetBundle;
class FontAwesomeAsset extends AssetBundle
{
public $css = [
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css',
];
// Không cần basePath/baseUrl cho external CDN
public $cssOptions = [
'integrity' => 'sha512-...',
'crossorigin' => 'anonymous',
];
}7. Tối Ưu Assets Cho Production
Combine và Compress Assets
Cấu hình asset manager cho production:
// config/web.php
'components' => [
'assetManager' => [
'appendTimestamp' => true, // Cache busting: style.css?v=1679142000
'bundles' => YII_ENV_PROD ? [
// Override jQuery dùng CDN trong production
'yii\web\JqueryAsset' => [
'sourcePath' => null,
'js' => [
'https://code.jquery.com/jquery-3.7.1.min.js',
],
],
] : [],
],
],Tắt Unused Asset Bundles
Nếu không cần jQuery hoặc Bootstrap globally:
// config/web.php
'components' => [
'assetManager' => [
'bundles' => [
'yii\web\JqueryAsset' => false, // Tắt jQuery
'yii\bootstrap5\BootstrapAsset' => false, // Tắt Bootstrap
],
],
],Asset Compression Với Command Line
Yii2 cung cấp built-in asset compression tool:
# Generate compression config
yii asset/template assets-config.php
# Chạy compression
yii asset assets-config.php config/assets-compressed.phpCombine và minify tất cả CSS/JS thành ít files hơn cho production.
8. Làm Việc Với Bootstrap
Yii2 có official Bootstrap integration qua yiisoft/yii2-bootstrap5.
Cài Đặt
composer require yiisoft/yii2-bootstrap5Bootstrap Widgets
use yii\bootstrap5\Nav;
use yii\bootstrap5\NavBar;
use yii\bootstrap5\Modal;
use yii\bootstrap5\Alert;
use yii\bootstrap5\Progress;
use yii\bootstrap5\Tabs;
// Navigation bar
NavBar::begin([
'brandLabel' => 'Task Manager',
'brandUrl' => Yii::$app->homeUrl,
'options' => ['class' => 'navbar-expand-lg navbar-dark bg-dark'],
]);
echo Nav::widget([
'options' => ['class' => 'navbar-nav'],
'items' => [
['label' => 'Tasks', 'url' => ['/task/index']],
['label' => 'Dashboard', 'url' => ['/site/dashboard']],
['label' => 'Cài đặt', 'items' => [
['label' => 'Hồ sơ', 'url' => ['/user/profile']],
['label' => 'Tùy chọn', 'url' => ['/user/preferences']],
'<div class="dropdown-divider"></div>',
['label' => 'Đăng xuất', 'url' => ['/site/logout'],
'linkOptions' => ['data-method' => 'post']],
]],
],
]);
NavBar::end();Bootstrap Modal Cho Quick Task Creation
use yii\bootstrap5\Modal;
<?php Modal::begin([
'id' => 'create-task-modal',
'title' => 'Tạo Nhanh Task',
'size' => Modal::SIZE_LARGE,
'footer' => Html::button('Đóng', [
'class' => 'btn btn-secondary',
'data-bs-dismiss' => 'modal',
]) . Html::submitButton('Tạo', [
'class' => 'btn btn-primary',
'form' => 'quick-task-form',
]),
]) ?>
<?php $form = ActiveForm::begin(['id' => 'quick-task-form']) ?>
<?= $form->field($model, 'title') ?>
<?= $form->field($model, 'status')->dropDownList(Task::getStatusOptions()) ?>
<?= $form->field($model, 'priority')->dropDownList(Task::getPriorityOptions()) ?>
<?php ActiveForm::end() ?>
<?php Modal::end() ?>
// Nút trigger
<?= Html::button('Task Mới', [
'class' => 'btn btn-primary',
'data-bs-toggle' => 'modal',
'data-bs-target' => '#create-task-modal',
]) ?>Tabs Cho Task Views
use yii\bootstrap5\Tabs;
<?= Tabs::widget([
'items' => [
[
'label' => 'Tất Cả Tasks',
'content' => $this->render('_grid', [
'dataProvider' => $allDataProvider,
]),
'active' => true,
],
[
'label' => 'Tasks Của Tôi',
'content' => $this->render('_grid', [
'dataProvider' => $myDataProvider,
]),
],
[
'label' => 'Quá Hạn',
'content' => $this->render('_grid', [
'dataProvider' => $overdueDataProvider,
]),
'options' => ['class' => 'text-danger'],
],
],
]) ?>9. Tích Hợp Modern JavaScript Frameworks
Yii2 có thể đóng vai trò backend API trong khi Vue.js, React, hoặc bất kỳ SPA nào xử lý frontend. Đây là các patterns phổ biến.
Pattern 1: Yii2 Làm Pure API Backend
Tách hoàn toàn frontend và backend:
project/
├── backend/ # Yii2 application (chỉ REST API)
│ ├── controllers/
│ │ └── api/
│ │ └── v1/
│ │ └── TaskController.php
│ └── config/
│ └── web.php # CORS enabled
├── frontend/ # Vue/React app (project riêng)
│ ├── src/
│ │ ├── App.vue
│ │ └── api/
│ │ └── tasks.js
│ └── package.jsonYii2 backend chỉ phục vụ JSON qua REST controllers (đã cover trong Bài #7). Frontend là standalone SPA build với tooling riêng.
Pattern 2: Yii2 + Embedded Vue/React Components
Giữ Yii2 render pages nhưng dùng Vue/React cho phần interactive:
// assets/VueAsset.php
namespace app\assets;
use yii\web\AssetBundle;
class VueAsset extends AssetBundle
{
public $js = [
'https://unpkg.com/vue@3/dist/vue.global.prod.js',
];
}// views/task/kanban.php
use app\assets\VueAsset;
VueAsset::register($this);
// Truyền data từ PHP sang JavaScript
$tasksJson = \yii\helpers\Json::encode($tasks);
$this->registerJs("window.INITIAL_TASKS = $tasksJson;", \yii\web\View::POS_HEAD);<!-- Yii2-rendered page với Vue component -->
<div id="kanban-app">
<div class="row">
<div class="col-4" v-for="column in columns" :key="column.status">
<h4>{{ column.label }}</h4>
<div v-for="task in column.tasks" :key="task.id"
class="card mb-2" draggable="true"
@dragstart="dragStart($event, task)">
<div class="card-body">
<h6>{{ task.title }}</h6>
<span :class="'badge bg-' + task.priorityColor">
{{ task.priorityLabel }}
</span>
</div>
</div>
</div>
</div>
</div>// web/js/kanban.js
const { createApp, ref, computed } = Vue;
createApp({
setup() {
const tasks = ref(window.INITIAL_TASKS);
const columns = computed(() => [
{ status: 1, label: 'Cần Làm', tasks: tasks.value.filter(t => t.status === 1) },
{ status: 2, label: 'Đang Làm', tasks: tasks.value.filter(t => t.status === 2) },
{ status: 3, label: 'Hoàn Thành', tasks: tasks.value.filter(t => t.status === 3) },
]);
async function updateTaskStatus(taskId, newStatus) {
await fetch(`/api/v1/tasks/${taskId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${window.API_TOKEN}`,
},
body: JSON.stringify({ status: newStatus }),
});
}
return { tasks, columns, updateTaskStatus };
},
}).mount('#kanban-app');Pattern 3: npm Assets Qua Asset Packagist
Cài npm packages qua Composer dùng asset-packagist.org:
// composer.json
{
"repositories": [
{
"type": "composer",
"url": "https://asset-packagist.org"
}
],
"require": {
"npm-asset/vue": "^3.4",
"npm-asset/chart.js": "^4.4"
}
}// assets/ChartJsAsset.php
namespace app\assets;
use yii\web\AssetBundle;
class ChartJsAsset extends AssetBundle
{
public $sourcePath = '@npm/chart.js/dist';
public $js = [
'chart.umd.js',
];
}Truyền Data An Toàn Từ PHP Sang JavaScript
Luôn encode data đúng cách để tránh XSS:
use yii\helpers\Json;
// Trong view
$this->registerJs(
'var taskConfig = ' . Json::htmlEncode([
'apiUrl' => \yii\helpers\Url::to(['/api/v1/tasks'], true),
'csrfToken' => \Yii::$app->request->csrfToken,
'userId' => \Yii::$app->user->id,
]) . ';',
\yii\web\View::POS_HEAD
);Json::htmlEncode() an toàn hơn Json::encode() vì nó escape </script> và các ký tự HTML quan trọng khác.
10. Ghép Tất Cả — Task Manager Dashboard
Đây là dashboard hoàn chỉnh kết hợp tất cả widgets:
// views/site/dashboard.php
use app\widgets\TaskStats;
use app\widgets\Card;
use yii\grid\GridView;
use yii\widgets\ListView;
use yii\widgets\Pjax;
use yii\bootstrap5\Tabs;
use app\assets\TaskAsset;
TaskAsset::register($this);
$this->title = 'Dashboard';
?>
<div class="site-dashboard">
<!-- Tổng quan thống kê -->
<?= TaskStats::widget(['userId' => Yii::$app->user->id]) ?>
<hr class="my-4">
<!-- Task Views với Pjax -->
<?php Pjax::begin(['id' => 'dashboard-pjax']) ?>
<?= Tabs::widget([
'items' => [
[
'label' => 'Tất Cả Tasks',
'content' => GridView::widget([
'dataProvider' => $allProvider,
'filterModel' => $searchModel,
'tableOptions' => ['class' => 'table table-hover'],
'columns' => [
[
'attribute' => 'title',
'format' => 'raw',
'value' => fn($m) => Html::a(
Html::encode($m->title),
['task/view', 'id' => $m->id]
),
],
[
'attribute' => 'status',
'format' => 'raw',
'filter' => Task::getStatusOptions(),
'value' => fn($m) => Html::tag('span',
$m->getStatusLabel(),
['class' => 'badge bg-' . $m->getStatusColor()]
),
],
'priority',
'due_date:date',
['class' => \yii\grid\ActionColumn::class,
'controller' => 'task'],
],
]),
'active' => true,
],
[
'label' => 'Quá Hạn',
'content' => ListView::widget([
'dataProvider' => $overdueProvider,
'itemView' => '/task/_task_card',
'options' => ['class' => 'row mt-3'],
'itemOptions' => ['class' => 'col-md-4 mb-3'],
'emptyText' => 'Không có tasks quá hạn! 🎉',
]),
],
[
'label' => 'Đã Hoàn Thành',
'content' => GridView::widget([
'dataProvider' => $doneProvider,
'columns' => [
'title',
'completed_at:datetime',
],
]),
],
],
]) ?>
<?php Pjax::end() ?>
</div>Dashboard controller:
// controllers/SiteController.php
public function actionDashboard()
{
$userId = \Yii::$app->user->id;
$searchModel = new TaskSearch();
return $this->render('dashboard', [
'searchModel' => $searchModel,
'allProvider' => $searchModel->search(\Yii::$app->request->queryParams),
'overdueProvider' => new ActiveDataProvider([
'query' => Task::find()
->where(['user_id' => $userId])
->andWhere(['<', 'due_date', date('Y-m-d')])
->andWhere(['!=', 'status', Task::STATUS_DONE]),
'sort' => ['defaultOrder' => ['due_date' => SORT_ASC]],
]),
'doneProvider' => new ActiveDataProvider([
'query' => Task::find()
->where(['user_id' => $userId, 'status' => Task::STATUS_DONE])
->orderBy(['completed_at' => SORT_DESC]),
'pagination' => ['pageSize' => 10],
]),
]);
}Bảng Tham Khảo Widget
| Widget | Use Case | Tính năng chính |
|---|---|---|
GridView | Data tables | Sorting, filtering, pagination built-in |
ListView | Card/list layouts | Custom item templates |
DetailView | Single record | Label-value pairs |
Pjax | AJAX updates | Không cần JavaScript |
ActiveForm | Forms | Client + server validation |
Nav / NavBar | Navigation | Dropdown menus, active state |
Modal | Dialogs | Form modals, confirmations |
Tabs | Tabbed content | Nhiều views trong một trang |
Alert | Notifications | Flash messages |
Progress | Progress bars | Animated, stacked bars |
Breadcrumbs | Navigation trail | Tự động generate từ controller |
Tiếp Theo
Task manager giờ đã có complete frontend:
GridViewvới sorting, filtering, và custom columnsListViewcho card-based layoutsDetailViewcho hiển thị single recordPjaxcho AJAX-powered interactions- Custom reusable widgets
- Asset bundles quản lý tất cả CSS/JS dependencies
- Bootstrap 5 integration cho professional UI components
- Con đường rõ ràng tới Vue.js hoặc React integration
Deep Dive → Gii Code Generator & Yii2 Extensions — tự động generate models, CRUD, và controllers với Gii, customize generators, và dùng extension ecosystem.
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.