Customizing the template within a Directive - javascript

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.

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

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

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.

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>

Related

Single edit hyperlink option for all the field edits

I have created an application in AngularJS with edit, save and cancel options, the application is working fine, right now i am having edit option for all the three fields, but how can i have a single edit option for making all the fields editable and save and cancel similarly
Can anyone please tell me some solution for this
DEMO
HTML
<div ng-controller="LocationFormCtrl">
<h2>Editors</h2>
<span ng-repeat="location in locations">
<div class="field">
<strong>State:</strong>
<div click-to-edit="location.state"><input ng-model="location.state"/></div>
</div>
<div class="field">
<strong>City:</strong>
<div click-to-edit="location.city">
<select ng-model="location.city" ng-options="loc.city as loc.city for loc in selectableCities"></select>
</div>
</div>
<div class="field">
<strong>Neighbourhood:</strong>
<div click-to-edit="location.neighbourhood"><input ng-model="location.neighbourhood"/></div>
</div>
<hr></hr>
</span>
</div>
I created a new directive that uses you directive. Also i updated your directive a bit.
Not sure if you wanted this.
you can find the jsFiddle demo here.
updated your directive so that it does not show individual save n cancel when in edit all mode.
you can find the updated jsFiddle demo here.
app.directive("clickToEditAll",function(){
var partial = '<div><div class="field">' +
'<strong>State:</strong>' +
'<div click-to-edit="myData.state" external-controle="externalControle"><input ng-model="myData.state"/></div>' +
' </div>' +
'<div class="field">' +
'<strong>City:</strong>' +
'<div click-to-edit="myData.city" external-controle="externalControle">' +
'<select ng-model="myData.city" ng-options="loc.city as loc.city for loc in selectableCities"></select>' +
'</div>' +
'</div>' +
'<div class="field">' +
'<strong>Neighbourhood:</strong>' +
'<div click-to-edit="myData.neighbourhood" external-controle="externalControle"><input ng-model="myData.neighbourhood"/></div>' +
'</div>' +
'<button ng-show="externalControle.editAll !== true" ng-click="editAllClicked()">Edit All</button><button ng-show="externalControle.editAll === true" ng-click="saveAllClicked()">Save</button><button ng-show="externalControle.editAll === true" ng-click="cancelAllClicked()">Cancel</button><hr/></div>'
var retValue = {
restrict: "A",
template:partial,
scope:{
myData:"=",
selectableCities:"=",
externalControle: "="
},
link:function(scope, elements, attributes){
scope.editAllClicked = function(){
scope.externalControle.editAll = true;
}
scope.saveAllClicked = function(){
scope.externalControle.saveAll = true;
}
scope.cancelAllClicked = function(){
scope.externalControle.cancelAll = true;
}
}
}
return retValue;
});

Label for not working in angularjs directive

I am using this custom template inside a directive to generate customized input fields with associated labels:
template: function(elem, attrs) {
var elemTpl = '<div> ' +
'<div class="form-group col-md-{{cols}}" >' +
'<label for="{{id}}" class="control-label input-sm">{{text}}</label> ' +
'<input type="{{type}}" ng-model="value" name="' + attrs.id + '" id="' + attrs.id +
'" class="form-control input-sm szpFocusable" placeholder="{{placeholder}}" ng-required="required" spellcheck="false"/>' +
'</div> ' +
'</div>';
return elemTpl;
}
The problem is that when the label is clicked, the input field is not being focused. When moving the code outside the directive and putting the input tags directly in the html, all works fine. The id and the name attributes are bound correctly to the appropriate scope fields.
Are you sure the ID you are using is unique?
Another option is to nest the <input> tag in the <label>, in that case you don't need the for argument:
var elemTpl = '<div> ' +
'<div class="form-group col-md-{{cols}}" >' +
'<label class="control-label input-sm">{{text}}' +
'<input type="{{type}}" ng-model="value" name="' + attrs.id + '" id="' + attrs.id + '" class="form-control input-sm szpFocusable" placeholder="{{placeholder}}" ng-required="required" spellcheck="false"/>' +
'</label>' +
'</div> ' +
'</div>';
Note:
The real answer is in the 2nd comment below: In a directive the content of element template is placed within the directive tag, in this case resulting in the id being used twice. This can be solved by adding replace : true to the return value of the directive.
This will replace your directive tag with the contents of your template.
app.directive('myDirective', function() {
return {
replace: 'true',
template: templateThatUsesId
};
});
Here there is a more accurate answer:
Attribute of label not working in angular directive?
When rendering directives with templates the container has the same id than the checkbox:
<div class="color-warning checkbox ng-isolate-scope ng-valid" ng-model="ckeck4" id="kitten>
<input id="kitten" type="checkbox">
<label for="kitten">CUSTOM LABEL</label>
</div>
The div that wrappers the rendered directive and the checkbox have the same id, not quite good.
In order to delete it we use a link function:
link: function(scope,el,attrs){
el.removeAttr("id")
}
This is because the link function is executed before angular renders the directive, so, we are removin the id from the div that wrappers the directive, here there is my complete code:
(function() {
'use strict';
angular.module('planetCheckbox', [])
.controller('PlanetCheckboxController', PlanetCheckboxController)
.directive('checkbox', DirectiveCheckbox);
function DirectiveCheckbox() {
return {
restrict: 'E',
replace: true,
scope: {
ngModel: '=',
classes: '#class',
customLabel: '#',
check: '=',
id:'#'//,
//radio :'#'
},
link: function(scope,el,attrs){
el.removeAttr("id")
},
controller: 'PlanetCheckboxController',
templateUrl: 'planet-checkbox/template.html'
}
};
PlanetCheckboxController.$inject = ['$scope'];
function PlanetCheckboxController($scope) {
$scope.ngModel = $scope.check;
};
angular.module('planetCheckbox')
.run(['$templateCache', function ($templateCache) {
$templateCache.put('planet-checkbox/template.html', '<div class="checkbox">'+
'<input id="{{id}}" type="checkbox" />' +
'<label for="{{id}}">CUSTOM LABEL</label>' +
'</div>'
);
}]);
})();
The living example is here: http://jsfiddle.net/cherniv/5u4Xp/
Label for is working for id and not for name
<label for="idxyz" class="control-label input-sm">{{text}}</label>
<input id="idxyz" type="{{type}}" ng-model="value" .....
It always works for me!

How to get an HTML element that called a function in AngularJS ?

I have an AngularJS directive, which needs to be appended after an HTML element which called it. The elements structure can have many nested buttons on different levels of DOM structure.
Right now a directive gets appended in the wrong place instead of a container element that contains the button, which called the append function.
It looks like this:
The text should be appended after a button, which was clicked.
Directive:
app.directive('recursiveFields', function ($compile, $http) {
return {
scope: {
field: '=field',
model: '=model'
},
restrict: 'E',
replace: true,
controller: "httpPostController",
template: '<div ng-repeat="nestedField in field.nestedFields"><div ng-show="{{!nestedField.isEntity && !nestedField.isEnum}}">' + '<p ng-show={{nestedField.isRequired}}>{{nestedField.name}}*: </p>' + '<p ng-show={{!nestedField.isRequired}}>{{nestedField.name}}: </p>' + '<input type="text" ng-model="model[nestedField.name]" ng-change="getCreateEntityAsText()"' + 'class="form-control" placeholder="{{parseClassName(nestedField.type)}}">' + '</div>' + '<div ng-show="{{nestedField.isEnum}}">' + '<p ng-show={{nestedField.isRequired}}>{{nestedField.name}}*: </p>' + '<p ng-show={{!nestedField.isRequired}}>{{nestedField.name}}: </p>' + '<select ng-model="model[nestedField.name]" ng-change="getCreateEntityAsText()" class="form-control">' + '<option></option>' + '<option ng-repeat="enumValue in nestedField.enumValues" label={{enumValue.name}}>{{enumValue.ordinal}}</option>' + '</select>' + '</div>' +
'<div ng-show="{{nestedField.restResourceName != null}}">' + '<accordion close-others="oneAtATime">' + '<accordion-group heading={{nestedField.name}} is-open="false">' + /*'<recursive-fields model="createEntityResource" field="field"></recursive-fields>'*/
'<button type="button" ng-click="appendDirective()">I should append a "recursiveFields" directive</button>' + '</accordion-group>' + '</accordion>' + '</div>' + '</div>',
link: function (scope, element, attrs) {
console.log("1");
if (scope.field.restResourceName != null) {
$http.get(CONSTANTS.EXPLAIN_URL + "/" + scope.field.restResourceName)
.success(function (data, status) {
scope.field.nestedFields = [];
data.content.resource.fields.forEach(function (field) {
if (field.isEnum) {
$http.get(CONSTANTS.ENUMS_URL + scope.$root.parseClassName(field.type)).success(function (data, status) {
field.enumValues = [];
for (var index in data.content.values) {
field.enumValues.push(data.content.values[index]);
}
})
}
scope.field.nestedFields.push(field);
})
})
}
scope.appendDirective = function () {
var recursiveFields = $("<p>Insert me</p>");
recursiveFields.insertAfter(element[0]);
$compile(recursiveFields)(scope);
}
}
}
})
Does anyone know how to solve this issue with Angular? Every useful answer is highly appreciated and evaluated.
Thank you.
ngClick has access to the $event that can be passed to your method like this:
<button type="button" ng-click="appendDirective($event)"
That event has a property target.
Check this: https://docs.angularjs.org/api/ng/directive/ngClick
and this: https://docs.angularjs.org/guide/expression#-event-

A way to share common template HTML and JavaScript behavior across AngularJS directives?

I'm building a form builder AngularJS app, and I have the following AngularJS directive representing the UI to edit a TextField:
angular.module('myApp.directives').directive('textFormFieldElement', ['$timeout', function($timeout) {
'use strict';
return {
restrict: 'A',
scope: {
element: '='
},
template: '<div class="form-element text-form-field">' +
' <span class="identifier">TF</span>' +
' <strong class="heading" ng-bind="headingText()"></strong>' +
' <div class="editor">' +
' <div class="form-group">' +
' <label>Enter the field name:</label>' +
' <input ng-model="element.fieldName" type="text" class="form-control" />' +
' </div>' +
' <div class="form-group">' +
' <label>Enter a label for the field:</label>' +
' <input ng-model="element.label" type="text" class="form-control" />' +
' </div>' +
' <div class="form-group">' +
' <label>Enter a note for the field:</label>' +
' <input ng-model="element.note" type="text" class="form-control" />' +
' </div>' +
' <div class="checkbox">' +
' <label>' +
' <input ng-model="element.required" type="checkbox" /> Required' +
' </label>' +
' </div>' +
' </div>' +
'</div>',
link: function(scope, element, attributes) {
scope.element.fieldName = scope.element.fieldName || 'TextField';
// Expand the editor when creating new elements, and focus on the first field once rendered.
if (!scope.element._id) {
$timeout(function() {
element.find('.editor').addClass('expanded');
element.find('.editor').find('input:first').select();
}, 10);
}
scope.headingText = function() {
if (scope.element.fieldName.length && scope.element.fieldName.length > 0) {
return scope.element.fieldName;
}
return 'TextField';
};
}
};
}]);
I also have other controls, like a PhoneField, an EmailField, and a RadioButtonListField.
These directives will often have common HTML in their templates as well as common JavaScript behavior. I'd like a way to share this across directives, without polluting the global namespace.
What are some ways this can be achieved?
For template sharing you could extract common template code into 'template directives', i.e. directives that only define template without behavior. This way you can re-use these template directives inside the templates of other directives.
For sharing common JavaScript behavior the two recommended ways are either via Angular services (service/factory) or in the case of directives using directive controller.
For the latter, see the Creating Directives that Communicate section at http://docs.angularjs.org/guide/directive on how to implement this.

Form field validation inside Angular directive not working properly

I'm using latest version of AngularJS which is 1.2rc3 along with Bootstrap for styling and have directive like this:
angular.module('form.field', [])
.directive('efield', function() {
return {
restrict: 'E',
scope: {
form: '#',
fname: '#',
label: '#'
},
template: "<div data-ng-class=\"{{form}}.{{fname}}.$valid ? 'form-group' : 'form-group has-error'\">" +
"<label class='control-label' for='{{fname}}'>{{label}}</label>" +
"<input type='text' class='form-control' name='{{fname}}' data-ng-required='false' data-ng-model='Form.test'>" +
"</div>"
}
});
html snippet looks like this:
<form name="form" novalidate="novalidate">
<efield form="form" fname="test" label="field"></efield>
</form>
I'm using directive to wrap field html and angular's validation directives to reduce boilerplate code. Problem is even if data-ng-required='false' is set on the input field, parent div is getting 'form-group has error' class instead of just 'form-group'. What am I doing wrong here?
You are running into problems due to the isolated scope you created in your directive. There are several ways to approach it. Here is one that will remove the isolated scope and use template:function(elem,attrs) instead
.directive('efield', function () {
return {
restrict: 'E',
template: function (elem, attrs) {
return "<div data-ng-class=\"" + attrs.form + "." + attrs.fname + ".$valid ? 'form-group' : 'form-group has-error'\">" +
"<label class='control-label' for='" + attrs.fname + "'>" + attrs.label + "</label>" +
"<input type='text' class='form-control' name='" + attrs.fname + "' data-ng-required='false' data-ng-model='Form.test'>" +
"</div>"
}
});
Now scope will be that of the parent scope
Take a look at this directive https://github.com/nelsonomuto/angular-ui-form-validation

Categories

Resources