AngularJs attributes vs scope in directives - angularjs

What is the difference while using variables though attributes vs scope while define directives? For example -
angular.module('tModule')
.directive('tModule', function() {
return {
restrict: 'E',
scope: true,
templateUrl: function(element, attributes) {
return attributes.variable1;
}
}
});
versus if I use scope. like below -
angular.module('tModule')
.directive('tModule', function() {
return {
restrict: 'E',
scope: {
variable1: "=variable1",
variable2: "=variable2"
},
templateUrl: function() {
return variable1;
}
}
});
What are the differences and advantages?

There are a few differences based on your example:
In your top example, specifying scope: true means to create a new instance of the parent scope. In the bottom example, using an object syntax means to create an isolated scope. So, when you pass parameters to a directive using the scope syntax like that, you are inherently creating an isolated scope for the directive.
If you want to take advantage of two-way data binding (=) or method invocation (&), then you'd want to use the second method of passing through scope instead of attributes.

Related

Can I have attributes in an Angular directive which uses parent scope?

I have a directive that uses the data from the parent scope. Now, I want to have an attribute in it, but still want to use the parent scope and not transfer all the other data in more attributes and duplicate objects.
var myDirective = function () {
return {
restrict: 'A',
templateUrl: '/Areas/GetAdvice/Templates/Directives/loan-comparison-table-directive.html?1',
//scope: - Don't add anything here to use the scope of the parent
// But how to use an attribute in this way?
controller: [
'$scope', function ($scope) {
// Some Code
}
}
So, how can I add an attribute (independent variable) in the directive, when having the parent scope inherited?
EDIT:
I want just a simple boolean property - something like isDescriptionVisible (true/false). The problem is that I use the same directive twice in my view, so I cannot have a variable in the parent scope, because it will affect both directives and I won't have control on them separately.
You need to create an isolated scope for the directive. This is the exact use case for it. Otherwise you need to create separate variables to track the booleans in the parent scope.
If you don't need to pass boolean value from the parent scope you can do the following:
var myDirective = function () {
return {
restrict: 'A',
templateUrl: '/Areas/GetAdvice/Templates/Directives/loan-comparison-table-directive.html?1',
scope: {}
controller: [
'$scope', function ($scope) {
$scope.isVisible = false;
}
}
If you need to pass it from the parent scope just simply replace
scope: {}
with
scope: {
isVisible: '='
}

What is the difference between using "require:ngController" and "controller:" in a directive?

I'm switching from a directive created using:
return {
restrict: 'E',
templateUrl: '/src/templates/noise/swatches.html',
link: link,
controller: "swatchesController"
};
and
<swatches-directive></swatches-directive>
to using:
return {
restrict: 'E',
templateUrl: '/src/templates/noise/swatches.html',
link: link,
require: "ngController"
};
and
<swatches-directive ng-controller="swatchesController"></swatches-directive>
This seems to have unanticipated side-effects on existing watches belonging to other directives against scope variables that the swatches-directive assigns to. From what I understand, the new way introduces a new scope, so assigning watched variables to the parent scope seems like it should work, but those watches refuse to trigger.
Are there fundamental differences between the two methods used above?
Few points to note:
You should use the controllerAs property to segregate the
controllers. It's considered as a best practice. You can read it here
Ideally, you should provide the controller within the directive itself. The way you're doing it causes spagetti scopes. Keep the directive scope separate from parent scopes. If you want you can pass the required dependencies to a directive.
return {
restrict: 'E',
templateUrl: '/src/templates/noise/swatches.html',
link: link,
require: "ngDirective", //Get the controller of that directive
link : function(scope, element, attributes, ngController){
//With require, you say: get the controller of that directive. You can
//then use it as 4th parameter (ngController)
}
};
Note: you can pass multiple controllers
return {
restrict: 'E',
templateUrl: '/src/templates/noise/swatches.html',
link: link,
require: ["ngDirective1", "ngDirective2"], //Get the controller of that directive
link : function(scope, element, attributes, controllers){
var ctrl1 = controllers[0];
var ctrl2 = controllers[1];
//Require can be a string or, as this example, an array.
}
};
The directive passed inside require must be in your directive.
If it is not, you can say: find it on container elements with ^:
require : '^ngDirective'

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

angular directive with isolate scope, fields inaccessible

I'm trying to write a directive which takes a scope variable name and assigns to it the result of passing a different named parameter into a function. Below, the files="result" is intended to create a {{result}} variable in the glob isolate scope. The contents of the "matching" variable are to be evaluated in the parent context, and assigned to an isolate 'matching' variable.
the directive then calls a function eventually assigning to the isolate variable pointed to by files (result here) the array returned. expansion of {{result}} could then be used for example in an ng-repeat.
The directive should be reusable without changing the variable names.
This isn't happening. If I assign everything to a parent, I can get it working but need to change the variable names each time.
angular.module('j20-glob', ['api'])
/*
* usage: <glob files="result" matching="/bin/{{prefix}}*">
* {{result}}
* </glob>
* should allow another just after the first without stomping result
* <glob files="result" matching="/something">{{result}}</glob>
*/
.directive('glob', ['$parse', 'api', function($parse, $api) {
return {
priority: 99, // it needs to run after the attributes are interpolated
restrict: 'AE',
scope: {
},
link: function(scope, iElement, iAttributes) {
var indexModel = $parse(iAttributes.files);
iAttributes.$observe('matching', function(value) {
if (!value)
return;
$api.glob(value).then(function(res) {
indexModel.assign(scope, res);
// indexModel.assign(scope.$parent, res);
});
});
}
}
}
]);
If I understand your code here, you are having a similar issue to what I answered here: Directive doesn't work when I which the version of Angular to 1.0.1 to 1.2.27.
You have created an Element Directive, called glob. The Directive has an Isolate Scope, which you attach a property, result in your example. This all works fine. The problem is, the property in the isolate scope is only accessible within the directive; and in your case, you are trying to access it outside the directive.
The Element <glob></glob> is your directive. This Element can be containers for other Elements, for example an angular expression {{result}} but these Elements are not part of the Directive, and therefore not scoped in the isolate.
If you were to include a template, and place {{result}} inside the template, you would see the expected result. However, this stops working if you change the variable you are passing in.
A rough draft of a working Directive using a transclude function might be something like:
.directive('glob', ['$parse', 'api', function($parse, $api) {
return {
priority: 99, // it needs to run after the attributes are interpolated
restrict: 'AE',
scope: {
},
transclude : true,
link: function(scope, iElement, iAttributes, ctrl, transclude) {
var indexModel = $parse(iAttributes.files);
iAttributes.$observe('matching', function(value) {
if (!value)
return;
$api.glob(value).then(function(res) {
indexModel.assign(scope, res);
// indexModel.assign(scope.$parent, res);
});
//append our scope into the DOM element (clone) instead of $scope
transclude(scope, function(clone, scope){
element.append(clone);
});
});
}
}
}
]);

directive's template not accessing the scope when isolating it

I'm trying to make a simple directive that gets an attribute and displays it from inside the directive
Here is the directive coed:
angular.module('JJJ')
.directive('jobCard', function () {
return {
template: '<div>name: {{job}}</div>',
restrict: 'E',
scope:{
job: "=j"
},
link: function postLink(scope, element, attrs) {
}
};
});
html usage:
<job-card ng-repeat="j in jobs" job="j.name"></job-card>
The directive doesnt show anything. Why is that?
By defining the scope variable job to =j, you are telling Angular to look for an attribute named j.
Isolated scope works by defining the name of the property in the isolated scope (ex job) and then setting that to a binding setting (ex. = is two-way). In order to name the attribute you can append the name of the attribute to the end of the binding setting (ex. =myAttribute would look for an attribute named myAttribute and set the value on the directive's scope property named job).
angular.module('JJJ')
.directive('jobCard', function () {
return {
template: '<div>name: {{job}}</div>',
restrict: 'E',
scope:{
job: "="
},
link: function postLink(scope, element, attrs) {
}
};
});
When you write an isolated scope like this:
scope: {
job: "=j"
}
It means that outer scope variable referred with attribute j is set with a two-way-data-binding to inner scope variable job.
You should write it like so:
angular.module('JJJ')
.directive('jobCard', function () {
return {
template: '<div>name: {{job}}</div>',
scope:{
job: "="
},
link: function postLink(scope, element, attrs) {
}
};
});
When you want to just display a value from the outer scope (say, this directive is within controller and job is used in controller) use '#' in the directive's scope object instead of '='. As llan pointed out '=' is a two way binding which updates value in the outer scope if the value is changed inside the directive scope. In your example, for instance, changing the value of job in the link function will change the job in the outer scope (in controller) as well.

Resources