I am using scope.$watch to look for changes on an attribute-based directive. These changes are initiated by an ng-model binding on an input element in the view. The directive attribute on the view is being watched using scope.$watch in the directive. Yet the change event never seems to fire in the directive. What is causing my code to break?
The portions highlighted in the code below, where I log to the console (in the directive code, with stars), never fire. The change to the controller scope, via ng-model on the input, is not being propagated to the directive.
If I change the attribute value to a static string, rather than binding it via ng-model, it works.
This code is taken from a working example in the AngularJS documentation here. I cannot 'spot the difference' as the code in the documentation is very similar to mine.
angular.module('myApp.advancedDirectives', [
'myApp.advancedDirectives.advancedDirectives-directive'
])
.value('data', { name: 'John', surname: 'Smith' })
angular.module('myApp.advancedDirectives.advancedDirectives-directive', [])
.directive('advancedDirectives', ['$interval', 'dateFilter', 'data', function ($interval, dateFilter, data) {
console.log(data.length);
function link(scope, element, attrs) {
var format,
timeoutId;
scope.$watch(attrs.advancedDirectives, function (theFormat) {
format = theFormat;
**console.log(theFormat);
updateTime();**
});
element.on('$destroy', function () {
$interval.cancel(timeoutId);
});
// start the UI update process; save the timeoutId for canceling
timeoutId = $interval(function () {
updateTime(); // update DOM
}, 1000);
function updateTime() {
scope.directiveScope2 = dateFilter(new Date(), format);
}
}
var theScope = {
directiveScope1: '=info'
}
return {
templateUrl: 'components/advancedDirectives/advancedDirectivesTemplate.html',
scope: theScope,
link: link
}
}]);
<div ng-controller="viewAdvancedDirectivesCtrl">
<div>
<div><input ng-model="theViewFormat"/></div>
<div>Data from the directive scope: <span advanced-directives="theViewFormat" info='data'></span></div>
</div>
</div>
<span style='background:yellow'>Advanced Directive. Here is some data: {{directiveScope1.name}} {{directiveScope1.surname}}, alive at {{directiveScope2}}</span>
As you are using isolated scope for your directive, theViewFormat value wouldn't be available inside your directive context.
Either you need to use $parent.+ attrs.advancedDirectives while placing $watch
OR
More preferred way would be pass advancedDirectives via attribute to isolated scope like you did for passing info data
var theScope = {
directiveScope1: '=info',
advanced: '=advancedDirectives'
}
Then simply your watch would be on 'advanced' string.
scope.$watch('advanced', function (theFormat) {
format = theFormat;
console.log(theFormat);
updateTime();**
});
Here is my answer:
Below, please see the view code.
Note: I used curly-braces for {{theViewFormat}}
<div ng-controller="viewAdvancedDirectivesCtrl">
<div>
<div>Data from the directive scope: <span advanced-directives="{{theViewFormat}}" info='data'></span></div>
</div>
</div>
The link code looks like below.
Note: Use of $observe (as recommended by #Anik)
Note: I am observing on the attrs, rather than on the directive scope object, therefore avoiding recursive calls to updateTime, and I am now observing the attribute change, rather than the scope change, which matches my requirement.
function link(scope, element, attrs) {
var format,
timeoutId;
attrs.$observe('advancedDirectives', function (theFormat) {
format = theFormat;
console.log('format changed: ' + format);
updateTime();
});
element.on('$destroy', function () {
$interval.cancel(timeoutId);
});
// start the UI update process; save the timeoutId for canceling
timeoutId = $interval(function () {
updateTime(); // update DOM
}, 1000);
function updateTime() {
scope.directiveScope2 = dateFilter(new Date(), format);
console.log('updateTime: ' + format + dateFilter(new Date(), format));
}
}
The updateTime() code, which updates the directive scope is as follows:
function updateTime() {
scope.directiveScope2 = dateFilter(new Date(), format);
console.log('updateTime: ' + format + dateFilter(new Date(), format));
}
Thanks to #Pankaj and #Anik for getting me a long way ahead.
Related
I have an element that shows a datetimepicker. It is timezone sensitive. The time is stored in an item object in the scope as item.time, while the TZ is stored as item.timezone.
I created a directive to render it. In order to actually render correctly, it needs to know the timezone, but at link time (when it is all processed), the controller has not yet correctly loaded item... or so I would think, but it has correctly loaded item, because item.time is there. Nonetheless, when the formatter gets called, it has attrs.timezone as "", even though modelValue is loaded correctly.
HTML:
<span datetimepicker="true" ng-model="item.time" timezone="{{item.timezone}}"></span>
And the JS, (leaving out most of it...)
.directive('datetimepicker',function () {
return {
restrict: 'A',
require: 'ngModel',
template: '<span><input readonly="readonly"></input><span class="glyphicon glyphicon-calendar"></span></span>',
replace: true,
link: function ($scope,element,attrs,ngModel) {
var format = "YYYY-MM-DD H:mm",
formatter = function (modelValue) {
// HERE IS MY PROBLEM: at this point, modelValue does == item.time, but attrs.timezone is "", even though I know it loads correctly!
var ret = modelValue ? moment(modelValue) : "", mtz;
if (ret.tz) {
mtz = ret.tz(attrs.timezone);
}
if (mtz) {
ret = mtz.format(format);
} else if (ret.format) {
ret = ret.format(format);
}
return ret;
};
ngModel.$formatters.push(formatter);
};
})
EDIT:
angular is properly evaluating the attr "timezone" on the element; it is just not available inside the link function, or even the formatter. Here is the html after loading:
<span timezone="America/Winnipeg" ng-model="item.time" datetimepicker="true" class="ng-valid ng-valid-datetime ng-dirty">
<input readonly="readonly"/><span class="glyphicon glyphicon-calendar">
</span>
EDIT: add controller, highly simplified
.controller('ItemDetail',['$scope','Item',
function ($scope,Item) {
var itemId = "40"; // not really, but good enough for here
Item.get({item: itemId},function (item) {
$scope.item = item;
},function (httpResponse) {
// do error reporting
});
// click button to enter edit mode, which hides <span> showing time and shows input with datepicker
$scope.edit = function () {
$scope.editMode = true;
};
$scope.save = function () {
// save the updates
$scope.item.$save();
$scope.editMode = false;
};
$scope.cancel = function () {
// cancel any updates
$scope.item.$reset();
$scope.editMode = false;
};
}])
I think you should use an isolate scope, bind the scope var through attribute bindings (= binding), and use scope.$watchto set the timezone with a callback:
// directive scope binding
scope: {
timezone: '='
}
// watch local scope var in the link function since it's now binded
scope.$watch('timezone',function(newval, oldval){
// do what you need to do from here when the timezone var gets set
mtz = ret.tz(attrs.timezone);
});
More information on scope bindings
I have a directive with isolated scope with a value with two way binding to the parent scope. I am calling a method that changes the value in the parent scope, but the change is not applied in my directive.(two way binding is not triggered). This question is very similar:
AngularJS: Parent scope not updated in directive (with isolated scope) two way binding
but I am not changing the value from the directive, but changing it only in the parent scope. I read the solution and in point five it is said:
The watch() created by the isolated scope checks whether it's value for the bi-directional binding is in sync with the parent's value. If it isn't the parent's value is copied to the isolated scope.
Which means that when my parent value is changed to 2, a watch is triggered. It checks whether parent value and directive value are the same - and if not it copies to directive value. Ok but my directive value is still 1 ... What am I missing ?
html :
<div data-ng-app="testApp">
<div data-ng-controller="testCtrl">
<strong>{{myValue}}</strong>
<span data-test-directive data-parent-item="myValue"
data-parent-update="update()"></span>
</div>
</div>
js:
var testApp = angular.module('testApp', []);
testApp.directive('testDirective', function ($timeout) {
return {
scope: {
key: '=parentItem',
parentUpdate: '&'
},
replace: true,
template:
'<button data-ng-click="lock()">Lock</button>' +
'</div>',
controller: function ($scope, $element, $attrs) {
$scope.lock = function () {
console.log('directive :', $scope.key);
$scope.parentUpdate();
//$timeout($scope.parentUpdate); // would work.
// expecting the value to be 2, but it is 1
console.log('directive :', $scope.key);
};
}
};
});
testApp.controller('testCtrl', function ($scope) {
$scope.myValue = '1';
$scope.update = function () {
// Expecting local variable k, or $scope.pkey to have been
// updated by calls in the directive's scope.
console.log('CTRL:', $scope.myValue);
$scope.myValue = "2";
console.log('CTRL:', $scope.myValue);
};
});
Fiddle
Use $scope.$apply() after changing the $scope.myValue in your controller like:
testApp.controller('testCtrl', function ($scope) {
$scope.myValue = '1';
$scope.update = function () {
// Expecting local variable k, or $scope.pkey to have been
// updated by calls in the directive's scope.
console.log('CTRL:', $scope.myValue);
$scope.myValue = "2";
$scope.$apply();
console.log('CTRL:', $scope.myValue);
};
});
The answer Use $scope.$apply() is completely incorrect.
The only way that I have seen to update the scope in your directive is like this:
angular.module('app')
.directive('navbar', function () {
return {
templateUrl: '../../views/navbar.html',
replace: 'true',
restrict: 'E',
scope: {
email: '='
},
link: function (scope, elem, attrs) {
scope.$on('userLoggedIn', function (event, args) {
scope.email = args.email;
});
scope.$on('userLoggedOut', function (event) {
scope.email = false;
console.log(newValue);
});
}
}
});
and emitting your events in the controller like this:
$rootScope.$broadcast('userLoggedIn', user);
This feels like such a hack I hope the angular gurus can see this post and provide a better answer, but as it is the accepted answer does not even work and just gives the error $digest already in progress
Using $apply() like the accepted answer can cause all sorts of bugs and potential performance hits as well. Settings up broadcasts and whatnot is a lot of work for this. I found the simple workaround just to use the standard timeout to trigger the event in the next cycle (which will be immediately because of the timeout). Surround the parentUpdate() call like so:
$timeout(function() {
$scope.parentUpdate();
});
Works perfectly for me. (note: 0ms is the default timeout time when not specified)
One thing most people forget is that you can't just declare an isolated scope with the object notation and expect parent scope properties to be bound. These bindings only work if attributes have been declared through which the binding 'magic' works. See for more information:
https://umur.io/angularjs-directives-using-isolated-scope-with-attributes/
Instead of using $scope.$apply(), try using $scope.$applyAsync();
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 found this Angular Directive online to add a twitter share button. It all seems staright forward but I can't work out what the attrs.$observe is actually doing.
I have looked in the docs but can't see $observe referenced anywhere.
The directive just seems to add the href which would come from the controller so can anyone explain what the rest of the code is doing?
module.directive('shareTwitter', ['$window', function($window) {
return {
restrict: 'A',
link: function($scope, element, attrs) {
$scope.share = function() {
var href = 'https://twitter.com/share';
$scope.url = attrs.shareUrl || $window.location.href;
$scope.text = attrs.shareText || false;
href += '?url=' + encodeURIComponent($scope.url);
if($scope.text) {
href += '&text=' + encodeURIComponent($scope.text);
}
element.attr('href', href);
}
$scope.share();
attrs.$observe('shareUrl', function() {
$scope.share();
});
attrs.$observe('shareText', function() {
$scope.share();
});
}
}
}]);
Twitter
In short:
Everytime 'shareTwitterUrl' or 'shareTwitterText' changes, it will call the share function.
From another stackoverflow answer: (https://stackoverflow.com/a/14907826/2874153)
$observe() is a method on the Attributes object, and as such, it can
only be used to observe/watch the value change of a DOM attribute. It
is only used/called inside directives. Use $observe when you need to
observe/watch a DOM attribute that contains interpolation (i.e.,
{{}}'s). E.g., attr1="Name: {{name}}", then in a directive:
attrs.$observe('attr1', ...). (If you try scope.$watch(attrs.attr1,
...) it won't work because of the {{}}s -- you'll get undefined.) Use
$watch for everything else.
From Angular docs: (http://docs.angularjs.org/api/ng/type/$compile.directive.Attributes)
$compile.directive.Attributes#$observe(key, fn);
Observes an interpolated attribute.
The observer function will be invoked once during the next $digest fol
lowing compilation. The observer is then invoked whenever the interpolated value changes.
<input type="text" ng-model="value" >
<p sr = "_{{value}}_">sr </p>
.directive('sr',function(){
return {
link: function(element, $scope, attrs){
attrs.$observe('sr', function() {
console.log('change observe')
});
}
};
})
Using a directive focus-me="inTextModeInput" in a text input
app.directive('focusMe', function($timeout) {
/*focuses on input
<input type="text" focus-me="focusInput">
*/
return {
scope: { trigger: '=focusMe' },
link: function(scope, element) {
scope.$watch('trigger', function(value) {
if(value === true) {
$timeout(function() {
element[0].focus();
scope.trigger = false;
});
}
});
}
};
});
Actually having 2 inputs, both uses focus-me
When i programatically set the value to focus on an input the ng-blur of other is not called.
NOTE : i am also using this in an ng-repeat.
Isolated scope
The blur is called, but you're not seeing that because you've created a directive with an isolated scope. The ng-blur is executed on the $parent scope. You should only use an isolated scope when the directive is implementing re-useable templates.
Two way binding on trigger
The line 'scope.trigger = false' is also setting a different boolean value because it's on a different scope. If you want to assign a value to a variable from a directive you should always wrap the value inside another object: var focus = { me: true } and set it like trigger=focus.me.
A better solution
But I wouldn't set the trigger to false at all. AngularJS is a MVC/MVVM based framework which has a model state for the user interface. This state should be idempotent; meaning that if you store the current state, reload the page and restore the state the user interface should be in the exact same situation as before.
So what you probably need is a directive that
Has no isolated scope (which allows all other directives to work: ng-blur, ng-focus, ...)
Keeps track of a boolean, which indicates the focus state
Sets this boolean to false when the element has lost focus
It's probably easier to see this thing in action: working plunker.
Maybe this (other) plunker will give you some more insight on scopes and directives.
Code
myApp.directive('myFocus', function($parse, $timeout) {
return {
restrict: 'A',
link: function myFocusLink($scope, $element, $attrs, ctrls) {
var e = $element[0];
// Grab a parser from the provided expression so we can
// read and assign a value to it.
var getModel = $parse($attrs.myFocus);
var setModel = getModel.assign;
// Watch the parser -- and focus if true or blur otherwise.
$scope.$watch(getModel, function(value) {
if(value) {
e.focus();
} else {
e.blur();
}
});
function onBlur() {
$timeout(function() {
setModel($scope, false);
});
}
function onFocus() {
$timeout(function() {
setModel($scope, true);
});
}
$element.on('focus', onFocus);
$element.on('blur', onBlur);
// Cleanup event registration if the scope is destroyed
$scope.$on('$destroy', function() {
$element.off('focus', onFocus);
$element.off('blur', onBlur);
});
}
};
});