10

My directive setup is as follows:

<div data-directive-a data-value="#33ff33" data-checked="true">
  <div data-directive-b></div>
</div>
  • I'm using transclusion to ensure directiveB gets rendered.
  • directiveA has a checkbox that is meant to change some value whenever it is checked.
  • this value needs to be accessible in directiveA and directiveB's scope.

I've managed to do this, but only by referencing $$prevSibling - is there a better way?

Here's the code: http://jsfiddle.net/janeklb/yugQf/ (in this sample, clicking the checkbox is simply meant to "clear" the value)

--

A bit more depth: The 'contents' of directiveA (that which is being transcluded into it) isn't always directiveB. Other directiveB-like directives will end up in there as well. The directiveB "types" will always be used within directiveA.

jlb
  • 19,090
  • 8
  • 34
  • 65
  • 2
    I recommend you using require definition in directive and pass parent controller to child controller – Ajay Beniwal Apr 04 '13 at 19:21
  • 1
    Why are these directives at all considering they don't do anything? If they are just illustrative of the issue, would these directives *always* be used together or could they potentially be used separately? Do you want directiveB to be transcluded always in reference to directiveA's scope instead of a new child of the parent? Why does directiveA have an isolate scope but directiveB declares no new scope at all? – Josh David Miller Apr 04 '13 at 19:23
  • 1
    @JoshDavidMiller I've added a bit more depth to the description above. With respect to no declared scope on `B` and the isolate scope on `A` -- I don't have it set up that way for any reason in particular. All I know is that I need to pass some data from the DOM to the directive(s), and the best way seemed to be `scope: { xxx: '@' }` – jlb Apr 04 '13 at 21:17
  • 1
    Thanks for the added clarification. The reason you're using `$$prevSibling` is because one is an isolate scope and the other is not. Will the `directiveB` "types" contain values from the parent scope or only from the scope of `directiveA`? – Josh David Miller Apr 04 '13 at 21:24
  • @Ajaybeni For this case I'd like to assume no parent controller, as it doesn't (currently) have the data model in it's scope. – jlb Apr 04 '13 at 21:24
  • @JoshDavidMiller I'm currently foreseeing that `directiveB` types will only reference scope from `directiveA` – jlb Apr 04 '13 at 21:25

2 Answers2

12

To avoid coupling your components together too much, I would avoid using $$prevSibling. The best solution since your directiveB-like components are expected to be used within directiveA components is to use require.

.directive( 'directiveB', function () {
  return {
    require: '^directiveA',
    scope: true,
    link: function ( scope, element, attrs, directiveA ) {
      scope.obj = directiveA.getObj();
    }
  };
})

The ^require indicates that somewhere on the element of this directive or on any element above it in the DOM hierarchy is a directive called directiveA, and we want to call methods on its controller.

.directive( 'directiveA', function () {
  return {
    // ...
    controller: function ( $scope ) {
      // ...
      this.getObj = function () {
        return $scope.obj;
      };
    }
  };
})

So now in directiveB you can use ng-model="obj.attr".

There are many variations on this, but considering how general the question was, I feel this is the best approach. Here's an updated Fiddle: http://jsfiddle.net/yugQf/7/.

Josh David Miller
  • 120,525
  • 16
  • 127
  • 95
  • In your fiddle, isolate scope properties `attr` and `value` are not used and can therefore be removed. – Mark Rajcok Apr 04 '13 at 22:32
  • @MarkRajcok - You're right. They came from the Fiddle he posted and he indicated in response to my question on his post that he wanted them, but they are not used. I removed them just the same to avoid confusion. Once again, thank you sir for the tip! – Josh David Miller Apr 04 '13 at 22:40
  • The $watch in directiveB is not needed. Since getObj() returns an object, simply assigning it to directiveB's scope is sufficient, because it will be a reference to the object in directiveA. So all that is needed in the link function is `scope.obj = directiveA.getObj();`. In this [Fiddle](http://jsfiddle.net/mrajcok/ffmC6/) I used `obj2` in directiveB to more clearly demonstrate the concept. Also, in the sample code above for directiveB, you have `scope: true`, but your fiddle doesn't have that. Which did you prefer/intend? (just curious, it works either way) – Mark Rajcok Apr 05 '13 at 03:44
  • You're right; I removed the `$watch` as the reference is sufficient. And I meant to create the child scope; it just didn't make its way to the Plunker. Anytime a directive creates a scope var, we need to at least use a child scope. Even though in this case we know that it *should* exist in a child scope anyway due to transclusion, it's always best to do it the right way now to prevent an odd regression later. Thanks for pointing these out. I've got to stop trying to answer these half-asleep... – Josh David Miller Apr 05 '13 at 04:48
  • Thanks for the rundown and the fiddle @JoshDavidMiller! – jlb Apr 05 '13 at 10:36
7

@Josh mentioned in his answer that

The best solution since your directiveB-like components are expected to be used within directiveA components is to use require.

I've been toying with this and I believe a controller on directiveA is the only solution (so +1 Josh). Here's what the scopes look like using the OP's fiddle: scopes picture

(Reverse the brown arrow and you have $$previousSibling instead of $$nextSibling.)

Other than $$previousSibling, scope 004 has no path to isolate scope 003. Note that scope 004 is the transcluded scope that directiveA creates, and since directiveB does not create a new scope, this scope is also used by directiveB.

Since the object you wish to share with directiveB is being created in directiveA's controller, we also can't use attributes to share data between the directives.


Creating a model inside a directive, and then sharing that model to the outside world is rather atypical. Normally, you'll want to define your models outside your directives and even outside your controllers (listen for a few minutes to Misko). Services are often a good place to store your models/data. Controllers should normally reference the parts of the model(s) that need to be projected into the view that they are associated with.

For simplicity, I'm going to define the model on a controller, then the directives will both access this model the normal way. For pedagogical purposes, directiveA will still use an isolate scope, and directiveB will create a new child scope using scope: new as in @Josh's answer. But any type (isolate, new child, no new scope) and combination will work, now that we have the model defined in a parent scope.

Ctrl:

$scope.model = {value: '#33ff33', checkedState = true};

HTML:

<div ng-controller="NoTouchPrevSibling">
   <div data-directive-a data-value="model.value" data-checked="model.checkedState">
      <div data-directive-b></div>
   </div>

For other pedagogical reasons, I opted to pass directiveA the two model properties as separate attributes, but the entire model/object could also have been passed. Since directiveB will create a child scope, it doesn't need to pass any attributes since it has access to all of the parent/controller scope properties.

Directives:

app.directive('directiveA', function () {
    return {
        template: '<div>' 
            + 'inside parent directive: {{checkedState}}'
            + '<input type="checkbox" ng-model="checkedState" />'
            + '<div ng-transclude></div>'
            + '</div>',
        transclude: true,
        replace: true,
        scope: {
              value: '=',
              checkedState: '=checked'
            },
    };
});
app.directive('directiveB', function () {
    return {
        template: '<div>' 
            + '<span>inside transcluded directive: {{model.checkedState}}</span>'
            + '<input type="text" ng-model="model.value" />'
            + '</div>',
        replace: true,
        scope: true
    };
});

Scopes:

scopes

Note that directiveB's child scope (006) inherits from directiveA's transcluded scope (005).

After clicking the checkbox and changing the value in the textbox:

scopes after interaction

Note that Angular handles updating the isolate scope properties. Normal JavaScript prototypal inheritance gives directiveB's child scope access to the model in the controller scope (003).

Fiddle

Community
  • 1
  • 1
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • +1 Very nice illustration! I always appreciate the inheritance diagrams you include in your scope-related answers. – Josh David Miller Apr 05 '13 at 04:54
  • +1 Thanks for the diagrams Mark - I've accepted Josh's answer as he was first, but your input is much appreciated. – jlb Apr 05 '13 at 10:37
  • 1
    @jlb, Josh provided the real answer, so he definitely deserves the accept. I was thinking about your (excellent) question some more this morning. It has been bothering me that we are constrained to using the `require` solution. I will be adding some additional thoughts (and diagrams) to my answer (when I get some time). To give you a quick rundown though: creating a model inside a directive and then sharing it outside is rather atypical. The more typical way is to have a controller reference a model, which can then be shared with all child/descendant directives. – Mark Rajcok Apr 05 '13 at 15:21
  • @jlb, FYI, I added my additional thoughts and diagrams. – Mark Rajcok Apr 06 '13 at 01:36
  • @MarkRajcok Thanks again, this time for the excellent additions! I think using a 'pure' controller in this case may make more sense. Maybe decoupling and abstracting the functionality (of the directives) a bit more would allow that solution to "feel right". – jlb Apr 07 '13 at 14:51