I have a directive that can be used multiple times on a page. In the template of this directive, I need to use IDs for an input-Element so I can "bind" a Label to it like so:
<input type="checkbox" id="item1" /><label for="item1">open</label>
Now the problem is, as soon as my directive is included multiple times, the ID "item1" is not unique anymore and the label doesn't work correctly (it should check/uncheck the checkbox when clicked).
How is this problem fixed? Is there a way to assign a "namespace" or "prefix" for the template (like asp.net does with the ctl00...-Prefix)? Or do I have to include an angular-Expression in each id-Attribute which consists of the directive-ID from the Scope + a static ID. Something like:
<input type="checkbox" id="{{directiveID}} + 'item1'" /><label for="{{directiveID}} + 'item1'">open</label>
Edit:
My Directive
module.directive('myDirective', function () {
return {
restrict: 'E',
scope: true,
templateUrl: 'partials/_myDirective.html',
controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
...
} //controller
};
}]);
My HTML
<div class="myDirective">
<input type="checkbox" id="item1" /><label for="item1">open</label>
</div>
HTML
<div class="myDirective">
<input type="checkbox" id="myItem_{{$id}}" />
<label for="myItem_{{$id}}">open myItem_{{$id}}</label>
</div>
UPDATE
Angular 1.3 introduced a native lazy one-time binding. from the angular expression documentation:
One-time binding
An expression that starts with :: is considered a
one-time expression. One-time expressions will stop recalculating once
they are stable, which happens after the first digest if the
expression result is a non-undefined value (see value stabilization
algorithm below).
Native Solution:
.directive('myDirective', function() {
var uniqueId = 1;
return {
restrict: 'E',
scope: true,
template: '<input type="checkbox" id="{{::uniqueId}}"/>' +
'<label for="{{::uniqueId}}">open</label>',
link: function(scope, elem, attrs) {
scope.uniqueId = 'item' + uniqueId++;
}
}
})
Only bind once:
If you only need to bind a value once you should not use bindings ({{}} / ng-bind)
bindings are expensive because they use $watch. In your example, upon every $digest, angular dirty checks your IDs for changes but you only set them once.
Check this module: https://github.com/Pasvaz/bindonce
Solution:
.directive('myDirective', function() {
var uniqueId = 1;
return {
restrict: 'E',
scope: true,
template: '<input type="checkbox"/><label>open</label>',
link: function(scope, elem, attrs) {
var item = 'item' + uniqueId++;
elem.find('input').attr('id' , item);
elem.find('label').attr('for', item);
}
}
})
We add a BlockId parameter to the scope, because we use the id in our Selenium tests for example. There is still a chance of them not being unique, but we prefer to have complete control over them. Another advantage is that we can give the item a more descriptive id.
Directive JS
module.directive('myDirective', function () {
return {
restrict: 'E',
scope: {
blockId: '#'
},
templateUrl: 'partials/_myDirective.html',
controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
...
} //controller
};
}]);
Directive HTML
<div class="myDirective">
<input type="checkbox" id="{{::blockId}}_item1" /><label for="{{::blockId}}_item1">open</label>
</div>
Usage
<my-directive block-id="descriptiveName"></my-directive>
Apart from Ilan and BuriB's solutions (which are more generic, which is good) I found a solution to my specific problem because I needed IDs for the "for" Attribute of the label. Instead the following code can be used:
<label><input type="checkbox"/>open</label>
The following Stackoverflow-Post has helped:
https://stackoverflow.com/a/14729165/1288552
Related
My overall aim is to componentize all our re-usable widgets as angular directives but am struggling when trying to make an angular form error message directive. I have read numerous posts but couldn't see how to achieve this.
All my directives have isolate scope.
My main issue is that I do not know how to get the pattern attribute to bind correctly to the ng-show attribute of the at-error-message directives span element so that the error message dynamically hides\shows based on the pattern.
The outer HTML is:
<body ng-controller="atVrmLookup">
<ng-form name="vrmForm" novalidate>
<at-input name="registration" label="Registration" required model="vrmLookup.registration" minlength="3"></at-input>
<at-error-message pattern="vrmForm.registration.$error.required" message="Please enter a registration"></at-error-message>
</ng-form>
</body>
The atInput directive is
uiComponents.directive('atInput', function () {
return {
// use an inline template for increased
template: '<div><label>{{label}}</label><div><input name="{{name}}" type="text" ng-model="model" ng-minlength="{{minlength}}"/></div></div>',
// restrict directive matching to elements
restrict: 'E',
scope: {
name: '#',
label: '#',
minlength: '#',
model:'=model'
},
compile: function (element, attr, scope) {
var input = element.find('input');
if (!_.isUndefined(attr.required)) {
input.attr("required", "true");
}
},
controller: function ($scope, $element, $attrs) {
// declare some default values
}
};
});
the atErrorMessageDirective is
uiComponents.directive('atErrorMessage', function () {
return {
// use an inline template for increased
template: '<span class="error" ng-show="pattern">{{message}}</span>',
// restrict directive matching to elements
restrict: 'E',
scope: {
message: '#',
pattern: '='
},
controller: function ($scope, $element, $attrs) {
// declare some default values
}
};
});
Here is plunkr to demonstrate the issue.
http://plnkr.co/edit/5tdsqSXg0y5bfQqJARFB
Any help would be appreciated.
The input name cannot be an angular binding expression. Instead, use a template function, and build your template string:
template: function($element, $attr) {
return '<div><label>{{label}}</label><div>' +
'<input name="' + $attr.name + '" type="text" ng-model="model" ng-minlength="{{minlength}}"/>' +
'</div></div>';
}
Alternate Solution
Implement a dynamic-name directive, and use the API exposed by the angular form directive to programmatically set the name of the ngModel, and add the ngModel control to the form:
.directive("dynamicName",[function(){
return {
restrict:"A",
require: ['ngModel', '^form'],
link:function(scope,element,attrs,ctrls){
ctrls[0].$name = scope.$eval(attrs.dynamicName) || attrs.dynamicName;
ctrls[1].$addControl(ctrls[0]);
}
};
}])
Then in your template:
template: '<div><label>{{label}}</label><div><input dynamic-name="{{name}}" type="text" ng-model="model" ng-minlength="{{minlength}}"/></div></div>',
Note: This solution works because fortunately, ngForm provides programmatic access to customize its behavior. If ngForm's controller had not exposed an API, then you might have been SOL. It's good practice to think of the API you expose from your own custom directive's controllers - you never know how it can be used by other directives.
How do I create an angular directive that adds a input in a form but also works with form validation.
For instance the following directive creates a text input:
var template = '\
<div class="control-group" ng-class="{ \'error\': {{formName}}[\'{{fieldName}}\'].$invalid}">\
<label for="{{formName}}-{{fieldName}} class="control-label">{{label}}</label>\
<div class="controls">\
<input id="{{formName}}-{{fieldName}}" name="{{fieldName}}" type="text" ng-model="model" />\
</div>\
</div>\
';
angular
.module('common')
.directive('formTextField', ['$compile', function ($compile) {
return {
replace: true,
restrict: 'E',
scope: {
model: '=iwModel',
formName: '#iwFormName',
fieldName: '#iwFieldName',
label: '#iwLabel'
},
transclude: false,
template: template
};
}])
;
However the input is not added to the form variable ($scope.formName). I've been able to work around that by using the dynamicName directive from the following stackoverflow question Angularjs: form validation and input directive but ng-class will still not work.
Update
It now seems to be working but it feels like a hack. I thought I needed the scope to grab the form and field names however I can read that directly from the attributes. Doing this shows the field in the controllers scope form variable. However the ng-class attribute does not apply. The only way around this is to add the html element a second time once the scope is available.
jsFiddle here
var template = '\
<div class="control-group">\
<label class="control-label"></label>\
<div class="controls">\
<input class="input-xlarge" type="text" />\
</div>\
</div>\
';
angular
.module('common')
.directive('formTextField', ['$compile', function ($compile) {
return {
replace: true,
restrict: 'E',
scope: false,
compile: function compile(tElement, tAttrs, transclude) {
var elem = $(template);
var formName = tAttrs.iwFormName;
var fieldName = tAttrs.iwFieldName;
var label = tAttrs.iwLabel;
var model = tAttrs.iwModel;
elem.attr('ng-class', '{ \'error\': ' + formName + '[\'' + fieldName + '\'].$invalid }');
elem.find('label').attr('for', formName + '-' + fieldName);
elem.find('label').html(label);
elem.find('input').attr('id', formName + '-' + fieldName);
elem.find('input').attr('name', fieldName);
elem.find('input').attr('ng-model', model);
// This one is required so that angular adds the input to the controllers form scope variable
tElement.replaceWith(elem);
return {
pre: function preLink(scope, iElement, iAttrs, controller) {
// This one is required for ng-class to apply correctly
elem.replaceWith($compile(elem)(scope));
}
};
}
};
}])
;
when I do something like this, I use the directive compile function to build my html prior to it being processed. For example:
myApp.directive('specialInput', ['$compile', function($compile){
return {
// create child scope for control
scope: true,
compile: function(elem, attrs) {
// use this area to build your desired dom object tree using angular.element (eg:)
var input = angular.element('<input/>');
// Once you have built and setup your html, replace the provided directive element
elem.replaceWith(input);
// Also note, that if you need the regular link function,
// you can return one from here like so (although optional)
return function(scope, elem, attrs) {
// do link function stuff here
};
}
};
}]);
I'm developing a AngularJS Directive that is a table. The parent most control ng-table needs to have a isolated scope for its options and model.
However, the children should 'inherit' that scope so that you don't have to pass tons of options to a very interconnected group of components.
For example:
<div ng-table="tableOptions" ng-model="results">
<div ng-table-header>
<div ng-table-header-column="column"
ng-repeat="column in tableOptions.columns">
{{column.name}}
</div>
</div>
<div ng-table-body>
<div ng-table-row="row"
ng-repeat="row in $results">
<div ng-table-cell="column"
ng-repeat="column in tableOptions.columns">
{{row[column.id]}}
</div>
</div>
</div>
<div ng-table-footer></div>
</div>
In the above example, ng-table-header, ng-header-column, etc all need to access properties from the parent control ng-table. Furthermore, all the directives replace and transclude.
I know I could broadcast events to the children/parent directives but is there a better way to restrict scope at the parent level and auto pass it to the children?
Oh dude, you have forced me to look how forms and inputs directives are implemented in angular.
So, angularjs has similar thing to what you want to implement - ng-form directive with ng-input.
When you use form or ng-form directive, it creates controller(directives can do it) and transclude html-code will inherit it. ng-input(input) asks for it in require option.
require: ['ngModel', '^?form', '^?ngModelOptions'],
So you get this controller in link function. So... you can call functions, do things, you know.
You can do it like this(just an idea, this is how it works):
.directive('coolTable', function() {
return {
...
controller: function($scope) {
$scope.columns = []; /* Here you will have all table columns, so you can send it's data to service and do any work */
$scope.registerColumn = function(column) {
$scope.columns.push(column);
};
},
...
};
})
.directive('column', function() {
return {
...
require: '^?coolTable',
...
link: function(scope, element, attrs, coolTable) {
coolTable.registerColumn(this);
},
};
})
<coolTable>
<column></column>
<column></column>
<column></column>
</coolTable>
I was able to find a work around looking at angular-ui code that created their own transclude directive to apply scope.
so my master table directive starts off like:
module.directive('ngTable', function ($rootScope, $timeout, $window, $compile) {
return {
restrict: 'AE',
transclude: true, // NOTE THIS
replace: true, // NOTE THIS
templateUrl: 'common/components/table/views/table.tpl.html',
scope: {
ngModel: "=",
tableOptions: "=ngTable"
},
controller: 'ngTableCtrl',
link: function ($scope, $element, $attributes, controller, ngModel) {
....
}
}
});
then I create my own transclude directive like:
module.directive('tableTransclude', function () {
return {
link: function ($scope, $element, $attrs, controller, $transclude) {
$transclude($scope, function (clone) {
$element.empty();
$element.append(clone);
});
}
};
})
and the usage in the template of table looks like:
<div table-transclude />
And presto!!! A little hacky but gets the job done. I understand why Angular did this but in some cases you need this type of scoping.
Not sure, how to describe this question more. So there is simple code and jsfiddle
html
<div>
<span format="the value is: {{value||'no-val'}}" value="100" my-test></span>
</div>
and javascript
App.directive('myTest', function() {
return {
restrict: 'A',
replace: true,
scope: {
format: '#'
},
template: "<span>{{format}}</span>",
link: function($scope, element, attrs) {
$scope.value = attrs.value
}
}
})
http://jsfiddle.net/VhvEy/2
My expectation <span>the value is: 100</span>
Reality <span>the value is: no-val</span>
Thanks for explanation!
When you are interpolating value it needs to refer to a scoped property in a controller.
Here is a working fiddle.
So, you need to add a controller:
App.controller('Ctrl', function($scope) {
$scope.value = 100;
})
and wire up the controller with ng-controller:
<div ng-controller="Ctrl">
The value attribute on the <span> will not automatically be bound to the angular scope.
EDIT:
If you really want to interpolate the template in the directive, you can override the compile function inside the directive:
App.directive('myTest', function($compile) {
return {
restrict: 'A',
scope: {
value: '#'
},
compile:function(element, attrs) {
var strTemplate = "<span>{{" + attrs.format +"}}</span>";
element.replaceWith(strTemplate);
}
}
})
To do this, don't interpolate in the html, just send through the text that you want to interpolate in the directive:
<span format="value || 'no-val'" value="100" my-test></span>
Here is an adapted working fiddle that shows this in action.
I am implementing a form builder in AngularJS and need to insert and reorder directives at runtime.
Don't even know where to start looking - all examples seem to only demonstrate static tree of directives. Two options to achieve dynamic behaviour are: a) compiling and inserting templates on the fly and b) using huge ng-switch of all possible directives. Both ways are ugly.
Can anyone suggest a better implementation?
Below is JS and html code for how I think formbuilder should look in an ideal world, please help me fill in 3 instances of TODO.
JSFiddle JavaScript:
angular.module('components', [])
.directive('checkbox', function() {
return {
restrict: 'E',
template: '<div class=f><input type=checkbox>{{name}}</input></div>'
};
})
.directive('textfield', function() {
return {
restrict: 'E',
template: '<div class=f><input type=text placeholder="{{name}}"></input></div>'
};
})
function FormBuilder($scope, $locale) {
$scope.title = 'test form';
$scope.fields = [];
$scope.add_checkbox = function() {
console.log('adding checkbox');
var field = null; // TODO: how do I instantiate a directive?
$scope.fields.push(field);
};
$scope.add_textfield = function() {
console.log('adding textfield');
var field = null; // TODO: how do I instantiate a directive?
$scope.fields.push(field);
};
}
HTML:
<div ng-app=components ng-controller=FormBuilder>
<button ng:click="add_checkbox()">new checbox</button>
<button ng:click="add_textfield()">new text field</button>
<h3>{{ title }}</h3>
<checkbox></checkbox>
<textfield></textfield>
<div ng:repeat="field in fields">
<!-- TODO field.get_html() - how? -->
</div>
</div>
I think you have a couple ways to do this as you mentioned and since you don't want to do a switch you can create a template file for each directive. ie checkbox.html, textfield.html and put the directive in each one. Then populate your fields array with ['checkbox.html', 'textarea.html'] when you add in your loop you just simply <div ng-include='field'></div>
Here is a demo: http://plnkr.co/edit/w6n6xpng6rP5WJHDlJ3Y?p=preview
You could also create another directive where you pass in the input type and have it inject it into the template. Here is a demo of this which allows you to avoid having to declare templates and letting a directive create them based on the field type:
http://plnkr.co/jhWGuMXZTuSpz8otsVRY
<div ng:repeat="field in fields">
<master-field type='field'></master-field>
</div>
This master-field directive just compiles a template based on the value of field.
.directive('masterField', function($compile) {
return {
restrict: 'E',
replace:true,
transclude: true,
scope:{
type:'='
},
template: '<div></div>',
controller: function ( $scope, $element, $attrs ) {},
link: function(scope, element, attrs) {
element.append( $compile('<' + scope.type+ '/></' +scope.type + '>')(scope) );
}
};
})