Unable to get the resolved attributes within custom directive - angularjs

I have been trying to write a custom directive for an input field with dynamic id, in the directive am unable to get the correct id.
<input id="myInput{{$index}}" my-dir="fn()"/>
myApp.directive('myDir', function ($parse) {
var obj = {
require: "ngModel",
link: {
post: function (scope, element, attrs) {
var fn = $parse(attrs.myDir);
var elementId = element.attr('id');
console.log(elementId); // Here I see myInput{{$index}} instead of myInput0, by this time angular is not resolving the value
}
}
};
return obj;
});
My question would be, how can I get the resolved value in the directive. Also I cannot use any isolated scope here due to other reasons.
Thanks in advance

You can use $observe to observe the value changes of attributes that contain interpolation (e.g. src="{{bar}}"). Not only is this very efficient but it's also the only way to easily get the actual value because during the linking phase the interpolation hasn't been evaluated yet and so the value is at this time set to undefined.
post: function (scope, element, attrs) {
attrs.$observe('id', function (id) {
console.log(id)
})
}

If you only want to evaluate the value once in the link function, you can use $interpolate (remember to inject it into your directive):
console.log($interpolate(element.attr('id'))(scope));
However, since you are likely using ng-repeat (because of the use of $index) I prefer #sza's answer, since your list may change, hence you may need to react to changes to your list.

Related

How to implement an ng-change for a custom directive

I have a directive with a template like
<div>
<div ng-repeat="item in items" ng-click="updateModel(item)">
<div>
My directive is declared as:
return {
templateUrl: '...',
restrict: 'E',
require: '^ngModel',
scope: {
items: '=',
ngModel: '=',
ngChange: '&'
},
link: function postLink(scope, element, attrs)
{
scope.updateModel = function(item)
{
scope.ngModel = item;
scope.ngChange();
}
}
}
I would like to have ng-change called when an item is clicked and the value of foo has been changed already.
That is, if my directive is implemented as:
<my-directive items=items ng-model="foo" ng-change="bar(foo)"></my-directive>
I would expect to call bar when the value of foo has been updated.
With code given above, ngChange is successfully called, but it is called with the old value of foo instead of the new updated value.
One way to solve the problem is to call ngChange inside a timeout to execute it at some point in the future, when the value of foo has been already changed. But this solution make me loose control over the order in which things are supposed to be executed and I assume that there should be a more elegant solution.
I could also use a watcher over foo in the parent scope, but this solution doesn't really give an ngChange method to be implmented and I have been told that watchers are great memory consumers.
Is there a way to make ngChange be executed synchronously without a timeout or a watcher?
Example: http://plnkr.co/edit/8H6QDO8OYiOyOx8efhyJ?p=preview
If you require ngModel you can just call $setViewValue on the ngModelController, which implicitly evaluates ng-change. The fourth parameter to the linking function should be the ngModelCtrl. The following code will make ng-change work for your directive.
link : function(scope, element, attrs, ngModelCtrl){
scope.updateModel = function(item) {
ngModelCtrl.$setViewValue(item);
}
}
In order for your solution to work, please remove ngChange and ngModel from isolate scope of myDirective.
Here's a plunk: http://plnkr.co/edit/UefUzOo88MwOMkpgeX07?p=preview
tl;dr
In my experience you just need to inherit from the ngModelCtrl. the ng-change expression will be automatically evaluated when you use the method ngModelCtrl.$setViewValue
angular.module("myApp").directive("myDirective", function(){
return {
require:"^ngModel", // this is important,
scope:{
... // put the variables you need here but DO NOT have a variable named ngModel or ngChange
},
link: function(scope, elt, attrs, ctrl){ // ctrl here is the ngModelCtrl
scope.setValue = function(value){
ctrl.$setViewValue(value); // this line will automatically eval your ng-change
};
}
};
});
More precisely
ng-change is evaluated during the ngModelCtrl.$commitViewValue() IF the object reference of your ngModel has changed. the method $commitViewValue() is called automatically by $setViewValue(value, trigger) if you do not use the trigger argument or have not precised any ngModelOptions.
I specified that the ng-change would be automatically triggered if the reference of the $viewValue changed. When your ngModel is a string or an int, you don't have to worry about it. If your ngModel is an object and your just changing some of its properties, then $setViewValue will not eval ngChange.
If we take the code example from the start of the post
scope.setValue = function(value){
ctrl.$setViewValue(value); // this line will automatically evalyour ng-change
};
scope.updateValue = function(prop1Value){
var vv = ctrl.$viewValue;
vv.prop1 = prop1Value;
ctrl.$setViewValue(vv); // this line won't eval the ng-change expression
};
After some research, it seems that the best approach is to use $timeout(callback, 0).
It automatically launches a $digest cycle just after the callback is executed.
So, in my case, the solution was to use
$timeout(scope.ngChange, 0);
This way, it doesn't matter what is the signature of your callback, it will be executed just as you defined it in the parent scope.
Here is the plunkr with such changes: http://plnkr.co/edit/9MGptJpSQslk8g8tD2bZ?p=preview
Samuli Ulmanen and lucienBertin's answers nail it, although a bit of further reading in the AngularJS documentation provides further advise on how to handle this (see https://docs.angularjs.org/api/ng/type/ngModel.NgModelController).
Specifically in the cases where you are passing objects to $setViewValue(myObj). AngularJS Documentatation states:
When used with standard inputs, the view value will always be a string (which is in some cases parsed into another type, such as a Date object for input[date].) However, custom controls might also pass objects to this method. In this case, we should make a copy of the object before passing it to $setViewValue. This is because ngModel does not perform a deep watch of objects, it only looks for a change of identity. If you only change the property of the object then ngModel will not realize that the object has changed and will not invoke the $parsers and $validators pipelines. For this reason, you should not change properties of the copy once it has been passed to $setViewValue. Otherwise you may cause the model value on the scope to change incorrectly.
For my specific case, my model is a moment date object, so I must clone the object first before then calling setViewValue. I am lucky here as moment provides a simple clone method: var b = moment(a);
link : function(scope, elements, attrs, ctrl) {
scope.updateModel = function (value) {
if (ctrl.$viewValue == value) {
var copyOfObject = moment(value);
ctrl.$setViewValue(copyOfObject);
}
else
{
ctrl.$setViewValue(value);
}
};
}
The fundamental issue here is that the underlying model does not get updated until the digest cycle that happens after scope.updateModel has finished executing. If the ngChange function requires details of the update that is being made then those details can be made available explicitly to ngChange, rather than relying on the model updating having been previously applied.
This can be done by providing a map of local variable names to values when calling ngChange. In this scenario, you can mapping the new value of the model to a name which can be referenced in the ng-change expression.
For example:
scope.updateModel = function(item)
{
scope.ngModel = item;
scope.ngChange({newValue: item});
}
In the HTML:
<my-directive ng-model="foo" items=items ng-change="bar(newValue)"></my-directive>
See: http://plnkr.co/edit/4CQBEV1S2wFFwKWbWec3?p=preview

Use scope in filter angularjs

I'm trying to create filter that will watch safe and try to use $compile on variable
Right now I can use this with no problem
.directive('safe', ['$compile',
function ($compile) {
return function (scope, element, attrs) {console.log(scope, element, attrs)
scope.$watch(
function (scope) {
// watch the 'safe' expression for changes
return scope.$eval(attrs.bindUnsafeHtml);
},
function (value) {
// when the 'safe' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
}
);
};
}
])
The usage for above code in the html file is
<span safe="'_agree_to_terms_and_conditions_'">
Now I want to be able to do something like this instead
{{ gettext("_agree_to_terms_and_conditions_" | safe }}
Now when I try to create filter I don't have scope available in there...
Is it even possible to acheive something like above?
PS. The name of the filter needs to match exactly like the given example since I'm trying to create a common usage of the function with different app for i18n.
This seems like a bad idea. From a filter, you wouldn't want to try and modify the $scope in any ways. You'll probably get errors about being in a $digest cycle already. But it is possible to pass things into the filter.
{{ gettext("_agree_to_terms_and_conditions_" | safe:this }}
app.filter('safe', function () {
return function (input, scope) {
//scope will be *this* that you passed in from the HTML
};
});
Just found an article where this has been asked before: Access scope variables from a filter in AngularJS. They also give similar warnings about not trying to modify the $scope.

Is it possible to provide an implicit way of converting object into string for angular templates?

Let's assume I've got a few objects that have the same prototype and I want to customize their display in Angular template. I know I can create my own filter, and then use it like that:
<p>{{anObjectOfProtoP | myCustomFilter}}</p>
or a function attached to $scope:
<p>{{myCustomFunction(anotherObjectOfProtoP)}}</p>
My question is: is it possible to achieve similar functionality without explicitly specifying the rendering function every time? The ideal solution would be if angular checked for function toAngularString on object inside the {{}}, and then used it's return value in template.
In other words, I'd like Angular to do
function (o) {
if (typeof o.toAngularString === 'function') return o.toAngularString();
return o;
}
on every object inside {{}}.
Depending on whether you use {{ ... }} or ng-bind syntax, the .toJSON and the .toString function on your object will be called to determine its representation. Hence, you can provide the representation you want in either .toString or .toJSON function of your object.
This discrepancy in which function is called has let to some problems, actually.
Another way you can do that is by writing your own directive my-toangularstr as this:
app.directive('myToangularstr', function () {
return {
scope: true,
template: '<span class="my-angular-value">{{ val.toAngularString() }}</span>',
link: function (scope, elem, attrs) {
scope.$watch(attrs['myToangularstr'], function (newVal) {
if (typeof newVal !== 'undefined') {
scope.val = newVal;
}
})
}
}
})
A working demo showing all three methods is here.
I think that is as close as one can get using the external API of angular.

how to set an interpolated value in angular directive?

How do I set the interpolated value in a directive? I can read the correct value from the following code, but I have not been able to set it.
js:
app.directive('ngMyDirective', function () {
return function(scope, element, attrs) {
console.log(scope.$eval(attrs.ngMyDirective));
//set the interpolated attrs.ngMyDirective value somehow!!!
}
});
html:
<div ng-my-directive="myscopevalue"></div>
where myscopevalue is a value on my controller's scope.
Whenever a directive does not use an isolate scope and you specify a scope property using an attribute, and you want to change that property's value, I suggest using $parse. (I think the syntax is nicer than $eval's.)
app.directive('ngMyDirective', function ($parse) {
return function(scope, element, attrs) {
var model = $parse(attrs.ngMyDirective);
console.log(model(scope));
model.assign(scope,'Anton');
console.log(model(scope));
}
});
fiddle
$parse works whether or not the attribute contains a dot.
If you want to set a value on the scope but don't know the name of the property (ahead of time), you can use object[property] syntax:
scope[attrs.myNgDirective] = 'newValue';
If the string in the attribute contains a dot (e.g. myObject.myProperty), this won't work; you can use $eval to do an assignment:
// like calling "myscopevalue = 'newValue'"
scope.$eval(attrs.myNgDirective + " = 'newValue'");
[Update: You should really use $parse instead of $eval. See Mark's answer.]
If you're using an isolate scope, you can use the = annotation:
app.directive('ngMyDirective', function () {
return {
scope: {
theValue: '=ngMyDirective'
},
link: function(scope, element, attrs) {
// will automatically change parent scope value
// associated by the variable name given to `attrs.ngMyDirective`
scope.theValue = 'newValue';
}
}
});
You can see an example of this in this Angular/jQuery color picker JSFiddle example, where assigning to scope.color inside the directive automatically updates the variable passed into the directive on the controller's scope.

Adding a directive to an existing element

I am attemping to add the required directive to an element, at some point in the future.
In the example, its if the model field is dirty, then make the element required.
I have attempted to just set the required attribute (being a little optimistic)
I am now compiling and linking the element and attempting to replace the old elemenet with the new element.
My element just disappears from the page?
Am I going about this the right way?
app.directive('requiredIfDirty', function ($compile, $timeout) {
return {
restrict: "A",
require: // element must have ng-model attribute.
'ngModel',
link: // scope = the parent scope
// elem = the element the directive is on
// attr = a dictionary of attributes on the element
// ctrl = the controller for ngModel.
function (scope, elem, attr, ctrl) {
var unsubscribe = scope.$watch(attr.ngModel, function (oldValue, newValue) {
if(angular.isUndefined(oldValue)) {
return;
}
attr.$set("required", true);
$timeout(function () {
var newElement = $compile(elem)(scope);
elem.replaceWith(newElement);
}, 1);
unsubscribe();
});
}
};
});
You would have to use Transclusion in your directive. This would allow you to yank your content, append required to it and then compile that. This is a great tutorial that explains the basic concept: Egghead.io - AngularJS - Transclusion Basics
You dont actually need to do that. Angular actually has a directive ng-required
see
http://docs.angularjs.org/api/ng.directive:input.text
You can provide an expression into ng-required on any field that has ng-model and it will add the required validator to it based on the expression evaluating to true.
From the docs
ngRequired(optional) – {string=} – Adds required attribute and
required validation constraint to the element when the ngRequired
expression evaluates to true. Use ngRequired instead of required when
you want to data-bind to the required attribute.

Resources