Transcluding Attributes in an AngularJS Directive - angularjs

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')});

Related

Angular Directive with dynamically generated input fields not able to display validation

After 3 days of scouring stackoverflow and other sites, I have found myself back at square one.
My task: I need to validate dynamically generated form fields.
The HTML:
<form name="myForm">
<form-field content="field" model="output[field.uniqueId]" ng-repeat="field in formFields"></form-field>
</form>
The controller:
var myApp = angular.module('myApp',[]);
function MyCtrl($scope) {
$scope.formFields = [
{
"fieldName": "Your Name",
"uniqueId": "your_name_0",
"fieldType": "text",
"isMandatory": true
},
{
"fieldName": "Description",
"uniqueId": "description_1",
"fieldType": "textarea",
"isMandatory": true,
}
];
$scope.output={};
}
The directive:
myApp.directive("formField",function($compile){
var templates = {
textTemplate:'<div class="form-group"><label for="{{content.uniqueId}}" >{{content.fieldName}}</label> <span ng-show="content.isMandatory" class="sub_reqText">*</span><span ng-show="form.content.fieldName.$invalid">Please check this field.</span><input type="text" ng-model="model" name="{{content.uniqueId}}" class="form-control" ng-required="content.isMandatory" id="{{content.uniqueId}}"/> </div><br>',
textareaTemplate:'<div class="form-group"><label for="{{content.uniqueId}}" >{{content.fieldName}}</label> <span ng-show="content.isMandatory" class="sub_reqText">*</span> <span ng-show="form.content.fieldName.$invalid">Please check this field.</span> <textarea ng-model="model" name="{{content.uniqueId}}" id="{{content.uniqueId}}" class="form-control" ng-required="content.isMandatory"></textarea> </div>'
};
var getTemplate = function(content, attrs){
var template = {};
template = templates[content.fieldType+"Template"];
if(typeof template != 'undefined' && template != null) {
return template;
}
else {
return '';
}
};
var linker = function(scope, element, attrs){
element.html(getTemplate(scope.content, attrs)).show();
$compile(element.contents())(scope);
}
return {
restrict:"E",
replace:true,
link:linker,
scope:{
content:'=',
model:'=?'
}
};
});
There is clearly some scope issue because I cannot access the form fields outside of the directive and I cannot access the form name inside the directive. I also know $scope.myForm.name property cannot be an angular binding expression but I am not sure how to rewrite it so that it works.
This is the jsfiddle: http://jsfiddle.net/scheedalla/57tt04ch/
Any guidance will be very useful, thank you!
While debugging the problem I found that, the name attribute is not properly compiled for form. It was showing {{content.uniqueId}} in name but actually it rendered properly on UI.
Eg.
For below html.
<input type="text" ng-model="model" name="{{content.uniqueId}}" class="form-control"
ng-required="content.isMandatory" id="{{content.uniqueId}}"/>
name rendered as name="your_name_0" but in form collection it was showing {{content.uniqueId}} with the interpolation directive.
Seems like name is not interpoluted properly.
Then found issue with AngularJS, "You can't set name attribute dynamically for form validation."
Note: Above mentioned issue has been fixed in Angular 1.3.(name
attributes interpolates properly)
& If you wanted to work them inside ng-repeat, then you should always use nested ng-form. Members inside ng-repeat will have their own form, and using that inner form you can handle your validation. Link For Reference
CODE CHANGE
var templates = {
textTemplate: '<ng-form name="form">'+
'<div class="form-group">'+
'<label for="{{content.uniqueId}}">{{content.fieldName}}</label> '+
'<span ng-show="content.isMandatory" class="sub_reqText">*</span>'+
'<span ng-show="form.input.$invalid">'+
'Please check this field.'+
'</span>'+
'<input type="text" ng-model="model1" name="input" class="form-control" ng-required="content.isMandatory" id="{{content.uniqueId}}" /> '+
'</div>'+
'</ng-form>'+
'<br>',
textareaTemplate: '<ng-form name="form">'+
'<div class="form-group">'+
'<label for="{{content.uniqueId}}">{{content.fieldName}}</label>'+
'<span ng-show="content.isMandatory" class="sub_reqText">*</span> '+
'<span ng-show="form.textarea.$invalid">Please check this field.</span>'+
'<textarea ng-model="model" name="textarea" id="{{content.uniqueId}}" class="form-control" ng-required="content.isMandatory"></textarea>'+
'</div>'+
'</ng-form>'
};
Only i changed the template html, basically added <ng-form></ng-form> for templates and handled the validation on basis it in inner form.
Here is your Working Fiddle
Hope this have cleared your understanding. Thanks.

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 can I make a directive accept some text that I pass in from inside an element?

I would like to create a directive to replace some code in my HTML.
Here's what I have right now:
<div class="gridFooter" ng-show="home.dataRetrieved">
<span ng-show="(home.grid.data).length">{{ (home.grid.data).length + " rows retrieved - " + home.grid.view.length + " displayed" }}</span>
<span ng-show="!(home.grid.data).length">There are no tests that match your selection criteria</span>
</div>
I created this basic directive but there are things missing:
app.directive('adminGridFooter', function () {
return {
template: '<div class="gridFooter" ng-show = "home.dataRetrieved" >\
<span ng-show = "(home.grid.data).length" >\
{{ (home.grid.data).length + " rows retrieved - " + home.grid.view.length + " displayed" }}\
</span >\
<span ng-show="!(home.grid.data).length" >xx</span >\
</div>'
};
});
How can I make it so I can pass in the string "xx" inside the element when I call the directive and will my directive just assume the current scope so that the home.dataRetrieved will work without change? Something like
Another question. Howe can I make the directivecompletely replace my call to it <admin-grid-footer></admin-grid-footer>. How can I make it so it replaces the element?
You are looking for transclude and replace:
app.directive('adminGridFooter', function () {
return {
replace:true,
transclude:true,
template: '<div class="gridFooter" ng-show = "home.dataRetrieved" >\
<span ng-show = "(home.grid.data).length" >\
{{ (home.grid.data).length + " rows retrieved - " + home.grid.view.length + " displayed" }}\
</span >\
<span ng-show="!(home.grid.data).length" ng-transclude></span >\
</div>'
};
});
PLUNKER
you can change your directive like this.
angular.module('onboardingApp').directive("adminGridFooter",function () {
return {
restrict: 'E',
link: function(scope, element, attributes) {
scope.customMessage = attributes["custommessage"];
},
templateUrl: '<div class="gridFooter" ng-show = "home.dataRetrieved" >\
<span ng-show = "(home.grid.data).length" >\
{{ (home.grid.data).length + " rows retrieved - " + home.grid.view.length + " displayed" }}\
</span >\
<span ng-show="!(home.grid.data).length" >{{customMessage}}</span >\
</div>',
};
});
then pass the value that u want in html
<admin-grid-footer customMessage="what ever you want"></admin-grid-footer>
At first, you can use templateUrl property and point it to separate HTML file instead of writing the whole HTML as a string.
The second, you can restrict the directive type by element, so you can use it only as an element (not as attribute nor as class). Here is how to do that: restrict: 'E'.
Finally, you can also specify a link function where you can get the attributes of your element and do whatever you need.
So, after these changes your code may look like this:
app.directive('adminGridFooter', function () {
return {
restrict: 'E',
templateUrl: 'adminGridFooter.html', // this contains your HTML
link: function(scope, element, attrs) {
scope.xx = attrs.xx;
}
}
});
And you can use it like this:
<adminGridFooter xx="someValue"></adminGridFooter>
And the last question:
...and will my directive just assume the current scope so that the home.dataRetrieved will work without change?
YES, by default it uses the scope where the directive was called, BUT you can filter scope variables and only use some of them, which you need inside of your directive. You can achieve this using isolated scopes.
Also, I strongly recommend to read about directives to have a basic knowledge and then continue with them.
The official documentation is a good starting point.
set the scope property to true, So the scope will be accessible in directive.
or
You can pass the data as an attribute.
app.directive('adminGridFooter', function() {
return {
restrict: 'E',
replace: true,
scope: true,
template: '<div class="gridFooter" ng-show="home.dataRetrieved" >\
<span ng-show = "home.grid.data.length > 0" >\
{{ (home.grid.data).length + " rows retrieved - " + home.grid.view.length + " displayed" }}\
</span >\
<span ng-show="home.grid.data.length === 0">There are no tests that match your selection criteria</span>\
</div>'
};
});
Or
scope:{
home:'='
}
PLUNKER

ng-repeat with compile or link?

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);

How to link ng-models inside directive?

LINK TO see visualisation oа my question LINK
I'm totally lost. Have read lots of SO replies (closest problem was here)
html:
<div title="Employment start date"
ng-model="tabsData.employment_start_date"
input-date="{{dateFormats.getCurFormat()}}"></div>
I need to show & edit (by custom directive) Date value;
How to bind (bidirectionally) my OUTER ng-model (tabsData.employment_start_date) with INNER ng-model (???) on <datepicker> ?
(see below <---------- HERE !!! sign tagretting to the <datepicker>'s ng-model attr in the snippet where I need to past Bidirectionally binding)
having some kind of directive:
directive('inputDate', function factory(dateFilter) {
return {
require:'^ngModel',
restrict:'A',
replace: true,
template:'<div class="control-group">
<div class="controls">
<label>{{title}}</label>
<input class="dateInputValue"
ng-model="formattedDate"
readonly
ng-click="showPicker=!showPicker"/>
<div class="datePickerBlock">
<button class="datePickerBtn"
ng-click="showPicker=!showPicker">
<i class="whhg icon-calendar"></i>
</button>
<datepicker
class="datePicker"
show-hide="{{showPicker}}"
ng-model=" ??? " <--------------------- HERE !!!!
show-weeks="true"
starting-day="1"
date-disabled="disabled(date, mode)">
</datepicker>
</div>
</div>
</div>',
link:function (scope, elm, attrs, ngModelCtrl) {
ngModelCtrl.$formatters.unshift(function (modelValue) {
scope.formattedDate = dateFilter(modelValue, attrs.inputDate || 'medium');
return scope.formattedDate;
});
ngModelCtrl.$parsers.unshift(function(viewValue) {
var date = new Date(viewValue);
return isNaN(date) ? '' : date;
});
}
};
});
In addition my 2nd question, why when I replace template to templateUrl property:
templateUrl: '/cached/ui-elements/inputBool.html' referencing to:
/* Template */
angular.module("/cached/ui-elements/inputDate.html", []).run(["$templateCache", function($templateCache) {
$templateCache.put("/cached/ui-elements/inputDate.html",
"<div class=\"controls\">\n"+
"<input class=\"dateInputValue\" " +
"ng-model=\"ngModelLocal\" " +
"readonly " +
"ng-click=\"showPicker=!showPicker\"/>\n"+
<MY-DATEPICKER ng-model="ngModelLocal"></MY-DATEPICKER>
"</div>"
}]);
attrs.dateFormat equals to {{dateFormats.getCurFormat()}} STRING! (without returning an actual expression execution result on the $scope as before with template property)
Guys help ;)
They way that you can do this is by using what is called "Isolate Scope" inside of your directive. If you need to understand a bit about, Egghead.io has several videos explaining the different ways to bind variables using the isolate scope.
I have done something like what you are asking for. It looks something like this:
<div ng-controller="MyCtrl">
--some additional html--
<div my-directive="blah" foo="scopevar1name" bar="scopevar2name"></div>
--some additional html--
</div
With "foo" and "bar" I pass in the name of the variables on my "MyCtrl" that I want to share with the bloody directive. Then inside the directive I have to do the following:
.directive('inputDate', function factory(dateFilter) {
return {
require:'^ngModel',
restrict:'A',
replace: true,
scope:{
"MYFOO" : "=foo",
"MYBAR" : "=bar"
},
template: "your html here",
link:function (scope, elm, attrs) {
IN HERE YOU CAN USE "scope.MYFOO" and "scope.MYBAR"
}
};
});
"scope.MYFOO" inside the directive is a reference to "MyCtrl.$scope.foo". Any changes you make in one will be reflected in the other.
Downfall, if "foo" is a non-mutable object (like a string or number or boolean or date) then this will not work. You will need to nest those primitives inside of the an actual object that can be mutated, and then bind to the object and reference it's children inside the directive. If you need to understand that more, let me know.

Resources