I've created a custom directive in AngularJS. The directive uses isolated scope, and it somehow prevents the binding for standard ngModel on the same element.
I want to create a confirm password field (text for readability in the example).
<input type="text" name="one" ng-model="fields.field_one">
<input type="text" validate-match="fields.field_one" name="two" ng-model="field_two">
My directive invalidates the field, when there is no match.
app.directive('validateMatch', function() {
return {
require: 'ngModel',
scope: { matchValue: '=validateMatch' },
link: function(scope, elm, attr, ctrl) {
scope.$watch('matchValue', function(value) {
ctrl.$setValidity('match',
ctrl.$viewValue === value
|| !ctrl.$viewValue && !value);
});
function validate(value) {
ctrl.$setValidity('match', value === scope.matchValue);
return value;
}
ctrl.$parsers.push(validate);
ctrl.$formatters.push(validate);
}
}
});
The thing is, why can't I change the value of that field by changing the model? First field works just fine.
Look at the plunker for details and commented code.
As mentioned in a comment, isolate scopes and ng-model don't mix well. Further, we shouldn't be using an isolate scope here, since we're trying to create a directive/component that needs to interact with another directive (ng-model in this case).
Since the validateMatch directive does not create any new properties, the directive does not need to create any new scope. $parse can be used to get the value of the property that attribute validate-match refers to:
app.directive('validateMatch', function($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elm, attr, ctrl) {
var model = $parse(attr.validateMatch);
// watch for linked field change (field_one)
scope.$watch(model, function(value) {
console.log('linked change:', value, ctrl.$viewValue);
// set valid if equal or both falsy (empty/undefined/null)
ctrl.$setValidity('match',
ctrl.$viewValue === value
|| !ctrl.$viewValue && !value);
});
// validate on parse/format (field_two)
function validate(value) {
var otherFieldValue = model(scope);
console.log('validate:', value, otherFieldValue);
// set valid if equal
ctrl.$setValidity('match', value === otherFieldValue);
return value;
}
ctrl.$parsers.push(validate);
ctrl.$formatters.push(validate);
}
};
});
plunker
Following Mark's suggestion, I managed to produce a work-around.
When isolated scope exists on the element, ngModel refers to it. The trick is to look the parent scope from within. You can either change the ngModel by hand (prepending it with $parent.), or automatize this process inside the directive by proper compile function.
This is how i did this:
compile: function(element, attrs, transclude) {
// reference parent scope, because isolated
// scopes are not looking up by default
attrs.$set('ngModel', '$parent.'+attrs.ngModel, false);
return function(scope, elm, attr, ctrl) {
// link function body there
}
}
For the full example, look at this plunk.
From what I understand of AngularJS Directives you can use the transclude parameter to access the parent scope of the controller.
Related
I'm building a directive that needs to initialize some data based on values passed into it through the scope. The problem is that when I try and initialize the data in the link function, the passed in value isn't available yet. Is there anyway to only run the initialization when the passed in value is available? I thought about using a watch as in the following code but it seems messy (and doesn't seem to work anyway).
.directive('etMemberActivitySummary', [function () {
return {
restrict: 'E',
templateUrl: '<div>My template</div>',
transclude: false,
scope: {
memberModel: '='
},
link: function(scope, element, attrs, controller) {
var watcher = scope.$watch(
function() {
return scope.memberModel
},
function(value) {
console.log(value);
if (value != null) {
console.log('Watch');
console.log(value);
watcher();
// Perform initialization based on scope.memberModel here
}
});
}
}
}])
Is there a correct way to do this? If it helps, the passed in value is in itself retrieved from a web service.
Update 1
Turns out that if I put an ng-if="ctrl.memberModel" on the directive usage like the following and get rid of all the watch stuff, it works. Is this the best way to do this?
<et-member-activity-summary member-model="ctrl.memberModel" ng-if="ctrl.memberModel"></et-member-activity-summary>
I have created a simple directive, called match that is used like:
<input match='pattern' />
The declaration line of my directive is:
app.directive('match', function () {
return {
restrict: 'A',
require: 'ngModel',
scope: {
pattern: '=match'
},
link: function (scope, element, attributes, ngModel) {
// doing stuff here
}
};
});
However, after a while I wanted to use BootstrapUI for angularjs, and as soon as I started using typeahead component, they encountered a problem on using the same scope:
Multiple directives [match, uibTypeaheadMatch] asking for new/isolated scope on
I need match, and typeahead together in one page. Typeahead is not under my control, and I don't want to change match's name.
What can I do to prevent their collision?
The problem is that both your directive and Typeahead directive, are asking for isolated scope on the same element and angular does not allow it.
To overcome this problem, define the directive in a different way:
app.directive('match', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attributes, ngModel) {
var match = attributes.match;
//do your stuff
}
};
});
I'm using ui-select plugin and I'm passing ng-model from my controller to a custom directive called richSelect but the ng-model doesn't seemed to get updated on select of any item.
<richselect ng-model="dataModel"></richselect>
Custom directive
app.directive('richselect', ['$compile', function ($compile) {
return {
restrict: 'AE',
scope: {
ngModel: '=' /* Model associated with the object */
},
link: function (scope, element, attrs, ngModel) {
scope.options = [
{
'Value' : 'value1',
'Desc' : 'Value One'
},
{
'Value' : 'value2',
'Desc' : 'Value Two'
}
]
scope.getRichSelectTemplate = function () {
return '<ui-select multiple ng-model="ngModel" theme="bootstrap" ng-disabled="disabled">' +
'{{ngModel}} <ui-select-match placeholder="Select">{{$select.selected.Desc}}</ui-select-match>' +
'<ui-select-choices repeat="option in options | filter: $select.search">' +
'<span ng-bind-html="option.Desc | highlight: $select.search"></span>' +
'</ui-select-choices>' +
'</ui-select>';
}
var linkFn = $compile(scope.getRichSelectTemplate())(scope);
element.append(linkFn);
}
}
}]);
Plnkr : http://plnkr.co/edit/Im8gpxEwnU7sgrKgqZXY?p=preview
Here, try this. I wasn't exactly sure what format or output you were trying to get, but this gets the selected options passed to the View.
EDIT - I got rid of the plunker that used to be here.
You have to use the ngModel.$setViewValue in order to change the value of ng-model in the directive in the view. Additionally, to get the value of the ui-select, you need to have ng-model pointed at the options.selected
Then it was just a matter of adding an ng-click that pointed to a function that updated the view with ngModel.$setViewValue(scope.options.selected.
Also, I believe you need to `require: 'ngModel' in your directive so you can access the ngModelController.
app.directive('richselect', ['$compile', function ($compile) {
return {
restrict: 'AE',
require: 'ngModel',
scope: {
blah: '=' /* Model associated with the object */
},
link: function (scope, element, attrs, ngModel) {
scope.changer = function() {
ngModel.$setViewValue(scope.options.selected)
console.log(scope.options.selected)
}
scope.options = [
{
'Value' : 'value1',
'Desc' : 'Value One'
},
{
'Value' : 'value2',
'Desc' : 'Value Two'
}
]
scope.getRichSelectTemplate = function () {
return '<ui-select multiple ng-model="options.selected" theme="bootstrap" ng-click="changer()" ng-disabled="disabled">' +
'{{options.selected}} <ui-select-match placeholder="Select">{{$select.selected.Desc}}</ui-select-match>' +
'<ui-select-choices repeat="option in options | filter: $select.search">' +
'<span ng-bind-html="option.Desc | highlight: $select.search"></span>' +
'</ui-select-choices>' +
'</ui-select>';
}
var linkFn = $compile(scope.getRichSelectTemplate())(scope);
element.append(linkFn);
}
}
}]);
EDIT:
After a lot of digging and tinkering, per the comment below - getting two-way binding working has proved somewhat elusive. I found it was quite easy to do using the standard ui-select directive, as seen here (modified example code from ui-select), because we can easily get access to the scope of the directive:
Standard Directive Demo
I also came across a similar wrapper as the one in the OP, but after playing with it,that one seemed to have the same issue - it's easy to get stuff out, but if you need to push data into the directive it doesn't want to go.
Interestingly, in my solution above, I can see that the `scope.options.selected' object actually contains the data, it just never gets down the the scope of the ui-select directive, and thus never allows us to push data in.
After encountering a similar issue with a different wrapper directive in a project I am working on, I figured out how to push data down through the different scopes.
My solution was to modify the ui-select script itself, adding an internal $watch function that checked for a variable in it's $parent scope. Since the ui-select directive uses scope: true, it creates a child scope (which, if I am not mistaken, the parent would be the directive in this OP).
Down at the bottom of the link function of the uiSelect directive I added the following watch function:
scope.$watch(function() {
return scope.$parent.myVar;
}, function(newVal) {
$select.selected = newVal;
})
In the link function of our directve here, I added this $watch function:
scope.$watch(function() {
return ngModel.$viewValue;
}, function(newVal) {
scope.myVar = newVal;
})
So what happens here is that if the $viewValue changes (i.e., we assign some data from a http service, etc. to the dataModel binding, the $watch function will catch it and assign it to scope.myVar. The $watch function inside the ui-select script watches scope.$parent.myVar for changes (We are telling it to watch a variable on the scope of it's parent). If it sees any changes it pushes them to $select.selected - THIS is where ui-select keeps whatever values that have been selected by clicking an item in the dropdown. We simply override that and insert whatever values we want.
Plunker - Two-way binding
First of all dataModel is a string. Since you defined multiple the model would be an array.
What's more important is that the uiSelectdirective creates a new scope. That means that ng-model="ngModel" does no longer point to dataModel. You effectively destroy the binding.
In your controller make dataModel an object:
$scope.dataModel = {};
In your directive let the selected values be bound to a property:
return '<ui-select multiple ng-model="ngModel.selection"
Now the the selected values will be bound to dataModel.selection.
If you don't use the ngModelController you shouldn't use ng-model with your directive.
Problem
Dynamically add the ng-bind attribute through a custom directive to be able to use ng-bind, ng-bind-html or ng-bind-html-unsafe in a custom directive with out manually adding to the template definition everywhere.
Example
http://jsfiddle.net/nstuart/hUxp7/2/
Broken Directive
angular.module('app').directive('bindTest', [
'$compile',
function ($compile) {
return {
restrict: 'A',
scope: true,
compile: function (tElem, tAttrs) {
if (!tElem.attr('ng-bind')) {
tElem.attr('ng-bind', 'content');
$compile(tElem)
}
return function (scope, elem, attrs) {
console.log('Linking...');
scope.content = "Content!";
};
}
};
}]);
Solution
No idea. Really I can not figure out why something like the above fiddle doesn't work. Tried it with and with out the extra $compile in there.
Workaround
I can work around it might adding a template value in the directive, but that wraps the content in an extra div, and I would like to be able to that if possible. (See fiddle)
Second Workaround
See the fiddle here: http://jsfiddle.net/nstuart/hUxp7/4/ (as suggested by Dr. Ikarus below). I'm considering this a workaround for right now, because it still feels like you should be able to modify the template before you get to the linking function and the changes should be found/applied.
You could do the compiling part inside the linking function, like this:
angular.module('app').directive('bindTest', ['$compile', function ($compile) {
return {
restrict: 'A',
scope: true,
link: {
post: function(scope, element, attrs){
if (!element.attr('ng-bind')) {
element.attr('ng-bind', 'content');
var compiledElement = $compile(element)(scope);
}
console.log('Linking...');
scope.content = "Content!";
}
}
};
}]);
Let me know how well this worked for you http://jsfiddle.net/bPCFj/
This way seems more elegant (no dependency with $compile) and appropriate to your case :
angular.module('app').directive('myCustomDirective', function () {
return {
restrict: 'A',
scope: {},
template: function(tElem, tAttrs) {
return tAttrs['ng-bind'];
},
link: function (scope, elem) {
scope.content = "Happy!";
}
};
});
jsFiddle : http://jsfiddle.net/hUxp7/8/
From Angular directive documentation :
You can specify template as a string representing the template or as a function which takes two arguments tElement and tAttrs (described in the compile function api below) and returns a string value representing the template.
The source code tells all! Check out the compileNodes() function and its use of collectDirectives().
First, collectDirectives finds all the directives on a single node. After we've collected all the directives on that node, then the directives are applied to the node.
So when your compile function on the bindTest directive executes, the running $compile() is past the point of collecting the directives to compile.
The extra call to $compile in your bindTest directive won't work because you are not linking the directive to the $scope. You don't have access to the $scope in the compile function, but you can use the same strategy in a link function where you do have access to the $scope
You guys were so close.
function MyDirective($compile) {
function compileMyDirective(tElement) {
tElement.attr('ng-bind', 'someScopeProp');
return postLinkMyDirective;
}
function postLinkMyDirective(iScope, iElement, iAttrs) {
if (!('ngBind' in iAttrs)) {
// Before $compile is run below, `ng-bind` is just a DOM attribute
// and thus is not in iAttrs yet.
$compile(iElement)(iScope);
}
}
var defObj = {
compile: compileMyDirective,
scope: {
someScopeProp: '=myDirective'
}
};
return defObj;
}
The result will be:
<ANY my-directive="'hello'" ng-bind="someScopeProp">hello</ANY>
I have an angular directive that is a custom attribute that possibly can contain a value like so:
<div my-directive="myVal"></div>
How do I read myVal (as a string) from inside my directive's link function?
The passed value is on the attributes object passed as the third argument to the link function. It's under a property matching the name of the directive.
app.directive('myDirective', function() {
return {
restrict: 'A',
link: function(scope, elem, attr) {
//read the passed value
alert(attr.myDirective);
}
}
});