Search code examples
javascriptphplistviewyii2pjax

Yii2 Pjax ListView With Custom ActiveForm Filters - Duplicate Url Params Issue


Key question is, does the \yii\widgets\ListView actually support filters with Pjax, like it's \yii\widgets\GridView counterpart? Here's what I have tried that has led to a duplicate url params issue:

I have a Gii-created search model with a custom param, $userRoleFilter:

<?php

namespace common\models;

use yii\base\Model;
use yii\data\ActiveDataProvider;
use yii\rbac\Item;

/**
 * UserSearch represents the model behind the search form of `common\models\User`.
 */
class UserSearch extends User
{
    public $userRoleFilter = null;

    /**
     * {@inheritdoc}
     */
    public function rules()
    {
        return [
            [['id', 'flags', 'confirmed_at', 'blocked_at', 'updated_at', 'created_at', 'last_login_at', 'auth_tf_enabled', 'password_changed_at', 'gdpr_consent', 'gdpr_consent_date', 'gdpr_deleted'], 'integer'],
            [['username', 'email', 'password_hash', 'auth_key', 'unconfirmed_email', 'registration_ip', 'last_login_ip', 'auth_tf_key', 'userRoleFilter'], 'safe'],
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function scenarios()
    {
        // bypass scenarios() implementation in the parent class
        return Model::scenarios();
    }

    /**
     * Creates data provider instance with search query applied
     *
     * @param array $params
     *
     * @return ActiveDataProvider
     */
    public function search($params)
    {
        $query = User::find();

        // add conditions that should always apply here

        $dataProvider = new ActiveDataProvider([
            'query' => $query,
            'pagination' => [
                'pageSize' => 20,
            ],
        ]);

        $this->load($params);

        if (!$this->validate()) {
            // uncomment the following line if you do not want to return any records when validation fails
            // $query->where('0=1');
            return $dataProvider;
        }

        // grid filtering conditions
        $query->andFilterWhere([
            'id' => $this->id,
            'flags' => $this->flags,
            'confirmed_at' => $this->confirmed_at,
            'blocked_at' => $this->blocked_at,
            'updated_at' => $this->updated_at,
            'created_at' => $this->created_at,
            'last_login_at' => $this->last_login_at,
            'auth_tf_enabled' => $this->auth_tf_enabled,
            'password_changed_at' => $this->password_changed_at,
            'gdpr_consent' => $this->gdpr_consent,
            'gdpr_consent_date' => $this->gdpr_consent_date,
            'gdpr_deleted' => $this->gdpr_deleted,
        ]);

        $query->andFilterWhere(['like', 'username', $this->username])
            ->andFilterWhere(['like', 'email', $this->email])
            ->andFilterWhere(['like', 'password_hash', $this->password_hash])
            ->andFilterWhere(['like', 'auth_key', $this->auth_key])
            ->andFilterWhere(['like', 'unconfirmed_email', $this->unconfirmed_email])
            ->andFilterWhere(['like', 'registration_ip', $this->registration_ip])
            ->andFilterWhere(['like', 'last_login_ip', $this->last_login_ip])
            ->andFilterWhere(['like', 'auth_tf_key', $this->auth_tf_key]);

        if (!empty($params['UserSearch']['userRoleFilter'])) {
            $userRoleFilter = $params['UserSearch']['userRoleFilter'];

            // inner join to the user role items to filter by assigned role
            $query->alias('u')
                ->innerJoin(
                    'auth_assignment AS aa',
                    'u.id = aa.user_id AND aa.item_name = :roleName',
                    ['roleName' => $userRoleFilter]
                )
                ->leftJoin(
                    'auth_item AS ai',
                    'aa.item_name = ai.name AND ai.type = :typeRole',
                    ['typeRole' => Item::TYPE_ROLE]
                );
        }

        return $dataProvider;
    }
}

Controller method:

    /**
     * Displays pricing calculation & data exports index.
     *
     * @return string
     */
    public function actionDisplayUsers()
    {
        $searchModel = new UserSearch();
        $dataProvider = $searchModel->search(Yii::$app->request->queryParams);

        return $this->render('display-users', [
            'searchModel' => $searchModel,
            'dataProvider' => $dataProvider,
        ]);
    }

And manually wrapped my custom yii\bootstrap\ActiveForm in the Pjax tags:

<?php

/* @var $this yii\web\View */
/* @var $searchModel common\models\UserSearch */
/* @var $dataProvider yii\data\ActiveDataProvider */

use common\components\rbac\UserManager;
use yii\bootstrap\ActiveForm;
use yii\widgets\ListView;
use yii\widgets\Pjax;

$this->title = Yii::t('app', 'Display Users');
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="site-display-users">

    <div class="body-content">

        <h1><?= $this->title ?></h1>

        <?php Pjax::begin([
            'options' => [
                'id' => 'site-display-users-pjax-container',
            ],
            'enablePushState' => true,
            'enableReplaceState' => false,
        ]); ?>

        <div class="well center-block meta-control">
            <?php $form = ActiveForm::begin([
                'id' => 'displayUsersForm',
                'method' => 'get',
                'options' => [
                    'data-pjax' => 1,
                ],
            ]); ?>
            <div class="row row-grid">
                <div class="col-xs-6">
                    <?=
                    $form
                        ->field($searchModel, 'userRoleFilter')
                        ->dropDownList(UserManager::getAvailableRoles(), [
                            'prompt' => 'Select User Role',
                            'id' => 'userRoleFilter',
                        ])
                    ?>
                </div>
                <div class="col-xs-6">
                </div>
            </div>
            <?php ActiveForm::end(); ?>
        </div>

        <div class="row">
            <div class="col-lg-12">
                <?php
                echo ListView::widget([
                    'dataProvider' => $dataProvider,
                    'itemView' => '_display-user',
                    'viewParams' => [
                        // add params to pass into view here
                    ],
                ]);
                ?>
            </div>
        </div>

        <?php Pjax::end(); ?>

    </div>
</div>

enter image description here

This works fine and filters the users in the ListView according to the selected role. But it is creating a duplicate url param after each time the filter is changed:

  1. /site/display-users
  2. /site/display-users?UserSearch%5BuserRoleFilter%5D=admin
  3. /site/display-users?UserSearch%5BuserRoleFilter%5D=admin&UserSearch%5BuserRoleFilter%5D=role-importer-user
  4. /site/display-users?UserSearch%5BuserRoleFilter%5D=admin&UserSearch%5BuserRoleFilter%5D=role-importer-user&UserSearch%5BuserRoleFilter%5D=admin

and so on...

I know that I can set the Pjax enablePushState and enableReplaceState values to false and then it does not keep creating history items and modifying the url in the browser, but just sends the same ever-lengthening url in the ajax request...

What can be done? Is there a better way to handle this? A setting I am missing to stop this duplication of get param keys stacking up in the url?


Solution

  • Found out the solution... turns out that the ActiveForm form action parameter needs to be explicitly defined so that this URI is used for each form submission rather than relying on the URL from the address bar.

                <?php $form = ActiveForm::begin([
                    'id' => 'displayUsersForm',
                    'method' => 'get',
                    'action' => Url::to(['site/display-users']),
                    'options' => [
                        'data-pjax' => 1,
                    ],
                ]); ?>
    

    See here for more details.