Back to blog

Yii2 Widgets, Asset Bundles & Frontend Integration

yii2phpfrontendwidgetsbackend
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ếnKiểuMô tả
$modelActiveRecordData model hiện tại
$keymixedGiá trị primary key
$indexintIndex zero-based của item
$widgetListViewListView 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ẽ:

LabelValue
ID42
TitleDeploy REST API
StatusIn Progress (badge)
Priority★★★
Due Date25/03/2026
Người thực hiệnchanh

Format Shortcuts

Yii2 hỗ trợ format shorthand trong attribute:format:

FormatVí 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ínhMô tảVí dụ
$basePathThư mục local cho published assets'@webroot'
$baseUrlURL prefix cho assets'@web'
$sourcePathThư mục nguồn (cho assets cần publishing)'@vendor/bower/lib'
$cssCSS files cần register['css/style.css']
$jsJS files cần register['js/app.js']
$dependsAsset bundles mà bundle này phụ thuộc['yii\web\YiiAsset']
$jsOptionsOptions cho JS tags['position' => View::POS_HEAD]
$cssOptionsOptions cho CSS tags['media' => 'print']
$publishOptionsOptions 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_HEADTrong <head>
View::POS_BEGINNgay sau <body> mở
View::POS_ENDNgay trước </body> (mặc định)
View::POS_READYTrong jQuery(document).ready()
View::POS_LOADTrong 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.php

Combine 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-bootstrap5

Bootstrap 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.json

Yii2 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

WidgetUse CaseTính năng chính
GridViewData tablesSorting, filtering, pagination built-in
ListViewCard/list layoutsCustom item templates
DetailViewSingle recordLabel-value pairs
PjaxAJAX updatesKhông cần JavaScript
ActiveFormFormsClient + server validation
Nav / NavBarNavigationDropdown menus, active state
ModalDialogsForm modals, confirmations
TabsTabbed contentNhiều views trong một trang
AlertNotificationsFlash messages
ProgressProgress barsAnimated, stacked bars
BreadcrumbsNavigation trailTự động generate từ controller

Tiếp Theo

Task manager giờ đã có complete frontend:

  • GridView với sorting, filtering, và custom columns
  • ListView cho card-based layouts
  • DetailView cho hiển thị single record
  • Pjax cho 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.

← RESTful API Development | → Tổng Quan Series

📬 Đă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.