Trouble with Angular Nested Directives when using ControllerAs - angularjs

I am building a huge form that calls various directives to build a complete form. The Main Page calling the Form Builder passes the ng-model data like this:
<div form-builder form-data=“formData”></div>
Then the Form Builder Page calls various child directive to build various sections of the Form:
FormBuilder.html:
<div form-fields></div>
<div photo-fields></div>
<div video-fields></div>
.. etc.. etc...
When using $scope in controller, I had no problem accessing the $scope in the child directives like this:
function formBuilder() {
return {
restrict: 'A',
replace: true,
scope: {
formData: '='
},
templateUrl: 'FormBuilder.html',
controller: function($scope) {
$scope.formSubmit = function() {
// Submits the formData.formFields and formData.photoFields
// to the server
// The data for these objects are created through
// the child directives below
}
}
}
}
function formFields() {
return {
restrict: 'A',
replace: true,
templateUrl: 'FormFields.html',
controller: function($scope) {
console.log($scope.formData.formFields);
}
}
}
function photoFields() {
return {
restrict: 'A',
replace: true,
templateUrl: 'PhotoFields.html',
controller: function($scope) {
console.log($scope.formData.photoFields);
}
}
}
... etc..
But ever since I got rid of the $scope and started using ControllerAs, I am having all sorts of trouble accessing 2 way binding with the Parent - Child Controllers.
function formBuilder() {
return {
restrict: 'A',
replace: true,
scope: {
formData: '='
},
templateUrl: 'FormBuilder.html',
controller: function() {
var vm = this;
console.log(vm.formData); // Its fine here
vm.formSubmit = function() {
// I cannot change formData.formFields and formData.photoFields
// from Child Directive "Controllers"
}
},
controllerAs: ‘fb’,
bindToController: true
}
}
function formFields() {
return {
restrict: 'A',
replace: true,
templateUrl: 'FormFields.html',
controller: function() {
var vm = this;
console.log(vm.formData.formFields);
// No way to access 2 way binding with this Object!!!
}
}
}
function photoFields() {
return {
restrict: 'A',
replace: true,
templateUrl: 'PhotoFields.html',
controller: function() {
var vm = this;
console.log(vm.formData.photoFields);
// No way to access 2 way binding with this Object!!!
}
}
}
Whatever I try, I am reaching a road block. Things I have tried are:
Isolated Scopes: I tried passing formData.formFields and
formData.photoFields as isolated scopes to the child directive,
but I then end up getting the $compile: MultiDir error due to
nested isolated scopes so it is not possible.
If I don’t have
individual directives for each form section and have all of them in
1 directive under formBuilder directive, then it becomes a
humungous directive. The above is just a sketch but each child
directive builds 1 big form put together in the end. So merging them
together is really the last resort since it does become hard to
maintain and unreadable.
I don’t think there is a way to access
Parent directive’s ControllerAs from Child Directive's Controller any other way
from what I have seen so far.
If I use the parent’s ControllerAs in
the child directive template’s ng-model like <input type=“text” ng-model=“fb.formData.formFields.text" />, that works fine, but I
need to access the same from the Child directive’s controller for
some processing which I am unable to do.
If I get rid of the
controllerAs and use the $scope again, it works like before but I am
trying to get rid of the $scope altogether to prepare myself for
future Angular changes.
Since it is an advanced form, I need to have separate directive to handle various form sections and since nested isolated scopes are not allowed since Angular 1.2, it is making it ever harder especially when trying to get rid of $scope using ControllerAs.
Can someone guide me what are my options here please? I thank you for reading my long post.

Basically you need to use require option of directive (require option is used for communicate directive with directive). Which will give access to its parent controller by just mentioning require option in child directive. Also you need to use bindToController: true which will basically add isolated scope data to the directive controller.
Code
function formBuilder() {
return {
restrict: 'A',
replace: true,
bindToController: true,
scope: {
formData: '='
},
templateUrl: 'FormBuilder.html',
controller: function($scope) {
$scope.formSubmit = function() {
// Submits the formData.formFields and formData.photoFields
// to the server
// The data for these objects are created through
// the child directives below
}
}
}
}
Then you need to add require option to child directives. Basically the require option will have formBuilder directive with ^(indicates formBuilder will be there in parent element) like require: '^formBuilder',.
By writing a require options you can get the controller of that directive in link function 4th parameter.
Code
function formFields() {
return {
restrict: 'A',
replace: true,
require: '^formBuilder',
templateUrl: 'FormFields.html',
//4th parameter is formBuilder controller
link: function(scope, element, attrs, formBuilderCtrl){
scope.formBuilderCtrl = formBuilderCtrl;
},
controller: function($scope, $timeout) {
var vm = this;
//getting the `formData` from `formBuilderCtrl` object
//added timeout here to run code after link function, means after next digest
$timeout(function(){
console.log($scope.formBuilderCtrl.formData.formFields);
})
}
}
}
function photoFields() {
return {
restrict: 'A',
replace: true,
require: '^formBuilder',
templateUrl: 'PhotoFields.html',
//4th parameter is formBuilder controller
link: function(scope, element, attrs, formBuilderCtrl){
scope.formBuilderCtrl = formBuilderCtrl;
},
controller: function($scope, $timeout) {
var vm = this;
console.log(vm.formData.photoFields);
//to run the code in next digest cycle, after link function gets called.
$timeout(function(){
console.log($scope.formBuilderCtrl.formData.formFields);
})
}
}
}
Edit
One problem with above solution is, in order to get access to the controller of parent directive in directive controller it self, I've did some tricky. 1st include the the formBuilderCtrl to the scope variable from link function 4th parameter. Then only you can get access to that controller using $scope(which you don't want there). Regarding same issue logged in Github with open status, you could check that out here.

Related

Angular - get parent directive's controller in child directive's controller (not link function)

I'm aware how I can get parent's directive controller in the child directive's link function.
However, I'd prefer to avoid using link function (and $scope all-together) and have all my code under the controller function of the directive.
angular.directive('parent', function(){
return {
templateUrl: '/parent.html',
scope: true,
bindToController: true,
controllerAs: 'parentCtrl',
controller: function(){
this.coolFunction = function(){
console.log('cool');
}
}
}
});
angular.directive('child', function(){
return {
templateUrl: '/child.html',
require: '^parent',
scope: true,
bindToController: true,
controllerAs: 'childCtrl',
controller: function() {
// I want to run coolFunction here.
// How do I do it?
}
}
});
Any help is appreciated!
The proper pattern for that would be
app.directive('child', function(){
return {
templateUrl: '/child.html',
require: ['child', '^parent'],
scope: true,
bindToController: true,
controllerAs: 'childCtrl',
controller: function() {
this.coolFunction = function () {
this._parent.coolFunction();
}
},
link: function (scope, element, attrs, ctrls) {
var childCtrl = ctrls[0];
var parentCtrl = ctrls[1];
childCtrl._parent = parentCtrl;
}
}
});
The bad thing is that _parent is being exposed to scope with controllerAs, but it will rarely be a problem.
Notice that you won't have access to parent controller from child until link glues them together. Which is fine as long as you use parent controller in child methods.
Controller provides methods and initial properties to view model (and it does it cleaner with controllerAs), link glues the stuff, that's how directives work.
Both $scope and link have their purposes in Angular 1.x and are indispensable even with latest community developments. Banishing them for no valid reason is overzealous and may lead to bad design solutions. The absence of 'link' and 'scope' words in code won't help to make the app easier to port to 2.x. Though learning Angular 2 now and developing proper habits for 1.x will.
You could inject '$element' into the controller and access the parent controller like -
controller: ($element) ->
var parentCtrl = $element.parent().controller('parent');
parentCtrl.coolFunction();
//..........
//..........
This may not be the most transparent way of accessing 'any' parent controller because it requires the specific name of the directive and it is jqlite and not pure Angular.
Found this thread useful - How to access parent directive's controller by requiring it recursively?
EDIT: Thanks to #Dmitry for figuring out that angular doesn't need '.parent' to get the controller. Updated code -
controller: ($element) ->
var parentCtrl = $element.controller('parent');
parentCtrl.coolFunction();
//..........
See here or here
var parentForm = $element.inheritedData('$formController') || ....
var parentForm = $element.controller('form')

AngularJS 1.4 - how to bind value between parent and child directive with isolated scope

I have two directives which both have an isolated scope. I would like to require the parent directive in my child directive, and then be able to watch if a scope variable of the parent changes. I don't want to modify the variable in the child, I just want to be able to read it.
I want to be able to add different children which both have access to the list, but I don't want to have to bind the list to every child. What is missing in the example below, is a way to watch the list which gets bound to the parent. I am able to pass the original list in, but as soon as it updates, the child will have an outdated model.
Parent directive:
angular
.module('app.parent', [])
.directive('parent', parent);
function parent() {
var directive = {
restrict: 'EA',
transclude: true,
template: '<div>parent <pre>{{vm.list}}</pre><ng-transclude></ng-transclude> </div>',
scope: true,
controller: ParentController,
controllerAs: 'vm',
bindToController: {
config: "=",
list: "="
}
};
return directive;
function ParentController() {
var vm = this;
}
}
child directive:
angular
.module('app.parent.child', ['app.parent'])
.directive('child', child);
function child() {
var directive = {
restrict: 'EA',
require: ['^^parent', '^child'],
template: '<div>child<pre>{{vm.list}}</pre></div>',
scope: true,
controller: ChildController,
link: linkFunc,
controllerAs: 'vm',
bindToController: {
config: "="
}
};
return directive;
function ChildController() {
var vm = this;
}
function linkFunc(scope, element, attrs, ctrls) {
var parentController = ctrls[0];
var vm = ctrls[1];
vm.list = parentController.list;
}
}
I have made a Plunkr with the code above. I am looking for a nice pattern to solve the issue I am having. Both directives will have their own unique config object passed in with configurations specific to the directive.
You can create a watcher on the child directive's scope object, but rather than watching a scope item, you can pass in a function as the first parameter to $watch() and simply return a value/object that you would like to watch.
So for instance inside your child directive's linkFunc()
scope.$watch(function() {
return parentController.list;
}, function(newList) {
vm.list = newList;
});
Modified your plunkr: http://plnkr.co/edit/6WzT8PQJRH1b5KuU0twn?p=preview

Angularjs Is there a cleaner more "angular" way to replace a transcluded class value in a directive template?

Below is the only way i could figure out how to get a directive to pull out an attribute from its origin element, get a new value by hitting a service, and then adding that new service method return as a class in the directive template. i'm wondering if there is an alternative pattern that might be cleaner then this pattern that might use ng-class or possibly ng-transclude:
html:
<my-directive data-condition="{{hour.condition}}"></my-directive>
js:
angular.module('myApp')
.directive('myDirective', function (myService) {
return {
transclude: true,
replace: true,
scope: true,
template: '<i class="{{wiIconClass}}"></i>',
restrict: 'E',
link: function($scope, $elm, attrs){
$scope.wiIconClass=myService.getValue(attrs.condition);
}
}
});
If your function myService.getValue is synchronous, you could simply do:
<div ng-class="getClass(hour.condition)">
And in your controller:
$scope.getClass = function(condition) {
return myService.getValue(condition);
}
Alternatively, you can directly put your service within your scope:
$scope.myService = myService;
So the HTML becomes
<div ng-class="myService.getValue(hour.condition)">
In both cases, you will need to inject your service into your controller:
myModule.controller('myController', function($scope, myService) {
// this controller has access to myService
})
I would use the Directives scope parameter instead of using the Directives Attribute values. This is because when using the attributes you will need to setup a $watch to see when that value updates, with using $scope you get the benefit of the binding aspect.
As far as to respond to the best way, its hard to say without knowing your actual task. You can have Angular update the elements css class value in several different ways.
Here's a working Plunker with some small updates to your existing code.
http://plnkr.co/edit/W0SOiBEDE03MgostqemT?p=preview
angular.module('myApp', [])
.controller('myController', function($scope) {
$scope.hour = {
condition: 'good'
};
})
.factory('myService', function() {
var condValues = {
good: 'good-class',
bad: 'bad-class'
};
return {
getValue: function(key) {
return condValues[key];
}
};
})
.directive('myDirective', function(myService) {
return {
transclude: true,
replace: true,
scope: {
condition: '='
},
template: '<i class="{{myService.getValue(condition)}}"></i>',
restrict: 'E',
link: function(scope, elm, attrs) {
scope.myService = myService;
}
};
});

In angular.js, can a directive controller access data in a page controller that loaded it?

In angular.js, can a directive controller access data in a page controller that loaded it?
/**
* Profile directive
*/
.directive('profile', function () {
return {
restrict: 'E',
replace: true,
templateUrl: '/partials/users/_profile.html',
scope: {
user: '=',
show: '=?'
},
controller: function($scope, $rootScope){
$scope.show = angular.isDefined($scope.show) ? $scope.show : { follow: true, link: true };
$scope.currentUser = $rootScope.currentUser;
//do stuff here and then set data in UserShowCtrl
}
};
});
The <profile user="user"></profile> method is called from ./users/show.html which uses the UserShowCtrl controller.
Is there anyway I can use scope on the profile directive with its own controller and still be able to pass data to the UserShowCtrl?
Even though the profile can be isolated to its own functionality, it still needs to set some data on the page level in the UserShowCtrl controller.
Here is where _user.html is loading the <profile> directive. The data for the page is served by the UserShowCtrl and has some collections that get updated when things happen, like following a user.
<ol class="following" ng-show="showConnections == 'following'">
<li ng-repeat="following in user.following">
<profile user="connections[following]"></profile>
</li>
</ol>
Right now there is an ng-click="follow(user)"> that is happening in the _profile.html. I would like to be able to have the directive handle this but also update the collections in the UserShowCtrl.
Edit: here is a plunker demonstrating what I'm trying to do:
http://plnkr.co/edit/9a5dxMVg9cKLptxnNfX3
You need to use a service in order to share any information between controllers, directives, services
something like
angular.module('myapp',[]).
service('myservice',function(){
return {a:'A',b:'B'}
}).
controller('mycontroller',['myservice',function(myservice){
//do someting with myservice
}]).
directive('mydirective',['myservice',function(myservice){
//do someting with myservice
}]);
there controller and directive access the same data through the service
You can access the parent scope from your directive with $scope.$parent.myvar.
myvar will be resolved in parent scope, which means prototypical scope inheritance is used to resolve the variable.
However, this does not guarantee that myvar is coming from the same scope as UserShowCtrl since its possible that any scope in between the 'profile' directive and UserShowCtrl's scope may override 'myvar'.
A better solution would be to use directive-to-directive communication. There are generally two ways for directives to communicate:
Through attributes passed into your directive. You've already used this method to import 'user' and 'show' from parent scope into your directive's isolated scope.
Requiring another directive. When you use 'require: ^UserShow', you are specifying that your 'profile' directive requires another directive as a dependency. The '^' means that it will search for the directive on the current element, or any parent element further up the DOM tree. UserShow's controller is then passed to your link function:
.directive('UserShow', function () {
return {
restrict: 'E',
controller: function($scope){
$scope.myvar = 'test';
this.setMyVar = function(var) {
$scope.myvar = var;
}
}
};
});
.directive('profile', function () {
return {
restrict: 'E',
replace: true,
templateUrl: '/partials/users/_profile.html',
require: '^UserShow',
scope: {
user: '=',
show: '=?'
},
controller: function($scope, $rootScope){
},
link: function(scope, element, attr, UserShowCtrl) {
UserShowCtrl.setMyVar('hello world!);
}
};
});
HTML:
<user-show>
<profile>...</profile>
</user-show>
I am not quite sure what your after.
You are already having 2 two-way data bindings, which means that if you change user in your directive, that will also flow to the outside scope.
So you already have a solution in front of you...
So if that is not "good enough", there is something missing in your question.
Here is an illustration: http://plnkr.co/edit/qEH2Pr1Pv7MTdXjHd4bD?p=preview
However, if you use something in your outside template that creates a child scope, binding it as "value" there is NOT enough, you need to have a . in there.
But that is where there is missing something to the question, if you share your show.html I may be able to find where the scope breaks apart and explain why...
Relevant Source from demo.js:
app.directive('profile', function () {
return {
restrict: 'E',
replace: true,
template: '<div><input type="text" ng-model="user"></input></div>',
scope: { //defines an isolate scope.
user: '=',
show: '=?'
},
controller: function($scope, $rootScope){
$scope.show = angular.isDefined($scope.show) ? $scope.show : { follow: true, link: true };
$scope.currentUser = $rootScope.currentUser;
$scope.user = "Changed by scope!";
//do stuff here and then set data in UserShowCtrl
}
};
});
app.controller('UserShowCtrl', function($scope) {
$scope.value = "Value set outside!";
$scope.alertValue = function() {
alert($scope.value);
}
});
Relevant Source from home.html:
<div ng-controller="UserShowCtrl">
{{ value }}
<profile user="value"></profile>
<button ng-click="alertValue()">ALERT!</button>
</div>

Access Parent Scope in Transcluded Directive

I would like to access a parent directive's scope, but I can't seem to get the right combination of settings. Is this possible and is it the right approach?
I really want to avoid putting something like SOME_CONST (which would help me make DOM updates through control flow) in MyCtrl
<div ng-controller="MyCtrl">
<parent>
<child></child>
</parent>
</div>
var myApp = angular.module('myApp',[]);
function MyCtrl($scope) {
$scope.obj = {prop:'foo'};
}
myApp.directive('parent', function() {
return {
scope: true,
transclude: true,
restrict: 'EA',
template: '<div ng-transclude><h1>I\'m parent {{obj.prop}}<h1></div>',
link: function(scope, elem, attrs) {
scope.SOME_CONST = 'someConst';
}
}
});
myApp.directive('child', function() {
return {
restrict: 'EA',
template: '<h1>I\'m child.... I want to access my parent\'s stuff, but I can\'t. I can access MyCtrlScope though, see <b>{{obj.prop}}</b></h1> how can I access the <b>SOME_CONST</b> value in my parent\'s link function? is this even a good idea? {{SOME_CONST}}. I really don\'t want to put everything inside the MyCtrl',
}
});
Please see this fiddle
Thanks
With transclude: true and scope: true, the parent directive creates two new scopes:
Scope 004 is a result of scope: true, and scope 005 is a result of transclude: true. Since the child directive does not create a new scope, it uses transcluded scope 005. As you can see from the diagram there is no path from scope 005 to scope 004 (except via private property $$prevSibling, which goes in the opposite direction of $$nextSibling -- but don't use those.)
#joakimbl's solution is probably best here, although I think it is more common to define an API on the parent directive's controller, rather than defining properties on this:
controller: function($scope) {
$scope.SOME_CONST = 'someConst';
this.getConst = function() {
return $scope.SOME_CONST;
}
}
Then in the child directive:
link:function(scope,element,attrs,parentCtrl){
scope.SOME_CONST = parentCtrl.getConst();
},
This is how the tabs and pane directives work on Angular's home page ("Create Components" example).
Normally the way you access a parent scope variable in a directive is through bi-directional binding (scope:{model:'=model'} - see the angular guide on directives) in the directive configuration), but since you're using transclusion this is not so straight forward. If the child directive will always be a child of the parent directive you can however configure it to require the parent, and then get access to the parent controller in the child link function:
myApp.directive('parent', function() {
return {
scope: true,
transclude: true,
restrict: 'EA',
template: '<div ng-transclude><h1>I\'m parent {{obj.prop}}<h1></div>',
controller: function($scope) {
$scope.SOME_CONST = 'someConst';
this.SOME_CONST = $scope.SOME_CONST;
}
}
});
myApp.directive('child', function() {
return {
restrict: 'EA',
require:'^parent',
scope:true,
link:function(scope,element,attrs,parentCtrl){
scope.SOME_CONST = parentCtrl.SOME_CONST;
},
template: '<h1>I\'m child.... I want to access my parent\'s stuff, but I can\'t. I can access MyCtrlScope though, see <b>{{obj.prop}}</b></h1> how can I access the <b>SOME_CONST</b> value in my parent\'s link function? is this even a good idea? {{SOME_CONST}}. I really don\'t want to put everything inside the MyCtrl',
}
});
See this update: http://jsfiddle.net/uN2uv/
I just had the same problem and finally solved it with the angular manual ;)
In short: you need to use a controller in your parent directive and require that controller in your child directive. This way you are able to get your parent properties.
See https://docs.angularjs.org/guide/directive
Chapter: Creating Directives that Communicate
I changed your fiddle to use a controller, now you can access your constant:
https://jsfiddle.net/bbrqdmt3/1/
var myApp = angular.module('myApp',[]);
function MyCtrl($scope) {
$scope.obj = {prop:'foo'};
}
myApp.directive('parent', function() {
return {
scope: true,
transclude: true,
restrict: 'EA',
template: '<div ng-transclude><h1>I\'m parent {{obj.prop}}<h1></div>',
controller: function($scope) {
this.getConst= function() {
return 'someConst';
}
},
}
});
myApp.directive('child', function() {
return {
restrict: 'EA',
require : '^parent',
link: function(scope, element, attrs, ctrl) {
scope.value= ctrl.getConst();
},
template: '<h1>I\'m child.... I want to access my parent\'s stuff, but I can\'t. I can access MyCtrlScope though, see <b>{{obj.prop}}</b></h1> how can I access the <b>SOME_CONST</b> value in my parent\'s link function? is this even a good idea? {{value}}. I really don\'t want to put everything inside the MyCtrl',
}
});
There's a transclude fn in the arguments of the link fn after the controller.
myApp.directive('parent', function() {
return {
scope: true,
transclude: true,
restrict: 'EA',
template: '<div><h1>I'm a parent header.</h1></div>',
link: function (scope, el, attrs, ctrl, transclude) {
transclude(scope, function (clone, scope) {
element.append(clone); // <-- will transclude it's own scope
});
},
controller: function($scope) {
$scope.parent = {
binding: 'I\'m a parent binding'
};
}
}
});
myApp.directive('child', function() {
return {
restrict: 'EA',
require:'^parent',
scope:true,
link:function(scope,element,attrs,parentCtrl){
},
template: '<div>{{parent.binding}}</div>' // <-- has access to parent's scope
}
});

Resources