Ajax, CJuiTabs and stacking event handlers

Hello,

I have the following view:




$this->widget('zii.widgets.jui.CJuiTabs', array(

    'tabs' => array(

        'Create' => array(

            'id' => 'createProduct',

            'ajax' => 'adminProduct/create'

        ),

        'Manage' => array(

            'id' => 'manageProducts',

            'ajax' => 'adminProduct/manage'

        )

    ),

    'htmlOptions' => array(

        'id' => 'productTab'

    ),

    'options' => array(

        'collapisible' => true

    ),

));



adminProduct/manage:




public function actionManage(){

	$model=new Product('search');

	$model->unsetAttributes();  // clear any default values

	if(isset($_GET['Product'])) {

            $model->attributes = $_GET['Product'];

        }

	$this->renderPartial('/product/manage', array('model'=>$model), false, true);

}



adminProduct/create:




public function actionCreate() {

        $params = array();

        $model = new Product();

	//$this->performAjaxValidation($model);

        $params['model'] = $model;

	if(isset($_POST['Product'])) {

            $model->attributes = $_POST['Product'];

            if($model->save()) {

                $params['model'] = new Product();

                $params['success'] = 'Product has successfully been added to the store!';

            }

            $this->renderPartial('/product/_create', $params);

        }

        else {

            $this->renderPartial('/product/_create', $params, false, true);

        }

 }



When the page initially loads it works fine. However if I then click on the "Manage" tab then click on the "Create" tab, the javascript inside the view is registered again. In this case it is some javascript attached to a CHtml::ajaxSubmitButton so when I submit the form it will in-fact submit twice. How can I solve this?

I’ve tried giving the ajaxSubmitButton a different ID each time it is rendered but I also have a CGridView with custom CButtonColumns so I need a more general solution. Ideally I’d like to just clear any existing event handlers but not sure how I do this.

Thanks for any input.

Edit:

For clarity I’ve attached the two views rendered by actionCreate and actionManage.

_create:




<div class="form">


<?php $form=$this->beginWidget('CActiveForm', array(

	'id'=>'product-form',

	//'enableAjaxValidation'=>true,

    //'enableClientValidation'=>true

)); ?>


    <?php if(isset($success)): ?>

        <div class="flash-success">

            <?php echo '<p>'.$success.'</p>'; ?>

        </div>

    <?php endif; ?>

    

	<?php echo $form->errorSummary($model); ?>


	<div class="row">

		<?php echo $form->labelEx($model,'name'); ?>

		<?php echo $form->textField($model,'name',array('size'=>60,'maxlength'=>256)); ?>

		<?php echo $form->error($model,'name'); ?>

	</div>


	<div class="row">

		<?php echo $form->labelEx($model,'description'); ?>

		<?php echo $form->textField($model,'description'); ?>

		<?php echo $form->error($model,'description'); ?>

	</div>


	<div class="row">

		<?php echo $form->labelEx($model,'price'); ?>

		<?php echo $form->textField($model,'price',array('size'=>10,'maxlength'=>10)); ?>

		<?php echo $form->error($model,'price'); ?>

	</div>


	<div class="row">

		<?php echo $form->labelEx($model,'quantity'); ?>

		<?php echo $form->textField($model,'quantity'); ?>

		<?php echo $form->error($model,'quantity'); ?>

	</div>


	<div class="row">

		<?php echo $form->labelEx($model,'category_id'); ?>

		<?php echo $form->dropDownList($model,'category_id', Category::getCategoryOptions()); ?>

		<?php echo $form->error($model,'category_id'); ?>

	</div>


	<div class="row buttons">

        <?php

            echo CHtml::ajaxSubmitButton(

                'Create',

                CHtml::normalizeUrl('adminProduct/create'),   

                array(

                    'beforeSend' => 'function(){

                        $("#ajax-response").html("<div class=\'flash-notice\'><p>Creating product..</p></div>");

                    }',

                    'success' => 'function(data){

                        $("#createProduct").html(data);

                    }',

                ),

                array('id' => 'productCreateSubmitButton')

            );

        ?>

	</div>


<?php $this->endWidget(); ?>

    

</div><!-- form -->




_manage:




<?php

Yii::app()->clientScript->registerScript('search', "

$('.search-button').click(function(){

	$('.search-form').toggle();

	return false;

});

$('.search-form form').submit(function(){

	$.fn.yiiGridView.update('product-grid', {

		data: $(this).serialize()

	});

	return false;

});

");

?>


<h1>Manage Products</h1>


<p>

You may optionally enter a comparison operator (<b>&lt;</b>, <b>&lt;=</b>, <b>&gt;</b>, <b>&gt;=</b>, <b>&lt;&gt;</b>

or <b>=</b>) at the beginning of each of your search values to specify how the comparison should be done.

</p>


<?php echo CHtml::link('Advanced Search','#',array('class'=>'search-button')); ?>

<div class="search-form" style="display:none">

<?php $this->renderPartial('/product/_search',array(

	'model'=>$model,

)); ?>

</div><!-- search-form -->


<?php $this->widget('zii.widgets.grid.CGridView', array(

	'id'=>'product-grid',

	'dataProvider'=>$model->search(),

	'filter'=>$model,

	'columns'=>array(

		'name',

		'description',

		'price',

		'quantity',

		array(

            'name' => 'category_id',

            'value' => '$data->category->name'

        ),

		array(

			'class'=>'CButtonColumn',

            'template' => '{update}',

            'buttons' => array(

                'update' => array(

                    'url' => '$this->grid->controller->createUrl("adminProduct/update", array("id" => $data->id))',

                    'click' => "function() {

                        var url = $(this).attr('href');

                        $.post(url, function(response) {

                            $('#productDialogContent').html(response);

                            $('#productDialog').dialog('open');

                        });

                        return false;

                    }"

                )

            )

		),

	),

));?>


<?php $this->beginWidget('zii.widgets.jui.CJuiDialog', array(

    'id' => 'productDialog',

    'options' => array(

        'autoOpen' => false,

        'modal' => true,

        'width' => 500,

        'height' => 500

    )

)); ?>


    <div id="productDialogContent"></div>


<?php $this->endWidget(); ?>



Bump.

I’m going to abandon this approach for the time being but I’m still curious if anyone has a good solution. I need to somehow determine if the event handlers have already been registered. If they have then I simply do a renderPartial without processing the output.

I didn’t test this myself, but I have come to similar problems, where Javascripts are loaded after an ajax request.

What we have done is extend CClientScript, this way:




<?php


/**

 * This class is used to don't import jquery over in ajax requests.

 *

 * @author Sebastián Thierer

 * @author Luis Lobo Borobia

 * @version 1.0

 *

 */

class FCClientScript extends CClientScript

{

	public $coreScriptsToSkipOnAjax = array( 'jquery', 'jquery.ui' );


	/**

     * Registers a script package that is listed in {@link packages}.

     * Handles special case for ajax requests

     * @param string $name the name of the script package.

     * @return CClientScript the CClientScript object itself (to support method chaining, available since version 1.1.5).

     * @see renderCoreScript

     */

	public function registerCoreScript( $name )

	{


		if( Yii::app()->request->getIsAjaxRequest() && is_array( $this->coreScriptsToSkipOnAjax ) )

		{

			foreach( $this->coreScriptsToSkipOnAjax as $nameToSkip )

				if( strcmp( $name, $nameToSkip ) == 0 )

				{

					return $this;

				}

		}


		if( !$this->isScriptRegistered( $name ) )

		{

			parent::registerCoreScript( $name );

		}


		return $this;

	}


	/**

     * Registers a javascript file. If file is already registered, it does not register it again.

     * @param string $url URL of the javascript file

     * @param integer $position the position of the JavaScript code. Valid values include the following:

     * <ul>

     * <li>CClientScript::POS_HEAD : the script is inserted in the head section right before the title element.</li>

     * <li>CClientScript::POS_BEGIN : the script is inserted at the beginning of the body section.</li>

     * <li>CClientScript::POS_END : the script is inserted at the end of the body section.</li>

     * </ul>

     * @return FCClientScript the FCClientScript object itself (to support method chaining, available since version 1.1.5).

     */

	public function registerScriptFile( $url, $position = self::POS_HEAD )

	{

		if( !$this->isScriptFileRegistered( $url ) )

		{

			parent::registerScriptFile( $url, $position );

		}

		return $this;

	}


	/**

     * Registers a CSS file. If file is already registered, it does not register it again.

     * @param string $url URL of the CSS file

     * @param string $media media that the CSS file should be applied to. If empty, it means all media types.

     * @return FCClientScript the FCClientScript object itself (to support method chaining, available since version 1.1.5).

     */

	public function registerCssFile( $url, $media = '' )

	{

		if( !$this->isCssFileRegistered( $url ) )

		{

			return parent::registerCssFile( $url, $media );

		}

		return $this;

	}


}

Also, I have added this public property to to Yii’s JuiWidget class:




	/**

     * @var boolean Wether to register the core jQuery UI package. If set to true, should also remove

     * jQuery UI javascript file from {@link scriptFile}

     * @since 1.1.11

     */

	public $registerCoreJQueryUI=false;



and on the registerCoreScripts function, I added a check to register Jquery.ui again.




	/**

     * Registers the core script files.

     * This method registers jquery and JUI JavaScript files and the theme CSS file.

     */

	protected function registerCoreScripts()

	{

		$cs=Yii::app()->getClientScript();

		if(is_string($this->cssFile))

			$cs->registerCssFile($this->themeUrl.'/'.$this->theme.'/'.$this->cssFile);

		else if(is_array($this->cssFile))

		{

			foreach($this->cssFile as $cssFile)

				$cs->registerCssFile($this->themeUrl.'/'.$this->theme.'/'.$cssFile);

		}


		$cs->registerCoreScript('jquery');

		if($this->registerCoreJQueryUI)

			$cs->registerCoreScript('jquery.ui');

		if(is_string($this->scriptFile))

			$this->registerScriptFile($this->scriptFile);

		else if(is_array($this->scriptFile))

		{

			foreach($this->scriptFile as $scriptFile)

				$this->registerScriptFile($scriptFile);

		}

	}



This is working for me. I didn’t submit this change yet to the framework to be evaluated as a good solution, since I didn’t have enough time.

Regards!

Hello Luis. Thank you very much for taking the time to reply with such a thoughtful answer. It’s nice to know that there is a solution out there for this. I feel there’s a simpler way to go about this though.

Is it possible to configure which jQuery function the ajaxSubmitButton will use to make the post? I believe using .on instead of the default .live might prevent the issue of multiple event handlers.

I haven’t used tabs with Yii yet so I can’t be sure what the problem is.

However this looks to me like that classic script issue in Yii (not a bug however :) ), generally solved by setting scriptMap[xxx] = false

http://www.yiiframework.com/wiki/72/cjuidialog-and-ajaxsubmitbutton/

That does work but I’m wondering if it’s the ideal solution. Configuring which jQuery function the ajaxSubmitButton uses seems more clean.