Pjax not functioning as expected with Modal

It’s rare that I post a request, but this problem has defeated me up to now.

I have a GridView, generated by Gii, on an index page with pjax enabled. The create, update and view (there is no delete) views are in a Modal: \yii\bootstrap5\Modal;. They work as expected until a filter, sort or javascript $(".grid-view").yiiGridView("applyFilter"); “refresh” operation. At which point these views will not open in the modal but “full page”.

Over approximately 8 hours, I have tried multiple fixes using enablePushState true and false, enableReplaceState true and false, timeout=false, timeout=5000 and other values, data-pjax = 0 on different controls and all the various different permutations of these (there are a lot). I have also tried all the fixes I can find (there are quite a few) on this forum and StackOverFlow. None work.

Steps to recreate:

  1. Using a database table, with Gii, create a Model and CRUD with enable Pjax selected
  2. Add a Modal “uses” to the index.php view: use \yii\bootstrap5\Modal;
  3. Add the following code above the GridView code:

  <?php
  Modal::begin([
    'title' => '<h1 class="text-center">' . \Yii::t('app', 'User Information') . '</h1>',
    'id' => 'modal',
    'size' => 'modal-md',
    'scrollable' => true,
    'toggleButton' => [
      'label' => \Yii::t('app', 'Create New User'),
      'class' => 'btn btn-success mb-3 modalButton',
      'value' => Url::to(['create']), //  The "view" file
    ],
    'dialogOptions' => [
      'aria-labelledby' => 'staticBackdropLabel',
    ],
    'clientOptions' => [
      'backdrop' => true,
      'data-bs-backdrop' => 'static',
      'data-bs-keyboard' => false,
    ],
  ]);

  echo '<div id="modalContent"></div>';

  Modal::end();
  ?>

  1. In actionCreate, actionUpdate and actionView change the return $this->render(...); statement to return $this->renderAjax(...);

Open in a Browser (I’ve tried Chromium, Firefox, Opera) and the modal dialog works just fine until, in the GridView, a sort or filter is performed.

I can live without Pjax but it would be nice to have for the user experience.

The page code for index.php is as follows:


<?php

use \app\models\UserInfo;
use \app\components\VAGlobals;
use \yii\bootstrap5\LinkPager;
use \yii\bootstrap5\Html;
use \yii\bootstrap5\Modal;
use \yii\helpers\Url;
use \yii\grid\ActionColumn;
use \yii\grid\GridView;

/** @var \yii\web\View $this */
/** @var \app\models\search\UserInfoSearch $searchModel */
/** @var \yii\data\ActiveDataProvider $dataProvider */
/** @var \app\components\VAGlobals */
$this->title = \Yii::t('app', 'User Information');
$this->params['breadcrumbs'][] = $this->title;
// Pass the Meta_Tags to the layout file
$this->params['meta_description'] = ucwords(Html::encode($this->title));
$this->params['meta_keywords'] = strtolower(Html::encode($this->title));
$this->params['meta_robots'] = strtolower('follow, noindex');
?>

<div class="user-info-index">

  <h1 class="text-center"><?= Html::encode($this->title) ?></h1>

  <!--Modal-->
  <?php
  Modal::begin([
    'title' => '<h1 class="text-center">' . \Yii::t('app', 'User Information') . '</h1>',
    'id' => 'modal',
    'size' => 'modal-md',
    'scrollable' => true,
    'toggleButton' => [
      'label' => \Yii::t('app', 'Create New User'),
      'class' => 'btn btn-success mb-3 modalButton',
      'value' => Url::to(['create']), //  The "view" file
    ],
    'dialogOptions' => [
      'aria-labelledby' => 'staticBackdropLabel',
    ],
    'clientOptions' => [
      'backdrop' => true,
      'data-bs-backdrop' => 'static',
      'data-bs-keyboard' => false,
    ],
  ]);

  echo '<div id="modalContent"></div>';

  Modal::end();
  ?>
  <?php // echo $this->render('_search', ['model' => $searchModel]);  ?>
  <?php \yii\widgets\Pjax::begin(['id' => 'userInfoPjax', 'timeout' => 5000]) ?>
  <?=
  GridView::widget([
    'dataProvider' => $dataProvider,
    'filterModel' => $searchModel,
    'layout' => '{summary}{items}', // Providing a Link Page below
    'columns' => [
      [
        'attribute' => 'id',
        'contentOptions' => [
          'style' => 'width: 60px; white-space: nowrap',
        ],
      ],
      [
        'attribute' => 'username',
        'contentOptions' => [
          'class' => ['fw-medium'],
        ],
      ],
      'first_name',
      'last_name',
      'email:email',
      'company_id',
      [
        'attribute' => 'status',
        'format' => 'html',
        'value' => function ($data)
        {
          return VAGlobals::getStatusBadge($data->status);
        },
        'filter' => $statusItems,
        'contentOptions' => ['style' => 'width: 70px'],
      ],
      [
        'attribute' => 'role',
        'value' => function ($data)
        {
          return VAGlobals::getRoleName($data->role);
        },
        'filter' => $roleItems,
        'contentOptions' => [
          'style' => 'width: 60px; white-space: nowrap',
        ],
      ],
      [
        'class' => ActionColumn::className(),
        'template' => '{view} {update}', // {delete} has been removed
        'contentOptions' => ['class' => ['text-center'], 'style' => 'width: 70px; white-space: nowrap; '],
        'buttons' => [
          'update' => function ($action, UserInfo $model)
          {
            return Html::a(VAGlobals::UPDATE_BUTTON, $action, [
              'id' => 'update' . $model->id,
              'value' => $action,
              'class' => 'modalButton',
              'data-bs-toggle' => 'modal',
              'data-bs-target' => '#modal',
            ]);
          },
          'view' => function ($action, UserInfo $model)
          {
            return Html::a(VAGlobals::VIEW_BUTTON, $action, [
              'id' => 'view' . $model->id,
              'value' => $action,
              'class' => 'modalButton',
              'data-bs-toggle' => 'modal',
              'data-bs-target' => '#modal',
            ]);
          },
        ],
      ],
    ],
  ]);
  ?>
  <?php \yii\widgets\Pjax::end() ?>


  <?= LinkPager::widget(['pagination' => $dataProvider->pagination]) ?>


</div>

The page code for view.php (the other two pages are similar but with a _form attached) is as follows:


<?php

use \yii\bootstrap5\Html;
use \yii\widgets\DetailView;
use \app\components\VAGlobals;

/** @var \yii\web\View $this */
/** @var \app\models\UserInfo $model */
/** @var \app\components\VAGlobals  */
$this->title = 'User: ' . $model->username;

\yii\web\YiiAsset::register($this);
?>
<div class="user-info-view">

  <div class="card border-success">
    <div class="card-header">
      <h4 class="text-center"><?= Html::encode($this->title) ?></h4>
    </div>
    <div class="card-body">

      <?=
      DetailView::widget([
        'model' => $model,
        'attributes' => [
          'id',
          'username',
          'first_name',
          'last_name',
          'email:email',
          'company_id',
          [
            'attribute' => 'status',
            'format' => 'html',
            'value' => VAGlobals::getStatusBadge($model->status),
          ],
          [
            'attribute' => 'role',
            'value' => VAGlobals::getRoleName($model->role),
          ],
          'created_at:datetime',
          [
            'attribute' => 'created_by',
            'label' => \Yii::t('app', 'Created By'),
            'value' => VAGlobals::getCreatedUpdated($model->created_by),
          ],
          'updated_at:datetime',
          [
            'attribute' => 'updated_by',
            'label' => \Yii::t('app', 'Updated By'),
            'value' => VAGlobals::getCreatedUpdated($model->updated_by),
          ],
        ],
      ])
      ?>

      <div class="text-end">
        <?= Html::Button(\Yii::t('app', 'Close'), ['class' => 'btn btn-secondary', 'data-bs-dismiss' => 'modal']) ?>
      </div>
    </div>
  </div>

Any help would be appreciated as I have exhausted this forum, other forums, stackexchange etc. and have not found a work-around…

Thank you.

The modal for ‘create’ and ‘update’ is located outside of the Pjax widget for Gridview, isn’t it?

Then you have to tell the Pjax widget that the forms for ‘create’ and ‘update’ should trigger Pjax requests by specifying formSelector property of the Pjax widget.

And, I’m not very sure, but it’s worth trying renderPartial instead of renderAjax, since the modal could be initialized (and closed) by the javascript sent by renderAjax.

Many thanks for the suggestions.

I have tried the Pjax widget in all parts of the page (create has been tried inside or outside the Pjax widget, update is always inside). Note, I’ve tried create both in the modal’s toggleButton and in its own separate button

In some places the “corruptions” are much worse than others, especially in relation to the LinkPager widget (when the modal dialog never works).

I have even tried JavaScript. One example, of very many, is shown below:


// Register JS code to rebind events after Pjax update
$this->registerJs(
    "
    $(document).on('pjax:success', function() {
        // Rebind events here
        $('.modalButton').on('click', function(e) {
            e.preventDefault();
            var url = $(this).attr('href');
            // Open modal dialog using AJAX
            $('#modal').load(url);
        });
    });
    ",
    \yii\web\View::POS_READY
);

What I haven’t tried are your suggestions re FormSelector and renderPartial. Although, I believe that the use of renderPartial is discouraged, I will still try it.

I have the complete, original JavaScript code for Pjax (not just the code bundled with the widget) and I am currently working my way through it to see if I can find the problem.

One of the reasons I am spending so much time with this problem is that, although Pjax isn’t really needed (it’s a nice to have) on this view. It, or Ajax, is an absolute necessity on some of the other, yet to be written, views.

I will try your suggestions and report back.

Thank you.

Hmm,

I wonder what’s wrong with the LinkPager of the grid.
It usually calls back the same page with an additional “page” parameter set, for example, “index?page=1”. And it will be handled by the pagination object of the data provider.

At what point did it begin to mulfunction? When you have added a modal?

Anyway, debugging pjax could be very time consuming task. Give yourself enough time, and good luck!

LinkPager has always done it (once you change a page) if it is included in the Pjax wrapper. The Dialog just doesn’t work. If it is called outside the Pjax widget wrapper, it works fine (usually - there are one or two switches it doesn’t like).

I think the whole Pjax problem has something to do with how the Url is push changed. Though, it also doesn’t work as expected with pure Ajax… This is based on “circumstantial evidence” and needs a great deal more investigation. Hence the combing through the original code. As soon as Pjax and Ajax refresh is removed from the page, everything works as expected.

I’ve tried all sorts of JavaScript (Ajax) workarounds (about 4 hours of Saturday morning!) but to no avail.

I’ve allocated most of Monday to trying to find a fix.

I have done some preliminary testing with renderPartial and formSelector. There is still a lot more testing to be done but, when 'formSelector' => false is set in the Pjax wdget, the Modal Dialogs are functioning correctly in my preliminary tests. This is strange as, according to my understanding of the documentation, 'data-pjax' => 0 on a specific control is supposed to do the same thing.

/**
 * @var string|null|false the jQuery selector of the forms whose submissions should trigger pjax requests.
 * If not set, all forms with `data-pjax` attribute within the enclosed content of Pjax will trigger pjax requests. ← [MY NOTE] my understanding is `'data-pjax' => 0` switches this off...
 * If set to false, no code will be registered to handle forms.
 * Note that if the response to the pjax request is a full page, a normal request will be sent again.
 */

There is no change when renderPartial(...) is used. It works correctly when 'formSelector' => false is used and doesn’t work when formSelector is omitted.

I will now run (a lot) more tests and see if this performs as expected in all scenarios.

Thank you for your suggestions. They are very helpful.

1 Like

when 'formSelector' => false is set in the Pjax wdget, the Modal Dialogs are functioning correctly in my preliminary tests. This is strange as, according to my understanding of the documentation, 'data-pjax' => 0 on a specific control is supposed to do the same thing.

Yeah, I agree with you. When a form or link is enclosed in a Pjax widget and it is marked as ‘data-pjax = 0’, then then it should result in a whole page redraw.

Just for clarification. Do you wrap the modal with the same Pjax widget that wraps the Gridview, or with another one?

As far as I understand, you are to wrap the Gridview and the Modal in a single Pjax widget, or wrap only the gridview and set formSelector something like #update-form, #create-form to handle the form submission.

If the Modal is wrapped in the same Pjax as the GridView it is guaranteed not to work correctly.

So far, the best results are with a single Pjax widget wrapping just the Gridview widget and nothing else.

If you set 'formSelector' => false the Pjax JavaScript code is still included with the page but it is only looking for a form which has ['data' => ['pjax' => true]] set. If both 'formSelector' => false and 'linkSelector' => false are set then there is no JavaScript code on the page. I thought it was still supposed to check for a form with ['data' => ['pjax' => true]] when 'formSelector' => false is set.

Anyhow, reading a lot further into the code etc. I am of the opinion that Pjax is probably not the way forward and nor is the Modal Widget. I am now looking at writing my own widgets which will play well together using the HTML5 elements as well as jQuery. This may or may not work but, I have the time to experiment.

If I find a working solution, I’ll post a Wiki.

1 Like

This code currently works but you can’t check for exterior changes using Ajax without page corruption and other bugs:


<?php

use \app\models\UserInfo;
use \app\components\VAGlobals;
use \yii\bootstrap5\LinkPager;
use \yii\bootstrap5\Html;
use \yii\bootstrap5\Modal;
use \yii\helpers\Url;
use \yii\grid\ActionColumn;
use \yii\grid\GridView;

/** @var \yii\web\View $this */
/** @var \app\models\search\UserInfoSearch $searchModel */
/** @var \yii\data\ActiveDataProvider $dataProvider */
/** @var \app\components\VAGlobals */
$this->title = \Yii::t('app', 'User Information');
$this->params['breadcrumbs'][] = $this->title;
// Pass the Meta_Tags to the layout file
$this->params['meta_description'] = ucwords(Html::encode($this->title));
$this->params['meta_keywords'] = strtolower(Html::encode($this->title));
$this->params['meta_robots'] = strtolower('follow, noindex');
//
// RefreshGridView avery 10 seconds (closes the Modal Dialog!)
//$this->registerJs('
//    setInterval(function(){
////         $.pjax.reload({container:"#pjax-container"});  // Doesn\'t work
//         $(".grid-view").yiiGridView("applyFilter"); // sometimes doesn\'t work
//    }, 10000);', \yii\web\VIEW::POS_HEAD);
?>

<div class="user-info-index">

  <h1 class="text-center"><?= Html::encode($this->title) ?></h1>

  <!--Modal-->
  <?php
  Modal::begin([
    'title' => '<h1 class="text-center">' . \Yii::t('app', 'User Information') . '</h1>',
    'id' => 'modal',
    'size' => 'modal-md',
    'scrollable' => true,
    'toggleButton' => [
      'label' => \Yii::t('app', 'Create New User'),
      'class' => 'btn btn-success mb-3 modalButton',
      'value' => Url::to(['create']), //  The "view" file
      'data-pjax' => 0,
    ],
    'dialogOptions' => [
      'aria-labelledby' => 'staticBackdropLabel',
    ],
    'clientOptions' => [
      'backdrop' => 'static',
      'keyboard' => false,
      'data-bs-backdrop' => 'static',
      'data-bs-keyboard' => false,
    ],
  ]);

  echo '<div id="modalContent"></div>';

  Modal::end();
  ?>
  <?php // echo $this->render('_search', ['model' => $searchModel]);  ?>
  <!--//  Pjax doesn't play nice with Modal Dialogs so 'formSelector' set to false.-->
  <?php \yii\widgets\Pjax::begin(['id' => 'pjax-container', 'timeout' => 5000, 'formSelector' => false, /* 'linkSelector' => false */]) ?>

  <?=
  GridView::widget([
    'dataProvider' => $dataProvider,
    'filterModel' => $searchModel,
    'layout' => '{summary}{items}', // Providing a Link Page below
    'columns' => [ // columns here
      [
        'class' => ActionColumn::className(),
        'template' => '{view} {update}', // {delete} has been removed
        'contentOptions' => ['class' => ['text-center'], 'style' => 'width: 70px; white-space: nowrap; '],
        'buttons' => [
          'update' => function ($action, UserInfo $model)
          {
            return Html::a(VAGlobals::UPDATE_BUTTON, $action, [
              'id' => 'update' . $model->id,
              'value' => $action,
              'class' => 'modalButton',
              'data-bs-toggle' => 'modal',
              'data-bs-target' => '#modal',
              'data-pjax' => 0,
            ]);
          },
          'view' => function ($action, UserInfo $model)
          {
            return Html::a(VAGlobals::VIEW_BUTTON, $action, [
              'id' => 'view' . $model->id,
              'value' => $action,
              'class' => 'modalButton',
              'data-bs-toggle' => 'modal',
              'data-bs-target' => '#modal',
              'data-pjax' => 0,
            ]);
          },
        ],
      ],
    ],
  ]);
  ?>
  <?php \yii\widgets\Pjax::end() ?>

  <?= LinkPager::widget(['pagination' => $dataProvider->pagination, 'options' => ['data-pjax' => 0,],]) ?>

</div>

Ive just noticed that your LinkPager has been outside of the Pjax widget from the beginning.
I’m sorry, I overlooked it.

But then, it’s an expected result that LinkPager breaks Pjax and causes whole page redraw.
Would you please try it inside the Pjax widget?

And just for clarification, data-pjax = 0 won’t have any effect outside the Pjax widget. It is meant for the forms or links inside the widget.

I have tried Pjax in all the different positions on the page. The only place it works correctly (excluding external JavaScripts like but not limited to: $(".grid-view").yiiGridView("applyFilter"); which always breaks it) is just before and just after GridView Widget. Anywhere else causes corruption.

The 'data-pjax' => 0 on nearly all the controls are there because I keep trying Pjax in different positions when I change an attribute. Easier to leave it there than remove it. Also, I comment it out as required during some of the tests.

The only way to get it to function correctly is with the code I posted above. Provided the Pjax widget has the following attributes:
'id' => 'pjax-container', 'timeout' => 5000, 'formSelector' => false, Note: timeout has to be 3 seconds or above or it will sometimes fail if the response takes too long, and id must be manually set.

If you are not using the Modal Widget (i.e. normal page views), Pjax works as expected as long as it only surrounds the GridView Widget, no other Widgets.

The bottom line is: Pjax does not play well, if at all, with any other scripts that alter the contents or view of the page unless a full and complete page refresh is performed from the server.