Angular directive run if scope values update - javascript

I have written a directive that sets the child elements of parent element equal to the height of the tallest child element. Here's the directive code:
app.directive('equalHeightChildren', function(){
return function(scope, element, attrs){
var $tallest = element;
$.each(element.children(), function(index, child){
if($(child).outerHeight() > $tallest.outerHeight()){
$tallest = $(child);
}
});
element.children().outerHeight($tallest.outerHeight() + 'px');
}
});
This code runs as the page loads and correctly adjust the heights. However, I have certain scope variables in my app that can change the height of these child elements (for example checking a checkbox displays a new form in one of the child elements, increasing its height). I was hoping that the directive would rerun itself when one of these variables changes, thereby re-adjusting the heights of the child elements. However, this doesn't seem to be happening.
Is there a way to have the directive run when the scope variables change? Or am I thinking about this incorrectly?

If you want it to re-run the each you might need a watcher on the element thats changing.
Another option is to a fire an event and have this directive handle it.
app.directive('equalHeightChildren', function(){
return function(scope, element, attrs){
var setTallest = function(){
var $tallest = element;
$.each(element.children(), function(index, child){
if($(child).outerHeight() > $tallest.outerHeight()){
$tallest = $(child);
}
});
element.children().outerHeight($tallest.outerHeight() + 'px');
}
setTallest();
//Use one of these to cause the height to be reset
//$scope.$watch('myVar', function() {setTallest(); });
// $rootScope.on('eventTrigger', setTallest);
}
});

Related

AngularJS: Add inline custom code using a directive

Here's the scenario.
In the app, you can add inline custom code (HTML attributes ex. style="", onclick="alert('Test')") in an element (ex. input texts, divs). The custom code is binded to the main model and loaded to the element using a custom directive I've created. I'm doing this to control dynamically generated fields that I want to hide and show based on different inputs.
This is my custom directive that loads inline attributes on the element:
app.directive('addCustomHtml', function() {
return {
scope: {
customHtml: "="
},
link: function(scope, element, attributes){
scope.$watch('customHtml', function(newVal, oldVal) {
if (newVal) {
var attrs = newVal.split('\n');
for (var i = 0; i < attrs.length; i++) {
var result = attrs[i].split('=');
var attr = result.splice(0,1);
attr.push(result.join('='));
if (attr[1]) {
element.attr(attr[0], attr[1].replace(/^"(.*)"$/, '$1'));
}
}
} else {
if (oldVal) {
var attrs = oldVal.split('\n');
for (var i = 0; i < attrs.length; i++) {
var attr = attrs[i].split('=');
if (attr[0]) {
element.removeAttr(attr[0]);
}
}
}
}
})
}
}
});
It is binded to the element like this:
<input type="checkbox" add-custom-html custom-html="checkbox1.customHtml">Yes
To see it in action, you can check the plunkr here: https://plnkr.co/edit/xjjMRPY3aE8IVLIeRZMp?p=preview
Now my problem is, when I try to add AngularJS directives (ex. ng-show, ng-if) using my custom directive, AngularJS doesn't seem to recognize them and the model scope I'm passing inside.
Another problem is when I try to add vanilla Javascript event functions (ex. onclick="", onchange=""), it does work but sometimes AngularJS does not read them especially when the element has an ng-change, ng-click attributes.
Again, I am doing this approach on the app because I have generic fields and I want to control some of them by adding this so called "custom codes".
Any help would be highly appreciated!!
If you want to add HTML code and compile it within current $scope, you should use the $compile service:
let someVar = $compile(yourHTML)($scope);
// you can now append someVar to any element and
// angular specific markup will work as expected
Being a service, you'll need to inject it into current controller (or pre/post link function) to be able to use it.

AngularJS doesn't parse dynamically loaded data from directive

I am trying to create N number of Select controls dynamically from directive based on array that is passed in from the attribute (where N is the length of the array).
Structure of an object of the array is as such:
selectDescription = {
array: arrayObject, //ng-options, a string as 'item as item.name for item in selectArray[0]'
change: methodName, //ng-change, actionname
level: levelNumber //level number
}
So the number of select controls inside span tag depends on the number of selectDescription(s) that I get from the attribute.
First select control is rendered successfully. Subsequent select controls should have been rendered on select of an option from previous rendered select controls. But it's not happening in my case. Although I am successfully appending angular elements in the current inputEl(on select of an option), it is not being rendered in the UI. I guess I am missing something very crucial.
On change of selectDescriptions, a flipped attribute is set, through which I am able to call scope.$editable.render() from link, which in turn runs render function to re-append elements after clearing the previous HTML inside span.
My Code:
app.directive('editableLocation', function(editableDirectiveFactory) {
var createElement = function(el, index){
var newElement = angular.element("<select/>");
newElement.attr('ng-model','$data'+index);
newElement.attr('ng-options',el.array);
newElement.attr('ng-change',el.change.substring(0, el.change.length - 1)+", $data"+index+")");
return newElement;
}
var descriptions = [] ;
var dir = editableDirectiveFactory({
directiveName: 'editableLocation',
inputTpl: '<span></span>',
render: function() {
this.parent.render.call(this);
this.inputEl.html("");
for(var i = 0 ; i < descriptions.length ; i ++){
this.inputEl.append(createElement(descriptions[i], i));
}
}
});
var linkOrg = dir.link;
dir.link = function(scope, el, attrs, ctrl) {
console.log(el);
descriptions = scope.$eval(attrs.description);
scope.$watch('flipped',function(newValue,oldValue){
if(newValue != 0){
scope.$editable.render();
}
});
return linkOrg(scope, el, attrs, ctrl);
};
return dir;
});
Since you are adding the dynamic HTML content in the link function of the Angular directive, Angular will not auto compile/parse it. You need to do it manually using $compile directive. So after you appended all the HTML, do the following (inject $compile in your code)
$compile(element.contents())(scope);
Where element is your any parent element where you are generating dynamic HTML and scope is the scope of the directive or any other scope which you want it to be attached to the dynamic HTML.
Looking at xeditable.js I have found that xeditable renders the UI by calling a show method defined in its editableController.
It is defined as:
self.show = function() {
self.setLocalValue();
self.render(); //calls 'render' function of 'editableDirectiveFactory'; that' where my custom UI lies
$element.after(self.editorEl); //attaches newelement(especially whole <form/> element)
$compile(self.editorEl)($scope); //renders whole UI(and also the newly attached one)
self.addListeners();
$element.addClass('editable-hide');
return self.onshow();
};
So what I felt is, I need to call this show method from my link function, which receives the controller.
This is what I did:
dir.link = function (scope, el, attrs, ctrl) {
$element = el;
scope.$watch(attrs.flipped, function (newValue, oldValue) {
//re-render element if flipped is changed; denoting description of select controls have been altered
if (newValue != 0) {
ctrl[0].show(); //this will call render function and also $compile({{content/html/element}})(scope)
}
});
return linkOrg(scope, el, attrs, ctrl);
};
And also you need to hide the previous <form/> element(which contains previous rendered UI), so that only one forms get displayed.
This is how I hid that previous <form/> element in render' function ofeditableDirectiveFactory`:
var prevForm = $element[0].nextElementSibling; //hide previous form element which would already contain previous select
if (prevForm)
prevForm.classList.add('editable-hide');
That solved my problem at least :)

How to properly implement an infinite scrolling AngularJS table without JQuery?

I have an AngularJS project (I don't use JQuery) where I need to display a table with users and load more as the user scrolls near the end of the page. I'm trying to implement this without relying on external libraries, since this is part of my requirements.
I've checked several examples like this, this and this.
But so far, none of the examples I've checked have helped me implement the infinite scrolling as I expected. They use different ways to calculate when to trigger the call, and some of this values return undefined to me (i.e. people say clientHeight and scrollHeight have different values, but to me it's always the same one, the total height including scrolling space).
I created a directive like the following:
usersModule.directive('whenScrollEnds', function($window) {
return {
restrict: "A",
link: function(scope, elm, attr) {
angular.element($window).bind('scroll', function() {
var hiddenContentHeight = elm[0].scrollHeight - angular.element($window)[0].innerHeight;
if ((hiddenContentHeight - angular.element($window)[0].scrollY) <= (10 )) {
angular.element($window)[0].requestAnimationFrame(function(){
scope.$apply(attr.whenScrollEnds);
})
}
});
}
};
});
This kind of works but there's the following problems/doubts which I hope someone can explain to me:
It triggers too fast. I want the loading to trigger when I'm near the bottom of the scrollable space, like near 90% or so.
The scrollHeight is only accesible through elm[0], angular.element($window)[0] has no scrollHeight property so it returns undefined, and elm[0] has no scrollY value.
The scrollY value I get is the distance the scrollbar has moved from the top, minus the scrollbar length, but I feel like that value is wrong.
Is binding the scroll event through angular.element($window).bind the right decision?
How can I implement a proper infinite scrolling table? Am I using the correct variables? Please provide an answer that uses Javascript and AngularJS only, JQuery or libraries solutions won't help me.
After checking several examples and trying to avoid those that used JQuery as part of the solution I found a way to make this work.
First my directive that would handle when the scroll ends:
usersModule.directive('whenScrollEnds', function($window) {
return {
restrict: "A",
link: function(scope, elm, attr) {
var raw = elm[0];
raw.addEventListener('scroll', function() {
if ((raw.scrollTop + raw.clientHeight) >= (raw.scrollHeight )) {
scope.$apply(attr.whenScrollEnds);
}
});
}
};
});
My view has the table inside a div like this:
<div id="usersScrollable" when-scroll-ends="uc.loadMoreUsers()">
<table id="tableUsers" class="table" ng-show="uc.users.length" >
<thead>
<tr>
<th>Email</th>
<th>Nombre Completo</th>
<th>Estado</th>
<th>Acción</th>
</tr>
</thead>
<tbody >
<tr ng-repeat="u in uc.users">
<td>{{u.email}}</td>
<td>{{u.fullName}}</td>
<td>{{uc.getStateString(u.active)}}</td>
<td><button type="button" class="btn btn-primary" ng-click="uc.edit(u)">Editar</button></td>
</tr>
</tbody>
</table>
</div>
Make sure the div that contains the table is the one listening to when the scrolling ends. This div has to have a set height, if it doesn't then the clientHeight property will be the same as scrollHeight (not sure what would happen if there's no height defined explicitly, like instead of setting the height you set the top or bottom properties).
In my controller, loadMoreUsers() is in charge of incrementing the page (in case there's more users, each call I get has the total of users so I can know before making another request how many users I have left), also it calls the function that makes the request to the web service.
The problem with the solution provided by Uriel Arvizu is that if the raw element is empty because it is waiting for a Http Request at the page load, all the following raw.scrollTop, raw.clientHeight) and raw.scrollHeight will have wrong dimensions and the scroll is no working anymore.
I would suggest this other solution that basically adds the scroll event to the $window without cacheing it, so it is always sized correctly when the Http response is received.
(function () {
'use strict';
angular.module('ra.infinite-scroll', []).directive('infiniteScroll', function ($window) {
return {
restrict: 'A',
scope: {
infiniteScrollCallbackFn: '&'
},
link: function (scope, elem, attrs) {
var percentage = (attrs.infiniteScrollPercentage !== undefined ? (attrs.infiniteScrollPercentage / 100) : '.9');
var $element = elem[0];
angular.element($window).on('scroll', function () {
if ((this.scrollY + this.innerHeight - $element.offsetTop) >= ($element.scrollHeight * percentage)) {
scope.$apply(scope.infiniteScrollCallbackFn);
}
});
}
};
});
})();
Also, with this module, you can pass 2 parameters in the HTML: a callback function and a percentage of the specific element (where the module is applied) that when it is reached the callback function is called, i.e. to repeat the Http request (default is 90% of that element).
<div infinite-scroll infinite-scroll-callback-fn="callBackFunction()" infinite-scroll-percentage="80">
// Here you may include the infinite repeating template
</div>
Using example code of Ferie, yet I had to use mousewheel event instead of scroll and had to use elem.scrollTop() instead of elem.scrollY
app.directive('infiniteScroll', function ($window) {
return {
restrict: 'A',
scope: {
infiniteScrollCallbackFn: '&'
},
link: function (scope, elem, attrs) {
var percentage = (attrs.infiniteScrollPercentage !== undefined ? (attrs.infiniteScrollPercentage / 100) : '.9');
var $element = elem[0];
angular.element($window).on('mousewheel', function () {
if ((elem.scrollTop() + this.innerHeight - $element.offsetTop) >= ($element.scrollHeight * percentage)) {
scope.$apply(scope.infiniteScrollCallbackFn);
}
});
}
};
});
when used in Angular table with a ng-repeat on the <tr>, I had to add the directive into the parent tbody of this tr in order to capture the right element containing the scroll state.
<tbody infinite-scroll infinite-scroll-callback-fn="callBackFunction()" infinite-scroll-percentage="80">

angular ng-click inside ng-repeat to update $scope and then use $apply to update dom

I'm using angular 1.2
ng-repeat creates divs that also contain ng-click
ng-click updates $scope when clicked
the change in $scope is reflected in ng-repeat using $apply
It works ... but I get an error when I click and I think I am applying $apply incorrectly
here is my jsfiddle link
function appcontrol ($scope, $log) {
// declare $scope vars
$scope.currentlist = [];
for (var i = 0; i < 10; i++) {
$scope.currentlist[i] = {'key':i, 'value':i};
}
$scope.extra = 'My Extra';
$scope.anotherextra = 'Another Extra';
// click handler
$scope.handleCellClick = function(cellnumber){
$log.log(cellnumber + ' clicked');
// update the $scope property
$scope.currentlist[cellnumber].value = 'AAA';
// push out the new change to the dom
$scope.$apply();
// trigger other stuff
}
// click handler
$scope.handleExtraClick = function(arg){
$log.log('extra clicked ', arg);
// update the $scope property
if (arg=='My Extra') $scope.extra = 'AAA';
if (arg=='Another Extra') $scope.anotherextra = 'AAA';
// push out the new change to the dom
$scope.$apply();
// trigger other stuff
}
}
and html
<div ng-controller="appcontrol">
<div ng-repeat="item in currentlist" id="cell{{item.value}}" ng-model="item.value" class="cell" ng-click="handleCellClick(item.key)">{{item.value}}</div>
<div id="cell{{extra}}" ng-click="handleExtraClick(extra)">{{extra}}</div>
<div id="cell{{anotherextra}}" ng-click="handleExtraClick(anotherextra)">{{anotherextra}}</div>
</div>
Angular already call $apply for you when you use ng-click. If you take out all the $scope.$apply() in your code, the error won't show up, and it would work exactly the same. Updated fiddle
You don't need $scope.$apply() way you are using it. AngularJS loads the data in $scope automatically. $apply() is used to execute an expression in angular from outside of the angular framework. Also, I would use this function very sparely as it slows your performance.
For example, you would use $apply() for data changed outside of angular like this:
//explicitly auto adjust width and height when user changes size
$scope.$apply( function(){
$scope.width = $window.innerWidth;
$scope.height = $window.innerHeight;
});
Checkout more here--> https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$apply

Angularjs watch for change in parent scope

I'm writing a directive and I need to watch the parent scope for a change. Not sure if I'm doing this the preferred way, but its not working with the following code:
scope.$watch(scope.$parent.data.overlaytype,function() {
console.log("Change Detected...");
})
This it logged on window load, but never again, even when overlaytype is changed.
How can I watch overlaytype for a change?
Edit: here is the entire Directive. Not entirely sure why I'm getting a child scope
/* Center overlays vertically directive */
aw.directive('center',function($window){
return {
restrict : "A",
link : function(scope,elem,attrs){
var resize = function() {
var winHeight = $window.innerHeight - 90,
overlayHeight = elem[0].offsetHeight,
diff = (winHeight - overlayHeight) / 2;
elem.css('top',diff+"px");
};
var watchForChange = function() {
return scope.$parent.data.overlaytype;
}
scope.$watch(watchForChange,function() {
$window.setTimeout(function() {
resize();
}, 1);
})
angular.element($window).bind('resize',function(e){
console.log(scope.$parent.data.overlaytype)
resize();
});
}
};
});
If you want to watch a property of a parent scope you can use $watch method from the parent scope.
//intead of $scope.$watch(...)
$scope.$parent.$watch('property', function(value){/* ... */});
EDIT 2016:
The above should work just fine, but it's not really a clean design. Try to use a directive or a component instead and declare its dependencies as bindings. This should lead to better performance and cleaner design.
I would suggest you to use the $broadcast between controller to perform this, which seems to be more the angular way of communication between parent/child controllers
The concept is simple, you watch the value in the parent controller, then, when a modification occurs, you can broadcast it and catch it in the child controller
Here's a fiddle demonstrating it : http://jsfiddle.net/DotDotDot/f733J/
The part in the parent controller looks like that :
$scope.$watch('overlaytype', function(newVal, oldVal){
if(newVal!=oldVal)
$scope.$broadcast('overlaychange',{"val":newVal})
});
and in the child controller :
$scope.$on('overlaychange', function(event, args){
console.log("change detected")
//any other action can be perfomed here
});
Good point with this solution, if you want to watch the modification in another child controller, you can just catch the same event
Have fun
Edit : I didn't see you last edit, but my solution works also for the directive, I updated the previous fiddle ( http://jsfiddle.net/DotDotDot/f733J/1/ )
I modified your directive to force it to create a child scope and create a controller :
directive('center',function($window){
return {
restrict : "A",
scope:true,
controller:function($scope){
$scope.overlayChanged={"isChanged":"No","value":""};
$scope.$on('overlaychange', function(event, args){
console.log("change detected")
//whatever you need to do
});
},
link : function(scope,elem,attrs){
var resize = function() {
var winHeight = $window.innerHeight - 90,
overlayHeight = elem[0].offsetHeight,
diff = (winHeight - overlayHeight) / 2;
elem.css('top',diff+"px");
};
angular.element($window).bind('resize',function(e){
console.log(scope.$parent.data.overlaytype)
resize();
});
}
};
});
You should have the data property on your child scope, scopes use prototypal inheritance between parent and child scopes.
Also, the first argument the $watch method expects is an expression or a function to evaluate and not a value from a variable., So you should send that instead.
If you're looking for watching a parent scope variable inside a child scope, you can add true as second argument on your $watch. This will trigger your watch every time your object is modified
$scope.$watch("searchContext", function (ctx) {
...
}, true);
Alright that took me a while here's my two cents, I do like the event option too though:
Updated fiddle
http://jsfiddle.net/enU5S/1/
The HTML
<div ng-app="myApp" ng-controller="MyCtrl">
<input type="text" model="model.someProperty"/>
<div awesome-sauce some-data="model.someProperty"></div>
</div>
The JS
angular.module("myApp", []).directive('awesomeSauce',function($window){
return {
restrict : "A",
template: "<div>Ch-ch-ch-changes: {{count}} {{someData}}</div>",
scope: {someData:"="},
link : function(scope,elem,attrs){
scope.count=0;
scope.$watch("someData",function() {
scope.count++;
})
}
};
}).controller("MyCtrl", function($scope){
$scope.model = {someProperty: "something here");
});
What I'm showing here is you can have a variable that has two way binding from the child and the parent but doesn't require that the child reach up to it's parent to get a property. The tendency to reach up for things can get crazy if you add a new parent above the directive.
If you type in the box it will update the model on the controller, this in turn is bound to the property on the directive so it will update in the directive. Within the directives link function it has a watch setup so anytime the scope variable changes it increments a counter.
See more on isolate scope and the differences between using = # or & here: http://www.egghead.io/

Categories

Resources