angularjs : directive and ng-class initialization inside ng-repeat - angularjs

I have an ng-class initialization issue when I move the code which was in my controller, but related to the DOM, to a directive.
To be short, here is some code:
view.html
<div ng-repeat="foo in foos" ng-click="rowClick($index)>
<div ng-class="changeOpacity($index)>
<span>{{foo.title}}</span>
<span>{{foo.name}}</span>
<span>{{foo.date}}</span>
...
// some div with hidden content. Show when clicking on the div
</div>
<div>
controller.js
$scope.rowClick = function (idx) {
// unfold the clicked div to show content
};
$scope.changeOpacity = function (idx) {
if (this is the div clicked) {
return {dark: true};
} else {
return {light: true};
}
So when I have a list of div. When I clicked on a div, all the other divs get darker excepts this one.
Everything is working fine when $scope.rowClick and $scope.changeOpacity are in my controller.
When I move rowClick and changeOpcaity to a directive:
myBar.js
myApp.directive('myBar', function () {
...
return {
restrict:'A',
link: function (scope, element) {
element.bind('click', function () {
...
same code from rowClick
...
scope.changeOpacity = function () {
...
same code from changeOpacity
}
scope.$apply();
}
}),
changeOpacity: function () {
...
return {light: true} // for all divs
}
}
});
Now my view.html is something like that:
<div ng-repeat="foo in foos" ng-click="rowClick($index) my-bar>
<div ng-class="changeOpacity($index)>
<span>{{foo.title}}</span>
<span>{{foo.name}}</span>
<span>{{foo.date}}</span>
...
// some div with hidden content. Show when clicking on the div
</div>
<div>
But now the divs are not initialized anymore with the ng-class. I have to click one time on each div to initialize it, with the link function which listen to the click.
How can I initialize the ng-class inside the directive ?
Thanx.

The problem is that you are putting the changeOpacity function in your scope only after a click. Do it in the link function instead like this:
link: function (scope, element) {
scope.changeOpacity = function () {
...
same code from changeOpacity
}
}
making it a field of the "directive object" has no effect.
However, in my opinion it would be more elegant to use a model property to indicate if the element is active or not and change this property in an ng-click attribute. You can then refer to this property in the ng-class directive.
Something like this:
<div ng-repeat="foo in foos" ng-click="foo.active = true>
<div ng-class="{light: active, dark: !active }>
<span>{{foo.title}}</span>
<span>{{foo.name}}</span>
<span>{{foo.date}}</span>
<div ng-show="foo.active">
// some div with hidden content. Show when clicking on the div
</div>
</div>

ng-repeat creates a child scope for each item within the array. That means scope within a directive within ng-repeat will be the child scope. Thus you can set properties directly on that child scope and use ng-class, ng-show etc
Following is very basic example of directive to toggle state based on ng-click handler
HTML
<div ng-repeat="foo in foos" ng-click="rowClick($index)" my-directive parent-array="foos">
<div ng-class="{activeClass: foo.isActive}" class="item">
<span>{{foo.name}}</span>
<div ng-show="foo.isActive">Hidden content {{$index+1}}</div>
</div>
</div>
Directive
app.directive('myDirective',function(){
return function(scope,elem,attrs){
/* ng-repeat adds properties like `$first` , `$last` that can be helpful*/
/* set default first element active*/
if(scope.$first){
scope.foo.isActive=true;
}
var parentArray=scope[attrs.parentArray];
/* update active state across array*/
scope.rowClick=function(idx){
angular.forEach(parentArray,function(item, index){
item.isActive = idx==index
})
}
}
})
DEMO
Here's another approach that stores currSelected item in the parent scope ( controller)
app.directive('myDirective',function(){
return function(scope,elem,attrs){
/* set default first element active*/
if(scope.$first){
scope.foo.isActive=true;
scope.$parent.currSelected=scope.foo
}
/* reset previous activeitem and make item clicked the active one*/
scope.rowClick=function(item){
scope.$parent.currSelected.isActive=false
scope.$parent.currSelected=item;
item.isActive=true
}
}
})
DEMO

Related

Change vm variable value after clicking anywhere apart from a specific element

When I click anywhere in the page apart from ul element (where countries are listed) and the suggestion-text input element (where I type country name), vm.suggested in controller should be set to null. As a result ul element will be closed automatically. How can I do this?
I've seen Click everywhere but here event and AngularJS dropdown directive hide when clicking outside where custom directive is discussed but I couldn't work out how to adapt it to my example.
Markup
<div>
<div id="suggestion-cover">
<input id="suggestion-text" type="text" ng-model="vm.countryName" ng-change="vm.countryNameChanged()">
<ul id="suggest" ng-if="vm.suggested">
<li ng-repeat="country in vm.suggested" ng-click="vm.select(country)">{{ country }}</li>
</ul>
</div>
<table class="table table-hover">
<tr>
<th>Teams</th>
</tr>
<tr ng-if="vm.teams">
<td><div ng-repeat="team in vm.teams">{{ team }}</div></td>
</tr>
</table>
<!-- There are many more elements here onwards -->
</div>
Controller
'use strict';
angular
.module('myApp')
.controller('readController', readController);
function readController() {
var vm = this;
vm.countryNameChanged = countryNameChanged;
vm.select = select;
vm.teams = {.....};
vm.countryName = null;
vm.suggested = null;
function countryNameChanged() {
// I have a logic here
}
function select(country) {
// I have a logic here
}
}
I solved the issue by calling controller function from within the directive so when user clicks outside (anywhere in the page) of the element, controller function gets triggered by directive.
View
<ul ng-if="vm.suggested" close-suggestion="vm.closeSuggestion()">
Controller
function closeSuggestion() {
vm.suggested = null;
}
Directive
angular.module('myApp').directive('closeSuggestion', [
'$document',
function (
$document
) {
return {
restrict: 'A',
scope: {
closeSuggestion: '&'
},
link: function (scope, element, attributes) {
$document.on('click', function (e) {
if (element !== e.target && !element[0].contains(e.target)) {
scope.$apply(function () {
scope.closeSuggestion();
});
}
});
}
}
}
]);
This is just an example but you can simply put ng-click on body that will reset your list to undefined.
Here's example:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
You will need on li elements:
$event.stopPropagation();
so your html:
<li ng-repeat="country in vm.suggested" ng-click="vm.select(country); $event.stopPropagation()">{{ country }}</li>
and your body tag:
<body ng-app="myWebApp" ng-controller="Controller01 as vm" ng-click="vm.suggested=undefined;">
UPDATE:
As I said it's only an example, you could potentially put it on body and then capture click there, and broadcast 'closeEvent' event throughout the app. You could then listen on your controller for that event - and close all. That would be one way to work around your problem, and I find it pretty decent solution.
Updated plunker showing communication between 2 controllers here:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
LAST UPDATE:
Ok, last try - create a directive or just a div doesn't really matter, and put it as an overlay when <li> elements are open, and on click close it down. Currently it's invisible - you can put some background color to visualize it.
Updated plunker:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
And finally totally different approach
After some giving it some thought I actually saw that we're looking at problem from the totally wrong perspective so final and in my opinion best solution for this problem would be to use ng-blur and put small timeout on function just enough so click is taken in case someone chose country:
on controller:
this.close = function () {
$timeout(()=>{
this.suggested = undefined;
}, 200);
}
on html:
<input id="suggestion-text" type="text" ng-model="vm.countryName" ng-change="vm.countryNameChanged()" ng-blur="vm.close()">
This way you won't have to do it jQuery way (your solution) which I was actually trying to avoid in all of my previous solutions.
Here is plnker: http://plnkr.co/edit/w5ETNCYsTHySyMW46WvO?p=preview

Use ngAnimate on template in directive

I have this function,
app.directive('movieDetails', MovieDetailsDirectiveFn)
.controller('MovieRowCtrl', ['$scope', '$rootScope', MovieRowCtrlFn]);
function MovieDetailsDirectiveFn() {
return {
restrict: 'E',
scope: {
movie: '='
},
templateUrl: '/tpl.html',
// template: '<div class="movie" style="background-image:url(https://image.tmdb.org/t/p/w1280{{movie.backdrop}})">{{movie.title}}</div>'
}
}
function MovieRowCtrlFn($scope, $rootScope) {
$scope.selectedMovie;
$scope.rowActive = false;
$scope.$on('movieRowActivated', ($event, dataObject) => {
if ($scope.$id != dataObject.targetScopeId) {
$scope.rowActive = false;
}
});
$scope.updateSelectedMovie = function updateVisibleMovieIndexFn(movie) {
$scope.selectedMovie = movie;
$rootScope.$broadcast('movieRowActivated', {targetScopeId: $scope.$id});
$scope.rowActive = true;
console.log ('hello')
}
}
And this html,
<div class="content" ng-repeat="movie in movieGroup" ng-init='$movieIndex = $index'>
<a href='' ng-click='updateSelectedMovie(movie)'>{{ movie.title }}</a>
</div>
<div ng-if='rowActive'>
<movie-details movie='selectedMovie'></movie-details>
</div>
When a user clicks on a button the updateSelectedMovie function triggers and the directive element movie-details is updated with new information.
Check the plunker here http://plnkr.co/edit/Ea1OtRUc1wvC4UNw1sNi?p=preview
What I want to do is animate a fadeOut/fadeIn transition when the movie-details directive element changes content. I've used ngAnimate on ui-router elements, since they get the ng-enter ng-animate classes etc. but that doesn't work with a directive.
For anyone having the same problemn. I "fixed" this by putting a ui-view inside the template that's being loaded by the directive. And fixed a state with it. So now the directive creates a template and goes to another state. The state then places the template inside the freshly created ui-view (from the directive) and now I can animate that element.

Directive link function not called

I have a portion of view that refreshes itself, say the div hides when an API call is in progress and shows up when the response is obtained.
This portion of view (div) has a angular directive.
View
<div ng-controller="myCtrl>
<input type="button" ng-click="callAPI()">
<div ng-show="isAPICallComplete">
<p data-my-directive="something" ng-repeat="name in names">{{name}}</p>
</div>
</div>
Directive
angular.module('myModule')
.controller('myCtrl', function ($scope, $http) {
$scope.callAPI = function () {
$http.get('someURL').then(function (response) {
$scope.isAPICallComplete = true;
$scope.names= response.names;
});
}
})
.directive('myDirective', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
console.log('reached directive');
}
}
});
With the above code, on page load the API call is already complete and hence the div shows up which then invokes the angular directive and I could see the log in console. But when on other conditions the API is called, the div hides itself and shows up again. In this case, the angular directive is not invoked (I don't see the console log message).
You can Do:
Just change the ng-show to ng-if it will work,
As the DOM will be created again on using ng-if
Just thought it is worth mentioning
ng-if removes or adds the element to the DOM whereas ng-show only hides or shows the element using css properties.

how to get event when user scroll to top in angular js?

could you please tell me how to get event when user scroll to top .Actually I am using ng-repeat in my example .I want to get event when user scroll to bottom and scroll to top .I have one div in which I used ng-repeat can we get event of top when user move to top after scrolling.Actually I need to show alert when user scroll to bottom and top of div in angular .here is my code
<body ng-controller="MyController">
<div style="width:90%;height:150px;border:1px solid red;overflow:auto">
<div ng-repeat="n in name">{{n.name}}</div>
</div>
You could put directives on your scrollable div that listen to the scroll event and check for the top or bottom being reached.
So, using your HTML, your div would look like this:
<div exec-on-scroll-to-top="ctrl.handleScrollToTop"
exec-on-scroll-to-bottom="ctrl.handleScrollToBottom"
style="width:90%;height:150px;border:1px solid red;overflow:auto">
<div ng-repeat="n in name">{{n.name}}</div>
</div>
With new directives exec-on-scroll-to-top and exec-on-scroll-to-bottom added. Each specifies a function in your controller that should execute when the particular event the directive is checking for occurs.
exec-on-scroll-to-top would look like this, just checking for the scrollable div's scrollTop property to be 0:
myapp.directive('execOnScrollToTop', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var fn = scope.$eval(attrs.execOnScrollToTop);
element.on('scroll', function (e) {
if (!e.target.scrollTop) {
console.log("scrolled to top...");
scope.$apply(fn);
}
});
}
};
});
And exec-on-scroll-to-bottom would look like this (keeping in mind that an element is fully scrolled when its (scrollHeight - scrollTop) === clientHeight):
myapp.directive('execOnScrollToBottom', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var fn = scope.$eval(attrs.execOnScrollToBottom),
clientHeight = element[0].clientHeight;
element.on('scroll', function (e) {
var el = e.target;
if ((el.scrollHeight - el.scrollTop) === clientHeight) { // fully scrolled
console.log("scrolled to bottom...");
scope.$apply(fn);
}
});
}
};
});
Here's a plunk. Open the console to see messages getting logged when the scrolling reaches the top or bottom.
This is a non angular way, but you can wrap it up in a directive which also allows reuse:
Use Javascript event listener:
div.addEventListener('scroll', function(){
if(this.scrollTop===0)
//do your stuff
});
Make sure to use $apply if you make any changes to the scope variables inside this listener.

Ng-controller on same element as ng-repeat - no two-way-data-binding

I can't get two-way-data-binding to work in an Angular js ng-repeat.
I have an ng-controller specified on the same element that has the ng-repeat specified -
I just learnt that by doing this, I can get a hold of each item that is being iterated over by ng-repeat. Here is the HTML:
<div ng-controller="OtherController">
<div id="hoverMe" ng-controller="ItemController" ng-mouseenter="addCaption()"
ng-mouseleave="saveCaption()" ng-repeat="image in images">
<div class="imgMarker" style="{{image.css}}">
<div ng-show="image.captionExists" class="carousel-caption">
<p class="lead" contenteditable="true">{{image.caption}}</p>
</div>
</div>
</div>
</div>
And here is the ItemController:
function ItemController($scope){
$scope.addCaption = function(){
if($scope.image.captionExists === false){
$scope.image.captionExists = true;
}
}
$scope.saveCaption = function(){
console.log($scope.image.caption);
}
}
And the OtherController:
function OtherController($scope){
$scope.images = ..
}
When I hover the mouse over the #hoverMe-div - the caption-div is added correctly. But when I input some text in the paragraph and then move the mouse away from the #hoveMe-div, the $scope.image-variables caption value is not updated in the saveCaption-method. I understand I'm missing something. But what is it?
You don't need a ng-controller specified on the same element that has the ng-repeat to be able to get each item.
You can get the item like this:
<div ng-repeat="image in images" ng-mouseenter="addCaption(image)" ng-mouseleave="saveCaption(image)" class="image">
And in your controller code:
$scope.addCaption = function (image) {
if(!image.captionExists){
image.captionExists = true;
}
};
To get contenteditable to work you need to use ng-model and a directive that updates the model correctly.
Here is a simple example based on the documentation:
app.directive('contenteditable', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, controller) {
element.on('blur', function() {
scope.$apply(function() {
controller.$setViewValue(element.html());
});
});
controller.$render = function(value) {
element.html(value);
};
}
};
});
Note that the directive probably needs more logic to be able to handle for example line breaks.
Here is a working Plunker: http://plnkr.co/edit/0L3NKS?p=preview
I assume you are editing the content in p contenteditable and are expecting that the model image.caption is update. To make it work you need to setup 2 way binding.
2 way binding is available for element that support ng-model or else data needs to be synced manually. Check the ngModelController documentation and the sample available there. It should serve your purpose.

Resources