AngularJS scroll directive - How to prevent re-rendering whole scope - angularjs

I have an AngularJS Application with a scroll directive implemented as the following:
http://jsfiddle.net/un6r4wts/
app = angular.module('myApp', []);
app.run(function ($rootScope) {
$rootScope.var1 = 'Var1';
$rootScope.var2 = function () { return Math.random(); };
});
app.directive("scroll", function ($window) {
return function(scope, element, attrs) {
angular.element($window).bind("scroll", function() {
if (this.pageYOffset >= 100) {
scope.scrolled = true;
} else {
scope.scrolled = false;
}
scope.$apply();
});
};
});
The HTML looks the following:
<div ng-app="myApp" scroll ng-class="{scrolled:scrolled}">
<header></header>
<section>
<div class="vars">
{{var1}}<br/><br/>
{{var2()}}
</div>
</section>
</div>
I only want the class scrolled to be added to the div once the page is scrolled more than 100px. Which is working just fine, but I only want that to happen! I don't want the whole scope to be re-rendered. So the function var2() should not be executed while scrolling. Unfortunately it is though.
Is there any way to have angular only execute the function which is bound to the window element without re-rendering the whole scope, or am I misunderstanding here something fundamentally to AngularJS?
See this fiddle:
http://jsfiddle.net/un6r4wts/
Edit:
This seems to be a topic about a similar problem:
Angularjs scope.$apply in directive's on scroll listener

If you want to calculate an expression only once, you can prefix it with '::', which does exactly that. See it in docs under One-time binding:
https://docs.angularjs.org/guide/expression
Note, this requires angular 1.3+.
The reason that the expressions are calculated is because when you change a property value on your scope, then dirty check starts and evaluates all the watches for dirty check. When the view uses {{ }} on some scope variable, it creates a binding (which comes along with a watch).

Related

AngularJS directive doesn't update scope value even with apply

I'm usin a directive to show a div on the screen only when the screen size is smaller than 600px. The problem is, the scope value isn't being updated, even using $apply() inside the directive.
This is the code:
function showBlock($window,$timeout) {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
scope.isBlock = false;
checkScreen();
function checkScreen() {
var wid = $window.innerWidth;
if (wid <= 600) {
if(!scope.isBlock) {
$timeout(function() {
scope.isBlock = true;
scope.$apply();
}, 100);
};
} else if (wid > 600) {
if(scope.isBlock) {
$timeout(function() {
scope.isBlock = false;
scope.$apply();
}, 100);
};
};
};
angular.element($window).bind('resize', function(){
checkScreen();
});
}
};
}
html:
<div ng-if="isBlock" show-block>
//..conent to show
</div>
<div ng-if="!isBlock" show-block>
//..other conent to show
</div>
Note: If I don't use $timeout I'll get the error
$digest already in progress
I used console logs inside to check if it's updating the value, and inside the directive everything works fine. But the changes doesn't go to the view. The block doesn't show.
You should use do rule in such cases to get the advantage of Prototypal Inheritance of AngularJS.
Basically you need to create a object, that will will have various property. Like in your case you could have $scope.model = {} and then place isBlock property inside it. So that when you are inside your directive, you will get access to parent scope. The reason behind it is, you are having scope: true, which says that the which has been created in directive is prototypically inherited from parent scope. That means all the reference type objects are available in your child scope.
Markup
<div ng-if="model.isBlock" show-block>
//..conent to show
</div>
<div ng-if="!model.isBlock" show-block>
//..other conent to show
</div>
Controller
app.controller('myCtrl', function($scope){
//your controller code here
//here you can have object defined here so that it can have properties in it
//and child scope will get access to it.
$scope.model = {}; //this is must to use dot rule,
//instead of toggle property here you could do it from directive too
$scope.isBlock = false; //just for demonstration purpose
});
and then inside your directive you should use scope.model.isBlock instead of scope.isBlock
Update
As you are using controllerAs pattern inside your code, you need to use scope.ag.model.isBlock. which will provide you an access to get that scope variable value inside your directive.
Basically you can get the parent controller value(used controllerAs pattern) make available controller value inside the child one. You can find object with your controller alias inside the $scope. Like here you have created ag as controller alias, so you need to do scope.ag.model to get the model value inside directive link function.
NOTE
You don't need to use $apply with $timeout, which may throw an error $apply in progress, so $timeout will run digest for you, you don't need to worry about to run digest.
Demo Here
I suspect it has something to do with the fact that the show-block directive wouldn't be fired if ng-if="isBlock" is never true, so it would never register the resize event.
In my experience linear code never works well with dynamic DOM properties such as window sizing. With code that is looking for screens size you need to put that in some sort of event / DOM observer e.g. in angular I'd use a $watch to observe the the dimensions. So to fix this you need to place you code in a $watch e.g below. I have not tested this code, just directional. You can watch $window.innerWidth or you can watch $element e.g. body depending on your objective. I say this as screens will be all over the place but if you control a DOM element, such as, body you have better control. also I've not use $timeout for brevity sake.
// watch window width
showBlock.$inject = ['$window'];
function bodyOverflow($window) {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($window.innerWidth, function (newWidth, oldWidth) {
if (newWidth !== oldWidth) {
return isBlock = newWidth <= 600;
}
})
}
};
}
// OR watch element width
showBlock.$inject = [];
function bodyOverflow() {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($element, function (new, old) {
if (newWidth) {
return isBlock = newWidth[0].offsetWidth <= 600;
}
})
}
};
}

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.

Angular ng-init pass element to scope

Is there a way to get the current element where my ng-init is currently binded on?
For example:
<div ng-init="doSomething($element)"></div>
I believe that there is a way for ng-click but I can't do this using ng-init like this:
<div ng-click="doSomething($event)"></div>
Controller:
$scope.doSomething = function(e){
var element = angular.element(e.srcElement);
}
How do I do this with ng-init?
Your HTML:
<div ng-app='app' ng-controller="Ctrl">
<div my-dir></div>
</div>
Your Javascript:
var app = angular.module('app', [], function () {});
app.controller('Ctrl', function ($scope) {
$scope.doSomething = function (e) {
alert(e);
};
});
app.directive('myDir', function () {
return function (scope, element, attrs) {
scope.doSomething(element);
};
});
From above, element will be your DOM object.
Don't do it.
As #CodeHater said, handling DOM manipulation in a directive is a better solution than engaging controller with the element.
I was also looking for similar thing but finally I created one more directive added to the child div element. In the directive code block I get the element object and placed all my event related function and other instructions over there. Also, I add the element to $scope object, this help me to use this object else where as well and no need to find it every time I need it.

Bind and parse HTML content

I am using AngularJS v1.2.1.
The improved ng-bind-html directive allows me to trust unsafe Html into my view.
Example
HTML:
<div ng-repeat="example in examples" ng-bind-html="example.content()"></div>
JS:
function controller($scope, $sce)
{
function ex()
{
this.click = function ()
{
alert("clicked");
}
this.content() = function ()
{
//if
return $sce.trustAsHtml('<button ng-click="click()">some text</button>');
// no problem, but click is not called
//when
return $sce.parseAsHtml('<button ng-click="click()">some text</button>');
//throw an error
}
}
$scope.examples = [new ex(), new ex()];
}
My question is, how to bind HTML content that may contain Angular expressions or directives ??
If you need dynamic templates per element, as your question suggests, one solution would be to use $compile within a directive to parse the HTML within the context of the local scope. A simple version of this is shown in this Plunk.
An example directive:
app.directive('customContent', function($compile) {
return function(scope, el, attrs) {
el.replaceWith($compile(scope.example.content)(scope));
}
});
The corresponding HTML:
<div ng-repeat="example in examples">
<div custom-content></div>
</div>
Notice that, in the Plunk controller, I've pulled out the click function into the scope for simplicity, since in the template HTML you are calling click() in the context of the scope, not on the example object. There are a couple ways you could use a different click function for each example, if that's what you'd like to do. This egghead.io screencast has a good example of passing an expression into a directive explicitly; in your case, it could be a click function or the whole example object, depending on what you need.

Sharing scope between controller & directive in AngularJS

I've created a directive to wrap a jQuery plugin, and I pass a config object for the plugin from the controller to the directive. (works)
In the config object is a callback that I want to call on an event. (works)
In the callback, I want to modify a property on the controller's $scope, which does not work. Angular does not recognize that the property has changed for some reason, which leads me to believe that the $scope in the callback is different than the controller's $scope. My problem is I just don't why.
Can anybody point me in the right direction?
Click here for Fiddle
app.js
var app = angular.module('app', [])
.directive('datepicker', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
// Uncommenting the line below causes
// the "date changed!" text to appear,
// as I expect it would.
// scope.dateChanged = true;
var dateInput = angular.element('.datepicker')
dateInput.datepicker(scope.datepickerOpts);
// The datepicker fires a changeDate event
// when a date is chosen. I want to execute the
// callback defined in a controller.
// ---
// PROBLEM:
// Angular does not recognize that $scope.dateChanged
// is changed in the callback. The view does not update.
dateInput.bind('changeDate', scope.onDateChange);
}
};
});
var myModule = angular.module('myModule', ['app'])
.controller('MyCtrl', ['$scope', function ($scope) {
$scope.dateChanged = false;
$scope.datepickerOpts = {
autoclose: true,
format: 'mm-dd-yyyy'
};
$scope.onDateChange = function () {
alert('onDateChange called!');
// ------------------
// PROBLEM AREA:
// This doesnt cause the "date changed!" text to show.
// ------------------
$scope.dateChanged = true;
setTimeout(function () {
$scope.dateChanged = false;
}, 5000);
};
}]);
html
<div ng-controller="MyCtrl">
<p ng-show="dateChanged">date changed!</p>
<input type="text" value="02-16-2012" class="datepicker" datepicker="">
</div>
There are a number of scope issues at work in your demo. First , within the dateChange callback, even though the function itself is declared inside the controller, the context of this within the callback is the bootstrap element since it is within a bootstrap handler.
Whenever you change angular scope values from within third party code , angular needs to know about it by using $apply. Generally best to keep all third party scopes inside the directive.
A more angular apprroach is to use ng-model on the input. Then use $.watch for changes to the model. This helps keep all the code inside the controller within angular context. Is rare in any angular application not to use ng-model on any form controls
<input type="text" class="datepicker" datepicker="" ng-model="myDate">
Within directive:
dateInput.bind('changeDate',function(){
scope.$apply(function(){
scope[attrs.ngModel] = element.val()
});
});
Then in Controller:
$scope.$watch('myDate',function(oldVal,newVal){
if(oldVal !=newVal){
/* since this code is in angular context will work for the hide/show now*/
$scope.dateChanged=true;
$timeout(function(){
$scope.dateChanged=false;
},5000);
}
});
Demo: http://jsfiddle.net/qxjck/10/
EDIT One more item that should change is remove var dateInput = angular.element('.datepicker') if you want to use this directive on more than one element in page. It is redundant being used in directive where element is one of the arguments in the link callback already, and is instance specific. Replace dateInput with element
The changeDate event bound to the input seems to be set up to fire outside of the Angular framework. To show the paragraph, call $scope.$apply() after setting dateChanged to true. To hide the paragraph after the delay, you can use $apply() again inside the function passed to setTimeout, but you're likely to keep out of further trouble using Angular's $timeout() instead.
Fiddle

Resources