Yii generated script and renderPartial

Hi, I'm loading a form in an Ajax tabbed page (calling the form controller with Ajax), and would like to use ajaxSubmitButton().

Since jQuery is already loaded in the main page, I'd use renderPartial(). But to get the Yii generated script snippet for the ajaxSubmitButton(), $processOutput=true is needed, but that will also add a line to the jQuery corescript.

The renderBodyBegin() and renderBodyEnd() are protected functions, so can't use that. Any option to generate the partial with corresponding generated script, but without corescript?

Yes, this is a tricky problem: whether or not to include js files in renderPartial. I can certainly turn those protected methods into public, but I think we should seek some better solutions. Any ideas?

Perhaps if $processOutput=true, only generate the script belonging to the partial itself? Strictly speaking, the corescript is not part of the partial.

Well, you don't know if a core script is loaded by the partial or not. We may consider adding some options in processOutput to allow explicitly specify whether or not to load some scripts.

Sounds good to me!

Found that when corescript (jquery) is outputted in the rendered partial, all previously loaded javascript will not work in the returned partial anymore (obviously).

Another solution might be to adjust the preg_replace in renderHead() so that it will only add POS_HEAD if there is an actual <head> tag in the html.

No ideas so far? This problem becomes a show stopper for a lot of AJAX stuff i try to do. E.g. in the setup described here, i want to load a “dayreport” into a div via AJAX that should contain some CHtml::linkButton(). They also don’t work, no matter if processOutput is true or false.

Could we overload the processOutput parameter, that if it's an array, it contains additional options, e.g. which core scripts to provide?

Edit: A “show stopper” obviously has not the meaning in english, i thought it would have ;)

Another idea: Something like the YUI Loader could solve the problem. I don’t know, if jQuery provides something similar. It’s like a “script manager” for the clientside. It would keep a list of all loaded js core scripts on the client, and e.g. on a AJAX response we could ask the loader for a specific script. If the loader can’t find the client script, it will load it.

Yes, that is probably a good idea.

Maybe we can use parts of YUI loader to create a thin script loader for Yii. I will need more time to think about this, but my first approach would be:

  1. Whenever a page uses AJAX to load views that contain other AJAX components, we would manually add the script loader to that page or even the layout. (This might be solved better; didn't have a closer look at the clientscript management of Yii):
<?php echo CHhtml::ajaxLoader() ?>


  1. renderPartial() could have another parameter somewhere, to decide, if the generated view should contain a js block, that registers any required script with the script loader and initializes it's control on success. The js could look like:
<script type="text/javascript">


/*<![CDATA[*/


jQuery.yii.load( {


    'url:'/assets/1c978867/jquery.ui.datepicker.js'),


    'success':function() { // do some init stuff here }


});


/*]]>*/


</script>


So to put it another way, the developer has to answer these questions:

  1. Do i need the script loader included on the page on first load?

  2. Can my partailly rendered view rely on a script loader present on the clientside?

If the above makes sense, i think i could come up with a simple js loader implementation, as i'll have some time the next days to study the YUI loader.

Yes, I think the loader idea is probably workable. Here's the scenario that I can think of:

  1. In the first run of the page (full page): if the page requires js files (or even css files?), we will insert the loader script in the head section. We use this loader to include the needed js files.

  2. In the following runs of the page (partial page due to ajax requests): if the page requires js files, we will still insert the loader script at the beginning. We use this loader to include the js files needed by the partial view.

So here are the requires for the loader script:

  1. It should be able to be included and run multiple times without side effects

  2. A js file can be loaded multiple times by the loader, but only the first time is effective, and the rest are ignored.

What do you think? If you can come up with a prototype, that will be really great!

Hmm. So you want to include the loader with every partial page? I guess, you want that, to automate the process of including the loader.

I'm not sure about that: I'd say, it would be easier to include the loader only once (first page load) and let the developer decide, to do so. If we send the loader with each AJAX response, this unneccessarily blows them up.

It's nice to automate things, but this often makes things obscure and you don't really know, whats going on behind the scenes. The principle of a loader is quite simple, and i think, when developing an AJAX page, it's easy to learn how to use the loader.

But i'll also think about your proposal…

Ok, I think it's fine that we only load loader the first time.

More (very free) thinking:

What if the loader is always included? It could take care of all the scripts/css we need on the clientside. To retain compatibility with older browsers, this could be made optional (turning it off, gives the current behaviour), but if anyone wants to use js enhanced stuff on his site, the loader is mandatory. The loader script will not be that big, so not a big deal, i think.

This would prevent duplicate script loading. I see not many alternatives, as the client is the only instance that knows, which scripts are already loaded. We can't solve this on the serverside.

While developing this feature, don’t forget about proposed CClientScript improvements which should allow packing all scripts into a single file so there’s no need for any loaders because all scripts will be always accessible.

You probably would not want to load all scripts into a single file, besides the fact you need to know what all will mean for the whole app, it could mean that you bloat your single file too much.

If the loader is available, it can test for the needed plugin functions as well, and load it dynamically if not present. The loader itself might not be needed on every page, only the paths to the required plugins.

For me it's a showstopper as well currently, eg. not using build-in JS functions since its destroying already present jQuery plugins, and a huge overhead.

After inspecting YUI Loader, i have some more ideas.

YUI Loader adds a <script> tag for each script, that needs to be loaded. This is a good thing, as the browser then will cache the loaded script. I've also thought about dependencies. But i tried to keep it as simple as possible. So how about this:

  1. The loader could become part of jquery.yii.js and provide 2 methods: jquery.yii.load() and jquery.yii.register(). So when using the loader, jquery must already be loaded.

  2. jquery.yii.load(urls, success) accepts two parameters: a literal objects with url/modulename pairs, and a callback function that’s called after all scripts loaded completely.

jquery.yii.load(


  {


    '/assets/1c9535/jquery.yiitab.js' : 'yiitab'   // value could also be an array


  }, 


  function () { 


    /* init stuff, that depends on the loaded script /* 


  }


);
  1. Every loaded js script needs an additional line in the end to register itself with the loader and let him know, that it's loaded completely. E.g. in jquery.yiitab.js this line would get added:
jquery.yii.register('yiitab');

An example page flow could then look like this:

1. Complete Page loads, with e.g. this content

<head>


...


<script type="text/javascript" src="/assets/d652f40f/jquery.js"></script>


<script type="text/javascript" src="/assets/1c978867/jquery.yii.js"></script>


...


</head>


<body>


....


<!-- loader call, generated by some view that uses a CTabView: -->


<script type="text/javascript">


/*<![CDATA[*/


jquery(function(){


    jquery.yii.load(


      {'/assets/abcdefg/jquery.yiitab.js':'yiitab'}, 


      function() { /* init tabview */}


    );


});


/*]]>*/


</script>


When the page is loaded, jquery.yiitab.js will be loaded and the tabview will get initalized afterwards. It's not required to use the loader here. The according script tag could also be added after the jquery.yii.js tag in the header.

2. An AJAX call is made to update parts of the page.

The loaded view contains another CTabView and a CStarRating. At the end of the HTML response, this script is added:



<!-- loader call, generated by some view that uses a CStarRating: -->


<script type="text/javascript">


/*<![CDATA[*/


jquery(function(){


    jquery.yii.load(


      {'/assets/abcdefg/jquery.yiitab.js':'yiitab'}, 


      function() { /* init tabview */}


    );


    jquery.yii.load(


      {


        '/assets/abcdefg/jquery.dimensions.js' : 'dimensions',


        '/assets/abcdefg/jquery.metadata.js' : 'metadata'


      }, 


      function() { /* init CStarRating */}


    );


});


/*]]>*/


</script>


The loader would find yiitab already loaded and immediately call the success callback. The required js for CStarRating would be loaded, and it's init also called afterwards.

To ease generation of these loader block, a util function could be used. It could also allow adding custom js files. So if someone wants to use a minified js file, it would generate something like this:



<script type="text/javascript">


/*<![CDATA[*/


jquery(function(){


    jquery.yii.load(


      {'/assets/abcdefg/my-custom-min.js': ['yiitab','bgiframe', 'dimension'] }, 


      function() { /* init stuff */}


    );


});


/*]]>*/


</script>


The min file must register the according modules:

yii.register( ['yiitab','bgiframe','dimension'] );

What do you guys think? Maybe we also need a type for the urls, to be able to include CSS.

Little addition: A more flexible (but also more verbose) syntax could look like this:

yii.load([


  {


    'name' : ['yiitab','bgiframe', 'dimension'],


    'url' : '/assets/abcdefg/my-custom-min.js'


  },


  {


    'name' : ['yiitabcss'],


    'url' : '/assets/abcdefg/yiitab.css',


    'type' : 'css' // defaults to js


   }


], 


function() { /* init */ }


);





Looks like a neat solution indeed!

Finally I get time to look at your proposal. ;)

Thanks for your investigation. It seems to me that your approach would require that widget init code always follows after the corresponding js file? Also, the js file is loaded at the end of HTML?

Maybe we can go this simpler way?

  1. In the initial page, we use <script> to include js/css files and also call a register() method to remember these inclusions;

  2. In the ajax responses, we use load() method to include js/css files.

  3. In both cases, the <script> tag and load() call should be able to appear anywhere (head, body begin, body end).

I am not sure whether or not we should deal with those callbacks. I think it makes things more complex.