I tried to use angular component replacing directive for creating component (a data table).
I want to use the component inside of ng-repeat like:
<div ng-repeat='item in items'>
<my-datatable></my-datatable>
</div>
The my-datatable component code look like this:
angular.module('App').component('myDatatable', {
bindings: {
ctrl: '=?',
datasource: '=?'
},
bindToController: true,
controllerAs: 'dataTable',
template: function(){},
controller: function(){}
});
First problem:
1. template function is not runnig for every instance of my-datatable component.
That mean, if inside my-datatable I have other components generated based by an unique Id of my-datatable (which can be create only in controller), it's difficult to write that code.
2. The nested isolate scope, create o much too long chains of $parent, for make a reference to some scope variable, something like this (if I use two nested my-datatable's):
ng-click="$parent.$parent.$parent.$parent.$parent.$parent.$parent.$parent.select($parent.$parent.$parent.$parent.dataItem,dataItem)"
So, my question is:
It is a way to avoid this problems using angular component?
Writing controls code with directive without isolate scope, I don't have this problems. That why I want to change my code for all angular components as directives.
I start to change may component into directive, and I remember few things:
For long chains of $parent.$parent..., I create in component controller a variable $parent with reference for parent scope of component, so if I need a reference inside component to outside scope, I can use: dataTable.$parent.
The good thing for isolate scope, it's I can destroy component and recreate, very simple and clean ( all the watchers are removed when I destroyed the isolate scope), which is not possible with directive way with no isolate scope.
That mean,it remain only the problem to generate unique id for each component instance in template function. In other words: how to run template function on each instance of component.
For exemplify:
https://jsfiddle.net/bogdanim36/t3gmzb6z/3/
And made some tests and I discover that it's happen the same with directive inside ng-repeat: template function is not running for every instance of component.
Finally I generate html code in controller, to solve this issue, That mean one more digest cycle. Is not what I want, but i decided it's the best solution in my case.
Related
I am facing a strange situation in which I have created a directive, to which a controller is attached, and one of the two tiny functions of the controller is never called from the view whereas the other function is.
Here is the plunker.
The message I expect is (bold is what does not show up)
You are limited to: Prison
I have already created tens of directives, whether in their own right or as wrappers around existing directives available on GitHub, from lightweight ones such as custom-select to behemoths such as angular-ui-grid.
I am at the end of my wits here as to why {{getArea()}} produces no text at all in the view. I've scrutinized the code, trying to do it with new eyes, so to speak, and I see nothing wrong. I've created a specific project in Eclipse for this tiny piece of code, installed Wampserver just so I could set breakpoints in Firebug and God knows to what great lengths I had to go just so that I could understand what is wrong with the code I wrote.
For instance, in isRestricted(), I can call getArea() without any problem. However, Angular seems to not find the function from the directive.
A few similar questions have already been asked but none of the errors (missing controller or ng-app specification, missing dependency list at module declaration, nested controllers, etc.) seem to apply. There's obviously an important lesson to be learned here and I'm truly eager to learn it.
EDIT: The lesson learned is that ng-if creates a new scope. That new scope comes in between the controller and the directive, which leads to the template of the directive losing access to anything defined in the controller (at least, that's how I would phrase it). (Note that a comment hinted at directive priority.)
There are several solutions, which all maintain the prototypical inheritance needed for the template to access the functions defined in the controller:
not using an isolate scope
not defining the ng-if directive on the top-level element of my directive, as that causes a conflict (between my controller's scope and the scope defined by ng-if). I believe ng-if wins here, which leads to the controller's scope being out of reach of the directive. Using ng-if on a child div does the trick (because then, the ng-if scope inherits my controller's scope, hence making the functions available to the template).
Because of the CSS styling needed with this directive, I have used scope: false.
<span class="scoop-badge-content">{{$parent.getArea()}}</span>
Or in directive :
scope:true
This is because ng-if use how own scope
The strange thing is that when i have this problem i usually use dot notation. But it doesn't work here, probably because we're inside a directive, and i didn't had the case until now.
EDIT : a last way of doing this chaging the template :
<div class="scoop-badge scoop-badge-ua">
<div ng-if="isRestricted()">
<span class="scoop-badge-title">You are limited to:</span>
<span class="scoop-badge-content">{{getArea()}}</span>
</div>
</div>
I think this work because you have replace true and ng-if will conflict with ng-scope if it's on the top DOM element.
When you have scope = {} in your directive, Angular creates an isolated scope. Which means it can't get to the getArea() function.
You can completely remove the scope = {} line or set it to scope = true or scope = false depending on what you're trying to achieve later on.
When set to scope = true Angular will create a new scope object and assign it to the directive. This scope object is prototypically inherited from its parent scope.
When set to scope = false the directive will use its parent scope. (This is the default value. It has the same effect if you remove this line).
More information about scopes here
Removing scope: {} from directive definition solves problem.
app.directive('scoopBadgeUa', function() {
return {
restrict : "A",
scope: {}, // This is not needed, creates conflict
templateUrl : "scoop-badge-ua.html",
replace : true,
controller : 'ScoopBadgeUaController',
};
});
Your code is correct, you don't have to do anything more than adding a <div></div> wrapping your code in scoop-badge-ua.html.
<div>
<div class="scoop-badge scoop-badge-ua" ng-class="{'visible': isRestricted()}" ng-if="isRestricted()">
<span class="scoop-badge-title">You are limited to:</span>
<span class="scoop-badge-content">{{getArea()}}</span>
</div>
</div>
I like this solution better than transclude: true because it doesn't include the directive's tag in your html code, which ultimately translates to a cleaner code.
I am creating reusable UI components with AngularJS directives. I would like to have a controller that contains my business logic with the nested components (directives). I want the directives to be able to manipulate a single property on the controller scope. The directives need to have an isolate scope because I might use the same directive more than once, and each instance needs to be bound to a particular controller scope property.
So far, the only way I can apply changes back to the controller's scope is to call scope.$apply() from the directive. But this breaks when I'm inside of an ng-click callback because of rootScope:inprog (scope operation in progress) errors.
So my question: What is the best way to make my controller aware when a child directive has updated a value on the controller's scope?
I've considered having a function on the controller that the directive could call to make an update, but that seems heavy to me.
Here is my code that breaks on an ng-click callback. Keep in mind that I don't just want to solve the ng-click issue. I want the best overall solution to apply reusable directives to modify a parent scope/model.
html
<div ng-controller="myCtrl">
<my-directive value="val1"></my-directive>
</div>
controller
...
.controller('myCtrl', ['$scope', function ($scope) {
$scope.val1 = 'something';
}});
directive
...
.directive('myDirective', [function () {
return {
link: function(scope) {
scope.buttonClick = function () {
var val = 'new value';
scope.value = val;
scope.$apply();
};
},
scope: {
value: '='
},
template: '<button ng-click="buttonClick()"></button>'
};
}]);
The purpose of two-way data binding in directives is exactly what you're asking about -- to "[allow] directives to modify a parent scope/model."
First, double-check that you have set up two-way data binding correctly on the directive attribute which exposes the variable you want to share between scopes. In the controller, you can use $watch to detect updates if you need to do something when the value changes. In addition, you have the option of adding an event-handler attribute to the directive. This allows the directive to call a function when something happens. Here's an example:
<div ng-controller="myCtrl">
<my-directive value="val1" on-val-change="myFunc"> <!-- Added on-change binding -->
<button ng-click="buttonClick()"></button>
</my-directive>
</div>
I think your question about $scope.apply is a red herring. I'm not sure what problem it was solving for you as you evolved this demo and question, but that's not what it's for, and FWIW your example works for me without it.
You're not supposed to have to worry about this issue ("make controller aware ... that [something] modified a value on a scope"); Angular's data binding takes care of that automatically.
It is a little complicated here because with the directive, there are multiple scopes to worry about. The outer scope belongs to the <div ng-controller=myCtrl>, and that scope has a .val property, and there's an inner scope created by the <my-directive> which also has a .val property, and the buttonClick handler inside myDirective modifies the inner one. But you declared myDirective's scope with value: '=' which sets up bidirectional syncing of that property value between the inner and outer scope.
So it should work automatically, and in the plunker I created from your question code, it does work automatically.
So where does scope.$apply come in? It's explicitly for triggering a digest cycle when Angular doesn't know it needs to. (And if you use it when Angular did know it needed a digest cycle already, you get a nested digest cycle and the "inprog" error you noticed.) Here's the doc link, from which I quote "$apply() is used to execute an expression in angular from outside of the angular framework". You need to use it, for example, when responding to an event handler set up with non-Angular methods -- direct DOM event bindings, jQuery, socket.io, etc. If you're using these mechanisms in an Angular app it's often best to wrap them in a directive or service that handles the Angular-to-non-Angular interface so the rest of your app doesn't have to worry about it.
(scope.$apply is actually a wrapper around scope.$digest that also manages exception handling. This isn't very clear from the docs. I find it easier to understand the name/behavior of $digest, and then consider $apply to be "the friendlier version of $digest that I'm actually supposed to use".)
One final note on $apply; it takes a function callback argument and you're supposed to do the work inside this callback. If you do some work and then call $apply with no arguments afterwards, it works, but at that point it's the same as $digest. So if you did need to use $apply here, it should look more like:
scope.buttonClick = function() {
scope.$apply(function() {
scope.value = newValue;
});
});
I tried to find linking of multiple controllers to a single custom directive, but no solution. Is this possible to achieve or not. Can anybody please tell me.
Requirement: I created a directive with a controller. I'm calling that directive in a page and the page is having its own controller. Now the page controller have a couple of functions. I'm using a template with some events. Those events are implemented in the page controller (parent controller). So those functions are not firing.
<div ng-controller="controllername">
<myDirective name-"name" event="doSomeEvent(params)"/>
In the controller i have a couple of functions like
app.controller("controllername",['$scope','function($scope))
{
$scope.functionName = function()
{
alert(1);
}]
}
This function is linked to the directive template. How to make this event fired?
my guess is that your directive has got an isolated scope.
meaning you have in your directive definition a line with scope: {}.
that makes it isolated and it can't see the parent scope (meaning that controller 'controllername' you have there)
remove the scope definition from the directive (remove the scope: {}) and you will have access to the parent scope.
and you will be able to use those function as if they were in the directive scope.
I am trying to make a transitive transclusion, or call it “directive inception”.
I made this example to illustrate what I am trying to do:
http://plnkr.co/edit/0hFFHknDps2krtK1D9ud?p=preview
The directive “first” wraps the directive “second” in its template and the two of them use transclusion.
What I want to do is to bind a value from a controller to the html that is a child of the “first” directive.
So I wanted my example to display:
<h1>Chained transclusions test</h1>
<div>
<h2>First directive</h2>
<div>
<h2>Second directive</h2>
<div>Controller hello</div>
</div>
</div>
Obviously that is not what I got.
I tried to analyze the scope with the developer tool and I was surprised by the result scope tree:
the result trees
I thought angularJS would create a new scope when using the transclude feature in a directive. And that this scope would be a non isolate sibling of my directive isolate scope. But I cannot see any sibling of my first directive scope (although it uses transclude). Plus, every children of my “first” directive has a scope isolated from the controller scope since the “first” directive scope is an isolated one.
I don’t understand the behavior here.
Is the transclude inclusion completely forbidden in angularJS ?
Is it possible to create a directive with transclusion, that wraps another directive that uses transclusion ?
It seems to me that this is the whole power behind web components, the fact that transclusion or any other special caracteristics should be seen as “implementation detail”, and the component should be able to use other directives that hide their own implementation details.
Without getting into the details of scope creation when using isolate scope with transclusion... it is possible to nest transclusions, but in your example, you need to make scope.controllerMsg available to the first directive's isolate scope:
JS:
app.directive('first', function(){
return {
...
scope: { controllerMsg: '=text'},
...
}
});
HTML:
<first text="controllerMsg">
{{controllerMsg}}
</first>
Demo
I've just begun creating Angular directives (I'm new to the framework, as well), but am running into issues wherein a nested directive seems to be ignored. The basis for my directives' code is UI Bootstrap's "tabs" and "pane" directives.
The gist is that I want to be able to compile a list of "components" inside a "layout". Ultimately, there should also be an attribute on each "component" tag that will instruct the layout to render content from some known template location. For now, however, I can't even get the "link" function inside the component directive to fire, even though I've got two components in my template.
Here's a plunk of my situation:
http://plnkr.co/edit/K4n2Mx3kZyvVYGDyJ7t9
You're mis-using ngTransclude by placing it inside of ngRepeat. It's sort of a chicken/egg situation where since there's nothing to repeat over, nothing gets transcluded.
Also, since you're specifying components in HTML, you don't even need ngRepeat in your template.
http://plnkr.co/edit/aYjdd4skbKC3FEM3lCfY?p=preview
template:
'<section class="layout">' +
'<h4>Before all components</h4>' +
'<div ng-transclude></div>' +
'<h4>After all components</h4>' +
'</section>'
When you use ng-repeat, it creates a new scope and it makes you ng-transclude not in the right scope for injecting the transclusion.
So when you remove the ng-repeat, you get the rendered components.
Now, in order to control the layout, you can either add the elements to the controllers like you do with their scope, and then layout them accordingly in the controller:
// inside the controller
this.addComponentElement = function (componentElement) {
componentElements.push(componentElement);
};
// watch for array changes and handle layout
Or, you can use the transclude function in the compile + link combination to get a reference to the transcluded dom and manipulate its layout:
compile:function(telement, tAttrs, transcludeFn){
return function(scope, element, attrs){
transcludeFn(scope, function(transcludedDom){
// layout the transcludedDom
})
}