Recursive call of directive crashing the browser tab - javascript

I have an observation directive which render observation.html
observation.js
angular.module('bahmni.clinical')
.directive('observation', function () {
var controller = function ($scope) {
console.log($scope.observation);
};
return {
restrict: 'E',
controller: controller,
scope: {
observation: "="
},
templateUrl: "views/observation.html"
};
});
I call observation directive from the observation.html. This will be done recursively.
observation.html
<fieldset>
<div class="form-field"
ng-class="{'is-abnormal': observation.abnormal, 'is-text': isText(observation)}">
<span class="field-attribute"><label>{{observation.concept.shortName || observation.concept.name}}</label></span>
<span class="value-text-only" ng-if="!observation.groupMembers">{{observation.getDisplayValue()}}</span>
<span class="label-add-on" ng-hide="!observation.unit"> {{observation.concept.units}}</span>
<div class="footer-note fr">
<span class="value-text-only time">{{observation.observationDateTime | date :'hh:mm a'}}</span>
</div>
</div>
</fieldset>
<div ng-repeat="observationMember in observation.groupMembers">
<observation observation="observationMember"></observation>
</div>
I call this for first time from someother directive.
someother.js
<observation observation="observation"></observation>
If i refresh the browser, The tab will be irresponsive. Don't know what is happening. Not able to debug because of the irresponsive tab.
I would really appretiate your answer.

ng-include fixed it.
used the following line
template: '<ng-include src="\'views/observation.html\'" />'
instead of -
templateUrl: "views/observation.html"

It sounds like an endless loop! I think your problem is how the defined local scope variable is defined. observationhas an two way databinding. So you will overwrite it with every recursive call. Try this to avoid the endless loop
scope: {
observation: "&"
},
This will create an local directive scope which are not affected from the parent scope.
Keep in mind there will be three ways to define local scope properties you can pass:
# Used to pass a string value into the directive
= Used to create a two-way binding to an object that is passed into the directive
& Allows an external function to be passed into the directive and
invoked
UPDATE 2
Your main problem seems that your variable observation will be overwritten on each recursive call. So you have two options:
Option 1: You use # and serialize your object with JSON.stringify(observation). This string can you provide via string interpolation to your directive. <observation observation="{{observation}}"></observation>
Option 2: You use & and pass the object to an helper function, which will be create and return an clone from your object.

Related

directive with bindToController can't get data from child directives

I'm tring to write a directive that builds an object from its child directive's input and pushes it
to an array provided as a parameter. Something like:
<aggregate-input on="SiteContent.data.objList">
<p aggregate-question>Question text</p>
<input aggregate-answer ng-model="answer" type="text" />
</aggregate-input>
<aggregate-input on="SiteContent.data.objList">
<p aggregate-question>Question text 2</p>
<input aggregate-answer ng-model="answer" type="text" />
</aggregate-input>
I'm looking to collect the data like:
SiteContent.data.objList === [
{question: 'Quesion text', answer: 'user input'},
{question: 'Quesion text 2', answer: 'user input 2'},
];
Here's the plunker with the code.
update 1: #jrsala included scope and bindToController syntax changes
I'm having trouble figuring out the way these directives should communicate. I'm expecting the input
object defined in the link will be isolated in the scope of each directive and pushed to the on object
provided. The result is that the input object is shared among all instances, and only one object gets
ever pushed to the array.
I'm guessing the transcluded scope rules are confusing me, but I really don't see where. Any ideas? Thanks!
First issue: your aggregate-input directive specifies an isolate scope with no bound properties but you still use an on attribute on the element with the directive on it:
<aggregate-input on="SiteContent.data.objList">
but in your JS,
{
restrict: 'E',
scope: {},
controller: aggregateInputController,
controllerAs: 'Aggregate',
bindToController: { on: '=' },
/* */
}
whereas what you need is
{
restrict: 'E',
scope: { on: '=' },
controller: aggregateInputController,
controllerAs: 'Aggregate',
bindToController: true // BOOLEAN REQUIRED HERE, NO OBJECT
}
As per the spec's paragraph on bindToController,
When an isolate scope is used for a component (see above), and controllerAs is used, bindToController: true will allow a component to have its properties bound to the controller, rather than to scope. When the controller is instantiated, the initial values of the isolate scope bindings are already available.
Then you do not need to assign the on property to your controller, it's done for you by Angular (also I did not understand why you did this.on = this.on || [], the this.on || part looks unnecessary to me).
I suppose you can apply that to the rest of the code and that should be a start. I'm going to look for more issues.
edit: Several more issues that I found:
If the scope of siteContent is isolated then the SiteContent controller is not accessible when the directive is compiled and Angular silently fails (like always...) when evaluating SiteContent.data.objList to pass it to the child directives. I fixed that by removing scope: {} from its definition.
It was necessary to move the functionality of aggregateInputLink over to aggregateInputController because, as usual, the child controllers execute before the link function, and since the aggregateQuestion directive makes the call InputCtrl.changeValue('question', elem.text()); in its controller, the scope.input assigned to in the parent directive post-link function did not exist yet.
function aggregateInputController($scope) {
$scope.input = {};
this.on.push($scope.input);
this.changeValue = function changeValue(field, value) {
$scope.input[field] = value;
};
}
As a reminder: controllers are executed in a pre-order fashion and link functions in a post-order fashion during the traversal of the directive tree.
Finally, after that, the SiteContent controller's data did not get rendered property since the collection used to iterate over in ng-repeat was erroneously SiteContent.objList instead of SiteContent.data.objList.
Link to final plunker

AngularJs: Binding a property of a scope object to a directive

I'm somewhat new to AngularJs, so forgive me if this is a newb question, but I've looked around a bit and haven't been able to figure this out.
I'm trying to send an attribute of an object into a directive and I'm not quite sure why this isn't working.
I've got a scope variable that's an object, something like:
$scope.player = {name:"", hitpoints:10};
In my HTML, I'm attempting to bind that to a directive:
<span accelerate target="player.hitpoints" increment="-1">Take Damage</span>
In my directive, I'm attempting to modify player.hitpoints like this:
scope[attrs.target] += attrs.increment;
When I trace it out, scope[attrs.target] is undefined, even though attrs.target is "player.hitpoints." When I use target="player", that traces out just fine but I don't want to have to manipulate the .hitpoints property explicitly in the directive.
Edit: I've made a jsfiddle to illustrate what I'm trying to do: http://jsfiddle.net/csafo41x/
There is a way to share scope between your controller and directive. Here is very good post by Dan Wahlin on scope sharing in Directive - http://weblogs.asp.net/dwahlin/creating-custom-angularjs-directives-part-2-isolate-scope
There are 3 ways to do so
# Used to pass a string value into the directive
= Used to create a two-way binding to an object that is passed into the directive
& Allows an external function to be passed into the directive and invoked
Just a very basic example on how the above mentioned scope are to be used
angular.module('directivesModule').directive('myIsolatedScopeWithModel', function () {
return {
scope: {
customer: '=' //Two-way data binding
},
template: '<ul><li ng-repeat="prop in customer">{{ prop }}</li></ul>'
};
});
There are a number of things going on here:
#1 - scope
Once you define your isolated scope (along the lines of #Yasser's answer), then you don't need to deal with attrs - just use scope.target.
#2 - template
Something actually needs to handle the click event. In your fiddle there is just <span class="btn"...>. You need ng-click somewhere. In your case, you probably want the directive to handle the click. So modify the directive's template and define the click handler in the directive's scope:
...
template: "<button class='btn' ng-click='click()'>Button</button>",
link: function(scope, element, attrs)
{
scope.click = function(){
scope.target += parseInt(attrs.increment);
}
}
...
#3 - transclude
Now, you need to get the contents of the directive to be the contents of the button within your directive's template. You can use transclude parameter with ng-transclude - for location, for that. So, the template above is modified to something like the following:
...
template: "<button class='btn' ng-click='click()'><div ng-transclude</div></button>",
transclude: true,
...
Here's your modified fiddle

AngularJs directive transclusion error and # binding

I have created a directive to understand transclusion and isolated scope.
Here is the HTML:
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<div>
<input type="text" ng-model="name" />
<input type="number" ng-model="friendCount" />
</div>
<my-friends name="{{name}}">
<p>Hi, I'm {{name}}, and I have {{friendCount}} friends</p>
</my-friends>
</div>
</div>
Directive:
var app = angular.module('myApp', []);
app.controller('MyCtrl', function ($scope) {
$scope.name = "John Doe";
$scope.friendCount = 3;
});
app.directive('myFriends', function () {
return {
restrict: 'E',
replace: true,
template: '<div>' +
' <h3> Isolated Scope: {{name}}</h3>' +
' <div ng-transclude></div>' +
'</div>',
transclude: true,
scope: {
name: '#'
},
link: function (scope, element, attrs) {
scope.name = "Anderson";
}
}
});
Open this fiddle: JsFiddle
I have two questions:
Why does the {{name}} in directive template says "John Doe" instead of "Anderson"? I expected it to be Anderson, since name property is prototypically inherited and as soon as write to it in the link function, it should lose that ancestral connection.
It seems to be transcluding correctly but why does it throw the error Error: [ngTransclude:orphan] in the dev tools console? Could it be the angular version I am using?
Any help is greatly appreciated.
Here is the fiddle: JsFiddle
UPDATE:
The transclusion error was due to loading angular twice by mistake.
When using the # binding you can overwrite the name inside directive after the initial digest cycle has completed, for example in an click handler or something. However, it will be overwritten as soon as you change the parent scope's name property.
The issue about # binding is my misunderstanding of isolated scopes. It is working the way it is supposed to in the example. Isolated scope does not prototypically inherit from parent. And the meaning of # binding, also referred to as read-only access or one-way binding, is that it will not let you update/write to parent scope.
UPDATED FIDDLE
1) With # binding, at the digest phase, angular will reevaluate the expression and set the value back to the current value of your controller which will overwrite your value set in the link function.
2) You have 2 versions of angular loaded in the fiddle. The angular loaded the second time walks the DOM again and tries to compile again the already compiled DOM. For more information, take a look at my answer to another question Illegal use of ngTransclude directive in the template
DEMO
The AngularJS documentation is in such a state of flux that I cannot find the page relevant to the different types of isolated scope bindings so I apologize for any inaccuracies.
How to make it work
scope: {
name: '=' //formerly #
},
IIRC the # binding takes the value passed in as a string literal. I am not positive why this means the link function assignment doesn't overwrite it, if I could find the section in the docs then I'd link to it.
The = binding binds to the value of a scope property and updates both the isolate scope and the assigning scope when updated.
<my-friends name="name"> //formerly name="{{name}}"
Using the = binding means that a scope property needs to be passed instead of a string value.
Again, sorry for any incorrect or vague information. I'll update the answer if I can find the dang documentation.

AngularJS : Modify model of parent scope in a directive with '=xxx' isolate scope?

I have an Angular directive which permit to render an user, and create a link to view the user profile, declared as:
.directive('foafPerson', function() {
return {
restrict: 'E',
templateUrl: 'templates/person.html',
scope: {
personModel: '=',
onClickBindTo: '=',
onPersonClick: '&'
}
};
As you can see, I'm trying 2 solutions to be able to visit and load the full user profile: onClickBindTo and onPersonClick
I use it like that to render a list of persons + their friends:
// display the current user
<foaf-person person-model="person" on-person-click="changeCurrentProfileUri(person.uri)" on-click-bind-to="currentProfileUri"></foaf-person>
// display his friends
<div class="profileRelationship" ng-repeat="relationship in relationships">
<foaf-person person-model="relationship" on-person-click="changeCurrentProfileUri(relationship.uri)" on-click-bind-to="currentProfileUri"></foaf-person>
</div>
On the template, I have a link that is supposed to change an attribute of the controller (called currentProfileUri)
<a href="" ng-click="onClickBindTo = personModel.uri">
{{personModel.name}}
<a/>
I can see that the controller scope variable currentProfileUri is available in the personTemplate.html because I added a debug input: <input type="text" ng-model="onClickBindTo"/>
Unfortunately, when I modify the input value, or when I click on the link, the currentProfileUri of the controller is not updated. Is this normal or am I missing something?
With the other method it seems to work fine:
<a href="" ng-click="onPersonClick()">
{{personModel.name}}
<a/>
So to modify a model of the parent scope, do we need to use parent scope functions?
By the way, passing an expression with &, I tried another solution: not using a function declared in the controller scope:
<foaf-person person-model="relationship" on-person-click="currentProfileUri = relationship.uri"></foaf-person>
How comes it does not work?
My controller has nothing really fancy:
$scope.currentProfileUri = 'https://my-profile.eu/people/deiu/card#me';
$scope.$watch('currentProfileUri', function() {
console.debug("currentProfileUri changed to "+$scope.currentProfileUri);
loadFullProfile($scope.currentProfileUri);
})
$scope.changeCurrentProfileUri = function changeCurrentProfileUri(uri) {
console.debug("Change current profile uri called with " + uri);
$scope.currentProfileUri = uri;
}
I am new to Angular and read everywhere that using an isolate scope permits a two-way data binding with the parent scope, so I don't understand why my changes are not propagated to the parent scope and my debug statement doesn't fire unless I use the scope function changeCurrentProfileUri
Can someone explain me how it works?
In your example the scopes hierarchy is the following:
controller scope
ng-repeat scope
foaf-person scope
so when you declare two-way binding for 'currentProfileUri' it is actually bound to the scope created by ng-repeat, not by the controller, and when your code changes value of onClickBindTo then angularjs executes 'currentProfileUri = newValue' in the ng-repeat scope.
Solution is to use objects instead of primitive values for two-way bindings - in this case scopes inheritance always work in proper way. I mean something like that:
// display the current user
<foaf-person person-model="person" on-click-bind-to="currentProfile.uri"></foaf-person>
// display his friends
<div class="profileRelationship" ng-repeat="relationship in relationships">
<foaf-person person-model="relationship" on-click-bind-to="currentProfile.uri"></foaf-person>
</div>
I've prepared a js-fiddle which illustrates this behavior

Can an angular directive pass arguments to functions in expressions specified in the directive's attributes?

I have a form directive that uses a specified callback attribute with an isolate scope:
scope: { callback: '&' }
It sits inside an ng-repeat so the expression I pass in includes the id of the object as an argument to the callback function:
<directive ng-repeat = "item in stuff" callback = "callback(item.id)"/>
When I've finished with the directive, it calls $scope.callback() from its controller function. For most cases this is fine, and it's all I want to do, but sometimes I'd like to add another argument from inside the directive itself.
Is there an angular expression that would allow this: $scope.callback(arg2), resulting in callback being called with arguments = [item.id, arg2]?
If not, what is the neatest way to do this?
I've found that this works:
<directive
ng-repeat = "item in stuff"
callback = "callback"
callback-arg="item.id"/>
With
scope { callback: '=', callbackArg: '=' }
and the directive calling
$scope.callback.apply(null, [$scope.callbackArg].concat([arg2, arg3]) );
But I don't think it's particularly neat and it involves puting extra stuff in the isolate scope.
Is there a better way?
Plunker playground here (have the console open).
If you declare your callback as mentioned by #lex82 like
callback = "callback(item.id, arg2)"
You can call the callback method in the directive scope with object map and it would do the binding correctly. Like
scope.callback({arg2:"some value"});
without requiring for $parse. See my fiddle(console log) http://jsfiddle.net/k7czc/2/
Update: There is a small example of this in the documentation:
& or &attr - provides a way to execute an expression in the context of
the parent scope. If no attr name is specified then the attribute name
is assumed to be the same as the local name. Given and widget definition of scope: {
localFn:'&myAttr' }, then isolate scope property localFn will point to
a function wrapper for the count = count + value expression. Often
it's desirable to pass data from the isolated scope via an expression
and to the parent scope, this can be done by passing a map of local
variable names and values into the expression wrapper fn. For example,
if the expression is increment(amount) then we can specify the amount
value by calling the localFn as localFn({amount: 22}).
Nothing wrong with the other answers, but I use the following technique when passing functions in a directive attribute.
Leave off the parenthesis when including the directive in your html:
<my-directive callback="someFunction" />
Then "unwrap" the function in your directive's link or controller. here is an example:
app.directive("myDirective", function() {
return {
restrict: "E",
scope: {
callback: "&"
},
template: "<div ng-click='callback(data)'></div>", // call function this way...
link: function(scope, element, attrs) {
// unwrap the function
scope.callback = scope.callback();
scope.data = "data from somewhere";
element.bind("click",function() {
scope.$apply(function() {
callback(data); // ...or this way
});
});
}
}
}]);
The "unwrapping" step allows the function to be called using a more natural syntax. It also ensures that the directive works properly even when nested within other directives that may pass the function. If you did not do the unwrapping, then if you have a scenario like this:
<outer-directive callback="someFunction" >
<middle-directive callback="callback" >
<inner-directive callback="callback" />
</middle-directive>
</outer-directive>
Then you would end up with something like this in your inner-directive:
callback()()()(data);
Which would fail in other nesting scenarios.
I adapted this technique from an excellent article by Dan Wahlin at http://weblogs.asp.net/dwahlin/creating-custom-angularjs-directives-part-3-isolate-scope-and-function-parameters
I added the unwrapping step to make calling the function more natural and to solve for the nesting issue which I had encountered in a project.
In directive (myDirective):
...
directive.scope = {
boundFunction: '&',
model: '=',
};
...
return directive;
In directive template:
<div
data-ng-repeat="item in model"
data-ng-click='boundFunction({param: item})'>
{{item.myValue}}
</div>
In source:
<my-directive
model='myData'
bound-function='myFunction(param)'>
</my-directive>
...where myFunction is defined in the controller.
Note that param in the directive template binds neatly to param in the source, and is set to item.
To call from within the link property of a directive ("inside" of it), use a very similar approach:
...
directive.link = function(isolatedScope) {
isolatedScope.boundFunction({param: "foo"});
};
...
return directive;
Yes, there is a better way: You can use the $parse service in your directive to evaluate an expression in the context of the parent scope while binding certain identifiers in the expression to values visible only inside your directive:
$parse(attributes.callback)(scope.$parent, { arg2: yourSecondArgument });
Add this line to the link function of the directive where you can access the directive's attributes.
Your callback attribute may then be set like callback = "callback(item.id, arg2)" because arg2 is bound to yourSecondArgument by the $parse service inside the directive. Directives like ng-click let you access the click event via the $event identifier inside the expression passed to the directive by using exactly this mechanism.
Note that you do not have to make callback a member of your isolated scope with this solution.
For me following worked:
in directive declare it like this:
.directive('myDirective', function() {
return {
restrict: 'E',
replace: true,
scope: {
myFunction: '=',
},
templateUrl: 'myDirective.html'
};
})
In directive template use it in following way:
<select ng-change="myFunction(selectedAmount)">
And then when you use the directive, pass the function like this:
<data-my-directive
data-my-function="setSelectedAmount">
</data-my-directive>
You pass the function by its declaration and it is called from directive and parameters are populated.

Categories

Resources