-->

Yii2 Pjax and ActiveForm beforeSubmit not working

2019-02-25 09:36发布

问题:

I am creating a loading indicator on my submit button and attaching the "start" procedure to the beforeSubmit event using the registerJs function.

It works properly the first time, but after the Pjax container is reloaded, the event won't fire when the form is submitted again.

I am enclosing the entire view file in the Pjax container so that the flash messages are properly displayed.

Here is the main.php layout file snippet:

<?php Pjax::begin([
'id' => 'pjax-container',
'enablePushState' => false,
]); ?>

<?= Alert::widget(); ?>

<?= $content; ?>

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

And here is the registerJs function, located in main.php in the section:

<?php
$js = <<< 'SCRIPT'
$('#form').on('beforeSubmit', function(){
alert('Submitted...');
//$("#spinner").fadeIn();
});
SCRIPT;
$this->registerJs($js);
?>

And here is the activeForm:

<?php $form = ActiveForm::begin(['id' => 'form', 'options' => ['data-pjax' => true]]); ?>

<?= $form->field($model, 'name')->textInput(['type' => 'text']) ?>

<?= $form->field($model, 'email')->textInput(['type' => 'email']) ?>

<?= $form->field($model, 'subject')->textInput(['type' => 'text']) ?>

<?php
echo $form->field($model, 'message')->textArea(['rows' => 6]);

echo $form->field($model, 'verify_code')->widget(Captcha::className(), [
        'captchaAction' => 'site/captcha',
        'template' => '<div class="row"><div class="col-lg-4">{image}</div><div class="col-lg-6">{input}</div></div>',
]);
?>

<button class="btn btn-success">
<span id="spinner" style="display: none; float:left; margin-right: 26px;">
<?php
echo Spinner::widget([
'pluginOptions' => [
    'top' => '50%',
    'left' => '10px',
    'lines' => 11,
    'length' => 5,
    'width' => 3,
    'corners' => 1,
    'trail' => 100,
    'speed' => 1.25,
    'radius' => 4,
],
]);
?>
</span>
<?php echo $model->isNewRecord ? 'Send Message' : 'Update'; ?>
</button>

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

I am using the Kartik Spinner Widget (Details and Demo)

Any ideas on why the beforeSubmit event won't fire after Pjax has loaded the data container after a successful for submit?

Thanks.

回答1:

This problem occurs for several reasons.

Firstly, Any code placed within $(document).ready(); and loaded on the ajax called page (contained page) will not be executed after it is loaded.

Secondly, any references to external scripts (<script src>) aren't guarantied to load before inline scripts. In the case of Yii2 these inline scripts would be registered with registerJs()

Getting Pjax to play nice with complex script dependent pages to load is not trivial.

A few things to solve your issues:

1) Inline scripts loaded with registerJs() and set with a position of POS_READY (default?) will be contained within a $(document).ready() if the view is rendered via render().

Thus it is important to render via renderAjax(). This will ensure that the inline scripts will run.

Sometimes (most times) you will need to use render() when statically loaded and renderAjax() when pjax loads a page. In your instance this is required in order to place the Pjax widget in the layout. You can use the following code for these situations:

if(Yii::$app->request->getHeaders()->has('X-PJAX'))
{
    return $this->renderAjax('myview');
}
else
{
    return $this->render('myview');
}

2) When writing external scripts that you know will load within the context of a pjax call. Use:

$(document).on('ready pjax:success', function(){
    // ...
});

This will load the script correctly. But keep in mind that it will reload/rebind the script every time a Pjax call succeeds. That might not be desired unfortunately.

Also keep in mind that the pjax:success event does not necessarily fire after the external scripts from the contained page are loaded. See 3)

3) To solve script order issues you will need to remove the following line from the pjax.js script:

obj.scripts = findAll(obj.contents, 'script[src]').remove() 

This will however create issues with browsers that are set to disable eval() (hence why the line exists in the first place)

Pjax will eventualy have this sorted. You can keep track of the progress here

If anything is unclear let me know and I'll rephrase.

UPDATE: Yii 2.0.7 has introduced changes. One of which is updating the bower\yii2-pjaxto 2.0.6. This makes point 3) no longer necessary and makes point 2) less important, although it's still a good thing to know.



回答2:

So. I have a solution, but it contradicts the documentation...

If you look at the actual redirect function (source):

public function redirect($url, $statusCode = 302, $checkAjax = true) 
743     { 
744         if (is_array($url) && isset($url[0])) { 
745             // ensure the route is absolute 
746             $url[0] = '/' . ltrim($url[0], '/'); 
747         } 
748         $url = Url::to($url); 
749         if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) { 
750             $url = Yii::$app->getRequest()->getHostInfo() . $url; 
751         } 
752  
753         if ($checkAjax) { 
754             if (Yii::$app->getRequest()->getIsPjax()) { 
755                 $this->getHeaders()->set('X-Pjax-Url', $url); 
756             } elseif (Yii::$app->getRequest()->getIsAjax()) { 
757                 $this->getHeaders()->set('X-Redirect', $url); 
758             } else { 
759                 $this->getHeaders()->set('Location', $url); 
760             } 
761         } else { 
762             $this->getHeaders()->set('Location', $url); 
763         } 
764  
765         $this->setStatusCode($statusCode); 
766  
767         return $this; 
768     } 

You can see that it checks for Pjax before Ajax and has two different headers as such. the X-Redirect header that is mentioned all over the web and multiple sites, does NOT work for a Pjax call. You must check for X-Pjax-Url for a Pjax call.

I register my own javascript function to handle the redirect like below:

$js = <<< 'SCRIPT'
$(document).on('pjax:complete', function (event, xhr, textStatus, options) {
//$(document).ajaxComplete(function (event, xhr, settings) {
var url = xhr.getResponseHeader('X-Pjax-Url');
if (url) {
    window.location = url;
}
});
SCRIPT;
$this->registerJs($js);

And for some reason, this on its own wasn't enough. The source function, by default runs the checkAjax code, but until I explicitly called it from my redirect code, it wouldn't work.

So here is what I use in my controller:

Yii::$app->response->redirect(Yii::getAlias('@web') . '/secure/dashboard/', true)->send();
return;

This has been discussed many, many times and I hope this helps others.