Dublicate live event handlers on ajax

2Sam, with this fragment of code your project works correct:




			$('body').delegate("a","click",function loader(e){

				if(e.isDefaultPrevented()){

					return;

				}

				var $this=$(this);

				if(!$this.attr("noajax")){

					$("body").undelegate("a","click",loader);

					console.log($("body").data('events'));

					$("#container").load($this.attr("href"),function(){

						$('body').delegate("a","click",loader);

					});

					return false;

				}

			});



I updated my sample project. Fix some mistakes. With current version of yii the project must work fine. But as i say earlier if you want to strip out undelegating before delegating i’ll have a trubble.

Now i work on extension wich simply converts any non ajax application to full ajax. A small part of my extension i use in sample.

Actually I don’t understand some fragments of this code…

  1. As I understand, if(!$this.attr(“noajax”)){ is just an addition to be able to force normal no-ajax way so it doesn’t matter in our case.

  2. I don’t see a valid reason to re-delegate each time content is loaded.




<div id="container">

  !!!content!!!

</div>



We’re delegaing link clicking to #container that is never being reloaded. As you know, delegate is based on DOM event bubbling so it doesn’t matter if we’re reloading content. Delegation should work fine.

It to hard to explain the problem in english. I try to explain in Russian. Sam Dark, can you explain the problems to mdomba ?

Как я понял команда yii хочет отказаться от undelegate перед delegate. Такой отказ вызовет ряд проблем в уже существущих аякс приложения. Как я уже писал раннее, в некоторых аякс приложениях обновляется целиком вся страница посредством renderPartial. И, для корректной работы этих приложений, необходимо(или-или):

  1. undelegate перед delegate, иначе события будут дважды навешаны на элемент. Однако, и при этом возникает ряд проблем.

Допустим мы повесили обработчик типа:




$("#body").delegate("a","click",function(e){

if(e.isDefaultPrevented()) return;

$("#content").load(this.href);

});



Рассмотрим что же происходит когда мы используем такой код. Но, сперва необходимо пояснить принцип delegate: обработчики обрабатываются сперва в порядке возврастания уровня контейнера в иерархии, а также внутри каждого из контейнеров delegate событий в порядке добавления этих событий. Отсюда следует, что для корректной работы тестового кода необходимо чтобы он вешался на body(так как CHtml::clientChange делегирует события на body и на этапе баблинга событие просто не достигнет нужных обработчиков) и чтобы был повешен после всех обработчиков, в противном случае он будет брать все события на себя и не давать другим обработчикам шанс выполниться. Отсюда и вытекает тот код что я опубликовал ранее. Допустим что undelegate перед delegate все же нет. Тогда, с любым из вариантов обработчика всех ссылок(правильным и нет) возникнут след. ситуации: "обработчик элемента"->"наш обработчик"->"дубли обработчика элемента"(1а) или "наш обработчик"-> "дубли обработчика элемента"(1б) для некорректного обработчика всех ссылок и "обработчик элемента"->"дубли этого же обработчика"->"наш обработчик всех ссылок"(2) для корректного. В некоторых случаях(1а и 2), когда обработчик элемента прерывает bubbling или возвращает false, то, казалось бы, все будет работать правильно, но, с большими утечками памяти. Во всех же остальных случаях либо после нашего обработчика уже ни один обработчик элемента не будет выполняться(1б), либо будут утечки памяти и риск мнодественной обработки одного и того же события одним и тем же обработчиком(то есть одним и тем же кодом обработчика, функции то разные). То есть возникнет так называемый geisen bug и утечки памяти. Более правильный вариант решения это вариант 2. Мало того, в текущей документации yii плохо раскрыт факт разницы между delegate и bind, в частности, в чем отличия при return false внутри обработчика для каждого из случаев. Так же стоит подчеркнуть, что delegate работает медленней чем bind.

  1. Использовать bind вместо delegate. Однако, в виду некоторых особенностей yii, для этого требуется ручной поиск всех мест где хелпер CHtml с использованием clientScript и явно не указано использование bind-а. Для этого требуется доп. анализ кода и это очень трудоемкий процесс. И после того как такие места выявлены, везде необходимо указать live=>false. Проблема вроде бы решается через свой хелпер, EHtml например, который переопределит реализацию clientChange. Однако из-за того что в php5.3 есть late static binding, то есть простое переопределение одного статического метода в подклассе(потомке CHtml) не поможет. Для решения проблемы через свой хелпер придется продублировать все методы из CHtml использующие clientChange c явным указанием не использовать live и вызовом родительской реализации. Переопределение поведения clientChange при это не требуется.

Вполне очевидно, что, учитывая моменты в 1 и 2, а так же и то, что jq пререшло на новый интерфейс - on/off и проблемы с delegate, описанные выше логично отказаться от delegate/сделать отключенным его по-умолчанию(+желательно иметь возможность указывать делигируемый контейнер)/добавить статическое св-во в CHtml ответственное за отключение delegate кроме принудительных случаев.

Еще раз подчеркну - да, сейчас все работает отлично. Меня все устраивает. Но, я раскрыл ряд моментов которые стоит учесть, иначе будут описанные проблемы.

Wow, that’s long one. Here’s the translation of the part I understand/agree with:

As I understand, Yii team members are going to get rid of calling undelegate() before delegate(). It will cause problems in existing applications. As I already mentioned, in some application the whole content area is being updated via AJAX call that’s getting content from Yii’s renderPartial. In order for this type of applications to work correctly you need either:

  1. undelegate() before delegate(). In case we’ll not do it event will be bound twice leading to some problems. Let’s assume we have event handler like:



$("#body").delegate("a","click",function(e){

  if(e.isDefaultPrevented()) return;

  $("#content").load(this.href);

});



First of all we need to make sure we understand how delegate works. Event bubbles from current element to the body. If there are multiple handlers bound to the same DOM node then events are executed in the same order they were added.

That means that in order for the test code to work correctly:

  1. It needs to delegate to body. That’s because CHtml::clientChange is delegating events to the body and when event bubbles our handler prevents it from propogating further and getting to the original handler.

  2. Our handler needs to be registered after all application handlers. If not then it will handle all events itself and will prevent other handlers to do their job.

That’s why I’ve implemented example the way I did.

Updated test application https://github.com/samdark/yii-fullajax. Code is simpler than Ekstazi’s one. I’ve delegated to <html> instead of <body> and was able to proceed w/o undelegate/delegate on each content reload. Also added more examples and confirmed that:

  1. Delegate to body w/o undelegate will lead to event handler duplication. That’s because body doesn’t reload.

  2. Live doesn’t work for some reason.

  3. On works fine.

Solution for #1 seems to be delegate only to widget container instead of <body> if it’s a widget and get rid of delegation for ajaxLinks and other CHtml-related things in favor of simple events binding.

Thx Sam, as i told yesterday i didn’t test yet the handler of links on html container.(Do you tested your solution in all browsers ?) Thank for this testing. I’ve tried to translate rest of my post.

If you want to drop undelegate supporting that means “heisenbug” affects(code will work correct only when attached handler with doubles return false/cancel bubbling) and memory leaks for many applications. Also delegate slower than bind in case of cheking context selector. Also developer must have a way to control of delegating container(body or him’s container like #container)/

The second way to create fullajax application is in use bind() instead of delegate(). But because of CHtml::clientChange() nuanses developer must find all parts of code that use clientChange nativelly/manually(also CActiveForm, some yii widgets and extensions) and explicitly use in these parts bind(exclude some moments when delegate() must be used). Make modifications in yii or yii extensions is not the good way. Developer also can use its EHtml helper instead of CHtml but in case of late static binding in php5.3 he must override all static methods of CHtml that use clientCHange().

Think that there are many ways to exclude these nuanses from yii:

  1. drop delegate() from CHtml::clientChange()

  2. disable delegate().

  3. add an mechanisme to allow/deny(enable/disable) delegate(). In both of cases delegate() must be allowed w/wo option in $htmlOptions

Jq also mentioned the new on/off method for attaching events and in next versions support of bind() and delegate() can be dropped.

Now all works fine (exclude minimal affects like mem leaks and others). But this part of code must be reorganized because of updates in jq. I wrote about potential affects.

Please explain more about the memory leaks you mentioned…

Guys we are on the 3 page in this thread… and I just feel we are turning in a circle here…

About if to drop undelegate()… IMO there is no more question… we need to drop it… I already wrote about that… and dont wont to repeat myself…

So if we agree on that… let’s work on finding a solution without undelegate()…

live() is deprecated… stop using it… you will do a favor to yourself and to all others :D

read about live() on jquery documentations… live woked in a way that all events where bubled to the top… delegate solved that a bit… but only on() made this as it should be… live() had and still has many problems just becasue of that he bubbled all events to the top… so by using delegate or on and delegating those events to the body we are doing almost the same thing… I’m not sure this is good… as for one thing… this is not advised by the jQuery team…

so I would suggest we just start using on() instead of bind()/live()/delegate() and find a solution with on()

Ekstazi

can you explain me why are you doing that full ajax app’s in this way, by converting all “a” links to a “$.load()”?

This way you don’t really get a full ajax application… you just simulate it (IMO)… and this can have other problems… on example is the delete button in CGridView…

but I have many other examples… where a user writes his custom jQuery/ajax code… that would not work wiht your solution at all.

Above that… I don’t see what you get by leaving just the header and footer and loading all the content on every link… that is not the point of ajaxed apps…

In general about this problem…

Why is undelegate() needed in those situation ?

Problem is that on the current page a handler is binded for an event to a DOM element… now on ajax call a new page is returned with the code that binds the same handler to the same event for the same DOM element (a duplicate)… when this is executed… the new returned handler is attached to the same DOM element…

this way we get two equal handlers attached to the same DOM element for the same event… and when the event is fired (for exmple a click)… two equal handlers are executed (for example same confirmation dialogs)

to fix this at first in Yii we added the undelegate before delegate so to cancel the first event before adding the net (equal one) but this make other side-effects and is not an optimised code… and in certain suituations it can undelegate code that should not be…

But the main problem here IMO is that if a handler is already binded to the DOM event… why would we undelegate it and than again delegate it when it’s always the same handler for the same DOM element… aren’t we doing here a sisyphean work?

So there are 2 theoretical solutions:

  • one is not to "live" bind - this way the first handler works until his original element in in the DOM, when on ajax call the same element is returned that is a new element for the DOM, so the first handler is not working anymore, and the second one can be registered without doubling handlers

  • second (and best IMO) would be not to return the binding code for the next page… .why delegating again the same handler to the same event if it is already there

The above link doesn’t work as it includes the dot, this is the correct one https://github.com/samdark/yii-fullajax

Delegate() is just the alias for live() so i know between the differences(and please do not put into question my competence in this matter).

I think that at first the CHtml::clientChange() must be reorganized and use new $.on() like this:




/** @var $cs CClientScript */

if(!empty($htmlOptions['container'])){

$cs->registerScript("yii.#.$id","$($htmlOptions['container']).on($event,#$id,$func)");

}else {

$cs->registerScript("yii.#.$id","$(#$id).on($event,$func)");

}



But what you will do with older delegating support and $htmlOptions[‘live’] ? Think that the simplest and best way is the first (use $.on() in “bind style”). The second way is harder. For this:

  1. Each page must have unique ids for its elements. Client side must store all executed inline script for each page. And do not execute the code again.

  2. It also can be realized with our CClientScript (too hard to explain the algorithm) wich will control all inline scripts around the session. But it too hard to do it.

So the second way is slower than the first. On server and client too(because jq checks the selector for each event handler by RegExp around a container). In fact the $.delegate()(or $.on with selector) worse than $.bind(or $.on in bind style) and slower. There are a few cases when $.delegate() is needed. And use it is not the good way. So the first way is the best.

About memory leaks, please see the trubble realization of gridview.js. Why you are using $.on() for body container and not for gridView container(see line 57,64,73,95,114). This solution is slowest. You can use something like this:




var container=$(options.id);

container.on("click",inputSelector,func(){});

//...



And this code have memory leaks because the gridview doesn’t use $.off() before $.on()(This problems also presents in CButtonColumn too). As an example of fullajax please see http://fullajax.ru/

P.S.: I have a feeling that you don’ t want to continue the development of the framework… There are several tickets on this subject, I’ve been explaining the same thing in already 3 pages now, but you just don’t understand me. I can’t just use my subclass helper CHtml(I have already described why) and refuse from a half of the regular classes in yii(and extensions as well), and I am not going to follow the slowest way for the development of ajax too. And I’m not alone.

Participate in further discussion makes no sense. As long as you do not want to understand me (and even a couple of people who have written about it) will not solve the problem. And those solutions that will be - will be extremely slow and labor intensive.

I’m writing all the time that live() is deprecated and you come out with that?

Please check the jQuery version 1.7 or the current one 1.7.1 source… delegate is the alias for on() !

and bind and live also.

I really want to find a solution for your problem… but the problem is that you are not helping with this… you are constantly returning to old code… you want me to understant you (I do)… but at the same time you don’t want to understant all other posibility of use of the framework…

We cannot implement in the framework something that will work for you but introduce unnecesary code for all others…

Samdark has made a test app… did you try it… can we discuss on that app… and try to come with a solution or you don’t see a point in that ?

Of course it is… that’s why all those other methods are deprecated and will be removed in some furher jQ version… and you can now see what live does …

As for the second method… you got it wrong… the "second" code could be prevented to be rendered right from Yii… non need for any client code… I just was writing the theory and you jumped to the client implementation…

Why do you think I dont want to countinue to develo the pframework… it’s just becasuse of that… .that I don’t want the undelegate/delegate there…

Again you are looking just at your problem… not at all the possibilities…

I’m looking at all the possibilities…

Did you eve got arround a Yii application where the developer uses custom jQuery code / events…

You mention other extensions… well with your current code and method of "converting" to ajax app… you will have problems as soon as the extension developer creates a customn jQuery code/event where he binds to any other container that is not the body…

Yes, i tried Sam Dark project and helped Sam with advices. But Sam Dark didn’t used a normal main layout and used only the part of code that like to mine. A want to find the solution, but the solution is only in the bind way($.on() in bind style). All other solutions will produce memory leaks in some cases. So if you’ll have many links with events in one page the delegate way of on() will be slower than bind. The delegate(mean also the new on) do not needed in normal way, it needed only for special tasks like CButtonColumn, CActiveForm, CGridView… Why do you want to use only the delegate way as default or do not add an options to CHtml for switching method of attaching events?