Directive template unique IDs for elements in AngularJS

63,149

Solution 1

HTML

    <div class="myDirective">
        <input type="checkbox" id="myItem_{{$id}}" />
        <label for="myItem_{{$id}}">open myItem_{{$id}}</label>
    </div>

Solution 2

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

Solution 3

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>

Solution 4

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

Share:
63,149
NoRyb
Author by

NoRyb

I speak German, English, Spanish and un petit peu French :)

Updated on May 31, 2020

Comments

  • NoRyb
    NoRyb about 4 years

    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>
    
  • J_A_X
    J_A_X over 9 years
    Should be mentioned that since 1.3 (still in RC at the time), you can do a bind once using the notation {{:yourExpression}}.
  • NoRyb
    NoRyb over 9 years
    The one-time-binding feature is now documented here: docs.angularjs.org/guide/expression#one-time-binding
  • Mohammad tanvirul islam
    Mohammad tanvirul islam over 9 years
    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!}
  • Sai Dubbaka
    Sai Dubbaka over 9 years
    But if you're using Bootstrap you don't use that approach. Because Bootstrap doesn't allow input's to be embedded inside labels.
  • Sai Dubbaka
    Sai Dubbaka over 9 years
    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.
  • penguin359
    penguin359 almost 9 years
    Should this also combine the {{::expression}} trick from @llan's answer to avoid creating more watches?
  • BuriB
    BuriB over 8 years
    @penguin359 yeap, you can do one time binding with {{::$id}} as well. At the time my comment was made 1.3 was not out yet.
  • Nik Sumeiko
    Nik Sumeiko about 8 years
    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…
  • geoidesic
    geoidesic over 7 years
    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?
  • geoidesic
    geoidesic over 7 years
    Mohammed your example is not clear. Pleas could you add an answer which shows more clearly what you are saying?
  • NoRyb
    NoRyb about 7 years
    @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.
  • user1438038
    user1438038 about 7 years
    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?
  • BuriB
    BuriB about 7 years
    @user1438038 could you create a jsfiddle.net example to showcase us your problem ?
  • azulBonnet
    azulBonnet over 4 years
    Is there an Angular2/8 corollary for this?