renderPartial и дублирование jQuery event handler-ов

Общие вопросы по использованию фреймворка. Если не знаете как что-то сделать и это про Yii, вам сюда.
Ответить
X-Loading
Сообщения: 26
Зарегистрирован: 2011.11.15, 14:55

renderPartial и дублирование jQuery event handler-ов

Сообщение X-Loading »

Всем привет!

Сталкиваюсь уже не первый раз с проблемой, которая довольно часто поднимается, но тем не менее я лично так и не понял, каков наиболее корректный способ её решения.
Пишу админку, в которой многие части интерфейса загружаются через AJAX-вызовы и renderPartial, например есть такое меню:
Изображение

По клику на ссылку "Отзывы на рассмотрении" в правой части загружается грид (TbExtendedGridView из YiiBooster, но не суть важно):
Изображение

В гриде в свою очередь: кнопки view/update загружают диалоги (CJuiDialog), а кнопки Bulk соответственно призваны выполнять bulk-операции.
Проблема состоит в том, что каждый клик на ссылке "Отзывы на рассмотрении" приводит к увеличению числа js-handlers на всех кнопках. Т.е. новые хэндлеры создаются, а старые не удаляются.
Таким образом каждый клик по кнопкам в гриде приведёт сначала к 2-м запросам, потом к 3-м и так далее.

Текущий код кнопок в гриде:

Код: Выделить всё

$viewDialog =<<<'EOT'
function() {
    var url = $(this).attr('href');
    $.get(url, function(r){
        $("#viewDialog").html(r).dialog("open");
    });
    return false;
}
EOT;

$updateDialog =<<<'EOT'
function() {
    var url = $(this).attr('href');
    $.get(url, function(r){
        $("#updateDialog").html(r).dialog("open");
    });
    return false;
}
EOT;
...
'buttons'=>array(
                'view' => array(
                    'icon'  => 'view',
                    'label' => 'View',
                    'url'   => 'Yii::app()->createUrl("/admin/productfeedback/view", array("id"=>$data->id))',
                    'click'=>$viewDialog,
                ),
                'update' => array(
                    'icon'  => 'view',
                    'label' => 'View',
                    'url'   => 'Yii::app()->createUrl("/admin/productfeedback/update", array("id"=>$data->id))',
                    'click'=>$updateDialog,
                ),
            ),
...
Прочитал очень много различных обсуждений по данной проблеме и на данном, и на англоязычном форуме и так и не пришёл к какому-либо конкретному выводу, как именно следует её правильно решать без костылей...
По факту это относится вообще ко всему контенту, загружаемому AJAX-ом: например сами ссылки "Отзывы на рассмотрении" и т.п. у меня также загружаются асинхронно при переходе в пункт "Отзывы" вышестоящего меню и при повторной загрузке клики на них также приводят к дуплицированию запросов... :(

Заранее благодарен за любую помощь!
yan
Сообщения: 942
Зарегистрирован: 2011.03.23, 09:28
Откуда: Уфа

Re: renderPartial и дублирование jQuery event handler-ов

Сообщение yan »

Это еще хорошо что судя по изложенному проблем с повторно загруженными js и css файлами не возникает :) На самом деле похоже что стандартными средствами эту проблему решить нельзя, по крайней мере я нигде не нашел упоминание о подобном решении, последнее время решаю так - на фронте запоминаю были ли загружены обработчики, js и css, и если уже были загружены - добавляю в аджакс-запросы параметр по которому контроллер понимает, что надо выдать чистый контент.
X-Loading
Сообщения: 26
Зарегистрирован: 2011.11.15, 14:55

Re: renderPartial и дублирование jQuery event handler-ов

Сообщение X-Loading »

yan, с дуплицированием скриптов помогло справиться расширение nlsclientscript, только сегодня подключил и остался очень доволен его работой, рекомендую ;)
Ваша идея приблизительно ясна, но я не очень понимаю, как именно она применима к обработчикам...
yan
Сообщения: 942
Зарегистрирован: 2011.03.23, 09:28
Откуда: Уфа

Re: renderPartial и дублирование jQuery event handler-ов

Сообщение yan »

в чем именно проблема применить такой подход к обработчикам?

это расширение не помогает кстати с цсс "The extension does not prevent the multiple loading of CSS files. "
X-Loading
Сообщения: 26
Зарегистрирован: 2011.11.15, 14:55

Re: renderPartial и дублирование jQuery event handler-ов

Сообщение X-Loading »

проблема в том, что я вообще слабо контролирую это "навешивание"
условно говоря, в рамках одинаковых запросов содержимое грида может меняться и не ясно как контроллер должен понять это...

Может быть кто-нибудь ещё выскажет идеи?
Аватара пользователя
flashimage
Сообщения: 1517
Зарегистрирован: 2011.01.23, 12:43

Re: renderPartial и дублирование jQuery event handler-ов

Сообщение flashimage »

С удовольствием поковыряюсь и выскажу соображения, но ковыряться пока не в чем...
Дайте больше инфы, что приходит как обрабатывается, куда уходит.
Предварительно хочу заметить, что я бы cgridview обновлял более естественным путем, нежели ajax запрос с HTML данными, но пока это только догадки...
Ах да еще для CJuiDialog неплохо было бы вызывать undelegate перед ajax запросом
http://www.yiiframework.com/wiki/72/cju ... tton#c3095 - может поможет
Бранчи это гомеоморфические эндофункторы, которые мапятся на субманифолды пространства Гилберта.
Аватара пользователя
tsurka
Сообщения: 222
Зарегистрирован: 2012.05.07, 17:10
Откуда: Приднестровье
Контактная информация:

Re: renderPartial и дублирование jQuery event handler-ов

Сообщение tsurka »

Я как-то решал эту проблему так: перенаследовал класс где иммется рендер функции, там поставил проверку на Yii::app()->request->getIsAjaxRequest, если не аякс, загружаются нормально все скрипты, если аякс, хтмл отдается без скриптов.
Позже где-то прочитал что для этого уже есть какое-то расширение.
X-Loading
Сообщения: 26
Зарегистрирован: 2011.11.15, 14:55

Re: renderPartial и дублирование jQuery event handler-ов

Сообщение X-Loading »

flashimage писал(а):С удовольствием поковыряюсь и выскажу соображения, но ковыряться пока не в чем...
Дайте больше инфы, что приходит как обрабатывается, куда уходит.
Предварительно хочу заметить, что я бы cgridview обновлял более естественным путем, нежели ajax запрос с HTML данными, но пока это только догадки...
Ах да еще для CJuiDialog неплохо было бы вызывать undelegate перед ajax запросом
http://www.yiiframework.com/wiki/72/cju ... tton#c3095 - может поможет
flashimage, привет!
Постараюсь объяснить подробнее: в моем случае речь не идёт об обновлении грида, речь идёт о перемещении между разными гридами - выбор пункта меню в левой части экрана вызывает загрузку нового грида в правой части (см. скриншоты в первом сообщении).
При этом вызывается следующий экшн:

Код: Выделить всё

public function actionAdminByStatus()
        {
                if(isset($_GET['status']))
                {
                //$reviews = Productfeedback::model()->findAllByAttributes('status'=>$_GET['status']);
                $dataProvider=new CActiveDataProvider('Productfeedback',array(   
                        'criteria'=>Array(
                            'select'    =>'t.id,user.email as user_email,product.name as product_name,t.feedback,t.image01,t.image02,t.image03,t.paidflag',
                            'with' => array('user','product'),
                                                //'join'      =>' JOIN  `productcategory` productcategory ON  `product`.`categoryid` =  `productcategory`.`id`',
                                                'condition' => 't.status = IFNULL('.$_GET['status'].',0)',
                            ),
                        )
                );

                $this->renderPartial('_adminbystatus',array(
                                'dataProvider'=>$dataProvider,
                        ),false,true);
                }
        } 
view/_adminbystatus.php:

Код: Выделить всё

<?php
$viewDialog =<<<'EOT'
function() {
    var url = $(this).attr('href');
    $.get(url, function(r){
        $("#viewDialog").html(r).dialog("open");
    });
    return false;
}
EOT;

$updateDialog =<<<'EOT'
function() {
    var url = $(this).attr('href');
    $.get(url, function(r){
        $("#updateDialog").html(r).dialog("open");
    });
    return false;
}
EOT;

$this->widget('bootstrap.widgets.TbExtendedGridView', array(
        'id'=>'productfeedbackbystatus-grid',
        'fixedHeader' => true,
        'type' => 'striped bordered',
        'dataProvider'=>$dataProvider,
        'responsiveTable' => true,
        'enableSorting' => false,
        'rowCssClassExpression'=>'($data->paidflag==Productfeedback::TYPE_PAID)?"success":"warning"',
        'enablePagination' => true,
        'pager' => array(
            //'header' => false,
            'class'=>'bootstrap.widgets.TbPager',
            'firstPageLabel' => 'First',
            'prevPageLabel' => 'Previous',
            'nextPageLabel' => 'Next',
            'lastPageLabel' => 'Last',
        ),
        //'filter'=>$products,
        'bulkActions' => array(
                'actionButtons' => array(
                                array(
                                        'buttonType' => 'button',
                                        'type' => 'success',
                                        'size' => 'small',
                                        'label' => 'Bulk Approve',
                                        'click' => 'js:function(values){console.log(values);}',
                                ),
                                array(
                                        'buttonType' => 'button',
                                        'type' => 'danger',
                                        'size' => 'small',
                                        'label' => 'Bulk Reject',
                                        'click' => 'js:function(values){console.log(values);}'
                                )
                        ),
                        // if grid doesn't have a checkbox column type, it will attach
                        // one and this configuration will be part of it
                        'checkBoxColumnConfig' => array(
                            'name' => 'id'
                        ),
        ),
        'columns'=>array(
                'user.email',   
                'product.name',
                'feedback',
                array('type'=>'image',
                        'name' => 'Image1',
                        'value' => '"/images/productreviews/$data->id/thumbs/$data->image01"',
                'headerHtmlOptions'=>array('width'=>'100px'),
            ),
        array('type'=>'image',
                        'name' => 'Image2',
                        'value' => '"/images/productreviews/$data->id/thumbs/$data->image02"',
                'headerHtmlOptions'=>array('width'=>'100px'),
            ),
        array('type'=>'image',
                        'name' => 'Image3',
                        'value' => '"/images/productreviews/$data->id/thumbs/$data->image03"',
                'headerHtmlOptions'=>array('width'=>'100px'),
            ),
                array(
                        'header'=>'Options',
                        'class'=>'CButtonColumn',
            'buttons'=>array(
                'view' => array(
                            'icon'  => 'view',
                            'label' => 'View',
                            'url'   => 'Yii::app()->createUrl("/admin/productfeedback/view", array("id"=>$data->id))',
                                        'click'=>$viewDialog,
                        ),
                        'update' => array(
                            'icon'  => 'view',
                            'label' => 'View',
                            'url'   => 'Yii::app()->createUrl("/admin/productfeedback/update", array("id"=>$data->id))',
                                        'click'=>$updateDialog,
                        ),
            ),
                ),
)));
?>
Вот собственно повторный вызов этого экшна и приводит к дуплицированию хэндлеров.
По поводу двойного сабмита диалога - я ещё кажется не заметил этой проблемы, спасибо - должно помочь!

tsurka, не очень понял связи с моим случаем - у меня по сути всегда AJAX и за контроль скриптов отвечает nlsclientscript, который я упоминал выше.
Аватара пользователя
flashimage
Сообщения: 1517
Зарегистрирован: 2011.01.23, 12:43

Re: renderPartial и дублирование jQuery event handler-ов

Сообщение flashimage »

Попробуй делать что-то типа этого перед ajax запросом

Код: Выделить всё

$("a").die();
$("a").undelegate();
или даже так, чтобы с меню не снимать хендлеры

Код: Выделить всё

$("a").not("menuClass").die().undelegate();
можно добавить и другие теги на которых у тебя хендлеры висят (типа img)...
Бранчи это гомеоморфические эндофункторы, которые мапятся на субманифолды пространства Гилберта.
yan
Сообщения: 942
Зарегистрирован: 2011.03.23, 09:28
Откуда: Уфа

Re: renderPartial и дублирование jQuery event handler-ов

Сообщение yan »

flashimage писал(а): можно добавить и другие теги на которых у тебя хендлеры висят (типа img)...
это только как временный костыль потянет, т.к. если делать глобальную очистку то может зацепить и нужные обработчики, а если уточнять контекст, то есть большая вероятность, что класс-ид со временем поменяется и все неожиданно сломается
Аватара пользователя
flashimage
Сообщения: 1517
Зарегистрирован: 2011.01.23, 12:43

Re: renderPartial и дублирование jQuery event handler-ов

Сообщение flashimage »

По поводу костыля абсолютно согласен, выдаю здесь все проходящие мысли. Еще вопрос - хендлеры, которые дблируются относятся к какому-то одному виджету?
Бранчи это гомеоморфические эндофункторы, которые мапятся на субманифолды пространства Гилберта.
X-Loading
Сообщения: 26
Зарегистрирован: 2011.11.15, 14:55

Re: renderPartial и дублирование jQuery event handler-ов

Сообщение X-Loading »

Тут смотря что понимать под "относятся" ;-)
Все волнующие меня на данный момент хэндлеры относятся к объектам, входящим в состав грида:
- это в первую очередь кнопки view, update, которые в свою очередь открывают CJuiDialog (тоже "относятся");
- а также кнопки 'Bulk Approve', 'Bulk Reject' после грида (фича TbExtendedGridView) для балк-экшнов. Показаны на скриншоте в первом сообщении.
Аватара пользователя
flashimage
Сообщения: 1517
Зарегистрирован: 2011.01.23, 12:43

Re: renderPartial и дублирование jQuery event handler-ов

Сообщение flashimage »

По моему,нашел я источник проблемы. Находится он в файле TbBulkActions.php в папке widgets бустера. Раскидыванием хендлеров занимается функция:

Код: Выделить всё

public function registerClientScript()
    {

        $js = <<<EOD
$(document).on("click", "#{$this->grid->id} input[type=checkbox]", function(){
    var grid = $("#{$this->grid->id}");
    if($("input[name='{$this->columnName}']:checked", grid).length)
    {

        $(".bulk-actions-btn", grid).removeClass("disabled");
        $("div.bulk-actions-blocker",grid).hide();
    }
    else
    {
        $(".bulk-actions-btn", grid).addClass("disabled");
        $("div.bulk-actions-blocker",grid).show();
    }
});
EOD;
        foreach ($this->events as $buttonId => $handler)
        {
            $js .= "\n$(document).on('click','#{$buttonId}', function(){var checked = $('input[name=\"{$this->columnName}\"]:checked');\n
            var fn = $handler; if($.isFunction(fn)){fn(checked);}\nreturn false;});\n";
        }
        Yii::app()->getClientScript()->registerScript(__CLASS__ . '#' . $this->id, $js);
    }

Как видно из кода, раскидываются они через on в рут дома (считай live), поэтому предлагаю для тебя такие правки:

Код: Выделить всё

public function registerClientScript()
    {

        $js = <<<EOD
$("#{$this->grid->id} input[type=checkbox]").off();
$(document).on("click", "#{$this->grid->id} input[type=checkbox]", function(){
    var grid = $("#{$this->grid->id}");
    if($("input[name='{$this->columnName}']:checked", grid).length)
    {

        $(".bulk-actions-btn", grid).removeClass("disabled");
        $("div.bulk-actions-blocker",grid).hide();
    }
    else
    {
        $(".bulk-actions-btn", grid).addClass("disabled");
        $("div.bulk-actions-blocker",grid).show();
    }
});
EOD;
        foreach ($this->events as $buttonId => $handler)
        {
           $js .= "\n$('#{$buttonId}').off();\n";
            $js .= "\n$(document).on('click','#{$buttonId}', function(){var checked = $('input[name=\"{$this->columnName}\"]:checked');\n
            var fn = $handler; if($.isFunction(fn)){fn(checked);}\nreturn false;});\n";
        }
        Yii::app()->getClientScript()->registerScript(__CLASS__ . '#' . $this->id, $js);
    }
Просто перед навешиванием хендлеров трем старые (изменения в двух местах).
вместо просто off() можно еще поставить off('click','**') чтобы только хендлеры на клик снимать. Делаю на коленке, поэтому сори за синтаксис если что))
Бранчи это гомеоморфические эндофункторы, которые мапятся на субманифолды пространства Гилберта.
X-Loading
Сообщения: 26
Зарегистрирован: 2011.11.15, 14:55

Re: renderPartial и дублирование jQuery event handler-ов

Сообщение X-Loading »

Совсем забыл сказать спасибо!

Вот как в итоге стал выглядеть registerClientScript в TbBulkActions (там нужно было ещё учитывать, что off нужно выполнять на том же самом уровне, что и on):

Код: Выделить всё

/**
     * Registers client script
     */
    public function registerClientScript()
    {

    $js = <<<EOD
$(document).off("click", "#{$this->grid->id} input[type=checkbox]");
var grid = $("#{$this->grid->id}");
$(document).on("click", "#{$this->grid->id} input[type=checkbox]", function(){
    if($("input[name='{$this->columnName}']:checked", grid).length)
    {

        $(".btn", grid).removeClass("disabled");
        $("div.bulk-actions-blocker",grid).hide();
    }
    else
    {
        $(".btn", grid).addClass("disabled");
        $("div.bulk-actions-blocker",grid).show();
    }
});
EOD;
        foreach ($this->events as $buttonId => $handler)
        {
           $js .= "\n$(document).off('click','#{$buttonId}');\n";
            $js .= "\n$(document).on('click','#{$buttonId}', function(){var checked = $('input[name=\"{$this->columnName}\"]:checked');\n
            var fn = $handler; if($.isFunction(fn)){fn(checked);}\nreturn false;});\n";
        }
        Yii::app()->getClientScript()->registerScript(__CLASS__ . '#' . $this->id, $js);
    }
Чтобы такой же неприятности не происходило с кнопками, отнаследовался от CButtonColumn и там в registerClientScript также добавил off:

Код: Выделить всё

/**
     * Registers the client scripts for the button column.
     */
    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']);
                //$js[]="jQuery(document).on('click','#{$this->grid->id} a.{$class}',$function);";
                $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}']);";
            }
        }

        if($js!==array())
            Yii::app()->getClientScript()->registerScript(__CLASS__.'#'.$this->id, implode("\n",$js));
    }
Аватара пользователя
flashimage
Сообщения: 1517
Зарегистрирован: 2011.01.23, 12:43

Re: renderPartial и дублирование jQuery event handler-ов

Сообщение flashimage »

"там нужно было ещё учитывать, что off нужно выполнять на том же самом уровне, что и on" - дада прощелкал ))
Бранчи это гомеоморфические эндофункторы, которые мапятся на субманифолды пространства Гилберта.
Ответить