Why is what I set in rootScope not available in a directive? - angularjs

I am setting the following:
function appRun(
$rootScope
) {
$rootScope.abc = 99;
}
I am calling a directive like this:
<admin-retrieve-button ctrl="exam" home="home" Network="Network"></admin-retrieve-button>
Here's my directive:
app.directive('adminRetrieveButton', ['stateService', function (stateService) {
return {
scope: {
ctrl: '=',
home: '=',
Network: '=',
abc: '='
},
restrict: 'E',
template: "xxxx {{ abc }} dddd",
link: function (scope, element, attrs) {
scope.stateService = stateService;
scope.entity = attrs["entity"];
}
};
}]);
However when my HTML page comes up it shows:
xxxx dddd
Can someone tell me why the rootScope value of 99 does not show up?

Because you're declaring an isolate scope on your directive, thus creating a new parent less scope. One way to fix this would be to obviously explicitly pass in abc in your HTML:
<admin-retrieve-button ctrl="exam" abc="abc" home="home" Network="Network"></admin-retrieve-button>
Or you could change your directive to not create a scope of its own:
scope:false
or just leave the scope property alone (don't declare it), however passing in a directives dependencies through a scope of its own is good practice IMO, it's more explicit and gets rid of hidden dependencies.

Related

$scope.$apply() is not updating the parent scope. Is this a binding issue?

Perhaps this is a poor way of doing this, so I'm open to changing this.
I am implementing a drag and drop function through a <drag-item> directive and a <drop-target> directive. The <drop-target> directive successfully calls this function in the parent scope:
$scope.testAddSet = function(){
console.log($scope.workoutItem); // gives me the last value
$scope.thisMonth[0].sets.push($scope.workoutItem);
$scope.$apply(); // needed to update the view, but uses the old value for workoutItem "test"
};
$scope.workoutItem = "test"; // value declared here
but my <drag-item> directive doesn't seem to update the value of $scope.workoutItem,
View:
<div ng-repeat="exercise in exercises">
<drag-item workout-item="workoutItem" exercise="exercise"></drag-item>
</div>
<drag-item> directive:
angular.module('app.directives.dragItem', [])
.directive('dragItem', function(){
return {
restrict: 'E',
scope: {
exercise: '=',
workoutItem: '='
},
templateUrl: "templates/dragTile.html",
link: function(scope, element, attrs){
element.on('dragstart', function (event) {
scope.workoutItem = scope.exercise.name; //correctly updates the local scope, but not the parent scope
console.log(scope.workoutItem);
});
}
};
});
How do I fix this?
Looks like the problem is that you are defining $scope.workoutItem in your parent scope and then directive is creating another workoutItem property at directive's scope level. To avoid this, it is recomended that you use dots(.) to define your models. Please try something like:
$scope.workout = {item: undefined}
and then in your view:
<div ng-repeat="exercise in exercises">
<drag-item workout-item="workout.item" exercise="exercise"></drag-item>
</div>
I think this should solve your problem without modifying the directive itself.

Passing a parent directive attribute to a child directive attribute

I'm creating directives for a library that customers can use. I need to let the customers create their own templates for a directive and pass the absolute url value of that template into my directives. One of my directives will have another custom directive inside of it, and it's template will be figured out based upon the value of one of the parent directive's attributes. Here's an example:
<parent-dir menu-template="this.html" item-template="that.html"></parent-dir>
I have a template for this directive that looks like this:
<ul style="list: none" ng-repeat="item in menu">
<child-dir template="{{itemTemplate}}"></child-dir>
</ul>
My directives look like this:
angular.module('myApp')
.directive('parentDir', function () {
return {
restrict: 'E',
scope: {
menuTemplate: '#',
itemTemplate: '#',
menuType: '#',
menuName: '#',
menuId: '#',
},
templateUrl: function (element, attrs) {
alert('Scope: ' + attrs.menuTemplate);
return attrs.menuTemplate;
},
controller: function ($scope, $element, $attrs) {
$scope.defaultSubmit = false;
alert('Menu: '+$attrs.menuTemplate);
alert('Item: ' + $attrs.itemTemplate);
$scope.itemTemplate = $attrs.itemTemplate;
if ($attrs.$attr.hasOwnProperty('defaultSubmit')) {
alert('It does');
$scope.defaultSubmit = true;
}
}
};
})
.directive('childDir', function () {
return {
restrict: 'E',
require: '^parentDir',
templateUrl: function (element, attrs) {
alert('Item Template: ' + attrs.template);
return attrs.template;
},
controller: function ($scope, $element, $attrs) {
$scope.job;
alert('Under job: ' + $scope.itemTemplate);
}
};
});
I'm not showing all of the code but this is the main piece of my problem. When I run this, I keep getting undefined for the template on the childDir.
What is the best practice in perpetuating the value of itemTemplate from the parentDir so that the childDir can use it as it's template?
The reason you're running into problems is because the function that generates the templateUrl is running before a scope has been assigned to your directive - something that has to be done before interpolated data can be replaced.
In other words: at the point that the templateUrl function runs, the value of the template attribute is still "{{itemTemplate}}". This will remain the case until the directive's link (preLink to be precise) function runs.
I created a plunker to demonstrate the point here. Be sure to open the console. You'll see that templateUrl runs before both the parent and child linking functions.
So what do you do instead?
Fortunately, angular provides a $templateRequest service which allows you to request the template in the same way it would using templateUrl (it also uses the $templateCache which is handy).
put this code in your link function:
$templateRequest(attrs.template)
.then(function (tplString){
// compile the template then link the result with the scope.
contents = $compile(tplString)(scope);
// Insert the compiled, linked element into the DOM
elem.append(contents);
})
You can then remove any reference to the template in the directive definition object, and this will safely run once the attribute has been interpolated.

Should I use isolate scope in this case?

I'm implementing an custom input widget. The real code is more complex, but generally it looks like this:
app.directive('inputWidget', function () {
return {
replace:true,
restrict: 'E',
templateUrl:"inputWidget.html",
compile: function (tElement, tAttributes){
//flow the bindings from the parent.
//I can do it dynamically, this is just a demo for the idea
tElement.find("input").attr("placeholder", tAttributes.placeholder);
tElement.find("input").attr("ng-model", tElement.attr("ng-model"));
}
};
});
inputWidget.html:
<div>
<input />
<span>
</span>
</div>
To use it:
<input-widget placeholder="{{name}}" ng-model="someProperty"></input-widget>
The placeholder is displayed correctly with above code because it uses the same scope of the parent: http://plnkr.co/edit/uhUEGBUCB8BcwxqvKRI9?p=preview
I'm wondering if I should use an isolate scope, like this:
app.directive('inputWidget', function () {
return {
replace:true,
restrict: 'E',
templateUrl:"inputWidget.html",
scope : {
placeholder: "#"
//more properties for ng-model,...
}
};
});
With this, the directive does not share the same scope with the parent which could be a good design. But the problem is this isolate scope definition will quickly become messy as we're putting DOM-related properties on it (placeholder, type, required,...) and every time we need to apply a new directive (custom validation on the input-widget), we need to define a property on the isolate scope to act as middle man.
I'm wondering whether it's a good idea to always define isolate scope on directive components.
In this case, I have 3 options:
Use the same scope as the parent.
Use isolate scope as I said above.
Use isolate scope but don't bind DOM-related properties to it, somehow flow the DOM-related properties from the parent directly. I'm not sure if it's a good idea and I don't know how to do it.
Please advice, thanks.
If the input-widget configuration is complex, I would use an options attribute, and also an isolated scope to make the attribute explicit and mandatory:
<input-widget options="{ placeholder: name, max-length: 5, etc }"
ng-model="name"></input-widget>
There is no need to flow any DOM attributes if you have the options model, and the ngModel:
app.directive('inputWidget', function () {
return {
replace:true,
restrict: 'E',
templateUrl:"inputWidget.html",
scope: { options:'=', ngModel: '='}
};
});
And in your template, you can bind attributes to your $scope view model, as you normally would:
<div>
<input placeholder="{{options.placeholder}}" ng-model="ngModel"/>
<span>
{{options}}
</span>
</div>
Demo
Personally, when developing for re-use, I prefer to use attributes as a means of configuring the directive and an isolated scope to make it more modular and readable. It behaves more like a component and usually without any need for outside context.
However, there are times when I find directives with child / inherited scopes useful. In those cases, I usually 'require' a parent directive to provide the context. The pair of directives work together so that less attributes has to flow to the child directive.
This is not a very trivial problem. This is because one could have arbitrary directives on the templated element that are presumably intended for <input>, and a proper solution should ensure that: 1) these directives compile and link only once and 2) compile against the actual <input> - not <input-widget>.
For this reason, I suggest using the actual <input> element, and add inputWidget directive as an attribute - this directive will apply the template, while the actual <input> element would host the other directives (like ng-model, ng-required, custom validators, etc...) that could operate on it.
<input input-widget
ng-model="someProp" placeholder="{{placeholder}}"
ng-required="isRequired"
p1="{{name}}" p2="name">
and inputWidget will use two compilation passes (modeled after ngInclude):
app.directive("inputWidget", function($templateRequest) {
return {
priority: 400,
terminal: true,
transclude: "element",
controller: angular.noop,
link: function(scope, element, attrs, ctrl, transclude) {
$templateRequest("inputWidget.template.html").then(function(templateHtml) {
ctrl.template = templateHtml;
transclude(scope, function(clone) {
element.after(clone);
});
});
}
};
});
app.directive("inputWidget", function($compile) {
return {
priority: -400,
require: "inputWidget",
scope: {
p1: "#", // variables used by the directive itself
p2: "=?" // for example, to augment the template
},
link: function(scope, element, attrs, ctrl, transclude) {
var templateEl = angular.element(ctrl.template);
element.after(templateEl);
$compile(templateEl)(scope);
templateEl.find("placeholder").replaceWith(element);
}
};
});
The template (inputWidget.template.html) has a <placeholder> element to mark where to place the original <input> element:
<div>
<pre>p1: {{p1}}</pre>
<div>
<placeholder></placeholder>
</div>
<pre>p2: {{p2}}</pre>
</div>
Demo
(EDIT) Why 2 compilation passes:
The solution above is a "workaround" that avoids a bug in Angular that was throwing with interpolate values being set on a comment element, which is what is left when transclude: element is used. This was fixed in v1.4.0-beta.6, and with the fix, the solution could be simplified to:
app.directive("inputWidget", function($compile, $templateRequest) {
return {
priority: 50, // has to be lower than 100 to get interpolated values
transclude: "element",
scope: {
p1: "#", // variables used by the directive itself
p2: "=" // for example, to augment the template
},
link: function(scope, element, attrs, ctrl, transclude) {
var dirScope = scope,
outerScope = scope.$parent;
$templateRequest("inputWidget.template.html").then(function(templateHtml) {
transclude(outerScope, function(clone) {
var templateClone = $compile(templateHtml)(dirScope);
templateClone.find("placeholder").replaceWith(clone);
element.after(templateClone);
});
});
}
};
});
Demo 2

ng-include on load with an object

This works
<div ng-include="template.html" onload="person='Jane'"></div>
^ This sets the local scope variable person in the include to 'Jane' (string)
But I want to pass a person object that is called user: {name: 'Jane' }
<div ng-include="template.html" onload="person=user"></div>
^ This sets the local scope variable person in the include to be 'undefined'
How is it possible to pass an object as a local variable to ng-include?
Maybe what you actually want is a custom directive:
<div person-directive="{name:'Jane'}"></div>
JS:
angular.module('myApp',[])
.directive('personDirective',function(){
return {
restrict: 'A',
scope: {
person: '=personDirective'
},
templateUrl: 'template.html'
};
});
With this, you bind the passed-in value to person in the loaded template.
Working fiddle
ng-include is not that reusable because it doesn't offer a way to set local variables. Using onload is bad because it litters the global scope. If the example gets a little more complicated, it'll fail.
Making a new directive instead of using ng-include is a cleaner solution.
The ideal usage looks like:
<div ng-include-template="template.html" ng-include-variables="{ person: 'Jane' }"></div>
<div ng-include-template="template.html" ng-include-variables="{ person: user }"></div>
The directive is:
.directive(
'ngIncludeTemplate'
() ->
{
templateUrl: (elem, attrs) -> attrs.ngIncludeTemplate
restrict: 'A'
scope: {
'ngIncludeVariables': '&'
}
link: (scope, elem, attrs) ->
vars = scope.ngIncludeVariables()
for key, value of vars
scope[key] = value
}
)
You can see that the directive doesn't use the global scope. Instead, the directive reads the object from ng-include-variables and add those members to its own local scope.
This is clean and generic.

ANGULAR: Not able to pass string containing {{}} in directive scope using # after upgrading to angular version to 1.2.3

I'm passing "/foo/bar/{{value}}" as a string using # to the directive so I can use the interpolate method to construct hrefs for a dropdown. I think angular is looking for value when compiling instead of passing the whole thing as a string. This was working fine in the angular v1.2.0.
The directive code goes like,
return {
templateUrl: '/views/directives/directive-name.html',
restrict: 'E',
replace: true,
scope: {
title: '#',
optHref: '#'
},
link: link: function postLink(scope, element, attrs) {
var hrefFormatter;
hrefFormatter = $interpolate(attrs.optHref);
scope.getHref = function(value, label) {
return hrefFormatter({ value: value, label: label });
};
}
Invoked like,
<directive-name
title ="name"
opt-href="/foo/bar/{{value}}" <
</directive-name>
Appreciate any pointers on what might have changed in angular to cause this or other pointers.
Instead of:
opt-href="/foo/bar/{{value}}"
Try this:
opt-href="'/foo/bar/{{value}}'"
Mind the additional '.
Thanks Michal! Used a controller function before the link to preserve the attrs.optHref in the scope. Something like,
controller: function ($scope, $element, $attrs) {
$scope.optHref = $attrs.optHref;
}
In the controller function the {{value}} was already interpolated in scope but existed in attrs. But in the link fn {{value}} was interpolated in attrs as well- so preserved it in scope and used scope.optHrefin the link fn.

Resources