y a-t-il un rappel de post-rendu pour la directive Angular JS?

139

Je viens de recevoir ma directive pour insérer un modèle à ajouter à son élément comme ceci:

# CoffeeScript
.directive 'dashboardTable', ->
  controller: lineItemIndexCtrl
  templateUrl: "<%= asset_path('angular/templates/line_items/dashboard_rows.html') %>"
  (scope, element, attrs) ->
    element.parent('table#line_items').dataTable()
    console.log 'Just to make sure this is run'

# HTML
<table id="line_items">
    <tbody dashboard-table>
    </tbody>
</table>

J'utilise également un plugin jQuery appelé DataTables. Son utilisation générale est la suivante: $ ('table # some_id'). DataTable (). Vous pouvez passer les données JSON dans l'appel dataTable () pour fournir les données de la table OU vous pouvez avoir les données déjà sur la page et il fera le reste .. Je fais ce dernier, ayant les lignes déjà sur la page HTML .

Mais le problème est que je dois appeler le dataTable () sur la table # line_items APRÈS DOM prêt. Ma directive ci-dessus appelle la méthode dataTable () AVANT que le modèle ne soit ajouté à l'élément de la directive. Existe-t-il un moyen d'appeler des fonctions APRÈS l'ajout?

Merci de votre aide!

MISE À JOUR 1 après la réponse d'Andy:

Je veux m'assurer que la méthode de lien n'est appelée qu'APRÈS que tout est sur la page, j'ai donc modifié la directive pour un petit test:

# CoffeeScript
#angular.module(...)
.directive 'dashboardTable', ->
    {
      link: (scope,element,attrs) -> 
        console.log 'Just to make sure this gets run'
        element.find('#sayboo').html('boo')

      controller: lineItemIndexCtrl
      template: "<div id='sayboo'></div>"

    }

Et je vois effectivement "boo" dans le div # sayboo.

Ensuite, j'essaye mon appel jquery datatable

.directive 'dashboardTable',  ->
    {
      link: (scope,element,attrs) -> 
        console.log 'Just to make sure this gets run'
        element.parent('table').dataTable() # NEW LINE

      controller: lineItemIndexCtrl
      templateUrl: "<%= asset_path('angular/templates/line_items/dashboard_rows.html') %>"
    }

Pas de chance là-bas

Ensuite, j'essaye d'ajouter un temps mort:

.directive 'dashboardTable', ($timeout) ->
    {
      link: (scope,element,attrs) -> 
        console.log 'Just to make sure this gets run'
        $timeout -> # NEW LINE
          element.parent('table').dataTable()
        ,5000
      controller: lineItemIndexCtrl
      templateUrl: "<%= asset_path('angular/templates/line_items/dashboard_rows.html') %>"
    }

Et ça marche. Je me demande donc ce qui ne va pas dans la version sans minuterie du code?

Nik So
la source
1
@adardesign Non je ne l'ai jamais fait, j'ai dû utiliser une minuterie. Pour une raison quelconque, le rappel n'est pas vraiment un rappel ici. J'ai une table avec 11 colonnes et des centaines de lignes, donc naturellement angulaire semble être un bon pari à utiliser pour la liaison de données; mais je dois aussi utiliser le plugin jquery Datatables qui est aussi simple que $ ('table'). datatable (). En utilisant une directive ou simplement avoir un objet json stupide avec toutes les lignes et utiliser ng-repeat pour itérer, je ne peux pas faire exécuter mon $ (). Datatable () APRÈS le rendu de l'élément html de la table, donc mon truc est actuellement de chronométrer pour vérifier si $ ('tr'). length> 3 (b / c de l'en-tête / pied de page)
Nik So
2
@adardesign Et oui, j'ai essayé toutes les méthodes de compilation, méthode de compilation retournant un objet contenant les méthodes postLink / preLink, méthode de compilation retournant juste une fonction (à savoir la fonction de liaison), méthode de liaison (sans la méthode de compilation car pour autant que je sache, si vous avez une méthode de compilation qui retourne une méthode de liaison, la fonction de liaison est ignorée). Aucune n'a fonctionné, il faut donc se fier au bon vieux $ timeout. Mettra à jour cet article si je trouve quelque chose qui fonctionne mieux ou simplement lorsque je trouve que le rappel agit vraiment comme un rappel
Nik So

Réponses:

215

Si le deuxième paramètre, "delay" n'est pas fourni, le comportement par défaut est d'exécuter la fonction une fois que le DOM a terminé le rendu. Donc, au lieu de setTimeout, utilisez $ timeout:

$timeout(function () {
    //DOM has finished rendering
});
parlement
la source
8
Pourquoi n'est-il pas expliqué dans la documentation ?
Gaui
23
Vous avez raison, ma réponse est un peu trompeuse car j'ai essayé de la simplifier. La réponse complète est que cet effet n'est pas le résultat d'Angular mais plutôt du navigateur. $timeout(fn)finalement, setTimeout(fn, 0)ce qui a pour effet d'interrompre l'exécution de Javascript et de permettre au navigateur de rendre le contenu en premier, avant de continuer l'exécution de ce Javascript.
parlement du
7
Considérez le navigateur comme la mise en file d'attente de certaines tâches telles que "l'exécution javascript" et le "rendu DOM" séparément, et quel setTimeout (fn, 0) pousse-t-il l '"exécution javascript" en cours à l'arrière de la file d'attente, après le rendu .
parlement du
2
@GabLeRoux yup, cela aura le même effet sauf que $ timeout a l'avantage supplémentaire d'appeler $ scope. $ Apply () après son exécution. Avec _.defer (), vous devrez l'appeler manuellement si myFunction change des variables sur la portée.
parlement
2
J'ai un scénario où cela n'aide pas où sur la page1 ng-repeat rend un tas d'éléments, puis je vais à la page2, puis je retourne à la page1 et j'essaye d'obtenir le haut des éléments ng-repeat parent ... il renvoie la mauvaise hauteur. Si je fais un timeout pendant environ 1000 ms, cela fonctionne.
yodalr
14

J'ai eu le même problème et je crois que la réponse est vraiment non. Voir le commentaire de Miško et quelques discussions dans le groupe .

Angular peut suivre que tous les appels de fonction qu'il effectue pour manipuler le DOM sont terminés, mais comme ces fonctions pourraient déclencher une logique asynchrone qui met toujours à jour le DOM après leur retour, Angular ne pouvait pas s'attendre à le savoir. Tout rappel donné par Angular peut parfois fonctionner, mais il ne serait pas sûr de s'y fier.

Nous avons résolu ce problème de manière heuristique avec un setTimeout, comme vous l'avez fait.

(Veuillez garder à l'esprit que tout le monde n'est pas d'accord avec moi - vous devriez lire les commentaires sur les liens ci-dessus et voir ce que vous en pensez.)

Roy Truelove
la source
7

Vous pouvez utiliser la fonction «lien», également appelée postLink, qui s'exécute après l'insertion du modèle.

app.directive('myDirective', function() {
  return {
    link: function(scope, elm, attrs) { /*I run after template is put in */ },
    template: '<b>Hello</b>'
  }
});

Lisez ceci si vous prévoyez de faire des directives, c'est d'une grande aide: http://docs.angularjs.org/guide/directive

Andrew Joslin
la source
Salut Andy, merci beaucoup d'avoir répondu; J'ai essayé la fonction de lien mais cela ne me dérangerait pas de la réessayer exactement comme vous la codez; J'ai passé les 1,5 derniers jours à lire cette page de directive; et en regardant également les exemples sur le site angular. Je vais essayer votre code maintenant.
Nik So
Ah, je vois maintenant que vous essayiez de faire un lien mais que vous le faisiez mal. Si vous retournez simplement une fonction, elle est supposée être un lien. Si vous retournez un objet, vous devez le renvoyer avec la clé comme «lien». Vous pouvez également renvoyer une fonction de liaison à partir de votre fonction de compilation.
Andrew Joslin
Salut Andy, j'ai récupéré mes résultats; J'ai presque perdu la raison, parce que j'ai vraiment fait ce que vous répondez ici. S'il vous plaît voir ma mise à jour
Nik So
Humm, essayez quelque chose comme: <table id = "bob"> <tbody dashboard-table = "# bob"> </tbody> </table> Puis dans votre lien, faites $ (attrs.dashboardTable) .dataTable () à assurez-vous qu'il est bien sélectionné. Ou je suppose que vous avez déjà essayé ça ... Je ne sais vraiment pas si le lien ne fonctionne pas.
Andrew Joslin
Celui-ci a fonctionné pour moi, je voulais déplacer des éléments dans le rendu post dom du modèle pour mon besoin, je l'ai fait dans la fonction de
lien.Merci
7

Bien que ma réponse ne soit pas liée aux tables de données, elle aborde le problème de la manipulation DOM et par exemple l'initialisation du plugin jQuery pour les directives utilisées sur les éléments dont le contenu est mis à jour de manière asynchrone.

Au lieu d'implémenter un délai d'attente, vous pouvez simplement ajouter une montre qui écoutera les changements de contenu (ou même des déclencheurs externes supplémentaires).

Dans mon cas, j'ai utilisé cette solution de contournement pour initialiser un plugin jQuery une fois que le ng-repeat a été fait, ce qui a créé mon DOM interne - dans un autre cas, je l'ai utilisé pour manipuler simplement le DOM après que la propriété scope ait été modifiée au niveau du contrôleur. Voici comment j'ai fait ...

HTML:

<div my-directive my-directive-watch="!!myContent">{{myContent}}</div>

JS:

app.directive('myDirective', [ function(){
    return {
        restrict : 'A',
        scope : {
            myDirectiveWatch : '='
        },
        compile : function(){
            return {
                post : function(scope, element, attributes){

                    scope.$watch('myDirectiveWatch', function(newVal, oldVal){
                        if (newVal !== oldVal) {
                            // Do stuff ...
                        }
                    });

                }
            }
        }
    }
}]);

Remarque: au lieu de simplement convertir la variable myContent en booléen à l'attribut my-directive-watch, on pourrait imaginer n'importe quelle expression arbitraire.

Remarque: L' isolement de la portée comme dans l'exemple ci-dessus ne peut être fait qu'une fois par élément - essayer de le faire avec plusieurs directives sur le même élément entraînera une erreur $ compile: multidir - voir: https://docs.angularjs.org / erreur / $ compile / multidir

conceptdeluxe
la source
7

Je suis peut-être en retard pour répondre à cette question. Mais quelqu'un peut encore tirer profit de ma réponse.

J'ai eu un problème similaire et dans mon cas je ne peux pas changer la directive puisque, c'est une bibliothèque et changer un code de la bibliothèque n'est pas une bonne pratique. Donc ce que j'ai fait a été d'utiliser une variable pour attendre le chargement de la page et d'utiliser ng-if dans mon html pour attendre le rendu de l'élément particulier.

Dans mon contrôleur:

$scope.render=false;

//this will fire after load the the page

angular.element(document).ready(function() {
    $scope.render=true;
});

Dans mon html (dans mon cas, le composant html est un canevas)

<canvas ng-if="render"> </canvas>
Madura Pradeep
la source
3

J'ai eu le même problème, mais en utilisant Angular + DataTable avec un fnDrawCallback+ groupement de lignes + $ directives imbriquées compilées. J'ai placé le $ timeout dans ma fnDrawCallbackfonction pour corriger le rendu de la pagination.

Avant l'exemple, basé sur la source de row_grouping:

var myDrawCallback = function myDrawCallbackFn(oSettings){
  var nTrs = $('table#result>tbody>tr');
  for(var i=0; i<nTrs.length; i++){
     //1. group rows per row_grouping example
     //2. $compile html templates to hook datatable into Angular lifecycle
  }
}

Après exemple:

var myDrawCallback = function myDrawCallbackFn(oSettings){
  var nTrs = $('table#result>tbody>tr');
  $timeout(function requiredRenderTimeoutDelay(){
    for(var i=0; i<nTrs.length; i++){
       //1. group rows per row_grouping example
       //2. $compile html templates to hook datatable into Angular lifecycle
    }
  ,50); //end $timeout
}

Même un court délai d'expiration était suffisant pour permettre à Angular de restituer mes directives Angular compilées.

JJ Zabkar
la source
Juste curieux, avez-vous une table assez grande avec de nombreuses colonnes? parce que j'ai trouvé que j'avais besoin de beaucoup de millisecondes ennuyeuses (> 100) pour ne pas laisser l'appel dataTable () s'étouffer
Nik So
J'ai trouvé que le problème s'est produit lors de la navigation dans les pages DataTable pour les ensembles de résultats de 2 lignes à plus de 150 lignes. Donc, non - je ne pense pas que la taille de la table soit le problème, mais peut-être que DataTable a ajouté suffisamment de frais généraux de rendu pour mâcher une partie de ces millisecondes. Mon objectif était de faire fonctionner le regroupement de lignes dans DataTable avec une intégration AngularJS minimale.
JJ Zabkar
2

Aucune des solutions n'a fonctionné pour moi n'accepte d'utiliser un délai d'expiration. C'est parce que j'utilisais un modèle qui était créé dynamiquement pendant le postLink.

Notez cependant qu'il peut y avoir un timeout de '0' car le timeout ajoute la fonction appelée à la file d'attente du navigateur qui se produira après le moteur de rendu angulaire car elle est déjà dans la file d'attente.

Reportez-vous à ceci: http://blog.brunoscopelliti.com/run-a-directive-after-the-dom-has-finished-rendering

Michael Smolenski
la source
0

Voici une directive pour avoir des actions programmées après un rendu peu profond. Par superficiel, je veux dire qu'il évaluera après cet élément même rendu et qui n'aura aucun rapport avec le moment où son contenu sera rendu. Donc, si vous avez besoin d'un sous-élément pour effectuer une action de post-rendu, vous devriez envisager de l'utiliser ici:

define(['angular'], function (angular) {
  'use strict';
  return angular.module('app.common.after-render', [])
    .directive('afterRender', [ '$timeout', function($timeout) {
    var def = {
        restrict : 'A', 
        terminal : true,
        transclude : false,
        link : function(scope, element, attrs) {
            if (attrs) { scope.$eval(attrs.afterRender) }
            scope.$emit('onAfterRender')
        }
    };
    return def;
    }]);
});

alors vous pouvez faire:

<div after-render></div>

ou avec toute expression utile comme:

<div after-render="$emit='onAfterThisConcreteThingRendered'"></div>

Sébastien Sastre
la source
Ce n'est pas vraiment après le rendu du contenu. Si j'avais une expression dans l'élément <div after-render> {{blah}} </div> à ce stade, l'expression n'est pas encore évaluée. Le contenu du div est toujours {{blah}} dans la fonction de lien. Donc, techniquement, vous déclenchez l'événement avant que le contenu ne soit rendu.
Edward Olamisan
C'est une action après rendu peu profonde, je n'ai jamais prétendu qu'elle était profonde
Sebastian Sastre
0

Je l'ai fait fonctionner avec la directive suivante:

app.directive('datatableSetup', function () {
    return { link: function (scope, elm, attrs) { elm.dataTable(); } }
});

Et dans le HTML:

<table class="table table-hover dataTable dataTable-columnfilter " datatable-setup="">

dépannage si ce qui précède ne fonctionne pas pour vous.

1) notez que «datatableSetup» est l'équivalent de «datatable-setup». Angular change le format en cas de chameau.

2) assurez-vous que l'application est définie avant la directive. Par exemple, définition et directive d'application simples.

var app = angular.module('app', []);
app.directive('datatableSetup', function () {
    return { link: function (scope, elm, attrs) { elm.dataTable(); } }
});
Anton
la source
0

Suite au fait que l'ordre de chargement ne peut être anticipé, une solution simple peut être utilisée.

Regardons la relation directive-«utilisateur de directive». Habituellement, l'utilisateur de la directive fournira des données à la directive ou utilisera certaines fonctionnalités (fonctions) fournies par la directive. La directive, d'autre part, s'attend à ce que certaines variables soient définies sur son champ d'application.

Si nous pouvons nous assurer que tous les joueurs ont toutes leurs exigences d'action remplies avant de tenter d'exécuter ces actions, tout devrait bien se passer.

Et maintenant la directive:

app.directive('aDirective', function () {
    return {
        scope: {
            input: '=',
            control: '='
        },
        link: function (scope, element) {
            function functionThatNeedsInput(){
                //use scope.input here
            }
            if ( scope.input){ //We already have input 
                functionThatNeedsInput();
            } else {
                scope.control.init = functionThatNeedsInput;
            }
          }

        };
})

et maintenant l'utilisateur de la directive html

<a-directive control="control" input="input"></a-directive>

et quelque part dans le contrôleur du composant qui utilise la directive:

$scope.control = {};
...
$scope.input = 'some data could be async';
if ( $scope.control.functionThatNeedsInput){
    $scope.control.functionThatNeedsInput();
}

C'est à peu près ça. Il y a beaucoup de frais généraux mais vous pouvez perdre le $ timeout. Nous supposons également que le composant qui utilise la directive est instancié avant la directive car nous dépendons de la variable de contrôle pour exister lorsque la directive est instanciée.

Eli
la source