I have an existing 3rd party directive for which I need to modify model and view values before they are shown resp. saved to the model. As I would like to avoid modifying external code, I implemented an additional directive which is set via attribute and which is about to modify the data through the $formatters and $parsers pipeline.
Basically something like this:
app.directive('myModifyingDirective', function() {
return {
require: 'ngModel',
restrict: 'A',
link: function(scope, element, attrs, ngModelController) {
ngModelController.$formatters.push(function(modelValue) {
return 'modified_' + modelValue;
});
// similar for $parsers
}
};
});
Markup looks something like:
<third-party-directive my-modifying-directive ng-model='data'></third-party-directive>`
The problem is, that third-party-directive also contributes to the $formatters, and at the end, the third-party-directives's formatter is last entry in the $formatters array, and thus executed before my-modifying-directive.
However, I require my-modifying-directive to be executed first.
Is there any mechanism how I could influence the order of the $parsers?
You can set the priority of the directive so that it is higher or lower than the priority of the 3rd party directive:
When there are multiple directives defined on a single DOM element, sometimes it is necessary to specify the order in which the directives are applied. The priority is used to sort the directives before their compile functions get called. Priority is defined as a number. Directives with greater numerical priority are compiled first. Pre-link functions are also run in priority order, but post-link functions are run in reverse order. The order of directives with the same priority is undefined. The default priority is 0.
Related
Trying to write a angular model that allows for a two way binding, I.E. where it has a ng-model variable, and if the controller updates it, it updates for the directive, if the directive updates it, it updates for the controller.
I have called the controller scope variable ng-model, and the directive model bindModel. In this case it sends an array of data, but that should not make a difference to how the binding works (I think at least).
So first the mapping, this is how the initial directive looks (link comes later).
return {
link: link,
scope: { cvSkills: '=',
bindModel:'=ngModel'},
restrict: 'E'
}
My understanding is (and I am uncertain about the exact scope at this moment) the directive will be aware of cvSkills (called cvSkills internally, and used to provide intial data), and bindModel which should pick up whats in ng-model).
The call in html is:
<skilltree cv-skills="cvskills" ng-model="CV._skillSet"></skilltree>
So the variables aren't actually (quite) in the directive yet. But there is an awareness of them, so I created two watchers (ignoring the cvskills one for now)
function link(scope,element, attr){
//var selected = ["55c8a069cca746f65c9836a3"];
var selected = [];
scope.$watch('bindModel', function(bindModel){
if (bindModel.length >> 0) {
console.log("Setting " + bindModel[0].skill)
selected = bindModel;
}
})
scope.$watch('cvSkills', function(cvSkills) {
So once the scope.$watch sees an update to bindModel, it picks it up and stores it to selected (same happens separately with cvSkills). In cvskills I then do updates to selected. So it will add additional data to selected if the user clicks buttons. All works and nothing special. My question is now. How do I then update bindModel (or ngModel) when there are updates to selected so that the controllers scope picks it up. Basically, how do I get the updates to propagate to ng-model="CV._skillSet"
And is this the right way to do it. I.E. make the scope aware, than pick up changes with scope.$watch and manually update the variable, or is the a more "direct" way of doing it?
=================== Fix using ngModel ===================
As per the article, if you add require: "ngModel" you get a fourth option to the function (and skip having a binding between ngModel and bindModel).
return {
link: link,
require: 'ngModel',
scope: { cvSkills: '='},
restrict: 'E'
}
Once you have done that, ngModel.viewValue will contain the data from ngModel= , in my case I am updating this in the code (which may be a bad idea). then call ngModel.setViewValue. It is probably safeish if you set the variable and then store it straight away (as follows)
function link(scope,element, attr, ngModel){
ngModel.$render = function() {
console.log("ngRender got called " + ngModel.$viewValue );
} ;
....
ngModel.$viewValue.push(newValue);
ngModel.$setViewValue(ngModel.$viewValue)
================= If you don't care about viewValue ==================
You can use modelValue instead of viewValue, and any updates to modelValue is propagated straight through.
function link(scope,element, attr, ngModel){
ngModel.$render = function() {
console.log("ngRender got called " + ngModel.$modelValue );
} ;
....
ngModel.$modelValue.push(newValue);
Using the require attribute of the directive you can have the controller API from ngModel directive. So you can update values.
Take a look at this very simple demo here : https://blog.hadrien.eu/2015/01/16/transformation-avec-ngmodel/
For more information here is the documentation : https://docs.angularjs.org/#!/api/ng/type/ngModel.NgModelController
I have a table that renders rows with ng-repeat. Inside one of the cells there is a select that is rendered with ng-options.
<tr ng-repeat="item in data.items" repeat-done>
<td >
...
<select class="selectpicker"
ng-model="person" ng-options="person.Surname for person in data.Persons track by person.Id">
<option value="">Introduce yourself...</option>
</select>
...
<td>
</tr>
When repeat is done I need to turn select into bootstrap-select (a nice looking dropdownlist). So after a little bit of researching I added the following directive:
app.directive('repeatDone', function () {
return function (scope, element, attrs) {
if (scope.$last) {
setTimeout(function() { $('.selectpicker').selectpicker(); }, 1);
};
};
});
which is specified at tr (see above).
It works. But I am a little bit worried whether there is a chance it will not work on slow PC/tablet/etc. As I understand AngularJS has an asynchronous nature. So while the last element of ng-repeat is being processed, it is still possible ng-options for this element (or may be for some previous element too) is not rendered. Or am I paranoiac?
You shouldn´t synchronize your directives using timeout. A lot of problems can appear.
You can use the option priority to sort when your directives are going to be used. Directives are executed on descendant order.
ngRepeat has priority 1000 (https://docs.angularjs.org/api/ng/directive/ngRepeat)
select has priority 0 (https://docs.angularjs.org/api/ng/directive/select)
If you declare your directive with a negative priority it will execute after ng-options.
app.directive('repeatDone', function () {
return {
priority: -5,
link: function (scope, element, attrs) {
$('.selectpicker').selectpicker();
}
}
});
According to Angular documentation:
priority
When there are multiple directives defined on a single DOM element,
sometimes it is necessary to specify the order in which the directives
are applied. The priority is used to sort the directives before their
compile functions get called. Priority is defined as a number.
Directives with greater numerical priority are compiled first.
Pre-link functions are also run in priority order, but post-link
functions are run in reverse order. The order of directives with the
same priority is undefined. The default priority is 0.
https://docs.angularjs.org/api/ng/service/$compile
I am trying to create a directive that will replace itself with the ng-pattern attribute. The attribute gets applied to the input element but the element basically becomes unusable after that. I can no longer enter characters into the text box.
Here is the plunkr
http://plnkr.co/edit/F6ZQYzxd8Y04Kz8xQmnZ?p=preview
I think I must be compiling the element incorrectly after the attribute is added.
app.directive('passwordPattern', ['$compile', function($compile){
return{
compile: function (element, attr){
element.removeAttr('password-pattern');
element.attr('ng-pattern', '/^[\\w\\S]{6,12}$/');
return {
pre: function preLink(scope, iElement, iAttrs, controller) { },
post: function postLink(scope, iElement, iAttrs, controller) {
$compile(iElement)(scope);
}
};
}
};
}]);
Any thoughts on a solution or why the textbox becomes unusable would be greatly apprecitated. Thanks.
In addition to priority: 1000, you need to add terminal: true.
The issue is that without terminal: true, the input directive gets compiled twice, and 2 sets of change listeners are getting added, which throws the ngModel directive logic off a bit.
The first compile Angular performs doesn't see the ng-pattern, so the input directive doesn't add the validateRegex parser to its list of parsers. However, the second compile (via your $compile(iElement, scope)) sees the ng-pattern and does add the validateRegex parser.
When you type, say 3, into the input box, the first change listener is called and sees the number 3. Since no ng-pattern was applied (which would've added the validateRegex $parser), no $parsers exist and the model is updated with 3 immediately.
However, when the second change listener is called, it sees the ng-pattern and calls validateRegex, which calls ngModel.$setValidity('pattern', false) and returns undefined (because the model should never be set to an invalid value). Here's the kicker - inside the ngModel directive, since the previous $viewValue of 3 and new value of undefined are out of sync, Angular calls the input directive's $render function, which updates the input to be empty. Thus when you type 3 (or anything) into the input box, it's immediately removed and appears to be broken.
A high priority (like 1000) and terminal: true will prevent the input directive (and most likely other directives unless you have one that's priority: 1001+) from being compiled the first time. This is great because you want the input directive to take into account ng-pattern - not without it in place. You don't want multiple sets of change listeners added to the same element or it may (and will) cause strange side-effects.
Another solution will be to override the pattern property of the $validators object in ngModel controller.
You can see an example of a validator function in ngModelController docs
Here's an example in a directive:
angular.module('mymodule')
.directive('mydirective', MyDirective);
function MyDirective() {
return {
restrict: 'A',
require: 'ngModel',
scope: {},
link: function(scope, element, attrs, ngModelController) {
ngModelController.$validators["pattern"] = validatePattern;
function validatePattern(modelValue, viewValue) {
var value = modelValue || viewValue;
return /[0-9]+/.test(value);
}
}
}
}
You can modify the above example to receive the pattern from the outside scope and change the validation function using a scope.$watch on the pattern.
Angular Documentation says: -
The compilation of the DOM is performed by the call to the $compile()
method. The method traverses the DOM and matches the directives. If a
match is found it is added to the list of directives associated with
the given DOM element. Once all directives for a given DOM element
have been identified they are sorted by priority and their
compile() functions are executed.
The ng-repeat directive I believe has a lower priority than custom directives, in certain use cases like dynamic id and custom directive. Does angular permit tinkering with priority of directives to choose execution of one before the other?
Yes, you can set the priority of a directive. ng-repeat has a priority of 1000, which is actually higher than custom directives (default priority is 0). You can use this number as a guide for how to set your own priority on your directives in relation to it.
angular.module('x').directive('customPriority', function() {
return {
priority: 1001,
restrict: 'E',
compile: function () {
return function () {...}
}
}
})
priority - When there are multiple directives defined on a single DOM element, sometimes it is necessary to specify the order in which the directives are applied. The priority is used to sort the directives before their compile functions get called. Priority is defined as a number. Directives with greater numerical priority are compiled first. The order of directives with the same priority is undefined. The default priority is 0.
AngularJS finds all directives associated with an element and processes it. This option tells angular to sort directives by priority so a directive having higher priority will be compiled or linked before others. The reason for having this option is that we can perform conditional check on the output of the previous directive compiled.
In the followed example, first add button and only after add class to current button:
Demo Fiddle
App.directive('btn', function() {
return {
restrict: 'A',
priority: 1,
link: function(scope, element, attrs) {
element.addClass('btn');
}
};
});
App.directive('primary', function() {
return {
restrict: 'A',
priority: 0,
link: function(scope, element, attrs) {
if (element.hasClass('btn')) {
element.addClass('btn-primary');
}
}
};
});
Related: How to understand the `terminal` of directive?
Why would someone set terminal: true and a priority on a directive rather than simply removing the lower priority directives? For example, they could write:
<tag directive-1 directive-2 directive-3></tag>
... and they could add priority: 100 and terminal: true to directive-3, so that only directive-3 would be applied to the element.
Why wouldn't someone instead change their template to:
<tag directive-3></tag>
Perhaps it simplifies the code in some cases by allowing multiple directives to be added to an element and offloading the work of deciding which ones to actually apply to Angular?
Thanks.
Setting the priority and terminal options is not about erasing directives, it's declaring the order of compilation and linking. Everybody points to ng-repeat as the prime example of priority + terminal + transclude, so I'll give a extremely simplified version of ng-repeat:
app.directive('fakeRepeat', function($log) {
return {
priority: 1000,
terminal: true,
transclude: 'element',
compile: function(el, attr, linker) {
return function(scope, $element, $attr) {
angular.forEach(scope.$eval($attr.fakeRepeat).reverse(), function(x) {
var child = scope.$new();
child[attr.binding] = x;
linker(child, function(clone) {
$element.after(clone);
})
})
}
}
}
});
The fake repeat directive can be used as so:
<ul>
<li fake-repeat="things" binding="t" add-letter>{{ t }}</li>
<ul>
Now extra directives can be attached to the same li that contains fake repeat, but their priority + terminal options will determine who gets compiled first, and when linking happens. Normally we expect the li element to be cloned and for and for the add-letter directive to be copied for each binding t, but that will only happen if add-letter
has a lower priority than fake-repeat.
Case 1: add-letter priority is 0
Linking is executed for each li generated.
Case 2: add-letter priority is 1001
Linking is executed before fake-repeat and thus before the transclude happens.
Case 3: add-letter priority is 1001 and terminal is true
Compilation stops before fake-repeat so the directive is never executed.
Here is a plunker with console logging for further exploration.
I believe terminal was created to work with directives that use transclusion or directives that are meant to replace all of an element's content.
If an element uses terminal then it does not want applicable directives to compile during the initial directive collection. The initial collection is triggered either by Angular's bootstrap process or a manual $compile. Just because the terminal directive does not want the lower priority directives to compile, does not mean that it does not want the directives to run later, which is why transclude is a perfect use case.
The contents are compiled and stored as a link function that can be evaluated against any scope at any time. This is how ngRepeat, and ngIf perform.
When writing a directive that uses transclusion maybe consider if it should use terminal as well.
I don't believe it's very useful when using it with directives that don't use transclude.