Dublicate live event handlers on ajax

I speak about full-ajax applications, not about ajax webpages. Yes, it is possible to create the ajax applications in hardy and slower way. But in situation when your have about 100 widgets/blocks on page your need to implement 100 actions/100 checks in the best way. So your still can load the whole page(this is a simple solution) and it doen’t matter nothing. Now i am not sure that the page which is loaded over ajax will be with the same id’s as the previous page (still also present a few yii id’s in the main layout and others). If i want to wrote an ajax application i must control all id’s on the all pages and all events delegating. The right solution in:

  1. Use as default the bind method and intelligent delegating for attaching events(use only that container’s for delegating what is the root of the widget).

  2. Or use undelegating before delegating as behavior.

I think that the first way is the simplest for all. Do your have any ideas ?

CActiveForm works perfectly as example(because the context for attaching events is the form as your use), but CButtonColumn not yet.

I have tried to start from scratch and came to somehow similar code to what is in the test case provided. Current CHtml live-die is not the way I think it should work and Maurizio was right stripping it down from grid view. Also I think properly delegating event handling to widget container instead of html body would not hurt. Will be more efficient and cleaner at least.

I will continue working on it.

If you want I’m willing to participate in your working…

I would like that we solve this problem and give out proper usage documentation for this kind of situations… as this seems the most critical problem for "advanced" users…

The latest jQuery version deprecated live()… introduced the on()/off() method that replaces bind(), live() and delegate() and it’s advised to change to that function…

on() is very good documented and now it’s more clear on how live() worked and how on() is working…

One of the main problems in all this examples is that the user/programmer/developer has to "know" if he needs "live" binding or not…

"Live" (we should find a better word for this) binding in those cases should be used for elements that could appear later (by some ajax call)… but at the same time do NOT set again the event… .as in that case the events stack up and we get what we get (same function calling grows exponentially)…

If the page/code returned by the ajax call returns even the code that binds the event… then "normal" binding should be used not the live one… as in those cases the binding will work until the element is replaced by the next ajax call…

So in conclusion… IMO there is no way the framework can solve this for the user… it’s up to the user to know when to use the live binding and when not… that was th point of adding that parameter to the htmlOptions in the first place…

Attaching to the widget container instead of the body is a way to go for sure… as this will even speed up a bit the jQuery processing of the events… but again care should be taken on how to make the binding "live" or not… the ideal solution would be to keep the container all the time and change only the content on every ajax call… this way we just need to "live" bind one time on the live container… and care should be taken to not return another "binding" code with the ajax request…

2mdomba, no-no, that’s not right way in case of it is only acceptable for widgets but not for whole page. As i explain earlier the simplest way to solve the problem in all situations is in use binding way for all default clientChange generated code and proper delegating on root widget container(the “on” method in jq also represent this feature). This behavior will also work when the widget content updated by ajax request. Also need to control and test preventDefault in all yii’s events.

Ekstazi, can you elaborate more on your idea, I did not get it completely?

If you check the jQuery new documentation they always advice to NOT bind on the body elements as this brings to slow event processing (that was how live() worked natively and it’s the reason why it’s deprecated)

The preventDefault is not good to depend on it on all situations… as a custom user code can fire it…

Yes, because of this i want to remove the




	/**

	 * This class was extended on purpose to show incorrect behavior w/o undelegate fix.

	*/

	class CHtml {

		// ...

		/**

		 * Generates the JavaScript with the specified client changes.

		 * @param string $event event name (without 'on')

		 * @param array $htmlOptions HTML attributes which may contain the following special attributes

		 * specifying the client change behaviors:

		 * <ul>

		 * <li>submit: string, specifies the URL that the button should submit to. If empty, the current requested URL will be used.</li>

		 * <li>params: array, name-value pairs that should be submitted together with the form. This is only used when 'submit' option is specified.</li>

		 * <li>csrf: boolean, whether a CSRF token should be submitted when {@link CHttpRequest::enableCsrfValidation} is true. Defaults to false.

		 * This option has been available since version 1.0.7. You may want to set this to be true if there is no enclosing

		 * form around this element. This option is meaningful only when 'submit' option is set.</li>

		 * <li>return: boolean, the return value of the javascript. Defaults to false, meaning that the execution of

		 * javascript would not cause the default behavior of the event. This option has been available since version 1.0.2.</li>

		 * <li>confirm: string, specifies the message that should show in a pop-up confirmation dialog.</li>

		 * <li>ajax: array, specifies the AJAX options (see {@link ajax}).</li>

		 * <li>live: boolean, whether the event handler should be bound in "live" (a jquery event concept). Defaults to true. This option has been available since version 1.1.6.</li>

		 * </ul>

		 * This parameter has been available since version 1.1.1.

		 */

		protected static function clientChange($event,&$htmlOptions)

		{

			if(!isset($htmlOptions['submit']) && !isset($htmlOptions['confirm']) && !isset($htmlOptions['ajax']))

				return;

// Only for compatibility with previous versions of yii

			$live=!empty($htmlOptions['live']);


			if(isset($htmlOptions['return']) && $htmlOptions['return'])

				$return='return true';

			else

				$return='return false';


			if(isset($htmlOptions['on'.$event]))

			{

				$handler=trim($htmlOptions['on'.$event],';').';';

				unset($htmlOptions['on'.$event]);

			}

			else

				$handler='';


			if(isset($htmlOptions['id']))

				$id=$htmlOptions['id'];

			else

				$id=$htmlOptions['id']=isset($htmlOptions['name'])?$htmlOptions['name']:self::ID_PREFIX.self::$count++;


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

			$cs->registerCoreScript('jquery');


			if(isset($htmlOptions['submit']))

			{

				$cs->registerCoreScript('yii');

				$request=Yii::app()->getRequest();

				if($request->enableCsrfValidation && isset($htmlOptions['csrf']) && $htmlOptions['csrf'])

					$htmlOptions['params'][$request->csrfTokenName]=$request->getCsrfToken();

				if(isset($htmlOptions['params']))

					$params=CJavaScript::encode($htmlOptions['params']);

				else

					$params='{}';

				if($htmlOptions['submit']!=='')

					$url=CJavaScript::quote(self::normalizeUrl($htmlOptions['submit']));

				else

					$url='';

				$handler.="jQuery.yii.submitForm(this,'$url',$params);{$return};";

			}


			if(isset($htmlOptions['ajax']))

				$handler.=self::ajax($htmlOptions['ajax'])."{$return};";


			if(isset($htmlOptions['confirm']))

			{

				$confirm='confirm(\''.CJavaScript::quote($htmlOptions['confirm']).'\')';

				if($handler!=='')

					$handler="if($confirm) {".$handler."} else return false;";

				else

					$handler="return $confirm;";

			}

// Only for compatibility with previous versions of yii

			if($live)

				$cs->registerScript('Yii.CHtml.#'.$id,"jQuery('body').on('$event','#$id',function(){{$handler}});");

			else

				$cs->registerScript('Yii.CHtml.#'.$id,"jQuery('#$id').on('$event', function(){{$handler}});");

			unset($htmlOptions['params'],$htmlOptions['submit'],$htmlOptions['ajax'],$htmlOptions['confirm'],$htmlOptions['return'],$htmlOptions['csrf']);

		}


	}



I wrote about something like this. Also i want to test all yii widgets. I’ll explain my results later.

Your can also deprecate old live using. (See my comments)

This code just changes from delegate to on… but it will not solve all problems… you will still get duplicate events or not ?

we need still to have the “live” option… as I explained before… depending on usage sometime you need it sometime not… it’s up to the user to decide what type of binding he needs and wants to use…

you need to think about other users usage… .do not think only on your case…

Ok.




	/**

	 * This class was extended on purpose to show incorrect behavior w/o undelegate fix.

	*/

	class CHtml {

// new option

		public static $useLiveEvents=true;

		

		/**

		 * Generates the JavaScript with the specified client changes.

		 * @param string $event event name (without 'on')

		 * @param array $htmlOptions HTML attributes which may contain the following special attributes

		 * specifying the client change behaviors:

		 * <ul>

		 * <li>submit: string, specifies the URL that the button should submit to. If empty, the current requested URL will be used.</li>

		 * <li>params: array, name-value pairs that should be submitted together with the form. This is only used when 'submit' option is specified.</li>

		 * <li>csrf: boolean, whether a CSRF token should be submitted when {@link CHttpRequest::enableCsrfValidation} is true. Defaults to false.

		 * This option has been available since version 1.0.7. You may want to set this to be true if there is no enclosing

		 * form around this element. This option is meaningful only when 'submit' option is set.</li>

		 * <li>return: boolean, the return value of the javascript. Defaults to false, meaning that the execution of

		 * javascript would not cause the default behavior of the event. This option has been available since version 1.0.2.</li>

		 * <li>confirm: string, specifies the message that should show in a pop-up confirmation dialog.</li>

		 * <li>ajax: array, specifies the AJAX options (see {@link ajax}).</li>

		 * <li>live: boolean, whether the event handler should be bound in "live" (a jquery event concept). Defaults to true. This option has been available since version 1.1.6.</li>

		 * </ul>

		 * This parameter has been available since version 1.1.1.

		 */

		protected static function clientChange($event,&$htmlOptions)

		{

			if(!isset($htmlOptions['submit']) && !isset($htmlOptions['confirm']) && !isset($htmlOptions['ajax']))

				return;


			$live=self::$useLiveEvents ? (isset($htmlOptions['live']) ? $htmlOptions(['live'] : true) : !empty($htmlOptions['live']);


			if(isset($htmlOptions['return']) && $htmlOptions['return'])

				$return='return true';

			else

				$return='return false';


			if(isset($htmlOptions['on'.$event]))

			{

				$handler=trim($htmlOptions['on'.$event],';').';';

				unset($htmlOptions['on'.$event]);

			}

			else

				$handler='';


			if(isset($htmlOptions['id']))

				$id=$htmlOptions['id'];

			else

				$id=$htmlOptions['id']=isset($htmlOptions['name'])?$htmlOptions['name']:self::ID_PREFIX.self::$count++;


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

			$cs->registerCoreScript('jquery');


			if(isset($htmlOptions['submit']))

			{

				$cs->registerCoreScript('yii');

				$request=Yii::app()->getRequest();

				if($request->enableCsrfValidation && isset($htmlOptions['csrf']) && $htmlOptions['csrf'])

					$htmlOptions['params'][$request->csrfTokenName]=$request->getCsrfToken();

				if(isset($htmlOptions['params']))

					$params=CJavaScript::encode($htmlOptions['params']);

				else

					$params='{}';

				if($htmlOptions['submit']!=='')

					$url=CJavaScript::quote(self::normalizeUrl($htmlOptions['submit']));

				else

					$url='';

				$handler.="jQuery.yii.submitForm(this,'$url',$params);{$return};";

			}


			if(isset($htmlOptions['ajax']))

				$handler.=self::ajax($htmlOptions['ajax'])."{$return};";


			if(isset($htmlOptions['confirm']))

			{

				$confirm='confirm(\''.CJavaScript::quote($htmlOptions['confirm']).'\')';

				if($handler!=='')

					$handler="if($confirm) {".$handler."} else return false;";

				else

					$handler="return $confirm;";

			}


			if($live)

				$cs->registerScript('Yii.CHtml.#'.$id,"jQuery('body').on('$event','#$id',function(){{$handler}});");

			else

				$cs->registerScript('Yii.CHtml.#'.$id,"jQuery('#$id').on('$event', function(){{$handler}});");

			unset($htmlOptions['params'],$htmlOptions['submit'],$htmlOptions['ajax'],$htmlOptions['confirm'],$htmlOptions['return'],$htmlOptions['csrf']);

		}


	}



This solution will be better ?

please… in the future… post only the changed code… I need to check the whole function to find just the line you changed… and remove the not needed comments - (19 lines in the header)

what is the point of the global useLiveEvents… why not leaving it as it is now… by default is true… if you dont want it… you set the htmlOption[‘live’] to false…

users that want this constantly off… can extend the clientscript and set it to false…

This is the CHtml code. Now i can’t globally remap events attaching method in fully completed non ajax yet project. What i can do ? I need to find all places where not use bind and fix all cases ? Or i can set one global options in CHtml and don’t worry about this ?

I’ll post my initial code to github very soon. Let’s not change default CHtml or widgets initially and try to achive so called fullajax without much serverside and clientside core rewrites. This way we’ll both fix framework-specific things preventing developers from implementing it and will have a good base for tutorial/extensions.

i agree that we should try to not change the core too much… as it should solve all user cases…

Ekstazi… instead of adding the global parameter that would be used "maybe" just by you… you can extend the clientscript something like




class EHtml extends Chtml

{

   protected static function clientChange($event,&$htmlOptions)

   {

      $htmlOptions['live']=false;

  	parent::clientChange($event,$htmlOptions);

   }

}



This way if you use EHtml the "live" is off globaly…

I also must to replace all CHtml usages to EHTml. Think that is not good idea. As earlier tell Sam Dark he also want to find the current trubbles in full-ajax apps. So i’m waiting for him.

And because of that i can’t globally remap all CHtml globaly usages to EHtml. I know what it will be available in yii2, but in current yii version i need to hardcode these things. I understand what your want. But i didn’t find yet another way for simple convertatation from non ajax to ajax. I mean ajax update for whole page(not in each case, but in some cases). Yes, i can write manually do not to use CHtml, but this is the not good way. In fact of this i ask your to add globally controlling of live option in CHtml as a simplest way for solution. I’m not laziness, but i want to do all correct. Do you understand me ?

Some extensions for yii (also linked with svn) use CHtml::clientChange. If i want to fix it i must get off svn supporting (from the author of extension).

Here’s the very basic fullajax app: https://github.com/samdark/yii-fullajax Works for simple links and properly loads inline JS.

The only third-party thing used is NLSClientScript that prevents scripts and css to load twice if these were already loaded. I’ve checked it in detail and I’m sure it doesn’t produce any side effects.

Known bugs

  • confirm() doesn’t work. Starts loading page before getting answer.

2Sam, cpnfirm also works. Your example works correct. But if core yii team decide to exclude undelegating before delegating it will be a big trubble not only for me. Because of that i was started this issue.