ng-repeat with compile or link? - angularjs

I'm trying to get forms with fields created from config, with validation and other fine goods.
Here: http://plnkr.co/edit/8cP5YMKUGu6LfBCsRxAZ?p=preview is a solution, which works, but when I try to go step more - to add logic e.g (if attrs.hasOwnProperty('required') then add something to template) - i'm stucked. (to be exact - in this plunker i'd like to remove all staff connected to remove and add it only in case if field-required is true.
So I THINK (but may be wrong) that I have to use some link or compile function which prepares the template for each field.
So I produce sth like this:
KPNDirectives.directive("formField", function () {
return {
restrict: 'E',
replace:true,
scope: {
fieldModel : '=ngModel'
},
link: function (scope, element, attrs) {
var type = attrs.fieldType || 'text'
var htmlText =
'<div class="form-group" ng-form="form" ng-class="{true: \'has-error\', false: \'has-success\'}[form.'+attrs.fieldName+'.$invalid]">'+
'<label class="control-label col-sm-2" for="'+attrs.fieldName+'">'+attrs.fieldLabel+'</label>'+
'<div class="col-sm-6">'+
'<input class="form-control" type="'+type+'" placeholder="enter valid name" name="'+attrs.fieldName+'" ng-model="'+scope.fieldModel+'" ng-minlength=3 ng-maxlength=20 required/>'+
'<span class="help-block">'+
'<span ng-show="form.'+attrs.fieldName+'.$error.required">Required field</span>'+
'<span ng-show="form.'+attrs.fieldName+'.$error.minlength">Too few chars - min is (6)</span>'+
'<span ng-show="form.'+attrs.fieldName+'.$error.maxlength">Too much chars - max is (20)</span>'+
' '+
'</span>'+
'</div>'+
'</div>';
element.replaceWith(htmlText);
}
}
});
But it doesn't work.
Here's plunker http://plnkr.co/edit/OZMuxzsnoVmATpeTdSW9?p=preview

Try this DEMO
There are some problems with your code
You have to define template for your directive: template:"<div></div>" and append html to it: element.append(htmlText);
Use fieldModel : '=' instead of fieldModel : '=ngModel' because you use field-model="field.model" in your html.
Use ng-model="fieldModel" instead of ng-model="'+scope.fieldModel+'"
You need to compile your html using $compile service: $compile(element.contents())(scope);

Related

angular-messages nested directive, how to keep it working with other directives

I am playing with AngularJS to discover its power but I have to admit the documentation is not very developed, so I'm asking here the community for a problem I'm facing with nested directives.
I'm more looking for the reasonment (and explanations on what I'm doing wrong) than a finished solution.
So here is the thing (I am using angular-messages but I don't think it's important as the problem would be common to any directive):
To quickly change the errors management I have decided to encapsulate the manager (angular-messages here) into a directive, so to display my errors on a form I do it this way :
<script type="text/ng-template" id="default-error-messages">
<error-message data-error="email" data-message="This field is not a valid email"></error-message>
<error-message data-error="required" data-message="This field is required"></error-message>
<error-message data-error="minlength" data-message="This field is too short"></error-message>
</script>
<form data-ng-submit="submitForm(registrationForm)" method="POST" name="registrationForm" novalidate>
<input type="email" name="email" data-ng-model="user.email" required>
<error-container data-watch-error-on="registrationForm.email.$error" data-default-errors="default-error-messages" data-ng-if="registrationForm.email.$dirty">
<error-message data-error="required" data-message="test"></error-message>
</error-container>
<button type="submit" data-ng-disabled="registrationForm.$invalid">Register</button>
</form>
directives.directive('errorContainer', ['$compile',function($compile){
return{
restrict: 'E',
transclude: true,
replace: false,
scope: {
watchErrorOn: '#'
},
template: '<div class="error-container" data-ng-transclude></div>',
compile: function(tElt, tAttrs, ctrl) {
return {
pre: function(scope, iElement, iAttrs){
iElement.find('.error-container').attr("data-ng-messages", scope.watchErrorOn);
},
post: function(scope, iElement, iAttrs){
if (angular.isDefined(iAttrs.defaultErrors)) {
var errorList = angular.element("<div data-ng-messages-include='" + (iAttrs.defaultErrors || 'default-error-messages') + "'></div>");
iElement.find('.error-container').append(errorList);
$compile(iElement)(scope);
}
}
}
},
link: function(scope, element, attrs, ctrl){
$compile(element)(scope);
}
}
}]);
directives.directive('errorMessage', ['$compile', function($compile){
return{
restrict: 'E',
template: '<div class="error"></div>',
replace: true,
scope:{
message:'#',
error:'#'
},
compile: function(tElt, tAttrs, ctrl){
return{
pre: function(scope, iElement, iAttrs){
iElement.attr('data-ng-message', scope.error);
iElement.text(scope.message);
}
}
}
}
}]);
As your surely know, It doesn't work, default errors are not included at all in the template.
I have try a lot of combinations on pre/post compile functions & link but nothing was successful.
I think this is a problem of priority on compilation, maybe ng-messages-include should be the last to compile but no idea on how, thank you in advance
You should be using = instead of # in directive isolated scope, which will pass data-watch-error-on="registrationForm.email.$error" value to the isolated scope directly watchErrorOn variable.
scope: {
watchErrorOn: '='
},
As you were using # that indicates one way binding & that need value to be pass in {{}} interpolation directive, but the important thing is you wanted to pass true & false value to the directive isolated scope, but after passing that value in the interpolated value to directive will convert that bool value to string like "false" & "true" that will unnecessary if else check inside our directive. Passing value using = is nothing but two way binding that will keep your value as boolean as you pass it from directive element.
Well, I found a solution after having been oriented by a post on GitHub.
The problem is, as suspected, caused by the compilation politics of AngularJS.
So in few words the key is to put include all directives directly on the template, including them after could still work but would require to compile manually the element.
The thing that is completely strange is that, naturally, pre-compile function would be same as "modifying the template string" before the compilation phase, but that is not the case, apparently.
So please consider the following solution that works well :
The html :
<form method="POST" name="registrationForm" novalidate>
<input type="email" name="email" placeholder="Email address" data-ng-model="user.email" autocomplete="off" required>
<button type="submit">Register</button>
<error-container data-messages-include="my-custom-messages" data-error-watch="registrationForm.email.$error">
<error-message data-error="required" data-message="This field is required (version 1)"></error-message>
<error-message data-error="email" data-message="This field must be an email"></error-message>
</error-container>
</form>
The directives :
var app = angular.module('app', ['ngMessages']);
app.directive('errorContainer', function(){
return{
template: '<div ng-messages="watch"><div ng-messages-include="{{ messagesInclude }}"></div><div ng-transclude></div></div>',
scope: {
watch: '=errorWatch',
messagesInclude: '#messagesInclude'
},
transclude: true
};
});
app.directive('errorMessage', [function(){
return{
scope: {
error: '#error',
message: '#message'
},
template: '<div ng-message="{{ error }}">{{ message }}</div>',
replace: true
}
}]);
The end word, AngularJS is definitely a great framework but some fundamental processes are, from my point of view, unfortunately not as sufficiently explicit.

Angular - validation messages for a form input directive with isolated scope

I think I am slowly going insane. What seems like a simple problem is giving me a headache :(
I have a form with a custom input element directive using isolate scope.
I simply want to able to display a error message based on the validity of the input elements "required" attribute but I seem to be going round in circles. I am not quite understanding the binding in this scenario.
Please take a look at my fiddle here:
http://jsfiddle.net/brogueady/zwbbLggL/
I would expect the error message "Invalid" to appear to the right of the input field because it is empty.
The HTML is
<div ng-app="UIComponents">
<form ng-submit="formSubmit()" name="vrmForm" >
<at-input name="registration" label="Registration" form="vrmForm" model="vrmLookup.registration" minlength="3" required>
</at-input>
</form>
</div>
The JS is
uiComponents.directive('atInput', function () {
return {
// use an inline template for increased
template: '<div>{{label}}</div><input name="{{name}}" required type="text" ng-model="model"/> <span class="error" ng-show="form.{{name}}.$error.required">Invalid</span>',
// restrict directive matching to elements
restrict: 'E',
scope: {
name: '#',
form: '=',
model: '=',
label: '#'
},
compile: function(element, attr) {
var input = element.find('input');
if (!_.isUndefined(attr.required)) {
input.attr("required", "true");
}
}
};
});
Thank you.
Your $scope.form.name property cannot be an angular binding expression. Return a template function instead from your directive and build the template string:
template: function($element, $attr) {
return '<div>{{label}}</div><input name="' + $attr.name + '" required type="text" ng-model="model"/> <span class="error" ng-show="form.' + $attr.name + '.$error.required">Invalid</span>';
},
Demo

How to add arbitrary attributes to an angular directive for data validation

I am attempting to create an angular directive that will be a custom tag for input fields in our application. Essentially what it will do is create the label, input field and the various bootstrap classes so there is a consistent look to them.
Along with that I would like it if I could add the various data validators that are appropriate for the particular input (such as required and custom validators) as attributes of the custom tag and then have those added to the input field and thus perform validation on that.
I have figured out a way that appears to put the attributes on the input field and the custom validator is getting called and properly evaluating the data, but the form never seems to think that the data is invalid. I think I am having a scope problem where the input being invalid is being set on the directive's scope rather than the parent scope but I'm not 100% sure about that and even if it is the problem I don't know how to fix it.
Here's a sample of what I'd like one of the tags to look like
<textinput ng-model="TestValue" name="TestValue" text="Label Text" config="GetConfigurationForm()" ngx-ip-address required></textinput>
which I want to generate something like
<div class="row">
<div class="form-group" ng-class="{ 'has-error': IsInvalid() }">
<label for="{{name}}" class="control-label">{{text}}</label>
<input id="{{name}}" type="text" class="form-control" ng-model="ngModel" name="{{name}}" ngx-ip-address required>
</div>
</div>
Note that the ngx-ip-address and required have been moved to the input field attributes.
My controller looks like the following (sorry it's so long)
var app = angular.module('test', []);
app.directive('ngxIpAddress', function()
{
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attributes, ngModel)
{
ngModel.$validators.ngxIpAddress = function(modelValue, viewValue)
{
// Value being blank is OK
if (ngModel.$isEmpty(modelValue))
return true;
// If the string starts with a character then
// this is not valid
if (isNaN(parseInt(viewValue[0])))
return false;
var blocks = viewValue.split(".");
if(blocks.length === 4)
{
return blocks.every(function(block)
{
return parseInt(block, 10) >= 0 && parseInt(block, 10) <= 255;
});
}
return false;
};
}
};
});
app.directive('textinput', function ()
{
return {
restrict: 'E',
scope: {
//# reads the attribute value, = provides two-way binding, & works with functions
ngModel: '=',
name: '#',
text: '#',
config: '&'
},
controller: function($scope) {
$scope.IsInvalid = function()
{
var getConfigurationFunction = $scope.config();
if (!getConfigurationFunction || !getConfigurationFunction[$scope.name])
return false;
return getConfigurationFunction[$scope.name].$invalid;
};
},
link: function(scope, element, attributes) {
var inputElement = element.find("input");
for (var attribute in attributes.$attr)
{
if (attribute !== "ngModel"
&& attribute !== "name"
&& attribute !== "text"
&& attribute !== "config")
{
inputElement.attr(attribute, attributes[attribute]);
}
}
},
template: '<div class="row">' +
'<div class="form-group" ng-class="{ \'has-error\': IsInvalid() }">' +
'<label for="{{name}}" class="control-label">{{text}}</label>' +
'<input id="{{name}}" type="text" class="form-control" ng-model="ngModel" name="{{name}}">' +
'</div>' +
'</div>'
};
});
app.controller(
"TestController",
[
"$scope",
function TestController(_scope)
{
_scope.TestValue = "TestTest";
_scope.GetConfigurationForm = function()
{
return _scope.ConfigurationForm;
};
}
]
);
If I put the attributes in the actual template then everything works as expected and the control turns red if the data isn't an ip address. When I add the attributes by moving them that doesn't work.
Here is a plunkr showing what I've got so far: http://plnkr.co/edit/EXkz4jmRif1KY0MdIpiR
Here is a plunkr showing what I'd like the end result to look like where I've added the tags to the template rather than the tag: http://plnkr.co/edit/mUGPcl1EzlHUiMrwshCr
To make this even more fun, in the future I will actually need to pass in a value to the data validation directives from the outside scope as well, but I'd like to get this working first.
Here you may find the correct answer.
The reasons of this issue are:
the attr will convert the attribute from ngxIpAddress to ngxipaddress, namely from the uppercase to lowercase, you can find this issue from this link. To solve it, just pass ngx-ip-address as parameter for function attr.
$compile(inputElement)(scope); need to be added into directive, when one directive is used in another directive. Here is one link.

Angular.JS, Data-binding in a directive template When the form validation checking

I have a binding problem. I want to validate the email address
A. <input type="email" required ng-model="emailAddress" name="{{name}}" />
B. <input type="email" required ng-model="emailAddress" name="emailAddress2" />
'A' is Not working, I want to working using 'A' expression.
But, B is Working.
I want to binding expression Using 'A' Expression.
.directive('emailInput', function() {
return {
require: '^form',
restrict: 'E',
template: '<input type="email" required ng-model="emailAddress" name="{{name}}" />',
scope: {
send2: '&'
},
link: function(scope, elem, attrs){
scope.name="emailAddress2"; // <--- this is not binding to input element
}
};
});
HTML
<email-input></email-input> inputIsValid={{myForm.emailAddress2.$valid}}
You need to set the name and then compile the input-element for angular to notice the new name and make $valid work.
...
link: function (scope, elem) {
var input = elem.children()[0];
input.name = "emailAddress2";
$compile(input)(scope);
}
...
From your HTML, modify it as follows:
<email-input name="name"></email-input> inputIsValid={{myForm.emailAddress2.$valid}}
Now, whatever value you change in the directive (scope.name = "whatever") will bind to the template.
I hope it helps.

Transcluding Attributes in an AngularJS Directive

I was creating a select replacement directive to make it easy to style up selects according to the design without having to always right a bunch of markup (i.e. the directive does it for you!).
I didn't realize that attributes don't transclude to where you put ng-transclude and just go to the root element.
I have an example here: http://plnkr.co/edit/OLLntqMzbGCJS7g7h1j4?p=preview
You can see that it looks great... but there's one major flaw. The id and name attributes aren't being transferred. Which, ya know, without name, it doesn't post to the server (this form ties into an existing system, so AJAXing the model isn't an option).
For example, this is what I start with:
<select class="my-select irrelevant-class" name="reason" id="reason" data-anything="banana">
<option value="">Reason for Contact...</option>
<option>Banana</option>
<option>Pizza</option>
<option>The good stuff</option>
<option>This is an example of a really, really, really, really, really, really long option item</option>
</select>
...this is what I want it to look like:
<div class="faux-select" ng-class="{ placeholder: default == viewVal, focus: obj.focus }">
<span class="faux-value">{{viewVal}}</span>
<span class="icon-arrow-down"></span>
<select ng-model="val" ng-focus="obj.focus = true" ng-blur="obj.focus = false" ng-transclude class="my-select irrelevant-class" name="reason" id="reason" data-anything="banana">
<option value="">Reason for Contact...</option>
<option>Banana</option>
<option>Pizza</option>
<option>The good stuff</option>
<option>This is an example of a really, really, really, really, really, really long option item</option>
</select>
</div>
...but this is what actually happens:
<div class="faux-select my-select irrelevant-class" ng-class="{ placeholder: default == viewVal, focus: obj.focus }" name="reason" id="reason" data-anything="banana">
<span class="faux-value">{{viewVal}}</span>
<span class="icon-arrow-down"></span>
<select ng-model="val" ng-focus="obj.focus = true" ng-blur="obj.focus = false" ng-transclude>
<option value="">Reason for Contact...</option>
<option>Banana</option>
<option>Pizza</option>
<option>The good stuff</option>
<option>This is an example of a really, really, really, really, really, really long option item</option>
</select>
</div>
Specifically, the issue is that there's no name attribute on the select, so it doesn't actually send the data to the server.
Obviously, I can use a pre-compile phase to transfer the name and id attributes (that's what I am doing for now), but it would be nice if it would just automatically transfer all of the attributes so they can add any classes, arbitrary data, (ng-)required, (ng-)disabled attributes, etc, etc.
I tried getting transclude: 'element' to work, but then I couldn't the other attributes from the template onto it.
Note, I saw the post here: How can I transclude into an attribute?, but it looks like they just manually transfer the data, and I am aiming to get it to auto-transfer all the attributes.
You could use the compile function to access the element's attributes and build the template.
app.directive('mySelect', [function () {
return {
transclude: true,
scope: true,
restrict: 'C',
compile: function (element, attrs) {
var template = '<div class="faux-select" ng-class="{ placeholder: default == viewVal, focus: obj.focus }">' +
'<span class="faux-value">{{viewVal}}</span>' +
'<span class="icon-arrow-down entypo-down-open-mini"></span>' +
'<select id="' + attrs.id + '" name="' + attrs.name + '" ng-model="val" ng-focus="obj.focus = true" ng-blur="obj.focus = false" ng-transclude>';
'</select>' +
'</div>';
element.replaceWith(template);
//return the postLink function
return function postLink(scope, elem, attrs) {
var $select = elem.find('select');
scope.default = scope.viewVal = elem.find('option')[0].innerHTML;
scope.$watch('val', function(val) {
if(val === '') scope.viewVal = scope.default;
else scope.viewVal = val;
});
if(!scope.val) scope.val = $select.find('option[selected]').val() || '';
}
}
};
}]);
The compile function is returning the postLink function, there are other ways to do this, you'll find more info here.
Here is a plunker
ng-transclude transcludes the content of an element on which the directive was placed. I would have assigned the attribute to its parent div and transcluded the entire select box in the template:
First Approach:
http://plnkr.co/edit/fEaJXh?p=preview
<div class="form-control my-select">
<select class="irrelevant-class" name="reason" id="reason" data-anything="banana">
<option value="">Reason for Contact...</option>
<option>Banana</option>
<option>Pizza</option>
<option>The good stuff</option>
<option>This is an example of a really, really, really, really, really, really long option item</option>
</select>
</div>
And remove replace option from the definition:
app.directive('mySelect', [function () {
return {
template:
'<div class="faux-select" ng-class="{ placeholder: default == viewVal, focus: obj.focus }">' +
'<span class="faux-value">{{viewVal}}</span>' +
'<span class="icon-arrow-down entypo-down-open-mini"></span>' +
'<div ng-transclude></div>' +
'</div>',
transclude: true,
//replace: true,
scope: true,
restrict: 'C',
link: function (scope, elem, attrs) {
var $select = elem.find('select');
scope.default = scope.viewVal = elem.find('option')[0].innerHTML;
scope.$watch('val', function(val) {
if(val === '') scope.viewVal = scope.default;
else scope.viewVal = val;
});
if(!scope.val) scope.val = $select.find('option[selected]').val() || '';
}
};
}]);
Second Approach:
In your demo, just include following line at the end of the link method:
$select.attr({'id': elem.attr('id'), 'name': elem.attr('name')});

Resources