Yii2: Problem with inline scripts and Pjax

Created on 25 Aug 2014  路  19Comments  路  Source: yiisoft/yii2

My page contain scripts with src and inline scripts(which use scripts with src)
When Im loading page, pjax first execute inline script(by context.html(...) ) and then scripts with src(by executeScriptTags())
Therefore im get an errors because inline scripts call not loaded objects from not loaded files

Can you recommend a solution to fix this problem?

pjax

Most helpful comment

Try this:

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

All 19 comments

Use jQuery document ready event. https://api.jquery.com/ready/

Pjax load page in ajaxMode, document ready(costant View::POS_READY ) is ignored in ajaxMode.

Try this:

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

@janisto,

As @samdark said, if ready event work successfuly, additional event pjax:success is not necessary.
The problem is that Yii2 in ajaxMode ignore View::POS_READY flag wich add jquery document ready event handlers
And even the first problem will be solved, second problem is that all code with no flag View::POS_READY, will not work by pjax loading

I'm going to revive this as it's quite a problem and I feel there is a "not too compromising" solution to make everyone's life easier.

I've been reading around this and here's what I've found. I will refer to the page loaded by Pjax as the contained page from here on.
The issue arises from the fact that Pjax will not guaranty the order of execution of scripts on the contained page. The main reason being a security reason ( https://github.com/defunkt/jquery-pjax/issues/331 ).
So any inline code in the contained page will probably be executed before the external js files are loaded, thus creating failures. A simple way of recreating this issue is to put a grid inside the contained page. The yii.grid.js will be executed after the inline code for checkboxes/etc. thus generating errors.

The .ready() event doesn't help as it's only triggered once, and then it isn't even possible to trigger it manually when an ajax page is loaded. You can find information on this here and here.
So @janisto is correct and one must use the following:

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

Of course, not all renderAjax() loaded pages are done through Pjax so we would have to isolate the requests with the X-PJAX header and apply the above to any inline scripts.
With this done things should behave properly. It is worth noting that the Pjax issue I linked early also has discussion around correcting the pjax behavior in some cases. The current proposal should work in all situations.

This is linked to https://github.com/yiisoft/yii2/issues/6655 (might be worth only keeping one of these open)

Ok I have to correct myself as I've tried a few things now.

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

Is not suffisent in itself. Even though this executes after Pjax adds the external scripts to the head it does not run after these scripts have finished loading. There may be a way to use the deferred promise but I haven't found it yet.

In the meantime the workaround I've found is:

$(document).on('ready pjax:success', function() {
    var timer = setInterval(function(){
        clearInterval(timer);
        // code here
    }, 200);
});

Needless to say that this is terribly ugly but if anyone is struggling hard with this issue, this is an option.

If you're wondering how to apply this to Yii2. Extend \yii\base\View and in renderBodyEndHtml() you can change things to

if (!empty($this->js[self::POS_READY])) {
    if (Yii::$app->request->getHeaders()->has('X-PJAX')) {
        $scripts[] = "jQuery(document).on('ready pjax:success', function () {
        var timer = setInterval(function(){clearInterval(timer);\n".implode("\n", $this->js[self::POS_READY])."\n}, 200)});";
    }
    else {
        $scripts[] = implode("\n", $this->js[self::POS_READY]);
    }
}

Needless to say, this is a terrible solution in itself. But if this issue is a blocking issue for you (as it is for me), this should help you manage until Pjax corrects it's issue 331 or a better solution is found.

It is worth noting the above will only work on POS_READY inline scripts. But the concept can be applied to whichever positions you want.

The most clean way right now is:

...Commenting the line (in pjax.js) obj.scripts = findAll(obj.contents, 'script[src]').remove() workarounded the issue.

@llfm That's correct. It's actually better than what I had proposed as pjax:success garanties that the script will be run after the scripts are assigned to the head but not after those scripts have loaded. So commenting the line in pjax.js is a must do.
pjax:success needs to be used in conjunction with ready for externally loaded scripts or they will not load though.

The most clean way right now is:

...Commenting the line (in pjax.js) obj.scripts = findAll(obj.contents, 'script[src]').remove() workarounded the issue.

This works for my problem, that my filter for pjax loaded Gridview wasn't working. But now my other javascript during pjax calls won't work anymore. For example a ActiveForm with a kartik select2 widget due to a reload of the select2 javascript code.

Is there any other solution?

Duplicates #3680
Duplicates #6655
Duplicates #8540
Duplicates #8916

It seems function extractContainer() should be modified indeed, as it was proposed, removing following section:

// Gather all script[src] elements
obj.scripts = findAll(obj.contents, 'script[src]').remove()

However, it should not be removed completely: the original check for existing load scripts with src, which is present at executeScriptTags() function, should remain, but moved to extractContainer().

So inside extractContainer() we should remove only those 'script' tags, which have 'src', which has been already present at the page. While function executeScriptTags() can be removed completely.

It seems the idea behind executeScriptTags() was putting all src-scripts at the 'head' section. However, I don't know for which purpose. Now it is obvious such approach leads to certain problems.

@klimov-paul Now I'm testing changes, proposed somewhere by @nkovacs
It works really good for our project and we didn't find any problems with it.
https://github.com/hiqdev/jquery-pjax/commit/4299981b7c9bedfd338022f6644672e0a7d6e350

That's the main commit and I'm going to create a PR soon

@klimov-paul If you do not remove the scripts from the contents, jquery will insert them synchronously. This is really bad, since the website will hang while the script file is loading. It's also deprecated.
The reason for putting them in head is simple. Once you load a script file, you cannot unload it.
So you might as well put it into head, instead of putting into a div in the middle of your page, where it will be deleted when the div's content is changed by another pjax call.
Putting it into head also makes it easier to find already loaded script files.

Also, that behavior is there for a reason:

However, the solution can't depend on eval [ndlr: jQuery script loading] because we want pjax to be compatible with sites that use Content Security Policy to disable eval for safety reasons.

I think the people at pjax want to make this an option to help out for cases like this one. We'd be better off participating here

I did participate there. They're not really interested.
My patch doesn't use eval.

My post wasn't really directed towards anyone in particular. I was actually adding to your argument :)

...Commenting the line (in pjax.js) obj.scripts = findAll(obj.contents, 'script[src]').remove() workarounded the issue.

works for me too

Me too ...Commenting the line (in pjax.js) obj.scripts = findAll(obj.contents, 'script[src]').remove() workarounded the issue. Work well

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rosancoderian picture rosancoderian  路  46Comments

deecode picture deecode  路  50Comments

njasm picture njasm  路  44Comments

cebe picture cebe  路  53Comments

schmunk42 picture schmunk42  路  47Comments