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

182

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

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

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

Вот jsFiddle с изложением моей проблемы: Fiddle-AngularIssue

== ОБНОВЛЕНИЕ ==

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

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

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

gorebash
источник
Это похоже на вопрос, который у меня был. stackoverflow.com/questions/11444494/… . Может быть, что-то там может помочь.
dnc253

Ответы:

39

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

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

см. jsfiddle .

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

dipold
источник
Спасибо, это очень полезно. Это лучшее решение для меня, потому что оно не требует манипуляций dom с контроллера и все равно будет работать, когда данные обновляются из действительно асинхронного ответа службы (и мне не нужно использовать $ evalAsync в моем контроллере).
Горбаш
9
Это осуждается? Когда я пытаюсь это сделать, он на самом деле вызывает непосредственно перед тем, как DOM рендерится. Это зависит от теневого дома или что-то?
Шейн
Я относительно новичок в Angular и не могу понять, как реализовать этот ответ в моем конкретном случае. Я искал разные ответы и догадки, но это не работает. Я не уверен, что должна смотреть функция «смотреть». Наше угловое приложение имеет различные директивы, которые будут варьироваться от страницы к странице, так как я знаю, что «смотреть»? Конечно, есть простой способ запустить какой-то код, когда страница будет завершена?
moosefetcher
63

Во-первых, правильное место, чтобы связываться с рендерингом - это директивы. Мой совет - обернуть 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);
                });
            });
        },
    };
});

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

Данияр
источник
Это может указывать на очевидное, но если это не работает для вас, проверьте асинхронные операции в функциях / контроллерах ссылок. Я не думаю, что $ evalAsync может принять это во внимание.
LOAS
Стоит отметить, что вы также можете комбинировать linkфункцию с templateUrl.
Wtower
35

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

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

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

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

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

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

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

Марк Райкок
источник
4
$scope.$evalAsync($scope.assignmentsLoaded(data));не имеет никакого смысла ИМО. Аргументом для $evalAsync()должна быть функция. В вашем примере вы вызываете функцию в тот момент, когда функция должна быть зарегистрирована для последующего выполнения.
hgoebl
@hgoebl, аргумент $ evalAsync может быть функцией или выражением. В этом случае я использовал выражение. Но вы правы, выражение выполняется немедленно. Я изменил ответ, чтобы обернуть его в функцию для последующего выполнения. Спасибо, что поймали это.
Марк Райкок,
26

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

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

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

fuzion9
источник
3
Это взломать наверняка, но это именно то, что мне нужно. Вынудил мой код работать точно после рендеринга (или очень близко к точному). В идеальном мире я бы так не поступил, но вот мы ...
Андрей
Мне нужно $anchorScrollбыло вызывать встроенный сервис после рендеринга DOM, и, похоже, ничего не помогло, кроме этого. Взломать, но работает.
Пэт Мильяччио
ng-init - лучшая альтернатива
Flying Gambit
1
ng-init может не всегда работать. Например, у меня есть ng-repeat, который добавляет несколько изображений в dom. Если я хочу вызвать функцию после добавления каждого изображения в ng-repeat, я должен использовать ng-show.
Майкл Бремеркамп,
7

Я думаю, что вы ищете $ evalAsync http://docs.angularjs.org/api/ng.$rootScope.Scope#$evalAsync

Миско Хевери
источник
2
Спасибо за комментарий. Возможно, я слишком новичок в Angular, но я не вижу, как это сделать в моем коде. Есть ли примеры, на которые вы могли бы указать мне?
Горбаш
4

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

$scope.$watchCollection( 'conferences', function( old, nuew ) {
        if( old === nuew ) return;
        $( '#dataTablex' ).dataTable().fnDestroy();
        $timeout(function () {
                $( '#dataTablex' ).dataTable();
        });
    });
Серхио Керон
источник
3

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

mgrimmenator
источник
2

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

Посмотрите, что ответ stackoverflow .

Энтони О.
источник
2

Ни $ 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>
Xchai
источник
1

В некоторых сценариях, когда вы обновляете сервис и перенаправляете на новое представление (страницу), а затем ваша директива загружается до обновления ваших сервисов, вы можете использовать $ 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);
        }
   };
});
Sudharshan
источник
1

Я пришел с довольно простым решением. Я не уверен, является ли это правильным способом, но это работает в практическом смысле. Давайте прямо посмотрим, что мы хотим сделать. Например, в директиве, включающей некоторые ng-repeats, я бы следил за длиной текста (у вас могут быть другие вещи!) Абзацев или всего 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длины (или любой другой меры) с вашей моделью, если вы хотите запускать код только один раз после завершения рендеринга. Я ценю любые мысли / комментарии по этому поводу.

Ehsan88
источник
0

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

Майкл Корк.
источник