AngularJS : Directive Isolated Scoping - angularjs

I'm developing a AngularJS Directive that is a table. The parent most control ng-table needs to have a isolated scope for its options and model.
However, the children should 'inherit' that scope so that you don't have to pass tons of options to a very interconnected group of components.
For example:
<div ng-table="tableOptions" ng-model="results">
<div ng-table-header>
<div ng-table-header-column="column"
ng-repeat="column in tableOptions.columns">
{{column.name}}
</div>
</div>
<div ng-table-body>
<div ng-table-row="row"
ng-repeat="row in $results">
<div ng-table-cell="column"
ng-repeat="column in tableOptions.columns">
{{row[column.id]}}
</div>
</div>
</div>
<div ng-table-footer></div>
</div>
In the above example, ng-table-header, ng-header-column, etc all need to access properties from the parent control ng-table. Furthermore, all the directives replace and transclude.
I know I could broadcast events to the children/parent directives but is there a better way to restrict scope at the parent level and auto pass it to the children?

Oh dude, you have forced me to look how forms and inputs directives are implemented in angular.
So, angularjs has similar thing to what you want to implement - ng-form directive with ng-input.
When you use form or ng-form directive, it creates controller(directives can do it) and transclude html-code will inherit it. ng-input(input) asks for it in require option.
require: ['ngModel', '^?form', '^?ngModelOptions'],
So you get this controller in link function. So... you can call functions, do things, you know.
You can do it like this(just an idea, this is how it works):
.directive('coolTable', function() {
return {
...
controller: function($scope) {
$scope.columns = []; /* Here you will have all table columns, so you can send it's data to service and do any work */
$scope.registerColumn = function(column) {
$scope.columns.push(column);
};
},
...
};
})
.directive('column', function() {
return {
...
require: '^?coolTable',
...
link: function(scope, element, attrs, coolTable) {
coolTable.registerColumn(this);
},
};
})
<coolTable>
<column></column>
<column></column>
<column></column>
</coolTable>

I was able to find a work around looking at angular-ui code that created their own transclude directive to apply scope.
so my master table directive starts off like:
module.directive('ngTable', function ($rootScope, $timeout, $window, $compile) {
return {
restrict: 'AE',
transclude: true, // NOTE THIS
replace: true, // NOTE THIS
templateUrl: 'common/components/table/views/table.tpl.html',
scope: {
ngModel: "=",
tableOptions: "=ngTable"
},
controller: 'ngTableCtrl',
link: function ($scope, $element, $attributes, controller, ngModel) {
....
}
}
});
then I create my own transclude directive like:
module.directive('tableTransclude', function () {
return {
link: function ($scope, $element, $attrs, controller, $transclude) {
$transclude($scope, function (clone) {
$element.empty();
$element.append(clone);
});
}
};
})
and the usage in the template of table looks like:
<div table-transclude />
And presto!!! A little hacky but gets the job done. I understand why Angular did this but in some cases you need this type of scoping.

Related

Cannot pass directive scope values to 3rd party directive attributes. (text-angular)

Basically, I have an input block, that uses Text-Angular* and I needed to wrap it into its own parent directive for easier manual configuration. I am trying to pass values into my directive's scope attributes, down to Text-Angular's directive, but it won't pickup what it needs in its own custom attributes, from the parent one. (it's for synchronization and saving its own state with the entered input from the user) I simply need to bridge the communication between parent and children elements that are separate from one another, while being able to communicate with Text-Angular.
(the third party is named Text-Angular and is linked via CDN)
This is the way I tried things. The problem mainly concerns ng-model.
HTML
mainfile.html
<div class="col-sm-12 p-l-0 form-group">
<label for="value" translate="text_language"></label>
<text-angular-wrap ng-model="vm.value.info" id="value" name="value"></text-angular-wrap>
</div>
template-file
It is wrapped in its own controller file.
<div ng-app="textAngularApp" ng-controller="textAngularEditor">
<text-angular ta-toolbar="[['h1','h2','h3'],['bold','italics','underline'],['ul','ol'],['indent','outdent']]" ng-model="ngModel" id="{{id}}" name="{{name}}">
</text-angular>
<pre>name: {{name}}</pre>
<pre>id: {{id}}</pre>
<pre>ng-model: {{ngModel}}</pre>
</div>
textAngularWrap.directive.js
(function () {
'use strict';
angular.module("file.UI")
.directive("textAngularWrap", function() {
return {
restrict: "AE",
scope: {
name: "#",
id: "#",
ngModel: "=",
},
templateUrl: helper.directive('TextAngularTemplate'),
require: "ngModel",
link: function($scope, elem, attr, ctrl) {
console.log($scope, elem, attr, ctrl);
$scope.name = attr.name,
$scope.id = attr.id;
$scope.ngModel = attr.ngModel;
}
}
});
})();
textAngular.js
(function () {
'use strict';
var textAngular = angular.module("textAngularApp", ['textAngular']);
textAngular.config(['$provide', function ($provide) {
$provide.decorator('taOptions', ['$delegate', function (taOptions) {
taOptions.defaultTagAttributes.a.target = '_blank';
taOptions.defaultTagAttributes.a.content = '';
return taOptions;
}]);
}]);
textAngular.controller('textAngularEditor', ['$scope', 'textAngularManager',
function textAngularEditor($scope, textAngularManager) {
$scope.htmlcontent = null;
$scope.disabled = false;
}]);
})();
I tried everything I could find, to trying to pass it down to the controller file, to trying to pass it in diverse ways with the directive, to play around with parent and children linking and nothing works.
ng-model in text-angular simply won't pickup whatever I pass down from the parent directive. It stays the same as "ngModel" in the inspector.
I'm out of ideas. How do I pass stuff down to a directive I cannot access?

Require a directive's controller that is nested in a parent directive, AngularJS

I'm wondering if there is a way to require the controller of a directive that exists/is nested somewhere as a common parent's child directive in AngularJS.
Directive Structure
Suppose I have the following structure for my directives:
<parent-directive>
<ul>
<li some-nested-directive ng-repeat="dir in directives"></li>
</ul>
<settings-menu></settings-menu>
</parent-directive>
Directive Definition
/*
* some-nested-directive directive definition
*/
.directive('someNestedDirective', function(){
// ...
return {
restrict: 'A',
controller: someNestedDirectiveController
};
});
/*
* settings-menu directive definition
*/
.directive('settingsMenu', function(){
return {
restrict: 'AE',
require: [], // how to require the nested-directive controller here?
link: function(scope, iElement, attrs, ctrls){
// ...
}
};
})
I've already checked out this SO question which states how to require controllers of directives that exist along the same line in a hierarchy.
But my question is regarding a way to do the same in a hierarchy of directives that NOT necessarily exist along the same line. And if this is not possible, what is a proper workaround for it. Any help would be appreciated.
EDIT
Also, can any of the prefixes for require (or a combination of them) be used to achieve the same?
One approach is to use parent directive as a way to pass references between controllers:
var mod = angular.module('test', []);
mod.directive('parent', function() {
return {
restrict: 'E',
transclude: true,
template: '<div>Parent <div ng-transclude=""></div></div>',
controller: function ParentCtrl() {}
}
});
mod.directive('dirA', function() {
return {
restrict: 'E',
template: '<div>Dir A <input type="text" ng-model="name"></div>',
require: ['dirA', '^^parent'],
link: function(scope, element, attrs, ctrls) {
//here we store this directive controller into parent directive controller instance
ctrls[1].dirA = ctrls[0];
},
controller: function DirACtrl($scope) {
$scope.name = 'Dir A Name';
this.name = function() {
return $scope.name;
};
}
}
});
mod.directive('dirB', function() {
return {
restrict: 'E',
template: '<div>Dir A <button ng-click="click()">Click</button></div>',
require: ['dirB', '^^parent'],
link: function(scope, element, attrs, ctrls) {
//let's assign parent controller instance to this directive controller instance
ctrls[0].parent = ctrls[1];
},
controller: function DirBCtrl($scope) {
var ctrl = this;
$scope.click = function() {
//access dirA controller through parent
alert(ctrl.parent.dirA.name());
};
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app='test'>
<parent>
<dir-a></dir-a>
<dir-b></dir-b>
</parent>
</div>
When using above approach it also makes sense to encapsulate how the dirA controller is stored inside parent controller i.e. by using a getter property or by exposing only the required properties of dirA controller.
I aggree with miensol's reply and I recommend that approach but in some cases you may need something like that;
<parent-directive>
<ul>
<some-nested-directive id="snd1" ng-repeat="dir in directives"></some-nested-directive>
</ul>
<settings-menu some-nested-directive-id="snd1"></settings-menu>
You can access the scope of some-nested-directive using its id from the settings-menu;
$("#" + scope.someNestedDirectiveId).scope()
Once I used this approach to cascade the values of a dropdown according to the choise of another independent dropdown.

how to call a controller function from ng-click directive?

I have two directives and a controller, the problem is that i can't call a function 'addMarkers()' of the controller from my directive .
i have the following codes:
derectives.js
app
.directive('collection', function () {
var tpl = '<ul><member ng-repeat="member in collection" member="member"></member></ul>';
return {
restrict: "E",
replace: true,
scope: {
collection: '='
},
template: tpl
}
})
app
.directive('member', function ($compile) {
var tpl = '<li><a ng-click="addMarkers(member)" >{{member.title}}</a>'+
'<input class="align" ng-if="!member.children" type="checkbox" ng-checked="true"/></li>';
return {
restrict: "E",
replace: true,
scope: {
member: '='
},
template: tpl,
link: function (scope, element, attrs) {
if (angular.isArray(scope.member.children)) {
element.append("<collection collection='member.children'></collection>");
$compile(element.contents())(scope)
}
}
}
})
controller.js
app
.controller('IndexCtrl', function($scope, itemProvider){
itemProvider.getItems().success(function(data){
$scope.items = data;
});
$scope.addMarkers = function(item){
alert("Helloo");
$scope.markers = itemProvider.addMarkers();
}
});
index.html
<div id="menu" ng-controller="IndexCtrl">
<nav>
<h2><i class="fa fa-reorder"></i>All Categories</h2>
<collection collection='items'></collection>
</nav>
</div>
$rootScope is the global scope which should be used only when required. It should be kept as clean as possible to avoid pollution of scope variables.
In order to access parent method from isolated scope you can use $parent service, as shown below:
scope.$parent.addMarkers();
example: basic example
In your case, The directive that wants to access the parent is again called from inside another directive,hence for such cases you can use $parent.$parent,
scope.$parent.$parent.addMarkers(); as shown in the following:
example:example for your case
This can be done if the number of directives using the parent scope is limited. If the hierarchy is long, then using multiple $parent makes the code clumsy. In those cases, it is preferable to add the parent method inside a service and call the service itself from the directives.
Example: service example
I should use $rootScope instead of $scope like below,
$rootScope.addMarkers = function(item){
alert("Helloo");
$scope.markers = itemProvider.addMarkers();
}

Directive template unique IDs for elements in AngularJS

I have a directive that can be used multiple times on a page. In the template of this directive, I need to use IDs for an input-Element so I can "bind" a Label to it like so:
<input type="checkbox" id="item1" /><label for="item1">open</label>
Now the problem is, as soon as my directive is included multiple times, the ID "item1" is not unique anymore and the label doesn't work correctly (it should check/uncheck the checkbox when clicked).
How is this problem fixed? Is there a way to assign a "namespace" or "prefix" for the template (like asp.net does with the ctl00...-Prefix)? Or do I have to include an angular-Expression in each id-Attribute which consists of the directive-ID from the Scope + a static ID. Something like:
<input type="checkbox" id="{{directiveID}} + 'item1'" /><label for="{{directiveID}} + 'item1'">open</label>
Edit:
My Directive
module.directive('myDirective', function () {
return {
restrict: 'E',
scope: true,
templateUrl: 'partials/_myDirective.html',
controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
...
} //controller
};
}]);
My HTML
<div class="myDirective">
<input type="checkbox" id="item1" /><label for="item1">open</label>
</div>
HTML
<div class="myDirective">
<input type="checkbox" id="myItem_{{$id}}" />
<label for="myItem_{{$id}}">open myItem_{{$id}}</label>
</div>
UPDATE
Angular 1.3 introduced a native lazy one-time binding. from the angular expression documentation:
One-time binding
An expression that starts with :: is considered a
one-time expression. One-time expressions will stop recalculating once
they are stable, which happens after the first digest if the
expression result is a non-undefined value (see value stabilization
algorithm below).
Native Solution:
.directive('myDirective', function() {
var uniqueId = 1;
return {
restrict: 'E',
scope: true,
template: '<input type="checkbox" id="{{::uniqueId}}"/>' +
'<label for="{{::uniqueId}}">open</label>',
link: function(scope, elem, attrs) {
scope.uniqueId = 'item' + uniqueId++;
}
}
})
Only bind once:
If you only need to bind a value once you should not use bindings ({{}} / ng-bind)
bindings are expensive because they use $watch. In your example, upon every $digest, angular dirty checks your IDs for changes but you only set them once.
Check this module: https://github.com/Pasvaz/bindonce
Solution:
.directive('myDirective', function() {
var uniqueId = 1;
return {
restrict: 'E',
scope: true,
template: '<input type="checkbox"/><label>open</label>',
link: function(scope, elem, attrs) {
var item = 'item' + uniqueId++;
elem.find('input').attr('id' , item);
elem.find('label').attr('for', item);
}
}
})
We add a BlockId parameter to the scope, because we use the id in our Selenium tests for example. There is still a chance of them not being unique, but we prefer to have complete control over them. Another advantage is that we can give the item a more descriptive id.
Directive JS
module.directive('myDirective', function () {
return {
restrict: 'E',
scope: {
blockId: '#'
},
templateUrl: 'partials/_myDirective.html',
controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
...
} //controller
};
}]);
Directive HTML
<div class="myDirective">
<input type="checkbox" id="{{::blockId}}_item1" /><label for="{{::blockId}}_item1">open</label>
</div>
Usage
<my-directive block-id="descriptiveName"></my-directive>
Apart from Ilan and BuriB's solutions (which are more generic, which is good) I found a solution to my specific problem because I needed IDs for the "for" Attribute of the label. Instead the following code can be used:
<label><input type="checkbox"/>open</label>
The following Stackoverflow-Post has helped:
https://stackoverflow.com/a/14729165/1288552

Set attribute value of angular directive from another controller

Angular directive;
.directive('ngFilemanager', function () {
return {
restrict: 'EA',
scope: {
thefilter: '=',
},
link: function (scope, element, attrs) {
},
templateUrl: '/templates/filemanager.html',
controller: FileManagerController
}
Html:
<div id="testcontainer" ng-controller="OtherController">
...
<div ng-click="vm.myfunction">Set Filter</div>
...
<div id="thefilemanager" ng-filemanager thefilter=""></div>
...
</div>
How can i set thefilter value in a function of OtherController?
I tried setting the attribute value by jquery but my ng-view isn't updated correctly then.
You've got bi-directional isolated scope so:
function OtherController($scope){
$scope.myfilter= "";
$scope.setFilter = function(what){
$scope.myfilter = what;
}
}
and HTML:
<div id="testcontainer" ng-controller="OtherController">
<div ng-click="setFilter('fun')">Set Filter</div>
<div id="thefilemanager" ng-filemanager thefilter="myfilter"></div>
</div>
Then when you change $scope.myfilter in the OtherController's scope, scope.thefilter changes in your directive's scope.
If the "other" controller is not a direct parent, you could use $emit or $broadcast depending on where the target is.
Here's an example using $broadcast instead:
app.controller('MainCtrl', function($scope) {
$scope.setFilter = function(what){
$scope.$broadcast('setFilter', what);
}
});
then inside your directive you can listen:
link: function (scope, element, attrs) {
scope.$on('setFilter', function(e, what){
scope.thefilter = what;
});
},
To make it work anywhere, you can $broadcast from $rootScope, but at that point you might want to re-evaluate why you have to do this. Angular itself does this a lot, for example, routeChangeSuccess event, but that doesn't mean you should do it.
This will work if the other controller is a parent of ngFilemanager
<div id="thefilemanager" ng-filemanager thefilter="theFilterValue"></div>
In some other controller
...
$scope.theFilterValue = 'some other value';
...
Look the doc's isolate scope on directives section

Resources