16

I had the idea to wrap inputs into custom directives to guarantee a consistent look and behavior through out my site. I also want to wrap bootstrap ui's datepicker and dropdown. Also, the directive should handle validation and display tooltips.

The HTML should look something like this:

<my-input required max-length='5' model='text' placeholder='text' name='text'/>

or

<my-datepicker required model='start' placeholder='start' name='start'/>

in the directives i want to create a dom structure like:

<div>
 <div>..</div> //display validation in here
 <div>..</div> //add button to toggle datepicker (or other stuff) in here
 <div>..</div> //add input field in here
</div>

I tried various ways to achieve this but always came across some tradeoffs:

  1. using transclude and replace to insert the input into the directives dom structure (in this case the directive would be restricted to 'A' not 'E' like in the example above). The problem here is, that there is no easy way to access the transcluded element as I want to add custom attributes in case of datepicker. I could use the transclude function and then recompile the template in the link function, but this seems a bit complex for this task. This also leads to problems with the transcluded scope and the toggle state for the datepicker (one is in the directives scope, the other in the transcluded scope).

  2. using replace only. In this case, all attributes are applied to the outermost div (even if I generate the template dom structure in the compile function). If I use just the input as template, then the attributes are on the input, but I need to generate the template in the link function an then recompile it. As far as I understand the phase model of angular, I would like to avoid recompiling and changing the template dom in the link function (although I've seen many people doing this).

Currently I'm working with the second approach and generating the template in the link function, but I was wondering if someone had some better ideas!

roemer
  • 537
  • 1
  • 5
  • 21
  • What sort of custom attributes do you want to be able to add? A full example of the HTML you'd want the directive to render into (custom attribs and all) would be useful here, I think. – S McCrohan Dec 19 '13 at 02:33
  • In case of the datepicker i would like to sett application wide standard values. The resulting html input tag should look like: `` – roemer Dec 19 '13 at 13:06

4 Answers4

8

Here's what I believe is the proper way to do this. Like the OP I wanted to be able to use an attribute directive to wrapper an input. But I also wanted it to work with ng-if and such without leaking any elements. As @jantimon pointed out, if you don't cleanup your wrapper elements they will linger after ng-if destroys the original element.

app.directive("checkboxWrapper", [function() {
    return {
      restrict: "A",
      link: function(scope, element, attrs, ctrl, transclude) {
        var wrapper = angular.element('<div class="wrapper">This input is wrappered</div>');

        element.after(wrapper);
        wrapper.prepend(element);

        scope.$on("$destroy", function() {
          wrapper.after(element);
          wrapper.remove();
        });
      }
    };
  }
]);

And here's a plunker you can play with.

IMPORTANT: scope vs element $destroy. You must put your cleanup in scope.$on("$destroy") and not in element.on("$destroy") (which is what I was originally attempting). If you do it in the latter (element) then an "ngIf end" comment tag will get leaked. This is due to how Angular's ngIf goes about cleaning up its end comment tag when it does its falsey logic. By putting your directive's cleanup code in the scope $destroy you can put the DOM back like it was before you wrappered the input and so ng-if's cleanup code is happy. By the time element.on("$destroy") is called, it is too late in the ng-if falsey flow to unwrap the original element without causing a comment tag leak.

BrandonLWhite
  • 1,866
  • 1
  • 23
  • 26
  • 1
    [Angular's jqLite has `wrap`](https://docs.angularjs.org/api/ng/function/angular.element#angular-s-jqlite). Why not use that vs the `after` and `prepend`? – TheSharpieOne Jun 18 '15 at 17:46
  • Trying `wrap` caused me some difficulty. For sure it would be cleaner/leaner syntax, but for some reason the cleanup doesn't work when using `wrap`. Try the `wrap` in the Plunker then toggle the Show checkbox (toggles the ngIf condition). I admit that I didn't investigate that further, but I'm interested in the details if anyone wants to look further into it. – BrandonLWhite Jul 16 '15 at 18:39
2

Why not doing a directive like that?

myApp.directive('wrapForm', function(){
    return {
        restrict: 'AC',
        link: function(scope, inputElement, attributes){                       
            var overallWrap = angular.element('<div />');
            var validation = angular.element('<div />').appendTo(overallWrap);
            var button = angular.element('<div />').appendTo(overallWrap);
            var inputWrap = angular.element('<div />').appendTo(overallWrap);

            overallWrap.insertBefore(inputElement);
            inputElement.appendTo(inputWrap);

            inputElement.on('keyup', function(){
                if (inputElement.val()) {
                    validation.text('Just empty fields are valid!');
                } else {
                    validation.text('');
                }
            });            
        }
    }
});

Fiddle: http://jsfiddle.net/bZ6WL/

Basically you take the original input field (which is, by the way, also an angularjs directive) and build the wrappings seperately. In this example I simply build the DIVs manually. For more complex stuff, you could also use a template which get $compile(d) by angularjs.

The advantage using this class or html attribute "wrapForm": You may use the same directive for several form input types.

Armin
  • 15,582
  • 10
  • 47
  • 64
  • I had seen this in fiddle but this is not working for me, don't know why.I have jquery-1.10.2 and angular.js in my application and still this doesn't work!! – Mital Pritmani Apr 15 '14 at 10:34
  • 1
    Careful - if you use this with ng-if it won't clear up the wrappers! – jantimon Sep 09 '14 at 12:29
  • This is a good suggestion but angular does not have appendTo or insertBefore. Append and after (respectively) can be used instead though to achieve a similar outcome. – Theo Dec 30 '14 at 10:53
2

Why not wrap the input in the compile function? The advantage is that you will not have to copy attributes and will not have to cleanup in the scope destroy function. Notice that you have to remove the directive attribute though to prevent circular execution.

(http://jsfiddle.net/oscott9/8er3fu0r/)

angular.module('directives').directive('wrappedWithDiv', [
    function() {
        var definition = {
            restrict: 'A',
            compile: function(element, attrs) {
                element.removeAttr("wrapped-with-div");
                element.replaceWith("<div style='border:2px solid blue'>" +
                    element[0].outerHTML + "</div>")
            }
        }
        return definition;
    }
]);
Jaime Gómez
  • 6,961
  • 3
  • 40
  • 41
Orri Scott
  • 29
  • 1
0

Based on this: http://angular-tips.com/blog/2014/03/transclusion-and-scopes/

This directive does transclusion, but the transcluded stuff uses the parent scope, so all bindings work as if the transcluded content was in the original scope where the wrapper is used. This of course includes ng-model, also min/max and other validation directives/attributes. Should work for any content. I'm not using the ng-transclude directive because I'm manually cloning the elements and supplying the parent(controller's) scope to them. "my-transclude" is used instead of ng-transclude to specify where to insert the transcluded content.

Too bad ng-transclude does not have a setting to control the scoping. It would make all this clunkyness unnecessary. And it looks like they won't fix it: https://github.com/angular/angular.js/issues/5489

    controlsModule.directive('myWrapper', function () {
        return {
            restrict: 'E',
            transclude: true,
            scope: {
                label: '@',
                labelClass: '@',
                hint: '@'
            },

            link: link,
            template:
                '<div class="form-group" title="{{hint}}"> \
                    <label class="{{labelClass}} control-label">{{label}}</label> \
                    <my-transclude></my-transclude> \
                 </div>'
        };

        function link(scope, iElement, iAttrs, ctrl, transclude) {

            transclude(scope.$parent,
                function (clone, scope) {

                    iElement.find("my-transclude").replaceWith(clone);

                    scope.$on("$destroy", function () {
                        clone.remove();
                    });
                });
        }
    });
Zar Shardan
  • 5,675
  • 2
  • 39
  • 37