AngularJS: Как запустить дополнительный код после того, как AngularJS отобразит шаблон?
У меня есть угловой шаблон в DOM. Когда мой контроллер получает новые данные от службы, он обновляет модель в области $ и повторно отображает шаблон. Пока все хорошо.
Проблема заключается в том, что мне нужно также выполнить дополнительную работу после того, как шаблон был перерисован и находится в DOM (в данном случае плагин jQuery).
Кажется, что должно быть событие для прослушивания, например AfterRender, но я не могу найти ничего подобного. Может быть, директива была бы способом пойти, но, похоже, она тоже сработала слишком рано.
Вот jsFiddle с изложением моей проблемы: Fiddle-AngularIssue
== ОБНОВЛЕНИЕ ==
Основываясь на полезных комментариях, я соответственно переключился на директиву для обработки манипуляций с DOM и реализовал модель $watch внутри директивы. Тем не менее, я все еще имею ту же самую проблему с базой; код внутри события $watch запускается до того, как шаблон был скомпилирован и вставлен в DOM, поэтому плагин jquery всегда оценивает пустую таблицу.
Интересно, что если я уберу асинхронный вызов, все будет работать нормально, так что это шаг в правильном направлении.
Вот мой обновленный Fiddle, чтобы отразить эти изменения: http://jsfiddle.net/uNREn/12/
13 ответов
Этот пост старый, но я изменил ваш код на:
scope.$watch("assignments", function (value) {//I change here
var val = value || null;
if (val)
element.dataTable({"bDestroy": true});
});
}
см. jsfiddle.
Я надеюсь, что это поможет вам
Во-первых, правильное место, чтобы связываться с рендерингом - это директивы. Мой совет - обернуть 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);
});
});
},
};
});
Надеюсь, что это поможет вам предотвратить некоторую борьбу.
Следуя совету Миско, если вы хотите асинхронную операцию, то вместо $timeout() (что не работает)
$timeout(function () { $scope.assignmentsLoaded(data); }, 1000);
используйте $evalAsync() (который работает)
$scope.$evalAsync(function() { $scope.assignmentsLoaded(data); } );
Скрипка Я также добавил ссылку "удалить строку данных", которая изменит $ scope.assignments, имитируя изменение данных / модели, чтобы показать, что изменение данных работает.
Раздел Runtime на странице концептуального обзора объясняет, что evalAsync следует использовать, когда вам нужно, чтобы что-то произошло за пределами текущего фрейма стека, но до того, как браузер отобразит. (Догадываясь здесь... "текущий кадр стека", вероятно, включает обновления Angular DOM.) Используйте $ timeout, если вам нужно, чтобы что-то произошло после рендеринга браузера.
Однако, как вы уже узнали, я не думаю, что здесь есть необходимость в асинхронной операции.
Я обнаружил, что самое простое (дешевое и веселое) решение - просто добавить пустой диапазон с помощью ng-show = "someFunctionThatAlwaysReturnsZeroOrNothing()" в конце последнего отображенного элемента. Эта функция будет запускаться, когда нужно проверить, должен ли отображаться элемент span. Выполните любой другой код в этой функции.
Я понимаю, что это не самый элегантный способ делать вещи, однако, он работает для меня...
У меня была похожая ситуация, хотя она была немного изменена, когда мне нужно было удалить индикатор загрузки, когда анимация началась, на мобильных устройствах angular инициализировался намного быстрее, чем отображаемая анимация, и использование ng-cloak было недостаточно, так как индикатор загрузки был удаляется задолго до того, как будут отображены реальные данные В этом случае я просто добавил функцию return 0 к первому отображаемому элементу, и в этой функции перевернул var, который скрывает индикатор загрузки. (конечно, я добавил ng-hide к индикатору загрузки, запускаемому этой функцией.
Я думаю, что вы ищете $ evalAsync http://docs.angularjs.org/api/ng.$rootScope.Scope
Наконец, я нашел решение, я использовал REST-сервис для обновления своей коллекции. Чтобы преобразовать датируемый jquery, необходимо выполнить следующий код:
$scope.$watchCollection( 'conferences', function( old, nuew ) {
if( old === nuew ) return;
$( '#dataTablex' ).dataTable().fnDestroy();
$timeout(function () {
$( '#dataTablex' ).dataTable();
});
});
Мне приходилось делать это довольно часто. у меня есть директива, и мне нужно сделать некоторые вещи jquery после того, как модель полностью загружена в DOM. поэтому я поместил свою логику в ссылку: function из директивы и обернул код в setTimeout(function() { ..... }, 1); setTimout сработает после загрузки DOM, и 1 миллисекунда - это самое короткое время после загрузки DOM до выполнения кода. это, кажется, работает для меня, но я бы хотел, чтобы angular вызывал событие, как только шаблон был загружен, чтобы директивы, используемые этим шаблоном, могли выполнять jquery и получать доступ к элементам DOM. надеюсь это поможет.
Вы также можете создать директиву, которая запускает ваш код в функции ссылки.
Посмотрите, что ответ stackru.
Ни $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>
Я пришел с довольно простым решением. Я не уверен, является ли это правильным способом, но это работает в практическом смысле. Давайте прямо посмотрим, что мы хотим отрендерить. Например, в директиве, которая включает в себя некоторые 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
s Длина (или любая другая мера) с вашей моделью, если вы хотите запускать код только один раз после завершения рендеринга. Я ценю любые мысли / комментарии по этому поводу.
В некоторых сценариях, когда вы обновляете сервис и перенаправляете на новое представление (страницу), а затем ваша директива загружается до обновления ваших сервисов, вы можете использовать $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);
}
};
});
Вы можете использовать модуль jQuery Passthrough утилит angular-ui. Я успешно связал плагин карусели jQuery touch с некоторыми изображениями, которые я извлекаю асинхронно из веб-службы и отображаю с помощью ng-repeat.
В моем решении у меня было несколько пользовательских директив, которые нужно было загрузить в первую очередь, потому что они содержали определение функций, которые вызывают их одноуровневые директивы. Например:
<div id="container">
<custom-directive1></custom-directive1>
<custom-directive2></custom-directive2>
<custom-directive3></custom-directive3>
</div>
К сожалению, ни одно из приведенных здесь решений не сработало для меня, потому что они работали только после рендеринга директивы, а не кода директивы.
Поэтому, когда я реализовал любое из приведенных выше решений для выполнения некоторой функции загрузки, даже если директива была обработана, область действия не знала, какие функции были внутри этих директив.
Итак, я создал наблюдаемое в любом месте моего контроллера:
//Call every time a directive is loaded
$scope.$watch('directiveLoaded', function (value) {
debugger;
if (value == document.querySelector('#container').children.length) {
//Its ok to use childHead as we have only one child scope
$scope.$$childHead.function1_Of_Directive1();
$scope.$$childHead.function1_Of_Directive2();
}
});
Затем у меня есть эти две директивы, где я размещаю
scope.$parent.directiveLoaded += 1;
внизу каждой директивы. Поскольку в контроллере у меня определена наблюдаемая, каждый раз, когда я обновляю переменную
directiveLoaded
, он выполняет наблюдаемую функцию. Да, я знаю, что это хак, но это небольшая цена, чтобы гарантировать, что все директивы завершат рендеринг вместе с их кодом перед выполнением финальной функции.
Чтобы завершить демонстрацию, вот две директивы, которые определяют функции, которые необходимо вызвать позже.
Директива 1
(function () {
app.directive('customDirective1', function () {
return {
restrict: 'E',
templateUrl: '/directive1.html',
link: function (scope) {
scope.function1_Of_Directive1 = function() {
scope.function2_Of_Directive2();
console.log("F1_D1")
}
//AT BOTTOM OF EVERY DIRECTIVE
scope.$parent.directiveLoaded += 1;
}
}
});
})();
Директива2
(function () {
app.directive('customDirective2', function () {
return {
restrict: 'E',
templateUrl: '/directive1.html',
link: function (scope) {
scope.function1_Of_Directive2 = function() {
console.log("F1_D2")
}
scope.function2_Of_Directive2 = function() {
console.log("F2_D2")
}
//AT BOTTOM OF EVERY DIRECTIVE
scope.$parent.directiveLoaded += 1;
}
}
});
})();