When to declare function with 'scope' vs. 'var' in directive? - angularjs

In this plunk I have a directive that is instantiated twice. In each case, the result of the directive (as defined by its template) is displayed correctly.
Question is whether getValue2() needs to be defined as scope.getValue2 or var getValue2. When to use each in the directive?
HTML
instance 1 = <div dirx varx="1"></div>
<br/>
instance 2 = <div dirx varx="2"></div>
Javascript
var app = angular.module('app', []);
app.controller('myCtl', function($scope) {
});
app.directive('dirx', function () {
var directive = {};
directive.restrict = 'EA';
directive.scope = {
varx: '='
};
directive.template = '{{getValue()}}';
directive.link = function (scope, element, attrs) {
scope.getValue = function(){
return getValue2();
};
var getValue2 = function() {
return scope.varx * 3;
}
};
return directive;
});

The only time you need declare something as a property on the $scope object is when it is part of your application state.
Angular 1.x will "dirty check" the $scope and make changes to the DOM. Anything on the $scope object can be watched, so you can observe the variable and trigger functions. This is why Angular searching & filtering can be done with almost no JS code at all. That being said, it's generally good practice to keep the '$scope' free of anything that isn't needed.
So as far as getValue() is concerned, is it being called on render or in a directive in your HTML? if the answer is "no", then it doesn't need to be declared as a property on the $scope object.
Since you're using getValue() in the directive template, it is being rendered in the UI and needs to be in Angular's $scope.
You can also just do:
directive.template = '{{ varx * 3 }}';
docs: https://docs.angularjs.org/guide/scope

The first thing is that the code contains unnecessary nested calls. it can be:
var getValue2 = function() {
return scope.varx * 3;
}
scope.getValue = getValue2;
The second thing is that getValue2 isn't reused and isn't needed, it can be:
scope.getValue = function() {
return scope.varx * 3;
}
Since getValue is used in template, it should be exposed to scope as scope.getValue. Even if it wouldn't be used in template, it's a good practice to expose functions to scope for testability. So if there's a real need for getValue2, defining and calling it as scope.getValue2 provides small overhead but improves testability.
Notice that the use of link function and direct access to scope object properties is an obsolete practice, while up-to-date approach involves controllerAs and this.

Related

Inserting a function into directive without isolate scope

I'm not sure if I'm going about this the right way. I am using the ui-select directive which does not seem to support the HTML required directive. So I built my own, ui-select-required. It seems I am unable to use isolate scope because ui-select already instantiates an isolate scope.
I want to make ui-select-required take in a function as an attribute. If the attribute is present, the it should validate with the return value of this function. If the attribute is not present then it should validate on presence of a value. This is all a part of a component.
product_details.js
angular
.module('ProductComponents')
.component('productDetails', {
bindings:{
product: '=product',
},
templateUrl: "/template/admin/products/details",
controllerAs: 'prodDetails',
controller: [
'v3Stitcher',
'AjaxLoaderSvc',
'ModelInformationSvc',
'$filter',
'$http',
'current_site',
function(
v3Stitcher,
AjaxLoaderSvc,
ModelInformationSvc,
$filter,
$http,
current_site
){
var prodDetails = this;
...
prodDetails.templateRequired = function(){
// Product types requiring a template
// 3 - customizable_downloadable
// 6 - static_variable_downloadable
var productTypes = [3, 6];
// Specification types requiring a template
var specificationTypes = ["print_on_demand"];
if(productTypes.indexOf(prodDetails.product.product_type) > -1){
return true;
}
if(specificationTypes.indexOf(prodDetails.specification.specification_type) > -1){
console.log('here'); // this gets called
return true;
}
return false;
};
.directive('uiSelectRequired',function(){
return {
restrict:'A',
require:'ngModel',
link:function(scope, elem, attrs, ctrl){
var form = angular.element(document).find('form');
var input = angular.element(elem.find('input')[0]);
var requiredFn = scope[attrs['requiredFn']];
if(requiredFn){
ctrl.$validators.uiSelectRequired = function(){
return requiredFn();
};
} else {
ctrl.$validators.uiSelectRequired = function(modelValue){
return !ctrl.$isEmpty(modelValue)
};
}
form.on('submit', function(){
if(ctrl.$invalid){
elem.find('span').removeClass('ng-valid').addClass('ng-invalid');
}
});
elem.on('change', function(){
if(ctrl.$invalid){
elem.find('span').removeClass('ng-invalid').addClass('ng-valid');
}
});
}
};
});
details.slim
label(ng-class="{'label label-danger': prodDetails.templateRequired()}")
| Template
ui-select(ng-model="prodDetails.product.template_id" name="template" ng-model-options="{ debounce: { default:500, blur: 0 } }" ui-select-required required-fn="prodDetails.templateRequired")
ui-select-match(placeholder="Search Templates...")
| {{$select.selected.name}}
ui-select-choices(position="down" repeat="template.id as template in prodDetails.templates" refresh="prodDetails.refreshTemplates($select.search)" minimum-input-length="1" refresh-delay="0")
| {{ template.name }}
br
| id: {{template.id}}
br
| created: {{template.created_at | date : 'yyyy-MM-dd'}}
The problem I'm having is that the variable requireFn is undefined. However, if in the HTML I send in the controller variable prodDetails alone then requireFn has the correct value of the controller variable.
I think your problem is that:
You are doing controllerAs: 'prodDetails' in your isolate scope and
You are looking to reference the function directly on the scope in your uiSelectRequired directive
I think if you switch this:
var requiredFn = scope[attrs['requiredFn']];
to:
var requiredFn = scope.$eval(attrs.requiredFn);
You should get what you are looking for. This is assuming that templateRequired property has been added to the productDetails component's controller instance.
To reiterate, your issue was that you were looking for the property directly on the isolate scope itself, where it has been added to the controller reference. By doing a scope.$eval, you will essentially be parsing the path prodDetails.templateRequired -- which will hopefully resolve to the function reference you were hoping to get in the first place.
Edit: So the second part of your question in the comments lead me to believe you never needed a function into a directive with isolate scope. I think what you are trying to do is make the template model required conditionally. Angular already gives you this functionality through required and ng-required directives. You state in your question these are not available on ui-select, but they are "helper" directives with ngModel. I believe this is a mostly working example of what you want to do where I switch to required/ng-required and eliminate the need for your custom directive.

Two-way binding with parent.parent scope

I have an issue where a directive with an isolate scope is not making changes via binding to its parent.parent scope.
In summary, I would expect that changes made to a property in the directive's isolate scope which is bound to a parent property would propagate all the way up parent scopes for that property (the property was inherited from a parent.parent scope to begin with), but it doesn't. It only changes the value on the immediate parent scope, while the parent.parent scope has the old value. Worse, any changes to the parent.parent scope property no longer trickle down to the isolate scope or its immediate parent. I understand this is the normal behavior of Javascript's prototype inheritance which Angular scopes are built on, but it's not wanted in this case of two-way data binding and I'm looking for a solution in Angular.
Here is an example of the behavior: http://jsfiddle.net/abeall/RmDuw/344/
My HTML contains a controller div (MyController1), in it is another controller div (MyController2), and in that is a directive (MyDirective):
<div ng-controller="MyController">
<p>MyController1: {{myMessage}} <button ng-click="change()">change</button></p>
<div ng-controller="MyController2">
<p>MyController2: {{myMessage}} <button ng-click="change()">change</button></p>
<p>MyDirective: <my-directive message="myMessage"/></p>
</div>
</div>
The Javascript defines myMessage on the outer MyController scope, the inner MyController2 scope inherits and binds myMessage to message on the directive, and the MyDirective defines message as an isolate scope property. At each level a change() function is defined which changes the local message property:
var app = angular.module('myApp', []);
function MyController($scope) {
var count = 0;
$scope.myMessage = "from controller";
$scope.change = function(){
$scope.myMessage = "from controller one " + (++count);
}
}
function MyController2($scope) {
var count = 0;
$scope.change = function(){
$scope.myMessage = "from controller two " + (++count);
}
}
app.directive('myDirective', function() {
return {
restrict: 'E',
replace: true,
template: '<span>Hello {{message}} <button ng-click="change()">change</button></span>',
scope: {
message: "="
},
link: function(scope, elm, attrs) {
var count = 0;
scope.change = function(){
scope.message = "from directive " + (++count);
}
}
};
});
What you'll notice is that:
If you click the MyController1's change button a few times, all levels are updated (it inherits downwards).
If you then click the MyDirective's change button, it updates MyController2 (binding upward) but does not change MyController1 in any way.
After this point, clicking MyController1's change button no longer trickles down to MyController2 and MyDirective scope, and vice versa. The two are now separated from each other. This is the problem.
So my question is:
Does Angular have a way to allow binding or inheritance of scope properties (myMessage in this case) to trickle all the way up parent scopes?
If not, in what way should I sync changes in a directive's isolate scope to a parent.parent controller scope's properties? The directive can not know about the structure of its parents.
as #Claies mentioned, using controller as is a great idea. This way you can be direct about which scope you want to update, as well as easily being able to pass scopes around in methods.
Here is a fiddle of the results you were likely expecting
Syntax: ng-controller="MyController as ctrl1"
Then inside: {{ctrl1.myMessage}} or ng-click="ctrl1.change()" and click="ctrl2.change(ctrl1)"
This changes the way you write your controller by leaving out the $scope dependency unless you need it for other reasons then holding your model.
function MyController () {
var count = 0
this.myMessage = "from controller"
this.change = function () {
this.myMessage = "from controller one " + (++count)
}
}
function MyController2 () {
var count = 0
this.change = function (ctrl) {
ctrl.myMessage = "from controller two " + (++count)
}
}
The easiest change is using $scope.msg.mymessage instead of just $scope.msg in your root controller.
function MyController($scope) {
var count = 0;
$scope.msg = {};
$scope.msg.myMessage = "from controller";
$scope.change = function(){
$scope.msg.myMessage = "from controller one " + (++count);
}
}
Here's a forked fiddle (that sounds funny) with the intended results.
http://jsfiddle.net/nk5cdrmx/

How to make a variable available in directive when it is set in scope on the controller level?

The controller is below the directive. I am trying to get the language code for the appropriate translation. This is what does it on the controller level.
PaymentPage.get($scope.pageTag)
.success(function(response) {
$scope.product = response;
console.log($scope.product);
// get the language of the product assuming its a valid language code
var lang_string = $scope.product.language;
//take the first two letters of the language and honoring the caveat of en-f for example
var lang = lang_string.substring(0,2);
// Translate the page guideline
$translate.use(lang);
loadCampaigns($scope.product.language);
initialiseBraintree(braintreeApiKey);
});
However, there is a popover directive just above the controller. The directive looks something like this. I thought I'd be able to retrieve the language code using $scope.product.language but I realize that is happening in the controller, so the directive can't access it.
.directive('popover', function($translate) {
// language scope is being set afterwards, must be set before to make use of $translate.use
//$translate.use('de');
var mouseOffset = 10;
return {
link: function (scope, element, attrs) {
// Returns a promise and must be handled with a .then
$translate('HOVER_MESSAGE').then(function (popover) {
console.log(popover);
var popoverElement = angular.element('<div class="myPopover hide">' + popover + '</div>');
element.after(popoverElement);
scope.popoverElement = popoverElement;
});
Also, the directive loads before the controller so other hacks like hiding language code in hidden spans also failed.
I can't try this out just now, but you should be able to use the model to bridge that scope and time gap. If you bind an attribute of your directive to product.language (from $scope.product.language in your controller) then it will re-render the directive when the controller provides the language code, and it will be available in the attrs passed to the directive link().
Something like, in your HTML:
<button id="buyNowButton" type="submit" popover language="{{product.language}}">
In your directive:
return {
link: function (scope, element, attrs) {
var lang = attrs.language;
...

Difference between adding functions to $scope and this in directive controller?

When going through egghead video, the one about Directive to directive communication suggests we use controller to add functions to 'this' object and access it from other directives.
The full code used in the video: Adding functions to this object
The relevant controller code is as follows:
controller: function($scope){
$scope.abilities = [];
this.addStrength = function(){
$scope.abilities.push("Strength");
}
this.addSpeed = function(){
$scope.abilities.push("Speed");
}
this.addFlight = function(){
$scope.abilities.push("Flight");
}
},
I was wondering instead of adding functions to 'this' why not add it to the $scope itself especially when we are using isolated scope?
Code adding functions to $scope: Adding functions to $scope
The relevant controller code is as follows:
controller: function($scope){
$scope.abilities = [];
$scope.addStrength = function(){
$scope.abilities.push("Strength");
};
$scope.addSpeed = function(){
$scope.abilities.push("Speed");
};
$scope.addFlight = function(){
$scope.abilities.push("Flight");
};
},
Or why have the controller function at all. Why can not we use the link function to achieve the same result?
Adding functions to $scope in the link function: Using link funtciont instead of controller
The relevant controller and link function is as follows:
controller: function($scope){
$scope.abilities = [];
$scope.addStrength = function(){
$scope.abilities.push("Strength");
};
$scope.addSpeed = function(){
$scope.abilities.push("Speed");
};
$scope.addFlight = function(){
$scope.abilities.push("Flight");
};
},
I am pretty sure there is valid reason to use controller and this object. I am not able to understand why.
You are correct that you can expose functions in the link function and get the same results. Directive controllers are a bit of an odd bird, but as I've written more complex directives, I've settled on pushing as much behavior into controllers and leaving DOM-related stuff in the link function. The reason why is:
I can pass in a controller name instead of having the function inside my directive; things get cleaner IMO
The controllers can expose public APIs used inside of sibling or related directives so you can have some interop and encourage SoC.
You can isolate the controller testing apart from the directive compilation if you like.
I typically only introduce controllers when there are perhaps complex state transitions, external resources being handled (ie $http), or if reuse is a concern.
You should note that Angular 1.2 exposes 'controllerAs' on directives which allows you to directly consume the controller in directive templates and reduce a bit of the ceremony the $scope composition introduces.

Modify $rootscope property from different controllers

In my rootscope I have a visible property which controls the visibility of a div
app.run(function ($rootScope) {
$rootScope.visible = false;
});
Example HTML:
<section ng-controller='oneCtrl'>
<button ng-click='toggle()'>toggle</button>
<div ng-show='visible'>
<button ng-click='toggle()'>×</button>
</div>
</section>
Controller:
var oneCtrl = function($scope){
$scope.toggle = function () {
$scope.visible = !$scope.visible;
};
}
The above section works fine, the element is shown or hide without problems. Now in the same page in a different section I try to change the visible variable to show the div but it doesn't work.
<section ng-controller='otherCtrl'>
<button ng-click='showDiv()'>show</button>
</section>
Controller:
var otherCtrl = function($scope){
$scope.showDiv = function () {
$scope.visible = true;
};
}
In AngularJS, $scopes prototypically inherit from their parent scope, all the way up to $rootScope. In JavaScript, primitive types are overwritten when a child changes them. So when you set $scope.visible in one of your controllers, the property on $rootScope was never touched, but rather a new visible property was added to the current scope.
In AngularJS, model values on the scope should always "have a dot", meaning be objects instead of primitives.
However, you can also solve your case by injecting $rootScope:
var otherCtrl = function($scope, $rootScope){
$scope.showDiv = function () {
$rootScope.visible = true;
};
}
How familiar are you with the concept of $scope? It looks to me based on your code that you're maintaining two separate $scope variables called "visible" in two different scopes. Do each of your controllers have their own scopes? This is often the case, in which case you're actually editing different variables both named "visible" when you do a $scope.visible = true in different controllers.
If the visible is truly in the rootscope you can do $rootScope.visible instead of $scope.visible, but this is kind of messy.
One option is to have that "otherCtrl" code section in a directive (you should probably be doing this anyway), and then two-way-bind the directive scope to the parent scope, which you can read up on here. That way both the directive and the page controller are using the same scope object.
In order to better debug your $scope, try the Chrome plugin for Angular, called Batarang. This let's you actually traverse ALL of your scopes and see the Model laid out for you, rather than just hoping you're looking in the right place.

Resources