how to exec js after AngularJS ng-repeat finished - angularjs

I'm new to AngularJS. I want to use ng-repeat to render a list of data.
Each of the data should have a <abbr class="timeago" title="2012-10-10 05:47:21"></abbr> alike after rendered. And then I could use jquery plugin timeago to turn it into human friendly text about 1 hour ago.
My code is as below. But it take no effect. Please help.
EDIT: My problem is that, I can get the right html rendered. But code in directive do not run.
the html:
<div ng-app='weiboApp' ng-controller="WeiboListCtrl">
<table><tbody>
<tr ng-repeat='w in weibo' weiboLister='w'>
<td>{{ w.text }}></td>
<td><abbr class="timeago" title="{{ w.created }}"></abbr></td>
</tr>
</tbody></table>
</div>
the js:
var module = angular
.module('weiboApp', [])
.directive('weiboLister', function () {
return {
restrict: 'A',
link: function (scope, element, attr) {
scope.watch('w', function (val) {
console.log(element); //this never run
element.find("abbr.timeago").timeago(); //this never run
}, true);
}
}
});
function WeiboListCtrl($scope, $http) {
$http.get('/api/weibo/list').success(function(data) {
$scope.weibo = data;
});
}

The problem turned out to be: should define directive with camel-case weiboLister and use it in html with snake-case weibo-lister. Thanks to #tosh shimayama.
The correct code as below: (I added a remove function in case you're looking for the same thing.)
the html:
<div ng-app='weiboApp' ng-controller="WeiboListCtrl">
<table><tbody>
<tr ng-repeat='w in weibo' weibo-lister='w'> <!--important to be snake-case here-->
<td>{{ w.text }}></td>
<td><abbr class="timeago" title="{{ w.created }}"></abbr></td>
<td><a ng-click='remove(w)'>×</a></td>
</tr>
</tbody></table>
</div>
the js:
var module = angular
.module('weiboApp', [])
.directive('weiboLister', function () {
function delete(id, s_function, f_function) {
//...
if success { s_function(); }
else { f_function(); }
}
return {
restrict: 'A',
link: function (scope, element, attr) {
scope.$watch('w', function (val) {
element.find("abbr.timeago").timeago();
}
scope.destroy = function(callback) {
deletenews(scope.w.id, function () {
//s_function, things you want to do when delete with success
element.fadeOut(400, function () {
//this callback will splice the element in model
if (callback) callback.apply(scope);
})
}, function () {
//f_function, when delete with failure
});
};
}
}
});
function WeiboListCtrl($scope, $http) {
$http.get('/api/weibo/list').success(function(data) {
$scope.weibo = data;
});
$scope.removeWeibo = function(w) {
var idx = $scope.weibo.indexOf(w);
if (idx !== -1) {
this.destroy(function() {
$scope.weibo.splice(idx, 1);
});
}
};
}

Related

AngularJS: Cloning element containing ng-repeat

I am creating a directive with Angular 1.6 which creates a fixed header for a table. I am trying to achieve this by clone the table header and fixing this. This all works fine in most of my tables. I am using scope: false to keep parent scope, as some header elements references e.g. sorting functions. This also works. My problem is with one table I have which creates columns based on an array, because I want to be able to change columns. The columns are added with ng-repeat. When I clone this header, the ng-repeat is not cloned.
What can I do to clone an element containing an ng-repeat?
HTML of table:
<table class="proloen-table no-last-border" table-fix-header>
<thead class="light-blue-background">
<tr>
<th>{{vm.testString}}</th>
<th ng-repeat="head in vm.tableHeaders">
<span>{{ head.label | translate }}</span>
<sorting sortkey="head.sort" color="'white'" filter="vm.state.sorting"></sorting>
</th>
</tr>
</thead>
...
</table>
Controller (with controllerAs: 'vm') has (among other things):
vm.testString = 'Test';
vm.tableHeaders = [{label: 'Column1', sort: 'prop1'}, {label: 'Column2', sort: 'prop2'}];
The directive is as follows:
.directive('tableFixHeader', function ($window, $compile) {
return {
restrict: 'A',
scope: false,
link: function (scope, element) {
var clone;
function init() {
element.wrap('<div class="fix-table-container"></div>');
clone = element.clone(true);
clone.find('tbody').remove().end().addClass('table-header-fixed');
clone.removeAttr('table-fix-header');
$compile(clone)(scope);
element.before(clone);
resizeFixed();
}
function resizeFixed() {
clone.find('th').each(function (index) {
$(this).css('width', element.find('th').eq(index).outerWidth() + 'px');
});
}
function scrollFixed() {
var offset = $($window).scrollTop(),
tableOffsetTop = element.offset().top,
tableOffsetBottom = tableOffsetTop + element.height() - element.find('thead').height();
if (offset < tableOffsetTop || offset > tableOffsetBottom){
clone.hide();
}
else if (offset >= tableOffsetTop && offset <= tableOffsetBottom && clone.is(':hidden')) {
clone.show();
}
}
$window.addEventListener('scroll', scrollFixed);
$window.addEventListener('resize', resizeFixed);
scope.$on('$destroy', function() {
$window.removeEventListener('scroll', scrollFixed);
$window.removeEventListener('resize', resizeFixed);
});
init();
}
};
});
The directive works fine for tables columns are fixed and the above example clones the first "hardcoded" column just fine, along the variable from controller. The problem arises when cloning the ng-repeat. I just can't seem to figure out how to clone the ng-repeat, so that it to will work and update when I update the list of columns.
You could try to send event with $scope.$emit when ng-repeat finished rendering. Or create own event emitter and connect your directives;
app.directive('onFinishRepeat', function(){
return {
restrict: 'A',
link: function($scope) {
if($scope.$last == true) {
$scope.$emit('ng-repeat', 'finish');
}
}
}
})
app.directive('tableFixHeader', function ($window, $compile) {
return {
restrict: 'A',
scope: false,
link: function (scope, element) {
var clone;
function init() {
element.wrap('<div class="fix-table-container"></div>');
clone = element.clone(true);
clone.find('tbody').remove().end().addClass('table-header-fixed');
clone.removeAttr('table-fix-header');
$compile(clone)(scope);
element.before(clone);
resizeFixed();
}
function resizeFixed() {
clone.find('th').each(function (index) {
$(this).css('width', element.find('th').eq(index).outerWidth() + 'px');
});
}
function scrollFixed() {
var offset = $($window).scrollTop(),
tableOffsetTop = element.offset().top,
tableOffsetBottom = tableOffsetTop + element.height() - element.find('thead').height();
if (offset < tableOffsetTop || offset > tableOffsetBottom){
clone.hide();
}
else if (offset >= tableOffsetTop && offset <= tableOffsetBottom && clone.is(':hidden')) {
clone.show();
}
}
$window.addEventListener('scroll', scrollFixed);
$window.addEventListener('resize', resizeFixed);
scope.$on('$destroy', function() {
$window.removeEventListener('scroll', scrollFixed);
$window.removeEventListener('resize', resizeFixed);
});
$scope.on('ng-repeat', function(event, data){
if(data == 'finish') {
init();
}
})
}
};
});
HTML
<table class="proloen-table no-last-border" table-fix-header>
<thead class="light-blue-background">
<tr>
<th>{{vm.testString}}</th>
<th ng-repeat="head in vm.tableHeaders" on-finish-repeat>
<span>{{ head.label | translate }}</span>
<sorting sortkey="head.sort" color="'white'" filter="vm.state.sorting"></sorting>
</th>
</tr>
</thead>
...
</table>

Angular sce.trustAsHtml not working

I have this angular controller :
var applicaton = angular.module("appUsed", ['ui.router','ngSanitize'] );
applicaton.controller('gamesController', ['$scope','$http','$sce','$stateParams',function(scope,http,sce,stateParams){
http.get('/'+stateParams.category+'/'+stateParams.id)
.success(function(result){
scope.Game = result.gameDetails;
scope.relatedGames = result.relatedGames;
console.log(scope.Game.title);
console.log(scope.Game.url);
scope.gameUrl = sce.trustAsHtml('<iframe allowfullscreen width="80%" height="600px src="'+scope.Game.url+'"></iframe>');
});
}]);
and this html :
<div class="game_and_description">
<div ng-bind-html="gameUrl"></div>
<h3> Description</h3>
<p> {{Game.description}}</p>
It shows me a white iframe. I searched over the internet and i've done everything right. The modules form angular ng-sanitize is running(called from <script> tag) and i have no error. the console log on scopes works like a charm. Don't know where should i look anymore. Please help.
You need to give a trust to the URL you are using in the iframe, and compile the html:
<div ng-controller="gamesController">
<div bind-html-compile="gameFrame"></div>
</div>
var myApp = angular
.module('appUsed',['ngSanitize'])
.controller('gamesController', ['$scope', '$sce', function (scope, sce) {
scope.Game = {
url: 'https://play.famobi.com/hop-dont-stop/A-DXC93'
};
scope.gameUrl = sce.trustAsResourceUrl(scope.Game.url);
scope.gameFrame = sce.trustAsHtml('<iframe allowfullscreen width="80%" height="600px" ng-src="{{gameUrl}}"></iframe>');
}])
.directive('bindHtmlCompile', ['$compile', function ($compile) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch(function () {
return scope.$eval(attrs.bindHtmlCompile);
}, function (value) {
element.html(value && value.toString());
var compileScope = scope;
if (attrs.bindHtmlScope) {
compileScope = scope.$eval(attrs.bindHtmlScope);
}
$compile(element.contents())(compileScope);
});
}
};
}]);
See https://github.com/incuna/angular-bind-html-compile.
The working fiddle: http://jsfiddle.net/masa671/k2e43nvf/
I had a similar problem. I solved it like this :
my view :
<div ng-bind-html="getDescription()"></div>
my controller :
$scope.getDescription = function () {
if ($scope.description != null && $scope.todo.description.length > 0) {
return $sce.trustAsHtml($scope.description);
} else {
return 'no description.';
}
};

How to pass dynamic object in angularjs directive

I am trying to generate multiple chart on single page with ng-repeat. The directive to achieve this accept chart data object as below.
module.directive('miChart', function () {
return {
restrict: 'E',
template: '<div></div>',
scope: {
chartData: "=value",
chartObj: "=?"
},
replace: true,
link: function ($scope, $element, $attrs) {
$scope.$watch('chartData', function (value) {
if (!value) {
return;
}
$scope.chartData.chart.renderTo = $scope.chartData.chart.renderTo || $element[0];
$scope.chartObj = new Highcharts.Chart($scope.chartData);
});
}
}
});
The basic html is as below.
<table>
<tr>
<td>
<mi-chart value="pieChartData" chart-obj="pieChartObj"></mi-chart>
</td>
<td>
<mi-chart ng-repeat="property in Property.Competitors" value="property.PropertyId" chart-obj="property.PropertyId"></mi-chart>
</td>
</tr>
</table>
Javascript code to assign values to scope.
dataPromise.then(function (serverResponse) {
var pieChart = serverResponse.data;
var pieChartData = getBaseChartData(pieChart.Primary);
var chartData = jQuery.extend(true, {}, pieChartData);
chartData = jQuery.extend(true, chartData, chartDiff);
$scope.pieChartData = chartData;
if (pieChart.Competitors.length > 0)
{
jQuery.each(pieChart.Competitors, function (index, CompData) {
var pieChartDataComp = getBaseChartData(CompData);
var chartDataComp = jQuery.extend(true, {}, pieChartDataComp);
chartDataComp = jQuery.extend(true, chartDataComp, chartDiff);
$scope[CompData.PropertyId] = chartDataComp;
}
});
}
});
In above code i am able to render my pieChartData with my directive.
But the code doesnt work in case of ng-repeat where i have to create dynamic object based on values in "Property.Competitors".
I can not use string(#) or one way binding(&) in my case as i have to deal with chart(highchart) object and watch functionality.
I am assigning dynamic scope values in my service with server side calls.
Can anybody help me out to generate multiple chart with ng-repeat and directive.
How can i create dynamic object and keep watch on that?

Using the enter key as tab using only angularjs and jqlite

I have looked at multiple threads and tried a vast variety of solutions.
Quite frankly I think I am losing my mind.
I have an ng-repeat with inputs. All that needs to happen is that when the user presses enter, it should shift focus to the next input, basically simulating the tab key functionality.
The code (incomplete):
HTML:
<body ng-app="ap" ng-controller="con">
<table>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
<tr ng-repeat='person in persons'>
<td>
<input type='text'
name="personName"
ng-model="person.name"
/>
</td>
<td>
<input type='number'
name="personName"
ng-model="person.age"
enter-as-tab
/>
</td>
</tr>
</table>
JS:
var app = angular.module("ap", []);
app.controller("con", function ($scope) {
$scope.persons = [
{ name: 'Susan', age: 1 },
{ name: 'Peter', age: 1 },
{ name: 'Jack', age: 2 }
];
});
app.directive('enterAsTab', function () {
return function (scope, element, attrs) {
element.bind("keydown keypress", function (event) {
if(event.which === 13) {
event.preventDefault();
// Go to next age input
}
});
};
});
Here is a link to the fiddle: fiddle
Ok, so I figured it out. Wasn't that difficult after all. Just got caught up in the whole "don't think jQuery while using Angular" mindset.
Here is the directive that I implemented:
app.directive('enterAsTab', function () {
return function (scope, element, attrs) {
element.bind("keydown keypress", function (event) {
if(event.which === 13) {
event.preventDefault();
var elementToFocus = element.next('tr').find('input')[1];
if(angular.isDefined(elementToFocus))
elementToFocus.focus();
}
});
};
});
Here is the link to the working fiddle: enter-as-tab
Starting from #avn's solution I made some changes to find recursively and focus to the next input text or input number, but only if the value is valid, or send the form. Was designed for ionic forms but could be adapted for any angular forms:
app.directive('enterAsTab', function () {
return {
restrict: 'A',
require: '^ngModel',
link: function (scope, element, attrs, ctrl) {
element.bind("keydown keypress", function (event) {
function isKeyEnterAndValid(){
return event.which === 13 && ctrl.$valid;
}
function nextItem(div, tag){
var next = div.next(tag);
if (!next) return nextItem(div, 'label');
return next;
}
function isTypeTextOrNumber(input){
return ['text', 'number'].indexOf(input.attr('type')) === -1;
}
function findInput(div){
var next = nextItem(div, 'div');
if (!next) return;
var input = next.find('input');
if (!input || isTypeTextOrNumber(input)){
return findInput(next);
}
return input[0];
}
if(isKeyEnterAndValid()) {
var nextInput = findInput(element.parent());
if(angular.isDefined(nextInput)){
event.preventDefault();
nextInput.focus();
}
}
});
}
};
});

AngularJS directive to scroll to a given item

I have a scope variable $scope.first_unread_id which is defined in my controller. In my template, I have:
<div id="items" >
<ul class="standard-list">
<li ng-repeat="item in items" scroll-to-id="first_unread_id">
<span class="content">{{ item.content }}</span>
</li>
</ul>
</div>
and my directive looks like:
angular.module('ScrollToId', []).
directive('scrollToId', function () {
return function (scope, element, attributes) {
var id = scope.$parent[attributes["scrollToId"]];
if (id === scope.item.id) {
setTimeout(function () {
window.scrollTo(0, element[0].offsetTop - 100)
}, 20);
}
}
});
it works, however, two questions:
Is there a better way of getting the "first_unread_id" off the controller scope into the direct than interrogating scope.$parent? This seems a bit 'icky'. I was hoping I could pass that through the view to the direct as a parameter w/o having to repeat that on ever li element.
Is there a better way to avoid the need of the setTimeout() call? Without it, it works sometimes - I imagine due to difference in timing of layout. I understand the syntax I have used is defining a link function - but it isn't clear to me if that is a pre or post-link by default - and if that even matters for my issue.
You shouldn't need the scope.$parent - since it will inherit the value from the parent scope, and when it changes in the parent scope it will be passed down.
The default is a post-link function. Do you have some images or something loading that would make the page layout change shortly after initial load? Have you tried a setTimeout with no time on it, eg setTimeout(function(){})? This would make sure this would go 'one after' everything else is done.
I would also change the logic of your directive a bit to make it more general. I would make it scroll to the element if a given condition is true.
Here are those 3 changes:
html:
<div id="items" >
<ul class="standard-list">
<li ng-repeat="item in items" scroll-if="item.id == first_unread_id">
<span class="content">{{ item.content }}</span>
</li>
</ul>
</div>
JS:
app.directive('scrollIf', function () {
return function (scope, element, attributes) {
setTimeout(function () {
if (scope.$eval(attributes.scrollIf)) {
window.scrollTo(0, element[0].offsetTop - 100)
}
});
}
});
Assuming that the parent element is the one where we scroll, this works for me:
app.directive('scrollIf', function () {
return function(scope, element, attrs) {
scope.$watch(attrs.scrollIf, function(value) {
if (value) {
// Scroll to ad.
var pos = $(element).position().top + $(element).parent().scrollTop();
$(element).parent().animate({
scrollTop : pos
}, 1000);
}
});
}
});
I ended up with the following code (which does not depend on jQ) which also works if the scrolling element is not the window.
app.directive('scrollIf', function () {
var getScrollingParent = function(element) {
element = element.parentElement;
while (element) {
if (element.scrollHeight !== element.clientHeight) {
return element;
}
element = element.parentElement;
}
return null;
};
return function (scope, element, attrs) {
scope.$watch(attrs.scrollIf, function(value) {
if (value) {
var sp = getScrollingParent(element[0]);
var topMargin = parseInt(attrs.scrollMarginTop) || 0;
var bottomMargin = parseInt(attrs.scrollMarginBottom) || 0;
var elemOffset = element[0].offsetTop;
var elemHeight = element[0].clientHeight;
if (elemOffset - topMargin < sp.scrollTop) {
sp.scrollTop = elemOffset - topMargin;
} else if (elemOffset + elemHeight + bottomMargin > sp.scrollTop + sp.clientHeight) {
sp.scrollTop = elemOffset + elemHeight + bottomMargin - sp.clientHeight;
}
}
});
}
});
Same as accepted answer, but uses the javascript built-in method "scrollIntoView":
angular.module('main').directive('scrollIf', function() {
return function(scope, element, attrs) {
scope.$watch(attrs.scrollIf, function(value) {
if (value) {
element[0].scrollIntoView({block: "end", behavior: "smooth"});
}
});
}
});
In combination with UI Router's $uiViewScroll I ended up with the following directive:
app.directive('scrollIf', function ($uiViewScroll) {
return function (scope, element, attrs) {
scope.$watch(attrs.scrollIf, function(value) {
if (value) {
$uiViewScroll(element);
}
});
}
});
In combo with #uri, this works for my dynamic content with ui-router and stateChangeSuccess in .run:
$rootScope.$on('$stateChangeSuccess',function(newRoute, oldRoute){
setTimeout(function () {
var postScroll = $state.params.postTitle;
var element = $('#'+postScroll);
var pos = $(element).position().top - 100 + $(element).parent().scrollTop();
$('body').animate({
scrollTop : pos
}, 1000);
}, 1000);
});
For an answer taking the best of the answers here, in ES6:
File: scroll.directive.js
export default function ScrollDirective() {
return {
restrict: 'A',
scope: {
uiScroll: '='
},
link: link
};
function link($scope, $element) {
setTimeout(() => {
if ($scope.uiScroll) {
$element[0].scrollIntoView({block: "end", behavior: "smooth"});
}
});
}
}
File scroll.module.js
import ScrollDirective from './scroll.directive';
export default angular.module('app.components.scroll', [])
.directive('uiScroll', ScrollDirective);
After importing it in your project, you can use it in the your html:
<div id="items" >
<ul class="standard-list">
<li ng-repeat="item in items" ui-scroll="true">
<span class="content">{{ item.content }}</span>
</li>
</ul>
</div>

Resources