Yii2 Widgets, Asset Bundles & Frontend Integration

Previous posts focused on the backend — Active Record, authentication, forms, REST APIs. But Yii2 also has a powerful frontend layer: widgets that generate complex HTML from PHP, an asset bundle system that manages CSS/JS dependencies, and Pjax for AJAX-powered page updates without writing JavaScript.
Widgets like GridView can render a full data table with sorting, pagination, filtering, and action buttons — from a single PHP call. ListView does the same for card-based layouts. DetailView displays a single record as a labeled table. And if the built-in widgets don't fit, you can build your own.
By the end of this post, your task manager will have a polished frontend with sortable data tables, card views, AJAX-powered interactions, managed assets, and a clear path to integrating modern JavaScript frameworks.
What You'll Build
✅ GridView for sortable, paginated data tables with action columns
✅ ListView for card-based and custom item layouts
✅ DetailView for single-record display
✅ Pjax for AJAX-powered page updates without full reloads
✅ Building reusable custom widgets
✅ Asset bundles for CSS/JS dependency management
✅ Integrating Bootstrap 5, Vue.js, and React with Yii2
1. GridView — Data Tables Made Easy
GridView is the workhorse of Yii2's frontend. Give it a data provider and it renders a complete HTML table with sorting, pagination, filtering, and action buttons.
Basic GridView
// views/task/index.php
use yii\grid\GridView;
use yii\grid\ActionColumn;
use yii\grid\SerialColumn;
<?= GridView::widget([
'dataProvider' => $dataProvider,
'columns' => [
['class' => SerialColumn::class], // Row numbers: 1, 2, 3...
'title',
'description:ntext', // format as plain text (HTML escaped)
'status',
'priority',
'due_date:date', // format as date
['class' => ActionColumn::class], // View, Update, Delete buttons
],
]) ?>The controller provides the 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,
]);
}This renders a full-featured data table — column headers are clickable for sorting, pagination appears at the bottom, and each row has view/update/delete links.
Custom Columns
Override column rendering for richer display:
<?= GridView::widget([
'dataProvider' => $dataProvider,
'tableOptions' => ['class' => 'table table-striped table-bordered'],
'columns' => [
['class' => SerialColumn::class],
[
'attribute' => 'title',
'format' => 'raw', // allow HTML
'value' => function ($model) {
return Html::a(
Html::encode($model->title),
['view', 'id' => $model->id],
['class' => 'fw-bold']
);
},
],
// Status as 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 with stars
[
'attribute' => 'priority',
'format' => 'raw',
'value' => function ($model) {
return str_repeat('★', $model->priority)
. str_repeat('☆', 3 - $model->priority);
},
],
// Due date with overdue highlighting
[
'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' => 'Mark Complete',
'data-method' => 'post',
]);
},
],
],
],
]) ?>GridView with Filtering (SearchModel)
For column-level filters, create a 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;
}
// Apply 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 with filter row:
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel, // Enables 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],
],
]) ?>Now each column has a text input or dropdown for filtering. Type in a column header and the table filters instantly.
2. ListView — Card-Based Layouts
ListView renders each item using a custom view partial — perfect for card layouts, blog post lists, or any non-tabular display.
Basic ListView
// views/task/cards.php
use yii\widgets\ListView;
<?= ListView::widget([
'dataProvider' => $dataProvider,
'itemView' => '_task_card', // renders views/task/_task_card.php per item
'layout' => '{summary}{items}{pager}',
'itemOptions' => ['class' => 'col-md-4 mb-3'],
'options' => ['class' => 'row'],
'emptyText' => 'No tasks found.',
]) ?>The 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">
Due: <?= Yii::$app->formatter->asDate($model->due_date) ?>
</small>
</div>
</div>
<div class="card-footer">
<?= Html::a('Edit', ['update', 'id' => $model->id],
['class' => 'btn btn-sm btn-outline-primary']) ?>
<?= Html::a('Delete', ['delete', 'id' => $model->id], [
'class' => 'btn btn-sm btn-outline-danger',
'data-method' => 'post',
'data-confirm' => 'Are you sure?',
]) ?>
</div>
</div>Variables Available in Item View
The item partial receives these variables automatically:
| Variable | Type | Description |
|---|---|---|
$model | ActiveRecord | The current data model |
$key | mixed | The primary key value |
$index | int | Zero-based index of the item |
$widget | ListView | The ListView widget instance |
ListView with Custom Summary
<?= ListView::widget([
'dataProvider' => $dataProvider,
'itemView' => '_task_card',
'summary' => 'Showing {begin}-{end} of {totalCount} tasks',
'pager' => [
'class' => \yii\widgets\LinkPager::class,
'options' => ['class' => 'pagination justify-content-center'],
'linkOptions' => ['class' => 'page-link'],
],
]) ?>Switching Between Grid and List View
A common pattern — let users toggle between table and card views:
// views/task/index.php
<?php
$viewMode = Yii::$app->request->get('view', 'grid');
?>
<div class="btn-group mb-3">
<?= Html::a('Table', ['index', 'view' => 'grid'],
['class' => 'btn btn-sm btn-' . ($viewMode === 'grid' ? 'primary' : 'outline-primary')]) ?>
<?= Html::a('Cards', ['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 — Single Record Display
DetailView displays a single model as a table of label-value pairs.
// views/task/view.php
use yii\widgets\DetailView;
<?= DetailView::widget([
'model' => $model,
'attributes' => [
'id',
'title',
'description:ntext', // format as 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' => 'Assigned To',
'value' => $model->user->username ?? 'Unassigned',
],
'created_at:datetime',
'updated_at:datetime',
],
]) ?>Output is a clean two-column table:
| Label | Value |
|---|---|
| ID | 42 |
| Title | Deploy REST API |
| Status | In Progress (badge) |
| Priority | ★★★ |
| Due Date | Mar 25, 2026 |
| Assigned To | chanh |
Format Shortcuts
Yii2 supports format shorthand in attribute:format:
| Format | Example | Output |
|---|---|---|
text | 'title:text' | HTML-escaped text |
ntext | 'description:ntext' | Multi-line text (nl2br) |
html | 'content:html' | Raw HTML (use cautiously) |
date | 'due_date:date' | Formatted date |
datetime | 'created_at:datetime' | Formatted date + time |
email | 'email:email' | Clickable mailto link |
url | 'website:url' | Clickable link |
boolean | 'is_active:boolean' | Yes/No |
integer | 'count:integer' | Number with grouping |
4. Pjax — AJAX Without Writing JavaScript
Pjax (pushState + AJAX) updates part of a page without a full reload. Wrap any widget in Pjax and sorting, pagination, and filtering happen via AJAX automatically.
Wrap GridView with 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() ?>Now clicking sort headers, pagination links, or typing in filter fields updates only the grid — no full page reload. The URL in the address bar updates via pushState, so bookmarks and the back button still work.
Pjax with Forms
Submit forms via AJAX and update only the Pjax container:
<?php Pjax::begin(['id' => 'task-create-pjax']) ?>
<?php $form = ActiveForm::begin([
'options' => ['data-pjax' => true], // Submit via Pjax
]) ?>
<?= $form->field($model, 'title') ?>
<?= $form->field($model, 'status')->dropDownList(Task::getStatusOptions()) ?>
<?= Html::submitButton('Create', ['class' => 'btn btn-primary']) ?>
<?php ActiveForm::end() ?>
<?php Pjax::end() ?>Triggering Pjax Reload from JavaScript
// Reload a specific Pjax container
$.pjax.reload({container: '#task-grid-pjax'});
// Reload after a 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 in ms
'enablePushState' => true, // Update URL bar
'enableReplaceState' => false, // Replace vs push history entry
'clientOptions' => [
'method' => 'GET',
],
]) ?>Pjax Events
// Show spinner during Pjax loading
$('#task-grid-pjax').on('pjax:send', function() {
$(this).addClass('loading');
});
$('#task-grid-pjax').on('pjax:complete', function() {
$(this).removeClass('loading');
});
// Handle Pjax errors
$('#task-grid-pjax').on('pjax:error', function(event) {
console.error('Pjax failed, falling back to full reload');
// Default: full page reload on error
});5. Building Custom Widgets
When built-in widgets don't fit your needs, create your own. A widget is a reusable component that encapsulates rendering logic.
Simple Widget
// widgets/TaskStats.php
namespace app\widgets;
use yii\base\Widget;
use app\models\Task;
class TaskStats extends Widget
{
/** @var int User ID to show stats for */
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]);
}
}The 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">Total</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">To Do</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">In Progress</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">Done</small>
</div>
</div>
</div>
</div>Usage in any view:
use app\widgets\TaskStats;
<?= TaskStats::widget(['userId' => Yii::$app->user->id]) ?>Widget with Begin/End (Content Wrapping)
For widgets that wrap content (like panels or 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 between begin() and 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
}
}Usage:
use app\widgets\Card;
<?php Card::begin(['title' => 'Task Summary', 'options' => ['class' => 'mb-3']]) ?>
<p>Your task dashboard content goes here.</p>
<?= TaskStats::widget(['userId' => Yii::$app->user->id]) ?>
<?php Card::end() ?>Widget with 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 = 'Time remaining';
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 = 'Overdue!';
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 + 'd ' + hours + 'h';
}
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' => 'Due in:',
]) ?>6. Asset Bundles — Managing CSS & JavaScript
Asset bundles are Yii2's way of managing CSS and JavaScript files. Instead of manually adding <link> and <script> tags, you declare asset bundles that handle dependencies, ordering, and publication.
How Asset Bundles Work
Yii2 resolves dependencies and outputs <link> and <script> tags in the correct order: jQuery first, then Yii core, then Bootstrap, then your app code.
The Default AppAsset
// 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 it in your layout:
// views/layouts/main.php
use app\assets\AppAsset;
AppAsset::register($this);Creating Custom Asset Bundles
For a 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', // loads after AppAsset
];
}Register only on pages that need it:
// views/task/index.php
use app\assets\TaskAsset;
TaskAsset::register($this);Asset Bundle Properties
| Property | Description | Example |
|---|---|---|
$basePath | Local directory for published assets | '@webroot' |
$baseUrl | URL prefix for assets | '@web' |
$sourcePath | Source directory (for assets needing publishing) | '@vendor/bower/lib' |
$css | CSS files to register | ['css/style.css'] |
$js | JS files to register | ['js/app.js'] |
$depends | Asset bundles this depends on | ['yii\web\YiiAsset'] |
$jsOptions | Options for JS tags | ['position' => View::POS_HEAD] |
$cssOptions | Options for CSS tags | ['media' => 'print'] |
$publishOptions | Options for asset publishing | ['forceCopy' => YII_DEBUG] |
Registering Inline CSS/JS
For one-off scripts, register them directly from views:
// Register inline CSS
$this->registerCss('
.task-overdue { background-color: #fff3f3; }
.task-done { opacity: 0.6; }
');
// Register inline JS at document end (default)
$this->registerJs('
$(".task-row").on("click", function() {
window.location = $(this).data("url");
});
');
// Register JS at specific position
$this->registerJs('var TASK_API = "/api/v1/tasks";', \yii\web\View::POS_HEAD);JS Positions
| Constant | Description |
|---|---|
View::POS_HEAD | Inside <head> |
View::POS_BEGIN | Right after <body> opens |
View::POS_END | Right before </body> (default) |
View::POS_READY | Inside jQuery(document).ready() |
View::POS_LOAD | Inside jQuery(window).on('load') |
CDN Assets
For assets served from a 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',
];
// No basePath/baseUrl needed for external CDN
public $cssOptions = [
'integrity' => 'sha512-...',
'crossorigin' => 'anonymous',
];
}7. Asset Optimization for Production
Combining and Compressing Assets
Configure asset manager for production:
// config/web.php
'components' => [
'assetManager' => [
'appendTimestamp' => true, // Cache busting: style.css?v=1679142000
'bundles' => YII_ENV_PROD ? [
// Override jQuery to use CDN in production
'yii\web\JqueryAsset' => [
'sourcePath' => null,
'js' => [
'https://code.jquery.com/jquery-3.7.1.min.js',
],
],
] : [],
],
],Disabling Unused Asset Bundles
If you don't need jQuery or Bootstrap globally:
// config/web.php
'components' => [
'assetManager' => [
'bundles' => [
'yii\web\JqueryAsset' => false, // Disable jQuery
'yii\bootstrap5\BootstrapAsset' => false, // Disable Bootstrap
],
],
],Asset Compression with Command Line
Yii2 provides a built-in asset compression tool:
# Generate compression config
yii asset/template assets-config.php
# Run compression
yii asset assets-config.php config/assets-compressed.phpThis combines and minifies all CSS/JS into fewer files for production.
8. Working with Bootstrap
Yii2 has official Bootstrap integration via yiisoft/yii2-bootstrap5.
Installation
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' => 'Settings', 'items' => [
['label' => 'Profile', 'url' => ['/user/profile']],
['label' => 'Preferences', 'url' => ['/user/preferences']],
'<div class="dropdown-divider"></div>',
['label' => 'Logout', 'url' => ['/site/logout'],
'linkOptions' => ['data-method' => 'post']],
]],
],
]);
NavBar::end();Bootstrap Modal for Quick Task Creation
use yii\bootstrap5\Modal;
<?php Modal::begin([
'id' => 'create-task-modal',
'title' => 'Quick Create Task',
'size' => Modal::SIZE_LARGE,
'footer' => Html::button('Close', [
'class' => 'btn btn-secondary',
'data-bs-dismiss' => 'modal',
]) . Html::submitButton('Create', [
'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() ?>
// Trigger button
<?= Html::button('New Task', [
'class' => 'btn btn-primary',
'data-bs-toggle' => 'modal',
'data-bs-target' => '#create-task-modal',
]) ?>Tabs for Task Views
use yii\bootstrap5\Tabs;
<?= Tabs::widget([
'items' => [
[
'label' => 'All Tasks',
'content' => $this->render('_grid', [
'dataProvider' => $allDataProvider,
]),
'active' => true,
],
[
'label' => 'My Tasks',
'content' => $this->render('_grid', [
'dataProvider' => $myDataProvider,
]),
],
[
'label' => 'Overdue',
'content' => $this->render('_grid', [
'dataProvider' => $overdueDataProvider,
]),
'options' => ['class' => 'text-danger'],
],
],
]) ?>9. Integrating Modern JavaScript Frameworks
Yii2 can serve as a backend API while Vue.js, React, or any SPA handles the frontend. Here are the common patterns.
Pattern 1: Yii2 as Pure API Backend
Separate frontend and backend completely:
project/
├── backend/ # Yii2 application (REST API only)
│ ├── controllers/
│ │ └── api/
│ │ └── v1/
│ │ └── TaskController.php
│ └── config/
│ └── web.php # CORS enabled
├── frontend/ # Vue/React app (separate project)
│ ├── src/
│ │ ├── App.vue
│ │ └── api/
│ │ └── tasks.js
│ └── package.jsonThe Yii2 backend serves only JSON via REST controllers (covered in Post #7). The frontend is a standalone SPA built with its own tooling.
Pattern 2: Yii2 + Embedded Vue/React Components
Keep Yii2 rendering pages but use Vue/React for interactive parts:
// 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);
// Pass data from PHP to JavaScript
$tasksJson = \yii\helpers\Json::encode($tasks);
$this->registerJs("window.INITIAL_TASKS = $tasksJson;", \yii\web\View::POS_HEAD);<!-- Yii2-rendered page with 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: 'To Do', tasks: tasks.value.filter(t => t.status === 1) },
{ status: 2, label: 'In Progress', tasks: tasks.value.filter(t => t.status === 2) },
{ status: 3, label: 'Done', 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 via Asset Packagist
Install npm packages through Composer using 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',
];
}Passing Data Safely from PHP to JavaScript
Always encode data properly to prevent XSS:
use yii\helpers\Json;
// In your 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() is safer than Json::encode() because it escapes </script> and other HTML-significant characters.
10. Putting It All Together — Task Manager Dashboard
Here's a complete dashboard combining all the 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">
<!-- Stats Overview -->
<?= TaskStats::widget(['userId' => Yii::$app->user->id]) ?>
<hr class="my-4">
<!-- Task Views with Pjax -->
<?php Pjax::begin(['id' => 'dashboard-pjax']) ?>
<?= Tabs::widget([
'items' => [
[
'label' => 'All 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' => 'Overdue',
'content' => ListView::widget([
'dataProvider' => $overdueProvider,
'itemView' => '/task/_task_card',
'options' => ['class' => 'row mt-3'],
'itemOptions' => ['class' => 'col-md-4 mb-3'],
'emptyText' => 'No overdue tasks! 🎉',
]),
],
[
'label' => 'Completed',
'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],
]),
]);
}Widget Quick Reference
| Widget | Use Case | Key Feature |
|---|---|---|
GridView | Data tables | Sorting, filtering, pagination built-in |
ListView | Card/list layouts | Custom item templates |
DetailView | Single record | Label-value pairs |
Pjax | AJAX updates | No JavaScript needed |
ActiveForm | Forms | Client + server validation |
Nav / NavBar | Navigation | Dropdown menus, active state |
Modal | Dialogs | Form modals, confirmations |
Tabs | Tabbed content | Multiple views in one page |
Alert | Notifications | Flash messages |
Progress | Progress bars | Animated, stacked bars |
Breadcrumbs | Navigation trail | Auto-generated from controller |
What's Next
Your task manager now has a complete frontend:
GridViewwith sorting, filtering, and custom columnsListViewfor card-based layoutsDetailViewfor single record displayPjaxfor AJAX-powered interactions- Custom reusable widgets
- Asset bundles managing all CSS/JS dependencies
- Bootstrap 5 integration for professional UI components
- A clear path to Vue.js or React integration
Deep Dive → Gii Code Generator & Yii2 Extensions — auto-generate models, CRUD, and controllers with Gii, customize generators, and use the extension ecosystem.
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.