AngularJS: attrs.$observe not firing after the first time - angularjs

jsFiddle here
In the fiddle, if you click on the submit button for the first time, notice that the <input> correctly gets focused on. On subsequent clicks, the focus isn't set any longer.
I noticed that the $observe callback isn't getting triggered when submit() changes isFocused, so I added a blur listener to explicitly reset the variable but that didn't help either.
How do I make the focus stick on submit?
Edit: Final working fiddle. Using $watch instead because I want it to work with arbitrary expressions

If you watch the element you'll notice that when the blur callback is fired the focus-on attribute isn't set to false. The reason for this is that the blur function is outside angular's scope and so it isn't aware of changes you make inside it.
To make angular aware of the change you need wrap any changes inside a call to $apply
element.bind('blur', function () {
scope.$apply(function() {
scope.isFocused = false;
console.log('blur');
});
});
http://jsfiddle.net/6vfGm/7/

I don't think you can observe the focusOn attribute because it doesn't really exist as an attribute of the directive. When you do <input focus-on='{{isFocused}}' /> the focus-on is actually your directive name.
Try this instead:
HTML
<div ng-app='foo'>
<form ng-controller='FormCtrl' ng-submit='submit()'>
<input focus-on focused='isFocused' />
<button>Submit</button>
</form>
</div>
Javascript
app.directive('focusOn', function () {
return {
scope: { focused: '=' },
link: function(scope, element) {
scope.$watch('focused', function (newValue) {
console.log('$watch', newValue)
if (newValue) {
element[0].focus();
}
});
element.bind('blur', function () {
scope.focused = false;
scope.$apply(); // required so that the scope is updated correctly.
console.log('blur');
});
}
};
});
app.controller('FormCtrl', function ($scope) {
$scope.submit = function () {
$scope.isFocused = true;
console.log('submit');
};
});
I created a focused attribute to bind the isFocused property to the directive scope (I could have named it focusOn, but I guess it would be weird to have <input type="text" focus-on focus-on="isFocused" />).
Here's your updated jsFiddle.
Update: I changed the attribute name from ng-model to focused so it makes more sense.

Related

Angular Bootstrap-Select timing issue to refresh

I love Bootstrap-Select and I am currently using it through the help of a directive made by another user joaoneto/angular-bootstrap-select and it works as intended except when I try to fill my <select> element with an $http or in my case a dataService wrapper. I seem to get some timing issue, the data comes after the selectpicker got displayed/refreshed and then I end up having an empty Bootstrap-Select list.. though with Firebug, I do see the list of values in the now hidden <select>. If I then go in console and manually execute a $('.selectpicker').selectpicker('refresh') it then works. I got it temporarily working by doing a patch and adding a .selectpicker('refresh') inside a $timeout but as you know it's not ideal since we're using jQuery directly in an ngController...ouch!So I believe the directive is possibly missing a watcher or at least something to trigger that the ngModel got changed or updated. Html sample code:
<div class="col-sm-5">
<select name="language" class="form-control show-tick"
ng-model="vm.profile.language"
selectpicker data-live-search="true"
ng-options="language.value as language.name for language in vm.languages">
</select>
<!-- also tried with an ng-repeat, which has the same effect -->
</div>
then inside my Angular Controller:
// get list of languages from DB
dataService
.getLanguages()
.then(function(data) {
vm.languages = data;
// need a timeout patch to properly refresh the Bootstrap-Select selectpicker
// not so good to use this inside an ngController but it's the only working way I have found
$timeout(function() {
$('.selectpicker, select[selectpicker]').selectpicker('refresh');
}, 1);
});
and here is the directive made by (joaoneto) on GitHub for Angular-Bootstrap-Select
function selectpickerDirective($parse, $timeout) {
return {
restrict: 'A',
priority: 1000,
link: function (scope, element, attrs) {
function refresh(newVal) {
scope.$applyAsync(function () {
if (attrs.ngOptions && /track by/.test(attrs.ngOptions)) element.val(newVal);
element.selectpicker('refresh');
});
}
attrs.$observe('spTheme', function (val) {
$timeout(function () {
element.data('selectpicker').$button.removeClass(function (i, c) {
return (c.match(/(^|\s)?btn-\S+/g) || []).join(' ');
});
element.selectpicker('setStyle', val);
});
});
$timeout(function () {
element.selectpicker($parse(attrs.selectpicker)());
element.selectpicker('refresh');
});
if (attrs.ngModel) {
scope.$watch(attrs.ngModel, refresh, true);
}
if (attrs.ngDisabled) {
scope.$watch(attrs.ngDisabled, refresh, true);
}
scope.$on('$destroy', function () {
$timeout(function () {
element.selectpicker('destroy');
});
});
}
};
}
One problem with the angular-bootstrap-select directive, is that it only watches ngModel, and not the object that's actually populating the options in the select. For example, if vm.profile.language is set to '' by default, and vm.languages has a '' option, the select won't update with the new options, because ngModel stays the same. I added a selectModel attribute to the select, and modified the angular-bootstrap-select code slightly.
<div class="col-sm-5">
<select name="language" class="form-control show-tick"
ng-model="vm.profile.language"
select-model="vm.languages"
selectpicker data-live-search="true"
ng-options="language.value as language.name for language in vm.languages">
</select>
</div>
Then, in the angular-bootstrap-select code, I added
if (attrs.selectModel) {
scope.$watch(attrs.selectModel, refresh, true);
}
Now, when vm.languages is updated, the select will be updated too. A better method would probably be to simply detect which object should be watched by using ngOptions, but using this method allows for use of ngRepeat within a select as well.
Edit:
An alternative to using selectModel is automatically detecting the object to watch from ngOptions.
if (attrs.ngOptions && / in /.test(attrs.ngOptions)) {
scope.$watch(attrs.ngOptions.split(' in ')[1], refresh, true);
}
Edit 2:
Rather than using the refresh function, you'd probably be better off just calling element.selectpicker('refresh'); again, as you only want to actually update the value of the select when ngModel changes. I ran into a scenario where the list of options were being updated, and the value of the select changed, but the model didn't change, and as a result it didn't match the selectpicker. This resolved it for me:
if (attrs.ngOptions && / in /.test(attrs.ngOptions)) {
scope.$watch(attrs.ngOptions.split(' in ')[1], function() {
scope.$applyAsync(function () {
element.selectpicker('refresh');
});
}, true);
}
Well, this is an old one... But I had to use it. This is what I added in the link(..) function of the directive:
scope.$watch(
_ => element[0].innerHTML,
(newVal, oldVal) => {
if (newVal !== oldVal)
{
element.selectpicker('refresh');
}
}
)

JQuery UI Spinner is not updating ng-model in angular

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);
}
});
};
});

Angular ng-blur not working with ng-hide

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);
});
}
};
});

scope.$watch in angular directive does not work proprely

I'm using Angular and Bootstrap.
I'm trying to replicate the functionality of ng-model for bootstrap checkbox. What i would like to accomplish is:
i would like that when i click on the checkbox (label really) the model change, and actually that works... but what does not work that when i try to watch the object for changes the behavior is weired, because i need two click insted of one for disable or enable the checkbox.
Moreover if inside the the label element that has as attribute cm-checkbox="model.prop" i put a {{model.anotherprop}} wont work (does not render anything).
From the documentation i understood that because i want the two-way data bind the scope must be defined as i did.
Thank you for your help!
I have the following HTML:
<label id="claim_free" class="checkbox" for="checkbox1" cm-checkbox="model.prop">
<span class="icons"><span class="first-icon fui-checkbox-unchecked"></span><span class="second-icon fui-checkbox-checked"></span></span><input name="claim_free" type="checkbox" value="" data-toggle="checkbox">
same lable text
</label>
And the following JS:
directive('cmCheckbox', function() {
return {
restrict: 'A',
scope: {'cmCheckbox':'='},
link: function(scope,elm,attrs) {
scope.$watch('cmCheckbox', function() {
console.log("first value for "+attrs.cmCheckbox+" is: "+scope.cmCheckbox);
if (!scope.cmCheckbox) {
console.log("checked");
$(elm).removeClass("checked");
$(elm).children("input").removeAttr("checked");
} else { // false and undefined
console.log("unchecked");
$(elm).addClass("checked");
$(elm).children("input").attr("checked","checked");
}
});
$(elm).on('click', function(e) {
e.preventDefault();
var currentValue = elm.hasClass("checked") ? false : true;
scope.$apply(function() {
scope.cmCheckbox = currentValue;
},true);
scope.$parent.$apply();
});
}
};
}).
Here is the jsFiddle: jsfiddle.net/pmcalabrese/66pCA/2

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