8

I'm kind of new in all this angular...

I have a custom directive, lets call it myDar. Inside this directive I define a link function. In my html I want to to use multiple nested tags of this directive as follows:

<myDar id="outer"><myDar id="inner"></myDar></myDar>

I want the link function of "outer" to be executed first. How do I do this?

That's the general question. If it helps then what I'm really trying to do is to create directives that wrap jquery ui layout (link to the website). So I have a directive for "ui-layout" and directives for "center", "west" etc. In the "ui-layout" directive I call to $(tElm).layout(options). I have a problem when creating a nested layout:

<ui-layout class="layout-container">
    <ui-layout-center>
        <ui-layout>
            <ui-layout-center>inner center</ui-layout-center>
            <ui-layout-west>inner west</ui-layout-west>
        </ui-layout>        
    </ui-layout-center>
    <ui-layout-west>west</ui-layout-west>
</ui-layout>

Angular executes first the link function of the inner "ui-layout" directive but for the jquery ui layout plugin to work it requires to call $(tElm).layout(options) of the outer first, otherwise the layout is not rendered correctly.

julius_am
  • 1,422
  • 2
  • 23
  • 42

2 Answers2

8

For this you will take advantage of the directive's controller. It will be a class defining a method to register nested controllers and another one for executing the desired command (here $(...).layout(...)) on this element and then on all of its children. This means that the outer directive is responsible for coordinating the creation of the layouts.

The full example code is:

app.directive("y", function() {
    function Controller($element) {
        this.$element = $element;
        this.children = [];
    }

    Controller.prototype.register = function(child) {
        this.children.push(child);
    };

    Controller.prototype.execute = function() {
        console.log("PAYLOAD: " + this.$element.attr("id"));
        for( var i=0; i < this.children.length; i++ ) {
            this.children[i].execute();
        }
    };

    return {
        require: "y",
        controller: ["$element", Controller],
        link: function(scope, element, attrs, ctrl) {
            var e = element.parent(), nested = false;
            while( e != null ) {
                if( e.controller("y") != null ) {
                    e.controller("y").register(ctrl);
                    nested = true;
                    break;
                }
                e = e.parent();
                if( typeof(e.tagName) === "undefined" ) break; //XXX Needed, at least for fiddle
            }
            if( !nested ) ctrl.execute();
        }
    };
});

Replace the line console.log("PAYLOAD: " + this.$element.attr("id")); with the actual code to run. See relevant fiddle: http://jsfiddle.net/8xSjZ/

If the outer directive was different than the current, getting the parent controller would be as easy as requiring "?^y". In this case, it gives us the current controller, therefore we have to loop (e.parent()) ourselfes.

Nikos Paraskevopoulos
  • 39,514
  • 12
  • 85
  • 90
  • Great answer. One question: how does the e.controller("y") work? Does this directly call the constructor of the Controller class? – Faredoon Aug 25 '14 at 08:15
  • 1
    Hi, thanks. Actually `e.controller()` is an Angular extension to jqLite (or jQuery). It is documented [here](https://docs.angularjs.org/api/ng/function/angular.element) - look for "jQuery/jqLite Extras". What it does in short is that it finds an *existing* controller with the given name, attached to the element. So it does *not* call the constructor, it expects the controller to be already constructed and attached to the element. – Nikos Paraskevopoulos Aug 25 '14 at 20:35
  • One more question: Consider I do not have nested a directive, so I have something like
    followed by another
    . While processing the first div in the link function, I would like to access the second div using angular.element("div[level='2']") and subsequently the controller using .controller("y"). See this plunkr:http://plnkr.co/edit/oIpvQP. I get a JQLite exception "Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element"
    – Faredoon Aug 27 '14 at 05:06
  • You would have to include jQuery for this, so that Angular uses jQuery instead of jQlite. jQlite is an implementation of a cut-down version of the jQuery API. It does not support this kind of selector (i.e. `"div[level='2']"`). I *guess* there should be some other way to do this by *reorganizing* your logic. – Nikos Paraskevopoulos Aug 27 '14 at 14:29
2

Depending on the exact use case a compile function might be a good fit. compile functions are executed before link functions and - what's more important - in reverse order. Meaning parent first.

app.directive("ui-layout", function() {
  return {
    restrict: 'E',
    compile: function(element, attrs) {
               //do layout stuff here
               //return a link function or nothing

Another solution could be to execute the relevant code of your child directives using $timeout.

app.directive("ui-layout-center", function($timeout) {
  return {
    restrict: 'E',
    link: function(scope, element, attrs) {
            $timeout(function() {
              //do layout stuff later
             }, 0);  
a better oliver
  • 26,330
  • 2
  • 58
  • 66