0

I'm using vanilla AngularJS v1.4.5 (no jQuery) and would like my custom directive to add an attribute to its grandparent element at compile time.

In the compile function, I can achieve this using the parent() method of element twice to get the grandparent element, and the attr() method to add my attribute. However, if the parent element has the ngIf directive, the grandparent element does not get the attribute.

angular.module('myApp', [])
    .directive('foo', fooDirective)
;

function fooDirective() {
    return {
        compile : compile,
        priority: 601 // ngIf is 600
    };

    function compile(element, attrs) {
        var parent, grandparent;

        parent = element.parent();            
        grandparent = parent.parent();

        parent.attr('foo', 'bar');
        grandparent.attr('foo', 'bar');
    }
}

JSFiddle

Here's what I know:

  • If ngIf is not used on the parent element, the attribute gets added to the grandparent.
  • The problem should not be related to scope, since this is taking place during the compile phase, before scope has been linked to any elements.
  • My compile function should be running before that of ngIf, which has a priority of 600 (and doesn't have a compile function).
  • ngIf completely removes and recreates the element in the DOM (along with its child elements), but that should not affect the grandparent element or change it's attributes.

Can anyone explain to me why I cannot add an attribute to my directive's grandparent element if the parent element has the ngIf directive?

Shaun Scovil
  • 3,905
  • 5
  • 39
  • 58
  • Just to be a stickler Angular by default uses jqLite so whether you like it or not you are using jQuery ;) – ductiletoaster Sep 16 '15 at 23:25
  • Directives typically deal only with their DOM subtree. It's odd to try to change an ancestor. You are also not supposed to do [DOM manipilation in compile other than to the template](https://docs.angularjs.org/api/ng/service/$compile#-compile-). What is your use case? As for `ngIf` - in the compile phase, `ngIf` transcludes your directive which compiles it at that point, but the element is not yet in the DOM until the link-phase – New Dev Sep 16 '15 at 23:35
  • @NewDev the attribute bit was a part of it, but my actual goal is to modify the template by moving the custom directive element so that it is a sibling of the parent element, apposed to a child. Regarding your comment about transclude: I was under the impression that the transclude function gets run after the compile function. You sure it runs before? – Shaun Scovil Sep 17 '15 at 00:25
  • @ductiletoaster to be an even bigger stickler, jqLite is a forked version of jQuery with its own features...so unless you include it, you're not using jQuery. :-) – Shaun Scovil Sep 17 '15 at 00:31
  • 1
    @ShaunScovil, can you append a use case to the question - it might help, because it seems what you trying to do is somewhat unnatural. The transclude function that gives the directive the clone of the transcluded content runs at link-time (so, after the compile), but the transclusion itself of the content (that is, the yanking out and compilation of the content) happens at compile-time. – New Dev Sep 17 '15 at 03:16

2 Answers2

2

So, to restate, the question is, why given the following:

<grand-parent>
  <parent ng-if="condition">
    <foo></foo>
  </parent>
</grand-parent>

when attempting to retrieve var grandparent = tElement.parent().parent() from within a compile of foo, grandparent doesn't refer to the <grand-parent> element.

The answer is because of ngIf-caused transclusion, even if condition === true.

Transclusion is the process where the contents (or the element + the contents, depending on the type of transclusion) are yanked out of DOM, compiled, and then made available, as a clone, to a transclusion function, which by itself is available as the 5th parameter of link function:

link: function(scope, element, attrs, ctrls, transcludeFn){
  transcludeFn(scope, function cloneAttachFn(clonedContent){

    // clonedContent is the subtree that was transcluded, compiled and cloned
    element.append(clonedContent);
  });
}

So, the compilation pass starts at <grand-parent>, then goes to <parent>, where it sees a directive - ngIf. Because ngIf has transclude: "element", it yanks <parent><foo></foo></parent> out of DOM, and compiles that. And so, the compilation proceeds to compile other lower-priority directives (if available) on <parent>, then compiles the foo directive.

At this point, <foo> is not under <grand-parent> and tElement.parent().parent() yields [].

New Dev
  • 48,427
  • 12
  • 87
  • 129
  • 1
    Perfect, thank you. As an additional resource, I found this comprehensive article on the topic: http://teropa.info/blog/2015/06/09/transclusion.html – Shaun Scovil Sep 17 '15 at 10:50
0

I am not entirely sure yet why this is happening but do have any particular reason why you are doing this with compile? I adjusted your directive to use link and it seemed to work just fine.

(function () {
    'use strict';

    angular.module('myApp', [])
        .directive('foo', fooDirective)
    ;

    function fooDirective() {
        return {
            link : link,
            priority: 601 // ngIf is 600
        };

        function link($scope, element, attrs) {
            var parent, grandparent;

            parent = element.parent();            
            grandparent = parent.parent();           
            parent.attr('foo', 'bar');
            grandparent.attr('foo', 'bar');
        }
    }

})();

Edit: Just like @NewDev said you should typically do DOM manipulation in the link-phase and not during compile.

ductiletoaster
  • 483
  • 2
  • 9
  • Yeah, in addition to adding an attribute I am trying to modify the template so that the element with my custom directive becomes a sibling of parent. I don't think I can do it in the link function because if the `ngIf` value evaluates to false, my child element will be removed from the DOM. – Shaun Scovil Sep 17 '15 at 01:20
  • Is there every a time where you don't want the "child" element moved? It seems to me the "child" should probably be a template of the directive and then added dynamically into the DOM at link time. Finally, have your directive set on this parent instead of this child which avoids this whole mess. I am probably over simplifying but perhaps you could explain the entire process you are try to accomplish – ductiletoaster Sep 17 '15 at 02:36
  • @ShaunScovil, but your question is only about adding an attribute. This answer provides the solution. And if `ng-if` removes your child from DOM, that is actually "by design" - i.e. that would be the expectation of someone using `ng-if`. So, I think this solves your question as stated, but to address the bigger issue (with making the element a sibling of grandparent), I suggest you ask a new question and explain what you are trying to do. Otherwise, we might be solving an [XY question](http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). – New Dev Sep 17 '15 at 03:26
  • @NewDev to be clear, my question was "why can't I add an attribute..." not "how do I...", so your comment on the question is more or less the answer I'm looking for -- though I am having Internet connection issues at the moment and only have my mobile phone to work with. If you can explain or reference a doc that explains more about the transclude lifecycle, I'd accept that. – Shaun Scovil Sep 17 '15 at 04:01