AngularJS: Linking multiple scope variables inside external template - angularjs

I am trying to build some custom directives for inputs with validations provided by ngMessages directive. Still, I can't link multiple variables from $scope to dynamically determine the form name and the input name. Here's my code so far:
The directive:
app.directive('textBox', ['$compile', function ($compile) {
return {
restrict: 'E',
scope: {
label: "#",
fieldName: "#",
bindTo: "="
},
require: "^form",
templateUrl: '/WebClient/Directives/TextBox/textBoxTemplate.html',
link: function (scope, element, attrs, ctrl) {
$scope.formName = ctrl.$name;
}
};
}]);
The template:
<div>
<label>{{label}}</label>
<input type="text" name="{{fieldName}}" ng-model="{{field}}" required />
<div ng-messages="{{formName}}.{{fieldName}}.$error">
<div ng-message="required">You left the field blank...</div>
<div ng-message="minlength">Your field is too short</div>
<div ng-message="maxlength">Your field is too long</div>
<div ng-message="email">Your field has an invalid email address</div>
</div>
</div>
The usage:
<text-box bind-to="myField" field-name="myField"></text-box>
The issues I encounter are related to the ng-messages attribute value. Doesn't seem to work when I use curly braces and it renders the text "formName.fieldName.$error" if I don't. The other issue is related to the ng-model, the same scenario applies.
Thank you!

You can pass the formController, ctrl in your link method to directive scope like scope.form = ctrl;.
Then you can add <div ng-messages="form[fieldName].$error"> to access the $error property.
Curly braces are only required for the name of the input field. For ng-model you can directly add the model with-out curlies because ng-model requires two-way binding to work.
Please have a look at the demo below or this jsfiddle.
angular.module('demoApp', ['ngMessages'])
.controller('mainController', MainController)
.directive('textBox', ['$compile', function ($compile) {
return {
restrict: 'E',
scope: {
label: "#",
fieldName: "#",
bindTo: "="
},
require: "^form",
templateUrl: '/WebClient/Directives/TextBox/textBoxTemplate.html',
link: function (scope, element, attrs, ctrl) {
//$scope.formName = ctrl.$name;
scope.form = ctrl;
}
};
}]);
function MainController($scope) {
$scope.myField = 'test';
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular-messages.js"></script>
<div ng-app="demoApp" ng-controller="mainController">
<form name="testForm">
<text-box bind-to="model" field-name="myField"></text-box>
</form>
{{model}}
<script type="text/ng-template" id="/WebClient/Directives/TextBox/textBoxTemplate.html">
<div>
<label>{{label}}</label>
<input type="text" name="{{fieldName}}" ng-model="bindTo" required />
<div ng-messages="form[fieldName].$error">
<div ng-message="required">You left the field blank...</div>
<div ng-message="minlength">Your field is too short</div>
<div ng-message="maxlength">Your field is too long</div>
<div ng-message="email">Your field has an invalid email address</div>
</div>
</div>
</script>
</div>

Related

AngularJS Directive - Template replacements inside ng-repeat

I have the following directive code (AngularJS 1.4.8). The issue I have is using template replacements inside the ng-repeat.
(function() {
'use strict';
angular.module('app').directive('formGroup', formGroup);
var template = '<div class="form-group">' +
'<label class="control-label">{{label}}</label>' +
'<div data-ng-transclude></div>' +
'<span class="text-danger field-validation-error" data-ng-repeat="errorMessage in {{form}}.$error.{{model}}.messages">' +
'{{errorMessage}}' +
'</span>' +
'</div>';
function formGroup () {
return {
restrict: 'A',
require: '^form',
transclude: true,
replace: true,
scope: {
label: '#',
model: '#'
},
template: template,
link: link
};
}
function link(scope, element, attrs, ctrl) {
scope.form = element.closest('form').attr('name');
}
})();
It is used in HTML in the following way (server-validate is another directive that puts in-line validation errors returned from the server into the ng-repeat in the directive).
<form name="myForm" data-ng-submit="submit()">
<div class="col-lg-4 col-md-6">
<div data-form-group data-label="Memeber number" data-model="memberNumber">
<input type="text"
class="form-control"
data-ng-model="model.memberNumber"
data-server-validate />
</div>
</form>
I want it to produce the following output HTML.
<form name="myForm" data-ng-submit="submit()">
<div class="form-group">
<label class="control-label">Member number</label>
<div>
<input type="text"
class="form-control"
data-ng-model="model.memberNumber"
data-server-validate" />
<span class="text-danger field-validation-error"
data-ng-repeat="errorMessage in myForm.$error.memberNumber.messages">
{{errorMessage}}
</span>
</div>
</div>
</form>
However, it gets the following error.
[Error] [$parse:syntax] Syntax Error: Token '{' invalid key at column 2 of the expression [{{form}}.$error.{{model}}.messages] starting at [{form}}.$error.{{model}}.messages]
Since form controller is injected as required, using the formcontroller, the error messages can be handled like below.
function link(scope, element, attrs, ctrl) {
var unbinder = scope.$watch(function () {return ctrl[scope.model] }, function(value) {
if (!value) return;
scope.errorMessagesList = ctrl[scope.model].$error;
unbinder();
})
}
The complete working sample is availabe in the fiddle:
http://jsfiddle.net/acb3c8n7/5/
In the link function use:
function link(scope, element, attrs, ctrl) {
scope.formString = element.closest('form').attr('name');
scope.form = scope.$eval(scope.formString);
}
In the template use:
ng-repeat="errorMessage in form.$error[model].messages"
The $eval method evaluates the string as an Angular expression and will return a reference to the myForm object.
closest() is not present in the elementargument (check this).
Also you have several errors, by example you don't provide a name attribute in the input, this causes that the scope.myForm object don't recognize the model string provided how a property.
<input type="text" name="memberNumber"
class="form-control"
data-ng-model="model.memberNumber"
data-server-validate" />
In your ng-repeat you shouldn't provide a expression like this:
{{form}}.$error.{{model}}.messages
you should be create a property to attach the form object and the use some like this --> http://codepen.io/gpincheiraa/pen/eZGZPX

Form field modified in directive linking function no longer validated correctly?

I have an input element on which I have ng-required ng-pattern="/[0-9]{5}/".
It also has a directive which adds an error message, and which has the following linking function and other related properties:
return {
scope: {},
require: ['^^form', 'ngModel'],
link: function(scope, element, attrs, ctrls) {
// Places error message element after input element.
var el = angular.element('<span>')
.append(element.clone().removeAttr('mp-validated-field')) // else recurses
.append('<span ng-show="showError" class="errorMsg">{{ error }}</span>');
el = el[0].innerHTML;
var tmp = $compile(el)(scope);
element.replaceWith(tmp);
element = tmp;
ctrls[0].$addControl(ctrls[1]);
},
controller: // etc
What should be happening here is that a span operating as an error message is placed after the input element automatically by the directive. As written, this happens, however I have to use $addControl to get the formController to re-register the input after that.
What happens when I run this with a blank input (so it should fail required) is that the formController's $error hash for this field contains {pattern: false}, which is to say it thinks there's only a pattern validation, no required validation, and that the pattern validation is passing despite that it's the regex /[0-9]{5}/ (zipcode).
What am I missing here?
Edit: I moved the ng-required and ng-pattern directives to be before my custom directive and now it recognizes both of them, however it just fails on ng-required arbitrarily every time, regardless of whether it is blank.
Try this solution jsfiddle.
angular.module('ExampleApp', [])
.controller('ExampleController', function($scope) {
})
.directive('myError', function($compile) {
return {
restrict: "A",
require: ['ngModel'],
scope: {
ngModel: "="
},
link: function(scope, elem, attr) {
var span = angular.element('<div ng-show="showError" class="error">{{error}}</div>');
elem.after(span);
$compile(span)(scope);
scope.error = 'Zip code must be not 0000';
scope.$watch('ngModel', function(val) {
if (val == '00000')
scope.showError = true;
else
scope.showError = false;
});
}
}
});
.error {
color: red;
font-style: italic;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="ExampleApp">
<div ng-controller="ExampleController">
<form name="testForm">
<input type="text" ng-model="zip" name="zip" my-error ng-pattern="/[0-9]{5}/" required>
<div ng-show='testForm.zip.$error.required' class='error'>Is required</div>
<div ng-show='testForm.zip.$error.pattern' class='error'>Invalid pattern</div>
<br>
<input type="text" ng-model="zip2" name="zip2" ng-pattern="/[0-9]{5}/" required my-error>
<div ng-show='testForm.zip2.$error.required' class='error'>Is required</div>
<div ng-show='testForm.zip2.$error.pattern' class='error'>Invalid pattern</div>
</form>
</div>
</div>
But for complex custom checks i recommend use-form-error.
Live example on jsfiddle.
angular.module('ExampleApp', ['use', 'ngMessages'])
.controller('ExampleController', function($scope) {
});
.errors {
color: maroon
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://cdn.rawgit.com/Stepan-Kasyanenko/use-form-error/master/src/use-form-error.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular-messages.min.js"></script>
<div ng-app="ExampleApp">
<div ng-controller="ExampleController">
<form name="myForm" use-form-error="formInvalid" use-error-expression="digit==9">
<div ng-messages="myForm.$error" style="color:maroon">
<div ng-message="formInvalid">You form is not valid</div>
</div>
<label>Your number should be even, not divisible by three and should not be 6,7,9:</label>
<input type="text" ng-model="digit" name="myDigit" ng-minlength="1" ng-maxlength="20" required use-form-error="isEven" use-error-expression="digit%2==0" />
<span use-form-error="isDividedThree" use-error-expression="digit%3==0" use-error-input="myForm.myDigit"></span>
<span use-form-error="formInvalid" use-error-expression="digit==7" use-error-input="myForm.myDigit"></span>
<span use-form-error="formInvalid" use-error-expression="digit==6"></span>
<pre>myForm.myDigit.$error = {{ myForm.myDigit.$error | json }}</pre>
<div ng-messages="myForm.myDigit.$error" ng-messages-multiple="true" style="color:maroon">
<div ng-message="required">You did not enter a digit</div>
<div ng-message="minlength">Your digit is too short</div>
<div ng-message="maxlength">Your digit is too long</div>
<div ng-message="isEven">Your digit is even</div>
<div ng-message="isDividedThree">Your digit is divided by three</div>
</div>
</form>
</div>
</div>

Get access to form controller validation errors in a custom directive

I have a directive that wraps a form element with some inputs. One of the options is passing in a formName. Usually, with a form with the example name of myForm, to show an error you would do something like myForm.firstName.$error.required.
But, how do I get access to the errors when the form name is dynamically being passed in to the directive?
example usage
<my-custom-form formName='myForm' formSubmit='parentCtrl.foo()'></my-custom-form>
directive
angular.module('example')
.directive('myCustomForm', [
function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'myCustomForm.directive.html',
scope: {
fornName: '#',
formSubmit: '&'
},
require: ['myCustomForm', 'form'],
link: function(scope, element, attrs, ctrls) {
var directiveCtrl = ctrls[0];
var formCtrl = ctrls[1];
scope.data = {};
scope.hasError = function(field) {
// how do i show the errors here?
};
scope.onSubmit = function() {
scope.formSubmit();
};
}
};
}]);
template
<form name="{{ formName }}" ng-submit="onSubmit()" novalidate>
<div class="form-group" ng-class="{'is-invalid': hasError('fullName') }">
<input type="text" name="fullName" ng-model="data.full_name" required />
<div ng-show="hasError('fullName')">
<p>How do I show this error?</p>
</div>
</div>
<div class="form-group" ng-class="{'is-invalid': hasError('email') }">
<input type="text" name="email" ng-model="data.email" ng-minlength="4" required />
<div ng-show="hasError('email')">
<p>How do I show this error?</p>
</div>
</div>
<button type="submit">Submit</button>
</form>
I think the only problem with your code is that the directive requires itself, I don't think that will work. Just removing the myCustomForm from the require works fine.
To check if the field has errors, you just need to check if the $error object in the form controller is empty.
require: ['form'],
link: function(scope, element, attrs, ctrls) {
var formCtrl = ctrls[0];
scope.data = {};
scope.hasError = function(field) {
// Field has errors if $error is not an empty object
return !angular.equals({}, formCtrl[field].$error);
};
Plunker

Custom AngularJS Directive For Working Hours

I was writing an angularJS directive to input opening hours. Something like:
Here is the directive:
.directive('openhoursDay', function() {
return {
scope: {
openhoursDay:"=",
openhoursActive: "=", //import referenced model to our directives scope
openhoursFrom: "=",
openhoursTo: "="
},
templateUrl: 'templates/open_hours.html',
link: function(scope, elem, attr, ctrl) {
}
}
});
The template:
<div >
<span>{{openhoursDay.day}}</span>
<input type="checkbox" ng-model="openhoursDay.active"/>
<input type="text" ng-model="openhoursDay.open"/>
<input type="text" ng-model="openhoursDay.close"/>
<br>
</div>
HTML:
<div ng-model="work.dt[0]" openhours-day="Sun" openhours-active="active" openhours-from="from" openhours-to="to"></div>
<div ng-model="work.dt[1]" openhours-day="Mon" openhours-active="active" openhours-from="from" openhours-to="to"></div>
<div ng-model="work.dt[2]" openhours-day="Tue" openhours-active="active" openhours-from="from" openhours-to="to"></div>
{{work}}
And Controller:
$scope.work={
dt:[]
};
The problem that I am facing is, scope work is never updated whatever I type on input box, or even if click-unclick checkbox. It remain unchanged as: {"dt":[]}
ng-model is for input fields. So you're passing it in but you weren't really using it for anything. Also you are reading the attributes passed in using = but perhaps you meant to use #. I've created a plunkr demonstrating how you could get this working.
Here's the directive:
.directive('openhoursDay', function() {
return {
scope: {
model:"=",
openhoursDay:"#",
openhoursActive: "#", //import referenced model to our directives scope
openhoursFrom: "#",
openhoursTo: "#"
},
templateUrl: 'open_hours.html',
link: function(scope, elem, attr, ctrl) {
scope.model = {};
scope.model.day = scope.openhoursDay;
scope.model.active = scope.openhoursActive;
scope.model.open = scope.openhoursFrom;
scope.model.close = scope.openhoursTo;
}
}
})
The template:
<div >
<span>{{model.day}}</span>
<input type="checkbox" ng-model="model.active"/>
<input type="text" ng-model="model.open"/>
<input type="text" ng-model="model.close"/>
<br>
</div>
HTML:
<div model="work.dt[0]" openhours-day="Sun" openhours-active="active" openhours-from="from" openhours-to="to"></div>
<div model="work.dt[1]" openhours-day="Mon" openhours-active="active" openhours-from="from" openhours-to="to"></div>
<div model="work.dt[2]" openhours-day="Tue" openhours-active="active" openhours-from="from" openhours-to="to"></div>
work:{{work}}
And Controller:
.controller('MainController', ['$scope', function($scope){
$scope.work={
dt:[]
};
}])
You have to pass the ng-model attribute to the isolated scope, and then, use it in the template as following:
.directive('openhoursDay', function() {
return {
scope: {
openhoursDay: "=",
openhoursActive: "=", //import referenced model to our directives scope
openhoursFrom: "=",
openhoursTo: "=",
ngModel: "=" // Here is the ng-model
},
template: '<div ><span>{{openhoursDay.day}}</span><input type="checkbox" ng-model="ngModel.openhoursDay.active"/><input type="text" ng-model="ngModel.openhoursDay.open"/><input type="text" ng-model="ngModel.openhoursDay.close"/><br> </div>',
link: function(scope, elem, attr, ctrl) {}
};
})
I have created a Plunkr which simulates your situation. You could check it out.

Passing a $scope variable to a directive

I am struggling to pass data from my model to a directive and have little to no idea how. I am an AngularJS newbie :).
My markup is as follows below. Currently witnessing two problems:
1) scope.$parent.companyName is always blank no matter what I type? I have declared an empty value because otherwise it's undefined and that seems like an anti-pattern.
2) Even if I get (1) working, it's relying on the layout of the model I give it because of $parent. How do I pass a string to the directive?
I have a view:
<ion-view>
<ion-header-bar class="bar-royal">
<h1 class="title">{{ registerTitle }}</h1>
</ion-header-bar>
<ion-content>
<div class="list list-inset">
<label>
{{ registerInfoLabel }}
</label>
<label class="item item-input">
<input type="text" placeholder="{{ companyNameLabel }}" ng-model="companyName">
</label>
<label>
<button class="button button-block button-royal" register-app>
{{ registerLabel }}
</button>
</label>
</div>
</ion-content>
</ion-view>
A controller:
angular.module('app')
.controller('RegisterController', ['$scope', '$appI18n', function ($scope, $appI18n) {
$scope.registerTitle = $appI18n.RegisterTitle;
$scope.registerLabel = $appI18n.RegisterLabel;
$scope.companyNameLabel = $appI18n.CompanyName;
$scope.registerInfoLabel = $appI18n.RegisterInfoLabel;
$scope.companyName = '';
}]);
And a directive:
angular.module('app')
.directive('registerApp', ['RegisterService', function (RegisterService) {
return {
restrict: 'AE',
link: function (scope, element, attrs) {
element.bind('click', function () {
alert(scope.$parent.companyName);
})
}
}
}]);
You can send a string in to the directive using an attribute. If you only have one parameter to send then you can use the directive itself like this:
<div register-app="companyName"></div>
And adding it to your directive scope:
scope: { registerApp : '=' },
Then it'll be available on your scope:
link: function (scope, element, attrs) {
console.log("cname ",scope.registerApp);
}
Here's an example of that working: http://jsfiddle.net/kgwkf02c/
Or you could send it in using it's own attribute (especially useful if you want to send in multiple values):
<div register-app cname="companyName"></div>
With the directive looking like:
scope: { cname : '=' },
link: function (scope, element, attrs) {
console.log("cname ",scope.cname);
}
Here's that version: http://jsfiddle.net/kgwkf02c/1/

Resources