Есть ли обратный вызов post render для Angular JS-директивы?

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

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

Я также использую плагин jQuery под названием DataTables. Общее его использование выглядит так: $('table # some_id'). DataTable(). Вы можете передать данные JSON в вызов dataTable(), чтобы предоставить данные таблицы, или вы можете иметь данные уже на странице, и это сделает все остальное. Я делаю последнее, имея строки уже на HTML-странице.

Но проблема в том, что я должен вызвать dataTable() в таблице # line_items ПОСЛЕ ДОМА. Моя директива выше вызывает метод dataTable() ПЕРЕД добавлением шаблона к элементу директивы. Есть ли способ, которым я могу вызывать функции ПОСЛЕ добавления?

Благодарим за помощь!

ОБНОВЛЕНИЕ 1 после ответа Энди:

Я хочу убедиться, что метод link вызван только после того, как все на странице, поэтому я изменил директиву для небольшого теста:

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

    }

И я действительно вижу "boo" в div # sayboo.

Затем я пытаюсь выполнить мой jquery datatable call

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

Не повезло там

Затем я пытаюсь добавить тайм-аут:

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

И это работает. Поэтому я задаюсь вопросом, что пошло не так в версии кода без таймера?

Ответ 1

Если второй параметр, "задержка" не указан, по умолчанию выполняется выполнение функции после завершения рендеринга DOM. Поэтому вместо setTimeout используйте $timeout:

$timeout(function () {
    //DOM has finished rendering
});

Ответ 2

У меня была такая же проблема, и я считаю, что ответ действительно нет. См. комментарий Мишко и некоторые обсуждения в группе.

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

Мы решили эту эвристику с помощью setTimeout, как и вы.

(Пожалуйста, имейте в виду, что не все согласны со мной - вы должны прочитать комментарии по ссылкам выше и посмотреть, что вы думаете.)

Ответ 3

Вы можете использовать функцию "link", также известную как postLink, которая запускается после ввода шаблона.

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

Дайте это прочитать, если вы планируете создавать директивы, это большая помощь: http://docs.angularjs.org/guide/directive

Ответ 4

Хотя мой ответ не связан с datatables, он решает проблему манипулирования DOM и, например, Инициализация плагина jQuery для директив, используемых для элементов, содержимое которых обновляется асинхронно.

Вместо реализации тайм-аута можно было просто добавить часы, которые будут прослушивать изменения контента (или даже дополнительные внешние триггеры).

В моем случае я использовал этот обходной путь для инициализации плагина jQuery после выполнения ng-repeat, который создал мою внутреннюю DOM - в другом случае я использовал его для просто манипулирования DOM после того, как свойство scope было изменено на контроллере. Вот как я это сделал...

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

                }
            }
        }
    }
}]);

Примечание. Вместо того, чтобы просто перебрасывать переменную myContent в bool в атрибуте my-directive-watch, можно было бы представить любое произвольное выражение там.

Примечание.. Изоляция области, как в приведенном выше примере, может выполняться только один раз для каждого элемента - попытка сделать это с помощью нескольких директив в одном элементе приведет к компиляции $: multidir Error - см.: https://docs.angularjs.org/error/$compile/multidir

Ответ 5

Может быть, опаздываю, чтобы ответить на этот вопрос. Но кто-то может получить выгоду из моего ответа.

У меня была аналогичная проблема, и в моем случае я не могу изменить директиву, так как это библиотека и изменение кода библиотеки не является хорошей практикой. Так что я использовал переменную, чтобы ждать загрузки страницы и использовать ng-if внутри моего html, чтобы ждать рендеринга конкретного элемента.

В моем контроллере:

$scope.render=false;

//this will fire after load the the page

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

В моем html (в моем случае компонент html - это холст)

<canvas ng-if="render"> </canvas>

Ответ 6

У меня была такая же проблема, но с использованием Angular + DataTable с fnDrawCallback + группировка строк + $скомпилированные вложенные директивы. Я поместил $timeout в мою функцию fnDrawCallback для исправления рендеринга разбиения на страницы.

До примера, на основе источника 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
  }
}

После примера:

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
}

Даже короткой задержки ожидания было достаточно, чтобы позволить Angular отображать мои скомпилированные директивы Angular.

Ответ 7

Ни один из решений, с которыми я работал, не согласился с использованием тайм-аута. Это связано с тем, что я использовал шаблон, который динамически создавался во время postLink.

Обратите внимание, что время ожидания "0" может быть превышено, так как тайм-аут добавляет вызываемую функцию в очередь браузера, которая будет возникать после механизма рендеринга angular, поскольку это уже находится в очереди.

Обратитесь к этому: http://blog.brunoscopelliti.com/run-a-directive-after-the-dom-has-finished-rendering

Ответ 8

Я получил эту работу со следующей директивой:

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

И в HTML:

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

устранение неполадок, если выше не работает для вас.

1) обратите внимание, что 'datatableSetup' является эквивалентом 'datatable-setup'. Angular изменяет формат в случае с верблюдом.

2) убедитесь, что приложение определено перед директивой. например простое определение приложения и директива.

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

Ответ 9

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

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

то вы можете сделать:

<div after-render></div>

или с любым полезным выражением, например:

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

Ответ 10

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

Посмотрим на директиву - отношение "пользователь директивы". Обычно пользователь директивы будет предоставлять некоторые данные в директиву или использовать некоторые функции (функции) для поставок директив. Директива, с другой стороны, ожидает, что в ее области будут определены некоторые переменные.

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

И теперь директива:

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

        };
})

и теперь пользователь директивы html

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

и где-то в контроллере компонента, который использует директиву:

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

Что об этом. Существует много накладных расходов, но вы можете потерять $timeout. Мы также предполагаем, что компонент, который использует директиву, создается перед директивой, потому что мы зависим от управляющей переменной, которая существует при создании экземпляра директивы.