Directive template unique IDs for elements in AngularJS
Asked Answered
K

4

71

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>
Kyla answered 9/1, 2014 at 13:44 Comment(0)
H
94

HTML

    <div class="myDirective">
        <input type="checkbox" id="myItem_{{$id}}" />
        <label for="myItem_{{$id}}">open myItem_{{$id}}</label>
    </div>
Howdah answered 9/1, 2014 at 15:2 Comment(8)
Elegant solution. $id is the unique scope id provided by $rootScope for every child scope. Obviously that can be used for any view which will have a different scope which mostly is the case.Dallon
Should this also combine the {{::expression}} trick from @llan's answer to avoid creating more watches?Subtractive
@Subtractive yeap, you can do one time binding with {{::$id}} as well. At the time my comment was made 1.3 was not out yet.Howdah
Interesting that it is not possible to query nested elements by dynamic selector inside directive's link method: element.find(`#myItem_ ${scope.$id}`), because template haven't compiled its dynamic values yet…Haldi
Is there any solution to this? (i.e. Nik's comment). What if within the link function you need to modify the elements with dynamic id... is there no way to achieve this?Estradiol
In my case id of label and text field are different, though both are nested in the same parent div. Does this only work when parent is a directive?Crop
@Crop could you create a jsfiddle.net example to showcase us your problem ?Howdah
Is there an Angular2/8 corollary for this?Inadvisable
M
51

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);
        }
    }
})
Miletus answered 9/1, 2014 at 14:6 Comment(5)
Should be mentioned that since 1.3 (still in RC at the time), you can do a bind once using the notation {{:yourExpression}}.Gingili
The one-time-binding feature is now documented here: docs.angularjs.org/guide/expression#one-time-bindingKyla
we can use this technique also by following approach link: function(scope, elem, attrs) { var item = 'item' + uniqueId++; elem.find('input').attr('id' , item); var element=document.getElementById(item); now in element you will get dom object and you can do anything :). it's really good technique for dynamically manipulate dom where element id is set dynamically!}Monogamist
Mohammed your example is not clear. Pleas could you add an answer which shows more clearly what you are saying?Estradiol
@Mohammadtanvirulislam DOM manipulation is generally a bad Idea. Why would you want to do what you stated? And it doesn't really add something to the original question/answer - it's an entirely different topic.Kyla
F
2

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>
Frausto answered 5/5, 2015 at 6:51 Comment(0)
K
1

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://mcmap.net/q/217353/-how-can-i-use-the-for-attribute-of-a-label-tag-without-the-id-attribute-on-the-input-tag

Kyla answered 9/1, 2014 at 16:11 Comment(1)
But if you're using Bootstrap you don't use that approach. Because Bootstrap doesn't allow input's to be embedded inside labels.Dallon

© 2022 - 2024 — McMap. All rights reserved.