AngularJS : Child directive scope to inherit from parent directive isolated scope? - angularjs

If a and b are my directives such that b is a child element of a :
<a>
<b></b>
</a>
Is it possible that if a has an isolated scope, then b could inherit from it?
Example js:
app.directive('a', function () {
return {
restrict: 'E',
scope: {},
controller: function ($scope) {
$scope.prop1 = ...
}
}
});
app.directive('b', function () {
return {
restrict: 'E',
controller: function ($scope) {
//how to access $scope.prop1 here?
}
}
});
With this, I'm trying to make directives that are reusable and are supposed to be used as nested within each other.
I know that I can require the controller of a on directive b to access it within the link function of b as one way to share the data between controllers, but that approach isn't working very well if I have more than one level of nesting.

This is where you need to use the manual transclusion function. If the parent directive has an isolate scope, the child DOM elements (and their directives) would not inherit from it (only, if they were in its template).
When you transclude, you can specify the scope explicitly:
.directive("a", function(){
return {
scope: {},
transclude: true,
link: function(scope, element, attrs, ctrls, transclude){
var newScope = scope.$new();
transclude(newScope, function(clone){
element.append(clone);
})
}
};
});
You should note, though, that although the above would work (in the sense that the child directive's scope would inherit the parent's isolate scope), it is also a somewhat confusing experience to the user of your directive.
To see why, imagine that a exposes some $innerProp on its scope. The user of a now has to know that such property is "magically" available. This makes the HTML less readable without knowing a lot about a:
<a>
<b item="$innerProp"></b>
</a>
Addendum
Depending on your use case, there might be other approaches that are more suitable. The above approach works better when a and b are independent, and when a uses its contents to allow its user to specify some template.
If b is only (or mostly) used as a child of a, then it should require it. a can expose whatever it needs via its controller API to b.
Lastly, if a has a well-defined structure, then it should use its template to specify b. In your example, this could easily be achieved with a template.

Related

Directive with it's own controller placed within ngAnimateSwap results in new controller being initialized on every 'swap'

I have created a directive (ParentDir) that has it's own controller and whose template binds to this controller. It communicates with another directive (Child1) that has it's own controller which 'requires' the first parent directive. Below is a simplified example:
Module.directive("ParentDir", function () {
return {
templateUrl: '../ParentTemplate',
restrict: 'AEC',
scope: {
},
controllerAs: 'parCtrl',
bindToController: true,
controller: ['$scope', function ($scope) {
parCtrl= this;
parCtrl.title = "PARENT 1 TITLE";
}]}
Module.directive("Child1", function () {
return {
templateUrl: '../Child1Template',
restrict: 'AEC',
require: '^^ParentDir',
scope: {},
controllerAs: 'ch1Ctrl',
bindToController: true,
link: function ($scope, element, attrs, parCtrl) {
$scope.parCtrl= parCtrl;
},
controller: ['$scope', function ($scope) {
ch1Ctrl= this;
ch1Ctrl.title = "CHILD 1 TITLE";
}]}
ParentDir html:
<child1> </child1>
Child1 html:
{{parCtrl.title}}
{{ch1Ctrl.title}}
Finally my ParentDirective is initialized in something like this:
<div ng-animate-swap="trigger" class="swapclass">
<parent-dir></parent-dir>
</div>
I need the entire parent directive's template to slide in certain cases. I also use the directive in other places where I don't need this and I can use it as is. In the cases where I do need the slide animation, I place it inside an ng-animate-swap as shown above. The problem is that every time the swap trigger changes, a new parCtrl is initialized causing everything to be reset!
How can I use animate swap with a directive that has isolate scope and it's own controller, without reinitializing the controller everytime a swap occurs?
Directives, as we know, are high-level markers that tell Angular's compiler to attach a specified behavior to that HTML element. When a directive is put on the DOM, Angular's $compile service matches directive names with their code, normalizes it and executes it.
However, ng-animate-swap removes its element from the DOM before appending the new one.
This means your directives are being recompiled for each swap, and new isolate scopes are created each time the animation happens.
The solution to this depends on the functionality of your app, how large your templates are, and how often you need to do the animation (or what the animation entails):
One solution is to create another directive outside of the swap animation that holds parCtrl.title and ch1Ctrl.title (or whatever other variables you have) and then is able to pass that information down to child scopes through prototypical inheritance:
<swap-dir>
<div ng-animate-swap="trigger" class="swapclass">
<parent-dir></parent-dir>
</div>
<swap-dir>
This could also be done with a controller, perhaps much more easily. What you choose to do would depend on where you're getting your scope variables from and how many different elements you have on the page.
However, ng-animate-swap creates its own scope, so while I believe this would work, the ever-fun JavaScript inheritance shenanigans could cause an issue here as well.
Another solution would be to skip ng-animate-swap altogether and just animate the template element with regular old CSS transitions, although this depends on what you're doing and how you want it to look.

angularjs inheriting scope in nested directives

example in: http://jsfiddle.net/avowkind/PS8UT/
I want a nested child directive to get its data from its wrapping parent directive if present, otherwise from the outer controller.
<div ng-controller="MyCtrl">
<parent index="1">
<child></child>
</parent>
<parent index="2">
<child></child>
</parent>
<h1>No Parent</h1>
<child></child>
</div>
<hr>
Desired output
Parent 1
Child of parent 1
Parent 2
Child of parent 2
No Parent
Child of parent 0
Currently my child object only sees the outer controller value:
Actual output
Parent 1
Child of parent 0
Parent 2
Child of parent 0
No Parent
Child of parent 0
This is the simple version; in reality the outer directives get data from a server that is formatted by the nested child so what is communicated is a complex object not a simple string.
Furthermore the child is a visualisation that will work on different data sets so the outer parent directive is not always the same type.
More generally the pattern I am trying to get here is to have separate directives for populating the model and viewing it. so a more realistic usage would be
<temperature-for city="Auckland">
<plot/>
<analysis/>
</temperature-for>
<humidity-for city="Hamilton">
<plot/>
<analysis/>
</temperature-for>
<test-data>
<plot/>
</test-data>
A different approach which I personally have had great success using is to define the plot and analysis directives as isolate scopes, and then two-way bind the required input.
This way the directive are completely standalone components, with a explicit, defined interface. I personally made a plotting directive like this:
<plot data="countries['Auckland'].plot.data" options="countries['Auckland'].plot.options" legend="external" zoom="xy"></plot>
Scope would look like:
scope: {
data: '=',
options: '=',
zoom: '#?', // '?' indicates optional
legend: '#?',
}
This way there's no confusion what data is required for this component to work, and you can write documentation inside the directive for the desired input attributes.
All in all, this is a pattern which works very well for a large portion of use cases in AngularJS, i.e. whenever there is a case for reusability.
Edit:
Just wanted to add to that: Looking at your HTML, there's absolutely no indication what those directives use, they could depend on anything (e.g. do they get all the data from a service? or do they depend on a parent scope? If so, what scope?)
There are a couple different ways to do this but assuming that you truly want to use the parent scopes here is a solution to go along with your fiddle.
var myApp = angular.module('myApp', []);
function MyCtrl($scope) {
$scope.index = 0;
}
myApp.directive('parent', function () {
return {
transclude: true,
scope: {
index: '='
},
restrict: 'EA',
template: '<h2>Parent {{ index }}</h2>',
compile: function(tE, tA, transcludeFn) {
return function (scope, elem, attrs) {
elem.append(transcludeFn(scope)[1]);
};
}
}
});
myApp.directive('child', function () {
return {
restrict: 'EA',
scope: false,
template: '<p>Child of parent {{ index }}</p>'
}
});
You can see a fork of your fiddle here.
The idea is that by getting rid of the ngTranscludeDirective and manually creating the transclusion, you can link the transclusion with the scope of your choosing. Then you can append the result where ever you like in the element resulting from your directive's compilation.
The other main point is to make sure the child directive doesn't create a scope (at all, whether an isolate scope, transcluded scope, or new scope).
I think this will give you the results you're asking for.
NOTE: Study your scopes well, because tweaking these behaviors can have unexpected results.
For example, if you add a linking function to the child directive and it sets index to 5:
link: function(scope) {
scope.index = 5;
}
this will not affect scope.items for children nested in the parent. However it WILL affect the an external parent scope (in this case MyCtrl's scope). Any directive not inside a parent directive will just keep altering the MyCtrl index.
However if you add a new property to scope in the child link function:
link: function(scope) {
scope.somethingElse = foo;
}
scope.somethingElse will be available on the parent scope whether nested in a parent directive or not.
You'll need to transclude and fine grain it according to your parent directive scope, you can't do it using the ng-transclude directive: http://jsfiddle.net/PS8UT/2/
var myApp = angular.module('myApp', []);
function MyCtrl($scope) {
$scope.index = 0;
}
myApp.directive('parent', function ($compile) {
return {
scope: {
index: '#'
},
restrict: 'EA',
transclude: true,
template: '<h2>Parent {{ index }}</h2>',
link: function (scope, elem, attrs, ctrl, transclude) {
transclude(scope, function(clone, s){
elem.append($compile(clone)(s)); // basically you need to reassign the inner child scope to your current isolated scope
});
}
}
});
myApp.directive('child', function () {
return {
//scope: true, // no need to create another scope since you want to use the parent
// scope: { }, // no index printed
restrict: 'EA',
template: '<p>Child of parent {{ index }}</p>'
}
});
Usually, when you are dealing with templates and transclusion, needing parent inheritage, is a pain. ng-transclude won't use your immediate parent scope as the parent, it usually use your controller scope. It's stated in angular docs $compile docs:
transclude
compile the content of the element and make it available to the
directive. Typically used with ngTransclude. The advantage of
transclusion is that the linking function receives a transclusion
function which is pre-bound to the correct scope. In a typical setup
the widget creates an isolate scope, but the transclusion is not a
child, but a sibling of the isolate scope. This makes it possible for
the widget to have private state, and the transclusion to be bound to
the parent (pre-isolate) scope.

AngularJS : Child input directive needs to compile in the scope of its parent for ng-model to bind

We have a contact form we use in many applications. There are many default values, validation rules, structure, etc, that are repeated. We're working on a set of directives in order to make the view more semantic and less verbose.
There are a few targets we're shooting for.
Defining the contact form model once in a parent directive like this: <div my-form model='formModel'>. Associated children directives would be able to get the base model from the model attribute.
Supply the default configuration (size, validation rules, placeholders, classes, etc) for each input, but allow the possibility for attributes to be overwritten if necessary. Thus, we are creating child directives using the my-form directive's controller for communication. We also want these child directives to bind to the application controller's model formModel.
I'm having some trouble with implementing this.
formModel is exposed through the parent directive's controller, but I'm having to manually $compile the child directive using scope.$parent in the link function. This seems smelly to me, but if I try to use the child directive's scope the compiled HTML contains the correct attribute (it's visible in the source), but it isn't bound to the controller and it doesn't appear on any scope when inspected with Batarang. I'm guessing I'm adding the attribute too late, but not sure how to add the attribute earlier.
Although I could just use ng-model on each of the child directives, this is exactly what I'm trying to avoid. I want the resulting view to be very clean, and having to specify the model names on every field is repetitive and error-prone. How else can I solve this?
Here is a jsfiddle that has a working but "smelly" setup of what I'm trying to accomplish.
angular.module('myApp', []).controller('myCtrl', function ($scope) {
$scope.formModel = {
name: 'foo',
email: 'foo#foobar.net'
};
})
.directive('myForm', function () {
return {
replace: true,
transclude: true,
scope: true,
template: '<div ng-form novalidate><div ng-transclude></div></div>',
controller: function ($scope, $element, $attrs) {
$scope.model = $attrs.myModel;
this.getModel = function () {
return $scope.model;
};
}
};
})
.directive('myFormName', function ($compile) {
return {
require: '^myForm',
replace: true,
link: function (scope, element, attrs, parentCtrl) {
var modelName = [parentCtrl.getModel(),attrs.id].join('.'),
template = '<input ng-model="' + modelName + '">';
element.replaceWith($compile(template)(scope.$parent));
}
};
});
There is a much simpler solution.
Working Fiddle Here
Parent Form Directive
First, establish an isolated scope for the parent form directive and import the my-model attribute with 2-way binding. This can be done by specifying scope: { model:'=myModel'}. There really is no need to specify prototypical scope inheritance because your directives make no use of it.
Your isolated scope now has the 'model' binding imported, and we can use this fact to compile and link child directives against the parent scope. For this to work, we are going to expose a compile function from the parent directive, that the child directives can call.
.directive('myForm', function ($compile) {
return {
replace: true,
transclude: true,
scope: { model:'=myModel'},
template: '<div ng-form novalidate><div ng-transclude></div></div>',
controller: function ($scope, $element, $attrs) {
this.compile = function (element) {
$compile(element)($scope);
};
}
};
Child Field Directive
Now its time to setup your child directive. In the directive definition, use require:'^myForm' to specify that it must always reside within the parent form directive. In your compile function, add the ng-model="model.{id attribute}". There is no need to figure out the name of the model, because we already know what 'model' will resolve to in the parent scope. Finally, in your link function, just call the parent controller's compile function that you setup earlier.
.directive('myFormName', function () {
return {
require: '^myForm',
scope: false,
compile: function (element, attrs) {
element.attr('ng-model', 'model.' + attrs.id);
return function(scope, element, attrs, parentCtrl) {
parentCtrl.compile(element);
};
}
};
});
This solution is minimal with very little DOM manipulation. Also it preserves the original intent of compiling and linking input form fields against the parent scope, with as little intrusion as possible.
It turns out this question has been asked before (and clarified) here, but never answered.
The question was also asked on the AngularJS mailing list, where the question WAS answered, although the solution results in some smelly code.
Following is Daniel Tabuenca's response from the AngularJS mailing list changed a bit to solve this question.
.directive('foo', function($compile) {
return {
restrict: 'A',
priority: 9999,
terminal: true, //Pause Compilation to give us the opportunity to add our directives
link: function postLink (scope, el, attr, parentCtrl) {
// parentCtrl.getModel() returns the base model name in the parent
var model = [parentCtrl.getModel(), attr.id].join('.');
attr.$set('ngModel', model);
// Resume the compilation phase after setting ngModel
$compile(el, null /* transclude function */, 9999 /* maxPriority */)(scope);
}
};
});
Explanation:
First, the myForm controller is instantiated. This happens before any pre-linking, which makes it possible to expose myForm's variables to the myFormName directive.
Next, myFormName is set to the highest priority (9999) and the property of terminal is set true. The devdocs say:
If set to true then the current priority will be the last set of directives which will execute (any directives at the current priority will still execute as the order of execution on same priority is undefined).
By calling $compile again with the same priority (9999), we resume directive compilation for any directive of a lower priority level.
This use of $compile appears to be undocumented, so use at your own risk.
I'd really like a nicer pattern for follow for this problem. Please let me know if there's a more maintainable way to achieve this end result. Thanks!

Access controller scope from directive

I've created a simple directive that displays sort column headers for a <table> I'm creating.
ngGrid.directive("sortColumn", function() {
return {
restrict: "E",
replace: true,
transclude: true,
scope: {
sortby: "#",
onsort: "="
},
template: "<span><a href='#' ng-click='sort()' ng-transclude></a></span>",
link: function(scope, element, attrs) {
scope.sort = function () {
// I want to call CONTROLLER.onSort here, but how do I access the controller scope?...
scope.controllerOnSort(scope.sortby);
};
}
};
});
Here's an example of some table headers being created:
<table id="mainGrid" ng-controller="GridCtrl>
<thead>
<tr>
<th><sort-column sortby="Name">Name</sort-column></th>
<th><sort-column sortby="DateCreated">Date Created</sort-column></th>
<th>Hi</th>
</tr>
</thead>
So when the sort column is clicked I want to fire the onControllerSort function on my grid controller.. but I'm stuck! So far the only way I've been able to do this is for each <sort-column>, add attributes for the "onSort" and reference those in the directive:
<sort-column onSort="controllerOnSort" sortby="Name">Name</sort-column>
But that's not very nice since I ALWAYS want to call controllerOnSort, so plumbing it in for every directive is a bit ugly. How can I do this within the directive without requiring unnecesary markup in my HTML? Both the directive and controller are defined within the same module if that helps.
Create a second directive as a wrapper:
ngGrid.directive("columnwrapper", function() {
return {
restrict: "E",
scope: {
onsort: '='
}
};
});
Then you can just reference the function to call once in the outer directive:
<columnwrapper onsort="controllerOnSort">
<sort-column sortby="Name">Name</sort-column>
<sort-column sortby="DateCreated">Date Created</sort-column>
</columnwrapper>
In the "sortColumn" directive you can then call that referenced function by calling
scope.$parent.onsort();
See this fiddle for a working example: http://jsfiddle.net/wZrjQ/1/
Of course if you don't care about having hardcoded dependencies, you could also stay with one directive and just call the function on the parent scope (that would then be the controller in question) through
scope.$parent.controllerOnSort():
I have another fiddle showing this: http://jsfiddle.net/wZrjQ/2
This solution would have the same effect (with the same criticism in regard to hard-coupling) as the solution in the other answer (https://stackoverflow.com/a/19385937/2572897) but is at least somewhat easier than that solution. If you couple hard anyway, i don't think there is a point in referencing the controller as it would most likely be available at $scope.$parent all the time (but beware of other elements setting up a scope).
I would go for the first solution, though. It adds some little markup but solves the problem and maintains a clean separation. Also you could be sure that $scope.$parent matches the outer directive if you use the second directive as a direct wrapper.
The & local scope property allows the consumer of a directive to pass in a function that the directive can invoke.
See details here.
Here is a answer to a similar question, which shows how to pass argument in the callback function from the directive code.
In your directive require the ngController and modify the link function as:
ngGrid.directive("sortColumn", function() {
return {
...
require: "ngController",
...
link: function(scope, element, attrs, ngCtrl) {
...
}
};
});
What you get as ngCtrl is your controller, GridCtrl. You dont get its scope though; you would have to do something in the lines of:
xxxx.controller("GridCtrl", function($scope, ...) {
// add stuff to scope as usual
$scope.xxxx = yyyy;
// Define controller public API
// NOTE: USING this NOT $scope
this.controllerOnSort = function(...) { ... };
});
Call it from the link function simply as:
ngCtrl.controllerOnSort(...);
Do note that this require will get the first parent ngController. If there is another controller specified between GridCtrl and the directive, you will get that one.
A fiddle that demonstrates the principle (a directive accessing a parent ng-controller with methods): http://jsfiddle.net/NAfm5/1/
People fear that this solution may introduce unwanted tight coupling. If this is indeed a concern, it can be addressed as:
Create a directive that will be side-by-side with the controller, lets call it master:
<table id="mainGrid" ng-controller="GridCtrl" master="controllerOnSort()">
This directive references the desired method of the controller (thus: decoupling).
The child directive (sort-column in your case) requires the master directive:
require: "^master"
Using the $parse service the specified method can be called from a member method of the master controller. See updated fiddle implementing this principle: http://jsfiddle.net/NAfm5/3/
There is another way to do this, although given my relative lack of experience I can't speak for the fitness of such a solution. I will pass it along anyhow just for informational purposes.
In your column, you create a scope variable attribute:
<sort-column data-sortby="sortby">Date Created</sort-column>
Then in your controller you define the scope variable:
$scope.sortby = 'DateCreated' // just a default sort here
Then add your sort function in controller:
$scope.onSort = function(val) {
$scope.sortby = val;
}
Then in your markup wire up ng-click:
<sort-column data-sortby="sortby" ng-click="onSort('DateCreated')">Date Created</sort-column>
Then in your directive you add the sortby attribute to directive scope:
scope: {
sortby: '=' // not sure if you need
}
And in your "link:" function add a $watch:
scope.$watch('sortby', function () {
... your sort logic here ...
}
The beauty of this approach IMO is that your directive is decoupled completely, you don't need to call back to onSort from the directive because you don't really leave onSort in the controller during that part of the execution path.
If you needed to tell your controller to wait for the sort to finish you could define an event in the controller:
$scope.$on("_sortFinished", function(event, message){
..do something...
});
Then in your directive simply emit the event then the process is done:
$scope.$emit('_sortFinished');
There's other ways to do that, and this kind of adds some tight-ish coupling because your controller has to listen for. and your directive has to emit a specific even... but that may not be an issue for you since they are closely related anyhow.
Call me crazy, but it seems easier to just get the controller from the element via the inbuilt method for that, rather than fiddling with require:
var mod = angular.module('something', []).directive('myDir',
function () {
return {
link: function (scope, element) {
console.log(element.controller('myDir'));
},
controller: function () {
this.works = function () {};
},
scope: {}
}
}
);
http://plnkr.co/edit/gY4rP0?p=preview

Call controller method from directive without defining it on the directive element - AngularJS

I'm sure there's a simple answer to this that i've just missed.
http://jsfiddle.net/jonathanwest/pDRxw/3/
Essentially, my directive will contain controls that will always call the same method in a controller which is external to the directive itself. As you can see from the above fiddle, I can make this work by defining the attribute with the method on the control directive, but as that method will always be called from the same button within the directive, I don't want to have to define the method to call. Instead the directive should know to call the controller edit method when that button is pressed. Therefore, the definition of the control would be:
<control title="Custom Title" />
How can I achieve this?
Actually I think doing that straightway using $parent is not a recommended way how directives should be defined. Because actually there is no visible dependency on what functions could be called from parent controller, making them little bit harder to re-use.
I do not know actual use case why you need this, but I assume that you use control several times and you do not want to copy-paste bunch of attributes that defines some common behavior.
In this case I would recommend little bit other approach: add some directive-container that will define that behavior, and control will require this directive as dependency:
myApp.directive('controlBehavior', function() {
return {
restrict: 'E',
scope: {
modifyfunc: '&'
},
controller: function($scope, $timeout) {
this.modifyfunc = $scope.modifyfunc;
}
};
});
myApp.directive('control', function() {
return {
restrict: 'E',
require: '^controlBehavior',
replace: true,
scope: {
title: "#"
},
template : '<div>{{title}}<button ng-click="edit()">Edit</button></div>',
link: function(scope, element, attr, behavior) {
scope.edit = behavior.modifyfunc;
}
}
});
Here is a fiddle to demonstrate this approach: http://jsfiddle.net/7EvpZ/4/
You can access the parent scope by using $parent property of the current scope.
http://jsfiddle.net/XEt7D/

Resources