¿Hay una devolución de llamada de renderizado posterior para la directiva Angular JS?

139

Acabo de recibir mi directiva para extraer una plantilla para agregarla a su elemento de esta manera:

# 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>

También estoy usando un complemento jQuery llamado DataTables. El uso general de este es el siguiente: $ ('table # some_id'). DataTable (). Puede pasar los datos JSON a la llamada dataTable () para proporcionar los datos de la tabla O puede tener los datos ya en la página y hará el resto ... Estoy haciendo lo último, teniendo las filas ya en la página HTML .

Pero el problema es que tengo que llamar a dataTable () en la tabla # line_items AFTER DOM ready. Mi directiva anterior llama al método dataTable () ANTES de que la plantilla se agregue al elemento de la directiva. ¿Hay alguna manera de que pueda llamar a las funciones DESPUÉS de la adición?

¡Gracias por tu ayuda!

ACTUALIZACIÓN 1 después de la respuesta de Andy:

Quiero asegurarme de que el método de enlace solo se llame DESPUÉS de que todo esté en la página, así que modifiqué la directiva para una pequeña prueba:

# 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>"

    }

Y de hecho veo "boo" en el div # sayboo.

Luego pruebo mi llamada 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') %>"
    }

No hay suerte

Luego trato de agregar un tiempo de espera:

.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') %>"
    }

Y eso funciona. ¿Entonces me pregunto qué sale mal en la versión sin código del temporizador?

Nik So
fuente
1
@adardesign No, nunca lo hice, tuve que usar un temporizador. Por alguna razón, la devolución de llamada no es una devolución de llamada aquí, realmente. Tengo una tabla con 11 columnas y cientos de filas, por lo que, naturalmente, angular parece una buena apuesta para el enlace de datos; pero también necesito usar el complemento jquery Datatables que es tan simple como $ ('table'). datatable (). Al usar la directiva o simplemente tener un objeto json tonto con todas las filas y usar ng-repeat para iterar, no puedo obtener mi $ (). Datatable () para ejecutar DESPUÉS de que se represente el elemento html de la tabla, así que mi truco actualmente es temporizar para verificar si $ ('tr'). longitud> 3 (b / c de encabezado / pie de página)
Nik So
2
@adardesign Y sí, probé todo el método de compilación, el método de compilación que devuelve un objeto que contiene métodos postLink / preLink, el método de compilación que devuelve solo una función (es decir, la función de enlace), el método de enlace (sin el método de compilación porque, por lo que puedo decir, si tiene un método de compilación que devuelve un método de vinculación, la función de vinculación se ignora). Ninguno funcionó, por lo que debe confiar en el viejo $ timeout. Actualizaré esta publicación si encuentro algo que funciona mejor o simplemente cuando descubro que la devolución de llamada realmente actúa como devolución de llamada
Nik So

Respuestas:

215

Si no se proporciona el segundo parámetro, "retraso", el comportamiento predeterminado es ejecutar la función después de que el DOM haya completado la representación. Entonces, en lugar de setTimeout, use $ timeout:

$timeout(function () {
    //DOM has finished rendering
});
parlamento
fuente
8
¿Por qué no se explica en los documentos ?
Gaui
23
Tienes razón, mi respuesta es un poco engañosa porque traté de hacerlo simple. La respuesta completa es que este efecto no es resultado de Angular sino del navegador. $timeout(fn)finalmente llama, lo setTimeout(fn, 0)que tiene el efecto de interrumpir la ejecución de Javascript y permitir que el navegador muestre primero el contenido, antes de continuar la ejecución de ese Javascript.
Parlamento
77
Piense en el navegador como poner en cola ciertas tareas como "ejecución de javascript" y "representación DOM" por separado, y qué setTimeout (fn, 0) empuja la "ejecución de javascript" actualmente en ejecución al final de la cola, después de la representación .
Parlamento
2
@GabLeRoux, sí, tendrá el mismo efecto, excepto que $ timeout tiene el beneficio adicional de llamar a $ scope. $ Apply () después de que se ejecute. Con _.defer () deberá llamarlo manualmente si myFunction cambia las variables en el ámbito.
Parlamento
2
Tengo un escenario en el que esto no ayuda cuando en la página1 ng-repeat representa un montón de elementos, luego voy a la página2 y luego vuelvo a la página1 e intento obtener el máximo de elementos ng-repeat parent ... devuelve la altura incorrecta. Si hago un tiempo de espera de 1000ms, entonces funciona.
Yodalr
14

Tuve el mismo problema y creo que la respuesta es realmente no. Vea el comentario de Miško y alguna discusión en el grupo .

Angular puede rastrear que todas las llamadas a funciones que realiza para manipular el DOM están completas, pero dado que esas funciones podrían desencadenar una lógica asíncrona que todavía está actualizando el DOM después de que regresen, no se puede esperar que Angular lo sepa. Cualquier devolución de llamada Angular puede funcionar a veces, pero no sería seguro confiar en ella.

Resolvimos esto heurísticamente con un setTimeout, como lo hiciste.

(Tenga en cuenta que no todos están de acuerdo conmigo, debe leer los comentarios en los enlaces anteriores y ver lo que piensa).

Roy Truelove
fuente
7

Puede usar la función 'enlace', también conocida como postLink, que se ejecuta después de colocar la plantilla.

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

Lea esto si planea hacer directivas, es de gran ayuda: http://docs.angularjs.org/guide/directive

Andrew Joslin
fuente
Hola Andy, muchas gracias por contestar; Intenté la función de enlace, pero no me importaría intentarlo exactamente como lo codificas; Pasé los últimos 1,5 días leyendo en esa página directiva; y mirando los ejemplos en el sitio de angular también. Intentará su código ahora.
Nik So
Ah, ahora veo que estabas intentando hacer un enlace pero lo estabas haciendo mal. Si solo devuelve una función, se supone que es un enlace. Si devuelve un objeto, debe devolverlo con la clave como 'enlace'. También puede devolver una función de enlace desde su función de compilación.
Andrew Joslin
Hola Andy, recuperé mis resultados; Casi pierdo la cordura, porque realmente hice básicamente cuál es tu respuesta aquí. Por favor vea mi actualización
Nik So
Humm, intente algo como: <table id = "bob"> <tbody dashboard-table = "# bob"> </tbody> </table> Luego, en su enlace, haga $ (attrs.dashboardTable) .dataTable () para asegúrese de que esté siendo seleccionado correctamente. O supongo que ya lo has intentado ... Realmente no estoy seguro de si el enlace no funciona.
Andrew Joslin
Esto funcionó para mí, quería mover elementos dentro de la presentación de la plantilla de dom dom para mi requerimiento, lo hice en la función de enlace
Gracias
7

Aunque mi respuesta no está relacionada con las tablas de datos, aborda el problema de la manipulación del DOM y, por ejemplo, la inicialización del complemento jQuery para directivas utilizadas en elementos que tienen sus contenidos actualizados de manera asíncrona.

En lugar de implementar un tiempo de espera, se podría agregar un reloj que escuchará los cambios de contenido (o incluso desencadenantes externos adicionales).

En mi caso, utilicé esta solución para inicializar un complemento jQuery una vez que se realizó la repetición ng que creó mi DOM interno; en otro caso, lo usé solo para manipular el DOM después de que la propiedad del alcance se modificó en el controlador. Así es como lo hice ...

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

                }
            }
        }
    }
}]);

Nota: en lugar de simplemente convertir la variable myContent en bool en el atributo my-directive-watch, uno podría imaginar cualquier expresión arbitraria allí.

Nota: El aislamiento del alcance como en el ejemplo anterior solo se puede hacer una vez por elemento; si se intenta hacer esto con varias directivas sobre el mismo elemento, se generará $ compile: multidir Error: consulte: https://docs.angularjs.org / error / $ compile / multidir

conceptdeluxe
fuente
7

Puede que llegue tarde para responder esta pregunta. Pero aún así alguien puede obtener beneficios de mi respuesta.

Tuve un problema similar y en mi caso no puedo cambiar la directiva, ya que es una biblioteca y cambiar un código de la biblioteca no es una buena práctica. Entonces, lo que hice fue usar una variable para esperar la carga de la página y usar ng-if dentro de mi html para esperar renderizar el elemento en particular.

En mi controlador:

$scope.render=false;

//this will fire after load the the page

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

En mi html (en mi caso, el componente html es un lienzo)

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

Tuve el mismo problema, pero usando Angular + DataTable con una fnDrawCallback+ agrupación de filas + $ directivas anidadas compiladas. Puse el $ timeout en mi fnDrawCallbackfunción para corregir la representación de paginación.

Antes del ejemplo, basado en la fuente 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
  }
}

Después del ejemplo:

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
}

Incluso un breve retraso de tiempo de espera fue suficiente para permitir que Angular procese mis directivas angulares compiladas.

JJ Zabkar
fuente
Por curiosidad, ¿tienes una mesa bastante grande con muchas columnas? porque descubrí que necesito muchos milisegundos molestos (> 100) para no permitir que la tabla DataTable () llame al estrangulador
Nik So
Encontré que el problema sucedió en la navegación de la página DataTable para conjuntos de resultados de 2 filas a más de 150 filas. Entonces, no, no creo que el problema fuera el tamaño de la tabla, pero tal vez DataTable agregó suficiente sobrecarga de procesamiento para eliminar algunos de esos milisegundos. Me enfoqué en lograr que la agrupación de filas funcionara en DataTable con una mínima integración de AngularJS.
JJ Zabkar
2

Ninguna de las soluciones que funcionó para mí acepta el uso de un tiempo de espera. Esto se debe a que estaba usando una plantilla que se estaba creando dinámicamente durante el postLink.

Sin embargo, tenga en cuenta que puede haber un tiempo de espera de '0' ya que el tiempo de espera agrega la función que se llama a la cola del navegador que se producirá después del motor de representación angular, ya que esto ya está en la cola.

Consulte esto: http://blog.brunoscopelliti.com/run-a-directive-after-the-dom-has-finished-rendering

Michael Smolenski
fuente
0

Aquí hay una directiva para tener acciones programadas después de un render superficial. Por superficial quiero decir que evaluará después de ese elemento renderizado y que no estará relacionado con cuándo se representará su contenido. Entonces, si necesita algún subelemento que realice una acción de renderizado posterior, debería considerar usarlo allí:

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

entonces puedes hacer:

<div after-render></div>

o con cualquier expresión útil como:

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

Sebastian Sastre
fuente
Esto no es realmente después de que se representa el contenido. Si tuviera una expresión dentro del elemento <div after-render> {{blah}} </div> en este punto, la expresión aún no se ha evaluado. El contenido del div todavía está {{blah}} dentro de la función de enlace. Entonces, técnicamente, está disparando el evento antes de que se represente el contenido.
Edward Olamisan
Esta es una acción de renderizado poco profunda, nunca he afirmado que sea profunda
Sebastian Sastre,
0

Obtuve esto trabajando con la siguiente directiva:

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

Y en el HTML:

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

resolución de problemas si lo anterior no funciona para usted.

1) tenga en cuenta que 'datatableSetup' es el equivalente de 'datatable-setup'. Angular cambia el formato a camello.

2) asegúrese de que la aplicación esté definida antes de la directiva. por ejemplo, definición y directiva de aplicación simple.

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

Siguiendo el hecho de que no se puede anticipar el orden de carga, se puede utilizar una solución simple.

Veamos la relación directiva-usuario de la directiva. Por lo general, el usuario de la directiva proporcionará algunos datos a la directiva o utilizará alguna funcionalidad (funciones) que proporciona la directiva. La directiva, por otro lado, espera que algunas variables se definan en su alcance.

Si podemos asegurarnos de que todos los jugadores cumplan con todos sus requisitos de acción antes de intentar ejecutar esas acciones, todo debería estar bien.

Y ahora la directiva:

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

        };
})

y ahora el usuario de la directiva html

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

y en algún lugar del controlador del componente que usa la directiva:

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

Eso es todo. Hay muchos gastos generales, pero puede perder el tiempo de espera $. También suponemos que el componente que usa la directiva se instancia antes de la directiva porque dependemos de la variable de control que exista cuando se instancia la directiva.

Eli
fuente