2

This is really a rather generic question, but as people on IRC appeared to have issues understanding what I was trying to do, I've provided some rather specific information in this post. The answer would likely be applicable to a far wider range of issues than just this specific one.

I'm looking to have one directive inherit behaviour of another directive, while also providing behaviour of its own. Below is an example case.

Say I have the following document:

<body ng-app="AppCore" dock-container>
    <pane dock="left" class="one">
        left
    </pane>
    <pane dock="fill" class="two" title="middle">
        <pane dock="top" class="four" height="100">
            middle top
        </pane>
        <pane dock="fill" class="five">
            middle fill
        </pane>
    </pane>
    <pane dock="right" class="three" title="whatever">
        right
    </pane>
</body>

Here, I have three different directives:

  • pane: An element directive. This represents a UI element (a pane or 'panel', as the name implies).
  • dock-container: An attribute directive. This acts as a modifier; it adds some bookkeeping data to the scope, and marks it as being a 'container'.
  • dock: An attribute directive. This also acts as a modifier; it indicates that the element should be positioned relative to the nearest dock-container parent - 'docking' here refers to making the element 'stick' to a side of the nearest container (or fill up the remaining space).

Now, the problem is that a pane should also implicitly be a dock-container - that is, it should behave exactly the same as an element with the dock-container directive would, in addition to pane-specific behaviour.

However, in the interest of clean markup, I don't want to have to explicitly specify dock-container on every pane - it should implicitly inherit from dock-container without having to actually specify the dock-container directive.

In other words, I'm looking to make my pane directive behave as if it were like this:

<pane dock-container>

... while it actually says this in the document:

<pane>

I don't particularly care how the pane directive inherits its behaviour from the dock-container directive, even if it means having the pane directive add the dock-container directive itself before Angular finishes processing the element. Ideally, the link function of the dock-container directive, not just the controller, would also be applied to the pane.

I am currently using element.parent().controller('dockContainer') to find the nearest dock-container from a dock directive - for this to work, the controller that I've defined for the dock-controller directive would have to be present on the pane element, regardless of the method of inheritance.

If your suggested inheritance implementation requires a different method of finding the nearest dock-container (or pane), that's fine too - the problem here is just the inheritance, the rest can be changed to accommodate the solution to this problem.

The following are not suitable options:

  • Copy-pasting the code for dock-container to the pane directive. I want to keep code duplication to a minimum, and it's quite possible that other directives in the future should also inherit from dock-container.
  • Manually adding dock-container to every pane element - this is exactly what I'm trying to avoid. The code I'm working on is meant to be used as a reusable set of directives, and I aim to keep the syntax as clean as possible. Every pane should inherit the dock-container behaviour, so there's no added value in requiring it to be specified explicitly.
  • Anything that overrides the behaviour for the pane directive. The dock-container behaviour should be an addition, not a replacement. The pane directive will also have some specific behaviour of its own, that the dock-container directive itself does not have.
  • Anything that makes it impossible for an element to have both a dock and dock-container directive - the search for the 'nearest dock-container' starts at the parent of the element, so an element will never be its own dock-container.
  • Anything that changes the type of element. The pane element should stay a pane element.

3 Answers 3

2

As a simpler alternative to my other answer, if you don't need there to be a pane element in the final rendered DOM, and no other directive has a template on the element or uses transclusion, then you can just use standard transclusion to dynamically add the dock-container directive:

app.directive('pane', function($compile) {
  return {
    restrict: 'E',
    replace: true,
    transclude: true,
    template: '<div dock-container ng-transclude></div>',
    link: function() {
      // Any special behaviour for a pane element
    }
  };
});

As seen in this Plunker

Sign up to request clarification or add additional context in comments.

Comments

0

even if it means having the pane directive add the dock-container directive itself before Angular finishes processing the element

You can do this by

  • Setting a very high priority to the pane directive
  • Set it to terminal: true to stop other directives being compiled after this one
  • In the linking function add dock-container attribute, and then recompile the element, passing the priority of the pane directive as the third parameter. This will allow all other directives with a lower priority, such as dock-container, and any other directive on the element, to run.

Example code of this is as follows

app.directive('pane', function($compile) {
  return {
    restrict: 'E',
    priority: 9999,
    terminal: true,
    link: function(scope, element, attrs, controllers, transclude) {
      element.attr('dock-container', '');
      $compile(element, null, 9999)(scope);
    }
  };
});

An example of this can be seen here

I've seen a few other suggestions about how to dynamically adding directives to elements, but this way minimises DOM manipulation, and as long as this really is the top priority directive on the element, it ensures that other directives on the same element only get compiled once, after the dynamic directive has been added.


As as sidenote/recommendation, unless I had a good reason too, in order to KISS I would just have both pane and dock-container explicitly on the same element. Yes, the template is a bit longer, but it explicitly shows what directives are in play on the element, and the interaction between the 2 directives is much more standard.

5 Comments

This solution appears to kind-of-work - the dock-container directive is added and processed correctly, but I'm now running into the following error: Error: [ngTransclude:orphan] Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element: <pane-contents ng-transclude="ng-transclude" dock="fill" height="auto" class="ng-scope">. Is it possible that the second $compile interferes with the transclude property? The pane-contents directive exists inside the pane template.
@SvenSlootweg It's hard to say without seeing the code. Post a plunker?
I've created a reduced testcase at plnkr.co/edit/x0HRNj01TVAdwkjrdR9n?p=preview - it doesn't have any of the styling or dockContainer-finding, it just sets a number of classes to demonstrate the functionality. If you look at the console log, the error I mentioned should appear. As you can see, aside from that one error, it appears to work correctly.
Ah... You're using transclusion on the pane. I suspect this throws the error due to the fact when it manually calls $compile, there is still an ng-include attribute in the DOM. I think my other solution is better in this case, as it keeps it simple, although there won't be a pane element in the DOM any more.
I hadn't gotten around to responding to your other solution yet, but I need the pane element to stay a pane element. Is there any way to make it work with this solution? It appears to still process the transclude anyway, so - unless I'm missing something - manually removing whatever is triggering it again should solve the problem. I'm just not quite sure what that "whatever" is.
0

You can try using a template with '<dock-container/>' within your pane directive defintion...

{
    template: '<dock-container/>',
    replace: true,
}

And if dockContainer exposes a controller, you can reference it in your dock directive something like...

{
    template: '<dock-container/>',
    replace: true,
    require: '^dockContainer',
    link: function ($scope, $element, $attrs, dockContainerController) {
    }
}

But please consult the docs for the appropriate require syntax, ie the ^.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.