Динамическое добавление ng-click в функцию директивной ссылки

Я пытаюсь создать директиву, которая позволила бы определить элемент как интерактивный или нет, и будет определяться следующим образом:

<page is-clickable="true">
    transcluded elements...
</page>

Я хочу, чтобы получившийся HTML был:

<page is-clickable="true" ng-click="onHandleClick()">
    transcluded elements...
</page>

Моя реализация директивы выглядит следующим образом:

app.directive('page', function() {
    return {
        restrict: 'E',
        template: '<div ng-transclude></div>',
        transclude: true,
        link: function(scope, element, attrs) {
            var isClickable = angular.isDefined(attrs.isClickable) && scope.$eval(attrs.isClickable) === true ? true : false;

            if (isClickable) {
                attrs.$set('ngClick', 'onHandleClick()');
            }

            scope.onHandleClick = function() {
                console.log('onHandleClick');
            };
        }
    };
});

Я вижу, что после добавления нового атрибута Angular не знает о ng-click, поэтому он не запускается. Я попытался добавить $compile после того, как атрибут установлен, но он вызывает бесконечный цикл link/compile.

Я знаю, что могу просто проверить внутри onHandleClick() функцию, если значение isClickable равно true, но мне любопытно, как можно было бы это сделать, динамически добавляя событие ng-click, потому что мне может понадобиться сделать это с помощью нескольких других директив ng-*, и я не хочу добавлять лишние накладные расходы. Любые идеи?

Ответ 1

Лучшее решение (новое):

Прочитав Angular docs, я наткнулся на это:

Вы можете указать шаблон как строку, представляющую шаблон, или как     функция, которая принимает два аргумента tElement и tAttrs (описанные в     функция api компиляции ниже) и возвращает строковое значение, представляющее     шаблон.

Итак, моя новая директива выглядит следующим образом: (Я считаю, что это подходящий способ "Angular" для этого типа вещей)

app.directive('page', function() {
    return {
        restrict: 'E',
        replace: true,
        template: function(tElement, tAttrs) {
            var isClickable = angular.isDefined(tAttrs.isClickable) && eval(tAttrs.isClickable) === true ? true : false;

            var clickAttr = isClickable ? 'ng-click="onHandleClick()"' : '';

            return '<div ' + clickAttr + ' ng-transclude></div>';
        },
        transclude: true,
        link: function(scope, element, attrs) {
            scope.onHandleClick = function() {
                console.log('onHandleClick');
            };
        }
    };
});

Обратите внимание на новую функцию шаблона. Теперь я манипулирую шаблоном внутри этой функции перед ее компиляцией.

Альтернативное решение (старое):

Добавлен replace: true, чтобы избавиться от проблемы бесконечного цикла при перекомпиляции директивы. И затем в функции связи я просто перекомпилирую элемент после добавления нового атрибута. Одна вещь, которую нужно отметить, потому что у меня была директива ng-transclude для моего элемента, мне нужно было удалить ее, чтобы она не пыталась перекрыть что-либо во втором компиляторе, потому что ничего не нужно переводить.

Вот как выглядит моя директива:

app.directive('page', function() {
    return {
        restrict: 'E',
        replace: true,
        template: '<div ng-transclude></div>',
        transclude: true,
        link: function(scope, element, attrs) {
            var isClickable = angular.isDefined(attrs.isClickable) && scope.$eval(attrs.isClickable) === true ? true : false;

            if (isClickable) {
                attrs.$set('ngClick', 'onHandleClick()');
                element.removeAttr('ng-transclude');
                $compile(element)(scope);
            }

            scope.onHandleClick = function() {
                console.log('onHandleClick');
            };
        }
    };
});

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

Ответ 2

Вы всегда можете просто изменить свой ng-click, чтобы выглядеть так:

ng-click="isClickable && someFunction()"

Никакой пользовательской директивы не требуется:)

Вот пример JSFiddle: http://jsfiddle.net/robianmcd/5D4VR/

Ответ 3

Обновленный ответ

"Путь Angular" вообще не будет ручным манипулятором DOM. Итак, нам нужно избавиться от добавления и удаления атрибутов.

DEMO

Измените шаблон на:

template: '<div ng-click="onHandleClick()" ng-transclude></div>'

И в директиве проверьте атрибут isClickable, чтобы решить, что делать при нажатии:

    link: function(scope, element, attrs) {
        var isClickable = angular.isDefined(attrs.isClickable) && scope.$eval(attrs.isClickable) === true ? true : false;

        scope.onHandleClick = function() {
            if (!isClickable) return;
            console.log('onHandleClick');
        };
    }

Вы также можете поместить атрибут isClickable в область директивы, чтобы он мог динамически изменять свое поведение.

Старый ответ (неверный)

link запускается после компиляции шаблона. Используйте controller для изменения шаблона перед компиляцией:

app.directive('page', function() {
    return {
        restrict: 'E',
        template: '<div ng-transclude></div>',
        transclude: true,
        controller: function(scope, element, attrs) {
            // your code
        }
    };
});

Ответ 4

HTML

<div page is-clickable="true">hhhh</div>

JS

app.directive('page', function($compile) {
                return {
                    priority:1001, // compiles first
                    terminal:true, // prevent lower priority directives to compile after it
                    template: '<div ng-transclude></div>',
                    transclude: true,
                    compile: function(el,attr,transclude) {
                        el.removeAttr('page'); // necessary to avoid infinite compile loop
                        var contents = el.contents().remove();
                        var compiledContents;
                        return function(scope){
                            var isClickable = angular.isDefined(attr.isClickable)?scope.$eval(attr.isClickable):false;
                            if(isClickable){
                                el.attr('ng-click','onHandleClick()');
                                var fn = $compile(el);
                                fn(scope);
                                scope.onHandleClick = function() {
                                    console.log('onHandleClick');
                                };
                            }
                            if(!compiledContents) {
                                compiledContents = $compile(contents, transclude);
                            }
                            compiledContents(scope, function(clone, scope) {
                                el.append(clone); 
                            });

                        };
                    },
                    link:function(scope){

                    }


                };
            });

кредит Erstad.Stephen и Илан Фрумер

BTW с ограничением: "E" браузер разбился: (

Ответ 5

Это моя версия решения @DiscGolfer, где я также добавил поддержку атрибутов.

.directive("page", function() {

  return {
    transclude: true,
    replace: true,
    template: function(tElement, tAttr) {

      var isClickable = angular.isDefined(tAttrs.isClickable) && eval(tAttrs.isClickable) === true ? true : false;

      if (isClickable) {
        tElement.attr("ng-click", "onHandleClick()");
      }
      tElement.attr("ng-transclude", "");
      if (tAttr.$attr.page === undefined) {
        return "<" + tElement[0].outerHTML.replace(/(^<\w+|\w+>$)/g, 'div') + ">";
      } else {
        tElement.removeAttr(tAttr.$attr.page);
        return tElement[0].outerHTML;
      }
    }

  };

Предоставляется более общий и полный выбор http://plnkr.co/edit/4PcMnpq59ebZr2VrOI07?p=preview

Единственная проблема с этим решением заключается в том, что replace устарел в AngularJS.

Ответ 6

Я думаю, что это должно быть лучше:

app.directive('page', function() {
    return {
        restrict: 'E',
        template: '<div ng-transclude></div>',
        transclude: true,
        link: function(scope, element, attrs) {
            var isClickable = angular.isDefined(attrs.isClickable) && scope.$eval(attrs.isClickable) === true ? true : false;

            if (isClickable) {
                angular.element(element).on('click', scope.onHandleClick);
            }

            scope.onHandleClick = function() {
                console.log('onHandleClick');
            };
        }
    };
});

Ответ 7

module.factory("ibDirectiveHelpers", ["ngClickDirective", function (ngClick) {
        return {
            click: function (scope, element, fn) {
                var attr = {ngClick: fn};
                ngClick[0].compile(element, attr)(scope, element, attr);
            }
        };
    }]);

использование:

module.controller("demoController",["$scope","$element","ibDirectiveHelpers",function($scope,$element,ibDirectiveHelpers){

$scope.demoMethod=function(){console.log("demoMethod");};
ibDirectiveHelpers.click($scope,$element,"demoMethod()");//uses html notation
 //or
ibDirectiveHelpers.click($scope,$element,function(){$scope.demoMethod();});//uses inline notation
}]