ngTransclude doesnt render content correctly - angularjs

Trying to dynamically pass error/feedback messages to a directive through Transclude. However the transcluded content doesnt redner corrrectly either due to the tag span or the innerForm.inputField.$error.....
EDIT: text between brackets [text] is test that needs to be translated. Dont be confused by that.
Cant find info about limitations about transcluding content. Hope to get some pointers here.
The Directive tag in the DOM
<input-validation-below
place-holder="[E-mail address]"
identifier="forgotten_email"
ng-model="email"
type="email">
<span data-ng-show="innerForm.inputField.$error.required">[E-mail address] [is required]</span>
<span data-ng-show="innerForm.inputField.$error.email">[This is not a valid] [E-mail address]</span>
</input-validation-below>
The Directive
angular.module('inputValidationBelow', []).directive('inputValidationBelow', function () {
return {
scope: {
login: '=ngModel',
placeHolder: '#',
identifier: '#',
type: '#'
},
templateUrl: 'scripts/directives/inputValidationBelow/inputValidationBelow.html',
restrict: 'E',
transclude: true,
require: ['ngModel', '^form'],
link: function (scope, element, attrs, controllers) {
scope.type = attrs.type;
var ngModelController = controllers[0];
ngModelController.$render = function () {
if (typeof ngModelController.$viewValue !== 'undefined') {
scope.login = ngModelController.$viewValue;
}
};
}
};
});
The Directive template
<ng-form name="innerForm"
<label class="full-width" for="inputField" data-ng-class="{ error: innerForm.inputField.$dirty && innerForm.inputField.$invalid }">
<!--[if lt IE 9]><p>{{placeHolder}}:</p></p><![endif]-->
<input class="form full-width" type="{{type}}" name="inputField" placeholder="{{placeHolder}}" id="inputField" data-ng-model="login[identifier]" required />
</label>
<div class="alert alert-input" data-ng-show="innerForm.inputField.$dirty && innerForm.inputField.$invalid">
<span class="alert alert-input" ng-transclude></span>
</div>
</ng-form>
--EDIT--
adding this line to the Link function $compile(element.contents())(scope); "fixes" the issue but gives me an error:
Error: [ngTransclude:orphan] Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element: <span class="alert alert-input" ng-transclude="">

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

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.

How to show form input errors using AngularJS UI Bootstrap tooltip?

For example I have the form where I am showing form input errors.
I need to show red badge (with 'hover to show errors') near input label if there are some errors. If user will hover this red badge - he will see list of errors using AngularJS UI Bootstrap tooltip.
I don't want to put list of errors into tooltip-html-unsafe attribute, because it is not convenient to edit and maintain.
This code is more declarative:
<validation-tooltip ng-show="appForm.number.$invalid && appForm.number.$dirty">
<ul>
<li ng-show="appForm.number.$error.required">this field is required</li>
<li ng-show="appForm.number.$error.number">should be number</li>
<li ng-show="appForm.number.$error.min">minimum - 5</li>
<li ng-show="appForm.number.$error.max">miximum - 20</li>
</ul>
</validation-tooltip>
than this code:
<span tooltip-html-unsafe="{{<ul><li>This field is required;</li><li>...</li></ul>}}">hover to show errors</span>
How can I write such validation-tooltip directive using AngularJS UI Bootstrap tooltip?
Or maybe can you suggest another approach to maintain validation error messages?
Demo Fiddle
Validation Tooltip Directive
The validationTooltip is the main directive. It has the following responsibilities:
Define the tool tip template through its transcluded contents
Keep track of validation expressions so that they can be evaluated with each digest cycle.
Expose a controller API for allowing valiationMessage directives to register themselves
Provide a 'target' attribute on the directive to specify which form field the badge (and the associated tooltip) will be bound to
Additional Notes
The tooltip template uses the transclusion function from the link function to bind the template to the directive's scope. There are two properties that are within scope that the template can bind to:
$form: Bound to the form model defined in parent scope. i.e. $scope.myForm
$field: Bound to the form.name model in parent scope. i.e. $scope.myForm.myInput
This allows the template to bind to validation properties such as $valid, $invalid, $pristine, $dirty, and $error without referring to the form name or the input field's name directly. For example, all of the following expressions are valid binding expressions:
$form properties:
`$form.$valid`
`$form.$invalid`
`$form.$dirty`
`$form.$pristine`
`$form.$error.required` etc...
$field properties:
`$field.$valid`
`$field.$invalid`
`$field.$dirty`
`$field.$pristine`
`$field.$error.required` etc...
Directive Implementation
app.directive('validationTooltip', function ($timeout) {
return {
restrict: 'E',
transclude: true,
require: '^form',
scope: {},
template: '<span class="label label-danger span1" ng-show="errorCount > 0">hover to show err</span>',
controller: function ($scope) {
var expressions = [];
$scope.errorCount = 0;
this.$addExpression = function (expr) {
expressions.push(expr);
}
$scope.$watch(function () {
var count = 0;
angular.forEach(expressions, function (expr) {
if ($scope.$eval(expr)) {
++count;
}
});
return count;
}, function (newVal) {
$scope.errorCount = newVal;
});
},
link: function (scope, element, attr, formController, transcludeFn) {
scope.$form = formController;
transcludeFn(scope, function (clone) {
var badge = element.find('.label');
var tooltip = angular.element('<div class="validationMessageTemplate tooltip-danger" />');
tooltip.append(clone);
element.append(tooltip);
$timeout(function () {
scope.$field = formController[attr.target];
badge.tooltip({
placement: 'right',
html: true,
title: clone
});
});
});
}
}
});
Validation Message Directive
The validationMessage directive keeps track of the validation messages to display in the tooltip. It uses ng-if to define the expression to evaluate. If there is no ng-if found on the element, then the expression simply evaluates to true (always shown).
app.directive('validationMessage', function () {
return {
restrict: 'A',
priority: 1000,
require: '^validationTooltip',
link: function (scope, element, attr, ctrl) {
ctrl.$addExpression(attr.ngIf || true );
}
}
});
Usage in HTML
Add a form with a name attribute
Add one or more form fields - each with a name attribute and an ng-model directive.
Declare a <validation-tooltip> element with a target attribute referring to the name of one of the form fields.
Apply the validation-message directive to each message with an optional ng-if binding expression.
<div ng-class="{'form-group': true, 'has-error':form.number.$invalid}">
<div class="row">
<div class="col-md-4">
<label for="number">Number</label>
<validation-tooltip target="number">
<ul class="list-unstyled">
<li validation-message ng-if="$field.$error.required">this field is required </li>
<li validation-message ng-if="$field.$error.number">should be number</li>
<li validation-message ng-if="$field.$error.min">minimum - 5</li>
<li validation-message ng-if="$field.$error.max">miximum - 20</li>
</ul>
</validation-tooltip>
</div>
</div>
<div class="row">
<div class="col-md-4">
<input type="number" min="5" max="20" ng-model="number" name="number" class="form-control" required />
</div>
</div>
</div>
#pixelbits answer is great. I used this instead:
<div class="form-group" ng-class="{ 'has-error': form.name.$dirty && form.name.$invalid }">
<label for="name" class="col-sm-4 control-label">What's your name?</label>
<div class="col-sm-6">
<input class="form-control has-feedback" id="name" name="name"
required
ng-minlength="4"
ng-model="formData.name"
tooltip="{{form.name.$valid ? '' : 'How clients see your name. Min 4 chars.'}}" tooltip-trigger="focus"
tooltip-placement="below">
<span class="glyphicon glyphicon-ok-sign text-success form-control-feedback" aria-hidden="true"
ng-show="form.name.$valid"></span>
</div>
</div>
The technique is ui-bootstrap's tooltip and set the tooltip text to '' when valid.
http://jsbin.com/ditekuvipa/2/edit
Great answer from #pixelbits. I used his directives and modified them slightly to allow the tooltip to display over the actual input as some users requested.
angular.module('app')
.directive('validationTooltip', ['$timeout', function ($timeout) {
function toggleTooltip(scope) {
if (!scope.tooltipInstance) {
return;
}
$timeout(function() {
if (scope.errorCount > 0 && (scope.showWhen == undefined || scope.showWhen())) {
scope.tooltipInstance.enable();
scope.tooltipInstance.show();
} else {
scope.tooltipInstance.disable();
scope.tooltipInstance.hide();
}
});
}
return {
restrict: 'E',
transclude: true,
require: '^form',
scope: {
showWhen: '&',
placement: '#',
},
template: '<div></div>',
controller: ['$scope', function ($scope) {
var expressions = [];
$scope.errorCount = 0;
this.$addExpression = function (expr) {
expressions.push(expr);
}
$scope.$watch(function () {
var count = 0;
angular.forEach(expressions, function (expr) {
if ($scope.$eval(expr)) {
++count;
}
});
return count;
}, function (newVal) {
$scope.errorCount = newVal;
toggleTooltip($scope);
});
}],
link: function (scope, element, attr, formController, transcludeFn) {
scope.$form = formController;
transcludeFn(scope, function (clone) {
var tooltip = angular.element('<div class="validationMessageTemplate" style="display: none;"/>');
tooltip.append(clone);
element.append(tooltip);
$timeout(function () {
scope.$field = formController[attr.target];
var targetElm = $('[name=' + attr.target + ']');
targetElm.tooltip({
placement: scope.placement != null ? scope.placement : 'bottom',
html: true,
title: clone,
});
scope.tooltipInstance = targetElm.data('bs.tooltip');
toggleTooltip(scope);
if (scope.showWhen) {
scope.$watch(scope.showWhen, function () {
toggleTooltip(scope);
});
}
});
});
}
}
}]);
The major change is that the directive uses jQuery to find the target element (which should be an input) via the name attribute, and initializes the tooltip on that element rather than the element of the directive. I also added a showWhen property to the scope as you may not always want your tooltip to show when the input is invalid (see example below).
The validationMessage directive is unchanged
angular.module('app').directive('validationMessage', function () {
return {
restrict: 'A',
priority: 1000,
require: '^validationTooltip',
link: function (scope, element, attr, ctrl) {
ctrl.$addExpression(attr.ngIf || true);
}
}
});
Usage in Html is also similar, with just the addition of showWhen if you want:
<div class="form-group" ng-class="{ 'has-error' : aForm.note.$invalid && (aForm.note.$dirty) }">
<label class="col-md-3 control-label">Note</label>
<div class="col-md-15">
<textarea
name="note"
class="form-control"
data-ng-model="foo.Note"
ng-required="bar.NoteRequired"></textarea>
<validation-tooltip target="note" show-when="aForm.note.$invalid && (aForm.note.$dirty)">
<ul class="validation-list">
<li validation-message ng-if="$field.$error.required">Note required</li>
</ul>
</validation-tooltip>
</div>
</div>
you can actually just use the tooltip-enable property:
<div class="showtooltip" tooltip-placement="left" tooltip-enable="$isValid" tooltip="tooltip message"></div>
My goal was to leverage both ng-messages and ui-bootstrap popover for validation feedback. I prefer the popover vs. the tooltip as it displays the help-block styles more clearly.
Here's the code:
<!-- Routing Number -->
<div class="form-group-sm" ng-class="{ 'has-error' : form.routingNumber.$invalid && !form.routingNumber.$pristine }">
<label class="control-label col-sm-4" for="routing-number">Routing #</label>
<div class="col-sm-8">
<input class="form-control input-sm text-box"
id="routing-number"
name="routingNumber"
ng-model="entity.ROUTINGNUM"
popover-class="help-block"
popover-is-open="form.routingNumber.$invalid"
popover-trigger="none"
required
uib-popover-template="'routing-number-validators'"
string-to-number
type="number" />
</div>
<!-- Validators -->
<script type="text/ng-template" id="routing-number-validators">
<div ng-messages="form.routingNumber.$error">
<div ng-messages-include="/app/modules/_core/views/validationMessages.html"></div>
</div>
</script>
</div>
Here is the validationMessages.html
<span ng-message="required">Required</span>
<span ng-message="max">Too high</span>
<span ng-message="min">Too low</span>
<span ng-message="minlength">Too short</span>
<span ng-message="maxlength">Too long</span>
<span ng-message="email">Invalid email</span>
Note: I had to upgrade to jQuery 2.1.4 to get the uib-popover-template directive to work.
Dependencies:
string-to-number
UI Bootstrap
ng-messages

AngularJs: transclude through include

I try to make the directive with dynamic template
app.directive('boolInput', function () {
'use strict';
var
restrict = 'E',
replace = true,
template = '<ng-include src="template"></ng-include>',
scope = {
value: "=",
template: "#"
},
link = function (scope, element, attributes) {
// some stuff
};
return {
link: link,
restrict: restrict,
replace: replace,
template: template,
scope: scope,
transclude: true
};
});
So I use
template = '<ng-include src="template"></ng-include>'
and
scope = {
//..
template: "#"
}
to pass template url via attribute. All work great instead of one thing. There is how I use directive:
<bool-input data-value="item.value" data-ng-repeat="item in source" data- template="templates/boolInput.html">
{{item.Text}}
</bool-input>
{{item.Text}} - should be transcluded into template
That template:
<div class="checkbox">
<label class="checkbox-custom" ng-transclude>
<input type="checkbox">
<i class="icon-unchecked checked"></i>
</label>
</div>
but this does not happen, as a result I see:
<ng-include src="template" data-value="item.value" data-ng-repeat="item in data" data-template="templates/boolInput.html" class="ng-scope"><div class="checkbox ng-scope">
<label class="checkbox-custom" ng-transclude="">
<input type="checkbox">
<i class="icon-unchecked checked"></i>
<!-- There should be the text -->
</label>
</div></ng-include>
This was 2 years ago... For now we can pass a attribute to templateUrl function
templateUrl: function (elememt, attrs) {
return attrs.template || '<some default template path>';
}

Resources