1

I am compiling and linking a directive with an isolate scope like so (please note this is manual compiling and linking for reasons outside the scope of this question):

    outerElement = angular.element(domElement);
    $injector = outerElement.injector();
    $compile = $injector.get('$compile'); 
    getTheIsolateScopeForMyDirectiveInstance().myProperty = 'foo'; // Pseudocode. I want myProperty to be available on the scope from inside the controller constructor function.
    link = $compile(angular.element('<my-directive></my-directive>'));
    // IIUC, the following line will instantiate 
    // the controller for the directive, injecting 
    // the isolate scope. I want to augment the 
    // isolate scope that is injected *before* it 
    // is injected. 
    // The value to augment the scope resides in 
    // *this* execution context.
    // How can I do so?
    renderedElement = link(outerElement.scope());
    element.append(renderedElement);

MyDirective has an isolate scope (the one I want to augment), and a controller associated with it.

The controller MyDirectiveController leverages the injector to have its isolate scope injected.

MyDirectiveController.$inject = [ '$scope' ];

I want to augment the isolate scope before it is injected into the instance of MyDirectiveController, with a value that is only known at run-time in the execution context of the code above.

How can I do this?

MyDirective

function MyDirective() {
    return {
        scope: {}, // I want to augment this before it is injected into  MyDirectiveController
        restrict: 'E',
        template: template,
        controller: 'myDirectiveController',
        controllerAs: 'ctrl',
        replace: true,
    };
}

MyDirectiveController

function MyDirectiveController($scope) {
    console.log($scope.myProperty); // Should be 'foo'.
}

MyDirectiveController.$inject = ['$scope'];

If this is impossible, is there another way instance-specific information can be made available to the controller and/or isolate scope of the directive?

The only way I can think of right now is to augment the scope supplied to link(outerElement.scope()) above, and then define a = property on the isolate scope of the directive.

Edit:

This is what I am now doing, and the myProperty value ends up on the parent of the isolate scope for the controller:

var isolate = outerElement.scope().$new(true);
isolate.myProperty = 'foo';
renderedElement = link(isolate);
element.append(renderedElement);

Given this, when MyDirectiveController is instantiated:

function MyDirectiveController($scope) {
  $scope.myProperty; // undefined
  $scope.$parent.myProperty; // 'foo'
}
Ben Aston
  • 53,718
  • 65
  • 205
  • 331

2 Answers2

2

The assumption here

The controller MyDirectiveController leverages the injector to have its isolate scope injected.

MyDirectiveController.$inject = [ '$scope' ];

I want to augment the isolate scope before it is injected into the instance of MyDirectiveController, with a value that is only known at run-time in the execution context of the code above.

is wrong. $inject is nothing but annotation, it will make a difference only when the code is minified.

And you can't 'inject' anything into isolate scope before the directive was linked. Some ugly monkey-patching could do the trick:

  var _new = scope.$new;
  scope.$new = function () {
    return angular.extend(_new.apply(this, arguments), {
      myProperty: 'foo'
    })
  };
  $compile(angular.element('<my-directive></my-directive>'))(scope);
  scope.$new = _new;

But even if possible, it would be appropriate for writing tests but not for production code.

The only straightforward way here (and the reason why isolated scope is used) is

function MyDirective() {
    return {
        scope: {
            myProperty: '='
        },
        ...
    };
}

and

$compile(angular.element('<my-directive my-property="myProperty"></my-directive>'))(scope);

where scope.myProperty equals to 'foo'.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • I have come to to the same conclusion. But I have another problem: even when using the parent scope binding approach { myProperty: '=' }, `myProperty` remains `undefined` on the directive isolate scope. Even when `isolateScope.$parent.myProperty === 'foo'`. Could this be a problem with the way I am manually compiling or linking the directive? Edit: aha, the attribute needs to be in the DOM node. – Ben Aston Aug 19 '15 at 19:18
  • 1
    Yes, here is a pitfall: controller is a bad place to get scope variables from bound attributes, just [had an answer](http://stackoverflow.com/questions/32098799/in-a-custom-directive-what-functionality-does-controller-have-over-link/32101712#32101712) on that. Interpolated `myProperty` from isolated scope will be there in pre-link and (post)link but not in controller. But it is not a problem if you do `$scope.$watch('myProperty', ...)` in controller. And it is a good habit to presume that data-bindings can change in Angular with time (and they actually do). – Estus Flask Aug 19 '15 at 19:28
1

UPDATE2: If you need isolated scope, create new scope :

 var myparentscope = outerElement.scope()
 var myscope = myparentscope.$new(true) // or $rootScope.$new(true, myparentscope), see true here, it isolates scope from parent.
 myscope.prop = 'new prop' // define on child scope.
 renderedElement = link(myscope);
 element.append(renderedElement);

UPDATE: last four lines in your code:

getTheIsolateScopeForMyDirectiveInstance().myProperty = 'foo'; 
link = $compile(angular.element('<my-directive></my-directive>'));
renderedElement = link(outerElement.scope());
element.append(renderedElement);

should be :

var myscope = outerElement.scope()
scope.myProperty = 'foo'
renderedElement = link(myscope);

Original answer:

 .directive('myDirective',['$compile', function($compile) {
      return {
         link:link,
         scope: {
         }
      }

      function link(scope, element) {
         scope.prop = 'new value'  // new property on scope           
         var renderedElement = $compile('... html ...')(scope);
         element.append(renderedElement);
      }
 })

Also

$compile = $injector.get('$compile'); // I presume this instantiates the controller associated with myDirective behind the scenes?

$inject.get('$compile') is convoluted way to get $compile service. If compile service is not (and it IS NOT) dependent on your service, you can specify it as you normally do with dependency injection.

Also I think you need to read this part about compile and link steps in directive life-cycle.

Compile step is not $compile service. Compile step is executed once per life time of the app, think of it as preparation for using all the instances of your directive.

Link steps (pre and post link) are taking your prepared directive and actually instantiating it for specific place/scope etc in your app.

Since you need to update scope - it is link step that you want to use.

Good video on the topic - https://egghead.io/lessons/angularjs-compile-pre-and-post-link

vittore
  • 17,449
  • 6
  • 44
  • 82
  • I am manually driving the rendering of a directive that is made available at some arbitrary point after angular application bootstrap, hence the convolution. If you know of a better way, please let me know. Conventional use of the injector is prohibited by my use case AFAIK. – Ben Aston Aug 19 '15 at 16:36
  • @BenAston I am not sure why you can't just use link. It will happen only when you are actually rendering your directive for the first time anyway. Also if your problem is bigger - create js fiddle or something with more details. – vittore Aug 19 '15 at 16:37
  • "Compile step is not $compile directive" - surely this is the compilation of the directive, preparing it for use. In this case this is the first time the running angular application has seen an instance of this directive. – Ben Aston Aug 19 '15 at 16:38
  • @BenAston `$compile` service - it was typo. – vittore Aug 19 '15 at 16:39
  • Okay, I didn't fully grok your answer. I will try your suggestion. – Ben Aston Aug 19 '15 at 16:39
  • @BenAston - check the video. I really doubt that you need anything more sophisticated than using compile, pre and postlink. – vittore Aug 19 '15 at 16:41
  • Sorry, I placed my comment in my question code on a line other than intended. Is it correct to say that `$compile(angular.element(''))` returns a link function? – Ben Aston Aug 19 '15 at 16:45
  • Re ". I really doubt that you need anything more sophisticated..." My use case is dynamic runtime loading of modules from outside of Angular. – Ben Aston Aug 19 '15 at 16:46
  • @BenAston it is really hard to speculate about it without seeing your code. – vittore Aug 19 '15 at 16:53
  • .@vittore Put another way: if the link function is the place to augment the scope before the directive-controller is instantiated, how can I pass a value to the link function from the lexical scope of the code in my question? – Ben Aston Aug 19 '15 at 17:04
  • @BenAston I either do not fully understand what you are asking or you didn't see code example in my answer `scope.prop = 'new value'` doing exactly that. – vittore Aug 19 '15 at 17:05
  • Your code is augmenting the scope, that is correct and what I want. But the value you are augmenting it with is defined in another lexical scope to the lexical scope of the code in my question (I am talking about JavaScript lexical scope, not AngularJS scope). If the link function is the place to augment the AngularJS scope for the directive-controller then great, but how can I supply a value to the link function from the lexical scope of the code in my question? In other words, the value I wish to augment the AngularJS scope with is not known until the code in my question is executed. – Ben Aston Aug 19 '15 at 17:08
  • @BenAston exactly the same way. I could've write `scope.prop = window` etc – vittore Aug 19 '15 at 17:10
  • Say we are just about to invoke the line `link = $compile(angular.element(''));`. Will this line inject the directive-scope into a newly instantiated directive-controller (if my controller is registered with the injector like so: `MyDirectiveController.$inject = [ '$scope' ];`? – Ben Aston Aug 19 '15 at 17:12
  • `renderedElement = link(outerElement.scope());` this is where you are injecting scope – vittore Aug 19 '15 at 17:14
  • If so, how do I get hold of that isolate scope (given we are at the aforementioned runtime location in the code) that is about to be instantiated and modify it before it is injected? – Ben Aston Aug 19 '15 at 17:14
  • scope is just an javascript object , you can just set properties on it. – vittore Aug 19 '15 at 17:15
  • re "this is where you are injecting scope" Okay that may be the location of my misunderstanding. Thanks. I had assumed that `outerElement.scope()` was the scope associated with the containing DOM node, and not the isolate scope of the directive. – Ben Aston Aug 19 '15 at 17:17
  • @BenAston It is but in your code you are not creating child scope for your directive. you can do so calling `scope.$new()` if you want to define new scope and property visible only to your directive not the parent scope. – vittore Aug 19 '15 at 17:23
  • @BenAston added clarification for child scope in answer. see update2 – vittore Aug 19 '15 at 17:25
  • Running your update (I could be wrong) is modifying the scope containing the directive instance. Can you confirm this is not the case. I want to modify the isolate scope of the directive instance. – Ben Aston Aug 19 '15 at 17:26
  • @BenAston see UPDATE2 in the answer and read about scope.$new via linked article from angular docs – vittore Aug 19 '15 at 17:28
  • Thanks for all your help. – Ben Aston Aug 19 '15 at 17:32
  • Okay, so injecting manually a new isolate scope per update 2, does not give me completely what I want (although it gets me closer). The `myProperty` ends up on the `$parent` of the isolate scope injected into the `MyDirectiveController` instance. I want `myProperty` to be directly on the isolate scope itself of the controller instance. Is this impossible? – Ben Aston Aug 19 '15 at 17:55
  • @BenAston so you are doing `myscope.pro='bla'` and it gets on parent scope? – vittore Aug 19 '15 at 18:10
  • Are you sure you didnt have property with the same name on outer scope before those lines? – vittore Aug 19 '15 at 18:23
  • @BenAston, create jsfiddle with your code, it would be easier to fix. – vittore Aug 19 '15 at 18:25
  • Yes. It makes sense. The scope passed into the link function `renderedElement = link(isolate);` is used as the parent of the isolate scope instantiated internally by AngularJS as part of the instantiation of the directive. I suspect doing what I wanted is impossible. The best approximation is to use the `=` syntax for my isolate scope to make `myProperty` directly available on the directive isolate scope. – Ben Aston Aug 19 '15 at 18:27
  • @BenAston take a look at plunkr i created to show what is going on http://plnkr.co/edit/cAZAMRUknx46N4h0QsFJ?p=preview basically controller is instantiated before prelink. thing is for normal angular js code it should be ok , as you are watching for changes on the scope. – vittore Aug 19 '15 at 18:40