AngularJS: Как запустить дополнительный код после того, как AngularJS предоставил шаблон?

У меня есть шаблон Angular в DOM. Когда мой контроллер получает новые данные от службы, он обновляет модель в области $scope и повторно отображает шаблон. Все хорошо до сих пор.

Проблема в том, что мне нужно также выполнить дополнительную работу после того, как шаблон был повторно отображен и находится в DOM (в данном случае плагин jQuery).

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

Вот jsFiddle, излагающий мою проблему: Fiddle-AngularIssue

== UPDATE ==

Основываясь на полезных комментариях, я, соответственно, переключился на директиву для обработки DOM-манипуляций и реализовал модель $watch внутри директивы. Однако у меня все еще есть одна и та же базовая проблема; код внутри события $watch срабатывает до того, как шаблон был скомпилирован и вставлен в DOM, поэтому плагин jquery всегда оценивает пустую таблицу.

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

Вот мой обновленный Fiddle, чтобы отразить эти изменения: http://jsfiddle.net/uNREn/12/

Ответ 1

Этот пост старый, но я меняю ваш код на:

scope.$watch("assignments", function (value) {//I change here
  var val = value || null;            
  if (val)
    element.dataTable({"bDestroy": true});
  });
}

см: http://jsfiddle.net/dnzY9/

Я надеюсь, что это поможет вам

Ответ 2

Во-первых, правильное место для беспорядка с рендерингом - это директивы. Моим советом было бы обернуть DOM, манипулируя плагинами jQuery такими директивами, как этот.

У меня была такая же проблема, и я придумал этот фрагмент. Он использует $watch и $evalAsync для обеспечения того, чтобы ваш код выполнялся после того, как были разрешены такие директивы, как ng-repeat, и были отобраны шаблоны, такие как {{ value }}.

app.directive('name', function() {
    return {
        link: function($scope, element, attrs) {
            // Trigger when number of children changes,
            // including by directives like ng-repeat
            var watch = $scope.$watch(function() {
                return element.children().length;
            }, function() {
                // Wait for templates to render
                $scope.$evalAsync(function() {
                    // Finally, directives are evaluated
                    // and templates are renderer here
                    var children = element.children();
                    console.log(children);
                });
            });
        },
    };
});

Надеюсь, это поможет вам предотвратить некоторую борьбу.

Ответ 3

Следуя совету Misko, если вы хотите выполнить операцию async, вместо $timeout() (который не работает)

$timeout(function () { $scope.assignmentsLoaded(data); }, 1000);

используйте $evalAsync() (который работает)

$scope.$evalAsync(function() { $scope.assignmentsLoaded(data); } );

Fiddle. Я также добавил ссылку "удалить строку данных", которая изменит параметры $scope.assignments, имитируя изменение данных/модели - чтобы показать, что изменение данных работает.

Раздел Runtime на странице "Концептуальный обзор" объясняет, что evalAsync следует использовать, когда вам нужно что-то произойти за пределами текущего кадра стека, но перед отображением браузера. (Предполагая, что здесь... "текущий стек кадров", вероятно, включает в себя обновления Angular DOM.) Используйте $timeout, если вам нужно что-то возникнуть после отображения браузера.

Однако, как вы уже выяснили, я не думаю, что здесь есть необходимость в async-операции.

Ответ 4

Я нашел, что самое простое (дешевое и веселое) решение просто добавляет пустой диапазон с ng-show = "someFunctionThatAlwaysReturnsZeroOrNothing()" в конец последнего обработанного элемента. Эта функция будет выполняться, когда необходимо проверить, должен ли отображаться элемент span. Выполните любой другой код в этой функции.

Я понимаю, что это не самый элегантный способ сделать что-то, однако он работает для меня...

У меня была аналогичная ситуация, хотя и слегка изменилась, когда мне нужно было удалить индикатор загрузки при начале анимации, на мобильных устройствах angular была инициализирована намного быстрее, чем анимация, отображаемая, а использование ng-cloak было недостаточным поскольку индикатор загрузки был удален задолго до отображения реальных данных. В этом случае я просто добавил функцию return 0 к первому визуализированному элементу, и в этой функции щелкнул var, который скрывает индикатор загрузки. (конечно, я добавил ng-hide к индикатору загрузки, вызванному этой функцией.

Ответ 6

Наконец, я нашел решение, я использовал службу REST для обновления моей коллекции. Для преобразования datatable jquery следует следующий код:

$scope.$watchCollection( 'conferences', function( old, nuew ) {
        if( old === nuew ) return;
        $( '#dataTablex' ).dataTable().fnDestroy();
        $timeout(function () {
                $( '#dataTablex' ).dataTable();
        });
    });

Ответ 7

Мне приходилось делать это довольно часто. У меня есть директива и вам нужно сделать некоторые jquery вещи после того, как материал модели полностью загружен в DOM. поэтому я поместил свою логику в ссылку: функцию директивы и оберните код в setTimeout (function() {.....}, 1); setTimout будет срабатывать после загрузки DOM и 1 milisecond - это самый короткий промежуток времени после загрузки DOM до того, как код будет выполнен. это, похоже, работает для меня, но я действительно желаю, чтобы angular поднял событие после того, как шаблон был загружен, так что директивы, используемые этим шаблоном, могли выполнять jquery-материал и получать доступ к элементам DOM. надеюсь, что это поможет.

Ответ 8

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

См. fooobar.com/questions/18590/....

Ответ 9

Ни $scope. $evalAsync(), либо $timeout (fn, 0) работали надежно для меня.

Мне пришлось совместить эти два. Я сделал директиву, а также поставил приоритет выше, чем значение по умолчанию для хорошей меры. Вот для него директива (обратите внимание, что я использую ngInject для вставки зависимостей):

app.directive('postrenderAction', postrenderAction);

/* @ngInject */
function postrenderAction($timeout) {
    // ### Directive Interface
    // Defines base properties for the directive.
    var directive = {
        restrict: 'A',
        priority: 101,
        link: link
    };
    return directive;

    // ### Link Function
    // Provides functionality for the directive during the DOM building/data binding stage.
    function link(scope, element, attrs) {
        $timeout(function() {
            scope.$evalAsync(attrs.postrenderAction);
        }, 0);
    }
}

Чтобы вызвать директиву, вы должны сделать следующее:

<div postrender-action="functionToRun()"></div>

Если вы хотите вызвать его после выполнения ng-repeat, я добавил пустой диапазон в ng-repeat и ng-if = "$ last":

<li ng-repeat="item in list">
    <!-- Do stuff with list -->
    ...

    <!-- Fire function after the last element is rendered -->
    <span ng-if="$last" postrender-action="$ctrl.postRender()"></span>
</li>

Ответ 10

Я пришел с довольно простым решением. Я не уверен, что это правильный способ сделать это, но он работает в практическом смысле. Позвольте прямо посмотреть, что мы хотим сделать. Например, в директиве, которая включает в себя некоторые ng-repeat s, я бы следил за длиной текста (вы можете иметь другие вещи!) Абзацев или всего html. Директива будет такой:

.directive('myDirective', [function () {
    'use strict';
    return {

        link: function (scope, element, attrs) {
            scope.$watch(function(){
               var whole_p_length = 0;
               var ps = element.find('p');
                for (var i=0;i<ps.length;i++){
                    if (ps[i].innerHTML == undefined){
                        continue
                    }
                    whole_p_length+= ps[i].innerHTML.length;
                }
                //it could be this too:  whole_p_length = element[0].innerHTML.length; but my test showed that the above method is a bit faster
                console.log(whole_p_length);
                return whole_p_length;
            }, function (value) {   
                //Code you want to be run after rendering changes
            });
        }
}]);

ПРИМЕЧАНИЕ, что код действительно запускается после рендеринга изменений, а не полного рендеринга. Но я думаю, что в большинстве случаев вы можете справляться с ситуациями всякий раз, когда происходят изменения. Также вы можете подумать о сравнении этой длины p (или любой другой меры) с вашей моделью, если вы хотите запустить свой код только один раз после завершения рендеринга. Я ценю любые мысли/комментарии по этому поводу.

Ответ 11

Вы можете использовать модуль jQuery Passthrough angular -ui utils. Я успешно привязал плагин jQuery touch carousel к некоторым изображениям, которые я извлекаю async из веб-службы и визуализирую их с помощью ng-repeat.

Ответ 12

В некоторых сценариях, в которых вы обновляете сервис и перенаправляете на новое представление (страница), а затем ваша директива загружается до обновления ваших сервисов, вы можете использовать $rootScope. $broadcast, если ваши $watch или $timeout не выполняются

Вид

<service-history log="log" data-ng-repeat="log in requiedData"></service-history>

контроллер

app.controller("MyController",['$scope','$rootScope', function($scope, $rootScope) {

   $scope.$on('$viewContentLoaded', function () {
       SomeSerive.getHistory().then(function(data) {
           $scope.requiedData = data;
           $rootScope.$broadcast("history-updation");
       });
  });

}]);

Директива

app.directive("serviceHistory", function() {
    return {
        restrict: 'E',
        replace: true,
        scope: {
           log: '='
        },
        link: function($scope, element, attrs) {
            function updateHistory() {
               if(log) {
                   //do something
               }
            }
            $rootScope.$on("history-updation", updateHistory);
        }
   };
});