web-dev-qa-db-fra.com

Ajouter ng-click dynamiquement dans la fonction de lien directif

J'essaie de créer une directive qui permettrait à un élément d'être défini comme cliquable ou non, et serait défini comme ceci:

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

Je veux que le HTML résultant soit:

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

Mon implémentation de directive ressemble à ceci:

_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');
            };
        }
    };
});
_

Je peux voir qu'après avoir ajouté le nouvel attribut, Angular ne connaît pas le _ng-click_, donc il ne se déclenche pas. J'ai essayé d'ajouter un _$compile_ après que l'attribut soit défini, mais il provoque une boucle de liaison/compilation infinie.

Je sais que je peux simplement vérifier à l'intérieur de la fonction onHandleClick() si la valeur isClickable est true, mais je suis curieux de savoir comment procéder en ajoutant dynamiquement un _ng-click_ événement car je dois peut-être faire cela avec plusieurs autres directives _ng-*_ et je ne veux pas ajouter de surcharge inutile. Des idées?

32
DRiFTy

Meilleure solution (nouvelle):

Après avoir lu les Angular docs je suis tombé sur ceci:

Vous pouvez spécifier le modèle en tant que chaîne représentant le modèle ou en tant que fonction qui prend deux arguments tElement et tAttrs (décrits dans l'api de la fonction de compilation ci-dessous) et renvoie une valeur de chaîne représentant le modèle.

Donc ma nouvelle directive ressemble à ceci: (je crois que c'est la façon "angulaire" appropriée de procéder pour ce genre de chose)

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');
            };
        }
    };
});

Remarquez la nouvelle fonction de modèle. Maintenant, je manipule le modèle à l'intérieur de cette fonction avant qu'il ne soit compilé.

Solution alternative (ancienne):

Ajoutée replace: true pour se débarrasser du problème de boucle infinie lors de la recompilation de la directive. Et puis dans la fonction de lien, je recompile simplement l'élément après avoir ajouté le nouvel attribut. Une chose à noter cependant, car j'avais un ng-transclude directive sur mon élément, j'avais besoin de la supprimer pour ne pas essayer de transclure quoi que ce soit sur la deuxième compilation, car il n'y a rien à transclure.

Voici à quoi ressemble ma directive maintenant:

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');
            };
        }
    };
});

Je ne pense pas que recompiler le modèle une deuxième fois soit idéal, donc je pense qu'il y a encore un moyen de le faire avant que le modèle ne soit compilé la première fois.

28
DRiFTy

Vous pouvez toujours simplement modifier votre ng-click pour ressembler à ceci:

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

Aucune directive personnalisée requise :)

Voici une démonstration de JSFiddle: http://jsfiddle.net/robianmcd/5D4VR/

15
rob

Réponse mise à jour

"La Angular Way" ne serait pas du tout une manipulation manuelle du DOM. Donc, nous devons nous débarrasser de l'ajout et de la suppression d'attributs.

DEMO

Modifiez le modèle en:

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

Et dans la directive, vérifiez l'attribut isClickable pour décider quoi faire lorsque vous cliquez dessus:

    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');
        };
    }

Vous pouvez également placer l'attribut isClickable dans la portée de la directive afin qu'il puisse changer dynamiquement son comportement.

Ancienne réponse (mauvaise)

link est exécuté après la compilation du modèle. Utilisez controller pour les modifications sur le modèle avant de compiler:

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

[~ # ~] 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){

                    }


                };
            });

crédit à Erstad.Stephen et Ilan Frumer

BTW avec restrict: 'E' le navigateur est tombé en panne :(

2
Whisher

Ceci est ma version de la solution @DiscGolfer où j'ai également ajouté la prise en charge des attributs.

.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;
      }
    }

  };

Un échantillon plus générique et complet est fourni http://plnkr.co/edit/4PcMnpq59ebZr2VrOI07?p=preview

Le seul problème avec cette solution est que replace est déconseillé dans AngularJS.

1
Archimedes Trajano

Je pense que ça devrait être mieux comme ça:

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');
            };
        }
    };
});
0
rneves