AngularJS - setting focus to element using NON-ISOLATE directive - angularjs

I know this question has been asked about 100 times (trust me, I've read them all), but I'm having trouble getting focus to go to an input box when the directive does NOT use isolate scope. The scope.$watch doesn't fire when the backing data changes.
Why not just use the one with isolate scope, you ask? Well, my understanding is that you should ONLY use isolate scope if your directive has a template.
The only differences in the directives is:
// works
app.directive('doesFocus', function ($timeout) {
return {
scope: { trigger: '#doesFocus' },
link: function (scope, element) {
scope.$watch('trigger', function (value) {
// sets focus
}
...
// does not work, and in fact when I inspect attrs.doesNotFocus it is undefined
app.directive('doesNotFocus', function ($timeout) {
return {
scope: false,
link: function (scope, element, attrs) {
scope.$watch(attrs.doesNotFocus, function (value) {
// sets focus
}
...
I'm on week 3 of using Angular, so I must be missing some silly semantic issue.
Here is a fiddle illustrating my issue.
http://jsfiddle.net/tpeiffer/eAFmJ/
EDIT
My actual problem was that my real code was like this (hazard of mocking the problem, you sometimes mask the real problem):
<input should-focus="{{isDrawerOpen()}" ... ></input>
but because I was using a function, not a property, I was missing the required ticks
<input should-focus="{{'isDrawerOpen()'}}" ... ></input>
Making this change fixed the problem and my directive can still be like this:
scope.$watch(attrs.shouldFocus, focusCallback(newValue));
END EDIT
Thanks for helping me in my quest for angular excellence!
Thad

Remove {{}} from your HTML. So instead of:
<input class="filter-item" placeholder="Enter filter"
does-not-focus="{{bottomDrawerOpen}}" type="text">
use
<input class="filter-item" placeholder="Enter filter"
does-not-focus="bottomDrawerOpen" type="text">
Then it works with watching attrs.doesNotFocus:
scope.$watch(attrs.doesNotFocus, function (value) {...} );
Fiddle

Your bottom drawer was watching a function isDrawerOpen(), not a property.
Change
scope.$watch('isDrawerOpen()',...);
to
scope.$watch('toggleBottomDrawer',...);

Related

Angularjs Interpolation using double curly braces not working under ng-if

UPDATE1: developed the plunker sample that will reproduce the problem. See below.
I have a strange problem in my project, where it appears in one place only. Finally, I was able to reproduce the problem using plunker sample:
http://plnkr.co/edit/JJbq54?p=preview
In the above sample, see the section "With ng-if" and "Without ng-if", enter something in the input text, and see how the double curly braces not working under ng-if, but ng-bind works fine. Also, if you remove check-if-required from the template sites-and-improvements.html also the problem is solved.
More details below:
I have the the following HTML5 code block:
<div ng-if="isFullCUSPAP" id="sites_and_imrpovements_comments">
<div class="form-row">
<div class="inputs-group">
<label>WIND TURBINE:</label>
<div class="input-binary">
<label>
<input type="radio" id="wind_turbine"
name="wind_turbine"
ng-model="$parent.wind_turbine"
value="Yes" force-model-update />
Yes
</label>
</div>
<div class="input-binary">
<label>
<input type="radio" id="wind_turbine"
name="wind_turbine"
ng-model="$parent.wind_turbine"
value="No" force-model-update />
No
</label>
</div>
<span ng-bind="wind_turbine"></span>
<span>wind_turbine = {{wind_turbine}}</span>
</div>
</div>
</div>
I know that ng-if will create a new child scope. See above code, scope variable wind_trubine. Only in this HTML5 file, the curly braces {{}} is not working. However, if I use ng-bind it works fine. In other HTML5 files, I have no problem what so ever. This HTML5 is implemented using directive as follows:
app.directive('sitesAndImprovements', function() {
return {
restrict: 'E',
replace:true,
templateUrl: '<path-to-file>/site-and-improvments.html',
link: function (scope, elem, attrs) {
//Business Logic for Sites and Improvements
}
}
})
And, simply, I put it in the parent as follows:
<sites-and-improvements></sites-and-improvements>
The only difference I could see, is that this implementation has two levels of nested ng-if, which would look like the following:
<div ng-if="some_expression">
...
...
<sites-and-improvements></sites-and-improvements>
...
...
</div>
Based on comments, I used controller As notation and defined MainController accordingly. See snapshots below. It seems there is a problem if ng-if is nested with two levels. The scope variable is completely confused. I don't get the same results using ng-bind and double curly braces.
If you examine the above snapshots, even though I used controller As notation, you will see that ng-bind gives different results when compared with interpolation using {{}}.
I even changed the default value of wind_turbine to be set as follows in the link function:
scope.MainController.wind_turbine = 'Yes';
I noticed that on page load, everything looks fine, but when I change the value of the input element wind_trubine using the mouse, all related reference are updated correctly except the one that uses {{}}.
Maybe this is because there are two nested levels of ng-if?
Appreciate your feedback.
Tarek
Remove the replace: true from the sites-and-improvements directive:
app.directive('sitesAndImprovements', function() {
return {
restrict: 'E',
̶r̶e̶p̶l̶a̶c̶e̶:̶t̶r̶u̶e̶,̶
templateUrl: 'site-and-improvments.html',
link: function (scope, elem, attrs) {
//debugger;
}
}
})
It is fighting the check-if-required directive:
app.directive('checkIfRequired', ['$compile', '$timeout', function ($compile, $timeout) {
return {
priority: 2000,
terminal: true,
link: function (scope, el, attrs) {
el.removeAttr('check-if-required');
$timeout(function(){
//debugger;
$(':input', el).each(function(key, child) {
if (child && child.id === 'test_me') {
angular.element(child).attr('ng-required', 'true');
}
if (child && child.id === 'testInput1') {
//debugger;
//angular.element(child).attr('ng-required', 'true');
}
});
$compile(el, null, 2000)(scope);
})
}
};
}])
The DEMO on PLNKR.
replace:true is Deprecated
From the Docs:
replace ([DEPRECATED!], will be removed in next major release - i.e. v2.0)
specify what the template should replace. Defaults to false.
true - the template will replace the directive's element.
false - the template will replace the contents of the directive's element.
-- AngularJS Comprehensive Directive API - replace deprecated
From GitHub:
Caitp-- It's deprecated because there are known, very silly problems with replace: true, a number of which can't really be fixed in a reasonable fashion. If you're careful and avoid these problems, then more power to you, but for the benefit of new users, it's easier to just tell them "this will give you a headache, don't do it".
-- AngularJS Issue #7636
For more information, see Explain replace=true in Angular Directives (Deprecated)
Another solution posted by AngularJS team here:
https://github.com/angular/angular.js/issues/16140#issuecomment-319332063
Basically, they recommend to convert the link() function to use compile() function instead. Here is the update code:
app.directive('checkIfRequired', ['$compile', '$timeout', function ($compile, $timeout) {
return {
priority: 2000,
terminal: true,
compile: function (el, attrs) {
el.removeAttr('check-if-required');
var children = $(':input', el);
children.each(function(key, child) {
if (child && child.id === 'test_me') {
angular.element(child).attr('ng-required', 'true');
}
});
var compiled = $compile(el, null, 2000);
return function( scope ) {
compiled( scope );
};
}
};
}]).directive('sitesAndImprovements', function() {
return {
restrict: 'E',
replace:true,
templateUrl: 'site-and-improvments.html'
}
});
The main problem I have with this solution is that I am using the scope parameter which is passed to the link() function. For example, in the .each() loop above, I need to get the value of the element ID which is based on interpolation using {{<angular expre>}}.
So I tried to use pre-link and post-link within the compile function where the scope is available. I noticed that the section with ng-if is removed when execution is in pre-link and then it is added shortly after that. So I had to use $watch to monitor changes to the children to run the needed process when required. I developed this plunker sample:
http://plnkr.co/edit/lsJvhr?p=preview
Even after all such effort, the issue is not resolved. So the bottom line for similar cases, is that if you need to use the scope then you have to remove replace: true.
Any feedback would be appreciated.
Tarek

handle submit event in directive as well

I’m quite new to Angular but I love it already!
I need to write a couple of reusable components for a wizard.
I would like to handle the submit event from the form in the directive as well, is this possible?
<form ng-submit="submit()" name="exampleForm">
<foo data="someData"></foo> <!-- I need to handle the submit event in directive as well-->
<input type="submit" id="submit" value="Submit"/>
</form>
If the user presses enter or clicks the button on the form the directive has to make a call to the backend and double check the data.
If the check is successful the form will be valid.
I built a simple example here
Thanks in advance!
Stefan
Yes it's possible.
The first issue is targeting the correct $scope. Right now your code targets the submit() function on the controller. The <form> can't see the $scope of the directive due to how the html elements are nested.
Since you want to submit() from the directive, then the directive template should also include the <form> element and the submit input button.
Invoking foo will look like this:
<foo data="someData" name="exampleForm"></foo>
If you would rather keep foo the way it is (and that's a totally legit way to go about it) then you'll need a new directive with the submit() function. So you'd have 2 directives working together (Very angular! Many wow!).
What you will want here is a custom directive for form validation. Custom form validators are added to the $validators object on the ngModelController.
For a simple example for an "integer" directive (found in the Angular docs here under "Custom Validation"), the directive would be defined like this:
var INTEGER_REGEXP = /^\-?\d+$/;
app.directive('integer', function() { return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$validators.integer = function(modelValue, viewValue) {
if (ctrl.$isEmpty(modelValue)) {
// consider empty models to be valid
return true;
}
if (INTEGER_REGEXP.test(viewValue)) {
// it is valid
return true;
}
// it is invalid
return false;
};
} }; });
And it would be used in the form like this:
<form name="form" class="css-form" novalidate>
<input type="number" ng-model="size" name="size" min="0" max="10" integer />{{size}}<br />
</form>
There is also an option for asynchronous validation, adding the validator to the $asyncValidatorsobject. More info is found at the link mentioned above. The working example using both is found here
Here's a working example:
http://plnkr.co/edit/sckCOq3a50PBjat2uixk?p=preview
I've created another 2-way binding on the directive's scope (called "submit"), so you can specify the name of the submit function as it will be seen from ExampleController's scope (I used "directiveSubmit" here).
scope: {
data: '=',
submit: '='
},
.
<foo data="someData" submit="directiveSubmit"></foo>
The directive's controller then creates that function and assigns it to its own scope, and that also assigns it to ExampleController's scope via the magic of 2-way binding.
Next you can run that submit function from ExampleController's scope and it will actually be referring to the newly created function in the directive.
<form ng-submit="directiveSubmit(exampleForm)" name="exampleForm" novalidate>
Notice that I'm also passing the exampleForm into that function. Without that the directive will have a hard time getting access to it.
Finally, I've also added novalidate to the form because if you don't do that some browsers (i.e. Chrome) will handle form validation in a way that may (or may not) be undesirable. In Chrome try removing that and submitting, then you'll see what I mean (I also made the "name" field required).

OnChange callback for md-slider in Angular Material Design

How can I know in controller that the value of < md-slider > from Angular Material Design has changed?
you can add ng-change on the directive
<md-slider min="0" max="50" ng-model="text" ng-change="myMethod()" md-discrete></md-slider>
I didn't find how to do it in a proper way, but you can do it by creating a directive and an event, like:
.directive('testDragEnd', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
element.on('$md.dragend', function() {
console.info('Drag Ended');
})
}
}
})
And the, you have to add the directive in the <md-slider>. (<md-slider test-drag-end></md-slider>).
I hope it helps.
In Angular Material's git hub there are few other events where you can do the same:
https://github.com/angular/material/blob/952ee3489e84226c73f83db15f8586db93cdca19/src/components/slider/slider.js
element
.on('keydown', keydownListener)
.on('$md.pressdown', onPressDown)
.on('$md.pressup', onPressUp)
.on('$md.dragstart', onDragStart)
.on('$md.drag', onDrag)
.on('$md.dragend', onDragEnd);
I used a $watch on the variable bound to the slider:
$scope.$watch(
function() {
return $scope.tex;
},
function(newValue, oldValue) {
$mdToast.show($mdToast.simple().content("Slider=" + newValue).position("top right").hideDelay(1500));
});
In my case my HTML looked like this:
<md-slider min="0" max="50" ng-model="tex" md-discrete></md-slider>
Good Luck.
I put a callback function in ng-blur, that worked for me. (Tried ng-change at first, but that became very slow).
Example:
<md-slider ng-model="gigabyte" ng-blur="myCallbackFunction()" id="myslider"> </md-slider>
<input flex type="number" ng-model="gigabyte" ng-blur="myCallbackFunction()" aria-controls="myslider">
ng-mouseup should do the trick. If you want to add logic (as I did) to ensure your value has changed before make your db call to update, you can add that inside your saveChanges() function inside your controller.
<md-slider ng-mouseup="vm.saveChanges(vm.myModel)"></md-slider>

autofocus doesn't work when used with ng-include

I want to set focus to one of the input box in partial view
like .
and including this by
This is working fine when page loads for the first time. but when I change the partials autofocus doesn't work .
I believe it is because of autofocus work on pageload how can it make work here
I am not sure how to fix the problem of page reload but I think we can work out another solution here. I wrote a small directive once to conditionally set focus on an input element.
Here is the code:
function SetFocusDirective($parse) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var model = $parse(attrs.setFocus);
scope.$watch(model, function (value) {
if (value === true) {
element[0].focus();
}
});
element.bind('blur', function () {
scope.$apply(model.assign(scope, false));
});
}
};
}
SetFocusDirective.$inject = ['$parse'];
app.directive('setFocus', SetFocusDirective);
And here is how you can use it:
<input type="text" ng-model="firstName" set-focus="autofocusFirstName">
Where autofocusFirstName is a $scope variable which should have a boolean value.
So every time your partials load, all the directives inside them will get linked and do their jobs. If you end up using this directive, you should be able to achieve what you want.

how to pass a json as a string param to a directive

When I try to eval the below json form it gives me an error -
eval("{form: 'form' , nameToMatch: 'password1'}")
Why is the above form not valid ?
However the below works fine -
eval("{form: 'form'}")
I am trying to pass the above json as a string, as a param input to a directive.
Below is the html -
<input type="password" name="password2" ng-model="user.confirmPassword" placeholder="Confirm Password" match="{form: 'form', nameToMatch: 'password1'}" required="required"/>
Thanks,
Murtaza
Put parens around your json:
eval("({form: 'form' , nameToMatch: 'password1'})")
Doesn't seem like an angular question though. Not sure what you're trying to do:
Anyhow, to pass the json to the directive there are lots of ways to do that. I'm not sure why you'd want to do that and not just pass an object though.
passing json can be done a lot of ways...
From your attributes object:
app.directive('foo', function () {
return function(scope, element, attrs) {
var obj = eval('(' + attrs.foo + ')');
};
});
where
<div foo="{'test':'wee'}"></div>
From an isolated scope:
app.directive('foo', function () {
return {
restrict: 'E',
scope: {
'jsonIn' : '#'
},
link: function(scope, element, attrs) {
var obj = eval('(' + scope.jsonIn + ')');
};
};
});
where
<foo json-in="{'test':'wee'}"></foo>
But it's by far better to avoid using the native eval at all costs, if you can. Which in almost all cases you can. If you have some data just put it in an object on a scoped parameter and pass it in either via a two-way property on an isolated scope, or by name and do an angular $eval on it.
EDIT: The pass an object in...
You could use two way binding on an isolated scope:
app.directive('foo', function (){
return {
restrict: 'E',
scope: {
'data' : '='
},
link: function(scope, elem, attrs) {
console.log(scope.data);
}
};
});
where
<foo data="{ test: 'wee' }"></foo>
The really cool thing about doing it this way, is if you're using a scoped property it will update bi-directionally:
app.controller('MainCtrl', function($scope) {
$scope.bar = { id: 123, name: 'Bob' };
});
where
<foo data="bar"></foo>
I hope that helps.
It looks like you are trying to confirm a password in a form. There are many ways that you can go about this without resorting to JSON to pass values around in AngularJS. The most useful resource I've found online is from this Google Group thread:
1) http://jsfiddle.net/pkozlowski_opensource/GcxuT/23/ will compare
value in a second field with model value of the first field
2)
http://jsfiddle.net/S8TYF/ will compare value in a second field with
input value of the first field
The difference might be subtle but has practical consequences: with
(2) the confirm validation will kick-in as soon as you start typing
anything in the first field. With (1) the confirm validation will
kick-in only after the first field is valid. In the e-mail confirm
example it means that you won't start showing confirmation errors till
e-mail validation errors are sorted out (so a user can focus on one
error at the time).
Source: https://groups.google.com/d/msg/angular/R4QeNsNksdY/migbplv8GxIJ
From the first link, the directive is used as follows:
<label>e-mail</label>
<input name="email" type="email" required ng-model="email">
<label>repeat e-mail</label>
<input name="emailRepeat" type="email" required
ng-model="emailRepeat"
ui-validate-equals="email">
Where the ui-validate-equals directive points to the email model that was defined in the first input.
There are some StackOverflow answers to this if you would like to look there as well for additional ideas to solve your problem.
See also. #Blesh has mentioned in passing, but not emphasized: Angular provides you with a 'safe'-ish version of eval(), which works perfectly for passing declarative objects/arrays into your directive if you don't want to declare them on your scope, and wisely don't want to use native eval(). In the OP's example, just use this inside the directive:
angular.$eval(attrs.match);
Doesn't seem like an angular question though.
Agreed - vanilla JS can handle this just fine.
// Markup (HTML)
<div mydirective='{"test1": "foo", "test2": "bar"}'></div>
// App (JS)
JSON.parse(attrs['mydirective']);
Use JSON.parse(string) in angular. be sure that your parameter is in string format.

Resources