I'm trying to create a "Clock" directive as a way to learn Angular. How would I go about making sure it "ticks?"
Here's what I tried fiddle Link:
angular.module('app')
.directive('clock' , function(){
return {
restrict: 'E',
template: '<div>{{date}}</div>',
link: function(scope, elem, attr){
scope.date = getDate();
setInterval(function(){
scope.date = getDate();
}, 100);
}
}
})
Using setInterval to upate the scope variable isn't working. I'm assuming because the directive isn't watching for it to be changes anymore?
It is updating the scope variable, but Angular isn't aware of the change.
It is recommended to use Angular $interval.
Here is your fiddle with $interval: https://jsfiddle.net/oafcde6g/11/
In other words, Angular doesn't do any magic by "listening" to scope properties, it simply does dirty-checking. But to do it, it needs to be notified that something changed. So if you really wanted to use setInterval instead of $interval, you could notify it manually that something changed:
setInterval(function () {
scope.$apply(function () {
scope.date = getDate();
});
});
This way, you're notifying it to run your anonymous function inside $apply and then digest the scope, checking if anything changed. If it did, it will update the view.
"Magic" of ng-* events (ng-click, ng-model etc)
Even though it looks like angular automatically knows to rerender the view when you click/change something by using it's built-in directives, it really isn't so.
Some simple implementation of ng-click would look something like:
angular.module('app')
.directive('ngClick' , function(){
return {
restrict: 'A',
link: function(scope, elem, attr){
elem.on('click', function () {
scope.$apply(function () {
scope.$eval(attr.ngClick);
});
});
}
}
})
You can see this for yourself if you try to bind click event normally, outside of angular context and make it change a scope variable. It won't update until you explicitly tell it to (like we did in setInterval example).
Related
To start off, I know there are loads of similar questions but none that I found which supports execution of arbitrary methods with event listeners.
I have this directive which executes functions passed when the window resizes.
app.directive('onResize', function() {
var directive = {
'link': function(scope, element, attrs) {
var onResizeHandler = scope.$eval(attrs.onResize);
angular.element(window).on('resize', onResizeHandler);
angular.element(window).on('$destory', function() {element.off();});
}
};
return directive;
});
I can trigger the above directive with
<div data-on-resize="stickyHeader">...</div>
Which runs my method, inside my controller.
app.controller('myController', [$scope, function($scope) {
$scope.stickyHeader = function() {
console.log('event triggered')
};
}]);
All the above code works fine, but I need to pass some arguments to stickyHeader as in data-on-resize="stickyHeader(arg1, arg2)" When I try that, I get Cannot read property 'call' of undefined at ng (angular.js:3795) in the console. Not sure what I can do to make my directive support arbitrary methods with arguments.
The directive needs to evaluate the AngularJS expression defined by the on-resize attribute every time the events occurs:
app.directive('onResize', function($window) {
var directive = {
link: function(scope, elem, attrs) {
angular.element($window).on('resize', onResizeHandler);
scope.$on('$destroy', function() {
angular.element($window).off('resize', onResizeHandler);
});
function onResizeHandler(event) {
scope.$eval(attrs.onResize, {$event: event});
scope.$apply();
}
}
};
return directive;
});
Also since the resize event comes from outside the AngularJS framework, the event needs to be brought into the AngularJS execution context with $apply().
Further, to avoid memory leaks, the event handler needs to be unbound when the scope is destroyed.
Usage:
<div data-on-resize="stickyHeader($event)">...</div>
For more information, see AngularJS Developer Guide - $event.
I have a directive that gets its data from a factory. It works fine, when loads first time. But later, when the factory data changed, directive do not react on this changes. How can i fix it?
appWD.directive('widgetName', ['WidgetData', function(WidgetData) {
return {
restrict: 'E',
templateUrl: '_widget.html',
link: function(scope, elem, attrs) {
scope.data = WidgetData.GetWidgetData('widgetName');
//both do not work
//scope.$watch(scope.data);
//scope.$watch(WidgetData.GetWidgetData('widgetName'));
}
};
}]);
GetWidgetData('widgetName') returns object.
codepen example: http://codepen.io/anon/pen/xwLLxM?editors=101
Try using
scope.$watchCollection(function(){return WidgetData.getWidgetData('widget');}, function() {
alert('scope.$watchCollection');
});
$watchCollection "shallow watches the properties of an object and fires whenever any of the properties change" AngularJs Docs
I created a small app to replicate your problem. Here is the Demo
setTimeout(function(){
$scope.$apply()
},0);
put this after data updation, this vl update UI
May work
//main controller
angular.module('myApp')
.controller('mainCtrl', function ($scope){
$scope.loadResults = function (){
console.log($scope.searchFilter);
};
});
// directive
angular.module('myApp')
.directive('customSearch', function () {
return {
scope: {
searchModel: '=ngModel',
searchChange: '&ngChange',
},
require: 'ngModel',
template: '<input type="text" ng-model="searchModel" ng-change="searchChange()"/>',
restrict: 'E'
};
});
// html
<custom-search ng-model="searchFilter" ng-change="loadResults()"></custom-search>
Here is a simplified directive to illustrate. When I type into the input, I expect the console.log in loadResults to log out exactly what I have already typed. It actually logs one character behind because loadResults is running just before the searchFilter var in the main controller is receiving the new value from the directive. Logging inside the directive however, everything works as expected. Why is this happening?
My Solution
After getting an understanding of what was happening with ngChange in my simple example, I realized my actual problem was complicated a bit more by the fact that the ngModel I am actually passing in is an object, whose properties i am changing, and also that I am using form validation with this directive as one of the inputs. I found that using $timeout and $eval inside the directive solved all of my problems:
//main controller
angular.module('myApp')
.controller('mainCtrl', function ($scope){
$scope.loadResults = function (){
console.log($scope.searchFilter);
};
});
// directive
angular.module('myApp')
.directive('customSearch', function ($timeout) {
return {
scope: {
searchModel: '=ngModel'
},
require: 'ngModel',
template: '<input type="text" ng-model="searchModel.subProp" ng-change="valueChange()"/>',
restrict: 'E',
link: function ($scope, $element, $attrs, ngModel)
{
$scope.valueChange = function()
{
$timeout(function()
{
if ($attrs.ngChange) $scope.$parent.$eval($attrs.ngChange);
}, 0);
};
}
};
});
// html
<custom-search ng-model="searchFilter" ng-change="loadResults()"></custom-search>
The reason for the behavior, as rightly pointed out in another answer, is because the two-way binding hasn't had a chance to change the outer searchFilter by the time searchChange(), and consequently, loadResults() was invoked.
The solution, however, is very hacky for two reasons.
One, the caller (the user of the directive), should not need to know about these workarounds with $timeout. If nothing else, the $timeout should have been done in the directive rather than in the View controller.
And two - a mistake also made by the OP - is that using ng-model comes with other "expectations" by users of such directives. Having ng-model means that other directives, like validators, parsers, formatters and view-change-listeners (like ng-change) could be used alongside it. To support it properly, one needs to require: "ngModel", rather than bind to its expression via scope: {}. Otherwise, things would not work as expected.
Here's how it's done - for another example, see the official documentation for creating a custom input control.
scope: true, // could also be {}, but I would avoid scope: false here
template: '<input ng-model="innerModel" ng-change="onChange()">',
require: "ngModel",
link: function(scope, element, attrs, ctrls){
var ngModel = ctrls; // ngModelController
// from model -> view
ngModel.$render = function(){
scope.innerModel = ngModel.$viewValue;
}
// from view -> model
scope.onChange = function(){
ngModel.$setViewValue(scope.innerModel);
}
}
Then, ng-change just automatically works, and so do other directives that support ngModel, like ng-required.
You answered your own question in the title! '=' is watched while '&' is not
Somewhere outside angular:
input view value changes
next digest cycle:
ng-model value changes and fires ng-change()
ng-change adds a $viewChangeListener and is called this same cycle.
See:
ngModel.js#L714 and ngChange.js implementation.
At that time $scope.searchFilter hasn't been updated. Console.log's old value
next digest cycle:
searchFilter is updated by data binding.
UPDATE: Only as a POC that you need 1 extra cycle for the value to propagate you can do the following. See the other anwser (#NewDev for a cleaner approach).
.controller('mainCtrl', function ($scope, $timeout){
$scope.loadResults = function (){
$timeout(function(){
console.log($scope.searchFilter);
});
};
});
Angular's ng-model is not updating when using jquery-ui spinner.
Here is the jsfiddle http://jsfiddle.net/gCzg7/1/
<div ng-app>
<div ng-controller="SpinnerCtrl">
<input type="text" id="spinner" ng-model="spinner"/><br/>
Value: {{spinner}}
</div>
</div>
<script>
$('#spinner').spinner({});
</script>
If you update the text box by typing it works fine (you can see the text change). But if you use the up or down arrows the model does not change.
Late answer, but... there's a very simple and clean "Angular way" to make sure that the spinner's spin events handle the update against ngModel without resorting to $apply (and especially without resorting to $parse or an emulation thereof).
All you need to do is define a very small directive with two traits:
The directive is placed as an attribute on the input element you want to turn into a spinner; and
The directive configures the spinner such that the spin event listener calls the ngModel controller's $setViewValue method with the spin event value.
Here's the directive in all its clear, tiny glory:
function jqSpinner() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, c) {
element.spinner({
spin: function (event, ui) {
c.$setViewValue(ui.value);
}
});
}
};
};
Note that $setViewValue is intended for exactly this situation:
This method should be called when an input directive wants to change
the view value; typically, this is done from within a DOM event
handler.
Here's a link to a working demo.
If the demo link provided above dies for some reason, here's the full example script:
(function () {
'use strict';
angular.module('ExampleApp', [])
.controller('ExampleController', ExampleController)
.directive('jqSpinner', jqSpinner);
function ExampleController() {
var c = this;
c.exampleValue = 123;
};
function jqSpinner() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, c) {
element.spinner({
spin: function (event, ui) {
c.$setViewValue(ui.value);
}
});
}
};
};
})();
And the minimal example template:
<div ng-app="ExampleApp" ng-controller="ExampleController as c">
<input jq-spinner ng-model="c.exampleValue" />
<p>{{c.exampleValue}}</p>
</div>
Your fiddle is showing something else.
Besides this: Angular can not know about any changes that occur from outside its scope without being aknowledged.
If you change a variable of the angular-scope from OUTSIDE angular, you need to call the apply()-Method to make Angular recognize those changes. Despite that implementing a spinner can be easily achieved with angular itself, in your case you must:
1. Move the spinner inside the SpinnerCtrl
2. Add the following to the SpinnerCtrl:
$('#spinner').spinner({
change: function( event, ui ) {
$scope.apply();
}
}
If you really need or want the jQuery-Plugin, then its probably best to not even have it in the controller itself, but put it inside a directive, since all DOM-Manipulation is ment to happen within directives in angular. But this is something that the AngularJS-Tutorials will also tell you.
Charminbear is right about needing $scope.$apply(). Their were several problems with this approach however. The 'change' event only fires when the spinner's focus is removed. So you have to click the spinner then click somewhere else. The 'spin' event is fired on each click. In addition, the model needs to be updated before $scope.$apply() is called.
Here is a working jsfiddle http://jsfiddle.net/3PVdE/
$timeout(function () {
$('#spinner').spinner({
spin: function (event, ui) {
var mdlAttr = $(this).attr('ng-model').split(".");
if (mdlAttr.length > 1) {
var objAttr = mdlAttr[mdlAttr.length - 1];
var s = $scope[mdlAttr[0]];
for (var i = 0; i < mdlAttr.length - 2; i++) {
s = s[mdlAttr[i]];
}
s[objAttr] = ui.value;
} else {
$scope[mdlAttr[0]] = ui.value;
}
$scope.$apply();
}
}, 0);
});
Here's a similar question and approach https://stackoverflow.com/a/12167566/584761
as #Charminbear said angular is not aware of the change.
However the problem is not angular is not aware of a change to the model rather that it is not aware to the change of the input.
here is a directive that fixes that:
directives.directive('numeric', function() {
return function(scope, element, attrs) {
$(element).spinner({
change: function(event, ui) {
$(element).change();
}
});
};
});
by running $(element).change() you inform angular that the input has changed and then angular updates the model and rebinds.
note change runs on blur of the input this might not be what you want.
I know I'm late to the party, but I do it by updating the model with the ui.value in the spin event. Here's the updated fiddle.
function SpinnerCtrl($scope, $timeout) {
$timeout(function () {
$('#spinner').spinner({
spin: function (event, ui) {
$scope.spinner = ui.value;
$scope.$apply();
}
}, 0);
});
}
If this method is "wrong", any suggestions would be appreciated.
Here is a solution that updates the model like coder’s solution, but it uses $parse instead of parsing the ng-model parameter itself.
app.directive('spinner', function($parse) {
return function(scope, element, attrs) {
$(element).spinner({
spin: function(event, ui) {
setTimeout(function() {
scope.$apply(function() {
scope._spinnerVal = = element.val();
$parse(attrs.ngModel + "=_spinnerVal")(scope);
delete scope._spinnerVal;
});
}, 0);
}
});
};
});
I have a simple custom directive myElement with a templateUrl that print a simple message:
<p>Message: {{message}}</p>
Here's the definition of my directive:
testapp.directive('myElement', function() {
return {
restrict: 'E',
template: '<p>Message: {{message}}</p>',
link: function(scope, elem, attrs) {
scope.message = 'This message is never updated... :(';
setTimeout(function() {
scope.message = "Why this message is never shown?";
}, 1000);
}
};
});
After 1 second, I expect an update of the message to "Why this message is never shown?". Unfortunately, the message is never updated.
Here is the jsFiddle: http://jsfiddle.net/seyz/SNMfc
Could you explain me why?
Due to how Angular dirty checking works, when you execute code outside of angular scope (e.g. using setTimeout, setInterval, or from within some third-party plugin callback) the changes produced by that code invocation won't be 'recognized' immediately by Angular scope.
For such scenarios you need to wrap your code inside the scope.$apply() method.
In this particular case you may simply use $timeout function to replace your setTimeout(fn, 1000)call with $timeout(fn, 1000)and your code will be wrapped with scope.$apply() call (Plunker).
You will need to use scope.$apply();
setTimeout(function() {
scope.message = "Why this message is never shown?";
scope.$apply();
}, 1000);
From the documentation:
$apply() is used to execute an expression in angular from outside of
the angular framework. (For example from browser DOM events,
setTimeout, XHR or third party libraries).