My overall aim is to componentize all our re-usable widgets as angular directives but am struggling when trying to make an angular form error message directive. I have read numerous posts but couldn't see how to achieve this.
All my directives have isolate scope.
My main issue is that I do not know how to get the pattern attribute to bind correctly to the ng-show attribute of the at-error-message directives span element so that the error message dynamically hides\shows based on the pattern.
The outer HTML is:
<body ng-controller="atVrmLookup">
<ng-form name="vrmForm" novalidate>
<at-input name="registration" label="Registration" required model="vrmLookup.registration" minlength="3"></at-input>
<at-error-message pattern="vrmForm.registration.$error.required" message="Please enter a registration"></at-error-message>
</ng-form>
</body>
The atInput directive is
uiComponents.directive('atInput', function () {
return {
// use an inline template for increased
template: '<div><label>{{label}}</label><div><input name="{{name}}" type="text" ng-model="model" ng-minlength="{{minlength}}"/></div></div>',
// restrict directive matching to elements
restrict: 'E',
scope: {
name: '#',
label: '#',
minlength: '#',
model:'=model'
},
compile: function (element, attr, scope) {
var input = element.find('input');
if (!_.isUndefined(attr.required)) {
input.attr("required", "true");
}
},
controller: function ($scope, $element, $attrs) {
// declare some default values
}
};
});
the atErrorMessageDirective is
uiComponents.directive('atErrorMessage', function () {
return {
// use an inline template for increased
template: '<span class="error" ng-show="pattern">{{message}}</span>',
// restrict directive matching to elements
restrict: 'E',
scope: {
message: '#',
pattern: '='
},
controller: function ($scope, $element, $attrs) {
// declare some default values
}
};
});
Here is plunkr to demonstrate the issue.
http://plnkr.co/edit/5tdsqSXg0y5bfQqJARFB
Any help would be appreciated.
The input name cannot be an angular binding expression. Instead, use a template function, and build your template string:
template: function($element, $attr) {
return '<div><label>{{label}}</label><div>' +
'<input name="' + $attr.name + '" type="text" ng-model="model" ng-minlength="{{minlength}}"/>' +
'</div></div>';
}
Alternate Solution
Implement a dynamic-name directive, and use the API exposed by the angular form directive to programmatically set the name of the ngModel, and add the ngModel control to the form:
.directive("dynamicName",[function(){
return {
restrict:"A",
require: ['ngModel', '^form'],
link:function(scope,element,attrs,ctrls){
ctrls[0].$name = scope.$eval(attrs.dynamicName) || attrs.dynamicName;
ctrls[1].$addControl(ctrls[0]);
}
};
}])
Then in your template:
template: '<div><label>{{label}}</label><div><input dynamic-name="{{name}}" type="text" ng-model="model" ng-minlength="{{minlength}}"/></div></div>',
Note: This solution works because fortunately, ngForm provides programmatic access to customize its behavior. If ngForm's controller had not exposed an API, then you might have been SOL. It's good practice to think of the API you expose from your own custom directive's controllers - you never know how it can be used by other directives.
Related
How do I create an angular directive that adds a input in a form but also works with form validation.
For instance the following directive creates a text input:
var template = '\
<div class="control-group" ng-class="{ \'error\': {{formName}}[\'{{fieldName}}\'].$invalid}">\
<label for="{{formName}}-{{fieldName}} class="control-label">{{label}}</label>\
<div class="controls">\
<input id="{{formName}}-{{fieldName}}" name="{{fieldName}}" type="text" ng-model="model" />\
</div>\
</div>\
';
angular
.module('common')
.directive('formTextField', ['$compile', function ($compile) {
return {
replace: true,
restrict: 'E',
scope: {
model: '=iwModel',
formName: '#iwFormName',
fieldName: '#iwFieldName',
label: '#iwLabel'
},
transclude: false,
template: template
};
}])
;
However the input is not added to the form variable ($scope.formName). I've been able to work around that by using the dynamicName directive from the following stackoverflow question Angularjs: form validation and input directive but ng-class will still not work.
Update
It now seems to be working but it feels like a hack. I thought I needed the scope to grab the form and field names however I can read that directly from the attributes. Doing this shows the field in the controllers scope form variable. However the ng-class attribute does not apply. The only way around this is to add the html element a second time once the scope is available.
jsFiddle here
var template = '\
<div class="control-group">\
<label class="control-label"></label>\
<div class="controls">\
<input class="input-xlarge" type="text" />\
</div>\
</div>\
';
angular
.module('common')
.directive('formTextField', ['$compile', function ($compile) {
return {
replace: true,
restrict: 'E',
scope: false,
compile: function compile(tElement, tAttrs, transclude) {
var elem = $(template);
var formName = tAttrs.iwFormName;
var fieldName = tAttrs.iwFieldName;
var label = tAttrs.iwLabel;
var model = tAttrs.iwModel;
elem.attr('ng-class', '{ \'error\': ' + formName + '[\'' + fieldName + '\'].$invalid }');
elem.find('label').attr('for', formName + '-' + fieldName);
elem.find('label').html(label);
elem.find('input').attr('id', formName + '-' + fieldName);
elem.find('input').attr('name', fieldName);
elem.find('input').attr('ng-model', model);
// This one is required so that angular adds the input to the controllers form scope variable
tElement.replaceWith(elem);
return {
pre: function preLink(scope, iElement, iAttrs, controller) {
// This one is required for ng-class to apply correctly
elem.replaceWith($compile(elem)(scope));
}
};
}
};
}])
;
when I do something like this, I use the directive compile function to build my html prior to it being processed. For example:
myApp.directive('specialInput', ['$compile', function($compile){
return {
// create child scope for control
scope: true,
compile: function(elem, attrs) {
// use this area to build your desired dom object tree using angular.element (eg:)
var input = angular.element('<input/>');
// Once you have built and setup your html, replace the provided directive element
elem.replaceWith(input);
// Also note, that if you need the regular link function,
// you can return one from here like so (although optional)
return function(scope, elem, attrs) {
// do link function stuff here
};
}
};
}]);
So, have created a few angular directives -- picture "user controls" around common data entry elements, like a label-textbox pair, etc
The problem we are having is that the zValidate directive does not seem to work from within the directive. Is there something we need to do to make nested directives work?
Edit
Here are the relevant code snippets.
So, first, we have a little directive that adds a label-input pair:
app.directive('afLabelInputPair', function ($compile) {
var directive = {
restrict: 'A',
transclude: true,
replace: true,
scope: { //#textValue =twoWayBinding &oneWayBinding
labelText: '#labelText',
afModel: '=',
afId: '#',
afPlaceholder: '#'
},
templateUrl: './app/templates/af-label-input-pair.html',
link: function (scope, element, attrs) {
scope.opts = attrs;
$compile(element.contents())(scope);
}
}
return directive;
});
Next, we have the template html (this is what's returned from templateUrl:
<div class="form-group">
<label class="control-label" for="{{afId}}">{{labelText}}</label>
<input id="{{afId}}" class="form-control" ng-model="afModel" placeholder="{{afPlaceholder}}" data-z-validate />
</div>
But, we don't get a display of breeze validation errors when we use this directive.
I have a directive that can be used multiple times on a page. In the template of this directive, I need to use IDs for an input-Element so I can "bind" a Label to it like so:
<input type="checkbox" id="item1" /><label for="item1">open</label>
Now the problem is, as soon as my directive is included multiple times, the ID "item1" is not unique anymore and the label doesn't work correctly (it should check/uncheck the checkbox when clicked).
How is this problem fixed? Is there a way to assign a "namespace" or "prefix" for the template (like asp.net does with the ctl00...-Prefix)? Or do I have to include an angular-Expression in each id-Attribute which consists of the directive-ID from the Scope + a static ID. Something like:
<input type="checkbox" id="{{directiveID}} + 'item1'" /><label for="{{directiveID}} + 'item1'">open</label>
Edit:
My Directive
module.directive('myDirective', function () {
return {
restrict: 'E',
scope: true,
templateUrl: 'partials/_myDirective.html',
controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
...
} //controller
};
}]);
My HTML
<div class="myDirective">
<input type="checkbox" id="item1" /><label for="item1">open</label>
</div>
HTML
<div class="myDirective">
<input type="checkbox" id="myItem_{{$id}}" />
<label for="myItem_{{$id}}">open myItem_{{$id}}</label>
</div>
UPDATE
Angular 1.3 introduced a native lazy one-time binding. from the angular expression documentation:
One-time binding
An expression that starts with :: is considered a
one-time expression. One-time expressions will stop recalculating once
they are stable, which happens after the first digest if the
expression result is a non-undefined value (see value stabilization
algorithm below).
Native Solution:
.directive('myDirective', function() {
var uniqueId = 1;
return {
restrict: 'E',
scope: true,
template: '<input type="checkbox" id="{{::uniqueId}}"/>' +
'<label for="{{::uniqueId}}">open</label>',
link: function(scope, elem, attrs) {
scope.uniqueId = 'item' + uniqueId++;
}
}
})
Only bind once:
If you only need to bind a value once you should not use bindings ({{}} / ng-bind)
bindings are expensive because they use $watch. In your example, upon every $digest, angular dirty checks your IDs for changes but you only set them once.
Check this module: https://github.com/Pasvaz/bindonce
Solution:
.directive('myDirective', function() {
var uniqueId = 1;
return {
restrict: 'E',
scope: true,
template: '<input type="checkbox"/><label>open</label>',
link: function(scope, elem, attrs) {
var item = 'item' + uniqueId++;
elem.find('input').attr('id' , item);
elem.find('label').attr('for', item);
}
}
})
We add a BlockId parameter to the scope, because we use the id in our Selenium tests for example. There is still a chance of them not being unique, but we prefer to have complete control over them. Another advantage is that we can give the item a more descriptive id.
Directive JS
module.directive('myDirective', function () {
return {
restrict: 'E',
scope: {
blockId: '#'
},
templateUrl: 'partials/_myDirective.html',
controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
...
} //controller
};
}]);
Directive HTML
<div class="myDirective">
<input type="checkbox" id="{{::blockId}}_item1" /><label for="{{::blockId}}_item1">open</label>
</div>
Usage
<my-directive block-id="descriptiveName"></my-directive>
Apart from Ilan and BuriB's solutions (which are more generic, which is good) I found a solution to my specific problem because I needed IDs for the "for" Attribute of the label. Instead the following code can be used:
<label><input type="checkbox"/>open</label>
The following Stackoverflow-Post has helped:
https://stackoverflow.com/a/14729165/1288552
I'm trying to create a custom component that uses a dynamic ng-model inside-out the directive.
As an example, I could invoke different components like:
<custom-dir ng-model="domainModel1"></custom-dir>
<custom-dir ng-model="domainModel2"></custom-dir>
With a directive like:
app.directive('customDir', function() {
return {
restrict: 'EA',
require: '^ngModel',
scope: {
ngModel: '=dirValue',
},
template: '<input ng-model="dirValue" />',
link: function(scope, element, attrs, ctrl) {
scope.dirValue = 'New';
}
};
});
The idea is that the textbox from the directive would change if the model changes, and in the other way around.
The thing is that I've tried different approaches with no success at all, you can check one of this here: http://plnkr.co/edit/7MzDJsP8ZJ59nASjz31g?p=preview In this example, I'm expecting to have the value 'New' in both of the inputs, since I'm changing the model from the directive and is a bi-directional bound (=). But somehow is not bound in the right way. :(
I will be really grateful if someone give some light on that. Thanks in advance!
Something like this?
http://jsfiddle.net/bateast/RJmhB/1/
HTML:
<body ng-app="test">
<my-dir ng-model="test"></my-dir>
<input type="text" ng-model="test"/>
</body>
JS:
angular.module('test', [])
.directive('myDir', function() {
return {
restrict: 'E',
scope: {
ngModel: '='
},
template: '<div><input type="text" ng-model="ngModel"></div>',
};
});
I’ve create a custom directive that contains a select input field.
I’m using ng-options to populate the select options and I'm currently passing in the data for the options using an options attribute bound to an isolate scope. See below.
<script>
recManagerApp.directive(myDirective, function () {
return {
restrict: 'E',
templateUrl: '/templates/directives/mydirective.html',
scope: {
mySelectedValue: "=",
options : "="
}
};
});
</script>
<my-directive my-selected-value="usersValue" options="myDataService.availbleOptions"></my-directive>
<div>
<select data-ng-model="mySelectedValue" data-ng-options="item for item in options">
<option value="">Select something</option>
</select>
</div>
The above works as expected, correctly populates the options, selects the correct value and has two-way binding to the property in the parent scope.
However, I’d rather not pass in the options using an attribute on the my-directive element and instead inject in a service (myDataService) that can provide the data for the ng-options. However, when I try this (various ways) no options are created, despite the service being injected correctly and the data being available. Can anyone suggest a way to do this?
recManagerApp.directive(myDirective, function (myDataService) {
return {
restrict: 'E',
templateUrl: '/templates/directives/mydirective.html',
scope: {
mySelectedValue: "=",
options : myDataService.availableOptions
}
};
});
Thanks
Mat
In my opinion, you have several options (as pointed out in the comments):
1. create controller for the directive
In you directive's template, use a controller, i.e.
<div ng-controller="SelectController">
<!-- your select with the ngOptions -->
</div>
and create the SelectController as a regular controller:
var app = angular.module("app.controllers", [])
app.controller("SelectController", ['$scope', 'myDataService', function(scope, service) {
scope.options = service.whatEverYourServiceDoesToProvideThis()
}]);
You can also give your directive a controller, which works just the same:
recManagerApp.directive(myDirective, function () {
return {
restrict: 'E',
templateUrl: '/templates/directives/mydirective.html',
scope: {
mySelectedValue: "=",
},
controller: ['$scope', 'myDataService', function(scope, service) {
scope.options = service.whatEverYourServiceDoesToProvideThis()
}]
};
});
2. injecting it into the directive and using it within link
recManagerApp.directive(myDirective, function (myDataService) {
return {
restrict: 'E',
templateUrl: '/templates/directives/mydirective.html',
scope: {
mySelectedValue: "="
},
link: function(scope) {
scope.options = myDataService.whatEverYourServiceDoesToProvideThis()
}
};
});