AngularJS: require with bindToController, required controller is not available? - angularjs

I have a directive where I user require and bindToController in the definition.
(function(){
'use strict';
angular.module('core')
.directive('formValidationManager', formValidationManager);
function formValidationManager() {
return {
require: {
formCtrl: 'form'
},
restrict: 'A',
controller: 'FormValidationManagerCtrl as fvmCtrl',
priority: -1,
bindToController: true
};
}
}());
According to the angular docs:
If the require property is an object and bindToController is truthy,
then the required controllers are bound to the controller using the
keys of the require property. This binding occurs after all the
controllers have been constructed but before $onInit is called. See
the $compileProvider helper for an example of how this can be used.
So I expect that in my controller:
(function () {
'use strict';
angular
.module('core')
.controller('FormValidationManagerCtrl', FormValidationManagerCtrl);
FormValidationManagerCtrl.$inject = ['$timeout', '$scope'];
function FormValidationManagerCtrl($timeout, $scope) {
var vm = this;
vm.API = {
validate: validate
};
//vm.$onInit = activate;
function activate() {
}
function validate() {
$scope.$broadcast('fvm.validating');
var firstInvalidField = true;
Object.keys(vm.formCtrl).forEach(function (key) {
var prop = vm.formCtrl[key];
if (prop && prop.hasOwnProperty('$setTouched') && prop.$invalid) {
prop.$setTouched();
if (firstInvalidField) {
$timeout(function(){
var el = $('[name="' + prop.$name + '"]')[0];
el.scrollIntoView();
}, 0);
firstInvalidField = false;
}
}
});
return firstInvalidField;
}
}
})();
vm.formCtrl will be populated with the form controller. However, it is undefined. Why is it undefined? Additionally, I tried to access it in the $onInit function but the $onInit function never got called. What am I doing wrong?
I am using the directive like so:
<form novalidate name="editUserForm" form-validation-manager>

I'm not sure if this is your issue, but I think your directive declaration should look like this:
controller: 'FormValidationManagerCtrl',
controllerAs: 'vm',
rather than:
controller: 'FormValidationManagerCtrl as fvmCtrl',

Looks like this is related to the angular version. I was in 1.4.6 which did not support this. I have upgraded to 1.5.0 in which the code in the OP is working great.

Related

Angular: How to encapsulate logic in a directive?

I wonder how I can encapsulate functionality inside of an angular directive according to Rober C. Martin's "Clean Code" book. I want to omit comments and use functions with speaking names instead.
Imagine this code:
app.directive('myDirective' function() {
return {
link: function(scope) {
// initialize visual user state
scope.visualUserState = {
// some very detailed state initialization
}
}
})
To encapsulate the load functionality, I would like to replace this code like this:
app.directive('myDirective' function() {
return {
link: function(scope) {
scope.initializeVisualUserState = function() {
scope.visualUserState = {
// some very detailed state initialization
}
}
scope.initializeVisualUserState();
}
})
What I do not like on the second approach is that "loadDataFromServer" is some functionality that is only used by the link function and not by the view, so I violate the rule that scope should only hold data and functions that is used to interact with the view.
Also the code is not very readable I think.
As the functionality handles very private stuff of the directive, I think that using and injecting a service is not the right way to go.
What would be a better practice to encapsulate this functionality?
You should use a controller to add logic to your directive. In your controller you can inject Services. It's best to write a service for a single purpose, and simply let your controller call the services.
In fact you should only use the link function if you need to have your DOM-node, which is actually pretty close to never.
Read the styleguide by John Papa
angular.module('myModule', []);
// Controller
(function() {
angular
.controller('myModule')
.controller('MyController', ['$scope', 'DataService', function($scope, DataService) {
DataService
.retrieveData()
.then(function(data) {
$scope.visualUserState = data;
});
}]);
})();
// Directive
(function() {
angular
.module('myModule')
.directive('myDirective', function() {
return {
'restrict': 'E',
'scope': true,
'controller': 'MyController',
'controllerAs': '$ctrl'
};
});
})();
(function(module){
module.directive('myDirective', myDirective);
function myDirective(){
var directive = {
link : link,
scope : {state : '='},
restrict : 'EA',
template : //Some template which uses state
}
return directive;
function link(){
}
};
module.controller('myController', myController);
myController.$inject = ['$scope', 'OtherService'];
function myController(scope, service){
//service is used to pull the data and set to $scope.userState.
}
})(angular.module('myApp'))
And your directive will be :
<myDirective state="userState"></myDirective>
Let me know if this helps.

Testing Directive Angular using ControllerAs

I'm trying to test a directive. using controllerAs creates a toplevel scope where we can access its properties. however when debugging I try to access element.scope().property --I am getting undefined. Any help on why would be greatly appreciated.
--BugDirective
(function() {
'use strict';
angular
.module('debug-blog-app')
.directive('avBug', avBug);
function avBug() {
return {
restrict: 'E',
templateUrl: 'views/directives/bug.html',
scope: {
bug: '='
},
controller: BugFormController,
controllerAs: 'bugCtrl',
bindToController: true
};
};
BugFormController.$inject = ['$window', '$scope', 'BugService'];
function BugFormController($window, $scope, BugService) {
var vm = this;
vm.updateBug = function(){
BugService.updateBug(vm.bug);
};
vm.deleteBug = function(){
if($window.confirm("Delete the Bug?!")){
return BugService.deleteBug(vm.bug.id)
.then(function(){
$scope.$emit('bug.deleted', vm.bug);
});
}
};
};
})();
--Spec
'use strict'
describe('avBug Directive', function () {
var bugCtrl,
element,
BugService,
$scope,
$rootScope;
beforeEach(module('app'));
beforeEach(inject(function($q, $compile, _$rootScope_, _BugService_) {
$rootScope = _$rootScope_;
var directiveMarkup = angular.element("<av-bug></av-Bug>");
element = $compile(directiveMarkup)($rootScope);
bugCtrl = element.scope().bugCtrl;
BugService = _BugService_;
spyOn(BugService, 'deleteBug').and.callFake(function() {
var deferred = $q.defer();
deferred.resolve('data');
return deferred.promise;
});
spyOn($rootScope,'$emit').and.callThrough();
}));
it('should delete a bug', function() {
bugCtrl.deleteBug(0);
expect(BugService.deleteBug).toHaveBeenCalledWith(0);
$rootScope.$digest();
expect($rootScope.$emit).toHaveBeenCalledWith('bug.deleted');
});
});
--index.html
<div class="container">
<div ui-view></div>
</div>
--home.html
<av-bug bug="bug" ng-repeat="bug in homeCtrl.bugs"></av-bug>
I would also add, for unit testing purposes, that you can get the parent controllerAs with the same function that #Ramy Deeb post.
vm = element.scope().$$childTail.nameOfParentControllerAs
And for testing the element isolate scope, just get
isolateScope = element.isolateScope()
I hope this can help anyone like me ending here searching how to unit test directives and controllers calling them, all of them being using ControllerAs syntax.
Thanks you.
I would comment, but I cannot.
Your controller is located at:
element.scope().$$childTail.bugCtrl

Angular: accessing directive attributes in controller constructor

(Using Angular 1.5)
I am trying to use the following design pattern for my angular app:
angular.module("app").directive("MyDirective", MyDirective);
function MyDirective () {
return {
bindToController: {
someId: "=",
otherAttr: "#"
},
controller: ["$attrs", MyController],
controllerAs: "ctrl"
};
}
function MyController ($attrs) {
var self = this;
console.log(self);
console.log($attrs);
}
I use my directive in my HTML like this:
<my-directive someId="someParentScope.model._id" otherAttr="1">
In the console, for self I see:
Object { otherAttr: undefined, someId: undefined }
and for $attrs I see:
Object { $attr: Object, $$element: Object, otherattr: "1",
someid: "someParentScope.model._id", otherAttr: undefined, someId: undefined,
$$observers: Object }
How may I accomplish what I am trying to accomplish (i.e. passing data from parent scope into my directive controller constructor), and why is even my single binding ("#" binding) otherAttr undefined in the controller constructor? Is this design pattern is flawed/misguided? Thanks!
looks like you have the naming conventions wrong on the directive, you should define the name on directive attribute as "data-content" and use on the controller as "dataContent"
Take a look this demo i've done
// directive HTML
<my-directive some-id="name" other-attr="1"></my-directive>
//app.js
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
});
app.directive('myDirective', function() {
return {
restrict: 'E',
bindToController: {
someId: "=",
otherAttr: "#"
},
controller: ["$attrs", MyController],
controllerAs: "ctrl"
}
});
function MyController ($attrs) {
var self = this;
console.log(self);
console.log($attrs);
}
http://plnkr.co/edit/P3VKdO?p=preview

AngularJS Directive accessing controler scope

TL;DR I solved my problem. Here is plunker with 3 different solutions:
http://plnkr.co/edit/E0ErKs?p=preview
I don`t like slider1 because it stores value in $scope ( {{sliderValue}} ) and according to recommendation from Angular Style Guide we should avoid that.
I don`t like slider2 because it assumes that controler have alias vm in a view (so we create some kind of coupling between view and directive).
Solution 3 looks OK for me. Am I missing something?
How would you write differently this directive to be in complience with Angular philosophy?
INITIAL QUESTION:
I am learning angular and not everything is clear to me yet.
I found this question:
How to use jQuery in AngularJS
So I created working example:
Directive:
(function() {
'use strict';
angular.module('demoApp').directive('slider', function () {
return {
restrict: 'A',
controller: function ($scope, $element, $attrs) {
$scope.onSlide = function (e, ui) {
$scope.sliderValue = ui.value;
$scope.$digest();
};
},
link: function (scope, el, attrs) {
var options = {
value: scope.sliderValue,
slide: scope.onSlide
};
// set up slider on load
angular.element(document).ready(function () {
scope.$slider = $(el).slider(options);
});
}
}
});
})();
Controller:
(function() {
'use strict';
angular.module('demoApp').controller('DemoAppTestCtrl', DemoAppTestCtrl);
DemoAppTestCtrl.$inject = [ '$scope' ];
function DemoAppTestCtrl($scope) {
$scope.sliderValue = 10;
}
})();
And Html page:
<div ng-controller="DemoAppTestCtrl as vm">
Value: {{sliderValue}}
<div slider></div>
</div>
Everything works fine. Angular put slider in place of <div slider> and I can move it and I see changing values in {{sliderValue}}.
Then I found this Angular Style Guide
https://github.com/johnpapa/angular-styleguide
In chapter about controllers they recommend to use controllerAs with vm syntax (because $scope is bad or something).
Ex:
function CustomerController() {
var vm = this;
vm.name = {};
vm.sendMessage = function() { };
}
So I changed my controller to this:
(function() {
'use strict';
angular.module('demoApp').controller('DemoAppTestCtrl', DemoAppTestCtrl);
DemoAppTestCtrl.$inject = [ ];
function DemoAppTestCtrl($scope) {
var vm = this;
vm.sliderValue = 10;
}
})();
And Html page to:
<div ng-controller="DemoAppTestCtrl as vm">
Value: {{vm.sliderValue}}
<div slider></div>
</div>
But i don`t know how to fix my directive.
I want the same functionality, when i move the slider i want to set vm.sliderValue inside controler instead $scope.sliderValue inside scope.
EDIT1:
I was able to make it work by adding $scope.vm inside controller and link functions (because my controller sits in scope as vm). But I am not sure if this is right way to do it, because now my directive assume that there is controller in scope under $scope.vm alias.
Is this bad design or normal way of doing things in Angular ?
(function () {
'use strict';
angular
.module('demoApp')
.directive('slider', slider);
slider.$inject = [ ];
function slider() {
return {
restrict: 'A',
controller: function ($scope, $element, $attrs) {
$scope.vm.onSlide = function (e, ui) {
$scope.vm.sliderValue = ui.value;
$scope.$digest();
};
},
link: function (scope, el, attrs) {
var options = {
value: scope.vm.sliderValue,
slide: scope.vm.onSlide
};
// set up slider on load
angular.element(document).ready(function () {
scope.$slider = $(el).slider(options);
});
}
}
}
})();
EDIT2:
I was able to create working Plunker with 3 different versions:
http://plnkr.co/edit/E0ErKs?p=preview
Use scope: false as a option in the directive.
http://www.undefinednull.com/2014/02/11/mastering-the-scope-of-a-directive-in-angularjs/
try something like this:
angular.module('myApp', []);
angular.module('myApp').controller('DemoAppTestCtrl', DemoAppTestCtrl);
function DemoAppTestCtrl() {
var vm = this;
vm.sliderValue = 10;
}
angular.module('myApp').directive('slider', sliderDirective );
function sliderDirective() {
return {
restrict: 'A',
controller: sliderController,
controllerAs: 'sliderCtrl',
template: "<p>{{sliderCtrl.test}}</p>"
}
}
function sliderController() {
var vm = this;
vm.test = "hello";
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="DemoAppTestCtrl as vm">
Value: {{vm.sliderValue}}
<div slider></div>
</div>
</div>

Extending a controller function within directive

I have a cancel function in my controller that I want to pass or bind to a directive. This function essentially clears the form. Like this:
app.controller('MyCtrl', ['$scope', function($scope){
var self = this;
self.cancel = function(){...
$scope.formName.$setPristine();
};
}]);
app.directive('customDirective', function() {
return {
restrict: 'E'
scope: {
cancel : '&onCancel'
},
templateUrl: 'form.html'
};
});
form.html
<div>
<form name="formName">
</form>
</div>
However, the $setPristine() don't work as the controller don't have access on the form DOM. Is it possible to extend the functionality of controller's cancel within the directive so that I will add $setPristine()?
Some suggested using jQuery to select the form DOM, (if it's the only way) how to do that exactly? Is there a more Angular way of doing this?
Since the <form> is inside the directive, the controller should have nothing to do with it. Knowing it would break encapsulation, i.e. leak implementation details from the directive to the controller.
A possible solution would be to pass an empty "holder" object to the directive and let the directive fill it with callback functions. I.e.:
app.controller('MyCtrl', ['$scope', function($scope) {
var self = this;
$scope.callbacks = {};
self.cancel = function() {
if( angular.isFunction($scope.callbacks.cancel) ) {
$scope.callbacks.cancel();
}
};
});
app.directive('customDirective', function() {
return {
restrict: 'E'
scope: {
callbacks: '='
},
templateUrl: 'form.html',
link: function(scope) {
scope.callbacks.cancel = function() {
scope.formName.$setPristine();
};
scope.$on('$destroy', function() {
delete scope.callbacks.cancel;
});
}
};
});
Use it as:
<custom-directive callbacks="callbacks"></custom-directive>
I'm not sure I am OK with this either though...

Resources