Yii Framework Forum: CButtonColumn 'click' live event attached multiple times when entire grid widget is replaced through ajax. - Yii Framework Forum

Jump to content

Page 1 of 1
  • You cannot start a new topic
  • You cannot reply to this topic

CButtonColumn 'click' live event attached multiple times when entire grid widget is replaced through ajax. Code snippet for Issue 3084: Rate Topic: -----

#1 User is offline   le_top 

  • Standard Member
  • PipPip
  • Yii
  • Group: Members
  • Posts: 294
  • Joined: 08-June 10
  • Location:France

Posted 09 January 2012 - 07:43 AM

This example is for Issue 3084.

The example contains a controller and a view - the sample data is include (this example shoud work 'as is'.

To demonstrate the bug, select 'Entity B' and then 'Entity A' again. This reloads the grid.

A click on 'update' or 'delete' will then open the alert window two times.

The files are also attached - the controller should go in 'controllers/' and the view in 'views/example/'.

ExampleController.php

<?php
class ExampleController extends CController {

    public function getSampleData() {
        return array(
        1 => array(
            	'displayname'=>'Entity A',
                'id' => 1,
                'tags'=>array(
                            array('id'=>1,'tag'=>'tag a'),
                            array('id'=>2,'tag'=>'tag b'),
                            array('id'=>3,'tag'=>'tag c'),
                            array('id'=>4,'tag'=>'tag d'),
                            array('id'=>5,'tag'=>'tag e'),
                        ),
        ),
        4 => array(
            	'displayname'=>'Entity B',
            	'id'=>4,
                'tags'=>array(
                            array('tag'=>'tag f'),
                            array('tag'=>'tag g'),
                            array('tag'=>'tag h'),
                            array('tag'=>'tag i'),
                            array('tag'=>'tag g'),
                        ),
        ),
        );
    }

    public function actionGrid() {
        $view='grid';
        $id = Yii::app()->request->getParam(id,1);
        $data = null;
        if(is_numeric($id)) {
            $alldata = $this->getSampleData();
            $data=$alldata[$id];
        }
        if($data===undefined) {
            echo 'Bad id';
        } else {
            $params = array('data'=>$data);
            if(Yii::app()->request->isAjaxRequest) {
                $this->renderPartial($view,$params, false, true);
                Yii::app()->end();
            } else {
                $this->render($view,$params);
            }
        }
    }
}





View grid.php

<?php
$id = $data['id'];
$gridid = 'grid-'.$id;  // Id of the grid
$gridajaxkey = 'grid';  // Id for updating the grid

$isAjaxRequest = Yii::app()->request->isAjaxRequest;
$ajaxParam = Yii::app()->request->getParam('ajax');
$renderGrid = !$isAjaxRequest||$ajaxParam===$gridajaxkey;
$renderGridContent = !$isAjaxRequest||($ajaxParam===$gridid)||$renderGrid;

if(!$isAjaxRequest) {
        echo CHtml::dropDownList('eid',
                    $id,
                    array(1=>'Entity A',4=>'Entity B'),
                    array('encode'=>false,
    	                  'onchange'=>CHtml::encode('jQuery("body").trigger("id_change",[this.value]);')
                )
        );

        if(true||Yii::app()->request->enableCsrfValidation)
		{
			$csrfTokenName = Yii::app()->request->csrfTokenName;
			$csrfToken = Yii::app()->request->csrfToken;
			$csrf = "\r\ndata:{ '$csrfTokenName':'$csrfToken' },";
		} else {
			$csrf = '';
		}

        $href=$this->createUrl('',array('ajax'=>$gridajaxkey));
        // Javascript to listen to entity id update event and update the grid view.
        $js=<<<EOD
        jQuery('body').bind('id_change',
function (event,id) {
    jQuery.ajax(
        {'type':'POST',
         'url':"$href&id="+id,
          $csrf
         'success':function(data){
               jQuery("#entity-detail").replaceWith(data);
          },
         'error':function(html){alert('Issue fetching entity data from server.');}
    });
   });
EOD;
        Yii::app()->getClientScript()->registerScript('trackinfoeidlistener', $js,CClientScript::POS_READY);

}
if($renderGrid) {
    echo '<div id="entity-detail">';

    echo '<div id="entity-detail-title" style="font-size:40px">';
    echo Yii::t('app','Detail for {displayname}',array('{displayname}'=>$data['displayname']));
    echo '</div>';
    echo '<div id="entity-detail-content">';
    //$dataprovider = new CActiveDataProvider('Tracker');
}

if($renderGridContent) {
    $dataprovider = new CArrayDataProvider($data['tags'],
    array(
        'id'=>'id',
         'pagination'=>array(
            'pageSize'=>2,
    ),
    ));
    $grid=$this->widget('zii.widgets.grid.CGridView', array(
            'dataProvider'=>$dataprovider,
            'id'=>$gridid,
            'template'=>"{items}\n{pager}", /* Default: '{summary}\n{items}\n{pager}'*/
            'ajaxUrl'=>$this->createUrl('',array('id'=>$id)),
        	'columns'=>array(
                'id',
                'tag',
                array_replace_recursive(
                    array('class'   =>'CButtonColumn',), //Yii::app()->params['defaultCButtonColumnConfig'],
                    array(
                            'template' =>'{update} {delete}',
                        	'buttons'=>array(
                            	'update'=>array(
                            		'click'=>'js:function(){alert("update clicked");return false;}',
                    ),
                            	'delete'=>array(
                            		'url'=>'Yii::app()->controller->createUrl("deletealert",array("id"=>$data->primaryKey))',
                            		'click'=>'js:function(){alert("delete clicked");return false;}',
                    ),
                    ),
                    )
                )
            )
        )
    );
}

if($renderGrid) {
    echo '</div>';
}
?>






My fix is the following code in CButtonColumn. The previous handler is removed (if any) before adding the new one. This solution also avoids ('live') which is no longer recommended in jQuery [otherwise 'die' must be used in stead of 'off'].


CButtonColumn.php - registerClientScript
	protected function registerClientScript()
	{
		$js=array();
		foreach($this->buttons as $id=>$button)
		{
			if(isset($button['click']))
			{
				$function=CJavaScript::encode($button['click']);
				$class=preg_replace('/\s+/','.',$button['options']['class']);[indent]                $js[]="if(typeof(_gridf)==='undefined'){_gridf={};}"
                ."if(typeof(_gridf['on-{$this->grid->id}-{$class}'])!=='undefined') {jQuery(document).off('click','#{$this->grid->id} a.{$class}',_gridf['on-{$this->grid->id}-{$class}']);}"
                ."_gridf['on-{$this->grid->id}-{$class}']=$function;"
                ."jQuery(document).on('click','#{$this->grid->id} a.{$class}',_gridf['on-{$this->grid->id}-{$class}']);";[/indent]

			}
		}

		if($js!==array())
			Yii::app()->getClientScript()->registerScript(__CLASS__.'#'.$this->id, implode("\n",$js));
	}

Attached File(s)


0

#2 User is offline   Maurizio Domba Cerin 

  • Yii - Yesss It Is !!!
  • Yii
  • Group: Yii Dev Team
  • Posts: 4,338
  • Joined: 12-October 09
  • Location:Croatia

Posted 09 January 2012 - 09:47 AM

There are different solutions to this... but adding off()/undelegate()/die() is not optimal at all... why remove a handler just so that you can add the same handler again.

A solution would be to not process the output when using renderpartial but that sometime is not possible as you need some script processed...

In the your code there is one more problem if you noticed... on every ajax call jquery.js, jquery-bbq.js and jquery.yiigridview.js are loaded again and again.

For this a solution would be to check if it's an ajax call and if it is then to prevent loading of those files with the scriptmap.

The best solution when using the grid is to use his update() method - $.fn.yiiGridView.update()
As you want to update some other parts of the page not only the grid, then you need to set the ajaxUpdate - http://www.yiiframew...axUpdate-detail

On the ajax call you just need to return all the containers that are needed to be replaced and all other will do the update method.

So by this solution... your grid.php code would be like this:

<?php

$id = $data['id'];
$gridid = 'grid-' . $id;  // Id of the grid
$gridajaxkey = 'grid';  // Id for updating the grid

if(!Yii::app()->request->isAjaxRequest)
{
	echo CHtml::dropDownList('eid', $id, 
		array(
			1 => 'Entity A',
			4 => 'Entity B'
		),
		array(
			'encode' => false,
			'onchange' => "$.fn.yiiGridView.update('grid-test',{
					data: {'id':this.value}
				});"
		)
	);
}
	echo '<div id="entity-detail">';

	echo '<div id="entity-detail-title" style="font-size:40px">';
	echo Yii::t('app','Detail for {displayname}',array('{displayname}' => $data['displayname']));
	echo '</div>';
	echo '<div id="entity-detail-content">';
	//$dataprovider = new CActiveDataProvider('Tracker');

	$dataprovider = new CArrayDataProvider($data['tags'],
			array(
				'id' => 'id',
				'pagination' => array(
					'pageSize' => 2,
				),
		));
	$grid = $this->widget('zii.widgets.grid.CGridView',array(
		'dataProvider' => $dataprovider,
		'id' => 'grid-test',
		'ajaxUpdate'=>'entity-detail-title',
		'template' => "{items}\n{pager}",/* Default: '{summary}\n{items}\n{pager}' */
		'ajaxUrl' => $this->createUrl(''),
		'columns' => array(
			'id',
			'tag',
			array_replace_recursive(
				array('class' => 'CButtonColumn',),//Yii::app()->params['defaultCButtonColumnConfig'],
				array(
				'template' => '{U} {D}',
				'buttons' => array(
					'U' => array(
						'click' => 'js:function(){alert("update clicked");return false;}',
					),
					'D' => array(
						'url' => 'Yii::app()->controller->createUrl("deletealert",array("id"=>$data["id"]))',
						'click' => 'js:function(){alert("delete clicked");return false;}',
					),
				),
				)
			)
		)
		)
	);
	echo '</div>';
?>


And here is the controller with some warnings/bugs fixed (the second element of the sampleData does not have the IDs)

<?php

class ExampleController extends Controller
{

	public function getSampleData()
	{
		return array(
			1 => array(
				'displayname' => 'Entity A',
				'id' => 1,
				'tags' => array(
					array('id' => 1,'tag' => 'tag a'),
					array('id' => 2,'tag' => 'tag b'),
					array('id' => 3,'tag' => 'tag c'),
					array('id' => 4,'tag' => 'tag d'),
					array('id' => 5,'tag' => 'tag e'),
				),
			),
			4 => array(
				'displayname' => 'Entity B',
				'id' => 4,
				'tags' => array(
					array('id' => 6,'tag' => 'tag f'),
					array('id' => 7,'tag' => 'tag g'),
					array('id' => 8,'tag' => 'tag h'),
					array('id' => 9,'tag' => 'tag i'),
					array('id' => 10,'tag' => 'tag j'),
				),
			),
		);
	}

	public function actionGrid()
	{
		$view = 'grid';
		$id = Yii::app()->request->getParam('id',1);
		$data = null;
		if(is_numeric($id))
		{
			$alldata = $this->getSampleData();
			$data = $alldata[$id];
		}

		if($data === 'undefined')
		{
			echo 'Bad id';
		}
		else
		{
			$params = array('data' => $data);
			if(Yii::app()->request->isAjaxRequest)
			{
				$this->renderPartial($view,$params,false,true);
				Yii::app()->end();
			}
			else
			{
				$this->render($view,$params);
			}
		}
	}

}

Find more about me.... btw. Do you know your WAN IP?
0

#3 User is offline   le_top 

  • Standard Member
  • PipPip
  • Yii
  • Group: Members
  • Posts: 294
  • Joined: 08-June 10
  • Location:France

Posted 09 January 2012 - 11:02 AM

Hi mdomba

Thanks for taking the time to reply.

View Postmdomba, on 09 January 2012 - 09:47 AM, said:

There are different solutions to this... but adding off()/undelegate()/die() is not optimal at all... why remove a handler just so that you can add the same handler again.

Either you avoid adding it twice, either you remove any existing one first and then always add the handler. The handler would only be removed when it is not the first time this code is executed (because there is none).
I just think that Yii would be more robust with a solution for this.
Also, undelegating is not new, this is in the 'jquery.yiigridview.js' file:
			$('body').undelegate(inputSelector, 'change').delegate(inputSelector, 'change', function(){
				var data = $(inputSelector).serialize();
				if(settings.pageVar!==undefined)
					data += '&'+settings.pageVar+'=1';
				$.fn.yiiGridView.update(id, {data: data});			});


View Postmdomba, on 09 January 2012 - 09:47 AM, said:

A solution would be to not process the output when using renderpartial but that sometime is not possible as you need some script processed...

Yep, in the real case there are also some jQuery UI elements that do not render correctly if there is no postprocessing.

View Postmdomba, on 09 January 2012 - 09:47 AM, said:

In the your code there is one more problem if you noticed... on every ajax call jquery.js, jquery-bbq.js and jquery.yiigridview.js are loaded again and again.
For this a solution would be to check if it's an ajax call and if it is then to prevent loading of those files with the scriptmap.

Yes that is another issue, but I fixed that using an extension that does not load the duplicates. I did not overload the example with the fix for that.

View Postmdomba, on 09 January 2012 - 09:47 AM, said:

The best solution when using the grid is to use his update() method - $.fn.yiiGridView.update()
As you want to update some other parts of the page not only the grid, then you need to set the ajaxUpdate - http://www.yiiframew...axUpdate-detail
On the ajax call you just need to return all the containers that are needed to be replaced and all other will do the update method.

As the grid is contained in another div that gets updated under certain circumstances the idea is to update the entire div at once rather the several parts.

Sorry for forgetting to add the 'id' in the second part of the sample data.


Regarding your fixes:
- as the grid is contained in another div, I prefer updating the entire div rather than cutting it in smaller pieces.
- calling yiiGridView.update with a parameter does not seem to update the default settings ' $.fn.yiiGridView.settings[id]; '. I do not know the possible side effects from that.
- I trigger an 'id_change' event so that the UI element responsible for the change does not have to know what needs to change (which is handled in the listeners). I changed tje relevant part in my example code to this:
        $js=<<<EOD

        jQuery('body').bind('id_change',function (event,id) {jQuery.fn.yiiGridView.update('$gridid',{data: {'id':id}});})
EOD;


and I changed the $gridid to be fixed.
I the 'id' is encoded in the generated Urls so I did not see a side effect of not updating $.fn.yiiGridView.settings[id]



0

#4 User is offline   Maurizio Domba Cerin 

  • Yii - Yesss It Is !!!
  • Yii
  • Group: Yii Dev Team
  • Posts: 4,338
  • Joined: 12-October 09
  • Location:Croatia

Posted 09 January 2012 - 03:14 PM

As I wrote already there is no point in undelegating just to again delegate the same equal method... why do the repetitive work... what is delegate then for?

The undelegate you see in the gridview.js file was a quick fix in previous versions... that is gone in the yii 1.1.9 version as now the new on() method is used.

If to update several parts or just one div... it's up to the developer... you just need to set the ajaxUpdate with appropriate ID(s)... I don't see any problem with any of those... updating several (two in your case) divs makes sense if they are on different parts on the page... you don't get any much faster code if you update just one div instead of two...

I did not understant the problem about updating settings with the parameter... but maybe you mean the gridID... I noticed in your code that you are changing the grid ID on ajax calls.... that is not good... a DOM element should always keep his ID, especialy if some events are binded or delegated to it.
Find more about me.... btw. Do you know your WAN IP?
0

#5 User is offline   le_top 

  • Standard Member
  • PipPip
  • Yii
  • Group: Members
  • Posts: 294
  • Joined: 08-June 10
  • Location:France

Posted 09 January 2012 - 03:33 PM

View Postmdomba, on 09 January 2012 - 03:14 PM, said:

As I wrote already there is no point in undelegating just to again delegate the same equal method... why do the repetitive work... what is delegate then for?

The delegate is still useful for updates of the grid data itself.

View Postmdomba, on 09 January 2012 - 03:14 PM, said:

I did not understant the problem about updating settings with the parameter...

The grid is initialized using a generated call like this:
[font=Consolas,]jQuery[/font][font=Consolas,]([/font][font=Consolas,]'#grid-1'[/font][font=Consolas,]).[/font][font=Consolas,]yiiGridView[/font][font=Consolas,]({[/font][font=Consolas,]'ajaxUpdate'[/font][font=Consolas,]:[[/font][font=Consolas,]'grid-1'[/font][font=Consolas,]],[/font][font=Consolas,]'ajaxVar'[/font][font=Consolas,]:[/font][font=Consolas,]'ajax'[/font][font=Consolas,],[/font][font=Consolas,]'pagerClass'[/font][font=Consolas,]:[/font][font=Consolas,]'pager'[/font][font=Consolas,],[/font][font=Consolas,]'loadingClass'[/font][font=Consolas,]:[/font][font=Consolas,]'grid-view-loading'[/font][font=Consolas,],[/font][font=Consolas,]'filterClass'[/font][font=Consolas,]:[/font][font=Consolas,]'filters'[/font][font=Consolas,],[/font][font=Consolas,]'tableClass'[/font][font=Consolas,]:[/font][font=Consolas,]'items'[/font][font=Consolas,],[/font][font=Consolas,]'selectableRows'[/font][font=Consolas,]:[/font][font=Consolas,]1[/font][font=Consolas,],[/font][font=Consolas,]'url'[/font][font=Consolas,]:[/font][font=Consolas,]'/workspace/locbox/access/index.php?r=example/grid&id=1'[/font][font=Consolas,],[/font][font=Consolas,]'pageVar'[/font][font=Consolas,]:[/font][font=Consolas,]'id_page'[/font][font=Consolas,]});[/font]

This initialisation is stored in a yiiGridView.settings array and referenced in several locations. The 'url' may need to change as the 'id' changes.

View Postmdomba, on 09 January 2012 - 03:14 PM, said:


but maybe you mean the gridID... I noticed in your code that you are changing the grid ID on ajax calls.... that is not good... a DOM element should always keep his ID, especialy if some events are binded or delegated to it.

The DOM Element (the grid) is in a div that sees its content replaced entirely. The purpose of having a specific id is to allow multiple grids on the same page.
0

#6 User is offline   Maurizio Domba Cerin 

  • Yii - Yesss It Is !!!
  • Yii
  • Group: Yii Dev Team
  • Posts: 4,338
  • Joined: 12-October 09
  • Location:Croatia

Posted 09 January 2012 - 03:55 PM

When I asked what is delegate for I was thinking in pure jQuery terms... in jQuery delegating means binding a custom code to an event even for future updates of the DOM elelement... that was introduced so that a developer can delegate his code only one time for all the future changes of the grid... by undelegating and again delegating you are breaking the main idea of that method.... we can talk abou this on and on... but it leads nowhere.. and there are solutions for this without doing it this way.

If you take any DOM element and change its ID... that always the same DOM element not a new one.
In the above example you don't have multiple grids... you have one grid to whom you want to change the ID - this are not multiple grids...

In the end.. I made a working solution... it works without the need to change the grid ID... the pagination works too... so it's up to you now if you want to build upon that solution or not..
Find more about me.... btw. Do you know your WAN IP?
0

#7 User is offline   le_top 

  • Standard Member
  • PipPip
  • Yii
  • Group: Members
  • Posts: 294
  • Joined: 08-June 10
  • Location:France

Posted 09 January 2012 - 04:06 PM

In jQuery terms: the 'on' AND the 'off' method both exist.

I agree that when you change the id of a DOM element it is still the same DOM element. But when you replace the content of a '<div>' the DOM elements that were in the '<div>' no longer exist do they? So any event attached directly to those get lost. As the events created with 'on' are attached to the body these continue to exist even after the replacement.

So maybe attaching the 'on' events to the uppermost level of the grid may be a generic solution. When the grid disappears, the events should disappear too.
Attaching the events related to the grid to the grid itself rather than the body, that should be ok - maybe that is what is been done for 1.1.9.
0

#8 User is offline   edwaa 

  • Junior Member
  • Pip
  • Yii
  • Group: Members
  • Posts: 21
  • Joined: 17-May 09
  • Location:Seattle, WA

Posted 25 May 2012 - 04:30 AM

I'm having an issue with this and using 1.1.9... if an ajax call returns all the code necessary for a GridView, and you click whatever button does this ajax call multiple times, you get a bunch of popup confirmations when you click the default delete button because of this code getting sent multiple times:

jQuery('#product-grid a.delete').live('click',function() { ... }); return false;


I've tried putting this in my code before the GridView gets placed:

jQuery('#product-grid a.delete').die('click');


... but it doesn't work. Any ideas?
0

#9 User is offline   le_top 

  • Standard Member
  • PipPip
  • Yii
  • Group: Members
  • Posts: 294
  • Joined: 08-June 10
  • Location:France

Posted 25 May 2012 - 05:13 AM

The Javascript code should be using 'on' in stead of 'live'.

In the 'CButtonColumn' class, in 'registerScripts', I have put:
if(true) {
				$class=preg_replace('/\s+/','.',$button['options']['class']);
                $js[]="if(typeof(_gridf)==='undefined'){_gridf={};}"
                ."if(typeof(_gridf['on-{$this->grid->id}-{$class}'])!=='undefined') {jQuery(document).off('click','#{$this->grid->id} a.{$class}',_gridf['on-{$this->grid->id}-{$class}']);}"
                ."_gridf['on-{$this->grid->id}-{$class}']=$function;"
                ."jQuery(document).on('click','#{$this->grid->id} a.{$class}',_gridf['on-{$this->grid->id}-{$class}']);";
} else {
				$class=preg_replace('/\s+/','.',$button['options']['class']);
				$js[]="jQuery('#{$this->grid->id} a.{$class}').live('click',$function);";
}



I do not remember if I wrote the alternative code or if I got it from somewhere.
It does some accounting regarding 'on' events that are already registered, switches them off if they exist and then recreate the event handler.
The 'else' part is never executed (it is the original code).

I know that when I do not have this code, I have the same issues as you have.
1

#10 User is offline   Philipp 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 8
  • Joined: 13-July 12

Posted 19 July 2012 - 05:09 PM

Is there any official solution in sight? Loading stuff via AJAX still breaks a lot of things which can be tackled by some extensions but not fully solved.
0

#11 User is offline   le_top 

  • Standard Member
  • PipPip
  • Yii
  • Group: Members
  • Posts: 294
  • Joined: 08-June 10
  • Location:France

Posted 29 August 2012 - 05:17 PM

For information, I just applied this change again after upgrading to version 1.12.
0

Share this topic:


Page 1 of 1
  • You cannot start a new topic
  • You cannot reply to this topic

1 User(s) are reading this topic
0 members, 1 guests, 0 anonymous users