98

I have a form that is using markup from Bootstrap, like the following:

<form class="form-horizontal"> <fieldset> <legend>Legend text</legend> <div class="control-group"> <label class="control-label" for="nameInput">Name</label> <div class="controls"> <input type="text" class="input-xlarge" id="nameInput"> <p class="help-block">Supporting help text</p> </div> </div> </fieldset> </form> 

There's a lot of boilerplate code in there, that I'd like to reduce to a new directive - form-input, like follows:

<form-input label="Name" form-id="nameInput"></form-input> 

generates:

 <div class="control-group"> <label class="control-label" for="nameInput">Name</label> <div class="controls"> <input type="text" class="input-xlarge" id="nameInput"> </div> </div> 

I have this much working via a simple template.

angular.module('formComponents', []) .directive('formInput', function() { return { restrict: 'E', scope: { label: 'bind', formId: 'bind' }, template: '<div class="control-group">' + '<label class="control-label" for="{{formId}}">{{label}}</label>' + '<div class="controls">' + '<input type="text" class="input-xlarge" id="{{formId}}" name="{{formId}}">' + '</div>' + '</div>' } }) 

However it's when I come to add in more advanced functionality that I'm getting stuck.

How can I support default values in the template?

I'd like to expose the "type" parameter as an optional attribute on my directive, eg:

<form-input label="Password" form-id="password" type="password"/></form-input> <form-input label="Email address" form-id="emailAddress" type="email" /></form-input> 

However, if nothing is specified, I'd like to default to "text". How can I support this?

How can I customize the template based on the presence / absence of attributes?

I'd also like to be able to support the "required" attribute, if it's present. Eg:

<form-input label="Email address" form-id="emailAddress" type="email" required/></form-input> 

If required is present in the directive, I'd like to add it to the generated <input /> in the output, and ignore it otherwise. I'm not sure how to achieve this.

I suspect these requirements may have moved beyond a simple template, and have to start using the pre-compile phases, but I'm at a loss where to start.

1
  • Am I the only one seeing the elephant in the room :) --> What if type is set dynamically via binding eg. type="{{ $ctrl.myForm.myField.type}}" ? I checked all methods below and could not find any solution which will work in this scenario. Looks like template function will see literal values of the attributes eg. tAttr['type'] == '{{ $ctrl.myForm.myField.type }}' instead of tAttr['type'] == 'password'. I am puzzled. Commented Mar 29, 2019 at 14:22

4 Answers 4

211
angular.module('formComponents', []) .directive('formInput', function() { return { restrict: 'E', compile: function(element, attrs) { var type = attrs.type || 'text'; var required = attrs.hasOwnProperty('required') ? "required='required'" : ""; var htmlText = '<div class="control-group">' + '<label class="control-label" for="' + attrs.formId + '">' + attrs.label + '</label>' + '<div class="controls">' + '<input type="' + type + '" class="input-xlarge" id="' + attrs.formId + '" name="' + attrs.formId + '" ' + required + '>' + '</div>' + '</div>'; element.replaceWith(htmlText); } }; }) 
Sign up to request clarification or add additional context in comments.

8 Comments

This is a little late, but if in htmlText you added an ng-click somewhere, would the only modification be to replace element.replaceWith(htmlText) with element.replaceWith($compile(htmlText))?
@Misko, you mentioned to get rid of scope. Why? I've a directive which does not compile when used with isolated scope.
this does not work if htmlText contains an ng-transclude directive
Unfortunately I've found that form validation doesn't seem to work with this, $error flags on the inserted input never get set. I had to do this within a directive's link property: $compile(htmlText)(scope,function(_el){ element.replaceWith(_el); }); in order for the form's controller to recognize its newly formed existence and include it in validation. I could not get it to work in a directive's compile property.
Okay, it's 2015 out there and I'm pretty sure there's something awfully wrong in generating markup in scripts manually.
|
38

Tried to use the solution proposed by Misko, but in my situation, some attributes, which needed to be merged into my template html, were themselves directives.

Unfortunately, not all of the directives referenced by the resulting template did work correctly. I did not have enough time to dive into angular code and find out the root cause, but found a workaround, which could potentially be helpful.

The solution was to move the code, which creates the template html, from compile to a template function. Example based on code from above:

 angular.module('formComponents', []) .directive('formInput', function() { return { restrict: 'E', template: function(element, attrs) { var type = attrs.type || 'text'; var required = attrs.hasOwnProperty('required') ? "required='required'" : ""; var htmlText = '<div class="control-group">' + '<label class="control-label" for="' + attrs.formId + '">' + attrs.label + '</label>' + '<div class="controls">' + '<input type="' + type + '" class="input-xlarge" id="' + attrs.formId + '" name="' + attrs.formId + '" ' + required + '>' + '</div>' + '</div>'; return htmlText; } compile: function(element, attrs) { //do whatever else is necessary } } }) 

5 Comments

This solved my problem with an embedded ng-click in the template
Thanks, this worked for me too. Wanted to wrap a directive to apply some default attributes.
Thanks, I wasn't even aware that the template accepted a function!
This is not a workaround. It is the right answer to the OP. Conditionally crafting a template depending on the attributes of the element is the exact purpose of a directive/component template function. You should not use compile for that. The Angular team is heavily encouraging this style of coding (not using the compile function).
This should be the correct answer,even i was not aware template takes a function :)
5

The above answers unfortunately don't quite work. In particular, the compile stage does not have access to scope, so you can't customize the field based on dynamic attributes. Using the linking stage seems to offer the most flexibility (in terms of asynchronously creating dom, etc.) The below approach addresses that:

<!-- Usage: --> <form> <form-field ng-model="formModel[field.attr]" field="field" ng-repeat="field in fields"> </form> 
// directive angular.module('app') .directive('formField', function($compile, $parse) { return { restrict: 'E', compile: function(element, attrs) { var fieldGetter = $parse(attrs.field); return function (scope, element, attrs) { var template, field, id; field = fieldGetter(scope); template = '..your dom structure here...' element.replaceWith($compile(template)(scope)); } } } }) 

I've created a gist with more complete code and a writeup of the approach.

4 Comments

nice approach. unfortunately when using with ngTransclude i get the following error: Error: [ngTransclude:orphan] Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found.
and why not use an isolated scope with 'field: "="'?
Very nice, thanks! Unfortunately your written approach is offline :(
Both the gist and writeup are dead links.
4

Here's what I ended up using.

I'm very new to AngularJS, so would love to see better / alternative solutions.

angular.module('formComponents', []) .directive('formInput', function() { return { restrict: 'E', scope: {}, link: function(scope, element, attrs) { var type = attrs.type || 'text'; var required = attrs.hasOwnProperty('required') ? "required='required'" : ""; var htmlText = '<div class="control-group">' + '<label class="control-label" for="' + attrs.formId + '">' + attrs.label + '</label>' + '<div class="controls">' + '<input type="' + type + '" class="input-xlarge" id="' + attrs.formId + '" name="' + attrs.formId + '" ' + required + '>' + '</div>' + '</div>'; element.html(htmlText); } } }) 

Example usage:

<form-input label="Application Name" form-id="appName" required/></form-input> <form-input type="email" label="Email address" form-id="emailAddress" required/></form-input> <form-input type="password" label="Password" form-id="password" /></form-input> 

6 Comments

A better solution is to: (1) use a compile function instead of linking function and do the replacement there. The template will not work in your case since you want to customize it. (2) get rid of scope:
@MiskoHevery Thanks for the feedback - would you mind explaining why a compile function is favoured to a link function here?
I think this is the answer, from docs.angularjs.org/guide/directive : "Any operation which can be shared among the instance of directives [e.g., transforming the template DOM] should be moved to the compile function for performance reasons."
@Marty Are you still able to bind one of your custom inputs to a model? (ie. <form-input ng-model="appName" label="Application Name" form-id="appName" required/></form-input>)
@MartyPitt From O'Reilly's "AngularJS" book: "So we've got the compile phase, which deals with transforming the template, and the link phase, which deals with modifying the data in the view. Along these lines, the primary difference between the compile and link functions in directives is that compile functions deal with transforming the template itself, and link functions deal with making a dynamic connection between model and view. It is in this second phase that scopes are attached to the compiled link functions, and the directive becomes live through data binding"
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.